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, 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                // v7.38 (read01) — pg_catalog.pg_attrdef (column defaults;
1165                // ORM reflection + pg_dump read the deparsed default text).
1166                "__spg_pg_attrdef" => {
1167                    let (schema, rows) =
1168                        crate::system_catalog::synth_pg_attrdef(self.active_catalog());
1169                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1170                }
1171                // v7.39 (RLS) — pg_catalog.pg_policy (raw) + pg_policies (view).
1172                "__spg_pg_policy" => {
1173                    let (schema, rows) =
1174                        crate::system_catalog::synth_pg_policy(self.active_catalog());
1175                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1176                }
1177                "__spg_pg_policies" => {
1178                    let (schema, rows) =
1179                        crate::system_catalog::synth_pg_policies(self.active_catalog());
1180                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1181                }
1182                // v7.37.24 (24.14) — pg_catalog.pg_collation.
1183                "__spg_pg_collation" => {
1184                    let (schema, rows) =
1185                        crate::system_catalog::synth_pg_collation(self.active_catalog());
1186                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1187                }
1188                // v7.37.23 (23.6-b) — pg_catalog.pg_tablespace.
1189                "__spg_pg_tablespace" => {
1190                    let (schema, rows) =
1191                        crate::system_catalog::synth_pg_tablespace(self.active_catalog());
1192                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1193                }
1194                // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_indexes view
1195                // for pgAdmin / DataGrip "indexes per table" listings.
1196                "__spg_pg_indexes" => {
1197                    let (schema, rows) = synth_pg_indexes(self.active_catalog());
1198                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1199                }
1200                // v7.39 (read01 round 50) — pg_catalog.pg_description, backing
1201                // psql's \d+ comment column and pg_dump's COMMENT ON emission.
1202                "__spg_pg_description" => {
1203                    let (schema, rows) =
1204                        crate::system_catalog::synth_pg_description(self.active_catalog());
1205                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1206                }
1207                // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_index (raw)
1208                // for index introspection by ORM compilers.
1209                "__spg_pg_index" => {
1210                    let (schema, rows) = synth_pg_index_raw(self.active_catalog());
1211                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1212                }
1213                // v7.17.0 Phase 3.P0-54 — pg_catalog.pg_constraint
1214                // for FK / UNIQUE / PK / CHECK introspection.
1215                "__spg_pg_constraint" => {
1216                    let (schema, rows) = synth_pg_constraint(self.active_catalog());
1217                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1218                }
1219                // v7.37 U11 — pg_catalog.pg_sequence, one row per CREATE
1220                // SEQUENCE (psql \d <seq> + ORM sequence introspection).
1221                "__spg_pg_sequence" => {
1222                    let (schema, rows) = synth_pg_sequence(self.active_catalog());
1223                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1224                }
1225                // v7.17.0 Phase 3.P0-55 — pg_catalog.pg_database /
1226                // pg_roles / pg_user. SPG is single-database so
1227                // pg_database surfaces just `postgres`; pg_roles
1228                // / pg_user walk the engine's UserStore.
1229                "__spg_pg_database" => {
1230                    let (schema, rows) = synth_pg_database(self);
1231                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1232                }
1233                "__spg_pg_roles" => {
1234                    let (schema, rows) = synth_pg_roles(self);
1235                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1236                }
1237                // v7.39 (round 542) — pg_user is a DIFFERENT view over the
1238                // same roles, with PG's own `use*` column names. It used to
1239                // publish pg_roles' columns under this name.
1240                "__spg_pg_user" => {
1241                    let (schema, rows) = crate::system_catalog::synth_pg_user(self);
1242                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1243                }
1244                // v7.39 (read01 round 58) — role membership.
1245                "__spg_pg_auth_members" => {
1246                    let (schema, rows) = crate::system_catalog::synth_pg_auth_members(self);
1247                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1248                }
1249                // v7.17.0 Phase 3.P0-56 — pg_catalog.pg_views. PG's
1250                // pg_views surfaces every CREATE VIEW result; SPG
1251                // ships one row per declared view from the catalog.
1252                "__spg_pg_views" => {
1253                    let (schema, rows) = synth_pg_views(self.active_catalog());
1254                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1255                }
1256                // v7.39 (round 143) — pg_catalog.pg_rules: one row per
1257                // catalogued query-rewrite RULE.
1258                "__spg_pg_rules" => {
1259                    let (schema, rows) =
1260                        crate::system_catalog::synth_pg_rules(self.active_catalog());
1261                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1262                }
1263                // v7.39 (round 312) — pg_catalog.pg_rewrite: the rule
1264                // catalogue `pg_get_ruledef(oid)` resolves against.
1265                "__spg_pg_rewrite" => {
1266                    let (schema, rows) =
1267                        crate::system_catalog::synth_pg_rewrite(self.active_catalog());
1268                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1269                }
1270                // v7.39 (round 542) — pg_catalog.pg_matviews, with rows
1271                // and PG's own column names.
1272                "__spg_pg_matviews" => {
1273                    let (schema, rows) =
1274                        crate::system_catalog::synth_pg_matviews(self.active_catalog());
1275                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1276                }
1277                // pg_catalog.pg_extension — native capability list
1278                // (mailrs embed round-12).
1279                // v7.39 (round 546) — the catalogs SPG has real content
1280                // for, from the facts it already holds.
1281                "__spg_pg_db_role_setting" => {
1282                    let (schema, rows) = crate::system_catalog::synth_pg_db_role_setting(self);
1283                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1284                }
1285                "__spg_pg_language" => {
1286                    let (schema, rows) = crate::system_catalog::synth_pg_language();
1287                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1288                }
1289                "__spg_pg_sequences" => {
1290                    let (schema, rows) =
1291                        crate::system_catalog::synth_pg_sequences(self.active_catalog());
1292                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1293                }
1294                "__spg_pg_range" => {
1295                    let (schema, rows) = crate::system_catalog::synth_pg_range();
1296                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1297                }
1298                "__spg_pg_partitioned_table" => {
1299                    let (schema, rows) =
1300                        crate::system_catalog::synth_pg_partitioned_table(self.active_catalog());
1301                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1302                }
1303                "__spg_pg_authid" => {
1304                    let (schema, rows) = crate::system_catalog::synth_pg_authid(self);
1305                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1306                }
1307                "__spg_pg_group" => {
1308                    let (schema, rows) = crate::system_catalog::synth_pg_group(self);
1309                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1310                }
1311                "__spg_pg_shadow" => {
1312                    let (schema, rows) = crate::system_catalog::synth_pg_shadow(self);
1313                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1314                }
1315                // v7.39 (round 544) — pg_cast, probed from the real
1316                // cast implementation.
1317                "__spg_pg_cast" => {
1318                    let (schema, rows) = crate::system_catalog::synth_pg_cast();
1319                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1320                }
1321                // v7.39 (round 541) — an empty catalog that exists.
1322                "__spg_pg_foreign_table" => {
1323                    let (schema, rows) = crate::system_catalog::synth_pg_foreign_table();
1324                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1325                }
1326                "__spg_pg_extension" => {
1327                    let (schema, rows) = synth_pg_extension();
1328                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1329                }
1330                // v7.39 (round 502) — the timezone catalogues.
1331                "__spg_pg_timezone_names" => {
1332                    let (schema, rows) = synth_pg_timezone_names(self);
1333                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1334                }
1335                "__spg_pg_timezone_abbrevs" => {
1336                    let (schema, rows) = synth_pg_timezone_abbrevs(self);
1337                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1338                }
1339                // v7.17.0 Phase 3.P0-57 — pg_catalog.pg_settings.
1340                "__spg_pg_settings" => {
1341                    let (schema, rows) = synth_pg_settings(self);
1342                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1343                }
1344                // v7.17.0 Phase 3.P0-63 — information_schema.KEY_COLUMN_USAGE.
1345                // v7.39 (read01 round 51) — information_schema.role_table_grants
1346                // and .table_privileges. Both report the owner's seven implicit
1347                // table privileges; SPG's single role owns everything.
1348                // v7.39 (read01 round 59) — information_schema.column_privileges.
1349                "__spg_info_column_privileges" => {
1350                    let (schema, rows) =
1351                        crate::system_catalog::synth_info_column_privileges(self.active_catalog());
1352                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1353                }
1354                "__spg_info_role_table_grants" | "__spg_info_table_privileges" => {
1355                    let grantee = self.current_role().to_string();
1356                    let (schema, rows) = crate::system_catalog::synth_info_role_table_grants(
1357                        self.active_catalog(),
1358                        &grantee,
1359                    );
1360                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1361                }
1362                "__spg_info_key_column_usage" => {
1363                    let (schema, rows) = synth_info_key_column_usage(self.active_catalog());
1364                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1365                }
1366                // v7.17.0 Phase 3.P0-64 — information_schema.REFERENTIAL_CONSTRAINTS.
1367                "__spg_info_referential_constraints" => {
1368                    let (schema, rows) = synth_info_referential_constraints(self.active_catalog());
1369                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1370                }
1371                // v7.17.0 Phase 3.P0-64 — information_schema.STATISTICS.
1372                "__spg_info_statistics" => {
1373                    let (schema, rows) = synth_info_statistics(self.active_catalog());
1374                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1375                }
1376                // v7.17.0 Phase 3.P0-64 — information_schema.ROUTINES.
1377                "__spg_info_routines" => {
1378                    let (schema, rows) = synth_info_routines();
1379                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1380                }
1381                // v7.37.24 (24.3) — information_schema.attributes.
1382                "__spg_info_attributes" => {
1383                    let (schema, rows) = crate::system_catalog::synth_information_schema_attributes(
1384                        self.active_catalog(),
1385                    );
1386                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1387                }
1388                // v7.37.24 (24.2) — information_schema.domains.
1389                "__spg_info_domains" => {
1390                    let (schema, rows) = crate::system_catalog::synth_information_schema_domains(
1391                        self.active_catalog(),
1392                    );
1393                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1394                }
1395                // v7.37.24 (24.9) — information_schema.schemata.
1396                "__spg_info_schemata" => {
1397                    let (schema, rows) = crate::system_catalog::synth_information_schema_schemata(
1398                        self.active_catalog(),
1399                    );
1400                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1401                }
1402                // v7.37.24 (24.9) — information_schema.views.
1403                "__spg_info_views" => {
1404                    let (schema, rows) = crate::system_catalog::synth_information_schema_views(
1405                        self.active_catalog(),
1406                    );
1407                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1408                }
1409                // v7.37.24 (24.9) — information_schema.table_constraints.
1410                "__spg_info_table_constraints" => {
1411                    let (schema, rows) =
1412                        crate::system_catalog::synth_information_schema_table_constraints(
1413                            self.active_catalog(),
1414                        );
1415                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1416                }
1417                // v7.37.17 — information_schema.constraint_column_usage.
1418                "__spg_info_constraint_column_usage" => {
1419                    let (schema, rows) = crate::system_catalog::synth_info_constraint_column_usage(
1420                        self.active_catalog(),
1421                    );
1422                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1423                }
1424                // v7.37.17 — information_schema.triggers.
1425                "__spg_info_triggers" => {
1426                    let (schema, rows) =
1427                        crate::system_catalog::synth_info_triggers(self.active_catalog());
1428                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1429                }
1430                // v7.37.17 — information_schema.check_constraints.
1431                "__spg_info_check_constraints" => {
1432                    let (schema, rows) =
1433                        crate::system_catalog::synth_info_check_constraints(self.active_catalog());
1434                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1435                }
1436                // v7.37.17 — information_schema.sequences.
1437                "__spg_info_sequences" => {
1438                    let (schema, rows) =
1439                        crate::system_catalog::synth_info_sequences(self.active_catalog());
1440                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1441                }
1442                // v7.17.0 Phase 3.P0-65 — mysql.user / mysql.db.
1443                "__spg_mysql_user" => {
1444                    let (schema, rows) = synth_mysql_user(self);
1445                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1446                }
1447                "__spg_mysql_db" => {
1448                    let (schema, rows) = synth_mysql_db();
1449                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1450                }
1451                // v7.39 (round 541) — the catalogs PG has that SPG is
1452                // genuinely empty of. Table-driven; see EMPTY_PG_CATALOGS.
1453                other if crate::system_catalog::synth_empty_pg_catalog(other).is_some() => {
1454                    let (schema, rows) =
1455                        crate::system_catalog::synth_empty_pg_catalog(other).expect("just checked");
1456                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1457                }
1458                _ => {
1459                    return Err(EngineError::Unsupported(alloc::format!(
1460                        "meta view {view:?} is not yet materialisable; \
1461                         v7.16.2 covers information_schema.columns / .tables \
1462                         and pg_catalog.pg_class / pg_attribute; \
1463                         v7.17.0 P0-50..P0-57 add pg_type / pg_proc / pg_namespace / \
1464                         pg_indexes / pg_index / pg_constraint / pg_database / pg_roles / \
1465                         pg_user / pg_views / pg_matviews / pg_settings"
1466                    )));
1467                }
1468            }
1469        }
1470        Ok(catalog)
1471    }
1472
1473    pub(crate) fn exec_with_ctes(
1474        &self,
1475        stmt: &SelectStatement,
1476        cancel: CancelToken<'_>,
1477    ) -> Result<QueryResult, EngineError> {
1478        cancel.check()?;
1479        // v7.37.43-T4.4 — `&self` SELECT path: only read-only CTE
1480        // bodies are supported here. Writable CTEs on a SELECT
1481        // outer require `&mut self` and route through the
1482        // top-level `exec_select_cancel_mut` entry; sentori
1483        // 0065's WITH-INSERT-INSERT shape comes in as a top-level
1484        // INSERT, not a SELECT, so this restriction is harmless
1485        // in practice.
1486        if stmt.ctes.iter().any(|c| c.body.is_modifying()) {
1487            // v7.39 (read01 round 81) — PG's wording. A data-modifying CTE
1488            // (`WITH d AS (DELETE … RETURNING …) …`) is only legal at the top
1489            // of a statement, not nested inside a subquery; this path is
1490            // reached exactly when one is nested. The old text described SPG's
1491            // own executor plumbing ("the top-level mutable entry"), which
1492            // means nothing to a client.
1493            return Err(EngineError::Unsupported(
1494                "WITH clause containing a data-modifying statement must be at the top level".into(),
1495            ));
1496        }
1497        let catalog = self.materialise_ctes_readonly(&stmt.ctes, cancel)?;
1498        // Strip CTEs from the body before running on the temp engine
1499        // so we don't recurse forever.
1500        let mut body = stmt.clone();
1501        body.ctes = Vec::new();
1502        let mut temp = Engine::restore(catalog);
1503        if let Some(c) = self.clock {
1504            temp = temp.with_clock(c);
1505        }
1506        if let Some(f) = self.salt_fn {
1507            temp = temp.with_salt_fn(f);
1508        }
1509        temp.exec_select_cancel(&body, cancel)
1510    }
1511
1512    /// v7.37.43-T4.4 — read-only CTE materialiser used by the
1513    /// `&self` SELECT path. Caller guarantees no modifying CTE
1514    /// bodies are present.
1515    pub(crate) fn materialise_ctes_readonly(
1516        &self,
1517        ctes: &[spg_sql::ast::Cte],
1518        cancel: CancelToken<'_>,
1519    ) -> Result<crate::Catalog, EngineError> {
1520        cancel.check()?;
1521        let mut catalog = self.active_catalog().clone();
1522        for cte in ctes {
1523            let body_select = cte.body.as_select().ok_or_else(|| {
1524                EngineError::Unsupported(alloc::format!(
1525                    "data-modifying CTE not supported on this SELECT entry"
1526                ))
1527            })?;
1528            // v7.39 (round 156) — a CTE may SHADOW a same-named real table
1529            // (PG scoping: the WITH name wins for the outer query and later
1530            // CTEs, while THIS body still sees the real table — a
1531            // non-recursive body's self-name is the table, probe P2). This
1532            // materialiser works on a CLONE, so the shadow is simply: run
1533            // the body against the untouched clone, then drop the real
1534            // table from the clone before installing the CTE's temp. A
1535            // RECURSIVE self-reference is the CTE itself (P6), so there the
1536            // drop happens before the iterating materialiser runs.
1537            let (columns, rows) = if cte.recursive && select_refers_to(body_select, &cte.name) {
1538                let synthetic = spg_sql::ast::Cte {
1539                    name: cte.name.clone(),
1540                    body: spg_sql::ast::CteBody::Select(body_select.clone()),
1541                    recursive: true,
1542                    column_overrides: cte.column_overrides.clone(),
1543                    search: None,
1544                    cycle: None,
1545                };
1546                if catalog.get(&cte.name).is_some() {
1547                    let _ = catalog.drop_table(&cte.name);
1548                }
1549                self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1550            } else {
1551                let mut cte_engine = Engine::restore(catalog.clone());
1552                if let Some(c) = self.clock {
1553                    cte_engine = cte_engine.with_clock(c);
1554                }
1555                if let Some(f) = self.salt_fn {
1556                    cte_engine = cte_engine.with_salt_fn(f);
1557                }
1558                let body_result = cte_engine.exec_select_cancel(body_select, cancel)?;
1559                let QueryResult::Rows { columns, rows } = body_result else {
1560                    return Err(EngineError::Unsupported(alloc::format!(
1561                        "CTE {:?} body did not return rows",
1562                        cte.name
1563                    )));
1564                };
1565                (columns, rows)
1566            };
1567            let inferred = infer_column_types(&columns, &rows);
1568            let mut columns = inferred;
1569            if !cte.column_overrides.is_empty() {
1570                if cte.column_overrides.len() != columns.len() {
1571                    return Err(EngineError::Unsupported(alloc::format!(
1572                        "CTE {:?} column list has {} names but body returns {} columns",
1573                        cte.name,
1574                        cte.column_overrides.len(),
1575                        columns.len()
1576                    )));
1577                }
1578                for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1579                    col.name.clone_from(name);
1580                }
1581            }
1582            let schema = TableSchema::new(cte.name.clone(), columns);
1583            // v7.39 (round 156) — the body ran against the untouched clone;
1584            // from here on the CTE name resolves to the temp (PG scoping).
1585            if catalog.get(&cte.name).is_some() {
1586                let _ = catalog.drop_table(&cte.name);
1587            }
1588            catalog.create_table(schema).map_err(EngineError::Storage)?;
1589            let table = catalog
1590                .get_mut(&cte.name)
1591                .expect("just-created CTE table must exist");
1592            for row in rows {
1593                table.insert(row).map_err(EngineError::Storage)?;
1594            }
1595        }
1596        Ok(catalog)
1597    }
1598
1599    /// v7.37.43-T4.4 — shared CTE materialiser (mutable variant).
1600    /// Retained for non-DML callers; the DML path (writable CTE on
1601    /// INSERT/UPDATE/DELETE outer) uses `run_with_cte_temps` in
1602    /// `dml.rs` which installs the CTE temps directly on the
1603    /// active catalog so the outer statement's writes hit real
1604    /// tables.
1605    #[allow(dead_code)]
1606    pub(crate) fn materialise_ctes(
1607        &mut self,
1608        ctes: &[spg_sql::ast::Cte],
1609        cancel: CancelToken<'_>,
1610    ) -> Result<crate::Catalog, EngineError> {
1611        cancel.check()?;
1612        // v7.37.43-T4.4 — modifying CTEs need to write through the
1613        // SAME catalog as the outer statement, not a clone (PG's
1614        // writable CTE puts all modifications in one transaction).
1615        // For the read-only case the original logic cloned, but
1616        // since the outer statement also goes through the cloned
1617        // engine and ALL writes must converge, we now drive the
1618        // accumulator off `self.active_catalog().clone()` and
1619        // commit the modifying writes directly to `self`'s active
1620        // catalog so the surface is consistent.
1621        let mut catalog = self.active_catalog().clone();
1622        // v7.39 (round 149) — a modifying CTE body's target must be a
1623        // real relation, never a sibling CTE (PG: relation does not
1624        // exist); checked before any alias lands in the accumulator.
1625        for cte in ctes {
1626            let body_target = match &cte.body {
1627                spg_sql::ast::CteBody::Select(_) => None,
1628                spg_sql::ast::CteBody::Insert(i) => Some(i.table.as_str()),
1629                spg_sql::ast::CteBody::Update(u) => Some(u.table.as_str()),
1630                spg_sql::ast::CteBody::Delete(d) => Some(d.table.as_str()),
1631                spg_sql::ast::CteBody::Merge(m) => Some(m.target.as_str()),
1632            };
1633            if let Some(t) = body_target
1634                && ctes.iter().any(|c| c.name.eq_ignore_ascii_case(t))
1635                && catalog.get(t).is_none()
1636            {
1637                return Err(EngineError::Storage(
1638                    spg_storage::StorageError::TableNotFound { name: t.into() },
1639                ));
1640            }
1641        }
1642        for cte in ctes {
1643            if catalog.get(&cte.name).is_some() {
1644                return Err(EngineError::Unsupported(alloc::format!(
1645                    "CTE name {:?} shadows an existing table; rename the CTE",
1646                    cte.name
1647                )));
1648            }
1649            let (columns, rows) = match &cte.body {
1650                // v7.39 (round 145) — see the sibling site: only a body that
1651                // truly self-references takes the iterating materialiser.
1652                spg_sql::ast::CteBody::Select(body)
1653                    if cte.recursive && select_refers_to(body, &cte.name) =>
1654                {
1655                    // Recursive CTE — the existing helper takes a
1656                    // SELECT body and the snapshot catalog.
1657                    let synthetic = spg_sql::ast::Cte {
1658                        name: cte.name.clone(),
1659                        body: spg_sql::ast::CteBody::Select(body.clone()),
1660                        recursive: true,
1661                        column_overrides: cte.column_overrides.clone(),
1662                        search: None,
1663                        cycle: None,
1664                    };
1665                    self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1666                }
1667                spg_sql::ast::CteBody::Select(body) => {
1668                    // v7.25 (round-17) — run against the accumulated
1669                    // catalog so later CTEs can reference earlier
1670                    // ones in the same WITH clause.
1671                    let mut cte_engine = Engine::restore(catalog.clone());
1672                    if let Some(c) = self.clock {
1673                        cte_engine = cte_engine.with_clock(c);
1674                    }
1675                    if let Some(f) = self.salt_fn {
1676                        cte_engine = cte_engine.with_salt_fn(f);
1677                    }
1678                    let body_result = cte_engine.exec_select_cancel(body, cancel)?;
1679                    let QueryResult::Rows { columns, rows } = body_result else {
1680                        return Err(EngineError::Unsupported(alloc::format!(
1681                            "CTE {:?} body did not return rows",
1682                            cte.name
1683                        )));
1684                    };
1685                    (columns, rows)
1686                }
1687                spg_sql::ast::CteBody::Insert(body) => {
1688                    self.exec_modifying_cte_insert(&cte.name, body, cancel)?
1689                }
1690                spg_sql::ast::CteBody::Update(body) => {
1691                    self.exec_modifying_cte_update(&cte.name, body, cancel)?
1692                }
1693                spg_sql::ast::CteBody::Delete(body) => {
1694                    self.exec_modifying_cte_delete(&cte.name, body, cancel)?
1695                }
1696                spg_sql::ast::CteBody::Merge(body) => {
1697                    self.exec_modifying_cte_merge(&cte.name, body, cancel)?
1698                }
1699            };
1700            // v4.22: the projection builder labels any non-column
1701            // expression as Text — including literal SELECT 1.
1702            // Promote each column's type to whatever the rows
1703            // actually carry so the CTE storage table accepts them.
1704            let inferred = infer_column_types(&columns, &rows);
1705            let mut columns = inferred;
1706            if !cte.column_overrides.is_empty() {
1707                if cte.column_overrides.len() != columns.len() {
1708                    return Err(EngineError::Unsupported(alloc::format!(
1709                        "CTE {:?} column list has {} names but body returns {} columns",
1710                        cte.name,
1711                        cte.column_overrides.len(),
1712                        columns.len()
1713                    )));
1714                }
1715                for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1716                    col.name.clone_from(name);
1717                }
1718            }
1719            let schema = TableSchema::new(cte.name.clone(), columns);
1720            catalog.create_table(schema).map_err(EngineError::Storage)?;
1721            let table = catalog
1722                .get_mut(&cte.name)
1723                .expect("just-created CTE table must exist");
1724            for row in rows {
1725                table.insert(row).map_err(EngineError::Storage)?;
1726            }
1727        }
1728        Ok(catalog)
1729    }
1730
1731    /// v7.37.43-T4.4 — execute an INSERT CTE body. Runs the INSERT
1732    /// against `self` (so the mutation lands in the active catalog
1733    /// inside the current transaction) and captures the RETURNING
1734    /// projection — column schema + rows — to materialise as the
1735    /// CTE alias's table. An INSERT without RETURNING produces a
1736    /// 0-row table with a synthetic single-column placeholder
1737    /// (matches PG: the CTE alias is still defined, but referencing
1738    /// it from the outer query without RETURNING raises a
1739    /// column-resolution error at scan time).
1740    fn exec_modifying_cte_insert(
1741        &mut self,
1742        cte_name: &str,
1743        body: &spg_sql::ast::InsertStatement,
1744        _cancel: CancelToken<'_>,
1745    ) -> Result<
1746        (
1747            Vec<spg_storage::ColumnSchema>,
1748            Vec<spg_storage::Row<'static>>,
1749        ),
1750        EngineError,
1751    > {
1752        // round 151 — a WITH-headed body keeps its own ctes; the body
1753        // statement routes through its writable-CTE entry (outer CTEs
1754        // are never copied into bodies, so no recursion risk).
1755        let body = body.clone();
1756        let result = self.exec_insert(body)?;
1757        match result {
1758            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1759            QueryResult::CommandOk { .. } => {
1760                // No RETURNING — emit a sentinel single-column
1761                // schema with zero rows so the alias is defined.
1762                let placeholder = spg_storage::ColumnSchema::new(
1763                    alloc::format!("{cte_name}_returning_absent"),
1764                    spg_storage::DataType::Text,
1765                    true,
1766                );
1767                Ok((alloc::vec![placeholder], Vec::new()))
1768            }
1769        }
1770    }
1771
1772    /// v7.37.43-T4.4 — execute an UPDATE CTE body, same semantics
1773    /// as INSERT above.
1774    fn exec_modifying_cte_update(
1775        &mut self,
1776        cte_name: &str,
1777        body: &spg_sql::ast::UpdateStatement,
1778        cancel: CancelToken<'_>,
1779    ) -> Result<
1780        (
1781            Vec<spg_storage::ColumnSchema>,
1782            Vec<spg_storage::Row<'static>>,
1783        ),
1784        EngineError,
1785    > {
1786        let body = body.clone();
1787        let result = self.exec_update_cancel(&body, cancel)?;
1788        match result {
1789            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1790            QueryResult::CommandOk { .. } => {
1791                let placeholder = spg_storage::ColumnSchema::new(
1792                    alloc::format!("{cte_name}_returning_absent"),
1793                    spg_storage::DataType::Text,
1794                    true,
1795                );
1796                Ok((alloc::vec![placeholder], Vec::new()))
1797            }
1798        }
1799    }
1800
1801    /// v7.37.43-T4.4 — execute a DELETE CTE body.
1802    fn exec_modifying_cte_delete(
1803        &mut self,
1804        cte_name: &str,
1805        body: &spg_sql::ast::DeleteStatement,
1806        cancel: CancelToken<'_>,
1807    ) -> Result<
1808        (
1809            Vec<spg_storage::ColumnSchema>,
1810            Vec<spg_storage::Row<'static>>,
1811        ),
1812        EngineError,
1813    > {
1814        let body = body.clone();
1815        let result = self.exec_delete_cancel(&body, cancel)?;
1816        match result {
1817            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1818            QueryResult::CommandOk { .. } => {
1819                let placeholder = spg_storage::ColumnSchema::new(
1820                    alloc::format!("{cte_name}_returning_absent"),
1821                    spg_storage::DataType::Text,
1822                    true,
1823                );
1824                Ok((alloc::vec![placeholder], Vec::new()))
1825            }
1826        }
1827    }
1828
1829    /// v7.39 (round 149) — execute a MERGE CTE body (PG 17).
1830    fn exec_modifying_cte_merge(
1831        &mut self,
1832        cte_name: &str,
1833        body: &spg_sql::ast::MergeStatement,
1834        cancel: CancelToken<'_>,
1835    ) -> Result<
1836        (
1837            Vec<spg_storage::ColumnSchema>,
1838            Vec<spg_storage::Row<'static>>,
1839        ),
1840        EngineError,
1841    > {
1842        let body = body.clone();
1843        let result = self.exec_merge_cancel(&body, cancel)?;
1844        match result {
1845            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1846            QueryResult::CommandOk { .. } => {
1847                let placeholder = spg_storage::ColumnSchema::new(
1848                    alloc::format!("{cte_name}_returning_absent"),
1849                    spg_storage::DataType::Text,
1850                    true,
1851                );
1852                Ok((alloc::vec![placeholder], Vec::new()))
1853            }
1854        }
1855    }
1856
1857    /// v4.22: materialise a WITH RECURSIVE CTE. The body must be a
1858    /// UNION (or UNION ALL) of an anchor that does not reference
1859    /// the CTE name, and one or more recursive terms that do. The
1860    /// anchor runs first; each subsequent iteration runs the
1861    /// recursive term against a temp catalog where the CTE name is
1862    /// bound to the *previous* iteration's output. Iteration stops
1863    /// when the recursive term yields no rows; UNION (DISTINCT)
1864    /// deduplicates against the accumulated result, UNION ALL does
1865    /// not. A hard cap on total rows prevents runaway queries.
1866    #[allow(clippy::too_many_lines)]
1867    pub(crate) fn materialise_recursive_cte(
1868        &self,
1869        cte: &spg_sql::ast::Cte,
1870        base_catalog: &Catalog,
1871        cancel: CancelToken<'_>,
1872    ) -> Result<(Vec<ColumnSchema>, Vec<Row<'static>>), EngineError> {
1873        const MAX_TOTAL_ROWS: usize = 1_000_000;
1874        const MAX_ITERATIONS: usize = 100_000;
1875        cancel.check()?;
1876        // v7.37.43-T4.4 — RECURSIVE only supports SELECT bodies;
1877        // a modifying recursive CTE is parser-rejectable but we
1878        // guard here defensively.
1879        let body_select = cte.body.as_select().ok_or_else(|| {
1880            EngineError::Unsupported(alloc::format!(
1881                "WITH RECURSIVE {:?} body must be a SELECT, not a data-modifying statement",
1882                cte.name
1883            ))
1884        })?;
1885        if body_select.unions.is_empty() {
1886            return Err(EngineError::Unsupported(alloc::format!(
1887                "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
1888                cte.name
1889            )));
1890        }
1891        // Anchor: the body's leading SELECT, with unions stripped.
1892        let mut anchor = body_select.clone();
1893        let all_union_terms = core::mem::take(&mut anchor.unions);
1894        anchor.ctes = Vec::new();
1895        // v7.37 D.42 — split the UNION members: those that do NOT reference the
1896        // CTE are additional ANCHOR terms, only the ones that do recurse. A
1897        // multi-row VALUES seed lowers to `SELECT r1 UNION ALL SELECT r2 UNION
1898        // ALL <recursive>`, so the leading SELECT alone is not the whole anchor —
1899        // treating the non-recursive `SELECT r2` as a recursive term made it
1900        // re-emit its constant row every iteration → runaway loop.
1901        let (anchor_terms, union_terms): (Vec<_>, Vec<_>) = all_union_terms
1902            .into_iter()
1903            .partition(|(_, t)| !select_refers_to(t, &cte.name));
1904        let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
1905        let QueryResult::Rows {
1906            columns: anchor_cols,
1907            rows: mut anchor_rows,
1908        } = anchor_result
1909        else {
1910            return Err(EngineError::Unsupported(alloc::format!(
1911                "WITH RECURSIVE {:?}: anchor did not return rows",
1912                cte.name
1913            )));
1914        };
1915        // Append every non-recursive UNION member's rows to the anchor set.
1916        for (_, term) in &anchor_terms {
1917            let mut term = term.clone();
1918            term.ctes = Vec::new();
1919            if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&term, cancel)? {
1920                anchor_rows.extend(rows);
1921            }
1922        }
1923        // The projection builder labels non-column expressions Text;
1924        // refine column types from the anchor's actual values so the
1925        // intermediate iter-catalog tables accept them.
1926        let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
1927        if !cte.column_overrides.is_empty() {
1928            if cte.column_overrides.len() != columns.len() {
1929                return Err(EngineError::Unsupported(alloc::format!(
1930                    "CTE {:?} column list has {} names but anchor returns {} columns",
1931                    cte.name,
1932                    cte.column_overrides.len(),
1933                    columns.len()
1934                )));
1935            }
1936            for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1937                col.name.clone_from(name);
1938            }
1939        }
1940        let mut all_rows: Vec<Row<'static>> = anchor_rows.clone();
1941        let mut working_set: Vec<Row<'static>> = anchor_rows;
1942        let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
1943        // Track at least one "all UNION ALL" flag — if every union
1944        // kind is ALL we skip the dedup step (faster + matches PG).
1945        let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
1946        if !all_union_all {
1947            for r in &all_rows {
1948                seen.insert(encode_row_key(r));
1949            }
1950        }
1951        // v7.39 (round 598) — the engine and its catalog are built ONCE.
1952        // Each iteration used to clone the catalog, create the CTE table,
1953        // and construct a whole `Engine` — which initialises 82 fields — to
1954        // hold that round's working set. A counting allocator put the loop
1955        // at 63 allocations and 104 kB per iteration, or 1 GB for a
1956        // 10,000-row recursive CTE, and none of it varied with how much
1957        // else was in the catalog: the per-round rebuild WAS the cost. The
1958        // table is emptied and refilled instead.
1959        let mut iter_catalog = base_catalog.clone();
1960        let schema = TableSchema::new(cte.name.clone(), columns.clone());
1961        iter_catalog
1962            .create_table(schema)
1963            .map_err(EngineError::Storage)?;
1964        let mut iter_engine = Engine::restore(iter_catalog);
1965        if let Some(c) = self.clock {
1966            iter_engine = iter_engine.with_clock(c);
1967        }
1968        if let Some(f) = self.salt_fn {
1969            iter_engine = iter_engine.with_salt_fn(f);
1970        }
1971        // The recursive terms are cloned once too — the clone stripped the
1972        // CTE list off each of them, per term per iteration.
1973        let recursive_terms: Vec<SelectStatement> = union_terms
1974            .iter()
1975            .map(|(_, t)| {
1976                let mut t = t.clone();
1977                t.ctes = Vec::new();
1978                t
1979            })
1980            .collect();
1981        // v7.39 (round 618) — plan every recursive term once. Taken only if
1982        // ALL of them plan, so a query never runs half on each path.
1983        let term_plans: Option<Vec<RecursiveTermPlan<'_>>> = recursive_terms
1984            .iter()
1985            .map(|t| plan_recursive_term(t, &cte.name, columns.len()))
1986            .collect();
1987        let fast_ctx = term_plans.as_ref().map(|plans| {
1988            let alias = plans[0].alias.clone();
1989            (alias, ())
1990        });
1991        for iter in 0..MAX_ITERATIONS {
1992            cancel.check()?;
1993            if working_set.is_empty() {
1994                break;
1995            }
1996            if let (Some(plans), Some((_, ()))) = (term_plans.as_ref(), fast_ctx.as_ref()) {
1997                // The worktable IS the working set: no table to empty and
1998                // refill, and no query execution per round.
1999                let mut next_set: Vec<Row<'static>> = Vec::new();
2000                for plan in plans {
2001                    let ctx = self.ev_ctx(&columns, Some(&plan.alias));
2002                    for row in &working_set {
2003                        cancel.check()?;
2004                        if let Some(w) = plan.where_ {
2005                            let v = eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
2006                            if !matches!(v, Value::Bool(true)) {
2007                                continue;
2008                            }
2009                        }
2010                        let mut vals: Vec<Value<'static>> = Vec::with_capacity(plan.items.len());
2011                        for it in &plan.items {
2012                            vals.push(eval::eval_expr(it, row, &ctx).map_err(EngineError::Eval)?);
2013                        }
2014                        let out = Row::new(vals);
2015                        if !all_union_all {
2016                            let key = encode_row_key(&out);
2017                            if !seen.insert(key) {
2018                                continue;
2019                            }
2020                        }
2021                        next_set.push(out);
2022                    }
2023                }
2024                if next_set.is_empty() {
2025                    break;
2026                }
2027                all_rows.extend(next_set.iter().cloned());
2028                working_set = next_set;
2029                if all_rows.len() > MAX_TOTAL_ROWS {
2030                    return Err(EngineError::Unsupported(alloc::format!(
2031                        "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2032                        cte.name
2033                    )));
2034                }
2035                if iter + 1 == MAX_ITERATIONS {
2036                    return Err(EngineError::Unsupported(alloc::format!(
2037                        "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2038                        cte.name
2039                    )));
2040                }
2041                continue;
2042            }
2043            {
2044                // Truncated rather than dropped and recreated: the table's
2045                // own structure is what dropping it throws away, and it is
2046                // identical every round.
2047                let cat = iter_engine.base_catalog_mut();
2048                let table = cat.get_mut(&cte.name).expect("created above");
2049                table.truncate();
2050                for row in &working_set {
2051                    table.insert(row.clone()).map_err(EngineError::Storage)?;
2052                }
2053            }
2054            // Run each recursive term in sequence and collect new rows.
2055            let mut next_set: Vec<Row<'static>> = Vec::new();
2056            for term in &recursive_terms {
2057                let r = iter_engine.exec_select_cancel(term, cancel)?;
2058                let QueryResult::Rows {
2059                    columns: rc,
2060                    rows: rs,
2061                } = r
2062                else {
2063                    return Err(EngineError::Unsupported(alloc::format!(
2064                        "WITH RECURSIVE {:?}: recursive term did not return rows",
2065                        cte.name
2066                    )));
2067                };
2068                if rc.len() != columns.len() {
2069                    return Err(EngineError::Unsupported(alloc::format!(
2070                        "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
2071                        cte.name,
2072                        rc.len(),
2073                        columns.len()
2074                    )));
2075                }
2076                for row in rs {
2077                    if !all_union_all {
2078                        let key = encode_row_key(&row);
2079                        if !seen.insert(key) {
2080                            continue;
2081                        }
2082                    }
2083                    next_set.push(row);
2084                }
2085            }
2086            if next_set.is_empty() {
2087                break;
2088            }
2089            all_rows.extend(next_set.iter().cloned());
2090            working_set = next_set;
2091            if all_rows.len() > MAX_TOTAL_ROWS {
2092                return Err(EngineError::Unsupported(alloc::format!(
2093                    "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2094                    cte.name
2095                )));
2096            }
2097            if iter + 1 == MAX_ITERATIONS {
2098                return Err(EngineError::Unsupported(alloc::format!(
2099                    "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2100                    cte.name
2101                )));
2102            }
2103        }
2104        Ok((columns, all_rows))
2105    }
2106
2107    pub(crate) fn resolve_select_subqueries(
2108        &self,
2109        stmt: &mut SelectStatement,
2110        cancel: CancelToken<'_>,
2111    ) -> Result<(), EngineError> {
2112        for item in &mut stmt.items {
2113            if let SelectItem::Expr { expr, alias } = item {
2114                // An UNCORRELATED subquery is replaced by its value right
2115                // here, and the shape the column was named for goes with
2116                // it: by projection time `SELECT EXISTS(SELECT 1)` is a
2117                // boolean literal, so SPG answered `?column?` where PG18
2118                // answers `exists`. Only a subquery at the TOP of the item
2119                // loses its name this way — one nested inside a call still
2120                // reports the call.
2121                if alias.is_none()
2122                    && matches!(
2123                        expr,
2124                        Expr::ScalarSubquery(_)
2125                            | Expr::Exists { .. }
2126                            | Expr::InSubquery { .. }
2127                            | Expr::RowInSubquery { .. }
2128                            | Expr::RowCmpSubquery { .. }
2129                    )
2130                {
2131                    *alias = Some(default_output_name(expr, self.backslash_escapes));
2132                }
2133                self.resolve_expr_subqueries(expr, cancel)?;
2134            }
2135        }
2136        if let Some(w) = &mut stmt.where_ {
2137            self.resolve_expr_subqueries(w, cancel)?;
2138        }
2139        // v7.24.1 — JOIN ON conditions can carry subqueries too;
2140        // they were never walked, so even an UNCORRELATED subquery
2141        // in ON hit "subquery reached row eval".
2142        if let Some(from) = &mut stmt.from {
2143            for j in &mut from.joins {
2144                if let Some(on) = &mut j.on {
2145                    self.resolve_expr_subqueries(on, cancel)?;
2146                }
2147            }
2148        }
2149        if let Some(gs) = &mut stmt.group_by {
2150            for g in gs {
2151                self.resolve_expr_subqueries(g, cancel)?;
2152            }
2153        }
2154        if let Some(h) = &mut stmt.having {
2155            self.resolve_expr_subqueries(h, cancel)?;
2156        }
2157        for o in &mut stmt.order_by {
2158            self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2159        }
2160        for (_, peer) in &mut stmt.unions {
2161            self.resolve_select_subqueries(peer, cancel)?;
2162        }
2163        Ok(())
2164    }
2165
2166    #[allow(clippy::only_used_in_recursion)] // engine handle reads aren't really pure
2167    pub(crate) fn resolve_expr_subqueries(
2168        &self,
2169        e: &mut Expr,
2170        cancel: CancelToken<'_>,
2171    ) -> Result<(), EngineError> {
2172        // Replace-on-this-node cases first.
2173        if let Some(replacement) = self.subquery_replacement(e, cancel)? {
2174            *e = replacement;
2175            return Ok(());
2176        }
2177        match e {
2178            Expr::NamedArg { expr, .. } => self.resolve_expr_subqueries(expr, cancel)?,
2179            Expr::Variadic(expr) => self.resolve_expr_subqueries(expr, cancel)?,
2180            Expr::AggregateOrdered { call, order_by, .. } => {
2181                self.resolve_expr_subqueries(call, cancel)?;
2182                for o in order_by.iter_mut() {
2183                    self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2184                }
2185            }
2186            Expr::Binary { lhs, rhs, .. } => {
2187                self.resolve_expr_subqueries(lhs, cancel)?;
2188                self.resolve_expr_subqueries(rhs, cancel)?;
2189            }
2190            Expr::Unary { expr, .. }
2191            | Expr::Cast { expr, .. }
2192            | Expr::IsNull { expr, .. }
2193            | Expr::BoolTest { expr, .. }
2194            | Expr::FieldAccess { base: expr, .. } => {
2195                self.resolve_expr_subqueries(expr, cancel)?;
2196            }
2197            Expr::FunctionCall { args, .. } => {
2198                for a in args {
2199                    self.resolve_expr_subqueries(a, cancel)?;
2200                }
2201            }
2202            Expr::Like { expr, pattern, .. } => {
2203                self.resolve_expr_subqueries(expr, cancel)?;
2204                self.resolve_expr_subqueries(pattern, cancel)?;
2205            }
2206            Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
2207            // v4.12 window functions — recurse into args + ORDER BY
2208            // + PARTITION BY in case they carry inner subqueries.
2209            Expr::WindowFunction {
2210                args,
2211                partition_by,
2212                order_by,
2213                ..
2214            } => {
2215                for a in args {
2216                    self.resolve_expr_subqueries(a, cancel)?;
2217                }
2218                for p in partition_by {
2219                    self.resolve_expr_subqueries(p, cancel)?;
2220                }
2221                for (e, _, _) in order_by {
2222                    self.resolve_expr_subqueries(e, cancel)?;
2223                }
2224            }
2225            // Subquery nodes are handled in subquery_replacement
2226            // (which returned None — defensive no-op); Literal /
2227            // Column are leaves.
2228            Expr::ScalarSubquery(_)
2229            | Expr::Exists { .. }
2230            | Expr::InSubquery { .. }
2231            | Expr::RowInSubquery { .. }
2232            | Expr::RowCmpSubquery { .. }
2233            | Expr::Literal(_)
2234            | Expr::Placeholder(_)
2235            | Expr::Column(_) => {}
2236            // v7.30.2 — list elements can carry scalar subqueries
2237            // (`x IN (1, (SELECT …))`).
2238            Expr::InList { expr, list, .. } => {
2239                self.resolve_expr_subqueries(expr, cancel)?;
2240                for item in list {
2241                    self.resolve_expr_subqueries(item, cancel)?;
2242                }
2243            }
2244            // v7.10.10 — recurse children.
2245            Expr::Array(items) => {
2246                for elem in items {
2247                    self.resolve_expr_subqueries(elem, cancel)?;
2248                }
2249            }
2250            Expr::ArraySubscript { target, index } => {
2251                self.resolve_expr_subqueries(target, cancel)?;
2252                self.resolve_expr_subqueries(index, cancel)?;
2253            }
2254            Expr::ArraySlice { target, lo, hi } => {
2255                self.resolve_expr_subqueries(target, cancel)?;
2256                if let Some(l) = lo {
2257                    self.resolve_expr_subqueries(l, cancel)?;
2258                }
2259                if let Some(h) = hi {
2260                    self.resolve_expr_subqueries(h, cancel)?;
2261                }
2262            }
2263            Expr::AnyAll { expr, array, .. } => {
2264                self.resolve_expr_subqueries(expr, cancel)?;
2265                // Quantified subquery — an uncorrelated one
2266                // materialises up front; a correlated one stays for
2267                // the per-row resolver.
2268                if let Expr::ScalarSubquery(inner) = array.as_mut() {
2269                    if !crate::subquery::select_is_correlated(inner) {
2270                        let s = (**inner).clone();
2271                        **array = self.materialize_quantified_rows(&s, cancel)?;
2272                    }
2273                } else {
2274                    self.resolve_expr_subqueries(array, cancel)?;
2275                }
2276            }
2277            Expr::Case {
2278                operand,
2279                branches,
2280                else_branch,
2281            } => {
2282                if let Some(o) = operand {
2283                    self.resolve_expr_subqueries(o, cancel)?;
2284                }
2285                for (w, t) in branches {
2286                    self.resolve_expr_subqueries(w, cancel)?;
2287                    self.resolve_expr_subqueries(t, cancel)?;
2288                }
2289                if let Some(e) = else_branch {
2290                    self.resolve_expr_subqueries(e, cancel)?;
2291                }
2292            }
2293        }
2294        Ok(())
2295    }
2296}
2297
2298impl Engine {
2299    /// v6.10.2 — projection for AS OF SEGMENT. Resolves
2300    /// `SelectItem::Wildcard` to all schema columns and
2301    /// `SelectItem::Expr` via the regular eval path.
2302    pub(crate) fn project_row_simple(
2303        &self,
2304        row: &Row<'static>,
2305        items: &[SelectItem],
2306        schema_cols: &[ColumnSchema],
2307        alias: &str,
2308    ) -> Result<Row<'static>, EngineError> {
2309        let ctx = self.ev_ctx(schema_cols, Some(alias));
2310        let cancel = CancelToken::none();
2311        let mut out_vals = Vec::new();
2312        for item in items {
2313            match item {
2314                // In a single-table projection (AS OF SEGMENT / RETURNING) a
2315                // qualified `t.*` covers exactly the same columns as a bare `*`.
2316                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2317                    out_vals.extend(row.values.iter().cloned());
2318                }
2319                SelectItem::Expr { expr, .. } => {
2320                    let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
2321                    out_vals.push(v);
2322                }
2323            }
2324        }
2325        Ok(Row::new(out_vals))
2326    }
2327
2328    /// v6.10.2 — derive the output `ColumnSchema` list for an
2329    /// AS OF SEGMENT projection. Wildcards take the full schema;
2330    /// expressions take the alias if present or a synthetic
2331    /// `?column?` (PG convention) otherwise.
2332    pub(crate) fn derive_output_columns(
2333        &self,
2334        items: &[SelectItem],
2335        schema_cols: &[ColumnSchema],
2336        table_alias: &str,
2337    ) -> Vec<ColumnSchema> {
2338        let mut out = Vec::new();
2339        for item in items {
2340            match item {
2341                // `t.*` / `OLD.*` / `NEW.*` all mirror the full table schema in
2342                // a single-table projection.
2343                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2344                    out.extend(schema_cols.iter().cloned());
2345                }
2346                SelectItem::Expr { expr, alias } => {
2347                    // Bare column references inherit the schema
2348                    // column's name + type — PG names `RETURNING id`
2349                    // "id" and types it BIGINT, and the sqlx embed
2350                    // path type-checks RowDescription against the
2351                    // Rust target (mailrs embed round-12).
2352                    if let Expr::Column(col) = expr
2353                        && let Some(sc) = schema_cols.iter().find(|c| c.name == col.name)
2354                    {
2355                        let name = alias.clone().unwrap_or_else(|| sc.name.clone());
2356                        let mut c = ColumnSchema::new(name, sc.ty, sc.nullable);
2357                        // v7.39 (read01 round 54) — carry the enum identity:
2358                        // it lives outside the DataType lattice, so a derived
2359                        // table built from this schema otherwise forgets it and
2360                        // the OUTER `ORDER BY <enum col>` silently sorts by the
2361                        // label's TEXT instead of member order.
2362                        c.user_enum_type = sc.user_enum_type.clone();
2363                        out.push(c);
2364                        continue;
2365                    }
2366                    let name = alias.clone().unwrap_or_else(|| "?column?".to_string());
2367                    // v7.30.4 (mailrs round-27, P0) — type the
2368                    // expression with the same inference the SELECT
2369                    // list uses (INT−INT=INT, BIGINT+INT=BIGINT…).
2370                    // The old Text default broke every typed decode
2371                    // of `RETURNING uidnext - 1 AS uid`: four days
2372                    // of inbound mail indexed nowhere. Inference
2373                    // failure keeps the old Text fallback rather
2374                    // than inventing new error paths here.
2375                    // v7.39 (round 258) — take the enum identity from the
2376                    // same projection build, not just the type: a constant
2377                    // SELECT (`SELECT 'ok'::mood AS x`, which is what a
2378                    // VALUES row lowers to) is an EXPRESSION, so it landed
2379                    // here and the derived table forgot the enum.
2380                    let (ty, nullable) = build_projection(
2381                        core::slice::from_ref(item),
2382                        schema_cols,
2383                        table_alias,
2384                        self.backslash_escapes,
2385                    )
2386                    .ok()
2387                    .and_then(|p| p.into_iter().next())
2388                    .map_or((DataType::Text, true), |p| (p.ty, p.nullable));
2389                    out.push(ColumnSchema::new(name, ty, nullable));
2390                }
2391            }
2392        }
2393        out
2394    }
2395
2396    /// v4.5: SELECT with cooperative cancellation. The token is
2397    /// honoured between UNION peers and inside the bare-SELECT row
2398    /// loop; HNSW kNN graph walks and the aggregate executor don't
2399    /// honour it yet (deferred — those paths bound their work
2400    /// internally by `LIMIT k` and `GROUP BY` cardinality).
2401    /// v7.38 (read01 P3.NEW3) — materialise a `spg_*` / `pg_*` meta-view by
2402    /// its (lowercased) name, or None if the name isn't a virtual view.
2403    /// Callers decide whether to return it directly (`SELECT *`) or stage
2404    /// it as a temp table for the full query pipeline.
2405    fn meta_view_result(&self, name: &str) -> Option<QueryResult> {
2406        Some(match name {
2407            "spg_statistic" => self.exec_spg_statistic(),
2408            "spg_stat_replication" => self.exec_spg_stat_replication(),
2409            "spg_stat_segment" => self.exec_spg_stat_segment(),
2410            "spg_memory_stats" => self.exec_spg_memory_stats(),
2411            "spg_stat_query" => self.exec_spg_stat_query(),
2412            "pg_stat_statements" => self.exec_pg_stat_statements(),
2413            "spg_stat_activity" => self.exec_spg_stat_activity(),
2414            "pg_stat_activity" => self.exec_pg_stat_activity(),
2415            "pg_locks" => self.exec_pg_locks(),
2416            "pg_statio_user_tables" => self.exec_pg_statio_user_tables(),
2417            "spg_stat_mvcc" => self.exec_spg_stat_mvcc(),
2418            "spg_partition_health" => self.exec_spg_partition_health(),
2419            "spg_audit_chain" => self.exec_spg_audit_chain(),
2420            "spg_audit_verify" => self.exec_spg_audit_verify(),
2421            "spg_table_ddl" => self.exec_spg_table_ddl(),
2422            "spg_role_ddl" => self.exec_spg_role_ddl(),
2423            "spg_database_ddl" => self.exec_spg_database_ddl(),
2424            _ => return None,
2425        })
2426    }
2427
2428    /// v7.39 (round 462) — the catalog an admin / stat view SELECT
2429    /// describes against: this engine's catalog with the view staged as a
2430    /// table, exactly as `exec_select_cancel_as` stages it for a
2431    /// non-bare query.
2432    ///
2433    /// These views never reach the catalog — each is a fixed row set built
2434    /// inside its own `exec_*` — so Describe reported no columns for all
2435    /// seventeen of them. Rows are deliberately not inserted: Describe
2436    /// only needs the shape, and `infer_column_types` reads the rows we
2437    /// already have in hand.
2438    pub(crate) fn admin_view_catalog(&self, stmt: &SelectStatement) -> Option<Catalog> {
2439        let from = stmt.from.as_ref()?;
2440        if !from.joins.is_empty() || self.active_catalog().get(&from.primary.name).is_some() {
2441            return None;
2442        }
2443        let lower = from.primary.name.to_ascii_lowercase();
2444        let QueryResult::Rows { columns, rows } = self.meta_view_result(&lower)? else {
2445            return None;
2446        };
2447        let mut catalog = self.active_catalog().clone();
2448        let cols = infer_column_types(&columns, &rows);
2449        catalog
2450            .create_table(TableSchema::new(from.primary.name.clone(), cols))
2451            .ok()?;
2452        Some(catalog)
2453    }
2454
2455    pub(crate) fn exec_select_cancel(
2456        &self,
2457        stmt: &SelectStatement,
2458        cancel: CancelToken<'_>,
2459    ) -> Result<QueryResult, EngineError> {
2460        self.exec_select_cancel_as(stmt, cancel, None)
2461    }
2462
2463    /// v7.39 (round 334, V55) — the same read core, authorised as
2464    /// `as_role`. A `SECURITY DEFINER` function's body runs as the
2465    /// function's OWNER: that is the entire point of the form, and without
2466    /// it every definer function failed with "permission denied" on the
2467    /// very table it exists to expose.
2468    /// v7.39 (round 559) — see the call site. `None` for anything but
2469    /// the bare shape, so every other query keeps its old path.
2470    fn try_bare_count_star(
2471        &self,
2472        stmt: &SelectStatement,
2473        as_role: Option<&str>,
2474    ) -> Result<Option<QueryResult>, EngineError> {
2475        use spg_sql::ast::SelectItem;
2476        if as_role.is_some()
2477            || !stmt.ctes.is_empty()
2478            || !stmt.unions.is_empty()
2479            || stmt.where_.is_some()
2480            || stmt.group_by.is_some()
2481            || stmt.having.is_some()
2482            || stmt.distinct
2483            || !stmt.order_by.is_empty()
2484            || stmt.limit.is_some()
2485            || stmt.offset.is_some()
2486            || stmt.items.len() != 1
2487        {
2488            return Ok(None);
2489        }
2490        let Some(from) = &stmt.from else {
2491            return Ok(None);
2492        };
2493        if !from.joins.is_empty()
2494            || stmt.locking.is_some()
2495            || from.primary.lateral_subquery.is_some()
2496            || from.primary.unnest_expr.is_some()
2497            || from.primary.generate_series_args.is_some()
2498            || from.primary.name.is_empty()
2499            || from.primary.name.starts_with("__spg_")
2500        {
2501            return Ok(None);
2502        }
2503        // A partition PARENT holds no rows of its own — they live in the
2504        // children — so its header count is 0 and the ordinary path has
2505        // to fan out. Caught by the partition conformance cases.
2506        //
2507        // v7.39 (round 645) — and an INHERITANCE parent holds only SOME
2508        // of them, which is worse: its header count is a real number,
2509        // just not the answer. `SELECT count(*) FROM par` returned 1
2510        // where PG returns 2, because this shortcut fired before the
2511        // fan-out could. The question is "does anything descend from
2512        // this", not "was it declared a partition parent".
2513        if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2514            return Ok(None);
2515        }
2516        let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2517            return Ok(None);
2518        };
2519        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
2520            return Ok(None);
2521        };
2522        if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
2523            return Ok(None);
2524        }
2525        // A row-security policy filters rows, so the header count is not
2526        // the answer; the ordinary path applies the policy.
2527        let Some(table) = self.active_catalog().get(&from.primary.name) else {
2528            return Ok(None);
2529        };
2530        if table.schema().row_security {
2531            return Ok(None);
2532        }
2533        // Rows frozen to the cold tier are not in `headers`, so the
2534        // header count would miss them. Caught by the cold-tier e2e.
2535        if table.has_cold_rows_fast() {
2536            return Ok(None);
2537        }
2538        let n = table.count_visible(&self.current_snapshot());
2539        let col = alias.clone().unwrap_or_else(|| String::from("count"));
2540        Ok(Some(QueryResult::Rows {
2541            columns: alloc::vec![ColumnSchema::new(col, DataType::BigInt, false)],
2542            rows: alloc::vec![Row::new(alloc::vec![Value::BigInt(
2543                i64::try_from(n).unwrap_or(i64::MAX)
2544            )])],
2545        }))
2546    }
2547
2548    /// v7.39 (round 560) — `SELECT <indexed col> FROM t WHERE <range on
2549    /// that col>` served from the index, never reading a row.
2550    ///
2551    /// Measured over pgwire on a 500k table, a 100k-row range: PG18's
2552    /// Index Only Scan 3.6 ms against SPG's 30 ms, widening with the row
2553    /// count (2x at 1k). PG needs its visibility map for this — a heap
2554    /// tuple carries its own visibility, so an index entry alone cannot
2555    /// say whether the row is live, and PG reads the heap for any page
2556    /// the map does not mark all-visible. SPG keeps a header array
2557    /// beside the rows, so the locator answers it directly and there is
2558    /// no map to be stale.
2559    /// v7.39 (round 564) — the shape test, once, for both the
2560    /// materialising scan and the streaming one.
2561    ///
2562    /// Two callers asking the same question in two places is how a fact
2563    /// starts drifting; the answer here is the single copy. Returns the
2564    /// table, the alias the predicate is written against, the projected
2565    /// column's position, and the name the single output column takes.
2566    pub(crate) fn index_only_shape<'s>(
2567        &'s self,
2568        stmt: &'s SelectStatement,
2569    ) -> Option<(&'s spg_storage::Table, &'s str, usize, String)> {
2570        use spg_sql::ast::SelectItem;
2571        if !stmt.ctes.is_empty()
2572            || !stmt.unions.is_empty()
2573            || stmt.group_by.is_some()
2574            || stmt.having.is_some()
2575            || stmt.distinct
2576            || stmt.locking.is_some()
2577            || !stmt.order_by.is_empty()
2578            || stmt.limit.is_some()
2579            || stmt.offset.is_some()
2580            || stmt.items.len() != 1
2581        {
2582            return None;
2583        }
2584        let (Some(from), Some(_)) = (&stmt.from, &stmt.where_) else {
2585            return None;
2586        };
2587        if !from.joins.is_empty()
2588            || from.primary.lateral_subquery.is_some()
2589            || from.primary.unnest_expr.is_some()
2590            || from.primary.generate_series_args.is_some()
2591            || from.primary.name.is_empty()
2592            || from.primary.name.starts_with("__spg_")
2593        {
2594            return None;
2595        }
2596        // v7.39 (round 645) — see the note on the sibling shortcut above:
2597        // an inheritance parent's own header count is not the answer.
2598        if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2599            return None;
2600        }
2601        let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2602            return None;
2603        };
2604        let spg_sql::ast::Expr::Column(c) = expr else {
2605            return None;
2606        };
2607        let alias_name = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
2608        if let Some(q) = c.qualifier.as_deref()
2609            && !q.eq_ignore_ascii_case(alias_name)
2610        {
2611            return None;
2612        }
2613        let table = self.active_catalog().get(&from.primary.name)?;
2614        if table.schema().row_security {
2615            return None;
2616        }
2617        let cols = &table.schema().columns;
2618        let pos = cols
2619            .iter()
2620            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
2621        let out = alias.clone().unwrap_or_else(|| cols[pos].name.clone());
2622        Some((table, alias_name, pos, out))
2623    }
2624
2625    /// v7.39 (round 565) — would this statement be answered out of the
2626    /// index alone?
2627    ///
2628    /// EXPLAIN has to name the node the executor will actually run, and
2629    /// the only honest way to know is to ask the same two questions the
2630    /// executor asks: the statement's shape, and everything decidable
2631    /// about the scan before it walks. Neither is re-stated here.
2632    pub(crate) fn stmt_takes_index_only_scan(&self, stmt: &SelectStatement) -> bool {
2633        let Some((table, alias_name, pos, _)) = self.index_only_shape(stmt) else {
2634            return false;
2635        };
2636        let Some(where_) = stmt.where_.as_ref() else {
2637            return false;
2638        };
2639        crate::index_access::index_only_precheck(
2640            where_,
2641            &table.schema().columns,
2642            table,
2643            alias_name,
2644            pos,
2645        )
2646        .is_some()
2647    }
2648
2649    fn try_index_only_scan(
2650        &self,
2651        stmt: &SelectStatement,
2652    ) -> Result<Option<QueryResult>, EngineError> {
2653        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2654            return Ok(None);
2655        };
2656        let where_ = stmt.where_.as_ref().expect("shape checked it");
2657        let cols = &table.schema().columns;
2658        let Some(values) = crate::index_access::try_index_only_range(
2659            where_,
2660            cols,
2661            table,
2662            alias_name,
2663            &self.current_snapshot(),
2664            pos,
2665        ) else {
2666            return Ok(None);
2667        };
2668        let schema = alloc::vec![ColumnSchema::new(
2669            out_name,
2670            cols[pos].ty,
2671            cols[pos].nullable
2672        )];
2673        Ok(Some(QueryResult::Rows {
2674            columns: schema,
2675            rows: values
2676                .into_iter()
2677                .map(|v| Row::new(alloc::vec![v]))
2678                .collect(),
2679        }))
2680    }
2681
2682    /// v7.39 (round 564) — the same scan, emitting each value instead of
2683    /// building a `Vec<Row>` for the encoder to walk once and drop.
2684    ///
2685    /// A profile of the server serving a 50k-row range put 10.2% of the
2686    /// connection thread's CPU on BUILDING that vector and another 9.7%
2687    /// on dropping it — a fifth of the query, spent allocating and
2688    /// freeing one single-element `Vec` per output row so that the wire
2689    /// encoder could borrow each value for a few nanoseconds. The
2690    /// streaming interface it then hands them to takes `&[Value]`
2691    /// already.
2692    ///
2693    /// Returns `None` when the shape does not apply, so the caller falls
2694    /// back before anything has been emitted.
2695    pub(crate) fn try_index_only_stream<F>(
2696        &self,
2697        stmt: &SelectStatement,
2698        emit: &mut F,
2699    ) -> Result<Option<usize>, EngineError>
2700    where
2701        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
2702    {
2703        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2704            return Ok(None);
2705        };
2706        let where_ = stmt.where_.as_ref().expect("shape checked it");
2707        let cols = &table.schema().columns;
2708        let schema = alloc::vec![ColumnSchema::new(
2709            out_name,
2710            cols[pos].ty,
2711            cols[pos].nullable
2712        )];
2713        let snapshot = self.current_snapshot();
2714        // The header goes out only once the walk has agreed to run — a
2715        // shape rejection after it would leave the client with a
2716        // RowDescription for a result that never comes.
2717        let mut wrote_header = false;
2718        let counted = crate::index_access::index_only_range_each(
2719            where_,
2720            cols,
2721            table,
2722            alias_name,
2723            &snapshot,
2724            pos,
2725            &mut |v: spg_storage::Value<'_>| {
2726                if !wrote_header {
2727                    emit(crate::StreamItem::Header(&schema))?;
2728                    wrote_header = true;
2729                }
2730                emit(crate::StreamItem::Row(crate::RowCells::Refs(&[&v])))
2731            },
2732        );
2733        match counted {
2734            None => Ok(None),
2735            Some(Err(e)) => Err(e),
2736            Some(Ok(n)) => {
2737                if !wrote_header {
2738                    emit(crate::StreamItem::Header(&schema))?;
2739                }
2740                Ok(Some(n))
2741            }
2742        }
2743    }
2744
2745    /// `DISTINCT ON`'s de-duplication, which runs after the inner
2746    /// SELECT has produced its rows.
2747    ///
2748    /// `#[inline(never)]` and out of `exec_select_cancel_as` for the
2749    /// reason round 848 established: a debug build gives every branch's
2750    /// locals a slot in the frame whichever branch runs, and this one is
2751    /// eighty lines of hashing, key slicing and survivor sorting that a
2752    /// statement without `DISTINCT ON` never touches. Round 867
2753    /// measured `exec_select_cancel_as` holding ~46 KB on a path that
2754    /// reaches none of it — the segment that had been blamed on
2755    /// `exec_bare_select_cancel`, which turned out to hold 2 KB.
2756    #[inline(never)]
2757    fn apply_distinct_on(
2758        &self,
2759        result: QueryResult,
2760        don_hidden: usize,
2761        don_limit: &(
2762            Option<spg_sql::ast::LimitExpr>,
2763            Option<spg_sql::ast::LimitExpr>,
2764        ),
2765        don_top1: usize,
2766        orig_order_by: &[spg_sql::ast::OrderBy],
2767    ) -> Result<QueryResult, EngineError> {
2768        let QueryResult::Rows { columns, rows } = result else {
2769            return Ok(result);
2770        };
2771        // The keys are the hidden trailing columns appended above.
2772        // v7.39 (round 729) — top-1 mode: the trailing columns are the
2773        // DON keys plus the ORDER tail; keep each group's best in one
2774        // hash pass, then sort the SURVIVORS with the original spec.
2775        let mut kept: alloc::vec::Vec<Row<'static>>;
2776        let key_start;
2777        if don_top1 > 0 {
2778            let tail = don_top1 - 1;
2779            key_start = columns.len().saturating_sub(don_hidden + tail);
2780            let ord_start = key_start + don_hidden;
2781            let tail_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by[don_hidden..]
2782                .iter()
2783                .map(|o| (o.desc, o.nulls_first))
2784                .collect();
2785            let mysql = self.backslash_escapes;
2786            let better = |a: &Row<'static>, b: &Row<'static>| -> bool {
2787                for (k, (desc, nf)) in tail_dirs.iter().enumerate() {
2788                    let av = a.values.get(ord_start + k).unwrap_or(&Value::Null);
2789                    let bv = b.values.get(ord_start + k).unwrap_or(&Value::Null);
2790                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2791                        core::cmp::Ordering::Less => return true,
2792                        core::cmp::Ordering::Greater => return false,
2793                        core::cmp::Ordering::Equal => {}
2794                    }
2795                }
2796                false
2797            };
2798            let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2799            let mut best: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
2800            let mut keybuf = String::new();
2801            for row in rows {
2802                keybuf.clear();
2803                for v in row.values.get(key_start..ord_start).unwrap_or(&[]) {
2804                    aggregate::push_canonical_key(&mut keybuf, v);
2805                }
2806                match slot.get(keybuf.as_str()) {
2807                    Some(&i) => {
2808                        if better(&row, &best[i]) {
2809                            best[i] = row;
2810                        }
2811                    }
2812                    None => {
2813                        slot.insert(keybuf.clone(), best.len());
2814                        best.push(row);
2815                    }
2816                }
2817            }
2818            // Survivors sort with the FULL original spec (keys are still
2819            // aboard as hidden columns).
2820            let full_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by
2821                .iter()
2822                .map(|o| (o.desc, o.nulls_first))
2823                .collect();
2824            best.sort_by(|a, b| {
2825                for (k, (desc, nf)) in full_dirs.iter().enumerate() {
2826                    let av = a.values.get(key_start + k).unwrap_or(&Value::Null);
2827                    let bv = b.values.get(key_start + k).unwrap_or(&Value::Null);
2828                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2829                        core::cmp::Ordering::Equal => {}
2830                        o => return o,
2831                    }
2832                }
2833                core::cmp::Ordering::Equal
2834            });
2835            for r in &mut best {
2836                r.values.truncate(key_start);
2837            }
2838            kept = best;
2839        } else {
2840            key_start = columns.len().saturating_sub(don_hidden);
2841            let mut seen: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
2842            kept = alloc::vec::Vec::new();
2843            for mut row in rows {
2844                let key: alloc::vec::Vec<Value<'static>> =
2845                    row.values.get(key_start..).unwrap_or(&[]).to_vec();
2846                if seen.iter().any(|k| k == &key) {
2847                    continue;
2848                }
2849                seen.push(key);
2850                row.values.truncate(key_start);
2851                kept.push(row);
2852            }
2853        }
2854        let mut columns = columns;
2855        columns.truncate(key_start);
2856        // PG limits what DISTINCT ON left, not what fed it.
2857        let kept = apply_deferred_limit(kept, don_limit);
2858        Ok(QueryResult::Rows {
2859            columns,
2860            rows: kept,
2861        })
2862    }
2863
2864    pub(crate) fn exec_select_cancel_as(
2865        &self,
2866        stmt: &SelectStatement,
2867        cancel: CancelToken<'_>,
2868        as_role: Option<&str>,
2869    ) -> Result<QueryResult, EngineError> {
2870        // v7.39 (round 763, F31-C1) — `SELECT *, count(*) … GROUP BY
2871        // <all columns>` is legal PG (the wildcard expands to grouped
2872        // columns); SPG refused the whole shape. Expand the wildcard
2873        // into explicit column refs up front — the aggregate layer's
2874        // existing "must appear in the GROUP BY clause" validation
2875        // then answers PG's sentence for any non-grouped column.
2876        if let Some(expanded) = self.expand_aggregate_wildcard(stmt) {
2877            return self.exec_select_cancel_as(&expanded, cancel, as_role);
2878        }
2879        // v7.39 (round 559) — `SELECT count(*) FROM t` without touching
2880        // a row.
2881        //
2882        // The aggregate layer already short-circuits this to
2883        // `rows.len()`, so the O(1) part was never the problem — the
2884        // cost is UPSTREAM, materialising every visible row so that
2885        // layer can take its length. Measured over pgwire on 500k rows:
2886        // PG18 8.2 ms with two parallel workers, 10.3 ms with
2887        // parallelism off, SPG 16.5 ms — 1.6x slower than a
2888        // single-threaded PG on the commonest aggregate there is, and no
2889        // ledger entry recorded it.
2890        //
2891        // Counting visible HEADERS needs no row at all. PG cannot do
2892        // this: its visibility lives in the heap tuples themselves, so
2893        // it has to read them (that is why its own count(*) is a full
2894        // scan, parallel or not).
2895        // v7.39 (read01 round 57) — the table-privilege gate on the common
2896        // read core. A superuser session returns from it immediately.
2897        // v7.39 (round 529) — resolve an ORDER BY that names an output
2898        // ALIAS. The statement-level pass never reached a SELECT nested in
2899        // a FROM clause, a CTE or a scalar subquery, so the same query
2900        // worked on its own and failed the moment anything wrapped it —
2901        // which is what generated SQL does constantly.
2902        let aliased;
2903        let stmt = if crate::orderby::order_by_names_an_alias(stmt) {
2904            let mut s = stmt.clone();
2905            crate::orderby::resolve_order_by_position(&mut s);
2906            aliased = s;
2907            &aliased
2908        } else {
2909            stmt
2910        };
2911        // v7.39 (round 529) — DISTINCT ON needs two things it did not have.
2912        //
2913        // Its keys were evaluated against the PROJECTED row, so a key that
2914        // is not in the select list — `SELECT DISTINCT ON (g) v FROM t
2915        // ORDER BY g, v DESC`, the canonical "latest row per group" — could
2916        // not be read at all and the query failed. PG evaluates them on the
2917        // input. They are projected as hidden columns here and stripped
2918        // again below, the same way the grouping-set ordering columns
2919        // already travel.
2920        //
2921        // And the dedup ran AFTER the inner statement's LIMIT, so
2922        // `… DISTINCT ON (g) … LIMIT 2` on four rows answered ONE row where
2923        // PG answers two: the limit had already taken two rows of the same
2924        // group before anything deduplicated them. A paginated DISTINCT ON
2925        // returned short pages, with no error. The limit is deferred to
2926        // after the dedup, which is PG's order.
2927        let don_stmt;
2928        // v7.39 (round 729) — the top-1 consumer needs the ORIGINAL
2929        // order spec (the rewritten stmt's is emptied).
2930        let orig_order_by = stmt.order_by.clone();
2931        let (stmt, don_hidden, don_limit, don_top1) = if stmt.distinct_on.is_empty() {
2932            (stmt, 0, (None, None), 0usize)
2933        } else {
2934            let mut s = stmt.clone();
2935            let hidden = s.distinct_on.len();
2936            for (i, e) in stmt.distinct_on.iter().enumerate() {
2937                s.items.push(SelectItem::Expr {
2938                    expr: e.clone(),
2939                    alias: Some(alloc::format!("__distinct_on_{i}")),
2940                });
2941            }
2942            // v7.39 (round 729) — group-top-1 short circuit. When the
2943            // DISTINCT ON keys are exactly the ORDER BY's leading keys,
2944            // the answer is "per group, the row that wins the remaining
2945            // order" — a single O(n) hash pass. The old path sorted the
2946            // ENTIRE input first (500k rows, ~180 ms on the panel cell)
2947            // to keep 100. The inner query runs UNSORTED with every
2948            // order key appended as a hidden column; the dedup below
2949            // keeps each group's best, then sorts the SURVIVORS.
2950            // Declared-collation order keys stay on the sorting path
2951            // (the value comparator here is collation-blind).
2952            let prefix_matches = s.order_by.len() >= hidden
2953                && stmt
2954                    .distinct_on
2955                    .iter()
2956                    .zip(s.order_by.iter())
2957                    .all(|(d, o)| *d == o.expr && !o.desc && o.nulls_first.is_none());
2958            let colls_plain =
2959                crate::orderby::order_by_collations(&s.order_by, &self.ev_ctx(&[], None))
2960                    .map(|cs| cs.iter().all(Option::is_none))
2961                    .unwrap_or(false);
2962            let top1_tail = if prefix_matches && colls_plain && s.group_by.is_none() {
2963                let tail = s.order_by.len() - hidden;
2964                for (j, o) in s.order_by[hidden..].iter().enumerate() {
2965                    s.items.push(SelectItem::Expr {
2966                        expr: o.expr.clone(),
2967                        alias: Some(alloc::format!("__don_ord_{j}")),
2968                    });
2969                }
2970                // Carry the tail's direction flags through the aliases'
2971                // ORDER; the survivors re-sort below with the full spec.
2972                s.order_by = Vec::new();
2973                tail + 1 // sentinel: 1 + number of tail keys (0 tail is still active)
2974            } else {
2975                0
2976            };
2977            // Only a folded literal is deferred; a placeholder or an
2978            // expression keeps the path it has today rather than being
2979            // resolved a second way here.
2980            let deferrable = matches!(
2981                (&s.limit, &s.offset),
2982                (
2983                    None | Some(spg_sql::ast::LimitExpr::Literal(_)),
2984                    None | Some(spg_sql::ast::LimitExpr::Literal(_))
2985                )
2986            );
2987            let deferred = if deferrable {
2988                (s.limit.take(), s.offset.take())
2989            } else {
2990                (None, None)
2991            };
2992            don_stmt = s;
2993            (&don_stmt, hidden, deferred, top1_tail)
2994        };
2995        self.acl_check_select_as(stmt, as_role)?;
2996        validate_aggregate_placement(stmt)?;
2997        // v7.39 (round 559) — the bare `count(*)` fast path, AFTER the
2998        // privilege gate above. Placed before it at first, and the
2999        // security-definer e2e caught it immediately: a SECURITY INVOKER
3000        // function whose body is `SELECT count(*) FROM t` answered
3001        // instead of being refused, because the fast path never reached
3002        // the check.
3003        if let Some(r) = self.try_bare_count_star(stmt, as_role)? {
3004            return Ok(r);
3005        }
3006        // v7.39 (round 560) — an index-only range scan. Same placement
3007        // reasoning as the count above: after the privilege gate.
3008        if let Some(r) = self.try_index_only_scan(stmt)? {
3009            return Ok(r);
3010        }
3011        validate_locking_clause(stmt)?;
3012        let result = self.exec_select_cancel_inner(stmt, cancel)?;
3013        // v7.39 (round 135) — drop the synthetic `__grp_ord_*` ordering columns
3014        // the parser injects for GROUPING() in ORDER BY on a grouping-set query.
3015        // They carry the per-branch mask through the UNION-ALL sort and must not
3016        // appear in the output. Stripped per SELECT level (grouping-set queries
3017        // are often wrapped in a derived subquery), before DISTINCT ON.
3018        let result = strip_synthetic_order_cols(result);
3019        // v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3020        // rows arrive here already ORDER BY'd; keep the FIRST row of
3021        // each group the expressions define (PG semantics). The
3022        // expressions evaluate against the projected schema — an
3023        // expression that isn't in the select list errors honestly.
3024        if stmt.distinct_on.is_empty() {
3025            return Ok(result);
3026        }
3027        self.apply_distinct_on(result, don_hidden, &don_limit, don_top1, &orig_order_by)
3028    }
3029
3030    /// The UNION chain: execute the head as a bare block, then fold each
3031    /// peer in with left-associative dedup.
3032    ///
3033    /// `#[inline(never)]` and out of `exec_select_cancel_inner` for the
3034    /// reason round 848 established. A statement with no unions returns
3035    /// one line above the call — and every nested subquery on a deep
3036    /// path is such a statement, so each level of the recursion carried
3037    /// 170 lines of locals it could not reach. Round 867 measured that
3038    /// frame at 34,800 bytes, the largest single one on the descent,
3039    /// after two earlier attributions had blamed its caller and then its
3040    /// callee: the gap between two marks is the frame of everything
3041    /// BETWEEN them, and this function had no mark of its own.
3042    #[inline(never)]
3043    fn exec_union_chain(
3044        &self,
3045        stmt_ref: &SelectStatement,
3046        stmt: &SelectStatement,
3047        cancel: CancelToken<'_>,
3048    ) -> Result<QueryResult, EngineError> {
3049        // UNION path: clone-strip the head into a bare block (its own
3050        // DISTINCT and any inner ORDER BY are dropped by parser rule —
3051        // the wrapper SelectStatement carries them), execute, then chain
3052        // peers with left-associative dedup semantics.
3053        // v7.39 (round 232) — the wrapper's ORDER BY addresses the head's
3054        // output columns; a position past their count is PG's 42P10.
3055        crate::orderby::check_order_by_positions(stmt_ref)?;
3056        let mut head_unknown = branch_unknown_mask(stmt_ref);
3057        let mut head = stmt_ref.clone();
3058        head.unions = Vec::new();
3059        head.order_by = Vec::new();
3060        head.limit = None;
3061        let QueryResult::Rows {
3062            mut columns,
3063            mut rows,
3064        } = self.exec_bare_select_cancel(&head, cancel)?
3065        else {
3066            unreachable!("bare SELECT cannot return CommandOk")
3067        };
3068        for (kind, peer) in &stmt_ref.unions {
3069            // v7.37.17 (17.6 siblings) — a peer carrying its own
3070            // unions is a nested INTERSECT group (the parser's
3071            // precedence regrouping); recurse through the
3072            // union-aware wrapper for it.
3073            let peer_result = if peer.unions.is_empty() {
3074                self.exec_bare_select_cancel(peer, cancel)?
3075            } else {
3076                self.exec_select_cancel(peer, cancel)?
3077            };
3078            let QueryResult::Rows {
3079                columns: peer_cols,
3080                rows: mut peer_rows,
3081            } = peer_result
3082            else {
3083                unreachable!("bare SELECT cannot return CommandOk")
3084            };
3085            if peer_cols.len() != columns.len() {
3086                // v7.39 (round 232) — PG's wording, which clients match on.
3087                return Err(EngineError::Unsupported(alloc::format!(
3088                    "each {} query must have the same number of columns",
3089                    set_op_name(*kind)
3090                )));
3091            }
3092            // v7.39 (round 232+233) — PG resolves each result column to one
3093            // type before it merges anything, and refuses the query when the
3094            // two branches have no common type. SPG's unifier
3095            // (`unify_union_columns`) is value-driven and deliberately
3096            // conservative — "a column where any cell fails to coerce is left
3097            // exactly as it was" — so a mismatch produced a column holding
3098            // BOTH types (`SELECT a, b FROM t UNION SELECT b, a FROM t` came
3099            // back with integers and text interleaved) instead of an error.
3100            //
3101            // The check has to read the branch ASTs, not just their schemas:
3102            // SPG has no `Unknown` DataType, so a bare `'a'` literal describes
3103            // as TEXT and is indistinguishable from a real text column by
3104            // schema alone — yet PG treats the two completely differently
3105            // (`SELECT 1 UNION SELECT 'a'` is an input-syntax error on the
3106            // literal, `SELECT 1 UNION SELECT 'a'::text` is a type mismatch).
3107            let peer_unknown = branch_unknown_mask(peer);
3108            for i in 0..columns.len() {
3109                let hu = head_unknown.get(i).copied().unwrap_or(false);
3110                let pu = peer_unknown.get(i).copied().unwrap_or(false);
3111                let (ht, pt) = (columns[i].ty, peer_cols[i].ty);
3112                match (hu, pu) {
3113                    // Both sides carry a real type: they must share a category.
3114                    (false, false) => {
3115                        if !crate::conversions::types_unify(ht, pt) {
3116                            return Err(EngineError::Unsupported(alloc::format!(
3117                                "{} types {} and {} cannot be matched",
3118                                set_op_name(*kind),
3119                                crate::conversions::pg_type_name_for_error(ht),
3120                                crate::conversions::pg_type_name_for_error(pt),
3121                            )));
3122                        }
3123                    }
3124                    // One side is an untyped literal: it takes the other's
3125                    // type, and failing to convert is the error PG reports.
3126                    (true, false) => {
3127                        coerce_branch_column(&mut rows, i, pt, &columns[i].name)?;
3128                        columns[i].ty = pt;
3129                        head_unknown[i] = false;
3130                    }
3131                    (false, true) => {
3132                        coerce_branch_column(&mut peer_rows, i, ht, &columns[i].name)?;
3133                    }
3134                    // Both untyped — nothing to resolve against yet.
3135                    (true, true) => {}
3136                }
3137            }
3138            // v7.37 D.26 — a UNION result column is nullable when ANY branch is
3139            // nullable (PG semantics). Previously the result kept only the head's
3140            // nullability, so `VALUES (1),(NULL)` (a UNION-ALL chain seeded by the
3141            // non-null `1`) wrongly reported the column NOT NULL, which let
3142            // `count(col)`'s NOT-NULL fast-path count the NULL row.
3143            for (i, pc) in peer_cols.iter().enumerate() {
3144                if pc.nullable {
3145                    columns[i].nullable = true;
3146                }
3147            }
3148            // v7.39 (round 410) — under MySQL, set-op dedup / matching folds
3149            // text by the session collation (CI + accent + PAD SPACE), like
3150            // GROUP BY. PG stays byte-exact.
3151            let mysql = self.backslash_escapes;
3152            match kind {
3153                UnionKind::All => rows.extend(peer_rows),
3154                UnionKind::Distinct => {
3155                    rows.extend(peer_rows);
3156                    rows = dedup_rows(rows, mysql);
3157                }
3158                // v7.37.17 (17.6 siblings) — PG set semantics.
3159                // v7.39 (round 591) — all four ask the same question of the
3160                // right side, and all four used to answer it by scanning it
3161                // once per left row. `PeerIndex` buckets it by the hash
3162                // DISTINCT already uses, so the answer is a lookup.
3163                // INTERSECT: distinct rows present on both sides.
3164                UnionKind::Intersect => {
3165                    let idx = PeerIndex::build(&peer_rows, mysql);
3166                    rows = dedup_rows(rows, mysql)
3167                        .into_iter()
3168                        .filter(|r| idx.contains(r))
3169                        .collect();
3170                }
3171                // INTERSECT ALL: multiset intersection — each row
3172                // keeps min(left count, right count) occurrences.
3173                UnionKind::IntersectAll => {
3174                    let mut idx = PeerIndex::build(&peer_rows, mysql);
3175                    let mut kept: Vec<Row<'static>> = Vec::new();
3176                    for r in rows {
3177                        if idx.take_one(&r) {
3178                            kept.push(r);
3179                        }
3180                    }
3181                    rows = kept;
3182                }
3183                // EXCEPT: distinct left rows absent from the right.
3184                UnionKind::Except => {
3185                    let idx = PeerIndex::build(&peer_rows, mysql);
3186                    rows = dedup_rows(rows, mysql)
3187                        .into_iter()
3188                        .filter(|r| !idx.contains(r))
3189                        .collect();
3190                }
3191                // EXCEPT ALL: multiset subtraction — each right
3192                // occurrence cancels one left occurrence.
3193                UnionKind::ExceptAll => {
3194                    let mut idx = PeerIndex::build(&peer_rows, mysql);
3195                    let mut kept: Vec<Row<'static>> = Vec::new();
3196                    for r in rows {
3197                        if !idx.take_one(&r) {
3198                            kept.push(r);
3199                        }
3200                    }
3201                    rows = kept;
3202                }
3203            }
3204        }
3205        // PG resolves a UNION / VALUES result column to one common type
3206        // and casts every branch to it (`SELECT '2020-01-01'::date UNION
3207        // ALL SELECT '2020-01-02'` → both DATE, not DATE + TEXT). SPG
3208        // built each branch independently, leaving mixed-type columns
3209        // that broke ORDER BY, comparisons, and value-based window
3210        // frames. Unify + coerce before the combined ORDER BY sees them.
3211        unify_union_columns(&mut columns, &mut rows);
3212        // ORDER BY at the top of a UNION applies to the combined result.
3213        // Eval against the projected schema (NOT the source table).
3214        if !stmt.order_by.is_empty() {
3215            // v7.39 (read01 round 54) — the combined-result ctx must carry the
3216            // catalog, and the projected columns must keep their enum identity
3217            // (`user_enum_type`), or `ORDER BY <enum col>` over a UNION sorts
3218            // by TEXT instead of member order — silently wrong rows, not an
3219            // error. (Same shape as the enum-order knife's GROUP BY fix.)
3220            let synth_ctx = EvalContext::new(&columns, None).with_catalog(self.active_catalog());
3221            // v7.37.17 (17.6 siblings) — positional keys (ORDER BY 1)
3222            // survive to here when the head projects a Wildcard (the
3223            // group-tail wrapper shape): map them onto the Nth
3224            // projected column so the combined sort works.
3225            let resolved_order: Vec<spg_sql::ast::OrderBy> = stmt
3226                .order_by
3227                .iter()
3228                .map(|o| {
3229                    let mut o = o.clone();
3230                    if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
3231                        && *n >= 1
3232                        && let Ok(idx) = usize::try_from(*n - 1)
3233                        && idx < columns.len()
3234                    {
3235                        o.expr = Expr::Column(spg_sql::ast::ColumnName {
3236                            qualifier: None,
3237                            name: columns[idx].name.clone(),
3238                        });
3239                    }
3240                    o
3241                })
3242                .collect();
3243            let descs: Vec<bool> = resolved_order.iter().map(|o| o.desc).collect();
3244            let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
3245            for r in rows {
3246                let keys = build_order_keys(&resolved_order, &r, &synth_ctx)?;
3247                tagged.push((keys, r));
3248            }
3249            sort_by_keys(&mut tagged, &descs);
3250            rows = tagged.into_iter().map(|(_, r)| r).collect();
3251        }
3252        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
3253        Ok(QueryResult::Rows { columns, rows })
3254    }
3255
3256    fn exec_select_cancel_inner(
3257        &self,
3258        stmt: &SelectStatement,
3259        cancel: CancelToken<'_>,
3260    ) -> Result<QueryResult, EngineError> {
3261        cancel.check()?;
3262        // v7.38 P0 元机制 A — first observable point inside the
3263        // planner / executor. Tests use this to inject a delay or
3264        // a cancellation race before any row is produced. Release
3265        // build expands to `let _ = (...);` — zero cost.
3266        crate::injection_point!("planner_first_row_fetch", &stmt.from);
3267        // v7.39 (round 705) — WINDOW-clause definitions nothing referenced.
3268        // PG analyses every definition, referenced or not, so `SELECT i FROM
3269        // t WINDOW w AS (ORDER BY nosuch)` fails there and silently
3270        // succeeded here (the parser used to drop the unreferenced defs
3271        // whole). The check is the CREATE VIEW check's shape (round 700): a
3272        // LIMIT-0 run of the same FROM with the definitions' key
3273        // expressions as the projection — it cannot disagree with what a
3274        // referencing window would have done, because it resolves the same
3275        // names the same way. Zero cost for the ordinary statement: the
3276        // list is empty unless a WINDOW clause left unreferenced defs.
3277        if !stmt.window_check_exprs.is_empty() {
3278            let mut probe = stmt.clone();
3279            probe.items = stmt
3280                .window_check_exprs
3281                .iter()
3282                .map(|e| spg_sql::ast::SelectItem::Expr {
3283                    expr: e.clone(),
3284                    alias: None,
3285                })
3286                .collect();
3287            probe.window_check_exprs = Vec::new();
3288            probe.distinct = false;
3289            probe.distinct_on = Vec::new();
3290            probe.group_by = None;
3291            probe.group_by_all = false;
3292            probe.having = None;
3293            probe.unions = Vec::new();
3294            probe.order_by = Vec::new();
3295            probe.locking = None;
3296            probe.limit = Some(spg_sql::ast::LimitExpr::Literal(0));
3297            probe.offset = None;
3298            probe.limit_with_ties = false;
3299            self.exec_select_cancel_inner(&probe, cancel)?;
3300        }
3301        // v7.39 (read01 round 74) — lower `(f(args)).*`. Naming a record's fields
3302        // takes the catalog, so the parser leaves a marker and the rewrite lands
3303        // here: the call moves into a LATERAL FROM item and the item becomes one
3304        // reference per declared column. `SELECT 'p', (rows_of(2)).*` is
3305        // `SELECT 'p', __rec.id, __rec.v FROM rows_of(2) AS __rec` — reusing the
3306        // set-returning FROM machinery of rounds 65 and 69 rather than growing a
3307        // second one.
3308        if let Some(lowered) = self.lower_record_expansion(stmt)? {
3309            return self.exec_select_cancel_inner(&lowered, cancel);
3310        }
3311        // v7.17.0 Phase 1.2 — user-defined VIEW expansion. If the
3312        // FROM / JOIN graph references any catalogued view name,
3313        // re-parse the view body and prepend it as a synthetic
3314        // CTE. Recurses on views-in-views via the regular CTE
3315        // dispatch below. Fast-path: skip the walker entirely when
3316        // the catalog has no views (the typical OLTP load).
3317        if !self.active_catalog().views_all().is_empty() {
3318            if let Some(rewritten) = self.expand_views_in_select(stmt)? {
3319                return self.exec_select_cancel(&rewritten, cancel);
3320            }
3321        }
3322        // v7.37.6-B(sentori Epic 2 P0)— `SELECT … FROM <partition-parent>`
3323        // gets rewritten to a UNION-ALL over the children that overlap
3324        // the WHERE-derived key range. Uses the same CTE-injection
3325        // trick as VIEW expansion above so downstream resolution
3326        // doesn't need a partition-aware code path.
3327        if let Some(rewritten) = self.expand_partition_parents_in_select(stmt)? {
3328            return self.exec_select_cancel(&rewritten, cancel);
3329        }
3330        // v7.16.2 — information_schema / pg_catalog virtual
3331        // views (mailrs round-10 A.3). If the SELECT touches a
3332        // synthetic meta-table name (`__spg_info_*` /
3333        // `__spg_pg_*` — produced by the parser for
3334        // `information_schema.X` / `pg_catalog.X`), clone the
3335        // catalog, materialise the requested view as a real
3336        // temporary table, and re-execute against an enriched
3337        // engine. Same pattern as `exec_with_ctes` for CTEs.
3338        if !self.meta_views_materialised && select_references_meta_view(stmt) {
3339            return self.exec_select_with_meta_views(stmt, cancel);
3340        }
3341        // v6.10.2 — cold-tier time-travel short-circuit. When the
3342        // primary TableRef carries `AS OF SEGMENT '<id>'`, run a
3343        // dedicated cold-segment scan instead of the regular
3344        // hot+index path. The scope is intentionally narrow for
3345        // v6.10.2 — bare `SELECT * FROM <t> AS OF SEGMENT 'id'`,
3346        // optionally with a single-column-equality WHERE. JOINs /
3347        // aggregates / ORDER BY / subqueries on top of a time-
3348        // travelled scan are STABILITY § "Out of v6.10".
3349        if let Some(from) = &stmt.from
3350            && let Some(seg_id) = from.primary.as_of_segment
3351        {
3352            return self.exec_select_as_of_segment(stmt, from, seg_id);
3353        }
3354        // v6.2.0 / v6.5.0 — virtual-table short-circuits. Detected
3355        // pre-CTE because they don't read from the catalog and
3356        // shouldn't participate in regular FROM resolution.
3357        // v6.2.0 / v6.5.0 / v7.38 (read01 P3.NEW3) — virtual-table
3358        // short-circuits. A meta-view FROM materialises to a fixed row
3359        // set. For a bare `SELECT *` we return it directly; otherwise we
3360        // stage it as a temp table and run the normal pipeline, so
3361        // projection / WHERE / ORDER BY / aggregates work over these views
3362        // (they were `SELECT *`-only before). A real table shadowing the
3363        // name wins (checked first), which also stops the staged re-run
3364        // from recursing back into meta-view detection.
3365        if let Some(from) = &stmt.from
3366            && from.joins.is_empty()
3367            && self.active_catalog().get(&from.primary.name).is_none()
3368        {
3369            let lower = from.primary.name.to_ascii_lowercase();
3370            if let Some(result) = self.meta_view_result(&lower) {
3371                let bare = stmt.where_.is_none()
3372                    && stmt.group_by.is_none()
3373                    && stmt.having.is_none()
3374                    && stmt.unions.is_empty()
3375                    && stmt.order_by.is_empty()
3376                    && stmt.limit.is_none()
3377                    && stmt.offset.is_none()
3378                    && !stmt.distinct
3379                    && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard));
3380                if bare {
3381                    return Ok(result);
3382                }
3383                if let QueryResult::Rows { columns, rows } = result {
3384                    let mut catalog = self.active_catalog().clone();
3385                    let cols = infer_column_types(&columns, &rows);
3386                    let schema = TableSchema::new(from.primary.name.clone(), cols);
3387                    catalog.create_table(schema).map_err(EngineError::Storage)?;
3388                    let t = catalog
3389                        .get_mut(&from.primary.name)
3390                        .expect("just-created meta-view table must exist");
3391                    for row in rows {
3392                        t.insert(row).map_err(EngineError::Storage)?;
3393                    }
3394                    let mut eng = Engine::restore(catalog);
3395                    if let Some(c) = self.clock {
3396                        eng = eng.with_clock(c);
3397                    }
3398                    if let Some(f) = self.salt_fn {
3399                        eng = eng.with_salt_fn(f);
3400                    }
3401                    // v7.39 (read01 pgstatfuncs.c) — carry the calling-
3402                    // connection identity so `WHERE pid = pg_backend_pid()`
3403                    // matches inside the staged meta-view run.
3404                    if let Some(f) = self.backend_pid_fn {
3405                        eng.set_backend_pid_fn(f);
3406                    }
3407                    return eng.exec_select_cancel(stmt, cancel);
3408                }
3409                return Ok(result);
3410            }
3411        }
3412        // v4.11: CTEs materialise into a temporary enriched catalog
3413        // *before* anything else — the body SELECT can then refer
3414        // to CTE names via the regular FROM-clause resolution.
3415        // Uncorrelated only: each CTE body runs once against the
3416        // current catalog, not against later CTEs' results (left-
3417        // to-right materialisation would relax this, but we keep
3418        // it simple for v4.11 MVP).
3419        if !stmt.ctes.is_empty() {
3420            return self.exec_with_ctes(stmt, cancel);
3421        }
3422        // v4.10: subqueries (uncorrelated) are resolved here, before
3423        // the executor sees the row loop. We clone the statement so
3424        // we can mutate without disturbing the caller's AST — most
3425        // queries pass through with no subquery nodes and the clone
3426        // is cheap; with subqueries the materialisation cost
3427        // dominates anyway.
3428        let mut stmt_owned;
3429        let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3430            stmt_owned = stmt.clone();
3431            // v7.33 (mailrs 7.32.1) — sublink pull-up first: an
3432            // aggregate-wrapped correlated scalar subquery whose
3433            // correlation key is UNIQUE/PK becomes a LEFT JOIN, so the
3434            // executor streams one join instead of splicing a per-row
3435            // subplan. Runs before the per-row/batch resolver, which then
3436            // only sees the subqueries the pull-up left behind.
3437            self.pull_up_unique_correlated_agg_subqueries(&mut stmt_owned);
3438            // v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC pull-up) —
3439            // the "per-key latest" scalar subquery shape (inbox / feed
3440            // / timeline applications) becomes a CTE + LEFT JOIN
3441            // against a GROUP BY pre-aggregation that reuses the v7.33
3442            // first_ordered argmax executor. Runs AFTER unique-key
3443            // pull-up (so the unique-key fast path still wins for
3444            // single-PK lookups) and BEFORE the EXISTS sublink rewrite.
3445            // Phase 1 (this commit) is skeleton only — no-op pass.
3446            self.pull_up_correlated_limit_one_subqueries(&mut stmt_owned);
3447            // v7.34.2 (mailrs prod NOT EXISTS) — plan-time `[NOT] EXISTS`
3448            // sublink pull-up to semi/anti-join, before the resolver gets
3449            // a chance to walk per-row.
3450            self.pull_up_exists_sublinks(&mut stmt_owned);
3451            // v7.37.4 — if the LIMIT 1 pullup added CTEs, route through
3452            // exec_with_ctes so they materialise once before the body
3453            // SELECT runs. exec_with_ctes strips ctes from the body
3454            // clone, then re-enters select.
3455            if !stmt_owned.ctes.is_empty() {
3456                return self.exec_with_ctes(&stmt_owned, cancel);
3457            }
3458            // v7.37.x (docker-fair INSUBQ attack) — short-circuit
3459            //   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
3460            // BEFORE `resolve_select_subqueries` materialises the inner
3461            // result as `Vec<Expr::Literal>` (~150 µs for the 6 k-row
3462            // INSUBQ benchmark). Run the inner once, collect the result
3463            // values into a `HashSet<i64>` directly, then probe A.pk per
3464            // value and tally. Returns `Some` when the shape matches.
3465            if let Some(out) = self.try_count_star_pk_in_subquery_fast(&stmt_owned, cancel)? {
3466                return Ok(out);
3467            }
3468            self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3469            &stmt_owned
3470        } else {
3471            stmt
3472        };
3473        if stmt_ref.unions.is_empty() {
3474            return self.exec_bare_select_cancel(stmt_ref, cancel);
3475        }
3476        self.exec_union_chain(stmt_ref, stmt, cancel)
3477    }
3478
3479    #[allow(clippy::too_many_lines)]
3480    #[allow(clippy::too_many_lines)] // huge match — splitting fragments the planner
3481    /// v7.11.7 — execute `SELECT … FROM unnest(expr) [AS] alias …`.
3482    /// Synthesises a single-column virtual table whose column type
3483    /// is TEXT and whose rows are the array elements. Routes
3484    /// through the regular projection / WHERE / ORDER BY / LIMIT
3485    /// machinery so set-returning UNNEST composes naturally with
3486    /// the rest of the SELECT surface.
3487    fn exec_select_unnest(
3488        &self,
3489        stmt: &SelectStatement,
3490        primary: &TableRef,
3491        cancel: CancelToken<'_>,
3492    ) -> Result<QueryResult, EngineError> {
3493        let expr = primary
3494            .unnest_expr
3495            .as_deref()
3496            .expect("caller guards unnest_expr.is_some()");
3497        // Multi-arg unnest(a, b, …) — parallel zip, NULL-padded.
3498        // N value columns instead of one; the shared builder does
3499        // the work and the tail below (WHERE / agg / projection)
3500        // runs against the wider schema.
3501        let multi: Option<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>)> =
3502            match unnest_zip_args(expr) {
3503                Some(args) => Some(unnest_zip_rows(args)?),
3504                None => None,
3505            };
3506        // Evaluate the array expression once. Empty schema / empty
3507        // row — uncorrelated UNNEST cannot reference outer columns.
3508        // v7.39 (read01 round 49) — the ctx must carry the catalog: the enum
3509        // introspection family (enum_range / enum_first / enum_last) resolves
3510        // its labels from the argument's STATIC enum type against the
3511        // catalog's enum registry. Without it `unnest(enum_range(NULL::mood))`
3512        // fell through to the generic arm, got NULL, and expanded to zero rows
3513        // — while the bare `SELECT enum_range(NULL::mood)` (whose ctx does
3514        // carry the catalog) worked.
3515        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
3516        let ctx = EvalContext::new(&empty_schema, None).with_catalog(self.active_catalog());
3517        let dummy_row = Row::new(alloc::vec::Vec::new());
3518        // v7.11.13 — unnest dispatches per array element type so
3519        // INT[] / BIGINT[] surface their PG types in projection.
3520        // v7.39 (round 758, F31-B8a) — the composite SRF names its own
3521        // columns (PG: lexeme | positions | weights); everything else
3522        // keeps the alias / "unnest" defaults below.
3523        let mut composite_names: Option<&[&str]> = None;
3524        let (dtypes, rows): (alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>) =
3525            if let Some(m) = multi {
3526                m
3527            } else {
3528                // v7.39 (round 236) — flatten a multidimensional array into
3529                // its row-major elements (PG) before the 1-D-only match.
3530                let unnest_src = {
3531                    let v = eval::eval_expr(expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
3532                    crate::eval::values::flatten_2d(&v).unwrap_or(v)
3533                };
3534                let mut return_multi: Option<(
3535                    alloc::vec::Vec<DataType>,
3536                    alloc::vec::Vec<Row<'static>>,
3537                )> = None;
3538                let (elem_dtype, rows): (DataType, alloc::vec::Vec<Row<'static>>) = match unnest_src
3539                {
3540                    Value::Null => (DataType::Text, alloc::vec::Vec::new()),
3541                    Value::TextArray(items) => {
3542                        let rows = items
3543                            .into_iter()
3544                            .map(|item| {
3545                                Row::new(alloc::vec![match item {
3546                                    Some(s) => Value::text(s),
3547                                    None => Value::Null,
3548                                }])
3549                            })
3550                            .collect();
3551                        (DataType::Text, rows)
3552                    }
3553                    Value::IntArray(items) => {
3554                        let rows = items
3555                            .into_iter()
3556                            .map(|item| {
3557                                Row::new(alloc::vec![match item {
3558                                    Some(n) => Value::Int(n),
3559                                    None => Value::Null,
3560                                }])
3561                            })
3562                            .collect();
3563                        (DataType::Int, rows)
3564                    }
3565                    Value::BigIntArray(items) => {
3566                        let rows = items
3567                            .into_iter()
3568                            .map(|item| {
3569                                Row::new(alloc::vec![match item {
3570                                    Some(n) => Value::BigInt(n),
3571                                    None => Value::Null,
3572                                }])
3573                            })
3574                            .collect();
3575                        (DataType::BigInt, rows)
3576                    }
3577                    Value::Multirange { kind, ranges } => {
3578                        let rows = ranges
3579                            .iter()
3580                            .map(|sp| {
3581                                Row::new(alloc::vec![Value::Range {
3582                                    kind,
3583                                    lower: sp.lower.clone(),
3584                                    upper: sp.upper.clone(),
3585                                    lower_inc: sp.lower_inc,
3586                                    upper_inc: sp.upper_inc,
3587                                    empty: false,
3588                                }])
3589                            })
3590                            .collect();
3591                        (DataType::Range(kind), rows)
3592                    }
3593                    // v7.39 (round 758, F31-B8a) — unnest(tsvector):
3594                    // one row per lexeme, PG18-measured columns
3595                    // lexeme | positions | weights (`a | {1,3} |
3596                    // {D,D}`); a position-less lexeme (a stripped
3597                    // vector) reads NULL in both array columns.
3598                    Value::TsVector(lexemes) => {
3599                        composite_names = Some(&["lexeme", "positions", "weights"]);
3600                        let rows = lexemes
3601                            .iter()
3602                            .map(|l| {
3603                                let (pos, wts) = if l.positions.is_empty() {
3604                                    (Value::Null, Value::Null)
3605                                } else {
3606                                    let letter = match l.weight {
3607                                        3 => "A",
3608                                        2 => "B",
3609                                        1 => "C",
3610                                        _ => "D",
3611                                    };
3612                                    (
3613                                        Value::SmallIntArray(
3614                                            l.positions
3615                                                .iter()
3616                                                .map(|p| {
3617                                                    Some(i16::try_from(*p).unwrap_or(i16::MAX))
3618                                                })
3619                                                .collect(),
3620                                        ),
3621                                        Value::TextArray(
3622                                            l.positions
3623                                                .iter()
3624                                                .map(|_| Some(letter.into()))
3625                                                .collect(),
3626                                        ),
3627                                    )
3628                                };
3629                                Row::new(alloc::vec![Value::text(l.word.clone()), pos, wts])
3630                            })
3631                            .collect();
3632                        return_multi = Some((
3633                            alloc::vec![
3634                                DataType::Text,
3635                                DataType::SmallIntArray,
3636                                DataType::TextArray
3637                            ],
3638                            rows,
3639                        ));
3640                        (DataType::Text, alloc::vec::Vec::new())
3641                    }
3642                    other => {
3643                        // v7.39 (round 622, S05a) — see table_access.rs:
3644                        // the same sentence, and it is a type mismatch.
3645                        return Err(EngineError::Eval(EvalError::TypeMismatch {
3646                            detail: alloc::format!(
3647                                "unnest() expects an array argument, got {}",
3648                                crate::conversions::pg_type_name_for_error_opt(other.data_type())
3649                            ),
3650                        }));
3651                    }
3652                };
3653                if let Some(m) = return_multi {
3654                    m
3655                } else {
3656                    (alloc::vec![elem_dtype], rows)
3657                }
3658            };
3659        let alias = primary
3660            .alias
3661            .clone()
3662            .unwrap_or_else(|| "unnest".to_string());
3663        // v7.13.2 — mailrs round-6 S5. Honour PG-standard
3664        // `UNNEST(arr) AS p(col_name)` column-list aliasing:
3665        // entries map positionally over the value columns. Without
3666        // the column list, a single column falls back to the table
3667        // alias (pre-v7.13.2 behaviour); multi-arg columns default
3668        // to PG's `unnest`.
3669        let n_vals = dtypes.len();
3670        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = dtypes
3671            .iter()
3672            .enumerate()
3673            .map(|(i, dt)| {
3674                let name = primary
3675                    .unnest_column_aliases
3676                    .get(i)
3677                    .cloned()
3678                    .unwrap_or_else(|| {
3679                        if let Some(names) = composite_names {
3680                            names
3681                                .get(i)
3682                                .map_or_else(|| "unnest".to_string(), |n| (*n).to_string())
3683                        } else if n_vals == 1 {
3684                            alias.clone()
3685                        } else {
3686                            "unnest".to_string()
3687                        }
3688                    });
3689                ColumnSchema::new(name, *dt, true)
3690            })
3691            .collect();
3692        // v7.39 (read01 round 78) — the item's row type IS this scalar when the
3693        // parser desugared a base-type-returning function here (see
3694        // TableRef::scalar_fn_item); the marker rides the column so it survives
3695        // every EvalContext an inner stage rebuilds.
3696        if primary.scalar_fn_item && schema_cols.len() == 1 {
3697            schema_cols[0].scalar_row_source = true;
3698        }
3699        // WITH ORDINALITY — trailing BIGINT counting rows from 1
3700        // in element order. The alias entry after the value
3701        // columns renames it (PG default: `ordinality`).
3702        let rows = if primary.with_ordinality {
3703            let ord_name = primary
3704                .unnest_column_aliases
3705                .get(n_vals)
3706                .cloned()
3707                .unwrap_or_else(|| "ordinality".to_string());
3708            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3709            rows.into_iter()
3710                .enumerate()
3711                .map(|(i, row)| {
3712                    let mut vals = row.values.clone();
3713                    vals.push(Value::BigInt(i as i64 + 1));
3714                    Row::new(vals)
3715                })
3716                .collect()
3717        } else {
3718            rows
3719        };
3720        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3721        // `EvalContext::new` drops it and every catalog-dependent cast
3722        // (regclass / enum / composite / domain) silently degrades.
3723        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3724        // Apply WHERE.
3725        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3726            let mut out = alloc::vec::Vec::with_capacity(rows.len());
3727            for row in rows {
3728                cancel.check()?;
3729                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3730                if matches!(v, Value::Bool(true)) {
3731                    out.push(row);
3732                }
3733            }
3734            out
3735        } else {
3736            rows
3737        };
3738        // v7.17.0 Phase 3.P0-48 — aggregate dispatch over the
3739        // unnest source. Same routing the relational scan path
3740        // already takes — without it `SELECT COUNT(*) FROM
3741        // unnest(ARRAY[…])` either errored at projection time or
3742        // returned the wrong shape.
3743        if aggregate::uses_aggregate(stmt) {
3744            // v7.29 — a per-query memo so correlated scalar
3745            // subqueries batch-evaluate once (group map) instead of
3746            // executing per group.
3747            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3748            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3749                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3750                    .map_err(|err| match err {
3751                        EngineError::Eval(ev) => ev,
3752                        other => eval::EvalError::TypeMismatch {
3753                            detail: alloc::format!("{other}"),
3754                        },
3755                    })
3756            };
3757            // v7.39 (round 656) — hand the rows over as they are rather than
3758            // collecting a second vector of `RowRef` wrappers. Note this is
3759            // a set-returning-function path, NOT the relational scan: the
3760            // measured O(rows) cost lived in `run_single_table_aggregate`,
3761            // and converting these four first was a miss that cost a full
3762            // round — every test stayed green and the number did not move.
3763            let agg = aggregate::run(
3764                stmt,
3765                crate::join::AggRows::Owned(&filtered),
3766                &schema_cols,
3767                Some(&alias),
3768                Some(&agg_correlated),
3769                self.parallel_runner.0.as_deref(),
3770                Some(self.active_catalog()),
3771                Some(self),
3772            )?;
3773            return self.finish_agg_result(agg, stmt, cancel);
3774        }
3775        // Projection.
3776        let projection =
3777            build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
3778        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
3779            alloc::vec::Vec::with_capacity(filtered.len());
3780        // v7.19 P5 — Set-Returning-Function in projection
3781        // position (PG `SELECT unnest(arr) FROM t` shape). When a
3782        // SELECT item evaluates to a top-level unnest(arr) call,
3783        // expand it: for each input row, evaluate the array, emit
3784        // one output row per element, broadcasting non-SRF
3785        // projections from the same input row. Multi-SRF + LCM
3786        // padding stays a documented carve-out; mailrs uses
3787        // single-SRF for redirect_uris.
3788        // v7.39 (read01 round 67) — EVERY set-returning item expands, in lockstep
3789        // (see `expand_srf_row`); a user `RETURNS SETOF` function counts too.
3790        let srf_idxs = self.srf_target_idxs(&projection);
3791        // v7.39 (round 621) — which input row each output row came from. An
3792        // SRF turns one input row into many, and the ORDER BY below used to
3793        // index the EXPANDED rows by the INPUT row's position: the result was
3794        // silently truncated to the input row count and left unsorted, so
3795        // `SELECT unnest(ARRAY[1,2]), y FROM unnest(ARRAY[5,6,7]) y ORDER BY 1`
3796        // answered three of its six rows, in no order. Without the ORDER BY
3797        // the same query was already right.
3798        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
3799        if !srf_idxs.is_empty() {
3800            let (rows, src) =
3801                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
3802            projected_rows = rows;
3803            src_of_row = src;
3804        } else {
3805            // v7.24 (round-16 B) — select-list subqueries resolve
3806            // per row (correlated-aware; plain exprs take the fast
3807            // path inside).
3808            let mut proj_memo = memoize::MemoizeCache::default();
3809            for row in &filtered {
3810                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
3811                for p in &projection {
3812                    vals.push(self.eval_expr_with_correlated(
3813                        &p.expr,
3814                        row,
3815                        &scan_ctx,
3816                        cancel,
3817                        Some(&mut proj_memo),
3818                    )?);
3819                }
3820                projected_rows.push(Row::new(vals));
3821            }
3822        }
3823        // ORDER BY / LIMIT — apply on the projected rows (cheap;
3824        // unnest result sets are small by design).
3825        let columns: alloc::vec::Vec<ColumnSchema> = projection
3826            .iter()
3827            // v7.39 (read01 round 54) — keep the column's enum identity through
3828            // the projection (it lives outside the DataType lattice), or a
3829            // derived table / UNION / windowed result forgets it and any outer
3830            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
3831            .map(|p| {
3832                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
3833                c.user_enum_type = p.user_enum_type.clone();
3834                c.mysql_fsp = p.mysql_fsp;
3835                c
3836            })
3837            .collect();
3838        // Re-evaluate ORDER BY against the source schema (pre-projection
3839        // so col refs by name still resolve through `scan_ctx`).
3840        // v7.39 (read01 round 80) — a positional key means the Nth OUTPUT
3841        // column. Evaluated as an expression it is just the constant N: the same
3842        // key for every row, so the sort ran and changed nothing.
3843        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
3844        if !order_by.is_empty() {
3845            // v7.39 (round 621) — one entry per OUTPUT row, not per input row.
3846            // A key that names a select-list item reads it out of the expanded
3847            // row (PG sorts AFTER the expansion); one that names a source
3848            // column the query does not project is evaluated on the input row
3849            // it came from, which is what `srf_order_output_cols` decides.
3850            let out_cols = if srf_idxs.is_empty() {
3851                alloc::vec![None; order_by.len()]
3852            } else {
3853                srf_order_output_cols(&order_by, &projection)
3854            };
3855            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
3856                .iter()
3857                .enumerate()
3858                .map(|(k, out)| -> Result<_, EngineError> {
3859                    let src = src_of_row.get(k).copied().unwrap_or(k);
3860                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
3861                        .iter()
3862                        .zip(out_cols.iter())
3863                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, &filtered[src], &scan_ctx))
3864                        .collect();
3865                    Ok((k, keys?))
3866                })
3867                .collect::<Result<_, _>>()?;
3868            indexed.sort_by(|a, b| {
3869                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
3870                    let o = &order_by[idx];
3871                    let cmp = order_by_value_cmp_in(
3872                        o.desc,
3873                        o.nulls_first,
3874                        ka,
3875                        kb,
3876                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
3877                    );
3878                    if cmp != core::cmp::Ordering::Equal {
3879                        return cmp;
3880                    }
3881                }
3882                core::cmp::Ordering::Equal
3883            });
3884            projected_rows = indexed
3885                .into_iter()
3886                .map(|(i, _)| projected_rows[i].clone())
3887                .collect();
3888        }
3889        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
3890        if stmt.distinct {
3891            projected_rows = dedup_rows(projected_rows, scan_ctx.mysql_dialect);
3892        }
3893        // LIMIT / OFFSET — apply at the tail.
3894        if let Some(offset) = stmt.offset_literal() {
3895            let off = (offset as usize).min(projected_rows.len());
3896            projected_rows.drain(..off);
3897        }
3898        if let Some(limit) = stmt.limit_literal() {
3899            projected_rows.truncate(limit as usize);
3900        }
3901        Ok(QueryResult::Rows {
3902            columns,
3903            rows: projected_rows,
3904        })
3905    }
3906
3907    /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop [,
3908    /// step])` set-returning source. Mirrors `exec_select_unnest`'s
3909    /// shape: evaluate the arg list once against an empty row,
3910    /// materialise the row stream by stepping start → stop, then
3911    /// route through the standard WHERE / projection / ORDER BY /
3912    /// LIMIT pipeline. Two arg-type combos in v7.17:
3913    ///   * integer / integer [/ integer] — SmallInt, Int, BigInt
3914    ///     (widened to BigInt internally; step defaults to 1)
3915    ///   * timestamp / timestamp / interval — date-range
3916    ///     iteration (mailrs's daily-report pattern)
3917    fn exec_select_generate_series(
3918        &self,
3919        stmt: &SelectStatement,
3920        primary: &TableRef,
3921        cancel: CancelToken<'_>,
3922    ) -> Result<QueryResult, EngineError> {
3923        let args = primary
3924            .generate_series_args
3925            .as_ref()
3926            .expect("caller guards generate_series_args.is_some()");
3927        let (elem_dtype, rows) = generate_series_rows(args, &cancel)?;
3928        let alias = primary
3929            .alias
3930            .clone()
3931            .unwrap_or_else(|| "generate_series".to_string());
3932        // `AS t(n)` — the first column-alias entry renames the
3933        // series column (PG semantics); bare alias keeps the
3934        // pre-existing behaviour of naming the column after it.
3935        let col_name = primary
3936            .unnest_column_aliases
3937            .first()
3938            .cloned()
3939            .unwrap_or_else(|| alias.clone());
3940        let col_schema = ColumnSchema::new(col_name, elem_dtype, true);
3941        let mut schema_cols = alloc::vec![col_schema.clone()];
3942        // WITH ORDINALITY — trailing BIGINT counting rows from 1;
3943        // the second column-alias entry renames it.
3944        let rows = if primary.with_ordinality {
3945            let ord_name = primary
3946                .unnest_column_aliases
3947                .get(1)
3948                .cloned()
3949                .unwrap_or_else(|| "ordinality".to_string());
3950            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3951            rows.into_iter()
3952                .enumerate()
3953                .map(|(i, row)| {
3954                    let mut vals = row.values.clone();
3955                    vals.push(Value::BigInt(i as i64 + 1));
3956                    Row::new(vals)
3957                })
3958                .collect()
3959        } else {
3960            rows
3961        };
3962        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3963        // `EvalContext::new` drops it and every catalog-dependent cast
3964        // (regclass / enum / composite / domain) silently degrades.
3965        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3966        // WHERE.
3967        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3968            let mut out = alloc::vec::Vec::with_capacity(rows.len());
3969            for row in rows {
3970                cancel.check()?;
3971                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3972                if matches!(v, Value::Bool(true)) {
3973                    out.push(row);
3974                }
3975            }
3976            out
3977        } else {
3978            rows
3979        };
3980        // v7.17.0 Phase 3.P0-48 — aggregate dispatch for set-
3981        // returning sources. When the SELECT projection contains
3982        // aggregate functions (COUNT/SUM/MIN/MAX/AVG/string_agg/
3983        // …) we route the filtered row stream through the same
3984        // aggregate executor the relational scan path uses, so
3985        // `SELECT COUNT(*) FROM generate_series(1, 100)` returns
3986        // a single 100 row instead of erroring at projection
3987        // time. GROUP BY / HAVING / ORDER BY over the aggregate
3988        // output all ride through `aggregate::run`.
3989        if aggregate::uses_aggregate(stmt) {
3990            // v7.29 — a per-query memo so correlated scalar
3991            // subqueries batch-evaluate once (group map) instead of
3992            // executing per group.
3993            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3994            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3995                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3996                    .map_err(|err| match err {
3997                        EngineError::Eval(ev) => ev,
3998                        other => eval::EvalError::TypeMismatch {
3999                            detail: alloc::format!("{other}"),
4000                        },
4001                    })
4002            };
4003            // v7.39 (round 656) — hand the rows over as they are rather than
4004            // collecting a second vector of `RowRef` wrappers. Note this is
4005            // a set-returning-function path, NOT the relational scan: the
4006            // measured O(rows) cost lived in `run_single_table_aggregate`,
4007            // and converting these four first was a miss that cost a full
4008            // round — every test stayed green and the number did not move.
4009            let agg = aggregate::run(
4010                stmt,
4011                crate::join::AggRows::Owned(&filtered),
4012                &schema_cols,
4013                Some(&alias),
4014                Some(&agg_correlated),
4015                self.parallel_runner.0.as_deref(),
4016                Some(self.active_catalog()),
4017                Some(self),
4018            )?;
4019            return self.finish_agg_result(agg, stmt, cancel);
4020        }
4021        // Projection.
4022        let projection =
4023            build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
4024        // v7.39 (round 621) — and here, for the same reason.
4025        let srf_idxs = self.srf_target_idxs(&projection);
4026        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4027        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
4028            alloc::vec::Vec::with_capacity(filtered.len());
4029        let mut proj_memo = memoize::MemoizeCache::default();
4030        if !srf_idxs.is_empty() {
4031            let (rows, src) =
4032                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
4033            projected_rows = rows;
4034            src_of_row = src;
4035        } else {
4036            for row in &filtered {
4037                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
4038                for p in &projection {
4039                    // v7.24 (round-16 B) — correlated-aware.
4040                    vals.push(self.eval_expr_with_correlated(
4041                        &p.expr,
4042                        row,
4043                        &scan_ctx,
4044                        cancel,
4045                        Some(&mut proj_memo),
4046                    )?);
4047                }
4048                projected_rows.push(Row::new(vals));
4049            }
4050        }
4051        let columns: alloc::vec::Vec<ColumnSchema> = projection
4052            .iter()
4053            // v7.39 (read01 round 54) — keep the column's enum identity through
4054            // the projection (it lives outside the DataType lattice), or a
4055            // derived table / UNION / windowed result forgets it and any outer
4056            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4057            .map(|p| {
4058                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
4059                c.user_enum_type = p.user_enum_type.clone();
4060                c.mysql_fsp = p.mysql_fsp;
4061                c
4062            })
4063            .collect();
4064        // ORDER BY against the source schema.
4065        // v7.39 (round 621) — one entry per OUTPUT row (a target-list SRF makes
4066        // more of them than there were inputs), and a positional key means the
4067        // Nth OUTPUT column, which is what `resolve_positional_order_by` does
4068        // and what the other two synthetic-source tails already did.
4069        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4070        if !order_by.is_empty() {
4071            let out_cols = if srf_idxs.is_empty() {
4072                alloc::vec![None; order_by.len()]
4073            } else {
4074                srf_order_output_cols(&order_by, &projection)
4075            };
4076            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4077                .iter()
4078                .enumerate()
4079                .map(|(k, out)| -> Result<_, EngineError> {
4080                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
4081                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4082                        .iter()
4083                        .zip(out_cols.iter())
4084                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, r, &scan_ctx))
4085                        .collect();
4086                    Ok((k, keys?))
4087                })
4088                .collect::<Result<_, _>>()?;
4089            indexed.sort_by(|a, b| {
4090                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4091                    let o = &stmt.order_by[idx];
4092                    let cmp = order_by_value_cmp_in(
4093                        o.desc,
4094                        o.nulls_first,
4095                        ka,
4096                        kb,
4097                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4098                    );
4099                    if cmp != core::cmp::Ordering::Equal {
4100                        return cmp;
4101                    }
4102                }
4103                core::cmp::Ordering::Equal
4104            });
4105            projected_rows = indexed
4106                .into_iter()
4107                .map(|(i, _)| projected_rows[i].clone())
4108                .collect();
4109        }
4110        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4111        if stmt.distinct {
4112            projected_rows = dedup_rows(projected_rows, scan_ctx.mysql_dialect);
4113        }
4114        if let Some(offset) = stmt.offset_literal() {
4115            let off = (offset as usize).min(projected_rows.len());
4116            projected_rows.drain(..off);
4117        }
4118        if let Some(limit) = stmt.limit_literal() {
4119            projected_rows.truncate(limit as usize);
4120        }
4121        Ok(QueryResult::Rows {
4122            columns,
4123            rows: projected_rows,
4124        })
4125    }
4126
4127    /// The FROM shapes that are not an ordinary table scan — joins, the
4128    /// set-returning sources, JSON_TABLE, a derived table, and the rest.
4129    ///
4130    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4131    /// reason round 848 established in the parser: a debug build gives
4132    /// EVERY branch's locals a slot in the frame, whichever branch runs.
4133    /// `exec_bare_select_cancel` measured 64,784 bytes and a nested query
4134    /// stacks several of them; a plain scan reaches none of these
4135    /// branches. Moving them out took the frame to 52,336.
4136    ///
4137    /// `Ok(None)` means "not one of these shapes, carry on".
4138    #[inline(never)]
4139    fn try_from_shape_paths(
4140        &self,
4141        stmt: &SelectStatement,
4142        from: &spg_sql::ast::FromClause,
4143        cancel: CancelToken<'_>,
4144    ) -> Result<Option<QueryResult>, EngineError> {
4145        if !from.joins.is_empty() {
4146            // v7.37.x (docker-fair LEFTJOIN 71 % attack) — LEFT JOIN
4147            // elimination: when a LEFT JOIN's right side is referenced
4148            // ONLY in the ON equality and the right-side join key is
4149            // UNIQUE/PK, the join preserves outer cardinality exactly
4150            // and contributes no values used downstream. Drop the
4151            // entire join. PG does this on the
4152            // `SELECT COUNT(*) FROM A LEFT JOIN B ON B.pk = A.fk` shape
4153            // — A's row count is what survives, B never has to be
4154            // touched.
4155            if let Some(eliminated) = self.try_eliminate_redundant_left_joins(stmt) {
4156                return self.exec_bare_select_cancel(&eliminated, cancel).map(Some);
4157            }
4158            // v7.38 P0 元机制 D — `SPG_TEST_DISABLE_JOINFOLD=1` skips
4159            // the v7.32 joinfold rewrite that turns inner JOINs into a
4160            // single-table scan when the catalogue can prove key-only
4161            // dependency. Tests use this to assert "without joinfold,
4162            // the join still executes correctly" (joinfold is a
4163            // semantically-equivalent rewrite, not a correctness fix).
4164            if !self.env_cfg().disable_joinfold {
4165                if let Some(folded) = self.try_fold_inner_joins(stmt, cancel)? {
4166                    return self.exec_bare_select_cancel(&folded, cancel).map(Some);
4167                }
4168            }
4169            return self.exec_joined_select(stmt, from, cancel).map(Some);
4170        }
4171        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>`. Synthesise a
4172        // single-column table at SELECT entry by evaluating the
4173        // expression once against the empty row (UNNEST is
4174        // uncorrelated in v7.11; correlated / LATERAL unnest is a
4175        // v7.12 carve-out). Build a virtual `Table` in a heap-only
4176        // catalog, then route to the regular scan path.
4177        if from.primary.unnest_expr.is_some() {
4178            return self
4179                .exec_select_unnest(stmt, &from.primary, cancel)
4180                .map(Some);
4181        }
4182        // v7.37.43-T4.5 — `FROM jsonb_each_text(<expr>)` set-
4183        // returning function. Same dispatch shape as unnest but
4184        // emits a two-column (key TEXT, value TEXT) row stream.
4185        if from.primary.jsonb_each_text_arg.is_some() {
4186            return self
4187                .exec_select_jsonb_each_text(stmt, &from.primary, cancel)
4188                .map(Some);
4189        }
4190        // v7.39 (read01 partitionfuncs.c) — FROM-position table functions
4191        // (pg_partition_tree / pg_partition_ancestors) dispatched by name.
4192        // v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))` whose entries have no
4193        // array form. Each function runs; the results zip in LOCKSTEP with the
4194        // shorter padded to NULL — the SAME rule the target-list SRFs follow
4195        // (round 67), which is why `srf_values` is what evaluates each entry.
4196        if from.primary.rows_from.is_some() {
4197            let (rows, mut schema_cols) = self.rows_from_rows(&from.primary)?;
4198            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4199                if let Some(col) = schema_cols.get_mut(i) {
4200                    col.name = new_name.clone();
4201                }
4202            }
4203            let alias = from
4204                .primary
4205                .alias
4206                .clone()
4207                .unwrap_or_else(|| from.primary.name.clone());
4208            return self
4209                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4210                .map(Some);
4211        }
4212        // v7.39 (round 205, JSON_TABLE) — `FROM JSON_TABLE(doc, '$p'
4213        // COLUMNS (...))`. Materialise the row stream + schema by
4214        // walking the row path, then run the regular pipeline over it.
4215        if let Some(jt) = &from.primary.json_table {
4216            let (rows, schema_cols) = self.json_table_rows(jt, None)?;
4217            let alias = from
4218                .primary
4219                .alias
4220                .clone()
4221                .unwrap_or_else(|| from.primary.name.clone());
4222            return self
4223                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4224                .map(Some);
4225        }
4226        if from.primary.table_fn_call.is_some() {
4227            let (rows, mut schema_cols) = self.table_fn_rows(&from.primary)?;
4228            // v7.39 (read01 round 68) — WITH ORDINALITY appends a BIGINT counter
4229            // (from 1, in output order) AFTER the function's own columns. The
4230            // alias list names it like any other, which is why it is appended
4231            // BEFORE the renaming pass below.
4232            let rows = if from.primary.with_ordinality {
4233                schema_cols.push(ColumnSchema::new(
4234                    "ordinality".to_string(),
4235                    DataType::BigInt,
4236                    false,
4237                ));
4238                rows.into_iter()
4239                    .enumerate()
4240                    .map(|(i, r)| {
4241                        let mut vals = r.values;
4242                        vals.push(Value::BigInt(i as i64 + 1));
4243                        Row::new(vals)
4244                    })
4245                    .collect()
4246            } else {
4247                rows
4248            };
4249            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4250                if let Some(col) = schema_cols.get_mut(i) {
4251                    col.name = new_name.clone();
4252                }
4253            }
4254            let alias = from
4255                .primary
4256                .alias
4257                .clone()
4258                .unwrap_or_else(|| from.primary.name.clone());
4259            return self
4260                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4261                .map(Some);
4262        }
4263        // v7.37.17 (17.6 siblings) — plain derived table in primary
4264        // position: `FROM ( SELECT … ) alias` (no joins). The inner
4265        // SELECT materialises once (it is uncorrelated by
4266        // construction), then the outer projection / WHERE /
4267        // aggregate / ORDER BY pipeline runs over the synthetic
4268        // table. Joined derived tables keep riding the LATERAL
4269        // machinery in join.rs.
4270        if from.joins.is_empty() && from.primary.lateral_subquery.is_some() {
4271            // v7.39 (round 727) — flatten first. A simple derived table
4272            // (bare-column projection over one stored table, nothing that
4273            // changes cardinality or order) used to force the inner
4274            // SELECT through the SERIAL row-at-a-time projection pipeline
4275            // just to materialise a synthetic table the outer query then
4276            // re-scans: `count(*) FROM (SELECT id v FROM d WHERE …) q`
4277            // measured 18.6 ms against PG's 5 — and bare count over the
4278            // same filter WITHOUT the wrapper is 2 ms here, because it
4279            // rides the fused parallel lane. Rewriting to the unwrapped
4280            // form is PG's subquery pull-up; the whole tree gets the
4281            // fast lanes back.
4282            if let Some(flat) = try_flatten_derived(stmt, &from.primary) {
4283                return self.exec_select_cancel(&flat, cancel).map(Some);
4284            }
4285            // v7.39 (round 742) — `SELECT count(*) FROM (SELECT … ORDER
4286            // BY … OFFSET k) q` is `greatest(count_of_inner - k, 0)`:
4287            // ORDER BY never changes the row count, and OFFSET drops
4288            // exactly k. The materialising path sorted 500k rows to
4289            // count 10k (57 ms); PG runs its parallel sort anyway
4290            // (28 ms). The rewrite skips the sort entirely on both
4291            // counts — a plan PG itself does not have.
4292            if let Some(rewritten) = try_count_over_offset(stmt, &from.primary) {
4293                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4294            }
4295            // v7.39 (round 743) — `count(*) OVER a derived whose only
4296            // item is unnest(ARRAY[k elements])` is `k * count(WHERE)`:
4297            // a constant-length array unnests to exactly k rows per
4298            // input row, NULL elements included. PG expands the set to
4299            // count it (6.6 ms on the panel cell); the identity doesn't.
4300            if let Some(rewritten) = try_count_over_const_unnest(stmt, &from.primary) {
4301                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4302            }
4303            return self
4304                .exec_select_derived(stmt, &from.primary, cancel)
4305                .map(Some);
4306        }
4307        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4308        // [, step])` set-returning source. Dispatch mirrors UNNEST:
4309        // materialise the row stream from a single eval pass, then
4310        // run the regular projection / WHERE / ORDER BY / LIMIT
4311        // pipeline over the synthetic single-column table.
4312        if from.primary.generate_series_args.is_some() {
4313            return self
4314                .exec_select_generate_series(stmt, &from.primary, cancel)
4315                .map(Some);
4316        }
4317        Ok(None)
4318    }
4319
4320    /// Pick an index seek for this WHERE, if any of the four apply:
4321    /// BTree equality, GIN `@@`, trigram LIKE, or JSONB `@>`.
4322    ///
4323    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4324    /// frame reason on `try_from_shape_paths`: in a debug build a
4325    /// closure's locals belong to the enclosing frame, and this one is
4326    /// four seek attempts wide on a function that nests.
4327    #[inline(never)]
4328    fn pick_indexed_rows<'r>(
4329        &'r self,
4330        stmt: &SelectStatement,
4331        table: &'r spg_storage::Table,
4332        schema_cols: &[spg_storage::ColumnSchema],
4333        alias: &str,
4334        ctx: &crate::eval::EvalContext<'_>,
4335        seek_snapshot: &crate::Snapshot,
4336    ) -> Option<Vec<Cow<'r, Row<'static>>>> {
4337        stmt.where_.as_ref().and_then(|w| {
4338            // BTree / col=literal seek first — covers the v7.11.3 multi-
4339            // column AND case and the leading-column equality lookup.
4340            try_index_seek(
4341                w,
4342                schema_cols,
4343                self.active_catalog(),
4344                table,
4345                alias,
4346                seek_snapshot,
4347            )
4348            .or_else(|| {
4349                // v7.12.3 — GIN-accelerated `WHERE col @@
4350                // tsquery` when the column has a `USING gin`
4351                // index. Returns an over-approximate candidate
4352                // set; the WHERE re-eval loop below verifies
4353                // the full `@@` predicate per row.
4354                try_gin_seek(
4355                    w,
4356                    schema_cols,
4357                    self.active_catalog(),
4358                    table,
4359                    alias,
4360                    ctx,
4361                    seek_snapshot,
4362                )
4363            })
4364            .or_else(|| {
4365                // v7.15.0 — trigram-GIN-accelerated
4366                // `WHERE col LIKE / ILIKE '<pat>'` when the
4367                // column has a `gin_trgm_ops` GIN index.
4368                // Over-approximate candidate set; the WHERE
4369                // re-eval verifies the LIKE per row.
4370                try_trgm_seek(w, schema_cols, table, alias, seek_snapshot)
4371            })
4372            .or_else(|| {
4373                // v7.37.8(sentori Epic 5 P2)— real JSONB-GIN
4374                // accelerated `WHERE col @> <jsonb_literal>`
4375                // when the column has a `USING gin` index. The
4376                // posting-list intersection returns an over-
4377                // approximate candidate set; the WHERE re-eval
4378                // verifies the full `@>` predicate per row.
4379                try_gin_jsonb_seek(w, schema_cols, table, alias, seek_snapshot)
4380            })
4381        })
4382    }
4383
4384    /// Index-seek fast paths: NSW kNN, the primary-key top-N walk, and
4385    /// the two `count(*)` short-circuits. Out-of-line for the frame
4386    /// reason on `try_from_shape_paths` — an ordinary scan reaches none
4387    /// of them, and in a debug build their locals sit in the frame
4388    /// regardless.
4389    #[inline(never)]
4390    fn try_seek_fast_paths(
4391        &self,
4392        stmt: &SelectStatement,
4393        table: &spg_storage::Table,
4394        schema_cols: &[spg_storage::ColumnSchema],
4395        alias: &str,
4396        seek_snapshot: &crate::Snapshot,
4397        cancel: CancelToken<'_>,
4398    ) -> Result<Option<QueryResult>, EngineError> {
4399        if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias, seek_snapshot) {
4400            // NSW kNN dispatches against the hot-tier vector index only
4401            // (vector cells aren't promoted to cold segments), so wrap
4402            // the returned row indices as `Cow::Borrowed` for the
4403            // unified `materialise_in_order` shape.
4404            let ordered: Vec<Cow<'_, Row<'static>>> = nsw_rows
4405                .into_iter()
4406                .filter_map(|i| table.rows().get(i).map(Cow::Borrowed))
4407                .collect();
4408            return materialise_in_order(
4409                stmt,
4410                schema_cols,
4411                alias,
4412                &ordered,
4413                self.backslash_escapes,
4414            )
4415            .map(Some);
4416        }
4417
4418        // v7.34.5 — ORDER BY <indexed col> [DESC|ASC] LIMIT N drives
4419        // the scan via the BTree iterator in the requested direction
4420        // and stops after `OFFSET + LIMIT` candidates pass WHERE. The
4421        // 80 ms `mailrs_prod_plain_limit` baseline at 250 k rows is
4422        // the load-bearing consumer; this skips the materialise-every-
4423        // row + partial-sort tail entirely. Walker output is already
4424        // in ORDER BY order so `materialise_in_order` (no extra sort)
4425        // is the natural sink.
4426        if let Some(walked) = try_pk_walk_top_n(
4427            stmt,
4428            self.active_catalog(),
4429            table,
4430            schema_cols,
4431            alias,
4432            self,
4433            cancel,
4434        ) {
4435            return materialise_in_order(stmt, schema_cols, alias, &walked, self.backslash_escapes)
4436                .map(Some);
4437        }
4438
4439        // Index seek: if WHERE is `col = literal` (or commuted) and the
4440        // referenced column has an index, dispatch each locator through
4441        // the catalog (hot tier → borrow, cold tier → page-read +
4442        // decode) and iterate just those rows. Otherwise fall back to a
4443        // v7.37.x (docker-fair INSUBQ attack) — short-circuit COUNT(*)
4444        // FROM A WHERE A.pk IN (large literal list). The post-subquery-
4445        // replacement shape of INSUBQ. Runs BEFORE `indexed_rows` so
4446        // we don't pay the row materialisation cost twice. Returns
4447        // a bare `Rows{count}` if the shape matches.
4448        if aggregate::uses_aggregate(stmt)
4449            && let Some(out) = self.try_count_star_pk_in_list_fast(stmt, table, schema_cols, alias)
4450        {
4451            return Ok(Some(out));
4452        }
4453        // v7.38 (perf) — `count(*) WHERE <indexed BETWEEN>`: count the in-range
4454        // locators directly, skipping row materialisation + WHERE re-eval.
4455        if aggregate::uses_aggregate(stmt)
4456            && let Some(out) = self.try_count_star_indexed_range_fast(
4457                stmt,
4458                table,
4459                schema_cols,
4460                alias,
4461                seek_snapshot,
4462            )
4463        {
4464            return Ok(Some(out));
4465        }
4466        Ok(None)
4467    }
4468
4469    /// The two rewrites that must happen before the FROM clause is even
4470    /// looked at: a meta-view reference needs the catalog views
4471    /// materialised, and a windowed projection belongs to the window
4472    /// executor. Out-of-line for the frame reason on
4473    /// `try_from_shape_paths`.
4474    #[inline(never)]
4475    fn try_pre_from_paths(
4476        &self,
4477        stmt: &SelectStatement,
4478        cancel: CancelToken<'_>,
4479    ) -> Result<Option<QueryResult>, EngineError> {
4480        if !self.meta_views_materialised && select_references_meta_view(stmt) {
4481            return self.exec_select_with_meta_views(stmt, cancel).map(Some);
4482        }
4483        // v4.12: window-function path. When the projection contains
4484        // any `name(args) OVER (...)` we route to the dedicated
4485        // executor — partition + sort + per-row window value before
4486        // the regular projection.
4487        if select_has_window(stmt) {
4488            // v7.37 D.23 — window functions run AFTER GROUP BY aggregation.
4489            // `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g`
4490            // needs the aggregation done first, then windows over the grouped
4491            // rows. Rewrite to an aggregate derived subquery + outer window query
4492            // (which the window-over-derived path, D.13, executes). Only fires on
4493            // the currently-erroring agg+window+GROUP BY shape, so it can't
4494            // regress working window-only or aggregate-only queries.
4495            if let Some(rewritten) = rewrite_agg_before_window(stmt) {
4496                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4497            }
4498            return self.exec_select_with_window(stmt, cancel).map(Some);
4499        }
4500        Ok(None)
4501    }
4502
4503    /// A projection naming `ctid` or another system column: the schema
4504    /// has to be widened with them before the scan. Out-of-line for the
4505    /// frame reason on `try_from_shape_paths`.
4506    #[inline(never)]
4507    fn try_ctid_projection(
4508        &self,
4509        stmt: &SelectStatement,
4510        primary: &spg_sql::ast::TableRef,
4511        table: &spg_storage::Table,
4512        schema_cols: &[spg_storage::ColumnSchema],
4513        alias: &str,
4514        cancel: CancelToken<'_>,
4515    ) -> Result<Option<QueryResult>, EngineError> {
4516        if references_ctid(stmt) {
4517            let snapshot = self.current_snapshot();
4518            let mut ext_cols = schema_cols.to_vec();
4519            for name in SYSTEM_COLUMNS {
4520                ext_cols.push(ColumnSchema::new(name.to_string(), DataType::Text, false));
4521            }
4522            let table_oid =
4523                crate::system_catalog::relation_oid(self.active_catalog(), &primary.name)
4524                    .unwrap_or(0);
4525            let headers = table.headers();
4526            let rows: Vec<Row<'static>> = table
4527                .scan_visible(&snapshot)
4528                .map(|(i, r)| {
4529                    let mut vals = r.values.clone();
4530                    // One block, offsets from 1, as PG numbers them.
4531                    vals.push(Value::Tid(0, i as u32 + 1));
4532                    let h = headers.get(i);
4533                    vals.push(Value::Xid(h.map_or(0, |h| h.xmin as u32)));
4534                    vals.push(Value::Xid(h.map_or(0, |h| h.xmax as u32)));
4535                    // SPG keeps no per-statement command ids; PG shows 0 for
4536                    // every row a reader can see, which is every row here.
4537                    vals.push(Value::Cid(0));
4538                    vals.push(Value::Cid(0));
4539                    vals.push(Value::BigInt(table_oid));
4540                    Row::new(vals)
4541                })
4542                .collect();
4543            return self
4544                .exec_select_over_rows(stmt, rows, ext_cols, alias, cancel)
4545                .map(Some);
4546        }
4547        Ok(None)
4548    }
4549
4550    /// A sequence read as a one-row relation (`SELECT last_value FROM
4551    /// seq`), which PG allows and psql's \\d relies on. Out-of-line for
4552    /// the frame reason on `try_from_shape_paths`.
4553    #[inline(never)]
4554    fn try_sequence_relation(
4555        &self,
4556        stmt: &SelectStatement,
4557        primary: &spg_sql::ast::TableRef,
4558        cancel: CancelToken<'_>,
4559    ) -> Result<Option<QueryResult>, EngineError> {
4560        if self.active_catalog().get(&primary.name).is_none()
4561            && let Some(seq) = self.active_catalog().sequence(&primary.name)
4562        {
4563            let rows = alloc::vec![Row::new(alloc::vec![
4564                Value::BigInt(seq.last_value),
4565                Value::BigInt(0),
4566                Value::Bool(seq.is_called),
4567            ])];
4568            let schema_cols = alloc::vec![
4569                ColumnSchema::new("last_value", DataType::BigInt, false),
4570                ColumnSchema::new("log_cnt", DataType::BigInt, false),
4571                ColumnSchema::new("is_called", DataType::Bool, false),
4572            ];
4573            let alias = primary
4574                .alias
4575                .clone()
4576                .unwrap_or_else(|| primary.name.clone());
4577            return self
4578                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4579                .map(Some);
4580        }
4581        Ok(None)
4582    }
4583
4584    pub(crate) fn exec_bare_select_cancel(
4585        &self,
4586        stmt: &SelectStatement,
4587        cancel: CancelToken<'_>,
4588    ) -> Result<QueryResult, EngineError> {
4589        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST N ROWS WITH TIES`
4590        // is meaningless without an ORDER BY; PG raises a hard
4591        // error and SPG mirrors the surface so the same DDL/app
4592        // path behaves identically on cutover.
4593        check_with_ties_requires_order_by(stmt)?;
4594        // v7.39 (round 229) — WHERE / HAVING run before the window pass, so
4595        // PG rejects window calls there outright. Checked here rather than
4596        // on the window path: `HAVING row_number() OVER () = 1` has no
4597        // window in its projection at all.
4598        crate::window::reject_window_in_row_clauses(stmt)?;
4599        // v7.39 (round 232) — the ORDER BY legality rules (positional
4600        // bounds, DISTINCT, DISTINCT ON). Same placement as the window
4601        // check: before anything scans.
4602        crate::orderby::check_order_by_legality(stmt)?;
4603        // v7.37.16 — resolve `USING` column-merge + `NATURAL JOIN` into an
4604        // equivalent statement the regular executor handles (merged join
4605        // columns collapse to a single unqualified output column; NATURAL
4606        // gets its common-column ON synthesised). The rewrite clears the
4607        // flags, so this re-entrant call is a no-op on the second pass.
4608        if let Some(rewritten) = self.desugar_using_natural(stmt)? {
4609            return self.exec_bare_select_cancel(&rewritten, cancel);
4610        }
4611        // v7.39 (RLS) Phase 3 — cross-table joins: wrap each RLS-enabled join
4612        // operand in a security-barrier subquery, then re-enter (the wrapped
4613        // operands are no longer bare RLS tables, so this is a no-op on the
4614        // second pass).
4615        if let Some(rewritten) = self.rls_rewrite_joins(stmt) {
4616            return self.exec_bare_select_cancel(&rewritten, cancel);
4617        }
4618        // v7.39 (RLS) Phase 1 — for a policy-subject (non-superuser) session,
4619        // AND the RLS USING predicate into a single-table SELECT's WHERE.
4620        // Superuser sessions and non-RLS tables get `None` (no clone, no
4621        // change). Applied inline (shadowing `stmt`) rather than via re-entry
4622        // so it can't re-inject on a recursive pass.
4623        let rls_stmt;
4624        let stmt = match self.rls_select_predicate(stmt)? {
4625            Some(pred) => {
4626                let mut s = stmt.clone();
4627                s.where_ = Some(match s.where_.take() {
4628                    Some(existing) => spg_sql::ast::Expr::Binary {
4629                        lhs: alloc::boxed::Box::new(existing),
4630                        op: spg_sql::ast::BinOp::And,
4631                        rhs: alloc::boxed::Box::new(pred),
4632                    },
4633                    None => pred,
4634                });
4635                rls_stmt = s;
4636                &rls_stmt
4637            }
4638            None => stmt,
4639        };
4640        // v7.16.2 — same meta-view dispatch as
4641        // `exec_select_cancel`, applied here too because
4642        // `subquery_replacement` enters this function directly
4643        // for Exists / ScalarSubquery / InSubquery resolution
4644        // (bypassing the top-level entry to avoid double
4645        // subquery walking). Without this dispatch the subquery
4646        // hits `__spg_info_columns` and reports TableNotFound.
4647        if let Some(done) = self.try_pre_from_paths(stmt, cancel)? {
4648            return Ok(done);
4649        }
4650        // Constant SELECT (no FROM) — evaluate each item once against an
4651        // empty dummy row. Useful for `SELECT 1`, `SELECT coalesce(...)`,
4652        // `SELECT '7'::INT`. Column references will surface as
4653        // ColumnNotFound on eval since the schema is empty.
4654        let Some(from) = &stmt.from else {
4655            return self.exec_constant_select(stmt);
4656        };
4657        // Multi-table FROM (one or more joined peers) goes through the
4658        // nested-loop join executor. Single-table FROM stays on the
4659        // existing scan + index-seek path.
4660        if let Some(done) = self.try_from_shape_paths(stmt, from, cancel)? {
4661            return Ok(done);
4662        }
4663        // NOT hooked up. `try_spill_sorted_scan` is written, correct and
4664        // tested — eight ORDER BY shapes byte-identical spilled against
4665        // in-memory, with 103 runs opened to prove the spill ran — and it
4666        // loses on wall clock, which is a hard stop whatever the memory
4667        // buys. Measured round 865, same psql client both sides, same
4668        // machine, row counts verified, and both sides confirmed to be
4669        // doing an external merge rather than an indexed walk:
4670        //
4671        //   PG18        178.7 - 187.0 ms   Sort Method: external merge, 85 MB
4672        //   SPG spilled 269.7 - 299.6 ms   33 spill files at peak
4673        //
4674        // Non-overlapping, about 1.55x. Re-enable by restoring the call
4675        // below once that closes; nothing else has to change, which is
4676        // the point of it being a separate path.
4677        //
4678        //   if let Some(done) = self.try_spill_sorted_scan(stmt, from, cancel)? {
4679        //       return Ok(done);
4680        //   }
4681        //
4682        // v7.37 (round 882) — this walk stays unhooked, but its streaming
4683        // twin `try_spill_sorted_stream` IS hooked, above the ORDER BY
4684        // bail in `try_exec_joined_streaming`. Collecting the answer was
4685        // most of what this one cost: handing rows over as the merge
4686        // produces them holds peak to the budget plus one row, and the
4687        // wall clock lands inside PG18's range rather than 1.55x outside
4688        // it. Numbers in `extsort.rs`'s header.
4689        let primary = &from.primary;
4690        // v7.39 (round 244) — a sequence is selectable as a one-row relation
4691        // in PG (`SELECT last_value FROM seq` — psql's \d and several ORMs
4692        // read it). Synthesize PG's three columns.
4693        if let Some(done) = self.try_sequence_relation(stmt, primary, cancel)? {
4694            return Ok(done);
4695        }
4696        let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4697            StorageError::TableNotFound {
4698                name: primary.name.clone(),
4699            }
4700        })?;
4701        let schema_cols = &table.schema().columns;
4702        // The qualifier accepted on column refs is the alias (if any) else the
4703        // bare table name.
4704        let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4705        // v7.39 (round 511) — `ctid`, PG's physical row identity. SPG had no
4706        // system columns at all: `SELECT ctid FROM t` answered "column
4707        // \"ctid\" does not exist", which takes out the dedup idiom every
4708        // PG user knows — `DELETE … WHERE ctid NOT IN (SELECT min(ctid) …
4709        // GROUP BY key)`.
4710        //
4711        // The value comes from the row's position, which the scan already
4712        // yields; the column is appended to the schema and the rows only
4713        // when the statement asks for it, so nothing else pays for it. That
4714        // also routes the query down the general path, past the index fast
4715        // paths below — they hand back rows without positions, and a ctid
4716        // that was sometimes right would be worse than none.
4717        if let Some(done) =
4718            self.try_ctid_projection(stmt, primary, table, schema_cols, alias, cancel)?
4719        {
4720            return Ok(done);
4721        }
4722        let ctx = self.ev_ctx(schema_cols, Some(alias));
4723
4724        // NSW kNN planner: `ORDER BY col <-> literal LIMIT k` with no
4725        // WHERE and an NSW index on `col` skips the full scan. The
4726        // walk returns rows already in ascending-distance order, so
4727        // ORDER BY / LIMIT are honoured implicitly.
4728        // Phase C.3 step 2c — compute the reader's MVCC snapshot once
4729        // and thread it into every index-seek fast path below. No-op
4730        // today (every hot header is committed-alive).
4731        let seek_snapshot = self.current_snapshot();
4732        if let Some(done) =
4733            self.try_seek_fast_paths(stmt, table, schema_cols, alias, &seek_snapshot, cancel)?
4734        {
4735            return Ok(done);
4736        }
4737        // full scan over the hot tier (cold-tier rows are only reached
4738        // via index seek in v5.1 — full table scans against cold-tier
4739        // data ship in v5.2 with the freezer's per-segment scan API).
4740        let indexed_rows =
4741            self.pick_indexed_rows(stmt, table, schema_cols, alias, &ctx, &seek_snapshot);
4742
4743        // Aggregate path: filter rows first, then hand off to the
4744        // aggregate executor which does its own projection + ORDER BY.
4745        if aggregate::uses_aggregate(stmt) {
4746            return self.run_single_table_aggregate(
4747                stmt,
4748                table,
4749                schema_cols,
4750                alias,
4751                indexed_rows,
4752                cancel,
4753            );
4754        }
4755        self.run_single_table_scan(stmt, table, schema_cols, alias, indexed_rows, cancel)
4756    }
4757
4758    /// v7.37.43-T4.5 — execute `SELECT … FROM jsonb_each_text(<expr>)`.
4759    /// Sentori migration 0067 uses this with `CROSS JOIN LATERAL`; the
4760    /// uncorrelated FROM-primary case is the simpler shape, used by
4761    /// e2e pins. Materialises the (key, value) pair stream into a
4762    /// synthetic two-column TEXT table, then routes through the
4763    /// regular projection / WHERE / ORDER BY pipeline.
4764    /// v7.39 (read01 partitionfuncs.c) — materialise a FROM-position
4765    /// v7.39 (round 205, JSON_TABLE) — materialise a JSON_TABLE FROM
4766    /// item into (rows, schema). `outer_doc` is `Some` only when this
4767    /// is a NESTED level being expanded against a parent row item's
4768    /// already-parsed sub-document; the top-level call parses the doc
4769    /// expr itself. Row/column paths reuse the existing jsonpath
4770    /// evaluator (`json::json_table_path`); coercion reuses
4771    /// `coerce_value` on the JSON scalar text, so a json string
4772    /// coerces to DATE by its content, matching PG.
4773    #[allow(clippy::type_complexity)]
4774    pub(crate) fn json_table_rows(
4775        &self,
4776        jt: &spg_sql::ast::JsonTable,
4777        outer_doc: Option<&crate::json::JsonValue>,
4778    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4779        // Column schema is static (independent of data): flatten the
4780        // COLUMNS tree in declaration order (NESTED contributes its
4781        // children inline, the PG output shape).
4782        let schema = json_table_schema(&jt.columns);
4783
4784        // PASSING variables → a single JsonValue object the jsonpath
4785        // engine reads `$name` from.
4786        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4787        let ctx = EvalContext::new(&empty_schema, None);
4788        let dummy = Row::new(alloc::vec::Vec::new());
4789        let vars: Option<crate::json::JsonValue> = if jt.passing.is_empty() {
4790            None
4791        } else {
4792            let mut entries = alloc::vec::Vec::new();
4793            for (name, e) in &jt.passing {
4794                let v = eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?;
4795                entries.push((name.clone(), value_to_json_value(&v)));
4796            }
4797            Some(crate::json::JsonValue::Object(entries))
4798        };
4799
4800        // The document root: a NESTED level gets it from the parent;
4801        // the top level parses its doc expr.
4802        let root_owned;
4803        let root: &crate::json::JsonValue = match outer_doc {
4804            Some(d) => d,
4805            None => {
4806                let doc_val = eval::eval_expr(&jt.doc, &dummy, &ctx).map_err(EngineError::Eval)?;
4807                let src = match &doc_val {
4808                    Value::Null => return Ok((alloc::vec::Vec::new(), schema)),
4809                    Value::Json(s) | Value::Text(s) => s.as_ref().to_string(),
4810                    other => {
4811                        return Err(EngineError::Unsupported(alloc::format!(
4812                            "JSON_TABLE document must be json/text, got {}",
4813                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
4814                        )));
4815                    }
4816                };
4817                root_owned = crate::json::parse_doc(&src).map_err(EngineError::Eval)?;
4818                &root_owned
4819            }
4820        };
4821
4822        let items = crate::json::json_table_path(root, &jt.row_path, vars.as_ref())
4823            .map_err(EngineError::Eval)?;
4824        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
4825        for (idx, item) in items.iter().enumerate() {
4826            self.json_table_emit_item(jt, item, idx, vars.as_ref(), &mut rows)?;
4827        }
4828        Ok((rows, schema))
4829    }
4830
4831    /// v7.39 (round 205) — emit the row(s) for one row-pattern item.
4832    /// Regular columns produce one value each; a NESTED column expands
4833    /// as an outer join (each nested match → one row sharing the
4834    /// parent cells; no nested match → one row with the nested cells
4835    /// NULL). Sibling NESTED at one level cross by concatenation of
4836    /// their independent expansions (PG's UNION-of-outer shape).
4837    fn json_table_emit_item(
4838        &self,
4839        jt: &spg_sql::ast::JsonTable,
4840        item: &crate::json::JsonValue,
4841        ordinality: usize,
4842        vars: Option<&crate::json::JsonValue>,
4843        out: &mut alloc::vec::Vec<Row<'static>>,
4844    ) -> Result<(), EngineError> {
4845        use spg_sql::ast::JsonTableColumn as C;
4846        // Parent cells (regular + ordinality), left-to-right; NESTED
4847        // columns contribute a run of child cells appended after.
4848        let mut parent_cells: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
4849        let mut nested_runs: alloc::vec::Vec<alloc::vec::Vec<Row<'static>>> =
4850            alloc::vec::Vec::new();
4851        let mut nested_widths: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4852        for col in &jt.columns {
4853            match col {
4854                C::Ordinality { .. } => {
4855                    parent_cells.push(Value::BigInt(ordinality as i64 + 1));
4856                }
4857                C::Regular { .. } => {
4858                    parent_cells.push(self.json_table_column_value(col, item, vars)?);
4859                }
4860                C::Nested { path, columns } => {
4861                    // Recurse: a nested JSON_TABLE over `item` filtered
4862                    // by `path`, with the same PASSING vars.
4863                    let sub = spg_sql::ast::JsonTable {
4864                        doc: jt.doc.clone(), // unused (outer_doc provided)
4865                        row_path: path.clone(),
4866                        columns: columns.clone(),
4867                        passing: alloc::vec::Vec::new(),
4868                    };
4869                    let (nrows, nschema) = self.json_table_rows(&sub, Some(item))?;
4870                    nested_widths.push(nschema.len());
4871                    nested_runs.push(nrows);
4872                }
4873            }
4874        }
4875        if nested_runs.is_empty() {
4876            out.push(Row::new(parent_cells));
4877            return Ok(());
4878        }
4879        // PG sibling-NESTED semantics: each sibling expands
4880        // INDEPENDENTLY and the results CONCATENATE — a row from
4881        // sibling s fills only s's cells, every other sibling's cells
4882        // NULL. An empty sibling contributes ZERO rows (not a NULL
4883        // row). Only when EVERY sibling is empty does the parent still
4884        // emit one all-NULL row (the outer-join guarantee that a parent
4885        // item is never dropped). Verified vs PG18 (r207): a=1,b=2 → 3
4886        // rows; a=1,b=[] → 1 row; all-empty → 1 NULL row.
4887        let before = out.len();
4888        for (s_idx, run) in nested_runs.iter().enumerate() {
4889            for nrow in run {
4890                let mut cells = parent_cells.clone();
4891                for (o_idx, w) in nested_widths.iter().enumerate() {
4892                    if o_idx == s_idx {
4893                        cells.extend(nrow.values.iter().cloned());
4894                    } else {
4895                        for _ in 0..*w {
4896                            cells.push(Value::Null);
4897                        }
4898                    }
4899                }
4900                out.push(Row::new(cells));
4901            }
4902        }
4903        if out.len() == before {
4904            // Every sibling empty → one all-NULL nested row.
4905            let mut cells = parent_cells.clone();
4906            for w in &nested_widths {
4907                for _ in 0..*w {
4908                    cells.push(Value::Null);
4909                }
4910            }
4911            out.push(Row::new(cells));
4912        }
4913        Ok(())
4914    }
4915
4916    /// v7.39 (round 205) — evaluate one Regular column against a row
4917    /// item: EXISTS → bool; else path → at most one value, coerced to
4918    /// the declared type with ON EMPTY / ON ERROR / DEFAULT behaviour.
4919    fn json_table_column_value(
4920        &self,
4921        col: &spg_sql::ast::JsonTableColumn,
4922        item: &crate::json::JsonValue,
4923        vars: Option<&crate::json::JsonValue>,
4924    ) -> Result<Value<'static>, EngineError> {
4925        use spg_sql::ast::{JsonTableColumn as C, JsonTableOnBehavior as B};
4926        let C::Regular {
4927            name,
4928            ty,
4929            path,
4930            exists,
4931            format_json,
4932            wrapper,
4933            on_empty,
4934            on_error,
4935        } = col
4936        else {
4937            unreachable!("caller guards Regular");
4938        };
4939        let matches = crate::json::json_table_path(item, path, vars).map_err(EngineError::Eval)?;
4940        if *exists {
4941            return Ok(Value::Bool(!matches.is_empty()));
4942        }
4943        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4944        let ctx = EvalContext::new(&empty_schema, None);
4945        let dummy = Row::new(alloc::vec::Vec::new());
4946        let default_of = |b: &B| -> Result<Option<Value<'static>>, EngineError> {
4947            match b {
4948                B::Null => Ok(Some(Value::Null)),
4949                B::Error => Ok(None),
4950                B::Default(e) => Ok(Some(
4951                    eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?,
4952                )),
4953            }
4954        };
4955        // Empty match set → ON EMPTY.
4956        if matches.is_empty() {
4957            return match default_of(on_empty)? {
4958                Some(v) => coerce_json_table_default(v, *ty, name),
4959                None => Err(EngineError::Unsupported(alloc::format!(
4960                    "no SQL/JSON item found for JSON_TABLE column {name:?}"
4961                ))),
4962            };
4963        }
4964        let first = &matches[0];
4965        // FORMAT JSON: return the PG-canonical json representation.
4966        // WITH WRAPPER wraps the whole match SET in an array (even a
4967        // single scalar → `[5]`); without it, the single match's json.
4968        if *format_json {
4969            let text = if *wrapper {
4970                crate::json::JsonValue::Array(matches.clone()).canonical_json_text()
4971            } else {
4972                first.canonical_json_text()
4973            };
4974            return Ok(Value::Json(alloc::borrow::Cow::Owned(text)));
4975        }
4976        if first.is_json_null() {
4977            return Ok(Value::Null);
4978        }
4979        // Coerce the scalar text to the declared type; on failure → ON
4980        // ERROR (default NULL, DEFAULT expr, or raise).
4981        let dt = crate::conversions::column_type_to_data_type(*ty);
4982        let scalar = Value::Text(alloc::borrow::Cow::Owned(first.scalar_text()));
4983        match crate::conversions::coerce_value(scalar, dt, name, 0) {
4984            Ok(v) => Ok(v),
4985            Err(e) => match default_of(on_error)? {
4986                Some(v) => coerce_json_table_default(v, *ty, name),
4987                None => Err(e),
4988            },
4989        }
4990    }
4991
4992    /// table function into (rows, default schema). Dispatch by name.
4993    pub(crate) fn table_fn_rows(
4994        &self,
4995        primary: &TableRef,
4996    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4997        let (fn_name, args) = primary
4998            .table_fn_call
4999            .as_deref()
5000            .expect("caller guards table_fn_call.is_some()");
5001        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5002        let ctx = EvalContext::new(&empty_schema, None);
5003        let dummy_row = Row::new(alloc::vec::Vec::new());
5004        let arg0: Option<Value<'static>> = match args.first() {
5005            Some(e) => Some(eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?),
5006            None => None,
5007        };
5008        match fn_name.as_str() {
5009            // v7.39 (read01 round 76) — `jsonb_populate_record(NULL::t, j)` /
5010            // `…_recordset` (+ json_ variants). The row shape is the BASE
5011            // argument's declared type — a table's or a composite type's
5012            // column list — which only the catalog knows, so the parser hands
5013            // the raw arguments here rather than desugaring blind.
5014            "jsonb_populate_record"
5015            | "json_populate_record"
5016            | "jsonb_populate_recordset"
5017            | "json_populate_recordset" => {
5018                let type_name = match args.first() {
5019                    Some(Expr::Cast {
5020                        target: spg_sql::ast::CastTarget::Named(n),
5021                        ..
5022                    }) => n.clone(),
5023                    _ => {
5024                        return Err(EngineError::Unsupported(alloc::format!(
5025                            "{fn_name}(): first argument must name a row type, \
5026                             e.g. NULL::mytable"
5027                        )));
5028                    }
5029                };
5030                let cat = self.active_catalog();
5031                let cols: alloc::vec::Vec<ColumnSchema> = if let Some(t) = cat.get(&type_name) {
5032                    t.schema().columns.clone()
5033                } else if let Some(c) = cat.composite_types().get(&type_name) {
5034                    c.fields
5035                        .iter()
5036                        .map(|(n, ty)| ColumnSchema::new(n.clone(), *ty, true))
5037                        .collect()
5038                } else {
5039                    return Err(EngineError::Unsupported(alloc::format!(
5040                        "type \"{type_name}\" does not exist"
5041                    )));
5042                };
5043                let json_arg = match args.get(1) {
5044                    Some(e) => eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?,
5045                    None => Value::Null,
5046                };
5047                // The set form iterates the JSON array; the scalar form is
5048                // the one-element case of the same walk.
5049                let docs: alloc::vec::Vec<Value<'static>> = if fn_name.ends_with("recordset") {
5050                    crate::json::array_element_rows(&json_arg, false, fn_name)
5051                        .map_err(EngineError::Eval)?
5052                        .into_iter()
5053                        .map(|s| s.map_or(Value::Null, Value::json))
5054                        .collect()
5055                } else if matches!(json_arg, Value::Null) {
5056                    alloc::vec::Vec::new()
5057                } else {
5058                    alloc::vec![json_arg]
5059                };
5060                let mut rows = alloc::vec::Vec::with_capacity(docs.len());
5061                for doc in &docs {
5062                    let mut vals = alloc::vec::Vec::with_capacity(cols.len());
5063                    for c in &cols {
5064                        // `->>` semantics: a missing key is NULL, present keys
5065                        // arrive as text and cast to the declared column type.
5066                        let raw = crate::json::path_get(doc, &Value::text(c.name.clone()), true)
5067                            .map_err(EngineError::Eval)?;
5068                        let v = if matches!(raw, Value::Null) {
5069                            Value::Null
5070                        } else {
5071                            crate::conversions::coerce_value(raw, c.ty, "", 0)
5072                                .map_err(|e| EngineError::Unsupported(alloc::format!("{e:?}")))?
5073                        };
5074                        vals.push(v);
5075                    }
5076                    rows.push(Row::new(vals));
5077                }
5078                Ok((rows, cols))
5079            }
5080            "pg_partition_tree" => {
5081                let cols = alloc::vec![
5082                    ColumnSchema::new("relid".to_string(), DataType::Text, true),
5083                    ColumnSchema::new("parentrelid".to_string(), DataType::Text, true),
5084                    ColumnSchema::new("isleaf".to_string(), DataType::Bool, true),
5085                    ColumnSchema::new("level".to_string(), DataType::Int, true),
5086                ];
5087                let Some(Value::Text(name)) = &arg0 else {
5088                    // NULL (or missing) argument → zero rows (PG).
5089                    return Ok((alloc::vec::Vec::new(), cols));
5090                };
5091                let entries = crate::partition_walks::tree_of(self.active_catalog(), name.as_ref());
5092                if entries.is_empty() && self.active_catalog().get(name.as_ref()).is_none() {
5093                    return Err(EngineError::Unsupported(alloc::format!(
5094                        "relation \"{name}\" does not exist"
5095                    )));
5096                }
5097                let rows = entries
5098                    .into_iter()
5099                    .map(|(relid, parent, isleaf, level)| {
5100                        Row::new(alloc::vec![
5101                            Value::text(relid),
5102                            parent.map_or(Value::Null, Value::text),
5103                            Value::Bool(isleaf),
5104                            #[allow(clippy::cast_possible_truncation)]
5105                            Value::Int(level as i32),
5106                        ])
5107                    })
5108                    .collect();
5109                Ok((rows, cols))
5110            }
5111            "pg_partition_ancestors" => {
5112                let cols =
5113                    alloc::vec![ColumnSchema::new("relid".to_string(), DataType::Text, true)];
5114                let Some(Value::Text(name)) = &arg0 else {
5115                    return Ok((alloc::vec::Vec::new(), cols));
5116                };
5117                let cat = self.active_catalog();
5118                if cat.get(name.as_ref()).is_none() {
5119                    return Err(EngineError::Unsupported(alloc::format!(
5120                        "relation \"{name}\" does not exist"
5121                    )));
5122                }
5123                // A relation outside any partition tree yields no rows (PG).
5124                let in_tree = cat
5125                    .get(name.as_ref())
5126                    .is_some_and(|t| t.schema().partition_role.is_some());
5127                let rows = if in_tree {
5128                    crate::partition_walks::ancestors_of(cat, name.as_ref())
5129                        .into_iter()
5130                        .map(|n| Row::new(alloc::vec![Value::text(n)]))
5131                        .collect()
5132                } else {
5133                    alloc::vec::Vec::new()
5134                };
5135                Ok((rows, cols))
5136            }
5137            // v7.39 (round 651) — `ts_debug(config, text)`: what the parser
5138            // saw, what each token was called, which dictionary took it
5139            // and what came out. It is a projection of the same tokenizer
5140            // and the same map the indexer uses, so it cannot describe a
5141            // pipeline other than the one that runs.
5142            "ts_debug" => {
5143                use crate::fts::{TokenType, TsDict};
5144                let cols = alloc::vec![
5145                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5146                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5147                    ColumnSchema::new("token".to_string(), DataType::Text, false),
5148                    ColumnSchema::new("dictionaries".to_string(), DataType::TextArray, false),
5149                    ColumnSchema::new("dictionary".to_string(), DataType::Text, true),
5150                    ColumnSchema::new("lexemes".to_string(), DataType::TextArray, true),
5151                ];
5152                // PG's one-arg form uses the session configuration; the
5153                // two-arg form names one.
5154                let (cfg_name, text) = match (&arg0, args.get(1)) {
5155                    (Some(Value::Text(c)), Some(t)) => {
5156                        let v = eval::eval_expr(t, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5157                        (c.to_string(), crate::eval::value_to_text(&v))
5158                    }
5159                    (Some(v), None) => (
5160                        alloc::string::String::from("english"),
5161                        crate::eval::value_to_text(v),
5162                    ),
5163                    _ => return Ok((alloc::vec::Vec::new(), cols)),
5164                };
5165                let english = match cfg_name
5166                    .trim()
5167                    .trim_start_matches("pg_catalog.")
5168                    .to_ascii_lowercase()
5169                    .as_str()
5170                {
5171                    "english" => true,
5172                    "simple" => false,
5173                    other => {
5174                        return Err(EngineError::Unsupported(alloc::format!(
5175                            "text search configuration \"{other}\" does not exist"
5176                        )));
5177                    }
5178                };
5179                let rows = crate::fts::tokenize_typed(&text)
5180                    .into_iter()
5181                    .map(|tok| {
5182                        let dict = tok.ty.dictionary(english);
5183                        let dname = dict.map(|d| match d {
5184                            TsDict::Simple => "simple",
5185                            TsDict::EnglishStem => "english_stem",
5186                        });
5187                        let folded = tok.text.to_lowercase();
5188                        let lexemes = dict.map(|d| match d {
5189                            TsDict::Simple => alloc::vec![Some(folded.clone())],
5190                            TsDict::EnglishStem => {
5191                                if crate::fts::is_english_stopword(&folded) {
5192                                    alloc::vec::Vec::new()
5193                                } else {
5194                                    alloc::vec![Some(crate::fts::porter_stem(&folded))]
5195                                }
5196                            }
5197                        });
5198                        Row::new(alloc::vec![
5199                            Value::text(tok.ty.alias()),
5200                            Value::text(tok.ty.description()),
5201                            Value::text(tok.text),
5202                            Value::TextArray(
5203                                dname
5204                                    .map(|n| alloc::vec![Some(alloc::string::String::from(n))])
5205                                    .unwrap_or_default(),
5206                            ),
5207                            dname.map_or(Value::Null, Value::text),
5208                            lexemes.map_or(Value::Null, Value::TextArray),
5209                        ])
5210                    })
5211                    .collect();
5212                let _ = TokenType::AsciiWord;
5213                Ok((rows, cols))
5214            }
5215            // v7.39 (round 651) — `ts_token_type('default')`, the list the
5216            // parser actually produces. It is a projection of the
5217            // `TokenType` enum the tokenizer and `pg_ts_config_map` both
5218            // read, so the three cannot disagree about what a token is.
5219            "ts_token_type" => {
5220                use crate::fts::TokenType as T;
5221                let cols = alloc::vec![
5222                    ColumnSchema::new("tokid".to_string(), DataType::Int, false),
5223                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5224                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5225                ];
5226                // PG takes the parser by name or oid; SPG has the one.
5227                if let Some(Value::Text(p)) = &arg0
5228                    && !p.eq_ignore_ascii_case("default")
5229                    && !p.eq_ignore_ascii_case("pg_catalog.default")
5230                {
5231                    return Err(EngineError::Unsupported(alloc::format!(
5232                        "text search parser \"{p}\" does not exist"
5233                    )));
5234                }
5235                const TYPES: &[T] = &[
5236                    T::AsciiWord,
5237                    T::Word,
5238                    T::NumWord,
5239                    T::Email,
5240                    T::Url,
5241                    T::Host,
5242                    T::SFloat,
5243                    T::Version,
5244                    T::HwordNumPart,
5245                    T::HwordPart,
5246                    T::HwordAsciiPart,
5247                    T::Blank,
5248                    T::Tag,
5249                    T::Protocol,
5250                    T::NumHword,
5251                    T::AsciiHword,
5252                    T::Hword,
5253                    T::UrlPath,
5254                    T::File,
5255                    T::Float,
5256                    T::Int,
5257                    T::Uint,
5258                    T::Entity,
5259                ];
5260                let rows = TYPES
5261                    .iter()
5262                    .map(|t| {
5263                        Row::new(alloc::vec![
5264                            Value::Int(*t as i32),
5265                            Value::text(t.alias()),
5266                            Value::text(t.description()),
5267                        ])
5268                    })
5269                    .collect();
5270                Ok((rows, cols))
5271            }
5272            // v7.39 (read01 round 65) — a set-returning USER function in FROM
5273            // (`FROM rows_of(2)`). Its body runs through the real executor, like
5274            // every other function body since round 63.
5275            other => {
5276                if !self.active_catalog().functions_named(other).is_empty() {
5277                    return self.exec_setof_user_function(other, args, primary.alias.as_deref());
5278                }
5279                Err(EngineError::Unsupported(alloc::format!(
5280                    "table function {other}() is not supported in FROM"
5281                )))
5282            }
5283        }
5284    }
5285
5286    /// v7.39 (read01 round 65) — run a `RETURNS SETOF <type>` / `RETURNS
5287    /// TABLE(…)` function in FROM position. The body is a SELECT; the arguments
5288    /// are bound into it as literals and it goes through the read path, so the
5289    /// rows it yields are exactly the rows a hand-written query would see.
5290    ///
5291    /// The column NAMES come from the declared shape: `RETURNS TABLE(id int, v
5292    /// text)` names them, and a `SETOF <scalar>` yields a single column named
5293    /// after the function — PG's rule, and what a bare `SELECT * FROM f()`
5294    /// shows.
5295    fn exec_setof_user_function(
5296        &self,
5297        name: &str,
5298        args: &[spg_sql::ast::Expr],
5299        // v7.39 (read01 round 65) — `FROM evens() AS x` names the single column
5300        // `x`: for a scalar SETOF, the table alias IS the column name (PG).
5301        alias: Option<&str>,
5302    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5303        // The call's arguments belong to the ENCLOSING query, so they are
5304        // evaluated here and the body sees values.
5305        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5306        let arg_ctx = self.ev_ctx(&empty, None);
5307        let dummy = Row::new(alloc::vec::Vec::new());
5308        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5309        for a in args {
5310            vals.push(eval::eval_expr(a, &dummy, &arg_ctx).map_err(EngineError::Eval)?);
5311        }
5312        self.setof_rows_of(name, &vals, alias)
5313    }
5314
5315    /// v7.39 (read01 round 67) — the set-returning core, on already-evaluated
5316    /// arguments. Shared by the FROM position and the target-list expansion, so
5317    /// a function cannot behave differently depending on where it is called.
5318    pub(crate) fn setof_rows_of(
5319        &self,
5320        name: &str,
5321        arg_values: &[Value<'static>],
5322        alias: Option<&str>,
5323    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5324        let cat = self.active_catalog();
5325        let overloads = cat.functions_named(name);
5326        let def = overloads
5327            .iter()
5328            .find(|f| spg_storage::function_arg_types(&f.args_repr).len() == arg_values.len())
5329            .ok_or_else(|| {
5330                EngineError::Unsupported(alloc::format!(
5331                    "function {name} does not exist with {} argument(s)",
5332                    arg_values.len()
5333                ))
5334            })?;
5335        let declared = def.returns.trim().to_string();
5336        let upper = declared.to_ascii_uppercase();
5337        if !upper.starts_with("SETOF") && !upper.starts_with("TABLE(") {
5338            return Err(EngineError::Unsupported(alloc::format!(
5339                "function {name}() does not return a set — it cannot be used in FROM"
5340            )));
5341        }
5342
5343        let arg_names_pl = spg_storage::function_arg_names(&def.args_repr);
5344        // v7.39 (read01 round 66) — a plpgsql SETOF body builds its rows with
5345        // RETURN NEXT / RETURN QUERY; the interpreter collects them.
5346        if def.language.eq_ignore_ascii_case("plpgsql") {
5347            let out_rows = self
5348                .call_plpgsql_setof_fn(def, &arg_names_pl, arg_values)
5349                .map_err(EngineError::Eval)?;
5350            let cols = setof_column_shape(&declared, name, alias, out_rows.first());
5351            let rows = out_rows.into_iter().map(Row::new).collect();
5352            return Ok((rows, cols));
5353        }
5354        let body = def.body.trim().trim_end_matches(';');
5355        let stmt = spg_sql::parser::parse_statement(body).map_err(|e| {
5356            EngineError::Unsupported(alloc::format!("function {name} body does not parse: {e}"))
5357        })?;
5358        let spg_sql::ast::Statement::Select(body_select) = stmt else {
5359            return Err(EngineError::Unsupported(alloc::format!(
5360                "function {name}(): a set-returning body must be a SELECT"
5361            )));
5362        };
5363        let arg_names = spg_storage::function_arg_names(&def.args_repr);
5364        let bound = crate::eval::bind_user_fn_args(
5365            self.active_catalog(),
5366            &body_select,
5367            &arg_names,
5368            arg_values,
5369        )
5370        .map_err(EngineError::Eval)?;
5371        let out = self.exec_select_cancel(&bound, crate::CancelToken::none())?;
5372        let QueryResult::Rows { columns, rows } = out else {
5373            return Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new()));
5374        };
5375        // Name the columns from the DECLARED shape — the same rule the plpgsql
5376        // path above uses, so a body's language cannot change the row shape.
5377        let cols = setof_column_shape_from(&declared, name, alias, &columns);
5378        Ok((rows, cols))
5379    }
5380
5381    fn exec_select_jsonb_each_text(
5382        &self,
5383        stmt: &SelectStatement,
5384        primary: &TableRef,
5385        cancel: CancelToken<'_>,
5386    ) -> Result<QueryResult, EngineError> {
5387        let (each_fn, arg_expr) = primary
5388            .jsonb_each_text_arg
5389            .as_ref()
5390            .map(|(name, expr)| (name.as_str(), expr.as_ref()))
5391            .expect("caller guards jsonb_each_text_arg.is_some()");
5392        // v7.37.17 (17.6 siblings) — the plain jsonb_each / json_each
5393        // forms keep JSON rendering in the value column (JSON null
5394        // stays jsonb 'null', strings keep their quotes).
5395        let as_text = each_fn.ends_with("_text");
5396        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5397        let ctx = EvalContext::new(&empty_schema, None);
5398        let dummy_row = Row::new(alloc::vec::Vec::new());
5399        let arg_value = eval::eval_expr(arg_expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5400        let pairs =
5401            crate::json::each_rows(&arg_value, as_text, each_fn).map_err(EngineError::Eval)?;
5402        let rows: alloc::vec::Vec<Row<'static>> = pairs
5403            .into_iter()
5404            .map(|(k, v)| {
5405                let key_val = Value::text(k);
5406                let value_val = match v {
5407                    Some(s) if as_text => Value::text(s),
5408                    Some(s) => Value::Json(alloc::borrow::Cow::Owned(s)),
5409                    None => Value::Null,
5410                };
5411                Row::new(alloc::vec![key_val, value_val])
5412            })
5413            .collect();
5414        let alias = primary.alias.clone().unwrap_or_else(|| each_fn.to_string());
5415        let value_dtype = if as_text {
5416            spg_storage::DataType::Text
5417        } else {
5418            spg_storage::DataType::Json
5419        };
5420        let key_col = ColumnSchema::new("key".to_string(), spg_storage::DataType::Text, false);
5421        let value_col = ColumnSchema::new("value".to_string(), value_dtype, as_text);
5422        let mut schema_cols = alloc::vec![key_col, value_col];
5423        // `AS t(k, v)` renames key/value positionally (PG behaviour); the
5424        // LATERAL-position form of the same call already honours it.
5425        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5426            if let Some(col) = schema_cols.get_mut(i) {
5427                col.name = new_name.clone();
5428            }
5429        }
5430        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
5431        // `EvalContext::new` drops it and every catalog-dependent cast
5432        // (regclass / enum / composite / domain) silently degrades.
5433        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
5434        // WHERE.
5435        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5436            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5437            for row in rows {
5438                cancel.check()?;
5439                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
5440                if matches!(v, Value::Bool(true)) {
5441                    out.push(row);
5442                }
5443            }
5444            out
5445        } else {
5446            rows
5447        };
5448        // Aggregate dispatch (e.g. SELECT COUNT(*) FROM jsonb_each_text…).
5449        if aggregate::uses_aggregate(stmt) {
5450            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5451            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5452                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5453                    .map_err(|err| match err {
5454                        EngineError::Eval(ev) => ev,
5455                        other => eval::EvalError::TypeMismatch {
5456                            detail: alloc::format!("{other}"),
5457                        },
5458                    })
5459            };
5460            // v7.39 (round 656) — hand the rows over as they are rather than
5461            // collecting a second vector of `RowRef` wrappers. Note this is
5462            // a set-returning-function path, NOT the relational scan: the
5463            // measured O(rows) cost lived in `run_single_table_aggregate`,
5464            // and converting these four first was a miss that cost a full
5465            // round — every test stayed green and the number did not move.
5466            let agg = aggregate::run(
5467                stmt,
5468                crate::join::AggRows::Owned(&filtered),
5469                &schema_cols,
5470                Some(&alias),
5471                Some(&agg_correlated),
5472                self.parallel_runner.0.as_deref(),
5473                Some(self.active_catalog()),
5474                Some(self),
5475            )?;
5476            return self.finish_agg_result(agg, stmt, cancel);
5477        }
5478        // Projection.
5479        let projection =
5480            build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
5481        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5482            alloc::vec::Vec::with_capacity(filtered.len());
5483        for row in &filtered {
5484            let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5485            for p in &projection {
5486                let v = eval::eval_expr(&p.expr, row, &scan_ctx).map_err(EngineError::Eval)?;
5487                vals.push(v);
5488            }
5489            projected_rows.push(Row::new(vals));
5490        }
5491        let columns: alloc::vec::Vec<ColumnSchema> = projection
5492            .iter()
5493            // v7.39 (read01 round 54) — keep the column's enum identity through
5494            // the projection (it lives outside the DataType lattice), or a
5495            // derived table / UNION / windowed result forgets it and any outer
5496            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5497            .map(|p| {
5498                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
5499                c.user_enum_type = p.user_enum_type.clone();
5500                c.mysql_fsp = p.mysql_fsp;
5501                c
5502            })
5503            .collect();
5504        // ORDER BY.
5505        if !stmt.order_by.is_empty() {
5506            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = filtered
5507                .iter()
5508                .enumerate()
5509                .map(|(i, r)| -> Result<_, EngineError> {
5510                    let keys: Result<Vec<Value<'static>>, EngineError> = stmt
5511                        .order_by
5512                        .iter()
5513                        .map(|ob| {
5514                            eval::eval_expr(&ob.expr, r, &scan_ctx).map_err(EngineError::Eval)
5515                        })
5516                        .collect();
5517                    Ok((i, keys?))
5518                })
5519                .collect::<Result<_, _>>()?;
5520            indexed.sort_by(|a, b| {
5521                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5522                    let o = &stmt.order_by[idx];
5523                    let cmp = order_by_value_cmp_in(
5524                        o.desc,
5525                        o.nulls_first,
5526                        ka,
5527                        kb,
5528                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5529                    );
5530                    if cmp != core::cmp::Ordering::Equal {
5531                        return cmp;
5532                    }
5533                }
5534                core::cmp::Ordering::Equal
5535            });
5536            projected_rows = indexed
5537                .into_iter()
5538                .map(|(i, _)| projected_rows[i].clone())
5539                .collect();
5540        }
5541        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5542        if stmt.distinct {
5543            projected_rows = dedup_rows(projected_rows, scan_ctx.mysql_dialect);
5544        }
5545        if let Some(offset) = stmt.offset_literal() {
5546            let off = (offset as usize).min(projected_rows.len());
5547            projected_rows.drain(..off);
5548        }
5549        if let Some(limit) = stmt.limit_literal() {
5550            projected_rows.truncate(limit as usize);
5551        }
5552        Ok(QueryResult::Rows {
5553            columns,
5554            rows: projected_rows,
5555        })
5556    }
5557
5558    /// v7.37.17 (17.6 siblings) — execute `SELECT … FROM
5559    /// ( SELECT … ) alias` in primary position. The inner SELECT
5560    /// materialises once through the regular bare-select executor
5561    /// (UNION tails included), then the outer WHERE / aggregate /
5562    /// projection / ORDER BY / LIMIT pipeline runs over the
5563    /// synthetic table — the same post-materialisation shape as
5564    /// exec_select_jsonb_each_text, generalised to N columns.
5565    fn exec_select_derived(
5566        &self,
5567        stmt: &SelectStatement,
5568        primary: &TableRef,
5569        cancel: CancelToken<'_>,
5570    ) -> Result<QueryResult, EngineError> {
5571        let inner = primary
5572            .lateral_subquery
5573            .as_deref()
5574            .expect("caller guards lateral_subquery.is_some()");
5575        // exec_select_cancel is the union-aware wrapper — the inner
5576        // SELECT may carry UNION tails on stmt.unions.
5577        let QueryResult::Rows {
5578            columns: inner_cols,
5579            rows,
5580        } = self.exec_select_cancel(inner, cancel)?
5581        else {
5582            return Err(EngineError::Unsupported(
5583                "derived table subquery must return rows".into(),
5584            ));
5585        };
5586        let alias = primary
5587            .alias
5588            .clone()
5589            .unwrap_or_else(|| primary.name.clone());
5590        // `AS t(a, b)` renames the materialised columns positionally
5591        // (extra inner columns keep their own names, PG behaviour).
5592        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = inner_cols;
5593        // v7.39 (read01 round 78) — a column-alias list longer than the item is
5594        // the error PG reports; SPG used to let the extra names through and then
5595        // fail two layers downstream with "column not found: <the extra name>".
5596        let n_out = schema_cols.len() + usize::from(primary.with_ordinality);
5597        if primary.unnest_column_aliases.len() > n_out {
5598            return Err(EngineError::Unsupported(alloc::format!(
5599                "table \"{alias}\" has {n_out} columns available but {} columns specified",
5600                primary.unnest_column_aliases.len()
5601            )));
5602        }
5603        if primary.scalar_fn_item && schema_cols.len() == 1 {
5604            schema_cols[0].scalar_row_source = true;
5605        }
5606        // v7.39 (read01 round 78) — WITH ORDINALITY on a table function that
5607        // rides this channel (regexp_matches): a trailing bigint counter, 1-based.
5608        // The column-alias list, if given, names it like any other column.
5609        let mut rows = rows;
5610        if primary.with_ordinality {
5611            schema_cols.push(ColumnSchema::new(
5612                "ordinality".to_string(),
5613                DataType::BigInt,
5614                false,
5615            ));
5616            rows = rows
5617                .into_iter()
5618                .enumerate()
5619                .map(|(i, r)| {
5620                    let mut v = r.values;
5621                    #[allow(clippy::cast_possible_wrap)]
5622                    v.push(Value::BigInt(i as i64 + 1));
5623                    Row::new(v)
5624                })
5625                .collect();
5626        }
5627        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5628            if let Some(col) = schema_cols.get_mut(i) {
5629                col.name = new_name.clone();
5630            }
5631        }
5632        self.exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
5633    }
5634
5635    /// v7.39 (read01 partitionfuncs.c) — shared synthetic-source SELECT
5636    /// pipeline (WHERE / aggregate / projection / ORDER BY / DISTINCT /
5637    /// OFFSET / LIMIT) over a pre-materialised row set. Drives the
5638    /// derived-table executor and the FROM-position table functions.
5639    fn exec_select_over_rows(
5640        &self,
5641        stmt: &SelectStatement,
5642        rows: alloc::vec::Vec<Row<'static>>,
5643        schema_cols: alloc::vec::Vec<ColumnSchema>,
5644        alias: &str,
5645        cancel: CancelToken<'_>,
5646    ) -> Result<QueryResult, EngineError> {
5647        let scan_ctx = self.ev_ctx(&schema_cols, Some(alias));
5648        // v7.37 D.21 — correlated subqueries in the WHERE / projection may
5649        // reference this derived table's columns (`… WHERE u.gg = t.g` where t
5650        // is `(VALUES …) t`). Resolve them per-row via eval_expr_with_correlated
5651        // (the same path the aggregate branch uses); the old plain eval_expr let
5652        // a ScalarSubquery reach row-eval unresolved ("engine resolver bug").
5653        let corr_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5654        // WHERE.
5655        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5656            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5657            for row in rows {
5658                cancel.check()?;
5659                let v = self.eval_expr_with_correlated(
5660                    w,
5661                    &row,
5662                    &scan_ctx,
5663                    cancel,
5664                    Some(&mut corr_memo.borrow_mut()),
5665                )?;
5666                if matches!(v, Value::Bool(true)) {
5667                    out.push(row);
5668                }
5669            }
5670            out
5671        } else {
5672            rows
5673        };
5674        // Aggregate dispatch.
5675        if aggregate::uses_aggregate(stmt) {
5676            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5677            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5678                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5679                    .map_err(|err| match err {
5680                        EngineError::Eval(ev) => ev,
5681                        other => eval::EvalError::TypeMismatch {
5682                            detail: alloc::format!("{other}"),
5683                        },
5684                    })
5685            };
5686            // v7.39 (round 656) — hand the rows over as they are rather than
5687            // collecting a second vector of `RowRef` wrappers. Note this is
5688            // a set-returning-function path, NOT the relational scan: the
5689            // measured O(rows) cost lived in `run_single_table_aggregate`,
5690            // and converting these four first was a miss that cost a full
5691            // round — every test stayed green and the number did not move.
5692            let agg = aggregate::run(
5693                stmt,
5694                crate::join::AggRows::Owned(&filtered),
5695                &schema_cols,
5696                Some(alias),
5697                Some(&agg_correlated),
5698                self.parallel_runner.0.as_deref(),
5699                Some(self.active_catalog()),
5700                Some(self),
5701            )?;
5702            return self.finish_agg_result(agg, stmt, cancel);
5703        }
5704        // Projection.
5705        let projection =
5706            build_projection(&stmt.items, &schema_cols, alias, self.backslash_escapes)?;
5707        // v7.39 (round 621) — a target-list SRF expands here too. This tail
5708        // serves VALUES, a derived table and `ROWS FROM (…)`, and knew nothing
5709        // about them: `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4)) v(x)`
5710        // answered `function unnest(integer[]) does not exist` for a query PG
5711        // answers.
5712        let srf_idxs = self.srf_target_idxs(&projection);
5713        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5714        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5715            alloc::vec::Vec::with_capacity(filtered.len());
5716        if !srf_idxs.is_empty() {
5717            let (rows, src) =
5718                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
5719            projected_rows = rows;
5720            src_of_row = src;
5721        } else {
5722            for row in &filtered {
5723                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5724                for p in &projection {
5725                    let v = self.eval_expr_with_correlated(
5726                        &p.expr,
5727                        row,
5728                        &scan_ctx,
5729                        cancel,
5730                        Some(&mut corr_memo.borrow_mut()),
5731                    )?;
5732                    vals.push(v);
5733                }
5734                projected_rows.push(Row::new(vals));
5735            }
5736        }
5737        let columns: alloc::vec::Vec<ColumnSchema> = projection
5738            .iter()
5739            // v7.39 (read01 round 54) — keep the column's enum identity through
5740            // the projection (it lives outside the DataType lattice), or a
5741            // derived table / UNION / windowed result forgets it and any outer
5742            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5743            .map(|p| {
5744                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
5745                c.user_enum_type = p.user_enum_type.clone();
5746                c.mysql_fsp = p.mysql_fsp;
5747                c
5748            })
5749            .collect();
5750        // ORDER BY over the source rows (same shape as the other
5751        // synthetic-table executors).
5752        // v7.39 (read01 round 80) — a positional key (`ORDER BY 1`) means the Nth
5753        // OUTPUT column. Evaluated as an expression, as it was here, the literal
5754        // `1` is just the constant 1: the same sort key for every row, so the
5755        // sort ran and changed nothing. `SELECT unnest(ARRAY['B','a','A','b'])
5756        // ORDER BY 1` (which the parser turns into `SELECT * FROM unnest(…)`,
5757        // landing on this executor) came back in input order.
5758        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
5759        if !order_by.is_empty() {
5760            // v7.39 (round 621) — one entry per OUTPUT row, since a target-list
5761            // SRF makes more of them than there were inputs.
5762            let out_cols = if srf_idxs.is_empty() {
5763                alloc::vec![None; order_by.len()]
5764            } else {
5765                srf_order_output_cols(&order_by, &projection)
5766            };
5767            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
5768                .iter()
5769                .enumerate()
5770                .map(|(k, out)| -> Result<_, EngineError> {
5771                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
5772                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
5773                        .iter()
5774                        .zip(out_cols.iter())
5775                        .map(|(ob, oc)| {
5776                            // v7.39 (read01 round 54) — this path builds its
5777                            // sort keys itself instead of going through
5778                            // `build_order_keys`, so it skipped the enum-ordinal
5779                            // substitution: an OUTER `ORDER BY <enum col>` over
5780                            // a DERIVED TABLE sorted by the label TEXT, not by
5781                            // member order. Silently wrong rows, not an error.
5782                            let v = srf_order_key(ob, *oc, out, r, &scan_ctx)?;
5783                            Ok(
5784                                match crate::orderby::enum_order_ordinal(&ob.expr, &v, &scan_ctx) {
5785                                    Some(ord) => Value::Float(ord),
5786                                    None => v,
5787                                },
5788                            )
5789                        })
5790                        .collect();
5791                    Ok((k, keys?))
5792                })
5793                .collect::<Result<_, _>>()?;
5794            indexed.sort_by(|a, b| {
5795                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5796                    let o = &stmt.order_by[idx];
5797                    let cmp = order_by_value_cmp_in(
5798                        o.desc,
5799                        o.nulls_first,
5800                        ka,
5801                        kb,
5802                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5803                    );
5804                    if cmp != core::cmp::Ordering::Equal {
5805                        return cmp;
5806                    }
5807                }
5808                core::cmp::Ordering::Equal
5809            });
5810            projected_rows = indexed
5811                .into_iter()
5812                .map(|(i, _)| projected_rows[i].clone())
5813                .collect();
5814        }
5815        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5816        if stmt.distinct {
5817            projected_rows = dedup_rows(projected_rows, scan_ctx.mysql_dialect);
5818        }
5819        if let Some(offset) = stmt.offset_literal() {
5820            let off = (offset as usize).min(projected_rows.len());
5821            projected_rows.drain(..off);
5822        }
5823        if let Some(limit) = stmt.limit_literal() {
5824            projected_rows.truncate(limit as usize);
5825        }
5826        Ok(QueryResult::Rows {
5827            columns,
5828            rows: projected_rows,
5829        })
5830    }
5831
5832    /// Constant `SELECT` with no FROM: evaluate each projection item
5833    /// once against an empty dummy row (`SELECT 1`, `SELECT '7'::INT`).
5834    fn exec_constant_select(&self, stmt: &SelectStatement) -> Result<QueryResult, EngineError> {
5835        let empty_schema: Vec<ColumnSchema> = Vec::new();
5836        let ctx = self.ev_ctx(&empty_schema, None);
5837        // v7.39 (read01 round 106) — an aggregate with no FROM runs over the
5838        // single implicit row (`SELECT count(*)` → 1, `SELECT sum(5)` → 5,
5839        // `SELECT string_agg('x',',')` → x). Before this it fell through to the
5840        // scalar projection, where the aggregate name looked like an unknown
5841        // function. The WHERE filters that one row, so `… WHERE false` leaves
5842        // the aggregate zero input rows (`count(*)` → 0).
5843        if aggregate::uses_aggregate(stmt) {
5844            let dummy = Row::new(Vec::new());
5845            let passes = match &stmt.where_ {
5846                Some(w) => matches!(eval::eval_expr(w, &dummy, &ctx)?, Value::Bool(true)),
5847                None => true,
5848            };
5849            let rows: Vec<RowRef<'_>> = if passes {
5850                alloc::vec![RowRef::Owned(&dummy)]
5851            } else {
5852                Vec::new()
5853            };
5854            let agg = aggregate::run(
5855                stmt,
5856                crate::join::AggRows::Refs(&rows),
5857                &empty_schema,
5858                None,
5859                None,
5860                self.parallel_runner.0.as_deref(),
5861                Some(self.active_catalog()),
5862                Some(self),
5863            )?;
5864            return self.finish_agg_result(agg, stmt, CancelToken::none());
5865        }
5866        let projection = build_projection(&stmt.items, &empty_schema, "", self.backslash_escapes)?;
5867        // `SELECT … WHERE cond` with no FROM — the one conceptual
5868        // row survives only when the condition is true (previously
5869        // the WHERE was silently ignored: `SELECT 1 WHERE false`
5870        // returned a row).
5871        let dummy_row = Row::new(Vec::new());
5872        if let Some(w) = &stmt.where_ {
5873            let cond = eval::eval_expr(w, &dummy_row, &ctx)?;
5874            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
5875                let columns: Vec<ColumnSchema> = projection
5876                    .into_iter()
5877                    .map(|p| {
5878                        let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
5879                        c.user_enum_type = p.user_enum_type;
5880                        c.collation_name = p.collation_name;
5881                        c.mysql_fsp = p.mysql_fsp;
5882                        c
5883                    })
5884                    .collect();
5885                return Ok(QueryResult::Rows {
5886                    columns,
5887                    rows: Vec::new(),
5888                });
5889            }
5890        }
5891        // v7.38 (read01, T15) — a top-level SRF that the parser did NOT rewrite
5892        // into a FROM item (regexp_matches, whose rows are arrays and so cannot
5893        // desugar to unnest) expands here: one output row per SRF row, sibling
5894        // scalar columns repeated. unnest / array_elements / path_query reach a
5895        // real FROM via the parser rewrite and never land here.
5896        // v7.39 (read01 round 67) — every SRF in the list, in lockstep.
5897        let srf_idxs = self.srf_target_idxs(&projection);
5898        if !srf_idxs.is_empty() {
5899            let mut rows = expand_srf_row(self, &projection, &srf_idxs, &dummy_row, &ctx)?;
5900            let columns: Vec<ColumnSchema> = projection
5901                .into_iter()
5902                .map(|p| {
5903                    let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
5904                    c.user_enum_type = p.user_enum_type;
5905                    c.collation_name = p.collation_name;
5906                    c.mysql_fsp = p.mysql_fsp;
5907                    c
5908                })
5909                .collect();
5910            // v7.39 (read01 round 80) — a FROM-less SELECT still has an ORDER BY,
5911            // an OFFSET and a LIMIT, and they apply to the rows the SRF expanded
5912            // to. This returned straight out of the expansion, so
5913            // `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came back in
5914            // input order — the sort was not wrong, it never ran. (There is
5915            // exactly one conceptual input row here, which is why the ordinary
5916            // scan pipeline is not on this path at all.)
5917            if !stmt.order_by.is_empty() {
5918                let synth_ctx =
5919                    EvalContext::new(&columns, None).with_catalog(self.active_catalog());
5920                let resolved: Vec<spg_sql::ast::OrderBy> = stmt
5921                    .order_by
5922                    .iter()
5923                    .map(|o| {
5924                        let mut o = o.clone();
5925                        if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
5926                            && *n >= 1
5927                            && let Ok(idx) = usize::try_from(*n - 1)
5928                            && idx < columns.len()
5929                        {
5930                            o.expr = Expr::Column(spg_sql::ast::ColumnName {
5931                                qualifier: None,
5932                                name: columns[idx].name.clone(),
5933                            });
5934                        }
5935                        o
5936                    })
5937                    .collect();
5938                let descs: Vec<bool> = resolved.iter().map(|o| o.desc).collect();
5939                let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
5940                for r in rows {
5941                    let keys = build_order_keys(&resolved, &r, &synth_ctx)?;
5942                    tagged.push((keys, r));
5943                }
5944                sort_by_keys(&mut tagged, &descs);
5945                rows = tagged.into_iter().map(|(_, r)| r).collect();
5946            }
5947            apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
5948            return Ok(QueryResult::Rows { columns, rows });
5949        }
5950        let mut values = Vec::with_capacity(projection.len());
5951        for p in &projection {
5952            values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
5953        }
5954        let columns: Vec<ColumnSchema> = projection
5955            .into_iter()
5956            .map(|p| {
5957                let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
5958                c.user_enum_type = p.user_enum_type;
5959                c.collation_name = p.collation_name;
5960                c.mysql_fsp = p.mysql_fsp;
5961                c
5962            })
5963            .collect();
5964        // v7.39 (round 239) — the FROM-less scalar path ignored LIMIT and
5965        // OFFSET entirely, so `SELECT 1 LIMIT 0` returned its row where PG
5966        // returns none. (The SRF and aggregate arms above already applied
5967        // them; this tail was the one that didn't.)
5968        let mut rows = alloc::vec![Row::new(values)];
5969        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
5970        Ok(QueryResult::Rows { columns, rows })
5971    }
5972
5973    /// v7.37.x (docker-fair INSUBQ attack) — pre-replacement short-
5974    /// circuit. Catches
5975    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
5976    /// BEFORE `resolve_select_subqueries` materialises the inner result
5977    /// as `Vec<Expr::Literal>`. Runs the inner once, collects the
5978    /// values into a `HashSet<i64>` directly, then probes A.pk per
5979    /// HashSet entry and tallies. Saves the Expr-literal roundtrip
5980    /// (~150 µs / query at INSUBQ benchmark scale).
5981    pub(crate) fn try_count_star_pk_in_subquery_fast(
5982        &self,
5983        stmt: &SelectStatement,
5984        cancel: CancelToken<'_>,
5985    ) -> Result<Option<QueryResult>, EngineError> {
5986        use spg_sql::ast::SelectItem;
5987        if stmt.distinct
5988            || stmt.limit_with_ties
5989            || stmt.group_by.is_some()
5990            || stmt.having.is_some()
5991            || !stmt.unions.is_empty()
5992            || !stmt.order_by.is_empty()
5993            || stmt.limit.is_some()
5994            || stmt.offset.is_some()
5995            || stmt.items.len() != 1
5996        {
5997            return Ok(None);
5998        }
5999        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6000            return Ok(None);
6001        };
6002        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6003            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6004        if !is_count_star {
6005            return Ok(None);
6006        }
6007        let Some(from) = stmt.from.as_ref() else {
6008            return Ok(None);
6009        };
6010        if !from.joins.is_empty()
6011            || from.primary.lateral_subquery.is_some()
6012            || from.primary.unnest_expr.is_some()
6013            || from.primary.generate_series_args.is_some()
6014            || from.primary.table_fn_call.is_some()
6015            || from.primary.as_of_segment.is_some()
6016        {
6017            return Ok(None);
6018        }
6019        let Some(where_expr) = stmt.where_.as_ref() else {
6020            return Ok(None);
6021        };
6022        // The WHERE conjunct must be a bare `<col> IN (subquery)` with
6023        // negated=false; no other predicates.
6024        let Expr::InSubquery {
6025            expr: col_expr,
6026            subquery,
6027            negated: false,
6028        } = where_expr
6029        else {
6030            return Ok(None);
6031        };
6032        let Expr::Column(c) = col_expr.as_ref() else {
6033            return Ok(None);
6034        };
6035        let outer_alias = from
6036            .primary
6037            .alias
6038            .as_deref()
6039            .unwrap_or(from.primary.name.as_str());
6040        if let Some(q) = c.qualifier.as_deref()
6041            && !q.eq_ignore_ascii_case(outer_alias)
6042        {
6043            return Ok(None);
6044        }
6045        // Outer column must be a single-column PK on integer family.
6046        let catalog = self.active_catalog();
6047        let Some(outer_table) = catalog.get(from.primary.name.as_str()) else {
6048            return Ok(None);
6049        };
6050        let outer_schema = outer_table.schema();
6051        let Some(outer_pos) = outer_schema
6052            .columns
6053            .iter()
6054            .position(|s| s.name.eq_ignore_ascii_case(&c.name))
6055        else {
6056            return Ok(None);
6057        };
6058        if !matches!(
6059            outer_schema.columns[outer_pos].ty,
6060            spg_storage::DataType::BigInt
6061                | spg_storage::DataType::Int
6062                | spg_storage::DataType::SmallInt
6063        ) {
6064            return Ok(None);
6065        }
6066        if !outer_schema
6067            .uniqueness_constraints
6068            .iter()
6069            .any(|u| u.is_primary_key && u.columns.as_slice() == [outer_pos])
6070        {
6071            return Ok(None);
6072        }
6073        let Some(idx) = outer_table.index_on(outer_pos) else {
6074            return Ok(None);
6075        };
6076        // Inner must be uncorrelated. The cheap-correlation pre-check
6077        // exists upstream; here we just attempt the bare exec.
6078        if crate::subquery::select_is_correlated(subquery) {
6079            return Ok(None);
6080        }
6081        let mut inner = (**subquery).clone();
6082        self.resolve_select_subqueries(&mut inner, cancel)?;
6083        let r = match self.exec_bare_select_cancel(&inner, cancel) {
6084            Ok(r) => r,
6085            Err(_) => return Ok(None),
6086        };
6087        let QueryResult::Rows { columns, rows, .. } = r else {
6088            return Ok(None);
6089        };
6090        if columns.len() != 1 {
6091            return Ok(None);
6092        }
6093        // v7.37.43 (INSUBQ B-1) — inner-uniqueness check. If the inner
6094        // subquery projects a column known to be UNIQUE/PK on its table
6095        // (statically: `SELECT <col> FROM <tbl> WHERE …` where <col> is
6096        // in `tbl.uniqueness_constraints`), survivor values are
6097        // guaranteed distinct and the per-survivor `HashSet::insert`
6098        // dedup check is redundant. ~25 ns × N_inner-survivors saved.
6099        //
6100        // Inlined check — gated on: no DISTINCT/GROUP/UNION/JOIN, single
6101        // projection that is a bare Column ref, table-column lookup in
6102        // catalog confirms the column appears as a unique constraint's
6103        // sole member. UNIQUE NOT NULL is required — a nullable unique
6104        // column may have multiple NULLs, but NULLs are already skipped
6105        // above (`Value::Null => continue`), so a UNIQUE-only column is
6106        // still safe to dedup-skip.
6107        let inner_unique = (|| -> bool {
6108            if inner.distinct
6109                || inner.group_by.is_some()
6110                || !inner.unions.is_empty()
6111                || inner.having.is_some()
6112                || inner.items.len() != 1
6113            {
6114                return false;
6115            }
6116            let Some(inner_from) = inner.from.as_ref() else {
6117                return false;
6118            };
6119            if !inner_from.joins.is_empty()
6120                || inner_from.primary.lateral_subquery.is_some()
6121                || inner_from.primary.unnest_expr.is_some()
6122                || inner_from.primary.generate_series_args.is_some()
6123                || inner_from.primary.table_fn_call.is_some()
6124            {
6125                return false;
6126            }
6127            let SelectItem::Expr { expr: proj, .. } = &inner.items[0] else {
6128                return false;
6129            };
6130            let Expr::Column(pc) = proj else {
6131                return false;
6132            };
6133            let inner_alias = inner_from
6134                .primary
6135                .alias
6136                .as_deref()
6137                .unwrap_or(inner_from.primary.name.as_str());
6138            if let Some(q) = pc.qualifier.as_deref()
6139                && !q.eq_ignore_ascii_case(inner_alias)
6140            {
6141                return false;
6142            }
6143            let Some(inner_table) = catalog.get(inner_from.primary.name.as_str()) else {
6144                return false;
6145            };
6146            let isch = inner_table.schema();
6147            let Some(ipos) = isch
6148                .columns
6149                .iter()
6150                .position(|s| s.name.eq_ignore_ascii_case(&pc.name))
6151            else {
6152                return false;
6153            };
6154            isch.uniqueness_constraints
6155                .iter()
6156                .any(|u| u.columns.as_slice() == [ipos])
6157        })();
6158        // Collect inner i64 values directly into a HashSet, then probe.
6159        let mut count: i64 = 0;
6160        let mut probed = if inner_unique {
6161            hashbrown::HashSet::<i64>::new()
6162        } else {
6163            hashbrown::HashSet::<i64>::with_capacity(rows.len())
6164        };
6165        for row in &rows {
6166            let v = row.values.first().cloned().unwrap_or(Value::Null);
6167            let n = match v {
6168                Value::BigInt(n) => n,
6169                Value::Int(n) => i64::from(n),
6170                Value::SmallInt(n) => i64::from(n),
6171                Value::Null => continue,
6172                _ => return Ok(None),
6173            };
6174            // De-duplicate inner key set so a duplicate inner value
6175            // doesn't double-count the same outer row. Skipped when
6176            // the inner projection is statically unique.
6177            if !inner_unique && !probed.insert(n) {
6178                continue;
6179            }
6180            // v7.37.43 (INSUBQ B-2 + B-4) — direct i64 PK probe, skipping
6181            // the `IndexKey::from_value` enum-dispatch and the per-call
6182            // `IndexKey` wrapper construction. The outer column is
6183            // already gated to integer-family above, so an i64 key
6184            // always corresponds to a valid PK lookup.
6185            if !idx.lookup_eq_i64(n).is_empty() {
6186                count += 1;
6187            }
6188        }
6189        let columns_out = alloc::vec![ColumnSchema::new(
6190            "count".to_string(),
6191            spg_storage::DataType::BigInt,
6192            false,
6193        )];
6194        let rows_out = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6195        Ok(Some(QueryResult::Rows {
6196            columns: columns_out,
6197            rows: rows_out,
6198        }))
6199    }
6200
6201    /// v7.37.x (docker-fair INSUBQ attack) — short-circuit
6202    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (literal list)
6203    /// (the post-subquery-replacement shape of the INSUBQ probe
6204    /// `SELECT COUNT(*) FROM A WHERE A.pk IN (SELECT k FROM B WHERE …)`).
6205    /// The general aggregate path materialises every seeked row into
6206    /// a `Vec<Cow<Row>>`, then runs the aggregate executor over it.
6207    /// For COUNT(*) we only care how many keys hit; iterate the list
6208    /// and tally `idx.lookup_eq(key)` non-empty results, skipping the
6209    /// row materialisation, the aggregate state machine, and the per-
6210    /// row WHERE re-eval (the seek already filtered by the same list).
6211    /// Returns `None` when the shape doesn't match.
6212    fn try_count_star_pk_in_list_fast(
6213        &self,
6214        stmt: &SelectStatement,
6215        table: &spg_storage::Table,
6216        schema_cols: &[ColumnSchema],
6217        alias: &str,
6218    ) -> Option<QueryResult> {
6219        use spg_sql::ast::{ColumnName, SelectItem};
6220        // Gates on the SELECT shape.
6221        if stmt.distinct
6222            || stmt.limit_with_ties
6223            || stmt.group_by.is_some()
6224            || stmt.having.is_some()
6225            || !stmt.unions.is_empty()
6226            || !stmt.order_by.is_empty()
6227            || stmt.limit.is_some()
6228            || stmt.offset.is_some()
6229            || stmt.items.len() != 1
6230        {
6231            return None;
6232        }
6233        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6234            return None;
6235        };
6236        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6237            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6238        if !is_count_star {
6239            return None;
6240        }
6241        // WHERE must be `<col> IN (literal list)` with no other
6242        // conjuncts (the seek result is a true subset of the row
6243        // population for this predicate).
6244        let where_expr = stmt.where_.as_ref()?;
6245        let Expr::InList {
6246            expr: col_expr,
6247            list,
6248            negated: false,
6249        } = where_expr
6250        else {
6251            return None;
6252        };
6253        let Expr::Column(c) = col_expr.as_ref() else {
6254            return None;
6255        };
6256        if let Some(q) = c.qualifier.as_deref()
6257            && !q.eq_ignore_ascii_case(alias)
6258        {
6259            return None;
6260        }
6261        let col_pos = schema_cols
6262            .iter()
6263            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
6264        // The column must be a single-column PK on an integer family
6265        // — the same gate the SCALARSQ + LEFT-ANTI-JOIN fast paths use,
6266        // so the antiset stays collision-free under `HashSet<i64>`.
6267        let schema = table.schema();
6268        if !matches!(
6269            schema.columns[col_pos].ty,
6270            spg_storage::DataType::BigInt
6271                | spg_storage::DataType::Int
6272                | spg_storage::DataType::SmallInt
6273        ) {
6274            return None;
6275        }
6276        if !schema
6277            .uniqueness_constraints
6278            .iter()
6279            .any(|u| u.is_primary_key && u.columns.as_slice() == [col_pos])
6280        {
6281            return None;
6282        }
6283        let idx = table.index_on(col_pos)?;
6284        // Tally non-empty seek results across all literal values.
6285        let mut count: i64 = 0;
6286        for lit in list {
6287            let Expr::Literal(l) = lit else {
6288                return None;
6289            };
6290            // r1039 — through the shared resolver, so a literal spelled
6291            // in another type ('5' against an integer PK) is read as the
6292            // column's before it becomes a key. This tally answers from
6293            // the index alone, so a key in the wrong space would return a
6294            // COUNT of zero rather than fall back to a scan.
6295            let col = schema.columns.get(col_pos)?;
6296            let v = crate::index_access::literal_as_column_value(l, col, col_pos)?;
6297            let key = spg_storage::IndexKey::from_value_for_column(&v, col.ty)?;
6298            if !idx.lookup_eq(&key).is_empty() {
6299                count += 1;
6300            }
6301        }
6302        let columns = alloc::vec![ColumnSchema::new(
6303            "count".to_string(),
6304            spg_storage::DataType::BigInt,
6305            false,
6306        )];
6307        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6308        let _ = ColumnName {
6309            qualifier: None,
6310            name: String::new(),
6311        };
6312        Some(QueryResult::Rows { columns, rows })
6313    }
6314
6315    /// v7.38 (perf, exact-range count) — `SELECT count(*) FROM t WHERE <col>
6316    /// BETWEEN a AND b` on an indexed column. The index range walk yields
6317    /// exactly the matching (visible) rows, so we count locators directly —
6318    /// skipping the row materialisation, the aggregate state machine, and the
6319    /// per-row WHERE re-eval the general path pays. Turns the `range_count`
6320    /// endpoint from tied-with-PG (superset re-eval) into a clear win. None
6321    /// when the shape doesn't match.
6322    fn try_count_star_indexed_range_fast(
6323        &self,
6324        stmt: &SelectStatement,
6325        table: &spg_storage::Table,
6326        schema_cols: &[ColumnSchema],
6327        alias: &str,
6328        snapshot: &spg_storage::snapshot::Snapshot,
6329    ) -> Option<QueryResult> {
6330        use spg_sql::ast::SelectItem;
6331        if stmt.distinct
6332            || stmt.limit_with_ties
6333            || stmt.group_by.is_some()
6334            || stmt.having.is_some()
6335            || !stmt.unions.is_empty()
6336            || !stmt.order_by.is_empty()
6337            || stmt.limit.is_some()
6338            || stmt.offset.is_some()
6339            || stmt.items.len() != 1
6340        {
6341            return None;
6342        }
6343        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6344            return None;
6345        };
6346        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6347            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6348        if !is_count_star {
6349            return None;
6350        }
6351        let where_expr = stmt.where_.as_ref()?;
6352        let count =
6353            crate::index_access::try_range_count(where_expr, schema_cols, table, alias, snapshot)?;
6354        let columns = alloc::vec![ColumnSchema::new(
6355            "count".to_string(),
6356            spg_storage::DataType::BigInt,
6357            false,
6358        )];
6359        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6360        Some(QueryResult::Rows { columns, rows })
6361    }
6362
6363    /// Single-table aggregate path: filter the (optionally index-seeked)
6364    /// rows, then hand off to the aggregate executor which does its own
6365    /// projection + ORDER BY before `finish_agg_result` applies LIMIT.
6366    fn run_single_table_aggregate<'a>(
6367        &self,
6368        stmt: &SelectStatement,
6369        table: &'a spg_storage::Table,
6370        schema_cols: &'a [ColumnSchema],
6371        alias: &str,
6372        indexed_rows: Option<Vec<Cow<'a, Row<'static>>>>,
6373        cancel: CancelToken<'_>,
6374    ) -> Result<QueryResult, EngineError> {
6375        // v7.38 (read01 U15) — per-scan sampler cell for TABLESAMPLE
6376        // REPEATABLE (see run_single_table_scan). Aggregates
6377        // (`count(*) FROM t TABLESAMPLE …`) filter through this ctx too.
6378        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6379        let ctx = self
6380            .ev_ctx(schema_cols, Some(alias))
6381            .with_sample_rng(&sample_cell);
6382        // v7.39 (round 657) — pre-sized. Pushing 500k pointers into a
6383        // `Vec::new()` walks the doubling chain 8, 16, … 262144, 524288,
6384        // and every abandoned buffer on the way stays resident: RSS is a
6385        // high-water mark, so the intermediates are paid for even though
6386        // they are freed. Round 656 measured the scan at 17 bytes/row
6387        // where the survivor list itself only needs 8.
6388        let mut filtered: Vec<&Row<'static>> = if stmt.where_.is_none() {
6389            Vec::with_capacity(table.rows().len())
6390        } else {
6391            // With a WHERE, the row count is an UPPER bound and reserving it
6392            // is the worse trade: `… WHERE id = 5` over 50M rows would take
6393            // 400 MB of pointers to hold one survivor. Let it grow.
6394            Vec::new()
6395        };
6396        // v6.2.6 — Memoize: per-query LRU cache for correlated
6397        // scalar subqueries. Fresh per row-loop entry so each
6398        // SELECT execution gets an isolated cache.
6399        let mut memo = memoize::MemoizeCache::new();
6400        // v7.37 (perf) — single-table aggregate's WHERE filter
6401        // pre-7.37 ran the slow tree-walker (`eval_expr_with_
6402        // correlated`) per row, even for subquery-free WHEREs that
6403        // the single-table SCAN path has compiled since v7.32
6404        // (perf knife D). The asymmetry meant a fold-to-filter
6405        // rewrite (joinfold) that swapped a JOIN for a single-table
6406        // aggregate over a compiled WHERE saw the tree-walker
6407        // instead — 25 k rows × `m.mailbox_id IN (25 lits)` cost
6408        // ~9 ms via the walker, vs ~1 ms via the compiled InSet
6409        // step. Compile once if eligible; fall back to the walker
6410        // for subquery-bearing or non-compilable WHEREs.
6411        let compiled_where: Option<eval::CompiledExpr> = stmt
6412            .where_
6413            .as_ref()
6414            .filter(|w| eval::fully_compilable(w))
6415            .map(|w| eval::compile_expr(w, &ctx));
6416        let mut eval_stack: Vec<Value<'static>> = Vec::new();
6417        let mut row_passes_where = |row: &Row<'static>,
6418                                    eval_stack: &mut Vec<Value<'static>>,
6419                                    memo: &mut memoize::MemoizeCache|
6420         -> Result<bool, EngineError> {
6421            match (&compiled_where, &stmt.where_) {
6422                (Some(cw), _) => {
6423                    // v7.39 (round 479) — the predicate wants a bool, not a
6424                    // Value. The owned entry ended in `Value::into_owned`
6425                    // and the caller then dropped it, once per row; round
6426                    // 478's profile put that pair above the comparison
6427                    // itself.
6428                    Ok(eval::compiled::eval_compiled_pred(
6429                        cw,
6430                        row,
6431                        &ctx,
6432                        eval_stack,
6433                        ctx.mysql_dialect,
6434                    )
6435                    .map_err(EngineError::Eval)?)
6436                }
6437                (None, Some(w)) => {
6438                    let cond = self.eval_expr_with_correlated(w, row, &ctx, cancel, Some(memo))?;
6439                    Ok(crate::eval::predicate_is_true(
6440                        &cond,
6441                        "WHERE",
6442                        ctx.mysql_dialect,
6443                    )?)
6444                }
6445                (None, None) => Ok(true),
6446            }
6447        };
6448        if let Some(rows) = &indexed_rows {
6449            for cow in rows {
6450                let row = cow.as_ref();
6451                if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6452                    continue;
6453                }
6454                filtered.push(row);
6455            }
6456        }
6457        // v7.36 (cold-tier coverage) — single-table aggregate's
6458        // non-indexed full scan was hot-only and silently lost cold
6459        // rows on COUNT/SUM/etc. Materialise cold rows once into
6460        // `cold_rows_storage` (Vec<Row<'static>>) so the `filtered: Vec<&Row<'static>>`
6461        // shape stays unchanged; the cold rows live until the end of
6462        // the aggregate run.
6463        let cold_rows_storage = if indexed_rows.is_none() {
6464            self.iter_cold_rows_of_table(table)
6465        } else {
6466            Vec::new()
6467        };
6468        if indexed_rows.is_none() {
6469            // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
6470            // single-table aggregate full-scan path. Mirrors the gate on
6471            // `run_single_table_scan`: this is a user-query result path,
6472            // so under gate-on (`SPG_MVCC_INPLACE`) it must skip rows the
6473            // reader's snapshot cannot see (e.g. tombstoned versions),
6474            // otherwise COUNT/SUM/etc. would tally dead rows. A no-op
6475            // under the default gate-off: every hot row is frozen or
6476            // committed-and-alive, so `is_row_visible` returns true.
6477            // Cold-tier rows are frozen (visible) by definition — left
6478            // ungated, matching the plain-scan path.
6479            let scan_snapshot = self.current_snapshot();
6480            // v7.39 (pg_stat knife B) — this full-scan branch walks
6481            // headers directly (serial and sharded alike); count the
6482            // sequential scan here.
6483            table.note_seq_scan();
6484            // v7.39 (parallel-agg P2) — the visibility probe + WHERE
6485            // filter dominate the pre-aggregate wall time on big
6486            // scans (P1's ground truth: accumulation is only ~17%).
6487            // Shard THAT work when the host injected an executor and
6488            // the WHERE is compiled (the compiled evaluator is pure
6489            // over &row; the tree-walker fallback can hit correlated
6490            // subqueries and stays serial). Shards return surviving
6491            // ROW INDICES — &Row can't cross the Box<dyn Any>'s
6492            // 'static bound — and the main thread only dereferences.
6493            let n = table.row_count();
6494            let par = self.parallel_runner.0.as_deref().filter(|_| {
6495                n >= crate::PARALLEL_MIN_ROWS && (stmt.where_.is_none() || compiled_where.is_some())
6496            });
6497            if let Some(r) = par {
6498                let n_shards = (n / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
6499                let chunk = n.div_ceil(n_shards);
6500                type ShardOut = Result<alloc::vec::Vec<usize>, EngineError>;
6501                let cw = &compiled_where;
6502                let snap_ref = &scan_snapshot;
6503                let results = r.run_shards(n_shards, &|s| {
6504                    let lo = s * chunk;
6505                    let hi = ((s + 1) * chunk).min(n);
6506                    let mut keep: alloc::vec::Vec<usize> = alloc::vec::Vec::with_capacity(hi - lo);
6507                    // EvalContext carries Cells (sampler / row counters)
6508                    // and is !Sync — each shard builds its own from the
6509                    // same Sync inputs. The compiled WHERE is gated to
6510                    // the pure-scalar whitelist, which reads none of the
6511                    // session state the engine-built ctx would add
6512                    // (TABLESAMPLE's __tsm_fract is not whitelisted, so
6513                    // sampled scans never take this branch).
6514                    let shard_ctx = EvalContext::new(schema_cols, Some(alias));
6515                    let mut stack: Vec<Value<'static>> = Vec::new();
6516                    let out: ShardOut = (|| {
6517                        for i in lo..hi {
6518                            if !table.is_row_visible(i, snap_ref) {
6519                                continue;
6520                            }
6521                            let row = &table.rows()[i];
6522                            // v7.39 (round 480) — the parallel full-scan
6523                            // shard is the path the aggregate benchmark
6524                            // actually takes, and it was still on the OWNED
6525                            // entry: round 480's profile attributed 68.7 %
6526                            // of `drop_glue<Value>` to this closure, which
6527                            // is why round 479's fix to the indexed path
6528                            // barely moved the total.
6529                            //
6530                            // The `matches!(…, Value::Bool(true))` form was
6531                            // also a narrower reading than the rest of the
6532                            // engine uses — `predicate_is_true` is what
6533                            // handles NULL and MySQL truthiness — so the
6534                            // bool entry fixes the shape as well as the cost.
6535                            let pass = match cw {
6536                                Some(c) => eval::compiled::eval_compiled_pred(
6537                                    c,
6538                                    row,
6539                                    &shard_ctx,
6540                                    &mut stack,
6541                                    shard_ctx.mysql_dialect,
6542                                )
6543                                .map_err(EngineError::Eval)?,
6544                                None => true,
6545                            };
6546                            if pass {
6547                                keep.push(i);
6548                            }
6549                        }
6550                        Ok(keep)
6551                    })();
6552                    alloc::boxed::Box::new(out)
6553                });
6554                // v7.39 (round 567) — `rows()` is a 32-way trie, so
6555                // indexing it is four dependent loads and a scan that
6556                // reads every row paid them every row. A profile of
6557                // `SELECT sum(id)` over 500k rows put 37.8% of the
6558                // connection thread's CPU on THIS ONE LINE. The cursor
6559                // holds the leaf, making that one descent per 32.
6560                let mut rows_cur = table.rows().run_cursor();
6561                for boxed in results {
6562                    let shard = boxed
6563                        .downcast::<ShardOut>()
6564                        .expect("runner echoes the closure's box");
6565                    for i in (*shard)? {
6566                        if let Some(row) = rows_cur.get(i) {
6567                            filtered.push(row);
6568                        }
6569                    }
6570                }
6571            } else {
6572                let mut rows_cur = table.rows().run_cursor();
6573                for i in 0..n {
6574                    if !table.is_row_visible(i, &scan_snapshot) {
6575                        continue;
6576                    }
6577                    let Some(row) = rows_cur.get(i) else { continue };
6578                    if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6579                        continue;
6580                    }
6581                    filtered.push(row);
6582                }
6583            }
6584            for row in &cold_rows_storage {
6585                if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6586                    continue;
6587                }
6588                filtered.push(row);
6589            }
6590        }
6591        // v7.29 — a per-query memo so correlated scalar
6592        // subqueries batch-evaluate once (group map) instead of
6593        // executing per group.
6594        let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
6595        let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
6596            self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
6597                .map_err(|err| match err {
6598                    EngineError::Eval(ev) => ev,
6599                    other => eval::EvalError::TypeMismatch {
6600                        detail: alloc::format!("{other}"),
6601                    },
6602                })
6603        };
6604        // v7.39 (round 656) — the plain relational scan. This collect() was
6605        // the measured defect: one 64-byte `RowRef` per surviving row to
6606        // wrap an 8-byte pointer `filtered` already holds. Scalar
6607        // aggregates measured ~81 bytes/row of working memory because of
6608        // it — 40 MB at 500k rows, 3.2 GB at 50M, for a query that returns
6609        // one number. `AggRows::Ptrs` reads the pointers directly.
6610        let agg = aggregate::run(
6611            stmt,
6612            crate::join::AggRows::Ptrs(&filtered),
6613            schema_cols,
6614            Some(alias),
6615            Some(&agg_correlated),
6616            self.parallel_runner.0.as_deref(),
6617            Some(self.active_catalog()),
6618            Some(self),
6619        )?;
6620        self.finish_agg_result(agg, stmt, cancel)
6621    }
6622
6623    /// Single-table scan + projection path: WHERE filter (compiled when
6624    /// subquery-free), ORDER BY keying, SRF expansion / projection, then
6625    /// sort + WITH TIES / DISTINCT / OFFSET-LIMIT.
6626    fn run_single_table_scan<'a>(
6627        &self,
6628        stmt: &SelectStatement,
6629        table: &'a spg_storage::Table,
6630        schema_cols: &'a [ColumnSchema],
6631        alias: &str,
6632        indexed_rows: Option<Vec<Cow<'a, Row<'static>>>>,
6633        cancel: CancelToken<'_>,
6634    ) -> Result<QueryResult, EngineError> {
6635        // v7.38 (read01 U15) — a fresh per-scan sampler cell for
6636        // `TABLESAMPLE … REPEATABLE(seed)`. Created before the ctx so the
6637        // deterministic `__tsm_fract(seed)` draws share one scan-local
6638        // state (isolated from the global random() PRNG); a fresh cell per
6639        // scan makes a repeat / rescan reproduce the same sample. Unused
6640        // and cheap when the query carries no sample.
6641        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6642        let ctx = self
6643            .ev_ctx(schema_cols, Some(alias))
6644            .with_sample_rng(&sample_cell);
6645        let projection = build_projection(&stmt.items, schema_cols, alias, self.backslash_escapes)?;
6646        // v7.19 P5 — single-table SELECT path for SRF
6647        // `SELECT unnest(arr) FROM t` shape. Detect a top-level
6648        // unnest in the projection list. When present, the
6649        // per-row processor emits one output row per array
6650        // element (broadcasting non-SRF projections from the
6651        // same input row). Empty / NULL arrays emit zero rows
6652        // for that input — PG semantics.
6653        // v7.39 (read01 round 67) — every SRF in the target list, in lockstep.
6654        let srf_idxs = self.srf_target_idxs(&projection);
6655        let srf_position = srf_idxs.first().copied();
6656        // v7.39 (round 599) — the SRF analysis is per QUERY, not per row.
6657        let mut srf_plan = if srf_position.is_some() {
6658            Some(build_srf_plan(self, &projection, &srf_idxs, &ctx)?)
6659        } else {
6660            None
6661        };
6662
6663        // Materialise the filter pass into `(order_key, projected_row)`
6664        // tuples. The order key is `None` when there's no ORDER BY clause.
6665        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
6666        // v7.33 (C1, ceiling-first/never-die) — charge each accumulated
6667        // output row to the per-query byte budget as it is built, so a
6668        // fat single-table scan / sort REJECTS with QueryBytesExceeded
6669        // at ~the ceiling instead of materialising the whole table and
6670        // only noticing at the final enforce_row_limit check. Without
6671        // this, N concurrent fat scans peak at N×table and OOM the host.
6672        // `max_query_bytes = None` (the embedded default) = no ceiling,
6673        // so existing unbudgeted behaviour is byte-identical.
6674        let mut budget = ByteBudget::new(self.max_query_bytes);
6675        // v6.2.6 — Memoize per-row WHERE eval shares one cache.
6676        let mut memo = memoize::MemoizeCache::new();
6677        // v7.32 (perf knife D) — subquery-free WHERE compiles once;
6678        // the row loop then runs a flat step program instead of a
6679        // tree interpretation per row.
6680        let compiled_where: Option<eval::CompiledExpr> = stmt
6681            .where_
6682            .as_ref()
6683            .filter(|w| eval::fully_compilable(w))
6684            .map(|w| eval::compile_expr(w, &ctx));
6685        let mut eval_stack: Vec<Value<'static>> = Vec::new();
6686        // v7.37.x (docker-fair SCALARSQ attack) — pre-analyse every
6687        // SELECT-item scalar subquery for the PK-probe fast path. The
6688        // analysis (gate checks + catalog lookups) takes ~500 ns; doing
6689        // it once per query instead of once per row × 100 rows saves
6690        // ~50 µs and lets the per-row evaluation reduce to a single
6691        // index probe + outer-column read.
6692        let scalarsq_fast: Vec<Option<crate::ScalarPkProbeFastPath>> = projection
6693            .iter()
6694            .map(|p| {
6695                if let Expr::ScalarSubquery(inner) = &p.expr {
6696                    self.analyse_scalar_count_pk_eq_probe(inner, schema_cols, alias)
6697                } else {
6698                    None
6699                }
6700            })
6701            .collect();
6702        let any_scalarsq_fast = scalarsq_fast.iter().any(Option::is_some);
6703        // v7.39 (round 487) — a projection item that is a bare column
6704        // reference binds its position ONCE per query.
6705        //
6706        // Per row it used to walk `eval_expr_with_correlated` (a memo
6707        // lookup for "does this have a subquery", then an un-memoised
6708        // `expr_may_use_in_set` tree walk), then `eval_expr`'s dispatch,
6709        // then `resolve_column`, which finds the column by scanning the
6710        // schema and comparing NAMES. On `SELECT g FROM h` that chain was
6711        // 19 % of self time for what is ultimately one cell read.
6712        //
6713        // `compile_column_pos` is the Step VM's resolver, already
6714        // `pub(crate)` and already reused by the aggregate's bind-once
6715        // path: it mirrors `resolve_column`'s happy layers and returns
6716        // None for anything that would reach an error, an ambiguity, or a
6717        // miss, so those still go the interpreter's way and keep its
6718        // exact message. A composite column is excluded for the same
6719        // reason `compile_into` excludes it — it must be rehydrated from
6720        // stored JSON, which is not a cell read.
6721        let proj_direct = bind_direct_columns(&projection, &ctx);
6722        let any_proj_direct = proj_direct.iter().any(Option::is_some);
6723        // v7.39 (round 605) — a projection item that cannot depend on the row
6724        // is evaluated once. `SELECT ('{"a":1}')::JSONB FROM j` cost TEN
6725        // allocations a row against one for a plain column, `'abc' || 'def'`
6726        // six and `upper('abc')` five, all of them producing the same value
6727        // 50,000 times. An item that fails to evaluate is left alone, so its
6728        // error still comes from the row loop in the interpreter's wording.
6729        let proj_const: Vec<Option<Value<'static>>> = projection
6730            .iter()
6731            .map(|p| crate::eval::compiled::constant_projection_value(&p.expr, &ctx))
6732            .collect();
6733        let any_proj_const = proj_const.iter().any(Option::is_some);
6734        crate::bump_counter!(crate::select::SCAN_PATH_ENTERED);
6735        // v7.39 (read01 round 80) — positional ORDER BY over a WILDCARD
6736        // projection. Statement prep (`resolve_order_by_position`) can only map
6737        // `ORDER BY 1` onto the first SELECT item when that item is an
6738        // expression; a `*` is not one, so the literal survived to here and was
6739        // evaluated as the CONSTANT 1 — the same key for every row, i.e. no sort
6740        // at all. The parser rewrites `SELECT unnest(a) x` into
6741        // `SELECT * FROM unnest(a) x`, so that innocuous-looking shape landed
6742        // exactly here: `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came
6743        // back in input order. The projection is built by now, so the Nth output
6744        // column is known — resolve against it.
6745        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
6746        // v7.39 (round 600) — the ORDER BY of an SRF query is decided on the
6747        // EXPANDED rows, so a key naming a select-list item reads that item.
6748        let srf_order_cols: Vec<Option<usize>> = if srf_position.is_some() {
6749            srf_order_output_cols(&order_by, &projection)
6750        } else {
6751            Vec::new()
6752        };
6753        let srf_key_bound: Vec<Option<usize>> = (0..order_by.len()).map(Some).collect();
6754        // v7.37.x (docker-fair SCALARSQ attack) — early-limit gate for
6755        // the no-ORDER-BY-no-DISTINCT-no-TIES-no-SRF-no-WHERE shape.
6756        // Hoisted above the closure so the projection-eval path can
6757        // gate `memo` passing on it: the SELECT-item correlated-scalar
6758        // batch path scans the FULL inner table once (~5 ms for 12.5 k
6759        // rows) and is only a win when N outer rows is large; for small
6760        // LIMITed shapes a per-row PK seek (~5 µs × 100 = 500 µs) wins.
6761        let early_cap: Option<usize> = if order_by.is_empty()
6762            && !stmt.distinct
6763            && !stmt.limit_with_ties
6764            && srf_position.is_none()
6765            && stmt.where_.is_none()
6766        {
6767            stmt.limit_literal()
6768                .map(|n| n.saturating_add(stmt.offset_literal().unwrap_or(0)) as usize)
6769        } else {
6770            None
6771        };
6772        // v7.38 (read01 B8) — streaming top-N budget. For `ORDER BY …
6773        // LIMIT k` (no DISTINCT / WITH TIES / SRF, and not forced to
6774        // full-sort by the test gate) keep only the running top-`keep`
6775        // rows in memory instead of materialising every projected row,
6776        // so a `… ORDER BY col LIMIT 10` over a huge table is O(keep)
6777        // space, not O(rows). `None` = accumulate everything (the prior
6778        // behaviour). The final `partial_sort_tagged(keep)` below still
6779        // runs and produces the identical rows.
6780        // v7.39 (round 683) — the declared collation for each ORDER BY
6781        // position, resolved once and carried beside `descs` for the same
6782        // reason `descs` is carried: it is per key position, not per row.
6783        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
6784        let topk_stream: Option<(usize, Vec<bool>)> = if !order_by.is_empty()
6785            && !stmt.distinct
6786            && !stmt.limit_with_ties
6787            && srf_position.is_none()
6788            && !self.env_cfg().disable_topk
6789        {
6790            stmt.limit_literal().and_then(|l| {
6791                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
6792                (keep >= 1).then(|| (keep, order_by.iter().map(|o| o.desc).collect()))
6793            })
6794        } else {
6795            None
6796        };
6797        // v7.37.16 — streaming DISTINCT seen-set: norm-hash → indices of
6798        // kept rows in `tagged`. Probing on the PROJECTED row as soon as
6799        // it is built means a duplicate costs neither a build_order_keys
6800        // eval (the dominant per-row cost of `DISTINCT … ORDER BY`) nor
6801        // a tagged slot, and the sort below runs over u survivors, not
6802        // n input rows — PG's hash-distinct-then-sort plan shape.
6803        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
6804            hashbrown::HashMap::new();
6805        let distinct_hb = hashbrown::DefaultHashBuilder::default();
6806        // v7.39 (round 485) — one projection buffer for the whole scan
6807        // rather than a fresh `Vec` per input row. A row that survives
6808        // the DISTINCT probe takes the buffer with it (`mem::take`) and
6809        // the next row allocates a new one; a row that duplicates an
6810        // earlier one leaves the buffer — and its capacity — in place.
6811        // The round-485 counter says 49 900 of `distinct_proj`'s 50 000
6812        // projected rows are duplicates, so that is 49 900 allocate /
6813        // free pairs the scan no longer performs. Shapes where every row
6814        // survives (plain projection, `DISTINCT` over a unique column)
6815        // allocate exactly as often as before.
6816        let mut proj_buf: Vec<Value<'static>> = Vec::new();
6817        // v7.39 (round 571) — buffers handed back by the top-N trim.
6818        // Round 485 made the scan share ONE projection buffer, but a
6819        // surviving row takes it (`mem::take`) and without DISTINCT
6820        // almost every row survives, so the next one starts from zero
6821        // capacity and allocates. The trim drops `keep` rows at a time
6822        // and their buffers come back here instead of being freed.
6823        let mut proj_pool: Vec<Vec<Value<'static>>> = Vec::new();
6824        let mut key_pool: Vec<Vec<crate::orderby::OrderKey>> = Vec::new();
6825        // v7.39 (round 581) — the worst row the accumulator is currently
6826        // keeping. Anything that loses to it cannot reach the answer, so
6827        // it is dropped before its projection is ever built.
6828        let mut topk_boundary: Option<Vec<crate::orderby::OrderKey>> = None;
6829        // v7.39 (round 582) — resolve each ORDER BY column once, not
6830        // once per row. See `order_by_bound_positions`.
6831        let order_bound =
6832            crate::orderby::order_by_bound_positions(&order_by, schema_cols, Some(alias));
6833        // v7.39 (round 581) — and it stops asking when the answer is
6834        // always "keep".
6835        //
6836        // The check earns its place only on rows it rejects. Over
6837        // ascending ids, `ORDER BY id DESC` never rejects one — every
6838        // row beats the current worst — so the comparison is pure
6839        // overhead there, measured at +5.5% in three batches out of
6840        // three. After a window of rows it looks at what it has
6841        // actually rejected and switches itself off if the shape is not
6842        // paying. The answers do not depend on it either way.
6843        const BOUNDARY_WINDOW: u32 = 8192;
6844        let mut boundary_checks: u32 = 0;
6845        let mut boundary_rejects: u32 = 0;
6846        let mut boundary_check_on = true;
6847        // Inline the per-row work in a closure so the indexed and full-
6848        // scan branches share the body.
6849        let mut process_row = |row: &Row<'static>, loop_idx: usize| -> Result<(), EngineError> {
6850            if loop_idx.is_multiple_of(256) {
6851                cancel.check()?;
6852            }
6853            if let Some(cw) = &compiled_where {
6854                let cond = eval::eval_compiled(cw, row, &ctx, &mut eval_stack)
6855                    .map_err(EngineError::Eval)?;
6856                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6857                    return Ok(());
6858                }
6859            } else if let Some(where_expr) = &stmt.where_ {
6860                let cond =
6861                    self.eval_expr_with_correlated(where_expr, row, &ctx, cancel, Some(&mut memo))?;
6862                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6863                    return Ok(());
6864                }
6865            }
6866            // Under DISTINCT the keys are built AFTER the dup probe
6867            // (survivors only); the non-distinct order is unchanged.
6868            // v7.39 (round 600) — an SRF query's keys are built per EXPANDED
6869            // row further down, and building them here would evaluate the
6870            // ORDER BY against the INPUT row: a key naming the SRF's own
6871            // output became a scalar call to it, which is where
6872            // "function unnest(integer[]) does not exist" came from.
6873            let order_keys = if order_by.is_empty() || stmt.distinct || srf_position.is_some() {
6874                Vec::new()
6875            } else {
6876                let mut buf = key_pool.pop().unwrap_or_default();
6877                crate::orderby::build_order_keys_bound(
6878                    &order_by,
6879                    &order_bound,
6880                    row,
6881                    &ctx,
6882                    &mut buf,
6883                )?;
6884                // v7.39 (round 581) — reject before projecting.
6885                //
6886                // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
6887                // 50 distinct `g` decides nearly every row on the FIRST
6888                // key, and PG answers it FASTER than the single-key form
6889                // (7.4 ms against 10.4) because a rejected row costs it
6890                // one comparison. SPG built both keys AND the projected
6891                // row for all 500k before throwing them away. The keys
6892                // are needed to compare; the projection is not.
6893                if boundary_check_on
6894                    && let Some((_, descs)) = &topk_stream
6895                    && let Some(b) = &topk_boundary
6896                {
6897                    boundary_checks += 1;
6898                    let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
6899                        == core::cmp::Ordering::Greater;
6900                    if loses {
6901                        boundary_rejects += 1;
6902                    }
6903                    if boundary_checks == BOUNDARY_WINDOW {
6904                        // Keep asking only if it has been rejecting at
6905                        // least a quarter of what it saw.
6906                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
6907                    }
6908                    if loses {
6909                        buf.clear();
6910                        key_pool.push(buf);
6911                        return Ok(());
6912                    }
6913                }
6914                buf
6915            };
6916            if srf_position.is_some() {
6917                let plan = srf_plan.as_mut().expect("srf_position implies a plan");
6918                for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
6919                    if stmt.distinct {
6920                        let bucket = seen_distinct
6921                            .entry(norm_hash_row(&out, &distinct_hb, ctx.mysql_dialect))
6922                            .or_default();
6923                        if bucket
6924                            .iter()
6925                            .any(|i| row_eq_norm(&tagged[i].1, &out, ctx.mysql_dialect))
6926                        {
6927                            continue;
6928                        }
6929                        bucket.push(tagged.len());
6930                    }
6931                    budget.charge(approx_row_bytes(&out))?;
6932                    // The keys come from THIS expanded row: a key naming a
6933                    // select-list item reads its value, anything else is
6934                    // still evaluated against the input row.
6935                    let keys = if order_by.is_empty() {
6936                        Vec::new()
6937                    } else {
6938                        let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
6939                        for (k, ob) in order_by.iter().enumerate() {
6940                            kv.push(match srf_order_cols.get(k).copied().flatten() {
6941                                Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
6942                                None => eval::eval_expr(&ob.expr, row, &ctx)
6943                                    .map_err(EngineError::Eval)?,
6944                            });
6945                        }
6946                        // Packed by the same code every other ORDER BY uses,
6947                        // so DESC / NULLS FIRST / the MySQL rule are not
6948                        // restated here.
6949                        let key_row = Row::new(kv);
6950                        let mut buf = Vec::new();
6951                        crate::orderby::build_order_keys_bound(
6952                            &order_by,
6953                            &srf_key_bound,
6954                            &key_row,
6955                            &ctx,
6956                            &mut buf,
6957                        )?;
6958                        buf
6959                    };
6960                    tagged.push((keys, out));
6961                }
6962            } else {
6963                let values = &mut proj_buf;
6964                values.clear();
6965                values.reserve(projection.len());
6966                for (i, p) in projection.iter().enumerate() {
6967                    // v7.37.x (docker-fair SCALARSQ attack) — pre-
6968                    // analysed PK-probe fast path. The per-row work is
6969                    // a read of outer.col from the row plus an index
6970                    // probe — no Expr clone, no walker, no
6971                    // `eval_expr_with_correlated` framework.
6972                    if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
6973                        values.push(self.probe_with_pk_fast_path(fp, row));
6974                        continue;
6975                    }
6976                    // v7.39 (round 605) — the same value every row.
6977                    if any_proj_const && let Some(v) = &proj_const[i] {
6978                        values.push(v.clone());
6979                        continue;
6980                    }
6981                    // v7.39 (round 487) — bound column: read the cell.
6982                    // This is `rehydrate_cell`'s body for a non-composite
6983                    // column, which is what the whole chain below reduces
6984                    // to once the name has been resolved.
6985                    if any_proj_direct && let Some(pos) = proj_direct[i] {
6986                        crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
6987                        values.push(row.values[pos].clone().into_owned());
6988                        continue;
6989                    }
6990                    // v7.24 (round-16 B) — correlated-aware.
6991                    // v7.37.x (docker-fair SCALARSQ attack) — share the
6992                    // per-row memo with projection. Required for the
6993                    // batch-evaluated correlated-scalar path to fire on
6994                    // SELECT-item scalar subqueries; otherwise each row
6995                    // re-executes the inner.
6996                    //
6997                    // Skip the memo when the outer row count is small
6998                    // (early-limited): the batch path scans the FULL
6999                    // inner table to build a GroupMap (~5 ms for a
7000                    // 12.5 k-row inner), while per-row execution with a
7001                    // PK index seek is ~5 µs per call — much cheaper for
7002                    // N ≤ ~1000 outer rows.
7003                    let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
7004                    let memo_arg = if pass_memo { Some(&mut memo) } else { None };
7005                    values.push(
7006                        self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7007                    );
7008                }
7009                crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7010                if stmt.distinct {
7011                    let bucket = seen_distinct
7012                        .entry(norm_hash_values(&proj_buf, &distinct_hb, ctx.mysql_dialect))
7013                        .or_default();
7014                    if bucket
7015                        .iter()
7016                        .any(|i| values_eq_norm(&tagged[i].1.values, &proj_buf, ctx.mysql_dialect))
7017                    {
7018                        crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7019                        return Ok(());
7020                    }
7021                    bucket.push(tagged.len());
7022                }
7023                let out = Row::new(core::mem::replace(
7024                    &mut proj_buf,
7025                    proj_pool.pop().unwrap_or_default(),
7026                ));
7027                let order_keys = if stmt.distinct && !order_by.is_empty() {
7028                    build_order_keys(&order_by, row, &ctx)?
7029                } else {
7030                    order_keys
7031                };
7032                budget.charge(approx_row_bytes(&out))?;
7033                tagged.push((order_keys, out));
7034            }
7035            // Streaming top-N: bound the accumulator to O(keep) rows.
7036            if let Some((k, descs)) = &topk_stream {
7037                crate::orderby::topk_trim_recycling(
7038                    &mut tagged,
7039                    *k,
7040                    descs,
7041                    &mut proj_pool,
7042                    &mut key_pool,
7043                    &mut topk_boundary,
7044                );
7045            }
7046            Ok(())
7047        };
7048        // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7049        // load-bearing full-scan path. This is the primary single-table
7050        // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7051        // in-place writers retain dead/old versions, an ungated scan
7052        // here would return them, so the gate must land BEFORE the
7053        // writers flip (see the plan's activation-order rule). A no-op
7054        // today: every hot row is frozen or committed-and-alive under
7055        // the reader's snapshot, so `is_row_visible` returns true for
7056        // all of them (verified by the full e2e suite staying green).
7057        let scan_snapshot = self.current_snapshot();
7058        let mut emitted: usize = 0;
7059        if let Some(rows) = &indexed_rows {
7060            for (loop_idx, cow) in rows.iter().enumerate() {
7061                if let Some(cap) = early_cap
7062                    && emitted >= cap
7063                {
7064                    break;
7065                }
7066                process_row(cow.as_ref(), loop_idx)?;
7067                emitted = emitted.saturating_add(1);
7068            }
7069        } else {
7070            // v7.39 (round 570) — the row store is a 32-way trie, so
7071            // indexing it is four dependent loads. Round 567 measured
7072            // -18% on the aggregate scan from holding the leaf between
7073            // rows; this is the same loop for the projecting scan.
7074            let mut rows_cur = table.rows().run_cursor();
7075            for i in 0..table.row_count() {
7076                if let Some(cap) = early_cap
7077                    && emitted >= cap
7078                {
7079                    break;
7080                }
7081                // Skip rows this snapshot cannot see (invisible rows do
7082                // not count toward the LIMIT).
7083                if !table.is_row_visible(i, &scan_snapshot) {
7084                    continue;
7085                }
7086                let Some(row) = rows_cur.get(i) else { continue };
7087                process_row(row, i)?;
7088                emitted = emitted.saturating_add(1);
7089            }
7090            // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7091            // rows into the same loop. The full-scan path here is the
7092            // load-bearing single-table SELECT executor, and pre-
7093            // 7.35.1 it only walked `table.rows()` (hot), so any
7094            // `SELECT … FROM t` against a table with cold segments
7095            // silently returned a subset.
7096            let cold_rows = self.iter_cold_rows_of_table(table);
7097            for (offset, row) in cold_rows.iter().enumerate() {
7098                if let Some(cap) = early_cap
7099                    && emitted >= cap
7100                {
7101                    break;
7102                }
7103                process_row(row, table.row_count() + offset)?;
7104                emitted = emitted.saturating_add(1);
7105            }
7106        }
7107
7108        // (DISTINCT already de-duped STREAMING inside process_row, so the
7109        // sort below only sees the u survivors and the partial-sort
7110        // budget applies to DISTINCT too.)
7111        if !order_by.is_empty() {
7112            // Partial-sort fast path: when LIMIT is small relative to
7113            // the row count, select_nth_unstable + sort just the
7114            // prefix is O(n + k log k) instead of O(n log n).
7115            // WITH TIES needs the full sort so the tie extension can
7116            // scan past `limit` to find rows that share the last-kept
7117            // row's key.
7118            let keep = if stmt.limit_with_ties
7119                // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7120                // forces the full-sort fallback by suppressing the
7121                // partial-sort `keep` budget. See
7122                // `xtests/sigil/test-mode-gucs.md`.
7123                || self.env_cfg().disable_topk
7124            {
7125                None
7126            } else {
7127                stmt.limit_literal()
7128                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7129            };
7130            let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7131            crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &order_colls);
7132        }
7133
7134        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7135        // past the truncated tail through every row that shares the
7136        // last-kept row's ORDER BY key. The tie check uses the
7137        // already-computed `(order_keys, row)` pairs so it matches
7138        // the sort comparator exactly. DISTINCT + WITH TIES falls
7139        // through to the no-ties path (PG also disallows their
7140        // combination; SPG silently drops the tie extension here so
7141        // the customer doesn't see a hard error mid-query — the
7142        // user-visible result is still correct, just narrower).
7143        let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7144            apply_offset_and_limit_tagged(
7145                &mut tagged,
7146                stmt.offset_literal(),
7147                stmt.limit_literal(),
7148                true,
7149            );
7150            tagged.into_iter().map(|(_, r)| r).collect()
7151        } else {
7152            // DISTINCT already de-duped pre-sort above.
7153            let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7154            apply_offset_and_limit(
7155                &mut output_rows,
7156                stmt.offset_literal(),
7157                stmt.limit_literal(),
7158            );
7159            output_rows
7160        };
7161
7162        let columns: Vec<ColumnSchema> = projection
7163            .into_iter()
7164            .map(|p| {
7165                let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
7166                c.user_enum_type = p.user_enum_type;
7167                c.collation_name = p.collation_name;
7168                c.mysql_fsp = p.mysql_fsp;
7169                c
7170            })
7171            .collect();
7172
7173        Ok(QueryResult::Rows {
7174            columns,
7175            rows: output_rows,
7176        })
7177    }
7178
7179    /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
7180    /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
7181    /// select items for the surviving rows only — PG's Result-above-
7182    /// Limit shape, where SubPlan loops equal the OUTPUT row count
7183    /// (50) instead of the group count (24k).
7184    fn finish_agg_result(
7185        &self,
7186        mut agg: aggregate::AggResult,
7187        stmt: &SelectStatement,
7188        cancel: CancelToken<'_>,
7189    ) -> Result<QueryResult, EngineError> {
7190        apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
7191        if !agg.deferred.is_empty() {
7192            apply_offset_and_limit(
7193                &mut agg.synth_rows,
7194                stmt.offset_literal(),
7195                stmt.limit_literal(),
7196            );
7197            let ctx = EvalContext::new(&agg.synth_schema, None);
7198            let mut memo = memoize::MemoizeCache::default();
7199            // v7.32 (architecture v2 P3) — keyed index-probe seeding.
7200            // Deferred subqueries are referenced only by surviving
7201            // select-list rows (≤ LIMIT), so their correlation keys are
7202            // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
7203            // each batchable subquery's group map over just those keys
7204            // via per-key index seek; the per-row splice loop below then
7205            // reuses the seeded map. A join-shaped or un-indexed inner
7206            // falls through to the all-keys batch inside the call (built
7207            // eagerly here instead of lazily on row 0 — same cost), so
7208            // it still pays the full scan, never the 715 ms per-row
7209            // direct eval; its index-nested-loop probe is the next
7210            // knife. Genuinely non-batchable shapes return None and are
7211            // left unseeded for the loop's per-row resolver, as before.
7212            for (_, expr) in &agg.deferred {
7213                let mut subs: Vec<&SelectStatement> = Vec::new();
7214                collect_scalar_subqueries(expr, &mut subs);
7215                for sub in subs {
7216                    let repr = alloc::format!("{sub}");
7217                    if memo.group_maps.contains_key(&repr) {
7218                        continue;
7219                    }
7220                    if let Some(gm) = self.try_batch_correlated_scalar(
7221                        sub,
7222                        Some((&agg.synth_rows, &ctx)),
7223                        cancel,
7224                    )? {
7225                        memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
7226                    }
7227                }
7228            }
7229            for (ri, srow) in agg.synth_rows.iter().enumerate() {
7230                cancel.check()?;
7231                for (col, expr) in &agg.deferred {
7232                    let v =
7233                        self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
7234                    if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
7235                        *cell = v;
7236                    }
7237                }
7238            }
7239        }
7240        Ok(QueryResult::Rows {
7241            columns: agg.columns,
7242            rows: agg.rows,
7243        })
7244    }
7245
7246    /// v7.37 — streaming projection for the joined-non-aggregate
7247    /// shape (multi-table FROM, all projection items bound, no
7248    /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
7249    /// UNION). Walks the deferred join survivors and emits
7250    /// `&[&Value]` borrowed straight out of the source tables — no
7251    /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
7252    /// on the mailrs `PROJ` shape (about 4 ms saved).
7253    ///
7254    /// Returns `Ok(None)` when the shape doesn't qualify; the caller
7255    /// then falls back to the materialising path.
7256    /// v7.37 (round 831) — stream a joinless SELECT straight off the
7257    /// stored table, one row at a time, without ever building a row set.
7258    ///
7259    /// Returns `Ok(None)` for anything this cannot serve, and the caller
7260    /// falls through to the deferred-join path exactly as before: a
7261    /// missing table, or a cold tier whose hydration the fallback handles.
7262    /// Sort a single-table scan through the external sorter, so the
7263    /// answer's size is bounded by `work_mem` and not by the input.
7264    ///
7265    /// Sorting held every row twice — the scan's `Vec<Row>` and the
7266    /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
7267    /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
7268    /// enough ORDER BY took the server down, which is a liveness
7269    /// problem before it is a performance one.
7270    ///
7271    /// A SEPARATE walk rather than a change to `run_single_table_scan`,
7272    /// following what round 831 did for the joinless shape. That
7273    /// function is 552 lines whose projection loop is entangled with
7274    /// DISTINCT (which indexes back into the tagged vector) and with
7275    /// streaming top-N (whose boundary moves as the scan runs); both
7276    /// assume the projection has already happened when a row is
7277    /// pushed, which is exactly what spilling has to defer. Two earlier
7278    /// attempts tried to rework that loop and were reverted. Here the
7279    /// existing path is untouched and this one only claims shapes it
7280    /// can serve, so a decline costs nothing.
7281    ///
7282    /// Records are SOURCE rows, not projected ones: `finish` re-derives
7283    /// keys from what it decodes, and an ORDER BY key need not be in
7284    /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
7285    fn try_spill_sorted_scan(
7286        &self,
7287        stmt: &SelectStatement,
7288        from: &FromClause,
7289        cancel: CancelToken<'_>,
7290    ) -> Result<Option<QueryResult>, EngineError> {
7291        // Shapes this walk does not serve. Each one either needs the
7292        // whole tagged vector addressable (DISTINCT probes back into
7293        // it, WITH TIES re-reads its tail) or is already bounded
7294        // without spilling (a LIMIT makes the partial sort O(keep)).
7295        if !self.can_spill()
7296            || stmt.order_by.is_empty()
7297            || stmt.distinct
7298            || stmt.limit_with_ties
7299            || stmt.limit_literal().is_some()
7300            || !from.joins.is_empty()
7301            || from.primary.lateral_subquery.is_some()
7302            || from.primary.unnest_expr.is_some()
7303            || from.primary.generate_series_args.is_some()
7304            || select_has_window(stmt)
7305        {
7306            return Ok(None);
7307        }
7308        // A parent's rows are its children's. These walks scan the named
7309        // relation alone, so a partitioned or inherited parent comes back
7310        // short — and silently: the corpus caught `SELECT id FROM pr
7311        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
7312        // parent's own rows instead of the partitions'. `ONLY` is exactly
7313        // the case that does not fan out, so it stays, which is the test
7314        // the FROM-clause fan-out itself makes.
7315        if !from.primary.only
7316            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7317        {
7318            return Ok(None);
7319        }
7320        let Some(table) = self.active_catalog().get(&from.primary.name) else {
7321            return Ok(None);
7322        };
7323        // Cold-tier rows live outside `rows()`; this walk would drop
7324        // them silently, the same reason round 831's walk declines.
7325        if table.has_cold_rows_fast() {
7326            return Ok(None);
7327        }
7328
7329        let alias = from
7330            .primary
7331            .alias
7332            .as_deref()
7333            .unwrap_or(from.primary.name.as_str());
7334        let cols = table.schema().columns.clone();
7335        let sess = self.dml_session();
7336        let ctx = EvalContext::new(&cols, Some(alias))
7337            .with_catalog(self.active_catalog())
7338            .with_session(&sess);
7339        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
7340        let order_by = stmt.order_by.clone();
7341        // The same one-shot resolution the general path does (round
7342        // 582): each ORDER BY column is bound once, not once per row.
7343        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
7344        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7345        // Resolved BEFORE the scan, because it now decides what the sort
7346        // STORES and not just what it decodes (round 995).
7347        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
7348
7349        let mut sorter = crate::extsort::ExternalSorter::new(
7350            self.temp_run_factory,
7351            self.session_work_mem_bytes(),
7352            cols.clone(),
7353            &descs,
7354        )
7355        .with_stats(&self.spill_stats)
7356        .with_pruned(&needed);
7357        let snapshot = self.current_snapshot();
7358        // One key buffer for the whole scan: `push` drains it and leaves
7359        // the capacity behind.
7360        let mut keys: Vec<OrderKey> = Vec::new();
7361        // r1024 — compile the predicate once for the scan.
7362        //
7363        // These two sorted-spill scans are the paths a single-table SELECT
7364        // with an ORDER BY takes, and they were the last row-returning ones
7365        // still walking the expression tree per row. r1023 did the
7366        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
7367        // exactly this shape.
7368        //
7369        // Found from the profile's CALL TREE rather than its leaves. The
7370        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
7371        // 261, `mod_op` 178 — and two attempts at reasoning out which
7372        // function asked for it were both wrong. The tree names the caller
7373        // chain, and it named this one.
7374        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
7375            .where_
7376            .as_ref()
7377            .filter(|w| crate::eval::fully_compilable(w))
7378            .map(|w| crate::eval::compile_expr(w, &ctx));
7379        let mut eval_stack: Vec<Value<'static>> = Vec::new();
7380        for (i, row) in table.scan_visible_from(0, &snapshot) {
7381            if i.is_multiple_of(256) {
7382                cancel.check()?;
7383            }
7384            if let Some(c) = &compiled_where {
7385                if !crate::eval::compiled::eval_compiled_pred(
7386                    c,
7387                    row,
7388                    &ctx,
7389                    &mut eval_stack,
7390                    ctx.mysql_dialect,
7391                )? {
7392                    continue;
7393                }
7394            } else if let Some(w) = &stmt.where_ {
7395                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
7396                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7397                    continue;
7398                }
7399            }
7400            keys.clear();
7401            crate::orderby::build_order_keys_bound(&order_by, &order_bound, row, &ctx, &mut keys)?;
7402            sorter.push(&mut keys, row)?;
7403        }
7404
7405        let key_ctx = &ctx;
7406        let rows = sorter.finish(
7407            |src, buf| {
7408                crate::orderby::build_order_keys_bound(&order_by, &order_bound, src, key_ctx, buf)
7409            },
7410            |src| {
7411                let mut values = Vec::with_capacity(projection.len());
7412                for p in &projection {
7413                    values.push(
7414                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
7415                    );
7416                }
7417                Ok(Row::new(values))
7418            },
7419        )?;
7420
7421        let columns: Vec<ColumnSchema> = projection
7422            .iter()
7423            .map(|p| {
7424                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
7425                c.user_enum_type = p.user_enum_type.clone();
7426                c.mysql_fsp = p.mysql_fsp;
7427                c
7428            })
7429            .collect();
7430        Ok(Some(QueryResult::Rows { columns, rows }))
7431    }
7432
7433    /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
7434    /// handing each row to the consumer instead of collecting the answer.
7435    ///
7436    /// That walk bounds the SORT and then returns `QueryResult::Rows`,
7437    /// which holds every output row. Measured at `work_mem = 4 MB` over
7438    /// 200-byte rows, RSS above the server's own baseline while the
7439    /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
7440    /// at 400k — linear — while the spill underneath worked correctly
7441    /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
7442    /// removes each file, so a count taken afterwards reads 0 whatever
7443    /// happened, and an earlier reading of "no spill at all" was that
7444    /// blind witness). The growth is the collected result, not the sort.
7445    ///
7446    /// Emitting makes peak the budget, one buffer per run and a single
7447    /// row — the state a merge already holds at every step. It also
7448    /// frees each projected row as the next is built rather than
7449    /// accumulating them, which is where the time is: a profile of the
7450    /// collecting walk put the allocator at 586 samples, more than every
7451    /// sort comparison combined (420), against 19 for `push` itself.
7452    /// v7.37 (round 923) — which of a sort record's columns the output half
7453    /// reads. The record is the SOURCE row (round 836), so a narrow projection
7454    /// decoded every column: skipping one 200-byte text halves a decode
7455    /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
7456    ///
7457    /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
7458    /// column reads NULL. Answers only when every projection item is a bare
7459    /// column reference AND every ORDER BY key is a bound column; anything
7460    /// else returns empty, decoding everything as before.
7461    /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
7462    /// drops references from expression kinds it does not enumerate.
7463    ///
7464    /// ORDER BY columns are included — the merge re-derives keys from the
7465    /// decoded row on the spilled path, so pruning one would sort NULLs.
7466    pub(crate) fn sort_record_columns_needed(
7467        items: &[SelectItem],
7468        order_bound: &[Option<usize>],
7469        arity: usize,
7470        ctx: &EvalContext,
7471    ) -> Vec<bool> {
7472        let all_bare = items.iter().all(|i| {
7473            matches!(
7474                i,
7475                SelectItem::Expr {
7476                    expr: Expr::Column(_),
7477                    ..
7478                }
7479            )
7480        });
7481        if !all_bare || order_bound.iter().any(Option::is_none) {
7482            return Vec::new();
7483        }
7484        let mut mask = alloc::vec![false; arity];
7485        for item in items {
7486            if let SelectItem::Expr {
7487                expr: Expr::Column(c),
7488                ..
7489            } = item
7490            {
7491                match crate::eval::find_column_pos(c, ctx) {
7492                    Some(p) if p < arity => mask[p] = true,
7493                    _ => return Vec::new(),
7494                }
7495            }
7496        }
7497        for p in order_bound.iter().flatten() {
7498            if *p < arity {
7499                mask[*p] = true;
7500            } else {
7501                return Vec::new();
7502            }
7503        }
7504        mask
7505    }
7506
7507    /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
7508    /// of sorting.
7509    ///
7510    /// PG serves such an ordering from the index and never sorts. We sorted:
7511    /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
7512    /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
7513    /// the sorter's own round trip — `ExternalSorter::finish_each` →
7514    /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
7515    /// Every row is encoded into the sorter's arena and decoded back out,
7516    /// for an order the index already holds.
7517    ///
7518    /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
7519    /// because it was built for top-N. This is the unbounded sibling.
7520    ///
7521    /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
7522    /// from a btree, so walking one would silently drop those rows. That is
7523    /// exactly the defect r1020 fixed on the top-N path, where it had
7524    /// shipped.
7525    /// r1044 — the index this statement's ORDER BY can be WALKED on,
7526    /// instead of sorted, or `None`.
7527    ///
7528    /// Extracted so `EXPLAIN` can ask the same question the executor
7529    /// answers. It could not, and said so: `SELECT pad FROM t ORDER BY
7530    /// id` on a 400,000-row table planned as `Sort` over `Seq Scan`
7531    /// while the executor walked the primary key — 34.9 ms against
7532    /// 147.0 for the same query ordered by an unindexed column, so the
7533    /// walk was plainly running. Round 551 fixed a different case of
7534    /// this and wrote the reason down: EXPLAIN is the first thing any
7535    /// performance question opens, and an instrument that misnames the
7536    /// access path is worse than one that says nothing.
7537    ///
7538    /// The gate is here once. Two copies of it is how the plan and the
7539    /// executor come to disagree again.
7540    pub(crate) fn index_order_walk_target(
7541        &self,
7542        stmt: &SelectStatement,
7543        from: &FromClause,
7544    ) -> Option<(String, usize)> {
7545        if stmt.order_by.len() != 1
7546            || !stmt.distinct_on.is_empty()
7547            || stmt.limit_with_ties
7548            || stmt.limit.is_some()
7549            || stmt.offset.is_some()
7550            || stmt.having.is_some()
7551            || stmt.group_by.is_some()
7552            || !stmt.unions.is_empty()
7553            || !from.joins.is_empty()
7554            || from.primary.lateral_subquery.is_some()
7555            || from.primary.unnest_expr.is_some()
7556            || from.primary.as_of_segment.is_some()
7557            || from.primary.generate_series_args.is_some()
7558            || select_has_window(stmt)
7559            || aggregate::uses_aggregate(stmt)
7560        {
7561            return None;
7562        }
7563        if stmt
7564            .items
7565            .iter()
7566            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
7567        {
7568            return None;
7569        }
7570        let table = self.active_catalog().get(&from.primary.name)?;
7571        if table.has_cold_rows_fast() {
7572            return None;
7573        }
7574        if !from.primary.only
7575            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7576        {
7577            return None;
7578        }
7579        let alias = from
7580            .primary
7581            .alias
7582            .as_deref()
7583            .unwrap_or(from.primary.name.as_str());
7584        let cols = &table.schema().columns;
7585        let order = &stmt.order_by[0];
7586        let Expr::Column(oc) = &order.expr else {
7587            return None;
7588        };
7589        if let Some(q) = &oc.qualifier
7590            && !q.eq_ignore_ascii_case(alias)
7591        {
7592            return None;
7593        }
7594        let order_pos = cols
7595            .iter()
7596            .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
7597        // r1047 — DISTINCT joins the walk when the projection IS the
7598        // order column, and only then. The index's keys are canonical
7599        // (r1039: representation equality is value equality — the
7600        // property every seek already depends on), so one key is one
7601        // distinct value and the walk can emit the first passing row of
7602        // each key group instead of hashing every row. On the release
7603        // sweep's `SELECT DISTINCT n FROM t ORDER BY n` — 400,000 rows,
7604        // 1,000 distinct values — the hash path priced at 21.3-22.7 ms
7605        // with an ablation floor of 14.8, because the hash must
7606        // normalize and probe ALL the rows; the walk visits each key
7607        // once. A wider projection makes DISTINCT about the whole tuple,
7608        // not the key, so anything else still declines.
7609        if stmt.distinct {
7610            let only_the_order_column = stmt.items.len() == 1
7611                && match &stmt.items[0] {
7612                    SelectItem::Expr {
7613                        expr: Expr::Column(c),
7614                        ..
7615                    } => {
7616                        c.name.eq_ignore_ascii_case(&oc.name)
7617                            && match &c.qualifier {
7618                                Some(q) => q.eq_ignore_ascii_case(alias),
7619                                None => true,
7620                            }
7621                    }
7622                    _ => false,
7623                };
7624            if !only_the_order_column {
7625                return None;
7626            }
7627        }
7628        // r1046 — a nullable key no longer refuses the walk; it changes
7629        // what the walk has to do. A NULL key is not in the btree, so
7630        // walking alone would silently drop those rows — the r1020
7631        // defect, which shipped once. The walk emits them separately, at
7632        // the end SQL puts them.
7633        //
7634        // Refusing was costing every nullable indexed column a 3.4x:
7635        // `SELECT id FROM t ORDER BY b` over 400,000 rows measured
7636        // 72.0 ms with the column nullable and 20.2 with the same data
7637        // under NOT NULL. `NOT NULL` is not the default, so that was the
7638        // common case paying for the uncommon one.
7639        let index = table.index_on(order_pos)?;
7640        if !matches!(index.kind, spg_storage::IndexKind::BTree(_))
7641            || index.expression.is_some()
7642            || index.partial_predicate.is_some()
7643        {
7644            return None;
7645        }
7646        Some((index.name.clone(), order_pos))
7647    }
7648
7649    fn try_index_order_stream<F>(
7650        &self,
7651        stmt: &SelectStatement,
7652        from: &FromClause,
7653        cancel: CancelToken<'_>,
7654        emit: &mut F,
7655    ) -> Result<Option<usize>, EngineError>
7656    where
7657        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
7658    {
7659        // r1044 — the shape gate lives in `index_order_walk_target`, so
7660        // `EXPLAIN` answers the same question. What stays here is the
7661        // part that RAISES (an illegal ORDER BY has to keep erroring
7662        // from where it did) and the bindings the walk needs.
7663        crate::orderby::check_order_by_legality(stmt)?;
7664        crate::orderby::check_order_by_positions(stmt)?;
7665        crate::window::reject_window_in_row_clauses(stmt)?;
7666        let Some((_, order_pos)) = self.index_order_walk_target(stmt, from) else {
7667            return Ok(None);
7668        };
7669        let Some(table) = self.active_catalog().get(&from.primary.name) else {
7670            return Ok(None);
7671        };
7672        let alias = from
7673            .primary
7674            .alias
7675            .as_deref()
7676            .unwrap_or(from.primary.name.as_str());
7677        let cols = table.schema().columns.clone();
7678        let order = &stmt.order_by[0];
7679        let Some(index) = table.index_on(order_pos) else {
7680            return Ok(None);
7681        };
7682
7683        let sess = self.dml_session();
7684        let ctx = EvalContext::new(&cols, Some(alias))
7685            .with_catalog(self.active_catalog())
7686            .with_session(&sess);
7687        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
7688        let columns: Vec<ColumnSchema> = projection
7689            .iter()
7690            .map(|p| {
7691                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
7692                c.user_enum_type = p.user_enum_type.clone();
7693                c.mysql_fsp = p.mysql_fsp;
7694                c
7695            })
7696            .collect();
7697        emit(crate::StreamItem::Header(&columns))?;
7698        let bound_pos: Vec<Option<usize>> = projection
7699            .iter()
7700            .map(|p| match &p.expr {
7701                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
7702                    Ok(Some(pos)) => Some(pos),
7703                    _ => None,
7704                },
7705                _ => None,
7706            })
7707            .collect();
7708
7709        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
7710            .where_
7711            .as_ref()
7712            .filter(|w| crate::eval::fully_compilable(w))
7713            .map(|w| crate::eval::compile_expr(w, &ctx));
7714        let mut eval_stack: Vec<Value<'static>> = Vec::new();
7715        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
7716        let snapshot = self.current_snapshot();
7717
7718        // A btree holds one locator per row VERSION, so a row whose key was
7719        // updated can sit under two keys and a dead one can sit beside its
7720        // replacement. The visibility gate drops the dead; `seen` drops a
7721        // live row that the walk reaches twice, which would otherwise be a
7722        // duplicated output row rather than a slow one.
7723        let mut emitted_rows = alloc::vec![false; table.rows().len()];
7724
7725        // r1046 — the rows the index cannot hold.
7726        //
7727        // A NULL key is not in the btree, so the walk below never reaches
7728        // those rows; they are emitted here, at the end SQL puts them.
7729        // PG's default is NULLS LAST ascending and NULLS FIRST
7730        // descending, and an explicit `NULLS FIRST` / `NULLS LAST` wins —
7731        // the same rule `order_by_value_cmp_raw` applies to the sort this
7732        // replaces, so the two orders agree.
7733        //
7734        // Finding them costs one pass over the column. That pass is why
7735        // this is still worth doing: the sort it replaces encodes and
7736        // decodes every row, and the walk plus the pass measured 72.0 ms
7737        // down to about 22 on 400,000 rows.
7738        let nulls_first = order.nulls_first.unwrap_or(order.desc);
7739        // r1047 — under DISTINCT the walk emits the FIRST passing row of
7740        // each key group and skips the rest; the gate admits DISTINCT
7741        // only when the projection is the order column itself, so one
7742        // canonical key is one output row. NULL is one distinct value,
7743        // so the NULL pass stops at its first emit too.
7744        let distinct = stmt.distinct;
7745        let mut count = 0usize;
7746        let mut visited = 0usize;
7747        let mut emit_null_rows = |emitted_rows: &mut alloc::vec::Vec<bool>,
7748                                  eval_stack: &mut Vec<Value<'static>>,
7749                                  values: &mut Vec<Value<'static>>,
7750                                  visited: &mut usize,
7751                                  emit: &mut F|
7752         -> Result<usize, EngineError> {
7753            if !cols[order_pos].nullable {
7754                return Ok(0);
7755            }
7756            let mut n = 0usize;
7757            for (ri, row) in table.rows().iter().enumerate() {
7758                if !matches!(row.values.get(order_pos), Some(Value::Null)) {
7759                    continue;
7760                }
7761                if emitted_rows.get(ri).copied().unwrap_or(true) {
7762                    continue;
7763                }
7764                if !table.is_row_visible(ri, &snapshot) {
7765                    continue;
7766                }
7767                *visited += 1;
7768                if visited.is_multiple_of(256) {
7769                    cancel.check()?;
7770                }
7771                emitted_rows[ri] = true;
7772                if Self::stream_project_row(
7773                    row,
7774                    stmt.where_.as_ref(),
7775                    compiled_where.as_ref(),
7776                    eval_stack,
7777                    &projection,
7778                    &bound_pos,
7779                    &ctx,
7780                    values,
7781                    emit,
7782                )? {
7783                    n += 1;
7784                    if distinct {
7785                        break;
7786                    }
7787                }
7788            }
7789            Ok(n)
7790        };
7791
7792        if nulls_first {
7793            count += emit_null_rows(
7794                &mut emitted_rows,
7795                &mut eval_stack,
7796                &mut values,
7797                &mut visited,
7798                emit,
7799            )?;
7800        }
7801
7802        let walker: alloc::boxed::Box<
7803            dyn Iterator<Item = (&spg_storage::IndexKey, &spg_storage::PostingList)>,
7804        > = if order.desc {
7805            alloc::boxed::Box::new(index.iter_desc())
7806        } else {
7807            alloc::boxed::Box::new(index.iter_asc())
7808        };
7809        for (_key, locators) in walker {
7810            for loc in locators {
7811                let spg_storage::RowLocator::Hot(ri) = *loc else {
7812                    continue;
7813                };
7814                if emitted_rows.get(ri).copied().unwrap_or(true) {
7815                    continue;
7816                }
7817                if !table.is_row_visible(ri, &snapshot) {
7818                    continue;
7819                }
7820                let Some(row) = table.rows().get(ri) else {
7821                    continue;
7822                };
7823                visited += 1;
7824                if visited.is_multiple_of(256) {
7825                    cancel.check()?;
7826                }
7827                emitted_rows[ri] = true;
7828                if Self::stream_project_row(
7829                    row,
7830                    stmt.where_.as_ref(),
7831                    compiled_where.as_ref(),
7832                    &mut eval_stack,
7833                    &projection,
7834                    &bound_pos,
7835                    &ctx,
7836                    &mut values,
7837                    emit,
7838                )? {
7839                    count += 1;
7840                    // One row per key group: the rest are the same value.
7841                    if distinct {
7842                        break;
7843                    }
7844                }
7845            }
7846        }
7847
7848        if !nulls_first {
7849            count += emit_null_rows(
7850                &mut emitted_rows,
7851                &mut eval_stack,
7852                &mut values,
7853                &mut visited,
7854                emit,
7855            )?;
7856        }
7857        Ok(Some(count))
7858    }
7859
7860    /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
7861    /// building an `OrderKey` vector per row.
7862    ///
7863    /// The row-returning sorted scan allocates twice per row: one
7864    /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
7865    /// projection. Counted over 400 k rows (r1030,
7866    /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
7867    /// allocations and 208 MB of traffic for an answer of four hundred
7868    /// thousand integers.
7869    ///
7870    /// The key half is pure ceremony on this shape.
7871    /// `sort_tagged_by_inline_int_key` already sorts indices rather than
7872    /// rows, so the per-row vector is built, has one integer taken out of
7873    /// it, and is then dragged through the permutation — it exists to carry
7874    /// a number the row's column already held. This lane carries the number
7875    /// instead, in a fixed-size array that lives inside the buffer element
7876    /// and allocates nothing. Same idea as the predicate VM's integer lane.
7877    ///
7878    /// Declines to `None` for anything it does not cover, and every caller
7879    /// falls through to the general path, so the gate list is the
7880    /// specification.
7881    ///
7882    /// Ties: equal keys keep scan order, as the stable sort on the general
7883    /// path does. Rows that tie on every ORDER BY term are entitled to any
7884    /// order among themselves either way — see `STABILITY.md`.
7885    fn try_int_key_sorted_stream<F>(
7886        &self,
7887        stmt: &SelectStatement,
7888        from: &FromClause,
7889        cancel: CancelToken<'_>,
7890        emit: &mut F,
7891    ) -> Result<Option<usize>, EngineError>
7892    where
7893        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
7894    {
7895        /// Sort terms this lane carries inline. Four covers every ORDER BY
7896        /// in the endpoint sweep and in the dogfood corpus; wider ones fall
7897        /// through rather than growing the buffer element for everybody.
7898        const MAX_KEYS: usize = 4;
7899
7900        if stmt.order_by.is_empty()
7901            || stmt.order_by.len() > MAX_KEYS
7902            || stmt.distinct
7903            || stmt.limit_with_ties
7904            || stmt.limit.is_some()
7905            || stmt.offset.is_some()
7906            || stmt.having.is_some()
7907            || stmt.group_by.is_some()
7908            || !stmt.unions.is_empty()
7909            || !from.joins.is_empty()
7910            || from.primary.lateral_subquery.is_some()
7911            || from.primary.unnest_expr.is_some()
7912            || from.primary.as_of_segment.is_some()
7913            || from.primary.generate_series_args.is_some()
7914            || select_has_window(stmt)
7915            || aggregate::uses_aggregate(stmt)
7916        {
7917            return Ok(None);
7918        }
7919        if stmt
7920            .items
7921            .iter()
7922            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
7923        {
7924            return Ok(None);
7925        }
7926        crate::orderby::check_order_by_legality(stmt)?;
7927        crate::orderby::check_order_by_positions(stmt)?;
7928        crate::window::reject_window_in_row_clauses(stmt)?;
7929        let Some(table) = self.active_catalog().get(&from.primary.name) else {
7930            return Ok(None);
7931        };
7932        if table.has_cold_rows_fast() {
7933            return Ok(None);
7934        }
7935        if !from.primary.only
7936            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7937        {
7938            return Ok(None);
7939        }
7940        let alias = from
7941            .primary
7942            .alias
7943            .as_deref()
7944            .unwrap_or(from.primary.name.as_str());
7945        let cols = table.schema().columns.clone();
7946
7947        // Every ORDER BY term must be a NOT NULL integer column of this
7948        // table. NOT NULL is what lets the key be a bare integer: with
7949        // NULLs the lane would have to carry their ordering too, and
7950        // getting that subtly wrong is the r1020 defect.
7951        let mut key_pos = [0usize; MAX_KEYS];
7952        let mut descs = [false; MAX_KEYS];
7953        // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
7954        // which the AST records as `None`; `unwrap_or(desc)` is how the
7955        // rest of the engine resolves it.
7956        let mut nulls_first = [false; MAX_KEYS];
7957        let n_keys = stmt.order_by.len();
7958        for (slot, order) in stmt.order_by.iter().enumerate() {
7959            let Expr::Column(oc) = &order.expr else {
7960                return Ok(None);
7961            };
7962            if let Some(q) = &oc.qualifier
7963                && !q.eq_ignore_ascii_case(alias)
7964            {
7965                return Ok(None);
7966            }
7967            let Some(pos) = cols
7968                .iter()
7969                .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
7970            else {
7971                return Ok(None);
7972            };
7973            if !matches!(
7974                cols[pos].ty,
7975                spg_storage::DataType::SmallInt
7976                    | spg_storage::DataType::Int
7977                    | spg_storage::DataType::BigInt
7978            ) {
7979                return Ok(None);
7980            }
7981            key_pos[slot] = pos;
7982            descs[slot] = order.desc;
7983            nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
7984        }
7985
7986        let sess = self.dml_session();
7987        let ctx = EvalContext::new(&cols, Some(alias))
7988            .with_catalog(self.active_catalog())
7989            .with_session(&sess);
7990        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
7991        let columns: Vec<ColumnSchema> = projection
7992            .iter()
7993            .map(|p| {
7994                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
7995                c.user_enum_type = p.user_enum_type.clone();
7996                c.mysql_fsp = p.mysql_fsp;
7997                c
7998            })
7999            .collect();
8000        let bound_pos: Vec<Option<usize>> = projection
8001            .iter()
8002            .map(|p| match &p.expr {
8003                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8004                    Ok(Some(pos)) => Some(pos),
8005                    _ => None,
8006                },
8007                _ => None,
8008            })
8009            .collect();
8010        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8011            .where_
8012            .as_ref()
8013            .filter(|w| crate::eval::fully_compilable(w))
8014            .map(|w| crate::eval::compile_expr(w, &ctx));
8015
8016        // The same first-observable point the materialising planner fires,
8017        // placed after the gates so it fires exactly once: this lane runs
8018        // BEFORE that planner and would otherwise be a hole in the
8019        // panic-isolation and cancellation-race coverage rather than a
8020        // faster path through it.
8021        crate::injection_point!("planner_first_row_fetch", &stmt.from);
8022
8023        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8024        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8025        let mut budget = ByteBudget::new(self.max_query_bytes);
8026        let snapshot = self.current_snapshot();
8027        // Keys, a NULL bit per key slot, and the row. The bitmask keeps
8028        // the element small: a nullable key still costs one bit rather
8029        // than a second array.
8030        let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
8031
8032        for (ri, row) in table.rows().iter().enumerate() {
8033            if ri.is_multiple_of(256) {
8034                cancel.check()?;
8035            }
8036            if !table.is_row_visible(ri, &snapshot) {
8037                continue;
8038            }
8039            // The key comes from the STORED row, before projection: an
8040            // ORDER BY column need not appear in the select list.
8041            let mut keys = [0i64; MAX_KEYS];
8042            let mut nulls = 0u8;
8043            let mut keyed = true;
8044            for slot in 0..n_keys {
8045                match row.values.get(key_pos[slot]) {
8046                    Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
8047                    Some(Value::Int(v)) => keys[slot] = i64::from(*v),
8048                    Some(Value::BigInt(v)) => keys[slot] = *v,
8049                    Some(Value::Null) | None => nulls |= 1 << slot,
8050                    // An integer column holding something else is a row
8051                    // this lane cannot order; hand the whole query back
8052                    // rather than guess at it.
8053                    _ => {
8054                        keyed = false;
8055                        break;
8056                    }
8057                }
8058            }
8059            if !keyed {
8060                return Ok(None);
8061            }
8062            if !Self::stream_filter_project(
8063                row,
8064                stmt.where_.as_ref(),
8065                compiled_where.as_ref(),
8066                &mut eval_stack,
8067                &projection,
8068                &bound_pos,
8069                &ctx,
8070                &mut values,
8071            )? {
8072                continue;
8073            }
8074            budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
8075            sorted.push((keys, nulls, core::mem::take(&mut values)));
8076            values.reserve(projection.len());
8077        }
8078
8079        sorted.sort_by(|a, b| {
8080            use core::cmp::Ordering;
8081            for slot in 0..n_keys {
8082                let bit = 1u8 << slot;
8083                let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
8084                    (true, true) => Ordering::Equal,
8085                    // Where the NULLs go is already decided — `nulls_first`
8086                    // resolved DESC's default when it was read. Reversing
8087                    // this for DESC as well would apply the direction
8088                    // twice and put them at the wrong end.
8089                    (true, false) => {
8090                        if nulls_first[slot] {
8091                            Ordering::Less
8092                        } else {
8093                            Ordering::Greater
8094                        }
8095                    }
8096                    (false, true) => {
8097                        if nulls_first[slot] {
8098                            Ordering::Greater
8099                        } else {
8100                            Ordering::Less
8101                        }
8102                    }
8103                    (false, false) => {
8104                        let o = a.0[slot].cmp(&b.0[slot]);
8105                        if descs[slot] { o.reverse() } else { o }
8106                    }
8107                };
8108                if ord != Ordering::Equal {
8109                    return ord;
8110                }
8111            }
8112            Ordering::Equal
8113        });
8114
8115        emit(crate::StreamItem::Header(&columns))?;
8116        let count = sorted.len();
8117        for (_, _, vals) in &sorted {
8118            emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
8119        }
8120        Ok(Some(count))
8121    }
8122
8123    fn try_spill_sorted_stream<F>(
8124        &self,
8125        stmt: &SelectStatement,
8126        from: &FromClause,
8127        cancel: CancelToken<'_>,
8128        emit: &mut F,
8129    ) -> Result<Option<usize>, EngineError>
8130    where
8131        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8132    {
8133        // The shapes `try_spill_sorted_scan` declines, plus the ones the
8134        // streaming executor does not carry (a LIMIT is already bounded
8135        // by a partial sort; the rest need the answer addressable).
8136        if !self.can_spill()
8137            || stmt.order_by.is_empty()
8138            || stmt.distinct
8139            || stmt.limit_with_ties
8140            || stmt.limit.is_some()
8141            || stmt.offset.is_some()
8142            || stmt.having.is_some()
8143            || stmt.group_by.is_some()
8144            || !stmt.unions.is_empty()
8145            || !from.joins.is_empty()
8146            || from.primary.lateral_subquery.is_some()
8147            || from.primary.unnest_expr.is_some()
8148            || from.primary.as_of_segment.is_some()
8149            || from.primary.generate_series_args.is_some()
8150            || select_has_window(stmt)
8151            || aggregate::uses_aggregate(stmt)
8152        {
8153            return Ok(None);
8154        }
8155        if stmt
8156            .items
8157            .iter()
8158            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8159        {
8160            return Ok(None);
8161        }
8162        // Everything `exec_bare_select_cancel` does before it scans runs
8163        // BELOW this path, so a statement claimed here skips it. Three of
8164        // those were missed on the way in and each was caught by a
8165        // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
8166        // ORDER BY 2` sorted happily instead of raising 42P10), the
8167        // cancellation check by another, the partition fan-out by the
8168        // differential corpus. What is reconciled, item by item: with-ties
8169        // needs ORDER BY (gated above), USING/NATURAL and RLS join
8170        // rewrites (joins gated above), the single-table RLS predicate
8171        // (the dispatcher declines a policy-subject table before this is
8172        // reached), the meta-view dispatch (those names are not in the
8173        // catalog, so the lookup below declines). These three are calls,
8174        // so the message and SQLSTATE are the ones the fall-back gives —
8175        // `select_has_window` above reads the select list and ORDER BY but
8176        // not WHERE, which is the case the third one covers.
8177        crate::orderby::check_order_by_legality(stmt)?;
8178        crate::orderby::check_order_by_positions(stmt)?;
8179        crate::window::reject_window_in_row_clauses(stmt)?;
8180        // A parent's rows are its children's. These walks scan the named
8181        // relation alone, so a partitioned or inherited parent comes back
8182        // short — and silently: the corpus caught `SELECT id FROM pr
8183        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8184        // parent's own rows instead of the partitions'. `ONLY` is exactly
8185        // the case that does not fan out, so it stays, which is the test
8186        // the FROM-clause fan-out itself makes.
8187        if !from.primary.only
8188            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8189        {
8190            return Ok(None);
8191        }
8192        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8193            return Ok(None);
8194        };
8195        // Cold-tier rows live outside `rows()`; this walk would drop
8196        // them silently, the same reason round 831's walk declines.
8197        if table.has_cold_rows_fast() {
8198            return Ok(None);
8199        }
8200
8201        let alias = from
8202            .primary
8203            .alias
8204            .as_deref()
8205            .unwrap_or(from.primary.name.as_str());
8206        let cols = table.schema().columns.clone();
8207        let sess = self.dml_session();
8208        let ctx = EvalContext::new(&cols, Some(alias))
8209            .with_catalog(self.active_catalog())
8210            .with_session(&sess);
8211        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8212        let order_by = stmt.order_by.clone();
8213        // The same one-shot resolution the general path does (round
8214        // 582): each ORDER BY column is bound once, not once per row.
8215        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8216        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8217        // Resolved BEFORE the scan, because it now decides what the sort
8218        // STORES and not just what it decodes (round 995).
8219        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8220
8221        let mut sorter = crate::extsort::ExternalSorter::new(
8222            self.temp_run_factory,
8223            self.session_work_mem_bytes(),
8224            cols.clone(),
8225            &descs,
8226        )
8227        .with_stats(&self.spill_stats)
8228        .with_pruned(&needed);
8229        let snapshot = self.current_snapshot();
8230        // One key buffer for the whole scan: `push` drains it and leaves
8231        // the capacity behind.
8232        let mut keys: Vec<OrderKey> = Vec::new();
8233        // r1024 — compile the predicate once for the scan.
8234        //
8235        // These two sorted-spill scans are the paths a single-table SELECT
8236        // with an ORDER BY takes, and they were the last row-returning ones
8237        // still walking the expression tree per row. r1023 did the
8238        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8239        // exactly this shape.
8240        //
8241        // Found from the profile's CALL TREE rather than its leaves. The
8242        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8243        // 261, `mod_op` 178 — and two attempts at reasoning out which
8244        // function asked for it were both wrong. The tree names the caller
8245        // chain, and it named this one.
8246        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8247            .where_
8248            .as_ref()
8249            .filter(|w| crate::eval::fully_compilable(w))
8250            .map(|w| crate::eval::compile_expr(w, &ctx));
8251        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8252        for (i, row) in table.scan_visible_from(0, &snapshot) {
8253            if i.is_multiple_of(256) {
8254                cancel.check()?;
8255            }
8256            if let Some(c) = &compiled_where {
8257                if !crate::eval::compiled::eval_compiled_pred(
8258                    c,
8259                    row,
8260                    &ctx,
8261                    &mut eval_stack,
8262                    ctx.mysql_dialect,
8263                )? {
8264                    continue;
8265                }
8266            } else if let Some(w) = &stmt.where_ {
8267                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8268                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8269                    continue;
8270                }
8271            }
8272            keys.clear();
8273            crate::orderby::build_order_keys_bound(&order_by, &order_bound, row, &ctx, &mut keys)?;
8274            sorter.push(&mut keys, row)?;
8275        }
8276
8277        let columns: Vec<ColumnSchema> = projection
8278            .iter()
8279            .map(|p| {
8280                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8281                c.user_enum_type = p.user_enum_type.clone();
8282                c.mysql_fsp = p.mysql_fsp;
8283                c
8284            })
8285            .collect();
8286        emit(crate::StreamItem::Header(&columns))?;
8287
8288        let key_ctx = &ctx;
8289        let mut emitted_since_check = 0usize;
8290        let n = sorter.finish_each(
8291            |src, buf| {
8292                crate::orderby::build_order_keys_bound(&order_by, &order_bound, src, key_ctx, buf)
8293            },
8294            |src, values| {
8295                for p in &projection {
8296                    values.push(
8297                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8298                    );
8299                }
8300                Ok(())
8301            },
8302            |cells| {
8303                // The merge is the long half of a big sort, and the scan's
8304                // check above stops running once it ends: a cancelled
8305                // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
8306                // anyway. Same stride as the scan.
8307                emitted_since_check += 1;
8308                if emitted_since_check >= 256 {
8309                    emitted_since_check = 0;
8310                    cancel.check()?;
8311                }
8312                emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
8313            },
8314        )?;
8315        Ok(Some(n))
8316    }
8317
8318    /// One row of the single-table streaming walk: the WHERE test, the
8319    /// projection, the emit. Returns whether a row was emitted.
8320    ///
8321    /// v7.39 (round 970) — factored out because the walk now has two ways
8322    /// to reach a row, the sequential scan and an index seek's candidate
8323    /// positions, and both must do IDENTICALLY this. A copy in each is how
8324    /// two paths for one job drift; this file already carries the cost of
8325    /// that lesson twice (rounds 823 and 961, both resolvers).
8326    ///
8327    /// `#[inline]` so the scan loop keeps the shape round 957 measured it
8328    /// in — a shared hot path pays for a new abstraction whether or not it
8329    /// uses it, and this one is on the scan.
8330    #[inline]
8331    #[allow(clippy::too_many_arguments)]
8332    fn stream_filter_project(
8333        row: &spg_storage::Row<'static>,
8334        where_: Option<&Expr>,
8335        // r1023 — the same WHERE, compiled once by the caller. `None` means
8336        // the expression did not qualify and `where_` is evaluated as before.
8337        compiled_where: Option<&crate::eval::CompiledExpr>,
8338        eval_stack: &mut Vec<Value<'static>>,
8339        projection: &[ProjectedItem],
8340        bound_pos: &[Option<usize>],
8341        ctx: &crate::eval::EvalContext<'_>,
8342        values: &mut Vec<Value<'static>>,
8343    ) -> Result<bool, EngineError> {
8344        // r1023 — this scan ran its predicate through the TREE INTERPRETER,
8345        // once per row, and it was the only row-returning path that did.
8346        // The aggregate path, `table_access`, and the PK walker all compile
8347        // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
8348        // server's live samples were `eval_expr` 99, `apply_binary` 81,
8349        // `mod_op` 29 — the interpreter, not delivery.
8350        //
8351        // The arithmetic accounted for it exactly. Over the wire, the same
8352        // filter costs 6.375 ms returning rows and 0.679 ms counting them;
8353        // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
8354        // which is what an interpreted predicate costs against the compiled
8355        // lane's 11.7. It was named "delivery after a filter" before this
8356        // profile, and it was never delivery.
8357        if let Some(c) = compiled_where {
8358            if !crate::eval::compiled::eval_compiled_pred(
8359                c,
8360                row,
8361                ctx,
8362                eval_stack,
8363                ctx.mysql_dialect,
8364            )? {
8365                return Ok(false);
8366            }
8367        } else if let Some(w) = where_ {
8368            let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
8369            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8370                return Ok(false);
8371            }
8372        }
8373        values.clear();
8374        for (p, bound) in projection.iter().zip(bound_pos) {
8375            values.push(match bound {
8376                Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
8377                None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
8378            });
8379        }
8380        Ok(true)
8381    }
8382
8383    /// The same filter and projection, then emit. Split from
8384    /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
8385    /// before it can emit them — a sort — runs the identical predicate and
8386    /// projection rather than a second copy of them.
8387    #[allow(clippy::too_many_arguments)]
8388    fn stream_project_row<F>(
8389        row: &spg_storage::Row<'static>,
8390        where_: Option<&Expr>,
8391        compiled_where: Option<&crate::eval::CompiledExpr>,
8392        eval_stack: &mut Vec<Value<'static>>,
8393        projection: &[ProjectedItem],
8394        bound_pos: &[Option<usize>],
8395        ctx: &crate::eval::EvalContext<'_>,
8396        values: &mut Vec<Value<'static>>,
8397        emit: &mut F,
8398    ) -> Result<bool, EngineError>
8399    where
8400        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8401    {
8402        if !Self::stream_filter_project(
8403            row,
8404            where_,
8405            compiled_where,
8406            eval_stack,
8407            projection,
8408            bound_pos,
8409            ctx,
8410            values,
8411        )? {
8412            return Ok(false);
8413        }
8414        emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
8415        Ok(true)
8416    }
8417
8418    fn try_stream_single_table<F>(
8419        &self,
8420        stmt: &SelectStatement,
8421        from: &FromClause,
8422        cancel: CancelToken<'_>,
8423        emit: &mut F,
8424    ) -> Result<Option<usize>, EngineError>
8425    where
8426        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8427    {
8428        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8429            return Ok(None);
8430        };
8431        // Cold-tier rows live outside `rows()`; the materialising fallback
8432        // covers both tiers and this walk would silently drop them.
8433        if table.has_cold_rows_fast() {
8434            return Ok(None);
8435        }
8436        let alias = from
8437            .primary
8438            .alias
8439            .as_deref()
8440            .unwrap_or(from.primary.name.as_str());
8441        let cols = table.schema().columns.clone();
8442        let sess = self.dml_session();
8443        let ctx = EvalContext::new(&cols, Some(alias))
8444            .with_catalog(self.active_catalog())
8445            .with_session(&sess);
8446        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8447
8448        let columns: Vec<ColumnSchema> = projection
8449            .iter()
8450            .map(|p| {
8451                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8452                c.user_enum_type = p.user_enum_type.clone();
8453                c.mysql_fsp = p.mysql_fsp;
8454                c
8455            })
8456            .collect();
8457        emit(crate::StreamItem::Header(&columns))?;
8458
8459        // v7.37 (round 957) — resolve each bare-column projection ONCE
8460        // instead of once per row. `find_column_pos`-style resolution is a
8461        // linear walk of the schema comparing column-name strings, and the
8462        // row loop below ran it for every cell of every row: measured at
8463        // 400k rows, binding it out of the loop took `SELECT pad` from
8464        // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
8465        //
8466        // ORDER BY has bound its keys this way since round 582
8467        // (`order_by_bound_positions`); the projection never did.
8468        //
8469        // `locate_column` is the same resolution `resolve_column` performs,
8470        // returning the site instead of the value, so the two cannot drift
8471        // apart the way a second hand-written resolver would. Anything it
8472        // declines — an expression, a whole-row reference, a name that does
8473        // not resolve — binds to `None` and takes the general path below,
8474        // errors included, so an empty table still reports nothing rather
8475        // than raising at bind time.
8476        let bound_pos: Vec<Option<usize>> = projection
8477            .iter()
8478            .map(|p| match &p.expr {
8479                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8480                    Ok(Some(pos)) => Some(pos),
8481                    _ => None,
8482                },
8483                _ => None,
8484            })
8485            .collect();
8486
8487        // One snapshot for the whole scan, as the materialising path takes.
8488        let snapshot = self.current_snapshot();
8489
8490        // v7.39 (round 970) — ask the indices BEFORE walking the table.
8491        //
8492        // This walk had no index step at all, and it is preferred over the
8493        // materialising path, which does have one (`pick_indexed_rows` ->
8494        // `try_index_seek`). So a primary-key point lookup — the commonest
8495        // statement there is — read every row: measured on 500k rows,
8496        // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
8497        // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
8498        // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
8499        //
8500        // The control that named it: `... OFFSET 0` — semantically the same
8501        // query — answered in 0.159 ms, because OFFSET is one of the shape
8502        // gates that declines this walk and sends the statement to the path
8503        // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
8504        // no semantics in common; what they share is making this function
8505        // stand down.
8506        //
8507        // The seek only NARROWS: every candidate still goes through the
8508        // full WHERE below, exactly as the mutation paths use it, so a
8509        // partial index match cannot change an answer. Positions come back
8510        // already visibility-filtered and already capped at a quarter of the
8511        // table (round 490), so a seek can never cost more than the scan it
8512        // replaces, and `None` means "walk the table" as before.
8513        //
8514        // Sorted because the scan would have produced table order and the
8515        // index produces key order. Without an ORDER BY neither is promised,
8516        // but a walk that silently reorders its answer when an index happens
8517        // to exist is a difference nobody asked for.
8518        let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
8519            crate::index_access::try_index_seek_positions(w, &cols, table, alias, &snapshot)
8520        });
8521
8522        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8523        // r1023 — compile the predicate once for the whole scan. Same gate
8524        // every other path uses: `fully_compilable` or keep the interpreter,
8525        // so a shape the VM cannot take answers exactly as it did before.
8526        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8527            .where_
8528            .as_ref()
8529            .filter(|w| crate::eval::fully_compilable(w))
8530            .map(|w| crate::eval::compile_expr(w, &ctx));
8531        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8532        let mut count: usize = 0;
8533        match seek_positions {
8534            Some(mut positions) => {
8535                positions.sort_unstable();
8536                for (n, pos) in positions.into_iter().enumerate() {
8537                    if n.is_multiple_of(256) {
8538                        cancel.check()?;
8539                    }
8540                    let Some(row) = table.rows().get(pos) else {
8541                        continue;
8542                    };
8543                    if Self::stream_project_row(
8544                        row,
8545                        stmt.where_.as_ref(),
8546                        compiled_where.as_ref(),
8547                        &mut eval_stack,
8548                        &projection,
8549                        &bound_pos,
8550                        &ctx,
8551                        &mut values,
8552                        emit,
8553                    )? {
8554                        count += 1;
8555                    }
8556                }
8557            }
8558            None => {
8559                for (i, row) in table.scan_visible_from(0, &snapshot) {
8560                    if i.is_multiple_of(256) {
8561                        cancel.check()?;
8562                    }
8563                    if Self::stream_project_row(
8564                        row,
8565                        stmt.where_.as_ref(),
8566                        compiled_where.as_ref(),
8567                        &mut eval_stack,
8568                        &projection,
8569                        &bound_pos,
8570                        &ctx,
8571                        &mut values,
8572                        emit,
8573                    )? {
8574                        count += 1;
8575                    }
8576                }
8577            }
8578        }
8579        Ok(Some(count))
8580    }
8581
8582    pub(crate) fn try_exec_joined_streaming<F>(
8583        &self,
8584        stmt: &SelectStatement,
8585        cancel: CancelToken<'_>,
8586        emit: &mut F,
8587    ) -> Result<Option<usize>, EngineError>
8588    where
8589        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8590    {
8591        // Shape gates — keep the streamable surface narrow on
8592        // purpose. The fall-back path still handles everything else.
8593        let Some(from) = &stmt.from else {
8594            return Ok(None);
8595        };
8596        // v7.37 (round 830) — decline anything a row-security policy binds
8597        // for this session. Policies are injected in
8598        // `exec_bare_select_cancel`, below this path, so a statement claimed
8599        // here would read the table unfiltered: measured, `SELECT val FROM
8600        // sec` returned all three rows to a session whose policy allows two,
8601        // while `SELECT upper(val) FROM sec` — declined by the shape gates
8602        // and so materialised — returned the correct two.
8603        //
8604        // Declining sends it to the path that enforces. Teaching this one to
8605        // inject the predicate itself would keep the streaming benefit for
8606        // RLS tables and is the better end state; it is not what a
8607        // correctness fix should carry, and the fall-back is exactly as
8608        // correct, only slower.
8609        if self.select_reads_policy_subject_table(stmt) {
8610            return Ok(None);
8611        }
8612        // v7.39 (round 790) — single-table SELECTs stream too. This
8613        // gate said "joins only" because the path was written for
8614        // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
8615        // fell to the materialising fallback, which builds the whole
8616        // `Vec<Row<'static>>` and only then iterates it. Measured on
8617        // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
8618        // reached through a one-row JOIN — 2.6x, purely for lacking a
8619        // join. The deferred-join structure handles one source as the
8620        // degenerate stride-1 case, so the walk below is unchanged.
8621        let _single_table = from.joins.is_empty();
8622        // An ORDER BY that the bounded sort can serve streams; everything
8623        // else still falls to the materialising fallback below.
8624        // r1025 — an ordering the index already holds needs no sort at all.
8625        // Tried before the spill sort, which is the path it replaces.
8626        if !stmt.order_by.is_empty()
8627            && from.joins.is_empty()
8628            && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
8629        {
8630            return Ok(Some(n));
8631        }
8632        if !stmt.order_by.is_empty()
8633            && from.joins.is_empty()
8634            && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
8635        {
8636            return Ok(Some(n));
8637        }
8638        // r1031 — integer keys carried inline instead of an `OrderKey`
8639        // vector per row. Tried AFTER the spill sort on purpose: this lane
8640        // buffers the whole answer, so anything the spill path would take
8641        // must keep taking it rather than be turned back into an in-memory
8642        // sort that answers with a budget error.
8643        if !stmt.order_by.is_empty()
8644            && from.joins.is_empty()
8645            && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
8646        {
8647            return Ok(Some(n));
8648        }
8649        if !stmt.order_by.is_empty()
8650            || stmt.limit.is_some()
8651            || stmt.offset.is_some()
8652            || stmt.having.is_some()
8653            || stmt.group_by.is_some()
8654            || stmt.distinct
8655            || !stmt.unions.is_empty()
8656            || stmt.limit_with_ties
8657        {
8658            return Ok(None);
8659        }
8660        if aggregate::uses_aggregate(stmt) {
8661            return Ok(None);
8662        }
8663        // No window / SRF on the streaming path.
8664        if select_has_window(stmt) {
8665            return Ok(None);
8666        }
8667        if stmt
8668            .items
8669            .iter()
8670            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8671        {
8672            return Ok(None);
8673        }
8674        // v7.37 (round 831) — a joinless FROM over a plain stored table
8675        // never needs the deferred structure, and building one costs the
8676        // whole table. `materialise_table_ref_filtered` clones every row
8677        // into a `Vec<Row<'static>>` before anything is filtered or
8678        // projected, so peak cost tracks the TABLE, not the result:
8679        // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
8680        // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
8681        // projection saving nothing, while an arithmetic projection — which
8682        // the shape gates decline, so it materialises through the ordinary
8683        // executor — cost +21 MB.
8684        //
8685        // Scanning in batches and releasing each one is what `cursor_fill`
8686        // already does for a lazy cursor, and it is the same walk: resume
8687        // from a slot, take visible rows, evaluate, hand them over, drop
8688        // them. Round 800's finding stands and is why this reads rows OUT
8689        // rather than seeding the join by index — touching the stored
8690        // `PersistentVec` in place makes the whole table resident, which is
8691        // worse than the copy. Each batch is copied, then freed.
8692        if from.joins.is_empty()
8693            && from.primary.unnest_expr.is_none()
8694            && from.primary.lateral_subquery.is_none()
8695            && from.primary.as_of_segment.is_none()
8696            && from.primary.generate_series_args.is_none()
8697            && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
8698        {
8699            return Ok(Some(n));
8700        }
8701        // Build the deferred join under the regular byte budget.
8702        let mut budget = ByteBudget::new(self.max_query_bytes);
8703        let deferred = {
8704            let mut needed = alloc::collections::BTreeSet::new();
8705            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
8706            self.build_joined_filtered_rows(
8707                from,
8708                stmt.where_.as_ref(),
8709                cancel,
8710                if prunable { Some(&needed) } else { None },
8711                &mut budget,
8712            )?
8713        };
8714        let combined_schema = &deferred.combined_schema;
8715        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
8716        // `::regclass` / enum cast in a joined projection or HAVING needs it.
8717        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
8718        // the same predicate the unjoined shape carries.
8719        let joined_sess = self.dml_session();
8720        let ctx = EvalContext::new(combined_schema, None)
8721            .with_catalog(self.active_catalog())
8722            .with_session(&joined_sess);
8723        let projection =
8724            build_projection(&stmt.items, combined_schema, "", self.backslash_escapes)?;
8725        // Every projection item must be a bound qualified column —
8726        // anything that needs `eval_expr_with_correlated` keeps the
8727        // materialising path.
8728        let bound_pos = |e: &Expr| -> Option<usize> {
8729            match e {
8730                // v7.39 (round 822) — an UNQUALIFIED column resolves here
8731                // too. The `qualifier.is_some()` guard this replaces meant
8732                // `SELECT pad FROM big` — the commonest projection there is
8733                // — never reached the streaming walk: it fell out at this
8734                // gate and re-ran on the materialising path, after the
8735                // deferred join structure had already been built and paid
8736                // for. Measured (round 821, statement_timeout=120 over 400k
8737                // rows): `big.pad` and `b.pad` streamed and cancelled at
8738                // ~65k rows in 0.14 s, while bare `pad` ran to completion in
8739                // 0.80 s with the timeout never consulted. `find_column_pos`
8740                // has always handled the unqualified case (it falls through
8741                // to a by-name match), so the guard narrowed the gate for no
8742                // reason it recorded.
8743                Expr::Column(c) => eval::find_column_pos(c, &ctx),
8744                _ => None,
8745            }
8746        };
8747        let proj_decomposed: Vec<(usize, usize)> = {
8748            let mut out = Vec::with_capacity(projection.len());
8749            for p in &projection {
8750                let Some(abs) = bound_pos(&p.expr) else {
8751                    return Ok(None);
8752                };
8753                let Some(k) = deferred
8754                    .offsets
8755                    .partition_point(|&o| o <= abs)
8756                    .checked_sub(1)
8757                else {
8758                    return Ok(None);
8759                };
8760                out.push((k, abs - deferred.offsets[k]));
8761            }
8762            out
8763        };
8764        // Emit columns once.
8765        let columns: Vec<ColumnSchema> = projection
8766            .iter()
8767            // v7.39 (read01 round 54) — keep the column's enum identity through
8768            // the projection (it lives outside the DataType lattice), or a
8769            // derived table / UNION / windowed result forgets it and any outer
8770            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
8771            .map(|p| {
8772                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8773                c.user_enum_type = p.user_enum_type.clone();
8774                c.mysql_fsp = p.mysql_fsp;
8775                c
8776            })
8777            .collect();
8778        emit(crate::StreamItem::Header(&columns))?;
8779        let sources_ref = &deferred.sources;
8780        let stride = deferred.stride;
8781        let survivors_ref = &deferred.survivors;
8782        let n_surv = if stride == 0 {
8783            0
8784        } else {
8785            survivors_ref.len() / stride
8786        };
8787        // Reused per-row cell-ref scratch — pushes are zero-alloc
8788        // after the first row.
8789        let null_value = Value::Null;
8790        let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
8791        let mut count: usize = 0;
8792        for surv_i in 0..n_surv {
8793            if surv_i.is_multiple_of(256) {
8794                cancel.check()?;
8795            }
8796            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
8797            cell_refs.clear();
8798            for &(k, col_in_src) in &proj_decomposed {
8799                let ri = tuple[k];
8800                let v: &Value = if ri == usize::MAX {
8801                    &null_value
8802                } else {
8803                    sources_ref[k]
8804                        .get(ri)
8805                        .and_then(|r| r.values.get(col_in_src))
8806                        .unwrap_or(&null_value)
8807                };
8808                cell_refs.push(v);
8809            }
8810            emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
8811            count += 1;
8812        }
8813        Ok(Some(count))
8814    }
8815
8816    fn exec_joined_select(
8817        &self,
8818        stmt: &SelectStatement,
8819        from: &FromClause,
8820        cancel: CancelToken<'_>,
8821    ) -> Result<QueryResult, EngineError> {
8822        // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
8823        // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
8824        // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
8825        // FROM B WHERE B.k = A.k)` into
8826        //   SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
8827        //   WHERE B.k IS NULL
8828        // The general join executor builds a hash, probes every outer
8829        // tuple, materialises (left_padded_with_null) for every miss,
8830        // then runs the aggregate over the result set. For COUNT(*) we
8831        // only need the count — skip the tuple materialisation. Build
8832        // a HashSet of B's unique join values, scan A's PK index, and
8833        // increment the counter on each miss. PG's Merge Anti-Join
8834        // does roughly this; ours becomes a simple HashSet probe.
8835        if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
8836            return Ok(out);
8837        }
8838        // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
8839        // When ORDER BY is on an indexed primary column, walking the
8840        // btree in the requested direction lets the streamer break
8841        // after `LIMIT + OFFSET` survivors without ever materialising
8842        // the rest of the join — the 80 ms `mailrs_prod_not_exists`
8843        // plateau is exactly this shape.
8844        if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
8845            return Ok(out);
8846        }
8847        // v7.30.3 (mailrs round-26) — the bounded single-join path
8848        // first; peak memory scales with LIMIT instead of the table.
8849        if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
8850            return Ok(out);
8851        }
8852        // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
8853        // WHERE materialisation to the shared helper so the LATERAL
8854        // / UNNEST / regular-catalog paths route through one place.
8855        // (`build_joined_filtered_rows` carries LATERAL support as
8856        // of Phase 3.P0-41.) Downstream we still handle aggregate /
8857        // projection / ORDER BY / DISTINCT / LIMIT inline because
8858        // those depend on the SelectStatement's items list.
8859        let mut budget = ByteBudget::new(self.max_query_bytes);
8860        let deferred = {
8861            let mut needed = alloc::collections::BTreeSet::new();
8862            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
8863            self.build_joined_filtered_rows(
8864                from,
8865                stmt.where_.as_ref(),
8866                cancel,
8867                if prunable { Some(&needed) } else { None },
8868                &mut budget,
8869            )?
8870        };
8871        let combined_schema = &deferred.combined_schema;
8872        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
8873        // `::regclass` / enum cast in a joined projection or HAVING needs it.
8874        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
8875        // the same predicate the unjoined shape carries.
8876        let joined_sess = self.dml_session();
8877        let ctx = EvalContext::new(combined_schema, None)
8878            .with_catalog(self.active_catalog())
8879            .with_session(&joined_sess);
8880        // Aggregate path: handle GROUP BY / aggregate calls over the
8881        // joined+filtered rows.
8882        if aggregate::uses_aggregate(stmt) {
8883            // v7.32 (P4 borrow channel, increment 2) — borrow each
8884            // surviving join tuple as a RowRef::Tuple; the aggregate
8885            // engine reads source cells by reference (bound fast path =
8886            // zero clone) instead of consuming materialised combined
8887            // Rows. This is where the +211k materialise_tuple_vals
8888            // clones disappear for the join+aggregate shape.
8889            let refs = deferred.row_refs();
8890            // v7.29 — a per-query memo so correlated scalar
8891            // subqueries batch-evaluate once (group map) instead of
8892            // executing per group.
8893            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
8894            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
8895                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
8896                    .map_err(|err| match err {
8897                        EngineError::Eval(ev) => ev,
8898                        other => eval::EvalError::TypeMismatch {
8899                            detail: alloc::format!("{other}"),
8900                        },
8901                    })
8902            };
8903            let agg = aggregate::run(
8904                stmt,
8905                crate::join::AggRows::Refs(&refs),
8906                combined_schema,
8907                None,
8908                Some(&agg_correlated),
8909                self.parallel_runner.0.as_deref(),
8910                Some(self.active_catalog()),
8911                Some(self),
8912            )?;
8913            return self.finish_agg_result(agg, stmt, cancel);
8914        }
8915
8916        let projection =
8917            build_projection(&stmt.items, combined_schema, "", self.backslash_escapes)?;
8918        // v7.39 (round 734) — a set-returning projection over a JOIN.
8919        // This executor's projection loop treats every item as a scalar,
8920        // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
8921        // "function unnest(integer[]) does not exist" where PG expands
8922        // it. The row-set executor already carries the full SRF pipeline
8923        // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
8924        // sharding): materialise the joined survivors and hand over. The
8925        // WHERE is cleared — the join already applied it, and combined
8926        // columns resolve identically in both executors.
8927        if !self.srf_target_idxs(&projection).is_empty() {
8928            let refs = deferred.row_refs();
8929            let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
8930            let mut s2 = stmt.clone();
8931            s2.where_ = None;
8932            let schema = combined_schema.clone();
8933            return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
8934        }
8935        // v7.33 (P4 borrow channel, increment 3) — project directly off
8936        // the deferred row-index tuples instead of materialising an
8937        // intermediate combined Row per survivor. A bound qualified
8938        // column is read by reference (`RowRef::get` → `tuple_value`) and
8939        // cloned ONCE into the output row; the old `materialise()` (a full
8940        // combined Row plus a source→intermediate clone per referenced
8941        // cell, for every survivor) is gone. A row materialises on demand
8942        // only when a projection or ORDER BY expression needs the eval
8943        // path (subquery / function / arithmetic / unqualified column).
8944        // Same bind-once classification the aggregate input fast path uses
8945        // (`accumulate_groups`), reading the same `tuple_value` mapping the
8946        // differential gate already covers.
8947        let refs = deferred.row_refs();
8948        let bound_pos = |e: &Expr| -> Option<usize> {
8949            match e {
8950                Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
8951                _ => None,
8952            }
8953        };
8954        let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
8955        let all_proj_bound = proj_pos.iter().all(Option::is_some);
8956        // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
8957        // pre-decompose each bound projection position into
8958        // `(source_k, col_in_source)` so the per-row column read
8959        // skips the per-cell `tuple_value` partition_point + slice
8960        // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
8961        // calls) that walk dominated; this version reaches into
8962        // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
8963        let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
8964            .iter()
8965            .map(|p| {
8966                p.and_then(|abs| {
8967                    let k = deferred
8968                        .offsets
8969                        .partition_point(|&o| o <= abs)
8970                        .checked_sub(1)?;
8971                    Some((k, abs - deferred.offsets[k]))
8972                })
8973            })
8974            .collect();
8975        // v7.39 (round 962) — which projection items are whole-row
8976        // references, and to which join source. The test is
8977        // `locate_column` declining the name, which is the SAME resolver
8978        // the evaluation path uses, so this cannot drift from it: a real
8979        // column carrying an alias's name resolves to a position and is
8980        // not reported here. The source index comes from the alias
8981        // prefix, the way the combined schema names its columns.
8982        let whole_row_src: Vec<Option<usize>> = projection
8983            .iter()
8984            .map(|p| {
8985                let Expr::Column(c) = &p.expr else {
8986                    return None;
8987                };
8988                if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
8989                    return None;
8990                }
8991                let prefix = alloc::format!("{name}.", name = c.name);
8992                let abs = deferred
8993                    .combined_schema
8994                    .iter()
8995                    .position(|s| s.name.starts_with(&prefix))?;
8996                deferred
8997                    .offsets
8998                    .partition_point(|&o| o <= abs)
8999                    .checked_sub(1)
9000            })
9001            .collect();
9002        // ORDER BY (when present) still evaluates against a materialised
9003        // Row — keep the order-key encoder correct rather than fork it.
9004        let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
9005        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
9006        let mut proj_memo = memoize::MemoizeCache::default();
9007        let sources_ref = &deferred.sources;
9008        let stride = deferred.stride;
9009        let survivors_ref = &deferred.survivors;
9010        let n_surv = survivors_ref.len() / stride.max(1);
9011        // v7.38 (read01 B8) — streaming top-N budget (see the sibling
9012        // single-table path). Bounds this JOIN projection's accumulator
9013        // to O(keep) for `ORDER BY … LIMIT k`.
9014        let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
9015            && !stmt.distinct
9016            && !stmt.limit_with_ties
9017            && !self.env_cfg().disable_topk
9018        {
9019            stmt.limit_literal().and_then(|l| {
9020                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
9021                (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
9022            })
9023        } else {
9024            None
9025        };
9026        // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
9027        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
9028            hashbrown::HashMap::new();
9029        let distinct_hb = hashbrown::DefaultHashBuilder::default();
9030        for surv_i in 0..n_surv {
9031            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
9032            let row = &refs[surv_i];
9033            let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
9034                Some(row.as_row())
9035            } else {
9036                None
9037            };
9038            let mut values = Vec::with_capacity(projection.len());
9039            for (i, p) in projection.iter().enumerate() {
9040                if let Some((k, col_in_src)) = proj_decomposed[i] {
9041                    // v7.36 — direct (source_k, col) lookup, no
9042                    // partition_point. tuple[k] is the row index in
9043                    // sources[k]; LEFT-NULL slots are `usize::MAX`.
9044                    let ri = tuple[k];
9045                    let v: Value<'static> = if ri == usize::MAX {
9046                        Value::Null
9047                    } else {
9048                        sources_ref[k]
9049                            .get(ri)
9050                            .and_then(|r| r.values.get(col_in_src))
9051                            .cloned()
9052                            .map(Value::into_owned)
9053                            .unwrap_or(Value::Null)
9054                    };
9055                    values.push(v);
9056                } else if let Some(pos) = proj_pos[i] {
9057                    // Bound but couldn't decompose (shouldn't normally
9058                    // happen — keep as a safe path).
9059                    values.push(
9060                        row.get(pos)
9061                            .cloned()
9062                            .map(Value::into_owned)
9063                            .unwrap_or(Value::Null),
9064                    );
9065                } else if let Some(k) = whole_row_src[i]
9066                    && tuple[k] == usize::MAX
9067                {
9068                    // v7.39 (round 962) — a whole-row reference to a side
9069                    // an OUTER join null-extended is NULL, not a
9070                    // composite whose fields are all NULL. PG18.4 answers
9071                    // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
9072                    // an empty cell; round 961 answered `(,)`.
9073                    //
9074                    // The evaluator below cannot tell the two apart: it
9075                    // reads the MATERIALISED combined row, where a
9076                    // null-extended side is indistinguishable from a real
9077                    // row whose every column is NULL — and that row is
9078                    // `(,)` in PG too, so guessing by "all fields NULL"
9079                    // would trade one wrong answer for another. The
9080                    // tuple, which is still in hand here, does know:
9081                    // `usize::MAX` is the sentinel the join writes for
9082                    // exactly this.
9083                    values.push(Value::Null);
9084                } else {
9085                    // Eval path — `materialised` is Some whenever any
9086                    // projection item is non-bound (need_eval_row true).
9087                    // v7.24 (round-16 B) — select-list subqueries under a
9088                    // JOIN go through the correlated-aware evaluator too.
9089                    let mrow = materialised.as_deref().expect("materialised for eval");
9090                    values.push(self.eval_expr_with_correlated(
9091                        &p.expr,
9092                        mrow,
9093                        &ctx,
9094                        cancel,
9095                        Some(&mut proj_memo),
9096                    )?);
9097                }
9098            }
9099            let out_row = Row::new(values);
9100            // v7.37.16 — streaming DISTINCT (see the scan-path twin):
9101            // probe on the projected row; duplicates skip the
9102            // build_order_keys eval and never enter `tagged`.
9103            if stmt.distinct {
9104                let bucket = seen_distinct
9105                    .entry(norm_hash_row(&out_row, &distinct_hb, ctx.mysql_dialect))
9106                    .or_default();
9107                if bucket
9108                    .iter()
9109                    .any(|i| row_eq_norm(&tagged[i].1, &out_row, ctx.mysql_dialect))
9110                {
9111                    continue;
9112                }
9113                bucket.push(tagged.len());
9114            }
9115            let order_keys = if stmt.order_by.is_empty() {
9116                Vec::new()
9117            } else {
9118                let mrow = materialised.as_deref().expect("materialised for order by");
9119                build_order_keys(&stmt.order_by, mrow, &ctx)?
9120            };
9121            budget.charge(approx_row_bytes(&out_row))?;
9122            tagged.push((order_keys, out_row));
9123            if let Some((k, descs)) = &topk_stream {
9124                topk_trim(&mut tagged, *k, descs);
9125            }
9126        }
9127        if !stmt.order_by.is_empty() {
9128            // v7.38 元机制 D acceptor — see other call site above.
9129            let keep = if self.env_cfg().disable_topk {
9130                None
9131            } else {
9132                stmt.limit_literal()
9133                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
9134            };
9135            let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
9136            // v7.39 (round 688) — the join's ORDER BY resolves its keys
9137            // against `ctx`, which is built from `build_combined_schema`, so
9138            // this is where a declared collation reaches the sort. There was
9139            // exactly ONE resolver call in the engine before this — the
9140            // single-table scan's — which is why every other shape sorted by
9141            // bytes no matter what the schemas carried.
9142            let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
9143            crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &colls);
9144        }
9145        let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
9146        apply_offset_and_limit(
9147            &mut output_rows,
9148            stmt.offset_literal(),
9149            stmt.limit_literal(),
9150        );
9151        let columns: Vec<ColumnSchema> = projection
9152            .into_iter()
9153            .map(|p| {
9154                let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
9155                c.user_enum_type = p.user_enum_type;
9156                c.collation_name = p.collation_name;
9157                c.mysql_fsp = p.mysql_fsp;
9158                c
9159            })
9160            .collect();
9161        Ok(QueryResult::Rows {
9162            columns,
9163            rows: output_rows,
9164        })
9165    }
9166}
9167
9168impl Engine {
9169    /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
9170    /// by id, decodes each row body against the table's current
9171    /// schema, applies the SELECT's projection + optional WHERE +
9172    /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
9173    /// / ORDER BY are unsupported on this path (STABILITY carve-
9174    /// out); operators wanting them should restore the segment
9175    /// into a regular table first.
9176    fn exec_select_as_of_segment(
9177        &self,
9178        stmt: &SelectStatement,
9179        from: &spg_sql::ast::FromClause,
9180        segment_id: u32,
9181    ) -> Result<QueryResult, EngineError> {
9182        // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
9183        // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
9184        if !from.joins.is_empty()
9185            || stmt.group_by.is_some()
9186            || stmt.having.is_some()
9187            || !stmt.unions.is_empty()
9188            || !stmt.order_by.is_empty()
9189            || stmt.offset.is_some()
9190            || stmt.distinct
9191            || aggregate::uses_aggregate(stmt)
9192        {
9193            return Err(EngineError::Unsupported(
9194                "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
9195                 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
9196                    .into(),
9197            ));
9198        }
9199        let table = self
9200            .active_catalog()
9201            .get(&from.primary.name)
9202            .ok_or_else(|| StorageError::TableNotFound {
9203                name: from.primary.name.clone(),
9204            })?;
9205        let schema = table.schema().clone();
9206        let schema_cols = &schema.columns;
9207        let alias = from
9208            .primary
9209            .alias
9210            .as_deref()
9211            .unwrap_or(from.primary.name.as_str());
9212        let ctx = self.ev_ctx(schema_cols, Some(alias));
9213        let seg = self
9214            .active_catalog()
9215            .cold_segment(segment_id)
9216            .ok_or_else(|| {
9217                EngineError::Unsupported(alloc::format!(
9218                    "AS OF SEGMENT: cold segment {segment_id} not registered"
9219                ))
9220            })?;
9221        let mut out_rows: Vec<Row<'static>> = Vec::new();
9222        let mut limit_remaining: Option<usize> =
9223            stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
9224        for (_key, body) in seg.scan() {
9225            let (row, _consumed) =
9226                spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
9227                    .map_err(EngineError::Storage)?;
9228            if let Some(where_expr) = &stmt.where_ {
9229                let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
9230                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9231                    continue;
9232                }
9233            }
9234            // Projection.
9235            let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
9236            out_rows.push(projected);
9237            if let Some(rem) = limit_remaining.as_mut() {
9238                if *rem == 0 {
9239                    out_rows.pop();
9240                    break;
9241                }
9242                *rem -= 1;
9243            }
9244        }
9245        // Output column schema: derive from SELECT items.
9246        let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
9247        Ok(QueryResult::Rows {
9248            columns,
9249            rows: out_rows,
9250        })
9251    }
9252
9253    /// v6.10.2 — simple-path WHERE eval that doesn't go through
9254    /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
9255    /// scan paths predicate against a snapshot frozen segment, no
9256    /// cross-row state.
9257    fn eval_expr_simple(
9258        &self,
9259        expr: &Expr,
9260        row: &Row<'static>,
9261        ctx: &EvalContext,
9262    ) -> Result<Value<'static>, EngineError> {
9263        let cancel = CancelToken::none();
9264        self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
9265    }
9266}
9267
9268// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
9269
9270/// One row-producing projection: an expression to evaluate, the resulting
9271/// column's user-visible name, its inferred type, and nullability.
9272#[derive(Debug, Clone)]
9273pub(crate) struct ProjectedItem {
9274    pub(crate) expr: Expr,
9275    pub(crate) output_name: String,
9276    pub(crate) ty: DataType,
9277    pub(crate) nullable: bool,
9278    /// v7.39 (read01 round 54) — a projected enum column keeps its enum
9279    /// identity. Enum-ness lives outside the DataType lattice (the value is a
9280    /// Text), so a projection that dropped this made the RESULT schema forget
9281    /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
9282    /// that schema, silently fell back to TEXT order instead of member order.
9283    pub(crate) user_enum_type: Option<String>,
9284    /// v7.39 (round 425) — a projected MySQL temporal column keeps its
9285    /// declared fractional-seconds precision, so the renderer can pad to
9286    /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
9287    /// a whole second). Like `user_enum_type` this lives outside the
9288    /// DataType lattice, so a projection that dropped it made the RESULT
9289    /// schema forget how wide the fraction should print.
9290    pub(crate) mysql_fsp: Option<u8>,
9291    /// v7.39 (round 688) — and its declared collation, the third thing to
9292    /// live outside the DataType lattice and the third to be lost the same
9293    /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
9294    /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
9295    /// projection rebuilt the output column and the ORDER BY resolves
9296    /// against THAT schema.
9297    pub(crate) collation_name: Option<String>,
9298}
9299
9300/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
9301/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
9302/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
9303/// the spec's "two NULLs are not distinct"; the second is a tolerated
9304/// quirk for v1 (no NaN literals are reachable from the SQL surface).
9305/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
9306fn expr_is_aggregate_call(e: &Expr) -> bool {
9307    match e {
9308        Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
9309        Expr::AggregateOrdered { .. } => true,
9310        _ => false,
9311    }
9312}
9313
9314/// Collect distinct top-level aggregate call expressions (dedup by value). Does
9315/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
9316/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
9317/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
9318/// than today — never a regression on a working query).
9319fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
9320    if expr_is_aggregate_call(e) {
9321        if !out.iter().any(|x| x == e) {
9322            out.push(e.clone());
9323        }
9324        return;
9325    }
9326    match e {
9327        Expr::Binary { lhs, rhs, .. } => {
9328            collect_agg_exprs(lhs, out);
9329            collect_agg_exprs(rhs, out);
9330        }
9331        Expr::Unary { expr, .. }
9332        | Expr::Cast { expr, .. }
9333        | Expr::IsNull { expr, .. }
9334        | Expr::BoolTest { expr, .. }
9335        | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
9336        Expr::FunctionCall { args, .. } => {
9337            for a in args {
9338                collect_agg_exprs(a, out);
9339            }
9340        }
9341        Expr::Like { expr, pattern, .. } => {
9342            collect_agg_exprs(expr, out);
9343            collect_agg_exprs(pattern, out);
9344        }
9345        Expr::Extract { source, .. } => collect_agg_exprs(source, out),
9346        Expr::WindowFunction {
9347            args,
9348            partition_by,
9349            order_by,
9350            ..
9351        } => {
9352            for a in args {
9353                collect_agg_exprs(a, out);
9354            }
9355            for p in partition_by {
9356                collect_agg_exprs(p, out);
9357            }
9358            for (o, _, _) in order_by {
9359                collect_agg_exprs(o, out);
9360            }
9361        }
9362        _ => {}
9363    }
9364}
9365
9366/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
9367fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
9368    if expr_is_aggregate_call(e) {
9369        if let Some(idx) = aggs.iter().position(|x| x == e) {
9370            *e = Expr::Column(ColumnName {
9371                qualifier: None,
9372                name: alloc::format!("__agg{idx}"),
9373            });
9374        }
9375        return;
9376    }
9377    match e {
9378        Expr::Binary { lhs, rhs, .. } => {
9379            replace_agg_exprs(lhs, aggs);
9380            replace_agg_exprs(rhs, aggs);
9381        }
9382        Expr::Unary { expr, .. }
9383        | Expr::Cast { expr, .. }
9384        | Expr::IsNull { expr, .. }
9385        | Expr::BoolTest { expr, .. }
9386        | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
9387        Expr::FunctionCall { args, .. } => {
9388            for a in args {
9389                replace_agg_exprs(a, aggs);
9390            }
9391        }
9392        Expr::Like { expr, pattern, .. } => {
9393            replace_agg_exprs(expr, aggs);
9394            replace_agg_exprs(pattern, aggs);
9395        }
9396        Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
9397        Expr::WindowFunction {
9398            args,
9399            partition_by,
9400            order_by,
9401            ..
9402        } => {
9403            for a in args {
9404                replace_agg_exprs(a, aggs);
9405            }
9406            for p in partition_by {
9407                replace_agg_exprs(p, aggs);
9408            }
9409            for (o, _, _) in order_by {
9410                replace_agg_exprs(o, aggs);
9411            }
9412        }
9413        _ => {}
9414    }
9415}
9416
9417/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
9418/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
9419/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
9420/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
9421/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
9422/// Returns None outside the bounded subset (leaves current behaviour). Only fires
9423/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
9424/// window-only / aggregate-only queries.
9425fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
9426    if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
9427        return None;
9428    }
9429    // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
9430    if !stmt.unions.is_empty() {
9431        return None;
9432    }
9433    let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
9434    if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
9435        return None;
9436    }
9437    stmt.from.as_ref()?;
9438    // Collect the aggregate calls to hoist from projection + outer ORDER BY.
9439    let mut aggs: Vec<Expr> = Vec::new();
9440    for item in &stmt.items {
9441        if let SelectItem::Expr { expr, .. } = item {
9442            collect_agg_exprs(expr, &mut aggs);
9443        }
9444    }
9445    for ob in &stmt.order_by {
9446        collect_agg_exprs(&ob.expr, &mut aggs);
9447    }
9448    // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
9449    let mut inner_items: Vec<SelectItem> = Vec::new();
9450    for g in &group_cols {
9451        inner_items.push(SelectItem::Expr {
9452            expr: g.clone(),
9453            alias: None,
9454        });
9455    }
9456    for (i, a) in aggs.iter().enumerate() {
9457        inner_items.push(SelectItem::Expr {
9458            expr: a.clone(),
9459            alias: Some(alloc::format!("__agg{i}")),
9460        });
9461    }
9462    let inner = SelectStatement {
9463        items: inner_items,
9464        distinct: false,
9465        distinct_on: Vec::new(),
9466        unions: Vec::new(),
9467        order_by: Vec::new(),
9468        limit: None,
9469        offset: None,
9470        limit_with_ties: false,
9471        window_check_exprs: Vec::new(),
9472        ..stmt.clone()
9473    };
9474    let derived = TableRef {
9475        name: "__aggwin".into(),
9476        alias: Some("__aggwin".into()),
9477        only: false,
9478        as_of_segment: None,
9479        unnest_expr: None,
9480        unnest_column_aliases: Vec::new(),
9481        with_ordinality: false,
9482        generate_series_args: None,
9483        lateral_subquery: Some(alloc::boxed::Box::new(inner)),
9484        jsonb_each_text_arg: None,
9485        table_fn_call: None,
9486        rows_from: None,
9487        json_table: None,
9488        scalar_fn_item: false,
9489    };
9490    // Outer window query over the derived rows: aggregates → __aggN column refs.
9491    let mut outer_items = stmt.items.clone();
9492    for item in &mut outer_items {
9493        if let SelectItem::Expr { expr, alias } = item {
9494            // Preserve PG's column label for a bare aggregate projection.
9495            if alias.is_none()
9496                && let Expr::FunctionCall { name, .. } = expr
9497                && crate::aggregate::is_aggregate_name(name)
9498            {
9499                *alias = Some(name.to_ascii_lowercase());
9500            }
9501            replace_agg_exprs(expr, &aggs);
9502        }
9503    }
9504    let mut outer_order = stmt.order_by.clone();
9505    for ob in &mut outer_order {
9506        replace_agg_exprs(&mut ob.expr, &aggs);
9507    }
9508    let mut outer_distinct_on = stmt.distinct_on.clone();
9509    for e in &mut outer_distinct_on {
9510        replace_agg_exprs(e, &aggs);
9511    }
9512    Some(SelectStatement {
9513        locking: None,
9514        ctes: Vec::new(),
9515        distinct: stmt.distinct,
9516        distinct_on: outer_distinct_on,
9517        items: outer_items,
9518        from: Some(FromClause {
9519            primary: derived,
9520            joins: Vec::new(),
9521        }),
9522        where_: None,
9523        group_by: None,
9524        group_by_all: false,
9525        having: None,
9526        unions: Vec::new(),
9527        order_by: outer_order,
9528        limit: stmt.limit.clone(),
9529        offset: stmt.offset.clone(),
9530        limit_with_ties: stmt.limit_with_ties,
9531        window_check_exprs: Vec::new(),
9532    })
9533}
9534
9535/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
9536/// membership.
9537///
9538/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
9539/// there?", and all four answered by scanning the whole right side once per
9540/// left row. The cost was (left rows x right rows), which is why
9541/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
9542/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
9543/// row that does not pays for all of it. Over 100k left rows, raising the
9544/// right side from 100 to 10,000 took 35 ms to 2848.
9545///
9546/// This is the shape round 485 already solved for DISTINCT, and it reuses
9547/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
9548/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
9549/// every bucket with the exact comparator, so a collision costs time and
9550/// never an answer.
9551struct PeerIndex<'r> {
9552    bh: hashbrown::DefaultHashBuilder,
9553    buckets: hashbrown::HashMap<u64, Vec<usize>>,
9554    rows: &'r [Row<'static>],
9555    mysql: bool,
9556}
9557
9558impl<'r> PeerIndex<'r> {
9559    fn build(rows: &'r [Row<'static>], mysql: bool) -> Self {
9560        // ONE hasher for the whole pass: the default builder is seeded per
9561        // instance, so a fresh one per row would put equal rows in different
9562        // buckets.
9563        let bh = hashbrown::DefaultHashBuilder::default();
9564        let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
9565            hashbrown::HashMap::with_capacity(rows.len());
9566        for (i, r) in rows.iter().enumerate() {
9567            buckets
9568                .entry(norm_hash_row(r, &bh, mysql))
9569                .or_default()
9570                .push(i);
9571        }
9572        Self {
9573            bh,
9574            buckets,
9575            rows,
9576            mysql,
9577        }
9578    }
9579
9580    fn contains(&self, r: &Row<'static>) -> bool {
9581        let h = norm_hash_row(r, &self.bh, self.mysql);
9582        self.buckets
9583            .get(&h)
9584            .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.mysql)))
9585    }
9586
9587    /// Remove ONE occurrence, so the multiset forms cancel row for row the
9588    /// way the pool they replaced did.
9589    fn take_one(&mut self, r: &Row<'static>) -> bool {
9590        let h = norm_hash_row(r, &self.bh, self.mysql);
9591        let Some(b) = self.buckets.get_mut(&h) else {
9592            return false;
9593        };
9594        let Some(pos) = b
9595            .iter()
9596            .position(|&i| row_eq_norm(&self.rows[i], r, self.mysql))
9597        else {
9598            return false;
9599        };
9600        b.swap_remove(pos);
9601        true
9602    }
9603}
9604
9605pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, mysql: bool) -> Vec<Row<'static>> {
9606    dedup_by_row(rows, |r| r, mysql)
9607}
9608
9609/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
9610/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
9611/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
9612/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
9613/// order is preserved, and correctness needs only the one-way guarantee
9614/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
9615/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
9616fn dedup_by_row<T>(items: Vec<T>, row_of: impl Fn(&T) -> &Row<'static>, mysql: bool) -> Vec<T> {
9617    if items.len() <= 32 {
9618        let mut out: Vec<T> = Vec::with_capacity(items.len());
9619        for it in items {
9620            if !out
9621                .iter()
9622                .any(|seen| row_eq_norm(row_of(seen), row_of(&it), mysql))
9623            {
9624                out.push(it);
9625            }
9626        }
9627        return out;
9628    }
9629    // ONE BuildHasher instance for the whole pass — the default builder
9630    // is randomly seeded PER INSTANCE, so a fresh one per row would give
9631    // equal rows different hashes and never dedup.
9632    let bh = hashbrown::DefaultHashBuilder::default();
9633    let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
9634    let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
9635        hashbrown::HashMap::with_capacity(items.len());
9636    for it in items {
9637        let h = norm_hash_row(row_of(&it), &bh, mysql);
9638        let bucket = buckets.entry(h).or_default();
9639        if !bucket
9640            .iter()
9641            .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), mysql))
9642        {
9643            bucket.push(out.len());
9644            out.push(it);
9645        }
9646    }
9647    out
9648}
9649
9650/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
9651/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
9652/// rows may collide (buckets are re-checked with the exact comparator).
9653///
9654/// Domain design mirrors `value_cmp`'s equivalence classes:
9655/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
9656///   shares one domain: a value that is an integer fitting i64 hashes the
9657///   i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
9658///   anything else hashes the f64 approximation computed by THE SAME
9659///   formula the value_cmp float arms use (`numeric_to_f64`), so
9660///   `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
9661///   hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
9662///   Known un-closable corner: an integer in [2^53, 2^63) can compare
9663///   Equal to a float via value_cmp's lossy f64 arm while hashing in the
9664///   exact-i64 domain — mixed int/float rows at that magnitude may miss a
9665///   dedup (PG itself compares int8↔float8 in the lossy float8 domain).
9666/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
9667///   compares them blank-insensitively; plain Text pairs that differ only
9668///   in trailing blanks merely collide and are separated exactly).
9669/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
9670///   hash their fields under a distinct tag.
9671/// - Everything value_cmp falls back to debug-format ordering for
9672///   (Json, arrays, vectors, geometry, ranges, …) shares one constant
9673///   bucket — degrades to the exact linear scan, never wrong.
9674fn norm_hash_row(row: &Row<'static>, bh: &hashbrown::DefaultHashBuilder, mysql: bool) -> u64 {
9675    norm_hash_values(&row.values, bh, mysql)
9676}
9677
9678/// v7.39 (round 485) — the same hash over a bare value slice, so the
9679/// DISTINCT probe can run against a reused buffer instead of demanding a
9680/// `Row` that has to be allocated first (see `values_eq_norm`).
9681fn norm_hash_values(
9682    values: &[Value<'static>],
9683    bh: &hashbrown::DefaultHashBuilder,
9684    mysql: bool,
9685) -> u64 {
9686    use core::hash::{BuildHasher, Hash, Hasher};
9687    let mut h = bh.build_hasher();
9688    for v in values {
9689        // v7.39 (round 410) — hash the folded key when the MySQL collation
9690        // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
9691        // `'A'` vs `'a '`) share a hash bucket.
9692        if mysql {
9693            if let Some(folded) = mysql_dedup_fold(v) {
9694                folded.hash(&mut h);
9695                continue;
9696            }
9697        }
9698        norm_hash_value(v, &mut h);
9699    }
9700    h.finish()
9701}
9702
9703/// r1044 — `10^p` as an `i128`, or `None` past what one holds.
9704///
9705/// `i128::MAX` is about 1.7e38, so 10^38 is the last power that fits.
9706const fn pow10_i128(p: u16) -> Option<i128> {
9707    const P: [i128; 39] = {
9708        let mut t = [1i128; 39];
9709        let mut i = 1;
9710        while i < 39 {
9711            t[i] = t[i - 1] * 10;
9712            i += 1;
9713        }
9714        t
9715    };
9716    if (p as usize) < P.len() {
9717        Some(P[p as usize])
9718    } else {
9719        None
9720    }
9721}
9722
9723fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
9724    const TAG_NULL: u8 = 0;
9725    const TAG_BOOL: u8 = 1;
9726    const TAG_NUM_I64: u8 = 2;
9727    const TAG_NUM_F64: u8 = 3;
9728    const TAG_TEXT: u8 = 4;
9729    const TAG_DATE: u8 = 6;
9730    const TAG_TIME: u8 = 7;
9731    const TAG_TIMESTAMP: u8 = 8;
9732    const TAG_TIMETZ: u8 = 10;
9733    const TAG_UUID: u8 = 11;
9734    const TAG_MONEY: u8 = 12;
9735    const TAG_BYTES: u8 = 13;
9736    const TAG_INTERVAL: u8 = 14;
9737    const TAG_CHAR1: u8 = 15;
9738    const TAG_OPAQUE: u8 = 255;
9739    // One shared writer for the numeric family: an integer value
9740    // representable as i64 goes exact (round-trip probe — no_std, so no
9741    // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
9742    // through 0i64, folding it into 0.0 as value_cmp requires.
9743    let num_f64 = |h: &mut H, x: f64| {
9744        if x.is_nan() {
9745            h.write_u8(TAG_NUM_F64);
9746            h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
9747            return;
9748        }
9749        const TWO63: f64 = 9_223_372_036_854_775_808.0;
9750        if (-TWO63..TWO63).contains(&x) {
9751            #[allow(clippy::cast_possible_truncation)]
9752            let n = x as i64;
9753            #[allow(clippy::cast_precision_loss)]
9754            if (n as f64) == x {
9755                h.write_u8(TAG_NUM_I64);
9756                h.write_i64(n);
9757                return;
9758            }
9759        }
9760        h.write_u8(TAG_NUM_F64);
9761        h.write_u64(x.to_bits());
9762    };
9763    match v {
9764        Value::Null => h.write_u8(TAG_NULL),
9765        Value::Bool(b) => {
9766            h.write_u8(TAG_BOOL);
9767            h.write_u8(u8::from(*b));
9768        }
9769        Value::SmallInt(n) => {
9770            h.write_u8(TAG_NUM_I64);
9771            h.write_i64(i64::from(*n));
9772        }
9773        Value::Int(n) => {
9774            h.write_u8(TAG_NUM_I64);
9775            h.write_i64(i64::from(*n));
9776        }
9777        Value::BigInt(n) => {
9778            h.write_u8(TAG_NUM_I64);
9779            h.write_i64(*n);
9780        }
9781        Value::Float(x) => num_f64(h, *x),
9782        Value::Numeric {
9783            scaled,
9784            scale,
9785            kind,
9786        } => match kind {
9787            spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
9788            spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
9789            spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
9790            spg_storage::NumericKind::Finite => {
9791                // Reduce trailing fractional zeros so 1.50 and 1.5 share a
9792                // representation, then: exact integers fitting i64 go to the
9793                // i64 domain; everything else uses numeric_to_f64 — the SAME
9794                // formula value_cmp's Numeric↔Float arm compares with.
9795                // r1044 — the reduction is required (`1.5` and `1.50` are
9796                // one value and must land in one bucket) and it used to
9797                // walk one digit at a time. That is O(scale), and scale
9798                // is not small in practice: `n / 100` on a NUMERIC
9799                // column stores `9.1900000000000000`, scale 16, so the
9800                // loop ran fourteen times PER ROW.
9801                //
9802                // Priced by ablation rather than guessed at — removing
9803                // the loop entirely took `SELECT DISTINCT n FROM t ORDER
9804                // BY n` over 400,000 rows from 52 ms to 14.8, against
9805                // PostgreSQL's 12.2-13.8. Two `pow10` lookup tables
9806                // tried first moved it not at all, which is why this one
9807                // was measured before it was written.
9808                //
9809                // Binary search over the same powers finds the whole
9810                // run of trailing zeros in at most six tests and one
9811                // division, instead of one test and one division per
9812                // digit.
9813                let (mut s, mut sc) = (*scaled, *scale);
9814                if sc > 0 && s != 0 {
9815                    let mut lo: u16 = 0;
9816                    let mut hi: u16 = sc;
9817                    while lo < hi {
9818                        let mid = (lo + hi).div_ceil(2);
9819                        match pow10_i128(mid) {
9820                            Some(p) if s % p == 0 => lo = mid,
9821                            _ => hi = mid - 1,
9822                        }
9823                    }
9824                    if lo > 0 {
9825                        if let Some(p) = pow10_i128(lo) {
9826                            s /= p;
9827                            sc -= lo;
9828                        }
9829                    }
9830                }
9831                if sc == 0 {
9832                    if let Ok(n) = i64::try_from(s) {
9833                        h.write_u8(TAG_NUM_I64);
9834                        h.write_i64(n);
9835                    } else {
9836                        num_f64(h, crate::orderby::numeric_to_f64(s, 0));
9837                    }
9838                } else {
9839                    num_f64(h, crate::orderby::numeric_to_f64(s, sc));
9840                }
9841            }
9842        },
9843        // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
9844        // value that also fits i128 reuses the Numeric path above so
9845        // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
9846        // any i128-representable value — constant bucket is safe.
9847        Value::NumericBig(b) => match b.to_i128() {
9848            Some(s) => norm_hash_value(
9849                &Value::Numeric {
9850                    scaled: s,
9851                    scale: b.scale(),
9852                    kind: spg_storage::NumericKind::Finite,
9853                },
9854                h,
9855            ),
9856            None => h.write_u8(TAG_OPAQUE),
9857        },
9858        // value_cmp compares Text↔BpChar blank-insensitively (both sides
9859        // trimmed), so both hash the trimmed bytes. Text pairs differing
9860        // only in trailing blanks collide and are split exactly in-bucket.
9861        Value::Text(s) | Value::BpChar(s) => {
9862            h.write_u8(TAG_TEXT);
9863            h.write(s.trim_end_matches(' ').as_bytes());
9864        }
9865        Value::Char1(c) => {
9866            h.write_u8(TAG_CHAR1);
9867            h.write_u8(*c);
9868        }
9869        Value::Date(d) => {
9870            h.write_u8(TAG_DATE);
9871            h.write_i32(*d);
9872        }
9873        Value::Time(t) => {
9874            h.write_u8(TAG_TIME);
9875            h.write_i64(*t);
9876        }
9877        Value::Timestamp(t) => {
9878            h.write_u8(TAG_TIMESTAMP);
9879            h.write_i64(*t);
9880        }
9881        Value::TimeTz { us, offset_secs } => {
9882            h.write_u8(TAG_TIMETZ);
9883            h.write_i64(*us);
9884            h.write_i32(*offset_secs);
9885        }
9886        Value::Uuid(u) => {
9887            h.write_u8(TAG_UUID);
9888            h.write(u);
9889        }
9890        Value::Money(c) => {
9891            h.write_u8(TAG_MONEY);
9892            h.write_i64(*c);
9893        }
9894        Value::Bytes(b) => {
9895            h.write_u8(TAG_BYTES);
9896            h.write(b.as_ref());
9897        }
9898        Value::Interval {
9899            months,
9900            days,
9901            micros,
9902        } => {
9903            h.write_u8(TAG_INTERVAL);
9904            h.write_i32(*months);
9905            h.write_i32(*days);
9906            h.write_i64(*micros);
9907        }
9908        // v7.37.16 — REAL joined the numeric value_cmp family (widened
9909        // to f64, same formulas as the arms), so it hashes in the shared
9910        // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
9911        // f32→f64 is exact, so equal-under-cmp implies equal bits here.
9912        Value::Real(x) => num_f64(h, f64::from(*x)),
9913        // Json (structural equality), vector families (float rendering),
9914        // arrays / geometry / net / ranges / composites (debug-format
9915        // fallback): one constant bucket — exact linear within.
9916        _ => h.write_u8(TAG_OPAQUE),
9917    }
9918}
9919
9920/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
9921/// treats numerically-equal exact values as one regardless of type or scale
9922/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
9923/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
9924/// `Row` `==` would keep them distinct.
9925/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
9926/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
9927/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
9928/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
9929/// the folded comparison key for a text value, None for anything else (which
9930/// keeps the byte-exact `value_cmp` path).
9931fn mysql_dedup_fold(v: &Value) -> Option<String> {
9932    match v {
9933        Value::Text(s) | Value::BpChar(s) => {
9934            Some(spg_storage::mysql_ci_fold(s.trim_end_matches(' ')))
9935        }
9936        _ => None,
9937    }
9938}
9939
9940/// v7.39 (round 485) — how many projected rows the single-table scan
9941/// builds, and how many of those the DISTINCT probe throws away again.
9942///
9943/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
9944/// 21 % of all samples in malloc/free called straight from the scan
9945/// closure. The closure's one per-row allocation is the projected
9946/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
9947/// instructions later — but "most" is a guess until it is a number, so
9948/// these count it. (Round 480 was spent acting on an inference about a
9949/// branch that turned out never to run.)
9950/// v7.39 (round 488) — reachability counters for round 487's projection
9951/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
9952/// and a never-called-function probe rules out code layout — so the
9953/// question is whether that shape reaches this code at all, which is a
9954/// number, not an inference.
9955pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
9956pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
9957
9958pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
9959pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
9960    core::sync::atomic::AtomicU64::new(0);
9961
9962pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, mysql: bool) -> bool {
9963    values_eq_norm(&a.values, &b.values, mysql)
9964}
9965
9966/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
9967/// DISTINCT probe can compare a reused projection buffer against a kept
9968/// row without building a `Row` for it.
9969pub(crate) fn values_eq_norm(a: &[Value<'static>], b: &[Value<'static>], mysql: bool) -> bool {
9970    a.len() == b.len()
9971        && a.iter().zip(b).all(|(x, y)| {
9972            if mysql {
9973                if let (Some(fx), Some(fy)) = (mysql_dedup_fold(x), mysql_dedup_fold(y)) {
9974                    return fx == fy;
9975                }
9976            }
9977            crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
9978        })
9979}
9980
9981/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
9982/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
9983/// order via the byte values; vectors are not sortable.
9984pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
9985    // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
9986    // so values sharing a ≥6-byte common prefix (`product_001` vs
9987    // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
9988    // order by their exact bytes instead of the old lossy f64 coarse key.
9989    // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
9990    // matches PG's default C / binary text collation. Every other type
9991    // keeps the lossless-enough `f64` fast path below.
9992    if let Value::Text(s) = v {
9993        return Ok(OrderKey::Text(s.as_ref().into()));
9994    }
9995    // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
9996    // byte order (PG bpcharcmp under C collation), so mixed-pad values of
9997    // the same logical string order equal.
9998    if let Value::BpChar(s) = v {
9999        return Ok(OrderKey::Text(s.trim_end_matches(' ').into()));
10000    }
10001    // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
10002    // carry the parsed value and compare it structurally (see
10003    // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
10004    if let Value::Json(s) = v {
10005        return Ok(match crate::json::parse(s) {
10006            Ok(jv) => OrderKey::Json(jv),
10007            Err(_) => OrderKey::Text(s.as_ref().into()),
10008        });
10009    }
10010    // v7.37 — byte-orderable types PG sorts byte-wise but that have no
10011    // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
10012    // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
10013    // matching PG's network ordering.
10014    match v {
10015        Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
10016        // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
10017        Value::NumericBig(b) => {
10018            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
10019                spg_storage::NumericKey::from_big(b),
10020            )));
10021        }
10022        Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
10023        Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
10024        Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
10025        Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
10026        Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
10027            let mut key = alloc::vec::Vec::with_capacity(18);
10028            key.push(*family);
10029            key.extend_from_slice(addr);
10030            key.push(*bits);
10031            return Ok(OrderKey::Bytes(key));
10032        }
10033        _ => {}
10034    }
10035    // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
10036    // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
10037    // own OrderKey so integer arrays sort numerically; a NULL element rides to
10038    // the end via the +INF sentinel.
10039    let inf = || OrderKey::NullBig;
10040    let arr = match v {
10041        Value::IntArray(a) => Some(
10042            a.iter()
10043                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10044                .collect(),
10045        ),
10046        Value::SmallIntArray(a) => Some(
10047            a.iter()
10048                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10049                .collect(),
10050        ),
10051        Value::BigIntArray(a) => Some(
10052            a.iter()
10053                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10054                .collect(),
10055        ),
10056        Value::BoolArray(a) => Some(
10057            a.iter()
10058                .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
10059                .collect(),
10060        ),
10061        Value::TextArray(a) => Some(
10062            a.iter()
10063                .map(|o| o.as_ref().map_or_else(inf, |s| OrderKey::Text(s.clone())))
10064                .collect(),
10065        ),
10066        #[allow(clippy::cast_precision_loss)]
10067        Value::FloatArray(a) => Some(
10068            a.iter()
10069                .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
10070                .collect(),
10071        ),
10072        // r1040 — array elements take the same exact key their scalar
10073        // form does; an f64 projection here would order `{0.1}` against
10074        // `{0.1000000000000000001}` by luck.
10075        Value::NumericArray(a) => Some(
10076            a.iter()
10077                .map(|o| {
10078                    o.map_or_else(inf, |(m, s)| {
10079                        OrderKey::Numeric(alloc::boxed::Box::new(
10080                            spg_storage::NumericKey::from_numeric(
10081                                m,
10082                                s,
10083                                spg_storage::NumericKind::Finite,
10084                            ),
10085                        ))
10086                    })
10087                })
10088                .collect(),
10089        ),
10090        Value::DateArray(a) => Some(
10091            a.iter()
10092                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10093                .collect(),
10094        ),
10095        _ => None,
10096    };
10097    if let Some(elements) = arr {
10098        return Ok(OrderKey::Array(elements));
10099    }
10100    // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
10101    // right, which is exactly the lexicographic element order an Array key
10102    // already gives: `(2,'b') < (9,'a')` because the leading field decides.
10103    if let Value::Composite(fields) = v {
10104        let elements = fields
10105            .iter()
10106            .map(|(_, fv)| value_to_order_key(fv))
10107            .collect::<Result<alloc::vec::Vec<_>, _>>()?;
10108        return Ok(OrderKey::Array(elements));
10109    }
10110    // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
10111    // Projecting these to f64 (the historic path) silently collapses BigInt /
10112    // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
10113    // the wrong order for large ids and microsecond timestamps.
10114    match v {
10115        Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
10116        Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
10117        Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
10118        // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
10119        // integer (days / micros / cents / calendar year); TIMETZ by the
10120        // UTC-equivalent micros (local wall - offset) so the same physical
10121        // instant in different zones sorts equal.
10122        Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
10123        Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
10124        Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
10125        Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
10126        Value::TimeTz { us, offset_secs } => {
10127            return Ok(OrderKey::Int(
10128                i128::from(*us) - i128::from(*offset_secs) * 1_000_000,
10129            ));
10130        }
10131        Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
10132        _ => {}
10133    }
10134    let num = match v {
10135        // Callers without NULLS FIRST/LAST context (array elements,
10136        // histogram sampling) put NULL last, as before.
10137        Value::Null => return Ok(OrderKey::NullBig),
10138        // v7.17.0 Phase 3.P0-38 — range ordering is not supported
10139        // in v7.17.0 (needs lex-then-inclusivity tiebreak).
10140        Value::Range { .. } => {
10141            return Err(EngineError::Unsupported(
10142                "ORDER BY of a range value is not supported in v7.17.0".into(),
10143            ));
10144        }
10145        // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
10146        Value::Hstore(_) => {
10147            return Err(EngineError::Unsupported(
10148                "ORDER BY of a hstore value is not supported".into(),
10149            ));
10150        }
10151        // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
10152        Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
10153            return Err(EngineError::Unsupported(
10154                "ORDER BY of a 2D array is not supported in v7.17.0".into(),
10155            ));
10156        }
10157        // r1039/r1040 — the exact canonical key, not an f64 projection.
10158        //
10159        // r1039 fixed the three specials, which carry a canonical zero in
10160        // `scaled` and so all sorted as the number 0. The projection
10161        // itself was the rest of the defect: "precision losses here only
10162        // matter for tie-breaks well past 15 significant digits" was the
10163        // comment, and the measurement disagreed — f64 called
10164        // `0.1` and `0.1000000000000000001` Equal, and a stable sort then
10165        // returned them in insertion order. Three of ten values came back
10166        // in the wrong place against PG18.4.
10167        Value::Numeric {
10168            scaled,
10169            scale,
10170            kind,
10171        } => {
10172            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
10173                spg_storage::NumericKey::from_numeric(*scaled, *scale, *kind),
10174            )));
10175        }
10176        Value::Float(x) => *x,
10177        // v7.37.16 — REAL sorts by its exact f64 widening (it had no
10178        // arm and fell through to the unsupported error).
10179        Value::Real(x) => f64::from(*x),
10180        Value::Bool(b) => {
10181            if *b {
10182                1.0
10183            } else {
10184                0.0
10185            }
10186        }
10187        Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
10188            return Err(EngineError::Unsupported(
10189                "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
10190            ));
10191        }
10192        // v7.37 — PG orders INTERVAL by its total time, treating a month as
10193        // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
10194        // f64 is exact for any interval under ~285 years, and only ORDER BY
10195        // tie-breaks past that magnitude lose precision. Matches the
10196        // min/max(interval) comparator in aggregate.rs.
10197        #[allow(clippy::cast_precision_loss)]
10198        Value::Interval {
10199            months,
10200            days,
10201            micros,
10202        } => {
10203            let total = i128::from(*months) * 30 * 86_400_000_000
10204                + i128::from(*days) * 86_400_000_000
10205                + i128::from(*micros);
10206            total as f64
10207        }
10208        Value::Json(_) => {
10209            return Err(EngineError::Unsupported(
10210                "ORDER BY of a JSON value is not supported — cast the document to text first"
10211                    .into(),
10212            ));
10213        }
10214        // v7.5.0 — Value is #[non_exhaustive]; future variants need
10215        // an explicit ORDER BY mapping. Surface as Unsupported until
10216        // engine support is added.
10217        _ => {
10218            return Err(EngineError::Unsupported(
10219                "ORDER BY of this value type is not supported".into(),
10220            ));
10221        }
10222    };
10223    Ok(OrderKey::Num(num))
10224}
10225
10226/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
10227/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
10228/// `EngineError` so the projection-build path keeps `UnknownQualifier`
10229/// vs `ColumnNotFound` distinct.
10230/// PG's name for the physical row identity. It is reserved there — no table
10231/// can have a column called this — which is what lets `*` skip it by name.
10232pub(crate) const CTID_COLUMN: &str = "ctid";
10233
10234/// v7.39 (round 512) — PG's system columns, in the order they are appended.
10235/// All six are reserved names there, which is what lets `*` skip them and
10236/// lets a scan tell them from a user column without a flag.
10237pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
10238
10239/// Is this name one of them?
10240pub(crate) fn is_system_column(name: &str) -> bool {
10241    SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
10242}
10243
10244/// Where the scan's appended system columns begin, if this schema carries
10245/// them: the trailing six, named in order. A catalog view with a column of
10246/// its own called `xmin` does not match, which is the point.
10247fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
10248    let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
10249    cols[start..]
10250        .iter()
10251        .zip(SYSTEM_COLUMNS)
10252        .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
10253        .then_some(start)
10254}
10255
10256/// v7.39 (round 540) — which positions `*` must skip.
10257///
10258/// The rule stays round 512's — the synthetic columns are the trailing
10259/// six of a relation's block, matched by POSITION so a genuine `xmin`
10260/// column is not lost — but a JOINED schema names its columns
10261/// `alias.column` and lays the peers out end to end, so a peer's six sit
10262/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
10263/// "trailing six" test back on the block it was written for.
10264fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
10265    let mut skip = alloc::vec![false; cols.len()];
10266    fn qualifier(n: &str) -> Option<&str> {
10267        n.rsplit_once('.').map(|(q, _)| q)
10268    }
10269    fn bare(n: &str) -> &str {
10270        n.rsplit('.').next().unwrap_or(n)
10271    }
10272    let mut i = 0;
10273    while i < cols.len() {
10274        let q = qualifier(&cols[i].name);
10275        let mut end = i;
10276        while end < cols.len() && qualifier(&cols[end].name) == q {
10277            end += 1;
10278        }
10279        if let Some(start) = (end - i)
10280            .checked_sub(SYSTEM_COLUMNS.len())
10281            .map(|off| i + off)
10282            && cols[start..end]
10283                .iter()
10284                .zip(SYSTEM_COLUMNS)
10285                .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
10286        {
10287            for s in skip.iter_mut().take(end).skip(start) {
10288                *s = true;
10289            }
10290        }
10291        i = end;
10292    }
10293    skip
10294}
10295
10296/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
10297/// read? Only then is the column materialised.
10298pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
10299    let mut found = false;
10300    crate::expr_analysis::visit_expr_columns_and_subqueries(
10301        e,
10302        &mut |c| {
10303            if is_system_column(&c.name) {
10304                found = true;
10305            }
10306        },
10307        &mut |_| {},
10308    );
10309    found
10310}
10311
10312fn references_ctid(stmt: &SelectStatement) -> bool {
10313    let in_expr = expr_references_ctid;
10314    stmt.items.iter().any(|i| match i {
10315        SelectItem::Expr { expr, .. } => in_expr(expr),
10316        _ => false,
10317    }) || stmt.where_.as_ref().is_some_and(in_expr)
10318        || stmt.order_by.iter().any(|o| in_expr(&o.expr))
10319        || stmt
10320            .group_by
10321            .as_ref()
10322            .is_some_and(|g| g.iter().any(in_expr))
10323        || stmt.having.as_ref().is_some_and(in_expr)
10324}
10325
10326/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
10327/// is a name the projection has to TYPE before any row exists.
10328///
10329/// Evaluation has answered this since round T9 (`resolve_column` builds a
10330/// `Value::Composite` of every column), but the typing side below had no
10331/// such branch and raised `column "t" does not exist` first — so the
10332/// feature was unreachable through a projection. Measured against PG18.4:
10333/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
10334///
10335/// The type is `Jsonb` + a composite marker, which is exactly how a
10336/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
10337/// the value travels as a `Value::Composite` and renders in the canonical
10338/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
10339/// so the marker names the alias and no rehydration keys off it — the
10340/// value arrives already built.
10341fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
10342    let mut s = ColumnSchema::new(
10343        alloc::string::String::from(alias),
10344        spg_storage::DataType::Jsonb,
10345        true,
10346    );
10347    s.user_composite_type = Some(alloc::string::String::from(alias));
10348    s
10349}
10350
10351pub(crate) fn resolve_projection_column<'a>(
10352    c: &ColumnName,
10353    schema_cols: &'a [ColumnSchema],
10354    table_alias: &str,
10355) -> Result<Cow<'a, ColumnSchema>, EngineError> {
10356    if let Some(q) = &c.qualifier {
10357        let composite = alloc::format!("{q}.{name}", name = c.name);
10358        if let Some(s) = schema_cols.iter().find(|s| s.name == composite) {
10359            return Ok(Cow::Borrowed(s));
10360        }
10361        // Single-table case: the qualifier may equal the active alias —
10362        // then look for the bare column name.
10363        if q == table_alias
10364            && let Some(s) = schema_cols.iter().find(|s| s.name == c.name)
10365        {
10366            return Ok(Cow::Borrowed(s));
10367        }
10368        // For multi-table schemas the qualifier is unknown only if no
10369        // column bears the "<q>." prefix. For single-table, the alias
10370        // mismatch alone is enough.
10371        let prefix = alloc::format!("{q}.");
10372        let qualifier_known =
10373            q == table_alias || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
10374        if !qualifier_known {
10375            return Err(EngineError::Eval(EvalError::UnknownQualifier {
10376                qualifier: q.clone(),
10377            }));
10378        }
10379        return Err(EngineError::Eval(EvalError::ColumnNotFound {
10380            name: c.name.clone(),
10381        }));
10382    }
10383    if let Some(s) = schema_cols.iter().find(|s| s.name == c.name) {
10384        return Ok(Cow::Borrowed(s));
10385    }
10386    let suffix = alloc::format!(".{name}", name = c.name);
10387    let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
10388    let first = matches.next();
10389    let extra = matches.next();
10390    match (first, extra) {
10391        (Some(s), None) => Ok(Cow::Borrowed(s)),
10392        (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
10393            detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
10394        })),
10395        // The whole-row reference, checked LAST so a real column carrying
10396        // the alias's name still wins — the same precedence
10397        // `resolve_column` applies on the evaluation side.
10398        //
10399        // Two schema shapes reach here. A single-table (or subquery, or
10400        // CTE) scan carries its alias and bare column names, so the name
10401        // has to equal the alias. A JOIN's combined schema carries no
10402        // alias at all and qualifies every column `alias.col`, so the
10403        // alias is identified by the prefix instead — which is exactly
10404        // how `whole_row_composite` picks the fields out on the
10405        // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
10406        // answers `(7,z)` on PG18.4 and errored here until this arm
10407        // covered the joined shape too.
10408        _ if !table_alias.is_empty() && c.name == table_alias => {
10409            Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
10410        }
10411        _ if table_alias.is_empty() && {
10412            let prefix = alloc::format!("{name}.", name = c.name);
10413            schema_cols.iter().any(|s| s.name.starts_with(&prefix))
10414        } =>
10415        {
10416            Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
10417        }
10418        _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
10419            name: c.name.clone(),
10420        })),
10421    }
10422}
10423
10424/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
10425/// parser to carry per-branch GROUPING() masks into a grouping-set query's
10426/// ORDER BY. They must never reach the output. No-op unless such a column is
10427/// present, so the common path is untouched.
10428/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
10429///
10430/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
10431/// a `LIMIT 2` that should have answered two groups answered one.
10432fn apply_deferred_limit(
10433    rows: alloc::vec::Vec<Row<'static>>,
10434    deferred: &(
10435        Option<spg_sql::ast::LimitExpr>,
10436        Option<spg_sql::ast::LimitExpr>,
10437    ),
10438) -> alloc::vec::Vec<Row<'static>> {
10439    let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
10440        Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
10441        _ => None,
10442    };
10443    let mut rows = rows;
10444    if let Some(off) = count(&deferred.1) {
10445        rows = rows.split_off(off.min(rows.len()));
10446    }
10447    if let Some(lim) = count(&deferred.0) {
10448        rows.truncate(lim);
10449    }
10450    rows
10451}
10452
10453fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
10454    let QueryResult::Rows { columns, rows } = result else {
10455        return result;
10456    };
10457    if !columns.iter().any(|c| c.name.starts_with("__grp_ord_")) {
10458        return QueryResult::Rows { columns, rows };
10459    }
10460    let keep: Vec<usize> = columns
10461        .iter()
10462        .enumerate()
10463        .filter(|(_, c)| !c.name.starts_with("__grp_ord_"))
10464        .map(|(i, _)| i)
10465        .collect();
10466    let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
10467    let new_rows: Vec<Row<'static>> = rows
10468        .into_iter()
10469        .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
10470        .collect();
10471    QueryResult::Rows {
10472        columns: new_cols,
10473        rows: new_rows,
10474    }
10475}
10476
10477/// v7.39 (round 487) — bind every projection item that is a bare column
10478/// reference to its position, once per query.
10479///
10480/// `#[inline(never)]` and out of line on purpose. Round 486 established
10481/// that adding code inside these scan bodies moves neighbouring hot
10482/// functions around under fat LTO: the first version of this had the loop
10483/// inline in `run_single_table_scan` and four aggregate shapes that never
10484/// touch that function — `full_agg`, `join_agg`, `group_500k`,
10485/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
10486/// the same machine. Keeping it out of line kept them still.
10487#[inline(never)]
10488fn bind_direct_columns(
10489    projection: &[ProjectedItem],
10490    ctx: &eval::EvalContext<'_>,
10491) -> Vec<Option<usize>> {
10492    projection
10493        .iter()
10494        .map(|p| match &p.expr {
10495            Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
10496                // Same exclusion `compile_into` makes: a composite column
10497                // has to be rehydrated from stored JSON, which is not a
10498                // cell read.
10499                ctx.columns
10500                    .get(*pos)
10501                    .is_none_or(|sc| sc.user_composite_type.is_none())
10502            }),
10503            _ => None,
10504        })
10505        .collect()
10506}
10507
10508/// v7.39 (round 505) — the name an un-aliased projected expression reports.
10509///
10510/// PG18 names a call for its function and everything else `?column?`;
10511/// measured with `\gdesc`. SPG used to print the parsed expression back
10512/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
10513/// name-keyed row access found nothing under `upper`.
10514///
10515/// The MySQL half is NOT this rule and is deliberately left alone here:
10516/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
10517/// which needs the parser to hand over spans the AST does not carry yet.
10518/// Until it does, a MySQL session keeps the printed form — closer to what
10519/// MariaDB answers than `?column?` would be.
10520pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
10521    if mysql {
10522        return expr.to_string();
10523    }
10524    spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
10525}
10526
10527pub(crate) fn build_projection(
10528    items: &[SelectItem],
10529    schema_cols: &[ColumnSchema],
10530    table_alias: &str,
10531    mysql: bool,
10532) -> Result<Vec<ProjectedItem>, EngineError> {
10533    build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0)
10534}
10535
10536/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
10537/// invisible to `*`.
10538///
10539/// The windowed-SELECT path appends a synthetic `__win_N` column per window
10540/// function so the rewritten projection can reference the computed values as
10541/// ordinary columns. `*` then expanded them too, and
10542/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
10543/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
10544/// silent one: the row simply had one more field than the client asked for.
10545///
10546/// Hidden by POSITION rather than by name, for the reason round 512 recorded
10547/// about the system columns: a name test looks safe until a real column
10548/// happens to carry the name. These are appended last, so the count is what
10549/// identifies them.
10550pub(crate) fn build_projection_hiding_tail(
10551    items: &[SelectItem],
10552    schema_cols: &[ColumnSchema],
10553    table_alias: &str,
10554    mysql: bool,
10555    hidden_tail: usize,
10556) -> Result<Vec<ProjectedItem>, EngineError> {
10557    let visible = schema_cols.len().saturating_sub(hidden_tail);
10558    // v7.39 (round 462) — a join's combined schema qualifies every column
10559    // `alias.col` so the deferred-join cell lookups resolve by composite
10560    // name. That is an internal convention, and `*` was handing it to the
10561    // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
10562    // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
10563    // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
10564    // already learned this for `q.*`; plain `*` never got the same rule.
10565    //
10566    // The signal is the schema itself, not the call site: only a combined
10567    // join schema arrives with no table alias AND every column qualified.
10568    // A single-table schema carries its alias, an empty schema has nothing
10569    // to strip, and a synthetic schema's names carry no dot.
10570    let joined_schema = table_alias.is_empty()
10571        && !schema_cols.is_empty()
10572        && schema_cols.iter().all(|c| c.name.contains('.'));
10573    let bare_name = |name: &str| -> String {
10574        if !joined_schema {
10575            return name.to_string();
10576        }
10577        match name.split_once('.') {
10578            Some((_, rest)) if !rest.is_empty() => rest.to_string(),
10579            _ => name.to_string(),
10580        }
10581    };
10582    let mut out = Vec::new();
10583    for item in items {
10584        match item {
10585            SelectItem::Wildcard => {
10586                // v7.39 (round 511) — `*` never expands a system column, as
10587                // PG's does not. They join the schema only when the statement
10588                // asked for them, so this matters for the mixed shape
10589                // `SELECT *, ctid FROM t`.
10590                //
10591                // v7.39 (round 512) — by POSITION, not by name. Matching on
10592                // the name alone looked safe because PG reserves them, and it
10593                // is not: `pg_replication_slots` genuinely has a column called
10594                // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
10595                // Only the trailing six, in the order the scan appends them,
10596                // are the synthetic ones.
10597                let sys_skip = synthetic_system_positions(schema_cols);
10598                for (idx, col) in schema_cols.iter().enumerate() {
10599                    if sys_skip[idx] || idx >= visible {
10600                        continue;
10601                    }
10602                    out.push(ProjectedItem {
10603                        expr: Expr::Column(ColumnName {
10604                            qualifier: None,
10605                            name: col.name.clone(),
10606                        }),
10607                        output_name: bare_name(&col.name),
10608                        ty: col.ty,
10609                        nullable: col.nullable,
10610                        user_enum_type: col.user_enum_type.clone(),
10611                        mysql_fsp: col.mysql_fsp,
10612                        collation_name: col.collation_name.clone(),
10613                    });
10614                }
10615            }
10616            // v7.39 (round 128) — `q.*` expands to every column belonging to
10617            // the qualifier `q`. Single-table schemas carry bare column names
10618            // reachable via `table_alias`; a join's combined schema carries
10619            // `alias.col` names, so a column belongs to `q` when its name has
10620            // the `q.` prefix. PG labels the expanded columns by their bare
10621            // name, so the `alias.` prefix is stripped from the output name.
10622            SelectItem::QualifiedWildcard(q) => {
10623                let prefix = alloc::format!("{q}.");
10624                let single_table = !table_alias.is_empty() && q == table_alias;
10625                let mut matched = 0usize;
10626                for col in &schema_cols[..visible] {
10627                    let belongs =
10628                        col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
10629                    if !belongs {
10630                        continue;
10631                    }
10632                    matched += 1;
10633                    let output_name = col
10634                        .name
10635                        .strip_prefix(&prefix)
10636                        .unwrap_or(&col.name)
10637                        .to_string();
10638                    out.push(ProjectedItem {
10639                        expr: Expr::Column(ColumnName {
10640                            qualifier: None,
10641                            name: col.name.clone(),
10642                        }),
10643                        output_name,
10644                        ty: col.ty,
10645                        nullable: col.nullable,
10646                        user_enum_type: col.user_enum_type.clone(),
10647                        mysql_fsp: col.mysql_fsp,
10648                        collation_name: col.collation_name.clone(),
10649                    });
10650                }
10651                if matched == 0 {
10652                    return Err(EngineError::Eval(EvalError::UnknownQualifier {
10653                        qualifier: q.clone(),
10654                    }));
10655                }
10656            }
10657            SelectItem::Expr { expr, alias } => {
10658                // Plain column ref keeps full schema info (real type +
10659                // nullability). For compound expressions try the
10660                // describe-side function-return-type table first
10661                // (e.g. `SELECT now()` → Timestamptz, `SELECT
10662                // concat(…)` → Text). Falls back to nullable Text
10663                // for shapes the describe path can't resolve.
10664                if let Expr::Column(c) = expr {
10665                    let sch = resolve_projection_column(c, schema_cols, table_alias)?;
10666                    let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
10667                    out.push(ProjectedItem {
10668                        expr: expr.clone(),
10669                        output_name,
10670                        ty: sch.ty,
10671                        nullable: sch.nullable,
10672                        // v7.39 (read01 round 54) — a bare enum column keeps
10673                        // its enum identity through the projection.
10674                        user_enum_type: sch.user_enum_type.clone(),
10675                        mysql_fsp: sch.mysql_fsp,
10676                        collation_name: sch.collation_name.clone(),
10677                    });
10678                } else if let Some(shape) = describe::describe_expr(expr, schema_cols) {
10679                    let output_name = alias
10680                        .clone()
10681                        .unwrap_or_else(|| default_output_name(expr, mysql));
10682                    out.push(ProjectedItem {
10683                        expr: expr.clone(),
10684                        output_name,
10685                        ty: shape.ty,
10686                        // v7.39 (round 258) — a projected EXPRESSION keeps its
10687                        // enum identity too, not just a bare column. `FROM
10688                        // (VALUES ('happy'::mood), …) t(m)` lowers to constant
10689                        // SELECTs, so the derived column arrived here as a cast
10690                        // and lost the enum — making the outer ORDER BY / min /
10691                        // max / array_agg sort by the label's TEXT.
10692                        nullable: shape.nullable,
10693                        user_enum_type: None,
10694                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
10695                        // A bare column reference keeps its collation; any
10696                        // other expression produces a new value and has none.
10697                        collation_name: match expr {
10698                            Expr::Column(c) => schema_cols
10699                                .iter()
10700                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
10701                                .and_then(|sc| sc.collation_name.clone()),
10702                            _ => None,
10703                        },
10704                    });
10705                } else {
10706                    let output_name = alias
10707                        .clone()
10708                        .unwrap_or_else(|| default_output_name(expr, mysql));
10709                    out.push(ProjectedItem {
10710                        expr: expr.clone(),
10711                        output_name,
10712                        // A user ENUM has no DataType of its own, so
10713                        // `describe_expr` cannot type `'ok'::mood` and the
10714                        // item lands HERE, defaulting to text — which is why
10715                        // pg_typeof answered `text` and a derived table sorted
10716                        // enum values by their label.
10717                        ty: DataType::Text,
10718                        nullable: true,
10719                        user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
10720                            .map(alloc::string::String::from),
10721                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
10722                        collation_name: match expr {
10723                            Expr::Column(c) => schema_cols
10724                                .iter()
10725                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
10726                                .and_then(|sc| sc.collation_name.clone()),
10727                            _ => None,
10728                        },
10729                    });
10730                }
10731            }
10732        }
10733    }
10734    Ok(out)
10735}
10736
10737// ---- v4.12 window-function helpers ----
10738// The (partition-key, order-key, original-index) tuple shape used
10739// across these helpers is intrinsic to the planner. Factoring it
10740// into a typedef adds indirection without making the code clearer,
10741// so several lints are allowed inline on the affected functions
10742// rather than module-wide.
10743
10744/// v4.22: pick more specific column types from observed rows when
10745/// the projection builder defaulted to Text (the v1.x behavior for
10746/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
10747/// land an Int column in the CTE storage table rather than failing
10748/// the insert with "expected TEXT, got INT".
10749pub(crate) fn infer_column_types(
10750    columns: &[ColumnSchema],
10751    rows: &[Row<'static>],
10752) -> Vec<ColumnSchema> {
10753    let mut out = columns.to_vec();
10754    for (col_idx, col) in out.iter_mut().enumerate() {
10755        if col.ty != DataType::Text {
10756            continue;
10757        }
10758        let mut inferred: Option<DataType> = None;
10759        let mut all_null = true;
10760        for row in rows {
10761            let Some(v) = row.values.get(col_idx) else {
10762                continue;
10763            };
10764            let ty = match v {
10765                Value::Null => continue,
10766                Value::SmallInt(_) => DataType::SmallInt,
10767                Value::Int(_) => DataType::Int,
10768                Value::BigInt(_) => DataType::BigInt,
10769                Value::Float(_) => DataType::Float,
10770                Value::Bool(_) => DataType::Bool,
10771                Value::Vector(_) => DataType::Vector {
10772                    dim: 0,
10773                    encoding: VecEncoding::F32,
10774                },
10775                // v7.38 (read01 U16) — carry array values through with an
10776                // array type so a recursive CTE that projects an array
10777                // (e.g. a SEARCH/CYCLE ord / path column) types the working
10778                // column as an array, not Text.
10779                Value::TextArray(_) => DataType::TextArray,
10780                Value::IntArray(_) => DataType::IntArray,
10781                Value::BigIntArray(_) => DataType::BigIntArray,
10782                Value::SmallIntArray(_) => DataType::SmallIntArray,
10783                Value::FloatArray(_) => DataType::FloatArray,
10784                Value::BoolArray(_) => DataType::BoolArray,
10785                // v7.39 (GUC knife 2) — an interval projection describes
10786                // as INTERVAL (typed drivers read the RowDescription OID).
10787                Value::Interval { .. } => DataType::Interval,
10788                _ => DataType::Text,
10789            };
10790            all_null = false;
10791            inferred = Some(match inferred {
10792                None => ty,
10793                Some(prev) if prev == ty => prev,
10794                Some(_) => DataType::Text,
10795            });
10796        }
10797        if let Some(t) = inferred {
10798            col.ty = t;
10799            col.nullable = true;
10800        } else if all_null {
10801            col.nullable = true;
10802        }
10803    }
10804    out
10805}
10806
10807/// Numeric widening rank for UNION type resolution (higher = wider).
10808fn numeric_rank(t: DataType) -> Option<u8> {
10809    match t {
10810        DataType::SmallInt => Some(1),
10811        DataType::Int => Some(2),
10812        DataType::BigInt => Some(3),
10813        DataType::Numeric { .. } => Some(4),
10814        DataType::Float => Some(5),
10815        _ => None,
10816    }
10817}
10818
10819/// Resolve the common result type for a UNION / VALUES column from the
10820/// set of concrete (non-NULL) branch types, following the safe subset
10821/// of PG's type resolution:
10822///   * all-numeric  → the widest numeric (int ∪ bigint → bigint, … ∪
10823///     numeric → numeric, … ∪ float → float);
10824///   * DATE ∪ TIMESTAMP → TIMESTAMP;
10825///   * exactly one concrete non-TEXT type mixed with TEXT literals →
10826///     that concrete type (the TEXT cells get parsed into it).
10827/// Returns `None` for anything ambiguous, so the caller leaves the
10828/// column untouched rather than risk a wrong or failing coercion.
10829fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
10830    // NB: types are collected from RUNTIME values, which are coarser
10831    // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
10832    // a single-concrete-type fast path must NOT overwrite the column
10833    // type — it would downgrade tstz to ts. NULL-only unification (PG:
10834    // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
10835    // row's pg_typeof) needs schema-level resolution — recorded, not
10836    // attempted here.
10837    if types.len() < 2 {
10838        return None;
10839    }
10840    if types.iter().all(|t| numeric_rank(*t).is_some()) {
10841        return types
10842            .iter()
10843            .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
10844            .copied();
10845    }
10846    let non_text: Vec<&DataType> = types
10847        .iter()
10848        .filter(|t| !matches!(t, DataType::Text))
10849        .collect();
10850    // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
10851    // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
10852    // if any is timestamp the result is timestamp (ts ∪ date). All values are
10853    // the same UTC-micros instant, so widening date/ts to tstz is lossless.
10854    if non_text.iter().all(|t| {
10855        matches!(
10856            t,
10857            DataType::Date | DataType::Timestamp | DataType::Timestamptz
10858        )
10859    }) && non_text
10860        .iter()
10861        .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
10862    {
10863        if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
10864            return Some(DataType::Timestamptz);
10865        }
10866        return Some(DataType::Timestamp);
10867    }
10868    // A single concrete non-TEXT type mixed with TEXT literals.
10869    if non_text.len() == 1 {
10870        return Some(*non_text[0]);
10871    }
10872    // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
10873    // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
10874    // text): resolve the concrete set first (PG treats the unknown-
10875    // typed string literals as castable to whatever the knowns
10876    // resolve to), then the TEXT cells parse into that target — the
10877    // caller's coercion dry-run still abandons the column if any
10878    // literal doesn't parse.
10879    if !non_text.is_empty() && non_text.len() < types.len() {
10880        let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
10881        return resolve_union_common_type(&concrete);
10882    }
10883    None
10884}
10885
10886/// Coerce every cell of a UNION / VALUES result column to one common
10887/// type (see [`resolve_union_common_type`]). Conservative: a column
10888/// whose branches already agree, or whose types don't resolve, or where
10889/// any cell fails to coerce, is left exactly as it was — this never
10890/// turns a previously-working query into an error.
10891fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
10892    for col_idx in 0..columns.len() {
10893        let mut seen: Vec<DataType> = Vec::new();
10894        for row in rows.iter() {
10895            if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
10896                if !seen.contains(&dt) {
10897                    seen.push(dt);
10898                }
10899            }
10900        }
10901        // v7.37.16 — a single concrete runtime type under a TEXT-typed
10902        // column means the column type came off a NULL (or unknown-text)
10903        // branch: NULL literals describe as TEXT (`L::Null → Text`), so
10904        // `VALUES (NULL),(1.5)` left the column "text" while every
10905        // non-NULL cell is numeric. Adopt the concrete type — schema
10906        // only, no cell changes. tstz-safe by construction: a real
10907        // timestamptz column's schema type is Timestamptz, not Text, so
10908        // the coarser runtime type (Value::Timestamp) can't downgrade it
10909        // through this arm; and a real text column's non-NULL cells are
10910        // Text, which keeps seen == [Text] and skips it.
10911        if seen.len() == 1
10912            && matches!(columns[col_idx].ty, DataType::Text)
10913            && !matches!(seen[0], DataType::Text)
10914        {
10915            columns[col_idx].ty = seen[0];
10916            continue;
10917        }
10918        let Some(target) = resolve_union_common_type(&seen) else {
10919            continue;
10920        };
10921        // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
10922        // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
10923        // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
10924        // existing numeric cell untouched and only promote integers (to scale 0)
10925        // rather than rescaling everything to the widest scale.
10926        let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
10927        // Dry-run the coercion; abandon the whole column if any fails.
10928        let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
10929        let mut ok = true;
10930        for row in rows.iter() {
10931            match row.values.get(col_idx) {
10932                Some(Value::Numeric { .. }) if scale_preserving_numeric => {
10933                    coerced.push(Some(row.values[col_idx].clone()));
10934                }
10935                Some(v) => {
10936                    let cell_target = if scale_preserving_numeric {
10937                        DataType::Numeric {
10938                            precision: 0,
10939                            scale: 0,
10940                        }
10941                    } else {
10942                        target
10943                    };
10944                    match crate::conversions::coerce_value(
10945                        v.clone(),
10946                        cell_target,
10947                        &columns[col_idx].name,
10948                        col_idx,
10949                    ) {
10950                        Ok(cv) => coerced.push(Some(cv)),
10951                        Err(_) => {
10952                            ok = false;
10953                            break;
10954                        }
10955                    }
10956                }
10957                None => coerced.push(None),
10958            }
10959        }
10960        if !ok {
10961            continue;
10962        }
10963        for (row, cv) in rows.iter_mut().zip(coerced) {
10964            if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
10965                *slot = nv;
10966            }
10967        }
10968        columns[col_idx].ty = target;
10969    }
10970}
10971
10972/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
10973/// dedup inside the recursive iteration. Crude but deterministic
10974/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
10975fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
10976    let mut out = Vec::new();
10977    for v in &row.values {
10978        // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
10979        // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
10980        // like PG (and like GROUP BY, which already normalizes). The old
10981        // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
10982        // the exact-decimal family through one scale-stripped canonical form.
10983        match v {
10984            Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
10985            Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
10986            Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
10987            Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
10988            other => {
10989                let s = alloc::format!("{other:?}|");
10990                out.extend_from_slice(s.as_bytes());
10991            }
10992        }
10993    }
10994    out
10995}
10996
10997/// Append a scale-independent canonical key for an exact-decimal value: strip
10998/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
10999/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
11000fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
11001    while scale > 0 && scaled % 10 == 0 {
11002        scaled /= 10;
11003        scale -= 1;
11004    }
11005    let s = alloc::format!("\u{1}{scaled}e-{scale}|");
11006    out.extend_from_slice(s.as_bytes());
11007}
11008
11009/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
11010/// (uncorrelated; outer refs were substituted upstream), then zip
11011/// them in parallel, NULL-padding shorter arrays to the longest
11012/// (PG's ROWS FROM shorthand). Shared by the primary-position
11013/// executor and the join-position materialiser, which both detect
11014/// the parser's `__unnest_zip` marker call.
11015pub(crate) fn unnest_zip_rows(
11016    args: &[Expr],
11017) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
11018    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
11019    let ctx = EvalContext::new(&empty_schema, None);
11020    let dummy_row = Row::new(alloc::vec::Vec::new());
11021    let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
11022    let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
11023        alloc::vec::Vec::with_capacity(args.len());
11024    for a in args {
11025        let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
11026        let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = match v {
11027            Value::Null => (DataType::Text, alloc::vec::Vec::new()),
11028            Value::TextArray(xs) => (
11029                DataType::Text,
11030                xs.into_iter()
11031                    .map(|x| x.map(Value::text).unwrap_or(Value::Null))
11032                    .collect(),
11033            ),
11034            Value::IntArray(xs) => (
11035                DataType::Int,
11036                xs.into_iter()
11037                    .map(|x| x.map(Value::Int).unwrap_or(Value::Null))
11038                    .collect(),
11039            ),
11040            Value::BigIntArray(xs) => (
11041                DataType::BigInt,
11042                xs.into_iter()
11043                    .map(|x| x.map(Value::BigInt).unwrap_or(Value::Null))
11044                    .collect(),
11045            ),
11046            other => {
11047                return Err(EngineError::Unsupported(alloc::format!(
11048                    "unnest() expects array arguments, got {}",
11049                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
11050                )));
11051            }
11052        };
11053        dtypes.push(dt);
11054        columns.push(items);
11055    }
11056    let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
11057    let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
11058    for i in 0..max_len {
11059        let vals: alloc::vec::Vec<Value<'static>> = columns
11060            .iter()
11061            .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
11062            .collect();
11063        rows.push(Row::new(vals));
11064    }
11065    Ok((dtypes, rows))
11066}
11067
11068/// Detect the parser's multi-arg unnest marker on an unnest_expr.
11069pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
11070    match expr {
11071        Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
11072        _ => None,
11073    }
11074}
11075
11076/// Evaluate generate_series arguments (uncorrelated — outer refs
11077/// were substituted upstream where applicable) and build the row
11078/// stream. Dispatches on the start value's shape and rejects
11079/// mixed-shape calls early (e.g. start = timestamp, stop =
11080/// integer) so the caller gets a clean error rather than a panic.
11081/// Shared by the primary-position executor and the join-position
11082/// materialiser.
11083pub(crate) fn generate_series_rows(
11084    args: &[Expr],
11085    cancel: &CancelToken<'_>,
11086) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
11087    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
11088    let ctx = EvalContext::new(&empty_schema, None);
11089    let dummy_row = Row::new(alloc::vec::Vec::new());
11090    let mut arg_values: alloc::vec::Vec<Value<'static>> =
11091        alloc::vec::Vec::with_capacity(args.len());
11092    for a in args {
11093        arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
11094    }
11095    generate_series_from_values(arg_values, args, cancel)
11096}
11097
11098/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
11099/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
11100/// full integer / numeric / timestamp overload set with the FROM-clause path.
11101/// Before this split the target-list arm reimplemented only the integer case,
11102/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
11103/// NULL for the timestamp column instead of the series. `arg_values` are the
11104/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
11105/// timestamp type resolution (it inspects the argument expressions' types).
11106pub(crate) fn generate_series_from_values(
11107    mut arg_values: alloc::vec::Vec<Value<'static>>,
11108    args: &[Expr],
11109    cancel: &CancelToken<'_>,
11110) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
11111    // PG: a NULL bound or step yields zero rows (also keeps the
11112    // NULL-padded lateral probe alive — schema without data).
11113    if arg_values.iter().any(|v| matches!(v, Value::Null)) {
11114        return Ok((DataType::BigInt, alloc::vec::Vec::new()));
11115    }
11116    // PG resolves `generate_series(date, date, interval)` to the
11117    // timestamp/timestamptz overload by implicitly casting each date
11118    // bound up to a timestamp at midnight (verified vs live PG18.4:
11119    // date args yield rows anchored at 00:00:00). SPG's TZ-naive
11120    // timestamp model renders the same instants, so fold any Date
11121    // bound to its midnight Timestamp (canonical `days *
11122    // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
11123    // the shape match so the existing timestamp arm drives the walk.
11124    // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
11125    // `generate_series(date, date, interval)` has no date overload, and among
11126    // the two candidates PG prefers the timestamptz one (timestamptz is the
11127    // preferred type of the datetime category), so the column comes back
11128    // `timestamp with time zone` — the rows render with a `+00` offset. A
11129    // timestamptz bound obviously lands there too. Only genuinely
11130    // timestamp-typed bounds keep the TZ-naive result type.
11131    let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
11132    let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
11133        || args.iter().any(|a| {
11134            crate::describe::describe_expr(a, &empty_cols)
11135                .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
11136        });
11137    for v in &mut arg_values {
11138        if let Value::Date(d) = *v {
11139            *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
11140        }
11141    }
11142    match arg_values.as_slice() {
11143        [Value::Timestamp(start), Value::Timestamp(stop), step] => {
11144            let interval_step = match step {
11145                Value::Interval { .. } => step.clone(),
11146                // v7.38 (read01) — PG resolves an unknown-type string step
11147                // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
11148                // a bare text step by parsing it the same way `::interval` does.
11149                Value::Text(s) => crate::conversions::coerce_value(
11150                    Value::text(s.as_ref()),
11151                    DataType::Interval,
11152                    "",
11153                    0,
11154                )
11155                .map_err(|_| {
11156                    EngineError::Unsupported(alloc::format!(
11157                        "generate_series(timestamp, timestamp, …): \
11158                         could not parse step {s:?} as INTERVAL"
11159                    ))
11160                })?,
11161                other => {
11162                    return Err(EngineError::Unsupported(alloc::format!(
11163                        "generate_series(timestamp, timestamp, …): \
11164                         step must be INTERVAL, got {}",
11165                        crate::conversions::pg_type_name_for_error_opt(other.data_type())
11166                    )));
11167                }
11168            };
11169            let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
11170            Ok((
11171                if tz {
11172                    DataType::Timestamptz
11173                } else {
11174                    DataType::Timestamp
11175                },
11176                rows,
11177            ))
11178        }
11179        [start, stop, step]
11180            if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
11181        {
11182            let s = value_to_i64(start);
11183            let e = value_to_i64(stop);
11184            let st = value_to_i64(step);
11185            // PG types the series by the argument type: int4 args → int4
11186            // elements, int8 (bigint) args → int8. Any BigInt operand widens.
11187            let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
11188            let rows = generate_series_integers(s, e, st, wide, cancel)?;
11189            Ok((
11190                if wide {
11191                    DataType::BigInt
11192                } else {
11193                    DataType::Int
11194                },
11195                rows,
11196            ))
11197        }
11198        [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
11199            let s = value_to_i64(start);
11200            let e = value_to_i64(stop);
11201            let wide = value_is_bigint(start) || value_is_bigint(stop);
11202            let rows = generate_series_integers(s, e, 1, wide, cancel)?;
11203            Ok((
11204                if wide {
11205                    DataType::BigInt
11206                } else {
11207                    DataType::Int
11208                },
11209                rows,
11210            ))
11211        }
11212        // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
11213        // series in exact numeric arithmetic; NaN / infinity bounds and a
11214        // zero step get dedicated wordings, and a mixed int/numeric call
11215        // resolves here via the implicit int→numeric cast.
11216        [_, _] | [_, _, _]
11217            if arg_values
11218                .iter()
11219                .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
11220                && arg_values.iter().all(|v| {
11221                    matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
11222                }) =>
11223        {
11224            use spg_storage::NumericKind as K;
11225            let words: [(&str, &str); 3] = [
11226                (
11227                    "start value cannot be NaN",
11228                    "start value cannot be infinity",
11229                ),
11230                ("stop value cannot be NaN", "stop value cannot be infinity"),
11231                ("step size cannot be NaN", "step size cannot be infinity"),
11232            ];
11233            for (i, v) in arg_values.iter().enumerate() {
11234                if let Value::Numeric { kind, .. } = v {
11235                    if *kind != K::Finite {
11236                        let (nan_w, inf_w) = words[i];
11237                        return Err(EngineError::Unsupported(
11238                            if *kind == K::NaN { nan_w } else { inf_w }.into(),
11239                        ));
11240                    }
11241                }
11242            }
11243            let big =
11244                |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
11245            let start = big(&arg_values[0]);
11246            let stop = big(&arg_values[1]);
11247            let step = if arg_values.len() == 3 {
11248                big(&arg_values[2])
11249            } else {
11250                spg_storage::bignum::BigNumeric::from_i128(1, 0)
11251            };
11252            if step.is_zero() {
11253                return Err(EngineError::Unsupported(
11254                    "step size cannot equal zero".into(),
11255                ));
11256            }
11257            let descending = step.parts().0;
11258            let mut rows = alloc::vec::Vec::new();
11259            let mut cur = start;
11260            const MAX_ROWS: usize = 10_000_000;
11261            loop {
11262                cancel.check()?;
11263                let c = cur.cmp(&stop);
11264                if descending {
11265                    if c == core::cmp::Ordering::Less {
11266                        break;
11267                    }
11268                } else if c == core::cmp::Ordering::Greater {
11269                    break;
11270                }
11271                if rows.len() >= MAX_ROWS {
11272                    return Err(EngineError::Unsupported(alloc::format!(
11273                        "generate_series() result exceeds {MAX_ROWS} rows"
11274                    )));
11275                }
11276                rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
11277                    cur.clone()
11278                )]));
11279                cur = cur.add(&step);
11280            }
11281            Ok((
11282                DataType::Numeric {
11283                    precision: 0,
11284                    scale: 0,
11285                },
11286                rows,
11287            ))
11288        }
11289        _ => Err(EngineError::Unsupported(alloc::format!(
11290            "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
11291             argument shapes; got {}",
11292            arg_values
11293                .iter()
11294                .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
11295                .collect::<alloc::vec::Vec<_>>()
11296                .join(", ")
11297        ))),
11298    }
11299}
11300
11301/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
11302/// Step direction follows the sign: positive step iterates upward
11303/// (stops when current > stop); negative iterates downward; zero
11304/// errors. Caller-facing row stream is `BigInt`-typed so a single
11305/// projection schema covers SmallInt / Int / BigInt callers.
11306fn generate_series_integers(
11307    start: i64,
11308    stop: i64,
11309    step: i64,
11310    wide: bool,
11311    cancel: &CancelToken<'_>,
11312) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
11313    if step == 0 {
11314        return Err(EngineError::Unsupported(
11315            "step size cannot equal zero".into(),
11316        ));
11317    }
11318    let mut out = alloc::vec::Vec::new();
11319    let mut cur = start;
11320    // Hard cap to keep a runaway call from eating all memory. PG
11321    // has no such cap but does honour query timeout; SPG's cancel
11322    // token will fire too — this is a defense-in-depth backstop.
11323    const MAX_ROWS: usize = 10_000_000;
11324    loop {
11325        cancel.check()?;
11326        if step > 0 && cur > stop {
11327            break;
11328        }
11329        if step < 0 && cur < stop {
11330            break;
11331        }
11332        out.push(Row::new(alloc::vec![if wide {
11333            Value::BigInt(cur)
11334        } else {
11335            Value::Int(cur as i32)
11336        }]));
11337        if out.len() > MAX_ROWS {
11338            return Err(EngineError::Unsupported(alloc::format!(
11339                "generate_series(): exceeded {MAX_ROWS} rows; \
11340                 narrow start/stop or use a larger step"
11341            )));
11342        }
11343        cur = match cur.checked_add(step) {
11344            Some(n) => n,
11345            None => break,
11346        };
11347    }
11348    Ok(out)
11349}
11350
11351/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
11352/// `Value::Interval { months, micros }` per the caller's guard;
11353/// each iteration adds the interval via `apply_binary_interval`
11354/// so month-shifting handles short-month rollover (PG semantics).
11355fn generate_series_timestamps(
11356    start: i64,
11357    stop: i64,
11358    step: Value,
11359    cancel: &CancelToken<'_>,
11360) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
11361    let (months, days, micros) = match &step {
11362        Value::Interval {
11363            months,
11364            days,
11365            micros,
11366        } => (*months, *days, *micros),
11367        _ => unreachable!("caller guards step.is_interval"),
11368    };
11369    if months == 0 && days == 0 && micros == 0 {
11370        return Err(EngineError::Unsupported(
11371            "generate_series(): INTERVAL step cannot be zero".into(),
11372        ));
11373    }
11374    let ascending = months > 0 || days > 0 || micros > 0;
11375    let mut out = alloc::vec::Vec::new();
11376    let mut cur = Value::Timestamp(start);
11377    const MAX_ROWS: usize = 10_000_000;
11378    loop {
11379        cancel.check()?;
11380        let cur_t = match cur {
11381            Value::Timestamp(t) => t,
11382            _ => unreachable!("loop invariant: cur is Timestamp"),
11383        };
11384        if ascending && cur_t > stop {
11385            break;
11386        }
11387        if !ascending && cur_t < stop {
11388            break;
11389        }
11390        out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
11391        if out.len() > MAX_ROWS {
11392            return Err(EngineError::Unsupported(alloc::format!(
11393                "generate_series(): exceeded {MAX_ROWS} rows; \
11394                 narrow start/stop or use a larger step"
11395            )));
11396        }
11397        let next = eval::apply_binary_interval(
11398            spg_sql::ast::BinOp::Add,
11399            &cur,
11400            &Value::Interval {
11401                months,
11402                days,
11403                micros,
11404            },
11405        )
11406        .map_err(EngineError::Eval)?;
11407        cur = match next {
11408            Some(v) => v,
11409            None => break,
11410        };
11411    }
11412    Ok(out)
11413}
11414
11415/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
11416/// WITH TIES` requires an `ORDER BY`. Without one, there's no
11417/// way to identify "ties" deterministically, so PG errors at
11418/// plan time. SPG mirrors that surface so the same DDL / app
11419/// behaviour holds on cutover.
11420fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
11421    if stmt.limit_with_ties && stmt.order_by.is_empty() {
11422        return Err(EngineError::Unsupported(alloc::string::String::from(
11423            "WITH TIES cannot be specified without ORDER BY clause",
11424        )));
11425    }
11426    Ok(())
11427}
11428
11429/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
11430/// (case-insensitive). Used by `exec_select_cancel`'s
11431/// projection loop to detect Set-Returning-Function rows that
11432/// need per-row expansion. Only the top-level call counts —
11433/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
11434/// projection's perspective; it would surface as an "unknown
11435/// function" mismatch downstream, which is what we want
11436/// (multi-SRF / nested SRF is documented carve-out for v7.19).
11437fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
11438    top_level_srf_kind(expr).is_some()
11439}
11440
11441/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
11442/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
11443/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
11444/// source row.
11445#[derive(Clone, Copy, PartialEq, Eq)]
11446pub(crate) enum SrfKind {
11447    Unnest,
11448    /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
11449    /// list. It used to be handled ONLY by the parser's lift into FROM, so a
11450    /// second one in the same list came back as "unknown function".
11451    GenerateSeries,
11452    GenerateSubscripts,
11453    /// `_text` variants unwrap scalars to their lexeme; the plain forms render
11454    /// every value as compact JSON text.
11455    ArrayElements {
11456        as_text: bool,
11457    },
11458    PathQuery,
11459    RegexpMatches,
11460    Each {
11461        as_text: bool,
11462    },
11463    ObjectKeys,
11464}
11465
11466/// Case-insensitive match against any of `names`.
11467fn name_is(name: &str, names: &[&str]) -> bool {
11468    names.iter().any(|n| name.eq_ignore_ascii_case(n))
11469}
11470
11471pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
11472    let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
11473        return None;
11474    };
11475    let n = args.len();
11476    // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
11477    // SELECT list (it returned an array there before) and shares the unnest
11478    // expansion machinery.
11479    if n == 1 && name.eq_ignore_ascii_case("unnest") {
11480        return Some(SrfKind::Unnest);
11481    }
11482    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
11483        return Some(SrfKind::GenerateSeries);
11484    }
11485    if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
11486        return Some(SrfKind::GenerateSubscripts);
11487    }
11488    // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
11489    // per element / match in the SELECT list; they collapsed to a single row
11490    // (a TextArray, or an "unknown function" error for `each`) before.
11491    if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
11492        return Some(SrfKind::ArrayElements { as_text: false });
11493    }
11494    if n == 1
11495        && name_is(
11496            name,
11497            &["jsonb_array_elements_text", "json_array_elements_text"],
11498        )
11499    {
11500        return Some(SrfKind::ArrayElements { as_text: true });
11501    }
11502    // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
11503    if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
11504        return Some(SrfKind::PathQuery);
11505    }
11506    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
11507        return Some(SrfKind::RegexpMatches);
11508    }
11509    if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
11510        return Some(SrfKind::Each { as_text: false });
11511    }
11512    if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
11513        return Some(SrfKind::Each { as_text: true });
11514    }
11515    if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
11516        return Some(SrfKind::ObjectKeys);
11517    }
11518    None
11519}
11520
11521/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
11522/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
11523/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
11524/// rows, as in PG).
11525pub(crate) fn top_level_srf_output(
11526    expr: &spg_sql::ast::Expr,
11527    row: &Row<'static>,
11528    ctx: &EvalContext<'_>,
11529) -> Result<Vec<Value<'static>>, EngineError> {
11530    let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
11531        (top_level_srf_kind(expr), expr)
11532    else {
11533        return Err(EngineError::Unsupported(
11534            "expected a SELECT-list SRF call".into(),
11535        ));
11536    };
11537    match kind {
11538        SrfKind::Unnest => {
11539            // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
11540            // the elements DIRECTLY: the old path built the whole
11541            // Value::Array (one eval + a clone per element) only for
11542            // array_value_to_elements to clone every element back out.
11543            // Any other argument shape (a column, a function result)
11544            // keeps the build-then-split path.
11545            if let spg_sql::ast::Expr::Array(items) = &args[0] {
11546                return items
11547                    .iter()
11548                    .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
11549                    .collect();
11550            }
11551            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11552            array_value_to_elements(&arr)
11553        }
11554        SrfKind::GenerateSeries => {
11555            // v7.39 (read01 round 96) — evaluate the args against the actual
11556            // row, then hand off to the shared core so the numeric and
11557            // timestamp/timestamptz overloads work here too (this arm used to
11558            // handle only integers, silently NULLing a temporal/numeric series
11559            // when it shared a target list with another SRF).
11560            let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
11561            for a in args {
11562                arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
11563            }
11564            let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
11565            Ok(rows
11566                .into_iter()
11567                .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
11568                .collect())
11569        }
11570        SrfKind::GenerateSubscripts => {
11571            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11572            let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
11573            if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
11574                return Ok(Vec::new());
11575            }
11576            let len = array_value_to_elements(&arr)?.len();
11577            Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
11578        }
11579        // One Value per array element (`_text` → text / SQL NULL, plain → the
11580        // element's compact JSON text) — the element list the FROM-clause form
11581        // materialises.
11582        SrfKind::ArrayElements { as_text } => {
11583            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11584            if matches!(arg, Value::Null) {
11585                return Ok(Vec::new());
11586            }
11587            let items =
11588                crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
11589            Ok(items
11590                .into_iter()
11591                .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
11592                .collect())
11593        }
11594        // The scalar form already yields a TextArray of the keys (or errors on
11595        // a non-object, like PG); expand it into rows.
11596        SrfKind::ObjectKeys => {
11597            let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
11598            array_value_to_elements(&v)
11599        }
11600        // One row per match, each a text[] of the pattern's capture groups.
11601        SrfKind::RegexpMatches => {
11602            let vals: Vec<Value<'static>> = args
11603                .iter()
11604                .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
11605                .collect::<Result<_, _>>()?;
11606            crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
11607        }
11608        // One composite `(key, value)` row per object member (plain → jsonb
11609        // value, `_text` → text / SQL NULL).
11610        SrfKind::Each { as_text } => {
11611            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11612            if matches!(arg, Value::Null) {
11613                return Ok(Vec::new());
11614            }
11615            let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
11616            Ok(pairs
11617                .into_iter()
11618                .map(|(k, v)| {
11619                    let val = if as_text {
11620                        v.map(Value::text).unwrap_or(Value::Null)
11621                    } else {
11622                        v.map(Value::json).unwrap_or(Value::Null)
11623                    };
11624                    Value::Composite(alloc::vec![
11625                        ("key".to_string(), Value::text(k)),
11626                        ("value".to_string(), val),
11627                    ])
11628                })
11629                .collect())
11630        }
11631        // One Value per matched JSON value.
11632        SrfKind::PathQuery => {
11633            let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11634            let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
11635            // v7.39 — optional vars document (3rd arg).
11636            let vars = match args.get(2) {
11637                Some(a) => {
11638                    let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
11639                    crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
11640                }
11641                None => None,
11642            };
11643            match crate::json::path_query_vars(&doc, &path, vars.as_ref())
11644                .map_err(EngineError::Eval)?
11645            {
11646                Value::Null => Ok(Vec::new()),
11647                Value::TextArray(items) => Ok(items
11648                    .into_iter()
11649                    .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
11650                    .collect()),
11651                other => Ok(alloc::vec![other]),
11652            }
11653        }
11654    }
11655}
11656
11657/// v7.19 P5 — turn an array-typed `Value` into the element list
11658/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
11659/// = (no rows)`). Non-array values fall through to a type-mismatch
11660/// error.
11661pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
11662    // v7.39 (round 236) — PG unnests a multidimensional array into its
11663    // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
11664    // rows). SPG stores 2-D arrays as their own variants, which fell
11665    // through to the type-mismatch arm below.
11666    if let Some(flat) = crate::eval::values::flatten_2d(v) {
11667        return array_value_to_elements(&flat);
11668    }
11669    match v {
11670        Value::Null => Ok(Vec::new()),
11671        Value::TextArray(items) => Ok(items
11672            .iter()
11673            .map(|opt| {
11674                opt.as_ref()
11675                    .map(|s| Value::text(s.clone()))
11676                    .unwrap_or(Value::Null)
11677            })
11678            .collect()),
11679        Value::IntArray(items) => Ok(items
11680            .iter()
11681            .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
11682            .collect()),
11683        Value::BigIntArray(items) => Ok(items
11684            .iter()
11685            .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
11686            .collect()),
11687        // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
11688        // range per canonical span.
11689        Value::Multirange { kind, ranges } => Ok(ranges
11690            .iter()
11691            .map(|s| Value::Range {
11692                kind: *kind,
11693                lower: s.lower.clone(),
11694                upper: s.upper.clone(),
11695                lower_inc: s.lower_inc,
11696                upper_inc: s.upper_inc,
11697                empty: false,
11698            })
11699            .collect()),
11700        other => Err(EngineError::Eval(EvalError::TypeMismatch {
11701            detail: alloc::format!(
11702                "unnest() expects an array argument, got {}",
11703                crate::conversions::pg_type_name_for_error_opt(other.data_type())
11704            ),
11705        })),
11706    }
11707}
11708
11709impl Engine {
11710    /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
11711    /// the SELECT's FROM / JOIN graph, re-parse each view's body
11712    /// source, and prepend it as a synthetic CTE on the
11713    /// returned SelectStatement. Returns `None` when no view
11714    /// references are found (caller proceeds with the original
11715    /// statement); returns `Some(rewritten)` otherwise (caller
11716    /// re-runs exec_select_cancel on the rewritten form so the
11717    /// regular CTE materialiser handles it).
11718    fn expand_views_in_select(
11719        &self,
11720        stmt: &SelectStatement,
11721    ) -> Result<Option<SelectStatement>, EngineError> {
11722        let cat = self.active_catalog();
11723        let mut referenced: Vec<String> = Vec::new();
11724        if let Some(from) = &stmt.from {
11725            collect_view_refs(&from.primary, cat, &mut referenced);
11726            for j in &from.joins {
11727                collect_view_refs(&j.table, cat, &mut referenced);
11728            }
11729        }
11730        // Don't expand a view name that's already shadowed by a
11731        // CTE on the same SELECT — the CTE wins per PG.
11732        referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
11733        if referenced.is_empty() {
11734            return Ok(None);
11735        }
11736        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
11737        for name in &referenced {
11738            let view = cat.view(name).ok_or_else(|| {
11739                EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
11740                    "view {name:?} disappeared mid-expansion"
11741                )))
11742            })?;
11743            let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
11744                EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
11745            })?;
11746            let Statement::Select(body) = parsed else {
11747                return Err(EngineError::Unsupported(alloc::format!(
11748                    "view {name:?} body is not a SELECT (catalog corruption)"
11749                )));
11750            };
11751            new_ctes.push(spg_sql::ast::Cte {
11752                name: name.clone(),
11753                body: spg_sql::ast::CteBody::Select(body),
11754                recursive: false,
11755                column_overrides: view.columns.clone(),
11756                search: None,
11757                cycle: None,
11758            });
11759        }
11760        let mut out = stmt.clone();
11761        // Prepend so view CTEs are visible to caller-supplied CTEs.
11762        new_ctes.extend(out.ctes);
11763        out.ctes = new_ctes;
11764        Ok(Some(out))
11765    }
11766
11767    /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
11768    /// any partition-parent table, rewrite the SELECT so each parent
11769    /// reference resolves to a CTE whose body is a `UNION ALL` over the
11770    /// children that pass the WHERE-derived partition-key range. Returns
11771    /// `None`(no rewrite needed)when no parent is referenced or all
11772    /// references are shadowed by a same-name CTE.
11773    ///
11774    /// Pruning vocabulary at v7.37.6-B:
11775    ///   * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
11776    ///     and `<key> BETWEEN literal AND literal`.
11777    ///   * Anything outside that(OR / nested IN / function call on the
11778    ///     key)defaults to "no pruning" — every child + DEFAULT lands
11779    ///     in the UNION. Correctness is preserved; only the plan size
11780    ///     widens.
11781    fn expand_partition_parents_in_select(
11782        &self,
11783        stmt: &SelectStatement,
11784    ) -> Result<Option<SelectStatement>, EngineError> {
11785        let cat = self.active_catalog();
11786        let Some(from) = &stmt.from else {
11787            return Ok(None);
11788        };
11789        let mut parent_refs: Vec<String> = Vec::new();
11790        collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
11791        for j in &from.joins {
11792            collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
11793        }
11794        // Drop names shadowed by a CTE on the same SELECT(PG semantics
11795        // — same as view expansion above).
11796        parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
11797        if parent_refs.is_empty() {
11798            return Ok(None);
11799        }
11800        // Synthesise a CTE name per parent so the existing
11801        // "CTE shadows a real table" guard doesn't fire (the parent
11802        // IS a real table in the catalog, unlike VIEW expansion's
11803        // case). The FROM-clause TableRef walker below rewrites
11804        // every parent reference to point at the synthetic CTE.
11805        let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
11806        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
11807        let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
11808        for parent_name in &parent_refs {
11809            // No children = no rewrite. The parent itself is a real
11810            // (empty-rows) table — the regular FROM-resolution path
11811            // will scan it and return 0 rows, matching the
11812            // "partition parent with no children" plan. Skipping the
11813            // CTE here also avoids `SELECT * FROM parent` re-entering
11814            // this rewrite on the synthetic body (infinite recursion).
11815            let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
11816                continue;
11817            };
11818            new_ctes.push(spg_sql::ast::Cte {
11819                name: synth_name(parent_name),
11820                body: spg_sql::ast::CteBody::Select(body),
11821                recursive: false,
11822                column_overrides: Vec::new(),
11823                search: None,
11824                cycle: None,
11825            });
11826            expanded_parents.push(parent_name.clone());
11827        }
11828        if expanded_parents.is_empty() {
11829            return Ok(None);
11830        }
11831        let mut out = stmt.clone();
11832        if let Some(from) = out.from.as_mut() {
11833            rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
11834            for j in &mut from.joins {
11835                rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
11836            }
11837        }
11838        new_ctes.extend(out.ctes);
11839        out.ctes = new_ctes;
11840        Ok(Some(out))
11841    }
11842
11843    /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
11844    /// Children include every overlap-hit `Range` plus(always)the
11845    /// `Default` child(if any). Returns `Ok(None)` when no children
11846    /// would survive — caller skips the CTE injection and lets the
11847    /// parent fall through to the regular(empty-rows)scan path,
11848    /// avoiding the infinite recursion that an empty-body CTE
11849    /// referencing the parent name would trigger.
11850    /// v7.37.16 (16.10) — public helper invoked from explain.rs to
11851    /// surface "which children survive the WHERE-clause prune" in
11852    /// EXPLAIN output. Returns `None` when `parent_name` isn't
11853    /// actually a partition parent; otherwise returns the list of
11854    /// children the planner would scan (same algorithm as
11855    /// [`Self::build_partition_parent_union_body`] but without the
11856    /// SQL re-parse).
11857    /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
11858    /// expression (the PG-shaped EXPLAIN's scan builder has no full
11859    /// SelectStatement in hand). Wraps the original by synthesising a
11860    /// minimal statement carrying just the predicate.
11861    pub(crate) fn explain_partition_kept_children_by_where(
11862        &self,
11863        parent_name: &str,
11864        where_: Option<&spg_sql::ast::Expr>,
11865    ) -> Option<Vec<alloc::string::String>> {
11866        let mut synth = SelectStatement::default();
11867        synth.where_ = where_.cloned();
11868        self.explain_partition_kept_children(parent_name, &synth)
11869    }
11870
11871    pub(crate) fn explain_partition_kept_children(
11872        &self,
11873        parent_name: &str,
11874        outer: &SelectStatement,
11875    ) -> Option<Vec<alloc::string::String>> {
11876        use spg_storage::PartitionRole;
11877        let cat = self.active_catalog();
11878        let parent = cat.get(parent_name)?;
11879        let (key_position, parent_kind) = match &parent.schema().partition_role {
11880            Some(PartitionRole::Parent {
11881                key_column_positions,
11882                kind,
11883                ..
11884            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
11885            _ => return None,
11886        };
11887        let key_col_name = parent.schema().columns[key_position].name.clone();
11888        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
11889            Some(expr) => extract_key_range(expr, &key_col_name),
11890            None => (None, None),
11891        };
11892        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
11893            Some(expr) => extract_key_eq_value(expr, &key_col_name),
11894            None => None,
11895        };
11896        let children = crate::partition::children_of_parent(cat, parent_name);
11897        let mut kept: Vec<alloc::string::String> = Vec::new();
11898        let mut default_child: Option<alloc::string::String> = None;
11899        for child_name in &children {
11900            let Some(child) = cat.get(child_name) else {
11901                continue;
11902            };
11903            match &child.schema().partition_role {
11904                Some(PartitionRole::Range { lower, upper, .. }) => {
11905                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
11906                        kept.push(child_name.clone());
11907                    }
11908                }
11909                Some(PartitionRole::List { values, .. }) => match &eq_value {
11910                    Some(v) => {
11911                        if values.iter().any(|b| b.equals_value(v)) {
11912                            kept.push(child_name.clone());
11913                        }
11914                    }
11915                    None => kept.push(child_name.clone()),
11916                },
11917                Some(PartitionRole::Hash {
11918                    modulus, remainder, ..
11919                }) => match &eq_value {
11920                    Some(v) => {
11921                        let h = crate::partition::pg_compatible_hash(v);
11922                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
11923                            kept.push(child_name.clone());
11924                        }
11925                    }
11926                    None => kept.push(child_name.clone()),
11927                },
11928                Some(PartitionRole::Default { .. }) => {
11929                    default_child = Some(child_name.clone());
11930                }
11931                _ => {}
11932            }
11933        }
11934        let _ = parent_kind;
11935        if let Some(d) = default_child {
11936            if kept.is_empty() || eq_value.is_none() {
11937                kept.push(d);
11938            }
11939        }
11940        Some(kept)
11941    }
11942
11943    fn build_partition_parent_union_body(
11944        &self,
11945        parent_name: &str,
11946        outer: &SelectStatement,
11947    ) -> Result<Option<SelectStatement>, EngineError> {
11948        use spg_storage::PartitionRole;
11949        let cat = self.active_catalog();
11950        let parent = cat.get(parent_name).ok_or_else(|| {
11951            EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
11952                "partition parent {parent_name:?} disappeared mid-expansion"
11953            )))
11954        })?;
11955        let (key_position, parent_kind) = match &parent.schema().partition_role {
11956            Some(PartitionRole::Parent {
11957                key_column_positions,
11958                kind,
11959                ..
11960            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
11961            // v7.39 (round 645) — an INHERITANCE parent, which has no
11962            // role of its own: the relationship is recorded only in the
11963            // children. Three things differ from a partition parent and
11964            // all three are in this body.
11965            //
11966            //   * The parent HOLDS ROWS, so it is a term of the union —
11967            //     `FROM ONLY`, or expanding it would recurse.
11968            //   * There is no partition key, so there is nothing to
11969            //     prune: every child is a term.
11970            //   * A child may declare columns of its own, so the terms
11971            //     name the PARENT's columns rather than `*`. PG's
11972            //     `SELECT * FROM parent` returns the parent's shape.
11973            //
11974            // Answered from this match rather than a branch before it —
11975            // round 644 measured what an extra early return beside an
11976            // existing test costs in this file.
11977            _ if crate::partition::has_inheritance_children(cat, parent_name) => {
11978                let cols = parent
11979                    .schema()
11980                    .columns
11981                    .iter()
11982                    .map(|c| quote_ident_for_sql(&c.name))
11983                    .collect::<Vec<_>>()
11984                    .join(", ");
11985                let carry_sys = references_ctid(outer);
11986                let sys = if carry_sys {
11987                    let mut t = alloc::string::String::new();
11988                    for s in SYSTEM_COLUMNS {
11989                        t.push_str(", ");
11990                        t.push_str(s);
11991                    }
11992                    t
11993                } else {
11994                    alloc::string::String::new()
11995                };
11996                let mut body = alloc::format!(
11997                    "SELECT {cols}{sys} FROM ONLY {}",
11998                    quote_ident_for_sql(parent_name)
11999                );
12000                for child in crate::partition::children_of_parent(cat, parent_name) {
12001                    body.push_str(&alloc::format!(
12002                        " UNION ALL SELECT {cols}{sys} FROM {}",
12003                        quote_ident_for_sql(&child)
12004                    ));
12005                }
12006                return parse_select_or_corrupt(&body).map(Some);
12007            }
12008            _ => {
12009                return Err(EngineError::Unsupported(alloc::format!(
12010                    "partition expansion: {parent_name:?} is not a parent"
12011                )));
12012            }
12013        };
12014        let key_col_name = parent.schema().columns[key_position].name.clone();
12015        // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
12016        // off the WHERE; for LIST / HASH we extract a single `=`
12017        // literal (and the rest of the planner falls back to "keep
12018        // every child" — same conservative path as 16.1/16.2).
12019        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
12020            Some(expr) => extract_key_range(expr, &key_col_name),
12021            None => (None, None),
12022        };
12023        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
12024            Some(expr) => extract_key_eq_value(expr, &key_col_name),
12025            None => None,
12026        };
12027        let children = crate::partition::children_of_parent(cat, parent_name);
12028        let mut kept: Vec<String> = Vec::new();
12029        let mut default_child: Option<String> = None;
12030        // First pass — apply per-strategy gates, defer DEFAULT until
12031        // we know whether some non-DEFAULT child matched.
12032        for child_name in &children {
12033            let Some(child) = cat.get(child_name) else {
12034                continue;
12035            };
12036            match &child.schema().partition_role {
12037                Some(PartitionRole::Range { lower, upper, .. }) => {
12038                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
12039                        kept.push(child_name.clone());
12040                    }
12041                }
12042                // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
12043                // = <lit>`, only the child whose values contain that
12044                // literal survives. Otherwise (no equality predicate
12045                // or planner couldn't extract one) keep the child
12046                // conservatively.
12047                Some(PartitionRole::List { values, .. }) => match &eq_value {
12048                    Some(v) => {
12049                        if values.iter().any(|b| b.equals_value(v)) {
12050                            kept.push(child_name.clone());
12051                        }
12052                    }
12053                    None => kept.push(child_name.clone()),
12054                },
12055                // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
12056                // we know the residue class deterministically, so
12057                // only the matching REMAINDER child survives.
12058                Some(PartitionRole::Hash {
12059                    modulus, remainder, ..
12060                }) => match &eq_value {
12061                    Some(v) => {
12062                        let h = crate::partition::pg_compatible_hash(v);
12063                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
12064                            kept.push(child_name.clone());
12065                        }
12066                    }
12067                    None => kept.push(child_name.clone()),
12068                },
12069                Some(PartitionRole::Default { .. }) => {
12070                    default_child = Some(child_name.clone());
12071                }
12072                _ => {}
12073            }
12074        }
12075        // PG-style DEFAULT semantics: the DEFAULT child must be
12076        // scanned iff some row could fall outside every concrete
12077        // child's bound predicate. We approximate that as "no
12078        // concrete child matched" (== full prune) — strictly
12079        // conservative for LIST / HASH (DEFAULT also catches rows
12080        // outside the union of value-sets / residues), and matches
12081        // PG for the equality case where we *do* know the routing
12082        // outcome.
12083        let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
12084        if let Some(d) = default_child {
12085            if kept.is_empty() {
12086                kept.push(d);
12087            } else if eq_value.is_none() {
12088                // Without an equality literal, the DEFAULT child may
12089                // still hold matching rows (e.g. LIKE on TEXT keys
12090                // for which a LIST partition exists). Keep it.
12091                kept.push(d);
12092            }
12093        }
12094        // Build the UNION ALL body text and re-parse — keeps the
12095        // rewrite expressible in surface SQL so the engine's existing
12096        // parser path handles the AST shape uniformly.
12097        if kept.is_empty() {
12098            // No children survive — caller falls back to scanning the
12099            // (empty) parent table. Returning None here is what
12100            // prevents the synthetic CTE from referring back to the
12101            // parent name and re-entering this rewrite pass.
12102            let _ = parent_name;
12103            return Ok(None);
12104        }
12105        // v7.39 (round 622, S05a) — the system columns of the CHILD the row
12106        // actually lives in.
12107        //
12108        // The parent is read through a synthetic CTE, so a `tableoid` on it
12109        // resolved against that CTE: every row of every child reported
12110        // `__spg_partition_pm`, an internal name no user ever typed, where
12111        // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
12112        // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
12113        // one asks "which partition is this row in", answering 0 rows where
12114        // PG answers 1. `ctid` had the same shape: it numbered the CTE's
12115        // output, so rows in different children got distinct ctids instead
12116        // of each child's own physical position.
12117        //
12118        // Naming them in the term is what carries them: the child scan
12119        // materialises its own six because the statement now references
12120        // them, and they land in SYSTEM_COLUMNS order right after the user
12121        // columns — the exact layout the positional `*` skip already
12122        // expects. Only done when the outer statement asks for one, so a
12123        // plain `SELECT * FROM parent` scans exactly what it scanned.
12124        let carry_sys = references_ctid(outer);
12125        let mut body = alloc::string::String::new();
12126        for (i, child_name) in kept.iter().enumerate() {
12127            if i > 0 {
12128                body.push_str(" UNION ALL ");
12129            }
12130            body.push_str("SELECT *");
12131            if carry_sys {
12132                for sys in SYSTEM_COLUMNS {
12133                    body.push_str(", ");
12134                    body.push_str(sys);
12135                }
12136            }
12137            body.push_str(" FROM ");
12138            body.push_str(&quote_ident_for_sql(child_name));
12139        }
12140        parse_select_or_corrupt(&body).map(Some)
12141    }
12142}
12143
12144/// Rewrite a `TableRef` pointing at a partition parent so it
12145/// references the synthetic CTE created by the expansion. If the
12146/// original ref had no alias, preserve the parent name as an alias
12147/// so column references like `events_partitioned.received_at`
12148/// keep resolving.
12149fn rewrite_partition_parent_table_ref(
12150    t: &mut spg_sql::ast::TableRef,
12151    parents: &[alloc::string::String],
12152    synth_name: &impl Fn(&str) -> alloc::string::String,
12153) {
12154    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
12155        return;
12156    }
12157    // v7.39 (round 644) — an ONLY reference stays pointed at the parent
12158    // itself. The rewrite is keyed on the NAME, so in
12159    // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
12160    // parent list and this then rewrote BOTH — including the one that
12161    // asked not to descend. PG answers 0 for that join; SPG answered 2.
12162    // Folded into the existing test — see the note in
12163    // `collect_partition_parent_refs` for what a separate one cost.
12164    if t.only || !parents.iter().any(|p| p == &t.name) {
12165        return;
12166    }
12167    if t.alias.is_none() {
12168        t.alias = Some(t.name.clone());
12169    }
12170    t.name = synth_name(&t.name);
12171}
12172
12173/// Walk a `TableRef` and push its `name` if it resolves to a partition
12174/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
12175/// `generate_series_args` references — those aren't catalog tables.
12176fn collect_partition_parent_refs(
12177    t: &spg_sql::ast::TableRef,
12178    cat: &spg_storage::Catalog,
12179    out: &mut Vec<alloc::string::String>,
12180) {
12181    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
12182        return;
12183    }
12184    // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
12185    // The keyword used to be absorbed at parse time, so this fanned out
12186    // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
12187    // answered 2 where PG answers 0.
12188    //
12189    // Folded into the existing test rather than given an early return of
12190    // its own: as two extra lines in this function's body it cost
12191    // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
12192    // outside the panel. Rounds 641 and 643 met the same wall from the
12193    // other two directions — adding to a hot function and taking away
12194    // from a cold one. What goes in a body near the row loop is a
12195    // codegen decision whatever its shape.
12196    if !t.only && crate::partition::has_children(cat, &t.name) {
12197        out.push(t.name.clone());
12198    }
12199}
12200
12201/// v7.37.6-B partition-key range derived from a WHERE expression.
12202/// `i64` microseconds since epoch with the same sign convention as
12203/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
12204/// / `=`),`false` ⇒ exclusive(`>` / `<`).
12205#[derive(Debug, Clone, Copy)]
12206pub(crate) struct PartitionFilterBound {
12207    pub micros: i64,
12208    pub inclusive: bool,
12209}
12210
12211/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
12212/// shapes; tighten the running lo / hi as we go. Anything outside that
12213/// (OR / nested calls / non-key columns)is ignored — caller treats
12214/// `None` as "no constraint on that side."
12215fn extract_key_range(
12216    expr: &spg_sql::ast::Expr,
12217    key_col: &str,
12218) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
12219    let mut lo: Option<PartitionFilterBound> = None;
12220    let mut hi: Option<PartitionFilterBound> = None;
12221    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
12222    while let Some(e) = stack.pop() {
12223        match e {
12224            spg_sql::ast::Expr::Binary {
12225                lhs,
12226                op: spg_sql::ast::BinOp::And,
12227                rhs,
12228            } => {
12229                stack.push(lhs);
12230                stack.push(rhs);
12231            }
12232            // BETWEEN is desugared at parse time into `lhs >= low AND
12233            // lhs <= high`, so it lands here as two regular Binary
12234            // arms via the AND walker above.
12235            spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
12236                let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
12237                    (Some(lhs.as_ref()), rhs.as_ref(), false)
12238                } else if is_column_ref(rhs, key_col) {
12239                    (Some(rhs.as_ref()), lhs.as_ref(), true)
12240                } else {
12241                    (None, lhs.as_ref(), false)
12242                };
12243                if col_ref.is_none() {
12244                    continue;
12245                }
12246                let Some(lit) = literal_to_micros(lit_side) else {
12247                    continue;
12248                };
12249                use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
12250                let effective_op = if swapped {
12251                    match op {
12252                        Lt => Gt,
12253                        LtEq => GtEq,
12254                        Gt => Lt,
12255                        GtEq => LtEq,
12256                        other => *other,
12257                    }
12258                } else {
12259                    *op
12260                };
12261                match effective_op {
12262                    Eq => {
12263                        tighten_lo(
12264                            &mut lo,
12265                            PartitionFilterBound {
12266                                micros: lit,
12267                                inclusive: true,
12268                            },
12269                        );
12270                        tighten_hi(
12271                            &mut hi,
12272                            PartitionFilterBound {
12273                                micros: lit,
12274                                inclusive: true,
12275                            },
12276                        );
12277                    }
12278                    GtEq => {
12279                        tighten_lo(
12280                            &mut lo,
12281                            PartitionFilterBound {
12282                                micros: lit,
12283                                inclusive: true,
12284                            },
12285                        );
12286                    }
12287                    Gt => {
12288                        tighten_lo(
12289                            &mut lo,
12290                            PartitionFilterBound {
12291                                micros: lit,
12292                                inclusive: false,
12293                            },
12294                        );
12295                    }
12296                    LtEq => {
12297                        tighten_hi(
12298                            &mut hi,
12299                            PartitionFilterBound {
12300                                micros: lit,
12301                                inclusive: true,
12302                            },
12303                        );
12304                    }
12305                    Lt => {
12306                        tighten_hi(
12307                            &mut hi,
12308                            PartitionFilterBound {
12309                                micros: lit,
12310                                inclusive: false,
12311                            },
12312                        );
12313                    }
12314                    _ => {}
12315                }
12316            }
12317            _ => {}
12318        }
12319    }
12320    (lo, hi)
12321}
12322
12323fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
12324    match slot {
12325        None => *slot = Some(new),
12326        Some(cur) => {
12327            if new.micros > cur.micros
12328                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
12329            {
12330                *slot = Some(new);
12331            }
12332        }
12333    }
12334}
12335
12336fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
12337    match slot {
12338        None => *slot = Some(new),
12339        Some(cur) => {
12340            if new.micros < cur.micros
12341                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
12342            {
12343                *slot = Some(new);
12344            }
12345        }
12346    }
12347}
12348
12349fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
12350    if let spg_sql::ast::Expr::Column(c) = e {
12351        c.name.eq_ignore_ascii_case(key_col)
12352    } else {
12353        false
12354    }
12355}
12356
12357/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
12358/// `key_col = <literal>` predicate out for LIST/HASH partition
12359/// pruning. Returns `None` when no equality literal can be lifted
12360/// (planner then keeps every child — correctness preserved). The
12361/// returned `Value<'static>` is an owned coercion so the caller can
12362/// outlive any AST node it was extracted from.
12363pub(crate) fn extract_key_eq_value(
12364    expr: &spg_sql::ast::Expr,
12365    key_col: &str,
12366) -> Option<spg_storage::Value<'static>> {
12367    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
12368    while let Some(e) = stack.pop() {
12369        match e {
12370            spg_sql::ast::Expr::Binary {
12371                lhs,
12372                op: spg_sql::ast::BinOp::And,
12373                rhs,
12374            } => {
12375                stack.push(lhs);
12376                stack.push(rhs);
12377            }
12378            spg_sql::ast::Expr::Binary {
12379                lhs,
12380                op: spg_sql::ast::BinOp::Eq,
12381                rhs,
12382            } => {
12383                let lit_side = if is_column_ref(lhs, key_col) {
12384                    rhs.as_ref()
12385                } else if is_column_ref(rhs, key_col) {
12386                    lhs.as_ref()
12387                } else {
12388                    continue;
12389                };
12390                let cloned = lit_side.clone();
12391                let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
12392                    continue;
12393                };
12394                // Coerce to an owned Value<'static> so the caller
12395                // can hold it past the WHERE expression's lifetime.
12396                let owned: spg_storage::Value<'static> = match v {
12397                    spg_storage::Value::Text(s) => {
12398                        spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
12399                    }
12400                    spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
12401                    spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
12402                    spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
12403                    spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
12404                    spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
12405                    spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
12406                    spg_storage::Value::Null => spg_storage::Value::Null,
12407                    // Anything else (Vector / Json / Bytes / Numeric /
12408                    // arrays / interval / …) isn't a current partition
12409                    // key type; skip without pruning.
12410                    _ => continue,
12411                };
12412                return Some(owned);
12413            }
12414            _ => {}
12415        }
12416    }
12417    None
12418}
12419
12420/// Coerce a literal Expr(after the parser folded sequence calls etc.)
12421/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
12422/// pruning and routing agree on the literal vocabulary. Returns
12423/// `None` when the literal isn't recognised(planner then skips
12424/// pruning on that branch — correctness preserved).
12425fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
12426    let cloned = e.clone();
12427    let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
12428    match value {
12429        spg_storage::Value::Timestamp(m) => Some(m),
12430        spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
12431        spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
12432        _ => None,
12433    }
12434}
12435
12436/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
12437/// satisfying the WHERE-derived filter range. PG-style half-open:
12438/// child upper exclusive. Filter inclusivity is honoured per-bound.
12439fn range_satisfies_filter(
12440    range_lo: &spg_storage::PartitionBound,
12441    range_hi: &spg_storage::PartitionBound,
12442    filter_lo: Option<&PartitionFilterBound>,
12443    filter_hi: Option<&PartitionFilterBound>,
12444) -> bool {
12445    use spg_storage::PartitionBound;
12446    // For each filter side, reject children that can't host any row
12447    // matching the predicate.
12448    if let Some(lo) = filter_lo {
12449        // child upper bound vs filter lower:
12450        //   if filter is x >= L, child rejects iff child.hi <= L
12451        //   if filter is x  > L, child rejects iff child.hi <= L
12452        //   (child.hi exclusive, so equality with L still rejects)
12453        match range_hi {
12454            PartitionBound::MinValue => return false,
12455            PartitionBound::MaxValue => {}
12456            PartitionBound::TimestampTz(hi) => {
12457                if *hi <= lo.micros {
12458                    return false;
12459                }
12460            }
12461            // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
12462            // matched against TIMESTAMPTZ filters here; keep child
12463            // (conservative: don't prune).
12464            PartitionBound::BigInt(_)
12465            | PartitionBound::Int(_)
12466            | PartitionBound::SmallInt(_)
12467            | PartitionBound::Date(_)
12468            | PartitionBound::Text(_) => {}
12469        }
12470    }
12471    if let Some(hi) = filter_hi {
12472        // child lower bound vs filter upper:
12473        //   if filter is x <= U, child rejects iff child.lo > U
12474        //   if filter is x  < U, child rejects iff child.lo >= U
12475        match range_lo {
12476            PartitionBound::MaxValue => return false,
12477            PartitionBound::MinValue => {}
12478            PartitionBound::TimestampTz(lo) => {
12479                let rejects = if hi.inclusive {
12480                    *lo > hi.micros
12481                } else {
12482                    *lo >= hi.micros
12483                };
12484                if rejects {
12485                    return false;
12486                }
12487            }
12488            PartitionBound::BigInt(_)
12489            | PartitionBound::Int(_)
12490            | PartitionBound::SmallInt(_)
12491            | PartitionBound::Date(_)
12492            | PartitionBound::Text(_) => {}
12493        }
12494    }
12495    true
12496}
12497
12498fn quote_ident_for_sql(name: &str) -> alloc::string::String {
12499    // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
12500    // identifier, otherwise quoted). Conservative: always quote so
12501    // children with reserved names round-trip safely through the
12502    // CTE-body parse.
12503    let mut out = alloc::string::String::with_capacity(name.len() + 2);
12504    out.push('"');
12505    for c in name.chars() {
12506        if c == '"' {
12507            out.push('"');
12508        }
12509        out.push(c);
12510    }
12511    out.push('"');
12512    out
12513}
12514
12515fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
12516    let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
12517        EngineError::Unsupported(alloc::format!(
12518            "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
12519        ))
12520    })?;
12521    let Statement::Select(body) = parsed else {
12522        return Err(EngineError::Unsupported(alloc::format!(
12523            "partition expansion: generated SQL {sql:?} is not a SELECT"
12524        )));
12525    };
12526    Ok(body)
12527}
12528
12529/// v7.39 (read01 round 65/66) — the column shape a set-returning function
12530/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
12531/// yields ONE column named after the call's alias when there is one (`FROM
12532/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
12533/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
12534fn setof_column_shape_from(
12535    declared: &str,
12536    name: &str,
12537    alias: Option<&str>,
12538    got: &[ColumnSchema],
12539) -> alloc::vec::Vec<ColumnSchema> {
12540    let upper = declared.to_ascii_uppercase();
12541    if upper.starts_with("TABLE(") {
12542        let raw = &declared["TABLE(".len()..declared.len() - 1];
12543        return raw
12544            .split(',')
12545            .zip(got.iter())
12546            .map(|(decl, g)| {
12547                let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
12548                ColumnSchema::new(cname.to_string(), g.ty, true)
12549            })
12550            .collect();
12551    }
12552    let cname = alias.unwrap_or(name);
12553    got.first()
12554        .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
12555        .unwrap_or_default()
12556}
12557
12558/// The plpgsql twin: the interpreter hands back raw value rows, so the types
12559/// come off the first row.
12560fn setof_column_shape(
12561    declared: &str,
12562    name: &str,
12563    alias: Option<&str>,
12564    first_row: Option<&alloc::vec::Vec<Value<'static>>>,
12565) -> alloc::vec::Vec<ColumnSchema> {
12566    let got: alloc::vec::Vec<ColumnSchema> = first_row
12567        .map(|r| {
12568            r.iter()
12569                .enumerate()
12570                .map(|(i, v)| {
12571                    ColumnSchema::new(
12572                        alloc::format!("col{i}"),
12573                        v.data_type().unwrap_or(DataType::Text),
12574                        true,
12575                    )
12576                })
12577                .collect()
12578        })
12579        .unwrap_or_default();
12580    setof_column_shape_from(declared, name, alias, &got)
12581}
12582
12583/// v7.39 (read01 round 67) — expand every set-returning call in a target list
12584/// for ONE input row, PG's ProjectSet semantics.
12585///
12586/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
12587/// output has as many rows as the LONGEST of them, and a shorter one is padded
12588/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
12589/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
12590/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
12591/// is zero rows, not one NULL row.
12592///
12593/// Non-SRF items repeat, evaluated once per output row from the same input row.
12594/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
12595/// used to reach the scalar function dispatcher, which reported the aggregate as
12596/// an *unknown function* — the same "symptom two layers above the cause" shape
12597/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
12598/// sees a call, not the clause it came from. The statement knows.
12599/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
12600/// clause may appear.
12601///
12602/// PG rejects `FOR UPDATE` on exactly the shapes that have no
12603/// identifiable base row to lock, each with its own wording. SPG
12604/// accepted all of them and locked nothing, so a query that PG refuses
12605/// outright came back looking like it had taken locks.
12606///
12607/// Every wording read off live PG 18.4.
12608fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
12609    let Some(lock) = &stmt.locking else {
12610        return Ok(());
12611    };
12612    let verb = lock_clause_verb(lock.strength);
12613    let refuse = |what: &str| {
12614        Err(EngineError::Unsupported(alloc::format!(
12615            "{verb} is not allowed with {what}"
12616        )))
12617    };
12618    if !stmt.unions.is_empty() {
12619        return refuse("UNION/INTERSECT/EXCEPT");
12620    }
12621    if stmt.distinct || !stmt.distinct_on.is_empty() {
12622        return refuse("DISTINCT clause");
12623    }
12624    if stmt.group_by.is_some() || stmt.group_by_all {
12625        return refuse("GROUP BY clause");
12626    }
12627    let has_agg = stmt.items.iter().any(|it| match it {
12628        spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
12629        _ => false,
12630    });
12631    if has_agg {
12632        return refuse("aggregate functions");
12633    }
12634    // `FOR UPDATE OF t` must name a relation that is actually in FROM.
12635    for want in &lock.of_tables {
12636        if !locking_from_names(stmt)
12637            .iter()
12638            .any(|n| n.eq_ignore_ascii_case(want))
12639        {
12640            return Err(EngineError::Unsupported(alloc::format!(
12641                "relation \"{want}\" in {verb} clause not found in FROM clause"
12642            )));
12643        }
12644    }
12645    Ok(())
12646}
12647
12648/// How PG names the clause in its diagnostics.
12649const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
12650    use spg_sql::ast::LockStrength as LS;
12651    match s {
12652        LS::Update => "FOR UPDATE",
12653        LS::NoKeyUpdate => "FOR NO KEY UPDATE",
12654        LS::Share => "FOR SHARE",
12655        LS::KeyShare => "FOR KEY SHARE",
12656    }
12657}
12658
12659/// Every relation name (or alias) the FROM clause exposes.
12660fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
12661    let mut out = alloc::vec::Vec::new();
12662    if let Some(f) = &stmt.from {
12663        let mut push = |t: &spg_sql::ast::TableRef| {
12664            if let Some(a) = &t.alias {
12665                out.push(a.clone());
12666            }
12667            out.push(t.name.clone());
12668        };
12669        push(&f.primary);
12670        for j in &f.joins {
12671            push(&j.table);
12672        }
12673    }
12674    out
12675}
12676
12677fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
12678    use spg_sql::ast::Expr;
12679    if let Some(w) = &stmt.where_
12680        && aggregate::contains_aggregate(w)
12681    {
12682        return Err(EngineError::Unsupported(
12683            "aggregate functions are not allowed in WHERE".into(),
12684        ));
12685    }
12686    let mut nested = false;
12687    let mut check = |e: &Expr| {
12688        let mut probe = e.clone();
12689        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
12690            let args = match n {
12691                Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
12692                _ => return false,
12693            };
12694            if args.iter().any(aggregate::contains_aggregate) {
12695                nested = true;
12696            }
12697            false
12698        });
12699    };
12700    for it in &stmt.items {
12701        if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
12702            check(expr);
12703        }
12704    }
12705    if let Some(h) = &stmt.having {
12706        check(h);
12707    }
12708    for o in &stmt.order_by {
12709        check(&o.expr);
12710    }
12711    if nested {
12712        return Err(EngineError::Unsupported(
12713            "aggregate function calls cannot be nested".into(),
12714        ));
12715    }
12716    Ok(())
12717}
12718
12719/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
12720/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
12721/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
12722/// to a set and then applies the enclosing expression once per element. SPG only
12723/// ever recognised an SRF that WAS the item, so everything above died on
12724/// "unknown function unnest" — the set-returning call, wrapped in anything at
12725/// all, fell through to the scalar function dispatcher which has no such name.
12726///
12727/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
12728/// rewritten to read that column, and the rewritten expression is evaluated once
12729/// per output row against the input row extended with the lifted values. The
12730/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
12731/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
12732/// executors (the single-table scan, the synthetic-table pipeline, and the
12733/// unnest FROM path) each evaluated the key as an ordinary expression, where the
12734/// literal `n` is just the constant n — the same sort key for every row. The
12735/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
12736/// back in input order, not in a wrong order. Statement prep resolves the common
12737/// case, but only when the SELECT item is an expression — a `*` is not one, and
12738/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
12739/// spelling landed on exactly the shape prep could not resolve.
12740///
12741/// A set-returning item is left alone: copying it into ORDER BY would make the
12742/// key "the whole set", evaluated once per INPUT row.
12743fn resolve_positional_order_by(
12744    order_by: &[spg_sql::ast::OrderBy],
12745    projection: &[ProjectedItem],
12746) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
12747    order_by
12748        .iter()
12749        .map(|o| {
12750            let mut o = o.clone();
12751            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
12752                && *n >= 1
12753                && let Ok(idx) = usize::try_from(*n - 1)
12754                && let Some(item) = projection.get(idx)
12755                && !expr_contains_builtin_srf(&item.expr)
12756            {
12757                o.expr = item.expr.clone();
12758            }
12759            o
12760        })
12761        .collect()
12762}
12763
12764/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
12765/// this expression? Statement preparation (`resolve_order_by_position`) runs
12766/// before any catalog is in hand, and it only needs to know "is this item's value
12767/// a set", which the builtin SRFs answer syntactically.
12768pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
12769    let mut found = false;
12770    let mut probe = e.clone();
12771    crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
12772        if is_top_level_unnest(n) {
12773            found = true;
12774            return true;
12775        }
12776        false
12777    });
12778    found
12779}
12780
12781/// v7.39 (round 599) — everything about a target-list SRF that does not
12782/// depend on the row.
12783///
12784/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
12785/// each SRF-bearing projection expression, walked and rewrote the tree,
12786/// formatted a `__srf_N` name per node, and copied the whole column schema.
12787/// A counting allocator put the path at 24 allocations per input row for a
12788/// single-element `unnest`, against 0 for the same scan without one — 211 MB
12789/// where the plain scan took 4.3 — and the shape held whatever the array
12790/// contained, which is what invariant work looks like.
12791struct SrfPlan {
12792    /// The lifted SRF calls, in slot order.
12793    nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
12794    /// Per projection position, the expression with its SRF calls replaced
12795    /// by `__srf_N` column references. `None` means the item has none.
12796    rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
12797    /// The input schema followed by one column per slot. Only the slots'
12798    /// TYPES vary per row, and they are patched in place.
12799    ext_cols: alloc::vec::Vec<ColumnSchema>,
12800    /// v7.39 (round 743) — the rewritten projection COMPILED against the
12801    /// extended schema, once per plan. The per-output-row evaluation ran
12802    /// the interpreter (~560 ns/row on the unnest panel cell); the Step
12803    /// VM reads the `__srf_N` slots as plain columns. `None` = that item
12804    /// is not fully compilable and keeps the interpreter.
12805    compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
12806    base_cols: usize,
12807}
12808
12809fn build_srf_plan(
12810    engine: &Engine,
12811    projection: &[ProjectedItem],
12812    srf_idxs: &[usize],
12813    ctx: &EvalContext<'_>,
12814) -> Result<SrfPlan, EngineError> {
12815    // Lift every SRF node out of every item that contains one.
12816    let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
12817    let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
12818    let mut reject: Option<EngineError> = None;
12819    for &i in srf_idxs {
12820        let mut e = projection[i].expr.clone();
12821        crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
12822            if reject.is_some() {
12823                return true;
12824            }
12825            // PG refuses a set-returning function inside a conditional: the set
12826            // would have to be produced before anyone knows whether the branch
12827            // is even taken.
12828            let conditional = match n {
12829                spg_sql::ast::Expr::Case { .. } => Some("CASE"),
12830                spg_sql::ast::Expr::FunctionCall { name, .. }
12831                    if name.eq_ignore_ascii_case("coalesce") =>
12832                {
12833                    Some("COALESCE")
12834                }
12835                _ => None,
12836            };
12837            if let Some(kind) = conditional
12838                && engine.expr_contains_srf(n)
12839            {
12840                reject = Some(EngineError::Unsupported(alloc::format!(
12841                    "set-returning functions are not allowed in {kind}"
12842                )));
12843                return true;
12844            }
12845            if !engine.is_srf_node(n) {
12846                return false;
12847            }
12848            let slot = nodes.len();
12849            nodes.push(n.clone());
12850            *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
12851                qualifier: None,
12852                name: alloc::format!("__srf_{slot}"),
12853            });
12854            true
12855        });
12856        rewritten[i] = Some(e);
12857    }
12858    if let Some(err) = reject {
12859        return Err(err);
12860    }
12861    let base_cols = ctx.columns.len();
12862    let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
12863    for slot in 0..nodes.len() {
12864        ext_cols.push(ColumnSchema::new(
12865            alloc::format!("__srf_{slot}"),
12866            DataType::Text,
12867            true,
12868        ));
12869    }
12870    // v7.39 (round 743) — compile the rewritten items against the
12871    // EXTENDED schema. The slot columns' declared type is a per-row
12872    // patched detail the compiled column read does not consult.
12873    let compiled: Vec<Option<eval::CompiledExpr>> = {
12874        let mut ext_ctx = ctx.clone();
12875        ext_ctx.columns = &ext_cols;
12876        projection
12877            .iter()
12878            .enumerate()
12879            .map(|(i, p)| {
12880                let e = rewritten[i].as_ref().unwrap_or(&p.expr);
12881                if eval::fully_compilable(e) {
12882                    Some(eval::compile_expr(e, &ext_ctx))
12883                } else {
12884                    None
12885                }
12886            })
12887            .collect()
12888    };
12889    Ok(SrfPlan {
12890        nodes,
12891        rewritten,
12892        ext_cols,
12893        compiled,
12894        base_cols,
12895    })
12896}
12897
12898/// One input row expanded through a plan built once for the whole scan.
12899/// v7.39 (round 621) — expand a projection whose target list contains
12900/// set-returning items, remembering which INPUT row each output row came from.
12901///
12902/// The three materialised-source tails — `FROM unnest(…)`, `FROM
12903/// generate_series(…)`, and the one that serves VALUES / a derived table /
12904/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
12905/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
12906/// v(x)` answered `function unnest(integer[]) does not exist` on all the
12907/// others, for a query PG answers. Sharing the expansion is the point: a
12908/// fourth copy would have been the fourth place to forget.
12909fn expand_projection_srfs(
12910    engine: &Engine,
12911    projection: &[ProjectedItem],
12912    srf_idxs: &[usize],
12913    filtered: &[Row<'static>],
12914    ctx: &EvalContext<'_>,
12915) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
12916    let mut out = alloc::vec::Vec::with_capacity(filtered.len());
12917    let mut src = alloc::vec::Vec::with_capacity(filtered.len());
12918    // v7.39 (round 726) — ONE plan for the whole scan. The per-row
12919    // spelling rebuilt it for every input row: a full clone of the
12920    // rewritten projection trees and the extended schema, 50k times on
12921    // the panel's unnest cell.
12922    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
12923    // v7.39 (round 733) — shard the expansion. Each shard clones the
12924    // plan (its ext_cols slot types are per-row mutable) and builds a
12925    // MINIMAL context — EvalContext is not Sync — which is sound only
12926    // when every expression involved is pure: the whole projection and
12927    // every SRF argument must be fully_compilable, or the row loop
12928    // stays serial with the full session context.
12929    // The projection is judged in its REWRITTEN form — the SRF call
12930    // itself is never compilable, but after the lift it is a plain
12931    // `__srf_N` column reference.
12932    let all_pure = projection
12933        .iter()
12934        .enumerate()
12935        .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
12936        && plan.nodes.iter().all(|n| match n {
12937            Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
12938            other => eval::fully_compilable(other),
12939        });
12940    if all_pure
12941        && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
12942        && let Some(r) = engine.parallel_runner.0.as_deref()
12943    {
12944        let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
12945        let chunk = filtered.len().div_ceil(n_shards);
12946        type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
12947        let schema_cols = ctx.columns;
12948        let alias = ctx.table_alias;
12949        let mysql = ctx.mysql_dialect;
12950        let style = ctx.render_style;
12951        let plan_ref = &plan;
12952        let results = r.run_shards(n_shards, &|si| {
12953            let lo = si * chunk;
12954            let hi = ((si + 1) * chunk).min(filtered.len());
12955            let mut sctx = eval::EvalContext::new(schema_cols, alias);
12956            sctx.mysql_dialect = mysql;
12957            sctx.render_style = style;
12958            // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
12959            // compiled programs); each shard rebuilds it, which also
12960            // recompiles against the shard's own context. Build errors
12961            // were already surfaced by the outer build above.
12962            let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
12963                Ok(p) => p,
12964                Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
12965            };
12966            let mut run = || -> ShardOut {
12967                let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
12968                let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
12969                for (i, row) in filtered[lo..hi].iter().enumerate() {
12970                    let expanded =
12971                        expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
12972                    sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
12973                    o.extend(expanded);
12974                }
12975                Ok((o, sidx))
12976            };
12977            alloc::boxed::Box::new(run())
12978        });
12979        for boxed in results {
12980            let shard = boxed
12981                .downcast::<ShardOut>()
12982                .expect("runner echoes the closure's box");
12983            let (o, sidx) = (*shard)?;
12984            out.extend(o);
12985            src.extend(sidx);
12986        }
12987        return Ok((out, src));
12988    }
12989    for (i, row) in filtered.iter().enumerate() {
12990        let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
12991        src.extend(core::iter::repeat_n(i, expanded.len()));
12992        out.extend(expanded);
12993    }
12994    Ok((out, src))
12995}
12996
12997/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
12998///
12999/// A key that names a select-list item reads it out of the EXPANDED row,
13000/// because PG sorts after the expansion. A key that names a source column the
13001/// query does not project is evaluated against the input row that output row
13002/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
13003fn srf_order_key(
13004    ob: &spg_sql::ast::OrderBy,
13005    out_col: Option<usize>,
13006    out: &Row<'static>,
13007    src: &Row<'static>,
13008    ctx: &EvalContext<'_>,
13009) -> Result<Value<'static>, EngineError> {
13010    match out_col {
13011        Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
13012        None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
13013    }
13014}
13015
13016fn expand_srf_row_with(
13017    engine: &Engine,
13018    plan: &mut SrfPlan,
13019    projection: &[ProjectedItem],
13020    row: &Row<'static>,
13021    ctx: &EvalContext<'_>,
13022) -> Result<Vec<Row<'static>>, EngineError> {
13023    let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
13024    for n in &plan.nodes {
13025        lists.push(engine.srf_values(n, row, ctx)?);
13026    }
13027    let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
13028    // Only the slots' element types depend on the row; the names and the
13029    // input schema around them do not.
13030    for (slot, list) in lists.iter().enumerate() {
13031        plan.ext_cols[plan.base_cols + slot].ty = list
13032            .iter()
13033            .find_map(|v| v.data_type())
13034            .unwrap_or(DataType::Text);
13035    }
13036    let mut ext_ctx = ctx.clone();
13037    ext_ctx.columns = &plan.ext_cols;
13038    let mut out = Vec::with_capacity(n_rows);
13039    // v7.39 (round 726) — the base columns are the SAME for every
13040    // expanded row; clone them once and rewrite only the SRF slots per
13041    // k. The old form cloned the whole input row per OUTPUT row — for
13042    // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
13043    // TEXT column the projection never reads.
13044    let base_len = row.values.len();
13045    let mut ext_vals = row.values.clone();
13046    ext_vals.resize(base_len + lists.len(), Value::Null);
13047    let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
13048    for k in 0..n_rows {
13049        for (slot, list) in lists.iter().enumerate() {
13050            // Past the end of THIS srf's rows → NULL (PG pads).
13051            ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
13052        }
13053        let ext_row = Row::new(core::mem::take(&mut ext_vals));
13054        let mut vals = Vec::with_capacity(projection.len());
13055        for (i, p) in projection.iter().enumerate() {
13056            // v7.39 (round 743) — compiled when possible; the
13057            // interpreter for the rest, with its exact wording.
13058            vals.push(match &plan.compiled[i] {
13059                Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
13060                    .map_err(EngineError::Eval)?,
13061                None => {
13062                    let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
13063                    eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
13064                }
13065            });
13066        }
13067        ext_vals = ext_row.values;
13068        out.push(Row::new(vals));
13069    }
13070    Ok(out)
13071}
13072
13073/// The one-shot spelling, for the callers that expand a single row.
13074/// v7.39 (round 600) — which output column each ORDER BY key names, for a
13075/// query whose target list contains a set-returning function.
13076///
13077/// The keys used to be built from the INPUT row, before the SRF expanded, so
13078/// anything that named the SRF's own output was evaluated as a scalar call:
13079/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
13080/// "function unnest(integer[]) does not exist", and so did the spellings that
13081/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
13082/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
13083/// back in input order. PG sorts AFTER the expansion, so a key that names a
13084/// select-list item reads that item's value out of the expanded row.
13085///
13086/// `None` keeps the key on the input row, which is where an ORDER BY naming
13087/// a column the query does not project has to be evaluated.
13088fn srf_order_output_cols(
13089    order_by: &[spg_sql::ast::OrderBy],
13090    projection: &[ProjectedItem],
13091) -> Vec<Option<usize>> {
13092    order_by
13093        .iter()
13094        .map(|ob| {
13095            // A positive ordinal is the Nth output column, directly.
13096            // `resolve_positional_order_by` deliberately leaves an ordinal
13097            // pointing at a set-returning item alone — copying the call into
13098            // ORDER BY would have made the key "the whole set" back when keys
13099            // came from the input row. Reading the expanded row's column is
13100            // what it should have meant, and is what this does.
13101            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
13102                && *n >= 1
13103                && let Ok(idx) = usize::try_from(*n - 1)
13104                && idx < projection.len()
13105            {
13106                return Some(idx);
13107            }
13108            // An unqualified name matching exactly one output name. SQL
13109            // resolves ORDER BY against the select list first, so this wins
13110            // over an input column of the same name — which is the whole
13111            // point of `SELECT g AS id … ORDER BY id`.
13112            if let Expr::Column(c) = &ob.expr
13113                && c.qualifier.is_none()
13114            {
13115                let mut hit = None;
13116                for (i, p) in projection.iter().enumerate() {
13117                    if p.output_name.eq_ignore_ascii_case(&c.name) {
13118                        if hit.is_some() {
13119                            hit = None;
13120                            break;
13121                        }
13122                        hit = Some(i);
13123                    }
13124                }
13125                if hit.is_some() {
13126                    return hit;
13127                }
13128            }
13129            // Or the same expression as a select-list item — which is what
13130            // `ORDER BY 1` becomes once `resolve_positional_order_by` has
13131            // run, and what a repeated `ORDER BY unnest(…)` is.
13132            projection.iter().position(|p| p.expr == ob.expr)
13133        })
13134        .collect()
13135}
13136
13137fn expand_srf_row(
13138    engine: &Engine,
13139    projection: &[ProjectedItem],
13140    srf_idxs: &[usize],
13141    row: &Row<'static>,
13142    ctx: &EvalContext<'_>,
13143) -> Result<Vec<Row<'static>>, EngineError> {
13144    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
13145    expand_srf_row_with(engine, &mut plan, projection, row, ctx)
13146}
13147
13148impl Engine {
13149    /// The rows one target-list SRF yields for an input row. `None` from
13150    /// `srf_target_idxs` means the expression is not set-returning at all.
13151    fn srf_values(
13152        &self,
13153        expr: &spg_sql::ast::Expr,
13154        row: &Row<'static>,
13155        ctx: &EvalContext<'_>,
13156    ) -> Result<Vec<Value<'static>>, EngineError> {
13157        if top_level_srf_kind(expr).is_some() {
13158            return top_level_srf_output(expr, row, ctx);
13159        }
13160        // A user set-returning function. Its body runs through the real
13161        // executor, like every function body since round 63.
13162        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
13163            return Err(EngineError::Unsupported(
13164                "expected a SELECT-list SRF call".into(),
13165            ));
13166        };
13167        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
13168        for a in args {
13169            vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
13170        }
13171        let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
13172        // v7.39 (read01 round 68) — in a target list a multi-column function is
13173        // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
13174        // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
13175        // what it is for. A single-column function contributes its bare value.
13176        Ok(rows
13177            .into_iter()
13178            .map(|r| {
13179                if r.values.len() == 1 {
13180                    r.values.into_iter().next().unwrap_or(Value::Null)
13181                } else {
13182                    Value::Composite(
13183                        cols.iter()
13184                            .map(|c| c.name.clone())
13185                            .zip(r.values)
13186                            .collect::<alloc::vec::Vec<_>>(),
13187                    )
13188                }
13189            })
13190            .collect())
13191    }
13192
13193    /// Is THIS node a set-returning call: one of the builtin kinds, or a user
13194    /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
13195    fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
13196        if is_top_level_unnest(e) {
13197            return true;
13198        }
13199        let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
13200            return false;
13201        };
13202        self.active_catalog().functions_named(name).iter().any(|f| {
13203            let r = f.returns.trim().to_ascii_uppercase();
13204            r.starts_with("SETOF") || r.starts_with("TABLE(")
13205        })
13206    }
13207
13208    /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
13209    fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
13210        let mut found = false;
13211        let mut probe = e.clone();
13212        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
13213            if self.is_srf_node(n) {
13214                found = true;
13215                return true;
13216            }
13217            false
13218        });
13219        found
13220    }
13221
13222    /// Which projection items CONTAIN a set-returning call. Before round 78 this
13223    /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
13224    /// ordinary scalar call all the way down to the function dispatcher, which
13225    /// then reported `unnest` as an unknown function.
13226    fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
13227        projection
13228            .iter()
13229            .enumerate()
13230            .filter(|(_, p)| self.expr_contains_srf(&p.expr))
13231            .map(|(i, _)| i)
13232            .collect()
13233    }
13234}
13235
13236impl Engine {
13237    /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
13238    /// no `(f(args)).*` item.
13239    fn lower_record_expansion(
13240        &self,
13241        stmt: &SelectStatement,
13242    ) -> Result<Option<SelectStatement>, EngineError> {
13243        use spg_sql::ast::{Expr, SelectItem};
13244        let is_marker = |it: &SelectItem| {
13245            matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
13246                if name == "__record_expand")
13247        };
13248        if !stmt.items.iter().any(is_marker) {
13249            return Ok(None);
13250        }
13251        let mut out = stmt.clone();
13252        let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
13253        let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
13254        for (n, item) in stmt.items.iter().enumerate() {
13255            if !is_marker(item) {
13256                items.push(item.clone());
13257                continue;
13258            }
13259            let SelectItem::Expr {
13260                expr: Expr::FunctionCall { args, .. },
13261                ..
13262            } = item
13263            else {
13264                unreachable!("checked by is_marker");
13265            };
13266            let Some(Expr::FunctionCall {
13267                name: fname,
13268                args: fargs,
13269            }) = args.first()
13270            else {
13271                return Err(EngineError::Unsupported(
13272                    "(<expr>).* expands a function's record — it needs a function call".into(),
13273                ));
13274            };
13275            let cols = self.setof_declared_columns(fname)?;
13276            let alias = alloc::format!("__rec{n}");
13277            let mut tref = bare_table_ref_named(&alias);
13278            tref.table_fn_call = Some(alloc::boxed::Box::new((
13279                fname.to_ascii_lowercase(),
13280                fargs.clone(),
13281            )));
13282            tref.alias = Some(alias.clone());
13283            lateral_refs.push(tref);
13284            for c in cols {
13285                items.push(SelectItem::Expr {
13286                    expr: Expr::Column(spg_sql::ast::ColumnName {
13287                        qualifier: Some(alias.clone()),
13288                        name: c,
13289                    }),
13290                    alias: None,
13291                });
13292            }
13293        }
13294        out.items = items;
13295        // The function joins the FROM. With no FROM it BECOMES the FROM; with one
13296        // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
13297        // (the arguments may reference the outer row — the round-69 correlation).
13298        for tref in lateral_refs {
13299            match &mut out.from {
13300                None => {
13301                    out.from = Some(spg_sql::ast::FromClause {
13302                        primary: tref,
13303                        joins: alloc::vec::Vec::new(),
13304                    });
13305                }
13306                Some(from) => from.joins.push(spg_sql::ast::FromJoin {
13307                    kind: spg_sql::ast::JoinKind::Cross,
13308                    table: tref,
13309                    on: None,
13310                    using_cols: None,
13311                    natural: false,
13312                }),
13313            }
13314        }
13315        Ok(Some(out))
13316    }
13317
13318    /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
13319    /// v text)` names them; a `SETOF <scalar>` is one column named after the
13320    /// function.
13321    fn setof_declared_columns(
13322        &self,
13323        name: &str,
13324    ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
13325        let cat = self.active_catalog();
13326        let overloads = cat.functions_named(name);
13327        let def = overloads.first().ok_or_else(|| {
13328            EngineError::Unsupported(alloc::format!("function {name} does not exist"))
13329        })?;
13330        let declared = def.returns.trim();
13331        let upper = declared.to_ascii_uppercase();
13332        if upper.starts_with("TABLE(") {
13333            let raw = &declared["TABLE(".len()..declared.len() - 1];
13334            return Ok(raw
13335                .split(',')
13336                .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
13337                .collect());
13338        }
13339        Ok(alloc::vec![name.to_string()])
13340    }
13341}
13342
13343/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
13344/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
13345/// COLUMNS list (data-independent), NESTED children inlined in
13346/// declaration order (PG's flattened output shape).
13347/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
13348/// correlated JSON_TABLE's static schema without evaluating its doc.
13349pub(crate) fn json_table_schema_pub(
13350    cols: &[spg_sql::ast::JsonTableColumn],
13351) -> alloc::vec::Vec<ColumnSchema> {
13352    json_table_schema(cols)
13353}
13354
13355fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
13356    use spg_sql::ast::JsonTableColumn as C;
13357    let mut out = alloc::vec::Vec::new();
13358    for c in cols {
13359        match c {
13360            C::Ordinality { name } => {
13361                out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
13362            }
13363            C::Regular {
13364                name, ty, exists, ..
13365            } => {
13366                let dt = if *exists {
13367                    DataType::Bool
13368                } else {
13369                    crate::conversions::column_type_to_data_type(*ty)
13370                };
13371                out.push(ColumnSchema::new(name.clone(), dt, true));
13372            }
13373            C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
13374        }
13375    }
13376    out
13377}
13378
13379/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
13380/// JSON_TABLE column's declared type (the DEFAULT expr may be a
13381/// string literal like `'none'` that must land as the column type).
13382fn coerce_json_table_default(
13383    v: Value<'static>,
13384    ty: spg_sql::ast::ColumnTypeName,
13385    name: &str,
13386) -> Result<Value<'static>, EngineError> {
13387    if v.is_null() {
13388        return Ok(Value::Null);
13389    }
13390    let dt = crate::conversions::column_type_to_data_type(ty);
13391    crate::conversions::coerce_value(v, dt, name, 0)
13392}
13393
13394/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
13395fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
13396    use crate::json::JsonValue as J;
13397    match v {
13398        Value::Null => J::Null,
13399        Value::Bool(b) => J::Bool(*b),
13400        Value::SmallInt(n) => J::Number(f64::from(*n)),
13401        Value::Int(n) => J::Number(f64::from(*n)),
13402        Value::BigInt(n) => J::Number(*n as f64),
13403        Value::Float(x) => J::Number(*x),
13404        Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
13405        other => J::String(crate::eval::value_to_text(other)),
13406    }
13407}
13408
13409fn bare_table_ref_named(name: &str) -> TableRef {
13410    TableRef {
13411        name: name.to_string(),
13412        alias: None,
13413        only: false,
13414        as_of_segment: None,
13415        unnest_expr: None,
13416        unnest_column_aliases: alloc::vec::Vec::new(),
13417        with_ordinality: false,
13418        generate_series_args: None,
13419        lateral_subquery: None,
13420        jsonb_each_text_arg: None,
13421        table_fn_call: None,
13422        rows_from: None,
13423        json_table: None,
13424        scalar_fn_item: false,
13425    }
13426}
13427
13428impl Engine {
13429    /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
13430    /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
13431    /// entries are the array-able SRFs, already lowered by the parser into their
13432    /// scalar array form.
13433    fn rows_from_rows(
13434        &self,
13435        primary: &TableRef,
13436    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
13437        let entries = primary
13438            .rows_from
13439            .as_ref()
13440            .expect("caller guards rows_from.is_some()");
13441        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13442        let ctx = self.ev_ctx(&empty, None);
13443        let dummy = Row::new(alloc::vec::Vec::new());
13444        let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
13445        let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13446        for (name, args) in entries {
13447            let (vals, colname) = if name == "__array" {
13448                // The parser lowered this one to `<array expr>`; its rows are the
13449                // array's elements.
13450                let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
13451                (
13452                    array_value_to_elements(&arr)?,
13453                    alloc::string::String::from("unnest"),
13454                )
13455            } else {
13456                let call = spg_sql::ast::Expr::FunctionCall {
13457                    name: name.clone(),
13458                    args: args.clone(),
13459                };
13460                (self.srf_values(&call, &dummy, &ctx)?, name.clone())
13461            };
13462            let ty = vals
13463                .first()
13464                .and_then(spg_storage::Value::data_type)
13465                .unwrap_or(DataType::Text);
13466            cols.push(ColumnSchema::new(colname, ty, true));
13467            lists.push(vals);
13468        }
13469        let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
13470        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
13471        for k in 0..n {
13472            let mut vals: alloc::vec::Vec<Value<'static>> =
13473                alloc::vec::Vec::with_capacity(lists.len() + 1);
13474            for l in &lists {
13475                vals.push(l.get(k).cloned().unwrap_or(Value::Null));
13476            }
13477            rows.push(Row::new(vals));
13478        }
13479        if primary.with_ordinality {
13480            cols.push(ColumnSchema::new(
13481                "ordinality".to_string(),
13482                DataType::BigInt,
13483                false,
13484            ));
13485            rows = rows
13486                .into_iter()
13487                .enumerate()
13488                .map(|(i, r)| {
13489                    let mut v = r.values;
13490                    v.push(Value::BigInt(i as i64 + 1));
13491                    Row::new(v)
13492                })
13493                .collect();
13494        }
13495        Ok((rows, cols))
13496    }
13497}
13498
13499/// v7.39 (round 232) — PG names the offending set operation in its
13500/// arity / type-mismatch messages ("each UNION query must have the same
13501/// number of columns"). `UNION ALL` is still spelled UNION there.
13502fn set_op_name(kind: UnionKind) -> &'static str {
13503    match kind {
13504        UnionKind::All | UnionKind::Distinct => "UNION",
13505        UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
13506        UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
13507    }
13508}
13509
13510/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
13511/// type: a bare string or NULL literal that no context has typed yet. SPG
13512/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
13513/// be the syntax. A wildcard or a non-literal expression is never unknown.
13514fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
13515    stmt.items
13516        .iter()
13517        .map(|item| match item {
13518            SelectItem::Expr { expr, .. } => matches!(
13519                expr,
13520                Expr::Literal(spg_sql::ast::Literal::String(_))
13521                    | Expr::Literal(spg_sql::ast::Literal::Null)
13522            ),
13523            _ => false,
13524        })
13525        .collect()
13526}
13527
13528/// v7.39 (round 233) — retype one branch column's cells, reporting the
13529/// conversion failure the way PG does rather than leaving the column
13530/// half-converted. Used when the other branch typed an untyped literal.
13531fn coerce_branch_column(
13532    rows: &mut [Row<'static>],
13533    col_idx: usize,
13534    target: DataType,
13535    col_name: &str,
13536) -> Result<(), EngineError> {
13537    for row in rows.iter_mut() {
13538        let Some(slot) = row.values.get_mut(col_idx) else {
13539            continue;
13540        };
13541        if matches!(slot, Value::Null) {
13542            continue;
13543        }
13544        *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
13545    }
13546    Ok(())
13547}
13548
13549/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
13550/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
13551/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
13552/// reference to q's output columns substituted by the underlying column.
13553///
13554/// Admission is deliberately narrow — anything that changes cardinality,
13555/// order, or scope stays on the materialising path:
13556/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
13557///   FROM with no ordinality or positional column aliases, and no
13558///   subquery anywhere its expressions (an inner scope could reference
13559///   q too — descending is a later knife);
13560/// * inner: one stored table, bare-column projection only, no
13561///   CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
13562/// * every outer column reference must resolve inside q's output list —
13563///   a name that does not is an ERROR today, and flattening would
13564///   silently legalise it against the base table.
13565fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
13566    use spg_sql::ast::SelectItem;
13567    let inner = primary.lateral_subquery.as_deref()?;
13568    // Outer shape.
13569    if !stmt.ctes.is_empty()
13570        || !stmt.unions.is_empty()
13571        || stmt.distinct
13572        || !stmt.distinct_on.is_empty()
13573        || !stmt.window_check_exprs.is_empty()
13574        || stmt.locking.is_some()
13575        || primary.with_ordinality
13576        || !primary.unnest_column_aliases.is_empty()
13577    {
13578        return None;
13579    }
13580    // Inner shape.
13581    if !inner.ctes.is_empty()
13582        || !inner.unions.is_empty()
13583        || inner.distinct
13584        || !inner.distinct_on.is_empty()
13585        || inner.group_by.is_some()
13586        || inner.group_by_all
13587        || inner.having.is_some()
13588        || !inner.order_by.is_empty()
13589        || inner.limit.is_some()
13590        || inner.offset.is_some()
13591        || !inner.window_check_exprs.is_empty()
13592        || inner.locking.is_some()
13593    {
13594        return None;
13595    }
13596    let ifrom = inner.from.as_ref()?;
13597    let it = &ifrom.primary;
13598    if !ifrom.joins.is_empty()
13599        || it.name.is_empty()
13600        || it.lateral_subquery.is_some()
13601        || it.unnest_expr.is_some()
13602        || it.generate_series_args.is_some()
13603        || it.as_of_segment.is_some()
13604        || it.jsonb_each_text_arg.is_some()
13605        || it.table_fn_call.is_some()
13606        || it.rows_from.is_some()
13607        || it.json_table.is_some()
13608        || it.with_ordinality
13609        || !it.unnest_column_aliases.is_empty()
13610    {
13611        return None;
13612    }
13613    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
13614        return None;
13615    }
13616    // The output map: q's visible name -> the underlying column.
13617    let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
13618    let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
13619        alloc::collections::BTreeMap::new();
13620    for item in &inner.items {
13621        let SelectItem::Expr { expr, alias } = item else {
13622            return None;
13623        };
13624        let Expr::Column(c) = expr else {
13625            return None;
13626        };
13627        if let Some(q) = c.qualifier.as_deref()
13628            && !q.eq_ignore_ascii_case(&inner_alias)
13629        {
13630            return None;
13631        }
13632        let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
13633        // A duplicated output name would make substitution ambiguous.
13634        if map
13635            .insert(out_name.to_ascii_lowercase(), c.clone())
13636            .is_some()
13637        {
13638            return None;
13639        }
13640    }
13641    if map.is_empty() {
13642        return None;
13643    }
13644    let derived_alias = primary
13645        .alias
13646        .clone()
13647        .unwrap_or_else(|| primary.name.clone())
13648        .to_ascii_lowercase();
13649    // Substitute in a clone; bail (None) on the first reference the map
13650    // cannot answer.
13651    let mut out = stmt.clone();
13652    let ok = core::cell::Cell::new(true);
13653    let mut subst = |e: &mut Expr| -> bool {
13654        match e {
13655            Expr::Column(c) => {
13656                match c.qualifier.as_deref() {
13657                    Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
13658                    None => {}
13659                    Some(_) => {
13660                        ok.set(false);
13661                        return true;
13662                    }
13663                }
13664                match map.get(&c.name.to_ascii_lowercase()) {
13665                    Some(target) => *c = target.clone(),
13666                    None => ok.set(false),
13667                }
13668                true
13669            }
13670            // Any subquery could reference q from its own scope;
13671            // descending is a later knife — bail for now.
13672            Expr::ScalarSubquery(_)
13673            | Expr::Exists { .. }
13674            | Expr::InSubquery { .. }
13675            | Expr::RowInSubquery { .. }
13676            | Expr::RowCmpSubquery { .. } => {
13677                ok.set(false);
13678                true
13679            }
13680            _ => false,
13681        }
13682    };
13683    for item in &mut out.items {
13684        match item {
13685            SelectItem::Expr { expr, .. } => {
13686                crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
13687            }
13688            // `SELECT * FROM (…) q` means q's columns, in q's order.
13689            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
13690        }
13691    }
13692    if let Some(w) = &mut out.where_ {
13693        crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
13694    }
13695    if let Some(gs) = &mut out.group_by {
13696        for g in gs {
13697            crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
13698        }
13699    }
13700    if let Some(h) = &mut out.having {
13701        crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
13702    }
13703    for o in &mut out.order_by {
13704        crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
13705    }
13706    for d in &mut out.distinct_on {
13707        crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
13708    }
13709    if !ok.get() {
13710        return None;
13711    }
13712    // FROM becomes the stored table; the filters conjoin.
13713    out.from = Some(spg_sql::ast::FromClause {
13714        primary: it.clone(),
13715        joins: Vec::new(),
13716    });
13717    out.where_ = match (inner.where_.clone(), out.where_.take()) {
13718        (Some(a), Some(b)) => Some(Expr::Binary {
13719            lhs: alloc::boxed::Box::new(a),
13720            op: spg_sql::ast::BinOp::And,
13721            rhs: alloc::boxed::Box::new(b),
13722        }),
13723        (Some(a), None) => Some(a),
13724        (None, b) => b,
13725    };
13726    Some(out)
13727}
13728
13729/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
13730/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
13731/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
13732/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
13733/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
13734/// DISTINCT, an SRF, or an unprovable inner shape stays put.
13735fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
13736    use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
13737    let inner = primary.lateral_subquery.as_deref()?;
13738    // Outer: exactly `SELECT count(*)`, nothing else.
13739    if !stmt.ctes.is_empty()
13740        || !stmt.unions.is_empty()
13741        || stmt.distinct
13742        || !stmt.distinct_on.is_empty()
13743        || stmt.where_.is_some()
13744        || stmt.group_by.is_some()
13745        || stmt.having.is_some()
13746        || !stmt.order_by.is_empty()
13747        || stmt.limit.is_some()
13748        || stmt.offset.is_some()
13749        || stmt.items.len() != 1
13750    {
13751        return None;
13752    }
13753    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
13754        return None;
13755    };
13756    let E::FunctionCall { name, args } = expr else {
13757        return None;
13758    };
13759    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
13760        return None;
13761    }
13762    // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
13763    let Some(LimitExpr::Literal(k)) = &inner.offset else {
13764        return None;
13765    };
13766    let k = i64::from(*k);
13767    if inner.limit.is_some() || inner.order_by.is_empty() {
13768        return None;
13769    }
13770    let mut counted = inner.clone();
13771    counted.order_by = Vec::new();
13772    counted.offset = None;
13773    // The stripped inner must now be a provable simple shape (its
13774    // items become irrelevant — count(*) reads none of them — but an
13775    // SRF item would change the row count, so the flatten predicate's
13776    // scrutiny still applies).
13777    let base = matview_flatten_probe(&counted)?;
13778    let mut out = stmt.clone();
13779    out.items = alloc::vec![SelectItem::Expr {
13780        expr: E::FunctionCall {
13781            name: String::from("greatest"),
13782            args: alloc::vec![
13783                E::Binary {
13784                    lhs: alloc::boxed::Box::new(E::FunctionCall {
13785                        name: String::from("count_star"),
13786                        args: alloc::vec![],
13787                    }),
13788                    op: spg_sql::ast::BinOp::Sub,
13789                    rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
13790                },
13791                E::Literal(spg_sql::ast::Literal::Integer(0)),
13792            ],
13793        },
13794        alias: Some(String::from("count")),
13795    }];
13796    out.from = Some(spg_sql::ast::FromClause {
13797        primary: base,
13798        joins: Vec::new(),
13799    });
13800    out.where_ = counted.where_.clone();
13801    Some(out)
13802}
13803
13804/// The inner-shape probe `try_count_over_offset` shares with the
13805/// flatten: single stored table, no modifiers, no subqueries, no SRF
13806/// items. Returns the base TableRef.
13807fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
13808    use spg_sql::ast::SelectItem;
13809    if !inner.ctes.is_empty()
13810        || !inner.unions.is_empty()
13811        || inner.distinct
13812        || !inner.distinct_on.is_empty()
13813        || inner.group_by.is_some()
13814        || inner.group_by_all
13815        || inner.having.is_some()
13816        || !inner.order_by.is_empty()
13817        || inner.limit.is_some()
13818        || inner.offset.is_some()
13819        || !inner.window_check_exprs.is_empty()
13820        || inner.locking.is_some()
13821    {
13822        return None;
13823    }
13824    let ifrom = inner.from.as_ref()?;
13825    let it = &ifrom.primary;
13826    if !ifrom.joins.is_empty()
13827        || it.name.is_empty()
13828        || it.lateral_subquery.is_some()
13829        || it.unnest_expr.is_some()
13830        || it.generate_series_args.is_some()
13831        || it.as_of_segment.is_some()
13832        || it.jsonb_each_text_arg.is_some()
13833        || it.table_fn_call.is_some()
13834        || it.rows_from.is_some()
13835        || it.json_table.is_some()
13836        || it.with_ordinality
13837    {
13838        return None;
13839    }
13840    for item in &inner.items {
13841        match item {
13842            SelectItem::Expr { expr, .. } => {
13843                if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
13844                    return None;
13845                }
13846            }
13847            SelectItem::Wildcard => {}
13848            SelectItem::QualifiedWildcard(_) => return None,
13849        }
13850    }
13851    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
13852        return None;
13853    }
13854    Some(it.clone())
13855}
13856
13857/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
13858/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
13859/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
13860/// constant-LENGTH array literal unnests to exactly k rows per input
13861/// row (NULL elements are rows too). One SRF item only, elements
13862/// subquery-free, and the stripped inner must pass the same probe the
13863/// count-over-offset rewrite uses.
13864fn try_count_over_const_unnest(
13865    stmt: &SelectStatement,
13866    primary: &TableRef,
13867) -> Option<SelectStatement> {
13868    use spg_sql::ast::{Expr as E, SelectItem};
13869    let inner = primary.lateral_subquery.as_deref()?;
13870    if !stmt.ctes.is_empty()
13871        || !stmt.unions.is_empty()
13872        || stmt.distinct
13873        || !stmt.distinct_on.is_empty()
13874        || stmt.where_.is_some()
13875        || stmt.group_by.is_some()
13876        || stmt.having.is_some()
13877        || !stmt.order_by.is_empty()
13878        || stmt.limit.is_some()
13879        || stmt.offset.is_some()
13880        || stmt.items.len() != 1
13881    {
13882        return None;
13883    }
13884    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
13885        return None;
13886    };
13887    let E::FunctionCall { name, args } = expr else {
13888        return None;
13889    };
13890    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
13891        return None;
13892    }
13893    // Inner: exactly one item, and it is unnest(ARRAY[...]).
13894    if inner.items.len() != 1
13895        || !inner.order_by.is_empty()
13896        || inner.limit.is_some()
13897        || inner.offset.is_some()
13898    {
13899        return None;
13900    }
13901    let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
13902        return None;
13903    };
13904    let E::FunctionCall {
13905        name: fname,
13906        args: fargs,
13907    } = item
13908    else {
13909        return None;
13910    };
13911    if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
13912        return None;
13913    }
13914    let E::Array(elems) = &fargs[0] else {
13915        return None;
13916    };
13917    if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
13918        return None;
13919    }
13920    let k = elems.len() as i64;
13921    // The stripped inner (the SRF item replaced by a plain constant)
13922    // must be the provable simple shape.
13923    let mut counted = inner.clone();
13924    counted.items = alloc::vec![SelectItem::Expr {
13925        expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
13926        alias: None,
13927    }];
13928    let base = matview_flatten_probe(&counted)?;
13929    let mut out = stmt.clone();
13930    out.items = alloc::vec![SelectItem::Expr {
13931        expr: E::Binary {
13932            lhs: alloc::boxed::Box::new(E::FunctionCall {
13933                name: String::from("count_star"),
13934                args: alloc::vec![],
13935            }),
13936            op: spg_sql::ast::BinOp::Mul,
13937            rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
13938        },
13939        alias: Some(String::from("count")),
13940    }];
13941    out.from = Some(spg_sql::ast::FromClause {
13942        primary: base,
13943        joins: Vec::new(),
13944    });
13945    out.where_ = counted.where_.clone();
13946    Some(out)
13947}