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        // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2657        // are not materialised here, and a partition parent's own
2658        // heap/indexes are empty (its rows live in the children).
2659        if !stmt.ctes.is_empty() {
2660            return Ok(None);
2661        }
2662        if let Some(from) = &stmt.from
2663            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2664        {
2665            return Ok(None);
2666        }
2667        let where_ = stmt.where_.as_ref().expect("shape checked it");
2668        let cols = &table.schema().columns;
2669        let Some(values) = crate::index_access::try_index_only_range(
2670            where_,
2671            cols,
2672            table,
2673            alias_name,
2674            &self.current_snapshot(),
2675            pos,
2676        ) else {
2677            return Ok(None);
2678        };
2679        let schema = alloc::vec![ColumnSchema::new(
2680            out_name,
2681            cols[pos].ty,
2682            cols[pos].nullable
2683        )];
2684        Ok(Some(QueryResult::Rows {
2685            columns: schema,
2686            rows: values
2687                .into_iter()
2688                .map(|v| Row::new(alloc::vec![v]))
2689                .collect(),
2690        }))
2691    }
2692
2693    /// v7.39 (round 564) — the same scan, emitting each value instead of
2694    /// building a `Vec<Row>` for the encoder to walk once and drop.
2695    ///
2696    /// A profile of the server serving a 50k-row range put 10.2% of the
2697    /// connection thread's CPU on BUILDING that vector and another 9.7%
2698    /// on dropping it — a fifth of the query, spent allocating and
2699    /// freeing one single-element `Vec` per output row so that the wire
2700    /// encoder could borrow each value for a few nanoseconds. The
2701    /// streaming interface it then hands them to takes `&[Value]`
2702    /// already.
2703    ///
2704    /// Returns `None` when the shape does not apply, so the caller falls
2705    /// back before anything has been emitted.
2706    pub(crate) fn try_index_only_stream<F>(
2707        &self,
2708        stmt: &SelectStatement,
2709        emit: &mut F,
2710    ) -> Result<Option<usize>, EngineError>
2711    where
2712        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
2713    {
2714        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2715            return Ok(None);
2716        };
2717        // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2718        // are not materialised here, and a partition parent's own
2719        // heap/indexes are empty (its rows live in the children).
2720        if !stmt.ctes.is_empty() {
2721            return Ok(None);
2722        }
2723        if let Some(from) = &stmt.from
2724            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2725        {
2726            return Ok(None);
2727        }
2728        let where_ = stmt.where_.as_ref().expect("shape checked it");
2729        let cols = &table.schema().columns;
2730        let schema = alloc::vec![ColumnSchema::new(
2731            out_name,
2732            cols[pos].ty,
2733            cols[pos].nullable
2734        )];
2735        let snapshot = self.current_snapshot();
2736        // The header goes out only once the walk has agreed to run — a
2737        // shape rejection after it would leave the client with a
2738        // RowDescription for a result that never comes.
2739        let mut wrote_header = false;
2740        let counted = crate::index_access::index_only_range_each(
2741            where_,
2742            cols,
2743            table,
2744            alias_name,
2745            &snapshot,
2746            pos,
2747            &mut |v: spg_storage::Value<'_>| {
2748                if !wrote_header {
2749                    emit(crate::StreamItem::Header(&schema))?;
2750                    wrote_header = true;
2751                }
2752                emit(crate::StreamItem::Row(crate::RowCells::Refs(&[&v])))
2753            },
2754        );
2755        match counted {
2756            None => Ok(None),
2757            Some(Err(e)) => Err(e),
2758            Some(Ok(n)) => {
2759                if !wrote_header {
2760                    emit(crate::StreamItem::Header(&schema))?;
2761                }
2762                Ok(Some(n))
2763            }
2764        }
2765    }
2766
2767    /// `DISTINCT ON`'s de-duplication, which runs after the inner
2768    /// SELECT has produced its rows.
2769    ///
2770    /// `#[inline(never)]` and out of `exec_select_cancel_as` for the
2771    /// reason round 848 established: a debug build gives every branch's
2772    /// locals a slot in the frame whichever branch runs, and this one is
2773    /// eighty lines of hashing, key slicing and survivor sorting that a
2774    /// statement without `DISTINCT ON` never touches. Round 867
2775    /// measured `exec_select_cancel_as` holding ~46 KB on a path that
2776    /// reaches none of it — the segment that had been blamed on
2777    /// `exec_bare_select_cancel`, which turned out to hold 2 KB.
2778    #[inline(never)]
2779    fn apply_distinct_on(
2780        &self,
2781        result: QueryResult,
2782        don_hidden: usize,
2783        don_limit: &(
2784            Option<spg_sql::ast::LimitExpr>,
2785            Option<spg_sql::ast::LimitExpr>,
2786        ),
2787        don_top1: usize,
2788        orig_order_by: &[spg_sql::ast::OrderBy],
2789    ) -> Result<QueryResult, EngineError> {
2790        let QueryResult::Rows { columns, rows } = result else {
2791            return Ok(result);
2792        };
2793        // The keys are the hidden trailing columns appended above.
2794        // v7.39 (round 729) — top-1 mode: the trailing columns are the
2795        // DON keys plus the ORDER tail; keep each group's best in one
2796        // hash pass, then sort the SURVIVORS with the original spec.
2797        let mut kept: alloc::vec::Vec<Row<'static>>;
2798        let key_start;
2799        if don_top1 > 0 {
2800            let tail = don_top1 - 1;
2801            key_start = columns.len().saturating_sub(don_hidden + tail);
2802            let ord_start = key_start + don_hidden;
2803            let tail_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by[don_hidden..]
2804                .iter()
2805                .map(|o| (o.desc, o.nulls_first))
2806                .collect();
2807            let mysql = self.backslash_escapes;
2808            let better = |a: &Row<'static>, b: &Row<'static>| -> bool {
2809                for (k, (desc, nf)) in tail_dirs.iter().enumerate() {
2810                    let av = a.values.get(ord_start + k).unwrap_or(&Value::Null);
2811                    let bv = b.values.get(ord_start + k).unwrap_or(&Value::Null);
2812                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2813                        core::cmp::Ordering::Less => return true,
2814                        core::cmp::Ordering::Greater => return false,
2815                        core::cmp::Ordering::Equal => {}
2816                    }
2817                }
2818                false
2819            };
2820            let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2821            let mut best: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
2822            let mut keybuf = String::new();
2823            for row in rows {
2824                keybuf.clear();
2825                for v in row.values.get(key_start..ord_start).unwrap_or(&[]) {
2826                    aggregate::push_canonical_key(&mut keybuf, v);
2827                }
2828                match slot.get(keybuf.as_str()) {
2829                    Some(&i) => {
2830                        if better(&row, &best[i]) {
2831                            best[i] = row;
2832                        }
2833                    }
2834                    None => {
2835                        slot.insert(keybuf.clone(), best.len());
2836                        best.push(row);
2837                    }
2838                }
2839            }
2840            // Survivors sort with the FULL original spec (keys are still
2841            // aboard as hidden columns).
2842            let full_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by
2843                .iter()
2844                .map(|o| (o.desc, o.nulls_first))
2845                .collect();
2846            best.sort_by(|a, b| {
2847                for (k, (desc, nf)) in full_dirs.iter().enumerate() {
2848                    let av = a.values.get(key_start + k).unwrap_or(&Value::Null);
2849                    let bv = b.values.get(key_start + k).unwrap_or(&Value::Null);
2850                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2851                        core::cmp::Ordering::Equal => {}
2852                        o => return o,
2853                    }
2854                }
2855                core::cmp::Ordering::Equal
2856            });
2857            for r in &mut best {
2858                r.values.truncate(key_start);
2859            }
2860            kept = best;
2861        } else {
2862            key_start = columns.len().saturating_sub(don_hidden);
2863            let mut seen: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
2864            kept = alloc::vec::Vec::new();
2865            for mut row in rows {
2866                let key: alloc::vec::Vec<Value<'static>> =
2867                    row.values.get(key_start..).unwrap_or(&[]).to_vec();
2868                if seen.iter().any(|k| k == &key) {
2869                    continue;
2870                }
2871                seen.push(key);
2872                row.values.truncate(key_start);
2873                kept.push(row);
2874            }
2875        }
2876        let mut columns = columns;
2877        columns.truncate(key_start);
2878        // PG limits what DISTINCT ON left, not what fed it.
2879        let kept = apply_deferred_limit(kept, don_limit);
2880        Ok(QueryResult::Rows {
2881            columns,
2882            rows: kept,
2883        })
2884    }
2885
2886    pub(crate) fn exec_select_cancel_as(
2887        &self,
2888        stmt: &SelectStatement,
2889        cancel: CancelToken<'_>,
2890        as_role: Option<&str>,
2891    ) -> Result<QueryResult, EngineError> {
2892        // v7.39 (round 763, F31-C1) — `SELECT *, count(*) … GROUP BY
2893        // <all columns>` is legal PG (the wildcard expands to grouped
2894        // columns); SPG refused the whole shape. Expand the wildcard
2895        // into explicit column refs up front — the aggregate layer's
2896        // existing "must appear in the GROUP BY clause" validation
2897        // then answers PG's sentence for any non-grouped column.
2898        if let Some(expanded) = self.expand_aggregate_wildcard(stmt) {
2899            return self.exec_select_cancel_as(&expanded, cancel, as_role);
2900        }
2901        // v7.39 (round 559) — `SELECT count(*) FROM t` without touching
2902        // a row.
2903        //
2904        // The aggregate layer already short-circuits this to
2905        // `rows.len()`, so the O(1) part was never the problem — the
2906        // cost is UPSTREAM, materialising every visible row so that
2907        // layer can take its length. Measured over pgwire on 500k rows:
2908        // PG18 8.2 ms with two parallel workers, 10.3 ms with
2909        // parallelism off, SPG 16.5 ms — 1.6x slower than a
2910        // single-threaded PG on the commonest aggregate there is, and no
2911        // ledger entry recorded it.
2912        //
2913        // Counting visible HEADERS needs no row at all. PG cannot do
2914        // this: its visibility lives in the heap tuples themselves, so
2915        // it has to read them (that is why its own count(*) is a full
2916        // scan, parallel or not).
2917        // v7.39 (read01 round 57) — the table-privilege gate on the common
2918        // read core. A superuser session returns from it immediately.
2919        // v7.39 (round 529) — resolve an ORDER BY that names an output
2920        // ALIAS. The statement-level pass never reached a SELECT nested in
2921        // a FROM clause, a CTE or a scalar subquery, so the same query
2922        // worked on its own and failed the moment anything wrapped it —
2923        // which is what generated SQL does constantly.
2924        let aliased;
2925        let stmt = if crate::orderby::order_by_names_an_alias(stmt) {
2926            let mut s = stmt.clone();
2927            crate::orderby::resolve_order_by_position(&mut s);
2928            aliased = s;
2929            &aliased
2930        } else {
2931            stmt
2932        };
2933        // v7.39 (round 529) — DISTINCT ON needs two things it did not have.
2934        //
2935        // Its keys were evaluated against the PROJECTED row, so a key that
2936        // is not in the select list — `SELECT DISTINCT ON (g) v FROM t
2937        // ORDER BY g, v DESC`, the canonical "latest row per group" — could
2938        // not be read at all and the query failed. PG evaluates them on the
2939        // input. They are projected as hidden columns here and stripped
2940        // again below, the same way the grouping-set ordering columns
2941        // already travel.
2942        //
2943        // And the dedup ran AFTER the inner statement's LIMIT, so
2944        // `… DISTINCT ON (g) … LIMIT 2` on four rows answered ONE row where
2945        // PG answers two: the limit had already taken two rows of the same
2946        // group before anything deduplicated them. A paginated DISTINCT ON
2947        // returned short pages, with no error. The limit is deferred to
2948        // after the dedup, which is PG's order.
2949        let don_stmt;
2950        // v7.39 (round 729) — the top-1 consumer needs the ORIGINAL
2951        // order spec (the rewritten stmt's is emptied).
2952        let orig_order_by = stmt.order_by.clone();
2953        let (stmt, don_hidden, don_limit, don_top1) = if stmt.distinct_on.is_empty() {
2954            (stmt, 0, (None, None), 0usize)
2955        } else {
2956            let mut s = stmt.clone();
2957            let hidden = s.distinct_on.len();
2958            for (i, e) in stmt.distinct_on.iter().enumerate() {
2959                s.items.push(SelectItem::Expr {
2960                    expr: e.clone(),
2961                    alias: Some(alloc::format!("__distinct_on_{i}")),
2962                });
2963            }
2964            // v7.39 (round 729) — group-top-1 short circuit. When the
2965            // DISTINCT ON keys are exactly the ORDER BY's leading keys,
2966            // the answer is "per group, the row that wins the remaining
2967            // order" — a single O(n) hash pass. The old path sorted the
2968            // ENTIRE input first (500k rows, ~180 ms on the panel cell)
2969            // to keep 100. The inner query runs UNSORTED with every
2970            // order key appended as a hidden column; the dedup below
2971            // keeps each group's best, then sorts the SURVIVORS.
2972            // Declared-collation order keys stay on the sorting path
2973            // (the value comparator here is collation-blind).
2974            let prefix_matches = s.order_by.len() >= hidden
2975                && stmt
2976                    .distinct_on
2977                    .iter()
2978                    .zip(s.order_by.iter())
2979                    .all(|(d, o)| *d == o.expr && !o.desc && o.nulls_first.is_none());
2980            let colls_plain =
2981                crate::orderby::order_by_collations(&s.order_by, &self.ev_ctx(&[], None))
2982                    .map(|cs| cs.iter().all(Option::is_none))
2983                    .unwrap_or(false);
2984            let top1_tail = if prefix_matches && colls_plain && s.group_by.is_none() {
2985                let tail = s.order_by.len() - hidden;
2986                for (j, o) in s.order_by[hidden..].iter().enumerate() {
2987                    s.items.push(SelectItem::Expr {
2988                        expr: o.expr.clone(),
2989                        alias: Some(alloc::format!("__don_ord_{j}")),
2990                    });
2991                }
2992                // Carry the tail's direction flags through the aliases'
2993                // ORDER; the survivors re-sort below with the full spec.
2994                s.order_by = Vec::new();
2995                tail + 1 // sentinel: 1 + number of tail keys (0 tail is still active)
2996            } else {
2997                0
2998            };
2999            // Only a folded literal is deferred; a placeholder or an
3000            // expression keeps the path it has today rather than being
3001            // resolved a second way here.
3002            let deferrable = matches!(
3003                (&s.limit, &s.offset),
3004                (
3005                    None | Some(spg_sql::ast::LimitExpr::Literal(_)),
3006                    None | Some(spg_sql::ast::LimitExpr::Literal(_))
3007                )
3008            );
3009            let deferred = if deferrable {
3010                (s.limit.take(), s.offset.take())
3011            } else {
3012                (None, None)
3013            };
3014            don_stmt = s;
3015            (&don_stmt, hidden, deferred, top1_tail)
3016        };
3017        self.acl_check_select_as(stmt, as_role)?;
3018        validate_aggregate_placement(stmt)?;
3019        // v7.39 (round 559) — the bare `count(*)` fast path, AFTER the
3020        // privilege gate above. Placed before it at first, and the
3021        // security-definer e2e caught it immediately: a SECURITY INVOKER
3022        // function whose body is `SELECT count(*) FROM t` answered
3023        // instead of being refused, because the fast path never reached
3024        // the check.
3025        if let Some(r) = self.try_bare_count_star(stmt, as_role)? {
3026            return Ok(r);
3027        }
3028        // v7.39 (round 560) — an index-only range scan. Same placement
3029        // reasoning as the count above: after the privilege gate.
3030        if let Some(r) = self.try_index_only_scan(stmt)? {
3031            return Ok(r);
3032        }
3033        validate_locking_clause(stmt)?;
3034        let result = self.exec_select_cancel_inner(stmt, cancel)?;
3035        // v7.39 (round 135) — drop the synthetic `__grp_ord_*` ordering columns
3036        // the parser injects for GROUPING() in ORDER BY on a grouping-set query.
3037        // They carry the per-branch mask through the UNION-ALL sort and must not
3038        // appear in the output. Stripped per SELECT level (grouping-set queries
3039        // are often wrapped in a derived subquery), before DISTINCT ON.
3040        let result = strip_synthetic_order_cols(result);
3041        // v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3042        // rows arrive here already ORDER BY'd; keep the FIRST row of
3043        // each group the expressions define (PG semantics). The
3044        // expressions evaluate against the projected schema — an
3045        // expression that isn't in the select list errors honestly.
3046        if stmt.distinct_on.is_empty() {
3047            return Ok(result);
3048        }
3049        self.apply_distinct_on(result, don_hidden, &don_limit, don_top1, &orig_order_by)
3050    }
3051
3052    /// The UNION chain: execute the head as a bare block, then fold each
3053    /// peer in with left-associative dedup.
3054    ///
3055    /// `#[inline(never)]` and out of `exec_select_cancel_inner` for the
3056    /// reason round 848 established. A statement with no unions returns
3057    /// one line above the call — and every nested subquery on a deep
3058    /// path is such a statement, so each level of the recursion carried
3059    /// 170 lines of locals it could not reach. Round 867 measured that
3060    /// frame at 34,800 bytes, the largest single one on the descent,
3061    /// after two earlier attributions had blamed its caller and then its
3062    /// callee: the gap between two marks is the frame of everything
3063    /// BETWEEN them, and this function had no mark of its own.
3064    #[inline(never)]
3065    fn exec_union_chain(
3066        &self,
3067        stmt_ref: &SelectStatement,
3068        stmt: &SelectStatement,
3069        cancel: CancelToken<'_>,
3070    ) -> Result<QueryResult, EngineError> {
3071        // UNION path: clone-strip the head into a bare block (its own
3072        // DISTINCT and any inner ORDER BY are dropped by parser rule —
3073        // the wrapper SelectStatement carries them), execute, then chain
3074        // peers with left-associative dedup semantics.
3075        // v7.39 (round 232) — the wrapper's ORDER BY addresses the head's
3076        // output columns; a position past their count is PG's 42P10.
3077        crate::orderby::check_order_by_positions(stmt_ref)?;
3078        let mut head_unknown = branch_unknown_mask(stmt_ref);
3079        let mut head = stmt_ref.clone();
3080        head.unions = Vec::new();
3081        head.order_by = Vec::new();
3082        head.limit = None;
3083        let QueryResult::Rows {
3084            mut columns,
3085            mut rows,
3086        } = self.exec_bare_select_cancel(&head, cancel)?
3087        else {
3088            unreachable!("bare SELECT cannot return CommandOk")
3089        };
3090        for (kind, peer) in &stmt_ref.unions {
3091            // v7.37.17 (17.6 siblings) — a peer carrying its own
3092            // unions is a nested INTERSECT group (the parser's
3093            // precedence regrouping); recurse through the
3094            // union-aware wrapper for it.
3095            let peer_result = if peer.unions.is_empty() {
3096                self.exec_bare_select_cancel(peer, cancel)?
3097            } else {
3098                self.exec_select_cancel(peer, cancel)?
3099            };
3100            let QueryResult::Rows {
3101                columns: peer_cols,
3102                rows: mut peer_rows,
3103            } = peer_result
3104            else {
3105                unreachable!("bare SELECT cannot return CommandOk")
3106            };
3107            if peer_cols.len() != columns.len() {
3108                // v7.39 (round 232) — PG's wording, which clients match on.
3109                return Err(EngineError::Unsupported(alloc::format!(
3110                    "each {} query must have the same number of columns",
3111                    set_op_name(*kind)
3112                )));
3113            }
3114            // v7.39 (round 232+233) — PG resolves each result column to one
3115            // type before it merges anything, and refuses the query when the
3116            // two branches have no common type. SPG's unifier
3117            // (`unify_union_columns`) is value-driven and deliberately
3118            // conservative — "a column where any cell fails to coerce is left
3119            // exactly as it was" — so a mismatch produced a column holding
3120            // BOTH types (`SELECT a, b FROM t UNION SELECT b, a FROM t` came
3121            // back with integers and text interleaved) instead of an error.
3122            //
3123            // The check has to read the branch ASTs, not just their schemas:
3124            // SPG has no `Unknown` DataType, so a bare `'a'` literal describes
3125            // as TEXT and is indistinguishable from a real text column by
3126            // schema alone — yet PG treats the two completely differently
3127            // (`SELECT 1 UNION SELECT 'a'` is an input-syntax error on the
3128            // literal, `SELECT 1 UNION SELECT 'a'::text` is a type mismatch).
3129            let peer_unknown = branch_unknown_mask(peer);
3130            for i in 0..columns.len() {
3131                let hu = head_unknown.get(i).copied().unwrap_or(false);
3132                let pu = peer_unknown.get(i).copied().unwrap_or(false);
3133                let (ht, pt) = (columns[i].ty, peer_cols[i].ty);
3134                match (hu, pu) {
3135                    // Both sides carry a real type: they must share a category.
3136                    (false, false) => {
3137                        if !crate::conversions::types_unify(ht, pt) {
3138                            return Err(EngineError::Unsupported(alloc::format!(
3139                                "{} types {} and {} cannot be matched",
3140                                set_op_name(*kind),
3141                                crate::conversions::pg_type_name_for_error(ht),
3142                                crate::conversions::pg_type_name_for_error(pt),
3143                            )));
3144                        }
3145                    }
3146                    // One side is an untyped literal: it takes the other's
3147                    // type, and failing to convert is the error PG reports.
3148                    (true, false) => {
3149                        coerce_branch_column(&mut rows, i, pt, &columns[i].name)?;
3150                        columns[i].ty = pt;
3151                        head_unknown[i] = false;
3152                    }
3153                    (false, true) => {
3154                        coerce_branch_column(&mut peer_rows, i, ht, &columns[i].name)?;
3155                    }
3156                    // Both untyped — nothing to resolve against yet.
3157                    (true, true) => {}
3158                }
3159            }
3160            // v7.37 D.26 — a UNION result column is nullable when ANY branch is
3161            // nullable (PG semantics). Previously the result kept only the head's
3162            // nullability, so `VALUES (1),(NULL)` (a UNION-ALL chain seeded by the
3163            // non-null `1`) wrongly reported the column NOT NULL, which let
3164            // `count(col)`'s NOT-NULL fast-path count the NULL row.
3165            for (i, pc) in peer_cols.iter().enumerate() {
3166                if pc.nullable {
3167                    columns[i].nullable = true;
3168                }
3169            }
3170            // v7.39 (round 410) — under MySQL, set-op dedup / matching folds
3171            // text by the session collation (CI + accent + PAD SPACE), like
3172            // GROUP BY. PG stays byte-exact.
3173            let mysql = self.backslash_escapes;
3174            match kind {
3175                UnionKind::All => rows.extend(peer_rows),
3176                UnionKind::Distinct => {
3177                    rows.extend(peer_rows);
3178                    rows = dedup_rows(rows, mysql);
3179                }
3180                // v7.37.17 (17.6 siblings) — PG set semantics.
3181                // v7.39 (round 591) — all four ask the same question of the
3182                // right side, and all four used to answer it by scanning it
3183                // once per left row. `PeerIndex` buckets it by the hash
3184                // DISTINCT already uses, so the answer is a lookup.
3185                // INTERSECT: distinct rows present on both sides.
3186                UnionKind::Intersect => {
3187                    let idx = PeerIndex::build(&peer_rows, mysql);
3188                    rows = dedup_rows(rows, mysql)
3189                        .into_iter()
3190                        .filter(|r| idx.contains(r))
3191                        .collect();
3192                }
3193                // INTERSECT ALL: multiset intersection — each row
3194                // keeps min(left count, right count) occurrences.
3195                UnionKind::IntersectAll => {
3196                    let mut idx = PeerIndex::build(&peer_rows, mysql);
3197                    let mut kept: Vec<Row<'static>> = Vec::new();
3198                    for r in rows {
3199                        if idx.take_one(&r) {
3200                            kept.push(r);
3201                        }
3202                    }
3203                    rows = kept;
3204                }
3205                // EXCEPT: distinct left rows absent from the right.
3206                UnionKind::Except => {
3207                    let idx = PeerIndex::build(&peer_rows, mysql);
3208                    rows = dedup_rows(rows, mysql)
3209                        .into_iter()
3210                        .filter(|r| !idx.contains(r))
3211                        .collect();
3212                }
3213                // EXCEPT ALL: multiset subtraction — each right
3214                // occurrence cancels one left occurrence.
3215                UnionKind::ExceptAll => {
3216                    let mut idx = PeerIndex::build(&peer_rows, mysql);
3217                    let mut kept: Vec<Row<'static>> = Vec::new();
3218                    for r in rows {
3219                        if !idx.take_one(&r) {
3220                            kept.push(r);
3221                        }
3222                    }
3223                    rows = kept;
3224                }
3225            }
3226        }
3227        // PG resolves a UNION / VALUES result column to one common type
3228        // and casts every branch to it (`SELECT '2020-01-01'::date UNION
3229        // ALL SELECT '2020-01-02'` → both DATE, not DATE + TEXT). SPG
3230        // built each branch independently, leaving mixed-type columns
3231        // that broke ORDER BY, comparisons, and value-based window
3232        // frames. Unify + coerce before the combined ORDER BY sees them.
3233        unify_union_columns(&mut columns, &mut rows);
3234        // ORDER BY at the top of a UNION applies to the combined result.
3235        // Eval against the projected schema (NOT the source table).
3236        if !stmt.order_by.is_empty() {
3237            // v7.39 (read01 round 54) — the combined-result ctx must carry the
3238            // catalog, and the projected columns must keep their enum identity
3239            // (`user_enum_type`), or `ORDER BY <enum col>` over a UNION sorts
3240            // by TEXT instead of member order — silently wrong rows, not an
3241            // error. (Same shape as the enum-order knife's GROUP BY fix.)
3242            let synth_ctx = EvalContext::new(&columns, None).with_catalog(self.active_catalog());
3243            // v7.37.17 (17.6 siblings) — positional keys (ORDER BY 1)
3244            // survive to here when the head projects a Wildcard (the
3245            // group-tail wrapper shape): map them onto the Nth
3246            // projected column so the combined sort works.
3247            let resolved_order: Vec<spg_sql::ast::OrderBy> = stmt
3248                .order_by
3249                .iter()
3250                .map(|o| {
3251                    let mut o = o.clone();
3252                    if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
3253                        && *n >= 1
3254                        && let Ok(idx) = usize::try_from(*n - 1)
3255                        && idx < columns.len()
3256                    {
3257                        o.expr = Expr::Column(spg_sql::ast::ColumnName {
3258                            qualifier: None,
3259                            name: columns[idx].name.clone(),
3260                        });
3261                    }
3262                    o
3263                })
3264                .collect();
3265            let descs: Vec<bool> = resolved_order.iter().map(|o| o.desc).collect();
3266            let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
3267            for r in rows {
3268                let keys = build_order_keys(&resolved_order, &r, &synth_ctx)?;
3269                tagged.push((keys, r));
3270            }
3271            sort_by_keys(&mut tagged, &descs);
3272            rows = tagged.into_iter().map(|(_, r)| r).collect();
3273        }
3274        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
3275        Ok(QueryResult::Rows { columns, rows })
3276    }
3277
3278    fn exec_select_cancel_inner(
3279        &self,
3280        stmt: &SelectStatement,
3281        cancel: CancelToken<'_>,
3282    ) -> Result<QueryResult, EngineError> {
3283        cancel.check()?;
3284        // v7.38 P0 元机制 A — first observable point inside the
3285        // planner / executor. Tests use this to inject a delay or
3286        // a cancellation race before any row is produced. Release
3287        // build expands to `let _ = (...);` — zero cost.
3288        crate::injection_point!("planner_first_row_fetch", &stmt.from);
3289        // v7.39 (round 705) — WINDOW-clause definitions nothing referenced.
3290        // PG analyses every definition, referenced or not, so `SELECT i FROM
3291        // t WINDOW w AS (ORDER BY nosuch)` fails there and silently
3292        // succeeded here (the parser used to drop the unreferenced defs
3293        // whole). The check is the CREATE VIEW check's shape (round 700): a
3294        // LIMIT-0 run of the same FROM with the definitions' key
3295        // expressions as the projection — it cannot disagree with what a
3296        // referencing window would have done, because it resolves the same
3297        // names the same way. Zero cost for the ordinary statement: the
3298        // list is empty unless a WINDOW clause left unreferenced defs.
3299        if !stmt.window_check_exprs.is_empty() {
3300            let mut probe = stmt.clone();
3301            probe.items = stmt
3302                .window_check_exprs
3303                .iter()
3304                .map(|e| spg_sql::ast::SelectItem::Expr {
3305                    expr: e.clone(),
3306                    alias: None,
3307                })
3308                .collect();
3309            probe.window_check_exprs = Vec::new();
3310            probe.distinct = false;
3311            probe.distinct_on = Vec::new();
3312            probe.group_by = None;
3313            probe.group_by_all = false;
3314            probe.having = None;
3315            probe.unions = Vec::new();
3316            probe.order_by = Vec::new();
3317            probe.locking = None;
3318            probe.limit = Some(spg_sql::ast::LimitExpr::Literal(0));
3319            probe.offset = None;
3320            probe.limit_with_ties = false;
3321            self.exec_select_cancel_inner(&probe, cancel)?;
3322        }
3323        // v7.39 (read01 round 74) — lower `(f(args)).*`. Naming a record's fields
3324        // takes the catalog, so the parser leaves a marker and the rewrite lands
3325        // here: the call moves into a LATERAL FROM item and the item becomes one
3326        // reference per declared column. `SELECT 'p', (rows_of(2)).*` is
3327        // `SELECT 'p', __rec.id, __rec.v FROM rows_of(2) AS __rec` — reusing the
3328        // set-returning FROM machinery of rounds 65 and 69 rather than growing a
3329        // second one.
3330        if let Some(lowered) = self.lower_record_expansion(stmt)? {
3331            return self.exec_select_cancel_inner(&lowered, cancel);
3332        }
3333        // v7.17.0 Phase 1.2 — user-defined VIEW expansion. If the
3334        // FROM / JOIN graph references any catalogued view name,
3335        // re-parse the view body and prepend it as a synthetic
3336        // CTE. Recurses on views-in-views via the regular CTE
3337        // dispatch below. Fast-path: skip the walker entirely when
3338        // the catalog has no views (the typical OLTP load).
3339        if !self.active_catalog().views_all().is_empty() {
3340            if let Some(rewritten) = self.expand_views_in_select(stmt)? {
3341                return self.exec_select_cancel(&rewritten, cancel);
3342            }
3343        }
3344        // v7.37.6-B(sentori Epic 2 P0)— `SELECT … FROM <partition-parent>`
3345        // gets rewritten to a UNION-ALL over the children that overlap
3346        // the WHERE-derived key range. Uses the same CTE-injection
3347        // trick as VIEW expansion above so downstream resolution
3348        // doesn't need a partition-aware code path.
3349        if let Some(rewritten) = self.expand_partition_parents_in_select(stmt)? {
3350            return self.exec_select_cancel(&rewritten, cancel);
3351        }
3352        // v7.16.2 — information_schema / pg_catalog virtual
3353        // views (mailrs round-10 A.3). If the SELECT touches a
3354        // synthetic meta-table name (`__spg_info_*` /
3355        // `__spg_pg_*` — produced by the parser for
3356        // `information_schema.X` / `pg_catalog.X`), clone the
3357        // catalog, materialise the requested view as a real
3358        // temporary table, and re-execute against an enriched
3359        // engine. Same pattern as `exec_with_ctes` for CTEs.
3360        if !self.meta_views_materialised && select_references_meta_view(stmt) {
3361            return self.exec_select_with_meta_views(stmt, cancel);
3362        }
3363        // v6.10.2 — cold-tier time-travel short-circuit. When the
3364        // primary TableRef carries `AS OF SEGMENT '<id>'`, run a
3365        // dedicated cold-segment scan instead of the regular
3366        // hot+index path. The scope is intentionally narrow for
3367        // v6.10.2 — bare `SELECT * FROM <t> AS OF SEGMENT 'id'`,
3368        // optionally with a single-column-equality WHERE. JOINs /
3369        // aggregates / ORDER BY / subqueries on top of a time-
3370        // travelled scan are STABILITY § "Out of v6.10".
3371        if let Some(from) = &stmt.from
3372            && let Some(seg_id) = from.primary.as_of_segment
3373        {
3374            return self.exec_select_as_of_segment(stmt, from, seg_id);
3375        }
3376        // v6.2.0 / v6.5.0 — virtual-table short-circuits. Detected
3377        // pre-CTE because they don't read from the catalog and
3378        // shouldn't participate in regular FROM resolution.
3379        // v6.2.0 / v6.5.0 / v7.38 (read01 P3.NEW3) — virtual-table
3380        // short-circuits. A meta-view FROM materialises to a fixed row
3381        // set. For a bare `SELECT *` we return it directly; otherwise we
3382        // stage it as a temp table and run the normal pipeline, so
3383        // projection / WHERE / ORDER BY / aggregates work over these views
3384        // (they were `SELECT *`-only before). A real table shadowing the
3385        // name wins (checked first), which also stops the staged re-run
3386        // from recursing back into meta-view detection.
3387        if let Some(from) = &stmt.from
3388            && from.joins.is_empty()
3389            && self.active_catalog().get(&from.primary.name).is_none()
3390        {
3391            let lower = from.primary.name.to_ascii_lowercase();
3392            if let Some(result) = self.meta_view_result(&lower) {
3393                let bare = stmt.where_.is_none()
3394                    && stmt.group_by.is_none()
3395                    && stmt.having.is_none()
3396                    && stmt.unions.is_empty()
3397                    && stmt.order_by.is_empty()
3398                    && stmt.limit.is_none()
3399                    && stmt.offset.is_none()
3400                    && !stmt.distinct
3401                    && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard));
3402                if bare {
3403                    return Ok(result);
3404                }
3405                if let QueryResult::Rows { columns, rows } = result {
3406                    let mut catalog = self.active_catalog().clone();
3407                    let cols = infer_column_types(&columns, &rows);
3408                    let schema = TableSchema::new(from.primary.name.clone(), cols);
3409                    catalog.create_table(schema).map_err(EngineError::Storage)?;
3410                    let t = catalog
3411                        .get_mut(&from.primary.name)
3412                        .expect("just-created meta-view table must exist");
3413                    for row in rows {
3414                        t.insert(row).map_err(EngineError::Storage)?;
3415                    }
3416                    let mut eng = Engine::restore(catalog);
3417                    if let Some(c) = self.clock {
3418                        eng = eng.with_clock(c);
3419                    }
3420                    if let Some(f) = self.salt_fn {
3421                        eng = eng.with_salt_fn(f);
3422                    }
3423                    // v7.39 (read01 pgstatfuncs.c) — carry the calling-
3424                    // connection identity so `WHERE pid = pg_backend_pid()`
3425                    // matches inside the staged meta-view run.
3426                    if let Some(f) = self.backend_pid_fn {
3427                        eng.set_backend_pid_fn(f);
3428                    }
3429                    return eng.exec_select_cancel(stmt, cancel);
3430                }
3431                return Ok(result);
3432            }
3433        }
3434        // v4.11: CTEs materialise into a temporary enriched catalog
3435        // *before* anything else — the body SELECT can then refer
3436        // to CTE names via the regular FROM-clause resolution.
3437        // Uncorrelated only: each CTE body runs once against the
3438        // current catalog, not against later CTEs' results (left-
3439        // to-right materialisation would relax this, but we keep
3440        // it simple for v4.11 MVP).
3441        if !stmt.ctes.is_empty() {
3442            return self.exec_with_ctes(stmt, cancel);
3443        }
3444        // v4.10: subqueries (uncorrelated) are resolved here, before
3445        // the executor sees the row loop. We clone the statement so
3446        // we can mutate without disturbing the caller's AST — most
3447        // queries pass through with no subquery nodes and the clone
3448        // is cheap; with subqueries the materialisation cost
3449        // dominates anyway.
3450        let mut stmt_owned;
3451        let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3452            stmt_owned = stmt.clone();
3453            // v7.33 (mailrs 7.32.1) — sublink pull-up first: an
3454            // aggregate-wrapped correlated scalar subquery whose
3455            // correlation key is UNIQUE/PK becomes a LEFT JOIN, so the
3456            // executor streams one join instead of splicing a per-row
3457            // subplan. Runs before the per-row/batch resolver, which then
3458            // only sees the subqueries the pull-up left behind.
3459            self.pull_up_unique_correlated_agg_subqueries(&mut stmt_owned);
3460            // v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC pull-up) —
3461            // the "per-key latest" scalar subquery shape (inbox / feed
3462            // / timeline applications) becomes a CTE + LEFT JOIN
3463            // against a GROUP BY pre-aggregation that reuses the v7.33
3464            // first_ordered argmax executor. Runs AFTER unique-key
3465            // pull-up (so the unique-key fast path still wins for
3466            // single-PK lookups) and BEFORE the EXISTS sublink rewrite.
3467            // Phase 1 (this commit) is skeleton only — no-op pass.
3468            self.pull_up_correlated_limit_one_subqueries(&mut stmt_owned);
3469            // v7.34.2 (mailrs prod NOT EXISTS) — plan-time `[NOT] EXISTS`
3470            // sublink pull-up to semi/anti-join, before the resolver gets
3471            // a chance to walk per-row.
3472            self.pull_up_exists_sublinks(&mut stmt_owned);
3473            // v7.37.4 — if the LIMIT 1 pullup added CTEs, route through
3474            // exec_with_ctes so they materialise once before the body
3475            // SELECT runs. exec_with_ctes strips ctes from the body
3476            // clone, then re-enters select.
3477            if !stmt_owned.ctes.is_empty() {
3478                return self.exec_with_ctes(&stmt_owned, cancel);
3479            }
3480            // v7.37.x (docker-fair INSUBQ attack) — short-circuit
3481            //   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
3482            // BEFORE `resolve_select_subqueries` materialises the inner
3483            // result as `Vec<Expr::Literal>` (~150 µs for the 6 k-row
3484            // INSUBQ benchmark). Run the inner once, collect the result
3485            // values into a `HashSet<i64>` directly, then probe A.pk per
3486            // value and tally. Returns `Some` when the shape matches.
3487            if let Some(out) = self.try_count_star_pk_in_subquery_fast(&stmt_owned, cancel)? {
3488                return Ok(out);
3489            }
3490            self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3491            &stmt_owned
3492        } else {
3493            stmt
3494        };
3495        if stmt_ref.unions.is_empty() {
3496            return self.exec_bare_select_cancel(stmt_ref, cancel);
3497        }
3498        self.exec_union_chain(stmt_ref, stmt, cancel)
3499    }
3500
3501    #[allow(clippy::too_many_lines)]
3502    #[allow(clippy::too_many_lines)] // huge match — splitting fragments the planner
3503    /// v7.11.7 — execute `SELECT … FROM unnest(expr) [AS] alias …`.
3504    /// Synthesises a single-column virtual table whose column type
3505    /// is TEXT and whose rows are the array elements. Routes
3506    /// through the regular projection / WHERE / ORDER BY / LIMIT
3507    /// machinery so set-returning UNNEST composes naturally with
3508    /// the rest of the SELECT surface.
3509    fn exec_select_unnest(
3510        &self,
3511        stmt: &SelectStatement,
3512        primary: &TableRef,
3513        cancel: CancelToken<'_>,
3514    ) -> Result<QueryResult, EngineError> {
3515        let expr = primary
3516            .unnest_expr
3517            .as_deref()
3518            .expect("caller guards unnest_expr.is_some()");
3519        // Multi-arg unnest(a, b, …) — parallel zip, NULL-padded.
3520        // N value columns instead of one; the shared builder does
3521        // the work and the tail below (WHERE / agg / projection)
3522        // runs against the wider schema.
3523        let multi: Option<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>)> =
3524            match unnest_zip_args(expr) {
3525                Some(args) => Some(unnest_zip_rows(args)?),
3526                None => None,
3527            };
3528        // Evaluate the array expression once. Empty schema / empty
3529        // row — uncorrelated UNNEST cannot reference outer columns.
3530        // v7.39 (read01 round 49) — the ctx must carry the catalog: the enum
3531        // introspection family (enum_range / enum_first / enum_last) resolves
3532        // its labels from the argument's STATIC enum type against the
3533        // catalog's enum registry. Without it `unnest(enum_range(NULL::mood))`
3534        // fell through to the generic arm, got NULL, and expanded to zero rows
3535        // — while the bare `SELECT enum_range(NULL::mood)` (whose ctx does
3536        // carry the catalog) worked.
3537        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
3538        let ctx = EvalContext::new(&empty_schema, None).with_catalog(self.active_catalog());
3539        let dummy_row = Row::new(alloc::vec::Vec::new());
3540        // v7.11.13 — unnest dispatches per array element type so
3541        // INT[] / BIGINT[] surface their PG types in projection.
3542        // v7.39 (round 758, F31-B8a) — the composite SRF names its own
3543        // columns (PG: lexeme | positions | weights); everything else
3544        // keeps the alias / "unnest" defaults below.
3545        let mut composite_names: Option<&[&str]> = None;
3546        let (dtypes, rows): (alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>) =
3547            if let Some(m) = multi {
3548                m
3549            } else {
3550                // v7.39 (round 236) — flatten a multidimensional array into
3551                // its row-major elements (PG) before the 1-D-only match.
3552                let unnest_src = {
3553                    let v = eval::eval_expr(expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
3554                    crate::eval::values::flatten_2d(&v).unwrap_or(v)
3555                };
3556                let mut return_multi: Option<(
3557                    alloc::vec::Vec<DataType>,
3558                    alloc::vec::Vec<Row<'static>>,
3559                )> = None;
3560                let (elem_dtype, rows): (DataType, alloc::vec::Vec<Row<'static>>) = match unnest_src
3561                {
3562                    Value::Null => (DataType::Text, alloc::vec::Vec::new()),
3563                    Value::TextArray(items) => {
3564                        let rows = items
3565                            .into_iter()
3566                            .map(|item| {
3567                                Row::new(alloc::vec![match item {
3568                                    Some(s) => Value::text(s),
3569                                    None => Value::Null,
3570                                }])
3571                            })
3572                            .collect();
3573                        (DataType::Text, rows)
3574                    }
3575                    Value::IntArray(items) => {
3576                        let rows = items
3577                            .into_iter()
3578                            .map(|item| {
3579                                Row::new(alloc::vec![match item {
3580                                    Some(n) => Value::Int(n),
3581                                    None => Value::Null,
3582                                }])
3583                            })
3584                            .collect();
3585                        (DataType::Int, rows)
3586                    }
3587                    Value::BigIntArray(items) => {
3588                        let rows = items
3589                            .into_iter()
3590                            .map(|item| {
3591                                Row::new(alloc::vec![match item {
3592                                    Some(n) => Value::BigInt(n),
3593                                    None => Value::Null,
3594                                }])
3595                            })
3596                            .collect();
3597                        (DataType::BigInt, rows)
3598                    }
3599                    Value::Multirange { kind, ranges } => {
3600                        let rows = ranges
3601                            .iter()
3602                            .map(|sp| {
3603                                Row::new(alloc::vec![Value::Range {
3604                                    kind,
3605                                    lower: sp.lower.clone(),
3606                                    upper: sp.upper.clone(),
3607                                    lower_inc: sp.lower_inc,
3608                                    upper_inc: sp.upper_inc,
3609                                    empty: false,
3610                                }])
3611                            })
3612                            .collect();
3613                        (DataType::Range(kind), rows)
3614                    }
3615                    // v7.39 (round 758, F31-B8a) — unnest(tsvector):
3616                    // one row per lexeme, PG18-measured columns
3617                    // lexeme | positions | weights (`a | {1,3} |
3618                    // {D,D}`); a position-less lexeme (a stripped
3619                    // vector) reads NULL in both array columns.
3620                    Value::TsVector(lexemes) => {
3621                        composite_names = Some(&["lexeme", "positions", "weights"]);
3622                        let rows = lexemes
3623                            .iter()
3624                            .map(|l| {
3625                                let (pos, wts) = if l.positions.is_empty() {
3626                                    (Value::Null, Value::Null)
3627                                } else {
3628                                    let letter = match l.weight {
3629                                        3 => "A",
3630                                        2 => "B",
3631                                        1 => "C",
3632                                        _ => "D",
3633                                    };
3634                                    (
3635                                        Value::SmallIntArray(
3636                                            l.positions
3637                                                .iter()
3638                                                .map(|p| {
3639                                                    Some(i16::try_from(*p).unwrap_or(i16::MAX))
3640                                                })
3641                                                .collect(),
3642                                        ),
3643                                        Value::TextArray(
3644                                            l.positions
3645                                                .iter()
3646                                                .map(|_| Some(letter.into()))
3647                                                .collect(),
3648                                        ),
3649                                    )
3650                                };
3651                                Row::new(alloc::vec![Value::text(l.word.clone()), pos, wts])
3652                            })
3653                            .collect();
3654                        return_multi = Some((
3655                            alloc::vec![
3656                                DataType::Text,
3657                                DataType::SmallIntArray,
3658                                DataType::TextArray
3659                            ],
3660                            rows,
3661                        ));
3662                        (DataType::Text, alloc::vec::Vec::new())
3663                    }
3664                    other => {
3665                        // v7.39 (round 622, S05a) — see table_access.rs:
3666                        // the same sentence, and it is a type mismatch.
3667                        return Err(EngineError::Eval(EvalError::TypeMismatch {
3668                            detail: alloc::format!(
3669                                "unnest() expects an array argument, got {}",
3670                                crate::conversions::pg_type_name_for_error_opt(other.data_type())
3671                            ),
3672                        }));
3673                    }
3674                };
3675                if let Some(m) = return_multi {
3676                    m
3677                } else {
3678                    (alloc::vec![elem_dtype], rows)
3679                }
3680            };
3681        let alias = primary
3682            .alias
3683            .clone()
3684            .unwrap_or_else(|| "unnest".to_string());
3685        // v7.13.2 — mailrs round-6 S5. Honour PG-standard
3686        // `UNNEST(arr) AS p(col_name)` column-list aliasing:
3687        // entries map positionally over the value columns. Without
3688        // the column list, a single column falls back to the table
3689        // alias (pre-v7.13.2 behaviour); multi-arg columns default
3690        // to PG's `unnest`.
3691        let n_vals = dtypes.len();
3692        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = dtypes
3693            .iter()
3694            .enumerate()
3695            .map(|(i, dt)| {
3696                let name = primary
3697                    .unnest_column_aliases
3698                    .get(i)
3699                    .cloned()
3700                    .unwrap_or_else(|| {
3701                        if let Some(names) = composite_names {
3702                            names
3703                                .get(i)
3704                                .map_or_else(|| "unnest".to_string(), |n| (*n).to_string())
3705                        } else if n_vals == 1 {
3706                            alias.clone()
3707                        } else {
3708                            "unnest".to_string()
3709                        }
3710                    });
3711                ColumnSchema::new(name, *dt, true)
3712            })
3713            .collect();
3714        // v7.39 (read01 round 78) — the item's row type IS this scalar when the
3715        // parser desugared a base-type-returning function here (see
3716        // TableRef::scalar_fn_item); the marker rides the column so it survives
3717        // every EvalContext an inner stage rebuilds.
3718        if primary.scalar_fn_item && schema_cols.len() == 1 {
3719            schema_cols[0].scalar_row_source = true;
3720        }
3721        // WITH ORDINALITY — trailing BIGINT counting rows from 1
3722        // in element order. The alias entry after the value
3723        // columns renames it (PG default: `ordinality`).
3724        let rows = if primary.with_ordinality {
3725            let ord_name = primary
3726                .unnest_column_aliases
3727                .get(n_vals)
3728                .cloned()
3729                .unwrap_or_else(|| "ordinality".to_string());
3730            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3731            rows.into_iter()
3732                .enumerate()
3733                .map(|(i, row)| {
3734                    let mut vals = row.values.clone();
3735                    vals.push(Value::BigInt(i as i64 + 1));
3736                    Row::new(vals)
3737                })
3738                .collect()
3739        } else {
3740            rows
3741        };
3742        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3743        // `EvalContext::new` drops it and every catalog-dependent cast
3744        // (regclass / enum / composite / domain) silently degrades.
3745        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3746        // Apply WHERE.
3747        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3748            let mut out = alloc::vec::Vec::with_capacity(rows.len());
3749            for row in rows {
3750                cancel.check()?;
3751                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3752                if matches!(v, Value::Bool(true)) {
3753                    out.push(row);
3754                }
3755            }
3756            out
3757        } else {
3758            rows
3759        };
3760        // v7.17.0 Phase 3.P0-48 — aggregate dispatch over the
3761        // unnest source. Same routing the relational scan path
3762        // already takes — without it `SELECT COUNT(*) FROM
3763        // unnest(ARRAY[…])` either errored at projection time or
3764        // returned the wrong shape.
3765        if aggregate::uses_aggregate(stmt) {
3766            // v7.29 — a per-query memo so correlated scalar
3767            // subqueries batch-evaluate once (group map) instead of
3768            // executing per group.
3769            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3770            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3771                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3772                    .map_err(|err| match err {
3773                        EngineError::Eval(ev) => ev,
3774                        other => eval::EvalError::TypeMismatch {
3775                            detail: alloc::format!("{other}"),
3776                        },
3777                    })
3778            };
3779            // v7.39 (round 656) — hand the rows over as they are rather than
3780            // collecting a second vector of `RowRef` wrappers. Note this is
3781            // a set-returning-function path, NOT the relational scan: the
3782            // measured O(rows) cost lived in `run_single_table_aggregate`,
3783            // and converting these four first was a miss that cost a full
3784            // round — every test stayed green and the number did not move.
3785            let agg = aggregate::run(
3786                stmt,
3787                crate::join::AggRows::Owned(&filtered),
3788                &schema_cols,
3789                Some(&alias),
3790                Some(&agg_correlated),
3791                self.parallel_runner.0.as_deref(),
3792                Some(self.active_catalog()),
3793                Some(self),
3794            )?;
3795            return self.finish_agg_result(agg, stmt, cancel);
3796        }
3797        // Projection.
3798        let projection =
3799            build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
3800        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
3801            alloc::vec::Vec::with_capacity(filtered.len());
3802        // v7.19 P5 — Set-Returning-Function in projection
3803        // position (PG `SELECT unnest(arr) FROM t` shape). When a
3804        // SELECT item evaluates to a top-level unnest(arr) call,
3805        // expand it: for each input row, evaluate the array, emit
3806        // one output row per element, broadcasting non-SRF
3807        // projections from the same input row. Multi-SRF + LCM
3808        // padding stays a documented carve-out; mailrs uses
3809        // single-SRF for redirect_uris.
3810        // v7.39 (read01 round 67) — EVERY set-returning item expands, in lockstep
3811        // (see `expand_srf_row`); a user `RETURNS SETOF` function counts too.
3812        let srf_idxs = self.srf_target_idxs(&projection);
3813        // v7.39 (round 621) — which input row each output row came from. An
3814        // SRF turns one input row into many, and the ORDER BY below used to
3815        // index the EXPANDED rows by the INPUT row's position: the result was
3816        // silently truncated to the input row count and left unsorted, so
3817        // `SELECT unnest(ARRAY[1,2]), y FROM unnest(ARRAY[5,6,7]) y ORDER BY 1`
3818        // answered three of its six rows, in no order. Without the ORDER BY
3819        // the same query was already right.
3820        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
3821        if !srf_idxs.is_empty() {
3822            let (rows, src) =
3823                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
3824            projected_rows = rows;
3825            src_of_row = src;
3826        } else {
3827            // v7.24 (round-16 B) — select-list subqueries resolve
3828            // per row (correlated-aware; plain exprs take the fast
3829            // path inside).
3830            let mut proj_memo = memoize::MemoizeCache::default();
3831            for row in &filtered {
3832                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
3833                for p in &projection {
3834                    vals.push(self.eval_expr_with_correlated(
3835                        &p.expr,
3836                        row,
3837                        &scan_ctx,
3838                        cancel,
3839                        Some(&mut proj_memo),
3840                    )?);
3841                }
3842                projected_rows.push(Row::new(vals));
3843            }
3844        }
3845        // ORDER BY / LIMIT — apply on the projected rows (cheap;
3846        // unnest result sets are small by design).
3847        let columns: alloc::vec::Vec<ColumnSchema> = projection
3848            .iter()
3849            // v7.39 (read01 round 54) — keep the column's enum identity through
3850            // the projection (it lives outside the DataType lattice), or a
3851            // derived table / UNION / windowed result forgets it and any outer
3852            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
3853            .map(|p| {
3854                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
3855                c.user_enum_type = p.user_enum_type.clone();
3856                c.mysql_fsp = p.mysql_fsp;
3857                c
3858            })
3859            .collect();
3860        // Re-evaluate ORDER BY against the source schema (pre-projection
3861        // so col refs by name still resolve through `scan_ctx`).
3862        // v7.39 (read01 round 80) — a positional key means the Nth OUTPUT
3863        // column. Evaluated as an expression it is just the constant N: the same
3864        // key for every row, so the sort ran and changed nothing.
3865        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
3866        if !order_by.is_empty() {
3867            // v7.39 (round 621) — one entry per OUTPUT row, not per input row.
3868            // A key that names a select-list item reads it out of the expanded
3869            // row (PG sorts AFTER the expansion); one that names a source
3870            // column the query does not project is evaluated on the input row
3871            // it came from, which is what `srf_order_output_cols` decides.
3872            let out_cols = if srf_idxs.is_empty() {
3873                alloc::vec![None; order_by.len()]
3874            } else {
3875                srf_order_output_cols(&order_by, &projection)
3876            };
3877            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
3878                .iter()
3879                .enumerate()
3880                .map(|(k, out)| -> Result<_, EngineError> {
3881                    let src = src_of_row.get(k).copied().unwrap_or(k);
3882                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
3883                        .iter()
3884                        .zip(out_cols.iter())
3885                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, &filtered[src], &scan_ctx))
3886                        .collect();
3887                    Ok((k, keys?))
3888                })
3889                .collect::<Result<_, _>>()?;
3890            indexed.sort_by(|a, b| {
3891                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
3892                    let o = &order_by[idx];
3893                    let cmp = order_by_value_cmp_in(
3894                        o.desc,
3895                        o.nulls_first,
3896                        ka,
3897                        kb,
3898                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
3899                    );
3900                    if cmp != core::cmp::Ordering::Equal {
3901                        return cmp;
3902                    }
3903                }
3904                core::cmp::Ordering::Equal
3905            });
3906            projected_rows = indexed
3907                .into_iter()
3908                .map(|(i, _)| projected_rows[i].clone())
3909                .collect();
3910        }
3911        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
3912        if stmt.distinct {
3913            projected_rows = dedup_rows(projected_rows, scan_ctx.mysql_dialect);
3914        }
3915        // LIMIT / OFFSET — apply at the tail.
3916        if let Some(offset) = stmt.offset_literal() {
3917            let off = (offset as usize).min(projected_rows.len());
3918            projected_rows.drain(..off);
3919        }
3920        if let Some(limit) = stmt.limit_literal() {
3921            projected_rows.truncate(limit as usize);
3922        }
3923        Ok(QueryResult::Rows {
3924            columns,
3925            rows: projected_rows,
3926        })
3927    }
3928
3929    /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop [,
3930    /// step])` set-returning source. Mirrors `exec_select_unnest`'s
3931    /// shape: evaluate the arg list once against an empty row,
3932    /// materialise the row stream by stepping start → stop, then
3933    /// route through the standard WHERE / projection / ORDER BY /
3934    /// LIMIT pipeline. Two arg-type combos in v7.17:
3935    ///   * integer / integer [/ integer] — SmallInt, Int, BigInt
3936    ///     (widened to BigInt internally; step defaults to 1)
3937    ///   * timestamp / timestamp / interval — date-range
3938    ///     iteration (mailrs's daily-report pattern)
3939    fn exec_select_generate_series(
3940        &self,
3941        stmt: &SelectStatement,
3942        primary: &TableRef,
3943        cancel: CancelToken<'_>,
3944    ) -> Result<QueryResult, EngineError> {
3945        let args = primary
3946            .generate_series_args
3947            .as_ref()
3948            .expect("caller guards generate_series_args.is_some()");
3949        let (elem_dtype, rows) = generate_series_rows(args, &cancel)?;
3950        let alias = primary
3951            .alias
3952            .clone()
3953            .unwrap_or_else(|| "generate_series".to_string());
3954        // `AS t(n)` — the first column-alias entry renames the
3955        // series column (PG semantics); bare alias keeps the
3956        // pre-existing behaviour of naming the column after it.
3957        let col_name = primary
3958            .unnest_column_aliases
3959            .first()
3960            .cloned()
3961            .unwrap_or_else(|| alias.clone());
3962        let col_schema = ColumnSchema::new(col_name, elem_dtype, true);
3963        let mut schema_cols = alloc::vec![col_schema.clone()];
3964        // WITH ORDINALITY — trailing BIGINT counting rows from 1;
3965        // the second column-alias entry renames it.
3966        let rows = if primary.with_ordinality {
3967            let ord_name = primary
3968                .unnest_column_aliases
3969                .get(1)
3970                .cloned()
3971                .unwrap_or_else(|| "ordinality".to_string());
3972            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3973            rows.into_iter()
3974                .enumerate()
3975                .map(|(i, row)| {
3976                    let mut vals = row.values.clone();
3977                    vals.push(Value::BigInt(i as i64 + 1));
3978                    Row::new(vals)
3979                })
3980                .collect()
3981        } else {
3982            rows
3983        };
3984        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3985        // `EvalContext::new` drops it and every catalog-dependent cast
3986        // (regclass / enum / composite / domain) silently degrades.
3987        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3988        // WHERE.
3989        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3990            let mut out = alloc::vec::Vec::with_capacity(rows.len());
3991            for row in rows {
3992                cancel.check()?;
3993                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3994                if matches!(v, Value::Bool(true)) {
3995                    out.push(row);
3996                }
3997            }
3998            out
3999        } else {
4000            rows
4001        };
4002        // v7.17.0 Phase 3.P0-48 — aggregate dispatch for set-
4003        // returning sources. When the SELECT projection contains
4004        // aggregate functions (COUNT/SUM/MIN/MAX/AVG/string_agg/
4005        // …) we route the filtered row stream through the same
4006        // aggregate executor the relational scan path uses, so
4007        // `SELECT COUNT(*) FROM generate_series(1, 100)` returns
4008        // a single 100 row instead of erroring at projection
4009        // time. GROUP BY / HAVING / ORDER BY over the aggregate
4010        // output all ride through `aggregate::run`.
4011        if aggregate::uses_aggregate(stmt) {
4012            // v7.29 — a per-query memo so correlated scalar
4013            // subqueries batch-evaluate once (group map) instead of
4014            // executing per group.
4015            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
4016            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
4017                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
4018                    .map_err(|err| match err {
4019                        EngineError::Eval(ev) => ev,
4020                        other => eval::EvalError::TypeMismatch {
4021                            detail: alloc::format!("{other}"),
4022                        },
4023                    })
4024            };
4025            // v7.39 (round 656) — hand the rows over as they are rather than
4026            // collecting a second vector of `RowRef` wrappers. Note this is
4027            // a set-returning-function path, NOT the relational scan: the
4028            // measured O(rows) cost lived in `run_single_table_aggregate`,
4029            // and converting these four first was a miss that cost a full
4030            // round — every test stayed green and the number did not move.
4031            let agg = aggregate::run(
4032                stmt,
4033                crate::join::AggRows::Owned(&filtered),
4034                &schema_cols,
4035                Some(&alias),
4036                Some(&agg_correlated),
4037                self.parallel_runner.0.as_deref(),
4038                Some(self.active_catalog()),
4039                Some(self),
4040            )?;
4041            return self.finish_agg_result(agg, stmt, cancel);
4042        }
4043        // Projection.
4044        let projection =
4045            build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
4046        // v7.39 (round 621) — and here, for the same reason.
4047        let srf_idxs = self.srf_target_idxs(&projection);
4048        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4049        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
4050            alloc::vec::Vec::with_capacity(filtered.len());
4051        let mut proj_memo = memoize::MemoizeCache::default();
4052        if !srf_idxs.is_empty() {
4053            let (rows, src) =
4054                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
4055            projected_rows = rows;
4056            src_of_row = src;
4057        } else {
4058            for row in &filtered {
4059                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
4060                for p in &projection {
4061                    // v7.24 (round-16 B) — correlated-aware.
4062                    vals.push(self.eval_expr_with_correlated(
4063                        &p.expr,
4064                        row,
4065                        &scan_ctx,
4066                        cancel,
4067                        Some(&mut proj_memo),
4068                    )?);
4069                }
4070                projected_rows.push(Row::new(vals));
4071            }
4072        }
4073        let columns: alloc::vec::Vec<ColumnSchema> = projection
4074            .iter()
4075            // v7.39 (read01 round 54) — keep the column's enum identity through
4076            // the projection (it lives outside the DataType lattice), or a
4077            // derived table / UNION / windowed result forgets it and any outer
4078            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4079            .map(|p| {
4080                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
4081                c.user_enum_type = p.user_enum_type.clone();
4082                c.mysql_fsp = p.mysql_fsp;
4083                c
4084            })
4085            .collect();
4086        // ORDER BY against the source schema.
4087        // v7.39 (round 621) — one entry per OUTPUT row (a target-list SRF makes
4088        // more of them than there were inputs), and a positional key means the
4089        // Nth OUTPUT column, which is what `resolve_positional_order_by` does
4090        // and what the other two synthetic-source tails already did.
4091        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4092        if !order_by.is_empty() {
4093            let out_cols = if srf_idxs.is_empty() {
4094                alloc::vec![None; order_by.len()]
4095            } else {
4096                srf_order_output_cols(&order_by, &projection)
4097            };
4098            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4099                .iter()
4100                .enumerate()
4101                .map(|(k, out)| -> Result<_, EngineError> {
4102                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
4103                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4104                        .iter()
4105                        .zip(out_cols.iter())
4106                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, r, &scan_ctx))
4107                        .collect();
4108                    Ok((k, keys?))
4109                })
4110                .collect::<Result<_, _>>()?;
4111            indexed.sort_by(|a, b| {
4112                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4113                    let o = &stmt.order_by[idx];
4114                    let cmp = order_by_value_cmp_in(
4115                        o.desc,
4116                        o.nulls_first,
4117                        ka,
4118                        kb,
4119                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4120                    );
4121                    if cmp != core::cmp::Ordering::Equal {
4122                        return cmp;
4123                    }
4124                }
4125                core::cmp::Ordering::Equal
4126            });
4127            projected_rows = indexed
4128                .into_iter()
4129                .map(|(i, _)| projected_rows[i].clone())
4130                .collect();
4131        }
4132        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4133        if stmt.distinct {
4134            projected_rows = dedup_rows(projected_rows, scan_ctx.mysql_dialect);
4135        }
4136        if let Some(offset) = stmt.offset_literal() {
4137            let off = (offset as usize).min(projected_rows.len());
4138            projected_rows.drain(..off);
4139        }
4140        if let Some(limit) = stmt.limit_literal() {
4141            projected_rows.truncate(limit as usize);
4142        }
4143        Ok(QueryResult::Rows {
4144            columns,
4145            rows: projected_rows,
4146        })
4147    }
4148
4149    /// The FROM shapes that are not an ordinary table scan — joins, the
4150    /// set-returning sources, JSON_TABLE, a derived table, and the rest.
4151    ///
4152    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4153    /// reason round 848 established in the parser: a debug build gives
4154    /// EVERY branch's locals a slot in the frame, whichever branch runs.
4155    /// `exec_bare_select_cancel` measured 64,784 bytes and a nested query
4156    /// stacks several of them; a plain scan reaches none of these
4157    /// branches. Moving them out took the frame to 52,336.
4158    ///
4159    /// `Ok(None)` means "not one of these shapes, carry on".
4160    #[inline(never)]
4161    fn try_from_shape_paths(
4162        &self,
4163        stmt: &SelectStatement,
4164        from: &spg_sql::ast::FromClause,
4165        cancel: CancelToken<'_>,
4166    ) -> Result<Option<QueryResult>, EngineError> {
4167        if !from.joins.is_empty() {
4168            // v7.37.x (docker-fair LEFTJOIN 71 % attack) — LEFT JOIN
4169            // elimination: when a LEFT JOIN's right side is referenced
4170            // ONLY in the ON equality and the right-side join key is
4171            // UNIQUE/PK, the join preserves outer cardinality exactly
4172            // and contributes no values used downstream. Drop the
4173            // entire join. PG does this on the
4174            // `SELECT COUNT(*) FROM A LEFT JOIN B ON B.pk = A.fk` shape
4175            // — A's row count is what survives, B never has to be
4176            // touched.
4177            if let Some(eliminated) = self.try_eliminate_redundant_left_joins(stmt) {
4178                return self.exec_bare_select_cancel(&eliminated, cancel).map(Some);
4179            }
4180            // v7.38 P0 元机制 D — `SPG_TEST_DISABLE_JOINFOLD=1` skips
4181            // the v7.32 joinfold rewrite that turns inner JOINs into a
4182            // single-table scan when the catalogue can prove key-only
4183            // dependency. Tests use this to assert "without joinfold,
4184            // the join still executes correctly" (joinfold is a
4185            // semantically-equivalent rewrite, not a correctness fix).
4186            if !self.env_cfg().disable_joinfold {
4187                if let Some(folded) = self.try_fold_inner_joins(stmt, cancel)? {
4188                    return self.exec_bare_select_cancel(&folded, cancel).map(Some);
4189                }
4190            }
4191            return self.exec_joined_select(stmt, from, cancel).map(Some);
4192        }
4193        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>`. Synthesise a
4194        // single-column table at SELECT entry by evaluating the
4195        // expression once against the empty row (UNNEST is
4196        // uncorrelated in v7.11; correlated / LATERAL unnest is a
4197        // v7.12 carve-out). Build a virtual `Table` in a heap-only
4198        // catalog, then route to the regular scan path.
4199        if from.primary.unnest_expr.is_some() {
4200            return self
4201                .exec_select_unnest(stmt, &from.primary, cancel)
4202                .map(Some);
4203        }
4204        // v7.37.43-T4.5 — `FROM jsonb_each_text(<expr>)` set-
4205        // returning function. Same dispatch shape as unnest but
4206        // emits a two-column (key TEXT, value TEXT) row stream.
4207        if from.primary.jsonb_each_text_arg.is_some() {
4208            return self
4209                .exec_select_jsonb_each_text(stmt, &from.primary, cancel)
4210                .map(Some);
4211        }
4212        // v7.39 (read01 partitionfuncs.c) — FROM-position table functions
4213        // (pg_partition_tree / pg_partition_ancestors) dispatched by name.
4214        // v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))` whose entries have no
4215        // array form. Each function runs; the results zip in LOCKSTEP with the
4216        // shorter padded to NULL — the SAME rule the target-list SRFs follow
4217        // (round 67), which is why `srf_values` is what evaluates each entry.
4218        if from.primary.rows_from.is_some() {
4219            let (rows, mut schema_cols) = self.rows_from_rows(&from.primary)?;
4220            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4221                if let Some(col) = schema_cols.get_mut(i) {
4222                    col.name = new_name.clone();
4223                }
4224            }
4225            let alias = from
4226                .primary
4227                .alias
4228                .clone()
4229                .unwrap_or_else(|| from.primary.name.clone());
4230            return self
4231                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4232                .map(Some);
4233        }
4234        // v7.39 (round 205, JSON_TABLE) — `FROM JSON_TABLE(doc, '$p'
4235        // COLUMNS (...))`. Materialise the row stream + schema by
4236        // walking the row path, then run the regular pipeline over it.
4237        if let Some(jt) = &from.primary.json_table {
4238            let (rows, schema_cols) = self.json_table_rows(jt, None)?;
4239            let alias = from
4240                .primary
4241                .alias
4242                .clone()
4243                .unwrap_or_else(|| from.primary.name.clone());
4244            return self
4245                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4246                .map(Some);
4247        }
4248        if from.primary.table_fn_call.is_some() {
4249            let (rows, mut schema_cols) = self.table_fn_rows(&from.primary)?;
4250            // v7.39 (read01 round 68) — WITH ORDINALITY appends a BIGINT counter
4251            // (from 1, in output order) AFTER the function's own columns. The
4252            // alias list names it like any other, which is why it is appended
4253            // BEFORE the renaming pass below.
4254            let rows = if from.primary.with_ordinality {
4255                schema_cols.push(ColumnSchema::new(
4256                    "ordinality".to_string(),
4257                    DataType::BigInt,
4258                    false,
4259                ));
4260                rows.into_iter()
4261                    .enumerate()
4262                    .map(|(i, r)| {
4263                        let mut vals = r.values;
4264                        vals.push(Value::BigInt(i as i64 + 1));
4265                        Row::new(vals)
4266                    })
4267                    .collect()
4268            } else {
4269                rows
4270            };
4271            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4272                if let Some(col) = schema_cols.get_mut(i) {
4273                    col.name = new_name.clone();
4274                }
4275            }
4276            let alias = from
4277                .primary
4278                .alias
4279                .clone()
4280                .unwrap_or_else(|| from.primary.name.clone());
4281            return self
4282                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4283                .map(Some);
4284        }
4285        // v7.37.17 (17.6 siblings) — plain derived table in primary
4286        // position: `FROM ( SELECT … ) alias` (no joins). The inner
4287        // SELECT materialises once (it is uncorrelated by
4288        // construction), then the outer projection / WHERE /
4289        // aggregate / ORDER BY pipeline runs over the synthetic
4290        // table. Joined derived tables keep riding the LATERAL
4291        // machinery in join.rs.
4292        if from.joins.is_empty() && from.primary.lateral_subquery.is_some() {
4293            // v7.39 (round 727) — flatten first. A simple derived table
4294            // (bare-column projection over one stored table, nothing that
4295            // changes cardinality or order) used to force the inner
4296            // SELECT through the SERIAL row-at-a-time projection pipeline
4297            // just to materialise a synthetic table the outer query then
4298            // re-scans: `count(*) FROM (SELECT id v FROM d WHERE …) q`
4299            // measured 18.6 ms against PG's 5 — and bare count over the
4300            // same filter WITHOUT the wrapper is 2 ms here, because it
4301            // rides the fused parallel lane. Rewriting to the unwrapped
4302            // form is PG's subquery pull-up; the whole tree gets the
4303            // fast lanes back.
4304            if let Some(flat) = try_flatten_derived(stmt, &from.primary) {
4305                return self.exec_select_cancel(&flat, cancel).map(Some);
4306            }
4307            // v7.39 (round 742) — `SELECT count(*) FROM (SELECT … ORDER
4308            // BY … OFFSET k) q` is `greatest(count_of_inner - k, 0)`:
4309            // ORDER BY never changes the row count, and OFFSET drops
4310            // exactly k. The materialising path sorted 500k rows to
4311            // count 10k (57 ms); PG runs its parallel sort anyway
4312            // (28 ms). The rewrite skips the sort entirely on both
4313            // counts — a plan PG itself does not have.
4314            if let Some(rewritten) = try_count_over_offset(stmt, &from.primary) {
4315                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4316            }
4317            // v7.39 (round 743) — `count(*) OVER a derived whose only
4318            // item is unnest(ARRAY[k elements])` is `k * count(WHERE)`:
4319            // a constant-length array unnests to exactly k rows per
4320            // input row, NULL elements included. PG expands the set to
4321            // count it (6.6 ms on the panel cell); the identity doesn't.
4322            if let Some(rewritten) = try_count_over_const_unnest(stmt, &from.primary) {
4323                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4324            }
4325            return self
4326                .exec_select_derived(stmt, &from.primary, cancel)
4327                .map(Some);
4328        }
4329        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4330        // [, step])` set-returning source. Dispatch mirrors UNNEST:
4331        // materialise the row stream from a single eval pass, then
4332        // run the regular projection / WHERE / ORDER BY / LIMIT
4333        // pipeline over the synthetic single-column table.
4334        if from.primary.generate_series_args.is_some() {
4335            return self
4336                .exec_select_generate_series(stmt, &from.primary, cancel)
4337                .map(Some);
4338        }
4339        Ok(None)
4340    }
4341
4342    /// Pick an index seek for this WHERE, if any of the four apply:
4343    /// BTree equality, GIN `@@`, trigram LIKE, or JSONB `@>`.
4344    ///
4345    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4346    /// frame reason on `try_from_shape_paths`: in a debug build a
4347    /// closure's locals belong to the enclosing frame, and this one is
4348    /// four seek attempts wide on a function that nests.
4349    #[inline(never)]
4350    fn pick_indexed_rows<'r>(
4351        &'r self,
4352        stmt: &SelectStatement,
4353        table: &'r spg_storage::Table,
4354        schema_cols: &[spg_storage::ColumnSchema],
4355        alias: &str,
4356        ctx: &crate::eval::EvalContext<'_>,
4357        seek_snapshot: &crate::Snapshot,
4358    ) -> Option<Vec<Cow<'r, Row<'static>>>> {
4359        stmt.where_.as_ref().and_then(|w| {
4360            // BTree / col=literal seek first — covers the v7.11.3 multi-
4361            // column AND case and the leading-column equality lookup.
4362            try_index_seek(
4363                w,
4364                schema_cols,
4365                self.active_catalog(),
4366                table,
4367                alias,
4368                seek_snapshot,
4369            )
4370            .or_else(|| {
4371                // v7.12.3 — GIN-accelerated `WHERE col @@
4372                // tsquery` when the column has a `USING gin`
4373                // index. Returns an over-approximate candidate
4374                // set; the WHERE re-eval loop below verifies
4375                // the full `@@` predicate per row.
4376                try_gin_seek(
4377                    w,
4378                    schema_cols,
4379                    self.active_catalog(),
4380                    table,
4381                    alias,
4382                    ctx,
4383                    seek_snapshot,
4384                )
4385            })
4386            .or_else(|| {
4387                // v7.15.0 — trigram-GIN-accelerated
4388                // `WHERE col LIKE / ILIKE '<pat>'` when the
4389                // column has a `gin_trgm_ops` GIN index.
4390                // Over-approximate candidate set; the WHERE
4391                // re-eval verifies the LIKE per row.
4392                try_trgm_seek(w, schema_cols, table, alias, seek_snapshot)
4393            })
4394            .or_else(|| {
4395                // v7.37.8(sentori Epic 5 P2)— real JSONB-GIN
4396                // accelerated `WHERE col @> <jsonb_literal>`
4397                // when the column has a `USING gin` index. The
4398                // posting-list intersection returns an over-
4399                // approximate candidate set; the WHERE re-eval
4400                // verifies the full `@>` predicate per row.
4401                try_gin_jsonb_seek(w, schema_cols, table, alias, seek_snapshot)
4402            })
4403        })
4404    }
4405
4406    /// Index-seek fast paths: NSW kNN, the primary-key top-N walk, and
4407    /// the two `count(*)` short-circuits. Out-of-line for the frame
4408    /// reason on `try_from_shape_paths` — an ordinary scan reaches none
4409    /// of them, and in a debug build their locals sit in the frame
4410    /// regardless.
4411    #[inline(never)]
4412    fn try_seek_fast_paths(
4413        &self,
4414        stmt: &SelectStatement,
4415        table: &spg_storage::Table,
4416        schema_cols: &[spg_storage::ColumnSchema],
4417        alias: &str,
4418        seek_snapshot: &crate::Snapshot,
4419        cancel: CancelToken<'_>,
4420    ) -> Result<Option<QueryResult>, EngineError> {
4421        if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias, seek_snapshot) {
4422            // NSW kNN dispatches against the hot-tier vector index only
4423            // (vector cells aren't promoted to cold segments), so wrap
4424            // the returned row indices as `Cow::Borrowed` for the
4425            // unified `materialise_in_order` shape.
4426            let ordered: Vec<Cow<'_, Row<'static>>> = nsw_rows
4427                .into_iter()
4428                .filter_map(|i| table.rows().get(i).map(Cow::Borrowed))
4429                .collect();
4430            return materialise_in_order(
4431                stmt,
4432                schema_cols,
4433                alias,
4434                &ordered,
4435                self.backslash_escapes,
4436            )
4437            .map(Some);
4438        }
4439
4440        // v7.34.5 — ORDER BY <indexed col> [DESC|ASC] LIMIT N drives
4441        // the scan via the BTree iterator in the requested direction
4442        // and stops after `OFFSET + LIMIT` candidates pass WHERE. The
4443        // 80 ms `mailrs_prod_plain_limit` baseline at 250 k rows is
4444        // the load-bearing consumer; this skips the materialise-every-
4445        // row + partial-sort tail entirely. Walker output is already
4446        // in ORDER BY order so `materialise_in_order` (no extra sort)
4447        // is the natural sink.
4448        if let Some(walked) = try_pk_walk_top_n(
4449            stmt,
4450            self.active_catalog(),
4451            table,
4452            schema_cols,
4453            alias,
4454            self,
4455            cancel,
4456        ) {
4457            return materialise_in_order(stmt, schema_cols, alias, &walked, self.backslash_escapes)
4458                .map(Some);
4459        }
4460
4461        // Index seek: if WHERE is `col = literal` (or commuted) and the
4462        // referenced column has an index, dispatch each locator through
4463        // the catalog (hot tier → borrow, cold tier → page-read +
4464        // decode) and iterate just those rows. Otherwise fall back to a
4465        // v7.37.x (docker-fair INSUBQ attack) — short-circuit COUNT(*)
4466        // FROM A WHERE A.pk IN (large literal list). The post-subquery-
4467        // replacement shape of INSUBQ. Runs BEFORE `indexed_rows` so
4468        // we don't pay the row materialisation cost twice. Returns
4469        // a bare `Rows{count}` if the shape matches.
4470        if aggregate::uses_aggregate(stmt)
4471            && let Some(out) = self.try_count_star_pk_in_list_fast(stmt, table, schema_cols, alias)
4472        {
4473            return Ok(Some(out));
4474        }
4475        // v7.38 (perf) — `count(*) WHERE <indexed BETWEEN>`: count the in-range
4476        // locators directly, skipping row materialisation + WHERE re-eval.
4477        if aggregate::uses_aggregate(stmt)
4478            && let Some(out) = self.try_count_star_indexed_range_fast(
4479                stmt,
4480                table,
4481                schema_cols,
4482                alias,
4483                seek_snapshot,
4484            )
4485        {
4486            return Ok(Some(out));
4487        }
4488        Ok(None)
4489    }
4490
4491    /// The two rewrites that must happen before the FROM clause is even
4492    /// looked at: a meta-view reference needs the catalog views
4493    /// materialised, and a windowed projection belongs to the window
4494    /// executor. Out-of-line for the frame reason on
4495    /// `try_from_shape_paths`.
4496    #[inline(never)]
4497    fn try_pre_from_paths(
4498        &self,
4499        stmt: &SelectStatement,
4500        cancel: CancelToken<'_>,
4501    ) -> Result<Option<QueryResult>, EngineError> {
4502        if !self.meta_views_materialised && select_references_meta_view(stmt) {
4503            return self.exec_select_with_meta_views(stmt, cancel).map(Some);
4504        }
4505        // v4.12: window-function path. When the projection contains
4506        // any `name(args) OVER (...)` we route to the dedicated
4507        // executor — partition + sort + per-row window value before
4508        // the regular projection.
4509        if select_has_window(stmt) {
4510            // v7.37 D.23 — window functions run AFTER GROUP BY aggregation.
4511            // `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g`
4512            // needs the aggregation done first, then windows over the grouped
4513            // rows. Rewrite to an aggregate derived subquery + outer window query
4514            // (which the window-over-derived path, D.13, executes). Only fires on
4515            // the currently-erroring agg+window+GROUP BY shape, so it can't
4516            // regress working window-only or aggregate-only queries.
4517            if let Some(rewritten) = rewrite_agg_before_window(stmt) {
4518                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4519            }
4520            return self.exec_select_with_window(stmt, cancel).map(Some);
4521        }
4522        Ok(None)
4523    }
4524
4525    /// A projection naming `ctid` or another system column: the schema
4526    /// has to be widened with them before the scan. Out-of-line for the
4527    /// frame reason on `try_from_shape_paths`.
4528    #[inline(never)]
4529    fn try_ctid_projection(
4530        &self,
4531        stmt: &SelectStatement,
4532        primary: &spg_sql::ast::TableRef,
4533        table: &spg_storage::Table,
4534        schema_cols: &[spg_storage::ColumnSchema],
4535        alias: &str,
4536        cancel: CancelToken<'_>,
4537    ) -> Result<Option<QueryResult>, EngineError> {
4538        if references_ctid(stmt) {
4539            let snapshot = self.current_snapshot();
4540            let mut ext_cols = schema_cols.to_vec();
4541            for name in SYSTEM_COLUMNS {
4542                ext_cols.push(ColumnSchema::new(name.to_string(), DataType::Text, false));
4543            }
4544            let table_oid =
4545                crate::system_catalog::relation_oid(self.active_catalog(), &primary.name)
4546                    .unwrap_or(0);
4547            let headers = table.headers();
4548            let rows: Vec<Row<'static>> = table
4549                .scan_visible(&snapshot)
4550                .map(|(i, r)| {
4551                    let mut vals = r.values.clone();
4552                    // One block, offsets from 1, as PG numbers them.
4553                    vals.push(Value::Tid(0, i as u32 + 1));
4554                    let h = headers.get(i);
4555                    vals.push(Value::Xid(h.map_or(0, |h| h.xmin as u32)));
4556                    vals.push(Value::Xid(h.map_or(0, |h| h.xmax as u32)));
4557                    // SPG keeps no per-statement command ids; PG shows 0 for
4558                    // every row a reader can see, which is every row here.
4559                    vals.push(Value::Cid(0));
4560                    vals.push(Value::Cid(0));
4561                    vals.push(Value::BigInt(table_oid));
4562                    Row::new(vals)
4563                })
4564                .collect();
4565            return self
4566                .exec_select_over_rows(stmt, rows, ext_cols, alias, cancel)
4567                .map(Some);
4568        }
4569        Ok(None)
4570    }
4571
4572    /// A sequence read as a one-row relation (`SELECT last_value FROM
4573    /// seq`), which PG allows and psql's \\d relies on. Out-of-line for
4574    /// the frame reason on `try_from_shape_paths`.
4575    #[inline(never)]
4576    fn try_sequence_relation(
4577        &self,
4578        stmt: &SelectStatement,
4579        primary: &spg_sql::ast::TableRef,
4580        cancel: CancelToken<'_>,
4581    ) -> Result<Option<QueryResult>, EngineError> {
4582        if self.active_catalog().get(&primary.name).is_none()
4583            && let Some(seq) = self.active_catalog().sequence(&primary.name)
4584        {
4585            let rows = alloc::vec![Row::new(alloc::vec![
4586                Value::BigInt(seq.last_value),
4587                Value::BigInt(0),
4588                Value::Bool(seq.is_called),
4589            ])];
4590            let schema_cols = alloc::vec![
4591                ColumnSchema::new("last_value", DataType::BigInt, false),
4592                ColumnSchema::new("log_cnt", DataType::BigInt, false),
4593                ColumnSchema::new("is_called", DataType::Bool, false),
4594            ];
4595            let alias = primary
4596                .alias
4597                .clone()
4598                .unwrap_or_else(|| primary.name.clone());
4599            return self
4600                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4601                .map(Some);
4602        }
4603        Ok(None)
4604    }
4605
4606    pub(crate) fn exec_bare_select_cancel(
4607        &self,
4608        stmt: &SelectStatement,
4609        cancel: CancelToken<'_>,
4610    ) -> Result<QueryResult, EngineError> {
4611        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST N ROWS WITH TIES`
4612        // is meaningless without an ORDER BY; PG raises a hard
4613        // error and SPG mirrors the surface so the same DDL/app
4614        // path behaves identically on cutover.
4615        check_with_ties_requires_order_by(stmt)?;
4616        // v7.39 (round 229) — WHERE / HAVING run before the window pass, so
4617        // PG rejects window calls there outright. Checked here rather than
4618        // on the window path: `HAVING row_number() OVER () = 1` has no
4619        // window in its projection at all.
4620        crate::window::reject_window_in_row_clauses(stmt)?;
4621        // v7.39 (round 232) — the ORDER BY legality rules (positional
4622        // bounds, DISTINCT, DISTINCT ON). Same placement as the window
4623        // check: before anything scans.
4624        crate::orderby::check_order_by_legality(stmt)?;
4625        // v7.37.16 — resolve `USING` column-merge + `NATURAL JOIN` into an
4626        // equivalent statement the regular executor handles (merged join
4627        // columns collapse to a single unqualified output column; NATURAL
4628        // gets its common-column ON synthesised). The rewrite clears the
4629        // flags, so this re-entrant call is a no-op on the second pass.
4630        if let Some(rewritten) = self.desugar_using_natural(stmt)? {
4631            return self.exec_bare_select_cancel(&rewritten, cancel);
4632        }
4633        // v7.39 (RLS) Phase 3 — cross-table joins: wrap each RLS-enabled join
4634        // operand in a security-barrier subquery, then re-enter (the wrapped
4635        // operands are no longer bare RLS tables, so this is a no-op on the
4636        // second pass).
4637        if let Some(rewritten) = self.rls_rewrite_joins(stmt) {
4638            return self.exec_bare_select_cancel(&rewritten, cancel);
4639        }
4640        // v7.39 (RLS) Phase 1 — for a policy-subject (non-superuser) session,
4641        // AND the RLS USING predicate into a single-table SELECT's WHERE.
4642        // Superuser sessions and non-RLS tables get `None` (no clone, no
4643        // change). Applied inline (shadowing `stmt`) rather than via re-entry
4644        // so it can't re-inject on a recursive pass.
4645        let rls_stmt;
4646        let stmt = match self.rls_select_predicate(stmt)? {
4647            Some(pred) => {
4648                let mut s = stmt.clone();
4649                s.where_ = Some(match s.where_.take() {
4650                    Some(existing) => spg_sql::ast::Expr::Binary {
4651                        lhs: alloc::boxed::Box::new(existing),
4652                        op: spg_sql::ast::BinOp::And,
4653                        rhs: alloc::boxed::Box::new(pred),
4654                    },
4655                    None => pred,
4656                });
4657                rls_stmt = s;
4658                &rls_stmt
4659            }
4660            None => stmt,
4661        };
4662        // v7.16.2 — same meta-view dispatch as
4663        // `exec_select_cancel`, applied here too because
4664        // `subquery_replacement` enters this function directly
4665        // for Exists / ScalarSubquery / InSubquery resolution
4666        // (bypassing the top-level entry to avoid double
4667        // subquery walking). Without this dispatch the subquery
4668        // hits `__spg_info_columns` and reports TableNotFound.
4669        if let Some(done) = self.try_pre_from_paths(stmt, cancel)? {
4670            return Ok(done);
4671        }
4672        // Constant SELECT (no FROM) — evaluate each item once against an
4673        // empty dummy row. Useful for `SELECT 1`, `SELECT coalesce(...)`,
4674        // `SELECT '7'::INT`. Column references will surface as
4675        // ColumnNotFound on eval since the schema is empty.
4676        let Some(from) = &stmt.from else {
4677            return self.exec_constant_select(stmt);
4678        };
4679        // Multi-table FROM (one or more joined peers) goes through the
4680        // nested-loop join executor. Single-table FROM stays on the
4681        // existing scan + index-seek path.
4682        if let Some(done) = self.try_from_shape_paths(stmt, from, cancel)? {
4683            return Ok(done);
4684        }
4685        // NOT hooked up. `try_spill_sorted_scan` is written, correct and
4686        // tested — eight ORDER BY shapes byte-identical spilled against
4687        // in-memory, with 103 runs opened to prove the spill ran — and it
4688        // loses on wall clock, which is a hard stop whatever the memory
4689        // buys. Measured round 865, same psql client both sides, same
4690        // machine, row counts verified, and both sides confirmed to be
4691        // doing an external merge rather than an indexed walk:
4692        //
4693        //   PG18        178.7 - 187.0 ms   Sort Method: external merge, 85 MB
4694        //   SPG spilled 269.7 - 299.6 ms   33 spill files at peak
4695        //
4696        // Non-overlapping, about 1.55x. Re-enable by restoring the call
4697        // below once that closes; nothing else has to change, which is
4698        // the point of it being a separate path.
4699        //
4700        //   if let Some(done) = self.try_spill_sorted_scan(stmt, from, cancel)? {
4701        //       return Ok(done);
4702        //   }
4703        //
4704        // v7.37 (round 882) — this walk stays unhooked, but its streaming
4705        // twin `try_spill_sorted_stream` IS hooked, above the ORDER BY
4706        // bail in `try_exec_joined_streaming`. Collecting the answer was
4707        // most of what this one cost: handing rows over as the merge
4708        // produces them holds peak to the budget plus one row, and the
4709        // wall clock lands inside PG18's range rather than 1.55x outside
4710        // it. Numbers in `extsort.rs`'s header.
4711        let primary = &from.primary;
4712        // v7.39 (round 244) — a sequence is selectable as a one-row relation
4713        // in PG (`SELECT last_value FROM seq` — psql's \d and several ORMs
4714        // read it). Synthesize PG's three columns.
4715        if let Some(done) = self.try_sequence_relation(stmt, primary, cancel)? {
4716            return Ok(done);
4717        }
4718        let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4719            StorageError::TableNotFound {
4720                name: primary.name.clone(),
4721            }
4722        })?;
4723        let schema_cols = &table.schema().columns;
4724        // The qualifier accepted on column refs is the alias (if any) else the
4725        // bare table name.
4726        let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4727        // v7.39 (round 511) — `ctid`, PG's physical row identity. SPG had no
4728        // system columns at all: `SELECT ctid FROM t` answered "column
4729        // \"ctid\" does not exist", which takes out the dedup idiom every
4730        // PG user knows — `DELETE … WHERE ctid NOT IN (SELECT min(ctid) …
4731        // GROUP BY key)`.
4732        //
4733        // The value comes from the row's position, which the scan already
4734        // yields; the column is appended to the schema and the rows only
4735        // when the statement asks for it, so nothing else pays for it. That
4736        // also routes the query down the general path, past the index fast
4737        // paths below — they hand back rows without positions, and a ctid
4738        // that was sometimes right would be worse than none.
4739        if let Some(done) =
4740            self.try_ctid_projection(stmt, primary, table, schema_cols, alias, cancel)?
4741        {
4742            return Ok(done);
4743        }
4744        let ctx = self.ev_ctx(schema_cols, Some(alias));
4745
4746        // NSW kNN planner: `ORDER BY col <-> literal LIMIT k` with no
4747        // WHERE and an NSW index on `col` skips the full scan. The
4748        // walk returns rows already in ascending-distance order, so
4749        // ORDER BY / LIMIT are honoured implicitly.
4750        // Phase C.3 step 2c — compute the reader's MVCC snapshot once
4751        // and thread it into every index-seek fast path below. No-op
4752        // today (every hot header is committed-alive).
4753        let seek_snapshot = self.current_snapshot();
4754        if let Some(done) =
4755            self.try_seek_fast_paths(stmt, table, schema_cols, alias, &seek_snapshot, cancel)?
4756        {
4757            return Ok(done);
4758        }
4759        // full scan over the hot tier (cold-tier rows are only reached
4760        // via index seek in v5.1 — full table scans against cold-tier
4761        // data ship in v5.2 with the freezer's per-segment scan API).
4762        let indexed_rows =
4763            self.pick_indexed_rows(stmt, table, schema_cols, alias, &ctx, &seek_snapshot);
4764
4765        // Aggregate path: filter rows first, then hand off to the
4766        // aggregate executor which does its own projection + ORDER BY.
4767        if aggregate::uses_aggregate(stmt) {
4768            return self.run_single_table_aggregate(
4769                stmt,
4770                table,
4771                schema_cols,
4772                alias,
4773                indexed_rows,
4774                cancel,
4775            );
4776        }
4777        self.run_single_table_scan(stmt, table, schema_cols, alias, indexed_rows, cancel)
4778    }
4779
4780    /// v7.37.43-T4.5 — execute `SELECT … FROM jsonb_each_text(<expr>)`.
4781    /// Sentori migration 0067 uses this with `CROSS JOIN LATERAL`; the
4782    /// uncorrelated FROM-primary case is the simpler shape, used by
4783    /// e2e pins. Materialises the (key, value) pair stream into a
4784    /// synthetic two-column TEXT table, then routes through the
4785    /// regular projection / WHERE / ORDER BY pipeline.
4786    /// v7.39 (read01 partitionfuncs.c) — materialise a FROM-position
4787    /// v7.39 (round 205, JSON_TABLE) — materialise a JSON_TABLE FROM
4788    /// item into (rows, schema). `outer_doc` is `Some` only when this
4789    /// is a NESTED level being expanded against a parent row item's
4790    /// already-parsed sub-document; the top-level call parses the doc
4791    /// expr itself. Row/column paths reuse the existing jsonpath
4792    /// evaluator (`json::json_table_path`); coercion reuses
4793    /// `coerce_value` on the JSON scalar text, so a json string
4794    /// coerces to DATE by its content, matching PG.
4795    #[allow(clippy::type_complexity)]
4796    pub(crate) fn json_table_rows(
4797        &self,
4798        jt: &spg_sql::ast::JsonTable,
4799        outer_doc: Option<&crate::json::JsonValue>,
4800    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4801        // Column schema is static (independent of data): flatten the
4802        // COLUMNS tree in declaration order (NESTED contributes its
4803        // children inline, the PG output shape).
4804        let schema = json_table_schema(&jt.columns);
4805
4806        // PASSING variables → a single JsonValue object the jsonpath
4807        // engine reads `$name` from.
4808        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4809        let ctx = EvalContext::new(&empty_schema, None);
4810        let dummy = Row::new(alloc::vec::Vec::new());
4811        let vars: Option<crate::json::JsonValue> = if jt.passing.is_empty() {
4812            None
4813        } else {
4814            let mut entries = alloc::vec::Vec::new();
4815            for (name, e) in &jt.passing {
4816                let v = eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?;
4817                entries.push((name.clone(), value_to_json_value(&v)));
4818            }
4819            Some(crate::json::JsonValue::Object(entries))
4820        };
4821
4822        // The document root: a NESTED level gets it from the parent;
4823        // the top level parses its doc expr.
4824        let root_owned;
4825        let root: &crate::json::JsonValue = match outer_doc {
4826            Some(d) => d,
4827            None => {
4828                let doc_val = eval::eval_expr(&jt.doc, &dummy, &ctx).map_err(EngineError::Eval)?;
4829                let src = match &doc_val {
4830                    Value::Null => return Ok((alloc::vec::Vec::new(), schema)),
4831                    Value::Json(s) | Value::Text(s) => s.as_ref().to_string(),
4832                    other => {
4833                        return Err(EngineError::Unsupported(alloc::format!(
4834                            "JSON_TABLE document must be json/text, got {}",
4835                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
4836                        )));
4837                    }
4838                };
4839                root_owned = crate::json::parse_doc(&src).map_err(EngineError::Eval)?;
4840                &root_owned
4841            }
4842        };
4843
4844        let items = crate::json::json_table_path(root, &jt.row_path, vars.as_ref())
4845            .map_err(EngineError::Eval)?;
4846        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
4847        for (idx, item) in items.iter().enumerate() {
4848            self.json_table_emit_item(jt, item, idx, vars.as_ref(), &mut rows)?;
4849        }
4850        Ok((rows, schema))
4851    }
4852
4853    /// v7.39 (round 205) — emit the row(s) for one row-pattern item.
4854    /// Regular columns produce one value each; a NESTED column expands
4855    /// as an outer join (each nested match → one row sharing the
4856    /// parent cells; no nested match → one row with the nested cells
4857    /// NULL). Sibling NESTED at one level cross by concatenation of
4858    /// their independent expansions (PG's UNION-of-outer shape).
4859    fn json_table_emit_item(
4860        &self,
4861        jt: &spg_sql::ast::JsonTable,
4862        item: &crate::json::JsonValue,
4863        ordinality: usize,
4864        vars: Option<&crate::json::JsonValue>,
4865        out: &mut alloc::vec::Vec<Row<'static>>,
4866    ) -> Result<(), EngineError> {
4867        use spg_sql::ast::JsonTableColumn as C;
4868        // Parent cells (regular + ordinality), left-to-right; NESTED
4869        // columns contribute a run of child cells appended after.
4870        let mut parent_cells: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
4871        let mut nested_runs: alloc::vec::Vec<alloc::vec::Vec<Row<'static>>> =
4872            alloc::vec::Vec::new();
4873        let mut nested_widths: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4874        for col in &jt.columns {
4875            match col {
4876                C::Ordinality { .. } => {
4877                    parent_cells.push(Value::BigInt(ordinality as i64 + 1));
4878                }
4879                C::Regular { .. } => {
4880                    parent_cells.push(self.json_table_column_value(col, item, vars)?);
4881                }
4882                C::Nested { path, columns } => {
4883                    // Recurse: a nested JSON_TABLE over `item` filtered
4884                    // by `path`, with the same PASSING vars.
4885                    let sub = spg_sql::ast::JsonTable {
4886                        doc: jt.doc.clone(), // unused (outer_doc provided)
4887                        row_path: path.clone(),
4888                        columns: columns.clone(),
4889                        passing: alloc::vec::Vec::new(),
4890                    };
4891                    let (nrows, nschema) = self.json_table_rows(&sub, Some(item))?;
4892                    nested_widths.push(nschema.len());
4893                    nested_runs.push(nrows);
4894                }
4895            }
4896        }
4897        if nested_runs.is_empty() {
4898            out.push(Row::new(parent_cells));
4899            return Ok(());
4900        }
4901        // PG sibling-NESTED semantics: each sibling expands
4902        // INDEPENDENTLY and the results CONCATENATE — a row from
4903        // sibling s fills only s's cells, every other sibling's cells
4904        // NULL. An empty sibling contributes ZERO rows (not a NULL
4905        // row). Only when EVERY sibling is empty does the parent still
4906        // emit one all-NULL row (the outer-join guarantee that a parent
4907        // item is never dropped). Verified vs PG18 (r207): a=1,b=2 → 3
4908        // rows; a=1,b=[] → 1 row; all-empty → 1 NULL row.
4909        let before = out.len();
4910        for (s_idx, run) in nested_runs.iter().enumerate() {
4911            for nrow in run {
4912                let mut cells = parent_cells.clone();
4913                for (o_idx, w) in nested_widths.iter().enumerate() {
4914                    if o_idx == s_idx {
4915                        cells.extend(nrow.values.iter().cloned());
4916                    } else {
4917                        for _ in 0..*w {
4918                            cells.push(Value::Null);
4919                        }
4920                    }
4921                }
4922                out.push(Row::new(cells));
4923            }
4924        }
4925        if out.len() == before {
4926            // Every sibling empty → one all-NULL nested row.
4927            let mut cells = parent_cells.clone();
4928            for w in &nested_widths {
4929                for _ in 0..*w {
4930                    cells.push(Value::Null);
4931                }
4932            }
4933            out.push(Row::new(cells));
4934        }
4935        Ok(())
4936    }
4937
4938    /// v7.39 (round 205) — evaluate one Regular column against a row
4939    /// item: EXISTS → bool; else path → at most one value, coerced to
4940    /// the declared type with ON EMPTY / ON ERROR / DEFAULT behaviour.
4941    fn json_table_column_value(
4942        &self,
4943        col: &spg_sql::ast::JsonTableColumn,
4944        item: &crate::json::JsonValue,
4945        vars: Option<&crate::json::JsonValue>,
4946    ) -> Result<Value<'static>, EngineError> {
4947        use spg_sql::ast::{JsonTableColumn as C, JsonTableOnBehavior as B};
4948        let C::Regular {
4949            name,
4950            ty,
4951            path,
4952            exists,
4953            format_json,
4954            wrapper,
4955            on_empty,
4956            on_error,
4957        } = col
4958        else {
4959            unreachable!("caller guards Regular");
4960        };
4961        let matches = crate::json::json_table_path(item, path, vars).map_err(EngineError::Eval)?;
4962        if *exists {
4963            return Ok(Value::Bool(!matches.is_empty()));
4964        }
4965        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4966        let ctx = EvalContext::new(&empty_schema, None);
4967        let dummy = Row::new(alloc::vec::Vec::new());
4968        let default_of = |b: &B| -> Result<Option<Value<'static>>, EngineError> {
4969            match b {
4970                B::Null => Ok(Some(Value::Null)),
4971                B::Error => Ok(None),
4972                B::Default(e) => Ok(Some(
4973                    eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?,
4974                )),
4975            }
4976        };
4977        // Empty match set → ON EMPTY.
4978        if matches.is_empty() {
4979            return match default_of(on_empty)? {
4980                Some(v) => coerce_json_table_default(v, *ty, name),
4981                None => Err(EngineError::Unsupported(alloc::format!(
4982                    "no SQL/JSON item found for JSON_TABLE column {name:?}"
4983                ))),
4984            };
4985        }
4986        let first = &matches[0];
4987        // FORMAT JSON: return the PG-canonical json representation.
4988        // WITH WRAPPER wraps the whole match SET in an array (even a
4989        // single scalar → `[5]`); without it, the single match's json.
4990        if *format_json {
4991            let text = if *wrapper {
4992                crate::json::JsonValue::Array(matches.clone()).canonical_json_text()
4993            } else {
4994                first.canonical_json_text()
4995            };
4996            return Ok(Value::Json(alloc::borrow::Cow::Owned(text)));
4997        }
4998        if first.is_json_null() {
4999            return Ok(Value::Null);
5000        }
5001        // Coerce the scalar text to the declared type; on failure → ON
5002        // ERROR (default NULL, DEFAULT expr, or raise).
5003        let dt = crate::conversions::column_type_to_data_type(*ty);
5004        let scalar = Value::Text(alloc::borrow::Cow::Owned(first.scalar_text()));
5005        match crate::conversions::coerce_value(scalar, dt, name, 0) {
5006            Ok(v) => Ok(v),
5007            Err(e) => match default_of(on_error)? {
5008                Some(v) => coerce_json_table_default(v, *ty, name),
5009                None => Err(e),
5010            },
5011        }
5012    }
5013
5014    /// table function into (rows, default schema). Dispatch by name.
5015    pub(crate) fn table_fn_rows(
5016        &self,
5017        primary: &TableRef,
5018    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5019        let (fn_name, args) = primary
5020            .table_fn_call
5021            .as_deref()
5022            .expect("caller guards table_fn_call.is_some()");
5023        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5024        let ctx = EvalContext::new(&empty_schema, None);
5025        let dummy_row = Row::new(alloc::vec::Vec::new());
5026        let arg0: Option<Value<'static>> = match args.first() {
5027            Some(e) => Some(eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?),
5028            None => None,
5029        };
5030        match fn_name.as_str() {
5031            // v7.39 (read01 round 76) — `jsonb_populate_record(NULL::t, j)` /
5032            // `…_recordset` (+ json_ variants). The row shape is the BASE
5033            // argument's declared type — a table's or a composite type's
5034            // column list — which only the catalog knows, so the parser hands
5035            // the raw arguments here rather than desugaring blind.
5036            "jsonb_populate_record"
5037            | "json_populate_record"
5038            | "jsonb_populate_recordset"
5039            | "json_populate_recordset" => {
5040                let type_name = match args.first() {
5041                    Some(Expr::Cast {
5042                        target: spg_sql::ast::CastTarget::Named(n),
5043                        ..
5044                    }) => n.clone(),
5045                    _ => {
5046                        return Err(EngineError::Unsupported(alloc::format!(
5047                            "{fn_name}(): first argument must name a row type, \
5048                             e.g. NULL::mytable"
5049                        )));
5050                    }
5051                };
5052                let cat = self.active_catalog();
5053                let cols: alloc::vec::Vec<ColumnSchema> = if let Some(t) = cat.get(&type_name) {
5054                    t.schema().columns.clone()
5055                } else if let Some(c) = cat.composite_types().get(&type_name) {
5056                    c.fields
5057                        .iter()
5058                        .map(|(n, ty)| ColumnSchema::new(n.clone(), *ty, true))
5059                        .collect()
5060                } else {
5061                    return Err(EngineError::Unsupported(alloc::format!(
5062                        "type \"{type_name}\" does not exist"
5063                    )));
5064                };
5065                let json_arg = match args.get(1) {
5066                    Some(e) => eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?,
5067                    None => Value::Null,
5068                };
5069                // The set form iterates the JSON array; the scalar form is
5070                // the one-element case of the same walk.
5071                let docs: alloc::vec::Vec<Value<'static>> = if fn_name.ends_with("recordset") {
5072                    crate::json::array_element_rows(&json_arg, false, fn_name)
5073                        .map_err(EngineError::Eval)?
5074                        .into_iter()
5075                        .map(|s| s.map_or(Value::Null, Value::json))
5076                        .collect()
5077                } else if matches!(json_arg, Value::Null) {
5078                    alloc::vec::Vec::new()
5079                } else {
5080                    alloc::vec![json_arg]
5081                };
5082                let mut rows = alloc::vec::Vec::with_capacity(docs.len());
5083                for doc in &docs {
5084                    let mut vals = alloc::vec::Vec::with_capacity(cols.len());
5085                    for c in &cols {
5086                        // `->>` semantics: a missing key is NULL, present keys
5087                        // arrive as text and cast to the declared column type.
5088                        let raw = crate::json::path_get(doc, &Value::text(c.name.clone()), true)
5089                            .map_err(EngineError::Eval)?;
5090                        let v = if matches!(raw, Value::Null) {
5091                            Value::Null
5092                        } else {
5093                            crate::conversions::coerce_value(raw, c.ty, "", 0)
5094                                .map_err(|e| EngineError::Unsupported(alloc::format!("{e:?}")))?
5095                        };
5096                        vals.push(v);
5097                    }
5098                    rows.push(Row::new(vals));
5099                }
5100                Ok((rows, cols))
5101            }
5102            "pg_partition_tree" => {
5103                let cols = alloc::vec![
5104                    ColumnSchema::new("relid".to_string(), DataType::Text, true),
5105                    ColumnSchema::new("parentrelid".to_string(), DataType::Text, true),
5106                    ColumnSchema::new("isleaf".to_string(), DataType::Bool, true),
5107                    ColumnSchema::new("level".to_string(), DataType::Int, true),
5108                ];
5109                let Some(Value::Text(name)) = &arg0 else {
5110                    // NULL (or missing) argument → zero rows (PG).
5111                    return Ok((alloc::vec::Vec::new(), cols));
5112                };
5113                let entries = crate::partition_walks::tree_of(self.active_catalog(), name.as_ref());
5114                if entries.is_empty() && self.active_catalog().get(name.as_ref()).is_none() {
5115                    return Err(EngineError::Unsupported(alloc::format!(
5116                        "relation \"{name}\" does not exist"
5117                    )));
5118                }
5119                let rows = entries
5120                    .into_iter()
5121                    .map(|(relid, parent, isleaf, level)| {
5122                        Row::new(alloc::vec![
5123                            Value::text(relid),
5124                            parent.map_or(Value::Null, Value::text),
5125                            Value::Bool(isleaf),
5126                            #[allow(clippy::cast_possible_truncation)]
5127                            Value::Int(level as i32),
5128                        ])
5129                    })
5130                    .collect();
5131                Ok((rows, cols))
5132            }
5133            "pg_partition_ancestors" => {
5134                let cols =
5135                    alloc::vec![ColumnSchema::new("relid".to_string(), DataType::Text, true)];
5136                let Some(Value::Text(name)) = &arg0 else {
5137                    return Ok((alloc::vec::Vec::new(), cols));
5138                };
5139                let cat = self.active_catalog();
5140                if cat.get(name.as_ref()).is_none() {
5141                    return Err(EngineError::Unsupported(alloc::format!(
5142                        "relation \"{name}\" does not exist"
5143                    )));
5144                }
5145                // A relation outside any partition tree yields no rows (PG).
5146                let in_tree = cat
5147                    .get(name.as_ref())
5148                    .is_some_and(|t| t.schema().partition_role.is_some());
5149                let rows = if in_tree {
5150                    crate::partition_walks::ancestors_of(cat, name.as_ref())
5151                        .into_iter()
5152                        .map(|n| Row::new(alloc::vec![Value::text(n)]))
5153                        .collect()
5154                } else {
5155                    alloc::vec::Vec::new()
5156                };
5157                Ok((rows, cols))
5158            }
5159            // v7.39 (round 651) — `ts_debug(config, text)`: what the parser
5160            // saw, what each token was called, which dictionary took it
5161            // and what came out. It is a projection of the same tokenizer
5162            // and the same map the indexer uses, so it cannot describe a
5163            // pipeline other than the one that runs.
5164            "ts_debug" => {
5165                use crate::fts::{TokenType, TsDict};
5166                let cols = alloc::vec![
5167                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5168                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5169                    ColumnSchema::new("token".to_string(), DataType::Text, false),
5170                    ColumnSchema::new("dictionaries".to_string(), DataType::TextArray, false),
5171                    ColumnSchema::new("dictionary".to_string(), DataType::Text, true),
5172                    ColumnSchema::new("lexemes".to_string(), DataType::TextArray, true),
5173                ];
5174                // PG's one-arg form uses the session configuration; the
5175                // two-arg form names one.
5176                let (cfg_name, text) = match (&arg0, args.get(1)) {
5177                    (Some(Value::Text(c)), Some(t)) => {
5178                        let v = eval::eval_expr(t, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5179                        (c.to_string(), crate::eval::value_to_text(&v))
5180                    }
5181                    (Some(v), None) => (
5182                        alloc::string::String::from("english"),
5183                        crate::eval::value_to_text(v),
5184                    ),
5185                    _ => return Ok((alloc::vec::Vec::new(), cols)),
5186                };
5187                let english = match cfg_name
5188                    .trim()
5189                    .trim_start_matches("pg_catalog.")
5190                    .to_ascii_lowercase()
5191                    .as_str()
5192                {
5193                    "english" => true,
5194                    "simple" => false,
5195                    other => {
5196                        return Err(EngineError::Unsupported(alloc::format!(
5197                            "text search configuration \"{other}\" does not exist"
5198                        )));
5199                    }
5200                };
5201                let rows = crate::fts::tokenize_typed(&text)
5202                    .into_iter()
5203                    .map(|tok| {
5204                        let dict = tok.ty.dictionary(english);
5205                        let dname = dict.map(|d| match d {
5206                            TsDict::Simple => "simple",
5207                            TsDict::EnglishStem => "english_stem",
5208                        });
5209                        let folded = tok.text.to_lowercase();
5210                        let lexemes = dict.map(|d| match d {
5211                            TsDict::Simple => alloc::vec![Some(folded.clone())],
5212                            TsDict::EnglishStem => {
5213                                if crate::fts::is_english_stopword(&folded) {
5214                                    alloc::vec::Vec::new()
5215                                } else {
5216                                    alloc::vec![Some(crate::fts::porter_stem(&folded))]
5217                                }
5218                            }
5219                        });
5220                        Row::new(alloc::vec![
5221                            Value::text(tok.ty.alias()),
5222                            Value::text(tok.ty.description()),
5223                            Value::text(tok.text),
5224                            Value::TextArray(
5225                                dname
5226                                    .map(|n| alloc::vec![Some(alloc::string::String::from(n))])
5227                                    .unwrap_or_default(),
5228                            ),
5229                            dname.map_or(Value::Null, Value::text),
5230                            lexemes.map_or(Value::Null, Value::TextArray),
5231                        ])
5232                    })
5233                    .collect();
5234                let _ = TokenType::AsciiWord;
5235                Ok((rows, cols))
5236            }
5237            // v7.39 (round 651) — `ts_token_type('default')`, the list the
5238            // parser actually produces. It is a projection of the
5239            // `TokenType` enum the tokenizer and `pg_ts_config_map` both
5240            // read, so the three cannot disagree about what a token is.
5241            "ts_token_type" => {
5242                use crate::fts::TokenType as T;
5243                let cols = alloc::vec![
5244                    ColumnSchema::new("tokid".to_string(), DataType::Int, false),
5245                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5246                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5247                ];
5248                // PG takes the parser by name or oid; SPG has the one.
5249                if let Some(Value::Text(p)) = &arg0
5250                    && !p.eq_ignore_ascii_case("default")
5251                    && !p.eq_ignore_ascii_case("pg_catalog.default")
5252                {
5253                    return Err(EngineError::Unsupported(alloc::format!(
5254                        "text search parser \"{p}\" does not exist"
5255                    )));
5256                }
5257                const TYPES: &[T] = &[
5258                    T::AsciiWord,
5259                    T::Word,
5260                    T::NumWord,
5261                    T::Email,
5262                    T::Url,
5263                    T::Host,
5264                    T::SFloat,
5265                    T::Version,
5266                    T::HwordNumPart,
5267                    T::HwordPart,
5268                    T::HwordAsciiPart,
5269                    T::Blank,
5270                    T::Tag,
5271                    T::Protocol,
5272                    T::NumHword,
5273                    T::AsciiHword,
5274                    T::Hword,
5275                    T::UrlPath,
5276                    T::File,
5277                    T::Float,
5278                    T::Int,
5279                    T::Uint,
5280                    T::Entity,
5281                ];
5282                let rows = TYPES
5283                    .iter()
5284                    .map(|t| {
5285                        Row::new(alloc::vec![
5286                            Value::Int(*t as i32),
5287                            Value::text(t.alias()),
5288                            Value::text(t.description()),
5289                        ])
5290                    })
5291                    .collect();
5292                Ok((rows, cols))
5293            }
5294            // v7.39 (read01 round 65) — a set-returning USER function in FROM
5295            // (`FROM rows_of(2)`). Its body runs through the real executor, like
5296            // every other function body since round 63.
5297            other => {
5298                if !self.active_catalog().functions_named(other).is_empty() {
5299                    return self.exec_setof_user_function(other, args, primary.alias.as_deref());
5300                }
5301                Err(EngineError::Unsupported(alloc::format!(
5302                    "table function {other}() is not supported in FROM"
5303                )))
5304            }
5305        }
5306    }
5307
5308    /// v7.39 (read01 round 65) — run a `RETURNS SETOF <type>` / `RETURNS
5309    /// TABLE(…)` function in FROM position. The body is a SELECT; the arguments
5310    /// are bound into it as literals and it goes through the read path, so the
5311    /// rows it yields are exactly the rows a hand-written query would see.
5312    ///
5313    /// The column NAMES come from the declared shape: `RETURNS TABLE(id int, v
5314    /// text)` names them, and a `SETOF <scalar>` yields a single column named
5315    /// after the function — PG's rule, and what a bare `SELECT * FROM f()`
5316    /// shows.
5317    fn exec_setof_user_function(
5318        &self,
5319        name: &str,
5320        args: &[spg_sql::ast::Expr],
5321        // v7.39 (read01 round 65) — `FROM evens() AS x` names the single column
5322        // `x`: for a scalar SETOF, the table alias IS the column name (PG).
5323        alias: Option<&str>,
5324    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5325        // The call's arguments belong to the ENCLOSING query, so they are
5326        // evaluated here and the body sees values.
5327        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5328        let arg_ctx = self.ev_ctx(&empty, None);
5329        let dummy = Row::new(alloc::vec::Vec::new());
5330        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5331        for a in args {
5332            vals.push(eval::eval_expr(a, &dummy, &arg_ctx).map_err(EngineError::Eval)?);
5333        }
5334        self.setof_rows_of(name, &vals, alias)
5335    }
5336
5337    /// v7.39 (read01 round 67) — the set-returning core, on already-evaluated
5338    /// arguments. Shared by the FROM position and the target-list expansion, so
5339    /// a function cannot behave differently depending on where it is called.
5340    pub(crate) fn setof_rows_of(
5341        &self,
5342        name: &str,
5343        arg_values: &[Value<'static>],
5344        alias: Option<&str>,
5345    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5346        let cat = self.active_catalog();
5347        let overloads = cat.functions_named(name);
5348        let def = overloads
5349            .iter()
5350            .find(|f| spg_storage::function_arg_types(&f.args_repr).len() == arg_values.len())
5351            .ok_or_else(|| {
5352                EngineError::Unsupported(alloc::format!(
5353                    "function {name} does not exist with {} argument(s)",
5354                    arg_values.len()
5355                ))
5356            })?;
5357        let declared = def.returns.trim().to_string();
5358        let upper = declared.to_ascii_uppercase();
5359        if !upper.starts_with("SETOF") && !upper.starts_with("TABLE(") {
5360            return Err(EngineError::Unsupported(alloc::format!(
5361                "function {name}() does not return a set — it cannot be used in FROM"
5362            )));
5363        }
5364
5365        let arg_names_pl = spg_storage::function_arg_names(&def.args_repr);
5366        // v7.39 (read01 round 66) — a plpgsql SETOF body builds its rows with
5367        // RETURN NEXT / RETURN QUERY; the interpreter collects them.
5368        if def.language.eq_ignore_ascii_case("plpgsql") {
5369            let out_rows = self
5370                .call_plpgsql_setof_fn(def, &arg_names_pl, arg_values)
5371                .map_err(EngineError::Eval)?;
5372            let cols = setof_column_shape(&declared, name, alias, out_rows.first());
5373            let rows = out_rows.into_iter().map(Row::new).collect();
5374            return Ok((rows, cols));
5375        }
5376        let body = def.body.trim().trim_end_matches(';');
5377        let stmt = spg_sql::parser::parse_statement(body).map_err(|e| {
5378            EngineError::Unsupported(alloc::format!("function {name} body does not parse: {e}"))
5379        })?;
5380        let spg_sql::ast::Statement::Select(body_select) = stmt else {
5381            return Err(EngineError::Unsupported(alloc::format!(
5382                "function {name}(): a set-returning body must be a SELECT"
5383            )));
5384        };
5385        let arg_names = spg_storage::function_arg_names(&def.args_repr);
5386        let bound = crate::eval::bind_user_fn_args(
5387            self.active_catalog(),
5388            &body_select,
5389            &arg_names,
5390            arg_values,
5391        )
5392        .map_err(EngineError::Eval)?;
5393        let out = self.exec_select_cancel(&bound, crate::CancelToken::none())?;
5394        let QueryResult::Rows { columns, rows } = out else {
5395            return Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new()));
5396        };
5397        // Name the columns from the DECLARED shape — the same rule the plpgsql
5398        // path above uses, so a body's language cannot change the row shape.
5399        let cols = setof_column_shape_from(&declared, name, alias, &columns);
5400        Ok((rows, cols))
5401    }
5402
5403    fn exec_select_jsonb_each_text(
5404        &self,
5405        stmt: &SelectStatement,
5406        primary: &TableRef,
5407        cancel: CancelToken<'_>,
5408    ) -> Result<QueryResult, EngineError> {
5409        let (each_fn, arg_expr) = primary
5410            .jsonb_each_text_arg
5411            .as_ref()
5412            .map(|(name, expr)| (name.as_str(), expr.as_ref()))
5413            .expect("caller guards jsonb_each_text_arg.is_some()");
5414        // v7.37.17 (17.6 siblings) — the plain jsonb_each / json_each
5415        // forms keep JSON rendering in the value column (JSON null
5416        // stays jsonb 'null', strings keep their quotes).
5417        let as_text = each_fn.ends_with("_text");
5418        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5419        let ctx = EvalContext::new(&empty_schema, None);
5420        let dummy_row = Row::new(alloc::vec::Vec::new());
5421        let arg_value = eval::eval_expr(arg_expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5422        let pairs =
5423            crate::json::each_rows(&arg_value, as_text, each_fn).map_err(EngineError::Eval)?;
5424        let rows: alloc::vec::Vec<Row<'static>> = pairs
5425            .into_iter()
5426            .map(|(k, v)| {
5427                let key_val = Value::text(k);
5428                let value_val = match v {
5429                    Some(s) if as_text => Value::text(s),
5430                    Some(s) => Value::Json(alloc::borrow::Cow::Owned(s)),
5431                    None => Value::Null,
5432                };
5433                Row::new(alloc::vec![key_val, value_val])
5434            })
5435            .collect();
5436        let alias = primary.alias.clone().unwrap_or_else(|| each_fn.to_string());
5437        let value_dtype = if as_text {
5438            spg_storage::DataType::Text
5439        } else {
5440            spg_storage::DataType::Json
5441        };
5442        let key_col = ColumnSchema::new("key".to_string(), spg_storage::DataType::Text, false);
5443        let value_col = ColumnSchema::new("value".to_string(), value_dtype, as_text);
5444        let mut schema_cols = alloc::vec![key_col, value_col];
5445        // `AS t(k, v)` renames key/value positionally (PG behaviour); the
5446        // LATERAL-position form of the same call already honours it.
5447        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5448            if let Some(col) = schema_cols.get_mut(i) {
5449                col.name = new_name.clone();
5450            }
5451        }
5452        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
5453        // `EvalContext::new` drops it and every catalog-dependent cast
5454        // (regclass / enum / composite / domain) silently degrades.
5455        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
5456        // WHERE.
5457        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5458            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5459            for row in rows {
5460                cancel.check()?;
5461                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
5462                if matches!(v, Value::Bool(true)) {
5463                    out.push(row);
5464                }
5465            }
5466            out
5467        } else {
5468            rows
5469        };
5470        // Aggregate dispatch (e.g. SELECT COUNT(*) FROM jsonb_each_text…).
5471        if aggregate::uses_aggregate(stmt) {
5472            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5473            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5474                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5475                    .map_err(|err| match err {
5476                        EngineError::Eval(ev) => ev,
5477                        other => eval::EvalError::TypeMismatch {
5478                            detail: alloc::format!("{other}"),
5479                        },
5480                    })
5481            };
5482            // v7.39 (round 656) — hand the rows over as they are rather than
5483            // collecting a second vector of `RowRef` wrappers. Note this is
5484            // a set-returning-function path, NOT the relational scan: the
5485            // measured O(rows) cost lived in `run_single_table_aggregate`,
5486            // and converting these four first was a miss that cost a full
5487            // round — every test stayed green and the number did not move.
5488            let agg = aggregate::run(
5489                stmt,
5490                crate::join::AggRows::Owned(&filtered),
5491                &schema_cols,
5492                Some(&alias),
5493                Some(&agg_correlated),
5494                self.parallel_runner.0.as_deref(),
5495                Some(self.active_catalog()),
5496                Some(self),
5497            )?;
5498            return self.finish_agg_result(agg, stmt, cancel);
5499        }
5500        // Projection.
5501        let projection =
5502            build_projection(&stmt.items, &schema_cols, &alias, self.backslash_escapes)?;
5503        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5504            alloc::vec::Vec::with_capacity(filtered.len());
5505        for row in &filtered {
5506            let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5507            for p in &projection {
5508                let v = eval::eval_expr(&p.expr, row, &scan_ctx).map_err(EngineError::Eval)?;
5509                vals.push(v);
5510            }
5511            projected_rows.push(Row::new(vals));
5512        }
5513        let columns: alloc::vec::Vec<ColumnSchema> = projection
5514            .iter()
5515            // v7.39 (read01 round 54) — keep the column's enum identity through
5516            // the projection (it lives outside the DataType lattice), or a
5517            // derived table / UNION / windowed result forgets it and any outer
5518            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5519            .map(|p| {
5520                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
5521                c.user_enum_type = p.user_enum_type.clone();
5522                c.mysql_fsp = p.mysql_fsp;
5523                c
5524            })
5525            .collect();
5526        // ORDER BY.
5527        if !stmt.order_by.is_empty() {
5528            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = filtered
5529                .iter()
5530                .enumerate()
5531                .map(|(i, r)| -> Result<_, EngineError> {
5532                    let keys: Result<Vec<Value<'static>>, EngineError> = stmt
5533                        .order_by
5534                        .iter()
5535                        .map(|ob| {
5536                            eval::eval_expr(&ob.expr, r, &scan_ctx).map_err(EngineError::Eval)
5537                        })
5538                        .collect();
5539                    Ok((i, keys?))
5540                })
5541                .collect::<Result<_, _>>()?;
5542            indexed.sort_by(|a, b| {
5543                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5544                    let o = &stmt.order_by[idx];
5545                    let cmp = order_by_value_cmp_in(
5546                        o.desc,
5547                        o.nulls_first,
5548                        ka,
5549                        kb,
5550                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5551                    );
5552                    if cmp != core::cmp::Ordering::Equal {
5553                        return cmp;
5554                    }
5555                }
5556                core::cmp::Ordering::Equal
5557            });
5558            projected_rows = indexed
5559                .into_iter()
5560                .map(|(i, _)| projected_rows[i].clone())
5561                .collect();
5562        }
5563        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5564        if stmt.distinct {
5565            projected_rows = dedup_rows(projected_rows, scan_ctx.mysql_dialect);
5566        }
5567        if let Some(offset) = stmt.offset_literal() {
5568            let off = (offset as usize).min(projected_rows.len());
5569            projected_rows.drain(..off);
5570        }
5571        if let Some(limit) = stmt.limit_literal() {
5572            projected_rows.truncate(limit as usize);
5573        }
5574        Ok(QueryResult::Rows {
5575            columns,
5576            rows: projected_rows,
5577        })
5578    }
5579
5580    /// v7.37.17 (17.6 siblings) — execute `SELECT … FROM
5581    /// ( SELECT … ) alias` in primary position. The inner SELECT
5582    /// materialises once through the regular bare-select executor
5583    /// (UNION tails included), then the outer WHERE / aggregate /
5584    /// projection / ORDER BY / LIMIT pipeline runs over the
5585    /// synthetic table — the same post-materialisation shape as
5586    /// exec_select_jsonb_each_text, generalised to N columns.
5587    fn exec_select_derived(
5588        &self,
5589        stmt: &SelectStatement,
5590        primary: &TableRef,
5591        cancel: CancelToken<'_>,
5592    ) -> Result<QueryResult, EngineError> {
5593        let inner = primary
5594            .lateral_subquery
5595            .as_deref()
5596            .expect("caller guards lateral_subquery.is_some()");
5597        // exec_select_cancel is the union-aware wrapper — the inner
5598        // SELECT may carry UNION tails on stmt.unions.
5599        let QueryResult::Rows {
5600            columns: inner_cols,
5601            rows,
5602        } = self.exec_select_cancel(inner, cancel)?
5603        else {
5604            return Err(EngineError::Unsupported(
5605                "derived table subquery must return rows".into(),
5606            ));
5607        };
5608        let alias = primary
5609            .alias
5610            .clone()
5611            .unwrap_or_else(|| primary.name.clone());
5612        // `AS t(a, b)` renames the materialised columns positionally
5613        // (extra inner columns keep their own names, PG behaviour).
5614        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = inner_cols;
5615        // v7.39 (read01 round 78) — a column-alias list longer than the item is
5616        // the error PG reports; SPG used to let the extra names through and then
5617        // fail two layers downstream with "column not found: <the extra name>".
5618        let n_out = schema_cols.len() + usize::from(primary.with_ordinality);
5619        if primary.unnest_column_aliases.len() > n_out {
5620            return Err(EngineError::Unsupported(alloc::format!(
5621                "table \"{alias}\" has {n_out} columns available but {} columns specified",
5622                primary.unnest_column_aliases.len()
5623            )));
5624        }
5625        if primary.scalar_fn_item && schema_cols.len() == 1 {
5626            schema_cols[0].scalar_row_source = true;
5627        }
5628        // v7.39 (read01 round 78) — WITH ORDINALITY on a table function that
5629        // rides this channel (regexp_matches): a trailing bigint counter, 1-based.
5630        // The column-alias list, if given, names it like any other column.
5631        let mut rows = rows;
5632        if primary.with_ordinality {
5633            schema_cols.push(ColumnSchema::new(
5634                "ordinality".to_string(),
5635                DataType::BigInt,
5636                false,
5637            ));
5638            rows = rows
5639                .into_iter()
5640                .enumerate()
5641                .map(|(i, r)| {
5642                    let mut v = r.values;
5643                    #[allow(clippy::cast_possible_wrap)]
5644                    v.push(Value::BigInt(i as i64 + 1));
5645                    Row::new(v)
5646                })
5647                .collect();
5648        }
5649        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5650            if let Some(col) = schema_cols.get_mut(i) {
5651                col.name = new_name.clone();
5652            }
5653        }
5654        self.exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
5655    }
5656
5657    /// v7.39 (read01 partitionfuncs.c) — shared synthetic-source SELECT
5658    /// pipeline (WHERE / aggregate / projection / ORDER BY / DISTINCT /
5659    /// OFFSET / LIMIT) over a pre-materialised row set. Drives the
5660    /// derived-table executor and the FROM-position table functions.
5661    fn exec_select_over_rows(
5662        &self,
5663        stmt: &SelectStatement,
5664        rows: alloc::vec::Vec<Row<'static>>,
5665        schema_cols: alloc::vec::Vec<ColumnSchema>,
5666        alias: &str,
5667        cancel: CancelToken<'_>,
5668    ) -> Result<QueryResult, EngineError> {
5669        let scan_ctx = self.ev_ctx(&schema_cols, Some(alias));
5670        // v7.37 D.21 — correlated subqueries in the WHERE / projection may
5671        // reference this derived table's columns (`… WHERE u.gg = t.g` where t
5672        // is `(VALUES …) t`). Resolve them per-row via eval_expr_with_correlated
5673        // (the same path the aggregate branch uses); the old plain eval_expr let
5674        // a ScalarSubquery reach row-eval unresolved ("engine resolver bug").
5675        let corr_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5676        // WHERE.
5677        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5678            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5679            for row in rows {
5680                cancel.check()?;
5681                let v = self.eval_expr_with_correlated(
5682                    w,
5683                    &row,
5684                    &scan_ctx,
5685                    cancel,
5686                    Some(&mut corr_memo.borrow_mut()),
5687                )?;
5688                if matches!(v, Value::Bool(true)) {
5689                    out.push(row);
5690                }
5691            }
5692            out
5693        } else {
5694            rows
5695        };
5696        // Aggregate dispatch.
5697        if aggregate::uses_aggregate(stmt) {
5698            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5699            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5700                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5701                    .map_err(|err| match err {
5702                        EngineError::Eval(ev) => ev,
5703                        other => eval::EvalError::TypeMismatch {
5704                            detail: alloc::format!("{other}"),
5705                        },
5706                    })
5707            };
5708            // v7.39 (round 656) — hand the rows over as they are rather than
5709            // collecting a second vector of `RowRef` wrappers. Note this is
5710            // a set-returning-function path, NOT the relational scan: the
5711            // measured O(rows) cost lived in `run_single_table_aggregate`,
5712            // and converting these four first was a miss that cost a full
5713            // round — every test stayed green and the number did not move.
5714            let agg = aggregate::run(
5715                stmt,
5716                crate::join::AggRows::Owned(&filtered),
5717                &schema_cols,
5718                Some(alias),
5719                Some(&agg_correlated),
5720                self.parallel_runner.0.as_deref(),
5721                Some(self.active_catalog()),
5722                Some(self),
5723            )?;
5724            return self.finish_agg_result(agg, stmt, cancel);
5725        }
5726        // Projection.
5727        let projection =
5728            build_projection(&stmt.items, &schema_cols, alias, self.backslash_escapes)?;
5729        // v7.39 (round 621) — a target-list SRF expands here too. This tail
5730        // serves VALUES, a derived table and `ROWS FROM (…)`, and knew nothing
5731        // about them: `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4)) v(x)`
5732        // answered `function unnest(integer[]) does not exist` for a query PG
5733        // answers.
5734        let srf_idxs = self.srf_target_idxs(&projection);
5735        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5736        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5737            alloc::vec::Vec::with_capacity(filtered.len());
5738        if !srf_idxs.is_empty() {
5739            let (rows, src) =
5740                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
5741            projected_rows = rows;
5742            src_of_row = src;
5743        } else {
5744            for row in &filtered {
5745                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5746                for p in &projection {
5747                    let v = self.eval_expr_with_correlated(
5748                        &p.expr,
5749                        row,
5750                        &scan_ctx,
5751                        cancel,
5752                        Some(&mut corr_memo.borrow_mut()),
5753                    )?;
5754                    vals.push(v);
5755                }
5756                projected_rows.push(Row::new(vals));
5757            }
5758        }
5759        let columns: alloc::vec::Vec<ColumnSchema> = projection
5760            .iter()
5761            // v7.39 (read01 round 54) — keep the column's enum identity through
5762            // the projection (it lives outside the DataType lattice), or a
5763            // derived table / UNION / windowed result forgets it and any outer
5764            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5765            .map(|p| {
5766                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
5767                c.user_enum_type = p.user_enum_type.clone();
5768                c.mysql_fsp = p.mysql_fsp;
5769                c
5770            })
5771            .collect();
5772        // ORDER BY over the source rows (same shape as the other
5773        // synthetic-table executors).
5774        // v7.39 (read01 round 80) — a positional key (`ORDER BY 1`) means the Nth
5775        // OUTPUT column. Evaluated as an expression, as it was here, the literal
5776        // `1` is just the constant 1: the same sort key for every row, so the
5777        // sort ran and changed nothing. `SELECT unnest(ARRAY['B','a','A','b'])
5778        // ORDER BY 1` (which the parser turns into `SELECT * FROM unnest(…)`,
5779        // landing on this executor) came back in input order.
5780        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
5781        if !order_by.is_empty() {
5782            // v7.39 (round 621) — one entry per OUTPUT row, since a target-list
5783            // SRF makes more of them than there were inputs.
5784            let out_cols = if srf_idxs.is_empty() {
5785                alloc::vec![None; order_by.len()]
5786            } else {
5787                srf_order_output_cols(&order_by, &projection)
5788            };
5789            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
5790                .iter()
5791                .enumerate()
5792                .map(|(k, out)| -> Result<_, EngineError> {
5793                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
5794                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
5795                        .iter()
5796                        .zip(out_cols.iter())
5797                        .map(|(ob, oc)| {
5798                            // v7.39 (read01 round 54) — this path builds its
5799                            // sort keys itself instead of going through
5800                            // `build_order_keys`, so it skipped the enum-ordinal
5801                            // substitution: an OUTER `ORDER BY <enum col>` over
5802                            // a DERIVED TABLE sorted by the label TEXT, not by
5803                            // member order. Silently wrong rows, not an error.
5804                            let v = srf_order_key(ob, *oc, out, r, &scan_ctx)?;
5805                            Ok(
5806                                match crate::orderby::enum_order_ordinal(&ob.expr, &v, &scan_ctx) {
5807                                    Some(ord) => Value::Float(ord),
5808                                    None => v,
5809                                },
5810                            )
5811                        })
5812                        .collect();
5813                    Ok((k, keys?))
5814                })
5815                .collect::<Result<_, _>>()?;
5816            indexed.sort_by(|a, b| {
5817                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5818                    let o = &stmt.order_by[idx];
5819                    let cmp = order_by_value_cmp_in(
5820                        o.desc,
5821                        o.nulls_first,
5822                        ka,
5823                        kb,
5824                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5825                    );
5826                    if cmp != core::cmp::Ordering::Equal {
5827                        return cmp;
5828                    }
5829                }
5830                core::cmp::Ordering::Equal
5831            });
5832            projected_rows = indexed
5833                .into_iter()
5834                .map(|(i, _)| projected_rows[i].clone())
5835                .collect();
5836        }
5837        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5838        if stmt.distinct {
5839            projected_rows = dedup_rows(projected_rows, scan_ctx.mysql_dialect);
5840        }
5841        if let Some(offset) = stmt.offset_literal() {
5842            let off = (offset as usize).min(projected_rows.len());
5843            projected_rows.drain(..off);
5844        }
5845        if let Some(limit) = stmt.limit_literal() {
5846            projected_rows.truncate(limit as usize);
5847        }
5848        Ok(QueryResult::Rows {
5849            columns,
5850            rows: projected_rows,
5851        })
5852    }
5853
5854    /// Constant `SELECT` with no FROM: evaluate each projection item
5855    /// once against an empty dummy row (`SELECT 1`, `SELECT '7'::INT`).
5856    fn exec_constant_select(&self, stmt: &SelectStatement) -> Result<QueryResult, EngineError> {
5857        let empty_schema: Vec<ColumnSchema> = Vec::new();
5858        let ctx = self.ev_ctx(&empty_schema, None);
5859        // v7.39 (read01 round 106) — an aggregate with no FROM runs over the
5860        // single implicit row (`SELECT count(*)` → 1, `SELECT sum(5)` → 5,
5861        // `SELECT string_agg('x',',')` → x). Before this it fell through to the
5862        // scalar projection, where the aggregate name looked like an unknown
5863        // function. The WHERE filters that one row, so `… WHERE false` leaves
5864        // the aggregate zero input rows (`count(*)` → 0).
5865        if aggregate::uses_aggregate(stmt) {
5866            let dummy = Row::new(Vec::new());
5867            let passes = match &stmt.where_ {
5868                Some(w) => matches!(eval::eval_expr(w, &dummy, &ctx)?, Value::Bool(true)),
5869                None => true,
5870            };
5871            let rows: Vec<RowRef<'_>> = if passes {
5872                alloc::vec![RowRef::Owned(&dummy)]
5873            } else {
5874                Vec::new()
5875            };
5876            let agg = aggregate::run(
5877                stmt,
5878                crate::join::AggRows::Refs(&rows),
5879                &empty_schema,
5880                None,
5881                None,
5882                self.parallel_runner.0.as_deref(),
5883                Some(self.active_catalog()),
5884                Some(self),
5885            )?;
5886            return self.finish_agg_result(agg, stmt, CancelToken::none());
5887        }
5888        let projection = build_projection(&stmt.items, &empty_schema, "", self.backslash_escapes)?;
5889        // `SELECT … WHERE cond` with no FROM — the one conceptual
5890        // row survives only when the condition is true (previously
5891        // the WHERE was silently ignored: `SELECT 1 WHERE false`
5892        // returned a row).
5893        let dummy_row = Row::new(Vec::new());
5894        if let Some(w) = &stmt.where_ {
5895            let cond = eval::eval_expr(w, &dummy_row, &ctx)?;
5896            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
5897                let columns: Vec<ColumnSchema> = projection
5898                    .into_iter()
5899                    .map(|p| {
5900                        let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
5901                        c.user_enum_type = p.user_enum_type;
5902                        c.collation_name = p.collation_name;
5903                        c.mysql_fsp = p.mysql_fsp;
5904                        c
5905                    })
5906                    .collect();
5907                return Ok(QueryResult::Rows {
5908                    columns,
5909                    rows: Vec::new(),
5910                });
5911            }
5912        }
5913        // v7.38 (read01, T15) — a top-level SRF that the parser did NOT rewrite
5914        // into a FROM item (regexp_matches, whose rows are arrays and so cannot
5915        // desugar to unnest) expands here: one output row per SRF row, sibling
5916        // scalar columns repeated. unnest / array_elements / path_query reach a
5917        // real FROM via the parser rewrite and never land here.
5918        // v7.39 (read01 round 67) — every SRF in the list, in lockstep.
5919        let srf_idxs = self.srf_target_idxs(&projection);
5920        if !srf_idxs.is_empty() {
5921            let mut rows = expand_srf_row(self, &projection, &srf_idxs, &dummy_row, &ctx)?;
5922            let columns: Vec<ColumnSchema> = projection
5923                .into_iter()
5924                .map(|p| {
5925                    let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
5926                    c.user_enum_type = p.user_enum_type;
5927                    c.collation_name = p.collation_name;
5928                    c.mysql_fsp = p.mysql_fsp;
5929                    c
5930                })
5931                .collect();
5932            // v7.39 (read01 round 80) — a FROM-less SELECT still has an ORDER BY,
5933            // an OFFSET and a LIMIT, and they apply to the rows the SRF expanded
5934            // to. This returned straight out of the expansion, so
5935            // `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came back in
5936            // input order — the sort was not wrong, it never ran. (There is
5937            // exactly one conceptual input row here, which is why the ordinary
5938            // scan pipeline is not on this path at all.)
5939            if !stmt.order_by.is_empty() {
5940                let synth_ctx =
5941                    EvalContext::new(&columns, None).with_catalog(self.active_catalog());
5942                let resolved: Vec<spg_sql::ast::OrderBy> = stmt
5943                    .order_by
5944                    .iter()
5945                    .map(|o| {
5946                        let mut o = o.clone();
5947                        if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
5948                            && *n >= 1
5949                            && let Ok(idx) = usize::try_from(*n - 1)
5950                            && idx < columns.len()
5951                        {
5952                            o.expr = Expr::Column(spg_sql::ast::ColumnName {
5953                                qualifier: None,
5954                                name: columns[idx].name.clone(),
5955                            });
5956                        }
5957                        o
5958                    })
5959                    .collect();
5960                let descs: Vec<bool> = resolved.iter().map(|o| o.desc).collect();
5961                let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
5962                for r in rows {
5963                    let keys = build_order_keys(&resolved, &r, &synth_ctx)?;
5964                    tagged.push((keys, r));
5965                }
5966                sort_by_keys(&mut tagged, &descs);
5967                rows = tagged.into_iter().map(|(_, r)| r).collect();
5968            }
5969            apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
5970            return Ok(QueryResult::Rows { columns, rows });
5971        }
5972        let mut values = Vec::with_capacity(projection.len());
5973        for p in &projection {
5974            values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
5975        }
5976        let columns: Vec<ColumnSchema> = projection
5977            .into_iter()
5978            .map(|p| {
5979                let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
5980                c.user_enum_type = p.user_enum_type;
5981                c.collation_name = p.collation_name;
5982                c.mysql_fsp = p.mysql_fsp;
5983                c
5984            })
5985            .collect();
5986        // v7.39 (round 239) — the FROM-less scalar path ignored LIMIT and
5987        // OFFSET entirely, so `SELECT 1 LIMIT 0` returned its row where PG
5988        // returns none. (The SRF and aggregate arms above already applied
5989        // them; this tail was the one that didn't.)
5990        let mut rows = alloc::vec![Row::new(values)];
5991        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
5992        Ok(QueryResult::Rows { columns, rows })
5993    }
5994
5995    /// v7.37.x (docker-fair INSUBQ attack) — pre-replacement short-
5996    /// circuit. Catches
5997    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
5998    /// BEFORE `resolve_select_subqueries` materialises the inner result
5999    /// as `Vec<Expr::Literal>`. Runs the inner once, collects the
6000    /// values into a `HashSet<i64>` directly, then probes A.pk per
6001    /// HashSet entry and tallies. Saves the Expr-literal roundtrip
6002    /// (~150 µs / query at INSUBQ benchmark scale).
6003    pub(crate) fn try_count_star_pk_in_subquery_fast(
6004        &self,
6005        stmt: &SelectStatement,
6006        cancel: CancelToken<'_>,
6007    ) -> Result<Option<QueryResult>, EngineError> {
6008        use spg_sql::ast::SelectItem;
6009        if stmt.distinct
6010            || stmt.limit_with_ties
6011            || stmt.group_by.is_some()
6012            || stmt.having.is_some()
6013            || !stmt.unions.is_empty()
6014            || !stmt.order_by.is_empty()
6015            || stmt.limit.is_some()
6016            || stmt.offset.is_some()
6017            || stmt.items.len() != 1
6018        {
6019            return Ok(None);
6020        }
6021        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6022            return Ok(None);
6023        };
6024        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6025            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6026        if !is_count_star {
6027            return Ok(None);
6028        }
6029        let Some(from) = stmt.from.as_ref() else {
6030            return Ok(None);
6031        };
6032        if !from.joins.is_empty()
6033            || from.primary.lateral_subquery.is_some()
6034            || from.primary.unnest_expr.is_some()
6035            || from.primary.generate_series_args.is_some()
6036            || from.primary.table_fn_call.is_some()
6037            || from.primary.as_of_segment.is_some()
6038        {
6039            return Ok(None);
6040        }
6041        let Some(where_expr) = stmt.where_.as_ref() else {
6042            return Ok(None);
6043        };
6044        // The WHERE conjunct must be a bare `<col> IN (subquery)` with
6045        // negated=false; no other predicates.
6046        let Expr::InSubquery {
6047            expr: col_expr,
6048            subquery,
6049            negated: false,
6050        } = where_expr
6051        else {
6052            return Ok(None);
6053        };
6054        let Expr::Column(c) = col_expr.as_ref() else {
6055            return Ok(None);
6056        };
6057        let outer_alias = from
6058            .primary
6059            .alias
6060            .as_deref()
6061            .unwrap_or(from.primary.name.as_str());
6062        if let Some(q) = c.qualifier.as_deref()
6063            && !q.eq_ignore_ascii_case(outer_alias)
6064        {
6065            return Ok(None);
6066        }
6067        // Outer column must be a single-column PK on integer family.
6068        let catalog = self.active_catalog();
6069        let Some(outer_table) = catalog.get(from.primary.name.as_str()) else {
6070            return Ok(None);
6071        };
6072        let outer_schema = outer_table.schema();
6073        let Some(outer_pos) = outer_schema
6074            .columns
6075            .iter()
6076            .position(|s| s.name.eq_ignore_ascii_case(&c.name))
6077        else {
6078            return Ok(None);
6079        };
6080        if !matches!(
6081            outer_schema.columns[outer_pos].ty,
6082            spg_storage::DataType::BigInt
6083                | spg_storage::DataType::Int
6084                | spg_storage::DataType::SmallInt
6085        ) {
6086            return Ok(None);
6087        }
6088        if !outer_schema
6089            .uniqueness_constraints
6090            .iter()
6091            .any(|u| u.is_primary_key && u.columns.as_slice() == [outer_pos])
6092        {
6093            return Ok(None);
6094        }
6095        let Some(idx) = outer_table.index_on(outer_pos) else {
6096            return Ok(None);
6097        };
6098        // Inner must be uncorrelated. The cheap-correlation pre-check
6099        // exists upstream; here we just attempt the bare exec.
6100        if crate::subquery::select_is_correlated(subquery) {
6101            return Ok(None);
6102        }
6103        let mut inner = (**subquery).clone();
6104        self.resolve_select_subqueries(&mut inner, cancel)?;
6105        let r = match self.exec_bare_select_cancel(&inner, cancel) {
6106            Ok(r) => r,
6107            Err(_) => return Ok(None),
6108        };
6109        let QueryResult::Rows { columns, rows, .. } = r else {
6110            return Ok(None);
6111        };
6112        if columns.len() != 1 {
6113            return Ok(None);
6114        }
6115        // v7.37.43 (INSUBQ B-1) — inner-uniqueness check. If the inner
6116        // subquery projects a column known to be UNIQUE/PK on its table
6117        // (statically: `SELECT <col> FROM <tbl> WHERE …` where <col> is
6118        // in `tbl.uniqueness_constraints`), survivor values are
6119        // guaranteed distinct and the per-survivor `HashSet::insert`
6120        // dedup check is redundant. ~25 ns × N_inner-survivors saved.
6121        //
6122        // Inlined check — gated on: no DISTINCT/GROUP/UNION/JOIN, single
6123        // projection that is a bare Column ref, table-column lookup in
6124        // catalog confirms the column appears as a unique constraint's
6125        // sole member. UNIQUE NOT NULL is required — a nullable unique
6126        // column may have multiple NULLs, but NULLs are already skipped
6127        // above (`Value::Null => continue`), so a UNIQUE-only column is
6128        // still safe to dedup-skip.
6129        let inner_unique = (|| -> bool {
6130            if inner.distinct
6131                || inner.group_by.is_some()
6132                || !inner.unions.is_empty()
6133                || inner.having.is_some()
6134                || inner.items.len() != 1
6135            {
6136                return false;
6137            }
6138            let Some(inner_from) = inner.from.as_ref() else {
6139                return false;
6140            };
6141            if !inner_from.joins.is_empty()
6142                || inner_from.primary.lateral_subquery.is_some()
6143                || inner_from.primary.unnest_expr.is_some()
6144                || inner_from.primary.generate_series_args.is_some()
6145                || inner_from.primary.table_fn_call.is_some()
6146            {
6147                return false;
6148            }
6149            let SelectItem::Expr { expr: proj, .. } = &inner.items[0] else {
6150                return false;
6151            };
6152            let Expr::Column(pc) = proj else {
6153                return false;
6154            };
6155            let inner_alias = inner_from
6156                .primary
6157                .alias
6158                .as_deref()
6159                .unwrap_or(inner_from.primary.name.as_str());
6160            if let Some(q) = pc.qualifier.as_deref()
6161                && !q.eq_ignore_ascii_case(inner_alias)
6162            {
6163                return false;
6164            }
6165            let Some(inner_table) = catalog.get(inner_from.primary.name.as_str()) else {
6166                return false;
6167            };
6168            let isch = inner_table.schema();
6169            let Some(ipos) = isch
6170                .columns
6171                .iter()
6172                .position(|s| s.name.eq_ignore_ascii_case(&pc.name))
6173            else {
6174                return false;
6175            };
6176            isch.uniqueness_constraints
6177                .iter()
6178                .any(|u| u.columns.as_slice() == [ipos])
6179        })();
6180        // Collect inner i64 values directly into a HashSet, then probe.
6181        let mut count: i64 = 0;
6182        let mut probed = if inner_unique {
6183            hashbrown::HashSet::<i64>::new()
6184        } else {
6185            hashbrown::HashSet::<i64>::with_capacity(rows.len())
6186        };
6187        for row in &rows {
6188            let v = row.values.first().cloned().unwrap_or(Value::Null);
6189            let n = match v {
6190                Value::BigInt(n) => n,
6191                Value::Int(n) => i64::from(n),
6192                Value::SmallInt(n) => i64::from(n),
6193                Value::Null => continue,
6194                _ => return Ok(None),
6195            };
6196            // De-duplicate inner key set so a duplicate inner value
6197            // doesn't double-count the same outer row. Skipped when
6198            // the inner projection is statically unique.
6199            if !inner_unique && !probed.insert(n) {
6200                continue;
6201            }
6202            // v7.37.43 (INSUBQ B-2 + B-4) — direct i64 PK probe, skipping
6203            // the `IndexKey::from_value` enum-dispatch and the per-call
6204            // `IndexKey` wrapper construction. The outer column is
6205            // already gated to integer-family above, so an i64 key
6206            // always corresponds to a valid PK lookup.
6207            if !idx.lookup_eq_i64(n).is_empty() {
6208                count += 1;
6209            }
6210        }
6211        let columns_out = alloc::vec![ColumnSchema::new(
6212            "count".to_string(),
6213            spg_storage::DataType::BigInt,
6214            false,
6215        )];
6216        let rows_out = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6217        Ok(Some(QueryResult::Rows {
6218            columns: columns_out,
6219            rows: rows_out,
6220        }))
6221    }
6222
6223    /// v7.37.x (docker-fair INSUBQ attack) — short-circuit
6224    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (literal list)
6225    /// (the post-subquery-replacement shape of the INSUBQ probe
6226    /// `SELECT COUNT(*) FROM A WHERE A.pk IN (SELECT k FROM B WHERE …)`).
6227    /// The general aggregate path materialises every seeked row into
6228    /// a `Vec<Cow<Row>>`, then runs the aggregate executor over it.
6229    /// For COUNT(*) we only care how many keys hit; iterate the list
6230    /// and tally `idx.lookup_eq(key)` non-empty results, skipping the
6231    /// row materialisation, the aggregate state machine, and the per-
6232    /// row WHERE re-eval (the seek already filtered by the same list).
6233    /// Returns `None` when the shape doesn't match.
6234    fn try_count_star_pk_in_list_fast(
6235        &self,
6236        stmt: &SelectStatement,
6237        table: &spg_storage::Table,
6238        schema_cols: &[ColumnSchema],
6239        alias: &str,
6240    ) -> Option<QueryResult> {
6241        use spg_sql::ast::{ColumnName, SelectItem};
6242        // Gates on the SELECT shape.
6243        if stmt.distinct
6244            || stmt.limit_with_ties
6245            || stmt.group_by.is_some()
6246            || stmt.having.is_some()
6247            || !stmt.unions.is_empty()
6248            || !stmt.order_by.is_empty()
6249            || stmt.limit.is_some()
6250            || stmt.offset.is_some()
6251            || stmt.items.len() != 1
6252        {
6253            return None;
6254        }
6255        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6256            return None;
6257        };
6258        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6259            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6260        if !is_count_star {
6261            return None;
6262        }
6263        // WHERE must be `<col> IN (literal list)` with no other
6264        // conjuncts (the seek result is a true subset of the row
6265        // population for this predicate).
6266        let where_expr = stmt.where_.as_ref()?;
6267        let Expr::InList {
6268            expr: col_expr,
6269            list,
6270            negated: false,
6271        } = where_expr
6272        else {
6273            return None;
6274        };
6275        let Expr::Column(c) = col_expr.as_ref() else {
6276            return None;
6277        };
6278        if let Some(q) = c.qualifier.as_deref()
6279            && !q.eq_ignore_ascii_case(alias)
6280        {
6281            return None;
6282        }
6283        let col_pos = schema_cols
6284            .iter()
6285            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
6286        // The column must be a single-column PK on an integer family
6287        // — the same gate the SCALARSQ + LEFT-ANTI-JOIN fast paths use,
6288        // so the antiset stays collision-free under `HashSet<i64>`.
6289        let schema = table.schema();
6290        if !matches!(
6291            schema.columns[col_pos].ty,
6292            spg_storage::DataType::BigInt
6293                | spg_storage::DataType::Int
6294                | spg_storage::DataType::SmallInt
6295        ) {
6296            return None;
6297        }
6298        if !schema
6299            .uniqueness_constraints
6300            .iter()
6301            .any(|u| u.is_primary_key && u.columns.as_slice() == [col_pos])
6302        {
6303            return None;
6304        }
6305        let idx = table.index_on(col_pos)?;
6306        // Tally non-empty seek results across all literal values.
6307        let mut count: i64 = 0;
6308        for lit in list {
6309            let Expr::Literal(l) = lit else {
6310                return None;
6311            };
6312            // r1039 — through the shared resolver, so a literal spelled
6313            // in another type ('5' against an integer PK) is read as the
6314            // column's before it becomes a key. This tally answers from
6315            // the index alone, so a key in the wrong space would return a
6316            // COUNT of zero rather than fall back to a scan.
6317            let col = schema.columns.get(col_pos)?;
6318            let v = crate::index_access::literal_as_column_value(l, col, col_pos)?;
6319            let key = spg_storage::IndexKey::from_value_for_column(&v, col.ty)?;
6320            if !idx.lookup_eq(&key).is_empty() {
6321                count += 1;
6322            }
6323        }
6324        let columns = alloc::vec![ColumnSchema::new(
6325            "count".to_string(),
6326            spg_storage::DataType::BigInt,
6327            false,
6328        )];
6329        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6330        let _ = ColumnName {
6331            qualifier: None,
6332            name: String::new(),
6333        };
6334        Some(QueryResult::Rows { columns, rows })
6335    }
6336
6337    /// v7.38 (perf, exact-range count) — `SELECT count(*) FROM t WHERE <col>
6338    /// BETWEEN a AND b` on an indexed column. The index range walk yields
6339    /// exactly the matching (visible) rows, so we count locators directly —
6340    /// skipping the row materialisation, the aggregate state machine, and the
6341    /// per-row WHERE re-eval the general path pays. Turns the `range_count`
6342    /// endpoint from tied-with-PG (superset re-eval) into a clear win. None
6343    /// when the shape doesn't match.
6344    fn try_count_star_indexed_range_fast(
6345        &self,
6346        stmt: &SelectStatement,
6347        table: &spg_storage::Table,
6348        schema_cols: &[ColumnSchema],
6349        alias: &str,
6350        snapshot: &spg_storage::snapshot::Snapshot,
6351    ) -> Option<QueryResult> {
6352        use spg_sql::ast::SelectItem;
6353        if stmt.distinct
6354            || stmt.limit_with_ties
6355            || stmt.group_by.is_some()
6356            || stmt.having.is_some()
6357            || !stmt.unions.is_empty()
6358            || !stmt.order_by.is_empty()
6359            || stmt.limit.is_some()
6360            || stmt.offset.is_some()
6361            || stmt.items.len() != 1
6362        {
6363            return None;
6364        }
6365        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6366            return None;
6367        };
6368        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6369            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6370        if !is_count_star {
6371            return None;
6372        }
6373        let where_expr = stmt.where_.as_ref()?;
6374        let count =
6375            crate::index_access::try_range_count(where_expr, schema_cols, table, alias, snapshot)?;
6376        let columns = alloc::vec![ColumnSchema::new(
6377            "count".to_string(),
6378            spg_storage::DataType::BigInt,
6379            false,
6380        )];
6381        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6382        Some(QueryResult::Rows { columns, rows })
6383    }
6384
6385    /// Single-table aggregate path: filter the (optionally index-seeked)
6386    /// rows, then hand off to the aggregate executor which does its own
6387    /// projection + ORDER BY before `finish_agg_result` applies LIMIT.
6388    fn run_single_table_aggregate<'a>(
6389        &self,
6390        stmt: &SelectStatement,
6391        table: &'a spg_storage::Table,
6392        schema_cols: &'a [ColumnSchema],
6393        alias: &str,
6394        indexed_rows: Option<Vec<Cow<'a, Row<'static>>>>,
6395        cancel: CancelToken<'_>,
6396    ) -> Result<QueryResult, EngineError> {
6397        // v7.38 (read01 U15) — per-scan sampler cell for TABLESAMPLE
6398        // REPEATABLE (see run_single_table_scan). Aggregates
6399        // (`count(*) FROM t TABLESAMPLE …`) filter through this ctx too.
6400        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6401        let ctx = self
6402            .ev_ctx(schema_cols, Some(alias))
6403            .with_sample_rng(&sample_cell);
6404        // v7.39 (round 657) — pre-sized. Pushing 500k pointers into a
6405        // `Vec::new()` walks the doubling chain 8, 16, … 262144, 524288,
6406        // and every abandoned buffer on the way stays resident: RSS is a
6407        // high-water mark, so the intermediates are paid for even though
6408        // they are freed. Round 656 measured the scan at 17 bytes/row
6409        // where the survivor list itself only needs 8.
6410        let mut filtered: Vec<&Row<'static>> = if stmt.where_.is_none() {
6411            Vec::with_capacity(table.rows().len())
6412        } else {
6413            // With a WHERE, the row count is an UPPER bound and reserving it
6414            // is the worse trade: `… WHERE id = 5` over 50M rows would take
6415            // 400 MB of pointers to hold one survivor. Let it grow.
6416            Vec::new()
6417        };
6418        // v6.2.6 — Memoize: per-query LRU cache for correlated
6419        // scalar subqueries. Fresh per row-loop entry so each
6420        // SELECT execution gets an isolated cache.
6421        let mut memo = memoize::MemoizeCache::new();
6422        // v7.37 (perf) — single-table aggregate's WHERE filter
6423        // pre-7.37 ran the slow tree-walker (`eval_expr_with_
6424        // correlated`) per row, even for subquery-free WHEREs that
6425        // the single-table SCAN path has compiled since v7.32
6426        // (perf knife D). The asymmetry meant a fold-to-filter
6427        // rewrite (joinfold) that swapped a JOIN for a single-table
6428        // aggregate over a compiled WHERE saw the tree-walker
6429        // instead — 25 k rows × `m.mailbox_id IN (25 lits)` cost
6430        // ~9 ms via the walker, vs ~1 ms via the compiled InSet
6431        // step. Compile once if eligible; fall back to the walker
6432        // for subquery-bearing or non-compilable WHEREs.
6433        let compiled_where: Option<eval::CompiledExpr> = stmt
6434            .where_
6435            .as_ref()
6436            .filter(|w| eval::fully_compilable(w))
6437            .map(|w| eval::compile_expr(w, &ctx));
6438        let mut eval_stack: Vec<Value<'static>> = Vec::new();
6439        let mut row_passes_where = |row: &Row<'static>,
6440                                    eval_stack: &mut Vec<Value<'static>>,
6441                                    memo: &mut memoize::MemoizeCache|
6442         -> Result<bool, EngineError> {
6443            match (&compiled_where, &stmt.where_) {
6444                (Some(cw), _) => {
6445                    // v7.39 (round 479) — the predicate wants a bool, not a
6446                    // Value. The owned entry ended in `Value::into_owned`
6447                    // and the caller then dropped it, once per row; round
6448                    // 478's profile put that pair above the comparison
6449                    // itself.
6450                    Ok(eval::compiled::eval_compiled_pred(
6451                        cw,
6452                        row,
6453                        &ctx,
6454                        eval_stack,
6455                        ctx.mysql_dialect,
6456                    )
6457                    .map_err(EngineError::Eval)?)
6458                }
6459                (None, Some(w)) => {
6460                    let cond = self.eval_expr_with_correlated(w, row, &ctx, cancel, Some(memo))?;
6461                    Ok(crate::eval::predicate_is_true(
6462                        &cond,
6463                        "WHERE",
6464                        ctx.mysql_dialect,
6465                    )?)
6466                }
6467                (None, None) => Ok(true),
6468            }
6469        };
6470        if let Some(rows) = &indexed_rows {
6471            for cow in rows {
6472                let row = cow.as_ref();
6473                if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6474                    continue;
6475                }
6476                filtered.push(row);
6477            }
6478        }
6479        // v7.36 (cold-tier coverage) — single-table aggregate's
6480        // non-indexed full scan was hot-only and silently lost cold
6481        // rows on COUNT/SUM/etc. Materialise cold rows once into
6482        // `cold_rows_storage` (Vec<Row<'static>>) so the `filtered: Vec<&Row<'static>>`
6483        // shape stays unchanged; the cold rows live until the end of
6484        // the aggregate run.
6485        let cold_rows_storage = if indexed_rows.is_none() {
6486            self.iter_cold_rows_of_table(table)
6487        } else {
6488            Vec::new()
6489        };
6490        if indexed_rows.is_none() {
6491            // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
6492            // single-table aggregate full-scan path. Mirrors the gate on
6493            // `run_single_table_scan`: this is a user-query result path,
6494            // so under gate-on (`SPG_MVCC_INPLACE`) it must skip rows the
6495            // reader's snapshot cannot see (e.g. tombstoned versions),
6496            // otherwise COUNT/SUM/etc. would tally dead rows. A no-op
6497            // under the default gate-off: every hot row is frozen or
6498            // committed-and-alive, so `is_row_visible` returns true.
6499            // Cold-tier rows are frozen (visible) by definition — left
6500            // ungated, matching the plain-scan path.
6501            let scan_snapshot = self.current_snapshot();
6502            // v7.39 (pg_stat knife B) — this full-scan branch walks
6503            // headers directly (serial and sharded alike); count the
6504            // sequential scan here.
6505            table.note_seq_scan();
6506            // v7.39 (parallel-agg P2) — the visibility probe + WHERE
6507            // filter dominate the pre-aggregate wall time on big
6508            // scans (P1's ground truth: accumulation is only ~17%).
6509            // Shard THAT work when the host injected an executor and
6510            // the WHERE is compiled (the compiled evaluator is pure
6511            // over &row; the tree-walker fallback can hit correlated
6512            // subqueries and stays serial). Shards return surviving
6513            // ROW INDICES — &Row can't cross the Box<dyn Any>'s
6514            // 'static bound — and the main thread only dereferences.
6515            let n = table.row_count();
6516            let par = self.parallel_runner.0.as_deref().filter(|_| {
6517                n >= crate::PARALLEL_MIN_ROWS && (stmt.where_.is_none() || compiled_where.is_some())
6518            });
6519            if let Some(r) = par {
6520                let n_shards = (n / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
6521                let chunk = n.div_ceil(n_shards);
6522                type ShardOut = Result<alloc::vec::Vec<usize>, EngineError>;
6523                let cw = &compiled_where;
6524                let snap_ref = &scan_snapshot;
6525                let results = r.run_shards(n_shards, &|s| {
6526                    let lo = s * chunk;
6527                    let hi = ((s + 1) * chunk).min(n);
6528                    let mut keep: alloc::vec::Vec<usize> = alloc::vec::Vec::with_capacity(hi - lo);
6529                    // EvalContext carries Cells (sampler / row counters)
6530                    // and is !Sync — each shard builds its own from the
6531                    // same Sync inputs. The compiled WHERE is gated to
6532                    // the pure-scalar whitelist, which reads none of the
6533                    // session state the engine-built ctx would add
6534                    // (TABLESAMPLE's __tsm_fract is not whitelisted, so
6535                    // sampled scans never take this branch).
6536                    let shard_ctx = EvalContext::new(schema_cols, Some(alias));
6537                    let mut stack: Vec<Value<'static>> = Vec::new();
6538                    let out: ShardOut = (|| {
6539                        for i in lo..hi {
6540                            if !table.is_row_visible(i, snap_ref) {
6541                                continue;
6542                            }
6543                            let row = &table.rows()[i];
6544                            // v7.39 (round 480) — the parallel full-scan
6545                            // shard is the path the aggregate benchmark
6546                            // actually takes, and it was still on the OWNED
6547                            // entry: round 480's profile attributed 68.7 %
6548                            // of `drop_glue<Value>` to this closure, which
6549                            // is why round 479's fix to the indexed path
6550                            // barely moved the total.
6551                            //
6552                            // The `matches!(…, Value::Bool(true))` form was
6553                            // also a narrower reading than the rest of the
6554                            // engine uses — `predicate_is_true` is what
6555                            // handles NULL and MySQL truthiness — so the
6556                            // bool entry fixes the shape as well as the cost.
6557                            let pass = match cw {
6558                                Some(c) => eval::compiled::eval_compiled_pred(
6559                                    c,
6560                                    row,
6561                                    &shard_ctx,
6562                                    &mut stack,
6563                                    shard_ctx.mysql_dialect,
6564                                )
6565                                .map_err(EngineError::Eval)?,
6566                                None => true,
6567                            };
6568                            if pass {
6569                                keep.push(i);
6570                            }
6571                        }
6572                        Ok(keep)
6573                    })();
6574                    alloc::boxed::Box::new(out)
6575                });
6576                // v7.39 (round 567) — `rows()` is a 32-way trie, so
6577                // indexing it is four dependent loads and a scan that
6578                // reads every row paid them every row. A profile of
6579                // `SELECT sum(id)` over 500k rows put 37.8% of the
6580                // connection thread's CPU on THIS ONE LINE. The cursor
6581                // holds the leaf, making that one descent per 32.
6582                let mut rows_cur = table.rows().run_cursor();
6583                for boxed in results {
6584                    let shard = boxed
6585                        .downcast::<ShardOut>()
6586                        .expect("runner echoes the closure's box");
6587                    for i in (*shard)? {
6588                        if let Some(row) = rows_cur.get(i) {
6589                            filtered.push(row);
6590                        }
6591                    }
6592                }
6593            } else {
6594                let mut rows_cur = table.rows().run_cursor();
6595                for i in 0..n {
6596                    if !table.is_row_visible(i, &scan_snapshot) {
6597                        continue;
6598                    }
6599                    let Some(row) = rows_cur.get(i) else { continue };
6600                    if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6601                        continue;
6602                    }
6603                    filtered.push(row);
6604                }
6605            }
6606            for row in &cold_rows_storage {
6607                if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6608                    continue;
6609                }
6610                filtered.push(row);
6611            }
6612        }
6613        // v7.29 — a per-query memo so correlated scalar
6614        // subqueries batch-evaluate once (group map) instead of
6615        // executing per group.
6616        let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
6617        let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
6618            self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
6619                .map_err(|err| match err {
6620                    EngineError::Eval(ev) => ev,
6621                    other => eval::EvalError::TypeMismatch {
6622                        detail: alloc::format!("{other}"),
6623                    },
6624                })
6625        };
6626        // v7.39 (round 656) — the plain relational scan. This collect() was
6627        // the measured defect: one 64-byte `RowRef` per surviving row to
6628        // wrap an 8-byte pointer `filtered` already holds. Scalar
6629        // aggregates measured ~81 bytes/row of working memory because of
6630        // it — 40 MB at 500k rows, 3.2 GB at 50M, for a query that returns
6631        // one number. `AggRows::Ptrs` reads the pointers directly.
6632        let agg = aggregate::run(
6633            stmt,
6634            crate::join::AggRows::Ptrs(&filtered),
6635            schema_cols,
6636            Some(alias),
6637            Some(&agg_correlated),
6638            self.parallel_runner.0.as_deref(),
6639            Some(self.active_catalog()),
6640            Some(self),
6641        )?;
6642        self.finish_agg_result(agg, stmt, cancel)
6643    }
6644
6645    /// Single-table scan + projection path: WHERE filter (compiled when
6646    /// subquery-free), ORDER BY keying, SRF expansion / projection, then
6647    /// sort + WITH TIES / DISTINCT / OFFSET-LIMIT.
6648    fn run_single_table_scan<'a>(
6649        &self,
6650        stmt: &SelectStatement,
6651        table: &'a spg_storage::Table,
6652        schema_cols: &'a [ColumnSchema],
6653        alias: &str,
6654        indexed_rows: Option<Vec<Cow<'a, Row<'static>>>>,
6655        cancel: CancelToken<'_>,
6656    ) -> Result<QueryResult, EngineError> {
6657        // v7.38 (read01 U15) — a fresh per-scan sampler cell for
6658        // `TABLESAMPLE … REPEATABLE(seed)`. Created before the ctx so the
6659        // deterministic `__tsm_fract(seed)` draws share one scan-local
6660        // state (isolated from the global random() PRNG); a fresh cell per
6661        // scan makes a repeat / rescan reproduce the same sample. Unused
6662        // and cheap when the query carries no sample.
6663        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6664        let ctx = self
6665            .ev_ctx(schema_cols, Some(alias))
6666            .with_sample_rng(&sample_cell);
6667        let projection = build_projection(&stmt.items, schema_cols, alias, self.backslash_escapes)?;
6668        // v7.19 P5 — single-table SELECT path for SRF
6669        // `SELECT unnest(arr) FROM t` shape. Detect a top-level
6670        // unnest in the projection list. When present, the
6671        // per-row processor emits one output row per array
6672        // element (broadcasting non-SRF projections from the
6673        // same input row). Empty / NULL arrays emit zero rows
6674        // for that input — PG semantics.
6675        // v7.39 (read01 round 67) — every SRF in the target list, in lockstep.
6676        let srf_idxs = self.srf_target_idxs(&projection);
6677        let srf_position = srf_idxs.first().copied();
6678        // v7.39 (round 599) — the SRF analysis is per QUERY, not per row.
6679        let mut srf_plan = if srf_position.is_some() {
6680            Some(build_srf_plan(self, &projection, &srf_idxs, &ctx)?)
6681        } else {
6682            None
6683        };
6684
6685        // Materialise the filter pass into `(order_key, projected_row)`
6686        // tuples. The order key is `None` when there's no ORDER BY clause.
6687        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
6688        // v7.33 (C1, ceiling-first/never-die) — charge each accumulated
6689        // output row to the per-query byte budget as it is built, so a
6690        // fat single-table scan / sort REJECTS with QueryBytesExceeded
6691        // at ~the ceiling instead of materialising the whole table and
6692        // only noticing at the final enforce_row_limit check. Without
6693        // this, N concurrent fat scans peak at N×table and OOM the host.
6694        // `max_query_bytes = None` (the embedded default) = no ceiling,
6695        // so existing unbudgeted behaviour is byte-identical.
6696        let mut budget = ByteBudget::new(self.max_query_bytes);
6697        // v6.2.6 — Memoize per-row WHERE eval shares one cache.
6698        let mut memo = memoize::MemoizeCache::new();
6699        // v7.32 (perf knife D) — subquery-free WHERE compiles once;
6700        // the row loop then runs a flat step program instead of a
6701        // tree interpretation per row.
6702        let compiled_where: Option<eval::CompiledExpr> = stmt
6703            .where_
6704            .as_ref()
6705            .filter(|w| eval::fully_compilable(w))
6706            .map(|w| eval::compile_expr(w, &ctx));
6707        let mut eval_stack: Vec<Value<'static>> = Vec::new();
6708        // v7.37.x (docker-fair SCALARSQ attack) — pre-analyse every
6709        // SELECT-item scalar subquery for the PK-probe fast path. The
6710        // analysis (gate checks + catalog lookups) takes ~500 ns; doing
6711        // it once per query instead of once per row × 100 rows saves
6712        // ~50 µs and lets the per-row evaluation reduce to a single
6713        // index probe + outer-column read.
6714        let scalarsq_fast: Vec<Option<crate::ScalarPkProbeFastPath>> = projection
6715            .iter()
6716            .map(|p| {
6717                if let Expr::ScalarSubquery(inner) = &p.expr {
6718                    self.analyse_scalar_count_pk_eq_probe(inner, schema_cols, alias)
6719                } else {
6720                    None
6721                }
6722            })
6723            .collect();
6724        let any_scalarsq_fast = scalarsq_fast.iter().any(Option::is_some);
6725        // v7.39 (round 487) — a projection item that is a bare column
6726        // reference binds its position ONCE per query.
6727        //
6728        // Per row it used to walk `eval_expr_with_correlated` (a memo
6729        // lookup for "does this have a subquery", then an un-memoised
6730        // `expr_may_use_in_set` tree walk), then `eval_expr`'s dispatch,
6731        // then `resolve_column`, which finds the column by scanning the
6732        // schema and comparing NAMES. On `SELECT g FROM h` that chain was
6733        // 19 % of self time for what is ultimately one cell read.
6734        //
6735        // `compile_column_pos` is the Step VM's resolver, already
6736        // `pub(crate)` and already reused by the aggregate's bind-once
6737        // path: it mirrors `resolve_column`'s happy layers and returns
6738        // None for anything that would reach an error, an ambiguity, or a
6739        // miss, so those still go the interpreter's way and keep its
6740        // exact message. A composite column is excluded for the same
6741        // reason `compile_into` excludes it — it must be rehydrated from
6742        // stored JSON, which is not a cell read.
6743        let proj_direct = bind_direct_columns(&projection, &ctx);
6744        let any_proj_direct = proj_direct.iter().any(Option::is_some);
6745        // v7.39 (round 605) — a projection item that cannot depend on the row
6746        // is evaluated once. `SELECT ('{"a":1}')::JSONB FROM j` cost TEN
6747        // allocations a row against one for a plain column, `'abc' || 'def'`
6748        // six and `upper('abc')` five, all of them producing the same value
6749        // 50,000 times. An item that fails to evaluate is left alone, so its
6750        // error still comes from the row loop in the interpreter's wording.
6751        let proj_const: Vec<Option<Value<'static>>> = projection
6752            .iter()
6753            .map(|p| crate::eval::compiled::constant_projection_value(&p.expr, &ctx))
6754            .collect();
6755        let any_proj_const = proj_const.iter().any(Option::is_some);
6756        crate::bump_counter!(crate::select::SCAN_PATH_ENTERED);
6757        // v7.39 (read01 round 80) — positional ORDER BY over a WILDCARD
6758        // projection. Statement prep (`resolve_order_by_position`) can only map
6759        // `ORDER BY 1` onto the first SELECT item when that item is an
6760        // expression; a `*` is not one, so the literal survived to here and was
6761        // evaluated as the CONSTANT 1 — the same key for every row, i.e. no sort
6762        // at all. The parser rewrites `SELECT unnest(a) x` into
6763        // `SELECT * FROM unnest(a) x`, so that innocuous-looking shape landed
6764        // exactly here: `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came
6765        // back in input order. The projection is built by now, so the Nth output
6766        // column is known — resolve against it.
6767        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
6768        // v7.39 (round 600) — the ORDER BY of an SRF query is decided on the
6769        // EXPANDED rows, so a key naming a select-list item reads that item.
6770        let srf_order_cols: Vec<Option<usize>> = if srf_position.is_some() {
6771            srf_order_output_cols(&order_by, &projection)
6772        } else {
6773            Vec::new()
6774        };
6775        let srf_key_bound: Vec<Option<usize>> = (0..order_by.len()).map(Some).collect();
6776        // v7.37.x (docker-fair SCALARSQ attack) — early-limit gate for
6777        // the no-ORDER-BY-no-DISTINCT-no-TIES-no-SRF-no-WHERE shape.
6778        // Hoisted above the closure so the projection-eval path can
6779        // gate `memo` passing on it: the SELECT-item correlated-scalar
6780        // batch path scans the FULL inner table once (~5 ms for 12.5 k
6781        // rows) and is only a win when N outer rows is large; for small
6782        // LIMITed shapes a per-row PK seek (~5 µs × 100 = 500 µs) wins.
6783        let early_cap: Option<usize> = if order_by.is_empty()
6784            && !stmt.distinct
6785            && !stmt.limit_with_ties
6786            && srf_position.is_none()
6787            && stmt.where_.is_none()
6788        {
6789            stmt.limit_literal()
6790                .map(|n| n.saturating_add(stmt.offset_literal().unwrap_or(0)) as usize)
6791        } else {
6792            None
6793        };
6794        // v7.38 (read01 B8) — streaming top-N budget. For `ORDER BY …
6795        // LIMIT k` (no DISTINCT / WITH TIES / SRF, and not forced to
6796        // full-sort by the test gate) keep only the running top-`keep`
6797        // rows in memory instead of materialising every projected row,
6798        // so a `… ORDER BY col LIMIT 10` over a huge table is O(keep)
6799        // space, not O(rows). `None` = accumulate everything (the prior
6800        // behaviour). The final `partial_sort_tagged(keep)` below still
6801        // runs and produces the identical rows.
6802        // v7.39 (round 683) — the declared collation for each ORDER BY
6803        // position, resolved once and carried beside `descs` for the same
6804        // reason `descs` is carried: it is per key position, not per row.
6805        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
6806        let topk_stream: Option<(usize, Vec<bool>)> = if !order_by.is_empty()
6807            && !stmt.distinct
6808            && !stmt.limit_with_ties
6809            && srf_position.is_none()
6810            && !self.env_cfg().disable_topk
6811        {
6812            stmt.limit_literal().and_then(|l| {
6813                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
6814                (keep >= 1).then(|| (keep, order_by.iter().map(|o| o.desc).collect()))
6815            })
6816        } else {
6817            None
6818        };
6819        // v7.37.16 — streaming DISTINCT seen-set: norm-hash → indices of
6820        // kept rows in `tagged`. Probing on the PROJECTED row as soon as
6821        // it is built means a duplicate costs neither a build_order_keys
6822        // eval (the dominant per-row cost of `DISTINCT … ORDER BY`) nor
6823        // a tagged slot, and the sort below runs over u survivors, not
6824        // n input rows — PG's hash-distinct-then-sort plan shape.
6825        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
6826            hashbrown::HashMap::new();
6827        let distinct_hb = hashbrown::DefaultHashBuilder::default();
6828        // v7.39 (round 485) — one projection buffer for the whole scan
6829        // rather than a fresh `Vec` per input row. A row that survives
6830        // the DISTINCT probe takes the buffer with it (`mem::take`) and
6831        // the next row allocates a new one; a row that duplicates an
6832        // earlier one leaves the buffer — and its capacity — in place.
6833        // The round-485 counter says 49 900 of `distinct_proj`'s 50 000
6834        // projected rows are duplicates, so that is 49 900 allocate /
6835        // free pairs the scan no longer performs. Shapes where every row
6836        // survives (plain projection, `DISTINCT` over a unique column)
6837        // allocate exactly as often as before.
6838        let mut proj_buf: Vec<Value<'static>> = Vec::new();
6839        // v7.39 (round 571) — buffers handed back by the top-N trim.
6840        // Round 485 made the scan share ONE projection buffer, but a
6841        // surviving row takes it (`mem::take`) and without DISTINCT
6842        // almost every row survives, so the next one starts from zero
6843        // capacity and allocates. The trim drops `keep` rows at a time
6844        // and their buffers come back here instead of being freed.
6845        let mut proj_pool: Vec<Vec<Value<'static>>> = Vec::new();
6846        let mut key_pool: Vec<Vec<crate::orderby::OrderKey>> = Vec::new();
6847        // v7.39 (round 581) — the worst row the accumulator is currently
6848        // keeping. Anything that loses to it cannot reach the answer, so
6849        // it is dropped before its projection is ever built.
6850        let mut topk_boundary: Option<Vec<crate::orderby::OrderKey>> = None;
6851        // v7.39 (round 582) — resolve each ORDER BY column once, not
6852        // once per row. See `order_by_bound_positions`.
6853        let order_bound =
6854            crate::orderby::order_by_bound_positions(&order_by, schema_cols, Some(alias));
6855        // v7.39 (round 581) — and it stops asking when the answer is
6856        // always "keep".
6857        //
6858        // The check earns its place only on rows it rejects. Over
6859        // ascending ids, `ORDER BY id DESC` never rejects one — every
6860        // row beats the current worst — so the comparison is pure
6861        // overhead there, measured at +5.5% in three batches out of
6862        // three. After a window of rows it looks at what it has
6863        // actually rejected and switches itself off if the shape is not
6864        // paying. The answers do not depend on it either way.
6865        const BOUNDARY_WINDOW: u32 = 8192;
6866        let mut boundary_checks: u32 = 0;
6867        let mut boundary_rejects: u32 = 0;
6868        let mut boundary_check_on = true;
6869        // Inline the per-row work in a closure so the indexed and full-
6870        // scan branches share the body.
6871        let mut process_row = |row: &Row<'static>, loop_idx: usize| -> Result<(), EngineError> {
6872            if loop_idx.is_multiple_of(256) {
6873                cancel.check()?;
6874            }
6875            if let Some(cw) = &compiled_where {
6876                let cond = eval::eval_compiled(cw, row, &ctx, &mut eval_stack)
6877                    .map_err(EngineError::Eval)?;
6878                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6879                    return Ok(());
6880                }
6881            } else if let Some(where_expr) = &stmt.where_ {
6882                let cond =
6883                    self.eval_expr_with_correlated(where_expr, row, &ctx, cancel, Some(&mut memo))?;
6884                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6885                    return Ok(());
6886                }
6887            }
6888            // Under DISTINCT the keys are built AFTER the dup probe
6889            // (survivors only); the non-distinct order is unchanged.
6890            // v7.39 (round 600) — an SRF query's keys are built per EXPANDED
6891            // row further down, and building them here would evaluate the
6892            // ORDER BY against the INPUT row: a key naming the SRF's own
6893            // output became a scalar call to it, which is where
6894            // "function unnest(integer[]) does not exist" came from.
6895            let order_keys = if order_by.is_empty() || stmt.distinct || srf_position.is_some() {
6896                Vec::new()
6897            } else {
6898                let mut buf = key_pool.pop().unwrap_or_default();
6899                crate::orderby::build_order_keys_bound(
6900                    &order_by,
6901                    &order_bound,
6902                    row,
6903                    &ctx,
6904                    &mut buf,
6905                )?;
6906                // v7.39 (round 581) — reject before projecting.
6907                //
6908                // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
6909                // 50 distinct `g` decides nearly every row on the FIRST
6910                // key, and PG answers it FASTER than the single-key form
6911                // (7.4 ms against 10.4) because a rejected row costs it
6912                // one comparison. SPG built both keys AND the projected
6913                // row for all 500k before throwing them away. The keys
6914                // are needed to compare; the projection is not.
6915                if boundary_check_on
6916                    && let Some((_, descs)) = &topk_stream
6917                    && let Some(b) = &topk_boundary
6918                {
6919                    boundary_checks += 1;
6920                    let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
6921                        == core::cmp::Ordering::Greater;
6922                    if loses {
6923                        boundary_rejects += 1;
6924                    }
6925                    if boundary_checks == BOUNDARY_WINDOW {
6926                        // Keep asking only if it has been rejecting at
6927                        // least a quarter of what it saw.
6928                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
6929                    }
6930                    if loses {
6931                        buf.clear();
6932                        key_pool.push(buf);
6933                        return Ok(());
6934                    }
6935                }
6936                buf
6937            };
6938            if srf_position.is_some() {
6939                let plan = srf_plan.as_mut().expect("srf_position implies a plan");
6940                for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
6941                    if stmt.distinct {
6942                        let bucket = seen_distinct
6943                            .entry(norm_hash_row(&out, &distinct_hb, ctx.mysql_dialect))
6944                            .or_default();
6945                        if bucket
6946                            .iter()
6947                            .any(|i| row_eq_norm(&tagged[i].1, &out, ctx.mysql_dialect))
6948                        {
6949                            continue;
6950                        }
6951                        bucket.push(tagged.len());
6952                    }
6953                    budget.charge(approx_row_bytes(&out))?;
6954                    // The keys come from THIS expanded row: a key naming a
6955                    // select-list item reads its value, anything else is
6956                    // still evaluated against the input row.
6957                    let keys = if order_by.is_empty() {
6958                        Vec::new()
6959                    } else {
6960                        let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
6961                        for (k, ob) in order_by.iter().enumerate() {
6962                            kv.push(match srf_order_cols.get(k).copied().flatten() {
6963                                Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
6964                                None => eval::eval_expr(&ob.expr, row, &ctx)
6965                                    .map_err(EngineError::Eval)?,
6966                            });
6967                        }
6968                        // Packed by the same code every other ORDER BY uses,
6969                        // so DESC / NULLS FIRST / the MySQL rule are not
6970                        // restated here.
6971                        let key_row = Row::new(kv);
6972                        let mut buf = Vec::new();
6973                        crate::orderby::build_order_keys_bound(
6974                            &order_by,
6975                            &srf_key_bound,
6976                            &key_row,
6977                            &ctx,
6978                            &mut buf,
6979                        )?;
6980                        buf
6981                    };
6982                    tagged.push((keys, out));
6983                }
6984            } else {
6985                let values = &mut proj_buf;
6986                values.clear();
6987                values.reserve(projection.len());
6988                for (i, p) in projection.iter().enumerate() {
6989                    // v7.37.x (docker-fair SCALARSQ attack) — pre-
6990                    // analysed PK-probe fast path. The per-row work is
6991                    // a read of outer.col from the row plus an index
6992                    // probe — no Expr clone, no walker, no
6993                    // `eval_expr_with_correlated` framework.
6994                    if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
6995                        values.push(self.probe_with_pk_fast_path(fp, row));
6996                        continue;
6997                    }
6998                    // v7.39 (round 605) — the same value every row.
6999                    if any_proj_const && let Some(v) = &proj_const[i] {
7000                        values.push(v.clone());
7001                        continue;
7002                    }
7003                    // v7.39 (round 487) — bound column: read the cell.
7004                    // This is `rehydrate_cell`'s body for a non-composite
7005                    // column, which is what the whole chain below reduces
7006                    // to once the name has been resolved.
7007                    if any_proj_direct && let Some(pos) = proj_direct[i] {
7008                        crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
7009                        values.push(row.values[pos].clone().into_owned());
7010                        continue;
7011                    }
7012                    // v7.24 (round-16 B) — correlated-aware.
7013                    // v7.37.x (docker-fair SCALARSQ attack) — share the
7014                    // per-row memo with projection. Required for the
7015                    // batch-evaluated correlated-scalar path to fire on
7016                    // SELECT-item scalar subqueries; otherwise each row
7017                    // re-executes the inner.
7018                    //
7019                    // Skip the memo when the outer row count is small
7020                    // (early-limited): the batch path scans the FULL
7021                    // inner table to build a GroupMap (~5 ms for a
7022                    // 12.5 k-row inner), while per-row execution with a
7023                    // PK index seek is ~5 µs per call — much cheaper for
7024                    // N ≤ ~1000 outer rows.
7025                    let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
7026                    let memo_arg = if pass_memo { Some(&mut memo) } else { None };
7027                    values.push(
7028                        self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7029                    );
7030                }
7031                crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7032                if stmt.distinct {
7033                    let bucket = seen_distinct
7034                        .entry(norm_hash_values(&proj_buf, &distinct_hb, ctx.mysql_dialect))
7035                        .or_default();
7036                    if bucket
7037                        .iter()
7038                        .any(|i| values_eq_norm(&tagged[i].1.values, &proj_buf, ctx.mysql_dialect))
7039                    {
7040                        crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7041                        return Ok(());
7042                    }
7043                    bucket.push(tagged.len());
7044                }
7045                let out = Row::new(core::mem::replace(
7046                    &mut proj_buf,
7047                    proj_pool.pop().unwrap_or_default(),
7048                ));
7049                let order_keys = if stmt.distinct && !order_by.is_empty() {
7050                    build_order_keys(&order_by, row, &ctx)?
7051                } else {
7052                    order_keys
7053                };
7054                budget.charge(approx_row_bytes(&out))?;
7055                tagged.push((order_keys, out));
7056            }
7057            // Streaming top-N: bound the accumulator to O(keep) rows.
7058            if let Some((k, descs)) = &topk_stream {
7059                crate::orderby::topk_trim_recycling(
7060                    &mut tagged,
7061                    *k,
7062                    descs,
7063                    &mut proj_pool,
7064                    &mut key_pool,
7065                    &mut topk_boundary,
7066                );
7067            }
7068            Ok(())
7069        };
7070        // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7071        // load-bearing full-scan path. This is the primary single-table
7072        // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7073        // in-place writers retain dead/old versions, an ungated scan
7074        // here would return them, so the gate must land BEFORE the
7075        // writers flip (see the plan's activation-order rule). A no-op
7076        // today: every hot row is frozen or committed-and-alive under
7077        // the reader's snapshot, so `is_row_visible` returns true for
7078        // all of them (verified by the full e2e suite staying green).
7079        let scan_snapshot = self.current_snapshot();
7080        let mut emitted: usize = 0;
7081        if let Some(rows) = &indexed_rows {
7082            for (loop_idx, cow) in rows.iter().enumerate() {
7083                if let Some(cap) = early_cap
7084                    && emitted >= cap
7085                {
7086                    break;
7087                }
7088                process_row(cow.as_ref(), loop_idx)?;
7089                emitted = emitted.saturating_add(1);
7090            }
7091        } else {
7092            // v7.39 (round 570) — the row store is a 32-way trie, so
7093            // indexing it is four dependent loads. Round 567 measured
7094            // -18% on the aggregate scan from holding the leaf between
7095            // rows; this is the same loop for the projecting scan.
7096            let mut rows_cur = table.rows().run_cursor();
7097            for i in 0..table.row_count() {
7098                if let Some(cap) = early_cap
7099                    && emitted >= cap
7100                {
7101                    break;
7102                }
7103                // Skip rows this snapshot cannot see (invisible rows do
7104                // not count toward the LIMIT).
7105                if !table.is_row_visible(i, &scan_snapshot) {
7106                    continue;
7107                }
7108                let Some(row) = rows_cur.get(i) else { continue };
7109                process_row(row, i)?;
7110                emitted = emitted.saturating_add(1);
7111            }
7112            // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7113            // rows into the same loop. The full-scan path here is the
7114            // load-bearing single-table SELECT executor, and pre-
7115            // 7.35.1 it only walked `table.rows()` (hot), so any
7116            // `SELECT … FROM t` against a table with cold segments
7117            // silently returned a subset.
7118            let cold_rows = self.iter_cold_rows_of_table(table);
7119            for (offset, row) in cold_rows.iter().enumerate() {
7120                if let Some(cap) = early_cap
7121                    && emitted >= cap
7122                {
7123                    break;
7124                }
7125                process_row(row, table.row_count() + offset)?;
7126                emitted = emitted.saturating_add(1);
7127            }
7128        }
7129
7130        // (DISTINCT already de-duped STREAMING inside process_row, so the
7131        // sort below only sees the u survivors and the partial-sort
7132        // budget applies to DISTINCT too.)
7133        if !order_by.is_empty() {
7134            // Partial-sort fast path: when LIMIT is small relative to
7135            // the row count, select_nth_unstable + sort just the
7136            // prefix is O(n + k log k) instead of O(n log n).
7137            // WITH TIES needs the full sort so the tie extension can
7138            // scan past `limit` to find rows that share the last-kept
7139            // row's key.
7140            let keep = if stmt.limit_with_ties
7141                // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7142                // forces the full-sort fallback by suppressing the
7143                // partial-sort `keep` budget. See
7144                // `xtests/sigil/test-mode-gucs.md`.
7145                || self.env_cfg().disable_topk
7146            {
7147                None
7148            } else {
7149                stmt.limit_literal()
7150                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7151            };
7152            let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7153            crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &order_colls);
7154        }
7155
7156        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7157        // past the truncated tail through every row that shares the
7158        // last-kept row's ORDER BY key. The tie check uses the
7159        // already-computed `(order_keys, row)` pairs so it matches
7160        // the sort comparator exactly. DISTINCT + WITH TIES falls
7161        // through to the no-ties path (PG also disallows their
7162        // combination; SPG silently drops the tie extension here so
7163        // the customer doesn't see a hard error mid-query — the
7164        // user-visible result is still correct, just narrower).
7165        let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7166            apply_offset_and_limit_tagged(
7167                &mut tagged,
7168                stmt.offset_literal(),
7169                stmt.limit_literal(),
7170                true,
7171            );
7172            tagged.into_iter().map(|(_, r)| r).collect()
7173        } else {
7174            // DISTINCT already de-duped pre-sort above.
7175            let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7176            apply_offset_and_limit(
7177                &mut output_rows,
7178                stmt.offset_literal(),
7179                stmt.limit_literal(),
7180            );
7181            output_rows
7182        };
7183
7184        let columns: Vec<ColumnSchema> = projection
7185            .into_iter()
7186            .map(|p| {
7187                let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
7188                c.user_enum_type = p.user_enum_type;
7189                c.collation_name = p.collation_name;
7190                c.mysql_fsp = p.mysql_fsp;
7191                c
7192            })
7193            .collect();
7194
7195        Ok(QueryResult::Rows {
7196            columns,
7197            rows: output_rows,
7198        })
7199    }
7200
7201    /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
7202    /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
7203    /// select items for the surviving rows only — PG's Result-above-
7204    /// Limit shape, where SubPlan loops equal the OUTPUT row count
7205    /// (50) instead of the group count (24k).
7206    fn finish_agg_result(
7207        &self,
7208        mut agg: aggregate::AggResult,
7209        stmt: &SelectStatement,
7210        cancel: CancelToken<'_>,
7211    ) -> Result<QueryResult, EngineError> {
7212        apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
7213        if !agg.deferred.is_empty() {
7214            apply_offset_and_limit(
7215                &mut agg.synth_rows,
7216                stmt.offset_literal(),
7217                stmt.limit_literal(),
7218            );
7219            let ctx = EvalContext::new(&agg.synth_schema, None);
7220            let mut memo = memoize::MemoizeCache::default();
7221            // v7.32 (architecture v2 P3) — keyed index-probe seeding.
7222            // Deferred subqueries are referenced only by surviving
7223            // select-list rows (≤ LIMIT), so their correlation keys are
7224            // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
7225            // each batchable subquery's group map over just those keys
7226            // via per-key index seek; the per-row splice loop below then
7227            // reuses the seeded map. A join-shaped or un-indexed inner
7228            // falls through to the all-keys batch inside the call (built
7229            // eagerly here instead of lazily on row 0 — same cost), so
7230            // it still pays the full scan, never the 715 ms per-row
7231            // direct eval; its index-nested-loop probe is the next
7232            // knife. Genuinely non-batchable shapes return None and are
7233            // left unseeded for the loop's per-row resolver, as before.
7234            for (_, expr) in &agg.deferred {
7235                let mut subs: Vec<&SelectStatement> = Vec::new();
7236                collect_scalar_subqueries(expr, &mut subs);
7237                for sub in subs {
7238                    let repr = alloc::format!("{sub}");
7239                    if memo.group_maps.contains_key(&repr) {
7240                        continue;
7241                    }
7242                    if let Some(gm) = self.try_batch_correlated_scalar(
7243                        sub,
7244                        Some((&agg.synth_rows, &ctx)),
7245                        cancel,
7246                    )? {
7247                        memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
7248                    }
7249                }
7250            }
7251            for (ri, srow) in agg.synth_rows.iter().enumerate() {
7252                cancel.check()?;
7253                for (col, expr) in &agg.deferred {
7254                    let v =
7255                        self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
7256                    if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
7257                        *cell = v;
7258                    }
7259                }
7260            }
7261        }
7262        Ok(QueryResult::Rows {
7263            columns: agg.columns,
7264            rows: agg.rows,
7265        })
7266    }
7267
7268    /// v7.37 — streaming projection for the joined-non-aggregate
7269    /// shape (multi-table FROM, all projection items bound, no
7270    /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
7271    /// UNION). Walks the deferred join survivors and emits
7272    /// `&[&Value]` borrowed straight out of the source tables — no
7273    /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
7274    /// on the mailrs `PROJ` shape (about 4 ms saved).
7275    ///
7276    /// Returns `Ok(None)` when the shape doesn't qualify; the caller
7277    /// then falls back to the materialising path.
7278    /// v7.37 (round 831) — stream a joinless SELECT straight off the
7279    /// stored table, one row at a time, without ever building a row set.
7280    ///
7281    /// Returns `Ok(None)` for anything this cannot serve, and the caller
7282    /// falls through to the deferred-join path exactly as before: a
7283    /// missing table, or a cold tier whose hydration the fallback handles.
7284    /// Sort a single-table scan through the external sorter, so the
7285    /// answer's size is bounded by `work_mem` and not by the input.
7286    ///
7287    /// Sorting held every row twice — the scan's `Vec<Row>` and the
7288    /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
7289    /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
7290    /// enough ORDER BY took the server down, which is a liveness
7291    /// problem before it is a performance one.
7292    ///
7293    /// A SEPARATE walk rather than a change to `run_single_table_scan`,
7294    /// following what round 831 did for the joinless shape. That
7295    /// function is 552 lines whose projection loop is entangled with
7296    /// DISTINCT (which indexes back into the tagged vector) and with
7297    /// streaming top-N (whose boundary moves as the scan runs); both
7298    /// assume the projection has already happened when a row is
7299    /// pushed, which is exactly what spilling has to defer. Two earlier
7300    /// attempts tried to rework that loop and were reverted. Here the
7301    /// existing path is untouched and this one only claims shapes it
7302    /// can serve, so a decline costs nothing.
7303    ///
7304    /// Records are SOURCE rows, not projected ones: `finish` re-derives
7305    /// keys from what it decodes, and an ORDER BY key need not be in
7306    /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
7307    fn try_spill_sorted_scan(
7308        &self,
7309        stmt: &SelectStatement,
7310        from: &FromClause,
7311        cancel: CancelToken<'_>,
7312    ) -> Result<Option<QueryResult>, EngineError> {
7313        // Shapes this walk does not serve. Each one either needs the
7314        // whole tagged vector addressable (DISTINCT probes back into
7315        // it, WITH TIES re-reads its tail) or is already bounded
7316        // without spilling (a LIMIT makes the partial sort O(keep)).
7317        if !self.can_spill()
7318            || stmt.order_by.is_empty()
7319            || stmt.distinct
7320            || stmt.limit_with_ties
7321            || stmt.limit_literal().is_some()
7322            || !from.joins.is_empty()
7323            || from.primary.lateral_subquery.is_some()
7324            || from.primary.unnest_expr.is_some()
7325            || from.primary.generate_series_args.is_some()
7326            || select_has_window(stmt)
7327        {
7328            return Ok(None);
7329        }
7330        // A parent's rows are its children's. These walks scan the named
7331        // relation alone, so a partitioned or inherited parent comes back
7332        // short — and silently: the corpus caught `SELECT id FROM pr
7333        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
7334        // parent's own rows instead of the partitions'. `ONLY` is exactly
7335        // the case that does not fan out, so it stays, which is the test
7336        // the FROM-clause fan-out itself makes.
7337        if !from.primary.only
7338            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7339        {
7340            return Ok(None);
7341        }
7342        let Some(table) = self.active_catalog().get(&from.primary.name) else {
7343            return Ok(None);
7344        };
7345        // Cold-tier rows live outside `rows()`; this walk would drop
7346        // them silently, the same reason round 831's walk declines.
7347        if table.has_cold_rows_fast() {
7348            return Ok(None);
7349        }
7350
7351        let alias = from
7352            .primary
7353            .alias
7354            .as_deref()
7355            .unwrap_or(from.primary.name.as_str());
7356        let cols = table.schema().columns.clone();
7357        let sess = self.dml_session();
7358        let ctx = EvalContext::new(&cols, Some(alias))
7359            .with_catalog(self.active_catalog())
7360            .with_session(&sess);
7361        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
7362        let order_by = stmt.order_by.clone();
7363        // The same one-shot resolution the general path does (round
7364        // 582): each ORDER BY column is bound once, not once per row.
7365        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
7366        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7367        // Resolved BEFORE the scan, because it now decides what the sort
7368        // STORES and not just what it decodes (round 995).
7369        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
7370
7371        let mut sorter = crate::extsort::ExternalSorter::new(
7372            self.temp_run_factory,
7373            self.session_work_mem_bytes(),
7374            cols.clone(),
7375            &descs,
7376        )
7377        .with_stats(&self.spill_stats)
7378        .with_pruned(&needed);
7379        let snapshot = self.current_snapshot();
7380        // One key buffer for the whole scan: `push` drains it and leaves
7381        // the capacity behind.
7382        let mut keys: Vec<OrderKey> = Vec::new();
7383        // r1024 — compile the predicate once for the scan.
7384        //
7385        // These two sorted-spill scans are the paths a single-table SELECT
7386        // with an ORDER BY takes, and they were the last row-returning ones
7387        // still walking the expression tree per row. r1023 did the
7388        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
7389        // exactly this shape.
7390        //
7391        // Found from the profile's CALL TREE rather than its leaves. The
7392        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
7393        // 261, `mod_op` 178 — and two attempts at reasoning out which
7394        // function asked for it were both wrong. The tree names the caller
7395        // chain, and it named this one.
7396        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
7397            .where_
7398            .as_ref()
7399            .filter(|w| crate::eval::fully_compilable(w))
7400            .map(|w| crate::eval::compile_expr(w, &ctx));
7401        let mut eval_stack: Vec<Value<'static>> = Vec::new();
7402        for (i, row) in table.scan_visible_from(0, &snapshot) {
7403            if i.is_multiple_of(256) {
7404                cancel.check()?;
7405            }
7406            if let Some(c) = &compiled_where {
7407                if !crate::eval::compiled::eval_compiled_pred(
7408                    c,
7409                    row,
7410                    &ctx,
7411                    &mut eval_stack,
7412                    ctx.mysql_dialect,
7413                )? {
7414                    continue;
7415                }
7416            } else if let Some(w) = &stmt.where_ {
7417                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
7418                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7419                    continue;
7420                }
7421            }
7422            keys.clear();
7423            crate::orderby::build_order_keys_bound(&order_by, &order_bound, row, &ctx, &mut keys)?;
7424            sorter.push(&mut keys, row)?;
7425        }
7426
7427        let key_ctx = &ctx;
7428        let rows = sorter.finish(
7429            |src, buf| {
7430                crate::orderby::build_order_keys_bound(&order_by, &order_bound, src, key_ctx, buf)
7431            },
7432            |src| {
7433                let mut values = Vec::with_capacity(projection.len());
7434                for p in &projection {
7435                    values.push(
7436                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
7437                    );
7438                }
7439                Ok(Row::new(values))
7440            },
7441        )?;
7442
7443        let columns: Vec<ColumnSchema> = projection
7444            .iter()
7445            .map(|p| {
7446                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
7447                c.user_enum_type = p.user_enum_type.clone();
7448                c.mysql_fsp = p.mysql_fsp;
7449                c
7450            })
7451            .collect();
7452        Ok(Some(QueryResult::Rows { columns, rows }))
7453    }
7454
7455    /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
7456    /// handing each row to the consumer instead of collecting the answer.
7457    ///
7458    /// That walk bounds the SORT and then returns `QueryResult::Rows`,
7459    /// which holds every output row. Measured at `work_mem = 4 MB` over
7460    /// 200-byte rows, RSS above the server's own baseline while the
7461    /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
7462    /// at 400k — linear — while the spill underneath worked correctly
7463    /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
7464    /// removes each file, so a count taken afterwards reads 0 whatever
7465    /// happened, and an earlier reading of "no spill at all" was that
7466    /// blind witness). The growth is the collected result, not the sort.
7467    ///
7468    /// Emitting makes peak the budget, one buffer per run and a single
7469    /// row — the state a merge already holds at every step. It also
7470    /// frees each projected row as the next is built rather than
7471    /// accumulating them, which is where the time is: a profile of the
7472    /// collecting walk put the allocator at 586 samples, more than every
7473    /// sort comparison combined (420), against 19 for `push` itself.
7474    /// v7.37 (round 923) — which of a sort record's columns the output half
7475    /// reads. The record is the SOURCE row (round 836), so a narrow projection
7476    /// decoded every column: skipping one 200-byte text halves a decode
7477    /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
7478    ///
7479    /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
7480    /// column reads NULL. Answers only when every projection item is a bare
7481    /// column reference AND every ORDER BY key is a bound column; anything
7482    /// else returns empty, decoding everything as before.
7483    /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
7484    /// drops references from expression kinds it does not enumerate.
7485    ///
7486    /// ORDER BY columns are included — the merge re-derives keys from the
7487    /// decoded row on the spilled path, so pruning one would sort NULLs.
7488    pub(crate) fn sort_record_columns_needed(
7489        items: &[SelectItem],
7490        order_bound: &[Option<usize>],
7491        arity: usize,
7492        ctx: &EvalContext,
7493    ) -> Vec<bool> {
7494        let all_bare = items.iter().all(|i| {
7495            matches!(
7496                i,
7497                SelectItem::Expr {
7498                    expr: Expr::Column(_),
7499                    ..
7500                }
7501            )
7502        });
7503        if !all_bare || order_bound.iter().any(Option::is_none) {
7504            return Vec::new();
7505        }
7506        let mut mask = alloc::vec![false; arity];
7507        for item in items {
7508            if let SelectItem::Expr {
7509                expr: Expr::Column(c),
7510                ..
7511            } = item
7512            {
7513                match crate::eval::find_column_pos(c, ctx) {
7514                    Some(p) if p < arity => mask[p] = true,
7515                    _ => return Vec::new(),
7516                }
7517            }
7518        }
7519        for p in order_bound.iter().flatten() {
7520            if *p < arity {
7521                mask[*p] = true;
7522            } else {
7523                return Vec::new();
7524            }
7525        }
7526        mask
7527    }
7528
7529    /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
7530    /// of sorting.
7531    ///
7532    /// PG serves such an ordering from the index and never sorts. We sorted:
7533    /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
7534    /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
7535    /// the sorter's own round trip — `ExternalSorter::finish_each` →
7536    /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
7537    /// Every row is encoded into the sorter's arena and decoded back out,
7538    /// for an order the index already holds.
7539    ///
7540    /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
7541    /// because it was built for top-N. This is the unbounded sibling.
7542    ///
7543    /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
7544    /// from a btree, so walking one would silently drop those rows. That is
7545    /// exactly the defect r1020 fixed on the top-N path, where it had
7546    /// shipped.
7547    /// r1044 — the index this statement's ORDER BY can be WALKED on,
7548    /// instead of sorted, or `None`.
7549    ///
7550    /// Extracted so `EXPLAIN` can ask the same question the executor
7551    /// answers. It could not, and said so: `SELECT pad FROM t ORDER BY
7552    /// id` on a 400,000-row table planned as `Sort` over `Seq Scan`
7553    /// while the executor walked the primary key — 34.9 ms against
7554    /// 147.0 for the same query ordered by an unindexed column, so the
7555    /// walk was plainly running. Round 551 fixed a different case of
7556    /// this and wrote the reason down: EXPLAIN is the first thing any
7557    /// performance question opens, and an instrument that misnames the
7558    /// access path is worse than one that says nothing.
7559    ///
7560    /// The gate is here once. Two copies of it is how the plan and the
7561    /// executor come to disagree again.
7562    pub(crate) fn index_order_walk_target(
7563        &self,
7564        stmt: &SelectStatement,
7565        from: &FromClause,
7566    ) -> Option<(String, usize)> {
7567        if stmt.order_by.len() != 1
7568            || !stmt.distinct_on.is_empty()
7569            || stmt.limit_with_ties
7570            || stmt.limit.is_some()
7571            || stmt.offset.is_some()
7572            || stmt.having.is_some()
7573            || stmt.group_by.is_some()
7574            || !stmt.unions.is_empty()
7575            || !from.joins.is_empty()
7576            || from.primary.lateral_subquery.is_some()
7577            || from.primary.unnest_expr.is_some()
7578            || from.primary.as_of_segment.is_some()
7579            || from.primary.generate_series_args.is_some()
7580            || select_has_window(stmt)
7581            || aggregate::uses_aggregate(stmt)
7582        {
7583            return None;
7584        }
7585        if stmt
7586            .items
7587            .iter()
7588            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
7589        {
7590            return None;
7591        }
7592        let table = self.active_catalog().get(&from.primary.name)?;
7593        if table.has_cold_rows_fast() {
7594            return None;
7595        }
7596        if !from.primary.only
7597            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7598        {
7599            return None;
7600        }
7601        let alias = from
7602            .primary
7603            .alias
7604            .as_deref()
7605            .unwrap_or(from.primary.name.as_str());
7606        let cols = &table.schema().columns;
7607        let order = &stmt.order_by[0];
7608        let Expr::Column(oc) = &order.expr else {
7609            return None;
7610        };
7611        if let Some(q) = &oc.qualifier
7612            && !q.eq_ignore_ascii_case(alias)
7613        {
7614            return None;
7615        }
7616        let order_pos = cols
7617            .iter()
7618            .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
7619        // r1047 — DISTINCT joins the walk when the projection IS the
7620        // order column, and only then. The index's keys are canonical
7621        // (r1039: representation equality is value equality — the
7622        // property every seek already depends on), so one key is one
7623        // distinct value and the walk can emit the first passing row of
7624        // each key group instead of hashing every row. On the release
7625        // sweep's `SELECT DISTINCT n FROM t ORDER BY n` — 400,000 rows,
7626        // 1,000 distinct values — the hash path priced at 21.3-22.7 ms
7627        // with an ablation floor of 14.8, because the hash must
7628        // normalize and probe ALL the rows; the walk visits each key
7629        // once. A wider projection makes DISTINCT about the whole tuple,
7630        // not the key, so anything else still declines.
7631        if stmt.distinct {
7632            let only_the_order_column = stmt.items.len() == 1
7633                && match &stmt.items[0] {
7634                    SelectItem::Expr {
7635                        expr: Expr::Column(c),
7636                        ..
7637                    } => {
7638                        c.name.eq_ignore_ascii_case(&oc.name)
7639                            && match &c.qualifier {
7640                                Some(q) => q.eq_ignore_ascii_case(alias),
7641                                None => true,
7642                            }
7643                    }
7644                    _ => false,
7645                };
7646            if !only_the_order_column {
7647                return None;
7648            }
7649        }
7650        // r1046 — a nullable key no longer refuses the walk; it changes
7651        // what the walk has to do. A NULL key is not in the btree, so
7652        // walking alone would silently drop those rows — the r1020
7653        // defect, which shipped once. The walk emits them separately, at
7654        // the end SQL puts them.
7655        //
7656        // Refusing was costing every nullable indexed column a 3.4x:
7657        // `SELECT id FROM t ORDER BY b` over 400,000 rows measured
7658        // 72.0 ms with the column nullable and 20.2 with the same data
7659        // under NOT NULL. `NOT NULL` is not the default, so that was the
7660        // common case paying for the uncommon one.
7661        let index = table.index_on(order_pos)?;
7662        if !matches!(index.kind, spg_storage::IndexKind::BTree(_))
7663            || index.expression.is_some()
7664            || index.partial_predicate.is_some()
7665        {
7666            return None;
7667        }
7668        Some((index.name.clone(), order_pos))
7669    }
7670
7671    fn try_index_order_stream<F>(
7672        &self,
7673        stmt: &SelectStatement,
7674        from: &FromClause,
7675        cancel: CancelToken<'_>,
7676        emit: &mut F,
7677    ) -> Result<Option<usize>, EngineError>
7678    where
7679        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
7680    {
7681        // r1044 — the shape gate lives in `index_order_walk_target`, so
7682        // `EXPLAIN` answers the same question. What stays here is the
7683        // part that RAISES (an illegal ORDER BY has to keep erroring
7684        // from where it did) and the bindings the walk needs.
7685        crate::orderby::check_order_by_legality(stmt)?;
7686        crate::orderby::check_order_by_positions(stmt)?;
7687        crate::window::reject_window_in_row_clauses(stmt)?;
7688        let Some((_, order_pos)) = self.index_order_walk_target(stmt, from) else {
7689            return Ok(None);
7690        };
7691        let Some(table) = self.active_catalog().get(&from.primary.name) else {
7692            return Ok(None);
7693        };
7694        let alias = from
7695            .primary
7696            .alias
7697            .as_deref()
7698            .unwrap_or(from.primary.name.as_str());
7699        let cols = table.schema().columns.clone();
7700        let order = &stmt.order_by[0];
7701        let Some(index) = table.index_on(order_pos) else {
7702            return Ok(None);
7703        };
7704
7705        let sess = self.dml_session();
7706        let ctx = EvalContext::new(&cols, Some(alias))
7707            .with_catalog(self.active_catalog())
7708            .with_session(&sess);
7709        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
7710        let columns: Vec<ColumnSchema> = projection
7711            .iter()
7712            .map(|p| {
7713                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
7714                c.user_enum_type = p.user_enum_type.clone();
7715                c.mysql_fsp = p.mysql_fsp;
7716                c
7717            })
7718            .collect();
7719        emit(crate::StreamItem::Header(&columns))?;
7720        let bound_pos: Vec<Option<usize>> = projection
7721            .iter()
7722            .map(|p| match &p.expr {
7723                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
7724                    Ok(Some(pos)) => Some(pos),
7725                    _ => None,
7726                },
7727                _ => None,
7728            })
7729            .collect();
7730
7731        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
7732            .where_
7733            .as_ref()
7734            .filter(|w| crate::eval::fully_compilable(w))
7735            .map(|w| crate::eval::compile_expr(w, &ctx));
7736        let mut eval_stack: Vec<Value<'static>> = Vec::new();
7737        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
7738        let snapshot = self.current_snapshot();
7739
7740        // A btree holds one locator per row VERSION, so a row whose key was
7741        // updated can sit under two keys and a dead one can sit beside its
7742        // replacement. The visibility gate drops the dead; `seen` drops a
7743        // live row that the walk reaches twice, which would otherwise be a
7744        // duplicated output row rather than a slow one.
7745        let mut emitted_rows = alloc::vec![false; table.rows().len()];
7746
7747        // r1046 — the rows the index cannot hold.
7748        //
7749        // A NULL key is not in the btree, so the walk below never reaches
7750        // those rows; they are emitted here, at the end SQL puts them.
7751        // PG's default is NULLS LAST ascending and NULLS FIRST
7752        // descending, and an explicit `NULLS FIRST` / `NULLS LAST` wins —
7753        // the same rule `order_by_value_cmp_raw` applies to the sort this
7754        // replaces, so the two orders agree.
7755        //
7756        // Finding them costs one pass over the column. That pass is why
7757        // this is still worth doing: the sort it replaces encodes and
7758        // decodes every row, and the walk plus the pass measured 72.0 ms
7759        // down to about 22 on 400,000 rows.
7760        let nulls_first = order.nulls_first.unwrap_or(order.desc);
7761        // r1047 — under DISTINCT the walk emits the FIRST passing row of
7762        // each key group and skips the rest; the gate admits DISTINCT
7763        // only when the projection is the order column itself, so one
7764        // canonical key is one output row. NULL is one distinct value,
7765        // so the NULL pass stops at its first emit too.
7766        let distinct = stmt.distinct;
7767        let mut count = 0usize;
7768        let mut visited = 0usize;
7769        let mut emit_null_rows = |emitted_rows: &mut alloc::vec::Vec<bool>,
7770                                  eval_stack: &mut Vec<Value<'static>>,
7771                                  values: &mut Vec<Value<'static>>,
7772                                  visited: &mut usize,
7773                                  emit: &mut F|
7774         -> Result<usize, EngineError> {
7775            if !cols[order_pos].nullable {
7776                return Ok(0);
7777            }
7778            let mut n = 0usize;
7779            for (ri, row) in table.rows().iter().enumerate() {
7780                if !matches!(row.values.get(order_pos), Some(Value::Null)) {
7781                    continue;
7782                }
7783                if emitted_rows.get(ri).copied().unwrap_or(true) {
7784                    continue;
7785                }
7786                if !table.is_row_visible(ri, &snapshot) {
7787                    continue;
7788                }
7789                *visited += 1;
7790                if visited.is_multiple_of(256) {
7791                    cancel.check()?;
7792                }
7793                emitted_rows[ri] = true;
7794                if Self::stream_project_row(
7795                    row,
7796                    stmt.where_.as_ref(),
7797                    compiled_where.as_ref(),
7798                    eval_stack,
7799                    &projection,
7800                    &bound_pos,
7801                    &ctx,
7802                    values,
7803                    emit,
7804                )? {
7805                    n += 1;
7806                    if distinct {
7807                        break;
7808                    }
7809                }
7810            }
7811            Ok(n)
7812        };
7813
7814        if nulls_first {
7815            count += emit_null_rows(
7816                &mut emitted_rows,
7817                &mut eval_stack,
7818                &mut values,
7819                &mut visited,
7820                emit,
7821            )?;
7822        }
7823
7824        let walker: alloc::boxed::Box<
7825            dyn Iterator<Item = (&spg_storage::IndexKey, &spg_storage::PostingList)>,
7826        > = if order.desc {
7827            alloc::boxed::Box::new(index.iter_desc())
7828        } else {
7829            alloc::boxed::Box::new(index.iter_asc())
7830        };
7831        for (_key, locators) in walker {
7832            for loc in locators {
7833                let spg_storage::RowLocator::Hot(ri) = *loc else {
7834                    continue;
7835                };
7836                if emitted_rows.get(ri).copied().unwrap_or(true) {
7837                    continue;
7838                }
7839                if !table.is_row_visible(ri, &snapshot) {
7840                    continue;
7841                }
7842                let Some(row) = table.rows().get(ri) else {
7843                    continue;
7844                };
7845                visited += 1;
7846                if visited.is_multiple_of(256) {
7847                    cancel.check()?;
7848                }
7849                emitted_rows[ri] = true;
7850                if Self::stream_project_row(
7851                    row,
7852                    stmt.where_.as_ref(),
7853                    compiled_where.as_ref(),
7854                    &mut eval_stack,
7855                    &projection,
7856                    &bound_pos,
7857                    &ctx,
7858                    &mut values,
7859                    emit,
7860                )? {
7861                    count += 1;
7862                    // One row per key group: the rest are the same value.
7863                    if distinct {
7864                        break;
7865                    }
7866                }
7867            }
7868        }
7869
7870        if !nulls_first {
7871            count += emit_null_rows(
7872                &mut emitted_rows,
7873                &mut eval_stack,
7874                &mut values,
7875                &mut visited,
7876                emit,
7877            )?;
7878        }
7879        Ok(Some(count))
7880    }
7881
7882    /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
7883    /// building an `OrderKey` vector per row.
7884    ///
7885    /// The row-returning sorted scan allocates twice per row: one
7886    /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
7887    /// projection. Counted over 400 k rows (r1030,
7888    /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
7889    /// allocations and 208 MB of traffic for an answer of four hundred
7890    /// thousand integers.
7891    ///
7892    /// The key half is pure ceremony on this shape.
7893    /// `sort_tagged_by_inline_int_key` already sorts indices rather than
7894    /// rows, so the per-row vector is built, has one integer taken out of
7895    /// it, and is then dragged through the permutation — it exists to carry
7896    /// a number the row's column already held. This lane carries the number
7897    /// instead, in a fixed-size array that lives inside the buffer element
7898    /// and allocates nothing. Same idea as the predicate VM's integer lane.
7899    ///
7900    /// Declines to `None` for anything it does not cover, and every caller
7901    /// falls through to the general path, so the gate list is the
7902    /// specification.
7903    ///
7904    /// Ties: equal keys keep scan order, as the stable sort on the general
7905    /// path does. Rows that tie on every ORDER BY term are entitled to any
7906    /// order among themselves either way — see `STABILITY.md`.
7907    fn try_int_key_sorted_stream<F>(
7908        &self,
7909        stmt: &SelectStatement,
7910        from: &FromClause,
7911        cancel: CancelToken<'_>,
7912        emit: &mut F,
7913    ) -> Result<Option<usize>, EngineError>
7914    where
7915        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
7916    {
7917        /// Sort terms this lane carries inline. Four covers every ORDER BY
7918        /// in the endpoint sweep and in the dogfood corpus; wider ones fall
7919        /// through rather than growing the buffer element for everybody.
7920        const MAX_KEYS: usize = 4;
7921
7922        if stmt.order_by.is_empty()
7923            || stmt.order_by.len() > MAX_KEYS
7924            || stmt.distinct
7925            || stmt.limit_with_ties
7926            || stmt.limit.is_some()
7927            || stmt.offset.is_some()
7928            || stmt.having.is_some()
7929            || stmt.group_by.is_some()
7930            || !stmt.unions.is_empty()
7931            || !from.joins.is_empty()
7932            || from.primary.lateral_subquery.is_some()
7933            || from.primary.unnest_expr.is_some()
7934            || from.primary.as_of_segment.is_some()
7935            || from.primary.generate_series_args.is_some()
7936            || select_has_window(stmt)
7937            || aggregate::uses_aggregate(stmt)
7938        {
7939            return Ok(None);
7940        }
7941        if stmt
7942            .items
7943            .iter()
7944            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
7945        {
7946            return Ok(None);
7947        }
7948        crate::orderby::check_order_by_legality(stmt)?;
7949        crate::orderby::check_order_by_positions(stmt)?;
7950        crate::window::reject_window_in_row_clauses(stmt)?;
7951        let Some(table) = self.active_catalog().get(&from.primary.name) else {
7952            return Ok(None);
7953        };
7954        if table.has_cold_rows_fast() {
7955            return Ok(None);
7956        }
7957        if !from.primary.only
7958            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7959        {
7960            return Ok(None);
7961        }
7962        let alias = from
7963            .primary
7964            .alias
7965            .as_deref()
7966            .unwrap_or(from.primary.name.as_str());
7967        let cols = table.schema().columns.clone();
7968
7969        // Every ORDER BY term must be a NOT NULL integer column of this
7970        // table. NOT NULL is what lets the key be a bare integer: with
7971        // NULLs the lane would have to carry their ordering too, and
7972        // getting that subtly wrong is the r1020 defect.
7973        let mut key_pos = [0usize; MAX_KEYS];
7974        let mut descs = [false; MAX_KEYS];
7975        // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
7976        // which the AST records as `None`; `unwrap_or(desc)` is how the
7977        // rest of the engine resolves it.
7978        let mut nulls_first = [false; MAX_KEYS];
7979        let n_keys = stmt.order_by.len();
7980        for (slot, order) in stmt.order_by.iter().enumerate() {
7981            let Expr::Column(oc) = &order.expr else {
7982                return Ok(None);
7983            };
7984            if let Some(q) = &oc.qualifier
7985                && !q.eq_ignore_ascii_case(alias)
7986            {
7987                return Ok(None);
7988            }
7989            let Some(pos) = cols
7990                .iter()
7991                .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
7992            else {
7993                return Ok(None);
7994            };
7995            if !matches!(
7996                cols[pos].ty,
7997                spg_storage::DataType::SmallInt
7998                    | spg_storage::DataType::Int
7999                    | spg_storage::DataType::BigInt
8000            ) {
8001                return Ok(None);
8002            }
8003            key_pos[slot] = pos;
8004            descs[slot] = order.desc;
8005            nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
8006        }
8007
8008        let sess = self.dml_session();
8009        let ctx = EvalContext::new(&cols, Some(alias))
8010            .with_catalog(self.active_catalog())
8011            .with_session(&sess);
8012        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8013        let columns: Vec<ColumnSchema> = projection
8014            .iter()
8015            .map(|p| {
8016                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8017                c.user_enum_type = p.user_enum_type.clone();
8018                c.mysql_fsp = p.mysql_fsp;
8019                c
8020            })
8021            .collect();
8022        let bound_pos: Vec<Option<usize>> = projection
8023            .iter()
8024            .map(|p| match &p.expr {
8025                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8026                    Ok(Some(pos)) => Some(pos),
8027                    _ => None,
8028                },
8029                _ => None,
8030            })
8031            .collect();
8032        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8033            .where_
8034            .as_ref()
8035            .filter(|w| crate::eval::fully_compilable(w))
8036            .map(|w| crate::eval::compile_expr(w, &ctx));
8037
8038        // The same first-observable point the materialising planner fires,
8039        // placed after the gates so it fires exactly once: this lane runs
8040        // BEFORE that planner and would otherwise be a hole in the
8041        // panic-isolation and cancellation-race coverage rather than a
8042        // faster path through it.
8043        crate::injection_point!("planner_first_row_fetch", &stmt.from);
8044
8045        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8046        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8047        let mut budget = ByteBudget::new(self.max_query_bytes);
8048        let snapshot = self.current_snapshot();
8049        // Keys, a NULL bit per key slot, and the row. The bitmask keeps
8050        // the element small: a nullable key still costs one bit rather
8051        // than a second array.
8052        let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
8053
8054        for (ri, row) in table.rows().iter().enumerate() {
8055            if ri.is_multiple_of(256) {
8056                cancel.check()?;
8057            }
8058            if !table.is_row_visible(ri, &snapshot) {
8059                continue;
8060            }
8061            // The key comes from the STORED row, before projection: an
8062            // ORDER BY column need not appear in the select list.
8063            let mut keys = [0i64; MAX_KEYS];
8064            let mut nulls = 0u8;
8065            let mut keyed = true;
8066            for slot in 0..n_keys {
8067                match row.values.get(key_pos[slot]) {
8068                    Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
8069                    Some(Value::Int(v)) => keys[slot] = i64::from(*v),
8070                    Some(Value::BigInt(v)) => keys[slot] = *v,
8071                    Some(Value::Null) | None => nulls |= 1 << slot,
8072                    // An integer column holding something else is a row
8073                    // this lane cannot order; hand the whole query back
8074                    // rather than guess at it.
8075                    _ => {
8076                        keyed = false;
8077                        break;
8078                    }
8079                }
8080            }
8081            if !keyed {
8082                return Ok(None);
8083            }
8084            if !Self::stream_filter_project(
8085                row,
8086                stmt.where_.as_ref(),
8087                compiled_where.as_ref(),
8088                &mut eval_stack,
8089                &projection,
8090                &bound_pos,
8091                &ctx,
8092                &mut values,
8093            )? {
8094                continue;
8095            }
8096            budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
8097            sorted.push((keys, nulls, core::mem::take(&mut values)));
8098            values.reserve(projection.len());
8099        }
8100
8101        sorted.sort_by(|a, b| {
8102            use core::cmp::Ordering;
8103            for slot in 0..n_keys {
8104                let bit = 1u8 << slot;
8105                let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
8106                    (true, true) => Ordering::Equal,
8107                    // Where the NULLs go is already decided — `nulls_first`
8108                    // resolved DESC's default when it was read. Reversing
8109                    // this for DESC as well would apply the direction
8110                    // twice and put them at the wrong end.
8111                    (true, false) => {
8112                        if nulls_first[slot] {
8113                            Ordering::Less
8114                        } else {
8115                            Ordering::Greater
8116                        }
8117                    }
8118                    (false, true) => {
8119                        if nulls_first[slot] {
8120                            Ordering::Greater
8121                        } else {
8122                            Ordering::Less
8123                        }
8124                    }
8125                    (false, false) => {
8126                        let o = a.0[slot].cmp(&b.0[slot]);
8127                        if descs[slot] { o.reverse() } else { o }
8128                    }
8129                };
8130                if ord != Ordering::Equal {
8131                    return ord;
8132                }
8133            }
8134            Ordering::Equal
8135        });
8136
8137        emit(crate::StreamItem::Header(&columns))?;
8138        let count = sorted.len();
8139        for (_, _, vals) in &sorted {
8140            emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
8141        }
8142        Ok(Some(count))
8143    }
8144
8145    fn try_spill_sorted_stream<F>(
8146        &self,
8147        stmt: &SelectStatement,
8148        from: &FromClause,
8149        cancel: CancelToken<'_>,
8150        emit: &mut F,
8151    ) -> Result<Option<usize>, EngineError>
8152    where
8153        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8154    {
8155        // The shapes `try_spill_sorted_scan` declines, plus the ones the
8156        // streaming executor does not carry (a LIMIT is already bounded
8157        // by a partial sort; the rest need the answer addressable).
8158        if !self.can_spill()
8159            || stmt.order_by.is_empty()
8160            || stmt.distinct
8161            || stmt.limit_with_ties
8162            || stmt.limit.is_some()
8163            || stmt.offset.is_some()
8164            || stmt.having.is_some()
8165            || stmt.group_by.is_some()
8166            || !stmt.unions.is_empty()
8167            || !from.joins.is_empty()
8168            || from.primary.lateral_subquery.is_some()
8169            || from.primary.unnest_expr.is_some()
8170            || from.primary.as_of_segment.is_some()
8171            || from.primary.generate_series_args.is_some()
8172            || select_has_window(stmt)
8173            || aggregate::uses_aggregate(stmt)
8174        {
8175            return Ok(None);
8176        }
8177        if stmt
8178            .items
8179            .iter()
8180            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8181        {
8182            return Ok(None);
8183        }
8184        // Everything `exec_bare_select_cancel` does before it scans runs
8185        // BELOW this path, so a statement claimed here skips it. Three of
8186        // those were missed on the way in and each was caught by a
8187        // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
8188        // ORDER BY 2` sorted happily instead of raising 42P10), the
8189        // cancellation check by another, the partition fan-out by the
8190        // differential corpus. What is reconciled, item by item: with-ties
8191        // needs ORDER BY (gated above), USING/NATURAL and RLS join
8192        // rewrites (joins gated above), the single-table RLS predicate
8193        // (the dispatcher declines a policy-subject table before this is
8194        // reached), the meta-view dispatch (those names are not in the
8195        // catalog, so the lookup below declines). These three are calls,
8196        // so the message and SQLSTATE are the ones the fall-back gives —
8197        // `select_has_window` above reads the select list and ORDER BY but
8198        // not WHERE, which is the case the third one covers.
8199        crate::orderby::check_order_by_legality(stmt)?;
8200        crate::orderby::check_order_by_positions(stmt)?;
8201        crate::window::reject_window_in_row_clauses(stmt)?;
8202        // A parent's rows are its children's. These walks scan the named
8203        // relation alone, so a partitioned or inherited parent comes back
8204        // short — and silently: the corpus caught `SELECT id FROM pr
8205        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8206        // parent's own rows instead of the partitions'. `ONLY` is exactly
8207        // the case that does not fan out, so it stays, which is the test
8208        // the FROM-clause fan-out itself makes.
8209        if !from.primary.only
8210            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8211        {
8212            return Ok(None);
8213        }
8214        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8215            return Ok(None);
8216        };
8217        // Cold-tier rows live outside `rows()`; this walk would drop
8218        // them silently, the same reason round 831's walk declines.
8219        if table.has_cold_rows_fast() {
8220            return Ok(None);
8221        }
8222
8223        let alias = from
8224            .primary
8225            .alias
8226            .as_deref()
8227            .unwrap_or(from.primary.name.as_str());
8228        let cols = table.schema().columns.clone();
8229        let sess = self.dml_session();
8230        let ctx = EvalContext::new(&cols, Some(alias))
8231            .with_catalog(self.active_catalog())
8232            .with_session(&sess);
8233        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8234        let order_by = stmt.order_by.clone();
8235        // The same one-shot resolution the general path does (round
8236        // 582): each ORDER BY column is bound once, not once per row.
8237        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8238        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8239        // Resolved BEFORE the scan, because it now decides what the sort
8240        // STORES and not just what it decodes (round 995).
8241        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8242
8243        let mut sorter = crate::extsort::ExternalSorter::new(
8244            self.temp_run_factory,
8245            self.session_work_mem_bytes(),
8246            cols.clone(),
8247            &descs,
8248        )
8249        .with_stats(&self.spill_stats)
8250        .with_pruned(&needed);
8251        let snapshot = self.current_snapshot();
8252        // One key buffer for the whole scan: `push` drains it and leaves
8253        // the capacity behind.
8254        let mut keys: Vec<OrderKey> = Vec::new();
8255        // r1024 — compile the predicate once for the scan.
8256        //
8257        // These two sorted-spill scans are the paths a single-table SELECT
8258        // with an ORDER BY takes, and they were the last row-returning ones
8259        // still walking the expression tree per row. r1023 did the
8260        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8261        // exactly this shape.
8262        //
8263        // Found from the profile's CALL TREE rather than its leaves. The
8264        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8265        // 261, `mod_op` 178 — and two attempts at reasoning out which
8266        // function asked for it were both wrong. The tree names the caller
8267        // chain, and it named this one.
8268        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8269            .where_
8270            .as_ref()
8271            .filter(|w| crate::eval::fully_compilable(w))
8272            .map(|w| crate::eval::compile_expr(w, &ctx));
8273        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8274        for (i, row) in table.scan_visible_from(0, &snapshot) {
8275            if i.is_multiple_of(256) {
8276                cancel.check()?;
8277            }
8278            if let Some(c) = &compiled_where {
8279                if !crate::eval::compiled::eval_compiled_pred(
8280                    c,
8281                    row,
8282                    &ctx,
8283                    &mut eval_stack,
8284                    ctx.mysql_dialect,
8285                )? {
8286                    continue;
8287                }
8288            } else if let Some(w) = &stmt.where_ {
8289                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8290                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8291                    continue;
8292                }
8293            }
8294            keys.clear();
8295            crate::orderby::build_order_keys_bound(&order_by, &order_bound, row, &ctx, &mut keys)?;
8296            sorter.push(&mut keys, row)?;
8297        }
8298
8299        let columns: Vec<ColumnSchema> = projection
8300            .iter()
8301            .map(|p| {
8302                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8303                c.user_enum_type = p.user_enum_type.clone();
8304                c.mysql_fsp = p.mysql_fsp;
8305                c
8306            })
8307            .collect();
8308        emit(crate::StreamItem::Header(&columns))?;
8309
8310        let key_ctx = &ctx;
8311        let mut emitted_since_check = 0usize;
8312        let n = sorter.finish_each(
8313            |src, buf| {
8314                crate::orderby::build_order_keys_bound(&order_by, &order_bound, src, key_ctx, buf)
8315            },
8316            |src, values| {
8317                for p in &projection {
8318                    values.push(
8319                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8320                    );
8321                }
8322                Ok(())
8323            },
8324            |cells| {
8325                // The merge is the long half of a big sort, and the scan's
8326                // check above stops running once it ends: a cancelled
8327                // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
8328                // anyway. Same stride as the scan.
8329                emitted_since_check += 1;
8330                if emitted_since_check >= 256 {
8331                    emitted_since_check = 0;
8332                    cancel.check()?;
8333                }
8334                emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
8335            },
8336        )?;
8337        Ok(Some(n))
8338    }
8339
8340    /// One row of the single-table streaming walk: the WHERE test, the
8341    /// projection, the emit. Returns whether a row was emitted.
8342    ///
8343    /// v7.39 (round 970) — factored out because the walk now has two ways
8344    /// to reach a row, the sequential scan and an index seek's candidate
8345    /// positions, and both must do IDENTICALLY this. A copy in each is how
8346    /// two paths for one job drift; this file already carries the cost of
8347    /// that lesson twice (rounds 823 and 961, both resolvers).
8348    ///
8349    /// `#[inline]` so the scan loop keeps the shape round 957 measured it
8350    /// in — a shared hot path pays for a new abstraction whether or not it
8351    /// uses it, and this one is on the scan.
8352    #[inline]
8353    #[allow(clippy::too_many_arguments)]
8354    fn stream_filter_project(
8355        row: &spg_storage::Row<'static>,
8356        where_: Option<&Expr>,
8357        // r1023 — the same WHERE, compiled once by the caller. `None` means
8358        // the expression did not qualify and `where_` is evaluated as before.
8359        compiled_where: Option<&crate::eval::CompiledExpr>,
8360        eval_stack: &mut Vec<Value<'static>>,
8361        projection: &[ProjectedItem],
8362        bound_pos: &[Option<usize>],
8363        ctx: &crate::eval::EvalContext<'_>,
8364        values: &mut Vec<Value<'static>>,
8365    ) -> Result<bool, EngineError> {
8366        // r1023 — this scan ran its predicate through the TREE INTERPRETER,
8367        // once per row, and it was the only row-returning path that did.
8368        // The aggregate path, `table_access`, and the PK walker all compile
8369        // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
8370        // server's live samples were `eval_expr` 99, `apply_binary` 81,
8371        // `mod_op` 29 — the interpreter, not delivery.
8372        //
8373        // The arithmetic accounted for it exactly. Over the wire, the same
8374        // filter costs 6.375 ms returning rows and 0.679 ms counting them;
8375        // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
8376        // which is what an interpreted predicate costs against the compiled
8377        // lane's 11.7. It was named "delivery after a filter" before this
8378        // profile, and it was never delivery.
8379        if let Some(c) = compiled_where {
8380            if !crate::eval::compiled::eval_compiled_pred(
8381                c,
8382                row,
8383                ctx,
8384                eval_stack,
8385                ctx.mysql_dialect,
8386            )? {
8387                return Ok(false);
8388            }
8389        } else if let Some(w) = where_ {
8390            let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
8391            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8392                return Ok(false);
8393            }
8394        }
8395        values.clear();
8396        for (p, bound) in projection.iter().zip(bound_pos) {
8397            values.push(match bound {
8398                Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
8399                None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
8400            });
8401        }
8402        Ok(true)
8403    }
8404
8405    /// The same filter and projection, then emit. Split from
8406    /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
8407    /// before it can emit them — a sort — runs the identical predicate and
8408    /// projection rather than a second copy of them.
8409    #[allow(clippy::too_many_arguments)]
8410    fn stream_project_row<F>(
8411        row: &spg_storage::Row<'static>,
8412        where_: Option<&Expr>,
8413        compiled_where: Option<&crate::eval::CompiledExpr>,
8414        eval_stack: &mut Vec<Value<'static>>,
8415        projection: &[ProjectedItem],
8416        bound_pos: &[Option<usize>],
8417        ctx: &crate::eval::EvalContext<'_>,
8418        values: &mut Vec<Value<'static>>,
8419        emit: &mut F,
8420    ) -> Result<bool, EngineError>
8421    where
8422        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8423    {
8424        if !Self::stream_filter_project(
8425            row,
8426            where_,
8427            compiled_where,
8428            eval_stack,
8429            projection,
8430            bound_pos,
8431            ctx,
8432            values,
8433        )? {
8434            return Ok(false);
8435        }
8436        emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
8437        Ok(true)
8438    }
8439
8440    fn try_stream_single_table<F>(
8441        &self,
8442        stmt: &SelectStatement,
8443        from: &FromClause,
8444        cancel: CancelToken<'_>,
8445        emit: &mut F,
8446    ) -> Result<Option<usize>, EngineError>
8447    where
8448        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8449    {
8450        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8451            return Ok(None);
8452        };
8453        // Cold-tier rows live outside `rows()`; the materialising fallback
8454        // covers both tiers and this walk would silently drop them.
8455        if table.has_cold_rows_fast() {
8456            return Ok(None);
8457        }
8458        let alias = from
8459            .primary
8460            .alias
8461            .as_deref()
8462            .unwrap_or(from.primary.name.as_str());
8463        let cols = table.schema().columns.clone();
8464        let sess = self.dml_session();
8465        let ctx = EvalContext::new(&cols, Some(alias))
8466            .with_catalog(self.active_catalog())
8467            .with_session(&sess);
8468        let projection = build_projection(&stmt.items, &cols, alias, self.backslash_escapes)?;
8469
8470        let columns: Vec<ColumnSchema> = projection
8471            .iter()
8472            .map(|p| {
8473                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8474                c.user_enum_type = p.user_enum_type.clone();
8475                c.mysql_fsp = p.mysql_fsp;
8476                c
8477            })
8478            .collect();
8479        emit(crate::StreamItem::Header(&columns))?;
8480
8481        // v7.37 (round 957) — resolve each bare-column projection ONCE
8482        // instead of once per row. `find_column_pos`-style resolution is a
8483        // linear walk of the schema comparing column-name strings, and the
8484        // row loop below ran it for every cell of every row: measured at
8485        // 400k rows, binding it out of the loop took `SELECT pad` from
8486        // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
8487        //
8488        // ORDER BY has bound its keys this way since round 582
8489        // (`order_by_bound_positions`); the projection never did.
8490        //
8491        // `locate_column` is the same resolution `resolve_column` performs,
8492        // returning the site instead of the value, so the two cannot drift
8493        // apart the way a second hand-written resolver would. Anything it
8494        // declines — an expression, a whole-row reference, a name that does
8495        // not resolve — binds to `None` and takes the general path below,
8496        // errors included, so an empty table still reports nothing rather
8497        // than raising at bind time.
8498        let bound_pos: Vec<Option<usize>> = projection
8499            .iter()
8500            .map(|p| match &p.expr {
8501                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8502                    Ok(Some(pos)) => Some(pos),
8503                    _ => None,
8504                },
8505                _ => None,
8506            })
8507            .collect();
8508
8509        // One snapshot for the whole scan, as the materialising path takes.
8510        let snapshot = self.current_snapshot();
8511
8512        // v7.39 (round 970) — ask the indices BEFORE walking the table.
8513        //
8514        // This walk had no index step at all, and it is preferred over the
8515        // materialising path, which does have one (`pick_indexed_rows` ->
8516        // `try_index_seek`). So a primary-key point lookup — the commonest
8517        // statement there is — read every row: measured on 500k rows,
8518        // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
8519        // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
8520        // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
8521        //
8522        // The control that named it: `... OFFSET 0` — semantically the same
8523        // query — answered in 0.159 ms, because OFFSET is one of the shape
8524        // gates that declines this walk and sends the statement to the path
8525        // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
8526        // no semantics in common; what they share is making this function
8527        // stand down.
8528        //
8529        // The seek only NARROWS: every candidate still goes through the
8530        // full WHERE below, exactly as the mutation paths use it, so a
8531        // partial index match cannot change an answer. Positions come back
8532        // already visibility-filtered and already capped at a quarter of the
8533        // table (round 490), so a seek can never cost more than the scan it
8534        // replaces, and `None` means "walk the table" as before.
8535        //
8536        // Sorted because the scan would have produced table order and the
8537        // index produces key order. Without an ORDER BY neither is promised,
8538        // but a walk that silently reorders its answer when an index happens
8539        // to exist is a difference nobody asked for.
8540        let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
8541            crate::index_access::try_index_seek_positions(w, &cols, table, alias, &snapshot)
8542        });
8543
8544        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8545        // r1023 — compile the predicate once for the whole scan. Same gate
8546        // every other path uses: `fully_compilable` or keep the interpreter,
8547        // so a shape the VM cannot take answers exactly as it did before.
8548        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8549            .where_
8550            .as_ref()
8551            .filter(|w| crate::eval::fully_compilable(w))
8552            .map(|w| crate::eval::compile_expr(w, &ctx));
8553        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8554        let mut count: usize = 0;
8555        match seek_positions {
8556            Some(mut positions) => {
8557                positions.sort_unstable();
8558                for (n, pos) in positions.into_iter().enumerate() {
8559                    if n.is_multiple_of(256) {
8560                        cancel.check()?;
8561                    }
8562                    let Some(row) = table.rows().get(pos) else {
8563                        continue;
8564                    };
8565                    if Self::stream_project_row(
8566                        row,
8567                        stmt.where_.as_ref(),
8568                        compiled_where.as_ref(),
8569                        &mut eval_stack,
8570                        &projection,
8571                        &bound_pos,
8572                        &ctx,
8573                        &mut values,
8574                        emit,
8575                    )? {
8576                        count += 1;
8577                    }
8578                }
8579            }
8580            None => {
8581                for (i, row) in table.scan_visible_from(0, &snapshot) {
8582                    if i.is_multiple_of(256) {
8583                        cancel.check()?;
8584                    }
8585                    if Self::stream_project_row(
8586                        row,
8587                        stmt.where_.as_ref(),
8588                        compiled_where.as_ref(),
8589                        &mut eval_stack,
8590                        &projection,
8591                        &bound_pos,
8592                        &ctx,
8593                        &mut values,
8594                        emit,
8595                    )? {
8596                        count += 1;
8597                    }
8598                }
8599            }
8600        }
8601        Ok(Some(count))
8602    }
8603
8604    pub(crate) fn try_exec_joined_streaming<F>(
8605        &self,
8606        stmt: &SelectStatement,
8607        cancel: CancelToken<'_>,
8608        emit: &mut F,
8609    ) -> Result<Option<usize>, EngineError>
8610    where
8611        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8612    {
8613        // Shape gates — keep the streamable surface narrow on
8614        // purpose. The fall-back path still handles everything else.
8615        let Some(from) = &stmt.from else {
8616            return Ok(None);
8617        };
8618        // v7.37 (round 830) — decline anything a row-security policy binds
8619        // for this session. Policies are injected in
8620        // `exec_bare_select_cancel`, below this path, so a statement claimed
8621        // here would read the table unfiltered: measured, `SELECT val FROM
8622        // sec` returned all three rows to a session whose policy allows two,
8623        // while `SELECT upper(val) FROM sec` — declined by the shape gates
8624        // and so materialised — returned the correct two.
8625        //
8626        // Declining sends it to the path that enforces. Teaching this one to
8627        // inject the predicate itself would keep the streaming benefit for
8628        // RLS tables and is the better end state; it is not what a
8629        // correctness fix should carry, and the fall-back is exactly as
8630        // correct, only slower.
8631        if self.select_reads_policy_subject_table(stmt) {
8632            return Ok(None);
8633        }
8634        // r1058 — a WITH list this path never materialises: the CTE
8635        // name would be resolved as a physical relation and error
8636        // ("relation \"big\" does not exist" over the extended
8637        // protocol, caught by the perm-runner's wire legs). The
8638        // materialising fallback owns CTE execution.
8639        if !stmt.ctes.is_empty() {
8640            return Ok(None);
8641        }
8642        // r1058 — rewritten system catalogs (`__spg_pg_stat_user_
8643        // tables` and kin) exist only as synth arms on the
8644        // materialising path; claiming one here errored "relation
8645        // does not exist" over the extended protocol for a query the
8646        // simple protocol answered. Prefix test only — a genuinely
8647        // missing relation must keep erroring in-path.
8648        if from.primary.name.starts_with("__spg_")
8649            || from
8650                .joins
8651                .iter()
8652                .any(|j| j.table.name.starts_with("__spg_"))
8653        {
8654            return Ok(None);
8655        }
8656        // r1058 — decline partitioned / inheritance parents, same
8657        // shape of bug as the RLS decline above: this path scans the
8658        // named table's own (empty) heap, so `SELECT id, region FROM
8659        // cust` on a partition parent streamed ZERO rows over the wire
8660        // while COUNT(*) — an aggregate, materialised below — said 3.
8661        // Caught by the perm-runner's server permutations; the
8662        // materialising fallback expands children correctly.
8663        if crate::partition::has_children(self.active_catalog(), &from.primary.name)
8664            || from
8665                .joins
8666                .iter()
8667                .any(|j| crate::partition::has_children(self.active_catalog(), &j.table.name))
8668        {
8669            return Ok(None);
8670        }
8671        // v7.39 (round 790) — single-table SELECTs stream too. This
8672        // gate said "joins only" because the path was written for
8673        // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
8674        // fell to the materialising fallback, which builds the whole
8675        // `Vec<Row<'static>>` and only then iterates it. Measured on
8676        // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
8677        // reached through a one-row JOIN — 2.6x, purely for lacking a
8678        // join. The deferred-join structure handles one source as the
8679        // degenerate stride-1 case, so the walk below is unchanged.
8680        let _single_table = from.joins.is_empty();
8681        // An ORDER BY that the bounded sort can serve streams; everything
8682        // else still falls to the materialising fallback below.
8683        // r1025 — an ordering the index already holds needs no sort at all.
8684        // Tried before the spill sort, which is the path it replaces.
8685        if !stmt.order_by.is_empty()
8686            && from.joins.is_empty()
8687            && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
8688        {
8689            return Ok(Some(n));
8690        }
8691        if !stmt.order_by.is_empty()
8692            && from.joins.is_empty()
8693            && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
8694        {
8695            return Ok(Some(n));
8696        }
8697        // r1031 — integer keys carried inline instead of an `OrderKey`
8698        // vector per row. Tried AFTER the spill sort on purpose: this lane
8699        // buffers the whole answer, so anything the spill path would take
8700        // must keep taking it rather than be turned back into an in-memory
8701        // sort that answers with a budget error.
8702        if !stmt.order_by.is_empty()
8703            && from.joins.is_empty()
8704            && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
8705        {
8706            return Ok(Some(n));
8707        }
8708        if !stmt.order_by.is_empty()
8709            || stmt.limit.is_some()
8710            || stmt.offset.is_some()
8711            || stmt.having.is_some()
8712            || stmt.group_by.is_some()
8713            || stmt.distinct
8714            || !stmt.unions.is_empty()
8715            || stmt.limit_with_ties
8716        {
8717            return Ok(None);
8718        }
8719        if aggregate::uses_aggregate(stmt) {
8720            return Ok(None);
8721        }
8722        // No window / SRF on the streaming path.
8723        if select_has_window(stmt) {
8724            return Ok(None);
8725        }
8726        if stmt
8727            .items
8728            .iter()
8729            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8730        {
8731            return Ok(None);
8732        }
8733        // v7.37 (round 831) — a joinless FROM over a plain stored table
8734        // never needs the deferred structure, and building one costs the
8735        // whole table. `materialise_table_ref_filtered` clones every row
8736        // into a `Vec<Row<'static>>` before anything is filtered or
8737        // projected, so peak cost tracks the TABLE, not the result:
8738        // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
8739        // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
8740        // projection saving nothing, while an arithmetic projection — which
8741        // the shape gates decline, so it materialises through the ordinary
8742        // executor — cost +21 MB.
8743        //
8744        // Scanning in batches and releasing each one is what `cursor_fill`
8745        // already does for a lazy cursor, and it is the same walk: resume
8746        // from a slot, take visible rows, evaluate, hand them over, drop
8747        // them. Round 800's finding stands and is why this reads rows OUT
8748        // rather than seeding the join by index — touching the stored
8749        // `PersistentVec` in place makes the whole table resident, which is
8750        // worse than the copy. Each batch is copied, then freed.
8751        if from.joins.is_empty()
8752            && from.primary.unnest_expr.is_none()
8753            && from.primary.lateral_subquery.is_none()
8754            && from.primary.as_of_segment.is_none()
8755            && from.primary.generate_series_args.is_none()
8756            && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
8757        {
8758            return Ok(Some(n));
8759        }
8760        // Build the deferred join under the regular byte budget.
8761        let mut budget = ByteBudget::new(self.max_query_bytes);
8762        let deferred = {
8763            let mut needed = alloc::collections::BTreeSet::new();
8764            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
8765            self.build_joined_filtered_rows(
8766                from,
8767                stmt.where_.as_ref(),
8768                cancel,
8769                if prunable { Some(&needed) } else { None },
8770                &mut budget,
8771            )?
8772        };
8773        let combined_schema = &deferred.combined_schema;
8774        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
8775        // `::regclass` / enum cast in a joined projection or HAVING needs it.
8776        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
8777        // the same predicate the unjoined shape carries.
8778        let joined_sess = self.dml_session();
8779        let ctx = EvalContext::new(combined_schema, None)
8780            .with_catalog(self.active_catalog())
8781            .with_session(&joined_sess);
8782        let projection =
8783            build_projection(&stmt.items, combined_schema, "", self.backslash_escapes)?;
8784        // Every projection item must be a bound qualified column —
8785        // anything that needs `eval_expr_with_correlated` keeps the
8786        // materialising path.
8787        let bound_pos = |e: &Expr| -> Option<usize> {
8788            match e {
8789                // v7.39 (round 822) — an UNQUALIFIED column resolves here
8790                // too. The `qualifier.is_some()` guard this replaces meant
8791                // `SELECT pad FROM big` — the commonest projection there is
8792                // — never reached the streaming walk: it fell out at this
8793                // gate and re-ran on the materialising path, after the
8794                // deferred join structure had already been built and paid
8795                // for. Measured (round 821, statement_timeout=120 over 400k
8796                // rows): `big.pad` and `b.pad` streamed and cancelled at
8797                // ~65k rows in 0.14 s, while bare `pad` ran to completion in
8798                // 0.80 s with the timeout never consulted. `find_column_pos`
8799                // has always handled the unqualified case (it falls through
8800                // to a by-name match), so the guard narrowed the gate for no
8801                // reason it recorded.
8802                Expr::Column(c) => eval::find_column_pos(c, &ctx),
8803                _ => None,
8804            }
8805        };
8806        let proj_decomposed: Vec<(usize, usize)> = {
8807            let mut out = Vec::with_capacity(projection.len());
8808            for p in &projection {
8809                let Some(abs) = bound_pos(&p.expr) else {
8810                    return Ok(None);
8811                };
8812                let Some(k) = deferred
8813                    .offsets
8814                    .partition_point(|&o| o <= abs)
8815                    .checked_sub(1)
8816                else {
8817                    return Ok(None);
8818                };
8819                out.push((k, abs - deferred.offsets[k]));
8820            }
8821            out
8822        };
8823        // Emit columns once.
8824        let columns: Vec<ColumnSchema> = projection
8825            .iter()
8826            // v7.39 (read01 round 54) — keep the column's enum identity through
8827            // the projection (it lives outside the DataType lattice), or a
8828            // derived table / UNION / windowed result forgets it and any outer
8829            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
8830            .map(|p| {
8831                let mut c = ColumnSchema::new(p.output_name.clone(), p.ty, p.nullable);
8832                c.user_enum_type = p.user_enum_type.clone();
8833                c.mysql_fsp = p.mysql_fsp;
8834                c
8835            })
8836            .collect();
8837        emit(crate::StreamItem::Header(&columns))?;
8838        let sources_ref = &deferred.sources;
8839        let stride = deferred.stride;
8840        let survivors_ref = &deferred.survivors;
8841        let n_surv = if stride == 0 {
8842            0
8843        } else {
8844            survivors_ref.len() / stride
8845        };
8846        // Reused per-row cell-ref scratch — pushes are zero-alloc
8847        // after the first row.
8848        let null_value = Value::Null;
8849        let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
8850        let mut count: usize = 0;
8851        for surv_i in 0..n_surv {
8852            if surv_i.is_multiple_of(256) {
8853                cancel.check()?;
8854            }
8855            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
8856            cell_refs.clear();
8857            for &(k, col_in_src) in &proj_decomposed {
8858                let ri = tuple[k];
8859                let v: &Value = if ri == usize::MAX {
8860                    &null_value
8861                } else {
8862                    sources_ref[k]
8863                        .get(ri)
8864                        .and_then(|r| r.values.get(col_in_src))
8865                        .unwrap_or(&null_value)
8866                };
8867                cell_refs.push(v);
8868            }
8869            emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
8870            count += 1;
8871        }
8872        Ok(Some(count))
8873    }
8874
8875    fn exec_joined_select(
8876        &self,
8877        stmt: &SelectStatement,
8878        from: &FromClause,
8879        cancel: CancelToken<'_>,
8880    ) -> Result<QueryResult, EngineError> {
8881        // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
8882        // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
8883        // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
8884        // FROM B WHERE B.k = A.k)` into
8885        //   SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
8886        //   WHERE B.k IS NULL
8887        // The general join executor builds a hash, probes every outer
8888        // tuple, materialises (left_padded_with_null) for every miss,
8889        // then runs the aggregate over the result set. For COUNT(*) we
8890        // only need the count — skip the tuple materialisation. Build
8891        // a HashSet of B's unique join values, scan A's PK index, and
8892        // increment the counter on each miss. PG's Merge Anti-Join
8893        // does roughly this; ours becomes a simple HashSet probe.
8894        if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
8895            return Ok(out);
8896        }
8897        // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
8898        // When ORDER BY is on an indexed primary column, walking the
8899        // btree in the requested direction lets the streamer break
8900        // after `LIMIT + OFFSET` survivors without ever materialising
8901        // the rest of the join — the 80 ms `mailrs_prod_not_exists`
8902        // plateau is exactly this shape.
8903        if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
8904            return Ok(out);
8905        }
8906        // v7.30.3 (mailrs round-26) — the bounded single-join path
8907        // first; peak memory scales with LIMIT instead of the table.
8908        if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
8909            return Ok(out);
8910        }
8911        // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
8912        // WHERE materialisation to the shared helper so the LATERAL
8913        // / UNNEST / regular-catalog paths route through one place.
8914        // (`build_joined_filtered_rows` carries LATERAL support as
8915        // of Phase 3.P0-41.) Downstream we still handle aggregate /
8916        // projection / ORDER BY / DISTINCT / LIMIT inline because
8917        // those depend on the SelectStatement's items list.
8918        let mut budget = ByteBudget::new(self.max_query_bytes);
8919        let deferred = {
8920            let mut needed = alloc::collections::BTreeSet::new();
8921            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
8922            self.build_joined_filtered_rows(
8923                from,
8924                stmt.where_.as_ref(),
8925                cancel,
8926                if prunable { Some(&needed) } else { None },
8927                &mut budget,
8928            )?
8929        };
8930        let combined_schema = &deferred.combined_schema;
8931        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
8932        // `::regclass` / enum cast in a joined projection or HAVING needs it.
8933        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
8934        // the same predicate the unjoined shape carries.
8935        let joined_sess = self.dml_session();
8936        let ctx = EvalContext::new(combined_schema, None)
8937            .with_catalog(self.active_catalog())
8938            .with_session(&joined_sess);
8939        // Aggregate path: handle GROUP BY / aggregate calls over the
8940        // joined+filtered rows.
8941        if aggregate::uses_aggregate(stmt) {
8942            // v7.32 (P4 borrow channel, increment 2) — borrow each
8943            // surviving join tuple as a RowRef::Tuple; the aggregate
8944            // engine reads source cells by reference (bound fast path =
8945            // zero clone) instead of consuming materialised combined
8946            // Rows. This is where the +211k materialise_tuple_vals
8947            // clones disappear for the join+aggregate shape.
8948            let refs = deferred.row_refs();
8949            // v7.29 — a per-query memo so correlated scalar
8950            // subqueries batch-evaluate once (group map) instead of
8951            // executing per group.
8952            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
8953            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
8954                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
8955                    .map_err(|err| match err {
8956                        EngineError::Eval(ev) => ev,
8957                        other => eval::EvalError::TypeMismatch {
8958                            detail: alloc::format!("{other}"),
8959                        },
8960                    })
8961            };
8962            let agg = aggregate::run(
8963                stmt,
8964                crate::join::AggRows::Refs(&refs),
8965                combined_schema,
8966                None,
8967                Some(&agg_correlated),
8968                self.parallel_runner.0.as_deref(),
8969                Some(self.active_catalog()),
8970                Some(self),
8971            )?;
8972            return self.finish_agg_result(agg, stmt, cancel);
8973        }
8974
8975        let projection =
8976            build_projection(&stmt.items, combined_schema, "", self.backslash_escapes)?;
8977        // v7.39 (round 734) — a set-returning projection over a JOIN.
8978        // This executor's projection loop treats every item as a scalar,
8979        // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
8980        // "function unnest(integer[]) does not exist" where PG expands
8981        // it. The row-set executor already carries the full SRF pipeline
8982        // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
8983        // sharding): materialise the joined survivors and hand over. The
8984        // WHERE is cleared — the join already applied it, and combined
8985        // columns resolve identically in both executors.
8986        if !self.srf_target_idxs(&projection).is_empty() {
8987            let refs = deferred.row_refs();
8988            let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
8989            let mut s2 = stmt.clone();
8990            s2.where_ = None;
8991            let schema = combined_schema.clone();
8992            return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
8993        }
8994        // v7.33 (P4 borrow channel, increment 3) — project directly off
8995        // the deferred row-index tuples instead of materialising an
8996        // intermediate combined Row per survivor. A bound qualified
8997        // column is read by reference (`RowRef::get` → `tuple_value`) and
8998        // cloned ONCE into the output row; the old `materialise()` (a full
8999        // combined Row plus a source→intermediate clone per referenced
9000        // cell, for every survivor) is gone. A row materialises on demand
9001        // only when a projection or ORDER BY expression needs the eval
9002        // path (subquery / function / arithmetic / unqualified column).
9003        // Same bind-once classification the aggregate input fast path uses
9004        // (`accumulate_groups`), reading the same `tuple_value` mapping the
9005        // differential gate already covers.
9006        let refs = deferred.row_refs();
9007        let bound_pos = |e: &Expr| -> Option<usize> {
9008            match e {
9009                Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
9010                _ => None,
9011            }
9012        };
9013        let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
9014        let all_proj_bound = proj_pos.iter().all(Option::is_some);
9015        // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
9016        // pre-decompose each bound projection position into
9017        // `(source_k, col_in_source)` so the per-row column read
9018        // skips the per-cell `tuple_value` partition_point + slice
9019        // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
9020        // calls) that walk dominated; this version reaches into
9021        // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
9022        let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
9023            .iter()
9024            .map(|p| {
9025                p.and_then(|abs| {
9026                    let k = deferred
9027                        .offsets
9028                        .partition_point(|&o| o <= abs)
9029                        .checked_sub(1)?;
9030                    Some((k, abs - deferred.offsets[k]))
9031                })
9032            })
9033            .collect();
9034        // v7.39 (round 962) — which projection items are whole-row
9035        // references, and to which join source. The test is
9036        // `locate_column` declining the name, which is the SAME resolver
9037        // the evaluation path uses, so this cannot drift from it: a real
9038        // column carrying an alias's name resolves to a position and is
9039        // not reported here. The source index comes from the alias
9040        // prefix, the way the combined schema names its columns.
9041        let whole_row_src: Vec<Option<usize>> = projection
9042            .iter()
9043            .map(|p| {
9044                let Expr::Column(c) = &p.expr else {
9045                    return None;
9046                };
9047                if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
9048                    return None;
9049                }
9050                let prefix = alloc::format!("{name}.", name = c.name);
9051                let abs = deferred
9052                    .combined_schema
9053                    .iter()
9054                    .position(|s| s.name.starts_with(&prefix))?;
9055                deferred
9056                    .offsets
9057                    .partition_point(|&o| o <= abs)
9058                    .checked_sub(1)
9059            })
9060            .collect();
9061        // ORDER BY (when present) still evaluates against a materialised
9062        // Row — keep the order-key encoder correct rather than fork it.
9063        let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
9064        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
9065        let mut proj_memo = memoize::MemoizeCache::default();
9066        let sources_ref = &deferred.sources;
9067        let stride = deferred.stride;
9068        let survivors_ref = &deferred.survivors;
9069        let n_surv = survivors_ref.len() / stride.max(1);
9070        // v7.38 (read01 B8) — streaming top-N budget (see the sibling
9071        // single-table path). Bounds this JOIN projection's accumulator
9072        // to O(keep) for `ORDER BY … LIMIT k`.
9073        let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
9074            && !stmt.distinct
9075            && !stmt.limit_with_ties
9076            && !self.env_cfg().disable_topk
9077        {
9078            stmt.limit_literal().and_then(|l| {
9079                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
9080                (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
9081            })
9082        } else {
9083            None
9084        };
9085        // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
9086        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
9087            hashbrown::HashMap::new();
9088        let distinct_hb = hashbrown::DefaultHashBuilder::default();
9089        for surv_i in 0..n_surv {
9090            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
9091            let row = &refs[surv_i];
9092            let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
9093                Some(row.as_row())
9094            } else {
9095                None
9096            };
9097            let mut values = Vec::with_capacity(projection.len());
9098            for (i, p) in projection.iter().enumerate() {
9099                if let Some((k, col_in_src)) = proj_decomposed[i] {
9100                    // v7.36 — direct (source_k, col) lookup, no
9101                    // partition_point. tuple[k] is the row index in
9102                    // sources[k]; LEFT-NULL slots are `usize::MAX`.
9103                    let ri = tuple[k];
9104                    let v: Value<'static> = if ri == usize::MAX {
9105                        Value::Null
9106                    } else {
9107                        sources_ref[k]
9108                            .get(ri)
9109                            .and_then(|r| r.values.get(col_in_src))
9110                            .cloned()
9111                            .map(Value::into_owned)
9112                            .unwrap_or(Value::Null)
9113                    };
9114                    values.push(v);
9115                } else if let Some(pos) = proj_pos[i] {
9116                    // Bound but couldn't decompose (shouldn't normally
9117                    // happen — keep as a safe path).
9118                    values.push(
9119                        row.get(pos)
9120                            .cloned()
9121                            .map(Value::into_owned)
9122                            .unwrap_or(Value::Null),
9123                    );
9124                } else if let Some(k) = whole_row_src[i]
9125                    && tuple[k] == usize::MAX
9126                {
9127                    // v7.39 (round 962) — a whole-row reference to a side
9128                    // an OUTER join null-extended is NULL, not a
9129                    // composite whose fields are all NULL. PG18.4 answers
9130                    // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
9131                    // an empty cell; round 961 answered `(,)`.
9132                    //
9133                    // The evaluator below cannot tell the two apart: it
9134                    // reads the MATERIALISED combined row, where a
9135                    // null-extended side is indistinguishable from a real
9136                    // row whose every column is NULL — and that row is
9137                    // `(,)` in PG too, so guessing by "all fields NULL"
9138                    // would trade one wrong answer for another. The
9139                    // tuple, which is still in hand here, does know:
9140                    // `usize::MAX` is the sentinel the join writes for
9141                    // exactly this.
9142                    values.push(Value::Null);
9143                } else {
9144                    // Eval path — `materialised` is Some whenever any
9145                    // projection item is non-bound (need_eval_row true).
9146                    // v7.24 (round-16 B) — select-list subqueries under a
9147                    // JOIN go through the correlated-aware evaluator too.
9148                    let mrow = materialised.as_deref().expect("materialised for eval");
9149                    values.push(self.eval_expr_with_correlated(
9150                        &p.expr,
9151                        mrow,
9152                        &ctx,
9153                        cancel,
9154                        Some(&mut proj_memo),
9155                    )?);
9156                }
9157            }
9158            let out_row = Row::new(values);
9159            // v7.37.16 — streaming DISTINCT (see the scan-path twin):
9160            // probe on the projected row; duplicates skip the
9161            // build_order_keys eval and never enter `tagged`.
9162            if stmt.distinct {
9163                let bucket = seen_distinct
9164                    .entry(norm_hash_row(&out_row, &distinct_hb, ctx.mysql_dialect))
9165                    .or_default();
9166                if bucket
9167                    .iter()
9168                    .any(|i| row_eq_norm(&tagged[i].1, &out_row, ctx.mysql_dialect))
9169                {
9170                    continue;
9171                }
9172                bucket.push(tagged.len());
9173            }
9174            let order_keys = if stmt.order_by.is_empty() {
9175                Vec::new()
9176            } else {
9177                let mrow = materialised.as_deref().expect("materialised for order by");
9178                build_order_keys(&stmt.order_by, mrow, &ctx)?
9179            };
9180            budget.charge(approx_row_bytes(&out_row))?;
9181            tagged.push((order_keys, out_row));
9182            if let Some((k, descs)) = &topk_stream {
9183                topk_trim(&mut tagged, *k, descs);
9184            }
9185        }
9186        if !stmt.order_by.is_empty() {
9187            // v7.38 元机制 D acceptor — see other call site above.
9188            let keep = if self.env_cfg().disable_topk {
9189                None
9190            } else {
9191                stmt.limit_literal()
9192                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
9193            };
9194            let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
9195            // v7.39 (round 688) — the join's ORDER BY resolves its keys
9196            // against `ctx`, which is built from `build_combined_schema`, so
9197            // this is where a declared collation reaches the sort. There was
9198            // exactly ONE resolver call in the engine before this — the
9199            // single-table scan's — which is why every other shape sorted by
9200            // bytes no matter what the schemas carried.
9201            let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
9202            crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &colls);
9203        }
9204        let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
9205        apply_offset_and_limit(
9206            &mut output_rows,
9207            stmt.offset_literal(),
9208            stmt.limit_literal(),
9209        );
9210        let columns: Vec<ColumnSchema> = projection
9211            .into_iter()
9212            .map(|p| {
9213                let mut c = ColumnSchema::new(p.output_name, p.ty, p.nullable);
9214                c.user_enum_type = p.user_enum_type;
9215                c.collation_name = p.collation_name;
9216                c.mysql_fsp = p.mysql_fsp;
9217                c
9218            })
9219            .collect();
9220        Ok(QueryResult::Rows {
9221            columns,
9222            rows: output_rows,
9223        })
9224    }
9225}
9226
9227impl Engine {
9228    /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
9229    /// by id, decodes each row body against the table's current
9230    /// schema, applies the SELECT's projection + optional WHERE +
9231    /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
9232    /// / ORDER BY are unsupported on this path (STABILITY carve-
9233    /// out); operators wanting them should restore the segment
9234    /// into a regular table first.
9235    fn exec_select_as_of_segment(
9236        &self,
9237        stmt: &SelectStatement,
9238        from: &spg_sql::ast::FromClause,
9239        segment_id: u32,
9240    ) -> Result<QueryResult, EngineError> {
9241        // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
9242        // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
9243        if !from.joins.is_empty()
9244            || stmt.group_by.is_some()
9245            || stmt.having.is_some()
9246            || !stmt.unions.is_empty()
9247            || !stmt.order_by.is_empty()
9248            || stmt.offset.is_some()
9249            || stmt.distinct
9250            || aggregate::uses_aggregate(stmt)
9251        {
9252            return Err(EngineError::Unsupported(
9253                "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
9254                 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
9255                    .into(),
9256            ));
9257        }
9258        let table = self
9259            .active_catalog()
9260            .get(&from.primary.name)
9261            .ok_or_else(|| StorageError::TableNotFound {
9262                name: from.primary.name.clone(),
9263            })?;
9264        let schema = table.schema().clone();
9265        let schema_cols = &schema.columns;
9266        let alias = from
9267            .primary
9268            .alias
9269            .as_deref()
9270            .unwrap_or(from.primary.name.as_str());
9271        let ctx = self.ev_ctx(schema_cols, Some(alias));
9272        let seg = self
9273            .active_catalog()
9274            .cold_segment(segment_id)
9275            .ok_or_else(|| {
9276                EngineError::Unsupported(alloc::format!(
9277                    "AS OF SEGMENT: cold segment {segment_id} not registered"
9278                ))
9279            })?;
9280        let mut out_rows: Vec<Row<'static>> = Vec::new();
9281        let mut limit_remaining: Option<usize> =
9282            stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
9283        for (_key, body) in seg.scan() {
9284            let (row, _consumed) =
9285                spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
9286                    .map_err(EngineError::Storage)?;
9287            if let Some(where_expr) = &stmt.where_ {
9288                let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
9289                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9290                    continue;
9291                }
9292            }
9293            // Projection.
9294            let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
9295            out_rows.push(projected);
9296            if let Some(rem) = limit_remaining.as_mut() {
9297                if *rem == 0 {
9298                    out_rows.pop();
9299                    break;
9300                }
9301                *rem -= 1;
9302            }
9303        }
9304        // Output column schema: derive from SELECT items.
9305        let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
9306        Ok(QueryResult::Rows {
9307            columns,
9308            rows: out_rows,
9309        })
9310    }
9311
9312    /// v6.10.2 — simple-path WHERE eval that doesn't go through
9313    /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
9314    /// scan paths predicate against a snapshot frozen segment, no
9315    /// cross-row state.
9316    fn eval_expr_simple(
9317        &self,
9318        expr: &Expr,
9319        row: &Row<'static>,
9320        ctx: &EvalContext,
9321    ) -> Result<Value<'static>, EngineError> {
9322        let cancel = CancelToken::none();
9323        self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
9324    }
9325}
9326
9327// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
9328
9329/// One row-producing projection: an expression to evaluate, the resulting
9330/// column's user-visible name, its inferred type, and nullability.
9331#[derive(Debug, Clone)]
9332pub(crate) struct ProjectedItem {
9333    pub(crate) expr: Expr,
9334    pub(crate) output_name: String,
9335    pub(crate) ty: DataType,
9336    pub(crate) nullable: bool,
9337    /// v7.39 (read01 round 54) — a projected enum column keeps its enum
9338    /// identity. Enum-ness lives outside the DataType lattice (the value is a
9339    /// Text), so a projection that dropped this made the RESULT schema forget
9340    /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
9341    /// that schema, silently fell back to TEXT order instead of member order.
9342    pub(crate) user_enum_type: Option<String>,
9343    /// v7.39 (round 425) — a projected MySQL temporal column keeps its
9344    /// declared fractional-seconds precision, so the renderer can pad to
9345    /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
9346    /// a whole second). Like `user_enum_type` this lives outside the
9347    /// DataType lattice, so a projection that dropped it made the RESULT
9348    /// schema forget how wide the fraction should print.
9349    pub(crate) mysql_fsp: Option<u8>,
9350    /// v7.39 (round 688) — and its declared collation, the third thing to
9351    /// live outside the DataType lattice and the third to be lost the same
9352    /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
9353    /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
9354    /// projection rebuilt the output column and the ORDER BY resolves
9355    /// against THAT schema.
9356    pub(crate) collation_name: Option<String>,
9357}
9358
9359/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
9360/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
9361/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
9362/// the spec's "two NULLs are not distinct"; the second is a tolerated
9363/// quirk for v1 (no NaN literals are reachable from the SQL surface).
9364/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
9365fn expr_is_aggregate_call(e: &Expr) -> bool {
9366    match e {
9367        Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
9368        Expr::AggregateOrdered { .. } => true,
9369        _ => false,
9370    }
9371}
9372
9373/// Collect distinct top-level aggregate call expressions (dedup by value). Does
9374/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
9375/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
9376/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
9377/// than today — never a regression on a working query).
9378fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
9379    if expr_is_aggregate_call(e) {
9380        if !out.iter().any(|x| x == e) {
9381            out.push(e.clone());
9382        }
9383        return;
9384    }
9385    match e {
9386        Expr::Binary { lhs, rhs, .. } => {
9387            collect_agg_exprs(lhs, out);
9388            collect_agg_exprs(rhs, out);
9389        }
9390        Expr::Unary { expr, .. }
9391        | Expr::Cast { expr, .. }
9392        | Expr::IsNull { expr, .. }
9393        | Expr::BoolTest { expr, .. }
9394        | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
9395        Expr::FunctionCall { args, .. } => {
9396            for a in args {
9397                collect_agg_exprs(a, out);
9398            }
9399        }
9400        Expr::Like { expr, pattern, .. } => {
9401            collect_agg_exprs(expr, out);
9402            collect_agg_exprs(pattern, out);
9403        }
9404        Expr::Extract { source, .. } => collect_agg_exprs(source, out),
9405        Expr::WindowFunction {
9406            args,
9407            partition_by,
9408            order_by,
9409            ..
9410        } => {
9411            for a in args {
9412                collect_agg_exprs(a, out);
9413            }
9414            for p in partition_by {
9415                collect_agg_exprs(p, out);
9416            }
9417            for (o, _, _) in order_by {
9418                collect_agg_exprs(o, out);
9419            }
9420        }
9421        _ => {}
9422    }
9423}
9424
9425/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
9426fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
9427    if expr_is_aggregate_call(e) {
9428        if let Some(idx) = aggs.iter().position(|x| x == e) {
9429            *e = Expr::Column(ColumnName {
9430                qualifier: None,
9431                name: alloc::format!("__agg{idx}"),
9432            });
9433        }
9434        return;
9435    }
9436    match e {
9437        Expr::Binary { lhs, rhs, .. } => {
9438            replace_agg_exprs(lhs, aggs);
9439            replace_agg_exprs(rhs, aggs);
9440        }
9441        Expr::Unary { expr, .. }
9442        | Expr::Cast { expr, .. }
9443        | Expr::IsNull { expr, .. }
9444        | Expr::BoolTest { expr, .. }
9445        | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
9446        Expr::FunctionCall { args, .. } => {
9447            for a in args {
9448                replace_agg_exprs(a, aggs);
9449            }
9450        }
9451        Expr::Like { expr, pattern, .. } => {
9452            replace_agg_exprs(expr, aggs);
9453            replace_agg_exprs(pattern, aggs);
9454        }
9455        Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
9456        Expr::WindowFunction {
9457            args,
9458            partition_by,
9459            order_by,
9460            ..
9461        } => {
9462            for a in args {
9463                replace_agg_exprs(a, aggs);
9464            }
9465            for p in partition_by {
9466                replace_agg_exprs(p, aggs);
9467            }
9468            for (o, _, _) in order_by {
9469                replace_agg_exprs(o, aggs);
9470            }
9471        }
9472        _ => {}
9473    }
9474}
9475
9476/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
9477/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
9478/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
9479/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
9480/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
9481/// Returns None outside the bounded subset (leaves current behaviour). Only fires
9482/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
9483/// window-only / aggregate-only queries.
9484fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
9485    if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
9486        return None;
9487    }
9488    // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
9489    if !stmt.unions.is_empty() {
9490        return None;
9491    }
9492    let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
9493    if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
9494        return None;
9495    }
9496    stmt.from.as_ref()?;
9497    // Collect the aggregate calls to hoist from projection + outer ORDER BY.
9498    let mut aggs: Vec<Expr> = Vec::new();
9499    for item in &stmt.items {
9500        if let SelectItem::Expr { expr, .. } = item {
9501            collect_agg_exprs(expr, &mut aggs);
9502        }
9503    }
9504    for ob in &stmt.order_by {
9505        collect_agg_exprs(&ob.expr, &mut aggs);
9506    }
9507    // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
9508    let mut inner_items: Vec<SelectItem> = Vec::new();
9509    for g in &group_cols {
9510        inner_items.push(SelectItem::Expr {
9511            expr: g.clone(),
9512            alias: None,
9513        });
9514    }
9515    for (i, a) in aggs.iter().enumerate() {
9516        inner_items.push(SelectItem::Expr {
9517            expr: a.clone(),
9518            alias: Some(alloc::format!("__agg{i}")),
9519        });
9520    }
9521    let inner = SelectStatement {
9522        items: inner_items,
9523        distinct: false,
9524        distinct_on: Vec::new(),
9525        unions: Vec::new(),
9526        order_by: Vec::new(),
9527        limit: None,
9528        offset: None,
9529        limit_with_ties: false,
9530        window_check_exprs: Vec::new(),
9531        ..stmt.clone()
9532    };
9533    let derived = TableRef {
9534        name: "__aggwin".into(),
9535        alias: Some("__aggwin".into()),
9536        only: false,
9537        as_of_segment: None,
9538        unnest_expr: None,
9539        unnest_column_aliases: Vec::new(),
9540        with_ordinality: false,
9541        generate_series_args: None,
9542        lateral_subquery: Some(alloc::boxed::Box::new(inner)),
9543        jsonb_each_text_arg: None,
9544        table_fn_call: None,
9545        rows_from: None,
9546        json_table: None,
9547        scalar_fn_item: false,
9548    };
9549    // Outer window query over the derived rows: aggregates → __aggN column refs.
9550    let mut outer_items = stmt.items.clone();
9551    for item in &mut outer_items {
9552        if let SelectItem::Expr { expr, alias } = item {
9553            // Preserve PG's column label for a bare aggregate projection.
9554            if alias.is_none()
9555                && let Expr::FunctionCall { name, .. } = expr
9556                && crate::aggregate::is_aggregate_name(name)
9557            {
9558                *alias = Some(name.to_ascii_lowercase());
9559            }
9560            replace_agg_exprs(expr, &aggs);
9561        }
9562    }
9563    let mut outer_order = stmt.order_by.clone();
9564    for ob in &mut outer_order {
9565        replace_agg_exprs(&mut ob.expr, &aggs);
9566    }
9567    let mut outer_distinct_on = stmt.distinct_on.clone();
9568    for e in &mut outer_distinct_on {
9569        replace_agg_exprs(e, &aggs);
9570    }
9571    Some(SelectStatement {
9572        locking: None,
9573        ctes: Vec::new(),
9574        distinct: stmt.distinct,
9575        distinct_on: outer_distinct_on,
9576        items: outer_items,
9577        from: Some(FromClause {
9578            primary: derived,
9579            joins: Vec::new(),
9580        }),
9581        where_: None,
9582        group_by: None,
9583        group_by_all: false,
9584        having: None,
9585        unions: Vec::new(),
9586        order_by: outer_order,
9587        limit: stmt.limit.clone(),
9588        offset: stmt.offset.clone(),
9589        limit_with_ties: stmt.limit_with_ties,
9590        window_check_exprs: Vec::new(),
9591    })
9592}
9593
9594/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
9595/// membership.
9596///
9597/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
9598/// there?", and all four answered by scanning the whole right side once per
9599/// left row. The cost was (left rows x right rows), which is why
9600/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
9601/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
9602/// row that does not pays for all of it. Over 100k left rows, raising the
9603/// right side from 100 to 10,000 took 35 ms to 2848.
9604///
9605/// This is the shape round 485 already solved for DISTINCT, and it reuses
9606/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
9607/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
9608/// every bucket with the exact comparator, so a collision costs time and
9609/// never an answer.
9610struct PeerIndex<'r> {
9611    bh: hashbrown::DefaultHashBuilder,
9612    buckets: hashbrown::HashMap<u64, Vec<usize>>,
9613    rows: &'r [Row<'static>],
9614    mysql: bool,
9615}
9616
9617impl<'r> PeerIndex<'r> {
9618    fn build(rows: &'r [Row<'static>], mysql: bool) -> Self {
9619        // ONE hasher for the whole pass: the default builder is seeded per
9620        // instance, so a fresh one per row would put equal rows in different
9621        // buckets.
9622        let bh = hashbrown::DefaultHashBuilder::default();
9623        let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
9624            hashbrown::HashMap::with_capacity(rows.len());
9625        for (i, r) in rows.iter().enumerate() {
9626            buckets
9627                .entry(norm_hash_row(r, &bh, mysql))
9628                .or_default()
9629                .push(i);
9630        }
9631        Self {
9632            bh,
9633            buckets,
9634            rows,
9635            mysql,
9636        }
9637    }
9638
9639    fn contains(&self, r: &Row<'static>) -> bool {
9640        let h = norm_hash_row(r, &self.bh, self.mysql);
9641        self.buckets
9642            .get(&h)
9643            .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.mysql)))
9644    }
9645
9646    /// Remove ONE occurrence, so the multiset forms cancel row for row the
9647    /// way the pool they replaced did.
9648    fn take_one(&mut self, r: &Row<'static>) -> bool {
9649        let h = norm_hash_row(r, &self.bh, self.mysql);
9650        let Some(b) = self.buckets.get_mut(&h) else {
9651            return false;
9652        };
9653        let Some(pos) = b
9654            .iter()
9655            .position(|&i| row_eq_norm(&self.rows[i], r, self.mysql))
9656        else {
9657            return false;
9658        };
9659        b.swap_remove(pos);
9660        true
9661    }
9662}
9663
9664pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, mysql: bool) -> Vec<Row<'static>> {
9665    dedup_by_row(rows, |r| r, mysql)
9666}
9667
9668/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
9669/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
9670/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
9671/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
9672/// order is preserved, and correctness needs only the one-way guarantee
9673/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
9674/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
9675fn dedup_by_row<T>(items: Vec<T>, row_of: impl Fn(&T) -> &Row<'static>, mysql: bool) -> Vec<T> {
9676    if items.len() <= 32 {
9677        let mut out: Vec<T> = Vec::with_capacity(items.len());
9678        for it in items {
9679            if !out
9680                .iter()
9681                .any(|seen| row_eq_norm(row_of(seen), row_of(&it), mysql))
9682            {
9683                out.push(it);
9684            }
9685        }
9686        return out;
9687    }
9688    // ONE BuildHasher instance for the whole pass — the default builder
9689    // is randomly seeded PER INSTANCE, so a fresh one per row would give
9690    // equal rows different hashes and never dedup.
9691    let bh = hashbrown::DefaultHashBuilder::default();
9692    let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
9693    let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
9694        hashbrown::HashMap::with_capacity(items.len());
9695    for it in items {
9696        let h = norm_hash_row(row_of(&it), &bh, mysql);
9697        let bucket = buckets.entry(h).or_default();
9698        if !bucket
9699            .iter()
9700            .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), mysql))
9701        {
9702            bucket.push(out.len());
9703            out.push(it);
9704        }
9705    }
9706    out
9707}
9708
9709/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
9710/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
9711/// rows may collide (buckets are re-checked with the exact comparator).
9712///
9713/// Domain design mirrors `value_cmp`'s equivalence classes:
9714/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
9715///   shares one domain: a value that is an integer fitting i64 hashes the
9716///   i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
9717///   anything else hashes the f64 approximation computed by THE SAME
9718///   formula the value_cmp float arms use (`numeric_to_f64`), so
9719///   `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
9720///   hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
9721///   Known un-closable corner: an integer in [2^53, 2^63) can compare
9722///   Equal to a float via value_cmp's lossy f64 arm while hashing in the
9723///   exact-i64 domain — mixed int/float rows at that magnitude may miss a
9724///   dedup (PG itself compares int8↔float8 in the lossy float8 domain).
9725/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
9726///   compares them blank-insensitively; plain Text pairs that differ only
9727///   in trailing blanks merely collide and are separated exactly).
9728/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
9729///   hash their fields under a distinct tag.
9730/// - Everything value_cmp falls back to debug-format ordering for
9731///   (Json, arrays, vectors, geometry, ranges, …) shares one constant
9732///   bucket — degrades to the exact linear scan, never wrong.
9733fn norm_hash_row(row: &Row<'static>, bh: &hashbrown::DefaultHashBuilder, mysql: bool) -> u64 {
9734    norm_hash_values(&row.values, bh, mysql)
9735}
9736
9737/// v7.39 (round 485) — the same hash over a bare value slice, so the
9738/// DISTINCT probe can run against a reused buffer instead of demanding a
9739/// `Row` that has to be allocated first (see `values_eq_norm`).
9740fn norm_hash_values(
9741    values: &[Value<'static>],
9742    bh: &hashbrown::DefaultHashBuilder,
9743    mysql: bool,
9744) -> u64 {
9745    use core::hash::{BuildHasher, Hash, Hasher};
9746    let mut h = bh.build_hasher();
9747    for v in values {
9748        // v7.39 (round 410) — hash the folded key when the MySQL collation
9749        // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
9750        // `'A'` vs `'a '`) share a hash bucket.
9751        if mysql {
9752            if let Some(folded) = mysql_dedup_fold(v) {
9753                folded.hash(&mut h);
9754                continue;
9755            }
9756        }
9757        norm_hash_value(v, &mut h);
9758    }
9759    h.finish()
9760}
9761
9762/// r1044 — `10^p` as an `i128`, or `None` past what one holds.
9763///
9764/// `i128::MAX` is about 1.7e38, so 10^38 is the last power that fits.
9765const fn pow10_i128(p: u16) -> Option<i128> {
9766    const P: [i128; 39] = {
9767        let mut t = [1i128; 39];
9768        let mut i = 1;
9769        while i < 39 {
9770            t[i] = t[i - 1] * 10;
9771            i += 1;
9772        }
9773        t
9774    };
9775    if (p as usize) < P.len() {
9776        Some(P[p as usize])
9777    } else {
9778        None
9779    }
9780}
9781
9782fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
9783    const TAG_NULL: u8 = 0;
9784    const TAG_BOOL: u8 = 1;
9785    const TAG_NUM_I64: u8 = 2;
9786    const TAG_NUM_F64: u8 = 3;
9787    const TAG_TEXT: u8 = 4;
9788    const TAG_DATE: u8 = 6;
9789    const TAG_TIME: u8 = 7;
9790    const TAG_TIMESTAMP: u8 = 8;
9791    const TAG_TIMETZ: u8 = 10;
9792    const TAG_UUID: u8 = 11;
9793    const TAG_MONEY: u8 = 12;
9794    const TAG_BYTES: u8 = 13;
9795    const TAG_INTERVAL: u8 = 14;
9796    const TAG_CHAR1: u8 = 15;
9797    const TAG_OPAQUE: u8 = 255;
9798    // One shared writer for the numeric family: an integer value
9799    // representable as i64 goes exact (round-trip probe — no_std, so no
9800    // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
9801    // through 0i64, folding it into 0.0 as value_cmp requires.
9802    let num_f64 = |h: &mut H, x: f64| {
9803        if x.is_nan() {
9804            h.write_u8(TAG_NUM_F64);
9805            h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
9806            return;
9807        }
9808        const TWO63: f64 = 9_223_372_036_854_775_808.0;
9809        if (-TWO63..TWO63).contains(&x) {
9810            #[allow(clippy::cast_possible_truncation)]
9811            let n = x as i64;
9812            #[allow(clippy::cast_precision_loss)]
9813            if (n as f64) == x {
9814                h.write_u8(TAG_NUM_I64);
9815                h.write_i64(n);
9816                return;
9817            }
9818        }
9819        h.write_u8(TAG_NUM_F64);
9820        h.write_u64(x.to_bits());
9821    };
9822    match v {
9823        Value::Null => h.write_u8(TAG_NULL),
9824        Value::Bool(b) => {
9825            h.write_u8(TAG_BOOL);
9826            h.write_u8(u8::from(*b));
9827        }
9828        Value::SmallInt(n) => {
9829            h.write_u8(TAG_NUM_I64);
9830            h.write_i64(i64::from(*n));
9831        }
9832        Value::Int(n) => {
9833            h.write_u8(TAG_NUM_I64);
9834            h.write_i64(i64::from(*n));
9835        }
9836        Value::BigInt(n) => {
9837            h.write_u8(TAG_NUM_I64);
9838            h.write_i64(*n);
9839        }
9840        Value::Float(x) => num_f64(h, *x),
9841        Value::Numeric {
9842            scaled,
9843            scale,
9844            kind,
9845        } => match kind {
9846            spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
9847            spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
9848            spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
9849            spg_storage::NumericKind::Finite => {
9850                // Reduce trailing fractional zeros so 1.50 and 1.5 share a
9851                // representation, then: exact integers fitting i64 go to the
9852                // i64 domain; everything else uses numeric_to_f64 — the SAME
9853                // formula value_cmp's Numeric↔Float arm compares with.
9854                // r1044 — the reduction is required (`1.5` and `1.50` are
9855                // one value and must land in one bucket) and it used to
9856                // walk one digit at a time. That is O(scale), and scale
9857                // is not small in practice: `n / 100` on a NUMERIC
9858                // column stores `9.1900000000000000`, scale 16, so the
9859                // loop ran fourteen times PER ROW.
9860                //
9861                // Priced by ablation rather than guessed at — removing
9862                // the loop entirely took `SELECT DISTINCT n FROM t ORDER
9863                // BY n` over 400,000 rows from 52 ms to 14.8, against
9864                // PostgreSQL's 12.2-13.8. Two `pow10` lookup tables
9865                // tried first moved it not at all, which is why this one
9866                // was measured before it was written.
9867                //
9868                // Binary search over the same powers finds the whole
9869                // run of trailing zeros in at most six tests and one
9870                // division, instead of one test and one division per
9871                // digit.
9872                let (mut s, mut sc) = (*scaled, *scale);
9873                if sc > 0 && s != 0 {
9874                    let mut lo: u16 = 0;
9875                    let mut hi: u16 = sc;
9876                    while lo < hi {
9877                        let mid = (lo + hi).div_ceil(2);
9878                        match pow10_i128(mid) {
9879                            Some(p) if s % p == 0 => lo = mid,
9880                            _ => hi = mid - 1,
9881                        }
9882                    }
9883                    if lo > 0 {
9884                        if let Some(p) = pow10_i128(lo) {
9885                            s /= p;
9886                            sc -= lo;
9887                        }
9888                    }
9889                }
9890                if sc == 0 {
9891                    if let Ok(n) = i64::try_from(s) {
9892                        h.write_u8(TAG_NUM_I64);
9893                        h.write_i64(n);
9894                    } else {
9895                        num_f64(h, crate::orderby::numeric_to_f64(s, 0));
9896                    }
9897                } else {
9898                    num_f64(h, crate::orderby::numeric_to_f64(s, sc));
9899                }
9900            }
9901        },
9902        // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
9903        // value that also fits i128 reuses the Numeric path above so
9904        // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
9905        // any i128-representable value — constant bucket is safe.
9906        Value::NumericBig(b) => match b.to_i128() {
9907            Some(s) => norm_hash_value(
9908                &Value::Numeric {
9909                    scaled: s,
9910                    scale: b.scale(),
9911                    kind: spg_storage::NumericKind::Finite,
9912                },
9913                h,
9914            ),
9915            None => h.write_u8(TAG_OPAQUE),
9916        },
9917        // value_cmp compares Text↔BpChar blank-insensitively (both sides
9918        // trimmed), so both hash the trimmed bytes. Text pairs differing
9919        // only in trailing blanks collide and are split exactly in-bucket.
9920        Value::Text(s) | Value::BpChar(s) => {
9921            h.write_u8(TAG_TEXT);
9922            h.write(s.trim_end_matches(' ').as_bytes());
9923        }
9924        Value::Char1(c) => {
9925            h.write_u8(TAG_CHAR1);
9926            h.write_u8(*c);
9927        }
9928        Value::Date(d) => {
9929            h.write_u8(TAG_DATE);
9930            h.write_i32(*d);
9931        }
9932        Value::Time(t) => {
9933            h.write_u8(TAG_TIME);
9934            h.write_i64(*t);
9935        }
9936        Value::Timestamp(t) => {
9937            h.write_u8(TAG_TIMESTAMP);
9938            h.write_i64(*t);
9939        }
9940        Value::TimeTz { us, offset_secs } => {
9941            h.write_u8(TAG_TIMETZ);
9942            h.write_i64(*us);
9943            h.write_i32(*offset_secs);
9944        }
9945        Value::Uuid(u) => {
9946            h.write_u8(TAG_UUID);
9947            h.write(u);
9948        }
9949        Value::Money(c) => {
9950            h.write_u8(TAG_MONEY);
9951            h.write_i64(*c);
9952        }
9953        Value::Bytes(b) => {
9954            h.write_u8(TAG_BYTES);
9955            h.write(b.as_ref());
9956        }
9957        Value::Interval {
9958            months,
9959            days,
9960            micros,
9961        } => {
9962            h.write_u8(TAG_INTERVAL);
9963            h.write_i32(*months);
9964            h.write_i32(*days);
9965            h.write_i64(*micros);
9966        }
9967        // v7.37.16 — REAL joined the numeric value_cmp family (widened
9968        // to f64, same formulas as the arms), so it hashes in the shared
9969        // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
9970        // f32→f64 is exact, so equal-under-cmp implies equal bits here.
9971        Value::Real(x) => num_f64(h, f64::from(*x)),
9972        // Json (structural equality), vector families (float rendering),
9973        // arrays / geometry / net / ranges / composites (debug-format
9974        // fallback): one constant bucket — exact linear within.
9975        _ => h.write_u8(TAG_OPAQUE),
9976    }
9977}
9978
9979/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
9980/// treats numerically-equal exact values as one regardless of type or scale
9981/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
9982/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
9983/// `Row` `==` would keep them distinct.
9984/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
9985/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
9986/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
9987/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
9988/// the folded comparison key for a text value, None for anything else (which
9989/// keeps the byte-exact `value_cmp` path).
9990fn mysql_dedup_fold(v: &Value) -> Option<String> {
9991    match v {
9992        Value::Text(s) | Value::BpChar(s) => {
9993            Some(spg_storage::mysql_ci_fold(s.trim_end_matches(' ')))
9994        }
9995        _ => None,
9996    }
9997}
9998
9999/// v7.39 (round 485) — how many projected rows the single-table scan
10000/// builds, and how many of those the DISTINCT probe throws away again.
10001///
10002/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
10003/// 21 % of all samples in malloc/free called straight from the scan
10004/// closure. The closure's one per-row allocation is the projected
10005/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
10006/// instructions later — but "most" is a guess until it is a number, so
10007/// these count it. (Round 480 was spent acting on an inference about a
10008/// branch that turned out never to run.)
10009/// v7.39 (round 488) — reachability counters for round 487's projection
10010/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
10011/// and a never-called-function probe rules out code layout — so the
10012/// question is whether that shape reaches this code at all, which is a
10013/// number, not an inference.
10014pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10015pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10016
10017pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10018pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
10019    core::sync::atomic::AtomicU64::new(0);
10020
10021pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, mysql: bool) -> bool {
10022    values_eq_norm(&a.values, &b.values, mysql)
10023}
10024
10025/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
10026/// DISTINCT probe can compare a reused projection buffer against a kept
10027/// row without building a `Row` for it.
10028pub(crate) fn values_eq_norm(a: &[Value<'static>], b: &[Value<'static>], mysql: bool) -> bool {
10029    a.len() == b.len()
10030        && a.iter().zip(b).all(|(x, y)| {
10031            if mysql {
10032                if let (Some(fx), Some(fy)) = (mysql_dedup_fold(x), mysql_dedup_fold(y)) {
10033                    return fx == fy;
10034                }
10035            }
10036            crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
10037        })
10038}
10039
10040/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
10041/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
10042/// order via the byte values; vectors are not sortable.
10043pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
10044    // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
10045    // so values sharing a ≥6-byte common prefix (`product_001` vs
10046    // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
10047    // order by their exact bytes instead of the old lossy f64 coarse key.
10048    // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
10049    // matches PG's default C / binary text collation. Every other type
10050    // keeps the lossless-enough `f64` fast path below.
10051    if let Value::Text(s) = v {
10052        return Ok(OrderKey::Text(s.as_ref().into()));
10053    }
10054    // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
10055    // byte order (PG bpcharcmp under C collation), so mixed-pad values of
10056    // the same logical string order equal.
10057    if let Value::BpChar(s) = v {
10058        return Ok(OrderKey::Text(s.trim_end_matches(' ').into()));
10059    }
10060    // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
10061    // carry the parsed value and compare it structurally (see
10062    // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
10063    if let Value::Json(s) = v {
10064        return Ok(match crate::json::parse(s) {
10065            Ok(jv) => OrderKey::Json(jv),
10066            Err(_) => OrderKey::Text(s.as_ref().into()),
10067        });
10068    }
10069    // v7.37 — byte-orderable types PG sorts byte-wise but that have no
10070    // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
10071    // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
10072    // matching PG's network ordering.
10073    match v {
10074        Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
10075        // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
10076        Value::NumericBig(b) => {
10077            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
10078                spg_storage::NumericKey::from_big(b),
10079            )));
10080        }
10081        Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
10082        Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
10083        Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
10084        Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
10085        Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
10086            let mut key = alloc::vec::Vec::with_capacity(18);
10087            key.push(*family);
10088            key.extend_from_slice(addr);
10089            key.push(*bits);
10090            return Ok(OrderKey::Bytes(key));
10091        }
10092        _ => {}
10093    }
10094    // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
10095    // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
10096    // own OrderKey so integer arrays sort numerically; a NULL element rides to
10097    // the end via the +INF sentinel.
10098    let inf = || OrderKey::NullBig;
10099    let arr = match v {
10100        Value::IntArray(a) => Some(
10101            a.iter()
10102                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10103                .collect(),
10104        ),
10105        Value::SmallIntArray(a) => Some(
10106            a.iter()
10107                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10108                .collect(),
10109        ),
10110        Value::BigIntArray(a) => Some(
10111            a.iter()
10112                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10113                .collect(),
10114        ),
10115        Value::BoolArray(a) => Some(
10116            a.iter()
10117                .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
10118                .collect(),
10119        ),
10120        Value::TextArray(a) => Some(
10121            a.iter()
10122                .map(|o| o.as_ref().map_or_else(inf, |s| OrderKey::Text(s.clone())))
10123                .collect(),
10124        ),
10125        #[allow(clippy::cast_precision_loss)]
10126        Value::FloatArray(a) => Some(
10127            a.iter()
10128                .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
10129                .collect(),
10130        ),
10131        // r1040 — array elements take the same exact key their scalar
10132        // form does; an f64 projection here would order `{0.1}` against
10133        // `{0.1000000000000000001}` by luck.
10134        Value::NumericArray(a) => Some(
10135            a.iter()
10136                .map(|o| {
10137                    o.map_or_else(inf, |(m, s)| {
10138                        OrderKey::Numeric(alloc::boxed::Box::new(
10139                            spg_storage::NumericKey::from_numeric(
10140                                m,
10141                                s,
10142                                spg_storage::NumericKind::Finite,
10143                            ),
10144                        ))
10145                    })
10146                })
10147                .collect(),
10148        ),
10149        Value::DateArray(a) => Some(
10150            a.iter()
10151                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
10152                .collect(),
10153        ),
10154        _ => None,
10155    };
10156    if let Some(elements) = arr {
10157        return Ok(OrderKey::Array(elements));
10158    }
10159    // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
10160    // right, which is exactly the lexicographic element order an Array key
10161    // already gives: `(2,'b') < (9,'a')` because the leading field decides.
10162    if let Value::Composite(fields) = v {
10163        let elements = fields
10164            .iter()
10165            .map(|(_, fv)| value_to_order_key(fv))
10166            .collect::<Result<alloc::vec::Vec<_>, _>>()?;
10167        return Ok(OrderKey::Array(elements));
10168    }
10169    // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
10170    // Projecting these to f64 (the historic path) silently collapses BigInt /
10171    // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
10172    // the wrong order for large ids and microsecond timestamps.
10173    match v {
10174        Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
10175        Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
10176        Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
10177        // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
10178        // integer (days / micros / cents / calendar year); TIMETZ by the
10179        // UTC-equivalent micros (local wall - offset) so the same physical
10180        // instant in different zones sorts equal.
10181        Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
10182        Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
10183        Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
10184        Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
10185        Value::TimeTz { us, offset_secs } => {
10186            return Ok(OrderKey::Int(
10187                i128::from(*us) - i128::from(*offset_secs) * 1_000_000,
10188            ));
10189        }
10190        Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
10191        _ => {}
10192    }
10193    let num = match v {
10194        // Callers without NULLS FIRST/LAST context (array elements,
10195        // histogram sampling) put NULL last, as before.
10196        Value::Null => return Ok(OrderKey::NullBig),
10197        // v7.17.0 Phase 3.P0-38 — range ordering is not supported
10198        // in v7.17.0 (needs lex-then-inclusivity tiebreak).
10199        Value::Range { .. } => {
10200            return Err(EngineError::Unsupported(
10201                "ORDER BY of a range value is not supported in v7.17.0".into(),
10202            ));
10203        }
10204        // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
10205        Value::Hstore(_) => {
10206            return Err(EngineError::Unsupported(
10207                "ORDER BY of a hstore value is not supported".into(),
10208            ));
10209        }
10210        // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
10211        Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
10212            return Err(EngineError::Unsupported(
10213                "ORDER BY of a 2D array is not supported in v7.17.0".into(),
10214            ));
10215        }
10216        // r1039/r1040 — the exact canonical key, not an f64 projection.
10217        //
10218        // r1039 fixed the three specials, which carry a canonical zero in
10219        // `scaled` and so all sorted as the number 0. The projection
10220        // itself was the rest of the defect: "precision losses here only
10221        // matter for tie-breaks well past 15 significant digits" was the
10222        // comment, and the measurement disagreed — f64 called
10223        // `0.1` and `0.1000000000000000001` Equal, and a stable sort then
10224        // returned them in insertion order. Three of ten values came back
10225        // in the wrong place against PG18.4.
10226        Value::Numeric {
10227            scaled,
10228            scale,
10229            kind,
10230        } => {
10231            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
10232                spg_storage::NumericKey::from_numeric(*scaled, *scale, *kind),
10233            )));
10234        }
10235        Value::Float(x) => *x,
10236        // v7.37.16 — REAL sorts by its exact f64 widening (it had no
10237        // arm and fell through to the unsupported error).
10238        Value::Real(x) => f64::from(*x),
10239        Value::Bool(b) => {
10240            if *b {
10241                1.0
10242            } else {
10243                0.0
10244            }
10245        }
10246        Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
10247            return Err(EngineError::Unsupported(
10248                "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
10249            ));
10250        }
10251        // v7.37 — PG orders INTERVAL by its total time, treating a month as
10252        // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
10253        // f64 is exact for any interval under ~285 years, and only ORDER BY
10254        // tie-breaks past that magnitude lose precision. Matches the
10255        // min/max(interval) comparator in aggregate.rs.
10256        #[allow(clippy::cast_precision_loss)]
10257        Value::Interval {
10258            months,
10259            days,
10260            micros,
10261        } => {
10262            let total = i128::from(*months) * 30 * 86_400_000_000
10263                + i128::from(*days) * 86_400_000_000
10264                + i128::from(*micros);
10265            total as f64
10266        }
10267        Value::Json(_) => {
10268            return Err(EngineError::Unsupported(
10269                "ORDER BY of a JSON value is not supported — cast the document to text first"
10270                    .into(),
10271            ));
10272        }
10273        // v7.5.0 — Value is #[non_exhaustive]; future variants need
10274        // an explicit ORDER BY mapping. Surface as Unsupported until
10275        // engine support is added.
10276        _ => {
10277            return Err(EngineError::Unsupported(
10278                "ORDER BY of this value type is not supported".into(),
10279            ));
10280        }
10281    };
10282    Ok(OrderKey::Num(num))
10283}
10284
10285/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
10286/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
10287/// `EngineError` so the projection-build path keeps `UnknownQualifier`
10288/// vs `ColumnNotFound` distinct.
10289/// PG's name for the physical row identity. It is reserved there — no table
10290/// can have a column called this — which is what lets `*` skip it by name.
10291pub(crate) const CTID_COLUMN: &str = "ctid";
10292
10293/// v7.39 (round 512) — PG's system columns, in the order they are appended.
10294/// All six are reserved names there, which is what lets `*` skip them and
10295/// lets a scan tell them from a user column without a flag.
10296pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
10297
10298/// Is this name one of them?
10299pub(crate) fn is_system_column(name: &str) -> bool {
10300    SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
10301}
10302
10303/// Where the scan's appended system columns begin, if this schema carries
10304/// them: the trailing six, named in order. A catalog view with a column of
10305/// its own called `xmin` does not match, which is the point.
10306fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
10307    let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
10308    cols[start..]
10309        .iter()
10310        .zip(SYSTEM_COLUMNS)
10311        .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
10312        .then_some(start)
10313}
10314
10315/// v7.39 (round 540) — which positions `*` must skip.
10316///
10317/// The rule stays round 512's — the synthetic columns are the trailing
10318/// six of a relation's block, matched by POSITION so a genuine `xmin`
10319/// column is not lost — but a JOINED schema names its columns
10320/// `alias.column` and lays the peers out end to end, so a peer's six sit
10321/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
10322/// "trailing six" test back on the block it was written for.
10323fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
10324    let mut skip = alloc::vec![false; cols.len()];
10325    fn qualifier(n: &str) -> Option<&str> {
10326        n.rsplit_once('.').map(|(q, _)| q)
10327    }
10328    fn bare(n: &str) -> &str {
10329        n.rsplit('.').next().unwrap_or(n)
10330    }
10331    let mut i = 0;
10332    while i < cols.len() {
10333        let q = qualifier(&cols[i].name);
10334        let mut end = i;
10335        while end < cols.len() && qualifier(&cols[end].name) == q {
10336            end += 1;
10337        }
10338        if let Some(start) = (end - i)
10339            .checked_sub(SYSTEM_COLUMNS.len())
10340            .map(|off| i + off)
10341            && cols[start..end]
10342                .iter()
10343                .zip(SYSTEM_COLUMNS)
10344                .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
10345        {
10346            for s in skip.iter_mut().take(end).skip(start) {
10347                *s = true;
10348            }
10349        }
10350        i = end;
10351    }
10352    skip
10353}
10354
10355/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
10356/// read? Only then is the column materialised.
10357pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
10358    let mut found = false;
10359    crate::expr_analysis::visit_expr_columns_and_subqueries(
10360        e,
10361        &mut |c| {
10362            if is_system_column(&c.name) {
10363                found = true;
10364            }
10365        },
10366        &mut |_| {},
10367    );
10368    found
10369}
10370
10371fn references_ctid(stmt: &SelectStatement) -> bool {
10372    let in_expr = expr_references_ctid;
10373    stmt.items.iter().any(|i| match i {
10374        SelectItem::Expr { expr, .. } => in_expr(expr),
10375        _ => false,
10376    }) || stmt.where_.as_ref().is_some_and(in_expr)
10377        || stmt.order_by.iter().any(|o| in_expr(&o.expr))
10378        || stmt
10379            .group_by
10380            .as_ref()
10381            .is_some_and(|g| g.iter().any(in_expr))
10382        || stmt.having.as_ref().is_some_and(in_expr)
10383}
10384
10385/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
10386/// is a name the projection has to TYPE before any row exists.
10387///
10388/// Evaluation has answered this since round T9 (`resolve_column` builds a
10389/// `Value::Composite` of every column), but the typing side below had no
10390/// such branch and raised `column "t" does not exist` first — so the
10391/// feature was unreachable through a projection. Measured against PG18.4:
10392/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
10393///
10394/// The type is `Jsonb` + a composite marker, which is exactly how a
10395/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
10396/// the value travels as a `Value::Composite` and renders in the canonical
10397/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
10398/// so the marker names the alias and no rehydration keys off it — the
10399/// value arrives already built.
10400fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
10401    let mut s = ColumnSchema::new(
10402        alloc::string::String::from(alias),
10403        spg_storage::DataType::Jsonb,
10404        true,
10405    );
10406    s.user_composite_type = Some(alloc::string::String::from(alias));
10407    s
10408}
10409
10410pub(crate) fn resolve_projection_column<'a>(
10411    c: &ColumnName,
10412    schema_cols: &'a [ColumnSchema],
10413    table_alias: &str,
10414) -> Result<Cow<'a, ColumnSchema>, EngineError> {
10415    if let Some(q) = &c.qualifier {
10416        let composite = alloc::format!("{q}.{name}", name = c.name);
10417        if let Some(s) = schema_cols.iter().find(|s| s.name == composite) {
10418            return Ok(Cow::Borrowed(s));
10419        }
10420        // Single-table case: the qualifier may equal the active alias —
10421        // then look for the bare column name.
10422        if q == table_alias
10423            && let Some(s) = schema_cols.iter().find(|s| s.name == c.name)
10424        {
10425            return Ok(Cow::Borrowed(s));
10426        }
10427        // For multi-table schemas the qualifier is unknown only if no
10428        // column bears the "<q>." prefix. For single-table, the alias
10429        // mismatch alone is enough.
10430        let prefix = alloc::format!("{q}.");
10431        let qualifier_known =
10432            q == table_alias || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
10433        if !qualifier_known {
10434            return Err(EngineError::Eval(EvalError::UnknownQualifier {
10435                qualifier: q.clone(),
10436            }));
10437        }
10438        return Err(EngineError::Eval(EvalError::ColumnNotFound {
10439            name: c.name.clone(),
10440        }));
10441    }
10442    if let Some(s) = schema_cols.iter().find(|s| s.name == c.name) {
10443        return Ok(Cow::Borrowed(s));
10444    }
10445    let suffix = alloc::format!(".{name}", name = c.name);
10446    let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
10447    let first = matches.next();
10448    let extra = matches.next();
10449    match (first, extra) {
10450        (Some(s), None) => Ok(Cow::Borrowed(s)),
10451        (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
10452            detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
10453        })),
10454        // The whole-row reference, checked LAST so a real column carrying
10455        // the alias's name still wins — the same precedence
10456        // `resolve_column` applies on the evaluation side.
10457        //
10458        // Two schema shapes reach here. A single-table (or subquery, or
10459        // CTE) scan carries its alias and bare column names, so the name
10460        // has to equal the alias. A JOIN's combined schema carries no
10461        // alias at all and qualifies every column `alias.col`, so the
10462        // alias is identified by the prefix instead — which is exactly
10463        // how `whole_row_composite` picks the fields out on the
10464        // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
10465        // answers `(7,z)` on PG18.4 and errored here until this arm
10466        // covered the joined shape too.
10467        _ if !table_alias.is_empty() && c.name == table_alias => {
10468            Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
10469        }
10470        _ if table_alias.is_empty() && {
10471            let prefix = alloc::format!("{name}.", name = c.name);
10472            schema_cols.iter().any(|s| s.name.starts_with(&prefix))
10473        } =>
10474        {
10475            Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
10476        }
10477        _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
10478            name: c.name.clone(),
10479        })),
10480    }
10481}
10482
10483/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
10484/// parser to carry per-branch GROUPING() masks into a grouping-set query's
10485/// ORDER BY. They must never reach the output. No-op unless such a column is
10486/// present, so the common path is untouched.
10487/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
10488///
10489/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
10490/// a `LIMIT 2` that should have answered two groups answered one.
10491fn apply_deferred_limit(
10492    rows: alloc::vec::Vec<Row<'static>>,
10493    deferred: &(
10494        Option<spg_sql::ast::LimitExpr>,
10495        Option<spg_sql::ast::LimitExpr>,
10496    ),
10497) -> alloc::vec::Vec<Row<'static>> {
10498    let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
10499        Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
10500        _ => None,
10501    };
10502    let mut rows = rows;
10503    if let Some(off) = count(&deferred.1) {
10504        rows = rows.split_off(off.min(rows.len()));
10505    }
10506    if let Some(lim) = count(&deferred.0) {
10507        rows.truncate(lim);
10508    }
10509    rows
10510}
10511
10512fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
10513    let QueryResult::Rows { columns, rows } = result else {
10514        return result;
10515    };
10516    if !columns.iter().any(|c| c.name.starts_with("__grp_ord_")) {
10517        return QueryResult::Rows { columns, rows };
10518    }
10519    let keep: Vec<usize> = columns
10520        .iter()
10521        .enumerate()
10522        .filter(|(_, c)| !c.name.starts_with("__grp_ord_"))
10523        .map(|(i, _)| i)
10524        .collect();
10525    let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
10526    let new_rows: Vec<Row<'static>> = rows
10527        .into_iter()
10528        .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
10529        .collect();
10530    QueryResult::Rows {
10531        columns: new_cols,
10532        rows: new_rows,
10533    }
10534}
10535
10536/// v7.39 (round 487) — bind every projection item that is a bare column
10537/// reference to its position, once per query.
10538///
10539/// `#[inline(never)]` and out of line on purpose. Round 486 established
10540/// that adding code inside these scan bodies moves neighbouring hot
10541/// functions around under fat LTO: the first version of this had the loop
10542/// inline in `run_single_table_scan` and four aggregate shapes that never
10543/// touch that function — `full_agg`, `join_agg`, `group_500k`,
10544/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
10545/// the same machine. Keeping it out of line kept them still.
10546#[inline(never)]
10547fn bind_direct_columns(
10548    projection: &[ProjectedItem],
10549    ctx: &eval::EvalContext<'_>,
10550) -> Vec<Option<usize>> {
10551    projection
10552        .iter()
10553        .map(|p| match &p.expr {
10554            Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
10555                // Same exclusion `compile_into` makes: a composite column
10556                // has to be rehydrated from stored JSON, which is not a
10557                // cell read.
10558                ctx.columns
10559                    .get(*pos)
10560                    .is_none_or(|sc| sc.user_composite_type.is_none())
10561            }),
10562            _ => None,
10563        })
10564        .collect()
10565}
10566
10567/// v7.39 (round 505) — the name an un-aliased projected expression reports.
10568///
10569/// PG18 names a call for its function and everything else `?column?`;
10570/// measured with `\gdesc`. SPG used to print the parsed expression back
10571/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
10572/// name-keyed row access found nothing under `upper`.
10573///
10574/// The MySQL half is NOT this rule and is deliberately left alone here:
10575/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
10576/// which needs the parser to hand over spans the AST does not carry yet.
10577/// Until it does, a MySQL session keeps the printed form — closer to what
10578/// MariaDB answers than `?column?` would be.
10579pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
10580    if mysql {
10581        return expr.to_string();
10582    }
10583    spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
10584}
10585
10586pub(crate) fn build_projection(
10587    items: &[SelectItem],
10588    schema_cols: &[ColumnSchema],
10589    table_alias: &str,
10590    mysql: bool,
10591) -> Result<Vec<ProjectedItem>, EngineError> {
10592    build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0)
10593}
10594
10595/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
10596/// invisible to `*`.
10597///
10598/// The windowed-SELECT path appends a synthetic `__win_N` column per window
10599/// function so the rewritten projection can reference the computed values as
10600/// ordinary columns. `*` then expanded them too, and
10601/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
10602/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
10603/// silent one: the row simply had one more field than the client asked for.
10604///
10605/// Hidden by POSITION rather than by name, for the reason round 512 recorded
10606/// about the system columns: a name test looks safe until a real column
10607/// happens to carry the name. These are appended last, so the count is what
10608/// identifies them.
10609pub(crate) fn build_projection_hiding_tail(
10610    items: &[SelectItem],
10611    schema_cols: &[ColumnSchema],
10612    table_alias: &str,
10613    mysql: bool,
10614    hidden_tail: usize,
10615) -> Result<Vec<ProjectedItem>, EngineError> {
10616    let visible = schema_cols.len().saturating_sub(hidden_tail);
10617    // v7.39 (round 462) — a join's combined schema qualifies every column
10618    // `alias.col` so the deferred-join cell lookups resolve by composite
10619    // name. That is an internal convention, and `*` was handing it to the
10620    // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
10621    // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
10622    // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
10623    // already learned this for `q.*`; plain `*` never got the same rule.
10624    //
10625    // The signal is the schema itself, not the call site: only a combined
10626    // join schema arrives with no table alias AND every column qualified.
10627    // A single-table schema carries its alias, an empty schema has nothing
10628    // to strip, and a synthetic schema's names carry no dot.
10629    let joined_schema = table_alias.is_empty()
10630        && !schema_cols.is_empty()
10631        && schema_cols.iter().all(|c| c.name.contains('.'));
10632    let bare_name = |name: &str| -> String {
10633        if !joined_schema {
10634            return name.to_string();
10635        }
10636        match name.split_once('.') {
10637            Some((_, rest)) if !rest.is_empty() => rest.to_string(),
10638            _ => name.to_string(),
10639        }
10640    };
10641    let mut out = Vec::new();
10642    for item in items {
10643        match item {
10644            SelectItem::Wildcard => {
10645                // v7.39 (round 511) — `*` never expands a system column, as
10646                // PG's does not. They join the schema only when the statement
10647                // asked for them, so this matters for the mixed shape
10648                // `SELECT *, ctid FROM t`.
10649                //
10650                // v7.39 (round 512) — by POSITION, not by name. Matching on
10651                // the name alone looked safe because PG reserves them, and it
10652                // is not: `pg_replication_slots` genuinely has a column called
10653                // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
10654                // Only the trailing six, in the order the scan appends them,
10655                // are the synthetic ones.
10656                let sys_skip = synthetic_system_positions(schema_cols);
10657                for (idx, col) in schema_cols.iter().enumerate() {
10658                    if sys_skip[idx] || idx >= visible {
10659                        continue;
10660                    }
10661                    out.push(ProjectedItem {
10662                        expr: Expr::Column(ColumnName {
10663                            qualifier: None,
10664                            name: col.name.clone(),
10665                        }),
10666                        output_name: bare_name(&col.name),
10667                        ty: col.ty,
10668                        nullable: col.nullable,
10669                        user_enum_type: col.user_enum_type.clone(),
10670                        mysql_fsp: col.mysql_fsp,
10671                        collation_name: col.collation_name.clone(),
10672                    });
10673                }
10674            }
10675            // v7.39 (round 128) — `q.*` expands to every column belonging to
10676            // the qualifier `q`. Single-table schemas carry bare column names
10677            // reachable via `table_alias`; a join's combined schema carries
10678            // `alias.col` names, so a column belongs to `q` when its name has
10679            // the `q.` prefix. PG labels the expanded columns by their bare
10680            // name, so the `alias.` prefix is stripped from the output name.
10681            SelectItem::QualifiedWildcard(q) => {
10682                let prefix = alloc::format!("{q}.");
10683                let single_table = !table_alias.is_empty() && q == table_alias;
10684                let mut matched = 0usize;
10685                for col in &schema_cols[..visible] {
10686                    let belongs =
10687                        col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
10688                    if !belongs {
10689                        continue;
10690                    }
10691                    matched += 1;
10692                    let output_name = col
10693                        .name
10694                        .strip_prefix(&prefix)
10695                        .unwrap_or(&col.name)
10696                        .to_string();
10697                    out.push(ProjectedItem {
10698                        expr: Expr::Column(ColumnName {
10699                            qualifier: None,
10700                            name: col.name.clone(),
10701                        }),
10702                        output_name,
10703                        ty: col.ty,
10704                        nullable: col.nullable,
10705                        user_enum_type: col.user_enum_type.clone(),
10706                        mysql_fsp: col.mysql_fsp,
10707                        collation_name: col.collation_name.clone(),
10708                    });
10709                }
10710                if matched == 0 {
10711                    return Err(EngineError::Eval(EvalError::UnknownQualifier {
10712                        qualifier: q.clone(),
10713                    }));
10714                }
10715            }
10716            SelectItem::Expr { expr, alias } => {
10717                // Plain column ref keeps full schema info (real type +
10718                // nullability). For compound expressions try the
10719                // describe-side function-return-type table first
10720                // (e.g. `SELECT now()` → Timestamptz, `SELECT
10721                // concat(…)` → Text). Falls back to nullable Text
10722                // for shapes the describe path can't resolve.
10723                if let Expr::Column(c) = expr {
10724                    let sch = resolve_projection_column(c, schema_cols, table_alias)?;
10725                    let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
10726                    out.push(ProjectedItem {
10727                        expr: expr.clone(),
10728                        output_name,
10729                        ty: sch.ty,
10730                        nullable: sch.nullable,
10731                        // v7.39 (read01 round 54) — a bare enum column keeps
10732                        // its enum identity through the projection.
10733                        user_enum_type: sch.user_enum_type.clone(),
10734                        mysql_fsp: sch.mysql_fsp,
10735                        collation_name: sch.collation_name.clone(),
10736                    });
10737                } else if let Some(shape) = describe::describe_expr(expr, schema_cols) {
10738                    let output_name = alias
10739                        .clone()
10740                        .unwrap_or_else(|| default_output_name(expr, mysql));
10741                    out.push(ProjectedItem {
10742                        expr: expr.clone(),
10743                        output_name,
10744                        ty: shape.ty,
10745                        // v7.39 (round 258) — a projected EXPRESSION keeps its
10746                        // enum identity too, not just a bare column. `FROM
10747                        // (VALUES ('happy'::mood), …) t(m)` lowers to constant
10748                        // SELECTs, so the derived column arrived here as a cast
10749                        // and lost the enum — making the outer ORDER BY / min /
10750                        // max / array_agg sort by the label's TEXT.
10751                        nullable: shape.nullable,
10752                        user_enum_type: None,
10753                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
10754                        // A bare column reference keeps its collation; any
10755                        // other expression produces a new value and has none.
10756                        collation_name: match expr {
10757                            Expr::Column(c) => schema_cols
10758                                .iter()
10759                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
10760                                .and_then(|sc| sc.collation_name.clone()),
10761                            _ => None,
10762                        },
10763                    });
10764                } else {
10765                    let output_name = alias
10766                        .clone()
10767                        .unwrap_or_else(|| default_output_name(expr, mysql));
10768                    out.push(ProjectedItem {
10769                        expr: expr.clone(),
10770                        output_name,
10771                        // A user ENUM has no DataType of its own, so
10772                        // `describe_expr` cannot type `'ok'::mood` and the
10773                        // item lands HERE, defaulting to text — which is why
10774                        // pg_typeof answered `text` and a derived table sorted
10775                        // enum values by their label.
10776                        ty: DataType::Text,
10777                        nullable: true,
10778                        user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
10779                            .map(alloc::string::String::from),
10780                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
10781                        collation_name: match expr {
10782                            Expr::Column(c) => schema_cols
10783                                .iter()
10784                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
10785                                .and_then(|sc| sc.collation_name.clone()),
10786                            _ => None,
10787                        },
10788                    });
10789                }
10790            }
10791        }
10792    }
10793    Ok(out)
10794}
10795
10796// ---- v4.12 window-function helpers ----
10797// The (partition-key, order-key, original-index) tuple shape used
10798// across these helpers is intrinsic to the planner. Factoring it
10799// into a typedef adds indirection without making the code clearer,
10800// so several lints are allowed inline on the affected functions
10801// rather than module-wide.
10802
10803/// v4.22: pick more specific column types from observed rows when
10804/// the projection builder defaulted to Text (the v1.x behavior for
10805/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
10806/// land an Int column in the CTE storage table rather than failing
10807/// the insert with "expected TEXT, got INT".
10808pub(crate) fn infer_column_types(
10809    columns: &[ColumnSchema],
10810    rows: &[Row<'static>],
10811) -> Vec<ColumnSchema> {
10812    let mut out = columns.to_vec();
10813    for (col_idx, col) in out.iter_mut().enumerate() {
10814        if col.ty != DataType::Text {
10815            continue;
10816        }
10817        let mut inferred: Option<DataType> = None;
10818        let mut all_null = true;
10819        for row in rows {
10820            let Some(v) = row.values.get(col_idx) else {
10821                continue;
10822            };
10823            let ty = match v {
10824                Value::Null => continue,
10825                Value::SmallInt(_) => DataType::SmallInt,
10826                Value::Int(_) => DataType::Int,
10827                Value::BigInt(_) => DataType::BigInt,
10828                Value::Float(_) => DataType::Float,
10829                Value::Bool(_) => DataType::Bool,
10830                Value::Vector(_) => DataType::Vector {
10831                    dim: 0,
10832                    encoding: VecEncoding::F32,
10833                },
10834                // v7.38 (read01 U16) — carry array values through with an
10835                // array type so a recursive CTE that projects an array
10836                // (e.g. a SEARCH/CYCLE ord / path column) types the working
10837                // column as an array, not Text.
10838                Value::TextArray(_) => DataType::TextArray,
10839                Value::IntArray(_) => DataType::IntArray,
10840                Value::BigIntArray(_) => DataType::BigIntArray,
10841                Value::SmallIntArray(_) => DataType::SmallIntArray,
10842                Value::FloatArray(_) => DataType::FloatArray,
10843                Value::BoolArray(_) => DataType::BoolArray,
10844                // v7.39 (GUC knife 2) — an interval projection describes
10845                // as INTERVAL (typed drivers read the RowDescription OID).
10846                Value::Interval { .. } => DataType::Interval,
10847                _ => DataType::Text,
10848            };
10849            all_null = false;
10850            inferred = Some(match inferred {
10851                None => ty,
10852                Some(prev) if prev == ty => prev,
10853                Some(_) => DataType::Text,
10854            });
10855        }
10856        if let Some(t) = inferred {
10857            col.ty = t;
10858            col.nullable = true;
10859        } else if all_null {
10860            col.nullable = true;
10861        }
10862    }
10863    out
10864}
10865
10866/// Numeric widening rank for UNION type resolution (higher = wider).
10867fn numeric_rank(t: DataType) -> Option<u8> {
10868    match t {
10869        DataType::SmallInt => Some(1),
10870        DataType::Int => Some(2),
10871        DataType::BigInt => Some(3),
10872        DataType::Numeric { .. } => Some(4),
10873        DataType::Float => Some(5),
10874        _ => None,
10875    }
10876}
10877
10878/// Resolve the common result type for a UNION / VALUES column from the
10879/// set of concrete (non-NULL) branch types, following the safe subset
10880/// of PG's type resolution:
10881///   * all-numeric  → the widest numeric (int ∪ bigint → bigint, … ∪
10882///     numeric → numeric, … ∪ float → float);
10883///   * DATE ∪ TIMESTAMP → TIMESTAMP;
10884///   * exactly one concrete non-TEXT type mixed with TEXT literals →
10885///     that concrete type (the TEXT cells get parsed into it).
10886/// Returns `None` for anything ambiguous, so the caller leaves the
10887/// column untouched rather than risk a wrong or failing coercion.
10888fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
10889    // NB: types are collected from RUNTIME values, which are coarser
10890    // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
10891    // a single-concrete-type fast path must NOT overwrite the column
10892    // type — it would downgrade tstz to ts. NULL-only unification (PG:
10893    // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
10894    // row's pg_typeof) needs schema-level resolution — recorded, not
10895    // attempted here.
10896    if types.len() < 2 {
10897        return None;
10898    }
10899    if types.iter().all(|t| numeric_rank(*t).is_some()) {
10900        return types
10901            .iter()
10902            .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
10903            .copied();
10904    }
10905    let non_text: Vec<&DataType> = types
10906        .iter()
10907        .filter(|t| !matches!(t, DataType::Text))
10908        .collect();
10909    // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
10910    // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
10911    // if any is timestamp the result is timestamp (ts ∪ date). All values are
10912    // the same UTC-micros instant, so widening date/ts to tstz is lossless.
10913    if non_text.iter().all(|t| {
10914        matches!(
10915            t,
10916            DataType::Date | DataType::Timestamp | DataType::Timestamptz
10917        )
10918    }) && non_text
10919        .iter()
10920        .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
10921    {
10922        if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
10923            return Some(DataType::Timestamptz);
10924        }
10925        return Some(DataType::Timestamp);
10926    }
10927    // A single concrete non-TEXT type mixed with TEXT literals.
10928    if non_text.len() == 1 {
10929        return Some(*non_text[0]);
10930    }
10931    // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
10932    // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
10933    // text): resolve the concrete set first (PG treats the unknown-
10934    // typed string literals as castable to whatever the knowns
10935    // resolve to), then the TEXT cells parse into that target — the
10936    // caller's coercion dry-run still abandons the column if any
10937    // literal doesn't parse.
10938    if !non_text.is_empty() && non_text.len() < types.len() {
10939        let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
10940        return resolve_union_common_type(&concrete);
10941    }
10942    None
10943}
10944
10945/// Coerce every cell of a UNION / VALUES result column to one common
10946/// type (see [`resolve_union_common_type`]). Conservative: a column
10947/// whose branches already agree, or whose types don't resolve, or where
10948/// any cell fails to coerce, is left exactly as it was — this never
10949/// turns a previously-working query into an error.
10950fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
10951    for col_idx in 0..columns.len() {
10952        let mut seen: Vec<DataType> = Vec::new();
10953        for row in rows.iter() {
10954            if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
10955                if !seen.contains(&dt) {
10956                    seen.push(dt);
10957                }
10958            }
10959        }
10960        // v7.37.16 — a single concrete runtime type under a TEXT-typed
10961        // column means the column type came off a NULL (or unknown-text)
10962        // branch: NULL literals describe as TEXT (`L::Null → Text`), so
10963        // `VALUES (NULL),(1.5)` left the column "text" while every
10964        // non-NULL cell is numeric. Adopt the concrete type — schema
10965        // only, no cell changes. tstz-safe by construction: a real
10966        // timestamptz column's schema type is Timestamptz, not Text, so
10967        // the coarser runtime type (Value::Timestamp) can't downgrade it
10968        // through this arm; and a real text column's non-NULL cells are
10969        // Text, which keeps seen == [Text] and skips it.
10970        if seen.len() == 1
10971            && matches!(columns[col_idx].ty, DataType::Text)
10972            && !matches!(seen[0], DataType::Text)
10973        {
10974            columns[col_idx].ty = seen[0];
10975            continue;
10976        }
10977        let Some(target) = resolve_union_common_type(&seen) else {
10978            continue;
10979        };
10980        // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
10981        // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
10982        // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
10983        // existing numeric cell untouched and only promote integers (to scale 0)
10984        // rather than rescaling everything to the widest scale.
10985        let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
10986        // Dry-run the coercion; abandon the whole column if any fails.
10987        let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
10988        let mut ok = true;
10989        for row in rows.iter() {
10990            match row.values.get(col_idx) {
10991                Some(Value::Numeric { .. }) if scale_preserving_numeric => {
10992                    coerced.push(Some(row.values[col_idx].clone()));
10993                }
10994                Some(v) => {
10995                    let cell_target = if scale_preserving_numeric {
10996                        DataType::Numeric {
10997                            precision: 0,
10998                            scale: 0,
10999                        }
11000                    } else {
11001                        target
11002                    };
11003                    match crate::conversions::coerce_value(
11004                        v.clone(),
11005                        cell_target,
11006                        &columns[col_idx].name,
11007                        col_idx,
11008                    ) {
11009                        Ok(cv) => coerced.push(Some(cv)),
11010                        Err(_) => {
11011                            ok = false;
11012                            break;
11013                        }
11014                    }
11015                }
11016                None => coerced.push(None),
11017            }
11018        }
11019        if !ok {
11020            continue;
11021        }
11022        for (row, cv) in rows.iter_mut().zip(coerced) {
11023            if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
11024                *slot = nv;
11025            }
11026        }
11027        columns[col_idx].ty = target;
11028    }
11029}
11030
11031/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
11032/// dedup inside the recursive iteration. Crude but deterministic
11033/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
11034fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
11035    let mut out = Vec::new();
11036    for v in &row.values {
11037        // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
11038        // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
11039        // like PG (and like GROUP BY, which already normalizes). The old
11040        // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
11041        // the exact-decimal family through one scale-stripped canonical form.
11042        match v {
11043            Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
11044            Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
11045            Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
11046            Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
11047            other => {
11048                let s = alloc::format!("{other:?}|");
11049                out.extend_from_slice(s.as_bytes());
11050            }
11051        }
11052    }
11053    out
11054}
11055
11056/// Append a scale-independent canonical key for an exact-decimal value: strip
11057/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
11058/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
11059fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
11060    while scale > 0 && scaled % 10 == 0 {
11061        scaled /= 10;
11062        scale -= 1;
11063    }
11064    let s = alloc::format!("\u{1}{scaled}e-{scale}|");
11065    out.extend_from_slice(s.as_bytes());
11066}
11067
11068/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
11069/// (uncorrelated; outer refs were substituted upstream), then zip
11070/// them in parallel, NULL-padding shorter arrays to the longest
11071/// (PG's ROWS FROM shorthand). Shared by the primary-position
11072/// executor and the join-position materialiser, which both detect
11073/// the parser's `__unnest_zip` marker call.
11074pub(crate) fn unnest_zip_rows(
11075    args: &[Expr],
11076) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
11077    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
11078    let ctx = EvalContext::new(&empty_schema, None);
11079    let dummy_row = Row::new(alloc::vec::Vec::new());
11080    let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
11081    let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
11082        alloc::vec::Vec::with_capacity(args.len());
11083    for a in args {
11084        let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
11085        let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = match v {
11086            Value::Null => (DataType::Text, alloc::vec::Vec::new()),
11087            Value::TextArray(xs) => (
11088                DataType::Text,
11089                xs.into_iter()
11090                    .map(|x| x.map(Value::text).unwrap_or(Value::Null))
11091                    .collect(),
11092            ),
11093            Value::IntArray(xs) => (
11094                DataType::Int,
11095                xs.into_iter()
11096                    .map(|x| x.map(Value::Int).unwrap_or(Value::Null))
11097                    .collect(),
11098            ),
11099            Value::BigIntArray(xs) => (
11100                DataType::BigInt,
11101                xs.into_iter()
11102                    .map(|x| x.map(Value::BigInt).unwrap_or(Value::Null))
11103                    .collect(),
11104            ),
11105            other => {
11106                return Err(EngineError::Unsupported(alloc::format!(
11107                    "unnest() expects array arguments, got {}",
11108                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
11109                )));
11110            }
11111        };
11112        dtypes.push(dt);
11113        columns.push(items);
11114    }
11115    let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
11116    let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
11117    for i in 0..max_len {
11118        let vals: alloc::vec::Vec<Value<'static>> = columns
11119            .iter()
11120            .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
11121            .collect();
11122        rows.push(Row::new(vals));
11123    }
11124    Ok((dtypes, rows))
11125}
11126
11127/// Detect the parser's multi-arg unnest marker on an unnest_expr.
11128pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
11129    match expr {
11130        Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
11131        _ => None,
11132    }
11133}
11134
11135/// Evaluate generate_series arguments (uncorrelated — outer refs
11136/// were substituted upstream where applicable) and build the row
11137/// stream. Dispatches on the start value's shape and rejects
11138/// mixed-shape calls early (e.g. start = timestamp, stop =
11139/// integer) so the caller gets a clean error rather than a panic.
11140/// Shared by the primary-position executor and the join-position
11141/// materialiser.
11142pub(crate) fn generate_series_rows(
11143    args: &[Expr],
11144    cancel: &CancelToken<'_>,
11145) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
11146    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
11147    let ctx = EvalContext::new(&empty_schema, None);
11148    let dummy_row = Row::new(alloc::vec::Vec::new());
11149    let mut arg_values: alloc::vec::Vec<Value<'static>> =
11150        alloc::vec::Vec::with_capacity(args.len());
11151    for a in args {
11152        arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
11153    }
11154    generate_series_from_values(arg_values, args, cancel)
11155}
11156
11157/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
11158/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
11159/// full integer / numeric / timestamp overload set with the FROM-clause path.
11160/// Before this split the target-list arm reimplemented only the integer case,
11161/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
11162/// NULL for the timestamp column instead of the series. `arg_values` are the
11163/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
11164/// timestamp type resolution (it inspects the argument expressions' types).
11165pub(crate) fn generate_series_from_values(
11166    mut arg_values: alloc::vec::Vec<Value<'static>>,
11167    args: &[Expr],
11168    cancel: &CancelToken<'_>,
11169) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
11170    // PG: a NULL bound or step yields zero rows (also keeps the
11171    // NULL-padded lateral probe alive — schema without data).
11172    if arg_values.iter().any(|v| matches!(v, Value::Null)) {
11173        return Ok((DataType::BigInt, alloc::vec::Vec::new()));
11174    }
11175    // PG resolves `generate_series(date, date, interval)` to the
11176    // timestamp/timestamptz overload by implicitly casting each date
11177    // bound up to a timestamp at midnight (verified vs live PG18.4:
11178    // date args yield rows anchored at 00:00:00). SPG's TZ-naive
11179    // timestamp model renders the same instants, so fold any Date
11180    // bound to its midnight Timestamp (canonical `days *
11181    // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
11182    // the shape match so the existing timestamp arm drives the walk.
11183    // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
11184    // `generate_series(date, date, interval)` has no date overload, and among
11185    // the two candidates PG prefers the timestamptz one (timestamptz is the
11186    // preferred type of the datetime category), so the column comes back
11187    // `timestamp with time zone` — the rows render with a `+00` offset. A
11188    // timestamptz bound obviously lands there too. Only genuinely
11189    // timestamp-typed bounds keep the TZ-naive result type.
11190    let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
11191    let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
11192        || args.iter().any(|a| {
11193            crate::describe::describe_expr(a, &empty_cols)
11194                .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
11195        });
11196    for v in &mut arg_values {
11197        if let Value::Date(d) = *v {
11198            *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
11199        }
11200    }
11201    match arg_values.as_slice() {
11202        [Value::Timestamp(start), Value::Timestamp(stop), step] => {
11203            let interval_step = match step {
11204                Value::Interval { .. } => step.clone(),
11205                // v7.38 (read01) — PG resolves an unknown-type string step
11206                // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
11207                // a bare text step by parsing it the same way `::interval` does.
11208                Value::Text(s) => crate::conversions::coerce_value(
11209                    Value::text(s.as_ref()),
11210                    DataType::Interval,
11211                    "",
11212                    0,
11213                )
11214                .map_err(|_| {
11215                    EngineError::Unsupported(alloc::format!(
11216                        "generate_series(timestamp, timestamp, …): \
11217                         could not parse step {s:?} as INTERVAL"
11218                    ))
11219                })?,
11220                other => {
11221                    return Err(EngineError::Unsupported(alloc::format!(
11222                        "generate_series(timestamp, timestamp, …): \
11223                         step must be INTERVAL, got {}",
11224                        crate::conversions::pg_type_name_for_error_opt(other.data_type())
11225                    )));
11226                }
11227            };
11228            let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
11229            Ok((
11230                if tz {
11231                    DataType::Timestamptz
11232                } else {
11233                    DataType::Timestamp
11234                },
11235                rows,
11236            ))
11237        }
11238        [start, stop, step]
11239            if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
11240        {
11241            let s = value_to_i64(start);
11242            let e = value_to_i64(stop);
11243            let st = value_to_i64(step);
11244            // PG types the series by the argument type: int4 args → int4
11245            // elements, int8 (bigint) args → int8. Any BigInt operand widens.
11246            let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
11247            let rows = generate_series_integers(s, e, st, wide, cancel)?;
11248            Ok((
11249                if wide {
11250                    DataType::BigInt
11251                } else {
11252                    DataType::Int
11253                },
11254                rows,
11255            ))
11256        }
11257        [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
11258            let s = value_to_i64(start);
11259            let e = value_to_i64(stop);
11260            let wide = value_is_bigint(start) || value_is_bigint(stop);
11261            let rows = generate_series_integers(s, e, 1, wide, cancel)?;
11262            Ok((
11263                if wide {
11264                    DataType::BigInt
11265                } else {
11266                    DataType::Int
11267                },
11268                rows,
11269            ))
11270        }
11271        // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
11272        // series in exact numeric arithmetic; NaN / infinity bounds and a
11273        // zero step get dedicated wordings, and a mixed int/numeric call
11274        // resolves here via the implicit int→numeric cast.
11275        [_, _] | [_, _, _]
11276            if arg_values
11277                .iter()
11278                .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
11279                && arg_values.iter().all(|v| {
11280                    matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
11281                }) =>
11282        {
11283            use spg_storage::NumericKind as K;
11284            let words: [(&str, &str); 3] = [
11285                (
11286                    "start value cannot be NaN",
11287                    "start value cannot be infinity",
11288                ),
11289                ("stop value cannot be NaN", "stop value cannot be infinity"),
11290                ("step size cannot be NaN", "step size cannot be infinity"),
11291            ];
11292            for (i, v) in arg_values.iter().enumerate() {
11293                if let Value::Numeric { kind, .. } = v {
11294                    if *kind != K::Finite {
11295                        let (nan_w, inf_w) = words[i];
11296                        return Err(EngineError::Unsupported(
11297                            if *kind == K::NaN { nan_w } else { inf_w }.into(),
11298                        ));
11299                    }
11300                }
11301            }
11302            let big =
11303                |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
11304            let start = big(&arg_values[0]);
11305            let stop = big(&arg_values[1]);
11306            let step = if arg_values.len() == 3 {
11307                big(&arg_values[2])
11308            } else {
11309                spg_storage::bignum::BigNumeric::from_i128(1, 0)
11310            };
11311            if step.is_zero() {
11312                return Err(EngineError::Unsupported(
11313                    "step size cannot equal zero".into(),
11314                ));
11315            }
11316            let descending = step.parts().0;
11317            let mut rows = alloc::vec::Vec::new();
11318            let mut cur = start;
11319            const MAX_ROWS: usize = 10_000_000;
11320            loop {
11321                cancel.check()?;
11322                let c = cur.cmp(&stop);
11323                if descending {
11324                    if c == core::cmp::Ordering::Less {
11325                        break;
11326                    }
11327                } else if c == core::cmp::Ordering::Greater {
11328                    break;
11329                }
11330                if rows.len() >= MAX_ROWS {
11331                    return Err(EngineError::Unsupported(alloc::format!(
11332                        "generate_series() result exceeds {MAX_ROWS} rows"
11333                    )));
11334                }
11335                rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
11336                    cur.clone()
11337                )]));
11338                cur = cur.add(&step);
11339            }
11340            Ok((
11341                DataType::Numeric {
11342                    precision: 0,
11343                    scale: 0,
11344                },
11345                rows,
11346            ))
11347        }
11348        _ => Err(EngineError::Unsupported(alloc::format!(
11349            "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
11350             argument shapes; got {}",
11351            arg_values
11352                .iter()
11353                .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
11354                .collect::<alloc::vec::Vec<_>>()
11355                .join(", ")
11356        ))),
11357    }
11358}
11359
11360/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
11361/// Step direction follows the sign: positive step iterates upward
11362/// (stops when current > stop); negative iterates downward; zero
11363/// errors. Caller-facing row stream is `BigInt`-typed so a single
11364/// projection schema covers SmallInt / Int / BigInt callers.
11365fn generate_series_integers(
11366    start: i64,
11367    stop: i64,
11368    step: i64,
11369    wide: bool,
11370    cancel: &CancelToken<'_>,
11371) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
11372    if step == 0 {
11373        return Err(EngineError::Unsupported(
11374            "step size cannot equal zero".into(),
11375        ));
11376    }
11377    let mut out = alloc::vec::Vec::new();
11378    let mut cur = start;
11379    // Hard cap to keep a runaway call from eating all memory. PG
11380    // has no such cap but does honour query timeout; SPG's cancel
11381    // token will fire too — this is a defense-in-depth backstop.
11382    const MAX_ROWS: usize = 10_000_000;
11383    loop {
11384        cancel.check()?;
11385        if step > 0 && cur > stop {
11386            break;
11387        }
11388        if step < 0 && cur < stop {
11389            break;
11390        }
11391        out.push(Row::new(alloc::vec![if wide {
11392            Value::BigInt(cur)
11393        } else {
11394            Value::Int(cur as i32)
11395        }]));
11396        if out.len() > MAX_ROWS {
11397            return Err(EngineError::Unsupported(alloc::format!(
11398                "generate_series(): exceeded {MAX_ROWS} rows; \
11399                 narrow start/stop or use a larger step"
11400            )));
11401        }
11402        cur = match cur.checked_add(step) {
11403            Some(n) => n,
11404            None => break,
11405        };
11406    }
11407    Ok(out)
11408}
11409
11410/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
11411/// `Value::Interval { months, micros }` per the caller's guard;
11412/// each iteration adds the interval via `apply_binary_interval`
11413/// so month-shifting handles short-month rollover (PG semantics).
11414fn generate_series_timestamps(
11415    start: i64,
11416    stop: i64,
11417    step: Value,
11418    cancel: &CancelToken<'_>,
11419) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
11420    let (months, days, micros) = match &step {
11421        Value::Interval {
11422            months,
11423            days,
11424            micros,
11425        } => (*months, *days, *micros),
11426        _ => unreachable!("caller guards step.is_interval"),
11427    };
11428    if months == 0 && days == 0 && micros == 0 {
11429        return Err(EngineError::Unsupported(
11430            "generate_series(): INTERVAL step cannot be zero".into(),
11431        ));
11432    }
11433    let ascending = months > 0 || days > 0 || micros > 0;
11434    let mut out = alloc::vec::Vec::new();
11435    let mut cur = Value::Timestamp(start);
11436    const MAX_ROWS: usize = 10_000_000;
11437    loop {
11438        cancel.check()?;
11439        let cur_t = match cur {
11440            Value::Timestamp(t) => t,
11441            _ => unreachable!("loop invariant: cur is Timestamp"),
11442        };
11443        if ascending && cur_t > stop {
11444            break;
11445        }
11446        if !ascending && cur_t < stop {
11447            break;
11448        }
11449        out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
11450        if out.len() > MAX_ROWS {
11451            return Err(EngineError::Unsupported(alloc::format!(
11452                "generate_series(): exceeded {MAX_ROWS} rows; \
11453                 narrow start/stop or use a larger step"
11454            )));
11455        }
11456        let next = eval::apply_binary_interval(
11457            spg_sql::ast::BinOp::Add,
11458            &cur,
11459            &Value::Interval {
11460                months,
11461                days,
11462                micros,
11463            },
11464        )
11465        .map_err(EngineError::Eval)?;
11466        cur = match next {
11467            Some(v) => v,
11468            None => break,
11469        };
11470    }
11471    Ok(out)
11472}
11473
11474/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
11475/// WITH TIES` requires an `ORDER BY`. Without one, there's no
11476/// way to identify "ties" deterministically, so PG errors at
11477/// plan time. SPG mirrors that surface so the same DDL / app
11478/// behaviour holds on cutover.
11479fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
11480    if stmt.limit_with_ties && stmt.order_by.is_empty() {
11481        return Err(EngineError::Unsupported(alloc::string::String::from(
11482            "WITH TIES cannot be specified without ORDER BY clause",
11483        )));
11484    }
11485    Ok(())
11486}
11487
11488/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
11489/// (case-insensitive). Used by `exec_select_cancel`'s
11490/// projection loop to detect Set-Returning-Function rows that
11491/// need per-row expansion. Only the top-level call counts —
11492/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
11493/// projection's perspective; it would surface as an "unknown
11494/// function" mismatch downstream, which is what we want
11495/// (multi-SRF / nested SRF is documented carve-out for v7.19).
11496fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
11497    top_level_srf_kind(expr).is_some()
11498}
11499
11500/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
11501/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
11502/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
11503/// source row.
11504#[derive(Clone, Copy, PartialEq, Eq)]
11505pub(crate) enum SrfKind {
11506    Unnest,
11507    /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
11508    /// list. It used to be handled ONLY by the parser's lift into FROM, so a
11509    /// second one in the same list came back as "unknown function".
11510    GenerateSeries,
11511    GenerateSubscripts,
11512    /// `_text` variants unwrap scalars to their lexeme; the plain forms render
11513    /// every value as compact JSON text.
11514    ArrayElements {
11515        as_text: bool,
11516    },
11517    PathQuery,
11518    RegexpMatches,
11519    Each {
11520        as_text: bool,
11521    },
11522    ObjectKeys,
11523}
11524
11525/// Case-insensitive match against any of `names`.
11526fn name_is(name: &str, names: &[&str]) -> bool {
11527    names.iter().any(|n| name.eq_ignore_ascii_case(n))
11528}
11529
11530pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
11531    let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
11532        return None;
11533    };
11534    let n = args.len();
11535    // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
11536    // SELECT list (it returned an array there before) and shares the unnest
11537    // expansion machinery.
11538    if n == 1 && name.eq_ignore_ascii_case("unnest") {
11539        return Some(SrfKind::Unnest);
11540    }
11541    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
11542        return Some(SrfKind::GenerateSeries);
11543    }
11544    if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
11545        return Some(SrfKind::GenerateSubscripts);
11546    }
11547    // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
11548    // per element / match in the SELECT list; they collapsed to a single row
11549    // (a TextArray, or an "unknown function" error for `each`) before.
11550    if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
11551        return Some(SrfKind::ArrayElements { as_text: false });
11552    }
11553    if n == 1
11554        && name_is(
11555            name,
11556            &["jsonb_array_elements_text", "json_array_elements_text"],
11557        )
11558    {
11559        return Some(SrfKind::ArrayElements { as_text: true });
11560    }
11561    // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
11562    if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
11563        return Some(SrfKind::PathQuery);
11564    }
11565    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
11566        return Some(SrfKind::RegexpMatches);
11567    }
11568    if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
11569        return Some(SrfKind::Each { as_text: false });
11570    }
11571    if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
11572        return Some(SrfKind::Each { as_text: true });
11573    }
11574    if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
11575        return Some(SrfKind::ObjectKeys);
11576    }
11577    None
11578}
11579
11580/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
11581/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
11582/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
11583/// rows, as in PG).
11584pub(crate) fn top_level_srf_output(
11585    expr: &spg_sql::ast::Expr,
11586    row: &Row<'static>,
11587    ctx: &EvalContext<'_>,
11588) -> Result<Vec<Value<'static>>, EngineError> {
11589    let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
11590        (top_level_srf_kind(expr), expr)
11591    else {
11592        return Err(EngineError::Unsupported(
11593            "expected a SELECT-list SRF call".into(),
11594        ));
11595    };
11596    match kind {
11597        SrfKind::Unnest => {
11598            // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
11599            // the elements DIRECTLY: the old path built the whole
11600            // Value::Array (one eval + a clone per element) only for
11601            // array_value_to_elements to clone every element back out.
11602            // Any other argument shape (a column, a function result)
11603            // keeps the build-then-split path.
11604            if let spg_sql::ast::Expr::Array(items) = &args[0] {
11605                return items
11606                    .iter()
11607                    .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
11608                    .collect();
11609            }
11610            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11611            array_value_to_elements(&arr)
11612        }
11613        SrfKind::GenerateSeries => {
11614            // v7.39 (read01 round 96) — evaluate the args against the actual
11615            // row, then hand off to the shared core so the numeric and
11616            // timestamp/timestamptz overloads work here too (this arm used to
11617            // handle only integers, silently NULLing a temporal/numeric series
11618            // when it shared a target list with another SRF).
11619            let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
11620            for a in args {
11621                arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
11622            }
11623            let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
11624            Ok(rows
11625                .into_iter()
11626                .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
11627                .collect())
11628        }
11629        SrfKind::GenerateSubscripts => {
11630            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11631            let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
11632            if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
11633                return Ok(Vec::new());
11634            }
11635            let len = array_value_to_elements(&arr)?.len();
11636            Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
11637        }
11638        // One Value per array element (`_text` → text / SQL NULL, plain → the
11639        // element's compact JSON text) — the element list the FROM-clause form
11640        // materialises.
11641        SrfKind::ArrayElements { as_text } => {
11642            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11643            if matches!(arg, Value::Null) {
11644                return Ok(Vec::new());
11645            }
11646            let items =
11647                crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
11648            Ok(items
11649                .into_iter()
11650                .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
11651                .collect())
11652        }
11653        // The scalar form already yields a TextArray of the keys (or errors on
11654        // a non-object, like PG); expand it into rows.
11655        SrfKind::ObjectKeys => {
11656            let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
11657            array_value_to_elements(&v)
11658        }
11659        // One row per match, each a text[] of the pattern's capture groups.
11660        SrfKind::RegexpMatches => {
11661            let vals: Vec<Value<'static>> = args
11662                .iter()
11663                .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
11664                .collect::<Result<_, _>>()?;
11665            crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
11666        }
11667        // One composite `(key, value)` row per object member (plain → jsonb
11668        // value, `_text` → text / SQL NULL).
11669        SrfKind::Each { as_text } => {
11670            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11671            if matches!(arg, Value::Null) {
11672                return Ok(Vec::new());
11673            }
11674            let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
11675            Ok(pairs
11676                .into_iter()
11677                .map(|(k, v)| {
11678                    let val = if as_text {
11679                        v.map(Value::text).unwrap_or(Value::Null)
11680                    } else {
11681                        v.map(Value::json).unwrap_or(Value::Null)
11682                    };
11683                    Value::Composite(alloc::vec![
11684                        ("key".to_string(), Value::text(k)),
11685                        ("value".to_string(), val),
11686                    ])
11687                })
11688                .collect())
11689        }
11690        // One Value per matched JSON value.
11691        SrfKind::PathQuery => {
11692            let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
11693            let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
11694            // v7.39 — optional vars document (3rd arg).
11695            let vars = match args.get(2) {
11696                Some(a) => {
11697                    let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
11698                    crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
11699                }
11700                None => None,
11701            };
11702            match crate::json::path_query_vars(&doc, &path, vars.as_ref())
11703                .map_err(EngineError::Eval)?
11704            {
11705                Value::Null => Ok(Vec::new()),
11706                Value::TextArray(items) => Ok(items
11707                    .into_iter()
11708                    .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
11709                    .collect()),
11710                other => Ok(alloc::vec![other]),
11711            }
11712        }
11713    }
11714}
11715
11716/// v7.19 P5 — turn an array-typed `Value` into the element list
11717/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
11718/// = (no rows)`). Non-array values fall through to a type-mismatch
11719/// error.
11720pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
11721    // v7.39 (round 236) — PG unnests a multidimensional array into its
11722    // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
11723    // rows). SPG stores 2-D arrays as their own variants, which fell
11724    // through to the type-mismatch arm below.
11725    if let Some(flat) = crate::eval::values::flatten_2d(v) {
11726        return array_value_to_elements(&flat);
11727    }
11728    match v {
11729        Value::Null => Ok(Vec::new()),
11730        Value::TextArray(items) => Ok(items
11731            .iter()
11732            .map(|opt| {
11733                opt.as_ref()
11734                    .map(|s| Value::text(s.clone()))
11735                    .unwrap_or(Value::Null)
11736            })
11737            .collect()),
11738        Value::IntArray(items) => Ok(items
11739            .iter()
11740            .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
11741            .collect()),
11742        Value::BigIntArray(items) => Ok(items
11743            .iter()
11744            .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
11745            .collect()),
11746        // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
11747        // range per canonical span.
11748        Value::Multirange { kind, ranges } => Ok(ranges
11749            .iter()
11750            .map(|s| Value::Range {
11751                kind: *kind,
11752                lower: s.lower.clone(),
11753                upper: s.upper.clone(),
11754                lower_inc: s.lower_inc,
11755                upper_inc: s.upper_inc,
11756                empty: false,
11757            })
11758            .collect()),
11759        other => Err(EngineError::Eval(EvalError::TypeMismatch {
11760            detail: alloc::format!(
11761                "unnest() expects an array argument, got {}",
11762                crate::conversions::pg_type_name_for_error_opt(other.data_type())
11763            ),
11764        })),
11765    }
11766}
11767
11768impl Engine {
11769    /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
11770    /// the SELECT's FROM / JOIN graph, re-parse each view's body
11771    /// source, and prepend it as a synthetic CTE on the
11772    /// returned SelectStatement. Returns `None` when no view
11773    /// references are found (caller proceeds with the original
11774    /// statement); returns `Some(rewritten)` otherwise (caller
11775    /// re-runs exec_select_cancel on the rewritten form so the
11776    /// regular CTE materialiser handles it).
11777    fn expand_views_in_select(
11778        &self,
11779        stmt: &SelectStatement,
11780    ) -> Result<Option<SelectStatement>, EngineError> {
11781        let cat = self.active_catalog();
11782        let mut referenced: Vec<String> = Vec::new();
11783        if let Some(from) = &stmt.from {
11784            collect_view_refs(&from.primary, cat, &mut referenced);
11785            for j in &from.joins {
11786                collect_view_refs(&j.table, cat, &mut referenced);
11787            }
11788        }
11789        // Don't expand a view name that's already shadowed by a
11790        // CTE on the same SELECT — the CTE wins per PG.
11791        referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
11792        if referenced.is_empty() {
11793            return Ok(None);
11794        }
11795        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
11796        for name in &referenced {
11797            let view = cat.view(name).ok_or_else(|| {
11798                EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
11799                    "view {name:?} disappeared mid-expansion"
11800                )))
11801            })?;
11802            let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
11803                EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
11804            })?;
11805            let Statement::Select(body) = parsed else {
11806                return Err(EngineError::Unsupported(alloc::format!(
11807                    "view {name:?} body is not a SELECT (catalog corruption)"
11808                )));
11809            };
11810            new_ctes.push(spg_sql::ast::Cte {
11811                name: name.clone(),
11812                body: spg_sql::ast::CteBody::Select(body),
11813                recursive: false,
11814                column_overrides: view.columns.clone(),
11815                search: None,
11816                cycle: None,
11817            });
11818        }
11819        let mut out = stmt.clone();
11820        // Prepend so view CTEs are visible to caller-supplied CTEs.
11821        new_ctes.extend(out.ctes);
11822        out.ctes = new_ctes;
11823        Ok(Some(out))
11824    }
11825
11826    /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
11827    /// any partition-parent table, rewrite the SELECT so each parent
11828    /// reference resolves to a CTE whose body is a `UNION ALL` over the
11829    /// children that pass the WHERE-derived partition-key range. Returns
11830    /// `None`(no rewrite needed)when no parent is referenced or all
11831    /// references are shadowed by a same-name CTE.
11832    ///
11833    /// Pruning vocabulary at v7.37.6-B:
11834    ///   * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
11835    ///     and `<key> BETWEEN literal AND literal`.
11836    ///   * Anything outside that(OR / nested IN / function call on the
11837    ///     key)defaults to "no pruning" — every child + DEFAULT lands
11838    ///     in the UNION. Correctness is preserved; only the plan size
11839    ///     widens.
11840    fn expand_partition_parents_in_select(
11841        &self,
11842        stmt: &SelectStatement,
11843    ) -> Result<Option<SelectStatement>, EngineError> {
11844        let cat = self.active_catalog();
11845        let Some(from) = &stmt.from else {
11846            return Ok(None);
11847        };
11848        let mut parent_refs: Vec<String> = Vec::new();
11849        collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
11850        for j in &from.joins {
11851            collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
11852        }
11853        // Drop names shadowed by a CTE on the same SELECT(PG semantics
11854        // — same as view expansion above).
11855        parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
11856        if parent_refs.is_empty() {
11857            return Ok(None);
11858        }
11859        // Synthesise a CTE name per parent so the existing
11860        // "CTE shadows a real table" guard doesn't fire (the parent
11861        // IS a real table in the catalog, unlike VIEW expansion's
11862        // case). The FROM-clause TableRef walker below rewrites
11863        // every parent reference to point at the synthetic CTE.
11864        let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
11865        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
11866        let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
11867        for parent_name in &parent_refs {
11868            // No children = no rewrite. The parent itself is a real
11869            // (empty-rows) table — the regular FROM-resolution path
11870            // will scan it and return 0 rows, matching the
11871            // "partition parent with no children" plan. Skipping the
11872            // CTE here also avoids `SELECT * FROM parent` re-entering
11873            // this rewrite on the synthetic body (infinite recursion).
11874            let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
11875                continue;
11876            };
11877            new_ctes.push(spg_sql::ast::Cte {
11878                name: synth_name(parent_name),
11879                body: spg_sql::ast::CteBody::Select(body),
11880                recursive: false,
11881                column_overrides: Vec::new(),
11882                search: None,
11883                cycle: None,
11884            });
11885            expanded_parents.push(parent_name.clone());
11886        }
11887        if expanded_parents.is_empty() {
11888            return Ok(None);
11889        }
11890        let mut out = stmt.clone();
11891        if let Some(from) = out.from.as_mut() {
11892            rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
11893            for j in &mut from.joins {
11894                rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
11895            }
11896        }
11897        new_ctes.extend(out.ctes);
11898        out.ctes = new_ctes;
11899        Ok(Some(out))
11900    }
11901
11902    /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
11903    /// Children include every overlap-hit `Range` plus(always)the
11904    /// `Default` child(if any). Returns `Ok(None)` when no children
11905    /// would survive — caller skips the CTE injection and lets the
11906    /// parent fall through to the regular(empty-rows)scan path,
11907    /// avoiding the infinite recursion that an empty-body CTE
11908    /// referencing the parent name would trigger.
11909    /// v7.37.16 (16.10) — public helper invoked from explain.rs to
11910    /// surface "which children survive the WHERE-clause prune" in
11911    /// EXPLAIN output. Returns `None` when `parent_name` isn't
11912    /// actually a partition parent; otherwise returns the list of
11913    /// children the planner would scan (same algorithm as
11914    /// [`Self::build_partition_parent_union_body`] but without the
11915    /// SQL re-parse).
11916    /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
11917    /// expression (the PG-shaped EXPLAIN's scan builder has no full
11918    /// SelectStatement in hand). Wraps the original by synthesising a
11919    /// minimal statement carrying just the predicate.
11920    pub(crate) fn explain_partition_kept_children_by_where(
11921        &self,
11922        parent_name: &str,
11923        where_: Option<&spg_sql::ast::Expr>,
11924    ) -> Option<Vec<alloc::string::String>> {
11925        let mut synth = SelectStatement::default();
11926        synth.where_ = where_.cloned();
11927        self.explain_partition_kept_children(parent_name, &synth)
11928    }
11929
11930    pub(crate) fn explain_partition_kept_children(
11931        &self,
11932        parent_name: &str,
11933        outer: &SelectStatement,
11934    ) -> Option<Vec<alloc::string::String>> {
11935        use spg_storage::PartitionRole;
11936        let cat = self.active_catalog();
11937        let parent = cat.get(parent_name)?;
11938        let (key_position, parent_kind) = match &parent.schema().partition_role {
11939            Some(PartitionRole::Parent {
11940                key_column_positions,
11941                kind,
11942                ..
11943            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
11944            _ => return None,
11945        };
11946        let key_col_name = parent.schema().columns[key_position].name.clone();
11947        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
11948            Some(expr) => extract_key_range(expr, &key_col_name),
11949            None => (None, None),
11950        };
11951        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
11952            Some(expr) => extract_key_eq_value(expr, &key_col_name),
11953            None => None,
11954        };
11955        let children = crate::partition::children_of_parent(cat, parent_name);
11956        let mut kept: Vec<alloc::string::String> = Vec::new();
11957        let mut default_child: Option<alloc::string::String> = None;
11958        for child_name in &children {
11959            let Some(child) = cat.get(child_name) else {
11960                continue;
11961            };
11962            match &child.schema().partition_role {
11963                Some(PartitionRole::Range { lower, upper, .. }) => {
11964                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
11965                        kept.push(child_name.clone());
11966                    }
11967                }
11968                Some(PartitionRole::List { values, .. }) => match &eq_value {
11969                    Some(v) => {
11970                        if values.iter().any(|b| b.equals_value(v)) {
11971                            kept.push(child_name.clone());
11972                        }
11973                    }
11974                    None => kept.push(child_name.clone()),
11975                },
11976                Some(PartitionRole::Hash {
11977                    modulus, remainder, ..
11978                }) => match &eq_value {
11979                    Some(v) => {
11980                        let h = crate::partition::pg_compatible_hash(v);
11981                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
11982                            kept.push(child_name.clone());
11983                        }
11984                    }
11985                    None => kept.push(child_name.clone()),
11986                },
11987                Some(PartitionRole::Default { .. }) => {
11988                    default_child = Some(child_name.clone());
11989                }
11990                _ => {}
11991            }
11992        }
11993        let _ = parent_kind;
11994        if let Some(d) = default_child {
11995            if kept.is_empty() || eq_value.is_none() {
11996                kept.push(d);
11997            }
11998        }
11999        Some(kept)
12000    }
12001
12002    fn build_partition_parent_union_body(
12003        &self,
12004        parent_name: &str,
12005        outer: &SelectStatement,
12006    ) -> Result<Option<SelectStatement>, EngineError> {
12007        use spg_storage::PartitionRole;
12008        let cat = self.active_catalog();
12009        let parent = cat.get(parent_name).ok_or_else(|| {
12010            EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
12011                "partition parent {parent_name:?} disappeared mid-expansion"
12012            )))
12013        })?;
12014        let (key_position, parent_kind) = match &parent.schema().partition_role {
12015            Some(PartitionRole::Parent {
12016                key_column_positions,
12017                kind,
12018                ..
12019            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
12020            // v7.39 (round 645) — an INHERITANCE parent, which has no
12021            // role of its own: the relationship is recorded only in the
12022            // children. Three things differ from a partition parent and
12023            // all three are in this body.
12024            //
12025            //   * The parent HOLDS ROWS, so it is a term of the union —
12026            //     `FROM ONLY`, or expanding it would recurse.
12027            //   * There is no partition key, so there is nothing to
12028            //     prune: every child is a term.
12029            //   * A child may declare columns of its own, so the terms
12030            //     name the PARENT's columns rather than `*`. PG's
12031            //     `SELECT * FROM parent` returns the parent's shape.
12032            //
12033            // Answered from this match rather than a branch before it —
12034            // round 644 measured what an extra early return beside an
12035            // existing test costs in this file.
12036            _ if crate::partition::has_inheritance_children(cat, parent_name) => {
12037                let cols = parent
12038                    .schema()
12039                    .columns
12040                    .iter()
12041                    .map(|c| quote_ident_for_sql(&c.name))
12042                    .collect::<Vec<_>>()
12043                    .join(", ");
12044                let carry_sys = references_ctid(outer);
12045                let sys = if carry_sys {
12046                    let mut t = alloc::string::String::new();
12047                    for s in SYSTEM_COLUMNS {
12048                        t.push_str(", ");
12049                        t.push_str(s);
12050                    }
12051                    t
12052                } else {
12053                    alloc::string::String::new()
12054                };
12055                let mut body = alloc::format!(
12056                    "SELECT {cols}{sys} FROM ONLY {}",
12057                    quote_ident_for_sql(parent_name)
12058                );
12059                for child in crate::partition::children_of_parent(cat, parent_name) {
12060                    body.push_str(&alloc::format!(
12061                        " UNION ALL SELECT {cols}{sys} FROM {}",
12062                        quote_ident_for_sql(&child)
12063                    ));
12064                }
12065                return parse_select_or_corrupt(&body).map(Some);
12066            }
12067            _ => {
12068                return Err(EngineError::Unsupported(alloc::format!(
12069                    "partition expansion: {parent_name:?} is not a parent"
12070                )));
12071            }
12072        };
12073        let key_col_name = parent.schema().columns[key_position].name.clone();
12074        // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
12075        // off the WHERE; for LIST / HASH we extract a single `=`
12076        // literal (and the rest of the planner falls back to "keep
12077        // every child" — same conservative path as 16.1/16.2).
12078        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
12079            Some(expr) => extract_key_range(expr, &key_col_name),
12080            None => (None, None),
12081        };
12082        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
12083            Some(expr) => extract_key_eq_value(expr, &key_col_name),
12084            None => None,
12085        };
12086        let children = crate::partition::children_of_parent(cat, parent_name);
12087        let mut kept: Vec<String> = Vec::new();
12088        let mut default_child: Option<String> = None;
12089        // First pass — apply per-strategy gates, defer DEFAULT until
12090        // we know whether some non-DEFAULT child matched.
12091        for child_name in &children {
12092            let Some(child) = cat.get(child_name) else {
12093                continue;
12094            };
12095            match &child.schema().partition_role {
12096                Some(PartitionRole::Range { lower, upper, .. }) => {
12097                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
12098                        kept.push(child_name.clone());
12099                    }
12100                }
12101                // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
12102                // = <lit>`, only the child whose values contain that
12103                // literal survives. Otherwise (no equality predicate
12104                // or planner couldn't extract one) keep the child
12105                // conservatively.
12106                Some(PartitionRole::List { values, .. }) => match &eq_value {
12107                    Some(v) => {
12108                        if values.iter().any(|b| b.equals_value(v)) {
12109                            kept.push(child_name.clone());
12110                        }
12111                    }
12112                    None => kept.push(child_name.clone()),
12113                },
12114                // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
12115                // we know the residue class deterministically, so
12116                // only the matching REMAINDER child survives.
12117                Some(PartitionRole::Hash {
12118                    modulus, remainder, ..
12119                }) => match &eq_value {
12120                    Some(v) => {
12121                        let h = crate::partition::pg_compatible_hash(v);
12122                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
12123                            kept.push(child_name.clone());
12124                        }
12125                    }
12126                    None => kept.push(child_name.clone()),
12127                },
12128                Some(PartitionRole::Default { .. }) => {
12129                    default_child = Some(child_name.clone());
12130                }
12131                _ => {}
12132            }
12133        }
12134        // PG-style DEFAULT semantics: the DEFAULT child must be
12135        // scanned iff some row could fall outside every concrete
12136        // child's bound predicate. We approximate that as "no
12137        // concrete child matched" (== full prune) — strictly
12138        // conservative for LIST / HASH (DEFAULT also catches rows
12139        // outside the union of value-sets / residues), and matches
12140        // PG for the equality case where we *do* know the routing
12141        // outcome.
12142        let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
12143        if let Some(d) = default_child {
12144            if kept.is_empty() {
12145                kept.push(d);
12146            } else if eq_value.is_none() {
12147                // Without an equality literal, the DEFAULT child may
12148                // still hold matching rows (e.g. LIKE on TEXT keys
12149                // for which a LIST partition exists). Keep it.
12150                kept.push(d);
12151            }
12152        }
12153        // Build the UNION ALL body text and re-parse — keeps the
12154        // rewrite expressible in surface SQL so the engine's existing
12155        // parser path handles the AST shape uniformly.
12156        if kept.is_empty() {
12157            // No children survive — caller falls back to scanning the
12158            // (empty) parent table. Returning None here is what
12159            // prevents the synthetic CTE from referring back to the
12160            // parent name and re-entering this rewrite pass.
12161            let _ = parent_name;
12162            return Ok(None);
12163        }
12164        // v7.39 (round 622, S05a) — the system columns of the CHILD the row
12165        // actually lives in.
12166        //
12167        // The parent is read through a synthetic CTE, so a `tableoid` on it
12168        // resolved against that CTE: every row of every child reported
12169        // `__spg_partition_pm`, an internal name no user ever typed, where
12170        // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
12171        // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
12172        // one asks "which partition is this row in", answering 0 rows where
12173        // PG answers 1. `ctid` had the same shape: it numbered the CTE's
12174        // output, so rows in different children got distinct ctids instead
12175        // of each child's own physical position.
12176        //
12177        // Naming them in the term is what carries them: the child scan
12178        // materialises its own six because the statement now references
12179        // them, and they land in SYSTEM_COLUMNS order right after the user
12180        // columns — the exact layout the positional `*` skip already
12181        // expects. Only done when the outer statement asks for one, so a
12182        // plain `SELECT * FROM parent` scans exactly what it scanned.
12183        let carry_sys = references_ctid(outer);
12184        let mut body = alloc::string::String::new();
12185        for (i, child_name) in kept.iter().enumerate() {
12186            if i > 0 {
12187                body.push_str(" UNION ALL ");
12188            }
12189            body.push_str("SELECT *");
12190            if carry_sys {
12191                for sys in SYSTEM_COLUMNS {
12192                    body.push_str(", ");
12193                    body.push_str(sys);
12194                }
12195            }
12196            body.push_str(" FROM ");
12197            body.push_str(&quote_ident_for_sql(child_name));
12198        }
12199        parse_select_or_corrupt(&body).map(Some)
12200    }
12201}
12202
12203/// Rewrite a `TableRef` pointing at a partition parent so it
12204/// references the synthetic CTE created by the expansion. If the
12205/// original ref had no alias, preserve the parent name as an alias
12206/// so column references like `events_partitioned.received_at`
12207/// keep resolving.
12208fn rewrite_partition_parent_table_ref(
12209    t: &mut spg_sql::ast::TableRef,
12210    parents: &[alloc::string::String],
12211    synth_name: &impl Fn(&str) -> alloc::string::String,
12212) {
12213    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
12214        return;
12215    }
12216    // v7.39 (round 644) — an ONLY reference stays pointed at the parent
12217    // itself. The rewrite is keyed on the NAME, so in
12218    // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
12219    // parent list and this then rewrote BOTH — including the one that
12220    // asked not to descend. PG answers 0 for that join; SPG answered 2.
12221    // Folded into the existing test — see the note in
12222    // `collect_partition_parent_refs` for what a separate one cost.
12223    if t.only || !parents.iter().any(|p| p == &t.name) {
12224        return;
12225    }
12226    if t.alias.is_none() {
12227        t.alias = Some(t.name.clone());
12228    }
12229    t.name = synth_name(&t.name);
12230}
12231
12232/// Walk a `TableRef` and push its `name` if it resolves to a partition
12233/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
12234/// `generate_series_args` references — those aren't catalog tables.
12235fn collect_partition_parent_refs(
12236    t: &spg_sql::ast::TableRef,
12237    cat: &spg_storage::Catalog,
12238    out: &mut Vec<alloc::string::String>,
12239) {
12240    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
12241        return;
12242    }
12243    // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
12244    // The keyword used to be absorbed at parse time, so this fanned out
12245    // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
12246    // answered 2 where PG answers 0.
12247    //
12248    // Folded into the existing test rather than given an early return of
12249    // its own: as two extra lines in this function's body it cost
12250    // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
12251    // outside the panel. Rounds 641 and 643 met the same wall from the
12252    // other two directions — adding to a hot function and taking away
12253    // from a cold one. What goes in a body near the row loop is a
12254    // codegen decision whatever its shape.
12255    if !t.only && crate::partition::has_children(cat, &t.name) {
12256        out.push(t.name.clone());
12257    }
12258}
12259
12260/// v7.37.6-B partition-key range derived from a WHERE expression.
12261/// `i64` microseconds since epoch with the same sign convention as
12262/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
12263/// / `=`),`false` ⇒ exclusive(`>` / `<`).
12264#[derive(Debug, Clone, Copy)]
12265pub(crate) struct PartitionFilterBound {
12266    pub micros: i64,
12267    pub inclusive: bool,
12268}
12269
12270/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
12271/// shapes; tighten the running lo / hi as we go. Anything outside that
12272/// (OR / nested calls / non-key columns)is ignored — caller treats
12273/// `None` as "no constraint on that side."
12274fn extract_key_range(
12275    expr: &spg_sql::ast::Expr,
12276    key_col: &str,
12277) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
12278    let mut lo: Option<PartitionFilterBound> = None;
12279    let mut hi: Option<PartitionFilterBound> = None;
12280    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
12281    while let Some(e) = stack.pop() {
12282        match e {
12283            spg_sql::ast::Expr::Binary {
12284                lhs,
12285                op: spg_sql::ast::BinOp::And,
12286                rhs,
12287            } => {
12288                stack.push(lhs);
12289                stack.push(rhs);
12290            }
12291            // BETWEEN is desugared at parse time into `lhs >= low AND
12292            // lhs <= high`, so it lands here as two regular Binary
12293            // arms via the AND walker above.
12294            spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
12295                let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
12296                    (Some(lhs.as_ref()), rhs.as_ref(), false)
12297                } else if is_column_ref(rhs, key_col) {
12298                    (Some(rhs.as_ref()), lhs.as_ref(), true)
12299                } else {
12300                    (None, lhs.as_ref(), false)
12301                };
12302                if col_ref.is_none() {
12303                    continue;
12304                }
12305                let Some(lit) = literal_to_micros(lit_side) else {
12306                    continue;
12307                };
12308                use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
12309                let effective_op = if swapped {
12310                    match op {
12311                        Lt => Gt,
12312                        LtEq => GtEq,
12313                        Gt => Lt,
12314                        GtEq => LtEq,
12315                        other => *other,
12316                    }
12317                } else {
12318                    *op
12319                };
12320                match effective_op {
12321                    Eq => {
12322                        tighten_lo(
12323                            &mut lo,
12324                            PartitionFilterBound {
12325                                micros: lit,
12326                                inclusive: true,
12327                            },
12328                        );
12329                        tighten_hi(
12330                            &mut hi,
12331                            PartitionFilterBound {
12332                                micros: lit,
12333                                inclusive: true,
12334                            },
12335                        );
12336                    }
12337                    GtEq => {
12338                        tighten_lo(
12339                            &mut lo,
12340                            PartitionFilterBound {
12341                                micros: lit,
12342                                inclusive: true,
12343                            },
12344                        );
12345                    }
12346                    Gt => {
12347                        tighten_lo(
12348                            &mut lo,
12349                            PartitionFilterBound {
12350                                micros: lit,
12351                                inclusive: false,
12352                            },
12353                        );
12354                    }
12355                    LtEq => {
12356                        tighten_hi(
12357                            &mut hi,
12358                            PartitionFilterBound {
12359                                micros: lit,
12360                                inclusive: true,
12361                            },
12362                        );
12363                    }
12364                    Lt => {
12365                        tighten_hi(
12366                            &mut hi,
12367                            PartitionFilterBound {
12368                                micros: lit,
12369                                inclusive: false,
12370                            },
12371                        );
12372                    }
12373                    _ => {}
12374                }
12375            }
12376            _ => {}
12377        }
12378    }
12379    (lo, hi)
12380}
12381
12382fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
12383    match slot {
12384        None => *slot = Some(new),
12385        Some(cur) => {
12386            if new.micros > cur.micros
12387                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
12388            {
12389                *slot = Some(new);
12390            }
12391        }
12392    }
12393}
12394
12395fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
12396    match slot {
12397        None => *slot = Some(new),
12398        Some(cur) => {
12399            if new.micros < cur.micros
12400                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
12401            {
12402                *slot = Some(new);
12403            }
12404        }
12405    }
12406}
12407
12408fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
12409    if let spg_sql::ast::Expr::Column(c) = e {
12410        c.name.eq_ignore_ascii_case(key_col)
12411    } else {
12412        false
12413    }
12414}
12415
12416/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
12417/// `key_col = <literal>` predicate out for LIST/HASH partition
12418/// pruning. Returns `None` when no equality literal can be lifted
12419/// (planner then keeps every child — correctness preserved). The
12420/// returned `Value<'static>` is an owned coercion so the caller can
12421/// outlive any AST node it was extracted from.
12422pub(crate) fn extract_key_eq_value(
12423    expr: &spg_sql::ast::Expr,
12424    key_col: &str,
12425) -> Option<spg_storage::Value<'static>> {
12426    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
12427    while let Some(e) = stack.pop() {
12428        match e {
12429            spg_sql::ast::Expr::Binary {
12430                lhs,
12431                op: spg_sql::ast::BinOp::And,
12432                rhs,
12433            } => {
12434                stack.push(lhs);
12435                stack.push(rhs);
12436            }
12437            spg_sql::ast::Expr::Binary {
12438                lhs,
12439                op: spg_sql::ast::BinOp::Eq,
12440                rhs,
12441            } => {
12442                let lit_side = if is_column_ref(lhs, key_col) {
12443                    rhs.as_ref()
12444                } else if is_column_ref(rhs, key_col) {
12445                    lhs.as_ref()
12446                } else {
12447                    continue;
12448                };
12449                let cloned = lit_side.clone();
12450                let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
12451                    continue;
12452                };
12453                // Coerce to an owned Value<'static> so the caller
12454                // can hold it past the WHERE expression's lifetime.
12455                let owned: spg_storage::Value<'static> = match v {
12456                    spg_storage::Value::Text(s) => {
12457                        spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
12458                    }
12459                    spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
12460                    spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
12461                    spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
12462                    spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
12463                    spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
12464                    spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
12465                    spg_storage::Value::Null => spg_storage::Value::Null,
12466                    // Anything else (Vector / Json / Bytes / Numeric /
12467                    // arrays / interval / …) isn't a current partition
12468                    // key type; skip without pruning.
12469                    _ => continue,
12470                };
12471                return Some(owned);
12472            }
12473            _ => {}
12474        }
12475    }
12476    None
12477}
12478
12479/// Coerce a literal Expr(after the parser folded sequence calls etc.)
12480/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
12481/// pruning and routing agree on the literal vocabulary. Returns
12482/// `None` when the literal isn't recognised(planner then skips
12483/// pruning on that branch — correctness preserved).
12484fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
12485    let cloned = e.clone();
12486    let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
12487    match value {
12488        spg_storage::Value::Timestamp(m) => Some(m),
12489        spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
12490        spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
12491        _ => None,
12492    }
12493}
12494
12495/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
12496/// satisfying the WHERE-derived filter range. PG-style half-open:
12497/// child upper exclusive. Filter inclusivity is honoured per-bound.
12498fn range_satisfies_filter(
12499    range_lo: &spg_storage::PartitionBound,
12500    range_hi: &spg_storage::PartitionBound,
12501    filter_lo: Option<&PartitionFilterBound>,
12502    filter_hi: Option<&PartitionFilterBound>,
12503) -> bool {
12504    use spg_storage::PartitionBound;
12505    // For each filter side, reject children that can't host any row
12506    // matching the predicate.
12507    if let Some(lo) = filter_lo {
12508        // child upper bound vs filter lower:
12509        //   if filter is x >= L, child rejects iff child.hi <= L
12510        //   if filter is x  > L, child rejects iff child.hi <= L
12511        //   (child.hi exclusive, so equality with L still rejects)
12512        match range_hi {
12513            PartitionBound::MinValue => return false,
12514            PartitionBound::MaxValue => {}
12515            PartitionBound::TimestampTz(hi) => {
12516                if *hi <= lo.micros {
12517                    return false;
12518                }
12519            }
12520            // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
12521            // matched against TIMESTAMPTZ filters here; keep child
12522            // (conservative: don't prune).
12523            PartitionBound::BigInt(_)
12524            | PartitionBound::Int(_)
12525            | PartitionBound::SmallInt(_)
12526            | PartitionBound::Date(_)
12527            | PartitionBound::Text(_) => {}
12528        }
12529    }
12530    if let Some(hi) = filter_hi {
12531        // child lower bound vs filter upper:
12532        //   if filter is x <= U, child rejects iff child.lo > U
12533        //   if filter is x  < U, child rejects iff child.lo >= U
12534        match range_lo {
12535            PartitionBound::MaxValue => return false,
12536            PartitionBound::MinValue => {}
12537            PartitionBound::TimestampTz(lo) => {
12538                let rejects = if hi.inclusive {
12539                    *lo > hi.micros
12540                } else {
12541                    *lo >= hi.micros
12542                };
12543                if rejects {
12544                    return false;
12545                }
12546            }
12547            PartitionBound::BigInt(_)
12548            | PartitionBound::Int(_)
12549            | PartitionBound::SmallInt(_)
12550            | PartitionBound::Date(_)
12551            | PartitionBound::Text(_) => {}
12552        }
12553    }
12554    true
12555}
12556
12557fn quote_ident_for_sql(name: &str) -> alloc::string::String {
12558    // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
12559    // identifier, otherwise quoted). Conservative: always quote so
12560    // children with reserved names round-trip safely through the
12561    // CTE-body parse.
12562    let mut out = alloc::string::String::with_capacity(name.len() + 2);
12563    out.push('"');
12564    for c in name.chars() {
12565        if c == '"' {
12566            out.push('"');
12567        }
12568        out.push(c);
12569    }
12570    out.push('"');
12571    out
12572}
12573
12574fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
12575    let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
12576        EngineError::Unsupported(alloc::format!(
12577            "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
12578        ))
12579    })?;
12580    let Statement::Select(body) = parsed else {
12581        return Err(EngineError::Unsupported(alloc::format!(
12582            "partition expansion: generated SQL {sql:?} is not a SELECT"
12583        )));
12584    };
12585    Ok(body)
12586}
12587
12588/// v7.39 (read01 round 65/66) — the column shape a set-returning function
12589/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
12590/// yields ONE column named after the call's alias when there is one (`FROM
12591/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
12592/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
12593fn setof_column_shape_from(
12594    declared: &str,
12595    name: &str,
12596    alias: Option<&str>,
12597    got: &[ColumnSchema],
12598) -> alloc::vec::Vec<ColumnSchema> {
12599    let upper = declared.to_ascii_uppercase();
12600    if upper.starts_with("TABLE(") {
12601        let raw = &declared["TABLE(".len()..declared.len() - 1];
12602        return raw
12603            .split(',')
12604            .zip(got.iter())
12605            .map(|(decl, g)| {
12606                let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
12607                ColumnSchema::new(cname.to_string(), g.ty, true)
12608            })
12609            .collect();
12610    }
12611    let cname = alias.unwrap_or(name);
12612    got.first()
12613        .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
12614        .unwrap_or_default()
12615}
12616
12617/// The plpgsql twin: the interpreter hands back raw value rows, so the types
12618/// come off the first row.
12619fn setof_column_shape(
12620    declared: &str,
12621    name: &str,
12622    alias: Option<&str>,
12623    first_row: Option<&alloc::vec::Vec<Value<'static>>>,
12624) -> alloc::vec::Vec<ColumnSchema> {
12625    let got: alloc::vec::Vec<ColumnSchema> = first_row
12626        .map(|r| {
12627            r.iter()
12628                .enumerate()
12629                .map(|(i, v)| {
12630                    ColumnSchema::new(
12631                        alloc::format!("col{i}"),
12632                        v.data_type().unwrap_or(DataType::Text),
12633                        true,
12634                    )
12635                })
12636                .collect()
12637        })
12638        .unwrap_or_default();
12639    setof_column_shape_from(declared, name, alias, &got)
12640}
12641
12642/// v7.39 (read01 round 67) — expand every set-returning call in a target list
12643/// for ONE input row, PG's ProjectSet semantics.
12644///
12645/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
12646/// output has as many rows as the LONGEST of them, and a shorter one is padded
12647/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
12648/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
12649/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
12650/// is zero rows, not one NULL row.
12651///
12652/// Non-SRF items repeat, evaluated once per output row from the same input row.
12653/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
12654/// used to reach the scalar function dispatcher, which reported the aggregate as
12655/// an *unknown function* — the same "symptom two layers above the cause" shape
12656/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
12657/// sees a call, not the clause it came from. The statement knows.
12658/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
12659/// clause may appear.
12660///
12661/// PG rejects `FOR UPDATE` on exactly the shapes that have no
12662/// identifiable base row to lock, each with its own wording. SPG
12663/// accepted all of them and locked nothing, so a query that PG refuses
12664/// outright came back looking like it had taken locks.
12665///
12666/// Every wording read off live PG 18.4.
12667fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
12668    let Some(lock) = &stmt.locking else {
12669        return Ok(());
12670    };
12671    let verb = lock_clause_verb(lock.strength);
12672    let refuse = |what: &str| {
12673        Err(EngineError::Unsupported(alloc::format!(
12674            "{verb} is not allowed with {what}"
12675        )))
12676    };
12677    if !stmt.unions.is_empty() {
12678        return refuse("UNION/INTERSECT/EXCEPT");
12679    }
12680    if stmt.distinct || !stmt.distinct_on.is_empty() {
12681        return refuse("DISTINCT clause");
12682    }
12683    if stmt.group_by.is_some() || stmt.group_by_all {
12684        return refuse("GROUP BY clause");
12685    }
12686    let has_agg = stmt.items.iter().any(|it| match it {
12687        spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
12688        _ => false,
12689    });
12690    if has_agg {
12691        return refuse("aggregate functions");
12692    }
12693    // `FOR UPDATE OF t` must name a relation that is actually in FROM.
12694    for want in &lock.of_tables {
12695        if !locking_from_names(stmt)
12696            .iter()
12697            .any(|n| n.eq_ignore_ascii_case(want))
12698        {
12699            return Err(EngineError::Unsupported(alloc::format!(
12700                "relation \"{want}\" in {verb} clause not found in FROM clause"
12701            )));
12702        }
12703    }
12704    Ok(())
12705}
12706
12707/// How PG names the clause in its diagnostics.
12708const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
12709    use spg_sql::ast::LockStrength as LS;
12710    match s {
12711        LS::Update => "FOR UPDATE",
12712        LS::NoKeyUpdate => "FOR NO KEY UPDATE",
12713        LS::Share => "FOR SHARE",
12714        LS::KeyShare => "FOR KEY SHARE",
12715    }
12716}
12717
12718/// Every relation name (or alias) the FROM clause exposes.
12719fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
12720    let mut out = alloc::vec::Vec::new();
12721    if let Some(f) = &stmt.from {
12722        let mut push = |t: &spg_sql::ast::TableRef| {
12723            if let Some(a) = &t.alias {
12724                out.push(a.clone());
12725            }
12726            out.push(t.name.clone());
12727        };
12728        push(&f.primary);
12729        for j in &f.joins {
12730            push(&j.table);
12731        }
12732    }
12733    out
12734}
12735
12736fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
12737    use spg_sql::ast::Expr;
12738    if let Some(w) = &stmt.where_
12739        && aggregate::contains_aggregate(w)
12740    {
12741        return Err(EngineError::Unsupported(
12742            "aggregate functions are not allowed in WHERE".into(),
12743        ));
12744    }
12745    let mut nested = false;
12746    let mut check = |e: &Expr| {
12747        let mut probe = e.clone();
12748        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
12749            let args = match n {
12750                Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
12751                _ => return false,
12752            };
12753            if args.iter().any(aggregate::contains_aggregate) {
12754                nested = true;
12755            }
12756            false
12757        });
12758    };
12759    for it in &stmt.items {
12760        if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
12761            check(expr);
12762        }
12763    }
12764    if let Some(h) = &stmt.having {
12765        check(h);
12766    }
12767    for o in &stmt.order_by {
12768        check(&o.expr);
12769    }
12770    if nested {
12771        return Err(EngineError::Unsupported(
12772            "aggregate function calls cannot be nested".into(),
12773        ));
12774    }
12775    Ok(())
12776}
12777
12778/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
12779/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
12780/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
12781/// to a set and then applies the enclosing expression once per element. SPG only
12782/// ever recognised an SRF that WAS the item, so everything above died on
12783/// "unknown function unnest" — the set-returning call, wrapped in anything at
12784/// all, fell through to the scalar function dispatcher which has no such name.
12785///
12786/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
12787/// rewritten to read that column, and the rewritten expression is evaluated once
12788/// per output row against the input row extended with the lifted values. The
12789/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
12790/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
12791/// executors (the single-table scan, the synthetic-table pipeline, and the
12792/// unnest FROM path) each evaluated the key as an ordinary expression, where the
12793/// literal `n` is just the constant n — the same sort key for every row. The
12794/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
12795/// back in input order, not in a wrong order. Statement prep resolves the common
12796/// case, but only when the SELECT item is an expression — a `*` is not one, and
12797/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
12798/// spelling landed on exactly the shape prep could not resolve.
12799///
12800/// A set-returning item is left alone: copying it into ORDER BY would make the
12801/// key "the whole set", evaluated once per INPUT row.
12802fn resolve_positional_order_by(
12803    order_by: &[spg_sql::ast::OrderBy],
12804    projection: &[ProjectedItem],
12805) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
12806    order_by
12807        .iter()
12808        .map(|o| {
12809            let mut o = o.clone();
12810            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
12811                && *n >= 1
12812                && let Ok(idx) = usize::try_from(*n - 1)
12813                && let Some(item) = projection.get(idx)
12814                && !expr_contains_builtin_srf(&item.expr)
12815            {
12816                o.expr = item.expr.clone();
12817            }
12818            o
12819        })
12820        .collect()
12821}
12822
12823/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
12824/// this expression? Statement preparation (`resolve_order_by_position`) runs
12825/// before any catalog is in hand, and it only needs to know "is this item's value
12826/// a set", which the builtin SRFs answer syntactically.
12827pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
12828    let mut found = false;
12829    let mut probe = e.clone();
12830    crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
12831        if is_top_level_unnest(n) {
12832            found = true;
12833            return true;
12834        }
12835        false
12836    });
12837    found
12838}
12839
12840/// v7.39 (round 599) — everything about a target-list SRF that does not
12841/// depend on the row.
12842///
12843/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
12844/// each SRF-bearing projection expression, walked and rewrote the tree,
12845/// formatted a `__srf_N` name per node, and copied the whole column schema.
12846/// A counting allocator put the path at 24 allocations per input row for a
12847/// single-element `unnest`, against 0 for the same scan without one — 211 MB
12848/// where the plain scan took 4.3 — and the shape held whatever the array
12849/// contained, which is what invariant work looks like.
12850struct SrfPlan {
12851    /// The lifted SRF calls, in slot order.
12852    nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
12853    /// Per projection position, the expression with its SRF calls replaced
12854    /// by `__srf_N` column references. `None` means the item has none.
12855    rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
12856    /// The input schema followed by one column per slot. Only the slots'
12857    /// TYPES vary per row, and they are patched in place.
12858    ext_cols: alloc::vec::Vec<ColumnSchema>,
12859    /// v7.39 (round 743) — the rewritten projection COMPILED against the
12860    /// extended schema, once per plan. The per-output-row evaluation ran
12861    /// the interpreter (~560 ns/row on the unnest panel cell); the Step
12862    /// VM reads the `__srf_N` slots as plain columns. `None` = that item
12863    /// is not fully compilable and keeps the interpreter.
12864    compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
12865    base_cols: usize,
12866}
12867
12868fn build_srf_plan(
12869    engine: &Engine,
12870    projection: &[ProjectedItem],
12871    srf_idxs: &[usize],
12872    ctx: &EvalContext<'_>,
12873) -> Result<SrfPlan, EngineError> {
12874    // Lift every SRF node out of every item that contains one.
12875    let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
12876    let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
12877    let mut reject: Option<EngineError> = None;
12878    for &i in srf_idxs {
12879        let mut e = projection[i].expr.clone();
12880        crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
12881            if reject.is_some() {
12882                return true;
12883            }
12884            // PG refuses a set-returning function inside a conditional: the set
12885            // would have to be produced before anyone knows whether the branch
12886            // is even taken.
12887            let conditional = match n {
12888                spg_sql::ast::Expr::Case { .. } => Some("CASE"),
12889                spg_sql::ast::Expr::FunctionCall { name, .. }
12890                    if name.eq_ignore_ascii_case("coalesce") =>
12891                {
12892                    Some("COALESCE")
12893                }
12894                _ => None,
12895            };
12896            if let Some(kind) = conditional
12897                && engine.expr_contains_srf(n)
12898            {
12899                reject = Some(EngineError::Unsupported(alloc::format!(
12900                    "set-returning functions are not allowed in {kind}"
12901                )));
12902                return true;
12903            }
12904            if !engine.is_srf_node(n) {
12905                return false;
12906            }
12907            let slot = nodes.len();
12908            nodes.push(n.clone());
12909            *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
12910                qualifier: None,
12911                name: alloc::format!("__srf_{slot}"),
12912            });
12913            true
12914        });
12915        rewritten[i] = Some(e);
12916    }
12917    if let Some(err) = reject {
12918        return Err(err);
12919    }
12920    let base_cols = ctx.columns.len();
12921    let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
12922    for slot in 0..nodes.len() {
12923        ext_cols.push(ColumnSchema::new(
12924            alloc::format!("__srf_{slot}"),
12925            DataType::Text,
12926            true,
12927        ));
12928    }
12929    // v7.39 (round 743) — compile the rewritten items against the
12930    // EXTENDED schema. The slot columns' declared type is a per-row
12931    // patched detail the compiled column read does not consult.
12932    let compiled: Vec<Option<eval::CompiledExpr>> = {
12933        let mut ext_ctx = ctx.clone();
12934        ext_ctx.columns = &ext_cols;
12935        projection
12936            .iter()
12937            .enumerate()
12938            .map(|(i, p)| {
12939                let e = rewritten[i].as_ref().unwrap_or(&p.expr);
12940                if eval::fully_compilable(e) {
12941                    Some(eval::compile_expr(e, &ext_ctx))
12942                } else {
12943                    None
12944                }
12945            })
12946            .collect()
12947    };
12948    Ok(SrfPlan {
12949        nodes,
12950        rewritten,
12951        ext_cols,
12952        compiled,
12953        base_cols,
12954    })
12955}
12956
12957/// One input row expanded through a plan built once for the whole scan.
12958/// v7.39 (round 621) — expand a projection whose target list contains
12959/// set-returning items, remembering which INPUT row each output row came from.
12960///
12961/// The three materialised-source tails — `FROM unnest(…)`, `FROM
12962/// generate_series(…)`, and the one that serves VALUES / a derived table /
12963/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
12964/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
12965/// v(x)` answered `function unnest(integer[]) does not exist` on all the
12966/// others, for a query PG answers. Sharing the expansion is the point: a
12967/// fourth copy would have been the fourth place to forget.
12968fn expand_projection_srfs(
12969    engine: &Engine,
12970    projection: &[ProjectedItem],
12971    srf_idxs: &[usize],
12972    filtered: &[Row<'static>],
12973    ctx: &EvalContext<'_>,
12974) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
12975    let mut out = alloc::vec::Vec::with_capacity(filtered.len());
12976    let mut src = alloc::vec::Vec::with_capacity(filtered.len());
12977    // v7.39 (round 726) — ONE plan for the whole scan. The per-row
12978    // spelling rebuilt it for every input row: a full clone of the
12979    // rewritten projection trees and the extended schema, 50k times on
12980    // the panel's unnest cell.
12981    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
12982    // v7.39 (round 733) — shard the expansion. Each shard clones the
12983    // plan (its ext_cols slot types are per-row mutable) and builds a
12984    // MINIMAL context — EvalContext is not Sync — which is sound only
12985    // when every expression involved is pure: the whole projection and
12986    // every SRF argument must be fully_compilable, or the row loop
12987    // stays serial with the full session context.
12988    // The projection is judged in its REWRITTEN form — the SRF call
12989    // itself is never compilable, but after the lift it is a plain
12990    // `__srf_N` column reference.
12991    let all_pure = projection
12992        .iter()
12993        .enumerate()
12994        .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
12995        && plan.nodes.iter().all(|n| match n {
12996            Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
12997            other => eval::fully_compilable(other),
12998        });
12999    if all_pure
13000        && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
13001        && let Some(r) = engine.parallel_runner.0.as_deref()
13002    {
13003        let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
13004        let chunk = filtered.len().div_ceil(n_shards);
13005        type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
13006        let schema_cols = ctx.columns;
13007        let alias = ctx.table_alias;
13008        let mysql = ctx.mysql_dialect;
13009        let style = ctx.render_style;
13010        let plan_ref = &plan;
13011        let results = r.run_shards(n_shards, &|si| {
13012            let lo = si * chunk;
13013            let hi = ((si + 1) * chunk).min(filtered.len());
13014            let mut sctx = eval::EvalContext::new(schema_cols, alias);
13015            sctx.mysql_dialect = mysql;
13016            sctx.render_style = style;
13017            // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
13018            // compiled programs); each shard rebuilds it, which also
13019            // recompiles against the shard's own context. Build errors
13020            // were already surfaced by the outer build above.
13021            let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
13022                Ok(p) => p,
13023                Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
13024            };
13025            let mut run = || -> ShardOut {
13026                let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
13027                let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
13028                for (i, row) in filtered[lo..hi].iter().enumerate() {
13029                    let expanded =
13030                        expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
13031                    sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
13032                    o.extend(expanded);
13033                }
13034                Ok((o, sidx))
13035            };
13036            alloc::boxed::Box::new(run())
13037        });
13038        for boxed in results {
13039            let shard = boxed
13040                .downcast::<ShardOut>()
13041                .expect("runner echoes the closure's box");
13042            let (o, sidx) = (*shard)?;
13043            out.extend(o);
13044            src.extend(sidx);
13045        }
13046        return Ok((out, src));
13047    }
13048    for (i, row) in filtered.iter().enumerate() {
13049        let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
13050        src.extend(core::iter::repeat_n(i, expanded.len()));
13051        out.extend(expanded);
13052    }
13053    Ok((out, src))
13054}
13055
13056/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
13057///
13058/// A key that names a select-list item reads it out of the EXPANDED row,
13059/// because PG sorts after the expansion. A key that names a source column the
13060/// query does not project is evaluated against the input row that output row
13061/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
13062fn srf_order_key(
13063    ob: &spg_sql::ast::OrderBy,
13064    out_col: Option<usize>,
13065    out: &Row<'static>,
13066    src: &Row<'static>,
13067    ctx: &EvalContext<'_>,
13068) -> Result<Value<'static>, EngineError> {
13069    match out_col {
13070        Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
13071        None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
13072    }
13073}
13074
13075fn expand_srf_row_with(
13076    engine: &Engine,
13077    plan: &mut SrfPlan,
13078    projection: &[ProjectedItem],
13079    row: &Row<'static>,
13080    ctx: &EvalContext<'_>,
13081) -> Result<Vec<Row<'static>>, EngineError> {
13082    let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
13083    for n in &plan.nodes {
13084        lists.push(engine.srf_values(n, row, ctx)?);
13085    }
13086    let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
13087    // Only the slots' element types depend on the row; the names and the
13088    // input schema around them do not.
13089    for (slot, list) in lists.iter().enumerate() {
13090        plan.ext_cols[plan.base_cols + slot].ty = list
13091            .iter()
13092            .find_map(|v| v.data_type())
13093            .unwrap_or(DataType::Text);
13094    }
13095    let mut ext_ctx = ctx.clone();
13096    ext_ctx.columns = &plan.ext_cols;
13097    let mut out = Vec::with_capacity(n_rows);
13098    // v7.39 (round 726) — the base columns are the SAME for every
13099    // expanded row; clone them once and rewrite only the SRF slots per
13100    // k. The old form cloned the whole input row per OUTPUT row — for
13101    // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
13102    // TEXT column the projection never reads.
13103    let base_len = row.values.len();
13104    let mut ext_vals = row.values.clone();
13105    ext_vals.resize(base_len + lists.len(), Value::Null);
13106    let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
13107    for k in 0..n_rows {
13108        for (slot, list) in lists.iter().enumerate() {
13109            // Past the end of THIS srf's rows → NULL (PG pads).
13110            ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
13111        }
13112        let ext_row = Row::new(core::mem::take(&mut ext_vals));
13113        let mut vals = Vec::with_capacity(projection.len());
13114        for (i, p) in projection.iter().enumerate() {
13115            // v7.39 (round 743) — compiled when possible; the
13116            // interpreter for the rest, with its exact wording.
13117            vals.push(match &plan.compiled[i] {
13118                Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
13119                    .map_err(EngineError::Eval)?,
13120                None => {
13121                    let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
13122                    eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
13123                }
13124            });
13125        }
13126        ext_vals = ext_row.values;
13127        out.push(Row::new(vals));
13128    }
13129    Ok(out)
13130}
13131
13132/// The one-shot spelling, for the callers that expand a single row.
13133/// v7.39 (round 600) — which output column each ORDER BY key names, for a
13134/// query whose target list contains a set-returning function.
13135///
13136/// The keys used to be built from the INPUT row, before the SRF expanded, so
13137/// anything that named the SRF's own output was evaluated as a scalar call:
13138/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
13139/// "function unnest(integer[]) does not exist", and so did the spellings that
13140/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
13141/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
13142/// back in input order. PG sorts AFTER the expansion, so a key that names a
13143/// select-list item reads that item's value out of the expanded row.
13144///
13145/// `None` keeps the key on the input row, which is where an ORDER BY naming
13146/// a column the query does not project has to be evaluated.
13147fn srf_order_output_cols(
13148    order_by: &[spg_sql::ast::OrderBy],
13149    projection: &[ProjectedItem],
13150) -> Vec<Option<usize>> {
13151    order_by
13152        .iter()
13153        .map(|ob| {
13154            // A positive ordinal is the Nth output column, directly.
13155            // `resolve_positional_order_by` deliberately leaves an ordinal
13156            // pointing at a set-returning item alone — copying the call into
13157            // ORDER BY would have made the key "the whole set" back when keys
13158            // came from the input row. Reading the expanded row's column is
13159            // what it should have meant, and is what this does.
13160            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
13161                && *n >= 1
13162                && let Ok(idx) = usize::try_from(*n - 1)
13163                && idx < projection.len()
13164            {
13165                return Some(idx);
13166            }
13167            // An unqualified name matching exactly one output name. SQL
13168            // resolves ORDER BY against the select list first, so this wins
13169            // over an input column of the same name — which is the whole
13170            // point of `SELECT g AS id … ORDER BY id`.
13171            if let Expr::Column(c) = &ob.expr
13172                && c.qualifier.is_none()
13173            {
13174                let mut hit = None;
13175                for (i, p) in projection.iter().enumerate() {
13176                    if p.output_name.eq_ignore_ascii_case(&c.name) {
13177                        if hit.is_some() {
13178                            hit = None;
13179                            break;
13180                        }
13181                        hit = Some(i);
13182                    }
13183                }
13184                if hit.is_some() {
13185                    return hit;
13186                }
13187            }
13188            // Or the same expression as a select-list item — which is what
13189            // `ORDER BY 1` becomes once `resolve_positional_order_by` has
13190            // run, and what a repeated `ORDER BY unnest(…)` is.
13191            projection.iter().position(|p| p.expr == ob.expr)
13192        })
13193        .collect()
13194}
13195
13196fn expand_srf_row(
13197    engine: &Engine,
13198    projection: &[ProjectedItem],
13199    srf_idxs: &[usize],
13200    row: &Row<'static>,
13201    ctx: &EvalContext<'_>,
13202) -> Result<Vec<Row<'static>>, EngineError> {
13203    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
13204    expand_srf_row_with(engine, &mut plan, projection, row, ctx)
13205}
13206
13207impl Engine {
13208    /// The rows one target-list SRF yields for an input row. `None` from
13209    /// `srf_target_idxs` means the expression is not set-returning at all.
13210    fn srf_values(
13211        &self,
13212        expr: &spg_sql::ast::Expr,
13213        row: &Row<'static>,
13214        ctx: &EvalContext<'_>,
13215    ) -> Result<Vec<Value<'static>>, EngineError> {
13216        if top_level_srf_kind(expr).is_some() {
13217            return top_level_srf_output(expr, row, ctx);
13218        }
13219        // A user set-returning function. Its body runs through the real
13220        // executor, like every function body since round 63.
13221        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
13222            return Err(EngineError::Unsupported(
13223                "expected a SELECT-list SRF call".into(),
13224            ));
13225        };
13226        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
13227        for a in args {
13228            vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
13229        }
13230        let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
13231        // v7.39 (read01 round 68) — in a target list a multi-column function is
13232        // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
13233        // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
13234        // what it is for. A single-column function contributes its bare value.
13235        Ok(rows
13236            .into_iter()
13237            .map(|r| {
13238                if r.values.len() == 1 {
13239                    r.values.into_iter().next().unwrap_or(Value::Null)
13240                } else {
13241                    Value::Composite(
13242                        cols.iter()
13243                            .map(|c| c.name.clone())
13244                            .zip(r.values)
13245                            .collect::<alloc::vec::Vec<_>>(),
13246                    )
13247                }
13248            })
13249            .collect())
13250    }
13251
13252    /// Is THIS node a set-returning call: one of the builtin kinds, or a user
13253    /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
13254    fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
13255        if is_top_level_unnest(e) {
13256            return true;
13257        }
13258        let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
13259            return false;
13260        };
13261        self.active_catalog().functions_named(name).iter().any(|f| {
13262            let r = f.returns.trim().to_ascii_uppercase();
13263            r.starts_with("SETOF") || r.starts_with("TABLE(")
13264        })
13265    }
13266
13267    /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
13268    fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
13269        let mut found = false;
13270        let mut probe = e.clone();
13271        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
13272            if self.is_srf_node(n) {
13273                found = true;
13274                return true;
13275            }
13276            false
13277        });
13278        found
13279    }
13280
13281    /// Which projection items CONTAIN a set-returning call. Before round 78 this
13282    /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
13283    /// ordinary scalar call all the way down to the function dispatcher, which
13284    /// then reported `unnest` as an unknown function.
13285    fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
13286        projection
13287            .iter()
13288            .enumerate()
13289            .filter(|(_, p)| self.expr_contains_srf(&p.expr))
13290            .map(|(i, _)| i)
13291            .collect()
13292    }
13293}
13294
13295impl Engine {
13296    /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
13297    /// no `(f(args)).*` item.
13298    fn lower_record_expansion(
13299        &self,
13300        stmt: &SelectStatement,
13301    ) -> Result<Option<SelectStatement>, EngineError> {
13302        use spg_sql::ast::{Expr, SelectItem};
13303        let is_marker = |it: &SelectItem| {
13304            matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
13305                if name == "__record_expand")
13306        };
13307        if !stmt.items.iter().any(is_marker) {
13308            return Ok(None);
13309        }
13310        let mut out = stmt.clone();
13311        let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
13312        let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
13313        for (n, item) in stmt.items.iter().enumerate() {
13314            if !is_marker(item) {
13315                items.push(item.clone());
13316                continue;
13317            }
13318            let SelectItem::Expr {
13319                expr: Expr::FunctionCall { args, .. },
13320                ..
13321            } = item
13322            else {
13323                unreachable!("checked by is_marker");
13324            };
13325            let Some(Expr::FunctionCall {
13326                name: fname,
13327                args: fargs,
13328            }) = args.first()
13329            else {
13330                return Err(EngineError::Unsupported(
13331                    "(<expr>).* expands a function's record — it needs a function call".into(),
13332                ));
13333            };
13334            let cols = self.setof_declared_columns(fname)?;
13335            let alias = alloc::format!("__rec{n}");
13336            let mut tref = bare_table_ref_named(&alias);
13337            tref.table_fn_call = Some(alloc::boxed::Box::new((
13338                fname.to_ascii_lowercase(),
13339                fargs.clone(),
13340            )));
13341            tref.alias = Some(alias.clone());
13342            lateral_refs.push(tref);
13343            for c in cols {
13344                items.push(SelectItem::Expr {
13345                    expr: Expr::Column(spg_sql::ast::ColumnName {
13346                        qualifier: Some(alias.clone()),
13347                        name: c,
13348                    }),
13349                    alias: None,
13350                });
13351            }
13352        }
13353        out.items = items;
13354        // The function joins the FROM. With no FROM it BECOMES the FROM; with one
13355        // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
13356        // (the arguments may reference the outer row — the round-69 correlation).
13357        for tref in lateral_refs {
13358            match &mut out.from {
13359                None => {
13360                    out.from = Some(spg_sql::ast::FromClause {
13361                        primary: tref,
13362                        joins: alloc::vec::Vec::new(),
13363                    });
13364                }
13365                Some(from) => from.joins.push(spg_sql::ast::FromJoin {
13366                    kind: spg_sql::ast::JoinKind::Cross,
13367                    table: tref,
13368                    on: None,
13369                    using_cols: None,
13370                    natural: false,
13371                }),
13372            }
13373        }
13374        Ok(Some(out))
13375    }
13376
13377    /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
13378    /// v text)` names them; a `SETOF <scalar>` is one column named after the
13379    /// function.
13380    fn setof_declared_columns(
13381        &self,
13382        name: &str,
13383    ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
13384        let cat = self.active_catalog();
13385        let overloads = cat.functions_named(name);
13386        let def = overloads.first().ok_or_else(|| {
13387            EngineError::Unsupported(alloc::format!("function {name} does not exist"))
13388        })?;
13389        let declared = def.returns.trim();
13390        let upper = declared.to_ascii_uppercase();
13391        if upper.starts_with("TABLE(") {
13392            let raw = &declared["TABLE(".len()..declared.len() - 1];
13393            return Ok(raw
13394                .split(',')
13395                .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
13396                .collect());
13397        }
13398        Ok(alloc::vec![name.to_string()])
13399    }
13400}
13401
13402/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
13403/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
13404/// COLUMNS list (data-independent), NESTED children inlined in
13405/// declaration order (PG's flattened output shape).
13406/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
13407/// correlated JSON_TABLE's static schema without evaluating its doc.
13408pub(crate) fn json_table_schema_pub(
13409    cols: &[spg_sql::ast::JsonTableColumn],
13410) -> alloc::vec::Vec<ColumnSchema> {
13411    json_table_schema(cols)
13412}
13413
13414fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
13415    use spg_sql::ast::JsonTableColumn as C;
13416    let mut out = alloc::vec::Vec::new();
13417    for c in cols {
13418        match c {
13419            C::Ordinality { name } => {
13420                out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
13421            }
13422            C::Regular {
13423                name, ty, exists, ..
13424            } => {
13425                let dt = if *exists {
13426                    DataType::Bool
13427                } else {
13428                    crate::conversions::column_type_to_data_type(*ty)
13429                };
13430                out.push(ColumnSchema::new(name.clone(), dt, true));
13431            }
13432            C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
13433        }
13434    }
13435    out
13436}
13437
13438/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
13439/// JSON_TABLE column's declared type (the DEFAULT expr may be a
13440/// string literal like `'none'` that must land as the column type).
13441fn coerce_json_table_default(
13442    v: Value<'static>,
13443    ty: spg_sql::ast::ColumnTypeName,
13444    name: &str,
13445) -> Result<Value<'static>, EngineError> {
13446    if v.is_null() {
13447        return Ok(Value::Null);
13448    }
13449    let dt = crate::conversions::column_type_to_data_type(ty);
13450    crate::conversions::coerce_value(v, dt, name, 0)
13451}
13452
13453/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
13454fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
13455    use crate::json::JsonValue as J;
13456    match v {
13457        Value::Null => J::Null,
13458        Value::Bool(b) => J::Bool(*b),
13459        Value::SmallInt(n) => J::Number(f64::from(*n)),
13460        Value::Int(n) => J::Number(f64::from(*n)),
13461        Value::BigInt(n) => J::Number(*n as f64),
13462        Value::Float(x) => J::Number(*x),
13463        Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
13464        other => J::String(crate::eval::value_to_text(other)),
13465    }
13466}
13467
13468fn bare_table_ref_named(name: &str) -> TableRef {
13469    TableRef {
13470        name: name.to_string(),
13471        alias: None,
13472        only: false,
13473        as_of_segment: None,
13474        unnest_expr: None,
13475        unnest_column_aliases: alloc::vec::Vec::new(),
13476        with_ordinality: false,
13477        generate_series_args: None,
13478        lateral_subquery: None,
13479        jsonb_each_text_arg: None,
13480        table_fn_call: None,
13481        rows_from: None,
13482        json_table: None,
13483        scalar_fn_item: false,
13484    }
13485}
13486
13487impl Engine {
13488    /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
13489    /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
13490    /// entries are the array-able SRFs, already lowered by the parser into their
13491    /// scalar array form.
13492    fn rows_from_rows(
13493        &self,
13494        primary: &TableRef,
13495    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
13496        let entries = primary
13497            .rows_from
13498            .as_ref()
13499            .expect("caller guards rows_from.is_some()");
13500        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13501        let ctx = self.ev_ctx(&empty, None);
13502        let dummy = Row::new(alloc::vec::Vec::new());
13503        let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
13504        let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
13505        for (name, args) in entries {
13506            let (vals, colname) = if name == "__array" {
13507                // The parser lowered this one to `<array expr>`; its rows are the
13508                // array's elements.
13509                let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
13510                (
13511                    array_value_to_elements(&arr)?,
13512                    alloc::string::String::from("unnest"),
13513                )
13514            } else {
13515                let call = spg_sql::ast::Expr::FunctionCall {
13516                    name: name.clone(),
13517                    args: args.clone(),
13518                };
13519                (self.srf_values(&call, &dummy, &ctx)?, name.clone())
13520            };
13521            let ty = vals
13522                .first()
13523                .and_then(spg_storage::Value::data_type)
13524                .unwrap_or(DataType::Text);
13525            cols.push(ColumnSchema::new(colname, ty, true));
13526            lists.push(vals);
13527        }
13528        let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
13529        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
13530        for k in 0..n {
13531            let mut vals: alloc::vec::Vec<Value<'static>> =
13532                alloc::vec::Vec::with_capacity(lists.len() + 1);
13533            for l in &lists {
13534                vals.push(l.get(k).cloned().unwrap_or(Value::Null));
13535            }
13536            rows.push(Row::new(vals));
13537        }
13538        if primary.with_ordinality {
13539            cols.push(ColumnSchema::new(
13540                "ordinality".to_string(),
13541                DataType::BigInt,
13542                false,
13543            ));
13544            rows = rows
13545                .into_iter()
13546                .enumerate()
13547                .map(|(i, r)| {
13548                    let mut v = r.values;
13549                    v.push(Value::BigInt(i as i64 + 1));
13550                    Row::new(v)
13551                })
13552                .collect();
13553        }
13554        Ok((rows, cols))
13555    }
13556}
13557
13558/// v7.39 (round 232) — PG names the offending set operation in its
13559/// arity / type-mismatch messages ("each UNION query must have the same
13560/// number of columns"). `UNION ALL` is still spelled UNION there.
13561fn set_op_name(kind: UnionKind) -> &'static str {
13562    match kind {
13563        UnionKind::All | UnionKind::Distinct => "UNION",
13564        UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
13565        UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
13566    }
13567}
13568
13569/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
13570/// type: a bare string or NULL literal that no context has typed yet. SPG
13571/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
13572/// be the syntax. A wildcard or a non-literal expression is never unknown.
13573fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
13574    stmt.items
13575        .iter()
13576        .map(|item| match item {
13577            SelectItem::Expr { expr, .. } => matches!(
13578                expr,
13579                Expr::Literal(spg_sql::ast::Literal::String(_))
13580                    | Expr::Literal(spg_sql::ast::Literal::Null)
13581            ),
13582            _ => false,
13583        })
13584        .collect()
13585}
13586
13587/// v7.39 (round 233) — retype one branch column's cells, reporting the
13588/// conversion failure the way PG does rather than leaving the column
13589/// half-converted. Used when the other branch typed an untyped literal.
13590fn coerce_branch_column(
13591    rows: &mut [Row<'static>],
13592    col_idx: usize,
13593    target: DataType,
13594    col_name: &str,
13595) -> Result<(), EngineError> {
13596    for row in rows.iter_mut() {
13597        let Some(slot) = row.values.get_mut(col_idx) else {
13598            continue;
13599        };
13600        if matches!(slot, Value::Null) {
13601            continue;
13602        }
13603        *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
13604    }
13605    Ok(())
13606}
13607
13608/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
13609/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
13610/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
13611/// reference to q's output columns substituted by the underlying column.
13612///
13613/// Admission is deliberately narrow — anything that changes cardinality,
13614/// order, or scope stays on the materialising path:
13615/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
13616///   FROM with no ordinality or positional column aliases, and no
13617///   subquery anywhere its expressions (an inner scope could reference
13618///   q too — descending is a later knife);
13619/// * inner: one stored table, bare-column projection only, no
13620///   CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
13621/// * every outer column reference must resolve inside q's output list —
13622///   a name that does not is an ERROR today, and flattening would
13623///   silently legalise it against the base table.
13624fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
13625    use spg_sql::ast::SelectItem;
13626    let inner = primary.lateral_subquery.as_deref()?;
13627    // Outer shape.
13628    if !stmt.ctes.is_empty()
13629        || !stmt.unions.is_empty()
13630        || stmt.distinct
13631        || !stmt.distinct_on.is_empty()
13632        || !stmt.window_check_exprs.is_empty()
13633        || stmt.locking.is_some()
13634        || primary.with_ordinality
13635        || !primary.unnest_column_aliases.is_empty()
13636    {
13637        return None;
13638    }
13639    // Inner shape.
13640    if !inner.ctes.is_empty()
13641        || !inner.unions.is_empty()
13642        || inner.distinct
13643        || !inner.distinct_on.is_empty()
13644        || inner.group_by.is_some()
13645        || inner.group_by_all
13646        || inner.having.is_some()
13647        || !inner.order_by.is_empty()
13648        || inner.limit.is_some()
13649        || inner.offset.is_some()
13650        || !inner.window_check_exprs.is_empty()
13651        || inner.locking.is_some()
13652    {
13653        return None;
13654    }
13655    let ifrom = inner.from.as_ref()?;
13656    let it = &ifrom.primary;
13657    if !ifrom.joins.is_empty()
13658        || it.name.is_empty()
13659        || it.lateral_subquery.is_some()
13660        || it.unnest_expr.is_some()
13661        || it.generate_series_args.is_some()
13662        || it.as_of_segment.is_some()
13663        || it.jsonb_each_text_arg.is_some()
13664        || it.table_fn_call.is_some()
13665        || it.rows_from.is_some()
13666        || it.json_table.is_some()
13667        || it.with_ordinality
13668        || !it.unnest_column_aliases.is_empty()
13669    {
13670        return None;
13671    }
13672    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
13673        return None;
13674    }
13675    // The output map: q's visible name -> the underlying column.
13676    let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
13677    let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
13678        alloc::collections::BTreeMap::new();
13679    for item in &inner.items {
13680        let SelectItem::Expr { expr, alias } = item else {
13681            return None;
13682        };
13683        let Expr::Column(c) = expr else {
13684            return None;
13685        };
13686        if let Some(q) = c.qualifier.as_deref()
13687            && !q.eq_ignore_ascii_case(&inner_alias)
13688        {
13689            return None;
13690        }
13691        let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
13692        // A duplicated output name would make substitution ambiguous.
13693        if map
13694            .insert(out_name.to_ascii_lowercase(), c.clone())
13695            .is_some()
13696        {
13697            return None;
13698        }
13699    }
13700    if map.is_empty() {
13701        return None;
13702    }
13703    let derived_alias = primary
13704        .alias
13705        .clone()
13706        .unwrap_or_else(|| primary.name.clone())
13707        .to_ascii_lowercase();
13708    // Substitute in a clone; bail (None) on the first reference the map
13709    // cannot answer.
13710    let mut out = stmt.clone();
13711    let ok = core::cell::Cell::new(true);
13712    let mut subst = |e: &mut Expr| -> bool {
13713        match e {
13714            Expr::Column(c) => {
13715                match c.qualifier.as_deref() {
13716                    Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
13717                    None => {}
13718                    Some(_) => {
13719                        ok.set(false);
13720                        return true;
13721                    }
13722                }
13723                match map.get(&c.name.to_ascii_lowercase()) {
13724                    Some(target) => *c = target.clone(),
13725                    None => ok.set(false),
13726                }
13727                true
13728            }
13729            // Any subquery could reference q from its own scope;
13730            // descending is a later knife — bail for now.
13731            Expr::ScalarSubquery(_)
13732            | Expr::Exists { .. }
13733            | Expr::InSubquery { .. }
13734            | Expr::RowInSubquery { .. }
13735            | Expr::RowCmpSubquery { .. } => {
13736                ok.set(false);
13737                true
13738            }
13739            _ => false,
13740        }
13741    };
13742    for item in &mut out.items {
13743        match item {
13744            SelectItem::Expr { expr, .. } => {
13745                crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
13746            }
13747            // `SELECT * FROM (…) q` means q's columns, in q's order.
13748            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
13749        }
13750    }
13751    if let Some(w) = &mut out.where_ {
13752        crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
13753    }
13754    if let Some(gs) = &mut out.group_by {
13755        for g in gs {
13756            crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
13757        }
13758    }
13759    if let Some(h) = &mut out.having {
13760        crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
13761    }
13762    for o in &mut out.order_by {
13763        crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
13764    }
13765    for d in &mut out.distinct_on {
13766        crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
13767    }
13768    if !ok.get() {
13769        return None;
13770    }
13771    // FROM becomes the stored table; the filters conjoin.
13772    out.from = Some(spg_sql::ast::FromClause {
13773        primary: it.clone(),
13774        joins: Vec::new(),
13775    });
13776    out.where_ = match (inner.where_.clone(), out.where_.take()) {
13777        (Some(a), Some(b)) => Some(Expr::Binary {
13778            lhs: alloc::boxed::Box::new(a),
13779            op: spg_sql::ast::BinOp::And,
13780            rhs: alloc::boxed::Box::new(b),
13781        }),
13782        (Some(a), None) => Some(a),
13783        (None, b) => b,
13784    };
13785    Some(out)
13786}
13787
13788/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
13789/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
13790/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
13791/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
13792/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
13793/// DISTINCT, an SRF, or an unprovable inner shape stays put.
13794fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
13795    use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
13796    let inner = primary.lateral_subquery.as_deref()?;
13797    // Outer: exactly `SELECT count(*)`, nothing else.
13798    if !stmt.ctes.is_empty()
13799        || !stmt.unions.is_empty()
13800        || stmt.distinct
13801        || !stmt.distinct_on.is_empty()
13802        || stmt.where_.is_some()
13803        || stmt.group_by.is_some()
13804        || stmt.having.is_some()
13805        || !stmt.order_by.is_empty()
13806        || stmt.limit.is_some()
13807        || stmt.offset.is_some()
13808        || stmt.items.len() != 1
13809    {
13810        return None;
13811    }
13812    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
13813        return None;
13814    };
13815    let E::FunctionCall { name, args } = expr else {
13816        return None;
13817    };
13818    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
13819        return None;
13820    }
13821    // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
13822    let Some(LimitExpr::Literal(k)) = &inner.offset else {
13823        return None;
13824    };
13825    let k = i64::from(*k);
13826    if inner.limit.is_some() || inner.order_by.is_empty() {
13827        return None;
13828    }
13829    let mut counted = inner.clone();
13830    counted.order_by = Vec::new();
13831    counted.offset = None;
13832    // The stripped inner must now be a provable simple shape (its
13833    // items become irrelevant — count(*) reads none of them — but an
13834    // SRF item would change the row count, so the flatten predicate's
13835    // scrutiny still applies).
13836    let base = matview_flatten_probe(&counted)?;
13837    let mut out = stmt.clone();
13838    out.items = alloc::vec![SelectItem::Expr {
13839        expr: E::FunctionCall {
13840            name: String::from("greatest"),
13841            args: alloc::vec![
13842                E::Binary {
13843                    lhs: alloc::boxed::Box::new(E::FunctionCall {
13844                        name: String::from("count_star"),
13845                        args: alloc::vec![],
13846                    }),
13847                    op: spg_sql::ast::BinOp::Sub,
13848                    rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
13849                },
13850                E::Literal(spg_sql::ast::Literal::Integer(0)),
13851            ],
13852        },
13853        alias: Some(String::from("count")),
13854    }];
13855    out.from = Some(spg_sql::ast::FromClause {
13856        primary: base,
13857        joins: Vec::new(),
13858    });
13859    out.where_ = counted.where_.clone();
13860    Some(out)
13861}
13862
13863/// The inner-shape probe `try_count_over_offset` shares with the
13864/// flatten: single stored table, no modifiers, no subqueries, no SRF
13865/// items. Returns the base TableRef.
13866fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
13867    use spg_sql::ast::SelectItem;
13868    if !inner.ctes.is_empty()
13869        || !inner.unions.is_empty()
13870        || inner.distinct
13871        || !inner.distinct_on.is_empty()
13872        || inner.group_by.is_some()
13873        || inner.group_by_all
13874        || inner.having.is_some()
13875        || !inner.order_by.is_empty()
13876        || inner.limit.is_some()
13877        || inner.offset.is_some()
13878        || !inner.window_check_exprs.is_empty()
13879        || inner.locking.is_some()
13880    {
13881        return None;
13882    }
13883    let ifrom = inner.from.as_ref()?;
13884    let it = &ifrom.primary;
13885    if !ifrom.joins.is_empty()
13886        || it.name.is_empty()
13887        || it.lateral_subquery.is_some()
13888        || it.unnest_expr.is_some()
13889        || it.generate_series_args.is_some()
13890        || it.as_of_segment.is_some()
13891        || it.jsonb_each_text_arg.is_some()
13892        || it.table_fn_call.is_some()
13893        || it.rows_from.is_some()
13894        || it.json_table.is_some()
13895        || it.with_ordinality
13896    {
13897        return None;
13898    }
13899    for item in &inner.items {
13900        match item {
13901            SelectItem::Expr { expr, .. } => {
13902                if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
13903                    return None;
13904                }
13905            }
13906            SelectItem::Wildcard => {}
13907            SelectItem::QualifiedWildcard(_) => return None,
13908        }
13909    }
13910    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
13911        return None;
13912    }
13913    Some(it.clone())
13914}
13915
13916/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
13917/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
13918/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
13919/// constant-LENGTH array literal unnests to exactly k rows per input
13920/// row (NULL elements are rows too). One SRF item only, elements
13921/// subquery-free, and the stripped inner must pass the same probe the
13922/// count-over-offset rewrite uses.
13923fn try_count_over_const_unnest(
13924    stmt: &SelectStatement,
13925    primary: &TableRef,
13926) -> Option<SelectStatement> {
13927    use spg_sql::ast::{Expr as E, SelectItem};
13928    let inner = primary.lateral_subquery.as_deref()?;
13929    if !stmt.ctes.is_empty()
13930        || !stmt.unions.is_empty()
13931        || stmt.distinct
13932        || !stmt.distinct_on.is_empty()
13933        || stmt.where_.is_some()
13934        || stmt.group_by.is_some()
13935        || stmt.having.is_some()
13936        || !stmt.order_by.is_empty()
13937        || stmt.limit.is_some()
13938        || stmt.offset.is_some()
13939        || stmt.items.len() != 1
13940    {
13941        return None;
13942    }
13943    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
13944        return None;
13945    };
13946    let E::FunctionCall { name, args } = expr else {
13947        return None;
13948    };
13949    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
13950        return None;
13951    }
13952    // Inner: exactly one item, and it is unnest(ARRAY[...]).
13953    if inner.items.len() != 1
13954        || !inner.order_by.is_empty()
13955        || inner.limit.is_some()
13956        || inner.offset.is_some()
13957    {
13958        return None;
13959    }
13960    let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
13961        return None;
13962    };
13963    let E::FunctionCall {
13964        name: fname,
13965        args: fargs,
13966    } = item
13967    else {
13968        return None;
13969    };
13970    if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
13971        return None;
13972    }
13973    let E::Array(elems) = &fargs[0] else {
13974        return None;
13975    };
13976    if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
13977        return None;
13978    }
13979    let k = elems.len() as i64;
13980    // The stripped inner (the SRF item replaced by a plain constant)
13981    // must be the provable simple shape.
13982    let mut counted = inner.clone();
13983    counted.items = alloc::vec![SelectItem::Expr {
13984        expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
13985        alias: None,
13986    }];
13987    let base = matview_flatten_probe(&counted)?;
13988    let mut out = stmt.clone();
13989    out.items = alloc::vec![SelectItem::Expr {
13990        expr: E::Binary {
13991            lhs: alloc::boxed::Box::new(E::FunctionCall {
13992                name: String::from("count_star"),
13993                args: alloc::vec![],
13994            }),
13995            op: spg_sql::ast::BinOp::Mul,
13996            rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
13997        },
13998        alias: Some(String::from("count")),
13999    }];
14000    out.from = Some(spg_sql::ast::FromClause {
14001        primary: base,
14002        joins: Vec::new(),
14003    });
14004    out.where_ = counted.where_.clone();
14005    Some(out)
14006}