Skip to main content

spg_engine/
select.rs

1//! SELECT execution — the window / meta-view / CTE variants and the
2//! subquery-resolution pre-pass. Lifted out of `lib.rs` (v7.32 engine
3//! modularisation). These `impl Engine` methods are dispatched from the
4//! bare-SELECT entry points and drive the non-trivial SELECT shapes.
5
6use alloc::borrow::Cow;
7use alloc::string::{String, ToString};
8use alloc::vec::Vec;
9
10use spg_sql::ast::{
11    ColumnName, Expr, FromClause, SelectItem, SelectStatement, Statement, TableRef, UnionKind,
12};
13use spg_storage::{
14    Catalog, ColumnSchema, DataType, Row, StorageError, TableSchema, Value, VecEncoding,
15};
16
17use crate::describe;
18use crate::eval::{EvalContext, EvalError};
19use crate::join::RowRef;
20use crate::system_catalog::collect_view_refs;
21use crate::{
22    ByteBudget, CancelToken, Engine, EngineError, OrderKey, QueryResult, aggregate,
23    apply_offset_and_limit, apply_offset_and_limit_tagged, approx_row_bytes, build_order_keys,
24    collect_meta_view_names, collect_qualified_refs, collect_scalar_subqueries,
25    collect_window_nodes, compute_window_partition, eval, expr_tree_has_subquery,
26    materialise_in_order, materialise_meta_view, memoize, order_by_value_cmp_in, partition_key_cmp,
27    rewrite_window_to_columns, select_has_window, select_references_meta_view, select_refers_to,
28    sort_by_keys, synth_info_key_column_usage, synth_info_referential_constraints,
29    synth_info_routines, synth_info_statistics, synth_information_schema_columns,
30    synth_information_schema_tables, synth_mysql_db, synth_mysql_user, synth_pg_attribute,
31    synth_pg_class, synth_pg_constraint, synth_pg_database, synth_pg_extension, synth_pg_index_raw,
32    synth_pg_indexes, synth_pg_namespace, synth_pg_operator, synth_pg_proc, synth_pg_roles,
33    synth_pg_sequence, synth_pg_settings, synth_pg_timezone_abbrevs, synth_pg_timezone_names,
34    synth_pg_trigger, synth_pg_type, synth_pg_views, topk_trim, try_gin_jsonb_seek, try_gin_seek,
35    try_index_seek, try_nsw_knn, try_pk_walk_top_n, try_trgm_seek, value_is_bigint,
36    value_is_integer, value_to_i64,
37};
38
39/// v7.39 (round 618) — a recursive term that can be run over the working set
40/// directly, instead of through a whole query execution per round.
41///
42/// PG plans the recursive term ONCE and re-scans a worktable each iteration.
43/// SPG emptied and refilled a real table and then called `exec_select_cancel`
44/// — FROM resolution, schema build, predicate compilation, projection build
45/// and result materialisation — for every round. Measured with the counting
46/// allocator on `WITH RECURSIVE r(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM r
47/// WHERE n < N)`: about 40 allocations and 99 kB PER ROUND while the working
48/// set is one row, or 1.98 GB at N = 20000.
49///
50/// This is the shape that covers the ordinary recursive term: read the CTE,
51/// filter it, project it. Anything else — a join, an aggregate, a window, a
52/// subquery, DISTINCT, GROUP BY, ORDER BY, LIMIT, a locking clause, a
53/// non-table source — returns `None` and keeps the general path, so the
54/// answers it gives are the ones that path gave.
55struct RecursiveTermPlan<'t> {
56    items: Vec<&'t Expr>,
57    where_: Option<&'t Expr>,
58    alias: String,
59}
60
61fn plan_recursive_term<'t>(
62    t: &'t SelectStatement,
63    cte_name: &str,
64    ncols: usize,
65) -> Option<RecursiveTermPlan<'t>> {
66    if !t.unions.is_empty()
67        || !t.ctes.is_empty()
68        || t.distinct
69        || !t.distinct_on.is_empty()
70        || t.group_by.is_some()
71        || t.group_by_all
72        || t.having.is_some()
73        || !t.order_by.is_empty()
74        || t.limit.is_some()
75        || t.offset.is_some()
76        || t.limit_with_ties
77        || t.locking.is_some()
78    {
79        return None;
80    }
81    let from = t.from.as_ref()?;
82    if !from.joins.is_empty() {
83        return None;
84    }
85    let p = &from.primary;
86    if !p.name.eq_ignore_ascii_case(cte_name)
87        || p.as_of_segment.is_some()
88        || p.unnest_expr.is_some()
89        || !p.unnest_column_aliases.is_empty()
90        || p.with_ordinality
91        || p.generate_series_args.is_some()
92        || p.lateral_subquery.is_some()
93        || p.jsonb_each_text_arg.is_some()
94        || p.table_fn_call.is_some()
95    {
96        return None;
97    }
98    let unsupported = |e: &Expr| {
99        crate::aggregate::contains_aggregate(e)
100            || crate::subquery::expr_has_subquery(e)
101            || crate::window::expr_has_window_pub(e)
102    };
103    let mut items: Vec<&Expr> = Vec::with_capacity(t.items.len());
104    for it in &t.items {
105        match it {
106            SelectItem::Expr { expr, .. } => {
107                if unsupported(expr) {
108                    return None;
109                }
110                items.push(expr);
111            }
112            // `*` would have to be expanded against the CTE's own schema;
113            // the general path already does that, so leave it there.
114            _ => return None,
115        }
116    }
117    if items.len() != ncols {
118        return None;
119    }
120    if let Some(w) = &t.where_
121        && unsupported(w)
122    {
123        return None;
124    }
125    Some(RecursiveTermPlan {
126        items,
127        where_: t.where_.as_ref(),
128        alias: p.alias.clone().unwrap_or_else(|| p.name.clone()),
129    })
130}
131
132impl Engine {
133    /// v4.12 window executor. Implements `ROW_NUMBER` / `RANK` /
134    /// `DENSE_RANK` and the partition-aware aggregates `SUM` /
135    /// `AVG` / `COUNT` / `MIN` / `MAX`. The plan is:
136    /// 1. Apply the WHERE filter.
137    /// 2. For each unique `WindowFunction` node in the projection,
138    ///    partition + sort, compute the per-row value.
139    /// 3. Append the window values as synthetic columns (`__win_N`)
140    ///    to the row schema.
141    /// 4. Rewrite the projection to read those columns.
142    /// 5. Hand off to the regular project / ORDER BY / LIMIT pipe.
143    #[allow(
144        clippy::too_many_lines,
145        clippy::type_complexity,
146        clippy::needless_range_loop
147    )] // window-eval is one cohesive pipe; splitting fragments
148    pub(crate) fn exec_select_with_window(
149        &self,
150        stmt: &SelectStatement,
151        cancel: CancelToken<'_>,
152    ) -> Result<QueryResult, EngineError> {
153        let from = stmt.from.as_ref().ok_or_else(|| {
154            EngineError::Unsupported("window functions require a FROM clause".into())
155        })?;
156        // v7.17.0 Phase 3.P0-43 — JOIN + window functions. Phase
157        // 3.6 rejected this combination outright ("queued for
158        // v5.x"); P0-43 materialises the join + WHERE through the
159        // existing nested-loop helper and runs the window pipeline
160        // on the joined row set with the combined `alias.col`
161        // schema. The window expressions resolve through the
162        // qualifier-aware column resolver same as the aggregate /
163        // projection paths on JOIN.
164        let (schema_cols_owned, alias_opt): (Vec<ColumnSchema>, Option<&str>);
165        // v7.39 (round 976) — rows this walk OWNS. A derived FROM item and
166        // a JOIN both produce rows that exist nowhere else, so they land
167        // here; a plain stored table does not, and borrows instead.
168        //
169        // It used to clone every row out of the table, on the reasoning
170        // that "the clone is cheap relative to the window computation that
171        // follows". Measured on 400k rows, `row_number() OVER ()` cost
172        // 31.881 ms against 46.520 with a 200-byte column added — so the
173        // clone tracks row width at about 36 ns per row per 200 bytes, and
174        // the window computation it was being compared against is a
175        // counter increment per row. Nothing downstream needs the rows
176        // owned: the very next statement used to be
177        // `filtered.iter().collect()` into the `&Row` slice the window
178        // pipeline actually reads.
179        let mut owned_rows: Vec<Row<'static>> = Vec::new();
180        // What the pipeline reads. Borrows `owned_rows` or the table.
181        let mut filtered: Vec<&Row<'static>> = Vec::new();
182        // Set by the branches that fill `owned_rows`, because "empty" is
183        // an answer a query can legitimately have and so cannot be the
184        // signal for which of the two holds the rows.
185        let mut rows_are_owned = false;
186        if from.joins.is_empty() {
187            let primary = &from.primary;
188            // v7.37 D.13 — window functions over a derived table (subquery /
189            // VALUES / unnest / generate_series). The catalog-by-name lookup
190            // below only finds real tables, so a derived primary threw
191            // TableNotFound. Materialise the derived rows + schema through the
192            // same helper the non-window FROM-primary path uses, then WHERE-
193            // filter and feed the identical window pipeline.
194            let is_derived = primary.lateral_subquery.is_some()
195                || primary.unnest_expr.is_some()
196                || primary.generate_series_args.is_some()
197                || primary.jsonb_each_text_arg.is_some()
198                || primary.table_fn_call.is_some();
199            if is_derived {
200                let (drows, dcols) = self.materialise_table_ref(primary)?;
201                schema_cols_owned = dcols;
202                alias_opt = primary.alias.as_deref();
203                let ctx = self.ev_ctx(&schema_cols_owned, alias_opt);
204                let mut owned: Vec<Row<'static>> = Vec::new();
205                for (i, row) in drows.into_iter().enumerate() {
206                    if i.is_multiple_of(256) {
207                        cancel.check()?;
208                    }
209                    if let Some(w) = &stmt.where_ {
210                        let cond = eval::eval_expr(w, &row, &ctx)?;
211                        if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
212                            continue;
213                        }
214                    }
215                    owned.push(row);
216                }
217                owned_rows = owned;
218                rows_are_owned = true;
219            } else {
220                let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
221                    StorageError::TableNotFound {
222                        name: primary.name.clone(),
223                    }
224                })?;
225                let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
226                schema_cols_owned = table.schema().columns.clone();
227                alias_opt = Some(alias);
228                let ctx = self.ev_ctx(&schema_cols_owned, alias_opt);
229                // The WHERE test, in ONE place, for all four ways a row can
230                // reach this walk. It deliberately does not touch the row
231                // collections: a closure that pushed into them would tie
232                // its argument to the closure body and no borrowed row
233                // could escape it, which is what forced the clone-shaped
234                // version of this loop in the first place.
235                let passes = |row: &Row<'static>| -> Result<bool, EngineError> {
236                    if let Some(w) = &stmt.where_ {
237                        let cond = eval::eval_expr(w, row, &ctx)?;
238                        if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
239                            return Ok(false);
240                        }
241                    }
242                    Ok(true)
243                };
244                // v7.37.15 Phase B — scan_visible filters rows by the
245                // engine's current snapshot. Phase B's `current_snapshot()`
246                // returns `Snapshot::unbounded()` so every row is visible,
247                // matching pre-v7.37.15 byte-for-byte. Phase C will wire
248                // real per-tx snapshots through this same callsite — no
249                // code change needed here when that lands.
250                let snap = self.current_snapshot();
251                if table.has_cold_rows_fast() {
252                    // v7.36 (cold-tier coverage) — a cold segment's rows
253                    // are produced on demand and live in a temporary this
254                    // walk cannot borrow from, so a table carrying any owns
255                    // its rows. Hot iter then cold iter, both through the
256                    // same WHERE, as before.
257                    let mut owned: Vec<Row<'static>> = Vec::new();
258                    for (i, row) in table.scan_visible(&snap) {
259                        if i.is_multiple_of(256) {
260                            cancel.check()?;
261                        }
262                        if passes(row)? {
263                            owned.push(row.clone());
264                        }
265                    }
266                    let hot_len = table.row_count();
267                    for (offset, row) in self.iter_cold_rows_of_table(table).iter().enumerate() {
268                        let i = hot_len + offset;
269                        if i.is_multiple_of(256) {
270                            cancel.check()?;
271                        }
272                        if passes(row)? {
273                            owned.push(row.clone());
274                        }
275                    }
276                    owned_rows = owned;
277                    rows_are_owned = true;
278                } else {
279                    // v7.39 (round 975) — ask the indices first, the way
280                    // the streaming walk has since round 970. This walk had
281                    // the same hole and it is reached by any statement
282                    // carrying a window function, so a WHERE that names an
283                    // indexed column read the whole table: measured on 400k
284                    // rows, `row_number() OVER () … WHERE id = 500` — a
285                    // ONE-row answer on a primary key — took 13.762 ms
286                    // against PG18.4's 0.151, while the same predicate
287                    // without the window took 0.091. The cost was
288                    // independent of how many rows survived (999 survivors
289                    // cost 13.312 ms) and of row width (13.312 narrow vs
290                    // 13.327 wide), which is what a full table walk looks
291                    // like and what a result-shaped cost does not.
292                    //
293                    // The seek only NARROWS — `passes` still applies the
294                    // whole WHERE — so no answer can change. Positions
295                    // arrive visibility-filtered by the same predicate the
296                    // scan applies and capped at a quarter of the table,
297                    // and `None` walks the table exactly as before.
298                    let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
299                        crate::index_access::try_index_seek_positions(
300                            w,
301                            &schema_cols_owned,
302                            table,
303                            alias,
304                            &snap,
305                            self.backslash_escapes,
306                        )
307                    });
308                    match seek_positions {
309                        Some(mut positions) => {
310                            // Table order, which is the order the scan
311                            // would have produced.
312                            positions.sort_unstable();
313                            for (n, pos) in positions.into_iter().enumerate() {
314                                if n.is_multiple_of(256) {
315                                    cancel.check()?;
316                                }
317                                let Some(row) = table.rows().get(pos) else {
318                                    continue;
319                                };
320                                if passes(row)? {
321                                    filtered.push(row);
322                                }
323                            }
324                        }
325                        None => {
326                            for (i, row) in table.scan_visible(&snap) {
327                                if i.is_multiple_of(256) {
328                                    cancel.check()?;
329                                }
330                                if passes(row)? {
331                                    filtered.push(row);
332                                }
333                            }
334                        }
335                    }
336                }
337            }
338        } else {
339            let deferred = self.build_joined_filtered_rows(
340                from,
341                stmt.where_.as_ref(),
342                cancel,
343                None,
344                &mut ByteBudget::new(self.max_query_bytes),
345            )?;
346            // A join's survivors are row-index tuples over its sources, so
347            // there is no single row to borrow — this branch owns them.
348            owned_rows = deferred.materialise();
349            rows_are_owned = true;
350            schema_cols_owned = deferred.combined_schema;
351            alias_opt = None;
352        }
353        if rows_are_owned {
354            filtered = owned_rows.iter().collect();
355        }
356        let schema_cols = &schema_cols_owned;
357        let ctx = self.ev_ctx(schema_cols, alias_opt);
358        let alias = alias_opt.unwrap_or("");
359        let n_rows = filtered.len();
360        // The window pipeline reads `&[&Row<'static>]`, and `filtered`
361        // already is one whichever branch produced it — the separate
362        // `filtered_refs` this used to build was the collect that made
363        // owning the rows look necessary.
364
365        // 2) Collect unique window function nodes from projection.
366        let mut window_nodes: Vec<Expr> = Vec::new();
367        for item in &stmt.items {
368            if let SelectItem::Expr { expr, .. } = item {
369                collect_window_nodes(expr, &mut window_nodes);
370            }
371        }
372        // v7.39 (round 592) — and from ORDER BY, which may name a window the
373        // select list never mentions. The order-key builder below rewrites
374        // window calls to `__win_N` columns, and a call that was never
375        // collected has no column to become.
376        for o in &stmt.order_by {
377            collect_window_nodes(&o.expr, &mut window_nodes);
378        }
379
380        // 3) For each window, compute per-row value.
381        // Index: same order as window_nodes; for row i, win_vals[w][i].
382        let mut win_vals: Vec<Vec<Value<'static>>> = Vec::with_capacity(window_nodes.len());
383        for wnode in &window_nodes {
384            let Expr::WindowFunction {
385                name,
386                args,
387                partition_by,
388                order_by,
389                frame,
390                null_treatment,
391                filter,
392            } = wnode
393            else {
394                unreachable!("collect_window_nodes pushes only WindowFunction");
395            };
396            // Compute (partition_key, order_key, original_index) for each row.
397            // v7.39 (round 593) — a key that is a plain column sits at the same
398            // position in every row, but was resolved BY NAME for each one. A
399            // per-library profile of `lag(id) OVER (ORDER BY id)` put
400            // `resolve_column` at 5.8% of the query on its own, with
401            // `rehydrate_cell` and the `eval_expr` dispatch behind it. Resolve
402            // once; anything that is not a plain column keeps the resolver.
403            let p_bound: Vec<Option<usize>> = partition_by
404                .iter()
405                .map(|e| crate::orderby::bound_column_position(e, schema_cols, alias_opt))
406                .collect();
407            let o_bound: Vec<Option<usize>> = order_by
408                .iter()
409                .map(|(e, _, _)| crate::orderby::bound_column_position(e, schema_cols, alias_opt))
410                .collect();
411            let arg_bound = args
412                .first()
413                .and_then(|a| crate::orderby::bound_column_position(a, schema_cols, alias_opt));
414            // v7.39 (round 690) — a window's ORDER BY over a column that
415            // declares a collation sorts by it, the same as a top-level
416            // ORDER BY. Resolved from the bound position, so only a bare
417            // column gets one; an expression produces a new value and the
418            // derivation that would give IT a collation is unbuilt.
419            let o_colls: Vec<Option<alloc::string::String>> = o_bound
420                .iter()
421                .map(|p| {
422                    p.and_then(|pos| schema_cols.get(pos))
423                        .and_then(|sc| sc.collation_name.clone())
424                        .filter(|n| crate::collate::is_supported(n))
425                })
426                .collect();
427            let mut indexed: Vec<(Vec<Value<'static>>, Vec<(Value, bool, Option<bool>)>, usize)> =
428                Vec::with_capacity(n_rows);
429            // v7.39 (round 731) — single bound INT partition key, no window
430            // ORDER BY: group on the i64 directly. The generic build paid
431            // two heap Vecs per row (pkey + empty okey) plus a canonical
432            // string encode per row just to bucket 500k rows into 100
433            // groups; the whole per-row key apparatus disappears here.
434            // Neither key Vec is read downstream on this path: the hash
435            // grouping replaces partition_key_cmp, and okey is empty by
436            // construction.
437            let int_pkey_fast = order_by.is_empty()
438                && partition_by.len() == 1
439                && p_bound[0].is_some_and(|pos| {
440                    matches!(
441                        schema_cols.get(pos).map(|c| c.ty),
442                        Some(
443                            spg_storage::DataType::Int
444                                | spg_storage::DataType::BigInt
445                                | spg_storage::DataType::SmallInt
446                        )
447                    )
448                });
449            // v7.39 (round 979) — the same idea for a single bound INT
450            // window ORDER BY: sort on the i64 instead of on a heap vector
451            // per row.
452            //
453            // Measured at 400k rows (round 978, ablation, answer checked
454            // byte-for-byte against the general path on a key column that
455            // is a permutation): `row_number() OVER (ORDER BY k)` went
456            // 157.057-157.868 ms to 31.253-31.679, which is 79.8% and puts
457            // it on top of the `OVER ()` baseline — the sort essentially
458            // disappears. Round 977 had already shown the cost was
459            // key-shaped rather than row-shaped: the sort's share was
460            // 132.0 ms on a three-integer table and 132.5 with a 200-byte
461            // column added, and a per-row COPY does scale with width
462            // (round 976 measured that at +36 ns/row/200 bytes).
463            //
464            // Gated to ROW_NUMBER, which is the one function that reads
465            // neither key vector — it numbers the order it is handed.
466            // `rank` and `dense_rank` compare adjacent entries' order keys
467            // in `compute_window_partition`, so leaving those vectors
468            // empty would silently give every row rank 1. A wider version
469            // would carry the i64 in the entry and teach those two to use
470            // it; this one is the part that can be shown correct by
471            // construction.
472            let int_okey_fast = partition_by.is_empty()
473                && order_by.len() == 1
474                && frame.is_none()
475                && filter.is_none()
476                && matches!(null_treatment, spg_sql::ast::NullTreatment::Respect)
477                && name.eq_ignore_ascii_case("row_number")
478                && o_bound[0].is_some_and(|pos| {
479                    matches!(
480                        schema_cols.get(pos).map(|c| c.ty),
481                        Some(
482                            spg_storage::DataType::Int
483                                | spg_storage::DataType::BigInt
484                                | spg_storage::DataType::SmallInt
485                        )
486                    )
487                });
488            // Set when a cell in that column turns out not to be an
489            // integer after all. The declared type says it should be, but
490            // "should" is not a thing to sort 400k rows on, so the general
491            // path takes over and this build is discarded.
492            let mut int_okey_bailed = false;
493            if int_okey_fast {
494                let pos = o_bound[0].expect("gated bound");
495                let desc = order_by[0].1;
496                // PG orders NULLs last ascending and first descending
497                // unless the query says otherwise.
498                let nulls_first = order_by[0].2.unwrap_or(desc);
499                let mut keyed: Vec<(bool, i64, usize)> = Vec::with_capacity(n_rows);
500                for (i, row) in filtered.iter().enumerate() {
501                    match row.values.get(pos) {
502                        Some(Value::Int(n)) => keyed.push((false, i64::from(*n), i)),
503                        Some(Value::BigInt(n)) => keyed.push((false, *n, i)),
504                        Some(Value::SmallInt(n)) => keyed.push((false, i64::from(*n), i)),
505                        Some(Value::Null) | None => keyed.push((true, 0, i)),
506                        Some(_) => {
507                            int_okey_bailed = true;
508                            break;
509                        }
510                    }
511                }
512                if !int_okey_bailed {
513                    // `null_rank` puts NULLs on the side the query asked
514                    // for; the row's original index breaks every tie, so
515                    // equal keys keep the order the scan produced — what
516                    // the stable sort below would have given them.
517                    let null_rank = |is_null: bool| -> u8 { u8::from(is_null != nulls_first) };
518                    keyed.sort_unstable_by(|a, b| {
519                        null_rank(a.0)
520                            .cmp(&null_rank(b.0))
521                            .then_with(|| {
522                                if a.0 {
523                                    core::cmp::Ordering::Equal
524                                } else if desc {
525                                    b.1.cmp(&a.1)
526                                } else {
527                                    a.1.cmp(&b.1)
528                                }
529                            })
530                            .then_with(|| a.2.cmp(&b.2))
531                    });
532                    for (_, _, i) in keyed {
533                        indexed.push((Vec::new(), Vec::new(), i));
534                    }
535                } else {
536                    indexed.clear();
537                }
538            }
539            if int_okey_fast && !int_okey_bailed {
540                // Ordered above; nothing else to build.
541            } else if int_pkey_fast {
542                let pos = p_bound[0].expect("gated bound");
543                let mut slot: hashbrown::HashMap<Option<i64>, usize> = hashbrown::HashMap::new();
544                let mut groups: Vec<Vec<usize>> = Vec::new();
545                for (i, row) in filtered.iter().enumerate() {
546                    let k: Option<i64> = match row.values.get(pos) {
547                        Some(Value::BigInt(n)) => Some(*n),
548                        Some(Value::Int(n)) => Some(i64::from(*n)),
549                        Some(Value::SmallInt(n)) => Some(i64::from(*n)),
550                        _ => None,
551                    };
552                    match slot.get(&k) {
553                        Some(&gi) => groups[gi].push(i),
554                        None => {
555                            slot.insert(k, groups.len());
556                            groups.push(alloc::vec![i]);
557                        }
558                    }
559                }
560                // The downstream partition-boundary scan compares pkeys
561                // of ADJACENT entries, so the key must ride along — one
562                // single-element Vec per row (half the generic build's
563                // allocations, no string encode).
564                for g in groups {
565                    for i in g {
566                        let k: Value<'static> = match filtered[i].values.get(pos) {
567                            Some(v) => v.clone(),
568                            None => Value::Null,
569                        };
570                        indexed.push((alloc::vec![k], Vec::new(), i));
571                    }
572                }
573            } else {
574                for (i, row) in filtered.iter().enumerate() {
575                    let pkey: Vec<Value<'static>> = partition_by
576                        .iter()
577                        .enumerate()
578                        .map(
579                            |(k, p)| match p_bound[k].and_then(|pos| row.values.get(pos)) {
580                                Some(v) => Ok(v.clone()),
581                                None => eval::eval_expr(p, row, &ctx),
582                            },
583                        )
584                        .collect::<Result<_, _>>()?;
585                    // v7.39 (read01 round 54) — a window's ORDER BY over an enum
586                    // column must sort by MEMBER order (enumsortorder), not the
587                    // label's text. Enum values are Text at runtime, so the raw
588                    // value key sorted alphabetically — `row_number() OVER (ORDER
589                    // BY mood)` numbered the rows happy,ok,sad. Substitute the
590                    // member ordinal, the same key the top-level ORDER BY uses.
591                    // (Closes the enum-order knife's recorded window residual.)
592                    let okey: Vec<(Value, bool, Option<bool>)> = order_by
593                        .iter()
594                        .enumerate()
595                        .map(|(k, (e, desc, nf))| -> Result<_, EngineError> {
596                            let v = match o_bound[k].and_then(|pos| row.values.get(pos)) {
597                                Some(v) => v.clone(),
598                                None => eval::eval_expr(e, row, &ctx)?,
599                            };
600                            let v = match crate::orderby::enum_order_ordinal(e, &v, &ctx) {
601                                Some(ord) => Value::Float(ord),
602                                None => v,
603                            };
604                            Ok((v, *desc, *nf))
605                        })
606                        .collect::<Result<_, _>>()?;
607                    indexed.push((pkey, okey, i));
608                }
609            }
610            // Sort by (partition_key, order_key). Partition key uses
611            // a stable encoded form; order key respects ASC/DESC.
612            // v7.39 (round 731) — with NO window ORDER BY the sort's only
613            // job was putting same-partition rows next to each other, and a
614            // 500k-row comparison sort is a spectacular way to hash-group:
615            // the panel's `sum(id) OVER (PARTITION BY g)` spent ~100 ms
616            // here. Group by encoded key instead, preserving row order
617            // inside each group — exactly what the stable sort preserved,
618            // so every function (row_number included) answers the same.
619            if int_okey_fast && !int_okey_bailed {
620                // Already ordered by the i64 key above.
621            } else if int_pkey_fast {
622                // Already grouped above; same-partition rows are adjacent
623                // in original row order.
624            } else if order_by.is_empty() && !partition_by.is_empty() {
625                let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
626                let mut groups: Vec<
627                    Vec<(Vec<Value<'static>>, Vec<(Value, bool, Option<bool>)>, usize)>,
628                > = Vec::new();
629                let mut keybuf = String::new();
630                for entry in indexed.drain(..) {
631                    keybuf.clear();
632                    for v in &entry.0 {
633                        crate::aggregate::push_canonical_key(&mut keybuf, v);
634                    }
635                    match slot.get(keybuf.as_str()) {
636                        Some(&gi) => groups[gi].push(entry),
637                        None => {
638                            slot.insert(keybuf.clone(), groups.len());
639                            groups.push(alloc::vec![entry]);
640                        }
641                    }
642                }
643                for g in groups {
644                    indexed.extend(g);
645                }
646            } else {
647                indexed.sort_by(|a, b| {
648                    let p_cmp = partition_key_cmp(&a.0, &b.0);
649                    if p_cmp != core::cmp::Ordering::Equal {
650                        return p_cmp;
651                    }
652                    crate::window::order_key_cmp_in(&a.1, &b.1, &o_colls)
653                });
654            }
655            // Per-partition compute.
656            let mut out_vals: Vec<Value<'static>> = alloc::vec![Value::Null; n_rows];
657            let mut p_start = 0;
658            while p_start < indexed.len() {
659                let mut p_end = p_start + 1;
660                while p_end < indexed.len()
661                    && partition_key_cmp(&indexed[p_start].0, &indexed[p_end].0)
662                        == core::cmp::Ordering::Equal
663                {
664                    p_end += 1;
665                }
666                // Compute the function within this partition slice.
667                compute_window_partition(
668                    name,
669                    args,
670                    arg_bound,
671                    !order_by.is_empty(),
672                    frame.as_ref(),
673                    *null_treatment,
674                    filter.as_deref(),
675                    &indexed[p_start..p_end],
676                    &filtered,
677                    &ctx,
678                    &mut out_vals,
679                )?;
680                p_start = p_end;
681            }
682            win_vals.push(out_vals);
683        }
684
685        // 4) Build extended schema: original columns + synthetic.
686        let mut ext_cols = schema_cols.clone();
687        for i in 0..window_nodes.len() {
688            ext_cols.push(ColumnSchema::new(
689                alloc::format!("__win_{i}"),
690                DataType::Text, // type doesn't matter for projection eval
691                true,
692            ));
693        }
694        // 6) Rewrite the projection: WindowFunction nodes → Column(__win_N).
695        let mut rewritten_items: Vec<SelectItem> = Vec::with_capacity(stmt.items.len());
696        for item in &stmt.items {
697            let new_item = match item {
698                SelectItem::Wildcard => SelectItem::Wildcard,
699                SelectItem::QualifiedWildcard(q) => SelectItem::QualifiedWildcard(q.clone()),
700                SelectItem::Expr { expr, alias } => {
701                    let mut e = expr.clone();
702                    rewrite_window_to_columns(&mut e, &window_nodes);
703                    // The rewrite swaps the window call for a synthetic
704                    // `__win_N` column, and the projection then reported
705                    // THAT as the column name — `SELECT count(*) OVER ()`
706                    // answered `__win_0`, an internal name, where PG18
707                    // answers `count`. Pin the name while the call the
708                    // column is named for is still in hand.
709                    let alias = if alias.is_none() && e != *expr {
710                        Some(default_output_name(expr, self.backslash_escapes))
711                    } else {
712                        alias.clone()
713                    };
714                    SelectItem::Expr { expr: e, alias }
715                }
716            };
717            rewritten_items.push(new_item);
718        }
719
720        // 7) Project into final rows. JOIN case uses None so the
721        // qualifier check in `resolve_column` falls through to the
722        // composite `alias.col` schema lookup; single-table case
723        // keeps the bare alias so `bare_col` resolution still
724        // works for the projection's per-row column references.
725        // v7.39 (read01 round 54) — build through `ev_ctx`, the canonical
726        // constructor: it threads the catalog (plus render style / tz / GUCs)
727        // that a bare `EvalContext::new` drops. Without the catalog the OUTER
728        // `ORDER BY <enum col>` of a windowed query sorted by TEXT — the
729        // window values were right, the row order silently was not.
730        let ext_ctx = self.ev_ctx(&ext_cols, alias_opt);
731        let projection = build_projection_hiding_tail(
732            &rewritten_items,
733            &ext_cols,
734            alias,
735            self.backslash_escapes,
736            window_nodes.len(),
737            Some(self.active_catalog()),
738        )?;
739        let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(n_rows);
740        // v7.39 (round 592) — the extended row (input columns plus the window
741        // values) used to be materialised for EVERY input row and kept until
742        // the projection had run: the input values cloned into a fresh Vec,
743        // then grown once to take the window columns. A counting allocator put
744        // the window path at 4 allocations a row where a plain derived table
745        // takes 1, and named all four — the input row, the clone, the growth,
746        // and the projected row. Only the last has to exist afterwards, so the
747        // extended row is one buffer refilled per row.
748        let mut ext_row: Row<'static> =
749            Row::new(Vec::with_capacity(schema_cols.len() + window_nodes.len()));
750        for i in 0..n_rows {
751            if i.is_multiple_of(256) {
752                cancel.check()?;
753            }
754            ext_row.values.clear();
755            ext_row.values.extend(filtered[i].values.iter().cloned());
756            for w in 0..window_nodes.len() {
757                ext_row.values.push(win_vals[w][i].clone());
758            }
759            let row = &ext_row;
760            let mut values = Vec::with_capacity(projection.len());
761            for p in &projection {
762                values.push(eval::eval_expr(&p.expr, row, &ext_ctx)?);
763            }
764            let order_keys = if stmt.order_by.is_empty() {
765                Vec::new()
766            } else {
767                let mut keys = Vec::with_capacity(stmt.order_by.len());
768                for o in &stmt.order_by {
769                    let mut e = o.expr.clone();
770                    rewrite_window_to_columns(&mut e, &window_nodes);
771                    let key = eval::eval_expr(&e, row, &ext_ctx)?;
772                    // v7.39 (read01 round 54) — this path builds its order keys
773                    // itself instead of going through `build_order_keys`, so it
774                    // skipped the enum-ordinal substitution: the OUTER
775                    // `ORDER BY <enum col>` of a windowed query sorted by the
776                    // label's TEXT, not by member order. The window values were
777                    // right and only the row order was wrong — silently.
778                    match crate::orderby::enum_order_ordinal(&e, &key, &ext_ctx) {
779                        Some(ord) => keys.push(value_to_order_key(&Value::Float(ord))?),
780                        None => keys.push(value_to_order_key(&key)?),
781                    }
782                }
783                keys
784            };
785            tagged.push((order_keys, Row::new(values)));
786        }
787        // ORDER BY + LIMIT/OFFSET on the projected rows.
788        if !stmt.order_by.is_empty() {
789            let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
790            sort_by_keys(&mut tagged, &descs);
791        }
792        let mut out_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
793        // v7.37 D.41 — `SELECT DISTINCT` over a window projection: the window
794        // pipeline builds one output row per input row, so DISTINCT must dedup the
795        // projected rows (PG evaluates window functions before DISTINCT). Applied
796        // after ORDER BY (duplicate rows share sort keys, so order is preserved)
797        // and before LIMIT.
798        if stmt.distinct {
799            // v7.38.14 — see the synthetic-source sites below: the mask was
800            // always available here, from the same projection this function
801            // already built.
802            out_rows = dedup_rows(
803                out_rows,
804                FoldSpec::of_masks(
805                    self.backslash_escapes,
806                    &fold_mask(&projection),
807                    &pad_mask(&projection),
808                ),
809            );
810        }
811        apply_offset_and_limit(&mut out_rows, stmt.offset_literal(), stmt.limit_literal());
812        let final_cols: Vec<ColumnSchema> = projection
813            .into_iter()
814            .map(|p| p.to_column_schema())
815            .collect();
816        Ok(QueryResult::Rows {
817            columns: final_cols,
818            rows: out_rows,
819        })
820    }
821
822    /// v4.11: materialise each CTE into a temp table inside a
823    /// cloned catalog, then run the body SELECT against a fresh
824    /// engine instance that owns the enriched catalog. The clone
825    /// is moderately expensive — only paid by CTE-bearing queries.
826    /// Subqueries inside CTE bodies / the main body resolve as
827    /// usual; `clock_fn` is propagated so `NOW()` lines up.
828    /// v7.16.2 — mailrs round-10 A.3. Materialise the
829    /// `information_schema.*` / `pg_catalog.*` virtual views
830    /// the SELECT references, then re-execute the SELECT
831    /// against an enriched catalog where those views are real
832    /// tables. Same pattern as `exec_with_ctes`. The temp
833    /// engine carries `meta_views_materialised = true` so its
834    /// own meta-dispatch short-circuits — without that we'd
835    /// infinite-recurse since the temp catalog's view name
836    /// still starts with `__spg_info_` and re-triggers the
837    /// check.
838    pub(crate) fn exec_select_with_meta_views(
839        &self,
840        stmt: &SelectStatement,
841        cancel: CancelToken<'_>,
842    ) -> Result<QueryResult, EngineError> {
843        let catalog = self.meta_view_catalog(stmt)?;
844        let mut temp = Engine::restore(catalog);
845        if let Some(c) = self.clock {
846            temp = temp.with_clock(c);
847        }
848        if let Some(f) = self.salt_fn {
849            temp = temp.with_salt_fn(f);
850        }
851        // v7.39 (round 522) — the temp engine holds the materialised
852        // catalog and, until now, nothing of the SESSION. So every
853        // session-scoped answer changed the moment a system view
854        // appeared in the FROM clause: `SELECT current_user` said
855        // `unmei` and `SELECT current_user FROM pg_class` said `admin`;
856        // `current_setting('work_mem')` fell back to the boot default
857        // after a SET; `application_name` read empty. A privilege check
858        // written against a catalog join was reading a different
859        // identity than the same check written without one.
860        //
861        // Carry what a session can be observed through — its parameters
862        // (which is also where the session user lives), the role store
863        // the privilege builtins read, the dialect, and the rendering
864        // settings a timestamp is spelled with.
865        temp.session_params.clone_from(&self.session_params);
866        temp.users.clone_from(&self.users);
867        temp.backslash_escapes = self.backslash_escapes;
868        temp.mysql_strict = self.mysql_strict;
869        temp.render_style = self.render_style;
870        temp.tz_offset_fn = self.tz_offset_fn;
871        temp.tz_localize_fn = self.tz_localize_fn;
872        temp.tz_abbrev_fn = self.tz_abbrev_fn;
873        temp.meta_views_materialised = true;
874        temp.exec_select_cancel(stmt, cancel)
875    }
876
877    /// v7.39 (round 462) — the catalog a meta-view SELECT resolves
878    /// against: this engine's catalog with every `__spg_*` view the
879    /// statement references materialised into it.
880    ///
881    /// Split out of `exec_select_with_meta_views` so Describe can reach
882    /// the same shapes execution reaches. Describe used to look the FROM
883    /// relation up in the plain catalog, where a system view does not
884    /// exist, and reported "no columns" for every one of them — so an
885    /// extended-protocol client reading `pg_stat_user_tables` got rows
886    /// with no column metadata. Sharing the materialisation means a
887    /// view added here is described correctly the day it is added.
888    pub(crate) fn meta_view_catalog(&self, stmt: &SelectStatement) -> Result<Catalog, EngineError> {
889        let mut needed: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
890        collect_meta_view_names(stmt, &mut needed);
891        let mut catalog = self.active_catalog().clone();
892        for view in &needed {
893            if catalog.get(view).is_some() {
894                continue;
895            }
896            match view.as_str() {
897                "__spg_info_columns" => {
898                    let (schema, rows) = synth_information_schema_columns(
899                        self.active_catalog(),
900                        self.backslash_escapes,
901                    );
902                    materialise_meta_view(&mut catalog, view, schema, rows)?;
903                }
904                "__spg_info_tables" => {
905                    let (schema, rows) = synth_information_schema_tables(self.active_catalog());
906                    materialise_meta_view(&mut catalog, view, schema, rows)?;
907                }
908                "__spg_pg_class" => {
909                    let (schema, rows) = synth_pg_class(
910                        self.active_catalog(),
911                        i64::try_from(self.vacuum_oldest_active()).unwrap_or(i64::MAX),
912                    );
913                    materialise_meta_view(&mut catalog, view, schema, rows)?;
914                }
915                "__spg_pg_attribute" => {
916                    let (schema, rows) = synth_pg_attribute(self.active_catalog());
917                    materialise_meta_view(&mut catalog, view, schema, rows)?;
918                }
919                // v7.17.0 Phase 3.P0-50 — pg_catalog.pg_type for
920                // sqlx / SQLAlchemy / Diesel / pgAdmin lookups.
921                "__spg_pg_type" => {
922                    let (schema, rows) = synth_pg_type(self.active_catalog());
923                    materialise_meta_view(&mut catalog, view, schema, rows)?;
924                }
925                // v7.39 (round 621) — pg_catalog.pg_operator, which did not
926                // exist at all.
927                "__spg_pg_operator" => {
928                    let (schema, rows) = synth_pg_operator(self.active_catalog());
929                    materialise_meta_view(&mut catalog, view, schema, rows)?;
930                }
931                // v7.17.0 Phase 3.P0-51 — pg_catalog.pg_proc for
932                // function-name introspection (ORM / pgAdmin).
933                "__spg_pg_proc" => {
934                    let (schema, rows) = synth_pg_proc(self.active_catalog());
935                    materialise_meta_view(&mut catalog, view, schema, rows)?;
936                }
937                // v7.24 (round-16 D) — pg_catalog.pg_trigger. The
938                // round-16 "why doesn't prod fire the trigger"
939                // question was unanswerable because triggers had NO
940                // introspection surface; tgname/tgenabled plus the
941                // pragmatic relname/timing/events/function columns
942                // make "is it registered and enabled" a one-liner.
943                "__spg_pg_trigger" => {
944                    let (schema, rows) = synth_pg_trigger(self.active_catalog());
945                    materialise_meta_view(&mut catalog, view, schema, rows)?;
946                }
947                // v7.17.0 Phase 3.P0-52 — pg_catalog.pg_namespace
948                // (schema list for admin tools' tree views).
949                "__spg_pg_namespace" => {
950                    let (schema, rows) = synth_pg_namespace(self.active_catalog());
951                    materialise_meta_view(&mut catalog, view, schema, rows)?;
952                }
953                // v7.39 — pg_tables convenience view (was a pgwire
954                // canned response that ignored projections).
955                "__spg_pg_tables" => {
956                    let (schema, rows) =
957                        crate::system_catalog::synth_pg_tables(self.active_catalog());
958                    materialise_meta_view(&mut catalog, view, schema, rows)?;
959                }
960                // v7.37.24 (24.1) — pg_catalog.pg_enum (label list
961                // for ENUM types; sqlx / ORM enum codecs read this).
962                "__spg_pg_enum" => {
963                    let (schema, rows) =
964                        crate::system_catalog::synth_pg_enum(self.active_catalog());
965                    materialise_meta_view(&mut catalog, view, schema, rows)?;
966                }
967                // v7.37.21 (21.13) — pg_catalog.pg_replication_slots
968                // (shape-stable empty until 21.12 persists slot state).
969                // v7.39 (round 277) — session-scoped prepared statements.
970                "__spg_pg_prepared_statements" => {
971                    let (schema, rows) = crate::system_catalog::synth_pg_prepared_statements(
972                        &self.prepared_statements,
973                    );
974                    materialise_meta_view(&mut catalog, view, schema, rows)?;
975                }
976                "__spg_pg_replication_slots" => {
977                    let (schema, rows) =
978                        crate::system_catalog::synth_pg_replication_slots(self.active_catalog());
979                    materialise_meta_view(&mut catalog, view, schema, rows)?;
980                }
981                // v7.37.21 (21.13-b) — pg_catalog.pg_publication
982                // (one row per CREATE PUBLICATION).
983                "__spg_pg_publication" => {
984                    let (schema, rows) = crate::system_catalog::synth_pg_publication(self);
985                    materialise_meta_view(&mut catalog, view, schema, rows)?;
986                }
987                // v7.37.21 (21.13-c) — pg_catalog.pg_subscription
988                // (one row per CREATE SUBSCRIPTION; subconninfo
989                // redacted so dashboards can't leak credentials).
990                "__spg_pg_subscription" => {
991                    let (schema, rows) = crate::system_catalog::synth_pg_subscription(self);
992                    materialise_meta_view(&mut catalog, view, schema, rows)?;
993                }
994                // v7.37.22 (22.x-stat-db) — pg_catalog.pg_stat_database
995                // (one row for SPG's single database; counters are
996                // shape-stable 0 until wiring lands).
997                "__spg_pg_stat_database" => {
998                    let (schema, rows) = crate::system_catalog::synth_pg_stat_database(
999                        self,
1000                        self.stat_tup_inserted,
1001                        self.stat_tup_updated,
1002                        self.stat_tup_deleted,
1003                    );
1004                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1005                }
1006                // v7.37.22 (22.14) — pg_catalog.pg_stat_user_tables
1007                // (per-table churn counters; live_tup = row count).
1008                "__spg_pg_stat_user_tables" => {
1009                    // r192 — DML counters come from the engine-side
1010                    // non-transactional map, not the (tx-shadowed)
1011                    // catalog tables.
1012                    let (schema, rows) = crate::system_catalog::synth_pg_stat_user_tables(
1013                        self.active_catalog(),
1014                        &self.table_write_stats,
1015                    );
1016                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1017                }
1018                // v7.37.22 (22.15) — pg_catalog.pg_stat_user_indexes
1019                // (per-index usage counters; flag unused indexes).
1020                "__spg_pg_stat_user_indexes" => {
1021                    let (schema, rows) =
1022                        crate::system_catalog::synth_pg_stat_user_indexes(self.active_catalog());
1023                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1024                }
1025                // v7.37.22 (22.16) — pg_catalog.pg_stat_bgwriter.
1026                "__spg_pg_stat_bgwriter" => {
1027                    let (schema, rows) =
1028                        crate::system_catalog::synth_pg_stat_bgwriter(self.active_catalog());
1029                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1030                }
1031                // v7.38 (read01 P3.14) — pg_catalog.pg_stat_checkpointer /
1032                // pg_stat_wal shell views (shape-stable, counters pending).
1033                "__spg_pg_stat_checkpointer" => {
1034                    let (schema, rows) =
1035                        crate::system_catalog::synth_pg_stat_checkpointer(self.active_catalog());
1036                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1037                }
1038                "__spg_pg_stat_wal" => {
1039                    let (schema, rows) =
1040                        crate::system_catalog::synth_pg_stat_wal(self.active_catalog());
1041                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1042                }
1043                // v7.38 (read01 P3.15) — pg_catalog.pg_stat_slru /
1044                // pg_stat_subscription_stats shell views.
1045                "__spg_pg_stat_slru" => {
1046                    let (schema, rows) =
1047                        crate::system_catalog::synth_pg_stat_slru(self.active_catalog());
1048                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1049                }
1050                "__spg_pg_stat_subscription_stats" => {
1051                    let (schema, rows) = crate::system_catalog::synth_pg_stat_subscription_stats(
1052                        self.active_catalog(),
1053                    );
1054                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1055                }
1056                // v7.37.22 (22.17) — pg_catalog.pg_stat_archiver.
1057                "__spg_pg_stat_archiver" => {
1058                    let (schema, rows) =
1059                        crate::system_catalog::synth_pg_stat_archiver(self.active_catalog());
1060                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1061                }
1062                // v7.37.21 (21.13-d) — pg_catalog.pg_stat_replication.
1063                "__spg_pg_stat_replication" => {
1064                    let (schema, rows) =
1065                        crate::system_catalog::synth_pg_stat_replication(self.active_catalog());
1066                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1067                }
1068                // v7.37.24 (24.13) — pg_catalog.pg_am.
1069                "__spg_pg_am" => {
1070                    let (schema, rows) = crate::system_catalog::synth_pg_am(self.active_catalog());
1071                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1072                }
1073                // v7.37.22 (22.18) — pg_catalog.pg_stat_io (PG 16+).
1074                "__spg_pg_stat_io" => {
1075                    let (schema, rows) =
1076                        crate::system_catalog::synth_pg_stat_io(self.active_catalog());
1077                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1078                }
1079                // v7.37.22 (22.19) — pg_catalog.pg_stat_user_functions.
1080                "__spg_pg_stat_user_functions" => {
1081                    let (schema, rows) =
1082                        crate::system_catalog::synth_pg_stat_user_functions(self.active_catalog());
1083                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1084                }
1085                // v7.39 (round 287) — pg_catalog.pg_largeobject{,_metadata}.
1086                "__spg_pg_largeobject" => {
1087                    let (schema, rows) =
1088                        crate::system_catalog::synth_pg_largeobject(self.active_catalog());
1089                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1090                }
1091                "__spg_pg_largeobject_metadata" => {
1092                    let (schema, rows) =
1093                        crate::system_catalog::synth_pg_largeobject_metadata(self.active_catalog());
1094                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1095                }
1096                // v7.37.23 (23.7-a) — pg_catalog.pg_statistic_ext.
1097                "__spg_pg_statistic_ext" => {
1098                    let (schema, rows) =
1099                        crate::system_catalog::synth_pg_statistic_ext(self.active_catalog());
1100                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1101                }
1102                // v7.38.18 — pg_catalog.pg_stats, the readable view.
1103                "__spg_pg_stats" => {
1104                    let (schema, rows) = crate::system_catalog::synth_pg_stats(
1105                        self.active_catalog(),
1106                        &self.statistics,
1107                    );
1108                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1109                }
1110                // v7.37.24 (24.15) — pg_catalog.pg_statistic.
1111                "__spg_pg_statistic" => {
1112                    let (schema, rows) = crate::system_catalog::synth_pg_statistic(
1113                        self.active_catalog(),
1114                        &self.statistics,
1115                    );
1116                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1117                }
1118                // v7.37.22 (22.20) — pg_catalog.pg_stat_progress_vacuum.
1119                "__spg_pg_stat_progress_vacuum" => {
1120                    let (schema, rows) =
1121                        crate::system_catalog::synth_pg_stat_progress_vacuum(self.active_catalog());
1122                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1123                }
1124                // v7.37.22 (22.21) — pg_catalog.pg_stat_progress_create_index.
1125                "__spg_pg_stat_progress_create_index" => {
1126                    let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_create_index(
1127                        self.active_catalog(),
1128                    );
1129                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1130                }
1131                // v7.37.22 (22.22) — pg_catalog.pg_stat_progress_analyze.
1132                "__spg_pg_stat_progress_analyze" => {
1133                    let (schema, rows) = crate::system_catalog::synth_pg_stat_progress_analyze(
1134                        self.active_catalog(),
1135                    );
1136                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1137                }
1138                // v7.37.24 (24.16) — pg_catalog.pg_inherits
1139                // (partition parent → child OID mapping).
1140                "__spg_pg_inherits" => {
1141                    let (schema, rows) =
1142                        crate::system_catalog::synth_pg_inherits(self.active_catalog());
1143                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1144                }
1145                // v7.39 (round 650) — the text-search catalogs, filled
1146                // with what SPG actually has rather than PG's thirty.
1147                "__spg_pg_ts_config_map" => {
1148                    let (schema, rows) =
1149                        crate::system_catalog::synth_pg_ts_config_map(self.active_catalog());
1150                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1151                }
1152                "__spg_pg_ts_config" => {
1153                    let (schema, rows) =
1154                        crate::system_catalog::synth_pg_ts_config(self.active_catalog());
1155                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1156                }
1157                "__spg_pg_ts_dict" => {
1158                    let (schema, rows) =
1159                        crate::system_catalog::synth_pg_ts_dict(self.active_catalog());
1160                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1161                }
1162                "__spg_pg_ts_parser" => {
1163                    let (schema, rows) =
1164                        crate::system_catalog::synth_pg_ts_parser(self.active_catalog());
1165                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1166                }
1167                "__spg_pg_ts_template" => {
1168                    let (schema, rows) =
1169                        crate::system_catalog::synth_pg_ts_template(self.active_catalog());
1170                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1171                }
1172                // v7.37.24 (24.17) — pg_catalog.pg_depend
1173                // (dependency graph; shape-stable empty since
1174                // SPG's drop enforcement is per-kind, not per-object).
1175                "__spg_pg_depend" => {
1176                    let (schema, rows) =
1177                        crate::system_catalog::synth_pg_depend(self.active_catalog());
1178                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1179                }
1180                // 7.38.1 S5.1 — pg_catalog.pg_opclass (pg_dump wall #1).
1181                "__spg_pg_opclass" => {
1182                    let (schema, rows) =
1183                        crate::system_catalog::synth_pg_opclass(self.active_catalog());
1184                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1185                }
1186                "__spg_pg_opfamily" => {
1187                    let (schema, rows) =
1188                        crate::system_catalog::synth_pg_opfamily(self.active_catalog());
1189                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1190                }
1191                "__spg_pg_amop" => {
1192                    let (schema, rows) =
1193                        crate::system_catalog::synth_pg_amop(self.active_catalog());
1194                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1195                }
1196                "__spg_pg_amproc" => {
1197                    let (schema, rows) =
1198                        crate::system_catalog::synth_pg_amproc(self.active_catalog());
1199                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1200                }
1201                // v7.38 (read01) — pg_catalog.pg_attrdef (column defaults;
1202                // ORM reflection + pg_dump read the deparsed default text).
1203                "__spg_pg_attrdef" => {
1204                    let (schema, rows) =
1205                        crate::system_catalog::synth_pg_attrdef(self.active_catalog());
1206                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1207                }
1208                // v7.39 (RLS) — pg_catalog.pg_policy (raw) + pg_policies (view).
1209                "__spg_pg_policy" => {
1210                    let (schema, rows) =
1211                        crate::system_catalog::synth_pg_policy(self.active_catalog());
1212                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1213                }
1214                "__spg_pg_policies" => {
1215                    let (schema, rows) =
1216                        crate::system_catalog::synth_pg_policies(self.active_catalog());
1217                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1218                }
1219                // v7.37.24 (24.14) — pg_catalog.pg_collation.
1220                "__spg_pg_collation" => {
1221                    let (schema, rows) =
1222                        crate::system_catalog::synth_pg_collation(self.active_catalog());
1223                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1224                }
1225                // v7.37.23 (23.6-b) — pg_catalog.pg_tablespace.
1226                "__spg_pg_tablespace" => {
1227                    let (schema, rows) =
1228                        crate::system_catalog::synth_pg_tablespace(self.active_catalog());
1229                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1230                }
1231                // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_indexes view
1232                // for pgAdmin / DataGrip "indexes per table" listings.
1233                "__spg_pg_indexes" => {
1234                    let (schema, rows) = synth_pg_indexes(self.active_catalog());
1235                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1236                }
1237                // v7.39 (read01 round 50) — pg_catalog.pg_description, backing
1238                // psql's \d+ comment column and pg_dump's COMMENT ON emission.
1239                "__spg_pg_description" => {
1240                    let (schema, rows) =
1241                        crate::system_catalog::synth_pg_description(self.active_catalog());
1242                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1243                }
1244                // v7.17.0 Phase 3.P0-53 — pg_catalog.pg_index (raw)
1245                // for index introspection by ORM compilers.
1246                "__spg_pg_index" => {
1247                    let (schema, rows) = synth_pg_index_raw(self.active_catalog());
1248                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1249                }
1250                // v7.17.0 Phase 3.P0-54 — pg_catalog.pg_constraint
1251                // for FK / UNIQUE / PK / CHECK introspection.
1252                "__spg_pg_constraint" => {
1253                    let (schema, rows) = synth_pg_constraint(self.active_catalog());
1254                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1255                }
1256                // v7.37 U11 — pg_catalog.pg_sequence, one row per CREATE
1257                // SEQUENCE (psql \d <seq> + ORM sequence introspection).
1258                "__spg_pg_sequence" => {
1259                    let (schema, rows) = synth_pg_sequence(self.active_catalog());
1260                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1261                }
1262                // v7.17.0 Phase 3.P0-55 — pg_catalog.pg_database /
1263                // pg_roles / pg_user. SPG is single-database so
1264                // pg_database surfaces just `postgres`; pg_roles
1265                // / pg_user walk the engine's UserStore.
1266                "__spg_pg_database" => {
1267                    let (schema, rows) = synth_pg_database(self);
1268                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1269                }
1270                "__spg_pg_roles" => {
1271                    let (schema, rows) = synth_pg_roles(self);
1272                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1273                }
1274                // v7.39 (round 542) — pg_user is a DIFFERENT view over the
1275                // same roles, with PG's own `use*` column names. It used to
1276                // publish pg_roles' columns under this name.
1277                "__spg_pg_user" => {
1278                    let (schema, rows) = crate::system_catalog::synth_pg_user(self);
1279                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1280                }
1281                // v7.39 (read01 round 58) — role membership.
1282                "__spg_pg_auth_members" => {
1283                    let (schema, rows) = crate::system_catalog::synth_pg_auth_members(self);
1284                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1285                }
1286                // v7.17.0 Phase 3.P0-56 — pg_catalog.pg_views. PG's
1287                // pg_views surfaces every CREATE VIEW result; SPG
1288                // ships one row per declared view from the catalog.
1289                "__spg_pg_views" => {
1290                    let (schema, rows) = synth_pg_views(self.active_catalog());
1291                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1292                }
1293                // v7.39 (round 143) — pg_catalog.pg_rules: one row per
1294                // catalogued query-rewrite RULE.
1295                "__spg_pg_rules" => {
1296                    let (schema, rows) =
1297                        crate::system_catalog::synth_pg_rules(self.active_catalog());
1298                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1299                }
1300                // v7.39 (round 312) — pg_catalog.pg_rewrite: the rule
1301                // catalogue `pg_get_ruledef(oid)` resolves against.
1302                "__spg_pg_rewrite" => {
1303                    let (schema, rows) =
1304                        crate::system_catalog::synth_pg_rewrite(self.active_catalog());
1305                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1306                }
1307                // v7.39 (round 542) — pg_catalog.pg_matviews, with rows
1308                // and PG's own column names.
1309                "__spg_pg_matviews" => {
1310                    let (schema, rows) =
1311                        crate::system_catalog::synth_pg_matviews(self.active_catalog());
1312                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1313                }
1314                // pg_catalog.pg_extension — native capability list
1315                // (mailrs embed round-12).
1316                // v7.39 (round 546) — the catalogs SPG has real content
1317                // for, from the facts it already holds.
1318                "__spg_pg_db_role_setting" => {
1319                    let (schema, rows) = crate::system_catalog::synth_pg_db_role_setting(self);
1320                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1321                }
1322                "__spg_pg_language" => {
1323                    let (schema, rows) = crate::system_catalog::synth_pg_language();
1324                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1325                }
1326                "__spg_pg_sequences" => {
1327                    let (schema, rows) =
1328                        crate::system_catalog::synth_pg_sequences(self.active_catalog());
1329                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1330                }
1331                "__spg_pg_range" => {
1332                    let (schema, rows) = crate::system_catalog::synth_pg_range();
1333                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1334                }
1335                "__spg_pg_partitioned_table" => {
1336                    let (schema, rows) =
1337                        crate::system_catalog::synth_pg_partitioned_table(self.active_catalog());
1338                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1339                }
1340                "__spg_pg_authid" => {
1341                    let (schema, rows) = crate::system_catalog::synth_pg_authid(self);
1342                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1343                }
1344                "__spg_pg_group" => {
1345                    let (schema, rows) = crate::system_catalog::synth_pg_group(self);
1346                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1347                }
1348                "__spg_pg_shadow" => {
1349                    let (schema, rows) = crate::system_catalog::synth_pg_shadow(self);
1350                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1351                }
1352                // v7.39 (round 544) — pg_cast, probed from the real
1353                // cast implementation.
1354                "__spg_pg_cast" => {
1355                    let (schema, rows) = crate::system_catalog::synth_pg_cast();
1356                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1357                }
1358                // v7.39 (round 541) — an empty catalog that exists.
1359                "__spg_pg_foreign_table" => {
1360                    let (schema, rows) = crate::system_catalog::synth_pg_foreign_table();
1361                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1362                }
1363                "__spg_pg_extension" => {
1364                    let (schema, rows) = synth_pg_extension();
1365                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1366                }
1367                // v7.39 (round 502) — the timezone catalogues.
1368                "__spg_pg_timezone_names" => {
1369                    let (schema, rows) = synth_pg_timezone_names(self);
1370                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1371                }
1372                "__spg_pg_timezone_abbrevs" => {
1373                    let (schema, rows) = synth_pg_timezone_abbrevs(self);
1374                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1375                }
1376                // v7.17.0 Phase 3.P0-57 — pg_catalog.pg_settings.
1377                "__spg_pg_settings" => {
1378                    let (schema, rows) = synth_pg_settings(self);
1379                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1380                }
1381                // v7.17.0 Phase 3.P0-63 — information_schema.KEY_COLUMN_USAGE.
1382                // v7.39 (read01 round 51) — information_schema.role_table_grants
1383                // and .table_privileges. Both report the owner's seven implicit
1384                // table privileges; SPG's single role owns everything.
1385                // v7.39 (read01 round 59) — information_schema.column_privileges.
1386                "__spg_info_column_privileges" => {
1387                    let (schema, rows) =
1388                        crate::system_catalog::synth_info_column_privileges(self.active_catalog());
1389                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1390                }
1391                "__spg_info_role_table_grants" | "__spg_info_table_privileges" => {
1392                    let grantee = self.current_role().to_string();
1393                    let (schema, rows) = crate::system_catalog::synth_info_role_table_grants(
1394                        self.active_catalog(),
1395                        &grantee,
1396                    );
1397                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1398                }
1399                "__spg_info_key_column_usage" => {
1400                    let (schema, rows) = synth_info_key_column_usage(self.active_catalog());
1401                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1402                }
1403                // v7.17.0 Phase 3.P0-64 — information_schema.REFERENTIAL_CONSTRAINTS.
1404                "__spg_info_referential_constraints" => {
1405                    let (schema, rows) = synth_info_referential_constraints(self.active_catalog());
1406                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1407                }
1408                // v7.17.0 Phase 3.P0-64 — information_schema.STATISTICS.
1409                "__spg_info_statistics" => {
1410                    let (schema, rows) = synth_info_statistics(self.active_catalog());
1411                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1412                }
1413                // v7.17.0 Phase 3.P0-64 — information_schema.ROUTINES.
1414                "__spg_info_routines" => {
1415                    let (schema, rows) = synth_info_routines();
1416                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1417                }
1418                // v7.37.24 (24.3) — information_schema.attributes.
1419                "__spg_info_attributes" => {
1420                    let (schema, rows) = crate::system_catalog::synth_information_schema_attributes(
1421                        self.active_catalog(),
1422                    );
1423                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1424                }
1425                // v7.37.24 (24.2) — information_schema.domains.
1426                "__spg_info_domains" => {
1427                    let (schema, rows) = crate::system_catalog::synth_information_schema_domains(
1428                        self.active_catalog(),
1429                    );
1430                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1431                }
1432                // v7.37.24 (24.9) — information_schema.schemata.
1433                "__spg_info_schemata" => {
1434                    let (schema, rows) = crate::system_catalog::synth_information_schema_schemata(
1435                        self.active_catalog(),
1436                    );
1437                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1438                }
1439                // v7.37.24 (24.9) — information_schema.views.
1440                "__spg_info_views" => {
1441                    let (schema, rows) = crate::system_catalog::synth_information_schema_views(
1442                        self.active_catalog(),
1443                    );
1444                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1445                }
1446                // v7.37.24 (24.9) — information_schema.table_constraints.
1447                "__spg_info_table_constraints" => {
1448                    let (schema, rows) =
1449                        crate::system_catalog::synth_information_schema_table_constraints(
1450                            self.active_catalog(),
1451                        );
1452                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1453                }
1454                // v7.37.17 — information_schema.constraint_column_usage.
1455                "__spg_info_constraint_column_usage" => {
1456                    let (schema, rows) = crate::system_catalog::synth_info_constraint_column_usage(
1457                        self.active_catalog(),
1458                    );
1459                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1460                }
1461                // v7.37.17 — information_schema.triggers.
1462                "__spg_info_triggers" => {
1463                    let (schema, rows) =
1464                        crate::system_catalog::synth_info_triggers(self.active_catalog());
1465                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1466                }
1467                // v7.37.17 — information_schema.check_constraints.
1468                "__spg_info_check_constraints" => {
1469                    let (schema, rows) =
1470                        crate::system_catalog::synth_info_check_constraints(self.active_catalog());
1471                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1472                }
1473                // v7.37.17 — information_schema.sequences.
1474                "__spg_info_sequences" => {
1475                    let (schema, rows) =
1476                        crate::system_catalog::synth_info_sequences(self.active_catalog());
1477                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1478                }
1479                // v7.17.0 Phase 3.P0-65 — mysql.user / mysql.db.
1480                "__spg_mysql_user" => {
1481                    let (schema, rows) = synth_mysql_user(self);
1482                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1483                }
1484                "__spg_mysql_db" => {
1485                    let (schema, rows) = synth_mysql_db();
1486                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1487                }
1488                // v7.39 (round 541) — the catalogs PG has that SPG is
1489                // genuinely empty of. Table-driven; see EMPTY_PG_CATALOGS.
1490                other if crate::system_catalog::synth_empty_pg_catalog(other).is_some() => {
1491                    let (schema, rows) =
1492                        crate::system_catalog::synth_empty_pg_catalog(other).expect("just checked");
1493                    materialise_meta_view(&mut catalog, view, schema, rows)?;
1494                }
1495                _ => {
1496                    return Err(EngineError::Unsupported(alloc::format!(
1497                        "meta view {view:?} is not yet materialisable; \
1498                         v7.16.2 covers information_schema.columns / .tables \
1499                         and pg_catalog.pg_class / pg_attribute; \
1500                         v7.17.0 P0-50..P0-57 add pg_type / pg_proc / pg_namespace / \
1501                         pg_indexes / pg_index / pg_constraint / pg_database / pg_roles / \
1502                         pg_user / pg_views / pg_matviews / pg_settings"
1503                    )));
1504                }
1505            }
1506        }
1507        Ok(catalog)
1508    }
1509
1510    pub(crate) fn exec_with_ctes(
1511        &self,
1512        stmt: &SelectStatement,
1513        cancel: CancelToken<'_>,
1514    ) -> Result<QueryResult, EngineError> {
1515        cancel.check()?;
1516        // v7.37.43-T4.4 — `&self` SELECT path: only read-only CTE
1517        // bodies are supported here. Writable CTEs on a SELECT
1518        // outer require `&mut self` and route through the
1519        // top-level `exec_select_cancel_mut` entry; sentori
1520        // 0065's WITH-INSERT-INSERT shape comes in as a top-level
1521        // INSERT, not a SELECT, so this restriction is harmless
1522        // in practice.
1523        if stmt.ctes.iter().any(|c| c.body.is_modifying()) {
1524            // v7.39 (read01 round 81) — PG's wording. A data-modifying CTE
1525            // (`WITH d AS (DELETE … RETURNING …) …`) is only legal at the top
1526            // of a statement, not nested inside a subquery; this path is
1527            // reached exactly when one is nested. The old text described SPG's
1528            // own executor plumbing ("the top-level mutable entry"), which
1529            // means nothing to a client.
1530            return Err(EngineError::Unsupported(
1531                "WITH clause containing a data-modifying statement must be at the top level".into(),
1532            ));
1533        }
1534        let catalog = self.materialise_ctes_readonly(&stmt.ctes, cancel)?;
1535        // Strip CTEs from the body before running on the temp engine
1536        // so we don't recurse forever.
1537        let mut body = stmt.clone();
1538        body.ctes = Vec::new();
1539        let mut temp = Engine::restore(catalog);
1540        if let Some(c) = self.clock {
1541            temp = temp.with_clock(c);
1542        }
1543        if let Some(f) = self.salt_fn {
1544            temp = temp.with_salt_fn(f);
1545        }
1546        temp.exec_select_cancel(&body, cancel)
1547    }
1548
1549    /// v7.37.43-T4.4 — read-only CTE materialiser used by the
1550    /// `&self` SELECT path. Caller guarantees no modifying CTE
1551    /// bodies are present.
1552    pub(crate) fn materialise_ctes_readonly(
1553        &self,
1554        ctes: &[spg_sql::ast::Cte],
1555        cancel: CancelToken<'_>,
1556    ) -> Result<crate::Catalog, EngineError> {
1557        cancel.check()?;
1558        let mut catalog = self.active_catalog().clone();
1559        for cte in ctes {
1560            let body_select = cte.body.as_select().ok_or_else(|| {
1561                EngineError::Unsupported(alloc::format!(
1562                    "data-modifying CTE not supported on this SELECT entry"
1563                ))
1564            })?;
1565            // v7.39 (round 156) — a CTE may SHADOW a same-named real table
1566            // (PG scoping: the WITH name wins for the outer query and later
1567            // CTEs, while THIS body still sees the real table — a
1568            // non-recursive body's self-name is the table, probe P2). This
1569            // materialiser works on a CLONE, so the shadow is simply: run
1570            // the body against the untouched clone, then drop the real
1571            // table from the clone before installing the CTE's temp. A
1572            // RECURSIVE self-reference is the CTE itself (P6), so there the
1573            // drop happens before the iterating materialiser runs.
1574            let (columns, rows) = if cte.recursive && select_refers_to(body_select, &cte.name) {
1575                let synthetic = spg_sql::ast::Cte {
1576                    name: cte.name.clone(),
1577                    body: spg_sql::ast::CteBody::Select(body_select.clone()),
1578                    recursive: true,
1579                    column_overrides: cte.column_overrides.clone(),
1580                    search: None,
1581                    cycle: None,
1582                };
1583                if catalog.get(&cte.name).is_some() {
1584                    let _ = catalog.drop_table(&cte.name);
1585                }
1586                self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1587            } else {
1588                let mut cte_engine = Engine::restore(catalog.clone());
1589                if let Some(c) = self.clock {
1590                    cte_engine = cte_engine.with_clock(c);
1591                }
1592                if let Some(f) = self.salt_fn {
1593                    cte_engine = cte_engine.with_salt_fn(f);
1594                }
1595                let body_result = cte_engine.exec_select_cancel(body_select, cancel)?;
1596                let QueryResult::Rows { columns, rows } = body_result else {
1597                    return Err(EngineError::Unsupported(alloc::format!(
1598                        "CTE {:?} body did not return rows",
1599                        cte.name
1600                    )));
1601                };
1602                (columns, rows)
1603            };
1604            let inferred = infer_column_types(&columns, &rows);
1605            let mut columns = inferred;
1606            if !cte.column_overrides.is_empty() {
1607                if cte.column_overrides.len() != columns.len() {
1608                    return Err(EngineError::Unsupported(alloc::format!(
1609                        "CTE {:?} column list has {} names but body returns {} columns",
1610                        cte.name,
1611                        cte.column_overrides.len(),
1612                        columns.len()
1613                    )));
1614                }
1615                for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1616                    col.name.clone_from(name);
1617                }
1618            }
1619            let schema = TableSchema::new(cte.name.clone(), columns);
1620            // v7.39 (round 156) — the body ran against the untouched clone;
1621            // from here on the CTE name resolves to the temp (PG scoping).
1622            if catalog.get(&cte.name).is_some() {
1623                let _ = catalog.drop_table(&cte.name);
1624            }
1625            catalog.create_table(schema).map_err(EngineError::Storage)?;
1626            let table = catalog
1627                .get_mut(&cte.name)
1628                .expect("just-created CTE table must exist");
1629            for row in rows {
1630                table.insert(row).map_err(EngineError::Storage)?;
1631            }
1632        }
1633        Ok(catalog)
1634    }
1635
1636    /// v7.37.43-T4.4 — shared CTE materialiser (mutable variant).
1637    /// Retained for non-DML callers; the DML path (writable CTE on
1638    /// INSERT/UPDATE/DELETE outer) uses `run_with_cte_temps` in
1639    /// `dml.rs` which installs the CTE temps directly on the
1640    /// active catalog so the outer statement's writes hit real
1641    /// tables.
1642    #[allow(dead_code)]
1643    pub(crate) fn materialise_ctes(
1644        &mut self,
1645        ctes: &[spg_sql::ast::Cte],
1646        cancel: CancelToken<'_>,
1647    ) -> Result<crate::Catalog, EngineError> {
1648        cancel.check()?;
1649        // v7.37.43-T4.4 — modifying CTEs need to write through the
1650        // SAME catalog as the outer statement, not a clone (PG's
1651        // writable CTE puts all modifications in one transaction).
1652        // For the read-only case the original logic cloned, but
1653        // since the outer statement also goes through the cloned
1654        // engine and ALL writes must converge, we now drive the
1655        // accumulator off `self.active_catalog().clone()` and
1656        // commit the modifying writes directly to `self`'s active
1657        // catalog so the surface is consistent.
1658        let mut catalog = self.active_catalog().clone();
1659        // v7.39 (round 149) — a modifying CTE body's target must be a
1660        // real relation, never a sibling CTE (PG: relation does not
1661        // exist); checked before any alias lands in the accumulator.
1662        for cte in ctes {
1663            let body_target = match &cte.body {
1664                spg_sql::ast::CteBody::Select(_) => None,
1665                spg_sql::ast::CteBody::Insert(i) => Some(i.table.as_str()),
1666                spg_sql::ast::CteBody::Update(u) => Some(u.table.as_str()),
1667                spg_sql::ast::CteBody::Delete(d) => Some(d.table.as_str()),
1668                spg_sql::ast::CteBody::Merge(m) => Some(m.target.as_str()),
1669            };
1670            if let Some(t) = body_target
1671                && ctes.iter().any(|c| c.name.eq_ignore_ascii_case(t))
1672                && catalog.get(t).is_none()
1673            {
1674                return Err(EngineError::Storage(
1675                    spg_storage::StorageError::TableNotFound { name: t.into() },
1676                ));
1677            }
1678        }
1679        for cte in ctes {
1680            if catalog.get(&cte.name).is_some() {
1681                return Err(EngineError::Unsupported(alloc::format!(
1682                    "CTE name {:?} shadows an existing table; rename the CTE",
1683                    cte.name
1684                )));
1685            }
1686            let (columns, rows) = match &cte.body {
1687                // v7.39 (round 145) — see the sibling site: only a body that
1688                // truly self-references takes the iterating materialiser.
1689                spg_sql::ast::CteBody::Select(body)
1690                    if cte.recursive && select_refers_to(body, &cte.name) =>
1691                {
1692                    // Recursive CTE — the existing helper takes a
1693                    // SELECT body and the snapshot catalog.
1694                    let synthetic = spg_sql::ast::Cte {
1695                        name: cte.name.clone(),
1696                        body: spg_sql::ast::CteBody::Select(body.clone()),
1697                        recursive: true,
1698                        column_overrides: cte.column_overrides.clone(),
1699                        search: None,
1700                        cycle: None,
1701                    };
1702                    self.materialise_recursive_cte(&synthetic, &catalog, cancel)?
1703                }
1704                spg_sql::ast::CteBody::Select(body) => {
1705                    // v7.25 (round-17) — run against the accumulated
1706                    // catalog so later CTEs can reference earlier
1707                    // ones in the same WITH clause.
1708                    let mut cte_engine = Engine::restore(catalog.clone());
1709                    if let Some(c) = self.clock {
1710                        cte_engine = cte_engine.with_clock(c);
1711                    }
1712                    if let Some(f) = self.salt_fn {
1713                        cte_engine = cte_engine.with_salt_fn(f);
1714                    }
1715                    let body_result = cte_engine.exec_select_cancel(body, cancel)?;
1716                    let QueryResult::Rows { columns, rows } = body_result else {
1717                        return Err(EngineError::Unsupported(alloc::format!(
1718                            "CTE {:?} body did not return rows",
1719                            cte.name
1720                        )));
1721                    };
1722                    (columns, rows)
1723                }
1724                spg_sql::ast::CteBody::Insert(body) => {
1725                    self.exec_modifying_cte_insert(&cte.name, body, cancel)?
1726                }
1727                spg_sql::ast::CteBody::Update(body) => {
1728                    self.exec_modifying_cte_update(&cte.name, body, cancel)?
1729                }
1730                spg_sql::ast::CteBody::Delete(body) => {
1731                    self.exec_modifying_cte_delete(&cte.name, body, cancel)?
1732                }
1733                spg_sql::ast::CteBody::Merge(body) => {
1734                    self.exec_modifying_cte_merge(&cte.name, body, cancel)?
1735                }
1736            };
1737            // v4.22: the projection builder labels any non-column
1738            // expression as Text — including literal SELECT 1.
1739            // Promote each column's type to whatever the rows
1740            // actually carry so the CTE storage table accepts them.
1741            let inferred = infer_column_types(&columns, &rows);
1742            let mut columns = inferred;
1743            if !cte.column_overrides.is_empty() {
1744                if cte.column_overrides.len() != columns.len() {
1745                    return Err(EngineError::Unsupported(alloc::format!(
1746                        "CTE {:?} column list has {} names but body returns {} columns",
1747                        cte.name,
1748                        cte.column_overrides.len(),
1749                        columns.len()
1750                    )));
1751                }
1752                for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1753                    col.name.clone_from(name);
1754                }
1755            }
1756            let schema = TableSchema::new(cte.name.clone(), columns);
1757            catalog.create_table(schema).map_err(EngineError::Storage)?;
1758            let table = catalog
1759                .get_mut(&cte.name)
1760                .expect("just-created CTE table must exist");
1761            for row in rows {
1762                table.insert(row).map_err(EngineError::Storage)?;
1763            }
1764        }
1765        Ok(catalog)
1766    }
1767
1768    /// v7.37.43-T4.4 — execute an INSERT CTE body. Runs the INSERT
1769    /// against `self` (so the mutation lands in the active catalog
1770    /// inside the current transaction) and captures the RETURNING
1771    /// projection — column schema + rows — to materialise as the
1772    /// CTE alias's table. An INSERT without RETURNING produces a
1773    /// 0-row table with a synthetic single-column placeholder
1774    /// (matches PG: the CTE alias is still defined, but referencing
1775    /// it from the outer query without RETURNING raises a
1776    /// column-resolution error at scan time).
1777    fn exec_modifying_cte_insert(
1778        &mut self,
1779        cte_name: &str,
1780        body: &spg_sql::ast::InsertStatement,
1781        _cancel: CancelToken<'_>,
1782    ) -> Result<
1783        (
1784            Vec<spg_storage::ColumnSchema>,
1785            Vec<spg_storage::Row<'static>>,
1786        ),
1787        EngineError,
1788    > {
1789        // round 151 — a WITH-headed body keeps its own ctes; the body
1790        // statement routes through its writable-CTE entry (outer CTEs
1791        // are never copied into bodies, so no recursion risk).
1792        let body = body.clone();
1793        let result = self.exec_insert(body)?;
1794        match result {
1795            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1796            QueryResult::CommandOk { .. } => {
1797                // No RETURNING — emit a sentinel single-column
1798                // schema with zero rows so the alias is defined.
1799                let placeholder = spg_storage::ColumnSchema::new(
1800                    alloc::format!("{cte_name}_returning_absent"),
1801                    spg_storage::DataType::Text,
1802                    true,
1803                );
1804                Ok((alloc::vec![placeholder], Vec::new()))
1805            }
1806        }
1807    }
1808
1809    /// v7.37.43-T4.4 — execute an UPDATE CTE body, same semantics
1810    /// as INSERT above.
1811    fn exec_modifying_cte_update(
1812        &mut self,
1813        cte_name: &str,
1814        body: &spg_sql::ast::UpdateStatement,
1815        cancel: CancelToken<'_>,
1816    ) -> Result<
1817        (
1818            Vec<spg_storage::ColumnSchema>,
1819            Vec<spg_storage::Row<'static>>,
1820        ),
1821        EngineError,
1822    > {
1823        let body = body.clone();
1824        let result = self.exec_update_cancel(&body, cancel)?;
1825        match result {
1826            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1827            QueryResult::CommandOk { .. } => {
1828                let placeholder = spg_storage::ColumnSchema::new(
1829                    alloc::format!("{cte_name}_returning_absent"),
1830                    spg_storage::DataType::Text,
1831                    true,
1832                );
1833                Ok((alloc::vec![placeholder], Vec::new()))
1834            }
1835        }
1836    }
1837
1838    /// v7.37.43-T4.4 — execute a DELETE CTE body.
1839    fn exec_modifying_cte_delete(
1840        &mut self,
1841        cte_name: &str,
1842        body: &spg_sql::ast::DeleteStatement,
1843        cancel: CancelToken<'_>,
1844    ) -> Result<
1845        (
1846            Vec<spg_storage::ColumnSchema>,
1847            Vec<spg_storage::Row<'static>>,
1848        ),
1849        EngineError,
1850    > {
1851        let body = body.clone();
1852        let result = self.exec_delete_cancel(&body, cancel)?;
1853        match result {
1854            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1855            QueryResult::CommandOk { .. } => {
1856                let placeholder = spg_storage::ColumnSchema::new(
1857                    alloc::format!("{cte_name}_returning_absent"),
1858                    spg_storage::DataType::Text,
1859                    true,
1860                );
1861                Ok((alloc::vec![placeholder], Vec::new()))
1862            }
1863        }
1864    }
1865
1866    /// v7.39 (round 149) — execute a MERGE CTE body (PG 17).
1867    fn exec_modifying_cte_merge(
1868        &mut self,
1869        cte_name: &str,
1870        body: &spg_sql::ast::MergeStatement,
1871        cancel: CancelToken<'_>,
1872    ) -> Result<
1873        (
1874            Vec<spg_storage::ColumnSchema>,
1875            Vec<spg_storage::Row<'static>>,
1876        ),
1877        EngineError,
1878    > {
1879        let body = body.clone();
1880        let result = self.exec_merge_cancel(&body, cancel)?;
1881        match result {
1882            QueryResult::Rows { columns, rows } => Ok((columns, rows)),
1883            QueryResult::CommandOk { .. } => {
1884                let placeholder = spg_storage::ColumnSchema::new(
1885                    alloc::format!("{cte_name}_returning_absent"),
1886                    spg_storage::DataType::Text,
1887                    true,
1888                );
1889                Ok((alloc::vec![placeholder], Vec::new()))
1890            }
1891        }
1892    }
1893
1894    /// v4.22: materialise a WITH RECURSIVE CTE. The body must be a
1895    /// UNION (or UNION ALL) of an anchor that does not reference
1896    /// the CTE name, and one or more recursive terms that do. The
1897    /// anchor runs first; each subsequent iteration runs the
1898    /// recursive term against a temp catalog where the CTE name is
1899    /// bound to the *previous* iteration's output. Iteration stops
1900    /// when the recursive term yields no rows; UNION (DISTINCT)
1901    /// deduplicates against the accumulated result, UNION ALL does
1902    /// not. A hard cap on total rows prevents runaway queries.
1903    #[allow(clippy::too_many_lines)]
1904    pub(crate) fn materialise_recursive_cte(
1905        &self,
1906        cte: &spg_sql::ast::Cte,
1907        base_catalog: &Catalog,
1908        cancel: CancelToken<'_>,
1909    ) -> Result<(Vec<ColumnSchema>, Vec<Row<'static>>), EngineError> {
1910        const MAX_TOTAL_ROWS: usize = 1_000_000;
1911        const MAX_ITERATIONS: usize = 100_000;
1912        cancel.check()?;
1913        // v7.37.43-T4.4 — RECURSIVE only supports SELECT bodies;
1914        // a modifying recursive CTE is parser-rejectable but we
1915        // guard here defensively.
1916        let body_select = cte.body.as_select().ok_or_else(|| {
1917            EngineError::Unsupported(alloc::format!(
1918                "WITH RECURSIVE {:?} body must be a SELECT, not a data-modifying statement",
1919                cte.name
1920            ))
1921        })?;
1922        if body_select.unions.is_empty() {
1923            return Err(EngineError::Unsupported(alloc::format!(
1924                "WITH RECURSIVE {:?} body must be a UNION of an anchor and a recursive term",
1925                cte.name
1926            )));
1927        }
1928        // Anchor: the body's leading SELECT, with unions stripped.
1929        let mut anchor = body_select.clone();
1930        let all_union_terms = core::mem::take(&mut anchor.unions);
1931        anchor.ctes = Vec::new();
1932        // v7.37 D.42 — split the UNION members: those that do NOT reference the
1933        // CTE are additional ANCHOR terms, only the ones that do recurse. A
1934        // multi-row VALUES seed lowers to `SELECT r1 UNION ALL SELECT r2 UNION
1935        // ALL <recursive>`, so the leading SELECT alone is not the whole anchor —
1936        // treating the non-recursive `SELECT r2` as a recursive term made it
1937        // re-emit its constant row every iteration → runaway loop.
1938        let (anchor_terms, union_terms): (Vec<_>, Vec<_>) = all_union_terms
1939            .into_iter()
1940            .partition(|(_, t)| !select_refers_to(t, &cte.name));
1941        let anchor_result = self.exec_select_cancel(&anchor, cancel)?;
1942        let QueryResult::Rows {
1943            columns: anchor_cols,
1944            rows: mut anchor_rows,
1945        } = anchor_result
1946        else {
1947            return Err(EngineError::Unsupported(alloc::format!(
1948                "WITH RECURSIVE {:?}: anchor did not return rows",
1949                cte.name
1950            )));
1951        };
1952        // Append every non-recursive UNION member's rows to the anchor set.
1953        for (_, term) in &anchor_terms {
1954            let mut term = term.clone();
1955            term.ctes = Vec::new();
1956            if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&term, cancel)? {
1957                anchor_rows.extend(rows);
1958            }
1959        }
1960        // The projection builder labels non-column expressions Text;
1961        // refine column types from the anchor's actual values so the
1962        // intermediate iter-catalog tables accept them.
1963        let mut columns = infer_column_types(&anchor_cols, &anchor_rows);
1964        if !cte.column_overrides.is_empty() {
1965            if cte.column_overrides.len() != columns.len() {
1966                return Err(EngineError::Unsupported(alloc::format!(
1967                    "CTE {:?} column list has {} names but anchor returns {} columns",
1968                    cte.name,
1969                    cte.column_overrides.len(),
1970                    columns.len()
1971                )));
1972            }
1973            for (col, name) in columns.iter_mut().zip(cte.column_overrides.iter()) {
1974                col.name.clone_from(name);
1975            }
1976        }
1977        let mut all_rows: Vec<Row<'static>> = anchor_rows.clone();
1978        let mut working_set: Vec<Row<'static>> = anchor_rows;
1979        let mut seen: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
1980        // Track at least one "all UNION ALL" flag — if every union
1981        // kind is ALL we skip the dedup step (faster + matches PG).
1982        let all_union_all = union_terms.iter().all(|(k, _)| matches!(k, UnionKind::All));
1983        if !all_union_all {
1984            for r in &all_rows {
1985                seen.insert(encode_row_key(r));
1986            }
1987        }
1988        // v7.39 (round 598) — the engine and its catalog are built ONCE.
1989        // Each iteration used to clone the catalog, create the CTE table,
1990        // and construct a whole `Engine` — which initialises 82 fields — to
1991        // hold that round's working set. A counting allocator put the loop
1992        // at 63 allocations and 104 kB per iteration, or 1 GB for a
1993        // 10,000-row recursive CTE, and none of it varied with how much
1994        // else was in the catalog: the per-round rebuild WAS the cost. The
1995        // table is emptied and refilled instead.
1996        let mut iter_catalog = base_catalog.clone();
1997        let schema = TableSchema::new(cte.name.clone(), columns.clone());
1998        iter_catalog
1999            .create_table(schema)
2000            .map_err(EngineError::Storage)?;
2001        let mut iter_engine = Engine::restore(iter_catalog);
2002        if let Some(c) = self.clock {
2003            iter_engine = iter_engine.with_clock(c);
2004        }
2005        if let Some(f) = self.salt_fn {
2006            iter_engine = iter_engine.with_salt_fn(f);
2007        }
2008        // The recursive terms are cloned once too — the clone stripped the
2009        // CTE list off each of them, per term per iteration.
2010        let recursive_terms: Vec<SelectStatement> = union_terms
2011            .iter()
2012            .map(|(_, t)| {
2013                let mut t = t.clone();
2014                t.ctes = Vec::new();
2015                t
2016            })
2017            .collect();
2018        // v7.39 (round 618) — plan every recursive term once. Taken only if
2019        // ALL of them plan, so a query never runs half on each path.
2020        let term_plans: Option<Vec<RecursiveTermPlan<'_>>> = recursive_terms
2021            .iter()
2022            .map(|t| plan_recursive_term(t, &cte.name, columns.len()))
2023            .collect();
2024        let fast_ctx = term_plans.as_ref().map(|plans| {
2025            let alias = plans[0].alias.clone();
2026            (alias, ())
2027        });
2028        for iter in 0..MAX_ITERATIONS {
2029            cancel.check()?;
2030            if working_set.is_empty() {
2031                break;
2032            }
2033            if let (Some(plans), Some((_, ()))) = (term_plans.as_ref(), fast_ctx.as_ref()) {
2034                // The worktable IS the working set: no table to empty and
2035                // refill, and no query execution per round.
2036                let mut next_set: Vec<Row<'static>> = Vec::new();
2037                for plan in plans {
2038                    let ctx = self.ev_ctx(&columns, Some(&plan.alias));
2039                    for row in &working_set {
2040                        cancel.check()?;
2041                        if let Some(w) = plan.where_ {
2042                            let v = eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
2043                            if !matches!(v, Value::Bool(true)) {
2044                                continue;
2045                            }
2046                        }
2047                        let mut vals: Vec<Value<'static>> = Vec::with_capacity(plan.items.len());
2048                        for it in &plan.items {
2049                            vals.push(eval::eval_expr(it, row, &ctx).map_err(EngineError::Eval)?);
2050                        }
2051                        let out = Row::new(vals);
2052                        if !all_union_all {
2053                            let key = encode_row_key(&out);
2054                            if !seen.insert(key) {
2055                                continue;
2056                            }
2057                        }
2058                        next_set.push(out);
2059                    }
2060                }
2061                if next_set.is_empty() {
2062                    break;
2063                }
2064                all_rows.extend(next_set.iter().cloned());
2065                working_set = next_set;
2066                if all_rows.len() > MAX_TOTAL_ROWS {
2067                    return Err(EngineError::Unsupported(alloc::format!(
2068                        "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2069                        cte.name
2070                    )));
2071                }
2072                if iter + 1 == MAX_ITERATIONS {
2073                    return Err(EngineError::Unsupported(alloc::format!(
2074                        "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2075                        cte.name
2076                    )));
2077                }
2078                continue;
2079            }
2080            {
2081                // Truncated rather than dropped and recreated: the table's
2082                // own structure is what dropping it throws away, and it is
2083                // identical every round.
2084                let cat = iter_engine.base_catalog_mut();
2085                let table = cat.get_mut(&cte.name).expect("created above");
2086                table.truncate();
2087                for row in &working_set {
2088                    table.insert(row.clone()).map_err(EngineError::Storage)?;
2089                }
2090            }
2091            // Run each recursive term in sequence and collect new rows.
2092            let mut next_set: Vec<Row<'static>> = Vec::new();
2093            for term in &recursive_terms {
2094                let r = iter_engine.exec_select_cancel(term, cancel)?;
2095                let QueryResult::Rows {
2096                    columns: rc,
2097                    rows: rs,
2098                } = r
2099                else {
2100                    return Err(EngineError::Unsupported(alloc::format!(
2101                        "WITH RECURSIVE {:?}: recursive term did not return rows",
2102                        cte.name
2103                    )));
2104                };
2105                if rc.len() != columns.len() {
2106                    return Err(EngineError::Unsupported(alloc::format!(
2107                        "WITH RECURSIVE {:?}: column count of recursive term ({}) does not match anchor ({})",
2108                        cte.name,
2109                        rc.len(),
2110                        columns.len()
2111                    )));
2112                }
2113                for row in rs {
2114                    if !all_union_all {
2115                        let key = encode_row_key(&row);
2116                        if !seen.insert(key) {
2117                            continue;
2118                        }
2119                    }
2120                    next_set.push(row);
2121                }
2122            }
2123            if next_set.is_empty() {
2124                break;
2125            }
2126            all_rows.extend(next_set.iter().cloned());
2127            working_set = next_set;
2128            if all_rows.len() > MAX_TOTAL_ROWS {
2129                return Err(EngineError::Unsupported(alloc::format!(
2130                    "WITH RECURSIVE {:?}: produced more than {MAX_TOTAL_ROWS} rows — likely runaway recursion",
2131                    cte.name
2132                )));
2133            }
2134            if iter + 1 == MAX_ITERATIONS {
2135                return Err(EngineError::Unsupported(alloc::format!(
2136                    "WITH RECURSIVE {:?}: exceeded {MAX_ITERATIONS} iterations",
2137                    cte.name
2138                )));
2139            }
2140        }
2141        Ok((columns, all_rows))
2142    }
2143
2144    pub(crate) fn resolve_select_subqueries(
2145        &self,
2146        stmt: &mut SelectStatement,
2147        cancel: CancelToken<'_>,
2148    ) -> Result<(), EngineError> {
2149        for item in &mut stmt.items {
2150            if let SelectItem::Expr { expr, alias } = item {
2151                // An UNCORRELATED subquery is replaced by its value right
2152                // here, and the shape the column was named for goes with
2153                // it: by projection time `SELECT EXISTS(SELECT 1)` is a
2154                // boolean literal, so SPG answered `?column?` where PG18
2155                // answers `exists`. Only a subquery at the TOP of the item
2156                // loses its name this way — one nested inside a call still
2157                // reports the call.
2158                if alias.is_none()
2159                    && matches!(
2160                        expr,
2161                        Expr::ScalarSubquery(_)
2162                            | Expr::Exists { .. }
2163                            | Expr::InSubquery { .. }
2164                            | Expr::RowInSubquery { .. }
2165                            | Expr::RowCmpSubquery { .. }
2166                    )
2167                {
2168                    *alias = Some(default_output_name(expr, self.backslash_escapes));
2169                }
2170                self.resolve_expr_subqueries(expr, cancel)?;
2171            }
2172        }
2173        if let Some(w) = &mut stmt.where_ {
2174            self.resolve_expr_subqueries(w, cancel)?;
2175        }
2176        // v7.24.1 — JOIN ON conditions can carry subqueries too;
2177        // they were never walked, so even an UNCORRELATED subquery
2178        // in ON hit "subquery reached row eval".
2179        if let Some(from) = &mut stmt.from {
2180            for j in &mut from.joins {
2181                if let Some(on) = &mut j.on {
2182                    self.resolve_expr_subqueries(on, cancel)?;
2183                }
2184            }
2185        }
2186        if let Some(gs) = &mut stmt.group_by {
2187            for g in gs {
2188                self.resolve_expr_subqueries(g, cancel)?;
2189            }
2190        }
2191        if let Some(h) = &mut stmt.having {
2192            self.resolve_expr_subqueries(h, cancel)?;
2193        }
2194        for o in &mut stmt.order_by {
2195            self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2196        }
2197        for (_, peer) in &mut stmt.unions {
2198            self.resolve_select_subqueries(peer, cancel)?;
2199        }
2200        Ok(())
2201    }
2202
2203    #[allow(clippy::only_used_in_recursion)] // engine handle reads aren't really pure
2204    pub(crate) fn resolve_expr_subqueries(
2205        &self,
2206        e: &mut Expr,
2207        cancel: CancelToken<'_>,
2208    ) -> Result<(), EngineError> {
2209        // Replace-on-this-node cases first.
2210        if let Some(replacement) = self.subquery_replacement(e, cancel)? {
2211            *e = replacement;
2212            return Ok(());
2213        }
2214        match e {
2215            Expr::NamedArg { expr, .. } => self.resolve_expr_subqueries(expr, cancel)?,
2216            Expr::Variadic(expr) => self.resolve_expr_subqueries(expr, cancel)?,
2217            Expr::AggregateOrdered { call, order_by, .. } => {
2218                self.resolve_expr_subqueries(call, cancel)?;
2219                for o in order_by.iter_mut() {
2220                    self.resolve_expr_subqueries(&mut o.expr, cancel)?;
2221                }
2222            }
2223            Expr::Binary { lhs, rhs, .. } => {
2224                self.resolve_expr_subqueries(lhs, cancel)?;
2225                self.resolve_expr_subqueries(rhs, cancel)?;
2226            }
2227            Expr::Unary { expr, .. }
2228            | Expr::Cast { expr, .. }
2229            | Expr::IsNull { expr, .. }
2230            | Expr::BoolTest { expr, .. }
2231            | Expr::FieldAccess { base: expr, .. } => {
2232                self.resolve_expr_subqueries(expr, cancel)?;
2233            }
2234            Expr::FunctionCall { args, .. } => {
2235                for a in args {
2236                    self.resolve_expr_subqueries(a, cancel)?;
2237                }
2238            }
2239            Expr::Like { expr, pattern, .. } => {
2240                self.resolve_expr_subqueries(expr, cancel)?;
2241                self.resolve_expr_subqueries(pattern, cancel)?;
2242            }
2243            Expr::Extract { source, .. } => self.resolve_expr_subqueries(source, cancel)?,
2244            // v4.12 window functions — recurse into args + ORDER BY
2245            // + PARTITION BY in case they carry inner subqueries.
2246            Expr::WindowFunction {
2247                args,
2248                partition_by,
2249                order_by,
2250                ..
2251            } => {
2252                for a in args {
2253                    self.resolve_expr_subqueries(a, cancel)?;
2254                }
2255                for p in partition_by {
2256                    self.resolve_expr_subqueries(p, cancel)?;
2257                }
2258                for (e, _, _) in order_by {
2259                    self.resolve_expr_subqueries(e, cancel)?;
2260                }
2261            }
2262            // Subquery nodes are handled in subquery_replacement
2263            // (which returned None — defensive no-op); Literal /
2264            // Column are leaves.
2265            Expr::ScalarSubquery(_)
2266            | Expr::Exists { .. }
2267            | Expr::InSubquery { .. }
2268            | Expr::RowInSubquery { .. }
2269            | Expr::RowCmpSubquery { .. }
2270            | Expr::Literal(_)
2271            | Expr::Placeholder(_)
2272            | Expr::Column(_) => {}
2273            // v7.30.2 — list elements can carry scalar subqueries
2274            // (`x IN (1, (SELECT …))`).
2275            Expr::InList { expr, list, .. } => {
2276                self.resolve_expr_subqueries(expr, cancel)?;
2277                for item in list {
2278                    self.resolve_expr_subqueries(item, cancel)?;
2279                }
2280            }
2281            // v7.10.10 — recurse children.
2282            Expr::Array(items) => {
2283                for elem in items {
2284                    self.resolve_expr_subqueries(elem, cancel)?;
2285                }
2286            }
2287            Expr::ArraySubscript { target, index } => {
2288                self.resolve_expr_subqueries(target, cancel)?;
2289                self.resolve_expr_subqueries(index, cancel)?;
2290            }
2291            Expr::ArraySlice { target, lo, hi } => {
2292                self.resolve_expr_subqueries(target, cancel)?;
2293                if let Some(l) = lo {
2294                    self.resolve_expr_subqueries(l, cancel)?;
2295                }
2296                if let Some(h) = hi {
2297                    self.resolve_expr_subqueries(h, cancel)?;
2298                }
2299            }
2300            Expr::AnyAll { expr, array, .. } => {
2301                self.resolve_expr_subqueries(expr, cancel)?;
2302                // Quantified subquery — an uncorrelated one
2303                // materialises up front; a correlated one stays for
2304                // the per-row resolver.
2305                if let Expr::ScalarSubquery(inner) = array.as_mut() {
2306                    if !crate::subquery::select_is_correlated(inner) {
2307                        let s = (**inner).clone();
2308                        **array = self.materialize_quantified_rows(&s, cancel)?;
2309                    }
2310                } else {
2311                    self.resolve_expr_subqueries(array, cancel)?;
2312                }
2313            }
2314            Expr::Case {
2315                operand,
2316                branches,
2317                else_branch,
2318            } => {
2319                if let Some(o) = operand {
2320                    self.resolve_expr_subqueries(o, cancel)?;
2321                }
2322                for (w, t) in branches {
2323                    self.resolve_expr_subqueries(w, cancel)?;
2324                    self.resolve_expr_subqueries(t, cancel)?;
2325                }
2326                if let Some(e) = else_branch {
2327                    self.resolve_expr_subqueries(e, cancel)?;
2328                }
2329            }
2330        }
2331        Ok(())
2332    }
2333}
2334
2335impl Engine {
2336    /// v6.10.2 — projection for AS OF SEGMENT. Resolves
2337    /// `SelectItem::Wildcard` to all schema columns and
2338    /// `SelectItem::Expr` via the regular eval path.
2339    pub(crate) fn project_row_simple(
2340        &self,
2341        row: &Row<'static>,
2342        items: &[SelectItem],
2343        schema_cols: &[ColumnSchema],
2344        alias: &str,
2345    ) -> Result<Row<'static>, EngineError> {
2346        let ctx = self.ev_ctx(schema_cols, Some(alias));
2347        let cancel = CancelToken::none();
2348        let mut out_vals = Vec::new();
2349        for item in items {
2350            match item {
2351                // In a single-table projection (AS OF SEGMENT / RETURNING) a
2352                // qualified `t.*` covers exactly the same columns as a bare `*`.
2353                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2354                    out_vals.extend(row.values.iter().cloned());
2355                }
2356                SelectItem::Expr { expr, .. } => {
2357                    let v = self.eval_expr_with_correlated(expr, row, &ctx, cancel, None)?;
2358                    out_vals.push(v);
2359                }
2360            }
2361        }
2362        Ok(Row::new(out_vals))
2363    }
2364
2365    /// v6.10.2 — derive the output `ColumnSchema` list for an
2366    /// AS OF SEGMENT projection. Wildcards take the full schema;
2367    /// expressions take the alias if present or a synthetic
2368    /// `?column?` (PG convention) otherwise.
2369    pub(crate) fn derive_output_columns(
2370        &self,
2371        items: &[SelectItem],
2372        schema_cols: &[ColumnSchema],
2373        table_alias: &str,
2374    ) -> Vec<ColumnSchema> {
2375        let mut out = Vec::new();
2376        for item in items {
2377            match item {
2378                // `t.*` / `OLD.*` / `NEW.*` all mirror the full table schema in
2379                // a single-table projection.
2380                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
2381                    out.extend(schema_cols.iter().cloned());
2382                }
2383                SelectItem::Expr { expr, alias } => {
2384                    // Bare column references inherit the schema
2385                    // column's name + type — PG names `RETURNING id`
2386                    // "id" and types it BIGINT, and the sqlx embed
2387                    // path type-checks RowDescription against the
2388                    // Rust target (mailrs embed round-12).
2389                    if let Expr::Column(col) = expr
2390                        && let Some(sc) = schema_cols.iter().find(|c| c.name == col.name)
2391                    {
2392                        let name = alias.clone().unwrap_or_else(|| sc.name.clone());
2393                        let mut c = ColumnSchema::new(name, sc.ty, sc.nullable);
2394                        // v7.39 (read01 round 54) — carry the enum identity:
2395                        // it lives outside the DataType lattice, so a derived
2396                        // table built from this schema otherwise forgets it and
2397                        // the OUTER `ORDER BY <enum col>` silently sorts by the
2398                        // label's TEXT instead of member order.
2399                        c.user_enum_type = sc.user_enum_type.clone();
2400                        out.push(c);
2401                        continue;
2402                    }
2403                    let name = alias.clone().unwrap_or_else(|| "?column?".to_string());
2404                    // v7.30.4 (mailrs round-27, P0) — type the
2405                    // expression with the same inference the SELECT
2406                    // list uses (INT−INT=INT, BIGINT+INT=BIGINT…).
2407                    // The old Text default broke every typed decode
2408                    // of `RETURNING uidnext - 1 AS uid`: four days
2409                    // of inbound mail indexed nowhere. Inference
2410                    // failure keeps the old Text fallback rather
2411                    // than inventing new error paths here.
2412                    // v7.39 (round 258) — take the enum identity from the
2413                    // same projection build, not just the type: a constant
2414                    // SELECT (`SELECT 'ok'::mood AS x`, which is what a
2415                    // VALUES row lowers to) is an EXPRESSION, so it landed
2416                    // here and the derived table forgot the enum.
2417                    let (ty, nullable) = build_projection(
2418                        core::slice::from_ref(item),
2419                        schema_cols,
2420                        table_alias,
2421                        self.backslash_escapes,
2422                        Some(self.active_catalog()),
2423                    )
2424                    .ok()
2425                    .and_then(|p| p.into_iter().next())
2426                    .map_or((DataType::Text, true), |p| (p.ty, p.nullable));
2427                    out.push(ColumnSchema::new(name, ty, nullable));
2428                }
2429            }
2430        }
2431        out
2432    }
2433
2434    /// v4.5: SELECT with cooperative cancellation. The token is
2435    /// honoured between UNION peers and inside the bare-SELECT row
2436    /// loop; HNSW kNN graph walks and the aggregate executor don't
2437    /// honour it yet (deferred — those paths bound their work
2438    /// internally by `LIMIT k` and `GROUP BY` cardinality).
2439    /// v7.38 (read01 P3.NEW3) — materialise a `spg_*` / `pg_*` meta-view by
2440    /// its (lowercased) name, or None if the name isn't a virtual view.
2441    /// Callers decide whether to return it directly (`SELECT *`) or stage
2442    /// it as a temp table for the full query pipeline.
2443    fn meta_view_result(&self, name: &str) -> Option<QueryResult> {
2444        Some(match name {
2445            "spg_statistic" => self.exec_spg_statistic(),
2446            "spg_stat_replication" => self.exec_spg_stat_replication(),
2447            "spg_stat_segment" => self.exec_spg_stat_segment(),
2448            "spg_memory_stats" => self.exec_spg_memory_stats(),
2449            "spg_stat_query" => self.exec_spg_stat_query(),
2450            "pg_stat_statements" => self.exec_pg_stat_statements(),
2451            "spg_stat_activity" => self.exec_spg_stat_activity(),
2452            "pg_stat_activity" => self.exec_pg_stat_activity(),
2453            "pg_locks" => self.exec_pg_locks(),
2454            "pg_statio_user_tables" => self.exec_pg_statio_user_tables(),
2455            "spg_stat_mvcc" => self.exec_spg_stat_mvcc(),
2456            "spg_partition_health" => self.exec_spg_partition_health(),
2457            "spg_audit_chain" => self.exec_spg_audit_chain(),
2458            "spg_audit_verify" => self.exec_spg_audit_verify(),
2459            "spg_table_ddl" => self.exec_spg_table_ddl(),
2460            "spg_role_ddl" => self.exec_spg_role_ddl(),
2461            "spg_database_ddl" => self.exec_spg_database_ddl(),
2462            _ => return None,
2463        })
2464    }
2465
2466    /// v7.39 (round 462) — the catalog an admin / stat view SELECT
2467    /// describes against: this engine's catalog with the view staged as a
2468    /// table, exactly as `exec_select_cancel_as` stages it for a
2469    /// non-bare query.
2470    ///
2471    /// These views never reach the catalog — each is a fixed row set built
2472    /// inside its own `exec_*` — so Describe reported no columns for all
2473    /// seventeen of them. Rows are deliberately not inserted: Describe
2474    /// only needs the shape, and `infer_column_types` reads the rows we
2475    /// already have in hand.
2476    pub(crate) fn admin_view_catalog(&self, stmt: &SelectStatement) -> Option<Catalog> {
2477        let from = stmt.from.as_ref()?;
2478        if !from.joins.is_empty() || self.active_catalog().get(&from.primary.name).is_some() {
2479            return None;
2480        }
2481        let lower = from.primary.name.to_ascii_lowercase();
2482        let QueryResult::Rows { columns, rows } = self.meta_view_result(&lower)? else {
2483            return None;
2484        };
2485        let mut catalog = self.active_catalog().clone();
2486        let cols = infer_column_types(&columns, &rows);
2487        catalog
2488            .create_table(TableSchema::new(from.primary.name.clone(), cols))
2489            .ok()?;
2490        Some(catalog)
2491    }
2492
2493    pub(crate) fn exec_select_cancel(
2494        &self,
2495        stmt: &SelectStatement,
2496        cancel: CancelToken<'_>,
2497    ) -> Result<QueryResult, EngineError> {
2498        self.exec_select_cancel_as(stmt, cancel, None)
2499    }
2500
2501    /// v7.39 (round 334, V55) — the same read core, authorised as
2502    /// `as_role`. A `SECURITY DEFINER` function's body runs as the
2503    /// function's OWNER: that is the entire point of the form, and without
2504    /// it every definer function failed with "permission denied" on the
2505    /// very table it exists to expose.
2506    /// v7.39 (round 559) — see the call site. `None` for anything but
2507    /// the bare shape, so every other query keeps its old path.
2508    fn try_bare_count_star(
2509        &self,
2510        stmt: &SelectStatement,
2511        as_role: Option<&str>,
2512    ) -> Result<Option<QueryResult>, EngineError> {
2513        use spg_sql::ast::SelectItem;
2514        if as_role.is_some()
2515            || !stmt.ctes.is_empty()
2516            || !stmt.unions.is_empty()
2517            || stmt.where_.is_some()
2518            || stmt.group_by.is_some()
2519            || stmt.having.is_some()
2520            || stmt.distinct
2521            || !stmt.order_by.is_empty()
2522            || stmt.limit.is_some()
2523            || stmt.offset.is_some()
2524            || stmt.items.len() != 1
2525        {
2526            return Ok(None);
2527        }
2528        let Some(from) = &stmt.from else {
2529            return Ok(None);
2530        };
2531        if !from.joins.is_empty()
2532            || stmt.locking.is_some()
2533            || from.primary.lateral_subquery.is_some()
2534            || from.primary.unnest_expr.is_some()
2535            || from.primary.generate_series_args.is_some()
2536            || from.primary.name.is_empty()
2537            || from.primary.name.starts_with("__spg_")
2538        {
2539            return Ok(None);
2540        }
2541        // A partition PARENT holds no rows of its own — they live in the
2542        // children — so its header count is 0 and the ordinary path has
2543        // to fan out. Caught by the partition conformance cases.
2544        //
2545        // v7.39 (round 645) — and an INHERITANCE parent holds only SOME
2546        // of them, which is worse: its header count is a real number,
2547        // just not the answer. `SELECT count(*) FROM par` returned 1
2548        // where PG returns 2, because this shortcut fired before the
2549        // fan-out could. The question is "does anything descend from
2550        // this", not "was it declared a partition parent".
2551        if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2552            return Ok(None);
2553        }
2554        let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2555            return Ok(None);
2556        };
2557        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
2558            return Ok(None);
2559        };
2560        if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
2561            return Ok(None);
2562        }
2563        // A row-security policy filters rows, so the header count is not
2564        // the answer; the ordinary path applies the policy.
2565        let Some(table) = self.active_catalog().get(&from.primary.name) else {
2566            return Ok(None);
2567        };
2568        if table.schema().row_security {
2569            return Ok(None);
2570        }
2571        // Rows frozen to the cold tier are not in `headers`, so the
2572        // header count would miss them. Caught by the cold-tier e2e.
2573        if table.has_cold_rows_fast() {
2574            return Ok(None);
2575        }
2576        let n = table.count_visible(&self.current_snapshot());
2577        let col = alias.clone().unwrap_or_else(|| String::from("count"));
2578        Ok(Some(QueryResult::Rows {
2579            columns: alloc::vec![ColumnSchema::new(col, DataType::BigInt, false)],
2580            rows: alloc::vec![Row::new(alloc::vec![Value::BigInt(
2581                i64::try_from(n).unwrap_or(i64::MAX)
2582            )])],
2583        }))
2584    }
2585
2586    /// v7.39 (round 560) — `SELECT <indexed col> FROM t WHERE <range on
2587    /// that col>` served from the index, never reading a row.
2588    ///
2589    /// Measured over pgwire on a 500k table, a 100k-row range: PG18's
2590    /// Index Only Scan 3.6 ms against SPG's 30 ms, widening with the row
2591    /// count (2x at 1k). PG needs its visibility map for this — a heap
2592    /// tuple carries its own visibility, so an index entry alone cannot
2593    /// say whether the row is live, and PG reads the heap for any page
2594    /// the map does not mark all-visible. SPG keeps a header array
2595    /// beside the rows, so the locator answers it directly and there is
2596    /// no map to be stale.
2597    /// v7.39 (round 564) — the shape test, once, for both the
2598    /// materialising scan and the streaming one.
2599    ///
2600    /// Two callers asking the same question in two places is how a fact
2601    /// starts drifting; the answer here is the single copy. Returns the
2602    /// table, the alias the predicate is written against, the projected
2603    /// column's position, and the name the single output column takes.
2604    pub(crate) fn index_only_shape<'s>(
2605        &'s self,
2606        stmt: &'s SelectStatement,
2607    ) -> Option<(&'s spg_storage::Table, &'s str, usize, String)> {
2608        use spg_sql::ast::SelectItem;
2609        if !stmt.ctes.is_empty()
2610            || !stmt.unions.is_empty()
2611            || stmt.group_by.is_some()
2612            || stmt.having.is_some()
2613            || stmt.distinct
2614            || stmt.locking.is_some()
2615            || !stmt.order_by.is_empty()
2616            || stmt.limit.is_some()
2617            || stmt.offset.is_some()
2618            || stmt.items.len() != 1
2619        {
2620            return None;
2621        }
2622        let (Some(from), Some(_)) = (&stmt.from, &stmt.where_) else {
2623            return None;
2624        };
2625        if !from.joins.is_empty()
2626            || from.primary.lateral_subquery.is_some()
2627            || from.primary.unnest_expr.is_some()
2628            || from.primary.generate_series_args.is_some()
2629            || from.primary.name.is_empty()
2630            || from.primary.name.starts_with("__spg_")
2631        {
2632            return None;
2633        }
2634        // v7.39 (round 645) — see the note on the sibling shortcut above:
2635        // an inheritance parent's own header count is not the answer.
2636        if crate::partition::has_children(self.active_catalog(), &from.primary.name) {
2637            return None;
2638        }
2639        let SelectItem::Expr { expr, alias } = &stmt.items[0] else {
2640            return None;
2641        };
2642        let spg_sql::ast::Expr::Column(c) = expr else {
2643            return None;
2644        };
2645        let alias_name = from.primary.alias.as_deref().unwrap_or(&from.primary.name);
2646        if let Some(q) = c.qualifier.as_deref()
2647            && !q.eq_ignore_ascii_case(alias_name)
2648        {
2649            return None;
2650        }
2651        let table = self.active_catalog().get(&from.primary.name)?;
2652        if table.schema().row_security {
2653            return None;
2654        }
2655        let cols = &table.schema().columns;
2656        let pos = cols
2657            .iter()
2658            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
2659        let out = alias.clone().unwrap_or_else(|| cols[pos].name.clone());
2660        Some((table, alias_name, pos, out))
2661    }
2662
2663    /// v7.39 (round 565) — would this statement be answered out of the
2664    /// index alone?
2665    ///
2666    /// EXPLAIN has to name the node the executor will actually run, and
2667    /// the only honest way to know is to ask the same two questions the
2668    /// executor asks: the statement's shape, and everything decidable
2669    /// about the scan before it walks. Neither is re-stated here.
2670    pub(crate) fn stmt_takes_index_only_scan(&self, stmt: &SelectStatement) -> bool {
2671        let Some((table, alias_name, pos, _)) = self.index_only_shape(stmt) else {
2672            return false;
2673        };
2674        let Some(where_) = stmt.where_.as_ref() else {
2675            return false;
2676        };
2677        crate::index_access::index_only_precheck(
2678            where_,
2679            &table.schema().columns,
2680            table,
2681            alias_name,
2682            pos,
2683            self.backslash_escapes,
2684        )
2685        .is_some()
2686    }
2687
2688    fn try_index_only_scan(
2689        &self,
2690        stmt: &SelectStatement,
2691    ) -> Result<Option<QueryResult>, EngineError> {
2692        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2693            return Ok(None);
2694        };
2695        // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2696        // are not materialised here, and a partition parent's own
2697        // heap/indexes are empty (its rows live in the children).
2698        if !stmt.ctes.is_empty() {
2699            return Ok(None);
2700        }
2701        if let Some(from) = &stmt.from
2702            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2703        {
2704            return Ok(None);
2705        }
2706        let where_ = stmt.where_.as_ref().expect("shape checked it");
2707        let cols = &table.schema().columns;
2708        let Some(values) = crate::index_access::try_index_only_range(
2709            where_,
2710            cols,
2711            table,
2712            alias_name,
2713            &self.current_snapshot(),
2714            pos,
2715            self.backslash_escapes,
2716        ) else {
2717            return Ok(None);
2718        };
2719        let schema = alloc::vec![ColumnSchema::new(
2720            out_name,
2721            cols[pos].ty,
2722            cols[pos].nullable
2723        )];
2724        Ok(Some(QueryResult::Rows {
2725            columns: schema,
2726            rows: values
2727                .into_iter()
2728                .map(|v| Row::new(alloc::vec![v]))
2729                .collect(),
2730        }))
2731    }
2732
2733    /// v7.39 (round 564) — the same scan, emitting each value instead of
2734    /// building a `Vec<Row>` for the encoder to walk once and drop.
2735    ///
2736    /// A profile of the server serving a 50k-row range put 10.2% of the
2737    /// connection thread's CPU on BUILDING that vector and another 9.7%
2738    /// on dropping it — a fifth of the query, spent allocating and
2739    /// freeing one single-element `Vec` per output row so that the wire
2740    /// encoder could borrow each value for a few nanoseconds. The
2741    /// streaming interface it then hands them to takes `&[Value]`
2742    /// already.
2743    ///
2744    /// Returns `None` when the shape does not apply, so the caller falls
2745    /// back before anything has been emitted.
2746    pub(crate) fn try_index_only_stream<F>(
2747        &self,
2748        stmt: &SelectStatement,
2749        emit: &mut F,
2750    ) -> Result<Option<usize>, EngineError>
2751    where
2752        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
2753    {
2754        let Some((table, alias_name, pos, out_name)) = self.index_only_shape(stmt) else {
2755            return Ok(None);
2756        };
2757        // r1058 — same declines as `try_exec_joined_streaming`: CTEs
2758        // are not materialised here, and a partition parent's own
2759        // heap/indexes are empty (its rows live in the children).
2760        if !stmt.ctes.is_empty() {
2761            return Ok(None);
2762        }
2763        if let Some(from) = &stmt.from
2764            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
2765        {
2766            return Ok(None);
2767        }
2768        let where_ = stmt.where_.as_ref().expect("shape checked it");
2769        let cols = &table.schema().columns;
2770        let schema = alloc::vec![ColumnSchema::new(
2771            out_name,
2772            cols[pos].ty,
2773            cols[pos].nullable
2774        )];
2775        let snapshot = self.current_snapshot();
2776        // The header goes out only once the walk has agreed to run — a
2777        // shape rejection after it would leave the client with a
2778        // RowDescription for a result that never comes.
2779        let mut wrote_header = false;
2780        let counted = crate::index_access::index_only_range_each(
2781            where_,
2782            cols,
2783            table,
2784            alias_name,
2785            &snapshot,
2786            pos,
2787            self.backslash_escapes,
2788            &mut |v: spg_storage::Value<'_>| {
2789                if !wrote_header {
2790                    emit(crate::StreamItem::Header(&schema))?;
2791                    wrote_header = true;
2792                }
2793                emit(crate::StreamItem::Row(crate::RowCells::Refs(&[&v])))
2794            },
2795        );
2796        match counted {
2797            None => Ok(None),
2798            Some(Err(e)) => Err(e),
2799            Some(Ok(n)) => {
2800                if !wrote_header {
2801                    emit(crate::StreamItem::Header(&schema))?;
2802                }
2803                Ok(Some(n))
2804            }
2805        }
2806    }
2807
2808    /// `DISTINCT ON`'s de-duplication, which runs after the inner
2809    /// SELECT has produced its rows.
2810    ///
2811    /// `#[inline(never)]` and out of `exec_select_cancel_as` for the
2812    /// reason round 848 established: a debug build gives every branch's
2813    /// locals a slot in the frame whichever branch runs, and this one is
2814    /// eighty lines of hashing, key slicing and survivor sorting that a
2815    /// statement without `DISTINCT ON` never touches. Round 867
2816    /// measured `exec_select_cancel_as` holding ~46 KB on a path that
2817    /// reaches none of it — the segment that had been blamed on
2818    /// `exec_bare_select_cancel`, which turned out to hold 2 KB.
2819    #[inline(never)]
2820    fn apply_distinct_on(
2821        &self,
2822        result: QueryResult,
2823        don_hidden: usize,
2824        don_limit: &(
2825            Option<spg_sql::ast::LimitExpr>,
2826            Option<spg_sql::ast::LimitExpr>,
2827        ),
2828        don_top1: usize,
2829        orig_order_by: &[spg_sql::ast::OrderBy],
2830    ) -> Result<QueryResult, EngineError> {
2831        let QueryResult::Rows { columns, rows } = result else {
2832            return Ok(result);
2833        };
2834        // The keys are the hidden trailing columns appended above.
2835        // v7.39 (round 729) — top-1 mode: the trailing columns are the
2836        // DON keys plus the ORDER tail; keep each group's best in one
2837        // hash pass, then sort the SURVIVORS with the original spec.
2838        let mut kept: alloc::vec::Vec<Row<'static>>;
2839        let key_start;
2840        if don_top1 > 0 {
2841            let tail = don_top1 - 1;
2842            key_start = columns.len().saturating_sub(don_hidden + tail);
2843            let ord_start = key_start + don_hidden;
2844            let tail_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by[don_hidden..]
2845                .iter()
2846                .map(|o| (o.desc, o.nulls_first))
2847                .collect();
2848            let mysql = self.backslash_escapes;
2849            let better = |a: &Row<'static>, b: &Row<'static>| -> bool {
2850                for (k, (desc, nf)) in tail_dirs.iter().enumerate() {
2851                    let av = a.values.get(ord_start + k).unwrap_or(&Value::Null);
2852                    let bv = b.values.get(ord_start + k).unwrap_or(&Value::Null);
2853                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2854                        core::cmp::Ordering::Less => return true,
2855                        core::cmp::Ordering::Greater => return false,
2856                        core::cmp::Ordering::Equal => {}
2857                    }
2858                }
2859                false
2860            };
2861            let mut slot: hashbrown::HashMap<String, usize> = hashbrown::HashMap::new();
2862            let mut best: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
2863            let mut keybuf = String::new();
2864            for row in rows {
2865                keybuf.clear();
2866                for v in row.values.get(key_start..ord_start).unwrap_or(&[]) {
2867                    aggregate::push_canonical_key(&mut keybuf, v);
2868                }
2869                match slot.get(keybuf.as_str()) {
2870                    Some(&i) => {
2871                        if better(&row, &best[i]) {
2872                            best[i] = row;
2873                        }
2874                    }
2875                    None => {
2876                        slot.insert(keybuf.clone(), best.len());
2877                        best.push(row);
2878                    }
2879                }
2880            }
2881            // Survivors sort with the FULL original spec (keys are still
2882            // aboard as hidden columns).
2883            let full_dirs: alloc::vec::Vec<(bool, Option<bool>)> = orig_order_by
2884                .iter()
2885                .map(|o| (o.desc, o.nulls_first))
2886                .collect();
2887            best.sort_by(|a, b| {
2888                for (k, (desc, nf)) in full_dirs.iter().enumerate() {
2889                    let av = a.values.get(key_start + k).unwrap_or(&Value::Null);
2890                    let bv = b.values.get(key_start + k).unwrap_or(&Value::Null);
2891                    match crate::order_by_value_cmp_in(*desc, *nf, av, bv, mysql) {
2892                        core::cmp::Ordering::Equal => {}
2893                        o => return o,
2894                    }
2895                }
2896                core::cmp::Ordering::Equal
2897            });
2898            for r in &mut best {
2899                r.values.truncate(key_start);
2900            }
2901            kept = best;
2902        } else {
2903            key_start = columns.len().saturating_sub(don_hidden);
2904            let mut seen: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
2905            kept = alloc::vec::Vec::new();
2906            for mut row in rows {
2907                let key: alloc::vec::Vec<Value<'static>> =
2908                    row.values.get(key_start..).unwrap_or(&[]).to_vec();
2909                if seen.iter().any(|k| k == &key) {
2910                    continue;
2911                }
2912                seen.push(key);
2913                row.values.truncate(key_start);
2914                kept.push(row);
2915            }
2916        }
2917        let mut columns = columns;
2918        columns.truncate(key_start);
2919        // PG limits what DISTINCT ON left, not what fed it.
2920        let kept = apply_deferred_limit(kept, don_limit);
2921        Ok(QueryResult::Rows {
2922            columns,
2923            rows: kept,
2924        })
2925    }
2926
2927    pub(crate) fn exec_select_cancel_as(
2928        &self,
2929        stmt: &SelectStatement,
2930        cancel: CancelToken<'_>,
2931        as_role: Option<&str>,
2932    ) -> Result<QueryResult, EngineError> {
2933        // v7.39 (round 763, F31-C1) — `SELECT *, count(*) … GROUP BY
2934        // <all columns>` is legal PG (the wildcard expands to grouped
2935        // columns); SPG refused the whole shape. Expand the wildcard
2936        // into explicit column refs up front — the aggregate layer's
2937        // existing "must appear in the GROUP BY clause" validation
2938        // then answers PG's sentence for any non-grouped column.
2939        if let Some(expanded) = self.expand_aggregate_wildcard(stmt) {
2940            return self.exec_select_cancel_as(&expanded, cancel, as_role);
2941        }
2942        // v7.39 (round 559) — `SELECT count(*) FROM t` without touching
2943        // a row.
2944        //
2945        // The aggregate layer already short-circuits this to
2946        // `rows.len()`, so the O(1) part was never the problem — the
2947        // cost is UPSTREAM, materialising every visible row so that
2948        // layer can take its length. Measured over pgwire on 500k rows:
2949        // PG18 8.2 ms with two parallel workers, 10.3 ms with
2950        // parallelism off, SPG 16.5 ms — 1.6x slower than a
2951        // single-threaded PG on the commonest aggregate there is, and no
2952        // ledger entry recorded it.
2953        //
2954        // Counting visible HEADERS needs no row at all. PG cannot do
2955        // this: its visibility lives in the heap tuples themselves, so
2956        // it has to read them (that is why its own count(*) is a full
2957        // scan, parallel or not).
2958        // v7.39 (read01 round 57) — the table-privilege gate on the common
2959        // read core. A superuser session returns from it immediately.
2960        // v7.39 (round 529) — resolve an ORDER BY that names an output
2961        // ALIAS. The statement-level pass never reached a SELECT nested in
2962        // a FROM clause, a CTE or a scalar subquery, so the same query
2963        // worked on its own and failed the moment anything wrapped it —
2964        // which is what generated SQL does constantly.
2965        let aliased;
2966        let stmt = if crate::orderby::order_by_names_an_alias(stmt) {
2967            let mut s = stmt.clone();
2968            crate::orderby::resolve_order_by_position(&mut s);
2969            aliased = s;
2970            &aliased
2971        } else {
2972            stmt
2973        };
2974        // v7.39 (round 529) — DISTINCT ON needs two things it did not have.
2975        //
2976        // Its keys were evaluated against the PROJECTED row, so a key that
2977        // is not in the select list — `SELECT DISTINCT ON (g) v FROM t
2978        // ORDER BY g, v DESC`, the canonical "latest row per group" — could
2979        // not be read at all and the query failed. PG evaluates them on the
2980        // input. They are projected as hidden columns here and stripped
2981        // again below, the same way the grouping-set ordering columns
2982        // already travel.
2983        //
2984        // And the dedup ran AFTER the inner statement's LIMIT, so
2985        // `… DISTINCT ON (g) … LIMIT 2` on four rows answered ONE row where
2986        // PG answers two: the limit had already taken two rows of the same
2987        // group before anything deduplicated them. A paginated DISTINCT ON
2988        // returned short pages, with no error. The limit is deferred to
2989        // after the dedup, which is PG's order.
2990        let don_stmt;
2991        // v7.39 (round 729) — the top-1 consumer needs the ORIGINAL
2992        // order spec (the rewritten stmt's is emptied).
2993        let orig_order_by = stmt.order_by.clone();
2994        let (stmt, don_hidden, don_limit, don_top1) = if stmt.distinct_on.is_empty() {
2995            (stmt, 0, (None, None), 0usize)
2996        } else {
2997            let mut s = stmt.clone();
2998            let hidden = s.distinct_on.len();
2999            for (i, e) in stmt.distinct_on.iter().enumerate() {
3000                s.items.push(SelectItem::Expr {
3001                    expr: e.clone(),
3002                    alias: Some(alloc::format!("__distinct_on_{i}")),
3003                });
3004            }
3005            // v7.39 (round 729) — group-top-1 short circuit. When the
3006            // DISTINCT ON keys are exactly the ORDER BY's leading keys,
3007            // the answer is "per group, the row that wins the remaining
3008            // order" — a single O(n) hash pass. The old path sorted the
3009            // ENTIRE input first (500k rows, ~180 ms on the panel cell)
3010            // to keep 100. The inner query runs UNSORTED with every
3011            // order key appended as a hidden column; the dedup below
3012            // keeps each group's best, then sorts the SURVIVORS.
3013            // Declared-collation order keys stay on the sorting path
3014            // (the value comparator here is collation-blind).
3015            let prefix_matches = s.order_by.len() >= hidden
3016                && stmt
3017                    .distinct_on
3018                    .iter()
3019                    .zip(s.order_by.iter())
3020                    .all(|(d, o)| *d == o.expr && !o.desc && o.nulls_first.is_none());
3021            let colls_plain =
3022                crate::orderby::order_by_collations(&s.order_by, &self.ev_ctx(&[], None))
3023                    .map(|cs| cs.iter().all(Option::is_none))
3024                    .unwrap_or(false);
3025            let top1_tail = if prefix_matches && colls_plain && s.group_by.is_none() {
3026                let tail = s.order_by.len() - hidden;
3027                for (j, o) in s.order_by[hidden..].iter().enumerate() {
3028                    s.items.push(SelectItem::Expr {
3029                        expr: o.expr.clone(),
3030                        alias: Some(alloc::format!("__don_ord_{j}")),
3031                    });
3032                }
3033                // Carry the tail's direction flags through the aliases'
3034                // ORDER; the survivors re-sort below with the full spec.
3035                s.order_by = Vec::new();
3036                tail + 1 // sentinel: 1 + number of tail keys (0 tail is still active)
3037            } else {
3038                0
3039            };
3040            // Only a folded literal is deferred; a placeholder or an
3041            // expression keeps the path it has today rather than being
3042            // resolved a second way here.
3043            let deferrable = matches!(
3044                (&s.limit, &s.offset),
3045                (
3046                    None | Some(spg_sql::ast::LimitExpr::Literal(_)),
3047                    None | Some(spg_sql::ast::LimitExpr::Literal(_))
3048                )
3049            );
3050            let deferred = if deferrable {
3051                (s.limit.take(), s.offset.take())
3052            } else {
3053                (None, None)
3054            };
3055            don_stmt = s;
3056            (&don_stmt, hidden, deferred, top1_tail)
3057        };
3058        self.acl_check_select_as(stmt, as_role)?;
3059        validate_aggregate_placement(stmt)?;
3060        // v7.39 (round 559) — the bare `count(*)` fast path, AFTER the
3061        // privilege gate above. Placed before it at first, and the
3062        // security-definer e2e caught it immediately: a SECURITY INVOKER
3063        // function whose body is `SELECT count(*) FROM t` answered
3064        // instead of being refused, because the fast path never reached
3065        // the check.
3066        if let Some(r) = self.try_bare_count_star(stmt, as_role)? {
3067            return Ok(r);
3068        }
3069        // v7.39 (round 560) — an index-only range scan. Same placement
3070        // reasoning as the count above: after the privilege gate.
3071        if let Some(r) = self.try_index_only_scan(stmt)? {
3072            return Ok(r);
3073        }
3074        validate_locking_clause(stmt)?;
3075        let result = self.exec_select_cancel_inner(stmt, cancel)?;
3076        // v7.39 (round 135) — drop the synthetic `__grp_ord_*` ordering columns
3077        // the parser injects for GROUPING() in ORDER BY on a grouping-set query.
3078        // They carry the per-branch mask through the UNION-ALL sort and must not
3079        // appear in the output. Stripped per SELECT level (grouping-set queries
3080        // are often wrapped in a derived subquery), before DISTINCT ON.
3081        let result = strip_synthetic_order_cols(result);
3082        // v7.37.17 (17.6 siblings) — `SELECT DISTINCT ON (exprs)`:
3083        // rows arrive here already ORDER BY'd; keep the FIRST row of
3084        // each group the expressions define (PG semantics). The
3085        // expressions evaluate against the projected schema — an
3086        // expression that isn't in the select list errors honestly.
3087        if stmt.distinct_on.is_empty() {
3088            return Ok(result);
3089        }
3090        self.apply_distinct_on(result, don_hidden, &don_limit, don_top1, &orig_order_by)
3091    }
3092
3093    /// The UNION chain: execute the head as a bare block, then fold each
3094    /// peer in with left-associative dedup.
3095    ///
3096    /// `#[inline(never)]` and out of `exec_select_cancel_inner` for the
3097    /// reason round 848 established. A statement with no unions returns
3098    /// one line above the call — and every nested subquery on a deep
3099    /// path is such a statement, so each level of the recursion carried
3100    /// 170 lines of locals it could not reach. Round 867 measured that
3101    /// frame at 34,800 bytes, the largest single one on the descent,
3102    /// after two earlier attributions had blamed its caller and then its
3103    /// callee: the gap between two marks is the frame of everything
3104    /// BETWEEN them, and this function had no mark of its own.
3105    #[inline(never)]
3106    fn exec_union_chain(
3107        &self,
3108        stmt_ref: &SelectStatement,
3109        stmt: &SelectStatement,
3110        cancel: CancelToken<'_>,
3111    ) -> Result<QueryResult, EngineError> {
3112        // UNION path: clone-strip the head into a bare block (its own
3113        // DISTINCT and any inner ORDER BY are dropped by parser rule —
3114        // the wrapper SelectStatement carries them), execute, then chain
3115        // peers with left-associative dedup semantics.
3116        // v7.39 (round 232) — the wrapper's ORDER BY addresses the head's
3117        // output columns; a position past their count is PG's 42P10.
3118        crate::orderby::check_order_by_positions(stmt_ref)?;
3119        let mut head_unknown = branch_unknown_mask(stmt_ref);
3120        let head_regcast = branch_regcast_mask(stmt_ref);
3121        let mut head = stmt_ref.clone();
3122        head.unions = Vec::new();
3123        head.order_by = Vec::new();
3124        head.limit = None;
3125        let QueryResult::Rows {
3126            mut columns,
3127            mut rows,
3128        } = self.exec_bare_select_cancel(&head, cancel)?
3129        else {
3130            unreachable!("bare SELECT cannot return CommandOk")
3131        };
3132        for (kind, peer) in &stmt_ref.unions {
3133            // v7.37.17 (17.6 siblings) — a peer carrying its own
3134            // unions is a nested INTERSECT group (the parser's
3135            // precedence regrouping); recurse through the
3136            // union-aware wrapper for it.
3137            let peer_result = if peer.unions.is_empty() {
3138                self.exec_bare_select_cancel(peer, cancel)?
3139            } else {
3140                self.exec_select_cancel(peer, cancel)?
3141            };
3142            let QueryResult::Rows {
3143                columns: peer_cols,
3144                rows: mut peer_rows,
3145            } = peer_result
3146            else {
3147                unreachable!("bare SELECT cannot return CommandOk")
3148            };
3149            if peer_cols.len() != columns.len() {
3150                // v7.39 (round 232) — PG's wording, which clients match on.
3151                return Err(EngineError::Unsupported(alloc::format!(
3152                    "each {} query must have the same number of columns",
3153                    set_op_name(*kind)
3154                )));
3155            }
3156            // v7.39 (round 232+233) — PG resolves each result column to one
3157            // type before it merges anything, and refuses the query when the
3158            // two branches have no common type. SPG's unifier
3159            // (`unify_union_columns`) is value-driven and deliberately
3160            // conservative — "a column where any cell fails to coerce is left
3161            // exactly as it was" — so a mismatch produced a column holding
3162            // BOTH types (`SELECT a, b FROM t UNION SELECT b, a FROM t` came
3163            // back with integers and text interleaved) instead of an error.
3164            //
3165            // The check has to read the branch ASTs, not just their schemas:
3166            // SPG has no `Unknown` DataType, so a bare `'a'` literal describes
3167            // as TEXT and is indistinguishable from a real text column by
3168            // schema alone — yet PG treats the two completely differently
3169            // (`SELECT 1 UNION SELECT 'a'` is an input-syntax error on the
3170            // literal, `SELECT 1 UNION SELECT 'a'::text` is a type mismatch).
3171            let peer_unknown = branch_unknown_mask(peer);
3172            let peer_regcast = branch_regcast_mask(peer);
3173            for i in 0..columns.len() {
3174                let hu = head_unknown.get(i).copied().unwrap_or(false);
3175                let pu = peer_unknown.get(i).copied().unwrap_or(false);
3176                let (ht, pt) = (columns[i].ty, peer_cols[i].ty);
3177                let reg_dual = peer_regcast.get(i).copied().unwrap_or(false)
3178                    || head_regcast.get(i).copied().unwrap_or(false);
3179                match (hu, pu) {
3180                    // Both sides carry a real type: they must share a category.
3181                    (false, false) => {
3182                        if !reg_dual && !crate::conversions::types_unify(ht, pt) {
3183                            return Err(EngineError::Unsupported(alloc::format!(
3184                                "{} types {} and {} cannot be matched",
3185                                set_op_name(*kind),
3186                                crate::conversions::pg_type_name_for_error(ht),
3187                                crate::conversions::pg_type_name_for_error(pt),
3188                            )));
3189                        }
3190                    }
3191                    // One side is an untyped literal: it takes the other's
3192                    // type, and failing to convert is the error PG reports.
3193                    (true, false) => {
3194                        coerce_branch_column(&mut rows, i, pt, &columns[i].name)?;
3195                        columns[i].ty = pt;
3196                        head_unknown[i] = false;
3197                    }
3198                    (false, true) => {
3199                        coerce_branch_column(&mut peer_rows, i, ht, &columns[i].name)?;
3200                    }
3201                    // Both untyped — nothing to resolve against yet.
3202                    (true, true) => {}
3203                }
3204            }
3205            // v7.37 D.26 — a UNION result column is nullable when ANY branch is
3206            // nullable (PG semantics). Previously the result kept only the head's
3207            // nullability, so `VALUES (1),(NULL)` (a UNION-ALL chain seeded by the
3208            // non-null `1`) wrongly reported the column NOT NULL, which let
3209            // `count(col)`'s NOT-NULL fast-path count the NULL row.
3210            for (i, pc) in peer_cols.iter().enumerate() {
3211                if pc.nullable {
3212                    columns[i].nullable = true;
3213                }
3214            }
3215            // v7.39 (round 410) — under MySQL, set-op dedup / matching folds
3216            // text by the session collation (CI + accent + PAD SPACE), like
3217            // GROUP BY. PG stays byte-exact.
3218            let mysql = self.backslash_escapes;
3219            // v7.38.14 — the mask, which 7.38.13 recorded as impossible here
3220            // and was wrong about. `columns` and `peer_cols` are both in
3221            // scope; what was actually missing is that the branches' output
3222            // schemas did not CARRY the collation, so a mask built from them
3223            // would have marked every column byte-wise. Unifying the
3224            // projection-to-schema conversion fixed the supply side, and the
3225            // mask is now buildable from what was always there.
3226            //
3227            // Either side byte-wise keeps the position byte-wise, mirroring
3228            // `eval::resolve::mysql_text_fold_applies`: a set operation
3229            // between a folding column and a declared-binary one must not
3230            // quietly fold the binary one's values away.
3231            let set_mask: alloc::vec::Vec<bool> = columns
3232                .iter()
3233                .zip(peer_cols.iter())
3234                .map(|(l, r)| {
3235                    matches!(l.collation, spg_storage::Collation::Binary)
3236                        || matches!(r.collation, spg_storage::Collation::Binary)
3237                })
3238                .collect();
3239            let fold = FoldSpec::of(mysql, &set_mask);
3240            match kind {
3241                UnionKind::All => rows.extend(peer_rows),
3242                UnionKind::Distinct => {
3243                    rows.extend(peer_rows);
3244                    rows = dedup_rows(rows, fold);
3245                }
3246                // v7.37.17 (17.6 siblings) — PG set semantics.
3247                // v7.39 (round 591) — all four ask the same question of the
3248                // right side, and all four used to answer it by scanning it
3249                // once per left row. `PeerIndex` buckets it by the hash
3250                // DISTINCT already uses, so the answer is a lookup.
3251                // INTERSECT: distinct rows present on both sides.
3252                UnionKind::Intersect => {
3253                    let idx = PeerIndex::build(&peer_rows, fold);
3254                    rows = dedup_rows(rows, fold)
3255                        .into_iter()
3256                        .filter(|r| idx.contains(r))
3257                        .collect();
3258                }
3259                // INTERSECT ALL: multiset intersection — each row
3260                // keeps min(left count, right count) occurrences.
3261                UnionKind::IntersectAll => {
3262                    let mut idx = PeerIndex::build(&peer_rows, fold);
3263                    let mut kept: Vec<Row<'static>> = Vec::new();
3264                    for r in rows {
3265                        if idx.take_one(&r) {
3266                            kept.push(r);
3267                        }
3268                    }
3269                    rows = kept;
3270                }
3271                // EXCEPT: distinct left rows absent from the right.
3272                UnionKind::Except => {
3273                    let idx = PeerIndex::build(&peer_rows, fold);
3274                    rows = dedup_rows(rows, fold)
3275                        .into_iter()
3276                        .filter(|r| !idx.contains(r))
3277                        .collect();
3278                }
3279                // EXCEPT ALL: multiset subtraction — each right
3280                // occurrence cancels one left occurrence.
3281                UnionKind::ExceptAll => {
3282                    let mut idx = PeerIndex::build(&peer_rows, fold);
3283                    let mut kept: Vec<Row<'static>> = Vec::new();
3284                    for r in rows {
3285                        if !idx.take_one(&r) {
3286                            kept.push(r);
3287                        }
3288                    }
3289                    rows = kept;
3290                }
3291            }
3292        }
3293        // PG resolves a UNION / VALUES result column to one common type
3294        // and casts every branch to it (`SELECT '2020-01-01'::date UNION
3295        // ALL SELECT '2020-01-02'` → both DATE, not DATE + TEXT). SPG
3296        // built each branch independently, leaving mixed-type columns
3297        // that broke ORDER BY, comparisons, and value-based window
3298        // frames. Unify + coerce before the combined ORDER BY sees them.
3299        unify_union_columns(&mut columns, &mut rows);
3300        // ORDER BY at the top of a UNION applies to the combined result.
3301        // Eval against the projected schema (NOT the source table).
3302        if !stmt.order_by.is_empty() {
3303            // v7.39 (read01 round 54) — the combined-result ctx must carry the
3304            // catalog, and the projected columns must keep their enum identity
3305            // (`user_enum_type`), or `ORDER BY <enum col>` over a UNION sorts
3306            // by TEXT instead of member order — silently wrong rows, not an
3307            // error. (Same shape as the enum-order knife's GROUP BY fix.)
3308            let synth_ctx = EvalContext::new(&columns, None).with_catalog(self.active_catalog());
3309            // v7.37.17 (17.6 siblings) — positional keys (ORDER BY 1)
3310            // survive to here when the head projects a Wildcard (the
3311            // group-tail wrapper shape): map them onto the Nth
3312            // projected column so the combined sort works.
3313            let resolved_order: Vec<spg_sql::ast::OrderBy> = stmt
3314                .order_by
3315                .iter()
3316                .map(|o| {
3317                    let mut o = o.clone();
3318                    if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
3319                        && *n >= 1
3320                        && let Ok(idx) = usize::try_from(*n - 1)
3321                        && idx < columns.len()
3322                    {
3323                        o.expr = Expr::Column(spg_sql::ast::ColumnName {
3324                            qualifier: None,
3325                            name: columns[idx].name.clone(),
3326                        });
3327                    }
3328                    o
3329                })
3330                .collect();
3331            let descs: Vec<bool> = resolved_order.iter().map(|o| o.desc).collect();
3332            let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
3333            for r in rows {
3334                let keys = build_order_keys(&resolved_order, &r, &synth_ctx)?;
3335                tagged.push((keys, r));
3336            }
3337            sort_by_keys(&mut tagged, &descs);
3338            rows = tagged.into_iter().map(|(_, r)| r).collect();
3339        }
3340        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
3341        Ok(QueryResult::Rows { columns, rows })
3342    }
3343
3344    fn exec_select_cancel_inner(
3345        &self,
3346        stmt: &SelectStatement,
3347        cancel: CancelToken<'_>,
3348    ) -> Result<QueryResult, EngineError> {
3349        cancel.check()?;
3350        // v7.38 P0 元机制 A — first observable point inside the
3351        // planner / executor. Tests use this to inject a delay or
3352        // a cancellation race before any row is produced. Release
3353        // build expands to `let _ = (...);` — zero cost.
3354        crate::injection_point!("planner_first_row_fetch", &stmt.from);
3355        // v7.39 (round 705) — WINDOW-clause definitions nothing referenced.
3356        // PG analyses every definition, referenced or not, so `SELECT i FROM
3357        // t WINDOW w AS (ORDER BY nosuch)` fails there and silently
3358        // succeeded here (the parser used to drop the unreferenced defs
3359        // whole). The check is the CREATE VIEW check's shape (round 700): a
3360        // LIMIT-0 run of the same FROM with the definitions' key
3361        // expressions as the projection — it cannot disagree with what a
3362        // referencing window would have done, because it resolves the same
3363        // names the same way. Zero cost for the ordinary statement: the
3364        // list is empty unless a WINDOW clause left unreferenced defs.
3365        if !stmt.window_check_exprs.is_empty() {
3366            let mut probe = stmt.clone();
3367            probe.items = stmt
3368                .window_check_exprs
3369                .iter()
3370                .map(|e| spg_sql::ast::SelectItem::Expr {
3371                    expr: e.clone(),
3372                    alias: None,
3373                })
3374                .collect();
3375            probe.window_check_exprs = Vec::new();
3376            probe.distinct = false;
3377            probe.distinct_on = Vec::new();
3378            probe.group_by = None;
3379            probe.group_by_all = false;
3380            probe.having = None;
3381            probe.unions = Vec::new();
3382            probe.order_by = Vec::new();
3383            probe.locking = None;
3384            probe.limit = Some(spg_sql::ast::LimitExpr::Literal(0));
3385            probe.offset = None;
3386            probe.limit_with_ties = false;
3387            self.exec_select_cancel_inner(&probe, cancel)?;
3388        }
3389        // v7.39 (read01 round 74) — lower `(f(args)).*`. Naming a record's fields
3390        // takes the catalog, so the parser leaves a marker and the rewrite lands
3391        // here: the call moves into a LATERAL FROM item and the item becomes one
3392        // reference per declared column. `SELECT 'p', (rows_of(2)).*` is
3393        // `SELECT 'p', __rec.id, __rec.v FROM rows_of(2) AS __rec` — reusing the
3394        // set-returning FROM machinery of rounds 65 and 69 rather than growing a
3395        // second one.
3396        if let Some(lowered) = self.lower_record_expansion(stmt)? {
3397            return self.exec_select_cancel_inner(&lowered, cancel);
3398        }
3399        // v7.17.0 Phase 1.2 — user-defined VIEW expansion. If the
3400        // FROM / JOIN graph references any catalogued view name,
3401        // re-parse the view body and prepend it as a synthetic
3402        // CTE. Recurses on views-in-views via the regular CTE
3403        // dispatch below. Fast-path: skip the walker entirely when
3404        // the catalog has no views (the typical OLTP load).
3405        if !self.active_catalog().views_all().is_empty() {
3406            if let Some(rewritten) = self.expand_views_in_select(stmt)? {
3407                return self.exec_select_cancel(&rewritten, cancel);
3408            }
3409        }
3410        // v7.37.6-B(sentori Epic 2 P0)— `SELECT … FROM <partition-parent>`
3411        // gets rewritten to a UNION-ALL over the children that overlap
3412        // the WHERE-derived key range. Uses the same CTE-injection
3413        // trick as VIEW expansion above so downstream resolution
3414        // doesn't need a partition-aware code path.
3415        if let Some(rewritten) = self.expand_partition_parents_in_select(stmt)? {
3416            return self.exec_select_cancel(&rewritten, cancel);
3417        }
3418        // v7.16.2 — information_schema / pg_catalog virtual
3419        // views (mailrs round-10 A.3). If the SELECT touches a
3420        // synthetic meta-table name (`__spg_info_*` /
3421        // `__spg_pg_*` — produced by the parser for
3422        // `information_schema.X` / `pg_catalog.X`), clone the
3423        // catalog, materialise the requested view as a real
3424        // temporary table, and re-execute against an enriched
3425        // engine. Same pattern as `exec_with_ctes` for CTEs.
3426        if !self.meta_views_materialised && select_references_meta_view(stmt) {
3427            return self.exec_select_with_meta_views(stmt, cancel);
3428        }
3429        // v6.10.2 — cold-tier time-travel short-circuit. When the
3430        // primary TableRef carries `AS OF SEGMENT '<id>'`, run a
3431        // dedicated cold-segment scan instead of the regular
3432        // hot+index path. The scope is intentionally narrow for
3433        // v6.10.2 — bare `SELECT * FROM <t> AS OF SEGMENT 'id'`,
3434        // optionally with a single-column-equality WHERE. JOINs /
3435        // aggregates / ORDER BY / subqueries on top of a time-
3436        // travelled scan are STABILITY § "Out of v6.10".
3437        if let Some(from) = &stmt.from
3438            && let Some(seg_id) = from.primary.as_of_segment
3439        {
3440            return self.exec_select_as_of_segment(stmt, from, seg_id);
3441        }
3442        // v6.2.0 / v6.5.0 — virtual-table short-circuits. Detected
3443        // pre-CTE because they don't read from the catalog and
3444        // shouldn't participate in regular FROM resolution.
3445        // v6.2.0 / v6.5.0 / v7.38 (read01 P3.NEW3) — virtual-table
3446        // short-circuits. A meta-view FROM materialises to a fixed row
3447        // set. For a bare `SELECT *` we return it directly; otherwise we
3448        // stage it as a temp table and run the normal pipeline, so
3449        // projection / WHERE / ORDER BY / aggregates work over these views
3450        // (they were `SELECT *`-only before). A real table shadowing the
3451        // name wins (checked first), which also stops the staged re-run
3452        // from recursing back into meta-view detection.
3453        if let Some(from) = &stmt.from
3454            && from.joins.is_empty()
3455            && self.active_catalog().get(&from.primary.name).is_none()
3456        {
3457            let lower = from.primary.name.to_ascii_lowercase();
3458            if let Some(result) = self.meta_view_result(&lower) {
3459                let bare = stmt.where_.is_none()
3460                    && stmt.group_by.is_none()
3461                    && stmt.having.is_none()
3462                    && stmt.unions.is_empty()
3463                    && stmt.order_by.is_empty()
3464                    && stmt.limit.is_none()
3465                    && stmt.offset.is_none()
3466                    && !stmt.distinct
3467                    && stmt.items.iter().all(|i| matches!(i, SelectItem::Wildcard));
3468                if bare {
3469                    return Ok(result);
3470                }
3471                if let QueryResult::Rows { columns, rows } = result {
3472                    let mut catalog = self.active_catalog().clone();
3473                    let cols = infer_column_types(&columns, &rows);
3474                    let schema = TableSchema::new(from.primary.name.clone(), cols);
3475                    catalog.create_table(schema).map_err(EngineError::Storage)?;
3476                    let t = catalog
3477                        .get_mut(&from.primary.name)
3478                        .expect("just-created meta-view table must exist");
3479                    for row in rows {
3480                        t.insert(row).map_err(EngineError::Storage)?;
3481                    }
3482                    let mut eng = Engine::restore(catalog);
3483                    if let Some(c) = self.clock {
3484                        eng = eng.with_clock(c);
3485                    }
3486                    if let Some(f) = self.salt_fn {
3487                        eng = eng.with_salt_fn(f);
3488                    }
3489                    // v7.39 (read01 pgstatfuncs.c) — carry the calling-
3490                    // connection identity so `WHERE pid = pg_backend_pid()`
3491                    // matches inside the staged meta-view run.
3492                    if let Some(f) = self.backend_pid_fn {
3493                        eng.set_backend_pid_fn(f);
3494                    }
3495                    return eng.exec_select_cancel(stmt, cancel);
3496                }
3497                return Ok(result);
3498            }
3499        }
3500        // v4.11: CTEs materialise into a temporary enriched catalog
3501        // *before* anything else — the body SELECT can then refer
3502        // to CTE names via the regular FROM-clause resolution.
3503        // Uncorrelated only: each CTE body runs once against the
3504        // current catalog, not against later CTEs' results (left-
3505        // to-right materialisation would relax this, but we keep
3506        // it simple for v4.11 MVP).
3507        if !stmt.ctes.is_empty() {
3508            return self.exec_with_ctes(stmt, cancel);
3509        }
3510        // v4.10: subqueries (uncorrelated) are resolved here, before
3511        // the executor sees the row loop. We clone the statement so
3512        // we can mutate without disturbing the caller's AST — most
3513        // queries pass through with no subquery nodes and the clone
3514        // is cheap; with subqueries the materialisation cost
3515        // dominates anyway.
3516        let mut stmt_owned;
3517        let stmt_ref: &SelectStatement = if expr_tree_has_subquery(stmt) {
3518            stmt_owned = stmt.clone();
3519            // v7.33 (mailrs 7.32.1) — sublink pull-up first: an
3520            // aggregate-wrapped correlated scalar subquery whose
3521            // correlation key is UNIQUE/PK becomes a LEFT JOIN, so the
3522            // executor streams one join instead of splicing a per-row
3523            // subplan. Runs before the per-row/batch resolver, which then
3524            // only sees the subqueries the pull-up left behind.
3525            self.pull_up_unique_correlated_agg_subqueries(&mut stmt_owned);
3526            // v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC pull-up) —
3527            // the "per-key latest" scalar subquery shape (inbox / feed
3528            // / timeline applications) becomes a CTE + LEFT JOIN
3529            // against a GROUP BY pre-aggregation that reuses the v7.33
3530            // first_ordered argmax executor. Runs AFTER unique-key
3531            // pull-up (so the unique-key fast path still wins for
3532            // single-PK lookups) and BEFORE the EXISTS sublink rewrite.
3533            // Phase 1 (this commit) is skeleton only — no-op pass.
3534            self.pull_up_correlated_limit_one_subqueries(&mut stmt_owned);
3535            // v7.34.2 (mailrs prod NOT EXISTS) — plan-time `[NOT] EXISTS`
3536            // sublink pull-up to semi/anti-join, before the resolver gets
3537            // a chance to walk per-row.
3538            self.pull_up_exists_sublinks(&mut stmt_owned);
3539            // v7.37.4 — if the LIMIT 1 pullup added CTEs, route through
3540            // exec_with_ctes so they materialise once before the body
3541            // SELECT runs. exec_with_ctes strips ctes from the body
3542            // clone, then re-enters select.
3543            if !stmt_owned.ctes.is_empty() {
3544                return self.exec_with_ctes(&stmt_owned, cancel);
3545            }
3546            // v7.37.x (docker-fair INSUBQ attack) — short-circuit
3547            //   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
3548            // BEFORE `resolve_select_subqueries` materialises the inner
3549            // result as `Vec<Expr::Literal>` (~150 µs for the 6 k-row
3550            // INSUBQ benchmark). Run the inner once, collect the result
3551            // values into a `HashSet<i64>` directly, then probe A.pk per
3552            // value and tally. Returns `Some` when the shape matches.
3553            if let Some(out) = self.try_count_star_pk_in_subquery_fast(&stmt_owned, cancel)? {
3554                return Ok(out);
3555            }
3556            self.resolve_select_subqueries(&mut stmt_owned, cancel)?;
3557            &stmt_owned
3558        } else {
3559            stmt
3560        };
3561        if stmt_ref.unions.is_empty() {
3562            return self.exec_bare_select_cancel(stmt_ref, cancel);
3563        }
3564        self.exec_union_chain(stmt_ref, stmt, cancel)
3565    }
3566
3567    #[allow(clippy::too_many_lines)]
3568    #[allow(clippy::too_many_lines)] // huge match — splitting fragments the planner
3569    /// v7.11.7 — execute `SELECT … FROM unnest(expr) [AS] alias …`.
3570    /// Synthesises a single-column virtual table whose column type
3571    /// is TEXT and whose rows are the array elements. Routes
3572    /// through the regular projection / WHERE / ORDER BY / LIMIT
3573    /// machinery so set-returning UNNEST composes naturally with
3574    /// the rest of the SELECT surface.
3575    fn exec_select_unnest(
3576        &self,
3577        stmt: &SelectStatement,
3578        primary: &TableRef,
3579        cancel: CancelToken<'_>,
3580    ) -> Result<QueryResult, EngineError> {
3581        let expr = primary
3582            .unnest_expr
3583            .as_deref()
3584            .expect("caller guards unnest_expr.is_some()");
3585        // Multi-arg unnest(a, b, …) — parallel zip, NULL-padded.
3586        // N value columns instead of one; the shared builder does
3587        // the work and the tail below (WHERE / agg / projection)
3588        // runs against the wider schema.
3589        let multi: Option<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>)> =
3590            match unnest_zip_args(expr) {
3591                Some(args) => Some(unnest_zip_rows(args)?),
3592                None => None,
3593            };
3594        // Evaluate the array expression once. Empty schema / empty
3595        // row — uncorrelated UNNEST cannot reference outer columns.
3596        // v7.39 (read01 round 49) — the ctx must carry the catalog: the enum
3597        // introspection family (enum_range / enum_first / enum_last) resolves
3598        // its labels from the argument's STATIC enum type against the
3599        // catalog's enum registry. Without it `unnest(enum_range(NULL::mood))`
3600        // fell through to the generic arm, got NULL, and expanded to zero rows
3601        // — while the bare `SELECT enum_range(NULL::mood)` (whose ctx does
3602        // carry the catalog) worked.
3603        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
3604        let ctx = EvalContext::new(&empty_schema, None).with_catalog(self.active_catalog());
3605        let dummy_row = Row::new(alloc::vec::Vec::new());
3606        // v7.11.13 — unnest dispatches per array element type so
3607        // INT[] / BIGINT[] surface their PG types in projection.
3608        // v7.39 (round 758, F31-B8a) — the composite SRF names its own
3609        // columns (PG: lexeme | positions | weights); everything else
3610        // keeps the alias / "unnest" defaults below.
3611        let mut composite_names: Option<&[&str]> = None;
3612        let (dtypes, rows): (alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>) =
3613            if let Some(m) = multi {
3614                m
3615            } else {
3616                // v7.39 (round 236) — flatten a multidimensional array into
3617                // its row-major elements (PG) before the 1-D-only match.
3618                let unnest_src = {
3619                    let v = eval::eval_expr(expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
3620                    crate::eval::values::flatten_2d(&v).unwrap_or(v)
3621                };
3622                let mut return_multi: Option<(
3623                    alloc::vec::Vec<DataType>,
3624                    alloc::vec::Vec<Row<'static>>,
3625                )> = None;
3626                let (elem_dtype, rows): (DataType, alloc::vec::Vec<Row<'static>>) = match unnest_src
3627                {
3628                    Value::Null => (DataType::Text, alloc::vec::Vec::new()),
3629                    Value::TextArray(items) => {
3630                        let rows = items
3631                            .into_iter()
3632                            .map(|item| {
3633                                Row::new(alloc::vec![match item {
3634                                    Some(s) => Value::text(s),
3635                                    None => Value::Null,
3636                                }])
3637                            })
3638                            .collect();
3639                        (DataType::Text, rows)
3640                    }
3641                    Value::IntArray(items) => {
3642                        let rows = items
3643                            .into_iter()
3644                            .map(|item| {
3645                                Row::new(alloc::vec![match item {
3646                                    Some(n) => Value::Int(n),
3647                                    None => Value::Null,
3648                                }])
3649                            })
3650                            .collect();
3651                        (DataType::Int, rows)
3652                    }
3653                    Value::BigIntArray(items) => {
3654                        let rows = items
3655                            .into_iter()
3656                            .map(|item| {
3657                                Row::new(alloc::vec![match item {
3658                                    Some(n) => Value::BigInt(n),
3659                                    None => Value::Null,
3660                                }])
3661                            })
3662                            .collect();
3663                        (DataType::BigInt, rows)
3664                    }
3665                    Value::Multirange { kind, ranges } => {
3666                        let rows = ranges
3667                            .iter()
3668                            .map(|sp| {
3669                                Row::new(alloc::vec![Value::Range {
3670                                    kind,
3671                                    lower: sp.lower.clone(),
3672                                    upper: sp.upper.clone(),
3673                                    lower_inc: sp.lower_inc,
3674                                    upper_inc: sp.upper_inc,
3675                                    empty: false,
3676                                }])
3677                            })
3678                            .collect();
3679                        (DataType::Range(kind), rows)
3680                    }
3681                    // v7.39 (round 758, F31-B8a) — unnest(tsvector):
3682                    // one row per lexeme, PG18-measured columns
3683                    // lexeme | positions | weights (`a | {1,3} |
3684                    // {D,D}`); a position-less lexeme (a stripped
3685                    // vector) reads NULL in both array columns.
3686                    Value::TsVector(lexemes) => {
3687                        composite_names = Some(&["lexeme", "positions", "weights"]);
3688                        let rows = lexemes
3689                            .iter()
3690                            .map(|l| {
3691                                let (pos, wts) = if l.positions.is_empty() {
3692                                    (Value::Null, Value::Null)
3693                                } else {
3694                                    let letter = match l.weight {
3695                                        3 => "A",
3696                                        2 => "B",
3697                                        1 => "C",
3698                                        _ => "D",
3699                                    };
3700                                    (
3701                                        Value::SmallIntArray(
3702                                            l.positions
3703                                                .iter()
3704                                                .map(|p| {
3705                                                    Some(i16::try_from(*p).unwrap_or(i16::MAX))
3706                                                })
3707                                                .collect(),
3708                                        ),
3709                                        Value::TextArray(
3710                                            l.positions
3711                                                .iter()
3712                                                .map(|_| Some(letter.into()))
3713                                                .collect(),
3714                                        ),
3715                                    )
3716                                };
3717                                Row::new(alloc::vec![Value::text(l.word.clone()), pos, wts])
3718                            })
3719                            .collect();
3720                        return_multi = Some((
3721                            alloc::vec![
3722                                DataType::Text,
3723                                DataType::SmallIntArray,
3724                                DataType::TextArray
3725                            ],
3726                            rows,
3727                        ));
3728                        (DataType::Text, alloc::vec::Vec::new())
3729                    }
3730                    other => {
3731                        // v7.39 (round 622, S05a) — see table_access.rs:
3732                        // the same sentence, and it is a type mismatch.
3733                        return Err(EngineError::Eval(EvalError::TypeMismatch {
3734                            detail: alloc::format!(
3735                                "unnest() expects an array argument, got {}",
3736                                crate::conversions::pg_type_name_for_error_opt(other.data_type())
3737                            ),
3738                        }));
3739                    }
3740                };
3741                if let Some(m) = return_multi {
3742                    m
3743                } else {
3744                    (alloc::vec![elem_dtype], rows)
3745                }
3746            };
3747        let alias = primary
3748            .alias
3749            .clone()
3750            .unwrap_or_else(|| "unnest".to_string());
3751        // v7.13.2 — mailrs round-6 S5. Honour PG-standard
3752        // `UNNEST(arr) AS p(col_name)` column-list aliasing:
3753        // entries map positionally over the value columns. Without
3754        // the column list, a single column falls back to the table
3755        // alias (pre-v7.13.2 behaviour); multi-arg columns default
3756        // to PG's `unnest`.
3757        let n_vals = dtypes.len();
3758        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = dtypes
3759            .iter()
3760            .enumerate()
3761            .map(|(i, dt)| {
3762                let name = primary
3763                    .unnest_column_aliases
3764                    .get(i)
3765                    .cloned()
3766                    .unwrap_or_else(|| {
3767                        if let Some(names) = composite_names {
3768                            names
3769                                .get(i)
3770                                .map_or_else(|| "unnest".to_string(), |n| (*n).to_string())
3771                        } else if n_vals == 1 {
3772                            alias.clone()
3773                        } else {
3774                            "unnest".to_string()
3775                        }
3776                    });
3777                ColumnSchema::new(name, *dt, true)
3778            })
3779            .collect();
3780        // v7.39 (read01 round 78) — the item's row type IS this scalar when the
3781        // parser desugared a base-type-returning function here (see
3782        // TableRef::scalar_fn_item); the marker rides the column so it survives
3783        // every EvalContext an inner stage rebuilds.
3784        if primary.scalar_fn_item && schema_cols.len() == 1 {
3785            schema_cols[0].scalar_row_source = true;
3786        }
3787        // WITH ORDINALITY — trailing BIGINT counting rows from 1
3788        // in element order. The alias entry after the value
3789        // columns renames it (PG default: `ordinality`).
3790        let rows = if primary.with_ordinality {
3791            let ord_name = primary
3792                .unnest_column_aliases
3793                .get(n_vals)
3794                .cloned()
3795                .unwrap_or_else(|| "ordinality".to_string());
3796            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
3797            rows.into_iter()
3798                .enumerate()
3799                .map(|(i, row)| {
3800                    let mut vals = row.values.clone();
3801                    vals.push(Value::BigInt(i as i64 + 1));
3802                    Row::new(vals)
3803                })
3804                .collect()
3805        } else {
3806            rows
3807        };
3808        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
3809        // `EvalContext::new` drops it and every catalog-dependent cast
3810        // (regclass / enum / composite / domain) silently degrades.
3811        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
3812        // Apply WHERE.
3813        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
3814            let mut out = alloc::vec::Vec::with_capacity(rows.len());
3815            for row in rows {
3816                cancel.check()?;
3817                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
3818                if matches!(v, Value::Bool(true)) {
3819                    out.push(row);
3820                }
3821            }
3822            out
3823        } else {
3824            rows
3825        };
3826        // v7.17.0 Phase 3.P0-48 — aggregate dispatch over the
3827        // unnest source. Same routing the relational scan path
3828        // already takes — without it `SELECT COUNT(*) FROM
3829        // unnest(ARRAY[…])` either errored at projection time or
3830        // returned the wrong shape.
3831        if aggregate::uses_aggregate(stmt) {
3832            // v7.29 — a per-query memo so correlated scalar
3833            // subqueries batch-evaluate once (group map) instead of
3834            // executing per group.
3835            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
3836            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
3837                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
3838                    .map_err(|err| match err {
3839                        EngineError::Eval(ev) => ev,
3840                        other => eval::EvalError::TypeMismatch {
3841                            detail: alloc::format!("{other}"),
3842                        },
3843                    })
3844            };
3845            // v7.39 (round 656) — hand the rows over as they are rather than
3846            // collecting a second vector of `RowRef` wrappers. Note this is
3847            // a set-returning-function path, NOT the relational scan: the
3848            // measured O(rows) cost lived in `run_single_table_aggregate`,
3849            // and converting these four first was a miss that cost a full
3850            // round — every test stayed green and the number did not move.
3851            let agg = aggregate::run(
3852                stmt,
3853                crate::join::AggRows::Owned(&filtered),
3854                &schema_cols,
3855                Some(&alias),
3856                Some(&agg_correlated),
3857                self.parallel_runner.0.as_deref(),
3858                Some(self.active_catalog()),
3859                Some(self),
3860            )?;
3861            return self.finish_agg_result(agg, stmt, cancel);
3862        }
3863        // Projection.
3864        let projection = build_projection(
3865            &stmt.items,
3866            &schema_cols,
3867            &alias,
3868            self.backslash_escapes,
3869            Some(self.active_catalog()),
3870        )?;
3871        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
3872            alloc::vec::Vec::with_capacity(filtered.len());
3873        // v7.19 P5 — Set-Returning-Function in projection
3874        // position (PG `SELECT unnest(arr) FROM t` shape). When a
3875        // SELECT item evaluates to a top-level unnest(arr) call,
3876        // expand it: for each input row, evaluate the array, emit
3877        // one output row per element, broadcasting non-SRF
3878        // projections from the same input row. Multi-SRF + LCM
3879        // padding stays a documented carve-out; mailrs uses
3880        // single-SRF for redirect_uris.
3881        // v7.39 (read01 round 67) — EVERY set-returning item expands, in lockstep
3882        // (see `expand_srf_row`); a user `RETURNS SETOF` function counts too.
3883        let srf_idxs = self.srf_target_idxs(&projection);
3884        // v7.39 (round 621) — which input row each output row came from. An
3885        // SRF turns one input row into many, and the ORDER BY below used to
3886        // index the EXPANDED rows by the INPUT row's position: the result was
3887        // silently truncated to the input row count and left unsorted, so
3888        // `SELECT unnest(ARRAY[1,2]), y FROM unnest(ARRAY[5,6,7]) y ORDER BY 1`
3889        // answered three of its six rows, in no order. Without the ORDER BY
3890        // the same query was already right.
3891        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
3892        if !srf_idxs.is_empty() {
3893            let (rows, src) =
3894                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
3895            projected_rows = rows;
3896            src_of_row = src;
3897        } else {
3898            // v7.24 (round-16 B) — select-list subqueries resolve
3899            // per row (correlated-aware; plain exprs take the fast
3900            // path inside).
3901            let mut proj_memo = memoize::MemoizeCache::default();
3902            for row in &filtered {
3903                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
3904                for p in &projection {
3905                    vals.push(self.eval_expr_with_correlated(
3906                        &p.expr,
3907                        row,
3908                        &scan_ctx,
3909                        cancel,
3910                        Some(&mut proj_memo),
3911                    )?);
3912                }
3913                projected_rows.push(Row::new(vals));
3914            }
3915        }
3916        // ORDER BY / LIMIT — apply on the projected rows (cheap;
3917        // unnest result sets are small by design).
3918        let columns: alloc::vec::Vec<ColumnSchema> = projection
3919            .iter()
3920            // v7.39 (read01 round 54) — keep the column's enum identity through
3921            // the projection (it lives outside the DataType lattice), or a
3922            // derived table / UNION / windowed result forgets it and any outer
3923            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
3924            .map(|p| p.to_column_schema())
3925            .collect();
3926        // Re-evaluate ORDER BY against the source schema (pre-projection
3927        // so col refs by name still resolve through `scan_ctx`).
3928        // v7.39 (read01 round 80) — a positional key means the Nth OUTPUT
3929        // column. Evaluated as an expression it is just the constant N: the same
3930        // key for every row, so the sort ran and changed nothing.
3931        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
3932        if !order_by.is_empty() {
3933            // v7.39 (round 621) — one entry per OUTPUT row, not per input row.
3934            // A key that names a select-list item reads it out of the expanded
3935            // row (PG sorts AFTER the expansion); one that names a source
3936            // column the query does not project is evaluated on the input row
3937            // it came from, which is what `srf_order_output_cols` decides.
3938            let out_cols = if srf_idxs.is_empty() {
3939                alloc::vec![None; order_by.len()]
3940            } else {
3941                srf_order_output_cols(&order_by, &projection)
3942            };
3943            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
3944                .iter()
3945                .enumerate()
3946                .map(|(k, out)| -> Result<_, EngineError> {
3947                    let src = src_of_row.get(k).copied().unwrap_or(k);
3948                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
3949                        .iter()
3950                        .zip(out_cols.iter())
3951                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, &filtered[src], &scan_ctx))
3952                        .collect();
3953                    Ok((k, keys?))
3954                })
3955                .collect::<Result<_, _>>()?;
3956            indexed.sort_by(|a, b| {
3957                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
3958                    let o = &order_by[idx];
3959                    let cmp = order_by_value_cmp_in(
3960                        o.desc,
3961                        o.nulls_first,
3962                        ka,
3963                        kb,
3964                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
3965                    );
3966                    if cmp != core::cmp::Ordering::Equal {
3967                        return cmp;
3968                    }
3969                }
3970                core::cmp::Ordering::Equal
3971            });
3972            projected_rows = indexed
3973                .into_iter()
3974                .map(|(i, _)| projected_rows[i].clone())
3975                .collect();
3976        }
3977        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
3978        if stmt.distinct {
3979            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
3980            // spec folds EVERY text position, so a column declared
3981            // `COLLATE utf8mb4_bin` had its values merged here exactly the
3982            // way 3b494b6e fixed on the main scan path. The projection is
3983            // already in scope at each of these sites, so the mask needs no
3984            // new plumbing -- it was simply never asked for.
3985            projected_rows = dedup_rows(
3986                projected_rows,
3987                FoldSpec::of_masks(
3988                    scan_ctx.mysql_dialect,
3989                    &fold_mask(&projection),
3990                    &pad_mask(&projection),
3991                ),
3992            );
3993        }
3994        // LIMIT / OFFSET — apply at the tail.
3995        if let Some(offset) = stmt.offset_literal() {
3996            let off = (offset as usize).min(projected_rows.len());
3997            projected_rows.drain(..off);
3998        }
3999        if let Some(limit) = stmt.limit_literal() {
4000            projected_rows.truncate(limit as usize);
4001        }
4002        Ok(QueryResult::Rows {
4003            columns,
4004            rows: projected_rows,
4005        })
4006    }
4007
4008    /// v7.17.0 Phase 3.10 — `FROM generate_series(start, stop [,
4009    /// step])` set-returning source. Mirrors `exec_select_unnest`'s
4010    /// shape: evaluate the arg list once against an empty row,
4011    /// materialise the row stream by stepping start → stop, then
4012    /// route through the standard WHERE / projection / ORDER BY /
4013    /// LIMIT pipeline. Two arg-type combos in v7.17:
4014    ///   * integer / integer [/ integer] — SmallInt, Int, BigInt
4015    ///     (widened to BigInt internally; step defaults to 1)
4016    ///   * timestamp / timestamp / interval — date-range
4017    ///     iteration (mailrs's daily-report pattern)
4018    fn exec_select_generate_series(
4019        &self,
4020        stmt: &SelectStatement,
4021        primary: &TableRef,
4022        cancel: CancelToken<'_>,
4023    ) -> Result<QueryResult, EngineError> {
4024        let args = primary
4025            .generate_series_args
4026            .as_ref()
4027            .expect("caller guards generate_series_args.is_some()");
4028        let (elem_dtype, rows) = generate_series_rows(args, &cancel)?;
4029        let alias = primary
4030            .alias
4031            .clone()
4032            .unwrap_or_else(|| "generate_series".to_string());
4033        // `AS t(n)` — the first column-alias entry renames the
4034        // series column (PG semantics); bare alias keeps the
4035        // pre-existing behaviour of naming the column after it.
4036        let col_name = primary
4037            .unnest_column_aliases
4038            .first()
4039            .cloned()
4040            .unwrap_or_else(|| alias.clone());
4041        let col_schema = ColumnSchema::new(col_name, elem_dtype, true);
4042        let mut schema_cols = alloc::vec![col_schema.clone()];
4043        // WITH ORDINALITY — trailing BIGINT counting rows from 1;
4044        // the second column-alias entry renames it.
4045        let rows = if primary.with_ordinality {
4046            let ord_name = primary
4047                .unnest_column_aliases
4048                .get(1)
4049                .cloned()
4050                .unwrap_or_else(|| "ordinality".to_string());
4051            schema_cols.push(ColumnSchema::new(ord_name, DataType::BigInt, false));
4052            rows.into_iter()
4053                .enumerate()
4054                .map(|(i, row)| {
4055                    let mut vals = row.values.clone();
4056                    vals.push(Value::BigInt(i as i64 + 1));
4057                    Row::new(vals)
4058                })
4059                .collect()
4060        } else {
4061            rows
4062        };
4063        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
4064        // `EvalContext::new` drops it and every catalog-dependent cast
4065        // (regclass / enum / composite / domain) silently degrades.
4066        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
4067        // WHERE.
4068        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
4069            let mut out = alloc::vec::Vec::with_capacity(rows.len());
4070            for row in rows {
4071                cancel.check()?;
4072                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
4073                if matches!(v, Value::Bool(true)) {
4074                    out.push(row);
4075                }
4076            }
4077            out
4078        } else {
4079            rows
4080        };
4081        // v7.17.0 Phase 3.P0-48 — aggregate dispatch for set-
4082        // returning sources. When the SELECT projection contains
4083        // aggregate functions (COUNT/SUM/MIN/MAX/AVG/string_agg/
4084        // …) we route the filtered row stream through the same
4085        // aggregate executor the relational scan path uses, so
4086        // `SELECT COUNT(*) FROM generate_series(1, 100)` returns
4087        // a single 100 row instead of erroring at projection
4088        // time. GROUP BY / HAVING / ORDER BY over the aggregate
4089        // output all ride through `aggregate::run`.
4090        if aggregate::uses_aggregate(stmt) {
4091            // v7.29 — a per-query memo so correlated scalar
4092            // subqueries batch-evaluate once (group map) instead of
4093            // executing per group.
4094            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
4095            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
4096                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
4097                    .map_err(|err| match err {
4098                        EngineError::Eval(ev) => ev,
4099                        other => eval::EvalError::TypeMismatch {
4100                            detail: alloc::format!("{other}"),
4101                        },
4102                    })
4103            };
4104            // v7.39 (round 656) — hand the rows over as they are rather than
4105            // collecting a second vector of `RowRef` wrappers. Note this is
4106            // a set-returning-function path, NOT the relational scan: the
4107            // measured O(rows) cost lived in `run_single_table_aggregate`,
4108            // and converting these four first was a miss that cost a full
4109            // round — every test stayed green and the number did not move.
4110            let agg = aggregate::run(
4111                stmt,
4112                crate::join::AggRows::Owned(&filtered),
4113                &schema_cols,
4114                Some(&alias),
4115                Some(&agg_correlated),
4116                self.parallel_runner.0.as_deref(),
4117                Some(self.active_catalog()),
4118                Some(self),
4119            )?;
4120            return self.finish_agg_result(agg, stmt, cancel);
4121        }
4122        // Projection.
4123        let projection = build_projection(
4124            &stmt.items,
4125            &schema_cols,
4126            &alias,
4127            self.backslash_escapes,
4128            Some(self.active_catalog()),
4129        )?;
4130        // v7.39 (round 621) — and here, for the same reason.
4131        let srf_idxs = self.srf_target_idxs(&projection);
4132        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4133        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
4134            alloc::vec::Vec::with_capacity(filtered.len());
4135        let mut proj_memo = memoize::MemoizeCache::default();
4136        if !srf_idxs.is_empty() {
4137            let (rows, src) =
4138                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
4139            projected_rows = rows;
4140            src_of_row = src;
4141        } else {
4142            for row in &filtered {
4143                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
4144                for p in &projection {
4145                    // v7.24 (round-16 B) — correlated-aware.
4146                    vals.push(self.eval_expr_with_correlated(
4147                        &p.expr,
4148                        row,
4149                        &scan_ctx,
4150                        cancel,
4151                        Some(&mut proj_memo),
4152                    )?);
4153                }
4154                projected_rows.push(Row::new(vals));
4155            }
4156        }
4157        let columns: alloc::vec::Vec<ColumnSchema> = projection
4158            .iter()
4159            // v7.39 (read01 round 54) — keep the column's enum identity through
4160            // the projection (it lives outside the DataType lattice), or a
4161            // derived table / UNION / windowed result forgets it and any outer
4162            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
4163            .map(|p| p.to_column_schema())
4164            .collect();
4165        // ORDER BY against the source schema.
4166        // v7.39 (round 621) — one entry per OUTPUT row (a target-list SRF makes
4167        // more of them than there were inputs), and a positional key means the
4168        // Nth OUTPUT column, which is what `resolve_positional_order_by` does
4169        // and what the other two synthetic-source tails already did.
4170        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
4171        if !order_by.is_empty() {
4172            let out_cols = if srf_idxs.is_empty() {
4173                alloc::vec![None; order_by.len()]
4174            } else {
4175                srf_order_output_cols(&order_by, &projection)
4176            };
4177            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
4178                .iter()
4179                .enumerate()
4180                .map(|(k, out)| -> Result<_, EngineError> {
4181                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
4182                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
4183                        .iter()
4184                        .zip(out_cols.iter())
4185                        .map(|(ob, oc)| srf_order_key(ob, *oc, out, r, &scan_ctx))
4186                        .collect();
4187                    Ok((k, keys?))
4188                })
4189                .collect::<Result<_, _>>()?;
4190            indexed.sort_by(|a, b| {
4191                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
4192                    let o = &stmt.order_by[idx];
4193                    let cmp = order_by_value_cmp_in(
4194                        o.desc,
4195                        o.nulls_first,
4196                        ka,
4197                        kb,
4198                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
4199                    );
4200                    if cmp != core::cmp::Ordering::Equal {
4201                        return cmp;
4202                    }
4203                }
4204                core::cmp::Ordering::Equal
4205            });
4206            projected_rows = indexed
4207                .into_iter()
4208                .map(|(i, _)| projected_rows[i].clone())
4209                .collect();
4210        }
4211        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
4212        if stmt.distinct {
4213            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
4214            // spec folds EVERY text position, so a column declared
4215            // `COLLATE utf8mb4_bin` had its values merged here exactly the
4216            // way 3b494b6e fixed on the main scan path. The projection is
4217            // already in scope at each of these sites, so the mask needs no
4218            // new plumbing -- it was simply never asked for.
4219            projected_rows = dedup_rows(
4220                projected_rows,
4221                FoldSpec::of_masks(
4222                    scan_ctx.mysql_dialect,
4223                    &fold_mask(&projection),
4224                    &pad_mask(&projection),
4225                ),
4226            );
4227        }
4228        if let Some(offset) = stmt.offset_literal() {
4229            let off = (offset as usize).min(projected_rows.len());
4230            projected_rows.drain(..off);
4231        }
4232        if let Some(limit) = stmt.limit_literal() {
4233            projected_rows.truncate(limit as usize);
4234        }
4235        Ok(QueryResult::Rows {
4236            columns,
4237            rows: projected_rows,
4238        })
4239    }
4240
4241    /// The FROM shapes that are not an ordinary table scan — joins, the
4242    /// set-returning sources, JSON_TABLE, a derived table, and the rest.
4243    ///
4244    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4245    /// reason round 848 established in the parser: a debug build gives
4246    /// EVERY branch's locals a slot in the frame, whichever branch runs.
4247    /// `exec_bare_select_cancel` measured 64,784 bytes and a nested query
4248    /// stacks several of them; a plain scan reaches none of these
4249    /// branches. Moving them out took the frame to 52,336.
4250    ///
4251    /// `Ok(None)` means "not one of these shapes, carry on".
4252    #[inline(never)]
4253    fn try_from_shape_paths(
4254        &self,
4255        stmt: &SelectStatement,
4256        from: &spg_sql::ast::FromClause,
4257        cancel: CancelToken<'_>,
4258    ) -> Result<Option<QueryResult>, EngineError> {
4259        if !from.joins.is_empty() {
4260            // v7.37.x (docker-fair LEFTJOIN 71 % attack) — LEFT JOIN
4261            // elimination: when a LEFT JOIN's right side is referenced
4262            // ONLY in the ON equality and the right-side join key is
4263            // UNIQUE/PK, the join preserves outer cardinality exactly
4264            // and contributes no values used downstream. Drop the
4265            // entire join. PG does this on the
4266            // `SELECT COUNT(*) FROM A LEFT JOIN B ON B.pk = A.fk` shape
4267            // — A's row count is what survives, B never has to be
4268            // touched.
4269            if let Some(eliminated) = self.try_eliminate_redundant_left_joins(stmt) {
4270                return self.exec_bare_select_cancel(&eliminated, cancel).map(Some);
4271            }
4272            // v7.38 P0 元机制 D — `SPG_TEST_DISABLE_JOINFOLD=1` skips
4273            // the v7.32 joinfold rewrite that turns inner JOINs into a
4274            // single-table scan when the catalogue can prove key-only
4275            // dependency. Tests use this to assert "without joinfold,
4276            // the join still executes correctly" (joinfold is a
4277            // semantically-equivalent rewrite, not a correctness fix).
4278            if !self.env_cfg().disable_joinfold {
4279                if let Some(folded) = self.try_fold_inner_joins(stmt, cancel)? {
4280                    return self.exec_bare_select_cancel(&folded, cancel).map(Some);
4281                }
4282            }
4283            return self.exec_joined_select(stmt, from, cancel).map(Some);
4284        }
4285        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>`. Synthesise a
4286        // single-column table at SELECT entry by evaluating the
4287        // expression once against the empty row (UNNEST is
4288        // uncorrelated in v7.11; correlated / LATERAL unnest is a
4289        // v7.12 carve-out). Build a virtual `Table` in a heap-only
4290        // catalog, then route to the regular scan path.
4291        if from.primary.unnest_expr.is_some() {
4292            return self
4293                .exec_select_unnest(stmt, &from.primary, cancel)
4294                .map(Some);
4295        }
4296        // v7.37.43-T4.5 — `FROM jsonb_each_text(<expr>)` set-
4297        // returning function. Same dispatch shape as unnest but
4298        // emits a two-column (key TEXT, value TEXT) row stream.
4299        if from.primary.jsonb_each_text_arg.is_some() {
4300            return self
4301                .exec_select_jsonb_each_text(stmt, &from.primary, cancel)
4302                .map(Some);
4303        }
4304        // v7.39 (read01 partitionfuncs.c) — FROM-position table functions
4305        // (pg_partition_tree / pg_partition_ancestors) dispatched by name.
4306        // v7.39 (read01 round 74) — `ROWS FROM (f(a), g(b))` whose entries have no
4307        // array form. Each function runs; the results zip in LOCKSTEP with the
4308        // shorter padded to NULL — the SAME rule the target-list SRFs follow
4309        // (round 67), which is why `srf_values` is what evaluates each entry.
4310        if from.primary.rows_from.is_some() {
4311            let (rows, mut schema_cols) = self.rows_from_rows(&from.primary)?;
4312            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4313                if let Some(col) = schema_cols.get_mut(i) {
4314                    col.name = new_name.clone();
4315                }
4316            }
4317            let alias = from
4318                .primary
4319                .alias
4320                .clone()
4321                .unwrap_or_else(|| from.primary.name.clone());
4322            return self
4323                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4324                .map(Some);
4325        }
4326        // v7.39 (round 205, JSON_TABLE) — `FROM JSON_TABLE(doc, '$p'
4327        // COLUMNS (...))`. Materialise the row stream + schema by
4328        // walking the row path, then run the regular pipeline over it.
4329        if let Some(jt) = &from.primary.json_table {
4330            let (rows, schema_cols) = self.json_table_rows(jt, None)?;
4331            let alias = from
4332                .primary
4333                .alias
4334                .clone()
4335                .unwrap_or_else(|| from.primary.name.clone());
4336            return self
4337                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4338                .map(Some);
4339        }
4340        if from.primary.table_fn_call.is_some() {
4341            let (rows, mut schema_cols) = self.table_fn_rows(&from.primary)?;
4342            // v7.39 (read01 round 68) — WITH ORDINALITY appends a BIGINT counter
4343            // (from 1, in output order) AFTER the function's own columns. The
4344            // alias list names it like any other, which is why it is appended
4345            // BEFORE the renaming pass below.
4346            let rows = if from.primary.with_ordinality {
4347                schema_cols.push(ColumnSchema::new(
4348                    "ordinality".to_string(),
4349                    DataType::BigInt,
4350                    false,
4351                ));
4352                rows.into_iter()
4353                    .enumerate()
4354                    .map(|(i, r)| {
4355                        let mut vals = r.values;
4356                        vals.push(Value::BigInt(i as i64 + 1));
4357                        Row::new(vals)
4358                    })
4359                    .collect()
4360            } else {
4361                rows
4362            };
4363            for (i, new_name) in from.primary.unnest_column_aliases.iter().enumerate() {
4364                if let Some(col) = schema_cols.get_mut(i) {
4365                    col.name = new_name.clone();
4366                }
4367            }
4368            let alias = from
4369                .primary
4370                .alias
4371                .clone()
4372                .unwrap_or_else(|| from.primary.name.clone());
4373            return self
4374                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4375                .map(Some);
4376        }
4377        // v7.37.17 (17.6 siblings) — plain derived table in primary
4378        // position: `FROM ( SELECT … ) alias` (no joins). The inner
4379        // SELECT materialises once (it is uncorrelated by
4380        // construction), then the outer projection / WHERE /
4381        // aggregate / ORDER BY pipeline runs over the synthetic
4382        // table. Joined derived tables keep riding the LATERAL
4383        // machinery in join.rs.
4384        if from.joins.is_empty() && from.primary.lateral_subquery.is_some() {
4385            // v7.39 (round 727) — flatten first. A simple derived table
4386            // (bare-column projection over one stored table, nothing that
4387            // changes cardinality or order) used to force the inner
4388            // SELECT through the SERIAL row-at-a-time projection pipeline
4389            // just to materialise a synthetic table the outer query then
4390            // re-scans: `count(*) FROM (SELECT id v FROM d WHERE …) q`
4391            // measured 18.6 ms against PG's 5 — and bare count over the
4392            // same filter WITHOUT the wrapper is 2 ms here, because it
4393            // rides the fused parallel lane. Rewriting to the unwrapped
4394            // form is PG's subquery pull-up; the whole tree gets the
4395            // fast lanes back.
4396            if let Some(flat) = try_flatten_derived(stmt, &from.primary) {
4397                return self.exec_select_cancel(&flat, cancel).map(Some);
4398            }
4399            // v7.39 (round 742) — `SELECT count(*) FROM (SELECT … ORDER
4400            // BY … OFFSET k) q` is `greatest(count_of_inner - k, 0)`:
4401            // ORDER BY never changes the row count, and OFFSET drops
4402            // exactly k. The materialising path sorted 500k rows to
4403            // count 10k (57 ms); PG runs its parallel sort anyway
4404            // (28 ms). The rewrite skips the sort entirely on both
4405            // counts — a plan PG itself does not have.
4406            if let Some(rewritten) = try_count_over_offset(stmt, &from.primary) {
4407                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4408            }
4409            // v7.39 (round 743) — `count(*) OVER a derived whose only
4410            // item is unnest(ARRAY[k elements])` is `k * count(WHERE)`:
4411            // a constant-length array unnests to exactly k rows per
4412            // input row, NULL elements included. PG expands the set to
4413            // count it (6.6 ms on the panel cell); the identity doesn't.
4414            if let Some(rewritten) = try_count_over_const_unnest(stmt, &from.primary) {
4415                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4416            }
4417            return self
4418                .exec_select_derived(stmt, &from.primary, cancel)
4419                .map(Some);
4420        }
4421        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
4422        // [, step])` set-returning source. Dispatch mirrors UNNEST:
4423        // materialise the row stream from a single eval pass, then
4424        // run the regular projection / WHERE / ORDER BY / LIMIT
4425        // pipeline over the synthetic single-column table.
4426        if from.primary.generate_series_args.is_some() {
4427            return self
4428                .exec_select_generate_series(stmt, &from.primary, cancel)
4429                .map(Some);
4430        }
4431        Ok(None)
4432    }
4433
4434    /// Pick an index seek for this WHERE, if any of the four apply:
4435    /// BTree equality, GIN `@@`, trigram LIKE, or JSONB `@>`.
4436    ///
4437    /// `#[inline(never)]` and out of `exec_bare_select_cancel` for the
4438    /// frame reason on `try_from_shape_paths`: in a debug build a
4439    /// closure's locals belong to the enclosing frame, and this one is
4440    /// four seek attempts wide on a function that nests.
4441    #[inline(never)]
4442    fn pick_indexed_rows<'r>(
4443        &'r self,
4444        stmt: &SelectStatement,
4445        table: &'r spg_storage::Table,
4446        schema_cols: &[spg_storage::ColumnSchema],
4447        alias: &str,
4448        ctx: &crate::eval::EvalContext<'_>,
4449        seek_snapshot: &crate::Snapshot,
4450    ) -> Option<crate::index_access::Seeked<'r>> {
4451        stmt.where_.as_ref().and_then(|w| {
4452            // BTree / col=literal seek first — covers the v7.11.3 multi-
4453            // column AND case and the leading-column equality lookup.
4454            try_index_seek(
4455                w,
4456                schema_cols,
4457                self.active_catalog(),
4458                table,
4459                alias,
4460                seek_snapshot,
4461                ctx.mysql_dialect,
4462            )
4463            .or_else(|| {
4464                // v7.12.3 — GIN-accelerated `WHERE col @@
4465                // tsquery` when the column has a `USING gin`
4466                // index. Returns an over-approximate candidate
4467                // set; the WHERE re-eval loop below verifies
4468                // the full `@@` predicate per row.
4469                try_gin_seek(
4470                    w,
4471                    schema_cols,
4472                    self.active_catalog(),
4473                    table,
4474                    alias,
4475                    ctx,
4476                    seek_snapshot,
4477                )
4478                .map(crate::index_access::Seeked::over_approximate)
4479            })
4480            .or_else(|| {
4481                // v7.15.0 — trigram-GIN-accelerated
4482                // `WHERE col LIKE / ILIKE '<pat>'` when the
4483                // column has a `gin_trgm_ops` GIN index.
4484                // Over-approximate candidate set; the WHERE
4485                // re-eval verifies the LIKE per row.
4486                try_trgm_seek(w, schema_cols, table, alias, seek_snapshot)
4487                    .map(crate::index_access::Seeked::over_approximate)
4488            })
4489            .or_else(|| {
4490                // v7.37.8(sentori Epic 5 P2)— real JSONB-GIN
4491                // accelerated `WHERE col @> <jsonb_literal>`
4492                // when the column has a `USING gin` index. The
4493                // posting-list intersection returns an over-
4494                // approximate candidate set; the WHERE re-eval
4495                // verifies the full `@>` predicate per row.
4496                try_gin_jsonb_seek(w, schema_cols, table, alias, seek_snapshot)
4497                    .map(crate::index_access::Seeked::over_approximate)
4498            })
4499        })
4500    }
4501
4502    /// Index-seek fast paths: NSW kNN, the primary-key top-N walk, and
4503    /// the two `count(*)` short-circuits. Out-of-line for the frame
4504    /// reason on `try_from_shape_paths` — an ordinary scan reaches none
4505    /// of them, and in a debug build their locals sit in the frame
4506    /// regardless.
4507    #[inline(never)]
4508    fn try_seek_fast_paths(
4509        &self,
4510        stmt: &SelectStatement,
4511        table: &spg_storage::Table,
4512        schema_cols: &[spg_storage::ColumnSchema],
4513        alias: &str,
4514        seek_snapshot: &crate::Snapshot,
4515        cancel: CancelToken<'_>,
4516    ) -> Result<Option<QueryResult>, EngineError> {
4517        if let Some(nsw_rows) = try_nsw_knn(stmt, table, schema_cols, alias, seek_snapshot) {
4518            // NSW kNN dispatches against the hot-tier vector index only
4519            // (vector cells aren't promoted to cold segments), so wrap
4520            // the returned row indices as `Cow::Borrowed` for the
4521            // unified `materialise_in_order` shape.
4522            let ordered: Vec<Cow<'_, Row<'static>>> = nsw_rows
4523                .into_iter()
4524                .filter_map(|i| table.rows().get(i).map(Cow::Borrowed))
4525                .collect();
4526            return materialise_in_order(
4527                stmt,
4528                schema_cols,
4529                alias,
4530                &ordered,
4531                self.backslash_escapes,
4532            )
4533            .map(Some);
4534        }
4535
4536        // v7.34.5 — ORDER BY <indexed col> [DESC|ASC] LIMIT N drives
4537        // the scan via the BTree iterator in the requested direction
4538        // and stops after `OFFSET + LIMIT` candidates pass WHERE. The
4539        // 80 ms `mailrs_prod_plain_limit` baseline at 250 k rows is
4540        // the load-bearing consumer; this skips the materialise-every-
4541        // row + partial-sort tail entirely. Walker output is already
4542        // in ORDER BY order so `materialise_in_order` (no extra sort)
4543        // is the natural sink.
4544        if let Some(walked) = try_pk_walk_top_n(
4545            stmt,
4546            self.active_catalog(),
4547            table,
4548            schema_cols,
4549            alias,
4550            self,
4551            cancel,
4552            self.backslash_escapes,
4553        ) {
4554            return materialise_in_order(stmt, schema_cols, alias, &walked, self.backslash_escapes)
4555                .map(Some);
4556        }
4557
4558        // Index seek: if WHERE is `col = literal` (or commuted) and the
4559        // referenced column has an index, dispatch each locator through
4560        // the catalog (hot tier → borrow, cold tier → page-read +
4561        // decode) and iterate just those rows. Otherwise fall back to a
4562        // v7.37.x (docker-fair INSUBQ attack) — short-circuit COUNT(*)
4563        // FROM A WHERE A.pk IN (large literal list). The post-subquery-
4564        // replacement shape of INSUBQ. Runs BEFORE `indexed_rows` so
4565        // we don't pay the row materialisation cost twice. Returns
4566        // a bare `Rows{count}` if the shape matches.
4567        if aggregate::uses_aggregate(stmt)
4568            && let Some(out) = self.try_count_star_pk_in_list_fast(stmt, table, schema_cols, alias)
4569        {
4570            return Ok(Some(out));
4571        }
4572        // v7.38 (perf) — `count(*) WHERE <indexed BETWEEN>`: count the in-range
4573        // locators directly, skipping row materialisation + WHERE re-eval.
4574        if aggregate::uses_aggregate(stmt)
4575            && let Some(out) = self.try_count_star_indexed_range_fast(
4576                stmt,
4577                table,
4578                schema_cols,
4579                alias,
4580                seek_snapshot,
4581            )
4582        {
4583            return Ok(Some(out));
4584        }
4585        Ok(None)
4586    }
4587
4588    /// The two rewrites that must happen before the FROM clause is even
4589    /// looked at: a meta-view reference needs the catalog views
4590    /// materialised, and a windowed projection belongs to the window
4591    /// executor. Out-of-line for the frame reason on
4592    /// `try_from_shape_paths`.
4593    #[inline(never)]
4594    fn try_pre_from_paths(
4595        &self,
4596        stmt: &SelectStatement,
4597        cancel: CancelToken<'_>,
4598    ) -> Result<Option<QueryResult>, EngineError> {
4599        if !self.meta_views_materialised && select_references_meta_view(stmt) {
4600            return self.exec_select_with_meta_views(stmt, cancel).map(Some);
4601        }
4602        // v4.12: window-function path. When the projection contains
4603        // any `name(args) OVER (...)` we route to the dedicated
4604        // executor — partition + sort + per-row window value before
4605        // the regular projection.
4606        if select_has_window(stmt) {
4607            // v7.37 D.23 — window functions run AFTER GROUP BY aggregation.
4608            // `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g`
4609            // needs the aggregation done first, then windows over the grouped
4610            // rows. Rewrite to an aggregate derived subquery + outer window query
4611            // (which the window-over-derived path, D.13, executes). Only fires on
4612            // the currently-erroring agg+window+GROUP BY shape, so it can't
4613            // regress working window-only or aggregate-only queries.
4614            if let Some(rewritten) = rewrite_agg_before_window(stmt) {
4615                return self.exec_select_cancel(&rewritten, cancel).map(Some);
4616            }
4617            return self.exec_select_with_window(stmt, cancel).map(Some);
4618        }
4619        Ok(None)
4620    }
4621
4622    /// A projection naming `ctid` or another system column: the schema
4623    /// has to be widened with them before the scan. Out-of-line for the
4624    /// frame reason on `try_from_shape_paths`.
4625    #[inline(never)]
4626    fn try_ctid_projection(
4627        &self,
4628        stmt: &SelectStatement,
4629        primary: &spg_sql::ast::TableRef,
4630        table: &spg_storage::Table,
4631        schema_cols: &[spg_storage::ColumnSchema],
4632        alias: &str,
4633        cancel: CancelToken<'_>,
4634    ) -> Result<Option<QueryResult>, EngineError> {
4635        if references_ctid(stmt) {
4636            let snapshot = self.current_snapshot();
4637            let mut ext_cols = schema_cols.to_vec();
4638            for name in SYSTEM_COLUMNS {
4639                ext_cols.push(ColumnSchema::new(name.to_string(), DataType::Text, false));
4640            }
4641            let table_oid =
4642                crate::system_catalog::relation_oid(self.active_catalog(), &primary.name)
4643                    .unwrap_or(0);
4644            let headers = table.headers();
4645            let rows: Vec<Row<'static>> = table
4646                .scan_visible(&snapshot)
4647                .map(|(i, r)| {
4648                    let mut vals = r.values.clone();
4649                    // One block, offsets from 1, as PG numbers them.
4650                    vals.push(Value::Tid(0, i as u32 + 1));
4651                    let h = headers.get(i);
4652                    vals.push(Value::Xid(h.map_or(0, |h| h.xmin as u32)));
4653                    vals.push(Value::Xid(h.map_or(0, |h| h.xmax as u32)));
4654                    // SPG keeps no per-statement command ids; PG shows 0 for
4655                    // every row a reader can see, which is every row here.
4656                    vals.push(Value::Cid(0));
4657                    vals.push(Value::Cid(0));
4658                    vals.push(Value::BigInt(table_oid));
4659                    Row::new(vals)
4660                })
4661                .collect();
4662            return self
4663                .exec_select_over_rows(stmt, rows, ext_cols, alias, cancel)
4664                .map(Some);
4665        }
4666        Ok(None)
4667    }
4668
4669    /// A sequence read as a one-row relation (`SELECT last_value FROM
4670    /// seq`), which PG allows and psql's \\d relies on. Out-of-line for
4671    /// the frame reason on `try_from_shape_paths`.
4672    #[inline(never)]
4673    fn try_sequence_relation(
4674        &self,
4675        stmt: &SelectStatement,
4676        primary: &spg_sql::ast::TableRef,
4677        cancel: CancelToken<'_>,
4678    ) -> Result<Option<QueryResult>, EngineError> {
4679        if self.active_catalog().get(&primary.name).is_none()
4680            && let Some(seq) = self.active_catalog().sequence(&primary.name)
4681        {
4682            let rows = alloc::vec![Row::new(alloc::vec![
4683                Value::BigInt(seq.last_value),
4684                Value::BigInt(0),
4685                Value::Bool(seq.is_called),
4686            ])];
4687            let schema_cols = alloc::vec![
4688                ColumnSchema::new("last_value", DataType::BigInt, false),
4689                ColumnSchema::new("log_cnt", DataType::BigInt, false),
4690                ColumnSchema::new("is_called", DataType::Bool, false),
4691            ];
4692            let alias = primary
4693                .alias
4694                .clone()
4695                .unwrap_or_else(|| primary.name.clone());
4696            return self
4697                .exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
4698                .map(Some);
4699        }
4700        Ok(None)
4701    }
4702
4703    pub(crate) fn exec_bare_select_cancel(
4704        &self,
4705        stmt: &SelectStatement,
4706        cancel: CancelToken<'_>,
4707    ) -> Result<QueryResult, EngineError> {
4708        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST N ROWS WITH TIES`
4709        // is meaningless without an ORDER BY; PG raises a hard
4710        // error and SPG mirrors the surface so the same DDL/app
4711        // path behaves identically on cutover.
4712        check_with_ties_requires_order_by(stmt)?;
4713        // v7.39 (round 229) — WHERE / HAVING run before the window pass, so
4714        // PG rejects window calls there outright. Checked here rather than
4715        // on the window path: `HAVING row_number() OVER () = 1` has no
4716        // window in its projection at all.
4717        crate::window::reject_window_in_row_clauses(stmt)?;
4718        // v7.39 (round 232) — the ORDER BY legality rules (positional
4719        // bounds, DISTINCT, DISTINCT ON). Same placement as the window
4720        // check: before anything scans.
4721        crate::orderby::check_order_by_legality(stmt)?;
4722        // v7.37.16 — resolve `USING` column-merge + `NATURAL JOIN` into an
4723        // equivalent statement the regular executor handles (merged join
4724        // columns collapse to a single unqualified output column; NATURAL
4725        // gets its common-column ON synthesised). The rewrite clears the
4726        // flags, so this re-entrant call is a no-op on the second pass.
4727        if let Some(rewritten) = self.desugar_using_natural(stmt)? {
4728            return self.exec_bare_select_cancel(&rewritten, cancel);
4729        }
4730        // v7.38.13 — a GROUP BY with no aggregate, whose select list is
4731        // exactly the group keys, IS a DISTINCT and was paying for the
4732        // aggregate executor to find that out. Same placement and shape
4733        // as the desugar above; the rewrite clears `group_by`, so the
4734        // re-entry is a no-op on the second pass. See `baregroup` for
4735        // what the gate rules out.
4736        if let Some(rewritten) = crate::baregroup::as_distinct(stmt) {
4737            return self.exec_bare_select_cancel(&rewritten, cancel);
4738        }
4739        // v7.39 (RLS) Phase 3 — cross-table joins: wrap each RLS-enabled join
4740        // operand in a security-barrier subquery, then re-enter (the wrapped
4741        // operands are no longer bare RLS tables, so this is a no-op on the
4742        // second pass).
4743        if let Some(rewritten) = self.rls_rewrite_joins(stmt) {
4744            return self.exec_bare_select_cancel(&rewritten, cancel);
4745        }
4746        // v7.39 (RLS) Phase 1 — for a policy-subject (non-superuser) session,
4747        // AND the RLS USING predicate into a single-table SELECT's WHERE.
4748        // Superuser sessions and non-RLS tables get `None` (no clone, no
4749        // change). Applied inline (shadowing `stmt`) rather than via re-entry
4750        // so it can't re-inject on a recursive pass.
4751        let rls_stmt;
4752        let stmt = match self.rls_select_predicate(stmt)? {
4753            Some(pred) => {
4754                let mut s = stmt.clone();
4755                s.where_ = Some(match s.where_.take() {
4756                    Some(existing) => spg_sql::ast::Expr::Binary {
4757                        lhs: alloc::boxed::Box::new(existing),
4758                        op: spg_sql::ast::BinOp::And,
4759                        rhs: alloc::boxed::Box::new(pred),
4760                    },
4761                    None => pred,
4762                });
4763                rls_stmt = s;
4764                &rls_stmt
4765            }
4766            None => stmt,
4767        };
4768        // v7.16.2 — same meta-view dispatch as
4769        // `exec_select_cancel`, applied here too because
4770        // `subquery_replacement` enters this function directly
4771        // for Exists / ScalarSubquery / InSubquery resolution
4772        // (bypassing the top-level entry to avoid double
4773        // subquery walking). Without this dispatch the subquery
4774        // hits `__spg_info_columns` and reports TableNotFound.
4775        if let Some(done) = self.try_pre_from_paths(stmt, cancel)? {
4776            return Ok(done);
4777        }
4778        // Constant SELECT (no FROM) — evaluate each item once against an
4779        // empty dummy row. Useful for `SELECT 1`, `SELECT coalesce(...)`,
4780        // `SELECT '7'::INT`. Column references will surface as
4781        // ColumnNotFound on eval since the schema is empty.
4782        let Some(from) = &stmt.from else {
4783            return self.exec_constant_select(stmt);
4784        };
4785        // Multi-table FROM (one or more joined peers) goes through the
4786        // nested-loop join executor. Single-table FROM stays on the
4787        // existing scan + index-seek path.
4788        if let Some(done) = self.try_from_shape_paths(stmt, from, cancel)? {
4789            return Ok(done);
4790        }
4791        // NOT hooked up. `try_spill_sorted_scan` is written, correct and
4792        // tested — eight ORDER BY shapes byte-identical spilled against
4793        // in-memory, with 103 runs opened to prove the spill ran — and it
4794        // loses on wall clock, which is a hard stop whatever the memory
4795        // buys. Measured round 865, same psql client both sides, same
4796        // machine, row counts verified, and both sides confirmed to be
4797        // doing an external merge rather than an indexed walk:
4798        //
4799        //   PG18        178.7 - 187.0 ms   Sort Method: external merge, 85 MB
4800        //   SPG spilled 269.7 - 299.6 ms   33 spill files at peak
4801        //
4802        // Non-overlapping, about 1.55x. Re-enable by restoring the call
4803        // below once that closes; nothing else has to change, which is
4804        // the point of it being a separate path.
4805        //
4806        //   if let Some(done) = self.try_spill_sorted_scan(stmt, from, cancel)? {
4807        //       return Ok(done);
4808        //   }
4809        //
4810        // v7.37 (round 882) — this walk stays unhooked, but its streaming
4811        // twin `try_spill_sorted_stream` IS hooked, above the ORDER BY
4812        // bail in `try_exec_joined_streaming`. Collecting the answer was
4813        // most of what this one cost: handing rows over as the merge
4814        // produces them holds peak to the budget plus one row, and the
4815        // wall clock lands inside PG18's range rather than 1.55x outside
4816        // it. Numbers in `extsort.rs`'s header.
4817        let primary = &from.primary;
4818        // v7.39 (round 244) — a sequence is selectable as a one-row relation
4819        // in PG (`SELECT last_value FROM seq` — psql's \d and several ORMs
4820        // read it). Synthesize PG's three columns.
4821        if let Some(done) = self.try_sequence_relation(stmt, primary, cancel)? {
4822            return Ok(done);
4823        }
4824        let table = self.active_catalog().get(&primary.name).ok_or_else(|| {
4825            StorageError::TableNotFound {
4826                name: primary.name.clone(),
4827            }
4828        })?;
4829        let schema_cols = &table.schema().columns;
4830        // The qualifier accepted on column refs is the alias (if any) else the
4831        // bare table name.
4832        let alias = primary.alias.as_deref().unwrap_or(primary.name.as_str());
4833        // v7.39 (round 511) — `ctid`, PG's physical row identity. SPG had no
4834        // system columns at all: `SELECT ctid FROM t` answered "column
4835        // \"ctid\" does not exist", which takes out the dedup idiom every
4836        // PG user knows — `DELETE … WHERE ctid NOT IN (SELECT min(ctid) …
4837        // GROUP BY key)`.
4838        //
4839        // The value comes from the row's position, which the scan already
4840        // yields; the column is appended to the schema and the rows only
4841        // when the statement asks for it, so nothing else pays for it. That
4842        // also routes the query down the general path, past the index fast
4843        // paths below — they hand back rows without positions, and a ctid
4844        // that was sometimes right would be worse than none.
4845        if let Some(done) =
4846            self.try_ctid_projection(stmt, primary, table, schema_cols, alias, cancel)?
4847        {
4848            return Ok(done);
4849        }
4850        let ctx = self.ev_ctx(schema_cols, Some(alias));
4851
4852        // NSW kNN planner: `ORDER BY col <-> literal LIMIT k` with no
4853        // WHERE and an NSW index on `col` skips the full scan. The
4854        // walk returns rows already in ascending-distance order, so
4855        // ORDER BY / LIMIT are honoured implicitly.
4856        // Phase C.3 step 2c — compute the reader's MVCC snapshot once
4857        // and thread it into every index-seek fast path below. No-op
4858        // today (every hot header is committed-alive).
4859        let seek_snapshot = self.current_snapshot();
4860        if let Some(done) =
4861            self.try_seek_fast_paths(stmt, table, schema_cols, alias, &seek_snapshot, cancel)?
4862        {
4863            return Ok(done);
4864        }
4865        // full scan over the hot tier (cold-tier rows are only reached
4866        // via index seek in v5.1 — full table scans against cold-tier
4867        // data ship in v5.2 with the freezer's per-segment scan API).
4868        let indexed_rows =
4869            self.pick_indexed_rows(stmt, table, schema_cols, alias, &ctx, &seek_snapshot);
4870
4871        // Aggregate path: filter rows first, then hand off to the
4872        // aggregate executor which does its own projection + ORDER BY.
4873        if aggregate::uses_aggregate(stmt) {
4874            return self.run_single_table_aggregate(
4875                stmt,
4876                table,
4877                schema_cols,
4878                alias,
4879                indexed_rows,
4880                cancel,
4881            );
4882        }
4883        self.run_single_table_scan(stmt, table, schema_cols, alias, indexed_rows, cancel)
4884    }
4885
4886    /// v7.37.43-T4.5 — execute `SELECT … FROM jsonb_each_text(<expr>)`.
4887    /// Sentori migration 0067 uses this with `CROSS JOIN LATERAL`; the
4888    /// uncorrelated FROM-primary case is the simpler shape, used by
4889    /// e2e pins. Materialises the (key, value) pair stream into a
4890    /// synthetic two-column TEXT table, then routes through the
4891    /// regular projection / WHERE / ORDER BY pipeline.
4892    /// v7.39 (read01 partitionfuncs.c) — materialise a FROM-position
4893    /// v7.39 (round 205, JSON_TABLE) — materialise a JSON_TABLE FROM
4894    /// item into (rows, schema). `outer_doc` is `Some` only when this
4895    /// is a NESTED level being expanded against a parent row item's
4896    /// already-parsed sub-document; the top-level call parses the doc
4897    /// expr itself. Row/column paths reuse the existing jsonpath
4898    /// evaluator (`json::json_table_path`); coercion reuses
4899    /// `coerce_value` on the JSON scalar text, so a json string
4900    /// coerces to DATE by its content, matching PG.
4901    #[allow(clippy::type_complexity)]
4902    pub(crate) fn json_table_rows(
4903        &self,
4904        jt: &spg_sql::ast::JsonTable,
4905        outer_doc: Option<&crate::json::JsonValue>,
4906    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
4907        // Column schema is static (independent of data): flatten the
4908        // COLUMNS tree in declaration order (NESTED contributes its
4909        // children inline, the PG output shape).
4910        let schema = json_table_schema(&jt.columns);
4911
4912        // PASSING variables → a single JsonValue object the jsonpath
4913        // engine reads `$name` from.
4914        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
4915        let ctx = EvalContext::new(&empty_schema, None);
4916        let dummy = Row::new(alloc::vec::Vec::new());
4917        let vars: Option<crate::json::JsonValue> = if jt.passing.is_empty() {
4918            None
4919        } else {
4920            let mut entries = alloc::vec::Vec::new();
4921            for (name, e) in &jt.passing {
4922                let v = eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?;
4923                entries.push((name.clone(), value_to_json_value(&v)));
4924            }
4925            Some(crate::json::JsonValue::Object(entries))
4926        };
4927
4928        // The document root: a NESTED level gets it from the parent;
4929        // the top level parses its doc expr.
4930        let root_owned;
4931        let root: &crate::json::JsonValue = match outer_doc {
4932            Some(d) => d,
4933            None => {
4934                let doc_val = eval::eval_expr(&jt.doc, &dummy, &ctx).map_err(EngineError::Eval)?;
4935                let src = match &doc_val {
4936                    Value::Null => return Ok((alloc::vec::Vec::new(), schema)),
4937                    Value::Json(s) | Value::Text(s) => s.as_ref().to_string(),
4938                    other => {
4939                        return Err(EngineError::Unsupported(alloc::format!(
4940                            "JSON_TABLE document must be json/text, got {}",
4941                            crate::conversions::pg_type_name_for_error_opt(other.data_type())
4942                        )));
4943                    }
4944                };
4945                root_owned = crate::json::parse_doc(&src).map_err(EngineError::Eval)?;
4946                &root_owned
4947            }
4948        };
4949
4950        let items = crate::json::json_table_path(root, &jt.row_path, vars.as_ref())
4951            .map_err(EngineError::Eval)?;
4952        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
4953        for (idx, item) in items.iter().enumerate() {
4954            self.json_table_emit_item(jt, item, idx, vars.as_ref(), &mut rows)?;
4955        }
4956        Ok((rows, schema))
4957    }
4958
4959    /// v7.39 (round 205) — emit the row(s) for one row-pattern item.
4960    /// Regular columns produce one value each; a NESTED column expands
4961    /// as an outer join (each nested match → one row sharing the
4962    /// parent cells; no nested match → one row with the nested cells
4963    /// NULL). Sibling NESTED at one level cross by concatenation of
4964    /// their independent expansions (PG's UNION-of-outer shape).
4965    fn json_table_emit_item(
4966        &self,
4967        jt: &spg_sql::ast::JsonTable,
4968        item: &crate::json::JsonValue,
4969        ordinality: usize,
4970        vars: Option<&crate::json::JsonValue>,
4971        out: &mut alloc::vec::Vec<Row<'static>>,
4972    ) -> Result<(), EngineError> {
4973        use spg_sql::ast::JsonTableColumn as C;
4974        // Parent cells (regular + ordinality), left-to-right; NESTED
4975        // columns contribute a run of child cells appended after.
4976        let mut parent_cells: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
4977        let mut nested_runs: alloc::vec::Vec<alloc::vec::Vec<Row<'static>>> =
4978            alloc::vec::Vec::new();
4979        let mut nested_widths: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
4980        for col in &jt.columns {
4981            match col {
4982                C::Ordinality { .. } => {
4983                    parent_cells.push(Value::BigInt(ordinality as i64 + 1));
4984                }
4985                C::Regular { .. } => {
4986                    parent_cells.push(self.json_table_column_value(col, item, vars)?);
4987                }
4988                C::Nested { path, columns } => {
4989                    // Recurse: a nested JSON_TABLE over `item` filtered
4990                    // by `path`, with the same PASSING vars.
4991                    let sub = spg_sql::ast::JsonTable {
4992                        doc: jt.doc.clone(), // unused (outer_doc provided)
4993                        row_path: path.clone(),
4994                        columns: columns.clone(),
4995                        passing: alloc::vec::Vec::new(),
4996                    };
4997                    let (nrows, nschema) = self.json_table_rows(&sub, Some(item))?;
4998                    nested_widths.push(nschema.len());
4999                    nested_runs.push(nrows);
5000                }
5001            }
5002        }
5003        if nested_runs.is_empty() {
5004            out.push(Row::new(parent_cells));
5005            return Ok(());
5006        }
5007        // PG sibling-NESTED semantics: each sibling expands
5008        // INDEPENDENTLY and the results CONCATENATE — a row from
5009        // sibling s fills only s's cells, every other sibling's cells
5010        // NULL. An empty sibling contributes ZERO rows (not a NULL
5011        // row). Only when EVERY sibling is empty does the parent still
5012        // emit one all-NULL row (the outer-join guarantee that a parent
5013        // item is never dropped). Verified vs PG18 (r207): a=1,b=2 → 3
5014        // rows; a=1,b=[] → 1 row; all-empty → 1 NULL row.
5015        let before = out.len();
5016        for (s_idx, run) in nested_runs.iter().enumerate() {
5017            for nrow in run {
5018                let mut cells = parent_cells.clone();
5019                for (o_idx, w) in nested_widths.iter().enumerate() {
5020                    if o_idx == s_idx {
5021                        cells.extend(nrow.values.iter().cloned());
5022                    } else {
5023                        for _ in 0..*w {
5024                            cells.push(Value::Null);
5025                        }
5026                    }
5027                }
5028                out.push(Row::new(cells));
5029            }
5030        }
5031        if out.len() == before {
5032            // Every sibling empty → one all-NULL nested row.
5033            let mut cells = parent_cells.clone();
5034            for w in &nested_widths {
5035                for _ in 0..*w {
5036                    cells.push(Value::Null);
5037                }
5038            }
5039            out.push(Row::new(cells));
5040        }
5041        Ok(())
5042    }
5043
5044    /// v7.39 (round 205) — evaluate one Regular column against a row
5045    /// item: EXISTS → bool; else path → at most one value, coerced to
5046    /// the declared type with ON EMPTY / ON ERROR / DEFAULT behaviour.
5047    fn json_table_column_value(
5048        &self,
5049        col: &spg_sql::ast::JsonTableColumn,
5050        item: &crate::json::JsonValue,
5051        vars: Option<&crate::json::JsonValue>,
5052    ) -> Result<Value<'static>, EngineError> {
5053        use spg_sql::ast::{JsonTableColumn as C, JsonTableOnBehavior as B};
5054        let C::Regular {
5055            name,
5056            ty,
5057            path,
5058            exists,
5059            format_json,
5060            wrapper,
5061            on_empty,
5062            on_error,
5063        } = col
5064        else {
5065            unreachable!("caller guards Regular");
5066        };
5067        let matches = crate::json::json_table_path(item, path, vars).map_err(EngineError::Eval)?;
5068        if *exists {
5069            return Ok(Value::Bool(!matches.is_empty()));
5070        }
5071        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5072        let ctx = EvalContext::new(&empty_schema, None);
5073        let dummy = Row::new(alloc::vec::Vec::new());
5074        let default_of = |b: &B| -> Result<Option<Value<'static>>, EngineError> {
5075            match b {
5076                B::Null => Ok(Some(Value::Null)),
5077                B::Error => Ok(None),
5078                B::Default(e) => Ok(Some(
5079                    eval::eval_expr(e, &dummy, &ctx).map_err(EngineError::Eval)?,
5080                )),
5081            }
5082        };
5083        // Empty match set → ON EMPTY.
5084        if matches.is_empty() {
5085            return match default_of(on_empty)? {
5086                Some(v) => coerce_json_table_default(v, *ty, name),
5087                None => Err(EngineError::Unsupported(alloc::format!(
5088                    "no SQL/JSON item found for JSON_TABLE column {name:?}"
5089                ))),
5090            };
5091        }
5092        let first = &matches[0];
5093        // FORMAT JSON: return the PG-canonical json representation.
5094        // WITH WRAPPER wraps the whole match SET in an array (even a
5095        // single scalar → `[5]`); without it, the single match's json.
5096        if *format_json {
5097            let text = if *wrapper {
5098                crate::json::JsonValue::Array(matches.clone()).canonical_json_text()
5099            } else {
5100                first.canonical_json_text()
5101            };
5102            return Ok(Value::Json(alloc::borrow::Cow::Owned(text)));
5103        }
5104        if first.is_json_null() {
5105            return Ok(Value::Null);
5106        }
5107        // Coerce the scalar text to the declared type; on failure → ON
5108        // ERROR (default NULL, DEFAULT expr, or raise).
5109        let dt = crate::conversions::column_type_to_data_type(*ty);
5110        let scalar = Value::Text(alloc::borrow::Cow::Owned(first.scalar_text()));
5111        match crate::conversions::coerce_value(scalar, dt, name, 0) {
5112            Ok(v) => Ok(v),
5113            Err(e) => match default_of(on_error)? {
5114                Some(v) => coerce_json_table_default(v, *ty, name),
5115                None => Err(e),
5116            },
5117        }
5118    }
5119
5120    /// table function into (rows, default schema). Dispatch by name.
5121    pub(crate) fn table_fn_rows(
5122        &self,
5123        primary: &TableRef,
5124    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5125        let (fn_name, args) = primary
5126            .table_fn_call
5127            .as_deref()
5128            .expect("caller guards table_fn_call.is_some()");
5129        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5130        let ctx = EvalContext::new(&empty_schema, None);
5131        let dummy_row = Row::new(alloc::vec::Vec::new());
5132        let arg0: Option<Value<'static>> = match args.first() {
5133            Some(e) => Some(eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?),
5134            None => None,
5135        };
5136        match fn_name.as_str() {
5137            // v7.39 (read01 round 76) — `jsonb_populate_record(NULL::t, j)` /
5138            // `…_recordset` (+ json_ variants). The row shape is the BASE
5139            // argument's declared type — a table's or a composite type's
5140            // column list — which only the catalog knows, so the parser hands
5141            // the raw arguments here rather than desugaring blind.
5142            "jsonb_populate_record"
5143            | "json_populate_record"
5144            | "jsonb_populate_recordset"
5145            | "json_populate_recordset" => {
5146                let type_name = match args.first() {
5147                    Some(Expr::Cast {
5148                        target: spg_sql::ast::CastTarget::Named(n),
5149                        ..
5150                    }) => n.clone(),
5151                    _ => {
5152                        return Err(EngineError::Unsupported(alloc::format!(
5153                            "{fn_name}(): first argument must name a row type, \
5154                             e.g. NULL::mytable"
5155                        )));
5156                    }
5157                };
5158                let cat = self.active_catalog();
5159                let cols: alloc::vec::Vec<ColumnSchema> = if let Some(t) = cat.get(&type_name) {
5160                    t.schema().columns.clone()
5161                } else if let Some(c) = cat.composite_types().get(&type_name) {
5162                    c.fields
5163                        .iter()
5164                        .map(|(n, ty)| ColumnSchema::new(n.clone(), *ty, true))
5165                        .collect()
5166                } else {
5167                    return Err(EngineError::Unsupported(alloc::format!(
5168                        "type \"{type_name}\" does not exist"
5169                    )));
5170                };
5171                let json_arg = match args.get(1) {
5172                    Some(e) => eval::eval_expr(e, &dummy_row, &ctx).map_err(EngineError::Eval)?,
5173                    None => Value::Null,
5174                };
5175                // The set form iterates the JSON array; the scalar form is
5176                // the one-element case of the same walk.
5177                let docs: alloc::vec::Vec<Value<'static>> = if fn_name.ends_with("recordset") {
5178                    crate::json::array_element_rows(&json_arg, false, fn_name)
5179                        .map_err(EngineError::Eval)?
5180                        .into_iter()
5181                        .map(|s| s.map_or(Value::Null, Value::json))
5182                        .collect()
5183                } else if matches!(json_arg, Value::Null) {
5184                    alloc::vec::Vec::new()
5185                } else {
5186                    alloc::vec![json_arg]
5187                };
5188                let mut rows = alloc::vec::Vec::with_capacity(docs.len());
5189                for doc in &docs {
5190                    let mut vals = alloc::vec::Vec::with_capacity(cols.len());
5191                    for c in &cols {
5192                        // `->>` semantics: a missing key is NULL, present keys
5193                        // arrive as text and cast to the declared column type.
5194                        let raw = crate::json::path_get(doc, &Value::text(c.name.clone()), true)
5195                            .map_err(EngineError::Eval)?;
5196                        let v = if matches!(raw, Value::Null) {
5197                            Value::Null
5198                        } else {
5199                            crate::conversions::coerce_value(raw, c.ty, "", 0)
5200                                .map_err(|e| EngineError::Unsupported(alloc::format!("{e:?}")))?
5201                        };
5202                        vals.push(v);
5203                    }
5204                    rows.push(Row::new(vals));
5205                }
5206                Ok((rows, cols))
5207            }
5208            // 7.38.1 S5.1 (pg_dump wall #3) — pg_options_to_table:
5209            // a text[] of 'name=value' reloptions/fdw options → one
5210            // (option_name, option_value) row per element. NULL or an
5211            // empty array yields zero rows (PG); an element without
5212            // '=' carries a NULL option_value, matching PG's split.
5213            "pg_options_to_table" => {
5214                let schema = alloc::vec![
5215                    ColumnSchema::new("option_name", DataType::Text, true),
5216                    ColumnSchema::new("option_value", DataType::Text, true),
5217                ];
5218                let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5219                if let Some(Value::TextArray(items)) = arg0 {
5220                    for item in items.into_iter().flatten() {
5221                        let (name, value) = match item.split_once('=') {
5222                            Some((n, v)) => (Value::text(n), Value::text(v)),
5223                            None => (Value::text(item.as_str()), Value::Null),
5224                        };
5225                        rows.push(Row::new(alloc::vec![name, value]));
5226                    }
5227                }
5228                Ok((rows, schema))
5229            }
5230            // 7.38.1 S5.1 (pg_dump wall) — pg_get_sequence_data(oid):
5231            // PG18's per-sequence state SRF, (last_value, is_called).
5232            // pg_dump reads it joined to pg_sequence for every dumped
5233            // sequence's setval line. The oid resolves through the
5234            // same relation_oid mapping seqrelid publishes.
5235            "pg_get_sequence_data" => {
5236                let schema = alloc::vec![
5237                    ColumnSchema::new("last_value", DataType::BigInt, false),
5238                    ColumnSchema::new("is_called", DataType::Bool, false),
5239                ];
5240                let want = match arg0 {
5241                    Some(Value::Int(n)) => i64::from(n),
5242                    Some(Value::BigInt(n)) => n,
5243                    _ => {
5244                        return Err(EngineError::Unsupported(
5245                            "pg_get_sequence_data(): argument must be a sequence oid".into(),
5246                        ));
5247                    }
5248                };
5249                let cat = self.active_catalog();
5250                let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::new();
5251                for (name, def) in cat.sequences_all() {
5252                    if crate::system_catalog::relation_oid(cat, name) == Some(want) {
5253                        rows.push(Row::new(alloc::vec![
5254                            Value::BigInt(def.last_value),
5255                            Value::Bool(def.is_called),
5256                        ]));
5257                        break;
5258                    }
5259                }
5260                Ok((rows, schema))
5261            }
5262            "pg_partition_tree" => {
5263                let cols = alloc::vec![
5264                    ColumnSchema::new("relid".to_string(), DataType::Text, true),
5265                    ColumnSchema::new("parentrelid".to_string(), DataType::Text, true),
5266                    ColumnSchema::new("isleaf".to_string(), DataType::Bool, true),
5267                    ColumnSchema::new("level".to_string(), DataType::Int, true),
5268                ];
5269                let Some(Value::Text(name)) = &arg0 else {
5270                    // NULL (or missing) argument → zero rows (PG).
5271                    return Ok((alloc::vec::Vec::new(), cols));
5272                };
5273                let entries = crate::partition_walks::tree_of(self.active_catalog(), name.as_ref());
5274                if entries.is_empty() && self.active_catalog().get(name.as_ref()).is_none() {
5275                    return Err(EngineError::Unsupported(alloc::format!(
5276                        "relation \"{name}\" does not exist"
5277                    )));
5278                }
5279                let rows = entries
5280                    .into_iter()
5281                    .map(|(relid, parent, isleaf, level)| {
5282                        Row::new(alloc::vec![
5283                            Value::text(relid),
5284                            parent.map_or(Value::Null, Value::text),
5285                            Value::Bool(isleaf),
5286                            #[allow(clippy::cast_possible_truncation)]
5287                            Value::Int(level as i32),
5288                        ])
5289                    })
5290                    .collect();
5291                Ok((rows, cols))
5292            }
5293            "pg_partition_ancestors" => {
5294                let cols =
5295                    alloc::vec![ColumnSchema::new("relid".to_string(), DataType::Text, true)];
5296                let Some(Value::Text(name)) = &arg0 else {
5297                    return Ok((alloc::vec::Vec::new(), cols));
5298                };
5299                let cat = self.active_catalog();
5300                if cat.get(name.as_ref()).is_none() {
5301                    return Err(EngineError::Unsupported(alloc::format!(
5302                        "relation \"{name}\" does not exist"
5303                    )));
5304                }
5305                // A relation outside any partition tree yields no rows (PG).
5306                let in_tree = cat
5307                    .get(name.as_ref())
5308                    .is_some_and(|t| t.schema().partition_role.is_some());
5309                let rows = if in_tree {
5310                    crate::partition_walks::ancestors_of(cat, name.as_ref())
5311                        .into_iter()
5312                        .map(|n| Row::new(alloc::vec![Value::text(n)]))
5313                        .collect()
5314                } else {
5315                    alloc::vec::Vec::new()
5316                };
5317                Ok((rows, cols))
5318            }
5319            // v7.39 (round 651) — `ts_debug(config, text)`: what the parser
5320            // saw, what each token was called, which dictionary took it
5321            // and what came out. It is a projection of the same tokenizer
5322            // and the same map the indexer uses, so it cannot describe a
5323            // pipeline other than the one that runs.
5324            "ts_debug" => {
5325                use crate::fts::{TokenType, TsDict};
5326                let cols = alloc::vec![
5327                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5328                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5329                    ColumnSchema::new("token".to_string(), DataType::Text, false),
5330                    ColumnSchema::new("dictionaries".to_string(), DataType::TextArray, false),
5331                    ColumnSchema::new("dictionary".to_string(), DataType::Text, true),
5332                    ColumnSchema::new("lexemes".to_string(), DataType::TextArray, true),
5333                ];
5334                // PG's one-arg form uses the session configuration; the
5335                // two-arg form names one.
5336                let (cfg_name, text) = match (&arg0, args.get(1)) {
5337                    (Some(Value::Text(c)), Some(t)) => {
5338                        let v = eval::eval_expr(t, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5339                        (c.to_string(), crate::eval::value_to_text(&v))
5340                    }
5341                    (Some(v), None) => (
5342                        alloc::string::String::from("english"),
5343                        crate::eval::value_to_text(v),
5344                    ),
5345                    _ => return Ok((alloc::vec::Vec::new(), cols)),
5346                };
5347                let english = match cfg_name
5348                    .trim()
5349                    .trim_start_matches("pg_catalog.")
5350                    .to_ascii_lowercase()
5351                    .as_str()
5352                {
5353                    "english" => true,
5354                    "simple" => false,
5355                    other => {
5356                        return Err(EngineError::Unsupported(alloc::format!(
5357                            "text search configuration \"{other}\" does not exist"
5358                        )));
5359                    }
5360                };
5361                let rows = crate::fts::tokenize_typed(&text)
5362                    .into_iter()
5363                    .map(|tok| {
5364                        let dict = tok.ty.dictionary(english);
5365                        let dname = dict.map(|d| match d {
5366                            TsDict::Simple => "simple",
5367                            TsDict::EnglishStem => "english_stem",
5368                        });
5369                        let folded = tok.text.to_lowercase();
5370                        let lexemes = dict.map(|d| match d {
5371                            TsDict::Simple => alloc::vec![Some(folded.clone())],
5372                            TsDict::EnglishStem => {
5373                                if crate::fts::is_english_stopword(&folded) {
5374                                    alloc::vec::Vec::new()
5375                                } else {
5376                                    alloc::vec![Some(crate::fts::porter_stem(&folded))]
5377                                }
5378                            }
5379                        });
5380                        Row::new(alloc::vec![
5381                            Value::text(tok.ty.alias()),
5382                            Value::text(tok.ty.description()),
5383                            Value::text(tok.text),
5384                            Value::TextArray(
5385                                dname
5386                                    .map(|n| alloc::vec![Some(alloc::string::String::from(n))])
5387                                    .unwrap_or_default(),
5388                            ),
5389                            dname.map_or(Value::Null, Value::text),
5390                            lexemes.map_or(Value::Null, Value::TextArray),
5391                        ])
5392                    })
5393                    .collect();
5394                let _ = TokenType::AsciiWord;
5395                Ok((rows, cols))
5396            }
5397            // v7.39 (round 651) — `ts_token_type('default')`, the list the
5398            // parser actually produces. It is a projection of the
5399            // `TokenType` enum the tokenizer and `pg_ts_config_map` both
5400            // read, so the three cannot disagree about what a token is.
5401            "ts_token_type" => {
5402                use crate::fts::TokenType as T;
5403                let cols = alloc::vec![
5404                    ColumnSchema::new("tokid".to_string(), DataType::Int, false),
5405                    ColumnSchema::new("alias".to_string(), DataType::Text, false),
5406                    ColumnSchema::new("description".to_string(), DataType::Text, false),
5407                ];
5408                // PG takes the parser by name or oid; SPG has the one.
5409                if let Some(Value::Text(p)) = &arg0
5410                    && !p.eq_ignore_ascii_case("default")
5411                    && !p.eq_ignore_ascii_case("pg_catalog.default")
5412                {
5413                    return Err(EngineError::Unsupported(alloc::format!(
5414                        "text search parser \"{p}\" does not exist"
5415                    )));
5416                }
5417                const TYPES: &[T] = &[
5418                    T::AsciiWord,
5419                    T::Word,
5420                    T::NumWord,
5421                    T::Email,
5422                    T::Url,
5423                    T::Host,
5424                    T::SFloat,
5425                    T::Version,
5426                    T::HwordNumPart,
5427                    T::HwordPart,
5428                    T::HwordAsciiPart,
5429                    T::Blank,
5430                    T::Tag,
5431                    T::Protocol,
5432                    T::NumHword,
5433                    T::AsciiHword,
5434                    T::Hword,
5435                    T::UrlPath,
5436                    T::File,
5437                    T::Float,
5438                    T::Int,
5439                    T::Uint,
5440                    T::Entity,
5441                ];
5442                let rows = TYPES
5443                    .iter()
5444                    .map(|t| {
5445                        Row::new(alloc::vec![
5446                            Value::Int(*t as i32),
5447                            Value::text(t.alias()),
5448                            Value::text(t.description()),
5449                        ])
5450                    })
5451                    .collect();
5452                Ok((rows, cols))
5453            }
5454            // v7.39 (read01 round 65) — a set-returning USER function in FROM
5455            // (`FROM rows_of(2)`). Its body runs through the real executor, like
5456            // every other function body since round 63.
5457            other => {
5458                if !self.active_catalog().functions_named(other).is_empty() {
5459                    return self.exec_setof_user_function(other, args, primary.alias.as_deref());
5460                }
5461                Err(EngineError::Unsupported(alloc::format!(
5462                    "table function {other}() is not supported in FROM"
5463                )))
5464            }
5465        }
5466    }
5467
5468    /// v7.39 (read01 round 65) — run a `RETURNS SETOF <type>` / `RETURNS
5469    /// TABLE(…)` function in FROM position. The body is a SELECT; the arguments
5470    /// are bound into it as literals and it goes through the read path, so the
5471    /// rows it yields are exactly the rows a hand-written query would see.
5472    ///
5473    /// The column NAMES come from the declared shape: `RETURNS TABLE(id int, v
5474    /// text)` names them, and a `SETOF <scalar>` yields a single column named
5475    /// after the function — PG's rule, and what a bare `SELECT * FROM f()`
5476    /// shows.
5477    fn exec_setof_user_function(
5478        &self,
5479        name: &str,
5480        args: &[spg_sql::ast::Expr],
5481        // v7.39 (read01 round 65) — `FROM evens() AS x` names the single column
5482        // `x`: for a scalar SETOF, the table alias IS the column name (PG).
5483        alias: Option<&str>,
5484    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5485        // The call's arguments belong to the ENCLOSING query, so they are
5486        // evaluated here and the body sees values.
5487        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5488        let arg_ctx = self.ev_ctx(&empty, None);
5489        let dummy = Row::new(alloc::vec::Vec::new());
5490        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
5491        for a in args {
5492            vals.push(eval::eval_expr(a, &dummy, &arg_ctx).map_err(EngineError::Eval)?);
5493        }
5494        self.setof_rows_of(name, &vals, alias)
5495    }
5496
5497    /// v7.39 (read01 round 67) — the set-returning core, on already-evaluated
5498    /// arguments. Shared by the FROM position and the target-list expansion, so
5499    /// a function cannot behave differently depending on where it is called.
5500    pub(crate) fn setof_rows_of(
5501        &self,
5502        name: &str,
5503        arg_values: &[Value<'static>],
5504        alias: Option<&str>,
5505    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
5506        let cat = self.active_catalog();
5507        let overloads = cat.functions_named(name);
5508        let def = overloads
5509            .iter()
5510            .find(|f| spg_storage::function_arg_types(&f.args_repr).len() == arg_values.len())
5511            .ok_or_else(|| {
5512                EngineError::Unsupported(alloc::format!(
5513                    "function {name} does not exist with {} argument(s)",
5514                    arg_values.len()
5515                ))
5516            })?;
5517        let declared = def.returns.trim().to_string();
5518        let upper = declared.to_ascii_uppercase();
5519        if !upper.starts_with("SETOF") && !upper.starts_with("TABLE(") {
5520            return Err(EngineError::Unsupported(alloc::format!(
5521                "function {name}() does not return a set — it cannot be used in FROM"
5522            )));
5523        }
5524
5525        let arg_names_pl = spg_storage::function_arg_names(&def.args_repr);
5526        // v7.39 (read01 round 66) — a plpgsql SETOF body builds its rows with
5527        // RETURN NEXT / RETURN QUERY; the interpreter collects them.
5528        if def.language.eq_ignore_ascii_case("plpgsql") {
5529            let out_rows = self
5530                .call_plpgsql_setof_fn(def, &arg_names_pl, arg_values)
5531                .map_err(EngineError::Eval)?;
5532            let cols = setof_column_shape(&declared, name, alias, out_rows.first());
5533            let rows = out_rows.into_iter().map(Row::new).collect();
5534            return Ok((rows, cols));
5535        }
5536        let body = def.body.trim().trim_end_matches(';');
5537        let stmt = spg_sql::parser::parse_statement(body).map_err(|e| {
5538            EngineError::Unsupported(alloc::format!("function {name} body does not parse: {e}"))
5539        })?;
5540        let spg_sql::ast::Statement::Select(body_select) = stmt else {
5541            return Err(EngineError::Unsupported(alloc::format!(
5542                "function {name}(): a set-returning body must be a SELECT"
5543            )));
5544        };
5545        let arg_names = spg_storage::function_arg_names(&def.args_repr);
5546        let bound = crate::eval::bind_user_fn_args(
5547            self.active_catalog(),
5548            &body_select,
5549            &arg_names,
5550            arg_values,
5551        )
5552        .map_err(EngineError::Eval)?;
5553        let out = self.exec_select_cancel(&bound, crate::CancelToken::none())?;
5554        let QueryResult::Rows { columns, rows } = out else {
5555            return Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new()));
5556        };
5557        // Name the columns from the DECLARED shape — the same rule the plpgsql
5558        // path above uses, so a body's language cannot change the row shape.
5559        let cols = setof_column_shape_from(&declared, name, alias, &columns);
5560        Ok((rows, cols))
5561    }
5562
5563    fn exec_select_jsonb_each_text(
5564        &self,
5565        stmt: &SelectStatement,
5566        primary: &TableRef,
5567        cancel: CancelToken<'_>,
5568    ) -> Result<QueryResult, EngineError> {
5569        let (each_fn, arg_expr) = primary
5570            .jsonb_each_text_arg
5571            .as_ref()
5572            .map(|(name, expr)| (name.as_str(), expr.as_ref()))
5573            .expect("caller guards jsonb_each_text_arg.is_some()");
5574        // v7.37.17 (17.6 siblings) — the plain jsonb_each / json_each
5575        // forms keep JSON rendering in the value column (JSON null
5576        // stays jsonb 'null', strings keep their quotes).
5577        let as_text = each_fn.ends_with("_text");
5578        let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
5579        let ctx = EvalContext::new(&empty_schema, None);
5580        let dummy_row = Row::new(alloc::vec::Vec::new());
5581        let arg_value = eval::eval_expr(arg_expr, &dummy_row, &ctx).map_err(EngineError::Eval)?;
5582        let pairs =
5583            crate::json::each_rows(&arg_value, as_text, each_fn).map_err(EngineError::Eval)?;
5584        let rows: alloc::vec::Vec<Row<'static>> = pairs
5585            .into_iter()
5586            .map(|(k, v)| {
5587                let key_val = Value::text(k);
5588                let value_val = match v {
5589                    Some(s) if as_text => Value::text(s),
5590                    Some(s) => Value::Json(alloc::borrow::Cow::Owned(s)),
5591                    None => Value::Null,
5592                };
5593                Row::new(alloc::vec![key_val, value_val])
5594            })
5595            .collect();
5596        let alias = primary.alias.clone().unwrap_or_else(|| each_fn.to_string());
5597        let value_dtype = if as_text {
5598            spg_storage::DataType::Text
5599        } else {
5600            spg_storage::DataType::Json
5601        };
5602        let key_col = ColumnSchema::new("key".to_string(), spg_storage::DataType::Text, false);
5603        let value_col = ColumnSchema::new("value".to_string(), value_dtype, as_text);
5604        let mut schema_cols = alloc::vec![key_col, value_col];
5605        // `AS t(k, v)` renames key/value positionally (PG behaviour); the
5606        // LATERAL-position form of the same call already honours it.
5607        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5608            if let Some(col) = schema_cols.get_mut(i) {
5609                col.name = new_name.clone();
5610            }
5611        }
5612        // v7.39 (read01 round 54) — `ev_ctx` threads the catalog; a bare
5613        // `EvalContext::new` drops it and every catalog-dependent cast
5614        // (regclass / enum / composite / domain) silently degrades.
5615        let scan_ctx = self.ev_ctx(&schema_cols, Some(&alias));
5616        // WHERE.
5617        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5618            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5619            for row in rows {
5620                cancel.check()?;
5621                let v = eval::eval_expr(w, &row, &scan_ctx).map_err(EngineError::Eval)?;
5622                if matches!(v, Value::Bool(true)) {
5623                    out.push(row);
5624                }
5625            }
5626            out
5627        } else {
5628            rows
5629        };
5630        // Aggregate dispatch (e.g. SELECT COUNT(*) FROM jsonb_each_text…).
5631        if aggregate::uses_aggregate(stmt) {
5632            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5633            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5634                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5635                    .map_err(|err| match err {
5636                        EngineError::Eval(ev) => ev,
5637                        other => eval::EvalError::TypeMismatch {
5638                            detail: alloc::format!("{other}"),
5639                        },
5640                    })
5641            };
5642            // v7.39 (round 656) — hand the rows over as they are rather than
5643            // collecting a second vector of `RowRef` wrappers. Note this is
5644            // a set-returning-function path, NOT the relational scan: the
5645            // measured O(rows) cost lived in `run_single_table_aggregate`,
5646            // and converting these four first was a miss that cost a full
5647            // round — every test stayed green and the number did not move.
5648            let agg = aggregate::run(
5649                stmt,
5650                crate::join::AggRows::Owned(&filtered),
5651                &schema_cols,
5652                Some(&alias),
5653                Some(&agg_correlated),
5654                self.parallel_runner.0.as_deref(),
5655                Some(self.active_catalog()),
5656                Some(self),
5657            )?;
5658            return self.finish_agg_result(agg, stmt, cancel);
5659        }
5660        // Projection.
5661        let projection = build_projection(
5662            &stmt.items,
5663            &schema_cols,
5664            &alias,
5665            self.backslash_escapes,
5666            Some(self.active_catalog()),
5667        )?;
5668        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5669            alloc::vec::Vec::with_capacity(filtered.len());
5670        for row in &filtered {
5671            let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5672            for p in &projection {
5673                let v = eval::eval_expr(&p.expr, row, &scan_ctx).map_err(EngineError::Eval)?;
5674                vals.push(v);
5675            }
5676            projected_rows.push(Row::new(vals));
5677        }
5678        let columns: alloc::vec::Vec<ColumnSchema> = projection
5679            .iter()
5680            // v7.39 (read01 round 54) — keep the column's enum identity through
5681            // the projection (it lives outside the DataType lattice), or a
5682            // derived table / UNION / windowed result forgets it and any outer
5683            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5684            .map(|p| p.to_column_schema())
5685            .collect();
5686        // ORDER BY.
5687        if !stmt.order_by.is_empty() {
5688            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = filtered
5689                .iter()
5690                .enumerate()
5691                .map(|(i, r)| -> Result<_, EngineError> {
5692                    let keys: Result<Vec<Value<'static>>, EngineError> = stmt
5693                        .order_by
5694                        .iter()
5695                        .map(|ob| {
5696                            eval::eval_expr(&ob.expr, r, &scan_ctx).map_err(EngineError::Eval)
5697                        })
5698                        .collect();
5699                    Ok((i, keys?))
5700                })
5701                .collect::<Result<_, _>>()?;
5702            indexed.sort_by(|a, b| {
5703                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5704                    let o = &stmt.order_by[idx];
5705                    let cmp = order_by_value_cmp_in(
5706                        o.desc,
5707                        o.nulls_first,
5708                        ka,
5709                        kb,
5710                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5711                    );
5712                    if cmp != core::cmp::Ordering::Equal {
5713                        return cmp;
5714                    }
5715                }
5716                core::cmp::Ordering::Equal
5717            });
5718            projected_rows = indexed
5719                .into_iter()
5720                .map(|(i, _)| projected_rows[i].clone())
5721                .collect();
5722        }
5723        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
5724        if stmt.distinct {
5725            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
5726            // spec folds EVERY text position, so a column declared
5727            // `COLLATE utf8mb4_bin` had its values merged here exactly the
5728            // way 3b494b6e fixed on the main scan path. The projection is
5729            // already in scope at each of these sites, so the mask needs no
5730            // new plumbing -- it was simply never asked for.
5731            projected_rows = dedup_rows(
5732                projected_rows,
5733                FoldSpec::of_masks(
5734                    scan_ctx.mysql_dialect,
5735                    &fold_mask(&projection),
5736                    &pad_mask(&projection),
5737                ),
5738            );
5739        }
5740        if let Some(offset) = stmt.offset_literal() {
5741            let off = (offset as usize).min(projected_rows.len());
5742            projected_rows.drain(..off);
5743        }
5744        if let Some(limit) = stmt.limit_literal() {
5745            projected_rows.truncate(limit as usize);
5746        }
5747        Ok(QueryResult::Rows {
5748            columns,
5749            rows: projected_rows,
5750        })
5751    }
5752
5753    /// v7.37.17 (17.6 siblings) — execute `SELECT … FROM
5754    /// ( SELECT … ) alias` in primary position. The inner SELECT
5755    /// materialises once through the regular bare-select executor
5756    /// (UNION tails included), then the outer WHERE / aggregate /
5757    /// projection / ORDER BY / LIMIT pipeline runs over the
5758    /// synthetic table — the same post-materialisation shape as
5759    /// exec_select_jsonb_each_text, generalised to N columns.
5760    fn exec_select_derived(
5761        &self,
5762        stmt: &SelectStatement,
5763        primary: &TableRef,
5764        cancel: CancelToken<'_>,
5765    ) -> Result<QueryResult, EngineError> {
5766        let inner = primary
5767            .lateral_subquery
5768            .as_deref()
5769            .expect("caller guards lateral_subquery.is_some()");
5770        // exec_select_cancel is the union-aware wrapper — the inner
5771        // SELECT may carry UNION tails on stmt.unions.
5772        let QueryResult::Rows {
5773            columns: inner_cols,
5774            rows,
5775        } = self.exec_select_cancel(inner, cancel)?
5776        else {
5777            return Err(EngineError::Unsupported(
5778                "derived table subquery must return rows".into(),
5779            ));
5780        };
5781        let alias = primary
5782            .alias
5783            .clone()
5784            .unwrap_or_else(|| primary.name.clone());
5785        // `AS t(a, b)` renames the materialised columns positionally
5786        // (extra inner columns keep their own names, PG behaviour).
5787        let mut schema_cols: alloc::vec::Vec<ColumnSchema> = inner_cols;
5788        // v7.39 (read01 round 78) — a column-alias list longer than the item is
5789        // the error PG reports; SPG used to let the extra names through and then
5790        // fail two layers downstream with "column not found: <the extra name>".
5791        let n_out = schema_cols.len() + usize::from(primary.with_ordinality);
5792        if primary.unnest_column_aliases.len() > n_out {
5793            return Err(EngineError::Unsupported(alloc::format!(
5794                "table \"{alias}\" has {n_out} columns available but {} columns specified",
5795                primary.unnest_column_aliases.len()
5796            )));
5797        }
5798        if primary.scalar_fn_item && schema_cols.len() == 1 {
5799            schema_cols[0].scalar_row_source = true;
5800        }
5801        // v7.39 (read01 round 78) — WITH ORDINALITY on a table function that
5802        // rides this channel (regexp_matches): a trailing bigint counter, 1-based.
5803        // The column-alias list, if given, names it like any other column.
5804        let mut rows = rows;
5805        if primary.with_ordinality {
5806            schema_cols.push(ColumnSchema::new(
5807                "ordinality".to_string(),
5808                DataType::BigInt,
5809                false,
5810            ));
5811            rows = rows
5812                .into_iter()
5813                .enumerate()
5814                .map(|(i, r)| {
5815                    let mut v = r.values;
5816                    #[allow(clippy::cast_possible_wrap)]
5817                    v.push(Value::BigInt(i as i64 + 1));
5818                    Row::new(v)
5819                })
5820                .collect();
5821        }
5822        for (i, new_name) in primary.unnest_column_aliases.iter().enumerate() {
5823            if let Some(col) = schema_cols.get_mut(i) {
5824                col.name = new_name.clone();
5825            }
5826        }
5827        self.exec_select_over_rows(stmt, rows, schema_cols, &alias, cancel)
5828    }
5829
5830    /// v7.39 (read01 partitionfuncs.c) — shared synthetic-source SELECT
5831    /// pipeline (WHERE / aggregate / projection / ORDER BY / DISTINCT /
5832    /// OFFSET / LIMIT) over a pre-materialised row set. Drives the
5833    /// derived-table executor and the FROM-position table functions.
5834    fn exec_select_over_rows(
5835        &self,
5836        stmt: &SelectStatement,
5837        rows: alloc::vec::Vec<Row<'static>>,
5838        schema_cols: alloc::vec::Vec<ColumnSchema>,
5839        alias: &str,
5840        cancel: CancelToken<'_>,
5841    ) -> Result<QueryResult, EngineError> {
5842        let scan_ctx = self.ev_ctx(&schema_cols, Some(alias));
5843        // v7.37 D.21 — correlated subqueries in the WHERE / projection may
5844        // reference this derived table's columns (`… WHERE u.gg = t.g` where t
5845        // is `(VALUES …) t`). Resolve them per-row via eval_expr_with_correlated
5846        // (the same path the aggregate branch uses); the old plain eval_expr let
5847        // a ScalarSubquery reach row-eval unresolved ("engine resolver bug").
5848        let corr_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5849        // WHERE.
5850        let filtered: alloc::vec::Vec<Row<'static>> = if let Some(w) = &stmt.where_ {
5851            let mut out = alloc::vec::Vec::with_capacity(rows.len());
5852            for row in rows {
5853                cancel.check()?;
5854                let v = self.eval_expr_with_correlated(
5855                    w,
5856                    &row,
5857                    &scan_ctx,
5858                    cancel,
5859                    Some(&mut corr_memo.borrow_mut()),
5860                )?;
5861                if matches!(v, Value::Bool(true)) {
5862                    out.push(row);
5863                }
5864            }
5865            out
5866        } else {
5867            rows
5868        };
5869        // Aggregate dispatch.
5870        if aggregate::uses_aggregate(stmt) {
5871            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
5872            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
5873                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
5874                    .map_err(|err| match err {
5875                        EngineError::Eval(ev) => ev,
5876                        other => eval::EvalError::TypeMismatch {
5877                            detail: alloc::format!("{other}"),
5878                        },
5879                    })
5880            };
5881            // v7.39 (round 656) — hand the rows over as they are rather than
5882            // collecting a second vector of `RowRef` wrappers. Note this is
5883            // a set-returning-function path, NOT the relational scan: the
5884            // measured O(rows) cost lived in `run_single_table_aggregate`,
5885            // and converting these four first was a miss that cost a full
5886            // round — every test stayed green and the number did not move.
5887            let agg = aggregate::run(
5888                stmt,
5889                crate::join::AggRows::Owned(&filtered),
5890                &schema_cols,
5891                Some(alias),
5892                Some(&agg_correlated),
5893                self.parallel_runner.0.as_deref(),
5894                Some(self.active_catalog()),
5895                Some(self),
5896            )?;
5897            return self.finish_agg_result(agg, stmt, cancel);
5898        }
5899        // Projection.
5900        let projection = build_projection(
5901            &stmt.items,
5902            &schema_cols,
5903            alias,
5904            self.backslash_escapes,
5905            Some(self.active_catalog()),
5906        )?;
5907        // v7.39 (round 621) — a target-list SRF expands here too. This tail
5908        // serves VALUES, a derived table and `ROWS FROM (…)`, and knew nothing
5909        // about them: `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4)) v(x)`
5910        // answered `function unnest(integer[]) does not exist` for a query PG
5911        // answers.
5912        let srf_idxs = self.srf_target_idxs(&projection);
5913        let mut src_of_row: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
5914        let mut projected_rows: alloc::vec::Vec<Row<'static>> =
5915            alloc::vec::Vec::with_capacity(filtered.len());
5916        if !srf_idxs.is_empty() {
5917            let (rows, src) =
5918                expand_projection_srfs(self, &projection, &srf_idxs, &filtered, &scan_ctx)?;
5919            projected_rows = rows;
5920            src_of_row = src;
5921        } else {
5922            for row in &filtered {
5923                let mut vals = alloc::vec::Vec::with_capacity(projection.len());
5924                for p in &projection {
5925                    let v = self.eval_expr_with_correlated(
5926                        &p.expr,
5927                        row,
5928                        &scan_ctx,
5929                        cancel,
5930                        Some(&mut corr_memo.borrow_mut()),
5931                    )?;
5932                    vals.push(v);
5933                }
5934                projected_rows.push(Row::new(vals));
5935            }
5936        }
5937        let columns: alloc::vec::Vec<ColumnSchema> = projection
5938            .iter()
5939            // v7.39 (read01 round 54) — keep the column's enum identity through
5940            // the projection (it lives outside the DataType lattice), or a
5941            // derived table / UNION / windowed result forgets it and any outer
5942            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
5943            .map(|p| p.to_column_schema())
5944            .collect();
5945        // ORDER BY over the source rows (same shape as the other
5946        // synthetic-table executors).
5947        // v7.39 (read01 round 80) — a positional key (`ORDER BY 1`) means the Nth
5948        // OUTPUT column. Evaluated as an expression, as it was here, the literal
5949        // `1` is just the constant 1: the same sort key for every row, so the
5950        // sort ran and changed nothing. `SELECT unnest(ARRAY['B','a','A','b'])
5951        // ORDER BY 1` (which the parser turns into `SELECT * FROM unnest(…)`,
5952        // landing on this executor) came back in input order.
5953        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
5954        if !order_by.is_empty() {
5955            // v7.39 (round 621) — one entry per OUTPUT row, since a target-list
5956            // SRF makes more of them than there were inputs.
5957            let out_cols = if srf_idxs.is_empty() {
5958                alloc::vec![None; order_by.len()]
5959            } else {
5960                srf_order_output_cols(&order_by, &projection)
5961            };
5962            let mut indexed: alloc::vec::Vec<(usize, Vec<Value<'static>>)> = projected_rows
5963                .iter()
5964                .enumerate()
5965                .map(|(k, out)| -> Result<_, EngineError> {
5966                    let r = &filtered[src_of_row.get(k).copied().unwrap_or(k)];
5967                    let keys: Result<Vec<Value<'static>>, EngineError> = order_by
5968                        .iter()
5969                        .zip(out_cols.iter())
5970                        .map(|(ob, oc)| {
5971                            // v7.39 (read01 round 54) — this path builds its
5972                            // sort keys itself instead of going through
5973                            // `build_order_keys`, so it skipped the enum-ordinal
5974                            // substitution: an OUTER `ORDER BY <enum col>` over
5975                            // a DERIVED TABLE sorted by the label TEXT, not by
5976                            // member order. Silently wrong rows, not an error.
5977                            let v = srf_order_key(ob, *oc, out, r, &scan_ctx)?;
5978                            Ok(
5979                                match crate::orderby::enum_order_ordinal(&ob.expr, &v, &scan_ctx) {
5980                                    Some(ord) => Value::Float(ord),
5981                                    None => v,
5982                                },
5983                            )
5984                        })
5985                        .collect();
5986                    Ok((k, keys?))
5987                })
5988                .collect::<Result<_, _>>()?;
5989            indexed.sort_by(|a, b| {
5990                for (idx, (ka, kb)) in a.1.iter().zip(b.1.iter()).enumerate() {
5991                    let o = &stmt.order_by[idx];
5992                    let cmp = order_by_value_cmp_in(
5993                        o.desc,
5994                        o.nulls_first,
5995                        ka,
5996                        kb,
5997                        scan_ctx.mysql_dialect && !crate::eval::is_binary_coerced(&o.expr),
5998                    );
5999                    if cmp != core::cmp::Ordering::Equal {
6000                        return cmp;
6001                    }
6002                }
6003                core::cmp::Ordering::Equal
6004            });
6005            projected_rows = indexed
6006                .into_iter()
6007                .map(|(i, _)| projected_rows[i].clone())
6008                .collect();
6009        }
6010        // v7.38 (read01) — DISTINCT over a synthetic source was dropped here.
6011        if stmt.distinct {
6012            // v7.38.14 — `FoldSpec::of`, not `::dialect`. The dialect-only
6013            // spec folds EVERY text position, so a column declared
6014            // `COLLATE utf8mb4_bin` had its values merged here exactly the
6015            // way 3b494b6e fixed on the main scan path. The projection is
6016            // already in scope at each of these sites, so the mask needs no
6017            // new plumbing -- it was simply never asked for.
6018            projected_rows = dedup_rows(
6019                projected_rows,
6020                FoldSpec::of_masks(
6021                    scan_ctx.mysql_dialect,
6022                    &fold_mask(&projection),
6023                    &pad_mask(&projection),
6024                ),
6025            );
6026        }
6027        if let Some(offset) = stmt.offset_literal() {
6028            let off = (offset as usize).min(projected_rows.len());
6029            projected_rows.drain(..off);
6030        }
6031        if let Some(limit) = stmt.limit_literal() {
6032            projected_rows.truncate(limit as usize);
6033        }
6034        Ok(QueryResult::Rows {
6035            columns,
6036            rows: projected_rows,
6037        })
6038    }
6039
6040    /// Constant `SELECT` with no FROM: evaluate each projection item
6041    /// once against an empty dummy row (`SELECT 1`, `SELECT '7'::INT`).
6042    fn exec_constant_select(&self, stmt: &SelectStatement) -> Result<QueryResult, EngineError> {
6043        let empty_schema: Vec<ColumnSchema> = Vec::new();
6044        let ctx = self.ev_ctx(&empty_schema, None);
6045        // v7.39 (read01 round 106) — an aggregate with no FROM runs over the
6046        // single implicit row (`SELECT count(*)` → 1, `SELECT sum(5)` → 5,
6047        // `SELECT string_agg('x',',')` → x). Before this it fell through to the
6048        // scalar projection, where the aggregate name looked like an unknown
6049        // function. The WHERE filters that one row, so `… WHERE false` leaves
6050        // the aggregate zero input rows (`count(*)` → 0).
6051        if aggregate::uses_aggregate(stmt) {
6052            let dummy = Row::new(Vec::new());
6053            let passes = match &stmt.where_ {
6054                Some(w) => matches!(eval::eval_expr(w, &dummy, &ctx)?, Value::Bool(true)),
6055                None => true,
6056            };
6057            let rows: Vec<RowRef<'_>> = if passes {
6058                alloc::vec![RowRef::Owned(&dummy)]
6059            } else {
6060                Vec::new()
6061            };
6062            let agg = aggregate::run(
6063                stmt,
6064                crate::join::AggRows::Refs(&rows),
6065                &empty_schema,
6066                None,
6067                None,
6068                self.parallel_runner.0.as_deref(),
6069                Some(self.active_catalog()),
6070                Some(self),
6071            )?;
6072            return self.finish_agg_result(agg, stmt, CancelToken::none());
6073        }
6074        let projection = build_projection(
6075            &stmt.items,
6076            &empty_schema,
6077            "",
6078            self.backslash_escapes,
6079            Some(self.active_catalog()),
6080        )?;
6081        // `SELECT … WHERE cond` with no FROM — the one conceptual
6082        // row survives only when the condition is true (previously
6083        // the WHERE was silently ignored: `SELECT 1 WHERE false`
6084        // returned a row).
6085        let dummy_row = Row::new(Vec::new());
6086        if let Some(w) = &stmt.where_ {
6087            let cond = eval::eval_expr(w, &dummy_row, &ctx)?;
6088            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
6089                let columns: Vec<ColumnSchema> = projection
6090                    .into_iter()
6091                    .map(|p| p.to_column_schema())
6092                    .collect();
6093                return Ok(QueryResult::Rows {
6094                    columns,
6095                    rows: Vec::new(),
6096                });
6097            }
6098        }
6099        // v7.38 (read01, T15) — a top-level SRF that the parser did NOT rewrite
6100        // into a FROM item (regexp_matches, whose rows are arrays and so cannot
6101        // desugar to unnest) expands here: one output row per SRF row, sibling
6102        // scalar columns repeated. unnest / array_elements / path_query reach a
6103        // real FROM via the parser rewrite and never land here.
6104        // v7.39 (read01 round 67) — every SRF in the list, in lockstep.
6105        let srf_idxs = self.srf_target_idxs(&projection);
6106        if !srf_idxs.is_empty() {
6107            let mut rows = expand_srf_row(self, &projection, &srf_idxs, &dummy_row, &ctx)?;
6108            let columns: Vec<ColumnSchema> = projection
6109                .into_iter()
6110                .map(|p| p.to_column_schema())
6111                .collect();
6112            // v7.39 (read01 round 80) — a FROM-less SELECT still has an ORDER BY,
6113            // an OFFSET and a LIMIT, and they apply to the rows the SRF expanded
6114            // to. This returned straight out of the expansion, so
6115            // `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came back in
6116            // input order — the sort was not wrong, it never ran. (There is
6117            // exactly one conceptual input row here, which is why the ordinary
6118            // scan pipeline is not on this path at all.)
6119            if !stmt.order_by.is_empty() {
6120                let synth_ctx =
6121                    EvalContext::new(&columns, None).with_catalog(self.active_catalog());
6122                let resolved: Vec<spg_sql::ast::OrderBy> = stmt
6123                    .order_by
6124                    .iter()
6125                    .map(|o| {
6126                        let mut o = o.clone();
6127                        if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
6128                            && *n >= 1
6129                            && let Ok(idx) = usize::try_from(*n - 1)
6130                            && idx < columns.len()
6131                        {
6132                            o.expr = Expr::Column(spg_sql::ast::ColumnName {
6133                                qualifier: None,
6134                                name: columns[idx].name.clone(),
6135                            });
6136                        }
6137                        o
6138                    })
6139                    .collect();
6140                let descs: Vec<bool> = resolved.iter().map(|o| o.desc).collect();
6141                let mut tagged: Vec<(Vec<OrderKey>, Row)> = Vec::with_capacity(rows.len());
6142                for r in rows {
6143                    let keys = build_order_keys(&resolved, &r, &synth_ctx)?;
6144                    tagged.push((keys, r));
6145                }
6146                sort_by_keys(&mut tagged, &descs);
6147                rows = tagged.into_iter().map(|(_, r)| r).collect();
6148            }
6149            apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6150            return Ok(QueryResult::Rows { columns, rows });
6151        }
6152        let mut values = Vec::with_capacity(projection.len());
6153        for p in &projection {
6154            values.push(eval::eval_expr(&p.expr, &dummy_row, &ctx)?);
6155        }
6156        let columns: Vec<ColumnSchema> = projection
6157            .into_iter()
6158            .map(|p| p.to_column_schema())
6159            .collect();
6160        // v7.39 (round 239) — the FROM-less scalar path ignored LIMIT and
6161        // OFFSET entirely, so `SELECT 1 LIMIT 0` returned its row where PG
6162        // returns none. (The SRF and aggregate arms above already applied
6163        // them; this tail was the one that didn't.)
6164        let mut rows = alloc::vec![Row::new(values)];
6165        apply_offset_and_limit(&mut rows, stmt.offset_literal(), stmt.limit_literal());
6166        Ok(QueryResult::Rows { columns, rows })
6167    }
6168
6169    /// v7.37.x (docker-fair INSUBQ attack) — pre-replacement short-
6170    /// circuit. Catches
6171    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (<uncorrelated subquery>)
6172    /// BEFORE `resolve_select_subqueries` materialises the inner result
6173    /// as `Vec<Expr::Literal>`. Runs the inner once, collects the
6174    /// values into a `HashSet<i64>` directly, then probes A.pk per
6175    /// HashSet entry and tallies. Saves the Expr-literal roundtrip
6176    /// (~150 µs / query at INSUBQ benchmark scale).
6177    pub(crate) fn try_count_star_pk_in_subquery_fast(
6178        &self,
6179        stmt: &SelectStatement,
6180        cancel: CancelToken<'_>,
6181    ) -> Result<Option<QueryResult>, EngineError> {
6182        use spg_sql::ast::SelectItem;
6183        if stmt.distinct
6184            || stmt.limit_with_ties
6185            || stmt.group_by.is_some()
6186            || stmt.having.is_some()
6187            || !stmt.unions.is_empty()
6188            || !stmt.order_by.is_empty()
6189            || stmt.limit.is_some()
6190            || stmt.offset.is_some()
6191            || stmt.items.len() != 1
6192        {
6193            return Ok(None);
6194        }
6195        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6196            return Ok(None);
6197        };
6198        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6199            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6200        if !is_count_star {
6201            return Ok(None);
6202        }
6203        let Some(from) = stmt.from.as_ref() else {
6204            return Ok(None);
6205        };
6206        if !from.joins.is_empty()
6207            || from.primary.lateral_subquery.is_some()
6208            || from.primary.unnest_expr.is_some()
6209            || from.primary.generate_series_args.is_some()
6210            || from.primary.table_fn_call.is_some()
6211            || from.primary.as_of_segment.is_some()
6212        {
6213            return Ok(None);
6214        }
6215        let Some(where_expr) = stmt.where_.as_ref() else {
6216            return Ok(None);
6217        };
6218        // The WHERE conjunct must be a bare `<col> IN (subquery)` with
6219        // negated=false; no other predicates.
6220        let Expr::InSubquery {
6221            expr: col_expr,
6222            subquery,
6223            negated: false,
6224        } = where_expr
6225        else {
6226            return Ok(None);
6227        };
6228        let Expr::Column(c) = col_expr.as_ref() else {
6229            return Ok(None);
6230        };
6231        let outer_alias = from
6232            .primary
6233            .alias
6234            .as_deref()
6235            .unwrap_or(from.primary.name.as_str());
6236        if let Some(q) = c.qualifier.as_deref()
6237            && !q.eq_ignore_ascii_case(outer_alias)
6238        {
6239            return Ok(None);
6240        }
6241        // Outer column must be a single-column PK on integer family.
6242        let catalog = self.active_catalog();
6243        let Some(outer_table) = catalog.get(from.primary.name.as_str()) else {
6244            return Ok(None);
6245        };
6246        let outer_schema = outer_table.schema();
6247        let Some(outer_pos) = outer_schema
6248            .columns
6249            .iter()
6250            .position(|s| s.name.eq_ignore_ascii_case(&c.name))
6251        else {
6252            return Ok(None);
6253        };
6254        if !matches!(
6255            outer_schema.columns[outer_pos].ty,
6256            spg_storage::DataType::BigInt
6257                | spg_storage::DataType::Int
6258                | spg_storage::DataType::SmallInt
6259        ) {
6260            return Ok(None);
6261        }
6262        if !outer_schema
6263            .uniqueness_constraints
6264            .iter()
6265            .any(|u| u.is_primary_key && u.columns.as_slice() == [outer_pos])
6266        {
6267            return Ok(None);
6268        }
6269        let Some(idx) = outer_table.index_on(outer_pos) else {
6270            return Ok(None);
6271        };
6272        // Inner must be uncorrelated. The cheap-correlation pre-check
6273        // exists upstream; here we just attempt the bare exec.
6274        if crate::subquery::select_is_correlated(subquery) {
6275            return Ok(None);
6276        }
6277        let mut inner = (**subquery).clone();
6278        self.resolve_select_subqueries(&mut inner, cancel)?;
6279        let r = match self.exec_bare_select_cancel(&inner, cancel) {
6280            Ok(r) => r,
6281            Err(_) => return Ok(None),
6282        };
6283        let QueryResult::Rows { columns, rows, .. } = r else {
6284            return Ok(None);
6285        };
6286        if columns.len() != 1 {
6287            return Ok(None);
6288        }
6289        // v7.37.43 (INSUBQ B-1) — inner-uniqueness check. If the inner
6290        // subquery projects a column known to be UNIQUE/PK on its table
6291        // (statically: `SELECT <col> FROM <tbl> WHERE …` where <col> is
6292        // in `tbl.uniqueness_constraints`), survivor values are
6293        // guaranteed distinct and the per-survivor `HashSet::insert`
6294        // dedup check is redundant. ~25 ns × N_inner-survivors saved.
6295        //
6296        // Inlined check — gated on: no DISTINCT/GROUP/UNION/JOIN, single
6297        // projection that is a bare Column ref, table-column lookup in
6298        // catalog confirms the column appears as a unique constraint's
6299        // sole member. UNIQUE NOT NULL is required — a nullable unique
6300        // column may have multiple NULLs, but NULLs are already skipped
6301        // above (`Value::Null => continue`), so a UNIQUE-only column is
6302        // still safe to dedup-skip.
6303        let inner_unique = (|| -> bool {
6304            if inner.distinct
6305                || inner.group_by.is_some()
6306                || !inner.unions.is_empty()
6307                || inner.having.is_some()
6308                || inner.items.len() != 1
6309            {
6310                return false;
6311            }
6312            let Some(inner_from) = inner.from.as_ref() else {
6313                return false;
6314            };
6315            if !inner_from.joins.is_empty()
6316                || inner_from.primary.lateral_subquery.is_some()
6317                || inner_from.primary.unnest_expr.is_some()
6318                || inner_from.primary.generate_series_args.is_some()
6319                || inner_from.primary.table_fn_call.is_some()
6320            {
6321                return false;
6322            }
6323            let SelectItem::Expr { expr: proj, .. } = &inner.items[0] else {
6324                return false;
6325            };
6326            let Expr::Column(pc) = proj else {
6327                return false;
6328            };
6329            let inner_alias = inner_from
6330                .primary
6331                .alias
6332                .as_deref()
6333                .unwrap_or(inner_from.primary.name.as_str());
6334            if let Some(q) = pc.qualifier.as_deref()
6335                && !q.eq_ignore_ascii_case(inner_alias)
6336            {
6337                return false;
6338            }
6339            let Some(inner_table) = catalog.get(inner_from.primary.name.as_str()) else {
6340                return false;
6341            };
6342            let isch = inner_table.schema();
6343            let Some(ipos) = isch
6344                .columns
6345                .iter()
6346                .position(|s| s.name.eq_ignore_ascii_case(&pc.name))
6347            else {
6348                return false;
6349            };
6350            isch.uniqueness_constraints
6351                .iter()
6352                .any(|u| u.columns.as_slice() == [ipos])
6353        })();
6354        // Collect inner i64 values directly into a HashSet, then probe.
6355        let mut count: i64 = 0;
6356        let mut probed = if inner_unique {
6357            hashbrown::HashSet::<i64>::new()
6358        } else {
6359            hashbrown::HashSet::<i64>::with_capacity(rows.len())
6360        };
6361        for row in &rows {
6362            let v = row.values.first().cloned().unwrap_or(Value::Null);
6363            let n = match v {
6364                Value::BigInt(n) => n,
6365                Value::Int(n) => i64::from(n),
6366                Value::SmallInt(n) => i64::from(n),
6367                Value::Null => continue,
6368                _ => return Ok(None),
6369            };
6370            // De-duplicate inner key set so a duplicate inner value
6371            // doesn't double-count the same outer row. Skipped when
6372            // the inner projection is statically unique.
6373            if !inner_unique && !probed.insert(n) {
6374                continue;
6375            }
6376            // v7.37.43 (INSUBQ B-2 + B-4) — direct i64 PK probe, skipping
6377            // the `IndexKey::from_value` enum-dispatch and the per-call
6378            // `IndexKey` wrapper construction. The outer column is
6379            // already gated to integer-family above, so an i64 key
6380            // always corresponds to a valid PK lookup.
6381            if !idx.lookup_eq_i64(n).is_empty() {
6382                count += 1;
6383            }
6384        }
6385        let columns_out = alloc::vec![ColumnSchema::new(
6386            "count".to_string(),
6387            spg_storage::DataType::BigInt,
6388            false,
6389        )];
6390        let rows_out = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6391        Ok(Some(QueryResult::Rows {
6392            columns: columns_out,
6393            rows: rows_out,
6394        }))
6395    }
6396
6397    /// v7.37.x (docker-fair INSUBQ attack) — short-circuit
6398    ///   SELECT COUNT(*) FROM A WHERE A.pk IN (literal list)
6399    /// (the post-subquery-replacement shape of the INSUBQ probe
6400    /// `SELECT COUNT(*) FROM A WHERE A.pk IN (SELECT k FROM B WHERE …)`).
6401    /// The general aggregate path materialises every seeked row into
6402    /// a `Vec<Cow<Row>>`, then runs the aggregate executor over it.
6403    /// For COUNT(*) we only care how many keys hit; iterate the list
6404    /// and tally `idx.lookup_eq(key)` non-empty results, skipping the
6405    /// row materialisation, the aggregate state machine, and the per-
6406    /// row WHERE re-eval (the seek already filtered by the same list).
6407    /// Returns `None` when the shape doesn't match.
6408    fn try_count_star_pk_in_list_fast(
6409        &self,
6410        stmt: &SelectStatement,
6411        table: &spg_storage::Table,
6412        schema_cols: &[ColumnSchema],
6413        alias: &str,
6414    ) -> Option<QueryResult> {
6415        use spg_sql::ast::{ColumnName, SelectItem};
6416        // Gates on the SELECT shape.
6417        if stmt.distinct
6418            || stmt.limit_with_ties
6419            || stmt.group_by.is_some()
6420            || stmt.having.is_some()
6421            || !stmt.unions.is_empty()
6422            || !stmt.order_by.is_empty()
6423            || stmt.limit.is_some()
6424            || stmt.offset.is_some()
6425            || stmt.items.len() != 1
6426        {
6427            return None;
6428        }
6429        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6430            return None;
6431        };
6432        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6433            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6434        if !is_count_star {
6435            return None;
6436        }
6437        // WHERE must be `<col> IN (literal list)` with no other
6438        // conjuncts (the seek result is a true subset of the row
6439        // population for this predicate).
6440        let where_expr = stmt.where_.as_ref()?;
6441        let Expr::InList {
6442            expr: col_expr,
6443            list,
6444            negated: false,
6445        } = where_expr
6446        else {
6447            return None;
6448        };
6449        let Expr::Column(c) = col_expr.as_ref() else {
6450            return None;
6451        };
6452        if let Some(q) = c.qualifier.as_deref()
6453            && !q.eq_ignore_ascii_case(alias)
6454        {
6455            return None;
6456        }
6457        let col_pos = schema_cols
6458            .iter()
6459            .position(|s| s.name.eq_ignore_ascii_case(&c.name))?;
6460        // The column must be a single-column PK on an integer family
6461        // — the same gate the SCALARSQ + LEFT-ANTI-JOIN fast paths use,
6462        // so the antiset stays collision-free under `HashSet<i64>`.
6463        let schema = table.schema();
6464        if !matches!(
6465            schema.columns[col_pos].ty,
6466            spg_storage::DataType::BigInt
6467                | spg_storage::DataType::Int
6468                | spg_storage::DataType::SmallInt
6469        ) {
6470            return None;
6471        }
6472        if !schema
6473            .uniqueness_constraints
6474            .iter()
6475            .any(|u| u.is_primary_key && u.columns.as_slice() == [col_pos])
6476        {
6477            return None;
6478        }
6479        let idx = table.index_on(col_pos)?;
6480        // Tally non-empty seek results across all literal values.
6481        let mut count: i64 = 0;
6482        for lit in list {
6483            let Expr::Literal(l) = lit else {
6484                return None;
6485            };
6486            // r1039 — through the shared resolver, so a literal spelled
6487            // in another type ('5' against an integer PK) is read as the
6488            // column's before it becomes a key. This tally answers from
6489            // the index alone, so a key in the wrong space would return a
6490            // COUNT of zero rather than fall back to a scan.
6491            let col = schema.columns.get(col_pos)?;
6492            let v = crate::index_access::literal_as_column_value(l, col, col_pos)?;
6493            let key = spg_storage::IndexKey::from_value_for_column(&v, col.ty)?;
6494            if !idx.lookup_eq(&key).is_empty() {
6495                count += 1;
6496            }
6497        }
6498        let columns = alloc::vec![ColumnSchema::new(
6499            "count".to_string(),
6500            spg_storage::DataType::BigInt,
6501            false,
6502        )];
6503        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6504        let _ = ColumnName {
6505            qualifier: None,
6506            name: String::new(),
6507        };
6508        Some(QueryResult::Rows { columns, rows })
6509    }
6510
6511    /// v7.38 (perf, exact-range count) — `SELECT count(*) FROM t WHERE <col>
6512    /// BETWEEN a AND b` on an indexed column. The index range walk yields
6513    /// exactly the matching (visible) rows, so we count locators directly —
6514    /// skipping the row materialisation, the aggregate state machine, and the
6515    /// per-row WHERE re-eval the general path pays. Turns the `range_count`
6516    /// endpoint from tied-with-PG (superset re-eval) into a clear win. None
6517    /// when the shape doesn't match.
6518    fn try_count_star_indexed_range_fast(
6519        &self,
6520        stmt: &SelectStatement,
6521        table: &spg_storage::Table,
6522        schema_cols: &[ColumnSchema],
6523        alias: &str,
6524        snapshot: &spg_storage::snapshot::Snapshot,
6525    ) -> Option<QueryResult> {
6526        use spg_sql::ast::SelectItem;
6527        if stmt.distinct
6528            || stmt.limit_with_ties
6529            || stmt.group_by.is_some()
6530            || stmt.having.is_some()
6531            || !stmt.unions.is_empty()
6532            || !stmt.order_by.is_empty()
6533            || stmt.limit.is_some()
6534            || stmt.offset.is_some()
6535            || stmt.items.len() != 1
6536        {
6537            return None;
6538        }
6539        let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
6540            return None;
6541        };
6542        let is_count_star = matches!(expr, Expr::FunctionCall { name, args }
6543            if name.eq_ignore_ascii_case("count_star") && args.is_empty());
6544        if !is_count_star {
6545            return None;
6546        }
6547        let where_expr = stmt.where_.as_ref()?;
6548        let count = crate::index_access::try_range_count(
6549            where_expr,
6550            schema_cols,
6551            table,
6552            alias,
6553            snapshot,
6554            self.backslash_escapes,
6555        )?;
6556        let columns = alloc::vec![ColumnSchema::new(
6557            "count".to_string(),
6558            spg_storage::DataType::BigInt,
6559            false,
6560        )];
6561        let rows = alloc::vec![Row::new(alloc::vec![Value::BigInt(count)])];
6562        Some(QueryResult::Rows { columns, rows })
6563    }
6564
6565    /// Single-table aggregate path: filter the (optionally index-seeked)
6566    /// rows, then hand off to the aggregate executor which does its own
6567    /// projection + ORDER BY before `finish_agg_result` applies LIMIT.
6568    fn run_single_table_aggregate<'a>(
6569        &self,
6570        stmt: &SelectStatement,
6571        table: &'a spg_storage::Table,
6572        schema_cols: &'a [ColumnSchema],
6573        alias: &str,
6574        indexed_rows: Option<crate::index_access::Seeked<'a>>,
6575        cancel: CancelToken<'_>,
6576    ) -> Result<QueryResult, EngineError> {
6577        // v7.38 (read01 U15) — per-scan sampler cell for TABLESAMPLE
6578        // REPEATABLE (see run_single_table_scan). Aggregates
6579        // (`count(*) FROM t TABLESAMPLE …`) filter through this ctx too.
6580        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6581        let ctx = self
6582            .ev_ctx(schema_cols, Some(alias))
6583            .with_sample_rng(&sample_cell);
6584        // v7.39 (round 657) — pre-sized. Pushing 500k pointers into a
6585        // `Vec::new()` walks the doubling chain 8, 16, … 262144, 524288,
6586        // and every abandoned buffer on the way stays resident: RSS is a
6587        // high-water mark, so the intermediates are paid for even though
6588        // they are freed. Round 656 measured the scan at 17 bytes/row
6589        // where the survivor list itself only needs 8.
6590        let mut filtered: Vec<&Row<'static>> = if stmt.where_.is_none() {
6591            Vec::with_capacity(table.rows().len())
6592        } else {
6593            // With a WHERE, the row count is an UPPER bound and reserving it
6594            // is the worse trade: `… WHERE id = 5` over 50M rows would take
6595            // 400 MB of pointers to hold one survivor. Let it grow.
6596            Vec::new()
6597        };
6598        // v6.2.6 — Memoize: per-query LRU cache for correlated
6599        // scalar subqueries. Fresh per row-loop entry so each
6600        // SELECT execution gets an isolated cache.
6601        let mut memo = memoize::MemoizeCache::new();
6602        // v7.37 (perf) — single-table aggregate's WHERE filter
6603        // pre-7.37 ran the slow tree-walker (`eval_expr_with_
6604        // correlated`) per row, even for subquery-free WHEREs that
6605        // the single-table SCAN path has compiled since v7.32
6606        // (perf knife D). The asymmetry meant a fold-to-filter
6607        // rewrite (joinfold) that swapped a JOIN for a single-table
6608        // aggregate over a compiled WHERE saw the tree-walker
6609        // instead — 25 k rows × `m.mailbox_id IN (25 lits)` cost
6610        // ~9 ms via the walker, vs ~1 ms via the compiled InSet
6611        // step. Compile once if eligible; fall back to the walker
6612        // for subquery-bearing or non-compilable WHEREs.
6613        let compiled_where: Option<eval::CompiledExpr> = stmt
6614            .where_
6615            .as_ref()
6616            .filter(|w| eval::fully_compilable(w))
6617            .map(|w| {
6618                // v7.38.8 — the scan filter runs the cheap half of its
6619                // conjunction first. Called from HERE and not from
6620                // `eval::compiled`, deliberately: the row loop lives in
6621                // that file, and adding a function to it cost this
6622                // query 11 % through layout alone while doing no work
6623                // for it. See `crate::qualorder`.
6624                match crate::qualorder::reordered(w) {
6625                    Some(r) => eval::compile_expr(&r, &ctx),
6626                    None => eval::compile_expr(w, &ctx),
6627                }
6628            });
6629        let mut eval_stack: Vec<Value<'static>> = Vec::new();
6630        let mut row_passes_where = |row: &Row<'static>,
6631                                    eval_stack: &mut Vec<Value<'static>>,
6632                                    memo: &mut memoize::MemoizeCache|
6633         -> Result<bool, EngineError> {
6634            match (&compiled_where, &stmt.where_) {
6635                (Some(cw), _) => {
6636                    // v7.39 (round 479) — the predicate wants a bool, not a
6637                    // Value. The owned entry ended in `Value::into_owned`
6638                    // and the caller then dropped it, once per row; round
6639                    // 478's profile put that pair above the comparison
6640                    // itself.
6641                    Ok(eval::compiled::eval_compiled_pred(
6642                        cw,
6643                        row,
6644                        &ctx,
6645                        eval_stack,
6646                        ctx.mysql_dialect,
6647                    )
6648                    .map_err(EngineError::Eval)?)
6649                }
6650                (None, Some(w)) => {
6651                    let cond = self.eval_expr_with_correlated(w, row, &ctx, cancel, Some(memo))?;
6652                    Ok(crate::eval::predicate_is_true(
6653                        &cond,
6654                        "WHERE",
6655                        ctx.mysql_dialect,
6656                    )?)
6657                }
6658                (None, None) => Ok(true),
6659            }
6660        };
6661        if let Some(seeked) = &indexed_rows {
6662            // v7.38.19 — an EXACT seek has already applied the whole
6663            // predicate, so asking again is asking the index's question
6664            // a second time, once per row.
6665            //
6666            // Profiled on `count(*) FROM events WHERE project_id = 3`
6667            // over 200,000 rows: `try_index_seek` 1,814 leaf samples and
6668            // `binop::compare` 1,633 — and `compare`'s first arm is
6669            // `(Int, Int) => a.cmp(b)`, so it was never that a
6670            // comparison is expensive. It was that 25,000 of them were
6671            // re-deciding what the walk had decided. The same query with
6672            // `GROUP BY project_id` bolted on ran in half the time,
6673            // doing strictly more work, because that path reached the
6674            // rows differently.
6675            //
6676            // `exact` is false for every arm that has not proven it —
6677            // the GIN, trigram and jsonb walks, an `AND` whose other
6678            // conjuncts went unapplied, a collated key, a type whose key
6679            // cannot name it. See `index_access::Seeked`.
6680            if seeked.exact {
6681                filtered.extend(seeked.rows.iter().map(Cow::as_ref));
6682            } else {
6683                for cow in &seeked.rows {
6684                    let row = cow.as_ref();
6685                    if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6686                        continue;
6687                    }
6688                    filtered.push(row);
6689                }
6690            }
6691        }
6692        // v7.36 (cold-tier coverage) — single-table aggregate's
6693        // non-indexed full scan was hot-only and silently lost cold
6694        // rows on COUNT/SUM/etc. Materialise cold rows once into
6695        // `cold_rows_storage` (Vec<Row<'static>>) so the `filtered: Vec<&Row<'static>>`
6696        // shape stays unchanged; the cold rows live until the end of
6697        // the aggregate run.
6698        let cold_rows_storage = if indexed_rows.is_none() {
6699            self.iter_cold_rows_of_table(table)
6700        } else {
6701            Vec::new()
6702        };
6703        if indexed_rows.is_none() {
6704            // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
6705            // single-table aggregate full-scan path. Mirrors the gate on
6706            // `run_single_table_scan`: this is a user-query result path,
6707            // so under gate-on (`SPG_MVCC_INPLACE`) it must skip rows the
6708            // reader's snapshot cannot see (e.g. tombstoned versions),
6709            // otherwise COUNT/SUM/etc. would tally dead rows. A no-op
6710            // under the default gate-off: every hot row is frozen or
6711            // committed-and-alive, so `is_row_visible` returns true.
6712            // Cold-tier rows are frozen (visible) by definition — left
6713            // ungated, matching the plain-scan path.
6714            let scan_snapshot = self.current_snapshot();
6715            // v7.39 (pg_stat knife B) — this full-scan branch walks
6716            // headers directly (serial and sharded alike); count the
6717            // sequential scan here.
6718            table.note_seq_scan();
6719            // v7.39 (parallel-agg P2) — the visibility probe + WHERE
6720            // filter dominate the pre-aggregate wall time on big
6721            // scans (P1's ground truth: accumulation is only ~17%).
6722            // Shard THAT work when the host injected an executor and
6723            // the WHERE is compiled (the compiled evaluator is pure
6724            // over &row; the tree-walker fallback can hit correlated
6725            // subqueries and stays serial). Shards return surviving
6726            // ROW INDICES — &Row can't cross the Box<dyn Any>'s
6727            // 'static bound — and the main thread only dereferences.
6728            let n = table.row_count();
6729            let par = self.parallel_runner.0.as_deref().filter(|_| {
6730                n >= crate::PARALLEL_MIN_ROWS && (stmt.where_.is_none() || compiled_where.is_some())
6731            });
6732            // v7.38.11 — ask the BRIN summary first. When it prunes,
6733            // the work left is a few thousand rows and sharding it
6734            // costs more than it saves, so the serial pruned loop below
6735            // takes it; the shard machinery is left exactly as it was
6736            // rather than taught about slots.
6737            let brin_slots = stmt
6738                .where_
6739                .as_ref()
6740                .and_then(|w| crate::brin::candidate_slots(w, table));
6741            let brin_prunes = brin_slots
6742                .as_ref()
6743                .is_some_and(|s| s.iter().map(core::ops::Range::len).sum::<usize>() * 2 < n);
6744            if let Some(r) = par
6745                && !brin_prunes
6746            {
6747                let n_shards = (n / crate::PARALLEL_MIN_ROWS).clamp(2, 8);
6748                let chunk = n.div_ceil(n_shards);
6749                type ShardOut = Result<alloc::vec::Vec<usize>, EngineError>;
6750                let cw = &compiled_where;
6751                let snap_ref = &scan_snapshot;
6752                let results = r.run_shards(n_shards, &|s| {
6753                    let lo = s * chunk;
6754                    let hi = ((s + 1) * chunk).min(n);
6755                    let mut keep: alloc::vec::Vec<usize> = alloc::vec::Vec::with_capacity(hi - lo);
6756                    // EvalContext carries Cells (sampler / row counters)
6757                    // and is !Sync — each shard builds its own from the
6758                    // same Sync inputs. The compiled WHERE is gated to
6759                    // the pure-scalar whitelist, which reads none of the
6760                    // session state the engine-built ctx would add
6761                    // (TABLESAMPLE's __tsm_fract is not whitelisted, so
6762                    // sampled scans never take this branch).
6763                    let shard_ctx = EvalContext::new(schema_cols, Some(alias));
6764                    let mut stack: Vec<Value<'static>> = Vec::new();
6765                    let out: ShardOut = (|| {
6766                        for i in lo..hi {
6767                            if !table.is_row_visible(i, snap_ref) {
6768                                continue;
6769                            }
6770                            let row = &table.rows()[i];
6771                            // v7.39 (round 480) — the parallel full-scan
6772                            // shard is the path the aggregate benchmark
6773                            // actually takes, and it was still on the OWNED
6774                            // entry: round 480's profile attributed 68.7 %
6775                            // of `drop_glue<Value>` to this closure, which
6776                            // is why round 479's fix to the indexed path
6777                            // barely moved the total.
6778                            //
6779                            // The `matches!(…, Value::Bool(true))` form was
6780                            // also a narrower reading than the rest of the
6781                            // engine uses — `predicate_is_true` is what
6782                            // handles NULL and MySQL truthiness — so the
6783                            // bool entry fixes the shape as well as the cost.
6784                            let pass = match cw {
6785                                Some(c) => eval::compiled::eval_compiled_pred(
6786                                    c,
6787                                    row,
6788                                    &shard_ctx,
6789                                    &mut stack,
6790                                    shard_ctx.mysql_dialect,
6791                                )
6792                                .map_err(EngineError::Eval)?,
6793                                None => true,
6794                            };
6795                            if pass {
6796                                keep.push(i);
6797                            }
6798                        }
6799                        Ok(keep)
6800                    })();
6801                    alloc::boxed::Box::new(out)
6802                });
6803                // v7.39 (round 567) — `rows()` is a 32-way trie, so
6804                // indexing it is four dependent loads and a scan that
6805                // reads every row paid them every row. A profile of
6806                // `SELECT sum(id)` over 500k rows put 37.8% of the
6807                // connection thread's CPU on THIS ONE LINE. The cursor
6808                // holds the leaf, making that one descent per 32.
6809                let mut rows_cur = table.rows().run_cursor();
6810                for boxed in results {
6811                    let shard = boxed
6812                        .downcast::<ShardOut>()
6813                        .expect("runner echoes the closure's box");
6814                    for i in (*shard)? {
6815                        if let Some(row) = rows_cur.get(i) {
6816                            filtered.push(row);
6817                        }
6818                    }
6819                }
6820            } else {
6821                let mut rows_cur = table.rows().run_cursor();
6822                // v7.38.11 — the slots the BRIN summary could not rule
6823                // out. The predicate still runs on every row that
6824                // survives: the summary decides what to SKIP, never
6825                // what to return.
6826                let ranges = brin_slots.unwrap_or_else(|| alloc::vec![0..n]);
6827                for range in ranges {
6828                    for i in range {
6829                        if !table.is_row_visible(i, &scan_snapshot) {
6830                            continue;
6831                        }
6832                        let Some(row) = rows_cur.get(i) else { continue };
6833                        if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6834                            continue;
6835                        }
6836                        filtered.push(row);
6837                    }
6838                }
6839            }
6840            for row in &cold_rows_storage {
6841                if !row_passes_where(row, &mut eval_stack, &mut memo)? {
6842                    continue;
6843                }
6844                filtered.push(row);
6845            }
6846        }
6847        // v7.29 — a per-query memo so correlated scalar
6848        // subqueries batch-evaluate once (group map) instead of
6849        // executing per group.
6850        let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
6851        let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
6852            self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
6853                .map_err(|err| match err {
6854                    EngineError::Eval(ev) => ev,
6855                    other => eval::EvalError::TypeMismatch {
6856                        detail: alloc::format!("{other}"),
6857                    },
6858                })
6859        };
6860        // v7.39 (round 656) — the plain relational scan. This collect() was
6861        // the measured defect: one 64-byte `RowRef` per surviving row to
6862        // wrap an 8-byte pointer `filtered` already holds. Scalar
6863        // aggregates measured ~81 bytes/row of working memory because of
6864        // it — 40 MB at 500k rows, 3.2 GB at 50M, for a query that returns
6865        // one number. `AggRows::Ptrs` reads the pointers directly.
6866        let agg = aggregate::run(
6867            stmt,
6868            crate::join::AggRows::Ptrs(&filtered),
6869            schema_cols,
6870            Some(alias),
6871            Some(&agg_correlated),
6872            self.parallel_runner.0.as_deref(),
6873            Some(self.active_catalog()),
6874            Some(self),
6875        )?;
6876        self.finish_agg_result(agg, stmt, cancel)
6877    }
6878
6879    /// Single-table scan + projection path: WHERE filter (compiled when
6880    /// subquery-free), ORDER BY keying, SRF expansion / projection, then
6881    /// sort + WITH TIES / DISTINCT / OFFSET-LIMIT.
6882    fn run_single_table_scan<'a>(
6883        &self,
6884        stmt: &SelectStatement,
6885        table: &'a spg_storage::Table,
6886        schema_cols: &'a [ColumnSchema],
6887        alias: &str,
6888        indexed_rows: Option<crate::index_access::Seeked<'a>>,
6889        cancel: CancelToken<'_>,
6890    ) -> Result<QueryResult, EngineError> {
6891        // v7.38 (read01 U15) — a fresh per-scan sampler cell for
6892        // `TABLESAMPLE … REPEATABLE(seed)`. Created before the ctx so the
6893        // deterministic `__tsm_fract(seed)` draws share one scan-local
6894        // state (isolated from the global random() PRNG); a fresh cell per
6895        // scan makes a repeat / rescan reproduce the same sample. Unused
6896        // and cheap when the query carries no sample.
6897        let sample_cell: core::cell::Cell<Option<u64>> = core::cell::Cell::new(None);
6898        let ctx = self
6899            .ev_ctx(schema_cols, Some(alias))
6900            .with_sample_rng(&sample_cell);
6901        let projection = build_projection(
6902            &stmt.items,
6903            schema_cols,
6904            alias,
6905            self.backslash_escapes,
6906            Some(self.active_catalog()),
6907        )?;
6908        // v7.19 P5 — single-table SELECT path for SRF
6909        // `SELECT unnest(arr) FROM t` shape. Detect a top-level
6910        // unnest in the projection list. When present, the
6911        // per-row processor emits one output row per array
6912        // element (broadcasting non-SRF projections from the
6913        // same input row). Empty / NULL arrays emit zero rows
6914        // for that input — PG semantics.
6915        // v7.39 (read01 round 67) — every SRF in the target list, in lockstep.
6916        let srf_idxs = self.srf_target_idxs(&projection);
6917        let srf_position = srf_idxs.first().copied();
6918        // v7.39 (round 599) — the SRF analysis is per QUERY, not per row.
6919        let mut srf_plan = if srf_position.is_some() {
6920            Some(build_srf_plan(self, &projection, &srf_idxs, &ctx)?)
6921        } else {
6922            None
6923        };
6924
6925        // Materialise the filter pass into `(order_key, projected_row)`
6926        // tuples. The order key is `None` when there's no ORDER BY clause.
6927        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
6928        // v7.33 (C1, ceiling-first/never-die) — charge each accumulated
6929        // output row to the per-query byte budget as it is built, so a
6930        // fat single-table scan / sort REJECTS with QueryBytesExceeded
6931        // at ~the ceiling instead of materialising the whole table and
6932        // only noticing at the final enforce_row_limit check. Without
6933        // this, N concurrent fat scans peak at N×table and OOM the host.
6934        // `max_query_bytes = None` (the embedded default) = no ceiling,
6935        // so existing unbudgeted behaviour is byte-identical.
6936        let mut budget = ByteBudget::new(self.max_query_bytes);
6937        // v6.2.6 — Memoize per-row WHERE eval shares one cache.
6938        let mut memo = memoize::MemoizeCache::new();
6939        // v7.32 (perf knife D) — subquery-free WHERE compiles once;
6940        // the row loop then runs a flat step program instead of a
6941        // tree interpretation per row.
6942        let compiled_where: Option<eval::CompiledExpr> = stmt
6943            .where_
6944            .as_ref()
6945            .filter(|w| eval::fully_compilable(w))
6946            .map(|w| {
6947                // v7.38.8 — the scan filter runs the cheap half of its
6948                // conjunction first. Called from HERE and not from
6949                // `eval::compiled`, deliberately: the row loop lives in
6950                // that file, and adding a function to it cost this
6951                // query 11 % through layout alone while doing no work
6952                // for it. See `crate::qualorder`.
6953                match crate::qualorder::reordered(w) {
6954                    Some(r) => eval::compile_expr(&r, &ctx),
6955                    None => eval::compile_expr(w, &ctx),
6956                }
6957            });
6958        let mut eval_stack: Vec<Value<'static>> = Vec::new();
6959        // v7.37.x (docker-fair SCALARSQ attack) — pre-analyse every
6960        // SELECT-item scalar subquery for the PK-probe fast path. The
6961        // analysis (gate checks + catalog lookups) takes ~500 ns; doing
6962        // it once per query instead of once per row × 100 rows saves
6963        // ~50 µs and lets the per-row evaluation reduce to a single
6964        // index probe + outer-column read.
6965        let scalarsq_fast: Vec<Option<crate::ScalarPkProbeFastPath>> = projection
6966            .iter()
6967            .map(|p| {
6968                if let Expr::ScalarSubquery(inner) = &p.expr {
6969                    self.analyse_scalar_count_pk_eq_probe(inner, schema_cols, alias)
6970                } else {
6971                    None
6972                }
6973            })
6974            .collect();
6975        let any_scalarsq_fast = scalarsq_fast.iter().any(Option::is_some);
6976        // v7.39 (round 487) — a projection item that is a bare column
6977        // reference binds its position ONCE per query.
6978        //
6979        // Per row it used to walk `eval_expr_with_correlated` (a memo
6980        // lookup for "does this have a subquery", then an un-memoised
6981        // `expr_may_use_in_set` tree walk), then `eval_expr`'s dispatch,
6982        // then `resolve_column`, which finds the column by scanning the
6983        // schema and comparing NAMES. On `SELECT g FROM h` that chain was
6984        // 19 % of self time for what is ultimately one cell read.
6985        //
6986        // `compile_column_pos` is the Step VM's resolver, already
6987        // `pub(crate)` and already reused by the aggregate's bind-once
6988        // path: it mirrors `resolve_column`'s happy layers and returns
6989        // None for anything that would reach an error, an ambiguity, or a
6990        // miss, so those still go the interpreter's way and keep its
6991        // exact message. A composite column is excluded for the same
6992        // reason `compile_into` excludes it — it must be rehydrated from
6993        // stored JSON, which is not a cell read.
6994        let proj_direct = bind_direct_columns(&projection, &ctx);
6995        let any_proj_direct = proj_direct.iter().any(Option::is_some);
6996        // v7.39 (round 605) — a projection item that cannot depend on the row
6997        // is evaluated once. `SELECT ('{"a":1}')::JSONB FROM j` cost TEN
6998        // allocations a row against one for a plain column, `'abc' || 'def'`
6999        // six and `upper('abc')` five, all of them producing the same value
7000        // 50,000 times. An item that fails to evaluate is left alone, so its
7001        // error still comes from the row loop in the interpreter's wording.
7002        let proj_const: Vec<Option<Value<'static>>> = projection
7003            .iter()
7004            .map(|p| crate::eval::compiled::constant_projection_value(&p.expr, &ctx))
7005            .collect();
7006        let any_proj_const = proj_const.iter().any(Option::is_some);
7007        crate::bump_counter!(crate::select::SCAN_PATH_ENTERED);
7008        // v7.39 (read01 round 80) — positional ORDER BY over a WILDCARD
7009        // projection. Statement prep (`resolve_order_by_position`) can only map
7010        // `ORDER BY 1` onto the first SELECT item when that item is an
7011        // expression; a `*` is not one, so the literal survived to here and was
7012        // evaluated as the CONSTANT 1 — the same key for every row, i.e. no sort
7013        // at all. The parser rewrites `SELECT unnest(a) x` into
7014        // `SELECT * FROM unnest(a) x`, so that innocuous-looking shape landed
7015        // exactly here: `SELECT unnest(ARRAY['B','a','A','b']) ORDER BY 1` came
7016        // back in input order. The projection is built by now, so the Nth output
7017        // column is known — resolve against it.
7018        let order_by = resolve_positional_order_by(&stmt.order_by, &projection);
7019        // v7.39 (round 600) — the ORDER BY of an SRF query is decided on the
7020        // EXPANDED rows, so a key naming a select-list item reads that item.
7021        let srf_order_cols: Vec<Option<usize>> = if srf_position.is_some() {
7022            srf_order_output_cols(&order_by, &projection)
7023        } else {
7024            Vec::new()
7025        };
7026        let srf_key_bound: Vec<Option<usize>> = (0..order_by.len()).map(Some).collect();
7027        // v7.37.x (docker-fair SCALARSQ attack) — early-limit gate for
7028        // the no-ORDER-BY-no-DISTINCT-no-TIES-no-SRF-no-WHERE shape.
7029        // Hoisted above the closure so the projection-eval path can
7030        // gate `memo` passing on it: the SELECT-item correlated-scalar
7031        // batch path scans the FULL inner table once (~5 ms for 12.5 k
7032        // rows) and is only a win when N outer rows is large; for small
7033        // LIMITed shapes a per-row PK seek (~5 µs × 100 = 500 µs) wins.
7034        let early_cap: Option<usize> = if order_by.is_empty()
7035            && !stmt.distinct
7036            && !stmt.limit_with_ties
7037            && srf_position.is_none()
7038            && stmt.where_.is_none()
7039        {
7040            stmt.limit_literal()
7041                .map(|n| n.saturating_add(stmt.offset_literal().unwrap_or(0)) as usize)
7042        } else {
7043            None
7044        };
7045        // v7.38 (read01 B8) — streaming top-N budget. For `ORDER BY …
7046        // LIMIT k` (no DISTINCT / WITH TIES / SRF, and not forced to
7047        // full-sort by the test gate) keep only the running top-`keep`
7048        // rows in memory instead of materialising every projected row,
7049        // so a `… ORDER BY col LIMIT 10` over a huge table is O(keep)
7050        // space, not O(rows). `None` = accumulate everything (the prior
7051        // behaviour). The final `partial_sort_tagged(keep)` below still
7052        // runs and produces the identical rows.
7053        // v7.39 (round 683) — the declared collation for each ORDER BY
7054        // position, resolved once and carried beside `descs` for the same
7055        // reason `descs` is carried: it is per key position, not per row.
7056        let order_colls = crate::orderby::order_by_collations(&order_by, &ctx)?;
7057        let topk_stream: Option<(usize, Vec<bool>)> = if !order_by.is_empty()
7058            && !stmt.distinct
7059            && !stmt.limit_with_ties
7060            && srf_position.is_none()
7061            && !self.env_cfg().disable_topk
7062        {
7063            stmt.limit_literal().and_then(|l| {
7064                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
7065                (keep >= 1).then(|| (keep, order_by.iter().map(|o| o.desc).collect()))
7066            })
7067        } else {
7068            None
7069        };
7070        // v7.38.19 — when the sort column is one the projection already
7071        // carries, build no key at all and sort by reading it.
7072        //
7073        // Restricted to the FULL sort: a top-N compares against a stored
7074        // boundary key and `WITH TIES` extends past the limit through the
7075        // keys, both of which need one to exist. DISTINCT keys on them
7076        // too, and an SRF's keys come from the EXPANDED row.
7077        // A COLLATION does not rule it out, but it has to be one that
7078        // orders these values the way bytes do -- decided on the values
7079        // themselves, further down, once they exist.
7080        let sort_by_output: Option<Vec<usize>> = if stmt.distinct
7081            || stmt.limit_with_ties
7082            || srf_position.is_some()
7083            || topk_stream.is_some()
7084        {
7085            None
7086        } else {
7087            order_by_output_cols_if_identical(&order_by, &projection, schema_cols)
7088        };
7089        // v7.37.16 — streaming DISTINCT seen-set: norm-hash → indices of
7090        // kept rows in `tagged`. Probing on the PROJECTED row as soon as
7091        // it is built means a duplicate costs neither a build_order_keys
7092        // eval (the dominant per-row cost of `DISTINCT … ORDER BY`) nor
7093        // a tagged slot, and the sort below runs over u survivors, not
7094        // n input rows — PG's hash-distinct-then-sort plan shape.
7095        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
7096            hashbrown::HashMap::new();
7097        let distinct_hb = hashbrown::DefaultHashBuilder::default();
7098        // v7.38.13 — which output positions must NOT fold. Built once per
7099        // scan from the projection, which carries the source column's
7100        // byte-wise-ness; see `FoldSpec`.
7101        let distinct_mask = fold_mask(&projection);
7102        // v7.39 (round 485) — one projection buffer for the whole scan
7103        // rather than a fresh `Vec` per input row. A row that survives
7104        // the DISTINCT probe takes the buffer with it (`mem::take`) and
7105        // the next row allocates a new one; a row that duplicates an
7106        // earlier one leaves the buffer — and its capacity — in place.
7107        // The round-485 counter says 49 900 of `distinct_proj`'s 50 000
7108        // projected rows are duplicates, so that is 49 900 allocate /
7109        // free pairs the scan no longer performs. Shapes where every row
7110        // survives (plain projection, `DISTINCT` over a unique column)
7111        // allocate exactly as often as before.
7112        let mut proj_buf: Vec<Value<'static>> = Vec::new();
7113        // v7.39 (round 571) — buffers handed back by the top-N trim.
7114        // Round 485 made the scan share ONE projection buffer, but a
7115        // surviving row takes it (`mem::take`) and without DISTINCT
7116        // almost every row survives, so the next one starts from zero
7117        // capacity and allocates. The trim drops `keep` rows at a time
7118        // and their buffers come back here instead of being freed.
7119        let mut proj_pool: Vec<Vec<Value<'static>>> = Vec::new();
7120        let mut key_pool: Vec<Vec<crate::orderby::OrderKey>> = Vec::new();
7121        // v7.39 (round 581) — the worst row the accumulator is currently
7122        // keeping. Anything that loses to it cannot reach the answer, so
7123        // it is dropped before its projection is ever built.
7124        let mut topk_boundary: Option<Vec<crate::orderby::OrderKey>> = None;
7125        // v7.38.20 — the boundary's own leading eight bytes, so a losing
7126        // row can be turned away before a key is built for it. Kept
7127        // beside the boundary and refreshed with it; `None` whenever the
7128        // boundary's first key is not one this can read, which sends
7129        // every row down the ordinary path.
7130        // v7.38.21 — and whether those bytes may be trusted under the
7131        // collation in force, which is the boundary's own text to answer.
7132        let mut topk_boundary_prefix: Option<(u64, bool)> = None;
7133        // v7.39 (round 582) — resolve each ORDER BY column once, not
7134        // once per row. See `order_by_bound_positions`.
7135        let order_bound =
7136            crate::orderby::order_by_bound_positions(&order_by, schema_cols, Some(alias));
7137        // v7.39 (round 581) — and it stops asking when the answer is
7138        // always "keep".
7139        //
7140        // The check earns its place only on rows it rejects. Over
7141        // ascending ids, `ORDER BY id DESC` never rejects one — every
7142        // row beats the current worst — so the comparison is pure
7143        // overhead there, measured at +5.5% in three batches out of
7144        // three. After a window of rows it looks at what it has
7145        // actually rejected and switches itself off if the shape is not
7146        // paying. The answers do not depend on it either way.
7147        // v7.38.21 — resolved once per query, not per row.
7148        //
7149        // No collation at all is the case v7.38.20 shipped. A DECLARED
7150        // one may still be answered by bytes, and which collations those
7151        // are is `Collated::ascii_byte_order`'s to say — the same
7152        // allowlist `byte_order_answers_the_collation` consults, so the
7153        // two cannot come to disagree about a collation. What that
7154        // allowlist requires of the TEXT is checked per row and on the
7155        // boundary, because a streaming top-N has no batch to check.
7156        let boundary_no_collation = order_colls.iter().all(Option::is_none);
7157        let boundary_collations_permit = boundary_no_collation
7158            || order_colls
7159                .iter()
7160                .flatten()
7161                .all(crate::collate::Collated::ascii_byte_order);
7162        const BOUNDARY_WINDOW: u32 = 8192;
7163        let mut boundary_checks: u32 = 0;
7164        let mut boundary_rejects: u32 = 0;
7165        let mut boundary_check_on = true;
7166        // Inline the per-row work in a closure so the indexed and full-
7167        // scan branches share the body.
7168        // v7.38.19 — `check_where` is per CALL SITE, not per closure: the
7169        // full-scan loops below must apply the predicate, and the
7170        // indexed loop must not when the seek already did. A captured
7171        // flag would have to be right for both.
7172        let mut process_row = |row: &Row<'static>,
7173                               loop_idx: usize,
7174                               check_where: bool|
7175         -> Result<(), EngineError> {
7176            if loop_idx.is_multiple_of(256) {
7177                cancel.check()?;
7178            }
7179            if !check_where {
7180                // The seek answered the whole predicate. See
7181                // `index_access::Seeked`.
7182            } else if let Some(cw) = &compiled_where {
7183                let cond = eval::eval_compiled(cw, row, &ctx, &mut eval_stack)
7184                    .map_err(EngineError::Eval)?;
7185                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7186                    return Ok(());
7187                }
7188            } else if let Some(where_expr) = &stmt.where_ {
7189                let cond =
7190                    self.eval_expr_with_correlated(where_expr, row, &ctx, cancel, Some(&mut memo))?;
7191                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
7192                    return Ok(());
7193                }
7194            }
7195            // Under DISTINCT the keys are built AFTER the dup probe
7196            // (survivors only); the non-distinct order is unchanged.
7197            // v7.39 (round 600) — an SRF query's keys are built per EXPANDED
7198            // row further down, and building them here would evaluate the
7199            // ORDER BY against the INPUT row: a key naming the SRF's own
7200            // output became a scalar call to it, which is where
7201            // "function unnest(integer[]) does not exist" came from.
7202            let order_keys = if order_by.is_empty()
7203                || stmt.distinct
7204                || srf_position.is_some()
7205                // v7.38.19 — the branch below builds whatever key it
7206                // needs from the projected values, collation included,
7207                // so nothing has to be built here for it.
7208                //
7209                // A draft that skipped them here but still let the
7210                // COLLATED case fall through to the key-based sort put a
7211                // mixed column back in INSERT order: every key empty,
7212                // every row equal, a stable sort faithfully preserving
7213                // nothing. The rule is one decision, not two.
7214                || sort_by_output.is_some()
7215            {
7216                Vec::new()
7217            } else {
7218                // v7.38.20 — turn a decisively losing row away before
7219                // its key is built. Only the FIRST key is read, and only
7220                // its leading eight bytes; a tie there decides nothing
7221                // and falls through to the full path below.
7222                //
7223                // ASC only: under DESC the boundary is the largest kept
7224                // key and the comparison flips, which this deliberately
7225                // does not try to express — a second direction in a
7226                // fast-path predicate is how one of them ends up wrong.
7227                if boundary_check_on
7228                    && let Some((_, descs)) = &topk_stream
7229                    && !descs.first().copied().unwrap_or(false)
7230                    && order_by.len() == 1
7231                    && boundary_collations_permit
7232                    && let Some((bp, boundary_is_ascii)) = topk_boundary_prefix
7233                    && let Some((rp, row_is_ascii)) =
7234                        crate::orderby::first_key_prefix(&order_bound, row)
7235                    && (boundary_no_collation || (boundary_is_ascii && row_is_ascii))
7236                    && rp > bp
7237                {
7238                    boundary_checks += 1;
7239                    boundary_rejects += 1;
7240                    if boundary_checks == BOUNDARY_WINDOW {
7241                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7242                    }
7243                    return Ok(());
7244                }
7245                let mut buf = key_pool.pop().unwrap_or_default();
7246                crate::orderby::build_order_keys_bound(
7247                    &order_by,
7248                    &order_bound,
7249                    &order_colls,
7250                    row,
7251                    &ctx,
7252                    &mut buf,
7253                )?;
7254                // v7.39 (round 581) — reject before projecting.
7255                //
7256                // `ORDER BY g DESC, id DESC LIMIT 10` over 500k rows with
7257                // 50 distinct `g` decides nearly every row on the FIRST
7258                // key, and PG answers it FASTER than the single-key form
7259                // (7.4 ms against 10.4) because a rejected row costs it
7260                // one comparison. SPG built both keys AND the projected
7261                // row for all 500k before throwing them away. The keys
7262                // are needed to compare; the projection is not.
7263                if boundary_check_on
7264                    && let Some((_, descs)) = &topk_stream
7265                    && let Some(b) = &topk_boundary
7266                {
7267                    boundary_checks += 1;
7268                    let loses = crate::orderby::cmp_multi_key_in(&buf, b, descs, &order_colls)
7269                        == core::cmp::Ordering::Greater;
7270                    if loses {
7271                        boundary_rejects += 1;
7272                    }
7273                    if boundary_checks == BOUNDARY_WINDOW {
7274                        // Keep asking only if it has been rejecting at
7275                        // least a quarter of what it saw.
7276                        boundary_check_on = boundary_rejects.saturating_mul(4) >= boundary_checks;
7277                    }
7278                    if loses {
7279                        buf.clear();
7280                        key_pool.push(buf);
7281                        return Ok(());
7282                    }
7283                }
7284                buf
7285            };
7286            if srf_position.is_some() {
7287                let plan = srf_plan.as_mut().expect("srf_position implies a plan");
7288                for out in expand_srf_row_with(self, plan, &projection, row, &ctx)? {
7289                    if stmt.distinct {
7290                        let bucket = seen_distinct
7291                            .entry(norm_hash_row(
7292                                &out,
7293                                &distinct_hb,
7294                                FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7295                            ))
7296                            .or_default();
7297                        if bucket.iter().any(|i| {
7298                            row_eq_norm(
7299                                &tagged[i].1,
7300                                &out,
7301                                FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7302                            )
7303                        }) {
7304                            continue;
7305                        }
7306                        bucket.push(tagged.len());
7307                    }
7308                    budget.charge(approx_row_bytes(&out))?;
7309                    // The keys come from THIS expanded row: a key naming a
7310                    // select-list item reads its value, anything else is
7311                    // still evaluated against the input row.
7312                    let keys = if order_by.is_empty() {
7313                        Vec::new()
7314                    } else {
7315                        let mut kv: Vec<Value<'static>> = Vec::with_capacity(order_by.len());
7316                        for (k, ob) in order_by.iter().enumerate() {
7317                            kv.push(match srf_order_cols.get(k).copied().flatten() {
7318                                Some(p) => out.values.get(p).cloned().unwrap_or(Value::Null),
7319                                None => eval::eval_expr(&ob.expr, row, &ctx)
7320                                    .map_err(EngineError::Eval)?,
7321                            });
7322                        }
7323                        // Packed by the same code every other ORDER BY uses,
7324                        // so DESC / NULLS FIRST / the MySQL rule are not
7325                        // restated here.
7326                        let key_row = Row::new(kv);
7327                        let mut buf = Vec::new();
7328                        crate::orderby::build_order_keys_bound(
7329                            &order_by,
7330                            &srf_key_bound,
7331                            &order_colls,
7332                            &key_row,
7333                            &ctx,
7334                            &mut buf,
7335                        )?;
7336                        buf
7337                    };
7338                    tagged.push((keys, out));
7339                }
7340            } else {
7341                let values = &mut proj_buf;
7342                values.clear();
7343                values.reserve(projection.len());
7344                for (i, p) in projection.iter().enumerate() {
7345                    // v7.37.x (docker-fair SCALARSQ attack) — pre-
7346                    // analysed PK-probe fast path. The per-row work is
7347                    // a read of outer.col from the row plus an index
7348                    // probe — no Expr clone, no walker, no
7349                    // `eval_expr_with_correlated` framework.
7350                    if any_scalarsq_fast && let Some(fp) = &scalarsq_fast[i] {
7351                        values.push(self.probe_with_pk_fast_path(fp, row));
7352                        continue;
7353                    }
7354                    // v7.39 (round 605) — the same value every row.
7355                    if any_proj_const && let Some(v) = &proj_const[i] {
7356                        values.push(v.clone());
7357                        continue;
7358                    }
7359                    // v7.39 (round 487) — bound column: read the cell.
7360                    // This is `rehydrate_cell`'s body for a non-composite
7361                    // column, which is what the whole chain below reduces
7362                    // to once the name has been resolved.
7363                    if any_proj_direct && let Some(pos) = proj_direct[i] {
7364                        crate::bump_counter!(crate::select::PROJ_DIRECT_FIRE);
7365                        values.push(row.values[pos].clone().into_owned());
7366                        continue;
7367                    }
7368                    // v7.24 (round-16 B) — correlated-aware.
7369                    // v7.37.x (docker-fair SCALARSQ attack) — share the
7370                    // per-row memo with projection. Required for the
7371                    // batch-evaluated correlated-scalar path to fire on
7372                    // SELECT-item scalar subqueries; otherwise each row
7373                    // re-executes the inner.
7374                    //
7375                    // Skip the memo when the outer row count is small
7376                    // (early-limited): the batch path scans the FULL
7377                    // inner table to build a GroupMap (~5 ms for a
7378                    // 12.5 k-row inner), while per-row execution with a
7379                    // PK index seek is ~5 µs per call — much cheaper for
7380                    // N ≤ ~1000 outer rows.
7381                    let pass_memo = early_cap.is_none_or(|cap| cap > 1000);
7382                    let memo_arg = if pass_memo { Some(&mut memo) } else { None };
7383                    values.push(
7384                        self.eval_expr_with_correlated(&p.expr, row, &ctx, cancel, memo_arg)?,
7385                    );
7386                }
7387                crate::bump_counter!(crate::select::PROJ_ROW_BUILT);
7388                if stmt.distinct {
7389                    let bucket = seen_distinct
7390                        .entry(norm_hash_values(
7391                            &proj_buf,
7392                            &distinct_hb,
7393                            FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7394                        ))
7395                        .or_default();
7396                    if bucket.iter().any(|i| {
7397                        values_eq_norm(
7398                            &tagged[i].1.values,
7399                            &proj_buf,
7400                            FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
7401                        )
7402                    }) {
7403                        crate::bump_counter!(crate::select::DISTINCT_DUP_DROPPED);
7404                        return Ok(());
7405                    }
7406                    bucket.push(tagged.len());
7407                }
7408                let out = Row::new(core::mem::replace(
7409                    &mut proj_buf,
7410                    proj_pool.pop().unwrap_or_default(),
7411                ));
7412                let order_keys = if stmt.distinct && !order_by.is_empty() {
7413                    // v7.38.13 — `&order_bound`, not `&[]`. Round 582 added
7414                    // the bound-cell path precisely so an ORDER BY key that
7415                    // names a column is READ instead of evaluated, and the
7416                    // non-DISTINCT branch above has passed it ever since;
7417                    // this branch never did, so `SELECT DISTINCT k .. ORDER
7418                    // BY k` resolved "k" by string for every surviving row.
7419                    let mut buf = key_pool.pop().unwrap_or_default();
7420                    crate::orderby::build_order_keys_bound(
7421                        &order_by,
7422                        &order_bound,
7423                        &order_colls,
7424                        row,
7425                        &ctx,
7426                        &mut buf,
7427                    )?;
7428                    buf
7429                } else {
7430                    order_keys
7431                };
7432                budget.charge(approx_row_bytes(&out))?;
7433                tagged.push((order_keys, out));
7434            }
7435            // Streaming top-N: bound the accumulator to O(keep) rows.
7436            if let Some((k, descs)) = &topk_stream {
7437                crate::orderby::topk_trim_recycling(
7438                    &mut tagged,
7439                    *k,
7440                    descs,
7441                    &mut proj_pool,
7442                    &mut key_pool,
7443                    &mut topk_boundary,
7444                );
7445                // The prefix follows the boundary it summarises.
7446                topk_boundary_prefix = topk_boundary
7447                    .as_ref()
7448                    .and_then(|b| b.first())
7449                    .and_then(crate::orderby::order_key_text_prefix);
7450            }
7451            Ok(())
7452        };
7453        // v7.37.15 (Phase C.3, step 2) — MVCC visibility gate for the
7454        // load-bearing full-scan path. This is the primary single-table
7455        // executor; pre-C.3 it read every hot-tier row raw. Once C.3's
7456        // in-place writers retain dead/old versions, an ungated scan
7457        // here would return them, so the gate must land BEFORE the
7458        // writers flip (see the plan's activation-order rule). A no-op
7459        // today: every hot row is frozen or committed-and-alive under
7460        // the reader's snapshot, so `is_row_visible` returns true for
7461        // all of them (verified by the full e2e suite staying green).
7462        let scan_snapshot = self.current_snapshot();
7463        let mut emitted: usize = 0;
7464        if let Some(seeked) = &indexed_rows {
7465            let recheck = !seeked.exact;
7466            for (loop_idx, cow) in seeked.rows.iter().enumerate() {
7467                if let Some(cap) = early_cap
7468                    && emitted >= cap
7469                {
7470                    break;
7471                }
7472                process_row(cow.as_ref(), loop_idx, recheck)?;
7473                emitted = emitted.saturating_add(1);
7474            }
7475        } else {
7476            // v7.39 (round 570) — the row store is a 32-way trie, so
7477            // indexing it is four dependent loads. Round 567 measured
7478            // -18% on the aggregate scan from holding the leaf between
7479            // rows; this is the same loop for the projecting scan.
7480            let mut rows_cur = table.rows().run_cursor();
7481            // v7.38.11 — see the aggregate scan above: a BRIN index on a
7482            // column this WHERE bounds says which slots cannot match.
7483            let brin_slots = stmt
7484                .where_
7485                .as_ref()
7486                .and_then(|w| crate::brin::candidate_slots(w, table))
7487                .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
7488            for i in brin_slots.into_iter().flatten() {
7489                if let Some(cap) = early_cap
7490                    && emitted >= cap
7491                {
7492                    break;
7493                }
7494                // Skip rows this snapshot cannot see (invisible rows do
7495                // not count toward the LIMIT).
7496                if !table.is_row_visible(i, &scan_snapshot) {
7497                    continue;
7498                }
7499                let Some(row) = rows_cur.get(i) else { continue };
7500                process_row(row, i, true)?;
7501                emitted = emitted.saturating_add(1);
7502            }
7503            // v7.35.1 (mailrs prod #6 follow-up) — fold cold-tier
7504            // rows into the same loop. The full-scan path here is the
7505            // load-bearing single-table SELECT executor, and pre-
7506            // 7.35.1 it only walked `table.rows()` (hot), so any
7507            // `SELECT … FROM t` against a table with cold segments
7508            // silently returned a subset.
7509            let cold_rows = self.iter_cold_rows_of_table(table);
7510            for (offset, row) in cold_rows.iter().enumerate() {
7511                if let Some(cap) = early_cap
7512                    && emitted >= cap
7513                {
7514                    break;
7515                }
7516                process_row(row, table.row_count() + offset, true)?;
7517                emitted = emitted.saturating_add(1);
7518            }
7519        }
7520
7521        // (DISTINCT already de-duped STREAMING inside process_row, so the
7522        // sort below only sees the u survivors and the partial-sort
7523        // budget applies to DISTINCT too.)
7524        if !order_by.is_empty() {
7525            // Partial-sort fast path: when LIMIT is small relative to
7526            // the row count, select_nth_unstable + sort just the
7527            // prefix is O(n + k log k) instead of O(n log n).
7528            // WITH TIES needs the full sort so the tie extension can
7529            // scan past `limit` to find rows that share the last-kept
7530            // row's key.
7531            let keep = if stmt.limit_with_ties
7532                // v7.38 元机制 D acceptor — `SPG_TEST_DISABLE_TOPK=1`
7533                // forces the full-sort fallback by suppressing the
7534                // partial-sort `keep` budget. See
7535                // `xtests/sigil/test-mode-gucs.md`.
7536                || self.env_cfg().disable_topk
7537            {
7538                None
7539            } else {
7540                stmt.limit_literal()
7541                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
7542            };
7543            let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
7544            if let Some(cols) = &sort_by_output {
7545                // No keys were built; the sort reads the projected row.
7546                // The comparator is the value-level one the window
7547                // functions and the key path both defer to, so DESC,
7548                // NULLS placement, the MySQL fold and the collation are
7549                // not restated here.
7550                let terms: Vec<(usize, bool, Option<bool>)> = cols
7551                    .iter()
7552                    .zip(order_by.iter())
7553                    .map(|(c, o)| (*c, o.desc, o.nulls_first))
7554                    .collect();
7555                let mysql = ctx.mysql_dialect;
7556                // v7.38.19 — sort a PERMUTATION carrying the first eight
7557                // bytes, not the rows.
7558                //
7559                // The elements above are `(Vec<OrderKey>, Row)`, 48 bytes,
7560                // and driftsort moves them ~n log n times: 7.4 M moves at
7561                // 400,000 rows. Worse, every comparison chases three
7562                // dependent loads PER SIDE to reach the byte it wants --
7563                // the row's `Vec`, the `Value`, then the string's own
7564                // buffer -- and a profile of this sort put 35% of its
7565                // working samples in the sort machinery around that.
7566                //
7567                // A `(u64, u32)` is 16 bytes and the comparison reads it
7568                // straight out of the array. The u64 is the first eight
7569                // bytes big-endian, zero-padded, which ORDERS THE SAME as
7570                // the string: if two differ inside those bytes they differ
7571                // at the same index either way, and a string shorter than
7572                // eight pads with zeros exactly where `[u8]`'s own
7573                // comparison runs out. Equal prefixes fall through to the
7574                // full comparator, so nothing rests on the padding being
7575                // clever.
7576                //
7577                // The tail-break on the index is what keeps the sort
7578                // STABLE, which `sort_by` was giving for free and an
7579                // unstable sort over a permutation would not.
7580                // v7.38.19 — three ways to sort these rows, and which
7581                // one is right turns on the values, which is why it is
7582                // decided here rather than at plan time.
7583                //
7584                //   * the collation orders these values the way bytes do
7585                //     -- take the eight-byte key below
7586                //   * it does not, but there IS a collation -- build its
7587                //     sort key once per row and order the permutation on
7588                //     those, which is what the key path did, done from
7589                //     the projected value instead of during the scan
7590                //   * no collation at all -- the eight-byte key again
7591                //
7592                // The middle case is the one a draft got wrong by
7593                // leaving the rows to a key path whose keys it had just
7594                // skipped building.
7595                let mut keep_sorted = false;
7596                let bytes_answer = byte_order_answers_the_collation(&tagged, &terms, &order_colls);
7597                if !bytes_answer && let Some(coll) = order_colls.first().and_then(Option::as_ref) {
7598                    let (first_col, first_desc, _) = terms[0];
7599                    let mut order: Vec<(Vec<u8>, u32)> = Vec::with_capacity(tagged.len());
7600                    for (i, row) in tagged.iter().enumerate() {
7601                        let k = match row.1.values.get(first_col) {
7602                            Some(Value::Text(t)) => coll.sort_key_of(t).unwrap_or_else(|| {
7603                                let mut v = Vec::with_capacity(t.len() + 1);
7604                                v.push(0);
7605                                v.extend_from_slice(t.as_bytes());
7606                                v
7607                            }),
7608                            _ => Vec::new(),
7609                        };
7610                        order.push((k, u32::try_from(i).unwrap_or(u32::MAX)));
7611                    }
7612                    order.sort_by(|(ka, ia), (kb, ib)| {
7613                        let c = ka.cmp(kb);
7614                        let c = if first_desc { c.reverse() } else { c };
7615                        if c != core::cmp::Ordering::Equal {
7616                            return c;
7617                        }
7618                        row_cmp_by_index(&tagged, &terms, &order_colls, mysql, *ia, *ib)
7619                            .then_with(|| ia.cmp(ib))
7620                    });
7621                    let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7622                        core::mem::take(&mut tagged).into_iter().map(Some).collect();
7623                    tagged = order
7624                        .iter()
7625                        .map(|&(_, i)| {
7626                            slots[i as usize]
7627                                .take()
7628                                .expect("the permutation names each row once")
7629                        })
7630                        .collect();
7631                    keep_sorted = true;
7632                }
7633                // v7.38.20 — a key that does NOT discriminate is still
7634                // worth sorting on, as long as the runs it leaves are
7635                // handled once instead of n log n times.
7636                //
7637                // `text (26 values)` is two hundred identical characters
7638                // drawn from twenty-six letters, so every eight-byte
7639                // prefix inside a letter is the same and 15,384 rows tie
7640                // on it. A comparison sort then asks ~7.4 M questions of
7641                // which nearly all are a two-hundred-byte `memcmp`
7642                // answering EQUAL: profiled, 30% of the working samples
7643                // sat in `memcmp` and 37% in the sort machinery.
7644                //
7645                // Sorting the integer keys is cheap. What each run needs
7646                // afterwards is ONE pass: if every value in it is equal,
7647                // input order already IS the stable answer, and proving
7648                // that costs n-1 comparisons rather than n log n. Only a
7649                // run that is not all-equal gets sorted.
7650                //
7651                // Single-term only. With a second ORDER BY column an
7652                // all-equal first term does not settle the row order --
7653                // the later terms still speak -- and the shortcut would
7654                // drop them.
7655                let all_keys = if keep_sorted {
7656                    None
7657                } else {
7658                    sort_keys_of(&tagged, terms[0].0)
7659                };
7660                let low_card = !keep_sorted
7661                    && terms.len() == 1
7662                    && all_keys
7663                        .as_ref()
7664                        .is_some_and(|(keys, exact)| !*exact && !key_discriminates(keys));
7665                let keyed =
7666                    all_keys.filter(|(keys, exact)| *exact || key_discriminates(keys) || low_card);
7667                if keep_sorted {
7668                    // The collated permutation above already placed every
7669                    // row. A draft let the byte-order fallback run after
7670                    // it and undo the whole thing.
7671                } else if let Some((mut order, exact)) = keyed {
7672                    let (first_col, first_desc, _) = terms[0];
7673                    let row_cmp = |ia: u32, ib: u32| -> core::cmp::Ordering {
7674                        let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
7675                        for (col, desc, nf) in &terms {
7676                            let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col))
7677                            else {
7678                                continue;
7679                            };
7680                            let ord = match (va, vb) {
7681                                (Value::Text(x), Value::Text(y)) if !mysql => {
7682                                    let c = crate::orderby::str_cmp_prefix_first(x, y);
7683                                    if *desc { c.reverse() } else { c }
7684                                }
7685                                _ => {
7686                                    crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7687                                }
7688                            };
7689                            if ord != core::cmp::Ordering::Equal {
7690                                return ord;
7691                            }
7692                        }
7693                        core::cmp::Ordering::Equal
7694                    };
7695                    let _ = first_col;
7696                    if low_card {
7697                        // Integer sort first, then one pass per run.
7698                        order.sort_unstable_by(|&(pa, ia), &(pb, ib)| {
7699                            let c = pa.cmp(&pb);
7700                            let c = if first_desc { c.reverse() } else { c };
7701                            c.then_with(|| ia.cmp(&ib))
7702                        });
7703                        let mut lo = 0;
7704                        while lo < order.len() {
7705                            let mut hi = lo + 1;
7706                            while hi < order.len() && order[hi].0 == order[lo].0 {
7707                                hi += 1;
7708                            }
7709                            if hi - lo > 1 {
7710                                let head = tagged[order[lo].1 as usize].1.values.get(first_col);
7711                                let uniform = order[lo + 1..hi].iter().all(|&(_, i)| {
7712                                    tagged[i as usize].1.values.get(first_col) == head
7713                                });
7714                                if !uniform {
7715                                    order[lo..hi].sort_by(|&(_, ia), &(_, ib)| {
7716                                        row_cmp(ia, ib).then_with(|| ia.cmp(&ib))
7717                                    });
7718                                }
7719                                // A uniform run is already in index
7720                                // order, which IS the stable answer.
7721                            }
7722                            lo = hi;
7723                        }
7724                    } else {
7725                        order.sort_unstable_by(|&(pa, ia), &(pb, ib)| {
7726                            let c = pa.cmp(&pb);
7727                            let c = if first_desc { c.reverse() } else { c };
7728                            if c != core::cmp::Ordering::Equal {
7729                                return c;
7730                            }
7731                            // An EXACT key that ties means the values are
7732                            // equal, so only the remaining terms can speak.
7733                            // A prefix that ties has decided nothing yet and
7734                            // the first term must be asked again, which
7735                            // `row_cmp` does by walking every term from the
7736                            // start.
7737                            if exact && terms.len() == 1 {
7738                                return ia.cmp(&ib);
7739                            }
7740                            row_cmp(ia, ib).then_with(|| ia.cmp(&ib))
7741                        });
7742                    }
7743                    let mut slots: Vec<Option<(Vec<crate::orderby::OrderKey>, Row<'static>)>> =
7744                        core::mem::take(&mut tagged).into_iter().map(Some).collect();
7745                    tagged = order
7746                        .iter()
7747                        .map(|&(_, i)| {
7748                            slots[i as usize]
7749                                .take()
7750                                .expect("the permutation names each row once")
7751                        })
7752                        .collect();
7753                } else {
7754                    tagged.sort_by(|a, b| {
7755                        for (i, (col, desc, nf)) in terms.iter().enumerate() {
7756                            let va = a.1.values.get(*col);
7757                            let vb = b.1.values.get(*col);
7758                            let (Some(va), Some(vb)) = (va, vb) else {
7759                                continue;
7760                            };
7761                            let _ = i;
7762                            // v7.38.19 — two non-NULL strings, no MySQL fold, is
7763                            // where a text sort spends every one of its ~7 M
7764                            // comparisons, and the shared comparator cannot be
7765                            // inlined into this loop: it carries NULL placement,
7766                            // the fold, the NUMERIC bignum gate and the float
7767                            // total order. Answering that one pair here is the
7768                            // same answer by the same route — `value_cmp`'s
7769                            // leading same-variant arm is `x.cmp(y)`, and the
7770                            // raw comparator's last act is this reverse.
7771                            let ord = match (va, vb) {
7772                                (Value::Text(x), Value::Text(y)) if !mysql => {
7773                                    let c = crate::orderby::str_cmp_prefix_first(x, y);
7774                                    if *desc { c.reverse() } else { c }
7775                                }
7776                                _ => {
7777                                    crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql)
7778                                }
7779                            };
7780                            if ord != core::cmp::Ordering::Equal {
7781                                return ord;
7782                            }
7783                        }
7784                        core::cmp::Ordering::Equal
7785                    });
7786                }
7787            } else {
7788                crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &order_colls);
7789            }
7790        }
7791
7792        // v7.17.0 Phase 3.P0-49 — `FETCH FIRST … WITH TIES` extends
7793        // past the truncated tail through every row that shares the
7794        // last-kept row's ORDER BY key. The tie check uses the
7795        // already-computed `(order_keys, row)` pairs so it matches
7796        // the sort comparator exactly. DISTINCT + WITH TIES falls
7797        // through to the no-ties path (PG also disallows their
7798        // combination; SPG silently drops the tie extension here so
7799        // the customer doesn't see a hard error mid-query — the
7800        // user-visible result is still correct, just narrower).
7801        let output_rows: Vec<Row<'static>> = if stmt.limit_with_ties && !stmt.distinct {
7802            apply_offset_and_limit_tagged(
7803                &mut tagged,
7804                stmt.offset_literal(),
7805                stmt.limit_literal(),
7806                true,
7807            );
7808            tagged.into_iter().map(|(_, r)| r).collect()
7809        } else {
7810            // DISTINCT already de-duped pre-sort above.
7811            let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
7812            apply_offset_and_limit(
7813                &mut output_rows,
7814                stmt.offset_literal(),
7815                stmt.limit_literal(),
7816            );
7817            output_rows
7818        };
7819
7820        let columns: Vec<ColumnSchema> = projection
7821            .into_iter()
7822            .map(|p| p.to_column_schema())
7823            .collect();
7824
7825        Ok(QueryResult::Rows {
7826            columns,
7827            rows: output_rows,
7828        })
7829    }
7830
7831    /// v7.31 (perf — PG lesson #1): shared aggregate finisher. Apply
7832    /// OFFSET/LIMIT first, then evaluate the deferred subquery-bearing
7833    /// select items for the surviving rows only — PG's Result-above-
7834    /// Limit shape, where SubPlan loops equal the OUTPUT row count
7835    /// (50) instead of the group count (24k).
7836    fn finish_agg_result(
7837        &self,
7838        mut agg: aggregate::AggResult,
7839        stmt: &SelectStatement,
7840        cancel: CancelToken<'_>,
7841    ) -> Result<QueryResult, EngineError> {
7842        apply_offset_and_limit(&mut agg.rows, stmt.offset_literal(), stmt.limit_literal());
7843        if !agg.deferred.is_empty() {
7844            apply_offset_and_limit(
7845                &mut agg.synth_rows,
7846                stmt.offset_literal(),
7847                stmt.limit_literal(),
7848            );
7849            let ctx = EvalContext::new(&agg.synth_schema, None);
7850            let mut memo = memoize::MemoizeCache::default();
7851            // v7.32 (architecture v2 P3) — keyed index-probe seeding.
7852            // Deferred subqueries are referenced only by surviving
7853            // select-list rows (≤ LIMIT), so their correlation keys are
7854            // exactly the ≤LIMIT group keys in `synth_rows`. Pre-build
7855            // each batchable subquery's group map over just those keys
7856            // via per-key index seek; the per-row splice loop below then
7857            // reuses the seeded map. A join-shaped or un-indexed inner
7858            // falls through to the all-keys batch inside the call (built
7859            // eagerly here instead of lazily on row 0 — same cost), so
7860            // it still pays the full scan, never the 715 ms per-row
7861            // direct eval; its index-nested-loop probe is the next
7862            // knife. Genuinely non-batchable shapes return None and are
7863            // left unseeded for the loop's per-row resolver, as before.
7864            for (_, expr) in &agg.deferred {
7865                let mut subs: Vec<&SelectStatement> = Vec::new();
7866                collect_scalar_subqueries(expr, &mut subs);
7867                for sub in subs {
7868                    let repr = alloc::format!("{sub}");
7869                    if memo.group_maps.contains_key(&repr) {
7870                        continue;
7871                    }
7872                    if let Some(gm) = self.try_batch_correlated_scalar(
7873                        sub,
7874                        Some((&agg.synth_rows, &ctx)),
7875                        cancel,
7876                    )? {
7877                        memo.group_maps.insert(repr, Some(alloc::rc::Rc::new(gm)));
7878                    }
7879                }
7880            }
7881            for (ri, srow) in agg.synth_rows.iter().enumerate() {
7882                cancel.check()?;
7883                for (col, expr) in &agg.deferred {
7884                    let v =
7885                        self.eval_expr_with_correlated(expr, srow, &ctx, cancel, Some(&mut memo))?;
7886                    if let Some(cell) = agg.rows[ri].values.get_mut(*col) {
7887                        *cell = v;
7888                    }
7889                }
7890            }
7891        }
7892        Ok(QueryResult::Rows {
7893            columns: agg.columns,
7894            rows: agg.rows,
7895        })
7896    }
7897
7898    /// v7.37 — streaming projection for the joined-non-aggregate
7899    /// shape (multi-table FROM, all projection items bound, no
7900    /// ORDER BY / DISTINCT / GROUP BY / HAVING / LIMIT / OFFSET /
7901    /// UNION). Walks the deferred join survivors and emits
7902    /// `&[&Value]` borrowed straight out of the source tables — no
7903    /// `.cloned()`, no `Vec<Row<'static>>`. Skips the 25 k × 3-TEXT clone tax
7904    /// on the mailrs `PROJ` shape (about 4 ms saved).
7905    ///
7906    /// Returns `Ok(None)` when the shape doesn't qualify; the caller
7907    /// then falls back to the materialising path.
7908    /// v7.37 (round 831) — stream a joinless SELECT straight off the
7909    /// stored table, one row at a time, without ever building a row set.
7910    ///
7911    /// Returns `Ok(None)` for anything this cannot serve, and the caller
7912    /// falls through to the deferred-join path exactly as before: a
7913    /// missing table, or a cold tier whose hydration the fallback handles.
7914    /// Sort a single-table scan through the external sorter, so the
7915    /// answer's size is bounded by `work_mem` and not by the input.
7916    ///
7917    /// Sorting held every row twice — the scan's `Vec<Row>` and the
7918    /// sort's `Vec<(keys, Row)>` beside it — with nothing bounding
7919    /// either: 807 MB at 400k rows, whatever `work_mem` said. A large
7920    /// enough ORDER BY took the server down, which is a liveness
7921    /// problem before it is a performance one.
7922    ///
7923    /// A SEPARATE walk rather than a change to `run_single_table_scan`,
7924    /// following what round 831 did for the joinless shape. That
7925    /// function is 552 lines whose projection loop is entangled with
7926    /// DISTINCT (which indexes back into the tagged vector) and with
7927    /// streaming top-N (whose boundary moves as the scan runs); both
7928    /// assume the projection has already happened when a row is
7929    /// pushed, which is exactly what spilling has to defer. Two earlier
7930    /// attempts tried to rework that loop and were reverted. Here the
7931    /// existing path is untouched and this one only claims shapes it
7932    /// can serve, so a decline costs nothing.
7933    ///
7934    /// Records are SOURCE rows, not projected ones: `finish` re-derives
7935    /// keys from what it decodes, and an ORDER BY key need not be in
7936    /// the projection — `SELECT pad FROM big ORDER BY id` (round 835).
7937    fn try_spill_sorted_scan(
7938        &self,
7939        stmt: &SelectStatement,
7940        from: &FromClause,
7941        cancel: CancelToken<'_>,
7942    ) -> Result<Option<QueryResult>, EngineError> {
7943        // Shapes this walk does not serve. Each one either needs the
7944        // whole tagged vector addressable (DISTINCT probes back into
7945        // it, WITH TIES re-reads its tail) or is already bounded
7946        // without spilling (a LIMIT makes the partial sort O(keep)).
7947        if !self.can_spill()
7948            || stmt.order_by.is_empty()
7949            || stmt.distinct
7950            || stmt.limit_with_ties
7951            || stmt.limit_literal().is_some()
7952            || !from.joins.is_empty()
7953            || from.primary.lateral_subquery.is_some()
7954            || from.primary.unnest_expr.is_some()
7955            || from.primary.generate_series_args.is_some()
7956            || select_has_window(stmt)
7957        {
7958            return Ok(None);
7959        }
7960        // A parent's rows are its children's. These walks scan the named
7961        // relation alone, so a partitioned or inherited parent comes back
7962        // short — and silently: the corpus caught `SELECT id FROM pr
7963        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
7964        // parent's own rows instead of the partitions'. `ONLY` is exactly
7965        // the case that does not fan out, so it stays, which is the test
7966        // the FROM-clause fan-out itself makes.
7967        if !from.primary.only
7968            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
7969        {
7970            return Ok(None);
7971        }
7972        let Some(table) = self.active_catalog().get(&from.primary.name) else {
7973            return Ok(None);
7974        };
7975        // Cold-tier rows live outside `rows()`; this walk would drop
7976        // them silently, the same reason round 831's walk declines.
7977        if table.has_cold_rows_fast() {
7978            return Ok(None);
7979        }
7980
7981        let alias = from
7982            .primary
7983            .alias
7984            .as_deref()
7985            .unwrap_or(from.primary.name.as_str());
7986        let cols = table.schema().columns.clone();
7987        let sess = self.dml_session();
7988        let ctx = EvalContext::new(&cols, Some(alias))
7989            .with_catalog(self.active_catalog())
7990            .with_session(&sess);
7991        let projection = build_projection(
7992            &stmt.items,
7993            &cols,
7994            alias,
7995            self.backslash_escapes,
7996            Some(self.active_catalog()),
7997        )?;
7998        let order_by = stmt.order_by.clone();
7999        // The same one-shot resolution the general path does (round
8000        // 582): each ORDER BY column is bound once, not once per row.
8001        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8002        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8003        // Resolved BEFORE the scan, because it now decides what the sort
8004        // STORES and not just what it decodes (round 995).
8005        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8006
8007        let mut sorter = crate::extsort::ExternalSorter::new(
8008            self.temp_run_factory,
8009            self.session_work_mem_bytes(),
8010            cols.clone(),
8011            &descs,
8012        )
8013        .with_stats(&self.spill_stats)
8014        .with_pruned(&needed);
8015        let snapshot = self.current_snapshot();
8016        // One key buffer for the whole scan: `push` drains it and leaves
8017        // the capacity behind.
8018        let mut keys: Vec<OrderKey> = Vec::new();
8019        // r1024 — compile the predicate once for the scan.
8020        //
8021        // These two sorted-spill scans are the paths a single-table SELECT
8022        // with an ORDER BY takes, and they were the last row-returning ones
8023        // still walking the expression tree per row. r1023 did the
8024        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8025        // exactly this shape.
8026        //
8027        // Found from the profile's CALL TREE rather than its leaves. The
8028        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8029        // 261, `mod_op` 178 — and two attempts at reasoning out which
8030        // function asked for it were both wrong. The tree names the caller
8031        // chain, and it named this one.
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        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8038        for (i, row) in table.scan_visible_from(0, &snapshot) {
8039            if i.is_multiple_of(256) {
8040                cancel.check()?;
8041            }
8042            if let Some(c) = &compiled_where {
8043                if !crate::eval::compiled::eval_compiled_pred(
8044                    c,
8045                    row,
8046                    &ctx,
8047                    &mut eval_stack,
8048                    ctx.mysql_dialect,
8049                )? {
8050                    continue;
8051                }
8052            } else if let Some(w) = &stmt.where_ {
8053                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
8054                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
8055                    continue;
8056                }
8057            }
8058            keys.clear();
8059            // `&[]`: this sorter compares with `cmp_multi_key_in(.., &[])`
8060            // (extsort.rs:213/543/1221), so the key must stay folded or the
8061            // two would disagree. See `build_order_keys_bound`.
8062            crate::orderby::build_order_keys_bound(
8063                &order_by,
8064                &order_bound,
8065                &[],
8066                row,
8067                &ctx,
8068                &mut keys,
8069            )?;
8070            sorter.push(&mut keys, row)?;
8071        }
8072
8073        let key_ctx = &ctx;
8074        let rows = sorter.finish(
8075            |src, buf| {
8076                crate::orderby::build_order_keys_bound(
8077                    &order_by,
8078                    &order_bound,
8079                    &[],
8080                    src,
8081                    key_ctx,
8082                    buf,
8083                )
8084            },
8085            |src| {
8086                let mut values = Vec::with_capacity(projection.len());
8087                for p in &projection {
8088                    values.push(
8089                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
8090                    );
8091                }
8092                Ok(Row::new(values))
8093            },
8094        )?;
8095
8096        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8097        Ok(Some(QueryResult::Rows { columns, rows }))
8098    }
8099
8100    /// v7.37 (round 882) — the bounded sort of `try_spill_sorted_scan`,
8101    /// handing each row to the consumer instead of collecting the answer.
8102    ///
8103    /// That walk bounds the SORT and then returns `QueryResult::Rows`,
8104    /// which holds every output row. Measured at `work_mem = 4 MB` over
8105    /// 200-byte rows, RSS above the server's own baseline while the
8106    /// query runs grew +30 MB at 100k rows, +68 MB at 200k and +137 MB
8107    /// at 400k — linear — while the spill underneath worked correctly
8108    /// (9 / 17 / 33 runs, witnessed DURING the query; `FileRun::drop`
8109    /// removes each file, so a count taken afterwards reads 0 whatever
8110    /// happened, and an earlier reading of "no spill at all" was that
8111    /// blind witness). The growth is the collected result, not the sort.
8112    ///
8113    /// Emitting makes peak the budget, one buffer per run and a single
8114    /// row — the state a merge already holds at every step. It also
8115    /// frees each projected row as the next is built rather than
8116    /// accumulating them, which is where the time is: a profile of the
8117    /// collecting walk put the allocator at 586 samples, more than every
8118    /// sort comparison combined (420), against 19 for `push` itself.
8119    /// v7.37 (round 923) — which of a sort record's columns the output half
8120    /// reads. The record is the SOURCE row (round 836), so a narrow projection
8121    /// decoded every column: skipping one 200-byte text halves a decode
8122    /// (2.17 -> 1.14 ms per pass at 10k rows, priced additively).
8123    ///
8124    /// Timid on purpose — a wrong mask is a SILENT wrong answer, a pruned
8125    /// column reads NULL. Answers only when every projection item is a bare
8126    /// column reference AND every ORDER BY key is a bound column; anything
8127    /// else returns empty, decoding everything as before.
8128    /// `explain.rs`'s `collect_column_refs` is NOT used: its `_ => {}` arm
8129    /// drops references from expression kinds it does not enumerate.
8130    ///
8131    /// ORDER BY columns are included — the merge re-derives keys from the
8132    /// decoded row on the spilled path, so pruning one would sort NULLs.
8133    pub(crate) fn sort_record_columns_needed(
8134        items: &[SelectItem],
8135        order_bound: &[Option<usize>],
8136        arity: usize,
8137        ctx: &EvalContext,
8138    ) -> Vec<bool> {
8139        let all_bare = items.iter().all(|i| {
8140            matches!(
8141                i,
8142                SelectItem::Expr {
8143                    expr: Expr::Column(_),
8144                    ..
8145                }
8146            )
8147        });
8148        if !all_bare || order_bound.iter().any(Option::is_none) {
8149            return Vec::new();
8150        }
8151        let mut mask = alloc::vec![false; arity];
8152        for item in items {
8153            if let SelectItem::Expr {
8154                expr: Expr::Column(c),
8155                ..
8156            } = item
8157            {
8158                match crate::eval::find_column_pos(c, ctx) {
8159                    Some(p) if p < arity => mask[p] = true,
8160                    _ => return Vec::new(),
8161                }
8162            }
8163        }
8164        for p in order_bound.iter().flatten() {
8165            if *p < arity {
8166                mask[*p] = true;
8167            } else {
8168                return Vec::new();
8169            }
8170        }
8171        mask
8172    }
8173
8174    /// r1025 — `ORDER BY <indexed NOT NULL column>` walks the index instead
8175    /// of sorting.
8176    ///
8177    /// PG serves such an ordering from the index and never sorts. We sorted:
8178    /// measured at 400,000 rows, `SELECT pad FROM t ORDER BY id` costs
8179    /// 138-144 ms against PG18's 64-75, and the call tree puts the cost in
8180    /// the sorter's own round trip — `ExternalSorter::finish_each` →
8181    /// `next_row` → `decode_row_body_dense_pruned` → `read_value_body`.
8182    /// Every row is encoded into the sorter's arena and decoded back out,
8183    /// for an order the index already holds.
8184    ///
8185    /// The walk exists — `try_pk_walk_top_n` — and requires a `LIMIT`,
8186    /// because it was built for top-N. This is the unbounded sibling.
8187    ///
8188    /// NOT NULL is a hard gate, not a simplification: a NULL key is absent
8189    /// from a btree, so walking one would silently drop those rows. That is
8190    /// exactly the defect r1020 fixed on the top-N path, where it had
8191    /// shipped.
8192    /// r1044 — the index this statement's ORDER BY can be WALKED on,
8193    /// instead of sorted, or `None`.
8194    ///
8195    /// Extracted so `EXPLAIN` can ask the same question the executor
8196    /// answers. It could not, and said so: `SELECT pad FROM t ORDER BY
8197    /// id` on a 400,000-row table planned as `Sort` over `Seq Scan`
8198    /// while the executor walked the primary key — 34.9 ms against
8199    /// 147.0 for the same query ordered by an unindexed column, so the
8200    /// walk was plainly running. Round 551 fixed a different case of
8201    /// this and wrote the reason down: EXPLAIN is the first thing any
8202    /// performance question opens, and an instrument that misnames the
8203    /// access path is worse than one that says nothing.
8204    ///
8205    /// The gate is here once. Two copies of it is how the plan and the
8206    /// executor come to disagree again.
8207    pub(crate) fn index_order_walk_target(
8208        &self,
8209        stmt: &SelectStatement,
8210        from: &FromClause,
8211    ) -> Option<(String, usize)> {
8212        if stmt.order_by.len() != 1
8213            || !stmt.distinct_on.is_empty()
8214            || stmt.limit_with_ties
8215            || stmt.limit.is_some()
8216            || stmt.offset.is_some()
8217            || stmt.having.is_some()
8218            || stmt.group_by.is_some()
8219            || !stmt.unions.is_empty()
8220            || !from.joins.is_empty()
8221            || from.primary.lateral_subquery.is_some()
8222            || from.primary.unnest_expr.is_some()
8223            || from.primary.as_of_segment.is_some()
8224            || from.primary.generate_series_args.is_some()
8225            || select_has_window(stmt)
8226            || aggregate::uses_aggregate(stmt)
8227        {
8228            return None;
8229        }
8230        if stmt
8231            .items
8232            .iter()
8233            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8234        {
8235            return None;
8236        }
8237        let table = self.active_catalog().get(&from.primary.name)?;
8238        if table.has_cold_rows_fast() {
8239            return None;
8240        }
8241        if !from.primary.only
8242            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8243        {
8244            return None;
8245        }
8246        let alias = from
8247            .primary
8248            .alias
8249            .as_deref()
8250            .unwrap_or(from.primary.name.as_str());
8251        let cols = &table.schema().columns;
8252        let order = &stmt.order_by[0];
8253        let Expr::Column(oc) = &order.expr else {
8254            return None;
8255        };
8256        if let Some(q) = &oc.qualifier
8257            && !q.eq_ignore_ascii_case(alias)
8258        {
8259            return None;
8260        }
8261        let order_pos = cols
8262            .iter()
8263            .position(|c| c.name.eq_ignore_ascii_case(&oc.name))?;
8264        // r1047 — DISTINCT joins the walk when the projection IS the
8265        // order column, and only then. The index's keys are canonical
8266        // (r1039: representation equality is value equality — the
8267        // property every seek already depends on), so one key is one
8268        // distinct value and the walk can emit the first passing row of
8269        // each key group instead of hashing every row. On the release
8270        // sweep's `SELECT DISTINCT n FROM t ORDER BY n` — 400,000 rows,
8271        // 1,000 distinct values — the hash path priced at 21.3-22.7 ms
8272        // with an ablation floor of 14.8, because the hash must
8273        // normalize and probe ALL the rows; the walk visits each key
8274        // once. A wider projection makes DISTINCT about the whole tuple,
8275        // not the key, so anything else still declines.
8276        if stmt.distinct {
8277            let only_the_order_column = stmt.items.len() == 1
8278                && match &stmt.items[0] {
8279                    SelectItem::Expr {
8280                        expr: Expr::Column(c),
8281                        ..
8282                    } => {
8283                        c.name.eq_ignore_ascii_case(&oc.name)
8284                            && match &c.qualifier {
8285                                Some(q) => q.eq_ignore_ascii_case(alias),
8286                                None => true,
8287                            }
8288                    }
8289                    _ => false,
8290                };
8291            if !only_the_order_column {
8292                return None;
8293            }
8294        }
8295        // r1046 — a nullable key no longer refuses the walk; it changes
8296        // what the walk has to do. A NULL key is not in the btree, so
8297        // walking alone would silently drop those rows — the r1020
8298        // defect, which shipped once. The walk emits them separately, at
8299        // the end SQL puts them.
8300        //
8301        // Refusing was costing every nullable indexed column a 3.4x:
8302        // `SELECT id FROM t ORDER BY b` over 400,000 rows measured
8303        // 72.0 ms with the column nullable and 20.2 with the same data
8304        // under NOT NULL. `NOT NULL` is not the default, so that was the
8305        // common case paying for the uncommon one.
8306        let index = table.index_on(order_pos)?;
8307        if !matches!(index.kind, spg_storage::IndexKind::BTree(_))
8308            || index.expression.is_some()
8309            || index.partial_predicate.is_some()
8310        {
8311            return None;
8312        }
8313        Some((index.name.clone(), order_pos))
8314    }
8315
8316    fn try_index_order_stream<F>(
8317        &self,
8318        stmt: &SelectStatement,
8319        from: &FromClause,
8320        cancel: CancelToken<'_>,
8321        emit: &mut F,
8322    ) -> Result<Option<usize>, EngineError>
8323    where
8324        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8325    {
8326        // r1044 — the shape gate lives in `index_order_walk_target`, so
8327        // `EXPLAIN` answers the same question. What stays here is the
8328        // part that RAISES (an illegal ORDER BY has to keep erroring
8329        // from where it did) and the bindings the walk needs.
8330        crate::orderby::check_order_by_legality(stmt)?;
8331        crate::orderby::check_order_by_positions(stmt)?;
8332        crate::window::reject_window_in_row_clauses(stmt)?;
8333        let Some((_, order_pos)) = self.index_order_walk_target(stmt, from) else {
8334            return Ok(None);
8335        };
8336        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8337            return Ok(None);
8338        };
8339        let alias = from
8340            .primary
8341            .alias
8342            .as_deref()
8343            .unwrap_or(from.primary.name.as_str());
8344        let cols = table.schema().columns.clone();
8345        let order = &stmt.order_by[0];
8346        let Some(index) = table.index_on(order_pos) else {
8347            return Ok(None);
8348        };
8349
8350        let sess = self.dml_session();
8351        let ctx = EvalContext::new(&cols, Some(alias))
8352            .with_catalog(self.active_catalog())
8353            .with_session(&sess);
8354        let projection = build_projection(
8355            &stmt.items,
8356            &cols,
8357            alias,
8358            self.backslash_escapes,
8359            Some(self.active_catalog()),
8360        )?;
8361        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8362        emit(crate::StreamItem::Header(&columns))?;
8363        let bound_pos: Vec<Option<usize>> = projection
8364            .iter()
8365            .map(|p| match &p.expr {
8366                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8367                    Ok(Some(pos)) => Some(pos),
8368                    _ => None,
8369                },
8370                _ => None,
8371            })
8372            .collect();
8373
8374        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8375            .where_
8376            .as_ref()
8377            .filter(|w| crate::eval::fully_compilable(w))
8378            .map(|w| crate::eval::compile_expr(w, &ctx));
8379        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8380        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8381        let snapshot = self.current_snapshot();
8382
8383        // A btree holds one locator per row VERSION, so a row whose key was
8384        // updated can sit under two keys and a dead one can sit beside its
8385        // replacement. The visibility gate drops the dead; `seen` drops a
8386        // live row that the walk reaches twice, which would otherwise be a
8387        // duplicated output row rather than a slow one.
8388        let mut emitted_rows = alloc::vec![false; table.rows().len()];
8389
8390        // r1046 — the rows the index cannot hold.
8391        //
8392        // A NULL key is not in the btree, so the walk below never reaches
8393        // those rows; they are emitted here, at the end SQL puts them.
8394        // PG's default is NULLS LAST ascending and NULLS FIRST
8395        // descending, and an explicit `NULLS FIRST` / `NULLS LAST` wins —
8396        // the same rule `order_by_value_cmp_raw` applies to the sort this
8397        // replaces, so the two orders agree.
8398        //
8399        // Finding them costs one pass over the column. That pass is why
8400        // this is still worth doing: the sort it replaces encodes and
8401        // decodes every row, and the walk plus the pass measured 72.0 ms
8402        // down to about 22 on 400,000 rows.
8403        let nulls_first = order.nulls_first.unwrap_or(order.desc);
8404        // r1047 — under DISTINCT the walk emits the FIRST passing row of
8405        // each key group and skips the rest; the gate admits DISTINCT
8406        // only when the projection is the order column itself, so one
8407        // canonical key is one output row. NULL is one distinct value,
8408        // so the NULL pass stops at its first emit too.
8409        let distinct = stmt.distinct;
8410        let mut count = 0usize;
8411        let mut visited = 0usize;
8412        let mut emit_null_rows = |emitted_rows: &mut alloc::vec::Vec<bool>,
8413                                  eval_stack: &mut Vec<Value<'static>>,
8414                                  values: &mut Vec<Value<'static>>,
8415                                  visited: &mut usize,
8416                                  emit: &mut F|
8417         -> Result<usize, EngineError> {
8418            if !cols[order_pos].nullable {
8419                return Ok(0);
8420            }
8421            let mut n = 0usize;
8422            for (ri, row) in table.rows().iter().enumerate() {
8423                if !matches!(row.values.get(order_pos), Some(Value::Null)) {
8424                    continue;
8425                }
8426                if emitted_rows.get(ri).copied().unwrap_or(true) {
8427                    continue;
8428                }
8429                if !table.is_row_visible(ri, &snapshot) {
8430                    continue;
8431                }
8432                *visited += 1;
8433                if visited.is_multiple_of(256) {
8434                    cancel.check()?;
8435                }
8436                emitted_rows[ri] = true;
8437                if Self::stream_project_row(
8438                    row,
8439                    stmt.where_.as_ref(),
8440                    compiled_where.as_ref(),
8441                    eval_stack,
8442                    &projection,
8443                    &bound_pos,
8444                    &ctx,
8445                    values,
8446                    emit,
8447                )? {
8448                    n += 1;
8449                    if distinct {
8450                        break;
8451                    }
8452                }
8453            }
8454            Ok(n)
8455        };
8456
8457        if nulls_first {
8458            count += emit_null_rows(
8459                &mut emitted_rows,
8460                &mut eval_stack,
8461                &mut values,
8462                &mut visited,
8463                emit,
8464            )?;
8465        }
8466
8467        let walker: alloc::boxed::Box<
8468            dyn Iterator<Item = (&spg_storage::IndexKey, &spg_storage::PostingList)>,
8469        > = if order.desc {
8470            alloc::boxed::Box::new(index.iter_desc())
8471        } else {
8472            alloc::boxed::Box::new(index.iter_asc())
8473        };
8474        for (_key, locators) in walker {
8475            for loc in locators {
8476                let spg_storage::RowLocator::Hot(ri) = *loc else {
8477                    continue;
8478                };
8479                if emitted_rows.get(ri).copied().unwrap_or(true) {
8480                    continue;
8481                }
8482                if !table.is_row_visible(ri, &snapshot) {
8483                    continue;
8484                }
8485                let Some(row) = table.rows().get(ri) else {
8486                    continue;
8487                };
8488                visited += 1;
8489                if visited.is_multiple_of(256) {
8490                    cancel.check()?;
8491                }
8492                emitted_rows[ri] = true;
8493                if Self::stream_project_row(
8494                    row,
8495                    stmt.where_.as_ref(),
8496                    compiled_where.as_ref(),
8497                    &mut eval_stack,
8498                    &projection,
8499                    &bound_pos,
8500                    &ctx,
8501                    &mut values,
8502                    emit,
8503                )? {
8504                    count += 1;
8505                    // One row per key group: the rest are the same value.
8506                    if distinct {
8507                        break;
8508                    }
8509                }
8510            }
8511        }
8512
8513        if !nulls_first {
8514            count += emit_null_rows(
8515                &mut emitted_rows,
8516                &mut eval_stack,
8517                &mut values,
8518                &mut visited,
8519                emit,
8520            )?;
8521        }
8522        Ok(Some(count))
8523    }
8524
8525    /// r1031 — `ORDER BY` over NOT NULL integer columns, sorted without
8526    /// building an `OrderKey` vector per row.
8527    ///
8528    /// The row-returning sorted scan allocates twice per row: one
8529    /// `Vec<OrderKey>` for the sort keys and one `Vec<Value>` for the
8530    /// projection. Counted over 400 k rows (r1030,
8531    /// `docs/PERF_SORTED_SCAN_ALLOCATIONS_2026-08-15.md`), that is 800,067
8532    /// allocations and 208 MB of traffic for an answer of four hundred
8533    /// thousand integers.
8534    ///
8535    /// The key half is pure ceremony on this shape.
8536    /// `sort_tagged_by_inline_int_key` already sorts indices rather than
8537    /// rows, so the per-row vector is built, has one integer taken out of
8538    /// it, and is then dragged through the permutation — it exists to carry
8539    /// a number the row's column already held. This lane carries the number
8540    /// instead, in a fixed-size array that lives inside the buffer element
8541    /// and allocates nothing. Same idea as the predicate VM's integer lane.
8542    ///
8543    /// Declines to `None` for anything it does not cover, and every caller
8544    /// falls through to the general path, so the gate list is the
8545    /// specification.
8546    ///
8547    /// Ties: equal keys keep scan order, as the stable sort on the general
8548    /// path does. Rows that tie on every ORDER BY term are entitled to any
8549    /// order among themselves either way — see `STABILITY.md`.
8550    fn try_int_key_sorted_stream<F>(
8551        &self,
8552        stmt: &SelectStatement,
8553        from: &FromClause,
8554        cancel: CancelToken<'_>,
8555        emit: &mut F,
8556    ) -> Result<Option<usize>, EngineError>
8557    where
8558        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8559    {
8560        /// Sort terms this lane carries inline. Four covers every ORDER BY
8561        /// in the endpoint sweep and in the dogfood corpus; wider ones fall
8562        /// through rather than growing the buffer element for everybody.
8563        const MAX_KEYS: usize = 4;
8564
8565        if stmt.order_by.is_empty()
8566            || stmt.order_by.len() > MAX_KEYS
8567            // v7.38.14 — DISTINCT is admitted when the projected set is
8568            // exactly the ORDER BY set, and only then. This lane sorts, and
8569            // when the sort key determines the projected row every duplicate
8570            // lands ADJACENT to its twin -- so the de-duplication is a
8571            // comparison with the previous row rather than a hash table, and
8572            // the reason this lane declined DISTINCT disappears with it. The
8573            // seen-set it could not offer held indices into a materialised
8574            // vector; there is no seen-set now.
8575            //
8576            // The gate is as narrow as the bare-GROUP-BY rewrite's for the
8577            // same reason: `ORDER BY a` over a projection of `a, b` does NOT
8578            // place duplicates of the PAIR adjacent, so set EQUALITY, never
8579            // overlap.
8580            || (stmt.distinct && !Self::distinct_is_adjacent_after_sort(stmt))
8581            || stmt.limit_with_ties
8582            || stmt.limit.is_some()
8583            || stmt.offset.is_some()
8584            || stmt.having.is_some()
8585            || stmt.group_by.is_some()
8586            || !stmt.unions.is_empty()
8587            || !from.joins.is_empty()
8588            || from.primary.lateral_subquery.is_some()
8589            || from.primary.unnest_expr.is_some()
8590            || from.primary.as_of_segment.is_some()
8591            || from.primary.generate_series_args.is_some()
8592            || select_has_window(stmt)
8593            || aggregate::uses_aggregate(stmt)
8594        {
8595            return Ok(None);
8596        }
8597        if stmt
8598            .items
8599            .iter()
8600            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8601        {
8602            return Ok(None);
8603        }
8604        crate::orderby::check_order_by_legality(stmt)?;
8605        crate::orderby::check_order_by_positions(stmt)?;
8606        crate::window::reject_window_in_row_clauses(stmt)?;
8607        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8608            return Ok(None);
8609        };
8610        if table.has_cold_rows_fast() {
8611            return Ok(None);
8612        }
8613        if !from.primary.only
8614            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8615        {
8616            return Ok(None);
8617        }
8618        let alias = from
8619            .primary
8620            .alias
8621            .as_deref()
8622            .unwrap_or(from.primary.name.as_str());
8623        let cols = table.schema().columns.clone();
8624
8625        // Every ORDER BY term must be a NOT NULL integer column of this
8626        // table. NOT NULL is what lets the key be a bare integer: with
8627        // NULLs the lane would have to carry their ordering too, and
8628        // getting that subtly wrong is the r1020 defect.
8629        let mut key_pos = [0usize; MAX_KEYS];
8630        let mut descs = [false; MAX_KEYS];
8631        // PG's default is NULLS LAST for ASC and NULLS FIRST for DESC,
8632        // which the AST records as `None`; `unwrap_or(desc)` is how the
8633        // rest of the engine resolves it.
8634        let mut nulls_first = [false; MAX_KEYS];
8635        let n_keys = stmt.order_by.len();
8636        for (slot, order) in stmt.order_by.iter().enumerate() {
8637            let Expr::Column(oc) = &order.expr else {
8638                return Ok(None);
8639            };
8640            if let Some(q) = &oc.qualifier
8641                && !q.eq_ignore_ascii_case(alias)
8642            {
8643                return Ok(None);
8644            }
8645            let Some(pos) = cols
8646                .iter()
8647                .position(|c| c.name.eq_ignore_ascii_case(&oc.name))
8648            else {
8649                return Ok(None);
8650            };
8651            if !matches!(
8652                cols[pos].ty,
8653                spg_storage::DataType::SmallInt
8654                    | spg_storage::DataType::Int
8655                    | spg_storage::DataType::BigInt
8656            ) {
8657                return Ok(None);
8658            }
8659            key_pos[slot] = pos;
8660            descs[slot] = order.desc;
8661            nulls_first[slot] = order.nulls_first.unwrap_or(order.desc);
8662        }
8663
8664        let sess = self.dml_session();
8665        let ctx = EvalContext::new(&cols, Some(alias))
8666            .with_catalog(self.active_catalog())
8667            .with_session(&sess);
8668        let projection = build_projection(
8669            &stmt.items,
8670            &cols,
8671            alias,
8672            self.backslash_escapes,
8673            Some(self.active_catalog()),
8674        )?;
8675        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
8676        let bound_pos: Vec<Option<usize>> = projection
8677            .iter()
8678            .map(|p| match &p.expr {
8679                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
8680                    Ok(Some(pos)) => Some(pos),
8681                    _ => None,
8682                },
8683                _ => None,
8684            })
8685            .collect();
8686        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8687            .where_
8688            .as_ref()
8689            .filter(|w| crate::eval::fully_compilable(w))
8690            .map(|w| crate::eval::compile_expr(w, &ctx));
8691
8692        // The same first-observable point the materialising planner fires,
8693        // placed after the gates so it fires exactly once: this lane runs
8694        // BEFORE that planner and would otherwise be a hole in the
8695        // panic-isolation and cancellation-race coverage rather than a
8696        // faster path through it.
8697        crate::injection_point!("planner_first_row_fetch", &stmt.from);
8698
8699        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8700        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
8701        let mut budget = ByteBudget::new(self.max_query_bytes);
8702        let snapshot = self.current_snapshot();
8703        // Keys, a NULL bit per key slot, and the row. The bitmask keeps
8704        // the element small: a nullable key still costs one bit rather
8705        // than a second array.
8706        let mut sorted: Vec<([i64; MAX_KEYS], u8, Vec<Value<'static>>)> = Vec::new();
8707
8708        for (ri, row) in table.rows().iter().enumerate() {
8709            if ri.is_multiple_of(256) {
8710                cancel.check()?;
8711            }
8712            if !table.is_row_visible(ri, &snapshot) {
8713                continue;
8714            }
8715            // The key comes from the STORED row, before projection: an
8716            // ORDER BY column need not appear in the select list.
8717            let mut keys = [0i64; MAX_KEYS];
8718            let mut nulls = 0u8;
8719            let mut keyed = true;
8720            for slot in 0..n_keys {
8721                match row.values.get(key_pos[slot]) {
8722                    Some(Value::SmallInt(v)) => keys[slot] = i64::from(*v),
8723                    Some(Value::Int(v)) => keys[slot] = i64::from(*v),
8724                    Some(Value::BigInt(v)) => keys[slot] = *v,
8725                    Some(Value::Null) | None => nulls |= 1 << slot,
8726                    // An integer column holding something else is a row
8727                    // this lane cannot order; hand the whole query back
8728                    // rather than guess at it.
8729                    _ => {
8730                        keyed = false;
8731                        break;
8732                    }
8733                }
8734            }
8735            if !keyed {
8736                return Ok(None);
8737            }
8738            if !Self::stream_filter_project(
8739                row,
8740                stmt.where_.as_ref(),
8741                compiled_where.as_ref(),
8742                &mut eval_stack,
8743                &projection,
8744                &bound_pos,
8745                &ctx,
8746                &mut values,
8747            )? {
8748                continue;
8749            }
8750            budget.charge(crate::bytebudget::approx_values_bytes(&values))?;
8751            sorted.push((keys, nulls, core::mem::take(&mut values)));
8752            values.reserve(projection.len());
8753        }
8754
8755        sorted.sort_by(|a, b| {
8756            use core::cmp::Ordering;
8757            for slot in 0..n_keys {
8758                let bit = 1u8 << slot;
8759                let ord = match (a.1 & bit != 0, b.1 & bit != 0) {
8760                    (true, true) => Ordering::Equal,
8761                    // Where the NULLs go is already decided — `nulls_first`
8762                    // resolved DESC's default when it was read. Reversing
8763                    // this for DESC as well would apply the direction
8764                    // twice and put them at the wrong end.
8765                    (true, false) => {
8766                        if nulls_first[slot] {
8767                            Ordering::Less
8768                        } else {
8769                            Ordering::Greater
8770                        }
8771                    }
8772                    (false, true) => {
8773                        if nulls_first[slot] {
8774                            Ordering::Greater
8775                        } else {
8776                            Ordering::Less
8777                        }
8778                    }
8779                    (false, false) => {
8780                        let o = a.0[slot].cmp(&b.0[slot]);
8781                        if descs[slot] { o.reverse() } else { o }
8782                    }
8783                };
8784                if ord != Ordering::Equal {
8785                    return ord;
8786                }
8787            }
8788            Ordering::Equal
8789        });
8790
8791        emit(crate::StreamItem::Header(&columns))?;
8792        // v7.38.14 — DISTINCT, de-duplicated against the PREVIOUS row.
8793        //
8794        // The gate above only admits DISTINCT when the sort key determines
8795        // the projected row, so every duplicate is adjacent to its twin by
8796        // the time this loop runs and one comparison replaces a hash table
8797        // of every row seen. Equality is `values_eq_norm` with the same mask
8798        // the materialising path builds -- deliberately the same function,
8799        // because a de-duplication that disagreed with the one on the other
8800        // path would make the answer depend on which lane a query took.
8801        //
8802        // A query that did not ask for DISTINCT pays one already-false bool
8803        // test per row: the short-circuit means the comparison never runs
8804        // and `prev` is never written.
8805        let dedup_mask = fold_mask(&projection);
8806        let fold = FoldSpec::of(self.backslash_escapes, &dedup_mask);
8807        let mut count = 0usize;
8808        let mut prev: Option<&[Value<'static>]> = None;
8809        for (_, _, vals) in &sorted {
8810            if stmt.distinct
8811                && let Some(p) = prev
8812                && values_eq_norm(p, vals, fold)
8813            {
8814                continue;
8815            }
8816            emit(crate::StreamItem::Row(crate::RowCells::Values(vals)))?;
8817            count += 1;
8818            if stmt.distinct {
8819                prev = Some(vals);
8820            }
8821        }
8822        Ok(Some(count))
8823    }
8824
8825    /// v7.38.14 — would sorting place every duplicate next to its twin?
8826    ///
8827    /// True when the projected expressions and the ORDER BY expressions are the
8828    /// same SET. Then the sort key determines the projected row, so equal rows
8829    /// are adjacent afterwards and an adjacent comparison de-duplicates exactly
8830    /// as a hash would -- and, because both sort paths are stable, the survivor
8831    /// is the first-seen row, which is the one the hash keeps too.
8832    ///
8833    /// A wildcard's expansion is not known here, so it is not a set this can
8834    /// compare; an ordinal ORDER BY names a select-list position rather than a
8835    /// value and is left alone.
8836    fn distinct_is_adjacent_after_sort(stmt: &SelectStatement) -> bool {
8837        if stmt.order_by.is_empty() || !stmt.distinct_on.is_empty() {
8838            return false;
8839        }
8840        let mut projected: alloc::vec::Vec<&Expr> =
8841            alloc::vec::Vec::with_capacity(stmt.items.len());
8842        for item in &stmt.items {
8843            match item {
8844                SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return false,
8845                SelectItem::Expr { expr, .. } => projected.push(expr),
8846            }
8847        }
8848        if projected.is_empty() {
8849            return false;
8850        }
8851        let keys: alloc::vec::Vec<&Expr> = stmt.order_by.iter().map(|o| &o.expr).collect();
8852        if keys
8853            .iter()
8854            .any(|k| matches!(k, Expr::Literal(spg_sql::ast::Literal::Integer(_))))
8855        {
8856            return false;
8857        }
8858        projected.iter().all(|p| keys.contains(p)) && keys.iter().all(|k| projected.contains(k))
8859    }
8860
8861    fn try_spill_sorted_stream<F>(
8862        &self,
8863        stmt: &SelectStatement,
8864        from: &FromClause,
8865        cancel: CancelToken<'_>,
8866        emit: &mut F,
8867    ) -> Result<Option<usize>, EngineError>
8868    where
8869        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
8870    {
8871        // The shapes `try_spill_sorted_scan` declines, plus the ones the
8872        // streaming executor does not carry (a LIMIT is already bounded
8873        // by a partial sort; the rest need the answer addressable).
8874        if !self.can_spill()
8875            || stmt.order_by.is_empty()
8876            || stmt.distinct
8877            || stmt.limit_with_ties
8878            || stmt.limit.is_some()
8879            || stmt.offset.is_some()
8880            || stmt.having.is_some()
8881            || stmt.group_by.is_some()
8882            || !stmt.unions.is_empty()
8883            || !from.joins.is_empty()
8884            || from.primary.lateral_subquery.is_some()
8885            || from.primary.unnest_expr.is_some()
8886            || from.primary.as_of_segment.is_some()
8887            || from.primary.generate_series_args.is_some()
8888            || select_has_window(stmt)
8889            || aggregate::uses_aggregate(stmt)
8890        {
8891            return Ok(None);
8892        }
8893        if stmt
8894            .items
8895            .iter()
8896            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
8897        {
8898            return Ok(None);
8899        }
8900        // Everything `exec_bare_select_cancel` does before it scans runs
8901        // BELOW this path, so a statement claimed here skips it. Three of
8902        // those were missed on the way in and each was caught by a
8903        // different gate — the ORDER BY rules by an e2e (`SELECT a FROM t
8904        // ORDER BY 2` sorted happily instead of raising 42P10), the
8905        // cancellation check by another, the partition fan-out by the
8906        // differential corpus. What is reconciled, item by item: with-ties
8907        // needs ORDER BY (gated above), USING/NATURAL and RLS join
8908        // rewrites (joins gated above), the single-table RLS predicate
8909        // (the dispatcher declines a policy-subject table before this is
8910        // reached), the meta-view dispatch (those names are not in the
8911        // catalog, so the lookup below declines). These three are calls,
8912        // so the message and SQLSTATE are the ones the fall-back gives —
8913        // `select_has_window` above reads the select list and ORDER BY but
8914        // not WHERE, which is the case the third one covers.
8915        crate::orderby::check_order_by_legality(stmt)?;
8916        crate::orderby::check_order_by_positions(stmt)?;
8917        crate::window::reject_window_in_row_clauses(stmt)?;
8918        // A parent's rows are its children's. These walks scan the named
8919        // relation alone, so a partitioned or inherited parent comes back
8920        // short — and silently: the corpus caught `SELECT id FROM pr
8921        // ORDER BY id` and `SELECT k FROM pl ORDER BY k` returning the
8922        // parent's own rows instead of the partitions'. `ONLY` is exactly
8923        // the case that does not fan out, so it stays, which is the test
8924        // the FROM-clause fan-out itself makes.
8925        if !from.primary.only
8926            && crate::partition::has_children(self.active_catalog(), &from.primary.name)
8927        {
8928            return Ok(None);
8929        }
8930        let Some(table) = self.active_catalog().get(&from.primary.name) else {
8931            return Ok(None);
8932        };
8933        // Cold-tier rows live outside `rows()`; this walk would drop
8934        // them silently, the same reason round 831's walk declines.
8935        if table.has_cold_rows_fast() {
8936            return Ok(None);
8937        }
8938
8939        let alias = from
8940            .primary
8941            .alias
8942            .as_deref()
8943            .unwrap_or(from.primary.name.as_str());
8944        let cols = table.schema().columns.clone();
8945        let sess = self.dml_session();
8946        let ctx = EvalContext::new(&cols, Some(alias))
8947            .with_catalog(self.active_catalog())
8948            .with_session(&sess);
8949        let projection = build_projection(
8950            &stmt.items,
8951            &cols,
8952            alias,
8953            self.backslash_escapes,
8954            Some(self.active_catalog()),
8955        )?;
8956        let order_by = stmt.order_by.clone();
8957        // The same one-shot resolution the general path does (round
8958        // 582): each ORDER BY column is bound once, not once per row.
8959        let order_bound = crate::orderby::order_by_bound_positions(&order_by, &cols, Some(alias));
8960        let descs: Vec<bool> = order_by.iter().map(|o| o.desc).collect();
8961        // Resolved BEFORE the scan, because it now decides what the sort
8962        // STORES and not just what it decodes (round 995).
8963        let needed = Self::sort_record_columns_needed(&stmt.items, &order_bound, cols.len(), &ctx);
8964
8965        let mut sorter = crate::extsort::ExternalSorter::new(
8966            self.temp_run_factory,
8967            self.session_work_mem_bytes(),
8968            cols.clone(),
8969            &descs,
8970        )
8971        .with_stats(&self.spill_stats)
8972        .with_pruned(&needed);
8973        let snapshot = self.current_snapshot();
8974        // One key buffer for the whole scan: `push` drains it and leaves
8975        // the capacity behind.
8976        let mut keys: Vec<OrderKey> = Vec::new();
8977        // r1024 — compile the predicate once for the scan.
8978        //
8979        // These two sorted-spill scans are the paths a single-table SELECT
8980        // with an ORDER BY takes, and they were the last row-returning ones
8981        // still walking the expression tree per row. r1023 did the
8982        // no-ORDER-BY sibling; the sweep's two remaining losing cells are
8983        // exactly this shape.
8984        //
8985        // Found from the profile's CALL TREE rather than its leaves. The
8986        // leaves say what is expensive — `eval_expr` 320, `apply_binary`
8987        // 261, `mod_op` 178 — and two attempts at reasoning out which
8988        // function asked for it were both wrong. The tree names the caller
8989        // chain, and it named this one.
8990        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
8991            .where_
8992            .as_ref()
8993            .filter(|w| crate::eval::fully_compilable(w))
8994            .map(|w| crate::eval::compile_expr(w, &ctx));
8995        let mut eval_stack: Vec<Value<'static>> = Vec::new();
8996        for (i, row) in table.scan_visible_from(0, &snapshot) {
8997            if i.is_multiple_of(256) {
8998                cancel.check()?;
8999            }
9000            if let Some(c) = &compiled_where {
9001                if !crate::eval::compiled::eval_compiled_pred(
9002                    c,
9003                    row,
9004                    &ctx,
9005                    &mut eval_stack,
9006                    ctx.mysql_dialect,
9007                )? {
9008                    continue;
9009                }
9010            } else if let Some(w) = &stmt.where_ {
9011                let cond = crate::eval::eval_expr(w, row, &ctx).map_err(EngineError::Eval)?;
9012                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9013                    continue;
9014                }
9015            }
9016            keys.clear();
9017            // `&[]`: this sorter compares with `cmp_multi_key_in(.., &[])`
9018            // (extsort.rs:213/543/1221), so the key must stay folded or the
9019            // two would disagree. See `build_order_keys_bound`.
9020            crate::orderby::build_order_keys_bound(
9021                &order_by,
9022                &order_bound,
9023                &[],
9024                row,
9025                &ctx,
9026                &mut keys,
9027            )?;
9028            sorter.push(&mut keys, row)?;
9029        }
9030
9031        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9032        emit(crate::StreamItem::Header(&columns))?;
9033
9034        let key_ctx = &ctx;
9035        let mut emitted_since_check = 0usize;
9036        let n = sorter.finish_each(
9037            |src, buf| {
9038                crate::orderby::build_order_keys_bound(
9039                    &order_by,
9040                    &order_bound,
9041                    &[],
9042                    src,
9043                    key_ctx,
9044                    buf,
9045                )
9046            },
9047            |src, values| {
9048                for p in &projection {
9049                    values.push(
9050                        crate::eval::eval_expr(&p.expr, src, key_ctx).map_err(EngineError::Eval)?,
9051                    );
9052                }
9053                Ok(())
9054            },
9055            |cells| {
9056                // The merge is the long half of a big sort, and the scan's
9057                // check above stops running once it ends: a cancelled
9058                // `SELECT pad FROM big ORDER BY id` delivered all 120k rows
9059                // anyway. Same stride as the scan.
9060                emitted_since_check += 1;
9061                if emitted_since_check >= 256 {
9062                    emitted_since_check = 0;
9063                    cancel.check()?;
9064                }
9065                emit(crate::StreamItem::Row(crate::RowCells::Values(cells)))
9066            },
9067        )?;
9068        Ok(Some(n))
9069    }
9070
9071    /// One row of the single-table streaming walk: the WHERE test, the
9072    /// projection, the emit. Returns whether a row was emitted.
9073    ///
9074    /// v7.39 (round 970) — factored out because the walk now has two ways
9075    /// to reach a row, the sequential scan and an index seek's candidate
9076    /// positions, and both must do IDENTICALLY this. A copy in each is how
9077    /// two paths for one job drift; this file already carries the cost of
9078    /// that lesson twice (rounds 823 and 961, both resolvers).
9079    ///
9080    /// `#[inline]` so the scan loop keeps the shape round 957 measured it
9081    /// in — a shared hot path pays for a new abstraction whether or not it
9082    /// uses it, and this one is on the scan.
9083    #[inline]
9084    #[allow(clippy::too_many_arguments)]
9085    fn stream_filter_project(
9086        row: &spg_storage::Row<'static>,
9087        where_: Option<&Expr>,
9088        // r1023 — the same WHERE, compiled once by the caller. `None` means
9089        // the expression did not qualify and `where_` is evaluated as before.
9090        compiled_where: Option<&crate::eval::CompiledExpr>,
9091        eval_stack: &mut Vec<Value<'static>>,
9092        projection: &[ProjectedItem],
9093        bound_pos: &[Option<usize>],
9094        ctx: &crate::eval::EvalContext<'_>,
9095        values: &mut Vec<Value<'static>>,
9096    ) -> Result<bool, EngineError> {
9097        // r1023 — this scan ran its predicate through the TREE INTERPRETER,
9098        // once per row, and it was the only row-returning path that did.
9099        // The aggregate path, `table_access`, and the PK walker all compile
9100        // theirs. Profiled: on `SELECT pad FROM d WHERE id % 3 = 0` the
9101        // server's live samples were `eval_expr` 99, `apply_binary` 81,
9102        // `mod_op` 29 — the interpreter, not delivery.
9103        //
9104        // The arithmetic accounted for it exactly. Over the wire, the same
9105        // filter costs 6.375 ms returning rows and 0.679 ms counting them;
9106        // the 5.70 ms difference over 50,000 scanned rows is 114 ns each,
9107        // which is what an interpreted predicate costs against the compiled
9108        // lane's 11.7. It was named "delivery after a filter" before this
9109        // profile, and it was never delivery.
9110        if let Some(c) = compiled_where {
9111            if !crate::eval::compiled::eval_compiled_pred(
9112                c,
9113                row,
9114                ctx,
9115                eval_stack,
9116                ctx.mysql_dialect,
9117            )? {
9118                return Ok(false);
9119            }
9120        } else if let Some(w) = where_ {
9121            let cond = crate::eval::eval_expr(w, row, ctx).map_err(EngineError::Eval)?;
9122            if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
9123                return Ok(false);
9124            }
9125        }
9126        values.clear();
9127        for (p, bound) in projection.iter().zip(bound_pos) {
9128            values.push(match bound {
9129                Some(pos) => crate::eval::column_at(*pos, row, ctx).map_err(EngineError::Eval)?,
9130                None => crate::eval::eval_expr(&p.expr, row, ctx).map_err(EngineError::Eval)?,
9131            });
9132        }
9133        Ok(true)
9134    }
9135
9136    /// The same filter and projection, then emit. Split from
9137    /// [`Self::stream_filter_project`] so a path that has to BUFFER rows
9138    /// before it can emit them — a sort — runs the identical predicate and
9139    /// projection rather than a second copy of them.
9140    #[allow(clippy::too_many_arguments)]
9141    fn stream_project_row<F>(
9142        row: &spg_storage::Row<'static>,
9143        where_: Option<&Expr>,
9144        compiled_where: Option<&crate::eval::CompiledExpr>,
9145        eval_stack: &mut Vec<Value<'static>>,
9146        projection: &[ProjectedItem],
9147        bound_pos: &[Option<usize>],
9148        ctx: &crate::eval::EvalContext<'_>,
9149        values: &mut Vec<Value<'static>>,
9150        emit: &mut F,
9151    ) -> Result<bool, EngineError>
9152    where
9153        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9154    {
9155        if !Self::stream_filter_project(
9156            row,
9157            where_,
9158            compiled_where,
9159            eval_stack,
9160            projection,
9161            bound_pos,
9162            ctx,
9163            values,
9164        )? {
9165            return Ok(false);
9166        }
9167        emit(crate::StreamItem::Row(crate::RowCells::Values(values)))?;
9168        Ok(true)
9169    }
9170
9171    fn try_stream_single_table<F>(
9172        &self,
9173        stmt: &SelectStatement,
9174        from: &FromClause,
9175        cancel: CancelToken<'_>,
9176        emit: &mut F,
9177    ) -> Result<Option<usize>, EngineError>
9178    where
9179        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9180    {
9181        let Some(table) = self.active_catalog().get(&from.primary.name) else {
9182            return Ok(None);
9183        };
9184        // Cold-tier rows live outside `rows()`; the materialising fallback
9185        // covers both tiers and this walk would silently drop them.
9186        if table.has_cold_rows_fast() {
9187            return Ok(None);
9188        }
9189        let alias = from
9190            .primary
9191            .alias
9192            .as_deref()
9193            .unwrap_or(from.primary.name.as_str());
9194        let cols = table.schema().columns.clone();
9195        let sess = self.dml_session();
9196        let ctx = EvalContext::new(&cols, Some(alias))
9197            .with_catalog(self.active_catalog())
9198            .with_session(&sess);
9199        let projection = build_projection(
9200            &stmt.items,
9201            &cols,
9202            alias,
9203            self.backslash_escapes,
9204            Some(self.active_catalog()),
9205        )?;
9206
9207        let columns: Vec<ColumnSchema> = projection.iter().map(|p| p.to_column_schema()).collect();
9208        emit(crate::StreamItem::Header(&columns))?;
9209
9210        // v7.37 (round 957) — resolve each bare-column projection ONCE
9211        // instead of once per row. `find_column_pos`-style resolution is a
9212        // linear walk of the schema comparing column-name strings, and the
9213        // row loop below ran it for every cell of every row: measured at
9214        // 400k rows, binding it out of the loop took `SELECT pad` from
9215        // 16.5-17.5 ms to 10.9-11.7 ms (-41%, two windows, round 954).
9216        //
9217        // ORDER BY has bound its keys this way since round 582
9218        // (`order_by_bound_positions`); the projection never did.
9219        //
9220        // `locate_column` is the same resolution `resolve_column` performs,
9221        // returning the site instead of the value, so the two cannot drift
9222        // apart the way a second hand-written resolver would. Anything it
9223        // declines — an expression, a whole-row reference, a name that does
9224        // not resolve — binds to `None` and takes the general path below,
9225        // errors included, so an empty table still reports nothing rather
9226        // than raising at bind time.
9227        let bound_pos: Vec<Option<usize>> = projection
9228            .iter()
9229            .map(|p| match &p.expr {
9230                Expr::Column(c) => match crate::eval::locate_column(c, &ctx) {
9231                    Ok(Some(pos)) => Some(pos),
9232                    _ => None,
9233                },
9234                _ => None,
9235            })
9236            .collect();
9237
9238        // One snapshot for the whole scan, as the materialising path takes.
9239        let snapshot = self.current_snapshot();
9240
9241        // v7.39 (round 970) — ask the indices BEFORE walking the table.
9242        //
9243        // This walk had no index step at all, and it is preferred over the
9244        // materialising path, which does have one (`pick_indexed_rows` ->
9245        // `try_index_seek`). So a primary-key point lookup — the commonest
9246        // statement there is — read every row: measured on 500k rows,
9247        // `SELECT * FROM big WHERE id = 250000` took 14.947 ms against
9248        // PG18.4's 0.172 ms, and the cost tracked the TABLE (1k 0.315 ms,
9249        // 10k 1.660, 100k 3.518), which is not what O(log n) looks like.
9250        //
9251        // The control that named it: `... OFFSET 0` — semantically the same
9252        // query — answered in 0.159 ms, because OFFSET is one of the shape
9253        // gates that declines this walk and sends the statement to the path
9254        // that seeks. `LIMIT 1` and `GROUP BY` did the same. The three have
9255        // no semantics in common; what they share is making this function
9256        // stand down.
9257        //
9258        // The seek only NARROWS: every candidate still goes through the
9259        // full WHERE below, exactly as the mutation paths use it, so a
9260        // partial index match cannot change an answer. Positions come back
9261        // already visibility-filtered and already capped at a quarter of the
9262        // table (round 490), so a seek can never cost more than the scan it
9263        // replaces, and `None` means "walk the table" as before.
9264        //
9265        // Sorted because the scan would have produced table order and the
9266        // index produces key order. Without an ORDER BY neither is promised,
9267        // but a walk that silently reorders its answer when an index happens
9268        // to exist is a difference nobody asked for.
9269        let seek_positions: Option<Vec<usize>> = stmt.where_.as_ref().and_then(|w| {
9270            crate::index_access::try_index_seek_positions(
9271                w,
9272                &cols,
9273                table,
9274                alias,
9275                &snapshot,
9276                self.backslash_escapes,
9277            )
9278        });
9279
9280        let mut values: Vec<Value<'static>> = Vec::with_capacity(projection.len());
9281        // r1023 — compile the predicate once for the whole scan. Same gate
9282        // every other path uses: `fully_compilable` or keep the interpreter,
9283        // so a shape the VM cannot take answers exactly as it did before.
9284        let compiled_where: Option<crate::eval::CompiledExpr> = stmt
9285            .where_
9286            .as_ref()
9287            .filter(|w| crate::eval::fully_compilable(w))
9288            .map(|w| crate::eval::compile_expr(w, &ctx));
9289        let mut eval_stack: Vec<Value<'static>> = Vec::new();
9290        let mut count: usize = 0;
9291        match seek_positions {
9292            Some(mut positions) => {
9293                positions.sort_unstable();
9294                for (n, pos) in positions.into_iter().enumerate() {
9295                    if n.is_multiple_of(256) {
9296                        cancel.check()?;
9297                    }
9298                    let Some(row) = table.rows().get(pos) else {
9299                        continue;
9300                    };
9301                    if Self::stream_project_row(
9302                        row,
9303                        stmt.where_.as_ref(),
9304                        compiled_where.as_ref(),
9305                        &mut eval_stack,
9306                        &projection,
9307                        &bound_pos,
9308                        &ctx,
9309                        &mut values,
9310                        emit,
9311                    )? {
9312                        count += 1;
9313                    }
9314                }
9315            }
9316            None => {
9317                // v7.38.11 — the streaming scan is the path a client
9318                // reaches over the wire, so it is the one that has to
9319                // ask the BRIN summary which slots can be skipped. The
9320                // predicate still runs on every row that survives.
9321                let slots = stmt
9322                    .where_
9323                    .as_ref()
9324                    .and_then(|w| crate::brin::candidate_slots(w, table))
9325                    .unwrap_or_else(|| alloc::vec![0..table.row_count()]);
9326                for (i, row) in table.scan_visible_slots(slots, &snapshot) {
9327                    if i.is_multiple_of(256) {
9328                        cancel.check()?;
9329                    }
9330                    if Self::stream_project_row(
9331                        row,
9332                        stmt.where_.as_ref(),
9333                        compiled_where.as_ref(),
9334                        &mut eval_stack,
9335                        &projection,
9336                        &bound_pos,
9337                        &ctx,
9338                        &mut values,
9339                        emit,
9340                    )? {
9341                        count += 1;
9342                    }
9343                }
9344            }
9345        }
9346        Ok(Some(count))
9347    }
9348
9349    pub(crate) fn try_exec_joined_streaming<F>(
9350        &self,
9351        stmt: &SelectStatement,
9352        cancel: CancelToken<'_>,
9353        emit: &mut F,
9354    ) -> Result<Option<usize>, EngineError>
9355    where
9356        F: FnMut(crate::StreamItem<'_>) -> Result<(), EngineError>,
9357    {
9358        // Shape gates — keep the streamable surface narrow on
9359        // purpose. The fall-back path still handles everything else.
9360        let Some(from) = &stmt.from else {
9361            return Ok(None);
9362        };
9363        // v7.37 (round 830) — decline anything a row-security policy binds
9364        // for this session. Policies are injected in
9365        // `exec_bare_select_cancel`, below this path, so a statement claimed
9366        // here would read the table unfiltered: measured, `SELECT val FROM
9367        // sec` returned all three rows to a session whose policy allows two,
9368        // while `SELECT upper(val) FROM sec` — declined by the shape gates
9369        // and so materialised — returned the correct two.
9370        //
9371        // Declining sends it to the path that enforces. Teaching this one to
9372        // inject the predicate itself would keep the streaming benefit for
9373        // RLS tables and is the better end state; it is not what a
9374        // correctness fix should carry, and the fall-back is exactly as
9375        // correct, only slower.
9376        if self.select_reads_policy_subject_table(stmt) {
9377            return Ok(None);
9378        }
9379        // r1058 — a WITH list this path never materialises: the CTE
9380        // name would be resolved as a physical relation and error
9381        // ("relation \"big\" does not exist" over the extended
9382        // protocol, caught by the perm-runner's wire legs). The
9383        // materialising fallback owns CTE execution.
9384        if !stmt.ctes.is_empty() {
9385            return Ok(None);
9386        }
9387        // r1058 — rewritten system catalogs (`__spg_pg_stat_user_
9388        // tables` and kin) exist only as synth arms on the
9389        // materialising path; claiming one here errored "relation
9390        // does not exist" over the extended protocol for a query the
9391        // simple protocol answered. Prefix test only — a genuinely
9392        // missing relation must keep erroring in-path.
9393        if from.primary.name.starts_with("__spg_")
9394            || from
9395                .joins
9396                .iter()
9397                .any(|j| j.table.name.starts_with("__spg_"))
9398        {
9399            return Ok(None);
9400        }
9401        // r1058 — decline partitioned / inheritance parents, same
9402        // shape of bug as the RLS decline above: this path scans the
9403        // named table's own (empty) heap, so `SELECT id, region FROM
9404        // cust` on a partition parent streamed ZERO rows over the wire
9405        // while COUNT(*) — an aggregate, materialised below — said 3.
9406        // Caught by the perm-runner's server permutations; the
9407        // materialising fallback expands children correctly.
9408        if crate::partition::has_children(self.active_catalog(), &from.primary.name)
9409            || from
9410                .joins
9411                .iter()
9412                .any(|j| crate::partition::has_children(self.active_catalog(), &j.table.name))
9413        {
9414            return Ok(None);
9415        }
9416        // v7.39 (round 790) — single-table SELECTs stream too. This
9417        // gate said "joins only" because the path was written for
9418        // mailrs's joined PROJ shape; a plain `SELECT <cols> FROM t`
9419        // fell to the materialising fallback, which builds the whole
9420        // `Vec<Row<'static>>` and only then iterates it. Measured on
9421        // 300k rows: 181 MB single-table vs 70 MB for the SAME rows
9422        // reached through a one-row JOIN — 2.6x, purely for lacking a
9423        // join. The deferred-join structure handles one source as the
9424        // degenerate stride-1 case, so the walk below is unchanged.
9425        let _single_table = from.joins.is_empty();
9426        // An ORDER BY that the bounded sort can serve streams; everything
9427        // else still falls to the materialising fallback below.
9428        // r1025 — an ordering the index already holds needs no sort at all.
9429        // Tried before the spill sort, which is the path it replaces.
9430        if !stmt.order_by.is_empty()
9431            && from.joins.is_empty()
9432            && let Some(n) = self.try_index_order_stream(stmt, from, cancel, emit)?
9433        {
9434            return Ok(Some(n));
9435        }
9436        if !stmt.order_by.is_empty()
9437            && from.joins.is_empty()
9438            && let Some(n) = self.try_spill_sorted_stream(stmt, from, cancel, emit)?
9439        {
9440            return Ok(Some(n));
9441        }
9442        // r1031 — integer keys carried inline instead of an `OrderKey`
9443        // vector per row. Tried AFTER the spill sort on purpose: this lane
9444        // buffers the whole answer, so anything the spill path would take
9445        // must keep taking it rather than be turned back into an in-memory
9446        // sort that answers with a budget error.
9447        if !stmt.order_by.is_empty()
9448            && from.joins.is_empty()
9449            && let Some(n) = self.try_int_key_sorted_stream(stmt, from, cancel, emit)?
9450        {
9451            return Ok(Some(n));
9452        }
9453        if !stmt.order_by.is_empty()
9454            || stmt.limit.is_some()
9455            || stmt.offset.is_some()
9456            || stmt.having.is_some()
9457            || stmt.group_by.is_some()
9458            || stmt.distinct
9459            || !stmt.unions.is_empty()
9460            || stmt.limit_with_ties
9461        {
9462            return Ok(None);
9463        }
9464        if aggregate::uses_aggregate(stmt) {
9465            return Ok(None);
9466        }
9467        // No window / SRF on the streaming path.
9468        if select_has_window(stmt) {
9469            return Ok(None);
9470        }
9471        if stmt
9472            .items
9473            .iter()
9474            .any(|i| matches!(i, SelectItem::Expr { expr, .. } if is_top_level_unnest(expr)))
9475        {
9476            return Ok(None);
9477        }
9478        // v7.37 (round 831) — a joinless FROM over a plain stored table
9479        // never needs the deferred structure, and building one costs the
9480        // whole table. `materialise_table_ref_filtered` clones every row
9481        // into a `Vec<Row<'static>>` before anything is filtered or
9482        // projected, so peak cost tracks the TABLE, not the result:
9483        // measured over 300k rows of 200 bytes, `SELECT id FROM big` and
9484        // `SELECT pad FROM big` both cost +107 MB over baseline, the narrow
9485        // projection saving nothing, while an arithmetic projection — which
9486        // the shape gates decline, so it materialises through the ordinary
9487        // executor — cost +21 MB.
9488        //
9489        // Scanning in batches and releasing each one is what `cursor_fill`
9490        // already does for a lazy cursor, and it is the same walk: resume
9491        // from a slot, take visible rows, evaluate, hand them over, drop
9492        // them. Round 800's finding stands and is why this reads rows OUT
9493        // rather than seeding the join by index — touching the stored
9494        // `PersistentVec` in place makes the whole table resident, which is
9495        // worse than the copy. Each batch is copied, then freed.
9496        if from.joins.is_empty()
9497            && from.primary.unnest_expr.is_none()
9498            && from.primary.lateral_subquery.is_none()
9499            && from.primary.as_of_segment.is_none()
9500            && from.primary.generate_series_args.is_none()
9501            && let Some(n) = self.try_stream_single_table(stmt, from, cancel, emit)?
9502        {
9503            return Ok(Some(n));
9504        }
9505        // Build the deferred join under the regular byte budget.
9506        let mut budget = ByteBudget::new(self.max_query_bytes);
9507        let deferred = {
9508            let mut needed = alloc::collections::BTreeSet::new();
9509            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
9510            self.build_joined_filtered_rows(
9511                from,
9512                stmt.where_.as_ref(),
9513                cancel,
9514                if prunable { Some(&needed) } else { None },
9515                &mut budget,
9516            )?
9517        };
9518        let combined_schema = &deferred.combined_schema;
9519        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
9520        // `::regclass` / enum cast in a joined projection or HAVING needs it.
9521        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
9522        // the same predicate the unjoined shape carries.
9523        let joined_sess = self.dml_session();
9524        // v7.38.18 — and the DIALECT. This context carried the catalog and
9525        // the session and not the one field that decides how text
9526        // compares, so a joined row was evaluated in PostgreSQL
9527        // semantics inside a MySQL session.
9528        //
9529        // It showed up only where the two sides had DIFFERENT text types:
9530        // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
9531        // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
9532        // were fine and the same comparison inside one table was fine.
9533        // Same-type pairs agree byte-for-byte after an ASCII lowercase,
9534        // so the wrong semantics were invisible until a CHAR's padding
9535        // had to be stripped and PostgreSQL's arm does not strip it.
9536        //
9537        // `with_engine` is what sets it; the next line already reaches
9538        // for `self.backslash_escapes`, so the dialect was in hand.
9539        let ctx = EvalContext::new(combined_schema, None)
9540            .with_catalog(self.active_catalog())
9541            .with_engine(self)
9542            .with_session(&joined_sess);
9543        let projection = build_projection(
9544            &stmt.items,
9545            combined_schema,
9546            "",
9547            self.backslash_escapes,
9548            Some(self.active_catalog()),
9549        )?;
9550        // Every projection item must be a bound qualified column —
9551        // anything that needs `eval_expr_with_correlated` keeps the
9552        // materialising path.
9553        let bound_pos = |e: &Expr| -> Option<usize> {
9554            match e {
9555                // v7.39 (round 822) — an UNQUALIFIED column resolves here
9556                // too. The `qualifier.is_some()` guard this replaces meant
9557                // `SELECT pad FROM big` — the commonest projection there is
9558                // — never reached the streaming walk: it fell out at this
9559                // gate and re-ran on the materialising path, after the
9560                // deferred join structure had already been built and paid
9561                // for. Measured (round 821, statement_timeout=120 over 400k
9562                // rows): `big.pad` and `b.pad` streamed and cancelled at
9563                // ~65k rows in 0.14 s, while bare `pad` ran to completion in
9564                // 0.80 s with the timeout never consulted. `find_column_pos`
9565                // has always handled the unqualified case (it falls through
9566                // to a by-name match), so the guard narrowed the gate for no
9567                // reason it recorded.
9568                Expr::Column(c) => eval::find_column_pos(c, &ctx),
9569                _ => None,
9570            }
9571        };
9572        let proj_decomposed: Vec<(usize, usize)> = {
9573            let mut out = Vec::with_capacity(projection.len());
9574            for p in &projection {
9575                let Some(abs) = bound_pos(&p.expr) else {
9576                    return Ok(None);
9577                };
9578                let Some(k) = deferred
9579                    .offsets
9580                    .partition_point(|&o| o <= abs)
9581                    .checked_sub(1)
9582                else {
9583                    return Ok(None);
9584                };
9585                out.push((k, abs - deferred.offsets[k]));
9586            }
9587            out
9588        };
9589        // Emit columns once.
9590        let columns: Vec<ColumnSchema> = projection
9591            .iter()
9592            // v7.39 (read01 round 54) — keep the column's enum identity through
9593            // the projection (it lives outside the DataType lattice), or a
9594            // derived table / UNION / windowed result forgets it and any outer
9595            // `ORDER BY <enum col>` silently sorts by the label's TEXT.
9596            .map(|p| p.to_column_schema())
9597            .collect();
9598        emit(crate::StreamItem::Header(&columns))?;
9599        let sources_ref = &deferred.sources;
9600        let stride = deferred.stride;
9601        let survivors_ref = &deferred.survivors;
9602        let n_surv = if stride == 0 {
9603            0
9604        } else {
9605            survivors_ref.len() / stride
9606        };
9607        // Reused per-row cell-ref scratch — pushes are zero-alloc
9608        // after the first row.
9609        let null_value = Value::Null;
9610        let mut cell_refs: Vec<&Value> = Vec::with_capacity(projection.len());
9611        let mut count: usize = 0;
9612        for surv_i in 0..n_surv {
9613            if surv_i.is_multiple_of(256) {
9614                cancel.check()?;
9615            }
9616            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
9617            cell_refs.clear();
9618            for &(k, col_in_src) in &proj_decomposed {
9619                let ri = tuple[k];
9620                let v: &Value = if ri == usize::MAX {
9621                    &null_value
9622                } else {
9623                    sources_ref[k]
9624                        .get(ri)
9625                        .and_then(|r| r.values.get(col_in_src))
9626                        .unwrap_or(&null_value)
9627                };
9628                cell_refs.push(v);
9629            }
9630            emit(crate::StreamItem::Row(crate::RowCells::Refs(&cell_refs)))?;
9631            count += 1;
9632        }
9633        Ok(Some(count))
9634    }
9635
9636    fn exec_joined_select(
9637        &self,
9638        stmt: &SelectStatement,
9639        from: &FromClause,
9640        cancel: CancelToken<'_>,
9641    ) -> Result<QueryResult, EngineError> {
9642        // v7.37.x (docker-fair NOTEX attack) — short-circuit COUNT(*)
9643        // over a LEFT ANTI JOIN. The v7.37.27 NOT EXISTS pullup
9644        // rewrites `SELECT COUNT(*) FROM A WHERE NOT EXISTS (SELECT 1
9645        // FROM B WHERE B.k = A.k)` into
9646        //   SELECT COUNT(*) FROM A LEFT JOIN B ON B.k = A.k
9647        //   WHERE B.k IS NULL
9648        // The general join executor builds a hash, probes every outer
9649        // tuple, materialises (left_padded_with_null) for every miss,
9650        // then runs the aggregate over the result set. For COUNT(*) we
9651        // only need the count — skip the tuple materialisation. Build
9652        // a HashSet of B's unique join values, scan A's PK index, and
9653        // increment the counter on each miss. PG's Merge Anti-Join
9654        // does roughly this; ours becomes a simple HashSet probe.
9655        if let Some(out) = self.try_count_star_left_anti_join_fast(stmt, from)? {
9656            return Ok(out);
9657        }
9658        // v7.34.5 (mailrs prod #5) — walker-driven join + early stop.
9659        // When ORDER BY is on an indexed primary column, walking the
9660        // btree in the requested direction lets the streamer break
9661        // after `LIMIT + OFFSET` survivors without ever materialising
9662        // the rest of the join — the 80 ms `mailrs_prod_not_exists`
9663        // plateau is exactly this shape.
9664        if let Some(out) = self.try_streamed_inner_join_walk_topn(stmt, from, cancel)? {
9665            return Ok(out);
9666        }
9667        // v7.30.3 (mailrs round-26) — the bounded single-join path
9668        // first; peak memory scales with LIMIT instead of the table.
9669        if let Some(out) = self.try_streamed_inner_join_topn(stmt, from, cancel)? {
9670            return Ok(out);
9671        }
9672        // v7.17.0 Phase 3.P0-43 + P0-41 — delegate the join +
9673        // WHERE materialisation to the shared helper so the LATERAL
9674        // / UNNEST / regular-catalog paths route through one place.
9675        // (`build_joined_filtered_rows` carries LATERAL support as
9676        // of Phase 3.P0-41.) Downstream we still handle aggregate /
9677        // projection / ORDER BY / DISTINCT / LIMIT inline because
9678        // those depend on the SelectStatement's items list.
9679        let mut budget = ByteBudget::new(self.max_query_bytes);
9680        let deferred = {
9681            let mut needed = alloc::collections::BTreeSet::new();
9682            let prunable = collect_qualified_refs(stmt, &mut needed).is_some();
9683            self.build_joined_filtered_rows(
9684                from,
9685                stmt.where_.as_ref(),
9686                cancel,
9687                if prunable { Some(&needed) } else { None },
9688                &mut budget,
9689            )?
9690        };
9691        let combined_schema = &deferred.combined_schema;
9692        // v7.39 (read01 round 53) — carry the catalog (see join.rs): a
9693        // `::regclass` / enum cast in a joined projection or HAVING needs it.
9694        // v7.39 (round 525) — and the session: a joined SELECT's WHERE is
9695        // the same predicate the unjoined shape carries.
9696        let joined_sess = self.dml_session();
9697        // v7.38.18 — and the DIALECT. This context carried the catalog and
9698        // the session and not the one field that decides how text
9699        // compares, so a joined row was evaluated in PostgreSQL
9700        // semantics inside a MySQL session.
9701        //
9702        // It showed up only where the two sides had DIFFERENT text types:
9703        // `a.c = b.s` with `c CHAR(8)` and `s TEXT` answered false, and a
9704        // join on it returned no rows, while `a.c = b.c` and `a.s = b.s`
9705        // were fine and the same comparison inside one table was fine.
9706        // Same-type pairs agree byte-for-byte after an ASCII lowercase,
9707        // so the wrong semantics were invisible until a CHAR's padding
9708        // had to be stripped and PostgreSQL's arm does not strip it.
9709        //
9710        // `with_engine` is what sets it; the next line already reaches
9711        // for `self.backslash_escapes`, so the dialect was in hand.
9712        let ctx = EvalContext::new(combined_schema, None)
9713            .with_catalog(self.active_catalog())
9714            .with_engine(self)
9715            .with_session(&joined_sess);
9716        // Aggregate path: handle GROUP BY / aggregate calls over the
9717        // joined+filtered rows.
9718        if aggregate::uses_aggregate(stmt) {
9719            // v7.32 (P4 borrow channel, increment 2) — borrow each
9720            // surviving join tuple as a RowRef::Tuple; the aggregate
9721            // engine reads source cells by reference (bound fast path =
9722            // zero clone) instead of consuming materialised combined
9723            // Rows. This is where the +211k materialise_tuple_vals
9724            // clones disappear for the join+aggregate shape.
9725            let refs = deferred.row_refs();
9726            // v7.29 — a per-query memo so correlated scalar
9727            // subqueries batch-evaluate once (group map) instead of
9728            // executing per group.
9729            let agg_memo = core::cell::RefCell::new(memoize::MemoizeCache::default());
9730            let agg_correlated = |e: &Expr, r: &Row<'static>, c: &EvalContext<'_>| {
9731                self.eval_expr_with_correlated(e, r, c, cancel, Some(&mut agg_memo.borrow_mut()))
9732                    .map_err(|err| match err {
9733                        EngineError::Eval(ev) => ev,
9734                        other => eval::EvalError::TypeMismatch {
9735                            detail: alloc::format!("{other}"),
9736                        },
9737                    })
9738            };
9739            let agg = aggregate::run(
9740                stmt,
9741                crate::join::AggRows::Refs(&refs),
9742                combined_schema,
9743                None,
9744                Some(&agg_correlated),
9745                self.parallel_runner.0.as_deref(),
9746                Some(self.active_catalog()),
9747                Some(self),
9748            )?;
9749            return self.finish_agg_result(agg, stmt, cancel);
9750        }
9751
9752        let projection = build_projection(
9753            &stmt.items,
9754            combined_schema,
9755            "",
9756            self.backslash_escapes,
9757            Some(self.active_catalog()),
9758        )?;
9759        // v7.39 (round 734) — a set-returning projection over a JOIN.
9760        // This executor's projection loop treats every item as a scalar,
9761        // so `SELECT unnest(ARRAY[a.id, b.g]) FROM a JOIN b …` died with
9762        // "function unnest(integer[]) does not exist" where PG expands
9763        // it. The row-set executor already carries the full SRF pipeline
9764        // (lockstep expansion, ORDER-BY-on-expanded-rows, the round-733
9765        // sharding): materialise the joined survivors and hand over. The
9766        // WHERE is cleared — the join already applied it, and combined
9767        // columns resolve identically in both executors.
9768        if !self.srf_target_idxs(&projection).is_empty() {
9769            let refs = deferred.row_refs();
9770            let rows: Vec<Row<'static>> = refs.iter().map(|r| r.as_row().into_owned()).collect();
9771            let mut s2 = stmt.clone();
9772            s2.where_ = None;
9773            let schema = combined_schema.clone();
9774            return self.exec_select_over_rows(&s2, rows, schema, "", cancel);
9775        }
9776        // v7.33 (P4 borrow channel, increment 3) — project directly off
9777        // the deferred row-index tuples instead of materialising an
9778        // intermediate combined Row per survivor. A bound qualified
9779        // column is read by reference (`RowRef::get` → `tuple_value`) and
9780        // cloned ONCE into the output row; the old `materialise()` (a full
9781        // combined Row plus a source→intermediate clone per referenced
9782        // cell, for every survivor) is gone. A row materialises on demand
9783        // only when a projection or ORDER BY expression needs the eval
9784        // path (subquery / function / arithmetic / unqualified column).
9785        // Same bind-once classification the aggregate input fast path uses
9786        // (`accumulate_groups`), reading the same `tuple_value` mapping the
9787        // differential gate already covers.
9788        let refs = deferred.row_refs();
9789        let bound_pos = |e: &Expr| -> Option<usize> {
9790            match e {
9791                Expr::Column(c) if c.qualifier.is_some() => eval::find_column_pos(c, &ctx),
9792                _ => None,
9793            }
9794        };
9795        let proj_pos: Vec<Option<usize>> = projection.iter().map(|p| bound_pos(&p.expr)).collect();
9796        let all_proj_bound = proj_pos.iter().all(Option::is_some);
9797        // v7.36 (perf — mailrs Phase 1, PROJ SPGS 8.93 → ?) —
9798        // pre-decompose each bound projection position into
9799        // `(source_k, col_in_source)` so the per-row column read
9800        // skips the per-cell `tuple_value` partition_point + slice
9801        // walk. For PROJ_25k (5 cols × 25k rows = 125k tuple_value
9802        // calls) that walk dominated; this version reaches into
9803        // `pipe.sources[k].get(tuple[k])?.values[col]` directly.
9804        let proj_decomposed: Vec<Option<(usize, usize)>> = proj_pos
9805            .iter()
9806            .map(|p| {
9807                p.and_then(|abs| {
9808                    let k = deferred
9809                        .offsets
9810                        .partition_point(|&o| o <= abs)
9811                        .checked_sub(1)?;
9812                    Some((k, abs - deferred.offsets[k]))
9813                })
9814            })
9815            .collect();
9816        // v7.39 (round 962) — which projection items are whole-row
9817        // references, and to which join source. The test is
9818        // `locate_column` declining the name, which is the SAME resolver
9819        // the evaluation path uses, so this cannot drift from it: a real
9820        // column carrying an alias's name resolves to a position and is
9821        // not reported here. The source index comes from the alias
9822        // prefix, the way the combined schema names its columns.
9823        let whole_row_src: Vec<Option<usize>> = projection
9824            .iter()
9825            .map(|p| {
9826                let Expr::Column(c) = &p.expr else {
9827                    return None;
9828                };
9829                if !matches!(eval::locate_column(c, &ctx), Ok(None)) {
9830                    return None;
9831                }
9832                let prefix = alloc::format!("{name}.", name = c.name);
9833                let abs = deferred
9834                    .combined_schema
9835                    .iter()
9836                    .position(|s| s.name.starts_with(&prefix))?;
9837                deferred
9838                    .offsets
9839                    .partition_point(|&o| o <= abs)
9840                    .checked_sub(1)
9841            })
9842            .collect();
9843        // ORDER BY (when present) still evaluates against a materialised
9844        // Row — keep the order-key encoder correct rather than fork it.
9845        let need_eval_row = !all_proj_bound || !stmt.order_by.is_empty();
9846        let mut tagged: Vec<(Vec<OrderKey>, Row<'static>)> = Vec::new();
9847        let mut proj_memo = memoize::MemoizeCache::default();
9848        let sources_ref = &deferred.sources;
9849        let stride = deferred.stride;
9850        let survivors_ref = &deferred.survivors;
9851        let n_surv = survivors_ref.len() / stride.max(1);
9852        // v7.38 (read01 B8) — streaming top-N budget (see the sibling
9853        // single-table path). Bounds this JOIN projection's accumulator
9854        // to O(keep) for `ORDER BY … LIMIT k`.
9855        let topk_stream: Option<(usize, Vec<bool>)> = if !stmt.order_by.is_empty()
9856            && !stmt.distinct
9857            && !stmt.limit_with_ties
9858            && !self.env_cfg().disable_topk
9859        {
9860            stmt.limit_literal().and_then(|l| {
9861                let keep = (l as usize).saturating_add(stmt.offset_literal().unwrap_or(0) as usize);
9862                (keep >= 1).then(|| (keep, stmt.order_by.iter().map(|o| o.desc).collect()))
9863            })
9864        } else {
9865            None
9866        };
9867        // v7.37.16 — streaming DISTINCT seen-set (see scan-path twin).
9868        let mut seen_distinct: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
9869            hashbrown::HashMap::new();
9870        let distinct_hb = hashbrown::DefaultHashBuilder::default();
9871        // v7.38.13 — which output positions must NOT fold. Built once per
9872        // scan from the projection, which carries the source column's
9873        // byte-wise-ness; see `FoldSpec`.
9874        let distinct_mask = fold_mask(&projection);
9875        for surv_i in 0..n_surv {
9876            let tuple = &survivors_ref[surv_i * stride..(surv_i + 1) * stride];
9877            let row = &refs[surv_i];
9878            let materialised: Option<Cow<'_, Row<'static>>> = if need_eval_row {
9879                Some(row.as_row())
9880            } else {
9881                None
9882            };
9883            let mut values = Vec::with_capacity(projection.len());
9884            for (i, p) in projection.iter().enumerate() {
9885                if let Some((k, col_in_src)) = proj_decomposed[i] {
9886                    // v7.36 — direct (source_k, col) lookup, no
9887                    // partition_point. tuple[k] is the row index in
9888                    // sources[k]; LEFT-NULL slots are `usize::MAX`.
9889                    let ri = tuple[k];
9890                    let v: Value<'static> = if ri == usize::MAX {
9891                        Value::Null
9892                    } else {
9893                        sources_ref[k]
9894                            .get(ri)
9895                            .and_then(|r| r.values.get(col_in_src))
9896                            .cloned()
9897                            .map(Value::into_owned)
9898                            .unwrap_or(Value::Null)
9899                    };
9900                    values.push(v);
9901                } else if let Some(pos) = proj_pos[i] {
9902                    // Bound but couldn't decompose (shouldn't normally
9903                    // happen — keep as a safe path).
9904                    values.push(
9905                        row.get(pos)
9906                            .cloned()
9907                            .map(Value::into_owned)
9908                            .unwrap_or(Value::Null),
9909                    );
9910                } else if let Some(k) = whole_row_src[i]
9911                    && tuple[k] == usize::MAX
9912                {
9913                    // v7.39 (round 962) — a whole-row reference to a side
9914                    // an OUTER join null-extended is NULL, not a
9915                    // composite whose fields are all NULL. PG18.4 answers
9916                    // `SELECT jb FROM wr LEFT JOIN jb ON <no match>` with
9917                    // an empty cell; round 961 answered `(,)`.
9918                    //
9919                    // The evaluator below cannot tell the two apart: it
9920                    // reads the MATERIALISED combined row, where a
9921                    // null-extended side is indistinguishable from a real
9922                    // row whose every column is NULL — and that row is
9923                    // `(,)` in PG too, so guessing by "all fields NULL"
9924                    // would trade one wrong answer for another. The
9925                    // tuple, which is still in hand here, does know:
9926                    // `usize::MAX` is the sentinel the join writes for
9927                    // exactly this.
9928                    values.push(Value::Null);
9929                } else {
9930                    // Eval path — `materialised` is Some whenever any
9931                    // projection item is non-bound (need_eval_row true).
9932                    // v7.24 (round-16 B) — select-list subqueries under a
9933                    // JOIN go through the correlated-aware evaluator too.
9934                    let mrow = materialised.as_deref().expect("materialised for eval");
9935                    values.push(self.eval_expr_with_correlated(
9936                        &p.expr,
9937                        mrow,
9938                        &ctx,
9939                        cancel,
9940                        Some(&mut proj_memo),
9941                    )?);
9942                }
9943            }
9944            let out_row = Row::new(values);
9945            // v7.37.16 — streaming DISTINCT (see the scan-path twin):
9946            // probe on the projected row; duplicates skip the
9947            // build_order_keys eval and never enter `tagged`.
9948            if stmt.distinct {
9949                let bucket = seen_distinct
9950                    .entry(norm_hash_row(
9951                        &out_row,
9952                        &distinct_hb,
9953                        FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
9954                    ))
9955                    .or_default();
9956                if bucket.iter().any(|i| {
9957                    row_eq_norm(
9958                        &tagged[i].1,
9959                        &out_row,
9960                        FoldSpec::of(ctx.mysql_dialect, &distinct_mask),
9961                    )
9962                }) {
9963                    continue;
9964                }
9965                bucket.push(tagged.len());
9966            }
9967            let order_keys = if stmt.order_by.is_empty() {
9968                Vec::new()
9969            } else {
9970                let mrow = materialised.as_deref().expect("materialised for order by");
9971                build_order_keys(&stmt.order_by, mrow, &ctx)?
9972            };
9973            budget.charge(approx_row_bytes(&out_row))?;
9974            tagged.push((order_keys, out_row));
9975            if let Some((k, descs)) = &topk_stream {
9976                topk_trim(&mut tagged, *k, descs);
9977            }
9978        }
9979        if !stmt.order_by.is_empty() {
9980            // v7.38 元机制 D acceptor — see other call site above.
9981            let keep = if self.env_cfg().disable_topk {
9982                None
9983            } else {
9984                stmt.limit_literal()
9985                    .map(|l| l as usize + stmt.offset_literal().map_or(0, |o| o as usize))
9986            };
9987            let descs: Vec<bool> = stmt.order_by.iter().map(|o| o.desc).collect();
9988            // v7.39 (round 688) — the join's ORDER BY resolves its keys
9989            // against `ctx`, which is built from `build_combined_schema`, so
9990            // this is where a declared collation reaches the sort. There was
9991            // exactly ONE resolver call in the engine before this — the
9992            // single-table scan's — which is why every other shape sorted by
9993            // bytes no matter what the schemas carried.
9994            let colls = crate::orderby::order_by_collations(&stmt.order_by, &ctx)?;
9995            crate::orderby::partial_sort_tagged_in(&mut tagged, keep, &descs, &colls);
9996        }
9997        let mut output_rows: Vec<Row<'static>> = tagged.into_iter().map(|(_, r)| r).collect();
9998        apply_offset_and_limit(
9999            &mut output_rows,
10000            stmt.offset_literal(),
10001            stmt.limit_literal(),
10002        );
10003        let columns: Vec<ColumnSchema> = projection
10004            .into_iter()
10005            .map(|p| p.to_column_schema())
10006            .collect();
10007        Ok(QueryResult::Rows {
10008            columns,
10009            rows: output_rows,
10010        })
10011    }
10012}
10013
10014impl Engine {
10015    /// v6.10.2 — cold-tier time-travel scan. Resolves the segment
10016    /// by id, decodes each row body against the table's current
10017    /// schema, applies the SELECT's projection + optional WHERE +
10018    /// optional LIMIT, returns a `Rows` result. JOINs / aggregates
10019    /// / ORDER BY are unsupported on this path (STABILITY carve-
10020    /// out); operators wanting them should restore the segment
10021    /// into a regular table first.
10022    fn exec_select_as_of_segment(
10023        &self,
10024        stmt: &SelectStatement,
10025        from: &spg_sql::ast::FromClause,
10026        segment_id: u32,
10027    ) -> Result<QueryResult, EngineError> {
10028        // v6.10.2 scope: no joins, no aggregates, no ORDER BY,
10029        // no GROUP BY / HAVING / UNION / OFFSET / DISTINCT.
10030        if !from.joins.is_empty()
10031            || stmt.group_by.is_some()
10032            || stmt.having.is_some()
10033            || !stmt.unions.is_empty()
10034            || !stmt.order_by.is_empty()
10035            || stmt.offset.is_some()
10036            || stmt.distinct
10037            || aggregate::uses_aggregate(stmt)
10038        {
10039            return Err(EngineError::Unsupported(
10040                "AS OF SEGMENT supports SELECT projection + WHERE + LIMIT only \
10041                 (joins / aggregates / ORDER BY are STABILITY § \"Out of v6.10\")"
10042                    .into(),
10043            ));
10044        }
10045        let table = self
10046            .active_catalog()
10047            .get(&from.primary.name)
10048            .ok_or_else(|| StorageError::TableNotFound {
10049                name: from.primary.name.clone(),
10050            })?;
10051        let schema = table.schema().clone();
10052        let schema_cols = &schema.columns;
10053        let alias = from
10054            .primary
10055            .alias
10056            .as_deref()
10057            .unwrap_or(from.primary.name.as_str());
10058        let ctx = self.ev_ctx(schema_cols, Some(alias));
10059        let seg = self
10060            .active_catalog()
10061            .cold_segment(segment_id)
10062            .ok_or_else(|| {
10063                EngineError::Unsupported(alloc::format!(
10064                    "AS OF SEGMENT: cold segment {segment_id} not registered"
10065                ))
10066            })?;
10067        let mut out_rows: Vec<Row<'static>> = Vec::new();
10068        let mut limit_remaining: Option<usize> =
10069            stmt.limit_literal().and_then(|n| usize::try_from(n).ok());
10070        for (_key, body) in seg.scan() {
10071            let (row, _consumed) =
10072                spg_storage::decode_row_body_dense(&body, &schema, seg.codec_version())
10073                    .map_err(EngineError::Storage)?;
10074            if let Some(where_expr) = &stmt.where_ {
10075                let cond = self.eval_expr_simple(where_expr, &row, &ctx)?;
10076                if !crate::eval::predicate_is_true(&cond, "WHERE", ctx.mysql_dialect)? {
10077                    continue;
10078                }
10079            }
10080            // Projection.
10081            let projected = self.project_row_simple(&row, &stmt.items, schema_cols, alias)?;
10082            out_rows.push(projected);
10083            if let Some(rem) = limit_remaining.as_mut() {
10084                if *rem == 0 {
10085                    out_rows.pop();
10086                    break;
10087                }
10088                *rem -= 1;
10089            }
10090        }
10091        // Output column schema: derive from SELECT items.
10092        let columns = self.derive_output_columns(&stmt.items, schema_cols, alias);
10093        Ok(QueryResult::Rows {
10094            columns,
10095            rows: out_rows,
10096        })
10097    }
10098
10099    /// v6.10.2 — simple-path WHERE eval that doesn't go through
10100    /// the correlated-subquery / Memoize machinery. AS OF SEGMENT
10101    /// scan paths predicate against a snapshot frozen segment, no
10102    /// cross-row state.
10103    fn eval_expr_simple(
10104        &self,
10105        expr: &Expr,
10106        row: &Row<'static>,
10107        ctx: &EvalContext,
10108    ) -> Result<Value<'static>, EngineError> {
10109        let cancel = CancelToken::none();
10110        self.eval_expr_with_correlated(expr, row, ctx, cancel, None)
10111    }
10112}
10113
10114// ---- SELECT result / projection / generate-series / SRF helpers (lib.rs split 12) ----
10115
10116/// One row-producing projection: an expression to evaluate, the resulting
10117/// column's user-visible name, its inferred type, and nullability.
10118#[derive(Debug, Clone)]
10119pub(crate) struct ProjectedItem {
10120    pub(crate) expr: Expr,
10121    pub(crate) output_name: String,
10122    pub(crate) ty: DataType,
10123    pub(crate) nullable: bool,
10124    /// v7.39 (read01 round 54) — a projected enum column keeps its enum
10125    /// identity. Enum-ness lives outside the DataType lattice (the value is a
10126    /// Text), so a projection that dropped this made the RESULT schema forget
10127    /// it — and a UNION's combined `ORDER BY <enum col>`, which sorts against
10128    /// that schema, silently fell back to TEXT order instead of member order.
10129    pub(crate) user_enum_type: Option<String>,
10130    /// v7.39 (round 425) — a projected MySQL temporal column keeps its
10131    /// declared fractional-seconds precision, so the renderer can pad to
10132    /// exactly that many digits (`DATETIME(3)` shows `.250`, and `.000` for
10133    /// a whole second). Like `user_enum_type` this lives outside the
10134    /// DataType lattice, so a projection that dropped it made the RESULT
10135    /// schema forget how wide the fraction should print.
10136    pub(crate) mysql_fsp: Option<u8>,
10137    /// v7.39 (round 688) — and its declared collation, the third thing to
10138    /// live outside the DataType lattice and the third to be lost the same
10139    /// way. Measured: `SELECT a.loc FROM a JOIN b … ORDER BY a.loc` over a
10140    /// column declared `COLLATE "en_US.utf8"` sorted by bytes, because the
10141    /// projection rebuilt the output column and the ORDER BY resolves
10142    /// against THAT schema.
10143    pub(crate) collation_name: Option<String>,
10144    /// v7.38.13 — and whether this position must NOT fold when DISTINCT
10145    /// de-dups it. The fourth thing to live outside the DataType lattice
10146    /// and the fourth to be lost the same way: a column declared
10147    /// `COLLATE utf8mb4_bin` is byte-wise, `SELECT DISTINCT t` folded it
10148    /// anyway, and `'a'` and `'A'` came back as one row where MariaDB 11
10149    /// returns two.
10150    ///
10151    /// A BOOL rather than the `Collation` enum on purpose. The enum's
10152    /// storage default is `Binary`, but the FOLD default under MySQL is
10153    /// case-insensitive — carrying the enum would silently mean
10154    /// "exempt" for every projected expression that is not a column.
10155    /// This field states the question it answers.
10156    pub(crate) fold_exempt: bool,
10157    /// v7.38.18 — does this column's collation make trailing spaces
10158    /// insignificant? A separate question from `fold_exempt`:
10159    /// `utf8mb4_bin` is fold-exempt AND pads, `utf8mb4_0900_ai_ci`
10160    /// folds and does not. Read off the same column, at the same
10161    /// place, so the two masks cannot drift apart.
10162    pub(crate) pads: bool,
10163}
10164
10165impl ProjectedItem {
10166    /// v7.38.14 — the output column this projected item describes.
10167    ///
10168    /// There were TWENTY-ONE places converting a `ProjectedItem` into a
10169    /// `ColumnSchema`, each written as `ColumnSchema::new(..)` followed by a
10170    /// hand-picked list of attributes to copy after it, and the lists did not
10171    /// agree: six carried enum identity, the collation NAME and MySQL fsp; ten
10172    /// carried the first and last but not the name; five carried nothing at
10173    /// all. Not one carried `collation`, the enum every MySQL text comparison
10174    /// actually reads.
10175    ///
10176    /// That is how a declared collation vanished between a subquery and the
10177    /// query that selects from it: the inner SELECT's output schema claimed
10178    /// `ColumnSchema::new`'s default, which is `Binary` — a value downstream
10179    /// reads as "byte-wise ON PURPOSE" rather than as "unknown", so the loss
10180    /// presents as a deliberate declaration.
10181    ///
10182    /// One conversion, so a field added to either type has one place to be
10183    /// remembered instead of twenty-one.
10184    pub(crate) fn to_column_schema(&self) -> ColumnSchema {
10185        let mut c = ColumnSchema::new(self.output_name.clone(), self.ty, self.nullable);
10186        c.user_enum_type.clone_from(&self.user_enum_type);
10187        c.collation_name.clone_from(&self.collation_name);
10188        c.mysql_fsp = self.mysql_fsp;
10189        // `fold_exempt` is the projection's answer to the same question
10190        // `ColumnSchema::collation` answers downstream, and it was computed
10191        // from the source column. Keeping the two in step here is what stops
10192        // a de-duplication site further on from asking the schema and being
10193        // told the opposite of what the projection knew.
10194        c.collation = if self.fold_exempt {
10195            spg_storage::Collation::Binary
10196        } else {
10197            spg_storage::Collation::CaseInsensitive
10198        };
10199        c
10200    }
10201}
10202
10203/// Dedupe a row set, preserving first-seen order. `Row`'s `PartialEq` is
10204/// structural (`Vec<Value<'static>>` ⇒ pairwise `Value` equality), which gives SQL
10205/// `NULL = NULL → TRUE` and `NaN = NaN → FALSE`. The first agrees with
10206/// the spec's "two NULLs are not distinct"; the second is a tolerated
10207/// quirk for v1 (no NaN literals are reachable from the SQL surface).
10208/// v7.37 D.23 — is this expression a bare (non-window) aggregate call?
10209fn expr_is_aggregate_call(e: &Expr) -> bool {
10210    match e {
10211        Expr::FunctionCall { name, .. } => crate::aggregate::is_aggregate_name(name),
10212        Expr::AggregateOrdered { .. } => true,
10213        _ => false,
10214    }
10215}
10216
10217/// Collect distinct top-level aggregate call expressions (dedup by value). Does
10218/// not recurse into an aggregate's own args (it's hoisted whole). Reuses the same
10219/// pragmatic variant set as `rewrite_window_to_columns`; aggregates nested in
10220/// uncovered variants simply aren't hoisted (the query keeps erroring, no worse
10221/// than today — never a regression on a working query).
10222fn collect_agg_exprs(e: &Expr, out: &mut Vec<Expr>) {
10223    if expr_is_aggregate_call(e) {
10224        if !out.iter().any(|x| x == e) {
10225            out.push(e.clone());
10226        }
10227        return;
10228    }
10229    match e {
10230        Expr::Binary { lhs, rhs, .. } => {
10231            collect_agg_exprs(lhs, out);
10232            collect_agg_exprs(rhs, out);
10233        }
10234        Expr::Unary { expr, .. }
10235        | Expr::Cast { expr, .. }
10236        | Expr::IsNull { expr, .. }
10237        | Expr::BoolTest { expr, .. }
10238        | Expr::FieldAccess { base: expr, .. } => collect_agg_exprs(expr, out),
10239        Expr::FunctionCall { args, .. } => {
10240            for a in args {
10241                collect_agg_exprs(a, out);
10242            }
10243        }
10244        Expr::Like { expr, pattern, .. } => {
10245            collect_agg_exprs(expr, out);
10246            collect_agg_exprs(pattern, out);
10247        }
10248        Expr::Extract { source, .. } => collect_agg_exprs(source, out),
10249        Expr::WindowFunction {
10250            args,
10251            partition_by,
10252            order_by,
10253            ..
10254        } => {
10255            for a in args {
10256                collect_agg_exprs(a, out);
10257            }
10258            for p in partition_by {
10259                collect_agg_exprs(p, out);
10260            }
10261            for (o, _, _) in order_by {
10262                collect_agg_exprs(o, out);
10263            }
10264        }
10265        _ => {}
10266    }
10267}
10268
10269/// Replace each aggregate call in `aggs` with a `Column(__aggN)` reference.
10270fn replace_agg_exprs(e: &mut Expr, aggs: &[Expr]) {
10271    if expr_is_aggregate_call(e) {
10272        if let Some(idx) = aggs.iter().position(|x| x == e) {
10273            *e = Expr::Column(ColumnName {
10274                qualifier: None,
10275                name: alloc::format!("__agg{idx}"),
10276            });
10277        }
10278        return;
10279    }
10280    match e {
10281        Expr::Binary { lhs, rhs, .. } => {
10282            replace_agg_exprs(lhs, aggs);
10283            replace_agg_exprs(rhs, aggs);
10284        }
10285        Expr::Unary { expr, .. }
10286        | Expr::Cast { expr, .. }
10287        | Expr::IsNull { expr, .. }
10288        | Expr::BoolTest { expr, .. }
10289        | Expr::FieldAccess { base: expr, .. } => replace_agg_exprs(expr, aggs),
10290        Expr::FunctionCall { args, .. } => {
10291            for a in args {
10292                replace_agg_exprs(a, aggs);
10293            }
10294        }
10295        Expr::Like { expr, pattern, .. } => {
10296            replace_agg_exprs(expr, aggs);
10297            replace_agg_exprs(pattern, aggs);
10298        }
10299        Expr::Extract { source, .. } => replace_agg_exprs(source, aggs),
10300        Expr::WindowFunction {
10301            args,
10302            partition_by,
10303            order_by,
10304            ..
10305        } => {
10306            for a in args {
10307                replace_agg_exprs(a, aggs);
10308            }
10309            for p in partition_by {
10310                replace_agg_exprs(p, aggs);
10311            }
10312            for (o, _, _) in order_by {
10313                replace_agg_exprs(o, aggs);
10314            }
10315        }
10316        _ => {}
10317    }
10318}
10319
10320/// v7.37 D.23 — window functions run AFTER GROUP BY aggregation. Rewrite
10321/// `SELECT g, sum(v), rank() OVER (ORDER BY sum(v)) FROM t GROUP BY g` into an
10322/// aggregate derived subquery (`SELECT g, sum(v) AS __agg0 FROM t GROUP BY g`) +
10323/// an outer window query over it (`SELECT g, __agg0, rank() OVER (ORDER BY
10324/// __agg0) FROM (...) __aggwin`), which the window-over-derived path (D.13) runs.
10325/// Returns None outside the bounded subset (leaves current behaviour). Only fires
10326/// on the currently-erroring agg+window+GROUP BY shape → cannot regress working
10327/// window-only / aggregate-only queries.
10328fn rewrite_agg_before_window(stmt: &SelectStatement) -> Option<SelectStatement> {
10329    if !(crate::aggregate::uses_aggregate(stmt) || stmt.group_by.is_some()) {
10330        return None;
10331    }
10332    // Bounded subset: no set-ops; GROUP BY keys must be simple columns.
10333    if !stmt.unions.is_empty() {
10334        return None;
10335    }
10336    let group_cols: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
10337    if group_cols.iter().any(|g| !matches!(g, Expr::Column(_))) {
10338        return None;
10339    }
10340    stmt.from.as_ref()?;
10341    // Collect the aggregate calls to hoist from projection + outer ORDER BY.
10342    let mut aggs: Vec<Expr> = Vec::new();
10343    for item in &stmt.items {
10344        if let SelectItem::Expr { expr, .. } = item {
10345            collect_agg_exprs(expr, &mut aggs);
10346        }
10347    }
10348    for ob in &stmt.order_by {
10349        collect_agg_exprs(&ob.expr, &mut aggs);
10350    }
10351    // Inner aggregate subquery: group cols (by name) + each aggregate as __aggN.
10352    let mut inner_items: Vec<SelectItem> = Vec::new();
10353    for g in &group_cols {
10354        inner_items.push(SelectItem::Expr {
10355            expr: g.clone(),
10356            alias: None,
10357        });
10358    }
10359    for (i, a) in aggs.iter().enumerate() {
10360        inner_items.push(SelectItem::Expr {
10361            expr: a.clone(),
10362            alias: Some(alloc::format!("__agg{i}")),
10363        });
10364    }
10365    let inner = SelectStatement {
10366        items: inner_items,
10367        distinct: false,
10368        distinct_on: Vec::new(),
10369        unions: Vec::new(),
10370        order_by: Vec::new(),
10371        limit: None,
10372        offset: None,
10373        limit_with_ties: false,
10374        window_check_exprs: Vec::new(),
10375        ..stmt.clone()
10376    };
10377    let derived = TableRef {
10378        name: "__aggwin".into(),
10379        alias: Some("__aggwin".into()),
10380        only: false,
10381        as_of_segment: None,
10382        unnest_expr: None,
10383        unnest_column_aliases: Vec::new(),
10384        with_ordinality: false,
10385        generate_series_args: None,
10386        lateral_subquery: Some(alloc::boxed::Box::new(inner)),
10387        jsonb_each_text_arg: None,
10388        table_fn_call: None,
10389        rows_from: None,
10390        json_table: None,
10391        scalar_fn_item: false,
10392    };
10393    // Outer window query over the derived rows: aggregates → __aggN column refs.
10394    let mut outer_items = stmt.items.clone();
10395    for item in &mut outer_items {
10396        if let SelectItem::Expr { expr, alias } = item {
10397            // Preserve PG's column label for a bare aggregate projection.
10398            if alias.is_none()
10399                && let Expr::FunctionCall { name, .. } = expr
10400                && crate::aggregate::is_aggregate_name(name)
10401            {
10402                *alias = Some(name.to_ascii_lowercase());
10403            }
10404            replace_agg_exprs(expr, &aggs);
10405        }
10406    }
10407    let mut outer_order = stmt.order_by.clone();
10408    for ob in &mut outer_order {
10409        replace_agg_exprs(&mut ob.expr, &aggs);
10410    }
10411    let mut outer_distinct_on = stmt.distinct_on.clone();
10412    for e in &mut outer_distinct_on {
10413        replace_agg_exprs(e, &aggs);
10414    }
10415    Some(SelectStatement {
10416        locking: None,
10417        ctes: Vec::new(),
10418        distinct: stmt.distinct,
10419        distinct_on: outer_distinct_on,
10420        items: outer_items,
10421        from: Some(FromClause {
10422            primary: derived,
10423            joins: Vec::new(),
10424        }),
10425        where_: None,
10426        group_by: None,
10427        group_by_all: false,
10428        having: None,
10429        unions: Vec::new(),
10430        order_by: outer_order,
10431        limit: stmt.limit.clone(),
10432        offset: stmt.offset.clone(),
10433        limit_with_ties: stmt.limit_with_ties,
10434        window_check_exprs: Vec::new(),
10435    })
10436}
10437
10438/// v7.39 (round 591) — the right-hand side of a set operation, bucketed for
10439/// membership.
10440///
10441/// INTERSECT, EXCEPT and their ALL forms all ask "is this left row over
10442/// there?", and all four answered by scanning the whole right side once per
10443/// left row. The cost was (left rows x right rows), which is why
10444/// `500k INTERSECT 1000` took 1.67 s while the same two inputs the other way
10445/// round took 20 ms: a left row that MATCHES stops the scan early, and a left
10446/// row that does not pays for all of it. Over 100k left rows, raising the
10447/// right side from 100 to 10,000 took 35 ms to 2848.
10448///
10449/// This is the shape round 485 already solved for DISTINCT, and it reuses
10450/// that machinery: bucket by `norm_hash_row`, whose only guarantee is the one
10451/// needed here — rows `row_eq_norm` calls equal hash the same — and settle
10452/// every bucket with the exact comparator, so a collision costs time and
10453/// never an answer.
10454struct PeerIndex<'r> {
10455    bh: hashbrown::DefaultHashBuilder,
10456    buckets: hashbrown::HashMap<u64, Vec<usize>>,
10457    rows: &'r [Row<'static>],
10458    fold: FoldSpec<'r>,
10459}
10460
10461impl<'r> PeerIndex<'r> {
10462    fn build(rows: &'r [Row<'static>], fold: FoldSpec<'r>) -> Self {
10463        // ONE hasher for the whole pass: the default builder is seeded per
10464        // instance, so a fresh one per row would put equal rows in different
10465        // buckets.
10466        let bh = hashbrown::DefaultHashBuilder::default();
10467        let mut buckets: hashbrown::HashMap<u64, Vec<usize>> =
10468            hashbrown::HashMap::with_capacity(rows.len());
10469        for (i, r) in rows.iter().enumerate() {
10470            buckets
10471                .entry(norm_hash_row(r, &bh, fold))
10472                .or_default()
10473                .push(i);
10474        }
10475        Self {
10476            bh,
10477            buckets,
10478            rows,
10479            fold,
10480        }
10481    }
10482
10483    fn contains(&self, r: &Row<'static>) -> bool {
10484        let h = norm_hash_row(r, &self.bh, self.fold);
10485        self.buckets
10486            .get(&h)
10487            .is_some_and(|b| b.iter().any(|&i| row_eq_norm(&self.rows[i], r, self.fold)))
10488    }
10489
10490    /// Remove ONE occurrence, so the multiset forms cancel row for row the
10491    /// way the pool they replaced did.
10492    fn take_one(&mut self, r: &Row<'static>) -> bool {
10493        let h = norm_hash_row(r, &self.bh, self.fold);
10494        let Some(b) = self.buckets.get_mut(&h) else {
10495            return false;
10496        };
10497        let Some(pos) = b
10498            .iter()
10499            .position(|&i| row_eq_norm(&self.rows[i], r, self.fold))
10500        else {
10501            return false;
10502        };
10503        b.swap_remove(pos);
10504        true
10505    }
10506}
10507
10508pub(crate) fn dedup_rows(rows: Vec<Row<'static>>, fold: FoldSpec<'_>) -> Vec<Row<'static>> {
10509    dedup_by_row(rows, |r| r, fold)
10510}
10511
10512/// v7.37.16 — hash-bucketed DISTINCT. The old `out.iter().any(row_eq_norm)`
10513/// was O(n·u) — `SELECT DISTINCT v` over 50 k rows with ~39 k unique values
10514/// ran 4 SECONDS (80 µs/row) vs PG's ~5 ms. Bucket rows by `norm_hash_row`
10515/// and run the exact `row_eq_norm` only within a bucket: first-occurrence
10516/// order is preserved, and correctness needs only the one-way guarantee
10517/// "row_eq_norm-Equal ⇒ equal hash" (collisions are re-checked exactly).
10518/// Small inputs keep the linear scan — no hasher setup for a 10-row page.
10519fn dedup_by_row<T>(
10520    items: Vec<T>,
10521    row_of: impl Fn(&T) -> &Row<'static>,
10522    fold: FoldSpec<'_>,
10523) -> Vec<T> {
10524    if items.len() <= 32 {
10525        let mut out: Vec<T> = Vec::with_capacity(items.len());
10526        for it in items {
10527            if !out
10528                .iter()
10529                .any(|seen| row_eq_norm(row_of(seen), row_of(&it), fold))
10530            {
10531                out.push(it);
10532            }
10533        }
10534        return out;
10535    }
10536    // ONE BuildHasher instance for the whole pass — the default builder
10537    // is randomly seeded PER INSTANCE, so a fresh one per row would give
10538    // equal rows different hashes and never dedup.
10539    let bh = hashbrown::DefaultHashBuilder::default();
10540    let mut out: Vec<T> = Vec::with_capacity(items.len().min(1024));
10541    let mut buckets: hashbrown::HashMap<u64, crate::distinct::DistinctBucket> =
10542        hashbrown::HashMap::with_capacity(items.len());
10543    for it in items {
10544        let h = norm_hash_row(row_of(&it), &bh, fold);
10545        let bucket = buckets.entry(h).or_default();
10546        if !bucket
10547            .iter()
10548            .any(|i| row_eq_norm(row_of(&out[i]), row_of(&it), fold))
10549        {
10550            bucket.push(out.len());
10551            out.push(it);
10552        }
10553    }
10554    out
10555}
10556
10557/// Hash companion to [`row_eq_norm`]. Guarantees only the direction dedup
10558/// needs: rows that `row_eq_norm` deems Equal hash identically; DISTINCT
10559/// rows may collide (buckets are re-checked with the exact comparator).
10560///
10561/// Domain design mirrors `value_cmp`'s equivalence classes:
10562/// - The numeric family (SmallInt/Int/BigInt/Float/Numeric/NumericBig)
10563///   shares one domain: a value that is an integer fitting i64 hashes the
10564///   i64 (so `Int(1)`, `BigInt(1)`, `Float(1.0)`, `Numeric(1.00)` agree);
10565///   anything else hashes the f64 approximation computed by THE SAME
10566///   formula the value_cmp float arms use (`numeric_to_f64`), so
10567///   `Numeric(0.5) == Float(0.5)` agree bit-for-bit. NaN (any family)
10568///   hashes a constant; ±Inf hash their f64 bits; -0.0 folds into 0.0.
10569///   Known un-closable corner: an integer in [2^53, 2^63) can compare
10570///   Equal to a float via value_cmp's lossy f64 arm while hashing in the
10571///   exact-i64 domain — mixed int/float rows at that magnitude may miss a
10572///   dedup (PG itself compares int8↔float8 in the lossy float8 domain).
10573/// - Text and BpChar share a trailing-blank-trimmed byte domain (value_cmp
10574///   compares them blank-insensitively; plain Text pairs that differ only
10575///   in trailing blanks merely collide and are separated exactly).
10576/// - Families value_cmp compares exactly (Bool/Date/Time/Timestamp/…)
10577///   hash their fields under a distinct tag.
10578/// - Everything value_cmp falls back to debug-format ordering for
10579///   (Json, arrays, vectors, geometry, ranges, …) shares one constant
10580///   bucket — degrades to the exact linear scan, never wrong.
10581fn norm_hash_row(
10582    row: &Row<'static>,
10583    bh: &hashbrown::DefaultHashBuilder,
10584    fold: FoldSpec<'_>,
10585) -> u64 {
10586    norm_hash_values(&row.values, bh, fold)
10587}
10588
10589/// v7.39 (round 485) — the same hash over a bare value slice, so the
10590/// DISTINCT probe can run against a reused buffer instead of demanding a
10591/// `Row` that has to be allocated first (see `values_eq_norm`).
10592fn norm_hash_values(
10593    values: &[Value<'static>],
10594    bh: &hashbrown::DefaultHashBuilder,
10595    fold: FoldSpec<'_>,
10596) -> u64 {
10597    use core::hash::{BuildHasher, Hash, Hasher};
10598    let mut h = bh.build_hasher();
10599    for (i, v) in values.iter().enumerate() {
10600        // v7.39 (round 410) — hash the folded key when the MySQL collation
10601        // deduplicates a text value, so `row_eq_norm`-equal rows (`'a'` vs
10602        // `'A'` vs `'a '`) share a hash bucket.
10603        //
10604        // v7.38.13 — per POSITION, in lockstep with `values_eq_norm`. A
10605        // byte-wise column that folded here while the comparator did not
10606        // would scatter equal rows across buckets and stop de-duplicating
10607        // at all; the hash and the comparator have to read the same mask.
10608        if fold.folds(i)
10609            && let Some(folded) = mysql_dedup_fold(v, fold.pads_at(i))
10610        {
10611            folded.hash(&mut h);
10612            continue;
10613        }
10614        norm_hash_value(v, &mut h);
10615    }
10616    h.finish()
10617}
10618
10619/// r1044 — `10^p` as an `i128`, or `None` past what one holds.
10620///
10621/// `i128::MAX` is about 1.7e38, so 10^38 is the last power that fits.
10622const fn pow10_i128(p: u16) -> Option<i128> {
10623    const P: [i128; 39] = {
10624        let mut t = [1i128; 39];
10625        let mut i = 1;
10626        while i < 39 {
10627            t[i] = t[i - 1] * 10;
10628            i += 1;
10629        }
10630        t
10631    };
10632    if (p as usize) < P.len() {
10633        Some(P[p as usize])
10634    } else {
10635        None
10636    }
10637}
10638
10639fn norm_hash_value<H: core::hash::Hasher>(v: &Value<'static>, h: &mut H) {
10640    const TAG_NULL: u8 = 0;
10641    const TAG_BOOL: u8 = 1;
10642    const TAG_NUM_I64: u8 = 2;
10643    const TAG_NUM_F64: u8 = 3;
10644    const TAG_TEXT: u8 = 4;
10645    const TAG_DATE: u8 = 6;
10646    const TAG_TIME: u8 = 7;
10647    const TAG_TIMESTAMP: u8 = 8;
10648    const TAG_TIMETZ: u8 = 10;
10649    const TAG_UUID: u8 = 11;
10650    const TAG_MONEY: u8 = 12;
10651    const TAG_BYTES: u8 = 13;
10652    const TAG_INTERVAL: u8 = 14;
10653    const TAG_CHAR1: u8 = 15;
10654    const TAG_OPAQUE: u8 = 255;
10655    // One shared writer for the numeric family: an integer value
10656    // representable as i64 goes exact (round-trip probe — no_std, so no
10657    // f64::trunc); otherwise the f64 approximation. -0.0 round-trips
10658    // through 0i64, folding it into 0.0 as value_cmp requires.
10659    let num_f64 = |h: &mut H, x: f64| {
10660        if x.is_nan() {
10661            h.write_u8(TAG_NUM_F64);
10662            h.write_u64(0x7ff8_dead_beef_0001); // one bucket for every NaN
10663            return;
10664        }
10665        const TWO63: f64 = 9_223_372_036_854_775_808.0;
10666        if (-TWO63..TWO63).contains(&x) {
10667            #[allow(clippy::cast_possible_truncation)]
10668            let n = x as i64;
10669            #[allow(clippy::cast_precision_loss)]
10670            if (n as f64) == x {
10671                h.write_u8(TAG_NUM_I64);
10672                h.write_i64(n);
10673                return;
10674            }
10675        }
10676        h.write_u8(TAG_NUM_F64);
10677        h.write_u64(x.to_bits());
10678    };
10679    match v {
10680        Value::Null => h.write_u8(TAG_NULL),
10681        Value::Bool(b) => {
10682            h.write_u8(TAG_BOOL);
10683            h.write_u8(u8::from(*b));
10684        }
10685        Value::SmallInt(n) => {
10686            h.write_u8(TAG_NUM_I64);
10687            h.write_i64(i64::from(*n));
10688        }
10689        Value::Int(n) => {
10690            h.write_u8(TAG_NUM_I64);
10691            h.write_i64(i64::from(*n));
10692        }
10693        Value::BigInt(n) => {
10694            h.write_u8(TAG_NUM_I64);
10695            h.write_i64(*n);
10696        }
10697        Value::Float(x) => num_f64(h, *x),
10698        Value::Numeric {
10699            scaled,
10700            scale,
10701            kind,
10702        } => match kind {
10703            spg_storage::NumericKind::NaN => num_f64(h, f64::NAN),
10704            spg_storage::NumericKind::PosInf => num_f64(h, f64::INFINITY),
10705            spg_storage::NumericKind::NegInf => num_f64(h, f64::NEG_INFINITY),
10706            spg_storage::NumericKind::Finite => {
10707                // Reduce trailing fractional zeros so 1.50 and 1.5 share a
10708                // representation, then: exact integers fitting i64 go to the
10709                // i64 domain; everything else uses numeric_to_f64 — the SAME
10710                // formula value_cmp's Numeric↔Float arm compares with.
10711                // r1044 — the reduction is required (`1.5` and `1.50` are
10712                // one value and must land in one bucket) and it used to
10713                // walk one digit at a time. That is O(scale), and scale
10714                // is not small in practice: `n / 100` on a NUMERIC
10715                // column stores `9.1900000000000000`, scale 16, so the
10716                // loop ran fourteen times PER ROW.
10717                //
10718                // Priced by ablation rather than guessed at — removing
10719                // the loop entirely took `SELECT DISTINCT n FROM t ORDER
10720                // BY n` over 400,000 rows from 52 ms to 14.8, against
10721                // PostgreSQL's 12.2-13.8. Two `pow10` lookup tables
10722                // tried first moved it not at all, which is why this one
10723                // was measured before it was written.
10724                //
10725                // Binary search over the same powers finds the whole
10726                // run of trailing zeros in at most six tests and one
10727                // division, instead of one test and one division per
10728                // digit.
10729                let (mut s, mut sc) = (*scaled, *scale);
10730                if sc > 0 && s != 0 {
10731                    let mut lo: u16 = 0;
10732                    let mut hi: u16 = sc;
10733                    while lo < hi {
10734                        let mid = (lo + hi).div_ceil(2);
10735                        match pow10_i128(mid) {
10736                            Some(p) if s % p == 0 => lo = mid,
10737                            _ => hi = mid - 1,
10738                        }
10739                    }
10740                    if lo > 0 {
10741                        if let Some(p) = pow10_i128(lo) {
10742                            s /= p;
10743                            sc -= lo;
10744                        }
10745                    }
10746                }
10747                if sc == 0 {
10748                    if let Ok(n) = i64::try_from(s) {
10749                        h.write_u8(TAG_NUM_I64);
10750                        h.write_i64(n);
10751                    } else {
10752                        num_f64(h, crate::orderby::numeric_to_f64(s, 0));
10753                    }
10754                } else {
10755                    num_f64(h, crate::orderby::numeric_to_f64(s, sc));
10756                }
10757            }
10758        },
10759        // Beyond-i128 NUMERIC compares exactly via numeric_bignum_cmp; a
10760        // value that also fits i128 reuses the Numeric path above so
10761        // Big(5) and Numeric(5) agree. A genuinely huge one can't equal
10762        // any i128-representable value — constant bucket is safe.
10763        Value::NumericBig(b) => match b.to_i128() {
10764            Some(s) => norm_hash_value(
10765                &Value::Numeric {
10766                    scaled: s,
10767                    scale: b.scale(),
10768                    kind: spg_storage::NumericKind::Finite,
10769                },
10770                h,
10771            ),
10772            None => h.write_u8(TAG_OPAQUE),
10773        },
10774        // value_cmp compares Text↔BpChar blank-insensitively (both sides
10775        // trimmed), so both hash the trimmed bytes. Text pairs differing
10776        // only in trailing blanks collide and are split exactly in-bucket.
10777        Value::Text(s) | Value::BpChar(s) => {
10778            h.write_u8(TAG_TEXT);
10779            h.write(s.trim_end_matches(' ').as_bytes());
10780        }
10781        Value::Char1(c) => {
10782            h.write_u8(TAG_CHAR1);
10783            h.write_u8(*c);
10784        }
10785        Value::Date(d) => {
10786            h.write_u8(TAG_DATE);
10787            h.write_i32(*d);
10788        }
10789        Value::Time(t) => {
10790            h.write_u8(TAG_TIME);
10791            h.write_i64(*t);
10792        }
10793        Value::Timestamp(t) => {
10794            h.write_u8(TAG_TIMESTAMP);
10795            h.write_i64(*t);
10796        }
10797        Value::TimeTz { us, offset_secs } => {
10798            h.write_u8(TAG_TIMETZ);
10799            h.write_i64(*us);
10800            h.write_i32(*offset_secs);
10801        }
10802        Value::Uuid(u) => {
10803            h.write_u8(TAG_UUID);
10804            h.write(u);
10805        }
10806        Value::Money(c) => {
10807            h.write_u8(TAG_MONEY);
10808            h.write_i64(*c);
10809        }
10810        Value::Bytes(b) => {
10811            h.write_u8(TAG_BYTES);
10812            h.write(b.as_ref());
10813        }
10814        Value::Interval {
10815            months,
10816            days,
10817            micros,
10818            kind,
10819        } => {
10820            h.write_u8(TAG_INTERVAL);
10821            h.write_i32(*months);
10822            h.write_i32(*days);
10823            h.write_i64(*micros);
10824        }
10825        // v7.37.16 — REAL joined the numeric value_cmp family (widened
10826        // to f64, same formulas as the arms), so it hashes in the shared
10827        // numeric domain: Real(1.5) must agree with Float(1.5)/Int/…
10828        // f32→f64 is exact, so equal-under-cmp implies equal bits here.
10829        Value::Real(x) => num_f64(h, f64::from(*x)),
10830        // Json (structural equality), vector families (float rendering),
10831        // arrays / geometry / net / ranges / composites (debug-format
10832        // fallback): one constant bucket — exact linear within.
10833        _ => h.write_u8(TAG_OPAQUE),
10834    }
10835}
10836
10837/// v7.38 (read01) — row equality for DISTINCT / UNION / INTERSECT / EXCEPT that
10838/// treats numerically-equal exact values as one regardless of type or scale
10839/// (`1 = 1.0 = 1.00`), matching PG (and GROUP BY). Uses the scale-aware
10840/// `orderby::value_cmp`, so `Int(1)` and `Numeric{10,1}` compare Equal; plain
10841/// `Row` `==` would keep them distinct.
10842/// v7.39 (round 410) — under the MySQL dialect a set operation / DISTINCT
10843/// deduplicates by the session collation (`utf8mb4_uca1400_ai_ci`, which is
10844/// case- and accent-insensitive and PAD SPACE): `'a'`, `'A'`, and `'a '`
10845/// collapse to one row, exactly as GROUP BY already folds its keys. Returns
10846/// the folded comparison key for a text value, None for anything else (which
10847/// keeps the byte-exact `value_cmp` path).
10848fn mysql_dedup_fold(v: &Value, pads: bool) -> Option<String> {
10849    match v {
10850        // v7.38.17 — CHAR's trailing spaces are padding; TEXT's are
10851        // data. The comment above named `utf8mb4_uca1400_ai_ci`, which
10852        // is MariaDB's default and PAD SPACE. SPG advertises MySQL 8.0,
10853        // whose default is NO PAD, so `'alpha'` and `'alpha  '` are two
10854        // rows to a `SELECT DISTINCT` and one to `count(DISTINCT)` was
10855        // the same question answered twice.
10856        // v7.38.18 — a CHAR's padding is the TYPE's and never counts; a
10857        // TEXT's is the collation's, which `pads` carries per position.
10858        Value::BpChar(s) => Some(spg_storage::mysql_compare_fold_char(s)),
10859        Value::Text(s) if pads => Some(spg_storage::mysql_compare_fold_char(s)),
10860        Value::Text(s) => Some(spg_storage::mysql_compare_fold(s)),
10861        _ => None,
10862    }
10863}
10864
10865/// v7.39 (round 485) — how many projected rows the single-table scan
10866/// builds, and how many of those the DISTINCT probe throws away again.
10867///
10868/// The round-485 profile of `SELECT DISTINCT g FROM h ORDER BY g` put
10869/// 21 % of all samples in malloc/free called straight from the scan
10870/// closure. The closure's one per-row allocation is the projected
10871/// `Vec<Value>`, and under DISTINCT most of those are discarded a few
10872/// instructions later — but "most" is a guess until it is a number, so
10873/// these count it. (Round 480 was spent acting on an inference about a
10874/// branch that turned out never to run.)
10875/// v7.39 (round 488) — reachability counters for round 487's projection
10876/// binding. The interleaved panel says round 487 costs `group_500k` 13 %,
10877/// and a never-called-function probe rules out code layout — so the
10878/// question is whether that shape reaches this code at all, which is a
10879/// number, not an inference.
10880pub static SCAN_PATH_ENTERED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10881pub static PROJ_DIRECT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10882
10883pub static PROJ_ROW_BUILT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
10884pub static DISTINCT_DUP_DROPPED: core::sync::atomic::AtomicU64 =
10885    core::sync::atomic::AtomicU64::new(0);
10886
10887/// v7.38.13 — how DISTINCT must compare one row of output.
10888///
10889/// The MySQL default collation folds case and trailing spaces when it
10890/// de-dups, but a column declared `COLLATE utf8mb4_bin` is BYTE-WISE and
10891/// must not fold — `e2e_mysql_collate_binary_round370` calls the
10892/// alternative "a silent data-integrity bug: `'a'` and `'A'` de-dup as
10893/// one when the schema asked to keep them apart", and names DISTINCT as
10894/// one of the sites that has to honour it.
10895///
10896/// It did not. `values_eq_norm` took a bare `bool` and folded every Text
10897/// value in a MySQL session, because a bool cannot see a column. The
10898/// GROUP BY path consults the schema and was right all along; the test
10899/// only ever exercised that spelling, so the DISTINCT hole was never
10900/// covered. `SELECT DISTINCT t` answered 2 where MariaDB 11 answers 4.
10901///
10902/// `binary` is indexed by OUTPUT POSITION; a position past its end folds,
10903/// which is what a caller with no schema to offer gets.
10904#[derive(Clone, Copy)]
10905pub(crate) struct FoldSpec<'c> {
10906    mysql: bool,
10907    binary: &'c [bool],
10908    /// v7.38.18 — the padding mask, in lockstep with `binary`. Read the
10909    /// note on `folds`: a hash and its comparator must consult the same
10910    /// masks or equal rows scatter across buckets.
10911    pads: &'c [bool],
10912}
10913
10914impl<'c> FoldSpec<'c> {
10915    /// No column information — every Text position folds under MySQL.
10916    pub(crate) const fn dialect(mysql: bool) -> Self {
10917        Self {
10918            mysql,
10919            binary: &[],
10920            pads: &[],
10921        }
10922    }
10923
10924    /// The mask read off the output columns.
10925    pub(crate) fn of(mysql: bool, binary: &'c [bool]) -> Self {
10926        Self {
10927            mysql,
10928            binary,
10929            pads: &[],
10930        }
10931    }
10932
10933    /// The masks read off the output columns — fold-exemption AND
10934    /// padding, which are different questions about the same collation.
10935    pub(crate) fn of_masks(mysql: bool, binary: &'c [bool], pads: &'c [bool]) -> Self {
10936        Self {
10937            mysql,
10938            binary,
10939            pads,
10940        }
10941    }
10942
10943    /// Does position `i` treat trailing spaces as insignificant?
10944    #[inline]
10945    fn pads_at(&self, i: usize) -> bool {
10946        self.pads.get(i).copied().unwrap_or(false)
10947    }
10948
10949    /// Does position `i` fold?
10950    #[inline]
10951    fn folds(&self, i: usize) -> bool {
10952        self.mysql && !self.binary.get(i).copied().unwrap_or(false)
10953    }
10954}
10955
10956/// The fold-exempt mask for a projection.
10957///
10958/// Read off `ProjectedItem`, not off the output `ColumnSchema`: the
10959/// projection rebuilds that schema through `ColumnSchema::new`, whose
10960/// collation default is `Binary` — a mask built from it would mark
10961/// EVERY column byte-wise and stop DISTINCT folding at all.
10962/// The padding mask for a projection, read off the same items as
10963/// [`fold_mask`] so the two cannot come from different places.
10964pub(crate) fn pad_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
10965    projection.iter().map(|p| p.pads).collect()
10966}
10967
10968pub(crate) fn fold_mask(projection: &[ProjectedItem]) -> alloc::vec::Vec<bool> {
10969    projection.iter().map(|p| p.fold_exempt).collect()
10970}
10971
10972/// v7.38.14 — the same mask, from an OUTPUT SCHEMA instead of a
10973/// projection.
10974///
10975/// Some de-duplication sites hold `Vec<ColumnSchema>` and never see the
10976/// `ProjectedItem`s it came from. `ProjectedItem::fold_exempt` is built
10977/// from exactly this test (`select.rs`, `build_projection`), so the two
10978/// must keep answering identically -- a site that decided "byte-wise" one
10979/// way while its neighbour decided the other is how the answer came to
10980/// depend on which executor ran the query.
10981///
10982/// The direction matters: `Collation::Binary` is `ColumnSchema::new`'s
10983/// DEFAULT, so a schema rebuilt without carrying the field reads as
10984/// "byte-wise on purpose" here. That is a real trap and it has caught
10985/// five fields so far; it is why S4 of this release exists.
10986/// v7.38.18 — the padding mask from output columns, the sibling of
10987/// [`fold_mask_of_columns`]. Whether a column folds and whether it
10988/// pads are different questions about the same collation.
10989pub(crate) fn pad_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
10990    columns
10991        .iter()
10992        .map(|c| crate::collate::pads_space(c.collation_name.as_deref()))
10993        .collect()
10994}
10995
10996pub(crate) fn fold_mask_of_columns(columns: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
10997    columns
10998        .iter()
10999        .map(|c| matches!(c.collation, spg_storage::Collation::Binary))
11000        .collect()
11001}
11002
11003pub(crate) fn row_eq_norm(a: &Row<'static>, b: &Row<'static>, fold: FoldSpec<'_>) -> bool {
11004    values_eq_norm(&a.values, &b.values, fold)
11005}
11006
11007/// v7.39 (round 485) — `row_eq_norm` over bare value slices, so the
11008/// DISTINCT probe can compare a reused projection buffer against a kept
11009/// row without building a `Row` for it.
11010pub(crate) fn values_eq_norm(
11011    a: &[Value<'static>],
11012    b: &[Value<'static>],
11013    fold: FoldSpec<'_>,
11014) -> bool {
11015    a.len() == b.len()
11016        && a.iter().zip(b).enumerate().all(|(i, (x, y))| {
11017            if fold.folds(i)
11018                && let (Some(fx), Some(fy)) = (
11019                    mysql_dedup_fold(x, fold.pads_at(i)),
11020                    mysql_dedup_fold(y, fold.pads_at(i)),
11021                )
11022            {
11023                return fx == fy;
11024            }
11025            crate::orderby::value_cmp(x, y) == core::cmp::Ordering::Equal
11026        })
11027}
11028
11029/// Coerce a `Value` to an `f64` sort key for ORDER BY. Numbers map directly;
11030/// NULL sorts last (treated as `+∞`); booleans are 0.0 / 1.0; text uses lex
11031/// order via the byte values; vectors are not sortable.
11032pub(crate) fn value_to_order_key(v: &Value) -> Result<OrderKey, EngineError> {
11033    // v7.37.16 — TEXT rides a FULL-precision key: carry the whole string
11034    // so values sharing a ≥6-byte common prefix (`product_001` vs
11035    // `product_002`, ISO timestamps stored as text, prefixed IDs / SKUs)
11036    // order by their exact bytes instead of the old lossy f64 coarse key.
11037    // Comparison is byte-lexicographic (see `order_key_elem_cmp`), which
11038    // matches PG's default C / binary text collation. Every other type
11039    // keeps the lossless-enough `f64` fast path below.
11040    if let Value::Text(s) = v {
11041        return Ok(OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())));
11042    }
11043    // v7.39 (bpchar epic) — bpchar sorts by its blank-stripped form then
11044    // byte order (PG bpcharcmp under C collation), so mixed-pad values of
11045    // the same logical string order equal.
11046    if let Value::BpChar(s) = v {
11047        return Ok(OrderKey::Text(crate::orderby::CompactText::new(
11048            s.trim_end_matches(' '),
11049        )));
11050    }
11051    // v7.38 (read01 P6.24) — jsonb sorts by PG's type-aware total order, so
11052    // carry the parsed value and compare it structurally (see
11053    // `order_key_elem_cmp`). Unparseable text falls back to a Text key.
11054    if let Value::Json(s) = v {
11055        return Ok(match crate::json::parse(s) {
11056            Ok(jv) => OrderKey::Json(jv),
11057            Err(_) => OrderKey::Text(crate::orderby::CompactText::new(s.as_ref())),
11058        });
11059    }
11060    // v7.37 — byte-orderable types PG sorts byte-wise but that have no
11061    // meaningful f64 projection. bytea/uuid/macaddr sort by their raw bytes;
11062    // inet/cidr by `[family, addr.., bits]` (family, then address, then mask),
11063    // matching PG's network ordering.
11064    match v {
11065        Value::Bytes(b) => return Ok(OrderKey::Bytes(b.as_ref().to_vec())),
11066        // v7.38 (read01, T3.C3) — arbitrary-precision NUMERIC sorts by exact value.
11067        Value::NumericBig(b) => {
11068            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11069                spg_storage::NumericKey::from_big(b),
11070            )));
11071        }
11072        Value::Uuid(u) => return Ok(OrderKey::Bytes(u.to_vec())),
11073        Value::Macaddr(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11074        Value::Macaddr8(m) => return Ok(OrderKey::Bytes(m.to_vec())),
11075        Value::PgLsn(l) => return Ok(OrderKey::Bytes(l.to_be_bytes().to_vec())),
11076        Value::Inet { family, bits, addr } | Value::Cidr { family, bits, addr } => {
11077            let mut key = alloc::vec::Vec::with_capacity(18);
11078            key.push(*family);
11079            key.extend_from_slice(addr);
11080            key.push(*bits);
11081            return Ok(OrderKey::Bytes(key));
11082        }
11083        _ => {}
11084    }
11085    // v7.38 (read01, U16) — one-dimensional arrays sort element-wise, then
11086    // shorter-first (PG: `{1} < {1,2} < {2} < {10}`). Each element carries its
11087    // own OrderKey so integer arrays sort numerically; a NULL element rides to
11088    // the end via the +INF sentinel.
11089    let inf = || OrderKey::NullBig;
11090    let arr = match v {
11091        Value::IntArray(a) => Some(
11092            a.iter()
11093                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11094                .collect(),
11095        ),
11096        Value::SmallIntArray(a) => Some(
11097            a.iter()
11098                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11099                .collect(),
11100        ),
11101        Value::BigIntArray(a) => Some(
11102            a.iter()
11103                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11104                .collect(),
11105        ),
11106        Value::BoolArray(a) => Some(
11107            a.iter()
11108                .map(|o| o.map_or_else(inf, |b| OrderKey::Int(i128::from(b))))
11109                .collect(),
11110        ),
11111        Value::TextArray(a) => Some(
11112            a.iter()
11113                .map(|o| {
11114                    o.as_ref()
11115                        .map_or_else(inf, |s| OrderKey::Text(crate::orderby::CompactText::new(s)))
11116                })
11117                .collect(),
11118        ),
11119        #[allow(clippy::cast_precision_loss)]
11120        Value::FloatArray(a) => Some(
11121            a.iter()
11122                .map(|o| o.map_or(OrderKey::NullBig, OrderKey::Num))
11123                .collect(),
11124        ),
11125        // r1040 — array elements take the same exact key their scalar
11126        // form does; an f64 projection here would order `{0.1}` against
11127        // `{0.1000000000000000001}` by luck.
11128        Value::NumericArray(a) => Some(
11129            a.iter()
11130                .map(|o| {
11131                    o.map_or_else(inf, |(m, s)| {
11132                        OrderKey::Numeric(alloc::boxed::Box::new(
11133                            spg_storage::NumericKey::from_numeric(
11134                                m,
11135                                s,
11136                                spg_storage::NumericKind::Finite,
11137                            ),
11138                        ))
11139                    })
11140                })
11141                .collect(),
11142        ),
11143        Value::DateArray(a) => Some(
11144            a.iter()
11145                .map(|o| o.map_or_else(inf, |n| OrderKey::Int(i128::from(n))))
11146                .collect(),
11147        ),
11148        _ => None,
11149    };
11150    if let Some(elements) = arr {
11151        return Ok(OrderKey::Array(elements));
11152    }
11153    // v7.39 (read01 round 56) — a COMPOSITE sorts field by field, left to
11154    // right, which is exactly the lexicographic element order an Array key
11155    // already gives: `(2,'b') < (9,'a')` because the leading field decides.
11156    if let Value::Composite(fields) = v {
11157        let elements = fields
11158            .iter()
11159            .map(|(_, fv)| value_to_order_key(fv))
11160            .collect::<Result<alloc::vec::Vec<_>, _>>()?;
11161        return Ok(OrderKey::Array(elements));
11162    }
11163    // v7.38 (read01 U31) — the integer-valued types carry an EXACT i128 key.
11164    // Projecting these to f64 (the historic path) silently collapses BigInt /
11165    // Timestamp / Time / TimeTz / Money values past 2^53, so `ORDER BY` gave
11166    // the wrong order for large ids and microsecond timestamps.
11167    match v {
11168        Value::SmallInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11169        Value::Int(n) => return Ok(OrderKey::Int(i128::from(*n))),
11170        Value::BigInt(n) => return Ok(OrderKey::Int(i128::from(*n))),
11171        // PG TIME/TIMESTAMP/DATE/MONEY/YEAR are ordered by their underlying
11172        // integer (days / micros / cents / calendar year); TIMETZ by the
11173        // UTC-equivalent micros (local wall - offset) so the same physical
11174        // instant in different zones sorts equal.
11175        Value::Date(d) => return Ok(OrderKey::Int(i128::from(*d))),
11176        Value::Timestamp(t) => return Ok(OrderKey::Int(i128::from(*t))),
11177        Value::Time(us) => return Ok(OrderKey::Int(i128::from(*us))),
11178        Value::Year(y) => return Ok(OrderKey::Int(i128::from(*y))),
11179        Value::TimeTz { us, offset_secs } => {
11180            return Ok(OrderKey::Int(
11181                i128::from(*us) - i128::from(*offset_secs) * 1_000_000,
11182            ));
11183        }
11184        Value::Money(c) => return Ok(OrderKey::Int(i128::from(*c))),
11185        _ => {}
11186    }
11187    let num = match v {
11188        // Callers without NULLS FIRST/LAST context (array elements,
11189        // histogram sampling) put NULL last, as before.
11190        Value::Null => return Ok(OrderKey::NullBig),
11191        // v7.17.0 Phase 3.P0-38 — range ordering is not supported
11192        // in v7.17.0 (needs lex-then-inclusivity tiebreak).
11193        Value::Range { .. } => {
11194            return Err(EngineError::Unsupported(
11195                "ORDER BY of a range value is not supported in v7.17.0".into(),
11196            ));
11197        }
11198        // v7.17.0 Phase 3.P0-39 — hstore is not orderable.
11199        Value::Hstore(_) => {
11200            return Err(EngineError::Unsupported(
11201                "ORDER BY of a hstore value is not supported".into(),
11202            ));
11203        }
11204        // v7.17.0 Phase 3.P0-40 — 2D arrays not orderable.
11205        Value::IntArray2D(_) | Value::BigIntArray2D(_) | Value::TextArray2D(_) => {
11206            return Err(EngineError::Unsupported(
11207                "ORDER BY of a 2D array is not supported in v7.17.0".into(),
11208            ));
11209        }
11210        // r1039/r1040 — the exact canonical key, not an f64 projection.
11211        //
11212        // r1039 fixed the three specials, which carry a canonical zero in
11213        // `scaled` and so all sorted as the number 0. The projection
11214        // itself was the rest of the defect: "precision losses here only
11215        // matter for tie-breaks well past 15 significant digits" was the
11216        // comment, and the measurement disagreed — f64 called
11217        // `0.1` and `0.1000000000000000001` Equal, and a stable sort then
11218        // returned them in insertion order. Three of ten values came back
11219        // in the wrong place against PG18.4.
11220        Value::Numeric {
11221            scaled,
11222            scale,
11223            kind,
11224        } => {
11225            return Ok(OrderKey::Numeric(alloc::boxed::Box::new(
11226                spg_storage::NumericKey::from_numeric(*scaled, *scale, *kind),
11227            )));
11228        }
11229        Value::Float(x) => *x,
11230        // v7.37.16 — REAL sorts by its exact f64 widening (it had no
11231        // arm and fell through to the unsupported error).
11232        Value::Real(x) => f64::from(*x),
11233        Value::Bool(b) => {
11234            if *b {
11235                1.0
11236            } else {
11237                0.0
11238            }
11239        }
11240        Value::Vector(_) | Value::Sq8Vector(_) | Value::HalfVector(_) => {
11241            return Err(EngineError::Unsupported(
11242                "ORDER BY of a raw vector column is not meaningful — use `<->`".into(),
11243            ));
11244        }
11245        // v7.37 — PG orders INTERVAL by its total time, treating a month as
11246        // 30 days (`1 hour < 90 min < 1 day < 1 mon`). Project to total micros;
11247        // f64 is exact for any interval under ~285 years, and only ORDER BY
11248        // tie-breaks past that magnitude lose precision. Matches the
11249        // min/max(interval) comparator in aggregate.rs.
11250        #[allow(clippy::cast_precision_loss)]
11251        Value::Interval {
11252            months,
11253            days,
11254            micros,
11255            kind,
11256        } => {
11257            let total = i128::from(*months) * 30 * 86_400_000_000
11258                + i128::from(*days) * 86_400_000_000
11259                + i128::from(*micros);
11260            total as f64
11261        }
11262        Value::Json(_) => {
11263            return Err(EngineError::Unsupported(
11264                "ORDER BY of a JSON value is not supported — cast the document to text first"
11265                    .into(),
11266            ));
11267        }
11268        // v7.5.0 — Value is #[non_exhaustive]; future variants need
11269        // an explicit ORDER BY mapping. Surface as Unsupported until
11270        // engine support is added.
11271        _ => {
11272            return Err(EngineError::Unsupported(
11273                "ORDER BY of this value type is not supported".into(),
11274            ));
11275        }
11276    };
11277    Ok(OrderKey::Num(num))
11278}
11279
11280/// Find the schema entry that a SELECT-list `Expr::Column` refers to.
11281/// Mirrors `resolve_column` in `eval.rs`, but returns a proper
11282/// `EngineError` so the projection-build path keeps `UnknownQualifier`
11283/// vs `ColumnNotFound` distinct.
11284/// PG's name for the physical row identity. It is reserved there — no table
11285/// can have a column called this — which is what lets `*` skip it by name.
11286pub(crate) const CTID_COLUMN: &str = "ctid";
11287
11288/// v7.39 (round 512) — PG's system columns, in the order they are appended.
11289/// All six are reserved names there, which is what lets `*` skip them and
11290/// lets a scan tell them from a user column without a flag.
11291pub(crate) const SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "xmax", "cmin", "cmax", "tableoid"];
11292
11293/// Is this name one of them?
11294pub(crate) fn is_system_column(name: &str) -> bool {
11295    SYSTEM_COLUMNS.iter().any(|s| name.eq_ignore_ascii_case(s))
11296}
11297
11298/// Where the scan's appended system columns begin, if this schema carries
11299/// them: the trailing six, named in order. A catalog view with a column of
11300/// its own called `xmin` does not match, which is the point.
11301fn system_column_tail_start(cols: &[ColumnSchema]) -> Option<usize> {
11302    let start = cols.len().checked_sub(SYSTEM_COLUMNS.len())?;
11303    cols[start..]
11304        .iter()
11305        .zip(SYSTEM_COLUMNS)
11306        .all(|(c, name)| c.name.eq_ignore_ascii_case(name))
11307        .then_some(start)
11308}
11309
11310/// v7.39 (round 540) — which positions `*` must skip.
11311///
11312/// The rule stays round 512's — the synthetic columns are the trailing
11313/// six of a relation's block, matched by POSITION so a genuine `xmin`
11314/// column is not lost — but a JOINED schema names its columns
11315/// `alias.column` and lays the peers out end to end, so a peer's six sit
11316/// in the MIDDLE of the whole list. Grouping by qualifier first puts the
11317/// "trailing six" test back on the block it was written for.
11318fn synthetic_system_positions(cols: &[ColumnSchema]) -> alloc::vec::Vec<bool> {
11319    let mut skip = alloc::vec![false; cols.len()];
11320    fn qualifier(n: &str) -> Option<&str> {
11321        n.rsplit_once('.').map(|(q, _)| q)
11322    }
11323    fn bare(n: &str) -> &str {
11324        n.rsplit('.').next().unwrap_or(n)
11325    }
11326    let mut i = 0;
11327    while i < cols.len() {
11328        let q = qualifier(&cols[i].name);
11329        let mut end = i;
11330        while end < cols.len() && qualifier(&cols[end].name) == q {
11331            end += 1;
11332        }
11333        if let Some(start) = (end - i)
11334            .checked_sub(SYSTEM_COLUMNS.len())
11335            .map(|off| i + off)
11336            && cols[start..end]
11337                .iter()
11338                .zip(SYSTEM_COLUMNS)
11339                .all(|(c, name)| bare(&c.name).eq_ignore_ascii_case(name))
11340        {
11341            for s in skip.iter_mut().take(end).skip(start) {
11342                *s = true;
11343            }
11344        }
11345        i = end;
11346    }
11347    skip
11348}
11349
11350/// v7.39 (round 511) — does this statement name `ctid` anywhere it would be
11351/// read? Only then is the column materialised.
11352pub(crate) fn expr_references_ctid(e: &Expr) -> bool {
11353    let mut found = false;
11354    crate::expr_analysis::visit_expr_columns_and_subqueries(
11355        e,
11356        &mut |c| {
11357            if is_system_column(&c.name) {
11358                found = true;
11359            }
11360        },
11361        &mut |_| {},
11362    );
11363    found
11364}
11365
11366fn references_ctid(stmt: &SelectStatement) -> bool {
11367    let in_expr = expr_references_ctid;
11368    stmt.items.iter().any(|i| match i {
11369        SelectItem::Expr { expr, .. } => in_expr(expr),
11370        _ => false,
11371    }) || stmt.where_.as_ref().is_some_and(in_expr)
11372        || stmt.order_by.iter().any(|o| in_expr(&o.expr))
11373        || stmt
11374            .group_by
11375            .as_ref()
11376            .is_some_and(|g| g.iter().any(in_expr))
11377        || stmt.having.as_ref().is_some_and(in_expr)
11378}
11379
11380/// v7.39 (round 961) — the whole-row schema for `SELECT t FROM t`, which
11381/// is a name the projection has to TYPE before any row exists.
11382///
11383/// Evaluation has answered this since round T9 (`resolve_column` builds a
11384/// `Value::Composite` of every column), but the typing side below had no
11385/// such branch and raised `column "t" does not exist` first — so the
11386/// feature was unreachable through a projection. Measured against PG18.4:
11387/// `SELECT wr FROM wr` answers `(7,z)` there and errored here.
11388///
11389/// The type is `Jsonb` + a composite marker, which is exactly how a
11390/// column DECLARED as a composite type is described (`ddl.rs`, round 56):
11391/// the value travels as a `Value::Composite` and renders in the canonical
11392/// `(7,z)` form. SPG has no catalog entry for a table's implicit row type,
11393/// so the marker names the alias and no rehydration keys off it — the
11394/// value arrives already built.
11395fn whole_row_projection_schema(alias: &str) -> ColumnSchema {
11396    let mut s = ColumnSchema::new(
11397        alloc::string::String::from(alias),
11398        spg_storage::DataType::Jsonb,
11399        true,
11400    );
11401    s.user_composite_type = Some(alloc::string::String::from(alias));
11402    s
11403}
11404
11405pub(crate) fn resolve_projection_column<'a>(
11406    c: &ColumnName,
11407    schema_cols: &'a [ColumnSchema],
11408    table_alias: &str,
11409) -> Result<Cow<'a, ColumnSchema>, EngineError> {
11410    if let Some(q) = &c.qualifier {
11411        let composite = alloc::format!("{q}.{name}", name = c.name);
11412        if let Some(s) = schema_cols.iter().find(|s| s.name == composite) {
11413            return Ok(Cow::Borrowed(s));
11414        }
11415        // Single-table case: the qualifier may equal the active alias —
11416        // then look for the bare column name.
11417        if q == table_alias
11418            && let Some(s) = schema_cols.iter().find(|s| s.name == c.name)
11419        {
11420            return Ok(Cow::Borrowed(s));
11421        }
11422        // For multi-table schemas the qualifier is unknown only if no
11423        // column bears the "<q>." prefix. For single-table, the alias
11424        // mismatch alone is enough.
11425        let prefix = alloc::format!("{q}.");
11426        let qualifier_known =
11427            q == table_alias || schema_cols.iter().any(|s| s.name.starts_with(&prefix));
11428        if !qualifier_known {
11429            return Err(EngineError::Eval(EvalError::UnknownQualifier {
11430                qualifier: q.clone(),
11431            }));
11432        }
11433        return Err(EngineError::Eval(EvalError::ColumnNotFound {
11434            name: c.name.clone(),
11435        }));
11436    }
11437    if let Some(s) = schema_cols.iter().find(|s| s.name == c.name) {
11438        return Ok(Cow::Borrowed(s));
11439    }
11440    let suffix = alloc::format!(".{name}", name = c.name);
11441    let mut matches = schema_cols.iter().filter(|s| s.name.ends_with(&suffix));
11442    let first = matches.next();
11443    let extra = matches.next();
11444    match (first, extra) {
11445        (Some(s), None) => Ok(Cow::Borrowed(s)),
11446        (Some(_), Some(_)) => Err(EngineError::Eval(EvalError::TypeMismatch {
11447            detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
11448        })),
11449        // The whole-row reference, checked LAST so a real column carrying
11450        // the alias's name still wins — the same precedence
11451        // `resolve_column` applies on the evaluation side.
11452        //
11453        // Two schema shapes reach here. A single-table (or subquery, or
11454        // CTE) scan carries its alias and bare column names, so the name
11455        // has to equal the alias. A JOIN's combined schema carries no
11456        // alias at all and qualifies every column `alias.col`, so the
11457        // alias is identified by the prefix instead — which is exactly
11458        // how `whole_row_composite` picks the fields out on the
11459        // evaluation side. Measured: `SELECT wr FROM wr JOIN jb ON …`
11460        // answers `(7,z)` on PG18.4 and errored here until this arm
11461        // covered the joined shape too.
11462        _ if !table_alias.is_empty() && c.name == table_alias => {
11463            Ok(Cow::Owned(whole_row_projection_schema(table_alias)))
11464        }
11465        _ if table_alias.is_empty() && {
11466            let prefix = alloc::format!("{name}.", name = c.name);
11467            schema_cols.iter().any(|s| s.name.starts_with(&prefix))
11468        } =>
11469        {
11470            Ok(Cow::Owned(whole_row_projection_schema(&c.name)))
11471        }
11472        _ => Err(EngineError::Eval(EvalError::ColumnNotFound {
11473            name: c.name.clone(),
11474        })),
11475    }
11476}
11477
11478/// v7.39 (round 135) — drop the synthetic `__grp_ord_*` columns injected by the
11479/// parser to carry per-branch GROUPING() masks into a grouping-set query's
11480/// ORDER BY. They must never reach the output. No-op unless such a column is
11481/// present, so the common path is untouched.
11482/// v7.39 (round 529) — the LIMIT / OFFSET that DISTINCT ON deferred.
11483///
11484/// PG limits what the dedup LEFT, not what fed it; SPG limited first, so
11485/// a `LIMIT 2` that should have answered two groups answered one.
11486fn apply_deferred_limit(
11487    rows: alloc::vec::Vec<Row<'static>>,
11488    deferred: &(
11489        Option<spg_sql::ast::LimitExpr>,
11490        Option<spg_sql::ast::LimitExpr>,
11491    ),
11492) -> alloc::vec::Vec<Row<'static>> {
11493    let count = |e: &Option<spg_sql::ast::LimitExpr>| match e {
11494        Some(spg_sql::ast::LimitExpr::Literal(n)) => Some(*n as usize),
11495        _ => None,
11496    };
11497    let mut rows = rows;
11498    if let Some(off) = count(&deferred.1) {
11499        rows = rows.split_off(off.min(rows.len()));
11500    }
11501    if let Some(lim) = count(&deferred.0) {
11502        rows.truncate(lim);
11503    }
11504    rows
11505}
11506
11507fn strip_synthetic_order_cols(result: QueryResult) -> QueryResult {
11508    let QueryResult::Rows { columns, rows } = result else {
11509        return result;
11510    };
11511    if !columns.iter().any(|c| c.name.starts_with("__grp_ord_")) {
11512        return QueryResult::Rows { columns, rows };
11513    }
11514    let keep: Vec<usize> = columns
11515        .iter()
11516        .enumerate()
11517        .filter(|(_, c)| !c.name.starts_with("__grp_ord_"))
11518        .map(|(i, _)| i)
11519        .collect();
11520    let new_cols: Vec<ColumnSchema> = keep.iter().map(|&i| columns[i].clone()).collect();
11521    let new_rows: Vec<Row<'static>> = rows
11522        .into_iter()
11523        .map(|r| Row::new(keep.iter().map(|&i| r.values[i].clone()).collect()))
11524        .collect();
11525    QueryResult::Rows {
11526        columns: new_cols,
11527        rows: new_rows,
11528    }
11529}
11530
11531/// v7.39 (round 487) — bind every projection item that is a bare column
11532/// reference to its position, once per query.
11533///
11534/// `#[inline(never)]` and out of line on purpose. Round 486 established
11535/// that adding code inside these scan bodies moves neighbouring hot
11536/// functions around under fat LTO: the first version of this had the loop
11537/// inline in `run_single_table_scan` and four aggregate shapes that never
11538/// touch that function — `full_agg`, `join_agg`, `group_500k`,
11539/// `filter_agg` — went up ~5 %, reproduced against the parent commit on
11540/// the same machine. Keeping it out of line kept them still.
11541#[inline(never)]
11542fn bind_direct_columns(
11543    projection: &[ProjectedItem],
11544    ctx: &eval::EvalContext<'_>,
11545) -> Vec<Option<usize>> {
11546    projection
11547        .iter()
11548        .map(|p| match &p.expr {
11549            Expr::Column(c) => eval::compile_column_pos(c, ctx).filter(|pos| {
11550                // Same exclusion `compile_into` makes: a composite column
11551                // has to be rehydrated from stored JSON, which is not a
11552                // cell read.
11553                ctx.columns
11554                    .get(*pos)
11555                    .is_none_or(|sc| sc.user_composite_type.is_none())
11556            }),
11557            _ => None,
11558        })
11559        .collect()
11560}
11561
11562/// v7.39 (round 505) — the name an un-aliased projected expression reports.
11563///
11564/// PG18 names a call for its function and everything else `?column?`;
11565/// measured with `\gdesc`. SPG used to print the parsed expression back
11566/// out for both dialects, so `SELECT upper(s)` reported `upper(s)` and
11567/// name-keyed row access found nothing under `upper`.
11568///
11569/// The MySQL half is NOT this rule and is deliberately left alone here:
11570/// MariaDB echoes the item's SOURCE TEXT verbatim (`a+b`, spacing and all),
11571/// which needs the parser to hand over spans the AST does not carry yet.
11572/// Until it does, a MySQL session keeps the printed form — closer to what
11573/// MariaDB answers than `?column?` would be.
11574pub(crate) fn default_output_name(expr: &Expr, mysql: bool) -> String {
11575    if mysql {
11576        return expr.to_string();
11577    }
11578    spg_sql::ast::figure_column_name(expr).unwrap_or_else(|| "?column?".to_string())
11579}
11580
11581pub(crate) fn build_projection(
11582    items: &[SelectItem],
11583    schema_cols: &[ColumnSchema],
11584    table_alias: &str,
11585    mysql: bool,
11586    cat: Option<&Catalog>,
11587) -> Result<Vec<ProjectedItem>, EngineError> {
11588    build_projection_hiding_tail(items, schema_cols, table_alias, mysql, 0, cat)
11589}
11590
11591/// v7.39 (round 592) — `build_projection` with the last `hidden_tail` columns
11592/// invisible to `*`.
11593///
11594/// The windowed-SELECT path appends a synthetic `__win_N` column per window
11595/// function so the rewritten projection can reference the computed values as
11596/// ordinary columns. `*` then expanded them too, and
11597/// `SELECT wr.*, row_number() OVER (ORDER BY id) FROM wr` came back with an
11598/// EXTRA column — the internal name's value, repeated. A wrong answer, and a
11599/// silent one: the row simply had one more field than the client asked for.
11600///
11601/// Hidden by POSITION rather than by name, for the reason round 512 recorded
11602/// about the system columns: a name test looks safe until a real column
11603/// happens to carry the name. These are appended last, so the count is what
11604/// identifies them.
11605pub(crate) fn build_projection_hiding_tail(
11606    items: &[SelectItem],
11607    schema_cols: &[ColumnSchema],
11608    table_alias: &str,
11609    mysql: bool,
11610    hidden_tail: usize,
11611    // v7.38.19 — the catalog, so a user-defined function's DECLARED
11612    // return type reaches the projection. Without it `describe_expr`
11613    // cannot type `f_sql()` and the column falls back to text, which is
11614    // what psql reads to decide alignment: `SELECT 7::bigint, f_sql()`
11615    // right-aligned one cell and left-aligned the other while both held
11616    // a bigint. Reported by sentori against 7.38.18 (their §2.2), who
11617    // also established that the EXECUTOR was never confused -- CTAS off
11618    // the same expression gives a bigint column, and arithmetic on it
11619    // works. Only the type travelling in the RowDescription was wrong.
11620    cat: Option<&Catalog>,
11621) -> Result<Vec<ProjectedItem>, EngineError> {
11622    let visible = schema_cols.len().saturating_sub(hidden_tail);
11623    // v7.39 (round 462) — a join's combined schema qualifies every column
11624    // `alias.col` so the deferred-join cell lookups resolve by composite
11625    // name. That is an internal convention, and `*` was handing it to the
11626    // client: PG18 answers `SELECT * FROM a JOIN b` with the BARE names
11627    // (`id, g, id, h` — duplicates and all), SPG answered `a.id, a.g,
11628    // b.id, b.h`, so name-keyed row access found nothing. Round 128 had
11629    // already learned this for `q.*`; plain `*` never got the same rule.
11630    //
11631    // The signal is the schema itself, not the call site: only a combined
11632    // join schema arrives with no table alias AND every column qualified.
11633    // A single-table schema carries its alias, an empty schema has nothing
11634    // to strip, and a synthetic schema's names carry no dot.
11635    let joined_schema = table_alias.is_empty()
11636        && !schema_cols.is_empty()
11637        && schema_cols.iter().all(|c| c.name.contains('.'));
11638    let bare_name = |name: &str| -> String {
11639        if !joined_schema {
11640            return name.to_string();
11641        }
11642        match name.split_once('.') {
11643            Some((_, rest)) if !rest.is_empty() => rest.to_string(),
11644            _ => name.to_string(),
11645        }
11646    };
11647    let mut out = Vec::new();
11648    for item in items {
11649        match item {
11650            SelectItem::Wildcard => {
11651                // v7.39 (round 511) — `*` never expands a system column, as
11652                // PG's does not. They join the schema only when the statement
11653                // asked for them, so this matters for the mixed shape
11654                // `SELECT *, ctid FROM t`.
11655                //
11656                // v7.39 (round 512) — by POSITION, not by name. Matching on
11657                // the name alone looked safe because PG reserves them, and it
11658                // is not: `pg_replication_slots` genuinely has a column called
11659                // `xmin`, and `SELECT * FROM pg_replication_slots` lost it.
11660                // Only the trailing six, in the order the scan appends them,
11661                // are the synthetic ones.
11662                let sys_skip = synthetic_system_positions(schema_cols);
11663                for (idx, col) in schema_cols.iter().enumerate() {
11664                    if sys_skip[idx] || idx >= visible {
11665                        continue;
11666                    }
11667                    out.push(ProjectedItem {
11668                        expr: Expr::Column(ColumnName {
11669                            qualifier: None,
11670                            name: col.name.clone(),
11671                        }),
11672                        output_name: bare_name(&col.name),
11673                        ty: col.ty,
11674                        nullable: col.nullable,
11675                        user_enum_type: col.user_enum_type.clone(),
11676                        mysql_fsp: col.mysql_fsp,
11677                        collation_name: col.collation_name.clone(),
11678                        fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
11679                        pads: crate::collate::pads_space(col.collation_name.as_deref()),
11680                    });
11681                }
11682            }
11683            // v7.39 (round 128) — `q.*` expands to every column belonging to
11684            // the qualifier `q`. Single-table schemas carry bare column names
11685            // reachable via `table_alias`; a join's combined schema carries
11686            // `alias.col` names, so a column belongs to `q` when its name has
11687            // the `q.` prefix. PG labels the expanded columns by their bare
11688            // name, so the `alias.` prefix is stripped from the output name.
11689            SelectItem::QualifiedWildcard(q) => {
11690                let prefix = alloc::format!("{q}.");
11691                let single_table = !table_alias.is_empty() && q == table_alias;
11692                let mut matched = 0usize;
11693                for col in &schema_cols[..visible] {
11694                    let belongs =
11695                        col.name.starts_with(&prefix) || (single_table && !col.name.contains('.'));
11696                    if !belongs {
11697                        continue;
11698                    }
11699                    matched += 1;
11700                    let output_name = col
11701                        .name
11702                        .strip_prefix(&prefix)
11703                        .unwrap_or(&col.name)
11704                        .to_string();
11705                    out.push(ProjectedItem {
11706                        expr: Expr::Column(ColumnName {
11707                            qualifier: None,
11708                            name: col.name.clone(),
11709                        }),
11710                        output_name,
11711                        ty: col.ty,
11712                        nullable: col.nullable,
11713                        user_enum_type: col.user_enum_type.clone(),
11714                        mysql_fsp: col.mysql_fsp,
11715                        collation_name: col.collation_name.clone(),
11716                        fold_exempt: matches!(col.collation, spg_storage::Collation::Binary),
11717                        pads: crate::collate::pads_space(col.collation_name.as_deref()),
11718                    });
11719                }
11720                if matched == 0 {
11721                    return Err(EngineError::Eval(EvalError::UnknownQualifier {
11722                        qualifier: q.clone(),
11723                    }));
11724                }
11725            }
11726            SelectItem::Expr { expr, alias } => {
11727                // Plain column ref keeps full schema info (real type +
11728                // nullability). For compound expressions try the
11729                // describe-side function-return-type table first
11730                // (e.g. `SELECT now()` → Timestamptz, `SELECT
11731                // concat(…)` → Text). Falls back to nullable Text
11732                // for shapes the describe path can't resolve.
11733                if let Expr::Column(c) = expr {
11734                    let sch = resolve_projection_column(c, schema_cols, table_alias)?;
11735                    let output_name = alias.clone().unwrap_or_else(|| c.name.clone());
11736                    out.push(ProjectedItem {
11737                        expr: expr.clone(),
11738                        output_name,
11739                        ty: sch.ty,
11740                        nullable: sch.nullable,
11741                        // v7.39 (read01 round 54) — a bare enum column keeps
11742                        // its enum identity through the projection.
11743                        user_enum_type: sch.user_enum_type.clone(),
11744                        mysql_fsp: sch.mysql_fsp,
11745                        collation_name: sch.collation_name.clone(),
11746                        // v7.38.13 — and its byte-wise-ness. This is the
11747                        // site `SELECT DISTINCT t FROM t` arrives at.
11748                        fold_exempt: matches!(sch.collation, spg_storage::Collation::Binary),
11749                        pads: crate::collate::pads_space(sch.collation_name.as_deref()),
11750                    });
11751                } else if let Some(shape) = describe::describe_expr_in(expr, schema_cols, cat) {
11752                    let output_name = alias
11753                        .clone()
11754                        .unwrap_or_else(|| default_output_name(expr, mysql));
11755                    out.push(ProjectedItem {
11756                        expr: expr.clone(),
11757                        // v7.38.18 — a projected EXPRESSION has no column collation
11758                        // to read, so it takes the session default, which is MySQL
11759                        // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
11760                        pads: false,
11761                        output_name,
11762                        ty: shape.ty,
11763                        // v7.39 (round 258) — a projected EXPRESSION keeps its
11764                        // enum identity too, not just a bare column. `FROM
11765                        // (VALUES ('happy'::mood), …) t(m)` lowers to constant
11766                        // SELECTs, so the derived column arrived here as a cast
11767                        // and lost the enum — making the outer ORDER BY / min /
11768                        // max / array_agg sort by the label's TEXT.
11769                        nullable: shape.nullable,
11770                        user_enum_type: None,
11771                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
11772                        // A bare column reference keeps its collation; any
11773                        // other expression produces a new value and has none.
11774                        collation_name: match expr {
11775                            Expr::Column(c) => schema_cols
11776                                .iter()
11777                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11778                                .and_then(|sc| sc.collation_name.clone()),
11779                            _ => None,
11780                        },
11781                        fold_exempt: match expr {
11782                            Expr::Column(c) => schema_cols
11783                                .iter()
11784                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11785                                .is_some_and(|sc| {
11786                                    matches!(sc.collation, spg_storage::Collation::Binary)
11787                                }),
11788                            // Not a column: no declared collation to honour,
11789                            // so the session default applies and it folds.
11790                            _ => false,
11791                        },
11792                    });
11793                } else {
11794                    let output_name = alias
11795                        .clone()
11796                        .unwrap_or_else(|| default_output_name(expr, mysql));
11797                    out.push(ProjectedItem {
11798                        expr: expr.clone(),
11799                        // v7.38.18 — a projected EXPRESSION has no column collation
11800                        // to read, so it takes the session default, which is MySQL
11801                        // 8.0's `utf8mb4_0900_ai_ci`: NO PAD.
11802                        pads: false,
11803                        output_name,
11804                        // A user ENUM has no DataType of its own, so
11805                        // `describe_expr` cannot type `'ok'::mood` and the
11806                        // item lands HERE, defaulting to text — which is why
11807                        // pg_typeof answered `text` and a derived table sorted
11808                        // enum values by their label.
11809                        ty: DataType::Text,
11810                        nullable: true,
11811                        user_enum_type: crate::eval::expr_enum_type_name_pub(expr, schema_cols)
11812                            .map(alloc::string::String::from),
11813                        mysql_fsp: crate::eval::expr_mysql_fsp(expr, schema_cols),
11814                        collation_name: match expr {
11815                            Expr::Column(c) => schema_cols
11816                                .iter()
11817                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11818                                .and_then(|sc| sc.collation_name.clone()),
11819                            _ => None,
11820                        },
11821                        fold_exempt: match expr {
11822                            Expr::Column(c) => schema_cols
11823                                .iter()
11824                                .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
11825                                .is_some_and(|sc| {
11826                                    matches!(sc.collation, spg_storage::Collation::Binary)
11827                                }),
11828                            // Not a column: no declared collation to honour,
11829                            // so the session default applies and it folds.
11830                            _ => false,
11831                        },
11832                    });
11833                }
11834            }
11835        }
11836    }
11837    Ok(out)
11838}
11839
11840// ---- v4.12 window-function helpers ----
11841// The (partition-key, order-key, original-index) tuple shape used
11842// across these helpers is intrinsic to the planner. Factoring it
11843// into a typedef adds indirection without making the code clearer,
11844// so several lints are allowed inline on the affected functions
11845// rather than module-wide.
11846
11847/// v4.22: pick more specific column types from observed rows when
11848/// the projection builder defaulted to Text (the v1.x behavior for
11849/// non-column expressions). Lets `WITH t(n) AS (SELECT 1 ...)`
11850/// land an Int column in the CTE storage table rather than failing
11851/// the insert with "expected TEXT, got INT".
11852pub(crate) fn infer_column_types(
11853    columns: &[ColumnSchema],
11854    rows: &[Row<'static>],
11855) -> Vec<ColumnSchema> {
11856    let mut out = columns.to_vec();
11857    for (col_idx, col) in out.iter_mut().enumerate() {
11858        if col.ty != DataType::Text {
11859            continue;
11860        }
11861        let mut inferred: Option<DataType> = None;
11862        let mut all_null = true;
11863        for row in rows {
11864            let Some(v) = row.values.get(col_idx) else {
11865                continue;
11866            };
11867            let ty = match v {
11868                Value::Null => continue,
11869                Value::SmallInt(_) => DataType::SmallInt,
11870                Value::Int(_) => DataType::Int,
11871                Value::BigInt(_) => DataType::BigInt,
11872                Value::Float(_) => DataType::Float,
11873                Value::Bool(_) => DataType::Bool,
11874                Value::Vector(_) => DataType::Vector {
11875                    dim: 0,
11876                    encoding: VecEncoding::F32,
11877                },
11878                // v7.38 (read01 U16) — carry array values through with an
11879                // array type so a recursive CTE that projects an array
11880                // (e.g. a SEARCH/CYCLE ord / path column) types the working
11881                // column as an array, not Text.
11882                Value::TextArray(_) => DataType::TextArray,
11883                Value::IntArray(_) => DataType::IntArray,
11884                Value::BigIntArray(_) => DataType::BigIntArray,
11885                Value::SmallIntArray(_) => DataType::SmallIntArray,
11886                Value::FloatArray(_) => DataType::FloatArray,
11887                Value::BoolArray(_) => DataType::BoolArray,
11888                // v7.39 (GUC knife 2) — an interval projection describes
11889                // as INTERVAL (typed drivers read the RowDescription OID).
11890                Value::Interval { .. } => DataType::Interval,
11891                _ => DataType::Text,
11892            };
11893            all_null = false;
11894            inferred = Some(match inferred {
11895                None => ty,
11896                Some(prev) if prev == ty => prev,
11897                Some(_) => DataType::Text,
11898            });
11899        }
11900        if let Some(t) = inferred {
11901            col.ty = t;
11902            col.nullable = true;
11903        } else if all_null {
11904            col.nullable = true;
11905        }
11906    }
11907    out
11908}
11909
11910/// Numeric widening rank for UNION type resolution (higher = wider).
11911fn numeric_rank(t: DataType) -> Option<u8> {
11912    match t {
11913        DataType::SmallInt => Some(1),
11914        DataType::Int => Some(2),
11915        DataType::BigInt => Some(3),
11916        DataType::Numeric { .. } => Some(4),
11917        DataType::Float => Some(5),
11918        _ => None,
11919    }
11920}
11921
11922/// Resolve the common result type for a UNION / VALUES column from the
11923/// set of concrete (non-NULL) branch types, following the safe subset
11924/// of PG's type resolution:
11925///   * all-numeric  → the widest numeric (int ∪ bigint → bigint, … ∪
11926///     numeric → numeric, … ∪ float → float);
11927///   * DATE ∪ TIMESTAMP → TIMESTAMP;
11928///   * exactly one concrete non-TEXT type mixed with TEXT literals →
11929///     that concrete type (the TEXT cells get parsed into it).
11930/// Returns `None` for anything ambiguous, so the caller leaves the
11931/// column untouched rather than risk a wrong or failing coercion.
11932fn resolve_union_common_type(types: &[DataType]) -> Option<DataType> {
11933    // NB: types are collected from RUNTIME values, which are coarser
11934    // than the schema (e.g. a timestamptz cell is Value::Timestamp), so
11935    // a single-concrete-type fast path must NOT overwrite the column
11936    // type — it would downgrade tstz to ts. NULL-only unification (PG:
11937    // `VALUES (NULL),(1.5)` types the column numeric even on the NULL
11938    // row's pg_typeof) needs schema-level resolution — recorded, not
11939    // attempted here.
11940    if types.len() < 2 {
11941        return None;
11942    }
11943    if types.iter().all(|t| numeric_rank(*t).is_some()) {
11944        return types
11945            .iter()
11946            .max_by_key(|t| numeric_rank(**t).unwrap_or(0))
11947            .copied();
11948    }
11949    let non_text: Vec<&DataType> = types
11950        .iter()
11951        .filter(|t| !matches!(t, DataType::Text))
11952        .collect();
11953    // v7.38 (T-tstz Phase 1) — temporal common type, per PG18.4: if any branch
11954    // is timestamptz the result is timestamptz (tstz ∪ ts, tstz ∪ date), else
11955    // if any is timestamp the result is timestamp (ts ∪ date). All values are
11956    // the same UTC-micros instant, so widening date/ts to tstz is lossless.
11957    if non_text.iter().all(|t| {
11958        matches!(
11959            t,
11960            DataType::Date | DataType::Timestamp | DataType::Timestamptz
11961        )
11962    }) && non_text
11963        .iter()
11964        .any(|t| matches!(t, DataType::Timestamp | DataType::Timestamptz))
11965    {
11966        if non_text.iter().any(|t| matches!(t, DataType::Timestamptz)) {
11967            return Some(DataType::Timestamptz);
11968        }
11969        return Some(DataType::Timestamp);
11970    }
11971    // A single concrete non-TEXT type mixed with TEXT literals.
11972    if non_text.len() == 1 {
11973        return Some(*non_text[0]);
11974    }
11975    // v7.37.16 — SEVERAL concrete types mixed with TEXT literals
11976    // (`VALUES ('NaN'::float8),(1.0),('NaN')` → float8 ∪ numeric ∪
11977    // text): resolve the concrete set first (PG treats the unknown-
11978    // typed string literals as castable to whatever the knowns
11979    // resolve to), then the TEXT cells parse into that target — the
11980    // caller's coercion dry-run still abandons the column if any
11981    // literal doesn't parse.
11982    if !non_text.is_empty() && non_text.len() < types.len() {
11983        let concrete: Vec<DataType> = non_text.iter().map(|t| **t).collect();
11984        return resolve_union_common_type(&concrete);
11985    }
11986    None
11987}
11988
11989/// Coerce every cell of a UNION / VALUES result column to one common
11990/// type (see [`resolve_union_common_type`]). Conservative: a column
11991/// whose branches already agree, or whose types don't resolve, or where
11992/// any cell fails to coerce, is left exactly as it was — this never
11993/// turns a previously-working query into an error.
11994fn unify_union_columns(columns: &mut [ColumnSchema], rows: &mut [Row<'static>]) {
11995    for col_idx in 0..columns.len() {
11996        let mut seen: Vec<DataType> = Vec::new();
11997        for row in rows.iter() {
11998            if let Some(dt) = row.values.get(col_idx).and_then(Value::data_type) {
11999                if !seen.contains(&dt) {
12000                    seen.push(dt);
12001                }
12002            }
12003        }
12004        // v7.37.16 — a single concrete runtime type under a TEXT-typed
12005        // column means the column type came off a NULL (or unknown-text)
12006        // branch: NULL literals describe as TEXT (`L::Null → Text`), so
12007        // `VALUES (NULL),(1.5)` left the column "text" while every
12008        // non-NULL cell is numeric. Adopt the concrete type — schema
12009        // only, no cell changes. tstz-safe by construction: a real
12010        // timestamptz column's schema type is Timestamptz, not Text, so
12011        // the coarser runtime type (Value::Timestamp) can't downgrade it
12012        // through this arm; and a real text column's non-NULL cells are
12013        // Text, which keeps seen == [Text] and skips it.
12014        if seen.len() == 1
12015            && matches!(columns[col_idx].ty, DataType::Text)
12016            && !matches!(seen[0], DataType::Text)
12017        {
12018            columns[col_idx].ty = seen[0];
12019            continue;
12020        }
12021        let Some(target) = resolve_union_common_type(&seen) else {
12022            continue;
12023        };
12024        // v7.38 (read01) — an unconstrained NUMERIC result column keeps each
12025        // value's own scale in PG (`VALUES (1.0),(1.00)` renders `1.0` / `1.00`,
12026        // not `1.00` / `1.00`). So when the common type is NUMERIC, leave an
12027        // existing numeric cell untouched and only promote integers (to scale 0)
12028        // rather than rescaling everything to the widest scale.
12029        let scale_preserving_numeric = matches!(target, DataType::Numeric { .. });
12030        // Dry-run the coercion; abandon the whole column if any fails.
12031        let mut coerced: Vec<Option<Value<'static>>> = Vec::with_capacity(rows.len());
12032        let mut ok = true;
12033        for row in rows.iter() {
12034            match row.values.get(col_idx) {
12035                Some(Value::Numeric { .. }) if scale_preserving_numeric => {
12036                    coerced.push(Some(row.values[col_idx].clone()));
12037                }
12038                Some(v) => {
12039                    let cell_target = if scale_preserving_numeric {
12040                        DataType::Numeric {
12041                            precision: 0,
12042                            scale: 0,
12043                        }
12044                    } else {
12045                        target
12046                    };
12047                    match crate::conversions::coerce_value(
12048                        v.clone(),
12049                        cell_target,
12050                        &columns[col_idx].name,
12051                        col_idx,
12052                    ) {
12053                        Ok(cv) => coerced.push(Some(cv)),
12054                        Err(_) => {
12055                            ok = false;
12056                            break;
12057                        }
12058                    }
12059                }
12060                None => coerced.push(None),
12061            }
12062        }
12063        if !ok {
12064            continue;
12065        }
12066        for (row, cv) in rows.iter_mut().zip(coerced) {
12067            if let (Some(slot), Some(nv)) = (row.values.get_mut(col_idx), cv) {
12068                *slot = nv;
12069            }
12070        }
12071        columns[col_idx].ty = target;
12072    }
12073}
12074
12075/// v4.22: encode a Row to a comparable byte key for UNION-DISTINCT
12076/// dedup inside the recursive iteration. Crude but deterministic
12077/// — Debug prints embed type discriminants so NULL ≠ "" ≠ 0.
12078fn encode_row_key(row: &Row<'static>) -> Vec<u8> {
12079    let mut out = Vec::new();
12080    for v in &row.values {
12081        // v7.38 (read01) — UNION / DISTINCT dedup must treat numerically-equal
12082        // exact values as one, regardless of type or scale (`1 = 1.0 = 1.00`),
12083        // like PG (and like GROUP BY, which already normalizes). The old
12084        // `{v:?}` key made `Numeric{10,1}` differ from `Numeric{100,2}`. Encode
12085        // the exact-decimal family through one scale-stripped canonical form.
12086        match v {
12087            Value::SmallInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12088            Value::Int(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12089            Value::BigInt(n) => encode_numeric_key(&mut out, i128::from(*n), 0),
12090            Value::Numeric { scaled, scale, .. } => encode_numeric_key(&mut out, *scaled, *scale),
12091            other => {
12092                let s = alloc::format!("{other:?}|");
12093                out.extend_from_slice(s.as_bytes());
12094            }
12095        }
12096    }
12097    out
12098}
12099
12100/// Append a scale-independent canonical key for an exact-decimal value: strip
12101/// trailing fractional zeros so `1`, `1.0`, `1.00` all key the same. The `\x01`
12102/// tag keeps a numeric key from colliding with a text value's `{v:?}` form.
12103fn encode_numeric_key(out: &mut Vec<u8>, mut scaled: i128, mut scale: u16) {
12104    while scale > 0 && scaled % 10 == 0 {
12105        scaled /= 10;
12106        scale -= 1;
12107    }
12108    let s = alloc::format!("\u{1}{scaled}e-{scale}|");
12109    out.extend_from_slice(s.as_bytes());
12110}
12111
12112/// Multi-arg `unnest(a, b, …)` — evaluate each array argument
12113/// (uncorrelated; outer refs were substituted upstream), then zip
12114/// them in parallel, NULL-padding shorter arrays to the longest
12115/// (PG's ROWS FROM shorthand). Shared by the primary-position
12116/// executor and the join-position materialiser, which both detect
12117/// the parser's `__unnest_zip` marker call.
12118pub(crate) fn unnest_zip_rows(
12119    args: &[Expr],
12120) -> Result<(alloc::vec::Vec<DataType>, alloc::vec::Vec<Row<'static>>), EngineError> {
12121    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12122    let ctx = EvalContext::new(&empty_schema, None);
12123    let dummy_row = Row::new(alloc::vec::Vec::new());
12124    let mut dtypes: alloc::vec::Vec<DataType> = alloc::vec::Vec::with_capacity(args.len());
12125    let mut columns: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> =
12126        alloc::vec::Vec::with_capacity(args.len());
12127    for a in args {
12128        let v = eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?;
12129        let (dt, items): (DataType, alloc::vec::Vec<Value<'static>>) = match v {
12130            Value::Null => (DataType::Text, alloc::vec::Vec::new()),
12131            Value::TextArray(xs) => (
12132                DataType::Text,
12133                xs.into_iter()
12134                    .map(|x| x.map(Value::text).unwrap_or(Value::Null))
12135                    .collect(),
12136            ),
12137            Value::IntArray(xs) => (
12138                DataType::Int,
12139                xs.into_iter()
12140                    .map(|x| x.map(Value::Int).unwrap_or(Value::Null))
12141                    .collect(),
12142            ),
12143            Value::BigIntArray(xs) => (
12144                DataType::BigInt,
12145                xs.into_iter()
12146                    .map(|x| x.map(Value::BigInt).unwrap_or(Value::Null))
12147                    .collect(),
12148            ),
12149            other => {
12150                return Err(EngineError::Unsupported(alloc::format!(
12151                    "unnest() expects array arguments, got {}",
12152                    crate::conversions::pg_type_name_for_error_opt(other.data_type())
12153                )));
12154            }
12155        };
12156        dtypes.push(dt);
12157        columns.push(items);
12158    }
12159    let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
12160    let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(max_len);
12161    for i in 0..max_len {
12162        let vals: alloc::vec::Vec<Value<'static>> = columns
12163            .iter()
12164            .map(|c| c.get(i).cloned().unwrap_or(Value::Null))
12165            .collect();
12166        rows.push(Row::new(vals));
12167    }
12168    Ok((dtypes, rows))
12169}
12170
12171/// Detect the parser's multi-arg unnest marker on an unnest_expr.
12172pub(crate) fn unnest_zip_args(expr: &Expr) -> Option<&[Expr]> {
12173    match expr {
12174        Expr::FunctionCall { name, args } if name == "__unnest_zip" => Some(args.as_slice()),
12175        _ => None,
12176    }
12177}
12178
12179/// Evaluate generate_series arguments (uncorrelated — outer refs
12180/// were substituted upstream where applicable) and build the row
12181/// stream. Dispatches on the start value's shape and rejects
12182/// mixed-shape calls early (e.g. start = timestamp, stop =
12183/// integer) so the caller gets a clean error rather than a panic.
12184/// Shared by the primary-position executor and the join-position
12185/// materialiser.
12186pub(crate) fn generate_series_rows(
12187    args: &[Expr],
12188    cancel: &CancelToken<'_>,
12189) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12190    let empty_schema: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12191    let ctx = EvalContext::new(&empty_schema, None);
12192    let dummy_row = Row::new(alloc::vec::Vec::new());
12193    let mut arg_values: alloc::vec::Vec<Value<'static>> =
12194        alloc::vec::Vec::with_capacity(args.len());
12195    for a in args {
12196        arg_values.push(eval::eval_expr(a, &dummy_row, &ctx).map_err(EngineError::Eval)?);
12197    }
12198    generate_series_from_values(arg_values, args, cancel)
12199}
12200
12201/// v7.39 (read01 round 96) — the value-producing core of `generate_series`,
12202/// split out so the SELECT-list SRF path (`top_level_srf_output`) shares the
12203/// full integer / numeric / timestamp overload set with the FROM-clause path.
12204/// Before this split the target-list arm reimplemented only the integer case,
12205/// so `SELECT generate_series(1,2), generate_series(ts, ts, interval)` yielded
12206/// NULL for the timestamp column instead of the series. `arg_values` are the
12207/// already-evaluated arguments; `args` is kept only for the timestamptz-vs-
12208/// timestamp type resolution (it inspects the argument expressions' types).
12209pub(crate) fn generate_series_from_values(
12210    mut arg_values: alloc::vec::Vec<Value<'static>>,
12211    args: &[Expr],
12212    cancel: &CancelToken<'_>,
12213) -> Result<(DataType, alloc::vec::Vec<Row<'static>>), EngineError> {
12214    // PG: a NULL bound or step yields zero rows (also keeps the
12215    // NULL-padded lateral probe alive — schema without data).
12216    if arg_values.iter().any(|v| matches!(v, Value::Null)) {
12217        return Ok((DataType::BigInt, alloc::vec::Vec::new()));
12218    }
12219    // PG resolves `generate_series(date, date, interval)` to the
12220    // timestamp/timestamptz overload by implicitly casting each date
12221    // bound up to a timestamp at midnight (verified vs live PG18.4:
12222    // date args yield rows anchored at 00:00:00). SPG's TZ-naive
12223    // timestamp model renders the same instants, so fold any Date
12224    // bound to its midnight Timestamp (canonical `days *
12225    // 86_400_000_000`, matching cast.rs `cast_to_timestamp`) before
12226    // the shape match so the existing timestamp arm drives the walk.
12227    // v7.39 (read01 round 76) — WHICH timestamp overload PG picks matters:
12228    // `generate_series(date, date, interval)` has no date overload, and among
12229    // the two candidates PG prefers the timestamptz one (timestamptz is the
12230    // preferred type of the datetime category), so the column comes back
12231    // `timestamp with time zone` — the rows render with a `+00` offset. A
12232    // timestamptz bound obviously lands there too. Only genuinely
12233    // timestamp-typed bounds keep the TZ-naive result type.
12234    let empty_cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
12235    let tz = arg_values.iter().any(|v| matches!(v, Value::Date(_)))
12236        || args.iter().any(|a| {
12237            crate::describe::describe_expr(a, &empty_cols)
12238                .is_some_and(|s| matches!(s.ty, DataType::Timestamptz))
12239        });
12240    for v in &mut arg_values {
12241        if let Value::Date(d) = *v {
12242            *v = Value::Timestamp(crate::conversions::date_days_to_micros(d));
12243        }
12244    }
12245    match arg_values.as_slice() {
12246        [Value::Timestamp(start), Value::Timestamp(stop), step] => {
12247            let interval_step = match step {
12248                Value::Interval { .. } => step.clone(),
12249                // v7.38 (read01) — PG resolves an unknown-type string step
12250                // (`generate_series(date, date, '2 days')`) to INTERVAL; accept
12251                // a bare text step by parsing it the same way `::interval` does.
12252                Value::Text(s) => crate::conversions::coerce_value(
12253                    Value::text(s.as_ref()),
12254                    DataType::Interval,
12255                    "",
12256                    0,
12257                )
12258                .map_err(|_| {
12259                    EngineError::Unsupported(alloc::format!(
12260                        "generate_series(timestamp, timestamp, …): \
12261                         could not parse step {s:?} as INTERVAL"
12262                    ))
12263                })?,
12264                other => {
12265                    return Err(EngineError::Unsupported(alloc::format!(
12266                        "generate_series(timestamp, timestamp, …): \
12267                         step must be INTERVAL, got {}",
12268                        crate::conversions::pg_type_name_for_error_opt(other.data_type())
12269                    )));
12270                }
12271            };
12272            let rows = generate_series_timestamps(*start, *stop, interval_step, cancel)?;
12273            Ok((
12274                if tz {
12275                    DataType::Timestamptz
12276                } else {
12277                    DataType::Timestamp
12278                },
12279                rows,
12280            ))
12281        }
12282        [start, stop, step]
12283            if value_is_integer(start) && value_is_integer(stop) && value_is_integer(step) =>
12284        {
12285            let s = value_to_i64(start);
12286            let e = value_to_i64(stop);
12287            let st = value_to_i64(step);
12288            // PG types the series by the argument type: int4 args → int4
12289            // elements, int8 (bigint) args → int8. Any BigInt operand widens.
12290            let wide = value_is_bigint(start) || value_is_bigint(stop) || value_is_bigint(step);
12291            let rows = generate_series_integers(s, e, st, wide, cancel)?;
12292            Ok((
12293                if wide {
12294                    DataType::BigInt
12295                } else {
12296                    DataType::Int
12297                },
12298                rows,
12299            ))
12300        }
12301        [start, stop] if value_is_integer(start) && value_is_integer(stop) => {
12302            let s = value_to_i64(start);
12303            let e = value_to_i64(stop);
12304            let wide = value_is_bigint(start) || value_is_bigint(stop);
12305            let rows = generate_series_integers(s, e, 1, wide, cancel)?;
12306            Ok((
12307                if wide {
12308                    DataType::BigInt
12309                } else {
12310                    DataType::Int
12311                },
12312                rows,
12313            ))
12314        }
12315        // v7.39 (read01 numeric.c) — the NUMERIC overload. PG walks the
12316        // series in exact numeric arithmetic; NaN / infinity bounds and a
12317        // zero step get dedicated wordings, and a mixed int/numeric call
12318        // resolves here via the implicit int→numeric cast.
12319        [_, _] | [_, _, _]
12320            if arg_values
12321                .iter()
12322                .any(|v| matches!(v, Value::Numeric { .. } | Value::NumericBig(_)))
12323                && arg_values.iter().all(|v| {
12324                    matches!(v, Value::Numeric { .. } | Value::NumericBig(_)) || value_is_integer(v)
12325                }) =>
12326        {
12327            use spg_storage::NumericKind as K;
12328            let words: [(&str, &str); 3] = [
12329                (
12330                    "start value cannot be NaN",
12331                    "start value cannot be infinity",
12332                ),
12333                ("stop value cannot be NaN", "stop value cannot be infinity"),
12334                ("step size cannot be NaN", "step size cannot be infinity"),
12335            ];
12336            for (i, v) in arg_values.iter().enumerate() {
12337                if let Value::Numeric { kind, .. } = v {
12338                    if *kind != K::Finite {
12339                        let (nan_w, inf_w) = words[i];
12340                        return Err(EngineError::Unsupported(
12341                            if *kind == K::NaN { nan_w } else { inf_w }.into(),
12342                        ));
12343                    }
12344                }
12345            }
12346            let big =
12347                |v: &Value<'_>| eval::binop::value_to_bignum(v).expect("finite numeric or integer");
12348            let start = big(&arg_values[0]);
12349            let stop = big(&arg_values[1]);
12350            let step = if arg_values.len() == 3 {
12351                big(&arg_values[2])
12352            } else {
12353                spg_storage::bignum::BigNumeric::from_i128(1, 0)
12354            };
12355            if step.is_zero() {
12356                return Err(EngineError::Unsupported(
12357                    "step size cannot equal zero".into(),
12358                ));
12359            }
12360            let descending = step.parts().0;
12361            let mut rows = alloc::vec::Vec::new();
12362            let mut cur = start;
12363            const MAX_ROWS: usize = 10_000_000;
12364            loop {
12365                cancel.check()?;
12366                let c = cur.cmp(&stop);
12367                if descending {
12368                    if c == core::cmp::Ordering::Less {
12369                        break;
12370                    }
12371                } else if c == core::cmp::Ordering::Greater {
12372                    break;
12373                }
12374                if rows.len() >= MAX_ROWS {
12375                    return Err(EngineError::Unsupported(alloc::format!(
12376                        "generate_series() result exceeds {MAX_ROWS} rows"
12377                    )));
12378                }
12379                rows.push(Row::new(alloc::vec![eval::binop::bignum_to_value(
12380                    cur.clone()
12381                )]));
12382                cur = cur.add(&step);
12383            }
12384            Ok((
12385                DataType::Numeric {
12386                    precision: 0,
12387                    scale: 0,
12388                },
12389                rows,
12390            ))
12391        }
12392        _ => Err(EngineError::Unsupported(alloc::format!(
12393            "generate_series(): v7.17 supports integer or (timestamp, timestamp, interval) \
12394             argument shapes; got {}",
12395            arg_values
12396                .iter()
12397                .map(|v| crate::conversions::pg_type_name_for_error_opt(v.data_type()))
12398                .collect::<alloc::vec::Vec<_>>()
12399                .join(", ")
12400        ))),
12401    }
12402}
12403
12404/// v7.17.0 Phase 3.10 — integer-mode generate_series materialiser.
12405/// Step direction follows the sign: positive step iterates upward
12406/// (stops when current > stop); negative iterates downward; zero
12407/// errors. Caller-facing row stream is `BigInt`-typed so a single
12408/// projection schema covers SmallInt / Int / BigInt callers.
12409fn generate_series_integers(
12410    start: i64,
12411    stop: i64,
12412    step: i64,
12413    wide: bool,
12414    cancel: &CancelToken<'_>,
12415) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
12416    if step == 0 {
12417        return Err(EngineError::Unsupported(
12418            "step size cannot equal zero".into(),
12419        ));
12420    }
12421    let mut out = alloc::vec::Vec::new();
12422    let mut cur = start;
12423    // Hard cap to keep a runaway call from eating all memory. PG
12424    // has no such cap but does honour query timeout; SPG's cancel
12425    // token will fire too — this is a defense-in-depth backstop.
12426    const MAX_ROWS: usize = 10_000_000;
12427    loop {
12428        cancel.check()?;
12429        if step > 0 && cur > stop {
12430            break;
12431        }
12432        if step < 0 && cur < stop {
12433            break;
12434        }
12435        out.push(Row::new(alloc::vec![if wide {
12436            Value::BigInt(cur)
12437        } else {
12438            Value::Int(cur as i32)
12439        }]));
12440        if out.len() > MAX_ROWS {
12441            return Err(EngineError::Unsupported(alloc::format!(
12442                "generate_series(): exceeded {MAX_ROWS} rows; \
12443                 narrow start/stop or use a larger step"
12444            )));
12445        }
12446        cur = match cur.checked_add(step) {
12447            Some(n) => n,
12448            None => break,
12449        };
12450    }
12451    Ok(out)
12452}
12453
12454/// v7.17.0 Phase 3.10 — timestamp-mode generate_series. step is a
12455/// `Value::Interval { months, micros }` per the caller's guard;
12456/// each iteration adds the interval via `apply_binary_interval`
12457/// so month-shifting handles short-month rollover (PG semantics).
12458fn generate_series_timestamps(
12459    start: i64,
12460    stop: i64,
12461    step: Value,
12462    cancel: &CancelToken<'_>,
12463) -> Result<alloc::vec::Vec<Row<'static>>, EngineError> {
12464    let (months, days, micros) = match &step {
12465        Value::Interval {
12466            months,
12467            days,
12468            micros,
12469            kind,
12470        } => (*months, *days, *micros),
12471        _ => unreachable!("caller guards step.is_interval"),
12472    };
12473    if months == 0 && days == 0 && micros == 0 {
12474        return Err(EngineError::Unsupported(
12475            "generate_series(): INTERVAL step cannot be zero".into(),
12476        ));
12477    }
12478    let ascending = months > 0 || days > 0 || micros > 0;
12479    let mut out = alloc::vec::Vec::new();
12480    let mut cur = Value::Timestamp(start);
12481    const MAX_ROWS: usize = 10_000_000;
12482    loop {
12483        cancel.check()?;
12484        let cur_t = match cur {
12485            Value::Timestamp(t) => t,
12486            _ => unreachable!("loop invariant: cur is Timestamp"),
12487        };
12488        if ascending && cur_t > stop {
12489            break;
12490        }
12491        if !ascending && cur_t < stop {
12492            break;
12493        }
12494        out.push(Row::new(alloc::vec![Value::Timestamp(cur_t)]));
12495        if out.len() > MAX_ROWS {
12496            return Err(EngineError::Unsupported(alloc::format!(
12497                "generate_series(): exceeded {MAX_ROWS} rows; \
12498                 narrow start/stop or use a larger step"
12499            )));
12500        }
12501        let next = eval::apply_binary_interval(
12502            spg_sql::ast::BinOp::Add,
12503            &cur,
12504            &Value::Interval {
12505                months,
12506                days,
12507                micros,
12508                kind: spg_storage::IntervalKind::Finite,
12509            },
12510        )
12511        .map_err(EngineError::Eval)?;
12512        cur = match next {
12513            Some(v) => v,
12514            None => break,
12515        };
12516    }
12517    Ok(out)
12518}
12519
12520/// v7.17.0 Phase 3.P0-49 — PG-canonical: `FETCH FIRST <n> ROWS
12521/// WITH TIES` requires an `ORDER BY`. Without one, there's no
12522/// way to identify "ties" deterministically, so PG errors at
12523/// plan time. SPG mirrors that surface so the same DDL / app
12524/// behaviour holds on cutover.
12525fn check_with_ties_requires_order_by(stmt: &SelectStatement) -> Result<(), EngineError> {
12526    if stmt.limit_with_ties && stmt.order_by.is_empty() {
12527        return Err(EngineError::Unsupported(alloc::string::String::from(
12528            "WITH TIES cannot be specified without ORDER BY clause",
12529        )));
12530    }
12531    Ok(())
12532}
12533
12534/// v7.19 P5 — true iff `expr` is `unnest(arg)` at the top level
12535/// (case-insensitive). Used by `exec_select_cancel`'s
12536/// projection loop to detect Set-Returning-Function rows that
12537/// need per-row expansion. Only the top-level call counts —
12538/// `coalesce(unnest(arr), 'x')` is NOT a SRF row from the
12539/// projection's perspective; it would surface as an "unknown
12540/// function" mismatch downstream, which is what we want
12541/// (multi-SRF / nested SRF is documented carve-out for v7.19).
12542fn is_top_level_unnest(expr: &spg_sql::ast::Expr) -> bool {
12543    top_level_srf_kind(expr).is_some()
12544}
12545
12546/// v7.38 (read01, T15) — which set-returning function a top-level SELECT-list
12547/// call is, if any. Matching is allocation-free (`eq_ignore_ascii_case`, no
12548/// `to_ascii_lowercase`) because `top_level_srf_output` classifies once per
12549/// source row.
12550#[derive(Clone, Copy, PartialEq, Eq)]
12551pub(crate) enum SrfKind {
12552    Unnest,
12553    /// v7.39 (read01 round 67) — `generate_series(a, b[, step])` in the target
12554    /// list. It used to be handled ONLY by the parser's lift into FROM, so a
12555    /// second one in the same list came back as "unknown function".
12556    GenerateSeries,
12557    GenerateSubscripts,
12558    /// `_text` variants unwrap scalars to their lexeme; the plain forms render
12559    /// every value as compact JSON text.
12560    ArrayElements {
12561        as_text: bool,
12562    },
12563    PathQuery,
12564    RegexpMatches,
12565    Each {
12566        as_text: bool,
12567    },
12568    ObjectKeys,
12569}
12570
12571/// Case-insensitive match against any of `names`.
12572fn name_is(name: &str, names: &[&str]) -> bool {
12573    names.iter().any(|n| name.eq_ignore_ascii_case(n))
12574}
12575
12576pub(crate) fn top_level_srf_kind(expr: &spg_sql::ast::Expr) -> Option<SrfKind> {
12577    let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
12578        return None;
12579    };
12580    let n = args.len();
12581    // v7.38 (read01) — generate_subscripts(arr, dim) is set-returning in the
12582    // SELECT list (it returned an array there before) and shares the unnest
12583    // expansion machinery.
12584    if n == 1 && name.eq_ignore_ascii_case("unnest") {
12585        return Some(SrfKind::Unnest);
12586    }
12587    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("generate_series") {
12588        return Some(SrfKind::GenerateSeries);
12589    }
12590    if n == 2 && name.eq_ignore_ascii_case("generate_subscripts") {
12591        return Some(SrfKind::GenerateSubscripts);
12592    }
12593    // v7.38 (read01, T15) — the jsonb/json SRF family and regexp_matches expand
12594    // per element / match in the SELECT list; they collapsed to a single row
12595    // (a TextArray, or an "unknown function" error for `each`) before.
12596    if n == 1 && name_is(name, &["jsonb_array_elements", "json_array_elements"]) {
12597        return Some(SrfKind::ArrayElements { as_text: false });
12598    }
12599    if n == 1
12600        && name_is(
12601            name,
12602            &["jsonb_array_elements_text", "json_array_elements_text"],
12603        )
12604    {
12605        return Some(SrfKind::ArrayElements { as_text: true });
12606    }
12607    // v7.39 (jsonpath depth) — 3rd arg = vars, 4th = silent.
12608    if (2..=4).contains(&n) && name_is(name, &["jsonb_path_query", "json_path_query"]) {
12609        return Some(SrfKind::PathQuery);
12610    }
12611    if (2..=3).contains(&n) && name.eq_ignore_ascii_case("regexp_matches") {
12612        return Some(SrfKind::RegexpMatches);
12613    }
12614    if n == 1 && name_is(name, &["jsonb_each", "json_each"]) {
12615        return Some(SrfKind::Each { as_text: false });
12616    }
12617    if n == 1 && name_is(name, &["jsonb_each_text", "json_each_text"]) {
12618        return Some(SrfKind::Each { as_text: true });
12619    }
12620    if n == 1 && name_is(name, &["jsonb_object_keys", "json_object_keys"]) {
12621        return Some(SrfKind::ObjectKeys);
12622    }
12623    None
12624}
12625
12626/// v7.38 (read01) — the row-set a top-level SELECT-list SRF emits: the elements
12627/// for `unnest(arr)`, or the 1-based subscripts `1..=length` for
12628/// `generate_subscripts(arr, 1)` (a non-1 dimension over a 1-D array yields no
12629/// rows, as in PG).
12630pub(crate) fn top_level_srf_output(
12631    expr: &spg_sql::ast::Expr,
12632    row: &Row<'static>,
12633    ctx: &EvalContext<'_>,
12634) -> Result<Vec<Value<'static>>, EngineError> {
12635    let (Some(kind), spg_sql::ast::Expr::FunctionCall { name, args }) =
12636        (top_level_srf_kind(expr), expr)
12637    else {
12638        return Err(EngineError::Unsupported(
12639            "expected a SELECT-list SRF call".into(),
12640        ));
12641    };
12642    match kind {
12643        SrfKind::Unnest => {
12644            // v7.39 (round 743) — `unnest(ARRAY[e1, …, ek])` evaluates
12645            // the elements DIRECTLY: the old path built the whole
12646            // Value::Array (one eval + a clone per element) only for
12647            // array_value_to_elements to clone every element back out.
12648            // Any other argument shape (a column, a function result)
12649            // keeps the build-then-split path.
12650            if let spg_sql::ast::Expr::Array(items) = &args[0] {
12651                return items
12652                    .iter()
12653                    .map(|e| eval::eval_expr(e, row, ctx).map_err(EngineError::Eval))
12654                    .collect();
12655            }
12656            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12657            array_value_to_elements(&arr)
12658        }
12659        SrfKind::GenerateSeries => {
12660            // v7.39 (read01 round 96) — evaluate the args against the actual
12661            // row, then hand off to the shared core so the numeric and
12662            // timestamp/timestamptz overloads work here too (this arm used to
12663            // handle only integers, silently NULLing a temporal/numeric series
12664            // when it shared a target list with another SRF).
12665            let mut arg_values: Vec<Value<'static>> = Vec::with_capacity(args.len());
12666            for a in args {
12667                arg_values.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
12668            }
12669            let (_, rows) = generate_series_from_values(arg_values, args, &CancelToken::none())?;
12670            Ok(rows
12671                .into_iter()
12672                .map(|r| r.values.into_iter().next().unwrap_or(Value::Null))
12673                .collect())
12674        }
12675        SrfKind::GenerateSubscripts => {
12676            let arr = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12677            let dim = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
12678            if !matches!(dim, Value::Int(1) | Value::BigInt(1) | Value::SmallInt(1)) {
12679                return Ok(Vec::new());
12680            }
12681            let len = array_value_to_elements(&arr)?.len();
12682            Ok((1..=len).map(|i| Value::Int(i as i32)).collect())
12683        }
12684        // One Value per array element (`_text` → text / SQL NULL, plain → the
12685        // element's compact JSON text) — the element list the FROM-clause form
12686        // materialises.
12687        SrfKind::ArrayElements { as_text } => {
12688            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12689            if matches!(arg, Value::Null) {
12690                return Ok(Vec::new());
12691            }
12692            let items =
12693                crate::json::array_element_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
12694            Ok(items
12695                .into_iter()
12696                .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
12697                .collect())
12698        }
12699        // The scalar form already yields a TextArray of the keys (or errors on
12700        // a non-object, like PG); expand it into rows.
12701        SrfKind::ObjectKeys => {
12702            let v = eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval)?;
12703            array_value_to_elements(&v)
12704        }
12705        // One row per match, each a text[] of the pattern's capture groups.
12706        SrfKind::RegexpMatches => {
12707            let vals: Vec<Value<'static>> = args
12708                .iter()
12709                .map(|a| eval::eval_expr(a, row, ctx).map_err(EngineError::Eval))
12710                .collect::<Result<_, _>>()?;
12711            crate::eval::regexp_matches_rows(&vals).map_err(EngineError::Eval)
12712        }
12713        // One composite `(key, value)` row per object member (plain → jsonb
12714        // value, `_text` → text / SQL NULL).
12715        SrfKind::Each { as_text } => {
12716            let arg = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12717            if matches!(arg, Value::Null) {
12718                return Ok(Vec::new());
12719            }
12720            let pairs = crate::json::each_rows(&arg, as_text, name).map_err(EngineError::Eval)?;
12721            Ok(pairs
12722                .into_iter()
12723                .map(|(k, v)| {
12724                    let val = if as_text {
12725                        v.map(Value::text).unwrap_or(Value::Null)
12726                    } else {
12727                        v.map(Value::json).unwrap_or(Value::Null)
12728                    };
12729                    Value::Composite(alloc::vec![
12730                        ("key".to_string(), Value::text(k)),
12731                        ("value".to_string(), val),
12732                    ])
12733                })
12734                .collect())
12735        }
12736        // One Value per matched JSON value.
12737        SrfKind::PathQuery => {
12738            let doc = eval::eval_expr(&args[0], row, ctx).map_err(EngineError::Eval)?;
12739            let path = eval::eval_expr(&args[1], row, ctx).map_err(EngineError::Eval)?;
12740            // v7.39 — optional vars document (3rd arg).
12741            let vars = match args.get(2) {
12742                Some(a) => {
12743                    let v = eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?;
12744                    crate::json::parse_path_vars(&v).map_err(EngineError::Eval)?
12745                }
12746                None => None,
12747            };
12748            match crate::json::path_query_vars(&doc, &path, vars.as_ref())
12749                .map_err(EngineError::Eval)?
12750            {
12751                Value::Null => Ok(Vec::new()),
12752                Value::TextArray(items) => Ok(items
12753                    .into_iter()
12754                    .map(|opt| opt.map(Value::text).unwrap_or(Value::Null))
12755                    .collect()),
12756                other => Ok(alloc::vec![other]),
12757            }
12758        }
12759    }
12760}
12761
12762/// v7.19 P5 — turn an array-typed `Value` into the element list
12763/// `unnest()` projection emits. NULL → empty list (PG: `unnest(NULL)
12764/// = (no rows)`). Non-array values fall through to a type-mismatch
12765/// error.
12766pub(crate) fn array_value_to_elements(v: &Value) -> Result<Vec<Value<'static>>, EngineError> {
12767    // v7.39 (round 236) — PG unnests a multidimensional array into its
12768    // elements in row-major order (`unnest(ARRAY[[1,2],[3,4]])` is four
12769    // rows). SPG stores 2-D arrays as their own variants, which fell
12770    // through to the type-mismatch arm below.
12771    if let Some(flat) = crate::eval::values::flatten_2d(v) {
12772        return array_value_to_elements(&flat);
12773    }
12774    match v {
12775        Value::Null => Ok(Vec::new()),
12776        Value::TextArray(items) => Ok(items
12777            .iter()
12778            .map(|opt| {
12779                opt.as_ref()
12780                    .map(|s| Value::text(s.clone()))
12781                    .unwrap_or(Value::Null)
12782            })
12783            .collect()),
12784        Value::IntArray(items) => Ok(items
12785            .iter()
12786            .map(|opt| opt.map(Value::Int).unwrap_or(Value::Null))
12787            .collect()),
12788        Value::BigIntArray(items) => Ok(items
12789            .iter()
12790            .map(|opt| opt.map(Value::BigInt).unwrap_or(Value::Null))
12791            .collect()),
12792        // v7.39 (read01 multirangetypes.c) — unnest(anymultirange): one
12793        // range per canonical span.
12794        Value::Multirange { kind, ranges } => Ok(ranges
12795            .iter()
12796            .map(|s| Value::Range {
12797                kind: *kind,
12798                lower: s.lower.clone(),
12799                upper: s.upper.clone(),
12800                lower_inc: s.lower_inc,
12801                upper_inc: s.upper_inc,
12802                empty: false,
12803            })
12804            .collect()),
12805        other => Err(EngineError::Eval(EvalError::TypeMismatch {
12806            detail: alloc::format!(
12807                "unnest() expects an array argument, got {}",
12808                crate::conversions::pg_type_name_for_error_opt(other.data_type())
12809            ),
12810        })),
12811    }
12812}
12813
12814impl Engine {
12815    /// v7.17.0 Phase 1.2 — find every catalog VIEW referenced in
12816    /// the SELECT's FROM / JOIN graph, re-parse each view's body
12817    /// source, and prepend it as a synthetic CTE on the
12818    /// returned SelectStatement. Returns `None` when no view
12819    /// references are found (caller proceeds with the original
12820    /// statement); returns `Some(rewritten)` otherwise (caller
12821    /// re-runs exec_select_cancel on the rewritten form so the
12822    /// regular CTE materialiser handles it).
12823    fn expand_views_in_select(
12824        &self,
12825        stmt: &SelectStatement,
12826    ) -> Result<Option<SelectStatement>, EngineError> {
12827        let cat = self.active_catalog();
12828        let mut referenced: Vec<String> = Vec::new();
12829        if let Some(from) = &stmt.from {
12830            collect_view_refs(&from.primary, cat, &mut referenced);
12831            for j in &from.joins {
12832                collect_view_refs(&j.table, cat, &mut referenced);
12833            }
12834        }
12835        // Don't expand a view name that's already shadowed by a
12836        // CTE on the same SELECT — the CTE wins per PG.
12837        referenced.retain(|n| !stmt.ctes.iter().any(|c| c.name == *n));
12838        if referenced.is_empty() {
12839            return Ok(None);
12840        }
12841        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(referenced.len());
12842        for name in &referenced {
12843            let view = cat.view(name).ok_or_else(|| {
12844                EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
12845                    "view {name:?} disappeared mid-expansion"
12846                )))
12847            })?;
12848            let parsed = spg_sql::parser::parse_statement(&view.body).map_err(|e| {
12849                EngineError::Unsupported(alloc::format!("view {name:?} body re-parse failed: {e}"))
12850            })?;
12851            let Statement::Select(body) = parsed else {
12852                return Err(EngineError::Unsupported(alloc::format!(
12853                    "view {name:?} body is not a SELECT (catalog corruption)"
12854                )));
12855            };
12856            new_ctes.push(spg_sql::ast::Cte {
12857                name: name.clone(),
12858                body: spg_sql::ast::CteBody::Select(body),
12859                recursive: false,
12860                column_overrides: view.columns.clone(),
12861                search: None,
12862                cycle: None,
12863            });
12864        }
12865        let mut out = stmt.clone();
12866        // Prepend so view CTEs are visible to caller-supplied CTEs.
12867        new_ctes.extend(out.ctes);
12868        out.ctes = new_ctes;
12869        Ok(Some(out))
12870    }
12871
12872    /// v7.37.6-B(sentori Epic 2 P0)— if `stmt`'s FROM-clause references
12873    /// any partition-parent table, rewrite the SELECT so each parent
12874    /// reference resolves to a CTE whose body is a `UNION ALL` over the
12875    /// children that pass the WHERE-derived partition-key range. Returns
12876    /// `None`(no rewrite needed)when no parent is referenced or all
12877    /// references are shadowed by a same-name CTE.
12878    ///
12879    /// Pruning vocabulary at v7.37.6-B:
12880    ///   * Flat `AND` chain over `<key> {>= | > | < | <= | =} literal`
12881    ///     and `<key> BETWEEN literal AND literal`.
12882    ///   * Anything outside that(OR / nested IN / function call on the
12883    ///     key)defaults to "no pruning" — every child + DEFAULT lands
12884    ///     in the UNION. Correctness is preserved; only the plan size
12885    ///     widens.
12886    fn expand_partition_parents_in_select(
12887        &self,
12888        stmt: &SelectStatement,
12889    ) -> Result<Option<SelectStatement>, EngineError> {
12890        let cat = self.active_catalog();
12891        let Some(from) = &stmt.from else {
12892            return Ok(None);
12893        };
12894        let mut parent_refs: Vec<String> = Vec::new();
12895        collect_partition_parent_refs(&from.primary, cat, &mut parent_refs);
12896        for j in &from.joins {
12897            collect_partition_parent_refs(&j.table, cat, &mut parent_refs);
12898        }
12899        // Drop names shadowed by a CTE on the same SELECT(PG semantics
12900        // — same as view expansion above).
12901        parent_refs.retain(|n| !stmt.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(n)));
12902        if parent_refs.is_empty() {
12903            return Ok(None);
12904        }
12905        // Synthesise a CTE name per parent so the existing
12906        // "CTE shadows a real table" guard doesn't fire (the parent
12907        // IS a real table in the catalog, unlike VIEW expansion's
12908        // case). The FROM-clause TableRef walker below rewrites
12909        // every parent reference to point at the synthetic CTE.
12910        let synth_name = |p: &str| alloc::format!("__spg_partition_{p}");
12911        let mut new_ctes: Vec<spg_sql::ast::Cte> = Vec::with_capacity(parent_refs.len());
12912        let mut expanded_parents: Vec<alloc::string::String> = Vec::new();
12913        for parent_name in &parent_refs {
12914            // No children = no rewrite. The parent itself is a real
12915            // (empty-rows) table — the regular FROM-resolution path
12916            // will scan it and return 0 rows, matching the
12917            // "partition parent with no children" plan. Skipping the
12918            // CTE here also avoids `SELECT * FROM parent` re-entering
12919            // this rewrite on the synthetic body (infinite recursion).
12920            let Some(body) = self.build_partition_parent_union_body(parent_name, stmt)? else {
12921                continue;
12922            };
12923            new_ctes.push(spg_sql::ast::Cte {
12924                name: synth_name(parent_name),
12925                body: spg_sql::ast::CteBody::Select(body),
12926                recursive: false,
12927                column_overrides: Vec::new(),
12928                search: None,
12929                cycle: None,
12930            });
12931            expanded_parents.push(parent_name.clone());
12932        }
12933        if expanded_parents.is_empty() {
12934            return Ok(None);
12935        }
12936        let mut out = stmt.clone();
12937        if let Some(from) = out.from.as_mut() {
12938            rewrite_partition_parent_table_ref(&mut from.primary, &expanded_parents, &synth_name);
12939            for j in &mut from.joins {
12940                rewrite_partition_parent_table_ref(&mut j.table, &expanded_parents, &synth_name);
12941            }
12942        }
12943        new_ctes.extend(out.ctes);
12944        out.ctes = new_ctes;
12945        Ok(Some(out))
12946    }
12947
12948    /// Build the `SELECT * FROM child1 UNION ALL …` body for one parent.
12949    /// Children include every overlap-hit `Range` plus(always)the
12950    /// `Default` child(if any). Returns `Ok(None)` when no children
12951    /// would survive — caller skips the CTE injection and lets the
12952    /// parent fall through to the regular(empty-rows)scan path,
12953    /// avoiding the infinite recursion that an empty-body CTE
12954    /// referencing the parent name would trigger.
12955    /// v7.37.16 (16.10) — public helper invoked from explain.rs to
12956    /// surface "which children survive the WHERE-clause prune" in
12957    /// EXPLAIN output. Returns `None` when `parent_name` isn't
12958    /// actually a partition parent; otherwise returns the list of
12959    /// children the planner would scan (same algorithm as
12960    /// [`Self::build_partition_parent_union_body`] but without the
12961    /// SQL re-parse).
12962    /// v7.39 (round 224) — the kept-children prune keyed off a bare WHERE
12963    /// expression (the PG-shaped EXPLAIN's scan builder has no full
12964    /// SelectStatement in hand). Wraps the original by synthesising a
12965    /// minimal statement carrying just the predicate.
12966    pub(crate) fn explain_partition_kept_children_by_where(
12967        &self,
12968        parent_name: &str,
12969        where_: Option<&spg_sql::ast::Expr>,
12970    ) -> Option<Vec<alloc::string::String>> {
12971        let mut synth = SelectStatement::default();
12972        synth.where_ = where_.cloned();
12973        self.explain_partition_kept_children(parent_name, &synth)
12974    }
12975
12976    pub(crate) fn explain_partition_kept_children(
12977        &self,
12978        parent_name: &str,
12979        outer: &SelectStatement,
12980    ) -> Option<Vec<alloc::string::String>> {
12981        use spg_storage::PartitionRole;
12982        let cat = self.active_catalog();
12983        let parent = cat.get(parent_name)?;
12984        let (key_position, parent_kind) = match &parent.schema().partition_role {
12985            Some(PartitionRole::Parent {
12986                key_column_positions,
12987                kind,
12988                ..
12989            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
12990            _ => return None,
12991        };
12992        let key_col_name = parent.schema().columns[key_position].name.clone();
12993        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
12994            Some(expr) => extract_key_range(expr, &key_col_name),
12995            None => (None, None),
12996        };
12997        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
12998            Some(expr) => extract_key_eq_value(expr, &key_col_name),
12999            None => None,
13000        };
13001        let children = crate::partition::children_of_parent(cat, parent_name);
13002        let mut kept: Vec<alloc::string::String> = Vec::new();
13003        let mut default_child: Option<alloc::string::String> = None;
13004        for child_name in &children {
13005            let Some(child) = cat.get(child_name) else {
13006                continue;
13007            };
13008            match &child.schema().partition_role {
13009                Some(PartitionRole::Range { lower, upper, .. }) => {
13010                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13011                        kept.push(child_name.clone());
13012                    }
13013                }
13014                Some(PartitionRole::List { values, .. }) => match &eq_value {
13015                    Some(v) => {
13016                        if values.iter().any(|b| b.equals_value(v)) {
13017                            kept.push(child_name.clone());
13018                        }
13019                    }
13020                    None => kept.push(child_name.clone()),
13021                },
13022                Some(PartitionRole::Hash {
13023                    modulus, remainder, ..
13024                }) => match &eq_value {
13025                    Some(v) => {
13026                        let h = crate::partition::pg_compatible_hash(v);
13027                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13028                            kept.push(child_name.clone());
13029                        }
13030                    }
13031                    None => kept.push(child_name.clone()),
13032                },
13033                Some(PartitionRole::Default { .. }) => {
13034                    default_child = Some(child_name.clone());
13035                }
13036                _ => {}
13037            }
13038        }
13039        let _ = parent_kind;
13040        if let Some(d) = default_child {
13041            if kept.is_empty() || eq_value.is_none() {
13042                kept.push(d);
13043            }
13044        }
13045        Some(kept)
13046    }
13047
13048    fn build_partition_parent_union_body(
13049        &self,
13050        parent_name: &str,
13051        outer: &SelectStatement,
13052    ) -> Result<Option<SelectStatement>, EngineError> {
13053        use spg_storage::PartitionRole;
13054        let cat = self.active_catalog();
13055        let parent = cat.get(parent_name).ok_or_else(|| {
13056            EngineError::Storage(spg_storage::StorageError::Corrupt(alloc::format!(
13057                "partition parent {parent_name:?} disappeared mid-expansion"
13058            )))
13059        })?;
13060        let (key_position, parent_kind) = match &parent.schema().partition_role {
13061            Some(PartitionRole::Parent {
13062                key_column_positions,
13063                kind,
13064                ..
13065            }) => (*key_column_positions.first().unwrap_or(&0), *kind),
13066            // v7.39 (round 645) — an INHERITANCE parent, which has no
13067            // role of its own: the relationship is recorded only in the
13068            // children. Three things differ from a partition parent and
13069            // all three are in this body.
13070            //
13071            //   * The parent HOLDS ROWS, so it is a term of the union —
13072            //     `FROM ONLY`, or expanding it would recurse.
13073            //   * There is no partition key, so there is nothing to
13074            //     prune: every child is a term.
13075            //   * A child may declare columns of its own, so the terms
13076            //     name the PARENT's columns rather than `*`. PG's
13077            //     `SELECT * FROM parent` returns the parent's shape.
13078            //
13079            // Answered from this match rather than a branch before it —
13080            // round 644 measured what an extra early return beside an
13081            // existing test costs in this file.
13082            _ if crate::partition::has_inheritance_children(cat, parent_name) => {
13083                let cols = parent
13084                    .schema()
13085                    .columns
13086                    .iter()
13087                    .map(|c| quote_ident_for_sql(&c.name))
13088                    .collect::<Vec<_>>()
13089                    .join(", ");
13090                let carry_sys = references_ctid(outer);
13091                let sys = if carry_sys {
13092                    let mut t = alloc::string::String::new();
13093                    for s in SYSTEM_COLUMNS {
13094                        t.push_str(", ");
13095                        t.push_str(s);
13096                    }
13097                    t
13098                } else {
13099                    alloc::string::String::new()
13100                };
13101                let mut body = alloc::format!(
13102                    "SELECT {cols}{sys} FROM ONLY {}",
13103                    quote_ident_for_sql(parent_name)
13104                );
13105                for child in crate::partition::children_of_parent(cat, parent_name) {
13106                    body.push_str(&alloc::format!(
13107                        " UNION ALL SELECT {cols}{sys} FROM {}",
13108                        quote_ident_for_sql(&child)
13109                    ));
13110                }
13111                return parse_select_or_corrupt(&body).map(Some);
13112            }
13113            _ => {
13114                return Err(EngineError::Unsupported(alloc::format!(
13115                    "partition expansion: {parent_name:?} is not a parent"
13116                )));
13117            }
13118        };
13119        let key_col_name = parent.schema().columns[key_position].name.clone();
13120        // v7.37.16 (16.7) — for RANGE we extract a (lo, hi) interval
13121        // off the WHERE; for LIST / HASH we extract a single `=`
13122        // literal (and the rest of the planner falls back to "keep
13123        // every child" — same conservative path as 16.1/16.2).
13124        let (lo_bound, hi_bound) = match outer.where_.as_ref() {
13125            Some(expr) => extract_key_range(expr, &key_col_name),
13126            None => (None, None),
13127        };
13128        let eq_value: Option<spg_storage::Value<'static>> = match outer.where_.as_ref() {
13129            Some(expr) => extract_key_eq_value(expr, &key_col_name),
13130            None => None,
13131        };
13132        let children = crate::partition::children_of_parent(cat, parent_name);
13133        let mut kept: Vec<String> = Vec::new();
13134        let mut default_child: Option<String> = None;
13135        // First pass — apply per-strategy gates, defer DEFAULT until
13136        // we know whether some non-DEFAULT child matched.
13137        for child_name in &children {
13138            let Some(child) = cat.get(child_name) else {
13139                continue;
13140            };
13141            match &child.schema().partition_role {
13142                Some(PartitionRole::Range { lower, upper, .. }) => {
13143                    if range_satisfies_filter(lower, upper, lo_bound.as_ref(), hi_bound.as_ref()) {
13144                        kept.push(child_name.clone());
13145                    }
13146                }
13147                // v7.37.16 (16.7) — LIST pruning: if WHERE has `key
13148                // = <lit>`, only the child whose values contain that
13149                // literal survives. Otherwise (no equality predicate
13150                // or planner couldn't extract one) keep the child
13151                // conservatively.
13152                Some(PartitionRole::List { values, .. }) => match &eq_value {
13153                    Some(v) => {
13154                        if values.iter().any(|b| b.equals_value(v)) {
13155                            kept.push(child_name.clone());
13156                        }
13157                    }
13158                    None => kept.push(child_name.clone()),
13159                },
13160                // v7.37.16 (16.7) — HASH pruning: with `key = <lit>`
13161                // we know the residue class deterministically, so
13162                // only the matching REMAINDER child survives.
13163                Some(PartitionRole::Hash {
13164                    modulus, remainder, ..
13165                }) => match &eq_value {
13166                    Some(v) => {
13167                        let h = crate::partition::pg_compatible_hash(v);
13168                        if h.rem_euclid(u64::from(*modulus)) == u64::from(*remainder) {
13169                            kept.push(child_name.clone());
13170                        }
13171                    }
13172                    None => kept.push(child_name.clone()),
13173                },
13174                Some(PartitionRole::Default { .. }) => {
13175                    default_child = Some(child_name.clone());
13176                }
13177                _ => {}
13178            }
13179        }
13180        // PG-style DEFAULT semantics: the DEFAULT child must be
13181        // scanned iff some row could fall outside every concrete
13182        // child's bound predicate. We approximate that as "no
13183        // concrete child matched" (== full prune) — strictly
13184        // conservative for LIST / HASH (DEFAULT also catches rows
13185        // outside the union of value-sets / residues), and matches
13186        // PG for the equality case where we *do* know the routing
13187        // outcome.
13188        let _ = parent_kind; // used to silence dead-code lint while 16.8-9 lands.
13189        if let Some(d) = default_child {
13190            if kept.is_empty() {
13191                kept.push(d);
13192            } else if eq_value.is_none() {
13193                // Without an equality literal, the DEFAULT child may
13194                // still hold matching rows (e.g. LIKE on TEXT keys
13195                // for which a LIST partition exists). Keep it.
13196                kept.push(d);
13197            }
13198        }
13199        // Build the UNION ALL body text and re-parse — keeps the
13200        // rewrite expressible in surface SQL so the engine's existing
13201        // parser path handles the AST shape uniformly.
13202        if kept.is_empty() {
13203            // No children survive — caller falls back to scanning the
13204            // (empty) parent table. Returning None here is what
13205            // prevents the synthetic CTE from referring back to the
13206            // parent name and re-entering this rewrite pass.
13207            let _ = parent_name;
13208            return Ok(None);
13209        }
13210        // v7.39 (round 622, S05a) — the system columns of the CHILD the row
13211        // actually lives in.
13212        //
13213        // The parent is read through a synthetic CTE, so a `tableoid` on it
13214        // resolved against that CTE: every row of every child reported
13215        // `__spg_partition_pm`, an internal name no user ever typed, where
13216        // PG reports `pm_a` / `pm_b`. That is not only a leak — it silently
13217        // empties `WHERE tableoid::regclass::TEXT = 'pm_a'`, which is how
13218        // one asks "which partition is this row in", answering 0 rows where
13219        // PG answers 1. `ctid` had the same shape: it numbered the CTE's
13220        // output, so rows in different children got distinct ctids instead
13221        // of each child's own physical position.
13222        //
13223        // Naming them in the term is what carries them: the child scan
13224        // materialises its own six because the statement now references
13225        // them, and they land in SYSTEM_COLUMNS order right after the user
13226        // columns — the exact layout the positional `*` skip already
13227        // expects. Only done when the outer statement asks for one, so a
13228        // plain `SELECT * FROM parent` scans exactly what it scanned.
13229        let carry_sys = references_ctid(outer);
13230        let mut body = alloc::string::String::new();
13231        for (i, child_name) in kept.iter().enumerate() {
13232            if i > 0 {
13233                body.push_str(" UNION ALL ");
13234            }
13235            body.push_str("SELECT *");
13236            if carry_sys {
13237                for sys in SYSTEM_COLUMNS {
13238                    body.push_str(", ");
13239                    body.push_str(sys);
13240                }
13241            }
13242            body.push_str(" FROM ");
13243            body.push_str(&quote_ident_for_sql(child_name));
13244        }
13245        parse_select_or_corrupt(&body).map(Some)
13246    }
13247}
13248
13249/// Rewrite a `TableRef` pointing at a partition parent so it
13250/// references the synthetic CTE created by the expansion. If the
13251/// original ref had no alias, preserve the parent name as an alias
13252/// so column references like `events_partitioned.received_at`
13253/// keep resolving.
13254fn rewrite_partition_parent_table_ref(
13255    t: &mut spg_sql::ast::TableRef,
13256    parents: &[alloc::string::String],
13257    synth_name: &impl Fn(&str) -> alloc::string::String,
13258) {
13259    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
13260        return;
13261    }
13262    // v7.39 (round 644) — an ONLY reference stays pointed at the parent
13263    // itself. The rewrite is keyed on the NAME, so in
13264    // `FROM ONLY po a JOIN po b` the un-qualified `b` put `po` on the
13265    // parent list and this then rewrote BOTH — including the one that
13266    // asked not to descend. PG answers 0 for that join; SPG answered 2.
13267    // Folded into the existing test — see the note in
13268    // `collect_partition_parent_refs` for what a separate one cost.
13269    if t.only || !parents.iter().any(|p| p == &t.name) {
13270        return;
13271    }
13272    if t.alias.is_none() {
13273        t.alias = Some(t.name.clone());
13274    }
13275    t.name = synth_name(&t.name);
13276}
13277
13278/// Walk a `TableRef` and push its `name` if it resolves to a partition
13279/// parent in `cat`. Skips `lateral_subquery` / `unnest_expr` /
13280/// `generate_series_args` references — those aren't catalog tables.
13281fn collect_partition_parent_refs(
13282    t: &spg_sql::ast::TableRef,
13283    cat: &spg_storage::Catalog,
13284    out: &mut Vec<alloc::string::String>,
13285) {
13286    if t.lateral_subquery.is_some() || t.unnest_expr.is_some() || t.generate_series_args.is_some() {
13287        return;
13288    }
13289    // v7.39 (round 644) — `FROM ONLY <parent>` scans the parent alone.
13290    // The keyword used to be absorbed at parse time, so this fanned out
13291    // anyway and `SELECT count(*) FROM ONLY <partitioned parent>`
13292    // answered 2 where PG answers 0.
13293    //
13294    // Folded into the existing test rather than given an early return of
13295    // its own: as two extra lines in this function's body it cost
13296    // `WHERE g BETWEEN 10 AND 20` **26x**, 5.9 ms to 155 ms, measured
13297    // outside the panel. Rounds 641 and 643 met the same wall from the
13298    // other two directions — adding to a hot function and taking away
13299    // from a cold one. What goes in a body near the row loop is a
13300    // codegen decision whatever its shape.
13301    if !t.only && crate::partition::has_children(cat, &t.name) {
13302        out.push(t.name.clone());
13303    }
13304}
13305
13306/// v7.37.6-B partition-key range derived from a WHERE expression.
13307/// `i64` microseconds since epoch with the same sign convention as
13308/// `Value::Timestamp`. Inclusive bool: `true` ⇒ inclusive(`>=` / `<=`
13309/// / `=`),`false` ⇒ exclusive(`>` / `<`).
13310#[derive(Debug, Clone, Copy)]
13311pub(crate) struct PartitionFilterBound {
13312    pub micros: i64,
13313    pub inclusive: bool,
13314}
13315
13316/// Walk a flat AND chain looking for `<key> <op> <timestamptz-literal>`
13317/// shapes; tighten the running lo / hi as we go. Anything outside that
13318/// (OR / nested calls / non-key columns)is ignored — caller treats
13319/// `None` as "no constraint on that side."
13320fn extract_key_range(
13321    expr: &spg_sql::ast::Expr,
13322    key_col: &str,
13323) -> (Option<PartitionFilterBound>, Option<PartitionFilterBound>) {
13324    let mut lo: Option<PartitionFilterBound> = None;
13325    let mut hi: Option<PartitionFilterBound> = None;
13326    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
13327    while let Some(e) = stack.pop() {
13328        match e {
13329            spg_sql::ast::Expr::Binary {
13330                lhs,
13331                op: spg_sql::ast::BinOp::And,
13332                rhs,
13333            } => {
13334                stack.push(lhs);
13335                stack.push(rhs);
13336            }
13337            // BETWEEN is desugared at parse time into `lhs >= low AND
13338            // lhs <= high`, so it lands here as two regular Binary
13339            // arms via the AND walker above.
13340            spg_sql::ast::Expr::Binary { lhs, op, rhs } => {
13341                let (col_ref, lit_side, swapped) = if is_column_ref(lhs, key_col) {
13342                    (Some(lhs.as_ref()), rhs.as_ref(), false)
13343                } else if is_column_ref(rhs, key_col) {
13344                    (Some(rhs.as_ref()), lhs.as_ref(), true)
13345                } else {
13346                    (None, lhs.as_ref(), false)
13347                };
13348                if col_ref.is_none() {
13349                    continue;
13350                }
13351                let Some(lit) = literal_to_micros(lit_side) else {
13352                    continue;
13353                };
13354                use spg_sql::ast::BinOp::{Eq, Gt, GtEq, Lt, LtEq};
13355                let effective_op = if swapped {
13356                    match op {
13357                        Lt => Gt,
13358                        LtEq => GtEq,
13359                        Gt => Lt,
13360                        GtEq => LtEq,
13361                        other => *other,
13362                    }
13363                } else {
13364                    *op
13365                };
13366                match effective_op {
13367                    Eq => {
13368                        tighten_lo(
13369                            &mut lo,
13370                            PartitionFilterBound {
13371                                micros: lit,
13372                                inclusive: true,
13373                            },
13374                        );
13375                        tighten_hi(
13376                            &mut hi,
13377                            PartitionFilterBound {
13378                                micros: lit,
13379                                inclusive: true,
13380                            },
13381                        );
13382                    }
13383                    GtEq => {
13384                        tighten_lo(
13385                            &mut lo,
13386                            PartitionFilterBound {
13387                                micros: lit,
13388                                inclusive: true,
13389                            },
13390                        );
13391                    }
13392                    Gt => {
13393                        tighten_lo(
13394                            &mut lo,
13395                            PartitionFilterBound {
13396                                micros: lit,
13397                                inclusive: false,
13398                            },
13399                        );
13400                    }
13401                    LtEq => {
13402                        tighten_hi(
13403                            &mut hi,
13404                            PartitionFilterBound {
13405                                micros: lit,
13406                                inclusive: true,
13407                            },
13408                        );
13409                    }
13410                    Lt => {
13411                        tighten_hi(
13412                            &mut hi,
13413                            PartitionFilterBound {
13414                                micros: lit,
13415                                inclusive: false,
13416                            },
13417                        );
13418                    }
13419                    _ => {}
13420                }
13421            }
13422            _ => {}
13423        }
13424    }
13425    (lo, hi)
13426}
13427
13428fn tighten_lo(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
13429    match slot {
13430        None => *slot = Some(new),
13431        Some(cur) => {
13432            if new.micros > cur.micros
13433                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
13434            {
13435                *slot = Some(new);
13436            }
13437        }
13438    }
13439}
13440
13441fn tighten_hi(slot: &mut Option<PartitionFilterBound>, new: PartitionFilterBound) {
13442    match slot {
13443        None => *slot = Some(new),
13444        Some(cur) => {
13445            if new.micros < cur.micros
13446                || (new.micros == cur.micros && !new.inclusive && cur.inclusive)
13447            {
13448                *slot = Some(new);
13449            }
13450        }
13451    }
13452}
13453
13454fn is_column_ref(e: &spg_sql::ast::Expr, key_col: &str) -> bool {
13455    if let spg_sql::ast::Expr::Column(c) = e {
13456        c.name.eq_ignore_ascii_case(key_col)
13457    } else {
13458        false
13459    }
13460}
13461
13462/// v7.37.16 (16.7) — walk an AND-chain WHERE and pull a single
13463/// `key_col = <literal>` predicate out for LIST/HASH partition
13464/// pruning. Returns `None` when no equality literal can be lifted
13465/// (planner then keeps every child — correctness preserved). The
13466/// returned `Value<'static>` is an owned coercion so the caller can
13467/// outlive any AST node it was extracted from.
13468pub(crate) fn extract_key_eq_value(
13469    expr: &spg_sql::ast::Expr,
13470    key_col: &str,
13471) -> Option<spg_storage::Value<'static>> {
13472    let mut stack: Vec<&spg_sql::ast::Expr> = alloc::vec![expr];
13473    while let Some(e) = stack.pop() {
13474        match e {
13475            spg_sql::ast::Expr::Binary {
13476                lhs,
13477                op: spg_sql::ast::BinOp::And,
13478                rhs,
13479            } => {
13480                stack.push(lhs);
13481                stack.push(rhs);
13482            }
13483            spg_sql::ast::Expr::Binary {
13484                lhs,
13485                op: spg_sql::ast::BinOp::Eq,
13486                rhs,
13487            } => {
13488                let lit_side = if is_column_ref(lhs, key_col) {
13489                    rhs.as_ref()
13490                } else if is_column_ref(rhs, key_col) {
13491                    lhs.as_ref()
13492                } else {
13493                    continue;
13494                };
13495                let cloned = lit_side.clone();
13496                let Ok(v) = crate::conversions::literal_expr_to_value(cloned) else {
13497                    continue;
13498                };
13499                // Coerce to an owned Value<'static> so the caller
13500                // can hold it past the WHERE expression's lifetime.
13501                let owned: spg_storage::Value<'static> = match v {
13502                    spg_storage::Value::Text(s) => {
13503                        spg_storage::Value::Text(alloc::borrow::Cow::Owned(s.into_owned()))
13504                    }
13505                    spg_storage::Value::SmallInt(n) => spg_storage::Value::SmallInt(n),
13506                    spg_storage::Value::Int(n) => spg_storage::Value::Int(n),
13507                    spg_storage::Value::BigInt(n) => spg_storage::Value::BigInt(n),
13508                    spg_storage::Value::Date(d) => spg_storage::Value::Date(d),
13509                    spg_storage::Value::Timestamp(t) => spg_storage::Value::Timestamp(t),
13510                    spg_storage::Value::Bool(b) => spg_storage::Value::Bool(b),
13511                    spg_storage::Value::Null => spg_storage::Value::Null,
13512                    // Anything else (Vector / Json / Bytes / Numeric /
13513                    // arrays / interval / …) isn't a current partition
13514                    // key type; skip without pruning.
13515                    _ => continue,
13516                };
13517                return Some(owned);
13518            }
13519            _ => {}
13520        }
13521    }
13522    None
13523}
13524
13525/// Coerce a literal Expr(after the parser folded sequence calls etc.)
13526/// to i64 microseconds. Mirrors `evaluate_partition_bound`'s shape so
13527/// pruning and routing agree on the literal vocabulary. Returns
13528/// `None` when the literal isn't recognised(planner then skips
13529/// pruning on that branch — correctness preserved).
13530fn literal_to_micros(e: &spg_sql::ast::Expr) -> Option<i64> {
13531    let cloned = e.clone();
13532    let value = crate::conversions::literal_expr_to_value(cloned).ok()?;
13533    match value {
13534        spg_storage::Value::Timestamp(m) => Some(m),
13535        spg_storage::Value::Date(days) => Some(i64::from(days) * 86_400i64 * 1_000_000i64),
13536        spg_storage::Value::Text(s) => crate::eval::parse_timestamp_literal(&s),
13537        _ => None,
13538    }
13539}
13540
13541/// `[range_lo, range_hi)` of a child is kept iff it can hold any row
13542/// satisfying the WHERE-derived filter range. PG-style half-open:
13543/// child upper exclusive. Filter inclusivity is honoured per-bound.
13544fn range_satisfies_filter(
13545    range_lo: &spg_storage::PartitionBound,
13546    range_hi: &spg_storage::PartitionBound,
13547    filter_lo: Option<&PartitionFilterBound>,
13548    filter_hi: Option<&PartitionFilterBound>,
13549) -> bool {
13550    use spg_storage::PartitionBound;
13551    // For each filter side, reject children that can't host any row
13552    // matching the predicate.
13553    if let Some(lo) = filter_lo {
13554        // child upper bound vs filter lower:
13555        //   if filter is x >= L, child rejects iff child.hi <= L
13556        //   if filter is x  > L, child rejects iff child.hi <= L
13557        //   (child.hi exclusive, so equality with L still rejects)
13558        match range_hi {
13559            PartitionBound::MinValue => return false,
13560            PartitionBound::MaxValue => {}
13561            PartitionBound::TimestampTz(hi) => {
13562                if *hi <= lo.micros {
13563                    return false;
13564                }
13565            }
13566            // v7.37.16 (16.6) — non-TIMESTAMPTZ bounds aren't
13567            // matched against TIMESTAMPTZ filters here; keep child
13568            // (conservative: don't prune).
13569            PartitionBound::BigInt(_)
13570            | PartitionBound::Int(_)
13571            | PartitionBound::SmallInt(_)
13572            | PartitionBound::Date(_)
13573            | PartitionBound::Text(_) => {}
13574        }
13575    }
13576    if let Some(hi) = filter_hi {
13577        // child lower bound vs filter upper:
13578        //   if filter is x <= U, child rejects iff child.lo > U
13579        //   if filter is x  < U, child rejects iff child.lo >= U
13580        match range_lo {
13581            PartitionBound::MaxValue => return false,
13582            PartitionBound::MinValue => {}
13583            PartitionBound::TimestampTz(lo) => {
13584                let rejects = if hi.inclusive {
13585                    *lo > hi.micros
13586                } else {
13587                    *lo >= hi.micros
13588                };
13589                if rejects {
13590                    return false;
13591                }
13592            }
13593            PartitionBound::BigInt(_)
13594            | PartitionBound::Int(_)
13595            | PartitionBound::SmallInt(_)
13596            | PartitionBound::Date(_)
13597            | PartitionBound::Text(_) => {}
13598        }
13599    }
13600    true
13601}
13602
13603fn quote_ident_for_sql(name: &str) -> alloc::string::String {
13604    // Match spg-sql's quoting rule(unquoted when ASCII-lowercase
13605    // identifier, otherwise quoted). Conservative: always quote so
13606    // children with reserved names round-trip safely through the
13607    // CTE-body parse.
13608    let mut out = alloc::string::String::with_capacity(name.len() + 2);
13609    out.push('"');
13610    for c in name.chars() {
13611        if c == '"' {
13612            out.push('"');
13613        }
13614        out.push(c);
13615    }
13616    out.push('"');
13617    out
13618}
13619
13620fn parse_select_or_corrupt(sql: &str) -> Result<SelectStatement, EngineError> {
13621    let parsed = spg_sql::parser::parse_statement(sql).map_err(|e| {
13622        EngineError::Unsupported(alloc::format!(
13623            "partition expansion: generated SQL {sql:?} failed to re-parse: {e}"
13624        ))
13625    })?;
13626    let Statement::Select(body) = parsed else {
13627        return Err(EngineError::Unsupported(alloc::format!(
13628            "partition expansion: generated SQL {sql:?} is not a SELECT"
13629        )));
13630    };
13631    Ok(body)
13632}
13633
13634/// v7.39 (read01 round 65/66) — the column shape a set-returning function
13635/// exposes. `RETURNS TABLE(id int, v text)` names them; a `SETOF <scalar>`
13636/// yields ONE column named after the call's alias when there is one (`FROM
13637/// odds() AS x` → `x`), else after the function. Get this wrong and the alias
13638/// resolves to the whole ROW: `SELECT x::text FROM odds() AS x` renders `(1)`.
13639fn setof_column_shape_from(
13640    declared: &str,
13641    name: &str,
13642    alias: Option<&str>,
13643    got: &[ColumnSchema],
13644) -> alloc::vec::Vec<ColumnSchema> {
13645    let upper = declared.to_ascii_uppercase();
13646    if upper.starts_with("TABLE(") {
13647        let raw = &declared["TABLE(".len()..declared.len() - 1];
13648        return raw
13649            .split(',')
13650            .zip(got.iter())
13651            .map(|(decl, g)| {
13652                let cname = decl.split_whitespace().next().unwrap_or(g.name.as_str());
13653                ColumnSchema::new(cname.to_string(), g.ty, true)
13654            })
13655            .collect();
13656    }
13657    let cname = alias.unwrap_or(name);
13658    got.first()
13659        .map(|c| alloc::vec![ColumnSchema::new(cname.to_string(), c.ty, true)])
13660        .unwrap_or_default()
13661}
13662
13663/// The plpgsql twin: the interpreter hands back raw value rows, so the types
13664/// come off the first row.
13665fn setof_column_shape(
13666    declared: &str,
13667    name: &str,
13668    alias: Option<&str>,
13669    first_row: Option<&alloc::vec::Vec<Value<'static>>>,
13670) -> alloc::vec::Vec<ColumnSchema> {
13671    let got: alloc::vec::Vec<ColumnSchema> = first_row
13672        .map(|r| {
13673            r.iter()
13674                .enumerate()
13675                .map(|(i, v)| {
13676                    ColumnSchema::new(
13677                        alloc::format!("col{i}"),
13678                        v.data_type().unwrap_or(DataType::Text),
13679                        true,
13680                    )
13681                })
13682                .collect()
13683        })
13684        .unwrap_or_default();
13685    setof_column_shape_from(declared, name, alias, &got)
13686}
13687
13688/// v7.39 (read01 round 67) — expand every set-returning call in a target list
13689/// for ONE input row, PG's ProjectSet semantics.
13690///
13691/// Several SRFs in one list run in **LOCKSTEP**, not as a cross product: the
13692/// output has as many rows as the LONGEST of them, and a shorter one is padded
13693/// with NULLs. (`SELECT generate_series(1,3), generate_series(10,11)` →
13694/// `1/10, 2/11, 3/NULL`.) A single SRF is the degenerate case of that, and an
13695/// SRF that yields no rows at all contributes none — `SELECT unnest('{}'::int[])`
13696/// is zero rows, not one NULL row.
13697///
13698/// Non-SRF items repeat, evaluated once per output row from the same input row.
13699/// v7.39 (read01 round 79) — where an aggregate may NOT appear. Both of these
13700/// used to reach the scalar function dispatcher, which reported the aggregate as
13701/// an *unknown function* — the same "symptom two layers above the cause" shape
13702/// round 78 found with SRFs. Neither can be diagnosed down there: the dispatcher
13703/// sees a call, not the clause it came from. The statement knows.
13704/// v7.39 (round 294, E3 Phase 1b) — PG's rules on WHERE a row-locking
13705/// clause may appear.
13706///
13707/// PG rejects `FOR UPDATE` on exactly the shapes that have no
13708/// identifiable base row to lock, each with its own wording. SPG
13709/// accepted all of them and locked nothing, so a query that PG refuses
13710/// outright came back looking like it had taken locks.
13711///
13712/// Every wording read off live PG 18.4.
13713fn validate_locking_clause(stmt: &SelectStatement) -> Result<(), EngineError> {
13714    let Some(lock) = &stmt.locking else {
13715        return Ok(());
13716    };
13717    let verb = lock_clause_verb(lock.strength);
13718    let refuse = |what: &str| {
13719        Err(EngineError::Unsupported(alloc::format!(
13720            "{verb} is not allowed with {what}"
13721        )))
13722    };
13723    if !stmt.unions.is_empty() {
13724        return refuse("UNION/INTERSECT/EXCEPT");
13725    }
13726    if stmt.distinct || !stmt.distinct_on.is_empty() {
13727        return refuse("DISTINCT clause");
13728    }
13729    if stmt.group_by.is_some() || stmt.group_by_all {
13730        return refuse("GROUP BY clause");
13731    }
13732    let has_agg = stmt.items.iter().any(|it| match it {
13733        spg_sql::ast::SelectItem::Expr { expr, .. } => crate::aggregate::contains_aggregate(expr),
13734        _ => false,
13735    });
13736    if has_agg {
13737        return refuse("aggregate functions");
13738    }
13739    // `FOR UPDATE OF t` must name a relation that is actually in FROM.
13740    for want in &lock.of_tables {
13741        if !locking_from_names(stmt)
13742            .iter()
13743            .any(|n| n.eq_ignore_ascii_case(want))
13744        {
13745            return Err(EngineError::Unsupported(alloc::format!(
13746                "relation \"{want}\" in {verb} clause not found in FROM clause"
13747            )));
13748        }
13749    }
13750    Ok(())
13751}
13752
13753/// How PG names the clause in its diagnostics.
13754const fn lock_clause_verb(s: spg_sql::ast::LockStrength) -> &'static str {
13755    use spg_sql::ast::LockStrength as LS;
13756    match s {
13757        LS::Update => "FOR UPDATE",
13758        LS::NoKeyUpdate => "FOR NO KEY UPDATE",
13759        LS::Share => "FOR SHARE",
13760        LS::KeyShare => "FOR KEY SHARE",
13761    }
13762}
13763
13764/// Every relation name (or alias) the FROM clause exposes.
13765fn locking_from_names(stmt: &SelectStatement) -> alloc::vec::Vec<String> {
13766    let mut out = alloc::vec::Vec::new();
13767    if let Some(f) = &stmt.from {
13768        let mut push = |t: &spg_sql::ast::TableRef| {
13769            if let Some(a) = &t.alias {
13770                out.push(a.clone());
13771            }
13772            out.push(t.name.clone());
13773        };
13774        push(&f.primary);
13775        for j in &f.joins {
13776            push(&j.table);
13777        }
13778    }
13779    out
13780}
13781
13782fn validate_aggregate_placement(stmt: &SelectStatement) -> Result<(), EngineError> {
13783    use spg_sql::ast::Expr;
13784    if let Some(w) = &stmt.where_
13785        && aggregate::contains_aggregate(w)
13786    {
13787        return Err(EngineError::Unsupported(
13788            "aggregate functions are not allowed in WHERE".into(),
13789        ));
13790    }
13791    let mut nested = false;
13792    let mut check = |e: &Expr| {
13793        let mut probe = e.clone();
13794        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
13795            let args = match n {
13796                Expr::FunctionCall { name, args } if aggregate::is_aggregate_name(name) => args,
13797                _ => return false,
13798            };
13799            if args.iter().any(aggregate::contains_aggregate) {
13800                nested = true;
13801            }
13802            false
13803        });
13804    };
13805    for it in &stmt.items {
13806        if let spg_sql::ast::SelectItem::Expr { expr, .. } = it {
13807            check(expr);
13808        }
13809    }
13810    if let Some(h) = &stmt.having {
13811        check(h);
13812    }
13813    for o in &stmt.order_by {
13814        check(&o.expr);
13815    }
13816    if nested {
13817        return Err(EngineError::Unsupported(
13818            "aggregate function calls cannot be nested".into(),
13819        ));
13820    }
13821    Ok(())
13822}
13823
13824/// v7.39 (read01 round 78) — an SRF may sit ANYWHERE inside a target-list
13825/// expression, not only as the whole item: `upper(unnest(a))`, `unnest(a) + 10`,
13826/// `'x:' || unnest(a)`, `(regexp_matches(s, p, 'g'))::text`. PG evaluates the SRF
13827/// to a set and then applies the enclosing expression once per element. SPG only
13828/// ever recognised an SRF that WAS the item, so everything above died on
13829/// "unknown function unnest" — the set-returning call, wrapped in anything at
13830/// all, fell through to the scalar function dispatcher which has no such name.
13831///
13832/// Each SRF node is lifted out into a synthetic column (`__srf_k`), the tree is
13833/// rewritten to read that column, and the rewritten expression is evaluated once
13834/// per output row against the input row extended with the lifted values. The
13835/// lift is by VALUE, not by literal: a text[] or a jsonb keeps its type exactly.
13836/// v7.39 (read01 round 80) — `ORDER BY <n>` names the Nth OUTPUT column. Three
13837/// executors (the single-table scan, the synthetic-table pipeline, and the
13838/// unnest FROM path) each evaluated the key as an ordinary expression, where the
13839/// literal `n` is just the constant n — the same sort key for every row. The
13840/// sort therefore ran and changed nothing, which is why nobody noticed: rows came
13841/// back in input order, not in a wrong order. Statement prep resolves the common
13842/// case, but only when the SELECT item is an expression — a `*` is not one, and
13843/// `SELECT unnest(a) x` becomes `SELECT * FROM unnest(a) x`, so the everyday
13844/// spelling landed on exactly the shape prep could not resolve.
13845///
13846/// A set-returning item is left alone: copying it into ORDER BY would make the
13847/// key "the whole set", evaluated once per INPUT row.
13848fn resolve_positional_order_by(
13849    order_by: &[spg_sql::ast::OrderBy],
13850    projection: &[ProjectedItem],
13851) -> alloc::vec::Vec<spg_sql::ast::OrderBy> {
13852    order_by
13853        .iter()
13854        .filter_map(|o| {
13855            let mut o = o.clone();
13856            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &o.expr
13857                && *n >= 1
13858                && let Ok(idx) = usize::try_from(*n - 1)
13859                && let Some(item) = projection.get(idx)
13860                && !expr_contains_builtin_srf(&item.expr)
13861            {
13862                // 7.38.1 S6.1 (gendiff fourth leg) — an ordinal whose
13863                // item is itself an integer LITERAL must not be
13864                // substituted textually: the literal would read as an
13865                // ordinal again downstream, and `SELECT 10 … ORDER BY
13866                // 1` died with "position 10 is not in select list"
13867                // where PG happily returns the rows. Ordering by a
13868                // constant orders nothing, so the key drops.
13869                if matches!(item.expr, Expr::Literal(spg_sql::ast::Literal::Integer(_))) {
13870                    return None;
13871                }
13872                o.expr = item.expr.clone();
13873            }
13874            Some(o)
13875        })
13876        .collect()
13877}
13878
13879/// v7.39 (read01 round 80) — does a BUILTIN set-returning call appear anywhere in
13880/// this expression? Statement preparation (`resolve_order_by_position`) runs
13881/// before any catalog is in hand, and it only needs to know "is this item's value
13882/// a set", which the builtin SRFs answer syntactically.
13883pub(crate) fn expr_contains_builtin_srf(e: &spg_sql::ast::Expr) -> bool {
13884    let mut found = false;
13885    let mut probe = e.clone();
13886    crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
13887        if is_top_level_unnest(n) {
13888            found = true;
13889            return true;
13890        }
13891        false
13892    });
13893    found
13894}
13895
13896/// v7.39 (round 599) — everything about a target-list SRF that does not
13897/// depend on the row.
13898///
13899/// `expand_srf_row` derived all of this again for EVERY input row: it cloned
13900/// each SRF-bearing projection expression, walked and rewrote the tree,
13901/// formatted a `__srf_N` name per node, and copied the whole column schema.
13902/// A counting allocator put the path at 24 allocations per input row for a
13903/// single-element `unnest`, against 0 for the same scan without one — 211 MB
13904/// where the plain scan took 4.3 — and the shape held whatever the array
13905/// contained, which is what invariant work looks like.
13906struct SrfPlan {
13907    /// The lifted SRF calls, in slot order.
13908    nodes: alloc::vec::Vec<spg_sql::ast::Expr>,
13909    /// Per projection position, the expression with its SRF calls replaced
13910    /// by `__srf_N` column references. `None` means the item has none.
13911    rewritten: alloc::vec::Vec<Option<spg_sql::ast::Expr>>,
13912    /// The input schema followed by one column per slot. Only the slots'
13913    /// TYPES vary per row, and they are patched in place.
13914    ext_cols: alloc::vec::Vec<ColumnSchema>,
13915    /// v7.39 (round 743) — the rewritten projection COMPILED against the
13916    /// extended schema, once per plan. The per-output-row evaluation ran
13917    /// the interpreter (~560 ns/row on the unnest panel cell); the Step
13918    /// VM reads the `__srf_N` slots as plain columns. `None` = that item
13919    /// is not fully compilable and keeps the interpreter.
13920    compiled: alloc::vec::Vec<Option<eval::CompiledExpr>>,
13921    base_cols: usize,
13922}
13923
13924fn build_srf_plan(
13925    engine: &Engine,
13926    projection: &[ProjectedItem],
13927    srf_idxs: &[usize],
13928    ctx: &EvalContext<'_>,
13929) -> Result<SrfPlan, EngineError> {
13930    // Lift every SRF node out of every item that contains one.
13931    let mut nodes: Vec<spg_sql::ast::Expr> = Vec::new();
13932    let mut rewritten: Vec<Option<spg_sql::ast::Expr>> = alloc::vec![None; projection.len()];
13933    let mut reject: Option<EngineError> = None;
13934    for &i in srf_idxs {
13935        let mut e = projection[i].expr.clone();
13936        crate::expr_analysis::rewrite_nodes_mut(&mut e, &mut |n| {
13937            if reject.is_some() {
13938                return true;
13939            }
13940            // PG refuses a set-returning function inside a conditional: the set
13941            // would have to be produced before anyone knows whether the branch
13942            // is even taken.
13943            let conditional = match n {
13944                spg_sql::ast::Expr::Case { .. } => Some("CASE"),
13945                spg_sql::ast::Expr::FunctionCall { name, .. }
13946                    if name.eq_ignore_ascii_case("coalesce") =>
13947                {
13948                    Some("COALESCE")
13949                }
13950                _ => None,
13951            };
13952            if let Some(kind) = conditional
13953                && engine.expr_contains_srf(n)
13954            {
13955                reject = Some(EngineError::Unsupported(alloc::format!(
13956                    "set-returning functions are not allowed in {kind}"
13957                )));
13958                return true;
13959            }
13960            if !engine.is_srf_node(n) {
13961                return false;
13962            }
13963            let slot = nodes.len();
13964            nodes.push(n.clone());
13965            *n = spg_sql::ast::Expr::Column(spg_sql::ast::ColumnName {
13966                qualifier: None,
13967                name: alloc::format!("__srf_{slot}"),
13968            });
13969            true
13970        });
13971        rewritten[i] = Some(e);
13972    }
13973    if let Some(err) = reject {
13974        return Err(err);
13975    }
13976    let base_cols = ctx.columns.len();
13977    let mut ext_cols: Vec<ColumnSchema> = ctx.columns.to_vec();
13978    for slot in 0..nodes.len() {
13979        ext_cols.push(ColumnSchema::new(
13980            alloc::format!("__srf_{slot}"),
13981            DataType::Text,
13982            true,
13983        ));
13984    }
13985    // v7.39 (round 743) — compile the rewritten items against the
13986    // EXTENDED schema. The slot columns' declared type is a per-row
13987    // patched detail the compiled column read does not consult.
13988    let compiled: Vec<Option<eval::CompiledExpr>> = {
13989        let mut ext_ctx = ctx.clone();
13990        ext_ctx.columns = &ext_cols;
13991        projection
13992            .iter()
13993            .enumerate()
13994            .map(|(i, p)| {
13995                let e = rewritten[i].as_ref().unwrap_or(&p.expr);
13996                if eval::fully_compilable(e) {
13997                    Some(eval::compile_expr(e, &ext_ctx))
13998                } else {
13999                    None
14000                }
14001            })
14002            .collect()
14003    };
14004    Ok(SrfPlan {
14005        nodes,
14006        rewritten,
14007        ext_cols,
14008        compiled,
14009        base_cols,
14010    })
14011}
14012
14013/// One input row expanded through a plan built once for the whole scan.
14014/// v7.39 (round 621) — expand a projection whose target list contains
14015/// set-returning items, remembering which INPUT row each output row came from.
14016///
14017/// The three materialised-source tails — `FROM unnest(…)`, `FROM
14018/// generate_series(…)`, and the one that serves VALUES / a derived table /
14019/// `ROWS FROM (…)` — are near-copies of each other, and only the first knew
14020/// about target-list SRFs. So `SELECT unnest(ARRAY[1,2]), x FROM (VALUES (3),(4))
14021/// v(x)` answered `function unnest(integer[]) does not exist` on all the
14022/// others, for a query PG answers. Sharing the expansion is the point: a
14023/// fourth copy would have been the fourth place to forget.
14024fn expand_projection_srfs(
14025    engine: &Engine,
14026    projection: &[ProjectedItem],
14027    srf_idxs: &[usize],
14028    filtered: &[Row<'static>],
14029    ctx: &EvalContext<'_>,
14030) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<usize>), EngineError> {
14031    let mut out = alloc::vec::Vec::with_capacity(filtered.len());
14032    let mut src = alloc::vec::Vec::with_capacity(filtered.len());
14033    // v7.39 (round 726) — ONE plan for the whole scan. The per-row
14034    // spelling rebuilt it for every input row: a full clone of the
14035    // rewritten projection trees and the extended schema, 50k times on
14036    // the panel's unnest cell.
14037    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
14038    // v7.39 (round 733) — shard the expansion. Each shard clones the
14039    // plan (its ext_cols slot types are per-row mutable) and builds a
14040    // MINIMAL context — EvalContext is not Sync — which is sound only
14041    // when every expression involved is pure: the whole projection and
14042    // every SRF argument must be fully_compilable, or the row loop
14043    // stays serial with the full session context.
14044    // The projection is judged in its REWRITTEN form — the SRF call
14045    // itself is never compilable, but after the lift it is a plain
14046    // `__srf_N` column reference.
14047    let all_pure = projection
14048        .iter()
14049        .enumerate()
14050        .all(|(i, p)| eval::fully_compilable(plan.rewritten[i].as_ref().unwrap_or(&p.expr)))
14051        && plan.nodes.iter().all(|n| match n {
14052            Expr::FunctionCall { args, .. } => args.iter().all(eval::fully_compilable),
14053            other => eval::fully_compilable(other),
14054        });
14055    if all_pure
14056        && filtered.len() >= crate::PARALLEL_MIN_ROWS / 5
14057        && let Some(r) = engine.parallel_runner.0.as_deref()
14058    {
14059        let n_shards = (filtered.len() / (crate::PARALLEL_MIN_ROWS / 5)).clamp(2, 8);
14060        let chunk = filtered.len().div_ceil(n_shards);
14061        type ShardOut = Result<(Vec<Row<'static>>, Vec<usize>), EngineError>;
14062        let schema_cols = ctx.columns;
14063        let alias = ctx.table_alias;
14064        let mysql = ctx.mysql_dialect;
14065        let style = ctx.render_style;
14066        let plan_ref = &plan;
14067        let results = r.run_shards(n_shards, &|si| {
14068            let lo = si * chunk;
14069            let hi = ((si + 1) * chunk).min(filtered.len());
14070            let mut sctx = eval::EvalContext::new(schema_cols, alias);
14071            sctx.mysql_dialect = mysql;
14072            sctx.render_style = style;
14073            // v7.39 (round 743) — SrfPlan is no longer Clone (it carries
14074            // compiled programs); each shard rebuilds it, which also
14075            // recompiles against the shard's own context. Build errors
14076            // were already surfaced by the outer build above.
14077            let mut local_plan = match build_srf_plan(engine, projection, srf_idxs, &sctx) {
14078                Ok(p) => p,
14079                Err(e) => return alloc::boxed::Box::new(ShardOut::Err(e)) as _,
14080            };
14081            let mut run = || -> ShardOut {
14082                let mut o: Vec<Row<'static>> = Vec::with_capacity(hi - lo);
14083                let mut sidx: Vec<usize> = Vec::with_capacity(hi - lo);
14084                for (i, row) in filtered[lo..hi].iter().enumerate() {
14085                    let expanded =
14086                        expand_srf_row_with(engine, &mut local_plan, projection, row, &sctx)?;
14087                    sidx.extend(core::iter::repeat_n(lo + i, expanded.len()));
14088                    o.extend(expanded);
14089                }
14090                Ok((o, sidx))
14091            };
14092            alloc::boxed::Box::new(run())
14093        });
14094        for boxed in results {
14095            let shard = boxed
14096                .downcast::<ShardOut>()
14097                .expect("runner echoes the closure's box");
14098            let (o, sidx) = (*shard)?;
14099            out.extend(o);
14100            src.extend(sidx);
14101        }
14102        return Ok((out, src));
14103    }
14104    for (i, row) in filtered.iter().enumerate() {
14105        let expanded = expand_srf_row_with(engine, &mut plan, projection, row, ctx)?;
14106        src.extend(core::iter::repeat_n(i, expanded.len()));
14107        out.extend(expanded);
14108    }
14109    Ok((out, src))
14110}
14111
14112/// v7.39 (round 621) — one ORDER BY key, read from wherever it lives.
14113///
14114/// A key that names a select-list item reads it out of the EXPANDED row,
14115/// because PG sorts after the expansion. A key that names a source column the
14116/// query does not project is evaluated against the input row that output row
14117/// came from. `out_col` is `srf_order_output_cols`'s verdict for this key.
14118fn srf_order_key(
14119    ob: &spg_sql::ast::OrderBy,
14120    out_col: Option<usize>,
14121    out: &Row<'static>,
14122    src: &Row<'static>,
14123    ctx: &EvalContext<'_>,
14124) -> Result<Value<'static>, EngineError> {
14125    match out_col {
14126        Some(i) => Ok(out.values.get(i).cloned().unwrap_or(Value::Null)),
14127        None => eval::eval_expr(&ob.expr, src, ctx).map_err(EngineError::Eval),
14128    }
14129}
14130
14131fn expand_srf_row_with(
14132    engine: &Engine,
14133    plan: &mut SrfPlan,
14134    projection: &[ProjectedItem],
14135    row: &Row<'static>,
14136    ctx: &EvalContext<'_>,
14137) -> Result<Vec<Row<'static>>, EngineError> {
14138    let mut lists: Vec<Vec<Value<'static>>> = Vec::with_capacity(plan.nodes.len());
14139    for n in &plan.nodes {
14140        lists.push(engine.srf_values(n, row, ctx)?);
14141    }
14142    let n_rows = lists.iter().map(Vec::len).max().unwrap_or(0);
14143    // Only the slots' element types depend on the row; the names and the
14144    // input schema around them do not.
14145    for (slot, list) in lists.iter().enumerate() {
14146        plan.ext_cols[plan.base_cols + slot].ty = list
14147            .iter()
14148            .find_map(|v| v.data_type())
14149            .unwrap_or(DataType::Text);
14150    }
14151    let mut ext_ctx = ctx.clone();
14152    ext_ctx.columns = &plan.ext_cols;
14153    let mut out = Vec::with_capacity(n_rows);
14154    // v7.39 (round 726) — the base columns are the SAME for every
14155    // expanded row; clone them once and rewrite only the SRF slots per
14156    // k. The old form cloned the whole input row per OUTPUT row — for
14157    // `unnest(ARRAY[id, g])` over d that was a 100k-fold clone of a
14158    // TEXT column the projection never reads.
14159    let base_len = row.values.len();
14160    let mut ext_vals = row.values.clone();
14161    ext_vals.resize(base_len + lists.len(), Value::Null);
14162    let mut eval_stack: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
14163    for k in 0..n_rows {
14164        for (slot, list) in lists.iter().enumerate() {
14165            // Past the end of THIS srf's rows → NULL (PG pads).
14166            ext_vals[base_len + slot] = list.get(k).cloned().unwrap_or(Value::Null);
14167        }
14168        let ext_row = Row::new(core::mem::take(&mut ext_vals));
14169        let mut vals = Vec::with_capacity(projection.len());
14170        for (i, p) in projection.iter().enumerate() {
14171            // v7.39 (round 743) — compiled when possible; the
14172            // interpreter for the rest, with its exact wording.
14173            vals.push(match &plan.compiled[i] {
14174                Some(c) => eval::eval_compiled(c, &ext_row, &ext_ctx, &mut eval_stack)
14175                    .map_err(EngineError::Eval)?,
14176                None => {
14177                    let expr = plan.rewritten[i].as_ref().unwrap_or(&p.expr);
14178                    eval::eval_expr(expr, &ext_row, &ext_ctx).map_err(EngineError::Eval)?
14179                }
14180            });
14181        }
14182        ext_vals = ext_row.values;
14183        out.push(Row::new(vals));
14184    }
14185    Ok(out)
14186}
14187
14188/// The one-shot spelling, for the callers that expand a single row.
14189/// v7.39 (round 600) — which output column each ORDER BY key names, for a
14190/// query whose target list contains a set-returning function.
14191///
14192/// The keys used to be built from the INPUT row, before the SRF expanded, so
14193/// anything that named the SRF's own output was evaluated as a scalar call:
14194/// `SELECT unnest(ARRAY[g,id]) v FROM sr ORDER BY v` answered
14195/// "function unnest(integer[]) does not exist", and so did the spellings that
14196/// repeat the call or reach it through `ORDER BY 1`. Where it did not error
14197/// it silently did nothing — `SELECT DISTINCT unnest(…) … ORDER BY 1` came
14198/// back in input order. PG sorts AFTER the expansion, so a key that names a
14199/// select-list item reads that item's value out of the expanded row.
14200///
14201/// `None` keeps the key on the input row, which is where an ORDER BY naming
14202/// a column the query does not project has to be evaluated.
14203/// v7.38.19 — the output column an ORDER BY term reads, when reading it
14204/// is provably the same as building a key from the input row.
14205///
14206/// A sort key is a COPY of the sort column, made because the source row
14207/// is gone by the time the sort runs — only the projection survives. On
14208/// `SELECT s_long FROM t ORDER BY s_long` that copy is of data the
14209/// projected row already holds, and on 400,000 rows of 192-character
14210/// text it is 400,000 allocations, 400,000 frees and 77 MB of copying.
14211/// A profile of that cell put the allocator at 2,025 leaf samples of the
14212/// working set, second only to the comparison chain.
14213///
14214/// The condition is narrow on purpose. `srf_order_output_cols` resolves
14215/// an ORDER BY term the way SQL does — a positional ordinal, or a name
14216/// matching the select list — and SQL resolves against the select list
14217/// BEFORE the input columns. The key path resolves against the INPUT
14218/// columns. For `SELECT g AS id … ORDER BY id` on a table that also has
14219/// an `id`, those are different columns, and swapping one for the other
14220/// would change answers rather than timings.
14221///
14222/// So this takes only the case where the two cannot disagree: a bare
14223/// unqualified column name, matching exactly one output item, whose own
14224/// expression is that same column. The projected cell then IS the input
14225/// cell, and the key would have been its copy.
14226/// True when comparing two of this column's VALUES gives the same order
14227/// as comparing the sort KEYS built from them.
14228///
14229/// It does not hold widely. A user ENUM stores its label as text but
14230/// orders by DECLARATION position; an array orders element-wise; a
14231/// domain or composite carries its own rules. For those the two paths
14232/// answer differently, and a sort that skipped the key would silently
14233/// reorder the result. This is the short list where they agree.
14234fn value_order_is_key_order(col: &ColumnSchema) -> bool {
14235    use spg_storage::DataType as T;
14236    col.user_enum_type.is_none()
14237        && col.user_domain_type.is_none()
14238        && col.user_composite_type.is_none()
14239        && col.collation_name.is_none()
14240        && col.collation == spg_storage::Collation::Binary
14241        && matches!(
14242            col.ty,
14243            T::SmallInt | T::Int | T::BigInt | T::Text | T::Varchar(_) | T::Bool | T::Uuid
14244        )
14245}
14246
14247/// The full ORDER BY comparison between two rows, named by index.
14248///
14249/// v7.38.19 — what a permutation sort falls back to when its key ties.
14250fn row_cmp_by_index(
14251    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
14252    terms: &[(usize, bool, Option<bool>)],
14253    colls: &[Option<crate::collate::Collated>],
14254    mysql: bool,
14255    ia: u32,
14256    ib: u32,
14257) -> core::cmp::Ordering {
14258    let (a, b) = (&tagged[ia as usize], &tagged[ib as usize]);
14259    for (i, (col, desc, nf)) in terms.iter().enumerate() {
14260        let (Some(va), Some(vb)) = (a.1.values.get(*col), b.1.values.get(*col)) else {
14261            continue;
14262        };
14263        let ord = match (va, vb) {
14264            (Value::Text(x), Value::Text(y)) => match colls.get(i).and_then(Option::as_ref) {
14265                Some(c) => {
14266                    let o = c.compare(x, y);
14267                    if *desc { o.reverse() } else { o }
14268                }
14269                None if !mysql => {
14270                    let o = crate::orderby::str_cmp_prefix_first(x, y);
14271                    if *desc { o.reverse() } else { o }
14272                }
14273                None => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
14274            },
14275            _ => crate::orderby::order_by_value_cmp_in(*desc, *nf, va, vb, mysql),
14276        };
14277        if ord != core::cmp::Ordering::Equal {
14278            return ord;
14279        }
14280    }
14281    core::cmp::Ordering::Equal
14282}
14283
14284/// Whether ordering these rows by BYTES is what the collation in force
14285/// would have answered anyway.
14286///
14287/// v7.38.19 — a collated sort used to be shut out of the keyed path
14288/// entirely, and the cost of that showed up the moment the byte path
14289/// got fast: on the same fixture, the same binary took 92 ms under `C`
14290/// and 371 ms under `en_US`, so declaring a collation had become a
14291/// four-fold tax on a query that sorts md5 hex.
14292///
14293/// It need not be. For several locales `[0-9a-z]` orders exactly as
14294/// bytes do -- `collate::ascii_byte_order` carries that fact, and the
14295/// test beside it re-derives the whole allowlist by sorting a corpus
14296/// twice rather than asserting it. So when the collation is one of
14297/// those AND every value in every sort column is drawn from that
14298/// alphabet, the byte answer IS the collated answer.
14299///
14300/// Both halves are required. A collation outside the list can put `z`
14301/// between `s` and `t`; a value outside the alphabet can be `Ápple`,
14302/// which no locale in the list orders by its bytes. Either one and this
14303/// returns false, and the sort takes the collator's own path.
14304fn byte_order_answers_the_collation(
14305    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
14306    terms: &[(usize, bool, Option<bool>)],
14307    colls: &[Option<crate::collate::Collated>],
14308) -> bool {
14309    if colls.iter().all(Option::is_none) {
14310        return true;
14311    }
14312    if !colls
14313        .iter()
14314        .flatten()
14315        .all(crate::collate::Collated::ascii_byte_order)
14316    {
14317        return false;
14318    }
14319    tagged.iter().all(|(_, row)| {
14320        terms.iter().all(|(col, _, _)| match row.values.get(*col) {
14321            // Only TEXT is collation-sensitive; a number or a NULL
14322            // orders the same under every collation there is.
14323            Some(Value::Text(t)) => crate::collate::is_ascii_alnum_lower(t),
14324            _ => true,
14325        })
14326    })
14327}
14328
14329/// An eight-byte key for each row's sort column, paired with the row's
14330/// index — or `None` when the column cannot give one on every row.
14331///
14332/// v7.38.19 — the pair is what the sort array holds instead of the row.
14333/// Two kinds of column can supply it:
14334///
14335///   * an INTEGER, whose whole value fits. Flipping the sign bit maps
14336///     the signed order onto the unsigned one, so the key is EXACT and
14337///     a comparison never has to look at the row at all.
14338///   * TEXT, as the first eight bytes big-endian, zero-padded. That
14339///     orders the same as the string — two that differ inside those
14340///     bytes differ at the same index either way, and one shorter than
14341///     eight pads with zeros exactly where `[u8]`'s own comparison runs
14342///     out — but it is a PREFIX, so equal keys must still ask the full
14343///     comparator.
14344///
14345/// The `None` is the safety of it: a NULL or any other type has no
14346/// faithful eight-byte key, so such a column takes the ordinary path
14347/// rather than being given a made-up one.
14348fn sort_keys_of(
14349    tagged: &[(Vec<crate::orderby::OrderKey>, Row<'static>)],
14350    col: usize,
14351) -> Option<(Vec<(u64, u32)>, bool)> {
14352    let n = u32::try_from(tagged.len()).ok()?;
14353    let mut out: Vec<(u64, u32)> = Vec::with_capacity(tagged.len());
14354    let exact = match tagged.first()?.1.values.get(col)? {
14355        Value::Text(_) => false,
14356        Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) => true,
14357        _ => return None,
14358    };
14359    for (i, row) in (0..n).zip(tagged.iter()) {
14360        let key = match row.1.values.get(col) {
14361            Some(Value::Text(t)) if !exact => {
14362                let mut k = [0u8; 8];
14363                let bytes = t.as_bytes();
14364                let take = bytes.len().min(8);
14365                k[..take].copy_from_slice(&bytes[..take]);
14366                u64::from_be_bytes(k)
14367            }
14368            Some(Value::SmallInt(v)) if exact => (i64::from(*v) as u64) ^ (1 << 63),
14369            Some(Value::Int(v)) if exact => (i64::from(*v) as u64) ^ (1 << 63),
14370            Some(Value::BigInt(v)) if exact => (*v as u64) ^ (1 << 63),
14371            _ => return None,
14372        };
14373        out.push((key, i));
14374    }
14375    Some((out, exact))
14376}
14377
14378/// Whether a PREFIX key is worth sorting a permutation on.
14379///
14380/// v7.38.19 — it is not always, and the panel says so in one cell. The
14381/// `text (26 values)` fixture is two hundred identical characters drawn
14382/// from twenty-six letters, so every eight-byte prefix inside a letter
14383/// is the same and 15,000 rows tie on it. Each tie then pays the prefix
14384/// compare, a two-hundred-byte comparison, AND a random read into a
14385/// 400,000-element array — while sorting the rows in place keeps the
14386/// partition contiguous. Measured: 160 ms sorting rows, 247 ms sorting
14387/// the permutation, on the very fixture built to be degenerate.
14388///
14389/// So the permutation is taken when the key DECIDES, and a sample says
14390/// whether it does. An exact key always decides; a prefix has to earn
14391/// it.
14392fn key_discriminates(keys: &[(u64, u32)]) -> bool {
14393    const SAMPLE: usize = 1024;
14394    let step = (keys.len() / SAMPLE).max(1);
14395    let mut seen: Vec<u64> = keys
14396        .iter()
14397        .step_by(step)
14398        .take(SAMPLE)
14399        .map(|&(k, _)| k)
14400        .collect();
14401    let taken = seen.len();
14402    if taken < 8 {
14403        return true;
14404    }
14405    seen.sort_unstable();
14406    seen.dedup();
14407    seen.len() * 2 >= taken
14408}
14409
14410fn order_by_output_cols_if_identical(
14411    order_by: &[spg_sql::ast::OrderBy],
14412    projection: &[ProjectedItem],
14413    schema_cols: &[ColumnSchema],
14414) -> Option<Vec<usize>> {
14415    if order_by.is_empty() {
14416        return None;
14417    }
14418    let mut out = Vec::with_capacity(order_by.len());
14419    for ob in order_by {
14420        let Expr::Column(c) = &ob.expr else {
14421            return None;
14422        };
14423        if c.qualifier.is_some() {
14424            return None;
14425        }
14426        let mut hit = None;
14427        for (i, p) in projection.iter().enumerate() {
14428            if !p.output_name.eq_ignore_ascii_case(&c.name) {
14429                continue;
14430            }
14431            if hit.is_some() {
14432                return None; // ambiguous — SQL would reject it too
14433            }
14434            // The item must BE that column, not merely be named for it.
14435            let Expr::Column(pc) = &p.expr else {
14436                return None;
14437            };
14438            if !pc.name.eq_ignore_ascii_case(&c.name) {
14439                return None;
14440            }
14441            let sc = schema_cols
14442                .iter()
14443                .find(|s| s.name.eq_ignore_ascii_case(&c.name))?;
14444            if !value_order_is_key_order(sc) {
14445                return None;
14446            }
14447            hit = Some(i);
14448        }
14449        out.push(hit?);
14450    }
14451    Some(out)
14452}
14453
14454fn srf_order_output_cols(
14455    order_by: &[spg_sql::ast::OrderBy],
14456    projection: &[ProjectedItem],
14457) -> Vec<Option<usize>> {
14458    order_by
14459        .iter()
14460        .map(|ob| {
14461            // A positive ordinal is the Nth output column, directly.
14462            // `resolve_positional_order_by` deliberately leaves an ordinal
14463            // pointing at a set-returning item alone — copying the call into
14464            // ORDER BY would have made the key "the whole set" back when keys
14465            // came from the input row. Reading the expanded row's column is
14466            // what it should have meant, and is what this does.
14467            if let Expr::Literal(spg_sql::ast::Literal::Integer(n)) = &ob.expr
14468                && *n >= 1
14469                && let Ok(idx) = usize::try_from(*n - 1)
14470                && idx < projection.len()
14471            {
14472                return Some(idx);
14473            }
14474            // An unqualified name matching exactly one output name. SQL
14475            // resolves ORDER BY against the select list first, so this wins
14476            // over an input column of the same name — which is the whole
14477            // point of `SELECT g AS id … ORDER BY id`.
14478            if let Expr::Column(c) = &ob.expr
14479                && c.qualifier.is_none()
14480            {
14481                let mut hit = None;
14482                for (i, p) in projection.iter().enumerate() {
14483                    if p.output_name.eq_ignore_ascii_case(&c.name) {
14484                        if hit.is_some() {
14485                            hit = None;
14486                            break;
14487                        }
14488                        hit = Some(i);
14489                    }
14490                }
14491                if hit.is_some() {
14492                    return hit;
14493                }
14494            }
14495            // Or the same expression as a select-list item — which is what
14496            // `ORDER BY 1` becomes once `resolve_positional_order_by` has
14497            // run, and what a repeated `ORDER BY unnest(…)` is.
14498            projection.iter().position(|p| p.expr == ob.expr)
14499        })
14500        .collect()
14501}
14502
14503fn expand_srf_row(
14504    engine: &Engine,
14505    projection: &[ProjectedItem],
14506    srf_idxs: &[usize],
14507    row: &Row<'static>,
14508    ctx: &EvalContext<'_>,
14509) -> Result<Vec<Row<'static>>, EngineError> {
14510    let mut plan = build_srf_plan(engine, projection, srf_idxs, ctx)?;
14511    expand_srf_row_with(engine, &mut plan, projection, row, ctx)
14512}
14513
14514impl Engine {
14515    /// The rows one target-list SRF yields for an input row. `None` from
14516    /// `srf_target_idxs` means the expression is not set-returning at all.
14517    fn srf_values(
14518        &self,
14519        expr: &spg_sql::ast::Expr,
14520        row: &Row<'static>,
14521        ctx: &EvalContext<'_>,
14522    ) -> Result<Vec<Value<'static>>, EngineError> {
14523        if top_level_srf_kind(expr).is_some() {
14524            return top_level_srf_output(expr, row, ctx);
14525        }
14526        // A user set-returning function. Its body runs through the real
14527        // executor, like every function body since round 63.
14528        let spg_sql::ast::Expr::FunctionCall { name, args } = expr else {
14529            return Err(EngineError::Unsupported(
14530                "expected a SELECT-list SRF call".into(),
14531            ));
14532        };
14533        let mut vals: alloc::vec::Vec<Value<'static>> = alloc::vec::Vec::new();
14534        for a in args {
14535            vals.push(eval::eval_expr(a, row, ctx).map_err(EngineError::Eval)?);
14536        }
14537        let (rows, cols) = self.setof_rows_of(name, &vals, None)?;
14538        // v7.39 (read01 round 68) — in a target list a multi-column function is
14539        // a RECORD, one composite value per row: `SELECT rows_of(2)` gives
14540        // `(2,b)`, `(3,c)`. Value::Composite has existed since round 56; this is
14541        // what it is for. A single-column function contributes its bare value.
14542        Ok(rows
14543            .into_iter()
14544            .map(|r| {
14545                if r.values.len() == 1 {
14546                    r.values.into_iter().next().unwrap_or(Value::Null)
14547                } else {
14548                    Value::Composite(
14549                        cols.iter()
14550                            .map(|c| c.name.clone())
14551                            .zip(r.values)
14552                            .collect::<alloc::vec::Vec<_>>(),
14553                    )
14554                }
14555            })
14556            .collect())
14557    }
14558
14559    /// Is THIS node a set-returning call: one of the builtin kinds, or a user
14560    /// function declared `RETURNS SETOF` / `RETURNS TABLE`.
14561    fn is_srf_node(&self, e: &spg_sql::ast::Expr) -> bool {
14562        if is_top_level_unnest(e) {
14563            return true;
14564        }
14565        let spg_sql::ast::Expr::FunctionCall { name, .. } = e else {
14566            return false;
14567        };
14568        self.active_catalog().functions_named(name).iter().any(|f| {
14569            let r = f.returns.trim().to_ascii_uppercase();
14570            r.starts_with("SETOF") || r.starts_with("TABLE(")
14571        })
14572    }
14573
14574    /// Does an SRF appear ANYWHERE in this expression (not only as its root)?
14575    fn expr_contains_srf(&self, e: &spg_sql::ast::Expr) -> bool {
14576        let mut found = false;
14577        let mut probe = e.clone();
14578        crate::expr_analysis::rewrite_nodes_mut(&mut probe, &mut |n| {
14579            if self.is_srf_node(n) {
14580                found = true;
14581                return true;
14582            }
14583            false
14584        });
14585        found
14586    }
14587
14588    /// Which projection items CONTAIN a set-returning call. Before round 78 this
14589    /// asked whether the item WAS one, so `upper(unnest(a))` looked like an
14590    /// ordinary scalar call all the way down to the function dispatcher, which
14591    /// then reported `unnest` as an unknown function.
14592    fn srf_target_idxs(&self, projection: &[ProjectedItem]) -> alloc::vec::Vec<usize> {
14593        projection
14594            .iter()
14595            .enumerate()
14596            .filter(|(_, p)| self.expr_contains_srf(&p.expr))
14597            .map(|(i, _)| i)
14598            .collect()
14599    }
14600}
14601
14602impl Engine {
14603    /// v7.39 (read01 round 74) — see the call site. `None` when the statement has
14604    /// no `(f(args)).*` item.
14605    fn lower_record_expansion(
14606        &self,
14607        stmt: &SelectStatement,
14608    ) -> Result<Option<SelectStatement>, EngineError> {
14609        use spg_sql::ast::{Expr, SelectItem};
14610        let is_marker = |it: &SelectItem| {
14611            matches!(it, SelectItem::Expr { expr: Expr::FunctionCall { name, .. }, .. }
14612                if name == "__record_expand")
14613        };
14614        if !stmt.items.iter().any(is_marker) {
14615            return Ok(None);
14616        }
14617        let mut out = stmt.clone();
14618        let mut items: alloc::vec::Vec<SelectItem> = alloc::vec::Vec::new();
14619        let mut lateral_refs: alloc::vec::Vec<TableRef> = alloc::vec::Vec::new();
14620        for (n, item) in stmt.items.iter().enumerate() {
14621            if !is_marker(item) {
14622                items.push(item.clone());
14623                continue;
14624            }
14625            let SelectItem::Expr {
14626                expr: Expr::FunctionCall { args, .. },
14627                ..
14628            } = item
14629            else {
14630                unreachable!("checked by is_marker");
14631            };
14632            let Some(Expr::FunctionCall {
14633                name: fname,
14634                args: fargs,
14635            }) = args.first()
14636            else {
14637                return Err(EngineError::Unsupported(
14638                    "(<expr>).* expands a function's record — it needs a function call".into(),
14639                ));
14640            };
14641            let cols = self.setof_declared_columns(fname)?;
14642            let alias = alloc::format!("__rec{n}");
14643            let mut tref = bare_table_ref_named(&alias);
14644            tref.table_fn_call = Some(alloc::boxed::Box::new((
14645                fname.to_ascii_lowercase(),
14646                fargs.clone(),
14647            )));
14648            tref.alias = Some(alias.clone());
14649            lateral_refs.push(tref);
14650            for c in cols {
14651                items.push(SelectItem::Expr {
14652                    expr: Expr::Column(spg_sql::ast::ColumnName {
14653                        qualifier: Some(alias.clone()),
14654                        name: c,
14655                    }),
14656                    alias: None,
14657                });
14658            }
14659        }
14660        out.items = items;
14661        // The function joins the FROM. With no FROM it BECOMES the FROM; with one
14662        // it is a cross join, which is what `SELECT …, (f(t.c)).* FROM t` means
14663        // (the arguments may reference the outer row — the round-69 correlation).
14664        for tref in lateral_refs {
14665            match &mut out.from {
14666                None => {
14667                    out.from = Some(spg_sql::ast::FromClause {
14668                        primary: tref,
14669                        joins: alloc::vec::Vec::new(),
14670                    });
14671                }
14672                Some(from) => from.joins.push(spg_sql::ast::FromJoin {
14673                    kind: spg_sql::ast::JoinKind::Cross,
14674                    table: tref,
14675                    on: None,
14676                    using_cols: None,
14677                    natural: false,
14678                }),
14679            }
14680        }
14681        Ok(Some(out))
14682    }
14683
14684    /// The column NAMES a set-returning function declares: `RETURNS TABLE(id int,
14685    /// v text)` names them; a `SETOF <scalar>` is one column named after the
14686    /// function.
14687    fn setof_declared_columns(
14688        &self,
14689        name: &str,
14690    ) -> Result<alloc::vec::Vec<alloc::string::String>, EngineError> {
14691        let cat = self.active_catalog();
14692        let overloads = cat.functions_named(name);
14693        let def = overloads.first().ok_or_else(|| {
14694            EngineError::Unsupported(alloc::format!("function {name} does not exist"))
14695        })?;
14696        let declared = def.returns.trim();
14697        let upper = declared.to_ascii_uppercase();
14698        if upper.starts_with("TABLE(") {
14699            let raw = &declared["TABLE(".len()..declared.len() - 1];
14700            return Ok(raw
14701                .split(',')
14702                .map(|d| d.split_whitespace().next().unwrap_or("col").to_string())
14703                .collect());
14704        }
14705        Ok(alloc::vec![name.to_string()])
14706    }
14707}
14708
14709/// A bare `TableRef` with a name — the FROM item a lowered record expansion adds.
14710/// v7.39 (round 205, JSON_TABLE) — the static output schema of a
14711/// COLUMNS list (data-independent), NESTED children inlined in
14712/// declaration order (PG's flattened output shape).
14713/// v7.39 (round 205) — pub(crate) shim so join.rs infers a wrapped
14714/// correlated JSON_TABLE's static schema without evaluating its doc.
14715pub(crate) fn json_table_schema_pub(
14716    cols: &[spg_sql::ast::JsonTableColumn],
14717) -> alloc::vec::Vec<ColumnSchema> {
14718    json_table_schema(cols)
14719}
14720
14721fn json_table_schema(cols: &[spg_sql::ast::JsonTableColumn]) -> alloc::vec::Vec<ColumnSchema> {
14722    use spg_sql::ast::JsonTableColumn as C;
14723    let mut out = alloc::vec::Vec::new();
14724    for c in cols {
14725        match c {
14726            C::Ordinality { name } => {
14727                out.push(ColumnSchema::new(name.clone(), DataType::BigInt, false));
14728            }
14729            C::Regular {
14730                name, ty, exists, ..
14731            } => {
14732                let dt = if *exists {
14733                    DataType::Bool
14734                } else {
14735                    crate::conversions::column_type_to_data_type(*ty)
14736                };
14737                out.push(ColumnSchema::new(name.clone(), dt, true));
14738            }
14739            C::Nested { columns, .. } => out.extend(json_table_schema(columns)),
14740        }
14741    }
14742    out
14743}
14744
14745/// v7.39 (round 205) — coerce a DEFAULT / literal value to a
14746/// JSON_TABLE column's declared type (the DEFAULT expr may be a
14747/// string literal like `'none'` that must land as the column type).
14748fn coerce_json_table_default(
14749    v: Value<'static>,
14750    ty: spg_sql::ast::ColumnTypeName,
14751    name: &str,
14752) -> Result<Value<'static>, EngineError> {
14753    if v.is_null() {
14754        return Ok(Value::Null);
14755    }
14756    let dt = crate::conversions::column_type_to_data_type(ty);
14757    crate::conversions::coerce_value(v, dt, name, 0)
14758}
14759
14760/// v7.39 (round 205) — a runtime Value → JsonValue for PASSING vars.
14761fn value_to_json_value(v: &Value<'_>) -> crate::json::JsonValue {
14762    use crate::json::JsonValue as J;
14763    match v {
14764        Value::Null => J::Null,
14765        Value::Bool(b) => J::Bool(*b),
14766        Value::SmallInt(n) => J::Number(f64::from(*n)),
14767        Value::Int(n) => J::Number(f64::from(*n)),
14768        Value::BigInt(n) => J::Number(*n as f64),
14769        Value::Float(x) => J::Number(*x),
14770        Value::Json(s) => crate::json::parse_doc(s).unwrap_or(J::Null),
14771        other => J::String(crate::eval::value_to_text(other)),
14772    }
14773}
14774
14775fn bare_table_ref_named(name: &str) -> TableRef {
14776    TableRef {
14777        name: name.to_string(),
14778        alias: None,
14779        only: false,
14780        as_of_segment: None,
14781        unnest_expr: None,
14782        unnest_column_aliases: alloc::vec::Vec::new(),
14783        with_ordinality: false,
14784        generate_series_args: None,
14785        lateral_subquery: None,
14786        jsonb_each_text_arg: None,
14787        table_fn_call: None,
14788        rows_from: None,
14789        json_table: None,
14790        scalar_fn_item: false,
14791    }
14792}
14793
14794impl Engine {
14795    /// v7.39 (read01 round 74) — run a `ROWS FROM (…)` list. Each entry yields its
14796    /// own rows; they zip in lockstep and a short one pads with NULL. `__array`
14797    /// entries are the array-able SRFs, already lowered by the parser into their
14798    /// scalar array form.
14799    fn rows_from_rows(
14800        &self,
14801        primary: &TableRef,
14802    ) -> Result<(alloc::vec::Vec<Row<'static>>, alloc::vec::Vec<ColumnSchema>), EngineError> {
14803        let entries = primary
14804            .rows_from
14805            .as_ref()
14806            .expect("caller guards rows_from.is_some()");
14807        let empty: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
14808        let ctx = self.ev_ctx(&empty, None);
14809        let dummy = Row::new(alloc::vec::Vec::new());
14810        let mut lists: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = alloc::vec::Vec::new();
14811        let mut cols: alloc::vec::Vec<ColumnSchema> = alloc::vec::Vec::new();
14812        for (name, args) in entries {
14813            let (vals, colname) = if name == "__array" {
14814                // The parser lowered this one to `<array expr>`; its rows are the
14815                // array's elements.
14816                let arr = eval::eval_expr(&args[0], &dummy, &ctx).map_err(EngineError::Eval)?;
14817                (
14818                    array_value_to_elements(&arr)?,
14819                    alloc::string::String::from("unnest"),
14820                )
14821            } else {
14822                let call = spg_sql::ast::Expr::FunctionCall {
14823                    name: name.clone(),
14824                    args: args.clone(),
14825                };
14826                (self.srf_values(&call, &dummy, &ctx)?, name.clone())
14827            };
14828            let ty = vals
14829                .first()
14830                .and_then(spg_storage::Value::data_type)
14831                .unwrap_or(DataType::Text);
14832            cols.push(ColumnSchema::new(colname, ty, true));
14833            lists.push(vals);
14834        }
14835        let n = lists.iter().map(alloc::vec::Vec::len).max().unwrap_or(0);
14836        let mut rows: alloc::vec::Vec<Row<'static>> = alloc::vec::Vec::with_capacity(n);
14837        for k in 0..n {
14838            let mut vals: alloc::vec::Vec<Value<'static>> =
14839                alloc::vec::Vec::with_capacity(lists.len() + 1);
14840            for l in &lists {
14841                vals.push(l.get(k).cloned().unwrap_or(Value::Null));
14842            }
14843            rows.push(Row::new(vals));
14844        }
14845        if primary.with_ordinality {
14846            cols.push(ColumnSchema::new(
14847                "ordinality".to_string(),
14848                DataType::BigInt,
14849                false,
14850            ));
14851            rows = rows
14852                .into_iter()
14853                .enumerate()
14854                .map(|(i, r)| {
14855                    let mut v = r.values;
14856                    v.push(Value::BigInt(i as i64 + 1));
14857                    Row::new(v)
14858                })
14859                .collect();
14860        }
14861        Ok((rows, cols))
14862    }
14863}
14864
14865/// v7.39 (round 232) — PG names the offending set operation in its
14866/// arity / type-mismatch messages ("each UNION query must have the same
14867/// number of columns"). `UNION ALL` is still spelled UNION there.
14868fn set_op_name(kind: UnionKind) -> &'static str {
14869    match kind {
14870        UnionKind::All | UnionKind::Distinct => "UNION",
14871        UnionKind::Intersect | UnionKind::IntersectAll => "INTERSECT",
14872        UnionKind::Except | UnionKind::ExceptAll => "EXCEPT",
14873    }
14874}
14875
14876/// v7.39 (round 233) — which output columns of a branch are PG's `unknown`
14877/// type: a bare string or NULL literal that no context has typed yet. SPG
14878/// has no `Unknown` DataType (both describe as TEXT), so the witness has to
14879/// be the syntax. A wildcard or a non-literal expression is never unknown.
14880/// 7.38.1 S5.1 — is this branch item a reg* cast? Its result column
14881/// LABELS as text (the wire render) but the value is an oid-carrying
14882/// dual, so a UNION with a numeric column must not be refused on the
14883/// label (pg_dump: `SELECT classid … UNION ALL SELECT
14884/// 'pg_opfamily'::regclass …`).
14885fn branch_regcast_mask(stmt: &SelectStatement) -> Vec<bool> {
14886    fn is_regcast(e: &Expr) -> bool {
14887        matches!(
14888            e,
14889            Expr::Cast {
14890                target: spg_sql::ast::CastTarget::RegType | spg_sql::ast::CastTarget::RegClass,
14891                ..
14892            }
14893        )
14894    }
14895    stmt.items
14896        .iter()
14897        .map(|item| match item {
14898            SelectItem::Expr { expr, .. } => is_regcast(expr),
14899            _ => false,
14900        })
14901        .collect()
14902}
14903
14904fn branch_unknown_mask(stmt: &SelectStatement) -> Vec<bool> {
14905    stmt.items
14906        .iter()
14907        .map(|item| match item {
14908            SelectItem::Expr { expr, .. } => matches!(
14909                expr,
14910                Expr::Literal(spg_sql::ast::Literal::String(_))
14911                    | Expr::Literal(spg_sql::ast::Literal::Null)
14912            ),
14913            _ => false,
14914        })
14915        .collect()
14916}
14917
14918/// v7.39 (round 233) — retype one branch column's cells, reporting the
14919/// conversion failure the way PG does rather than leaving the column
14920/// half-converted. Used when the other branch typed an untyped literal.
14921fn coerce_branch_column(
14922    rows: &mut [Row<'static>],
14923    col_idx: usize,
14924    target: DataType,
14925    col_name: &str,
14926) -> Result<(), EngineError> {
14927    for row in rows.iter_mut() {
14928        let Some(slot) = row.values.get_mut(col_idx) else {
14929            continue;
14930        };
14931        if matches!(slot, Value::Null) {
14932            continue;
14933        }
14934        *slot = crate::conversions::coerce_value(slot.clone(), target, col_name, col_idx)?;
14935    }
14936    Ok(())
14937}
14938
14939/// v7.39 (round 727) — PG-style pull-up of a SIMPLE derived table:
14940/// `SELECT … FROM (SELECT <bare columns> FROM t [WHERE …]) q …`
14941/// rewrites to `SELECT …' FROM t [WHERE inner AND outer'] …` with every
14942/// reference to q's output columns substituted by the underlying column.
14943///
14944/// Admission is deliberately narrow — anything that changes cardinality,
14945/// order, or scope stays on the materialising path:
14946/// * outer: no CTEs / unions / DISTINCT [ON] / windows, single derived
14947///   FROM with no ordinality or positional column aliases, and no
14948///   subquery anywhere its expressions (an inner scope could reference
14949///   q too — descending is a later knife);
14950/// * inner: one stored table, bare-column projection only, no
14951///   CTE/union/DISTINCT/GROUP/HAVING/ORDER/LIMIT/OFFSET/windows/locking;
14952/// * every outer column reference must resolve inside q's output list —
14953///   a name that does not is an ERROR today, and flattening would
14954///   silently legalise it against the base table.
14955fn try_flatten_derived(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
14956    use spg_sql::ast::SelectItem;
14957    let inner = primary.lateral_subquery.as_deref()?;
14958    // Outer shape.
14959    if !stmt.ctes.is_empty()
14960        || !stmt.unions.is_empty()
14961        || stmt.distinct
14962        || !stmt.distinct_on.is_empty()
14963        || !stmt.window_check_exprs.is_empty()
14964        || stmt.locking.is_some()
14965        || primary.with_ordinality
14966        || !primary.unnest_column_aliases.is_empty()
14967    {
14968        return None;
14969    }
14970    // Inner shape.
14971    if !inner.ctes.is_empty()
14972        || !inner.unions.is_empty()
14973        || inner.distinct
14974        || !inner.distinct_on.is_empty()
14975        || inner.group_by.is_some()
14976        || inner.group_by_all
14977        || inner.having.is_some()
14978        || !inner.order_by.is_empty()
14979        || inner.limit.is_some()
14980        || inner.offset.is_some()
14981        || !inner.window_check_exprs.is_empty()
14982        || inner.locking.is_some()
14983    {
14984        return None;
14985    }
14986    let ifrom = inner.from.as_ref()?;
14987    let it = &ifrom.primary;
14988    if !ifrom.joins.is_empty()
14989        || it.name.is_empty()
14990        || it.lateral_subquery.is_some()
14991        || it.unnest_expr.is_some()
14992        || it.generate_series_args.is_some()
14993        || it.as_of_segment.is_some()
14994        || it.jsonb_each_text_arg.is_some()
14995        || it.table_fn_call.is_some()
14996        || it.rows_from.is_some()
14997        || it.json_table.is_some()
14998        || it.with_ordinality
14999        || !it.unnest_column_aliases.is_empty()
15000    {
15001        return None;
15002    }
15003    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
15004        return None;
15005    }
15006    // The output map: q's visible name -> the underlying column.
15007    let inner_alias = it.alias.clone().unwrap_or_else(|| it.name.clone());
15008    let mut map: alloc::collections::BTreeMap<String, spg_sql::ast::ColumnName> =
15009        alloc::collections::BTreeMap::new();
15010    for item in &inner.items {
15011        let SelectItem::Expr { expr, alias } = item else {
15012            return None;
15013        };
15014        let Expr::Column(c) = expr else {
15015            return None;
15016        };
15017        if let Some(q) = c.qualifier.as_deref()
15018            && !q.eq_ignore_ascii_case(&inner_alias)
15019        {
15020            return None;
15021        }
15022        let out_name = alias.clone().unwrap_or_else(|| c.name.clone());
15023        // A duplicated output name would make substitution ambiguous.
15024        if map
15025            .insert(out_name.to_ascii_lowercase(), c.clone())
15026            .is_some()
15027        {
15028            return None;
15029        }
15030    }
15031    if map.is_empty() {
15032        return None;
15033    }
15034    let derived_alias = primary
15035        .alias
15036        .clone()
15037        .unwrap_or_else(|| primary.name.clone())
15038        .to_ascii_lowercase();
15039    // Substitute in a clone; bail (None) on the first reference the map
15040    // cannot answer.
15041    let mut out = stmt.clone();
15042    let ok = core::cell::Cell::new(true);
15043    let mut subst = |e: &mut Expr| -> bool {
15044        match e {
15045            Expr::Column(c) => {
15046                match c.qualifier.as_deref() {
15047                    Some(q) if q.eq_ignore_ascii_case(&derived_alias) => {}
15048                    None => {}
15049                    Some(_) => {
15050                        ok.set(false);
15051                        return true;
15052                    }
15053                }
15054                match map.get(&c.name.to_ascii_lowercase()) {
15055                    Some(target) => *c = target.clone(),
15056                    None => ok.set(false),
15057                }
15058                true
15059            }
15060            // Any subquery could reference q from its own scope;
15061            // descending is a later knife — bail for now.
15062            Expr::ScalarSubquery(_)
15063            | Expr::Exists { .. }
15064            | Expr::InSubquery { .. }
15065            | Expr::RowInSubquery { .. }
15066            | Expr::RowCmpSubquery { .. } => {
15067                ok.set(false);
15068                true
15069            }
15070            _ => false,
15071        }
15072    };
15073    for item in &mut out.items {
15074        match item {
15075            SelectItem::Expr { expr, .. } => {
15076                crate::expr_analysis::rewrite_nodes_mut(expr, &mut subst);
15077            }
15078            // `SELECT * FROM (…) q` means q's columns, in q's order.
15079            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return None,
15080        }
15081    }
15082    if let Some(w) = &mut out.where_ {
15083        crate::expr_analysis::rewrite_nodes_mut(w, &mut subst);
15084    }
15085    if let Some(gs) = &mut out.group_by {
15086        for g in gs {
15087            crate::expr_analysis::rewrite_nodes_mut(g, &mut subst);
15088        }
15089    }
15090    if let Some(h) = &mut out.having {
15091        crate::expr_analysis::rewrite_nodes_mut(h, &mut subst);
15092    }
15093    for o in &mut out.order_by {
15094        crate::expr_analysis::rewrite_nodes_mut(&mut o.expr, &mut subst);
15095    }
15096    for d in &mut out.distinct_on {
15097        crate::expr_analysis::rewrite_nodes_mut(d, &mut subst);
15098    }
15099    if !ok.get() {
15100        return None;
15101    }
15102    // FROM becomes the stored table; the filters conjoin.
15103    out.from = Some(spg_sql::ast::FromClause {
15104        primary: it.clone(),
15105        joins: Vec::new(),
15106    });
15107    out.where_ = match (inner.where_.clone(), out.where_.take()) {
15108        (Some(a), Some(b)) => Some(Expr::Binary {
15109            lhs: alloc::boxed::Box::new(a),
15110            op: spg_sql::ast::BinOp::And,
15111            rhs: alloc::boxed::Box::new(b),
15112        }),
15113        (Some(a), None) => Some(a),
15114        (None, b) => b,
15115    };
15116    Some(out)
15117}
15118
15119/// v7.39 (round 742) — rewrite `SELECT count(*) FROM (SELECT <plain>
15120/// FROM t [WHERE p] ORDER BY … OFFSET k [no LIMIT]) q` into
15121/// `SELECT greatest(count(*) - k, 0) FROM t [WHERE p]`. Sound because
15122/// ORDER BY is count-invariant and OFFSET k drops exactly min(k, n)
15123/// rows. Admission mirrors the flatten's conservatism; a LIMIT, a
15124/// DISTINCT, an SRF, or an unprovable inner shape stays put.
15125fn try_count_over_offset(stmt: &SelectStatement, primary: &TableRef) -> Option<SelectStatement> {
15126    use spg_sql::ast::{Expr as E, LimitExpr, SelectItem};
15127    let inner = primary.lateral_subquery.as_deref()?;
15128    // Outer: exactly `SELECT count(*)`, nothing else.
15129    if !stmt.ctes.is_empty()
15130        || !stmt.unions.is_empty()
15131        || stmt.distinct
15132        || !stmt.distinct_on.is_empty()
15133        || stmt.where_.is_some()
15134        || stmt.group_by.is_some()
15135        || stmt.having.is_some()
15136        || !stmt.order_by.is_empty()
15137        || stmt.limit.is_some()
15138        || stmt.offset.is_some()
15139        || stmt.items.len() != 1
15140    {
15141        return None;
15142    }
15143    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
15144        return None;
15145    };
15146    let E::FunctionCall { name, args } = expr else {
15147        return None;
15148    };
15149    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
15150        return None;
15151    }
15152    // Inner: flatten-shaped plus ORDER BY and a literal OFFSET, no LIMIT.
15153    let Some(LimitExpr::Literal(k)) = &inner.offset else {
15154        return None;
15155    };
15156    let k = i64::from(*k);
15157    if inner.limit.is_some() || inner.order_by.is_empty() {
15158        return None;
15159    }
15160    let mut counted = inner.clone();
15161    counted.order_by = Vec::new();
15162    counted.offset = None;
15163    // The stripped inner must now be a provable simple shape (its
15164    // items become irrelevant — count(*) reads none of them — but an
15165    // SRF item would change the row count, so the flatten predicate's
15166    // scrutiny still applies).
15167    let base = matview_flatten_probe(&counted)?;
15168    let mut out = stmt.clone();
15169    out.items = alloc::vec![SelectItem::Expr {
15170        expr: E::FunctionCall {
15171            name: String::from("greatest"),
15172            args: alloc::vec![
15173                E::Binary {
15174                    lhs: alloc::boxed::Box::new(E::FunctionCall {
15175                        name: String::from("count_star"),
15176                        args: alloc::vec![],
15177                    }),
15178                    op: spg_sql::ast::BinOp::Sub,
15179                    rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
15180                },
15181                E::Literal(spg_sql::ast::Literal::Integer(0)),
15182            ],
15183        },
15184        alias: Some(String::from("count")),
15185    }];
15186    out.from = Some(spg_sql::ast::FromClause {
15187        primary: base,
15188        joins: Vec::new(),
15189    });
15190    out.where_ = counted.where_.clone();
15191    Some(out)
15192}
15193
15194/// The inner-shape probe `try_count_over_offset` shares with the
15195/// flatten: single stored table, no modifiers, no subqueries, no SRF
15196/// items. Returns the base TableRef.
15197fn matview_flatten_probe(inner: &SelectStatement) -> Option<TableRef> {
15198    use spg_sql::ast::SelectItem;
15199    if !inner.ctes.is_empty()
15200        || !inner.unions.is_empty()
15201        || inner.distinct
15202        || !inner.distinct_on.is_empty()
15203        || inner.group_by.is_some()
15204        || inner.group_by_all
15205        || inner.having.is_some()
15206        || !inner.order_by.is_empty()
15207        || inner.limit.is_some()
15208        || inner.offset.is_some()
15209        || !inner.window_check_exprs.is_empty()
15210        || inner.locking.is_some()
15211    {
15212        return None;
15213    }
15214    let ifrom = inner.from.as_ref()?;
15215    let it = &ifrom.primary;
15216    if !ifrom.joins.is_empty()
15217        || it.name.is_empty()
15218        || it.lateral_subquery.is_some()
15219        || it.unnest_expr.is_some()
15220        || it.generate_series_args.is_some()
15221        || it.as_of_segment.is_some()
15222        || it.jsonb_each_text_arg.is_some()
15223        || it.table_fn_call.is_some()
15224        || it.rows_from.is_some()
15225        || it.json_table.is_some()
15226        || it.with_ordinality
15227    {
15228        return None;
15229    }
15230    for item in &inner.items {
15231        match item {
15232            SelectItem::Expr { expr, .. } => {
15233                if crate::expr_has_subquery(expr) || expr_contains_builtin_srf(expr) {
15234                    return None;
15235                }
15236            }
15237            SelectItem::Wildcard => {}
15238            SelectItem::QualifiedWildcard(_) => return None,
15239        }
15240    }
15241    if inner.where_.as_ref().is_some_and(crate::expr_has_subquery) {
15242        return None;
15243    }
15244    Some(it.clone())
15245}
15246
15247/// v7.39 (round 743) — rewrite `SELECT count(*) FROM (SELECT
15248/// unnest(ARRAY[e1..ek]) [AS v] FROM t [WHERE p]) q` into
15249/// `SELECT count(*) * k FROM t [WHERE p]`. Sound because a
15250/// constant-LENGTH array literal unnests to exactly k rows per input
15251/// row (NULL elements are rows too). One SRF item only, elements
15252/// subquery-free, and the stripped inner must pass the same probe the
15253/// count-over-offset rewrite uses.
15254fn try_count_over_const_unnest(
15255    stmt: &SelectStatement,
15256    primary: &TableRef,
15257) -> Option<SelectStatement> {
15258    use spg_sql::ast::{Expr as E, SelectItem};
15259    let inner = primary.lateral_subquery.as_deref()?;
15260    if !stmt.ctes.is_empty()
15261        || !stmt.unions.is_empty()
15262        || stmt.distinct
15263        || !stmt.distinct_on.is_empty()
15264        || stmt.where_.is_some()
15265        || stmt.group_by.is_some()
15266        || stmt.having.is_some()
15267        || !stmt.order_by.is_empty()
15268        || stmt.limit.is_some()
15269        || stmt.offset.is_some()
15270        || stmt.items.len() != 1
15271    {
15272        return None;
15273    }
15274    let SelectItem::Expr { expr, .. } = &stmt.items[0] else {
15275        return None;
15276    };
15277    let E::FunctionCall { name, args } = expr else {
15278        return None;
15279    };
15280    if !name.eq_ignore_ascii_case("count_star") || !args.is_empty() {
15281        return None;
15282    }
15283    // Inner: exactly one item, and it is unnest(ARRAY[...]).
15284    if inner.items.len() != 1
15285        || !inner.order_by.is_empty()
15286        || inner.limit.is_some()
15287        || inner.offset.is_some()
15288    {
15289        return None;
15290    }
15291    let SelectItem::Expr { expr: item, .. } = &inner.items[0] else {
15292        return None;
15293    };
15294    let E::FunctionCall {
15295        name: fname,
15296        args: fargs,
15297    } = item
15298    else {
15299        return None;
15300    };
15301    if !fname.eq_ignore_ascii_case("unnest") || fargs.len() != 1 {
15302        return None;
15303    }
15304    let E::Array(elems) = &fargs[0] else {
15305        return None;
15306    };
15307    if elems.is_empty() || elems.iter().any(crate::expr_has_subquery) {
15308        return None;
15309    }
15310    let k = elems.len() as i64;
15311    // The stripped inner (the SRF item replaced by a plain constant)
15312    // must be the provable simple shape.
15313    let mut counted = inner.clone();
15314    counted.items = alloc::vec![SelectItem::Expr {
15315        expr: E::Literal(spg_sql::ast::Literal::Integer(1)),
15316        alias: None,
15317    }];
15318    let base = matview_flatten_probe(&counted)?;
15319    let mut out = stmt.clone();
15320    out.items = alloc::vec![SelectItem::Expr {
15321        expr: E::Binary {
15322            lhs: alloc::boxed::Box::new(E::FunctionCall {
15323                name: String::from("count_star"),
15324                args: alloc::vec![],
15325            }),
15326            op: spg_sql::ast::BinOp::Mul,
15327            rhs: alloc::boxed::Box::new(E::Literal(spg_sql::ast::Literal::Integer(k))),
15328        },
15329        alias: Some(String::from("count")),
15330    }];
15331    out.from = Some(spg_sql::ast::FromClause {
15332        primary: base,
15333        joins: Vec::new(),
15334    });
15335    out.where_ = counted.where_.clone();
15336    Some(out)
15337}