Skip to main content

spg_engine/
subquery.rs

1//! Correlated-subquery evaluation split out of `lib.rs` (lib.rs split
2//! 5): the per-row `eval_expr_with_correlated` path (clones the
3//! expression, substitutes outer-row columns into each surviving
4//! subquery node, runs the inner SELECT, folds the literal result back)
5//! plus the `subquery_replacement` pre-walk that materialises
6//! uncorrelated subquery nodes once, and the `try_batch_correlated_scalar`
7//! keyed-probe optimisation (round-22 phase 3) that runs a correlated
8//! scalar subquery ONCE without the correlation and folds rows into a
9//! key→value map. `impl Engine` methods; the bare-SELECT / DML / join
10//! row loops drive `eval_expr_with_correlated`, and `select.rs` drives
11//! `subquery_replacement` / `try_batch_correlated_scalar`.
12
13use alloc::string::String;
14use alloc::vec::Vec;
15
16use spg_sql::ast::{
17    BinOp, ColumnName, Cte, Expr, FromJoin, JoinKind, LimitExpr, Literal, SelectItem,
18    SelectStatement, TableRef, UnOp,
19};
20
21/// v7.37.4 — fire counter for the LIMIT 1 pullup pass. Tests inspect
22/// this to confirm whether the rewrite actually triggered on a given
23/// SQL shape (semantic-equivalence tests pass either way). Relaxed
24/// ordering is fine: tests synchronize on full query execution.
25pub static PULLUP_LIMIT1_FIRE_COUNT: core::sync::atomic::AtomicU64 =
26    core::sync::atomic::AtomicU64::new(0);
27
28/// v7.37.4 A' — per-keyed-probe / per-fallback counters for the
29/// batched scalar subquery resolver. Used by perf-gate instrumentation
30/// to distinguish "keyed path fires but per-probe is slow" from
31/// "keyed path never fires" — A' targets the former.
32pub static BATCHED_SCALAR_KEYED_FIRE_COUNT: core::sync::atomic::AtomicU64 =
33    core::sync::atomic::AtomicU64::new(0);
34pub static BATCHED_SCALAR_KEYED_PROBE_COUNT: core::sync::atomic::AtomicU64 =
35    core::sync::atomic::AtomicU64::new(0);
36pub static BATCHED_SCALAR_FALL_THROUGH_COUNT: core::sync::atomic::AtomicU64 =
37    core::sync::atomic::AtomicU64::new(0);
38
39/// v7.37.4 A' — EXISTS path counters. Distinguish whether mailrs
40/// prod's 2-column NOT EXISTS goes through the cheap
41/// `try_batch_correlated_exists` (one inner scan + per-row hash
42/// probe) or the slow `pull_up_exists_sublinks` rewrite (rejects
43/// multi-column correlation today). Ablation finding 2026-06-19:
44/// the NOT EXISTS conjunct in `/api/conversations` costs ~165 ms
45/// per 100k bench iteration — figure out which path is actually
46/// being taken.
47/// v7.37.7 round-2 — counts every entry into `try_pull_up_exists_sublink`.
48/// Paired with `EXISTS_PULLUP_FIRE_COUNT` (which only fires on successful
49/// rewrite) and `EXISTS_PULLUP_BAIL_*` (per-guard rejection) so we can
50/// see WHICH guard rejects the mailrs Class B prod shape on a stress run.
51pub static EXISTS_PULLUP_CANDIDATE_COUNT: core::sync::atomic::AtomicU64 =
52    core::sync::atomic::AtomicU64::new(0);
53/// Bail at line 2369: inner has CTE / UNION / GROUP BY / HAVING / DISTINCT
54/// / ORDER BY / LIMIT / OFFSET.
55pub static EXISTS_PULLUP_BAIL_INNER_SHAPE: core::sync::atomic::AtomicU64 =
56    core::sync::atomic::AtomicU64::new(0);
57/// Bail at line 2380: inner from has joins / lateral / unnest / generate_series / as_of.
58pub static EXISTS_PULLUP_BAIL_INNER_FROM: core::sync::atomic::AtomicU64 =
59    core::sync::atomic::AtomicU64::new(0);
60/// Bail at line 2405: inner has no WHERE.
61pub static EXISTS_PULLUP_BAIL_NO_WHERE: core::sync::atomic::AtomicU64 =
62    core::sync::atomic::AtomicU64::new(0);
63/// Bail at line 2446: a WHERE conjunct is not `outer=inner` Eq AND not all-inner.
64pub static EXISTS_PULLUP_BAIL_RESIDUAL_NOT_INNER: core::sync::atomic::AtomicU64 =
65    core::sync::atomic::AtomicU64::new(0);
66/// Bail at line 2451: no correlation pair found.
67pub static EXISTS_PULLUP_BAIL_NO_CORR: core::sync::atomic::AtomicU64 =
68    core::sync::atomic::AtomicU64::new(0);
69/// Bail at line 2459: multi-col + EXISTS_PULLUP_MULTICOL_DISABLE knob.
70pub static EXISTS_PULLUP_BAIL_MULTICOL_DISABLED: core::sync::atomic::AtomicU64 =
71    core::sync::atomic::AtomicU64::new(0);
72/// Bail at line 2475: positive EXISTS + inner key not single-col UNIQUE.
73pub static EXISTS_PULLUP_BAIL_UNIQUE_KEY_MISSING: core::sync::atomic::AtomicU64 =
74    core::sync::atomic::AtomicU64::new(0);
75pub static EXISTS_PULLUP_FIRE_COUNT: core::sync::atomic::AtomicU64 =
76    core::sync::atomic::AtomicU64::new(0);
77pub static EXISTS_BATCH_FIRE_COUNT: core::sync::atomic::AtomicU64 =
78    core::sync::atomic::AtomicU64::new(0);
79pub static EXISTS_BATCH_FALL_THROUGH_COUNT: core::sync::atomic::AtomicU64 =
80    core::sync::atomic::AtomicU64::new(0);
81
82/// v7.37.4 A'' — differential knob. When true, the multi-column
83/// branch of `try_pull_up_exists_sublink` rejects (falling back to
84/// the v7.34.2 batch resolver path); single-column EXISTS pullup
85/// still fires. Lets the differential e2e prove byte-equal results
86/// between the new pullup path and the legacy batch path. Default
87/// false — production never sets this.
88pub static EXISTS_PULLUP_MULTICOL_DISABLE: core::sync::atomic::AtomicBool =
89    core::sync::atomic::AtomicBool::new(false);
90
91use spg_storage::{Row, Value};
92
93use crate::eval::{self, EvalContext};
94use crate::substitute::value_to_literal_expr;
95use crate::{
96    CancelToken, Engine, EngineError, QueryResult, aggregate, memoize, order_by_value_cmp, reorder,
97    value_cmp, visit_expr_columns_and_subqueries,
98};
99
100/// Build the boolean expression for `(row) <op> (rhs)`, mirroring the
101/// parser's literal-row lowering: `=` is an AND of per-column equalities,
102/// `<>` its negation, and the ordering operators lower to the standard
103/// lexicographic `a<x OR (a=x AND (b<y OR …))` form. Evaluating the result
104/// carries SQL three-valued logic for free (NULL propagates through
105/// `=` / `<` / AND / OR / NOT). Used to resolve `RowCmpSubquery` once the
106/// subquery's single row is known.
107/// v7.39 (round 341, V66) — a scalar subquery must project exactly ONE
108/// column. Nothing checked, so `SELECT (SELECT a, b FROM t LIMIT 1)`
109/// silently answered the FIRST column where PG 18.4 raises
110/// `subquery must return only one column` — a wrong answer, not a
111/// missing feature. Zero columns became reachable in this round
112/// (PG allows an empty target list), which is what surfaced it.
113fn scalar_subquery_arity(ncols: usize) -> Result<(), EngineError> {
114    if ncols == 1 {
115        Ok(())
116    } else {
117        Err(EngineError::Unsupported(
118            "subquery must return only one column".into(),
119        ))
120    }
121}
122
123fn build_row_comparison(row: &[Expr], op: spg_sql::ast::BinOp, rhs: &[Expr]) -> Expr {
124    use alloc::boxed::Box;
125    use spg_sql::ast::{BinOp, UnOp};
126    fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
127        let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
128            lhs: Box::new(l.clone()),
129            op: BinOp::Eq,
130            rhs: Box::new(r.clone()),
131        });
132        let first = it.next().expect("row has >= 1 element");
133        it.fold(first, |acc, e| Expr::Binary {
134            lhs: Box::new(acc),
135            op: BinOp::And,
136            rhs: Box::new(e),
137        })
138    }
139    fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
140        if lhs.len() == 1 {
141            return Expr::Binary {
142                lhs: Box::new(lhs[0].clone()),
143                op: last,
144                rhs: Box::new(rhs[0].clone()),
145            };
146        }
147        let head_strict = Expr::Binary {
148            lhs: Box::new(lhs[0].clone()),
149            op: strict,
150            rhs: Box::new(rhs[0].clone()),
151        };
152        let head_eq = Expr::Binary {
153            lhs: Box::new(lhs[0].clone()),
154            op: BinOp::Eq,
155            rhs: Box::new(rhs[0].clone()),
156        };
157        Expr::Binary {
158            lhs: Box::new(head_strict),
159            op: BinOp::Or,
160            rhs: Box::new(Expr::Binary {
161                lhs: Box::new(head_eq),
162                op: BinOp::And,
163                rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
164            }),
165        }
166    }
167    match op {
168        BinOp::Eq => row_eq(row, rhs),
169        BinOp::NotEq => Expr::Unary {
170            op: UnOp::Not,
171            expr: Box::new(row_eq(row, rhs)),
172        },
173        BinOp::Lt => row_lex(row, rhs, BinOp::Lt, BinOp::Lt),
174        BinOp::LtEq => row_lex(row, rhs, BinOp::Lt, BinOp::LtEq),
175        BinOp::Gt => row_lex(row, rhs, BinOp::Gt, BinOp::Gt),
176        BinOp::GtEq => row_lex(row, rhs, BinOp::Gt, BinOp::GtEq),
177        _ => Expr::Literal(Literal::Bool(false)), // parser restricts op to the six above
178    }
179}
180
181impl Engine {
182    /// v4.23: per-row eval that handles correlated subqueries.
183    /// Equivalent to `eval::eval_expr` when the expression has no
184    /// subqueries; otherwise clones the expression, substitutes
185    /// outer-row columns into each surviving subquery node, runs
186    /// the inner SELECT, and replaces the node with the literal
187    /// result. Only the WHERE-filter call sites use this path so
188    /// the uncorrelated fast path is preserved everywhere else.
189    /// v7.39.12 — an `ORDER BY` whose key is a correlated scalar
190    /// subquery, resolved for one row.
191    ///
192    /// Reported by sentori against 7.39.11, and it predates it:
193    ///
194    /// ```text
195    ///   SELECT i.id FROM issues i
196    ///    ORDER BY (SELECT max(e.occurred_at) FROM events e
197    ///               WHERE e.issue_id = i.id) DESC NULLS LAST
198    ///   ERROR:  subquery reached row eval — engine resolver bug
199    /// ```
200    ///
201    /// The message names itself. Uncorrelated subqueries in `ORDER BY`
202    /// are replaced by a literal before execution, and correlated ones
203    /// cannot be — they have a different value per row — so they
204    /// reached the per-row evaluator, which is the one place that
205    /// cannot run a subquery. Everything adjacent answers: the same
206    /// correlated subquery in the SELECT list, in `WHERE`, an
207    /// uncorrelated one in `ORDER BY`, and both rewrites (`LATERAL`,
208    /// and the `GROUP BY` join) — so it is the correlation AND the
209    /// `ORDER BY` position together.
210    ///
211    /// It is what `backfill_split` does, a shipped subcommand of
212    /// theirs, and the statement raised rather than mis-sorting.
213    ///
214    /// Returns the order list unchanged when no key holds a subquery,
215    /// which is every ordinary statement and costs one tree walk.
216    pub(crate) fn order_by_resolved_for_row(
217        &self,
218        order_by: &[spg_sql::ast::OrderBy],
219        row: &Row<'static>,
220        ctx: &EvalContext<'_>,
221        cancel: CancelToken<'_>,
222    ) -> Result<Option<alloc::vec::Vec<spg_sql::ast::OrderBy>>, EngineError> {
223        if !order_by.iter().any(|o| expr_has_subquery(&o.expr)) {
224            return Ok(None);
225        }
226        let mut out = alloc::vec::Vec::with_capacity(order_by.len());
227        for o in order_by {
228            if !expr_has_subquery(&o.expr) {
229                out.push(o.clone());
230                continue;
231            }
232            let v = self.eval_expr_with_correlated(&o.expr, row, ctx, cancel, None)?;
233            let mut o2 = o.clone();
234            // The same materialisation a resolved subquery takes, so a
235            // key keeps the type its inner SELECT declared — see
236            // `value_to_literal_expr_typed`.
237            o2.expr = crate::substitute::value_to_literal_expr(v)?;
238            out.push(o2);
239        }
240        Ok(Some(out))
241    }
242
243    pub(crate) fn eval_expr_with_correlated(
244        &self,
245        expr: &Expr,
246        row: &Row<'static>,
247        ctx: &EvalContext<'_>,
248        cancel: CancelToken<'_>,
249        mut memo: Option<&mut memoize::MemoizeCache>,
250    ) -> Result<Value<'static>, EngineError> {
251        // v7.30.2 (mailrs round-25) — the has-subquery walk is
252        // O(tree) and a materialised `IN (…)` list makes the tree
253        // huge; cache the answer per expression address so the
254        // per-row dispatch stops re-walking 24k list elements.
255        let has_subq = if let Some(m) = memo.as_deref_mut() {
256            let key = core::ptr::from_ref::<Expr>(expr) as usize;
257            match m.has_subquery.get(&key) {
258                Some(b) => *b,
259                None => {
260                    let b = expr_has_subquery(expr);
261                    m.has_subquery.insert(key, b);
262                    b
263                }
264            }
265        } else {
266            expr_has_subquery(expr)
267        };
268        if !has_subq {
269            // A large materialised `IN (…)` list inside the WHERE
270            // makes the plain eval O(rows × list); route through the
271            // per-query membership set (built once, keyed by node
272            // address) when one is reachable on the AND spine.
273            if let Some(m) = memo.as_deref_mut()
274                && expr_may_use_in_set(expr)
275            {
276                return eval_with_in_sets(expr, row, ctx, m);
277            }
278            return eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval);
279        }
280        // v7.29 (3c) - per-expression plan: the batch maps for this
281        // host expression's scalar subqueries are looked up by the
282        // expression's ADDRESS (stable across the row loop), so the
283        // hot path does zero AST formatting. Building the plan (and
284        // its Display-keyed group maps) happens once per expression.
285        if let Some(m) = memo.as_deref_mut() {
286            let key = core::ptr::from_ref::<Expr>(expr) as usize;
287            // Plan hit: skip the collection walk entirely (it ran
288            // once per group otherwise - 70k walks per inbox query).
289            // The memo is per-query and host expressions outlive it,
290            // so an address that hit once stays valid.
291            let plan_hit = m.expr_plans.contains_key(&key);
292            let exists_plan_hit = m.exists_plans.contains_key(&key);
293            let mut subs: Vec<&SelectStatement> = Vec::new();
294            let mut exists_subs: Vec<&SelectStatement> = Vec::new();
295            if !plan_hit {
296                collect_scalar_subqueries(expr, &mut subs);
297            }
298            if !exists_plan_hit {
299                collect_exists_subqueries(expr, &mut exists_subs);
300            }
301            if !plan_hit && !subs.is_empty() {
302                let mut plan: Vec<Option<alloc::rc::Rc<memoize::GroupMap>>> =
303                    Vec::with_capacity(subs.len());
304                for sub in &subs {
305                    let repr = alloc::format!("{sub}");
306                    if !m.group_maps.contains_key(&repr) {
307                        let built = self
308                            .try_batch_correlated_scalar(sub, None, cancel)?
309                            .map(alloc::rc::Rc::new);
310                        m.group_maps.insert(repr.clone(), built);
311                    }
312                    plan.push(m.group_maps.get(&repr).cloned().flatten());
313                }
314                let mut template = expr.clone();
315                hollow_scalar_subqueries(&mut template);
316                m.expr_plans.insert(key, (subs.len(), plan, template));
317            }
318            // v7.34.2 — parallel EXISTS plan. Walk host ONCE in pre-order,
319            // build a decorrelated key-set for each EXISTS subquery via
320            // `try_batch_correlated_exists`, and cache the vec by host_ptr.
321            // Per-row dispatch below uses `splice_planned_exists` which
322            // increments an ordinal cursor — no `alloc::format!` per row.
323            if !exists_plan_hit && !exists_subs.is_empty() {
324                let mut eplan: Vec<Option<alloc::rc::Rc<memoize::ExistsSet>>> =
325                    Vec::with_capacity(exists_subs.len());
326                for sub in &exists_subs {
327                    let built = self
328                        .try_batch_correlated_exists(sub, cancel)?
329                        .map(alloc::rc::Rc::new);
330                    if built.is_some() {
331                        EXISTS_BATCH_FIRE_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
332                    } else {
333                        EXISTS_BATCH_FALL_THROUGH_COUNT
334                            .fetch_add(1, core::sync::atomic::Ordering::Relaxed);
335                    }
336                    eplan.push(built);
337                }
338                m.exists_plans.insert(key, eplan);
339            }
340            // Fast-path gate: take it if we have a planned scalar set, a
341            // planned EXISTS set, or both — anything that lets us skip
342            // the per-row `expr.clone()` + `resolve_correlated_in_expr`
343            // dispatch for the corresponding subquery class.
344            // v7.39 (round 616) — the predicate that IS a single EXISTS needs
345            // no tree at all.
346            //
347            // The splice below replaces the EXISTS node with a boolean, and to
348            // do that it CLONES the whole expression for every outer row —
349            // which, for an `Expr::Exists`, clones the entire subquery AST
350            // with it. Measured over 100k rows,
351            // `WHERE EXISTS (… b.id = a.id + 1)` cost 18 allocations a row
352            // against 1 for the uncorrelated-key form, and 64.3 ms against
353            // 12.2. When the whole predicate is that node there is nothing to
354            // splice into: read the verdict and hand it back. Taken here,
355            // before the plan is cloned out of the memo, so the row loop does
356            // not copy that either. `NOT EXISTS (…)` arrives as a `Not` over
357            // the node rather than as `negated`, so both spellings are read.
358            if !m.expr_plans.contains_key(&key)
359                && let Some((negated, wrapped_in_not)) = bare_exists_shape(expr)
360                && let Some(plan) = m.exists_plans.get(&key)
361                && plan.len() == 1
362                && let Some(Some(es)) = plan.first()
363            {
364                let bit = planned_exists_bit(es, negated, row, ctx)?;
365                return Ok(Value::Bool(if wrapped_in_not { !bit } else { bit }));
366            }
367            let scalar_ready = m
368                .expr_plans
369                .get(&key)
370                .map(|(_, plan, _)| !plan.is_empty() && plan.iter().all(|p| p.is_some()))
371                .unwrap_or(false);
372            let exists_ready = m
373                .exists_plans
374                .get(&key)
375                .map(|plan| !plan.is_empty() && plan.iter().all(|p| p.is_some()))
376                .unwrap_or(false);
377            if scalar_ready || exists_ready {
378                // Fast path: every planned subquery resolves via its
379                // map; clone the (hollowed-where-scalar) template,
380                // splice map values, eval. EXISTS bodies are NOT
381                // hollowed (we don't traverse into them during splice —
382                // `splice_planned_exists` consumes the EXISTS node
383                // wholesale), so cloning the original `expr` works for
384                // the EXISTS-only path.
385                let scalar_plan = m
386                    .expr_plans
387                    .get(&key)
388                    .map(|(_, plan, template)| (plan.clone(), template.clone()));
389                let exists_plan = m.exists_plans.get(&key).cloned();
390                let mut e = match &scalar_plan {
391                    Some((_, template)) => template.clone(),
392                    None => expr.clone(),
393                };
394                let mut all_ok = true;
395                if let Some((plan, _)) = &scalar_plan {
396                    let mut idx = 0usize;
397                    all_ok &= splice_planned_subqueries(&mut e, plan, &mut idx, row, ctx)?;
398                }
399                if all_ok && let Some(plan) = &exists_plan {
400                    let mut idx = 0usize;
401                    all_ok &= splice_planned_exists(&mut e, plan, &mut idx, row, ctx)?;
402                }
403                if all_ok {
404                    if expr_has_subquery(&e) {
405                        self.resolve_correlated_in_expr(&mut e, row, ctx, cancel, memo)?;
406                    }
407                    return eval::eval_expr(&e, row, ctx).map_err(EngineError::Eval);
408                }
409            }
410        }
411        let mut e = expr.clone();
412        self.resolve_correlated_in_expr(&mut e, row, ctx, cancel, memo)?;
413        eval::eval_expr(&e, row, ctx).map_err(EngineError::Eval)
414    }
415
416    /// Quantified `op ANY / ALL (SELECT …)` — materialise every
417    /// row of the single-column subquery into an ARRAY[…] literal
418    /// the existing AnyAll three-valued eval consumes (empty
419    /// result → empty array: ANY false, ALL true — PG semantics).
420    pub(crate) fn materialize_quantified_rows(
421        &self,
422        inner: &SelectStatement,
423        cancel: CancelToken<'_>,
424    ) -> Result<Expr, EngineError> {
425        let r = self.exec_select_cancel(inner, cancel)?;
426        let QueryResult::Rows { rows, .. } = r else {
427            return Err(EngineError::Unsupported(
428                "ANY/ALL subquery: inner did not return rows".into(),
429            ));
430        };
431        let mut items = alloc::vec::Vec::with_capacity(rows.len());
432        for r0 in rows {
433            let v = r0.values.into_iter().next().unwrap_or(Value::Null);
434            items.push(value_to_literal_expr(v)?);
435        }
436        Ok(Expr::Array(items))
437    }
438
439    fn resolve_correlated_in_expr(
440        &self,
441        e: &mut Expr,
442        row: &Row<'static>,
443        ctx: &EvalContext<'_>,
444        cancel: CancelToken<'_>,
445        mut memo: Option<&mut memoize::MemoizeCache>,
446    ) -> Result<(), EngineError> {
447        match e {
448            Expr::Collate { expr, .. } | Expr::NamedArg { expr, .. } | Expr::Variadic(expr) => {
449                self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
450            }
451            Expr::AggregateOrdered { call, order_by, .. } => {
452                self.resolve_correlated_in_expr(call, row, ctx, cancel, memo.as_deref_mut())?;
453                for o in order_by.iter_mut() {
454                    self.resolve_correlated_in_expr(
455                        &mut o.expr,
456                        row,
457                        ctx,
458                        cancel,
459                        memo.as_deref_mut(),
460                    )?;
461                }
462            }
463            Expr::ScalarSubquery(inner) => {
464                // v7.29 (round-22 phase 3) — batch path first: a
465                // correlated scalar of the `inner_col = outer_col
466                // [ORDER BY … LIMIT 1]` shape evaluates ONCE as a
467                // grouped scan; per-row resolution becomes a map
468                // lookup. 23.5k per-group executions (~900 ms) became
469                // one scan + lookups.
470                // v7.37.x (docker-fair SCALARSQ attack) — pointer-keyed
471                // fast cache. The inner SelectStatement is stable for
472                // the duration of the query, so its address makes a
473                // unique key that costs nothing to compute (vs
474                // `alloc::format!("{}", inner)` ~ 500 ns × N outer
475                // rows of pure repr churn).
476                if memo.is_some() {
477                    let ptr_key = core::ptr::from_ref::<SelectStatement>(&**inner) as usize;
478                    let entry_known = memo
479                        .as_ref()
480                        .is_some_and(|m| m.group_maps_by_ptr.contains_key(&ptr_key));
481                    if !entry_known {
482                        let built = self
483                            .try_batch_correlated_scalar(inner, None, cancel)?
484                            .map(alloc::rc::Rc::new);
485                        if let Some(m) = memo.as_deref_mut() {
486                            m.group_maps_by_ptr.insert(ptr_key, built);
487                        }
488                    }
489                    if let Some(m) = memo.as_deref_mut()
490                        && let Some(Some(gm)) = m.group_maps_by_ptr.get(&ptr_key)
491                    {
492                        let (outer_col, map, empty_default) = gm.as_ref();
493                        let key_v = eval::eval_expr(&Expr::Column(outer_col.clone()), row, ctx)
494                            .map_err(EngineError::Eval)?;
495                        // v7.37.x — scalar subquery empty-set semantics:
496                        // `COUNT(*)` / `COUNT(col)` over no rows = 0,
497                        // every other aggregate = NULL. The batched
498                        // GroupMap omits keys whose inner-table partition
499                        // was empty; treat such misses as the per-
500                        // aggregate empty-default.
501                        //
502                        // v7.39 (round 620) — and a NULL correlation key is
503                        // one of those misses, not a NULL answer. `b.g =
504                        // NULL` matches nothing, so the subquery runs over an
505                        // EMPTY set and the aggregate's own empty-set value
506                        // decides it: `count` answers 0. A NULL key cannot
507                        // collide with a real group either, because the map
508                        // builder skips NULL keys when it groups the inner
509                        // rows.
510                        let v = map
511                            .get(&aggregate::encode_key(core::slice::from_ref(&key_v)))
512                            .cloned()
513                            .unwrap_or_else(|| empty_default.clone());
514                        *e = value_to_literal_expr(v)?;
515                        return Ok(());
516                    }
517                }
518                // v6.2.6 — Memoize: build the cache key from the
519                // pre-substitution subquery repr + the outer row's
520                // values. Two outer rows with identical correlated
521                // values hit the same entry.
522                let cache_key = memo.as_ref().map(|_| memoize::CacheKey {
523                    subquery_repr: alloc::format!("{}", **inner),
524                    outer_values: row.values.iter().cloned().map(Value::into_owned).collect(),
525                });
526                if let (Some(cache), Some(k)) = (memo.as_deref_mut(), cache_key.as_ref())
527                    && let Some(cached) = cache.get(k)
528                {
529                    *e = value_to_literal_expr(cached)?;
530                    return Ok(());
531                }
532                // v7.37.x (docker-fair SCALARSQ attack) — direct PK probe
533                // fast path. The shape
534                //   (SELECT COUNT(*) FROM T WHERE T.pk = outer.col)
535                // — common SCALARSQ shape and what the docker-fair
536                // SCALARSQ benchmark exercises — is a 1-bit lookup:
537                // the probe either finds 1 row or 0. Skip
538                // `exec_select_cancel`'s parse / resolve / plan /
539                // aggregate roundtrip; do an index seek on T.pk
540                // directly and return `Int(0)` or `Int(1)`. PG with a
541                // cached prepared plan does roughly this; SCALARSQ
542                // drops from per-row ~3 µs to per-row ~100 ns.
543                if let Some(v) = self.try_scalar_count_pk_eq_probe(inner, row, ctx)? {
544                    *e = value_to_literal_expr(v)?;
545                    return Ok(());
546                }
547                let mut s = (**inner).clone();
548                substitute_outer_columns(&mut s, row, ctx, self.active_catalog());
549                let r = self.exec_select_cancel(&s, cancel)?;
550                let QueryResult::Rows { columns, rows, .. } = r else {
551                    return Err(EngineError::Unsupported(
552                        "scalar subquery: inner did not return rows".into(),
553                    ));
554                };
555                scalar_subquery_arity(columns.len())?;
556                let value = match rows.as_slice() {
557                    [] => Value::Null,
558                    [r0] => r0.values.first().cloned().unwrap_or(Value::Null),
559                    _ => {
560                        return Err(EngineError::CardinalityViolation);
561                    }
562                };
563                if let (Some(cache), Some(k)) = (memo.as_deref_mut(), cache_key) {
564                    cache.insert(k, value.clone());
565                }
566                *e = value_to_literal_expr(value)?;
567            }
568            Expr::Exists { subquery, negated } => {
569                // v7.34 (mailrs conn-pool P0) — semi/anti-join batch path
570                // first: a correlated `[NOT] EXISTS` of the
571                // `inner.k = outer.col [AND inner-preds]` shape builds its
572                // inner key-set ONCE (keyed by repr in the per-query memo);
573                // per-row resolution becomes a membership test. 24k per-row
574                // inner executions became one scan + 24k lookups.
575                if memo.is_some() {
576                    let repr = alloc::format!("{}", **subquery);
577                    let known = memo
578                        .as_ref()
579                        .is_some_and(|m| m.exists_sets.contains_key(&repr));
580                    if !known {
581                        let built = self
582                            .try_batch_correlated_exists(subquery, cancel)?
583                            .map(alloc::rc::Rc::new);
584                        if let Some(m) = memo.as_deref_mut() {
585                            m.exists_sets.insert(repr.clone(), built);
586                        }
587                    }
588                    if let Some(m) = memo.as_deref_mut()
589                        && let Some(Some(es)) = m.exists_sets.get(&repr)
590                    {
591                        let (outer_cols, set) = es.as_ref();
592                        let mut key_vals: Vec<Value<'static>> =
593                            Vec::with_capacity(outer_cols.len());
594                        let mut any_null = false;
595                        for oc in outer_cols {
596                            // v7.39 (round 596) — an expression now, evaluated
597                            // directly rather than rebuilt as a column node.
598                            let v = eval::eval_expr(oc, row, ctx).map_err(EngineError::Eval)?;
599                            if matches!(v, Value::Null) {
600                                any_null = true;
601                            }
602                            key_vals.push(v);
603                        }
604                        // NULL key component → never matches → not present.
605                        let present =
606                            !any_null && set.contains(&aggregate::encode_canonical_key(&key_vals));
607                        let bit = if *negated { !present } else { present };
608                        *e = Expr::Literal(Literal::Bool(bit));
609                        return Ok(());
610                    }
611                }
612                let mut s = (**subquery).clone();
613                substitute_outer_columns(&mut s, row, ctx, self.active_catalog());
614                let r = self.exec_select_cancel(&s, cancel)?;
615                let exists = matches!(r, QueryResult::Rows { rows, .. } if !rows.is_empty());
616                let bit = if *negated { !exists } else { exists };
617                *e = Expr::Literal(Literal::Bool(bit));
618            }
619            Expr::InSubquery {
620                expr: lhs,
621                subquery,
622                negated,
623            } => {
624                self.resolve_correlated_in_expr(lhs, row, ctx, cancel, memo.as_deref_mut())?;
625                let lhs_val = eval::eval_expr(lhs, row, ctx).map_err(EngineError::Eval)?;
626                let mut s = (**subquery).clone();
627                substitute_outer_columns(&mut s, row, ctx, self.active_catalog());
628                let r = self.exec_select_cancel(&s, cancel)?;
629                let QueryResult::Rows { columns, rows, .. } = r else {
630                    return Err(EngineError::Unsupported(
631                        "IN-subquery: inner did not return rows".into(),
632                    ));
633                };
634                if columns.len() != 1 {
635                    // v7.39 (round 341, V66) — PG's two wordings, measured
636                    // on 18.4: `subquery has too few columns` /
637                    // `subquery has too many columns`. SPG named its own
638                    // internal shape ("IN-subquery must project exactly
639                    // one column; got 0").
640                    return Err(EngineError::Unsupported(
641                        if columns.is_empty() {
642                            "subquery has too few columns"
643                        } else {
644                            "subquery has too many columns"
645                        }
646                        .into(),
647                    ));
648                }
649                let mut found = false;
650                let mut any_null = false;
651                for r0 in rows {
652                    let v = r0.values.into_iter().next().unwrap_or(Value::Null);
653                    if v.is_null() {
654                        any_null = true;
655                        continue;
656                    }
657                    if value_cmp(&v, &lhs_val) == core::cmp::Ordering::Equal {
658                        found = true;
659                        break;
660                    }
661                }
662                if !found && any_null {
663                    // SQL three-valued logic: no match but the IN-list held a
664                    // NULL → the predicate is UNKNOWN (NULL), not false. This is
665                    // the classic `x NOT IN (… NULL …)` gotcha — every non-match
666                    // row evaluates to NULL and is filtered. PG-verified.
667                    *e = Expr::Literal(Literal::Null);
668                    return Ok(());
669                }
670                let bit = if found { !*negated } else { *negated };
671                *e = Expr::Literal(Literal::Bool(bit));
672            }
673            Expr::RowInSubquery {
674                row: row_exprs,
675                subquery,
676                negated,
677            } => {
678                // `(a, b, …) [NOT] IN (SELECT x, y, …)` with PG's row
679                // three-valued logic: the result is OR over subquery rows
680                // of the per-row AND of column equalities. A row is a
681                // definite mismatch as soon as one column is unequal (both
682                // non-NULL); if no column is definitely unequal but some
683                // comparison involved a NULL, that row is UNKNOWN. So the
684                // predicate is TRUE if any row fully matches, else NULL if
685                // any row was UNKNOWN, else FALSE.
686                for el in row_exprs.iter_mut() {
687                    self.resolve_correlated_in_expr(el, row, ctx, cancel, memo.as_deref_mut())?;
688                }
689                let lhs_vals: Vec<Value> = row_exprs
690                    .iter()
691                    .map(|el| eval::eval_expr(el, row, ctx).map_err(EngineError::Eval))
692                    .collect::<Result<_, _>>()?;
693                let mut s = (**subquery).clone();
694                substitute_outer_columns(&mut s, row, ctx, self.active_catalog());
695                let r = self.exec_select_cancel(&s, cancel)?;
696                let QueryResult::Rows { columns, rows, .. } = r else {
697                    return Err(EngineError::Unsupported(
698                        "row IN-subquery: inner did not return rows".into(),
699                    ));
700                };
701                if columns.len() != lhs_vals.len() {
702                    return Err(EngineError::Unsupported(alloc::format!(
703                        "row IN-subquery: left side has {} column(s), subquery returns {}",
704                        lhs_vals.len(),
705                        columns.len()
706                    )));
707                }
708                let mut found = false;
709                let mut any_null = false;
710                'rows: for r0 in rows {
711                    let mut has_null = false;
712                    for (j, sub_v) in r0.values.iter().enumerate() {
713                        let lv = &lhs_vals[j];
714                        if lv.is_null() || sub_v.is_null() {
715                            has_null = true;
716                        } else if value_cmp(lv, sub_v) != core::cmp::Ordering::Equal {
717                            continue 'rows; // one column unequal → row is FALSE
718                        }
719                    }
720                    if has_null {
721                        any_null = true; // all non-NULL columns matched → UNKNOWN
722                    } else {
723                        found = true; // full definite match
724                        break;
725                    }
726                }
727                if !found && any_null {
728                    *e = Expr::Literal(Literal::Null);
729                    return Ok(());
730                }
731                let bit = if found { !*negated } else { *negated };
732                *e = Expr::Literal(Literal::Bool(bit));
733            }
734            Expr::RowCmpSubquery {
735                row: row_exprs,
736                op,
737                subquery,
738            } => {
739                // `(a, b, …) <op> (correlated SELECT)` — run the subquery for
740                // this outer row, then compare the tuple. Zero rows → NULL
741                // (PG scalar-subquery rule); more than one row is an error.
742                for el in row_exprs.iter_mut() {
743                    self.resolve_correlated_in_expr(el, row, ctx, cancel, memo.as_deref_mut())?;
744                }
745                let mut s = (**subquery).clone();
746                substitute_outer_columns(&mut s, row, ctx, self.active_catalog());
747                let r = self.exec_select_cancel(&s, cancel)?;
748                let QueryResult::Rows {
749                    columns, mut rows, ..
750                } = r
751                else {
752                    return Err(EngineError::Unsupported(
753                        "row comparison subquery: inner did not return rows".into(),
754                    ));
755                };
756                if rows.is_empty() {
757                    *e = Expr::Literal(Literal::Null);
758                    return Ok(());
759                }
760                if rows.len() > 1 {
761                    return Err(EngineError::CardinalityViolation);
762                }
763                if columns.len() != row_exprs.len() {
764                    return Err(EngineError::Unsupported(alloc::format!(
765                        "row comparison: left side has {} column(s), subquery returns {}",
766                        row_exprs.len(),
767                        columns.len()
768                    )));
769                }
770                let rhs: Vec<Expr> = rows
771                    .remove(0)
772                    .values
773                    .into_iter()
774                    .map(value_to_literal_expr)
775                    .collect::<Result<_, _>>()?;
776                let cmp = build_row_comparison(row_exprs, *op, &rhs);
777                let v = eval::eval_expr(&cmp, row, ctx).map_err(EngineError::Eval)?;
778                *e = value_to_literal_expr(v)?;
779            }
780            Expr::Binary { lhs, rhs, .. } => {
781                self.resolve_correlated_in_expr(lhs, row, ctx, cancel, memo.as_deref_mut())?;
782                self.resolve_correlated_in_expr(rhs, row, ctx, cancel, memo.as_deref_mut())?;
783            }
784            Expr::Unary { expr, .. }
785            | Expr::Cast { expr, .. }
786            | Expr::IsNull { expr, .. }
787            | Expr::BoolTest { expr, .. }
788            | Expr::FieldAccess { base: expr, .. } => {
789                self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
790            }
791            Expr::Like { expr, pattern, .. } => {
792                self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
793                self.resolve_correlated_in_expr(pattern, row, ctx, cancel, memo.as_deref_mut())?;
794            }
795            Expr::FunctionCall { args, .. } => {
796                for a in args {
797                    self.resolve_correlated_in_expr(a, row, ctx, cancel, memo.as_deref_mut())?;
798                }
799            }
800            Expr::Extract { source, .. } => {
801                self.resolve_correlated_in_expr(source, row, ctx, cancel, memo.as_deref_mut())?;
802            }
803            Expr::WindowFunction { .. }
804            | Expr::Literal(_)
805            | Expr::Placeholder(_)
806            | Expr::Column(_) => {}
807            // v7.10.10 — recurse children.
808            Expr::Array(items) => {
809                for elem in items {
810                    self.resolve_correlated_in_expr(elem, row, ctx, cancel, memo.as_deref_mut())?;
811                }
812            }
813            Expr::ArraySubscript { target, index } => {
814                self.resolve_correlated_in_expr(target, row, ctx, cancel, memo.as_deref_mut())?;
815                self.resolve_correlated_in_expr(index, row, ctx, cancel, memo.as_deref_mut())?;
816            }
817            Expr::ArraySlice { target, lo, hi } => {
818                self.resolve_correlated_in_expr(target, row, ctx, cancel, memo.as_deref_mut())?;
819                if let Some(l) = lo {
820                    self.resolve_correlated_in_expr(l, row, ctx, cancel, memo.as_deref_mut())?;
821                }
822                if let Some(h) = hi {
823                    self.resolve_correlated_in_expr(h, row, ctx, cancel, memo.as_deref_mut())?;
824                }
825            }
826            Expr::AnyAll { expr, array, .. } => {
827                self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
828                // Quantified subquery — substitute the outer row's
829                // values and materialise all rows into an ARRAY.
830                if let Expr::ScalarSubquery(inner) = array.as_mut() {
831                    let mut s = (**inner).clone();
832                    substitute_outer_columns(&mut s, row, ctx, self.active_catalog());
833                    **array = self.materialize_quantified_rows(&s, cancel)?;
834                } else {
835                    self.resolve_correlated_in_expr(array, row, ctx, cancel, memo.as_deref_mut())?;
836                }
837            }
838            Expr::InList { expr, list, .. } => {
839                self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
840                for item in list {
841                    self.resolve_correlated_in_expr(item, row, ctx, cancel, memo.as_deref_mut())?;
842                }
843            }
844            Expr::Case {
845                operand,
846                branches,
847                else_branch,
848            } => {
849                if let Some(o) = operand {
850                    self.resolve_correlated_in_expr(o, row, ctx, cancel, memo.as_deref_mut())?;
851                }
852                for (w, t) in branches {
853                    self.resolve_correlated_in_expr(w, row, ctx, cancel, memo.as_deref_mut())?;
854                    self.resolve_correlated_in_expr(t, row, ctx, cancel, memo.as_deref_mut())?;
855                }
856                if let Some(e) = else_branch {
857                    self.resolve_correlated_in_expr(e, row, ctx, cancel, memo.as_deref_mut())?;
858                }
859            }
860        }
861        Ok(())
862    }
863
864    /// v4.10: pre-walk the WHERE / projection / etc. of a SELECT and
865    /// replace every subquery node with a materialised literal. SPG
866    /// only supports uncorrelated subqueries — the inner SELECT does
867    /// not see outer-row columns, so the result is the same for every
868    /// outer row and can be evaluated once.
869    ///
870    /// Returns the rewritten statement; the caller passes this to the
871    /// regular row-loop executor which no longer sees Subquery nodes
872    /// in its tree.
873    /// The `Expr::RowCmpSubquery` arm of `subquery_replacement`, lifted out.
874    ///
875    /// `#[inline(never)]`: `subquery_replacement` recurses, and a debug
876    /// build keeps EVERY arm's locals — each of these clones a
877    /// `SelectStatement`, 800 bytes before its contents — in the frame
878    /// whichever arm runs. The frame measured 32,928 bytes and the
879    /// deepest descent of one nested query holds two of them.
880    ///
881    /// Taking `e` whole and re-binding here keeps the original
882    /// bindings and their types exactly; the `else` arm cannot happen,
883    /// since the caller dispatches on this variant.
884    #[inline(never)]
885    fn arm_row_cmp_subquery(
886        &self,
887        e: &Expr,
888        cancel: CancelToken<'_>,
889    ) -> Result<Option<Expr>, EngineError> {
890        let Expr::RowCmpSubquery { row, op, subquery } = e else {
891            return Ok(None);
892        };
893
894        if select_is_correlated(subquery) {
895            return Ok(None);
896        }
897        let mut s = (**subquery).clone();
898        self.resolve_select_subqueries(&mut s, cancel)?;
899        let r = match self.exec_select_cancel(&s, cancel) {
900            Ok(r) => r,
901            Err(e) if is_correlation_error(&e) => return Ok(None),
902            Err(e) => return Err(e),
903        };
904        let QueryResult::Rows {
905            columns, mut rows, ..
906        } = r
907        else {
908            return Err(EngineError::Unsupported(
909                "row comparison subquery: inner statement did not return rows".into(),
910            ));
911        };
912        // Zero rows → NULL (scalar-subquery rule); >1 rows is an error.
913        if rows.is_empty() {
914            return Ok(Some(Expr::Literal(Literal::Null)));
915        }
916        if rows.len() > 1 {
917            return Err(EngineError::CardinalityViolation);
918        }
919        if columns.len() != row.len() {
920            return Err(EngineError::Unsupported(alloc::format!(
921                "row comparison: left side has {} column(s), subquery returns {}",
922                row.len(),
923                columns.len()
924            )));
925        }
926        let rhs: Vec<Expr> = rows
927            .remove(0)
928            .values
929            .into_iter()
930            .map(value_to_literal_expr)
931            .collect::<Result<_, _>>()?;
932        // Defer the left row's evaluation to the row loop by returning
933        // the built comparison expression (its 3VL is correct).
934        Ok(Some(build_row_comparison(row, *op, &rhs)))
935    }
936
937    /// The `Expr::RowInSubquery` arm of `subquery_replacement`, lifted out.
938    ///
939    /// `#[inline(never)]`: `subquery_replacement` recurses, and a debug
940    /// build keeps EVERY arm's locals — each of these clones a
941    /// `SelectStatement`, 800 bytes before its contents — in the frame
942    /// whichever arm runs. The frame measured 32,928 bytes and the
943    /// deepest descent of one nested query holds two of them.
944    ///
945    /// Taking `e` whole and re-binding here keeps the original
946    /// bindings and their types exactly; the `else` arm cannot happen,
947    /// since the caller dispatches on this variant.
948    #[inline(never)]
949    fn arm_row_in_subquery(
950        &self,
951        e: &Expr,
952        cancel: CancelToken<'_>,
953    ) -> Result<Option<Expr>, EngineError> {
954        let Expr::RowInSubquery {
955            row,
956            subquery,
957            negated,
958        } = e
959        else {
960            return Ok(None);
961        };
962
963        use alloc::boxed::Box;
964        // Correlated → per-row `resolve_correlated_in_expr` handles
965        // it; leave the node in place.
966        if select_is_correlated(subquery) {
967            return Ok(None);
968        }
969        let mut s = (**subquery).clone();
970        self.resolve_select_subqueries(&mut s, cancel)?;
971        let r = match self.exec_select_cancel(&s, cancel) {
972            Ok(r) => r,
973            Err(e) if is_correlation_error(&e) => return Ok(None),
974            Err(e) => return Err(e),
975        };
976        let QueryResult::Rows { columns, rows, .. } = r else {
977            return Err(EngineError::Unsupported(
978                "row IN-subquery: inner statement did not return rows".into(),
979            ));
980        };
981        if columns.len() != row.len() {
982            return Err(EngineError::Unsupported(alloc::format!(
983                "row IN-subquery: left side has {} column(s), subquery returns {}",
984                row.len(),
985                columns.len()
986            )));
987        }
988        // Uncorrelated: the subquery's rows are now constants, so fold
989        // to `(a=r1c1 AND …) OR (a=r2c1 AND …) …`. This defers the
990        // left row's evaluation to the per-row loop and reproduces
991        // PG's row-IN three-valued logic for free (`=` / AND / OR all
992        // propagate NULL). An empty result is `false`.
993        let mut alts: Vec<Expr> = Vec::with_capacity(rows.len());
994        for r0 in rows {
995            let mut conj: Option<Expr> = None;
996            for (lhs_el, v) in row.iter().zip(r0.values) {
997                let eq = Expr::Binary {
998                    lhs: Box::new(lhs_el.clone()),
999                    op: BinOp::Eq,
1000                    rhs: Box::new(value_to_literal_expr(v)?),
1001                };
1002                conj = Some(match conj {
1003                    None => eq,
1004                    Some(prev) => Expr::Binary {
1005                        lhs: Box::new(prev),
1006                        op: BinOp::And,
1007                        rhs: Box::new(eq),
1008                    },
1009                });
1010            }
1011            if let Some(c) = conj {
1012                alts.push(c);
1013            }
1014        }
1015        let combined = match alts.into_iter().reduce(|acc, e| Expr::Binary {
1016            lhs: Box::new(acc),
1017            op: BinOp::Or,
1018            rhs: Box::new(e),
1019        }) {
1020            Some(c) => c,
1021            None => Expr::Literal(Literal::Bool(false)),
1022        };
1023        let result = if *negated {
1024            Expr::Unary {
1025                op: UnOp::Not,
1026                expr: Box::new(combined),
1027            }
1028        } else {
1029            combined
1030        };
1031        Ok(Some(result))
1032    }
1033
1034    /// The `Expr::InSubquery` arm of `subquery_replacement`, lifted out.
1035    ///
1036    /// `#[inline(never)]`: `subquery_replacement` recurses, and a debug
1037    /// build keeps EVERY arm's locals — each of these clones a
1038    /// `SelectStatement`, 800 bytes before its contents — in the frame
1039    /// whichever arm runs. The frame measured 32,928 bytes and the
1040    /// deepest descent of one nested query holds two of them.
1041    ///
1042    /// Taking `e` whole and re-binding here keeps the original
1043    /// bindings and their types exactly; the `else` arm cannot happen,
1044    /// since the caller dispatches on this variant.
1045    #[inline(never)]
1046    fn arm_in_subquery(
1047        &self,
1048        e: &Expr,
1049        cancel: CancelToken<'_>,
1050    ) -> Result<Option<Expr>, EngineError> {
1051        let Expr::InSubquery {
1052            expr,
1053            subquery,
1054            negated,
1055        } = e
1056        else {
1057            return Ok(None);
1058        };
1059
1060        if select_is_correlated(subquery) {
1061            return Ok(None);
1062        }
1063        let mut s = (**subquery).clone();
1064        self.resolve_select_subqueries(&mut s, cancel)?;
1065        let r = match self.exec_select_cancel(&s, cancel) {
1066            Ok(r) => r,
1067            Err(e) if is_correlation_error(&e) => return Ok(None),
1068            Err(e) => return Err(e),
1069        };
1070        let QueryResult::Rows { columns, rows, .. } = r else {
1071            return Err(EngineError::Unsupported(
1072                "IN-subquery: inner statement did not return rows".into(),
1073            ));
1074        };
1075        if columns.len() != 1 {
1076            // v7.39 (round 341, V66) — PG's two wordings, measured
1077            // on 18.4: `subquery has too few columns` /
1078            // `subquery has too many columns`. SPG named its own
1079            // internal shape ("IN-subquery must project exactly
1080            // one column; got 0").
1081            return Err(EngineError::Unsupported(
1082                if columns.is_empty() {
1083                    "subquery has too few columns"
1084                } else {
1085                    "subquery has too many columns"
1086                }
1087                .into(),
1088            ));
1089        }
1090        // v7.30.2 (mailrs round-25) — flat InList, NOT an OR-Eq
1091        // chain: chain depth scaled with the inner result's ROW
1092        // COUNT, so one 24k-match search overflowed the worker
1093        // stack (recursive eval + recursive Box drop) and
1094        // aborted the embedding host process.
1095        let mut list: Vec<Expr> = Vec::with_capacity(rows.len());
1096        for row in rows {
1097            let v = row.values.into_iter().next().unwrap_or(Value::Null);
1098            list.push(value_to_literal_expr(v)?);
1099        }
1100        Ok(Some(Expr::InList {
1101            expr: expr.clone(),
1102            list,
1103            negated: *negated,
1104        }))
1105    }
1106
1107    /// The `Expr::Exists` arm of `subquery_replacement`, lifted out for the frame
1108    /// reason on `arm_in_subquery`.
1109    #[inline(never)]
1110    fn arm_exists(&self, e: &Expr, cancel: CancelToken<'_>) -> Result<Option<Expr>, EngineError> {
1111        let Expr::Exists { subquery, negated } = e else {
1112            return Ok(None);
1113        };
1114
1115        if select_is_correlated(subquery) {
1116            return Ok(None);
1117        }
1118        let mut s = (**subquery).clone();
1119        self.resolve_select_subqueries(&mut s, cancel)?;
1120        let r = match self.exec_select_cancel(&s, cancel) {
1121            Ok(r) => r,
1122            Err(e) if is_correlation_error(&e) => return Ok(None),
1123            Err(e) => return Err(e),
1124        };
1125        let exists = match r {
1126            QueryResult::Rows { rows, .. } => !rows.is_empty(),
1127            QueryResult::CommandOk { .. } => false,
1128        };
1129        let bit = if *negated { !exists } else { exists };
1130        Ok(Some(Expr::Literal(Literal::Bool(bit))))
1131    }
1132
1133    /// The `Expr::ScalarSubquery` arm of `subquery_replacement`, lifted out for the frame
1134    /// reason on `arm_in_subquery`.
1135    #[inline(never)]
1136    fn arm_scalar_subquery(
1137        &self,
1138        e: &Expr,
1139        cancel: CancelToken<'_>,
1140    ) -> Result<Option<Expr>, EngineError> {
1141        let Expr::ScalarSubquery(inner) = e else {
1142            return Ok(None);
1143        };
1144
1145        // v7.32 (R30) — a correlated subquery is resolved by
1146        // the per-row / post-LIMIT correlated path; executing
1147        // it here only to catch the correlation error first
1148        // materialises (and discards) its whole inner FROM.
1149        if select_is_correlated(inner) {
1150            return Ok(None);
1151        }
1152        let mut s = (**inner).clone();
1153        // Recurse into the inner SELECT first so nested
1154        // subqueries materialise bottom-up.
1155        self.resolve_select_subqueries(&mut s, cancel)?;
1156        let r = match self.exec_select_cancel(&s, cancel) {
1157            Ok(r) => r,
1158            Err(e) if is_correlation_error(&e) => return Ok(None),
1159            Err(e) => return Err(e),
1160        };
1161        let QueryResult::Rows { columns, rows, .. } = r else {
1162            return Err(EngineError::Unsupported(
1163                "scalar subquery: inner statement did not return rows".into(),
1164            ));
1165        };
1166        scalar_subquery_arity(columns.len())?;
1167        let value = match rows.as_slice() {
1168            [] => Value::Null,
1169            [row] => row.values.first().cloned().unwrap_or(Value::Null),
1170            _ => {
1171                return Err(EngineError::CardinalityViolation);
1172            }
1173        };
1174        // v7.39.12 — hand the conversion what the subquery DECLARED,
1175        // because some types are not recoverable from the value.
1176        Ok(Some(crate::substitute::value_to_literal_expr_typed(
1177            value,
1178            columns.first().map(|c| c.ty),
1179        )?))
1180    }
1181
1182    pub(crate) fn subquery_replacement(
1183        &self,
1184        e: &Expr,
1185        cancel: CancelToken<'_>,
1186    ) -> Result<Option<Expr>, EngineError> {
1187        match e {
1188            Expr::ScalarSubquery(..) => self.arm_scalar_subquery(e, cancel),
1189            Expr::Exists { .. } => self.arm_exists(e, cancel),
1190            Expr::InSubquery { .. } => self.arm_in_subquery(e, cancel),
1191            Expr::RowInSubquery { .. } => self.arm_row_in_subquery(e, cancel),
1192            Expr::RowCmpSubquery { .. } => self.arm_row_cmp_subquery(e, cancel),
1193            _ => Ok(None),
1194        }
1195    }
1196}
1197
1198impl Engine {
1199    /// v7.29 (round-22 phase 3) — try to batch-evaluate a correlated
1200    /// scalar subquery of the shape
1201    ///   (SELECT expr FROM … WHERE inner_preds AND inner_col = outer_col
1202    ///    [ORDER BY o [DESC]] [LIMIT 1])
1203    /// by running the subquery ONCE without the correlation and
1204    /// folding rows into a key→value map (group top-1 when ordered).
1205    /// Returns None when the shape doesn't qualify; correctness then
1206    /// falls back to per-row execution.
1207    pub(crate) fn try_batch_correlated_scalar(
1208        &self,
1209        inner: &SelectStatement,
1210        restrict: Option<(&[Row<'static>], &EvalContext<'_>)>,
1211        cancel: CancelToken<'_>,
1212    ) -> Result<Option<memoize::GroupMap>, EngineError> {
1213        use spg_sql::ast::{BinOp, SelectItem as SI};
1214        if !inner.ctes.is_empty()
1215            || !inner.unions.is_empty()
1216            || inner.group_by.is_some()
1217            || inner.having.is_some()
1218            || inner.distinct
1219            || inner.items.len() != 1
1220            || inner.order_by.len() > 1
1221            || inner.offset.is_some()
1222        {
1223            return Ok(None);
1224        }
1225        // LIMIT must be absent or literally 1 (top-1 semantics).
1226        if let Some(le) = &inner.limit
1227            && le.as_literal() != Some(1)
1228        {
1229            return Ok(None);
1230        }
1231        let Some(from) = &inner.from else {
1232            return Ok(None);
1233        };
1234        if from.primary.lateral_subquery.is_some() || from.primary.unnest_expr.is_some() {
1235            return Ok(None);
1236        }
1237        // Inner alias set.
1238        let mut inner_aliases: Vec<String> = Vec::new();
1239        inner_aliases.push(
1240            from.primary
1241                .alias
1242                .clone()
1243                .unwrap_or_else(|| from.primary.name.clone()),
1244        );
1245        for j in &from.joins {
1246            if j.table.lateral_subquery.is_some() || j.table.unnest_expr.is_some() {
1247                return Ok(None);
1248            }
1249            inner_aliases.push(
1250                j.table
1251                    .alias
1252                    .clone()
1253                    .unwrap_or_else(|| j.table.name.clone()),
1254            );
1255        }
1256        let is_inner = |c: &spg_sql::ast::ColumnName| -> bool {
1257            match &c.qualifier {
1258                Some(q) => inner_aliases.iter().any(|a| a.eq_ignore_ascii_case(q)),
1259                None => false,
1260            }
1261        };
1262        let is_outer = |c: &spg_sql::ast::ColumnName| -> bool {
1263            match &c.qualifier {
1264                Some(q) => !inner_aliases.iter().any(|a| a.eq_ignore_ascii_case(q)),
1265                // Synthetic group columns arrive bare after the
1266                // aggregate rewrite.
1267                None => c.name.starts_with("__grp_") || c.name.starts_with("__agg_"),
1268            }
1269        };
1270        // Every expression OTHER than the correlation conjunct must be
1271        // fully inner (qualified to inner aliases).
1272        let all_inner = |e: &Expr| -> bool {
1273            let mut cols: Vec<spg_sql::ast::ColumnName> = Vec::new();
1274            let mut subs: Vec<&SelectStatement> = Vec::new();
1275            visit_expr_columns_and_subqueries(e, &mut |c| cols.push(c.clone()), &mut |sub| {
1276                subs.push(sub)
1277            });
1278            subs.is_empty() && cols.iter().all(|c| is_inner(c) && !c.name.is_empty())
1279        };
1280        let Some(w) = &inner.where_ else {
1281            return Ok(None);
1282        };
1283        let conjuncts = reorder::split_and_conjunctions(w);
1284        let mut corr: Option<(spg_sql::ast::ColumnName, spg_sql::ast::ColumnName)> = None; // (inner, outer)
1285        let mut rest: Vec<&Expr> = Vec::new();
1286        for c in conjuncts {
1287            if let Expr::Binary {
1288                lhs,
1289                op: BinOp::Eq,
1290                rhs,
1291            } = c
1292                && let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref())
1293            {
1294                let pair = if is_inner(a) && is_outer(b) {
1295                    Some((a.clone(), b.clone()))
1296                } else if is_inner(b) && is_outer(a) {
1297                    Some((b.clone(), a.clone()))
1298                } else {
1299                    None
1300                };
1301                if let Some(p) = pair {
1302                    if corr.is_some() {
1303                        return Ok(None); // more than one correlation
1304                    }
1305                    corr = Some(p);
1306                    continue;
1307                }
1308            }
1309            if !all_inner(c) {
1310                return Ok(None);
1311            }
1312            rest.push(c);
1313        }
1314        let Some((inner_col, outer_col)) = corr else {
1315            return Ok(None);
1316        };
1317        let SI::Expr { expr: out_expr, .. } = &inner.items[0] else {
1318            return Ok(None);
1319        };
1320        if !all_inner(out_expr) {
1321            return Ok(None);
1322        }
1323        let order = inner.order_by.first();
1324        if let Some(o) = order
1325            && !all_inner(&o.expr)
1326        {
1327            return Ok(None);
1328        }
1329        // Build the batch statement: SELECT inner_col, [order], expr
1330        // FROM … WHERE rest — no correlation, no order, no limit.
1331        let mut batch = inner.clone();
1332        batch.limit = None;
1333        batch.offset = None;
1334        batch.order_by = Vec::new();
1335        batch.where_ = rest
1336            .iter()
1337            .map(|e| (*e).clone())
1338            .reduce(|a, b| Expr::Binary {
1339                lhs: alloc::boxed::Box::new(a),
1340                op: BinOp::And,
1341                rhs: alloc::boxed::Box::new(b),
1342            });
1343        let mut items: Vec<SI> = alloc::vec![SI::Expr {
1344            expr: Expr::Column(inner_col.clone()),
1345            alias: None,
1346        }];
1347        if let Some(o) = order {
1348            items.push(SI::Expr {
1349                expr: o.expr.clone(),
1350                alias: None,
1351            });
1352        }
1353        items.push(SI::Expr {
1354            expr: out_expr.clone(),
1355            alias: None,
1356        });
1357        batch.items = items;
1358        // v7.37.x (docker-fair SCALARSQ-aggregate path) — when the
1359        // inner output expression is an aggregate (e.g. `COUNT(*)`
1360        // for the `(SELECT COUNT(*) FROM inner WHERE inner.k =
1361        // outer.k)` scalar subquery shape), the batch query
1362        // `SELECT inner.k, COUNT(*) FROM inner` is invalid SQL
1363        // without `GROUP BY inner.k`. Inject the GROUP BY so the
1364        // aggregate executor produces (key → count) pairs, matching
1365        // the per-key scalar-subquery semantics. Pre-7.37.x this
1366        // case mis-executed as a single anonymous group and either
1367        // returned a wrong total or surfaced an `UnknownQualifier`
1368        // (when the rewriter couldn't bind the bare column ref).
1369        if aggregate::contains_aggregate(out_expr) {
1370            batch.group_by = Some(alloc::vec![Expr::Column(inner_col.clone())]);
1371        }
1372        // v7.32 (architecture v2 P3) — keyed index-probe. When the
1373        // caller hands a restriction set (the ≤LIMIT surviving outer
1374        // rows of a post-LIMIT deferred subquery) AND the correlation
1375        // column is backed by an index, evaluate only the surviving
1376        // correlation keys via per-key index seek instead of scanning
1377        // the whole inner relation. This is PG's SubPlan with an index
1378        // scan: 50 seeks of ~µs each vs a 24k-row all-keys batch
1379        // (~16 ms). The grouping below is shared — keyed result ≡
1380        // full-batch result for the covered keys, so semantics are
1381        // identical.
1382        //
1383        // The inner relation may itself be a join. The correlation
1384        // column names the *driving* table; PG, MySQL and MariaDB all
1385        // plan a correlated join subquery the same way — seek the
1386        // correlation index, then index-nested-loop to the joined
1387        // table. We promote that table to drive `batch` (an all-INNER
1388        // chain only) so the per-key `inner_col = <lit>` predicate
1389        // becomes a primary index seek and the existing INL path joins
1390        // the rest. A correlation column without a usable index, or a
1391        // join the promotion can't safely reorder, returns None and
1392        // the caller falls back to the lazy all-keys batch (no
1393        // regression).
1394        let keyed: Option<(&[Row<'static>], &EvalContext<'_>)> =
1395            restrict.and_then(|(rows, rctx)| {
1396                // Resolve the table that owns the correlation column.
1397                let driver_name: &str = if from.joins.is_empty() {
1398                    from.primary.name.as_str()
1399                } else {
1400                    let q = inner_col.qualifier.as_deref()?;
1401                    let primary_alias = from
1402                        .primary
1403                        .alias
1404                        .as_deref()
1405                        .unwrap_or(from.primary.name.as_str());
1406                    if primary_alias.eq_ignore_ascii_case(q) {
1407                        from.primary.name.as_str()
1408                    } else {
1409                        from.joins
1410                            .iter()
1411                            .find(|j| {
1412                                j.table
1413                                    .alias
1414                                    .as_deref()
1415                                    .unwrap_or(j.table.name.as_str())
1416                                    .eq_ignore_ascii_case(q)
1417                            })
1418                            .map(|j| j.table.name.as_str())?
1419                    }
1420                };
1421                let table = self.active_catalog().get(driver_name)?;
1422                let pos = table
1423                    .schema()
1424                    .columns
1425                    .iter()
1426                    .position(|c| c.name.eq_ignore_ascii_case(&inner_col.name))?;
1427                table.index_on(pos)?;
1428                // v7.33 (mailrs 7.32.1) — cost guard. The keyed path runs one
1429                // index seek (a full `exec_select_cancel` round trip) per
1430                // surviving correlation key. That wins when few keys survive
1431                // (a tight outer LIMIT leaves a handful), but a *correlated
1432                // select-list subquery with no outer LIMIT* leaves every group
1433                // alive — `restrict` is then all ~N groups, and N seeks dwarf
1434                // a single grouped all-keys scan of the same driver. Reproduced
1435                // on the conversation aggregation (`get_conversations_by_thread_ids`,
1436                // no LIMIT): 24k per-key seeks took 78–155 ms vs ~one scan.
1437                // Fall through to the all-keys batch (`keyed = None` → the
1438                // `else` arm below) when the survivor set is large relative to
1439                // the driver; the batch's group map ⊇ the keyed map for every
1440                // covered key, so the result is identical. Crossover ~rows/4
1441                // (measured per-seek exec overhead vs per-row scan cost).
1442                if rows.len().saturating_mul(4) >= table.row_count() {
1443                    return None;
1444                }
1445                // For a join inner, drive the seek from the correlation
1446                // table so `inner_col = <lit>` lands as a primary index
1447                // seek (else the source-order primary scans the full
1448                // relation and the join hash-builds the whole peer — the
1449                // 12 GB all-keys hog R30 hit at prod scale).
1450                if !from.joins.is_empty() {
1451                    let driver_alias = inner_col.qualifier.as_deref()?;
1452                    if !reorder::drive_from(&mut batch, driver_alias) {
1453                        return None;
1454                    }
1455                }
1456                Some((rows, rctx))
1457            });
1458        let rows = if let Some((restrict_rows, rctx)) = keyed {
1459            BATCHED_SCALAR_KEYED_FIRE_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
1460            // v7.37.4 A' — collect the deduped surviving correlation
1461            // keys, then issue ONE `inner.k IN (lit1, …, litN)` probe
1462            // instead of N separate `inner.k = lit` probes. The v7.34.3
1463            // IN-list seek path treats the literal list as a bitmap-
1464            // style index sweep (single index lookup per literal,
1465            // unioned), so the total cost is O(N seeks + matched rows)
1466            // — same asymptotic as the N-probe loop but without N
1467            // rounds of stmt clone + plan + executor stack overhead.
1468            //
1469            // Per-probe overhead measured on mailrs prod 100k:
1470            //   - sequential: 50 probes × ~1.7 ms = ~85 ms per subq
1471            //   - 3 subqueries × ~85 ms = ~255 ms of the 388 ms total
1472            // IN-list batched probe is one stmt + N IN-list literals,
1473            // amortising the plan + setup over all keys.
1474            let mut seen: alloc::collections::BTreeSet<String> =
1475                alloc::collections::BTreeSet::new();
1476            let mut key_lits: Vec<Expr> = Vec::new();
1477            for srow in restrict_rows {
1478                cancel.check()?;
1479                let kv = eval::eval_expr(&Expr::Column(outer_col.clone()), srow, rctx)
1480                    .map_err(EngineError::Eval)?;
1481                if matches!(kv, Value::Null) {
1482                    continue;
1483                }
1484                if !seen.insert(aggregate::encode_key(core::slice::from_ref(&kv))) {
1485                    continue;
1486                }
1487                key_lits.push(value_to_literal_expr(kv)?);
1488            }
1489            if key_lits.is_empty() {
1490                Vec::new()
1491            } else {
1492                let in_pred = Expr::InList {
1493                    expr: alloc::boxed::Box::new(Expr::Column(inner_col.clone())),
1494                    list: key_lits,
1495                    negated: false,
1496                };
1497                let mut probe = batch.clone();
1498                probe.where_ = Some(match probe.where_.take() {
1499                    Some(w) => Expr::Binary {
1500                        lhs: alloc::boxed::Box::new(w),
1501                        op: BinOp::And,
1502                        rhs: alloc::boxed::Box::new(in_pred),
1503                    },
1504                    None => in_pred,
1505                });
1506                BATCHED_SCALAR_KEYED_PROBE_COUNT
1507                    .fetch_add(1, core::sync::atomic::Ordering::Relaxed);
1508                if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&probe, cancel)? {
1509                    rows
1510                } else {
1511                    Vec::new()
1512                }
1513            }
1514        } else {
1515            BATCHED_SCALAR_FALL_THROUGH_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
1516            let r = self.exec_select_cancel(&batch, cancel)?;
1517            let QueryResult::Rows { rows, .. } = r else {
1518                return Ok(None);
1519            };
1520            rows
1521        };
1522        let has_order = order.is_some();
1523        let (desc, nf) = order
1524            .map(|o| (o.desc, o.nulls_first))
1525            .unwrap_or((false, None));
1526        let mut best: alloc::collections::BTreeMap<String, (Option<Value>, Value)> =
1527            alloc::collections::BTreeMap::new();
1528        for row in rows {
1529            let key_v = row.values.first().cloned().unwrap_or(Value::Null);
1530            if matches!(key_v, Value::Null) {
1531                continue;
1532            }
1533            let key = aggregate::encode_key(core::slice::from_ref(&key_v));
1534            let (ord_v, out_v) = if has_order {
1535                (
1536                    Some(row.values.get(1).cloned().unwrap_or(Value::Null)),
1537                    row.values.get(2).cloned().unwrap_or(Value::Null),
1538                )
1539            } else {
1540                (None, row.values.get(1).cloned().unwrap_or(Value::Null))
1541            };
1542            match best.get(&key) {
1543                None => {
1544                    best.insert(key, (ord_v, out_v));
1545                }
1546                Some((cur_ord, _)) if has_order => {
1547                    // The sorted-first row wins: candidate beats the
1548                    // incumbent when it compares LESS under the key's
1549                    // ordering.
1550                    let cand = ord_v.clone().unwrap_or(Value::Null);
1551                    let cur = cur_ord.clone().unwrap_or(Value::Null);
1552                    if order_by_value_cmp(desc, nf, &cand, &cur) == core::cmp::Ordering::Less {
1553                        best.insert(key, (ord_v, out_v));
1554                    }
1555                }
1556                Some(_) => {} // unordered: first row stands (any row is valid)
1557            }
1558        }
1559        let map = best.into_iter().map(|(k, (_, v))| (k, v)).collect();
1560        // v7.37.x (docker-fair SCALARSQ attack) — empty-default per
1561        // PG scalar-subquery aggregate semantics. Captured here so the
1562        // splice path doesn't have to re-introspect a possibly-hollowed
1563        // inner template.
1564        let empty_default = scalar_subquery_empty_default(inner);
1565        Ok(Some((outer_col, map, empty_default)))
1566    }
1567}
1568
1569impl Engine {
1570    /// v7.34 (mailrs conn-pool-exhaustion P0) — decorrelate a correlated
1571    /// `[NOT] EXISTS` into a hash semi/anti-join. Recognise
1572    ///   EXISTS (SELECT … FROM t [joins]
1573    ///           WHERE k1 = o1 AND … AND kN = oN AND <inner-preds>)
1574    /// run the inner ONCE without the correlation, collect the set of
1575    /// inner key-tuples `(k1,…,kN)` that satisfy the inner-preds; an outer
1576    /// row's EXISTS then reduces to a membership test on `(o1,…,oN)`. The
1577    /// reported `count_unseen` ran two correlated `NOT EXISTS` per ~24k
1578    /// join survivors (~48k inner executions, 98.7% of a 1.4 s query);
1579    /// this turns each into one scan + 24k lookups.
1580    ///
1581    /// Multi-column correlation is supported (the prod `snoozed` anti-join
1582    /// correlates on both `thread_id` and `account_address`). NULL is
1583    /// exact: an outer key with any NULL component is never present
1584    /// (`NULL = k` is never true), so EXISTS=false / NOT EXISTS=true,
1585    /// identical to the per-row resolver. Returns None when the shape
1586    /// doesn't qualify — the caller falls back to per-row execution, so
1587    /// there is no regression.
1588    pub(crate) fn try_batch_correlated_exists(
1589        &self,
1590        inner: &SelectStatement,
1591        cancel: CancelToken<'_>,
1592    ) -> Result<Option<memoize::ExistsSet>, EngineError> {
1593        use spg_sql::ast::SelectItem as SI;
1594        if !inner.ctes.is_empty()
1595            || !inner.unions.is_empty()
1596            || inner.group_by.is_some()
1597            || inner.having.is_some()
1598            || inner.distinct
1599        {
1600            return Ok(None);
1601        }
1602        let Some(from) = &inner.from else {
1603            return Ok(None);
1604        };
1605        if from.primary.lateral_subquery.is_some()
1606            || from.primary.unnest_expr.is_some()
1607            || from.primary.generate_series_args.is_some()
1608            || from.primary.as_of_segment.is_some()
1609        {
1610            return Ok(None);
1611        }
1612        let mut inner_aliases: Vec<String> = Vec::new();
1613        inner_aliases.push(
1614            from.primary
1615                .alias
1616                .clone()
1617                .unwrap_or_else(|| from.primary.name.clone()),
1618        );
1619        for j in &from.joins {
1620            if j.table.lateral_subquery.is_some() || j.table.unnest_expr.is_some() {
1621                return Ok(None);
1622            }
1623            inner_aliases.push(
1624                j.table
1625                    .alias
1626                    .clone()
1627                    .unwrap_or_else(|| j.table.name.clone()),
1628            );
1629        }
1630        let is_inner = |c: &spg_sql::ast::ColumnName| -> bool {
1631            match &c.qualifier {
1632                Some(q) => inner_aliases.iter().any(|a| a.eq_ignore_ascii_case(q)),
1633                None => false,
1634            }
1635        };
1636        let is_outer = |c: &spg_sql::ast::ColumnName| -> bool {
1637            match &c.qualifier {
1638                Some(q) => !inner_aliases.iter().any(|a| a.eq_ignore_ascii_case(q)),
1639                None => c.name.starts_with("__grp_") || c.name.starts_with("__agg_"),
1640            }
1641        };
1642        let all_inner = |e: &Expr| -> bool {
1643            let mut cols: Vec<spg_sql::ast::ColumnName> = Vec::new();
1644            let mut subs: Vec<&SelectStatement> = Vec::new();
1645            visit_expr_columns_and_subqueries(e, &mut |c| cols.push(c.clone()), &mut |sub| {
1646                subs.push(sub)
1647            });
1648            subs.is_empty() && cols.iter().all(|c| is_inner(c) && !c.name.is_empty())
1649        };
1650        let Some(w) = &inner.where_ else {
1651            return Ok(None);
1652        };
1653        let conjuncts = reorder::split_and_conjunctions(w);
1654        // v7.39 (round 596) — the outer side of a correlation may be an
1655        // EXPRESSION over outer columns, not only a bare column. Deliberately
1656        // an allowlist of node kinds rather than "does it only mention outer
1657        // columns": a node the walk did not know about, or a function whose
1658        // volatility SPG cannot look up, would both be admitted silently, and
1659        // a volatile key would probe the wrong bucket. Same rule round 590
1660        // used for the computed JOIN key.
1661        let outer_only_key = |e: &Expr| -> bool {
1662            fn shape(e: &Expr, is_outer: &dyn Fn(&spg_sql::ast::ColumnName) -> bool) -> bool {
1663                use spg_sql::ast::BinOp as B;
1664                match e {
1665                    Expr::Column(c) => is_outer(c),
1666                    Expr::Literal(_) => true,
1667                    Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => shape(expr, is_outer),
1668                    Expr::Binary { lhs, op, rhs } => {
1669                        matches!(op, B::Add | B::Sub | B::Mul | B::Div | B::IntDiv | B::Mod)
1670                            && shape(lhs, is_outer)
1671                            && shape(rhs, is_outer)
1672                    }
1673                    _ => false,
1674                }
1675            }
1676            fn mentions_column(e: &Expr) -> bool {
1677                match e {
1678                    Expr::Column(_) => true,
1679                    Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => mentions_column(expr),
1680                    Expr::Binary { lhs, rhs, .. } => mentions_column(lhs) || mentions_column(rhs),
1681                    _ => false,
1682                }
1683            }
1684            shape(e, &is_outer) && mentions_column(e)
1685        };
1686        let mut inner_keys: Vec<spg_sql::ast::ColumnName> = Vec::new();
1687        let mut outer_cols: Vec<Expr> = Vec::new();
1688        let mut rest: Vec<&Expr> = Vec::new();
1689        for c in conjuncts {
1690            if let Expr::Binary {
1691                lhs,
1692                op: BinOp::Eq,
1693                rhs,
1694            } = c
1695            {
1696                let pair = match (lhs.as_ref(), rhs.as_ref()) {
1697                    (Expr::Column(a), other) if is_inner(a) && outer_only_key(other) => {
1698                        Some((a.clone(), other.clone()))
1699                    }
1700                    (other, Expr::Column(b)) if is_inner(b) && outer_only_key(other) => {
1701                        Some((b.clone(), other.clone()))
1702                    }
1703                    _ => None,
1704                };
1705                if let Some((ic, oc)) = pair {
1706                    inner_keys.push(ic);
1707                    outer_cols.push(oc);
1708                    continue;
1709                }
1710            }
1711            // A non-correlation conjunct must be purely inner (carried
1712            // into the build scan). Anything else (outer-only filter,
1713            // mixed expression) is beyond this rewrite.
1714            if !all_inner(c) {
1715                return Ok(None);
1716            }
1717            rest.push(c);
1718        }
1719        if inner_keys.is_empty() {
1720            return Ok(None); // uncorrelated — materialised elsewhere
1721        }
1722        // Build: SELECT k1,…,kN FROM <inner from> WHERE <rest> — no
1723        // correlation, no order/limit. The inner relation may be a join;
1724        // exec handles it.
1725        let mut batch = inner.clone();
1726        batch.limit = None;
1727        batch.offset = None;
1728        batch.order_by = Vec::new();
1729        batch.distinct = false;
1730        batch.where_ = rest
1731            .iter()
1732            .map(|e| (*e).clone())
1733            .reduce(|a, b| Expr::Binary {
1734                lhs: alloc::boxed::Box::new(a),
1735                op: BinOp::And,
1736                rhs: alloc::boxed::Box::new(b),
1737            });
1738        batch.items = inner_keys
1739            .iter()
1740            .map(|c| SI::Expr {
1741                expr: Expr::Column(c.clone()),
1742                alias: None,
1743            })
1744            .collect();
1745        let r = self.exec_select_cancel(&batch, cancel)?;
1746        let QueryResult::Rows { rows, .. } = r else {
1747            return Ok(None);
1748        };
1749        let n = inner_keys.len();
1750        let mut set: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
1751        for row in rows {
1752            let keys = row.values.get(..n).unwrap_or(&row.values);
1753            // A NULL key component can never satisfy `k = outer`, so the
1754            // tuple matches no outer row — drop it from the set.
1755            if keys.iter().any(|v| matches!(v, Value::Null)) {
1756                continue;
1757            }
1758            set.insert(aggregate::encode_canonical_key(keys));
1759        }
1760        Ok(Some((outer_cols, set)))
1761    }
1762}
1763
1764impl Engine {
1765    /// v7.33 (mailrs 7.32.1) — sublink pull-up for aggregate-wrapped
1766    /// correlated scalar subqueries. Rewrite
1767    ///   AGG( (SELECT j_col FROM t j WHERE j.key = outer.col [AND inner preds]) )
1768    /// into a LEFT JOIN plus a plain column reference:
1769    ///   AGG(j.j_col) … LEFT JOIN t AS j ON j.key = outer.col [AND inner preds]
1770    /// when `t.key` carries a single-column UNIQUE / PRIMARY KEY constraint.
1771    /// That constraint guarantees the join matches AT MOST ONE inner row
1772    /// per outer row, which is exactly the scalar subquery's at-most-one
1773    /// contract (NULL on no match), so the aggregate folds an identical
1774    /// per-row value stream — only now the executor streams one join
1775    /// instead of splicing a per-row subplan (the R31 path cloned a hollow
1776    /// template per outer row: ~24k clones for the mailrs conversation
1777    /// aggregation).
1778    ///
1779    /// Scoped tightly for safety: the subquery must sit inside an aggregate
1780    /// argument (so the joined column is always folded, never a bare
1781    /// select-list column a GROUP BY would reject); the inner must be a
1782    /// single plain-table scan projecting one inner column with exactly one
1783    /// `inner.key = outer.col` correlation (both qualified) plus optional
1784    /// all-inner predicates; and the select list must have no bare wildcard
1785    /// (a join would widen `*`). Anything else is left for the existing
1786    /// per-row / batch resolver. Returns true when it rewrote at least one.
1787    /// v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC subquery pullup) —
1788    /// plan-time rewrite of the "per-key latest" select-list scalar
1789    /// subquery pattern:
1790    ///
1791    /// ```sql
1792    /// SELECT outer.k,
1793    ///        (SELECT proj_expr FROM inner
1794    ///          WHERE inner.k = outer.k AND <non_corr_preds>
1795    ///          ORDER BY sort_key DESC LIMIT 1) AS latest_proj
1796    ///   FROM outer
1797    /// ```
1798    ///
1799    /// becomes (semantically equivalent, executor-friendly):
1800    ///
1801    /// ```sql
1802    /// WITH __cl1_N AS (
1803    ///   SELECT inner.k AS jk,
1804    ///          (array_agg(proj_expr ORDER BY sort_key DESC NULLS LAST))[1] AS pj
1805    ///     FROM <inner.from>
1806    ///    WHERE <non_corr_preds>
1807    ///    GROUP BY inner.k
1808    /// )
1809    /// SELECT outer.k, MAX(__cl1_N.pj) AS latest_proj
1810    ///   FROM outer LEFT JOIN __cl1_N ON __cl1_N.jk = outer.k
1811    /// ```
1812    ///
1813    /// The CTE materialises once for the whole outer scan; LEFT JOIN
1814    /// on the GROUP-BY-unique `jk` column never multiplies outer rows.
1815    /// The `array_agg(... ORDER BY ...)[1]` form reuses the v7.33
1816    /// `first_ordered` argmax executor (per-group keep the first row,
1817    /// no array build).
1818    ///
1819    /// Common shape across inbox / feed / timeline applications:
1820    /// thread latest message, user latest transaction, device latest
1821    /// heartbeat. **Not a mailrs-specific patch** — any client query
1822    /// in this shape gets the rewrite.
1823    ///
1824    /// Acceptance (`try_pull_up_limit_one`):
1825    /// - inner: single SELECT, LIMIT 1 + ORDER BY <expr>, no GROUP BY /
1826    ///   HAVING / DISTINCT / CTE / UNION / OFFSET, single projection
1827    /// - inner FROM: may contain JOINs (INNER) over plain tables; no
1828    ///   LATERAL / UNNEST / generate_series / AS OF; no outer reference
1829    ///   inside join ON
1830    /// - WHERE: exactly one `inner.k = outer.col` (qualified columns)
1831    ///   + non-correlated all-inner predicates
1832    /// - projection: scalar expression, no aggregates / windows
1833    /// - outer: SelectStatement with FROM, no wildcards
1834    ///
1835    /// Returns true when at least one ScalarSubquery was rewritten.
1836    /// Returns false (no-op) when nothing in the statement matches —
1837    /// the existing per-row resolver then handles whatever's left.
1838    pub(crate) fn pull_up_correlated_limit_one_subqueries(
1839        &self,
1840        stmt: &mut SelectStatement,
1841    ) -> bool {
1842        // Phase 5 differential knob: an `AtomicBool` switch will land
1843        // alongside the byte-equal differential e2e (no_std rules out
1844        // std::env::var here). Production keeps the pass default-on.
1845        //
1846        // Outer FROM required (no FROM → nothing to JOIN against);
1847        // outer wildcards (`SELECT *`) widen the projection and would
1848        // surface the joined CTE's columns — refuse for safety.
1849        if stmt.from.is_none() || stmt.items.iter().any(|i| matches!(i, SelectItem::Wildcard)) {
1850            return false;
1851        }
1852        // Aliases an outer-correlation column may qualify to. Same
1853        // collection rule as `pull_up_unique_correlated_agg_subqueries`.
1854        let outer_aliases: alloc::collections::BTreeSet<String> = {
1855            let from = stmt.from.as_ref().expect("from present");
1856            let mut s = alloc::collections::BTreeSet::new();
1857            let push = |s: &mut alloc::collections::BTreeSet<String>, t: &TableRef| {
1858                s.insert(
1859                    t.alias
1860                        .clone()
1861                        .unwrap_or_else(|| t.name.clone())
1862                        .to_ascii_lowercase(),
1863                );
1864            };
1865            push(&mut s, &from.primary);
1866            for j in &from.joins {
1867                push(&mut s, &j.table);
1868            }
1869            s
1870        };
1871        let outer_has_group_by = stmt.group_by.is_some() || stmt.group_by_all;
1872        let mut new_ctes: Vec<Cte> = Vec::new();
1873        let mut new_joins: Vec<FromJoin> = Vec::new();
1874        let cte_seed = stmt.ctes.len();
1875        for item in &mut stmt.items {
1876            if let SelectItem::Expr { expr, .. } = item {
1877                self.pull_up_walk_limit_one(
1878                    expr,
1879                    false,
1880                    &outer_aliases,
1881                    outer_has_group_by,
1882                    cte_seed,
1883                    &mut new_ctes,
1884                    &mut new_joins,
1885                );
1886            }
1887        }
1888        if new_ctes.is_empty() {
1889            return false;
1890        }
1891        PULLUP_LIMIT1_FIRE_COUNT
1892            .fetch_add(new_ctes.len() as u64, core::sync::atomic::Ordering::Relaxed);
1893        stmt.ctes.extend(new_ctes);
1894        stmt.from
1895            .as_mut()
1896            .expect("from present")
1897            .joins
1898            .extend(new_joins);
1899        true
1900    }
1901
1902    /// v7.37.4 — recursive mutable walk over a select-list expression
1903    /// for the LIMIT 1 pullup. Tracks `in_agg` so a ScalarSubquery
1904    /// already inside an aggregate doesn't get a redundant MAX wrapper
1905    /// (the outer aggregate folds whatever cell value the join supplies).
1906    #[allow(clippy::too_many_arguments)]
1907    fn pull_up_walk_limit_one(
1908        &self,
1909        e: &mut Expr,
1910        in_agg: bool,
1911        outer_aliases: &alloc::collections::BTreeSet<String>,
1912        outer_has_group_by: bool,
1913        cte_seed: usize,
1914        ctes_out: &mut Vec<Cte>,
1915        joins_out: &mut Vec<FromJoin>,
1916    ) {
1917        match e {
1918            Expr::ScalarSubquery(inner) => {
1919                if let Some((cte, join, cte_col)) =
1920                    self.try_pull_up_limit_one(inner, outer_aliases, cte_seed + ctes_out.len())
1921                {
1922                    ctes_out.push(cte);
1923                    joins_out.push(join);
1924                    // Outer needs a single scalar per outer row. With a
1925                    // LEFT JOIN against the CTE (sq.jk UNIQUE by GROUP
1926                    // BY), sq.pj is functionally a single value per
1927                    // join key — but a strict GROUP BY checker won't
1928                    // know that. When the outer query has its own
1929                    // GROUP BY and this position isn't already wrapped
1930                    // in an aggregate, wrap in MAX(sq.pj) so the
1931                    // checker sees an aggregate; MAX over a single
1932                    // value equals the value (any aggregate would).
1933                    let col_expr = Expr::Column(cte_col);
1934                    *e = if outer_has_group_by && !in_agg {
1935                        Expr::FunctionCall {
1936                            name: "max".into(),
1937                            args: alloc::vec![col_expr],
1938                        }
1939                    } else {
1940                        col_expr
1941                    };
1942                }
1943                // Otherwise leave for the existing per-row resolver.
1944                // The subquery body is a separate scope — don't descend.
1945            }
1946            Expr::FunctionCall { name, args } => {
1947                let child = in_agg || aggregate::is_aggregate_name(name);
1948                for a in args.iter_mut() {
1949                    self.pull_up_walk_limit_one(
1950                        a,
1951                        child,
1952                        outer_aliases,
1953                        outer_has_group_by,
1954                        cte_seed,
1955                        ctes_out,
1956                        joins_out,
1957                    );
1958                }
1959            }
1960            Expr::AggregateOrdered {
1961                call,
1962                order_by,
1963                filter,
1964                ..
1965            } => {
1966                self.pull_up_walk_limit_one(
1967                    call,
1968                    true,
1969                    outer_aliases,
1970                    outer_has_group_by,
1971                    cte_seed,
1972                    ctes_out,
1973                    joins_out,
1974                );
1975                for o in order_by.iter_mut() {
1976                    self.pull_up_walk_limit_one(
1977                        &mut o.expr,
1978                        true,
1979                        outer_aliases,
1980                        outer_has_group_by,
1981                        cte_seed,
1982                        ctes_out,
1983                        joins_out,
1984                    );
1985                }
1986                if let Some(f) = filter {
1987                    self.pull_up_walk_limit_one(
1988                        f,
1989                        true,
1990                        outer_aliases,
1991                        outer_has_group_by,
1992                        cte_seed,
1993                        ctes_out,
1994                        joins_out,
1995                    );
1996                }
1997            }
1998            Expr::Binary { lhs, rhs, .. } => {
1999                self.pull_up_walk_limit_one(
2000                    lhs,
2001                    in_agg,
2002                    outer_aliases,
2003                    outer_has_group_by,
2004                    cte_seed,
2005                    ctes_out,
2006                    joins_out,
2007                );
2008                self.pull_up_walk_limit_one(
2009                    rhs,
2010                    in_agg,
2011                    outer_aliases,
2012                    outer_has_group_by,
2013                    cte_seed,
2014                    ctes_out,
2015                    joins_out,
2016                );
2017            }
2018            Expr::Unary { expr, .. }
2019            | Expr::Cast { expr, .. }
2020            | Expr::IsNull { expr, .. }
2021            | Expr::BoolTest { expr, .. }
2022            | Expr::FieldAccess { base: expr, .. } => {
2023                self.pull_up_walk_limit_one(
2024                    expr,
2025                    in_agg,
2026                    outer_aliases,
2027                    outer_has_group_by,
2028                    cte_seed,
2029                    ctes_out,
2030                    joins_out,
2031                );
2032            }
2033            Expr::Like { expr, pattern, .. } => {
2034                self.pull_up_walk_limit_one(
2035                    expr,
2036                    in_agg,
2037                    outer_aliases,
2038                    outer_has_group_by,
2039                    cte_seed,
2040                    ctes_out,
2041                    joins_out,
2042                );
2043                self.pull_up_walk_limit_one(
2044                    pattern,
2045                    in_agg,
2046                    outer_aliases,
2047                    outer_has_group_by,
2048                    cte_seed,
2049                    ctes_out,
2050                    joins_out,
2051                );
2052            }
2053            Expr::InList { expr, list, .. } => {
2054                self.pull_up_walk_limit_one(
2055                    expr,
2056                    in_agg,
2057                    outer_aliases,
2058                    outer_has_group_by,
2059                    cte_seed,
2060                    ctes_out,
2061                    joins_out,
2062                );
2063                for it in list.iter_mut() {
2064                    self.pull_up_walk_limit_one(
2065                        it,
2066                        in_agg,
2067                        outer_aliases,
2068                        outer_has_group_by,
2069                        cte_seed,
2070                        ctes_out,
2071                        joins_out,
2072                    );
2073                }
2074            }
2075            Expr::Case {
2076                operand,
2077                branches,
2078                else_branch,
2079            } => {
2080                if let Some(o) = operand {
2081                    self.pull_up_walk_limit_one(
2082                        o,
2083                        in_agg,
2084                        outer_aliases,
2085                        outer_has_group_by,
2086                        cte_seed,
2087                        ctes_out,
2088                        joins_out,
2089                    );
2090                }
2091                for (w, t) in branches.iter_mut() {
2092                    self.pull_up_walk_limit_one(
2093                        w,
2094                        in_agg,
2095                        outer_aliases,
2096                        outer_has_group_by,
2097                        cte_seed,
2098                        ctes_out,
2099                        joins_out,
2100                    );
2101                    self.pull_up_walk_limit_one(
2102                        t,
2103                        in_agg,
2104                        outer_aliases,
2105                        outer_has_group_by,
2106                        cte_seed,
2107                        ctes_out,
2108                        joins_out,
2109                    );
2110                }
2111                if let Some(eb) = else_branch {
2112                    self.pull_up_walk_limit_one(
2113                        eb,
2114                        in_agg,
2115                        outer_aliases,
2116                        outer_has_group_by,
2117                        cte_seed,
2118                        ctes_out,
2119                        joins_out,
2120                    );
2121                }
2122            }
2123            // Same boundary policy as `pull_up_walk` — don't descend
2124            // into window calls, EXISTS, etc.
2125            _ => {}
2126        }
2127    }
2128
2129    /// v7.37.4 — decide whether a correlated scalar subquery qualifies
2130    /// for the LIMIT 1 → CTE pullup. Returns the CTE to add to outer
2131    /// `WITH`, the LEFT JOIN to append, and the (qualified) column
2132    /// that replaces the subquery node. None means: leave it for the
2133    /// per-row resolver.
2134    fn try_pull_up_limit_one(
2135        &self,
2136        inner: &SelectStatement,
2137        outer_aliases: &alloc::collections::BTreeSet<String>,
2138        alias_n: usize,
2139    ) -> Option<(Cte, FromJoin, ColumnName)> {
2140        // v7.37.4 A phase-2 finding (2026-06-19): the CTE rewrite
2141        // fires correctly on the mailrs prod subq 3 shape (verified
2142        // via PULLUP_LIMIT1_FIRE_COUNT in `pullup_fires_on_mailrs_subq3_shape`)
2143        // but PRODUCES A REGRESSION on the full prod SQL — mini cold
2144        // 100k SPGE 388.5 → 523.8 ms (+35%). Root cause:
2145        //   1. SPG's existing `try_batch_correlated_scalar` already
2146        //      handles the LIMIT 1 + ORDER BY 1 shape via post-LIMIT
2147        //      defer + keyed index seek (~ µs per surfaced outer key).
2148        //   2. The CTE form forces a full inner-table GROUP BY scan
2149        //      (~ 100 ms for 100k messages), then exec_with_ctes
2150        //      strips ctes + re-enters the body — extra catalog
2151        //      clone + double scan.
2152        //   3. Outer LIMIT 50 + GROUP BY thread_id means only ~50
2153        //      outer keys ultimately matter; CTE pre-aggregates ALL
2154        //      keys eagerly, wasting work for the unsurfaced 99 %.
2155        //
2156        // The CTE rewrite is right shape FOR the wrong root cause.
2157        // Real ceiling-first target is to make the existing batch
2158        // resolver's keyed-restriction path fire for the mailrs
2159        // GROUP BY + LIMIT shape, not to bypass it with a CTE.
2160        //
2161        // Keep the implementation dormant — the walker + gate
2162        // analysis stays as reference; turning this back on requires
2163        // a cost gate that proves CTE materialise + LEFT JOIN beats
2164        // the batch resolver for the SHAPE AT HAND (rare in practice).
2165        return None;
2166        #[allow(unreachable_code)]
2167        // Inner shape gates.
2168        if !inner.ctes.is_empty()
2169            || !inner.unions.is_empty()
2170            || inner.group_by.is_some()
2171            || inner.group_by_all
2172            || inner.having.is_some()
2173            || inner.distinct
2174            || inner.offset.is_some()
2175            || inner.items.len() != 1
2176            || inner.order_by.is_empty()
2177        {
2178            return None;
2179        }
2180        // LIMIT must be the literal 1 (placeholders bind late; we
2181        // can't guarantee the value here).
2182        match inner.limit {
2183            Some(LimitExpr::Literal(1)) => {}
2184            _ => return None,
2185        }
2186        let from = inner.from.as_ref()?;
2187        // Phase 2: single plain-table inner. Phase 3 lifts this gate
2188        // to allow inner INNER JOINs whose ON clauses are all-inner.
2189        if !from.joins.is_empty()
2190            || from.primary.lateral_subquery.is_some()
2191            || from.primary.unnest_expr.is_some()
2192            || from.primary.generate_series_args.is_some()
2193            || from.primary.as_of_segment.is_some()
2194        {
2195            return None;
2196        }
2197        let inner_table = from.primary.name.clone();
2198        let inner_alias = from
2199            .primary
2200            .alias
2201            .clone()
2202            .unwrap_or_else(|| inner_table.clone());
2203        let is_inner = |c: &ColumnName| -> bool {
2204            c.qualifier
2205                .as_deref()
2206                .is_some_and(|q| q.eq_ignore_ascii_case(&inner_alias))
2207        };
2208        let is_outer = |c: &ColumnName| -> bool {
2209            c.qualifier
2210                .as_deref()
2211                .is_some_and(|q| outer_aliases.contains(&q.to_ascii_lowercase()))
2212        };
2213        // Projection: scalar expression; reject aggregates / windows /
2214        // nested subqueries / outer references (the pulled-up SELECT
2215        // is uncorrelated GROUP BY — an outer column reference would
2216        // dangle).
2217        let SelectItem::Expr {
2218            expr: proj_expr,
2219            alias: _,
2220        } = &inner.items[0]
2221        else {
2222            return None;
2223        };
2224        if proj_has_disqualifying_shape(proj_expr, &inner_alias, outer_aliases) {
2225            return None;
2226        }
2227        // WHERE: exactly one `inner.k = outer.col`, plus all-inner
2228        // residual predicates.
2229        let where_ = inner.where_.as_ref()?;
2230        let mut corr: Option<(String, ColumnName)> = None;
2231        let mut non_corr: Vec<Expr> = Vec::new();
2232        for c in reorder::split_and_conjunctions(where_) {
2233            if let Expr::Binary {
2234                lhs,
2235                op: BinOp::Eq,
2236                rhs,
2237            } = c
2238                && let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref())
2239            {
2240                let pair = if is_inner(a) && is_outer(b) {
2241                    Some((a.name.clone(), b.clone()))
2242                } else if is_inner(b) && is_outer(a) {
2243                    Some((b.name.clone(), a.clone()))
2244                } else {
2245                    None
2246                };
2247                if let Some(p) = pair {
2248                    if corr.is_some() {
2249                        return None; // more than one correlation key
2250                    }
2251                    corr = Some(p);
2252                    continue;
2253                }
2254            }
2255            if !expr_is_all_inner(c, &inner_alias) {
2256                return None;
2257            }
2258            non_corr.push(c.clone());
2259        }
2260        let (inner_key, outer_col) = corr?;
2261        // ORDER BY: every key must be all-inner. Outer-referencing
2262        // sort keys would dangle after pullup.
2263        for ob in &inner.order_by {
2264            if !expr_is_all_inner(&ob.expr, &inner_alias) {
2265                return None;
2266            }
2267        }
2268        // Proj must also be all-inner (uncorrelated CTE body).
2269        if !expr_is_all_inner(proj_expr, &inner_alias) {
2270            return None;
2271        }
2272        // Build the CTE body:
2273        //   SELECT <inner.k> AS jk,
2274        //          (array_agg(<proj> ORDER BY <sort_keys>))[1] AS pj
2275        //     FROM <inner.from> WHERE <non_corr_AND_chain>
2276        //    GROUP BY <inner.k>
2277        let cte_name = alloc::format!("__cl1_{alias_n}");
2278        let jk_expr = Expr::Column(ColumnName {
2279            qualifier: Some(inner_alias.clone()),
2280            name: inner_key.clone(),
2281        });
2282        let argmax = Expr::ArraySubscript {
2283            target: alloc::boxed::Box::new(Expr::AggregateOrdered {
2284                call: alloc::boxed::Box::new(Expr::FunctionCall {
2285                    name: "array_agg".into(),
2286                    args: alloc::vec![proj_expr.clone()],
2287                }),
2288                order_by: inner.order_by.clone(),
2289                distinct: false,
2290                filter: None,
2291            }),
2292            index: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(1))),
2293        };
2294        let body_where = if non_corr.is_empty() {
2295            None
2296        } else {
2297            let mut iter = non_corr.into_iter();
2298            let head = iter.next().expect("non_corr nonempty in this branch");
2299            Some(iter.fold(head, |acc, p| Expr::Binary {
2300                lhs: alloc::boxed::Box::new(acc),
2301                op: BinOp::And,
2302                rhs: alloc::boxed::Box::new(p),
2303            }))
2304        };
2305        let body = SelectStatement {
2306            locking: None,
2307            ctes: Vec::new(),
2308            distinct: false,
2309            distinct_on: Vec::new(),
2310            items: alloc::vec![
2311                SelectItem::Expr {
2312                    expr: jk_expr.clone(),
2313                    alias: Some("jk".into()),
2314                },
2315                SelectItem::Expr {
2316                    expr: argmax,
2317                    alias: Some("pj".into()),
2318                },
2319            ],
2320            from: Some(from.clone()),
2321            where_: body_where,
2322            group_by: Some(alloc::vec![jk_expr]),
2323            group_by_all: false,
2324            having: None,
2325            unions: Vec::new(),
2326            order_by: Vec::new(),
2327            limit: None,
2328            offset: None,
2329            limit_with_ties: false,
2330            window_check_exprs: Vec::new(),
2331        };
2332        let cte = Cte {
2333            name: cte_name.clone(),
2334            body: spg_sql::ast::CteBody::Select(body),
2335            recursive: false,
2336            column_overrides: Vec::new(),
2337            search: None,
2338            cycle: None,
2339        };
2340        // LEFT JOIN __cl1_N ON __cl1_N.jk = <outer_col>
2341        let join = FromJoin {
2342            kind: JoinKind::Left,
2343            table: TableRef {
2344                name: cte_name.clone(),
2345                alias: None,
2346                only: false,
2347                as_of_segment: None,
2348                unnest_expr: None,
2349                unnest_column_aliases: Vec::new(),
2350                with_ordinality: false,
2351                generate_series_args: None,
2352                lateral_subquery: None,
2353                jsonb_each_text_arg: None,
2354                table_fn_call: None,
2355                rows_from: None,
2356                json_table: None,
2357                scalar_fn_item: false,
2358            },
2359            on: Some(Expr::Binary {
2360                lhs: alloc::boxed::Box::new(Expr::Column(ColumnName {
2361                    qualifier: Some(cte_name.clone()),
2362                    name: "jk".into(),
2363                })),
2364                op: BinOp::Eq,
2365                rhs: alloc::boxed::Box::new(Expr::Column(outer_col)),
2366            }),
2367            using_cols: None,
2368            natural: false,
2369        };
2370        let repl = ColumnName {
2371            qualifier: Some(cte_name),
2372            name: "pj".into(),
2373        };
2374        Some((cte, join, repl))
2375    }
2376
2377    pub(crate) fn pull_up_unique_correlated_agg_subqueries(
2378        &self,
2379        stmt: &mut SelectStatement,
2380    ) -> bool {
2381        if stmt.from.is_none() || stmt.items.iter().any(|i| matches!(i, SelectItem::Wildcard)) {
2382            return false;
2383        }
2384        // Aliases an outer-correlation column may qualify to.
2385        let outer_aliases: alloc::collections::BTreeSet<String> = {
2386            let from = stmt.from.as_ref().expect("from present");
2387            let mut s = alloc::collections::BTreeSet::new();
2388            let push = |s: &mut alloc::collections::BTreeSet<String>, t: &TableRef| {
2389                s.insert(
2390                    t.alias
2391                        .clone()
2392                        .unwrap_or_else(|| t.name.clone())
2393                        .to_ascii_lowercase(),
2394                );
2395            };
2396            push(&mut s, &from.primary);
2397            for j in &from.joins {
2398                push(&mut s, &j.table);
2399            }
2400            s
2401        };
2402        let mut new_joins: Vec<FromJoin> = Vec::new();
2403        for item in &mut stmt.items {
2404            if let SelectItem::Expr { expr, .. } = item {
2405                self.pull_up_walk(expr, false, &outer_aliases, &mut new_joins);
2406            }
2407        }
2408        if new_joins.is_empty() {
2409            return false;
2410        }
2411        stmt.from
2412            .as_mut()
2413            .expect("from present")
2414            .joins
2415            .extend(new_joins);
2416        true
2417    }
2418
2419    /// Recursive mutable walk over an expression tracking whether we are
2420    /// inside an aggregate argument. A correlated scalar subquery found in
2421    /// aggregate context that `try_pull_up_join` accepts is replaced in
2422    /// place by the joined column; the join is queued in `joins_out`.
2423    fn pull_up_walk(
2424        &self,
2425        e: &mut Expr,
2426        in_agg: bool,
2427        outer_aliases: &alloc::collections::BTreeSet<String>,
2428        joins_out: &mut Vec<FromJoin>,
2429    ) {
2430        match e {
2431            Expr::ScalarSubquery(inner) => {
2432                if in_agg
2433                    && let Some((join, col)) =
2434                        self.try_pull_up_join(inner, outer_aliases, joins_out.len())
2435                {
2436                    joins_out.push(join);
2437                    *e = Expr::Column(col);
2438                }
2439                // Otherwise leave for the existing resolver; the subquery
2440                // body is a separate scope, so don't descend into it.
2441            }
2442            Expr::FunctionCall { name, args } => {
2443                let child = in_agg || aggregate::is_aggregate_name(name);
2444                for a in args.iter_mut() {
2445                    self.pull_up_walk(a, child, outer_aliases, joins_out);
2446                }
2447            }
2448            Expr::AggregateOrdered {
2449                call,
2450                order_by,
2451                filter,
2452                ..
2453            } => {
2454                self.pull_up_walk(call, true, outer_aliases, joins_out);
2455                for o in order_by.iter_mut() {
2456                    self.pull_up_walk(&mut o.expr, true, outer_aliases, joins_out);
2457                }
2458                if let Some(f) = filter {
2459                    self.pull_up_walk(f, true, outer_aliases, joins_out);
2460                }
2461            }
2462            Expr::Binary { lhs, rhs, .. } => {
2463                self.pull_up_walk(lhs, in_agg, outer_aliases, joins_out);
2464                self.pull_up_walk(rhs, in_agg, outer_aliases, joins_out);
2465            }
2466            Expr::Unary { expr, .. }
2467            | Expr::Cast { expr, .. }
2468            | Expr::IsNull { expr, .. }
2469            | Expr::BoolTest { expr, .. }
2470            | Expr::FieldAccess { base: expr, .. } => {
2471                self.pull_up_walk(expr, in_agg, outer_aliases, joins_out);
2472            }
2473            Expr::Like { expr, pattern, .. } => {
2474                self.pull_up_walk(expr, in_agg, outer_aliases, joins_out);
2475                self.pull_up_walk(pattern, in_agg, outer_aliases, joins_out);
2476            }
2477            Expr::InList { expr, list, .. } => {
2478                self.pull_up_walk(expr, in_agg, outer_aliases, joins_out);
2479                for it in list.iter_mut() {
2480                    self.pull_up_walk(it, in_agg, outer_aliases, joins_out);
2481                }
2482            }
2483            Expr::Case {
2484                operand,
2485                branches,
2486                else_branch,
2487            } => {
2488                if let Some(o) = operand {
2489                    self.pull_up_walk(o, in_agg, outer_aliases, joins_out);
2490                }
2491                for (w, t) in branches.iter_mut() {
2492                    self.pull_up_walk(w, in_agg, outer_aliases, joins_out);
2493                    self.pull_up_walk(t, in_agg, outer_aliases, joins_out);
2494                }
2495                if let Some(eb) = else_branch {
2496                    self.pull_up_walk(eb, in_agg, outer_aliases, joins_out);
2497                }
2498            }
2499            // Window functions, EXISTS / IN subqueries, and other variants
2500            // are intentionally not descended for this rewrite — the
2501            // common aggregate-arg shapes above cover the reported load and
2502            // anything missed simply keeps its existing evaluation.
2503            _ => {}
2504        }
2505    }
2506
2507    /// Decide whether a correlated scalar subquery qualifies for the
2508    /// unique-key LEFT JOIN pull-up. Returns the join to append and the
2509    /// column that replaces the subquery node, or None to leave it alone.
2510    fn try_pull_up_join(
2511        &self,
2512        inner: &SelectStatement,
2513        outer_aliases: &alloc::collections::BTreeSet<String>,
2514        alias_n: usize,
2515    ) -> Option<(FromJoin, ColumnName)> {
2516        // Inner must be a single plain-table scan with one projected
2517        // column and none of the shape-breaking clauses.
2518        if !inner.ctes.is_empty()
2519            || !inner.unions.is_empty()
2520            || inner.group_by.is_some()
2521            || inner.having.is_some()
2522            || inner.distinct
2523            || !inner.order_by.is_empty()
2524            || inner.limit.is_some()
2525            || inner.offset.is_some()
2526            || inner.items.len() != 1
2527        {
2528            return None;
2529        }
2530        let from = inner.from.as_ref()?;
2531        if !from.joins.is_empty()
2532            || from.primary.lateral_subquery.is_some()
2533            || from.primary.unnest_expr.is_some()
2534            || from.primary.generate_series_args.is_some()
2535            || from.primary.as_of_segment.is_some()
2536        {
2537            return None;
2538        }
2539        let inner_table = from.primary.name.clone();
2540        let inner_alias = from
2541            .primary
2542            .alias
2543            .clone()
2544            .unwrap_or_else(|| inner_table.clone());
2545        let is_inner = |c: &ColumnName| -> bool {
2546            c.qualifier
2547                .as_deref()
2548                .is_some_and(|q| q.eq_ignore_ascii_case(&inner_alias))
2549        };
2550        let is_outer = |c: &ColumnName| -> bool {
2551            c.qualifier
2552                .as_deref()
2553                .is_some_and(|q| outer_aliases.contains(&q.to_ascii_lowercase()))
2554        };
2555        // Projected column: a single inner-qualified column.
2556        let SelectItem::Expr { expr: out_expr, .. } = &inner.items[0] else {
2557            return None;
2558        };
2559        let Expr::Column(out_col) = out_expr else {
2560            return None;
2561        };
2562        if !is_inner(out_col) {
2563            return None;
2564        }
2565        // WHERE: exactly one `inner.key = outer.col`, rest all-inner.
2566        let w = inner.where_.as_ref()?;
2567        let mut corr: Option<(String, ColumnName)> = None;
2568        let mut rest: Vec<Expr> = Vec::new();
2569        for c in reorder::split_and_conjunctions(w) {
2570            if let Expr::Binary {
2571                lhs,
2572                op: BinOp::Eq,
2573                rhs,
2574            } = c
2575                && let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref())
2576            {
2577                let pair = if is_inner(a) && is_outer(b) {
2578                    Some((a.name.clone(), b.clone()))
2579                } else if is_inner(b) && is_outer(a) {
2580                    Some((b.name.clone(), a.clone()))
2581                } else {
2582                    None
2583                };
2584                if let Some(p) = pair {
2585                    if corr.is_some() {
2586                        return None; // more than one correlation
2587                    }
2588                    corr = Some(p);
2589                    continue;
2590                }
2591            }
2592            if !expr_is_all_inner(c, &inner_alias) {
2593                return None;
2594            }
2595            rest.push(c.clone());
2596        }
2597        let (inner_key, outer_col) = corr?;
2598        // Safety gate: the correlation key must be UNIQUE / PRIMARY KEY on
2599        // the inner table so the join can't multiply outer rows.
2600        if !self.column_is_single_unique(&inner_table, &inner_key) {
2601            return None;
2602        }
2603        // Build the LEFT JOIN against a fresh alias.
2604        let fresh = alloc::format!("__plj_{alias_n}");
2605        let key_eq = Expr::Binary {
2606            lhs: alloc::boxed::Box::new(Expr::Column(ColumnName {
2607                qualifier: Some(fresh.clone()),
2608                name: inner_key,
2609            })),
2610            op: BinOp::Eq,
2611            rhs: alloc::boxed::Box::new(Expr::Column(outer_col)),
2612        };
2613        let on = rest
2614            .into_iter()
2615            .map(|mut e| {
2616                rename_qualifier(&mut e, &inner_alias, &fresh);
2617                e
2618            })
2619            .fold(key_eq, |acc, pred| Expr::Binary {
2620                lhs: alloc::boxed::Box::new(acc),
2621                op: BinOp::And,
2622                rhs: alloc::boxed::Box::new(pred),
2623            });
2624        let join = FromJoin {
2625            kind: JoinKind::Left,
2626            table: TableRef {
2627                name: inner_table,
2628                alias: Some(fresh.clone()),
2629                only: false,
2630                as_of_segment: None,
2631                unnest_expr: None,
2632                unnest_column_aliases: Vec::new(),
2633                with_ordinality: false,
2634                generate_series_args: None,
2635                lateral_subquery: None,
2636                jsonb_each_text_arg: None,
2637                table_fn_call: None,
2638                rows_from: None,
2639                json_table: None,
2640                scalar_fn_item: false,
2641            },
2642            on: Some(on),
2643            using_cols: None,
2644            natural: false,
2645        };
2646        let repl = ColumnName {
2647            qualifier: Some(fresh),
2648            name: out_col.name.clone(),
2649        };
2650        Some((join, repl))
2651    }
2652
2653    /// v7.34.2 (mailrs prod NOT EXISTS hot-path) — plan-time EXISTS /
2654    /// NOT EXISTS sublink pull-up to semi/anti-join. PostgreSQL's
2655    /// `convert_EXISTS_sublink_to_join`-flavoured rewrite: a correlated
2656    /// `[NOT] EXISTS (SELECT … FROM t WHERE t.k = outer.col [AND inner])`
2657    /// in the WHERE-AND spine collapses to a real JOIN against `t`. The
2658    /// per-row dispatch (clone host expr × 25 k + splice + eval) goes
2659    /// away entirely — the executor streams one tight join loop the
2660    /// same way it would for a hand-written JOIN.
2661    ///
2662    /// Shape rules:
2663    ///   * NOT EXISTS  → LEFT JOIN t AS __exsj_N ON t.k = outer.col [AND …]
2664    ///                   AND a survivor `__exsj_N.k IS NULL` conjunct
2665    ///                   stays in WHERE. Safe regardless of uniqueness:
2666    ///                   IS-NULL only fires on the LEFT-JOIN pad row,
2667    ///                   so duplicate inner matches collapse cleanly
2668    ///                   (any match drops the outer row; only no-match
2669    ///                   outer rows survive).
2670    ///   * EXISTS      → INNER JOIN. Safe only when inner.k is single-
2671    ///                   column UNIQUE / PRIMARY KEY (otherwise INNER
2672    ///                   would multiply outer rows). Gated by
2673    ///                   `column_is_single_unique`. No survivor needed
2674    ///                   in WHERE — the join itself encodes EXISTS=true.
2675    ///
2676    /// Eligible inner: single plain-table FROM, no nested JOIN / CTE /
2677    /// UNION / GROUP / HAVING / DISTINCT / ORDER / LIMIT / OFFSET, and
2678    /// WHERE = exactly one `inner.k = outer.col` correlation plus
2679    /// optional all-inner predicates that ride into the ON clause.
2680    /// Anything else is left for the per-row resolver.
2681    ///
2682    /// Returns true when at least one conjunct was pulled up.
2683    pub(crate) fn pull_up_exists_sublinks(&self, stmt: &mut SelectStatement) -> bool {
2684        if stmt.from.is_none() {
2685            return false;
2686        }
2687        let Some(where_expr) = stmt.where_.take() else {
2688            return false;
2689        };
2690        // v7.37.4 A'' — pre-disambiguate outer unqualified column refs
2691        // whose name would collide with a future pulled-up inner
2692        // table's columns. mailrs `/api/conversations` uses bare
2693        // `thread_id != ''` in outer WHERE; once we add
2694        // `__exsj_0 LEFT JOIN snoozed_conversations` (also with a
2695        // `thread_id` column), the resolver raises "ambiguous column".
2696        // Conservative: scan EXISTS / NOT EXISTS subqueries in the
2697        // WHERE we just took out, look up each inner plain-table's
2698        // column set, and for every collision column that exists in
2699        // exactly one outer table, pre-qualify it to that owning alias.
2700        let mut collision_names: alloc::collections::BTreeSet<String> =
2701            alloc::collections::BTreeSet::new();
2702        for c in reorder::split_and_conjunctions(&where_expr) {
2703            let inner_subq: Option<&SelectStatement> = match c {
2704                Expr::Exists { subquery, .. } => Some(subquery.as_ref()),
2705                Expr::Unary {
2706                    op: UnOp::Not,
2707                    expr,
2708                } => match expr.as_ref() {
2709                    Expr::Exists { subquery, .. } => Some(subquery.as_ref()),
2710                    _ => None,
2711                },
2712                _ => None,
2713            };
2714            let Some(inner) = inner_subq else { continue };
2715            let Some(from) = &inner.from else { continue };
2716            if !from.joins.is_empty() {
2717                continue;
2718            }
2719            let Some(t) = self.active_catalog().get(&from.primary.name) else {
2720                continue;
2721            };
2722            for col in &t.schema().columns {
2723                collision_names.insert(col.name.to_ascii_lowercase());
2724            }
2725        }
2726        let mut where_expr = where_expr;
2727        if !collision_names.is_empty() {
2728            let from = stmt.from.as_ref().expect("from present");
2729            let outer_tables: Vec<(String, String)> = {
2730                let mut v = Vec::new();
2731                let collect = |v: &mut Vec<(String, String)>, t: &TableRef| {
2732                    let alias = t.alias.clone().unwrap_or_else(|| t.name.clone());
2733                    v.push((alias, t.name.clone()));
2734                };
2735                collect(&mut v, &from.primary);
2736                for j in &from.joins {
2737                    collect(&mut v, &j.table);
2738                }
2739                v
2740            };
2741            let mut owner: alloc::collections::BTreeMap<String, String> =
2742                alloc::collections::BTreeMap::new();
2743            for col_lc in &collision_names {
2744                let mut matches: Vec<String> = Vec::new();
2745                for (alias, tname) in &outer_tables {
2746                    let Some(t) = self.active_catalog().get(tname) else {
2747                        continue;
2748                    };
2749                    if t.schema()
2750                        .columns
2751                        .iter()
2752                        .any(|c| c.name.eq_ignore_ascii_case(col_lc))
2753                    {
2754                        matches.push(alias.clone());
2755                    }
2756                }
2757                if matches.len() == 1 {
2758                    owner.insert(col_lc.clone(), matches.remove(0));
2759                }
2760            }
2761            if !owner.is_empty() {
2762                disambiguate_stmt_unqualified_columns(stmt, &owner);
2763                disambiguate_expr_unqualified_columns(&mut where_expr, &owner);
2764            }
2765        }
2766        let outer_aliases: alloc::collections::BTreeSet<String> = {
2767            let from = stmt.from.as_ref().expect("from present");
2768            let mut s = alloc::collections::BTreeSet::new();
2769            let push = |s: &mut alloc::collections::BTreeSet<String>, t: &TableRef| {
2770                s.insert(
2771                    t.alias
2772                        .clone()
2773                        .unwrap_or_else(|| t.name.clone())
2774                        .to_ascii_lowercase(),
2775                );
2776            };
2777            push(&mut s, &from.primary);
2778            for j in &from.joins {
2779                push(&mut s, &j.table);
2780            }
2781            s
2782        };
2783        // v7.39 (round 721) — alias -> stored-table name, so the computed
2784        // correlation half can check its outer columns' types (int-only is
2785        // the admission bar; see the extraction).
2786        let outer_tables: alloc::collections::BTreeMap<String, String> = {
2787            let from = stmt.from.as_ref().expect("from present");
2788            let mut m = alloc::collections::BTreeMap::new();
2789            let push = |m: &mut alloc::collections::BTreeMap<String, String>, t: &TableRef| {
2790                m.insert(
2791                    t.alias
2792                        .clone()
2793                        .unwrap_or_else(|| t.name.clone())
2794                        .to_ascii_lowercase(),
2795                    t.name.clone(),
2796                );
2797            };
2798            push(&mut m, &from.primary);
2799            for j in &from.joins {
2800                push(&mut m, &j.table);
2801            }
2802            m
2803        };
2804        let conjuncts = reorder::split_and_conjunctions(&where_expr);
2805        let mut survivors: Vec<Expr> = Vec::new();
2806        let mut new_joins: Vec<FromJoin> = Vec::new();
2807        let mut rewrote_any = false;
2808        for c in conjuncts {
2809            // v7.34.3 — the parser emits `NOT EXISTS(...)` as
2810            // `Expr::Unary{Not, Exists{negated:false, …}}`, NOT as
2811            // `Exists{negated:true}`. Match both shapes so the
2812            // pull-up handles both `EXISTS` and `NOT EXISTS`.
2813            let parsed: Option<(&SelectStatement, bool)> = match c {
2814                Expr::Exists { subquery, negated } => Some((subquery.as_ref(), *negated)),
2815                Expr::Unary {
2816                    op: UnOp::Not,
2817                    expr,
2818                } => match expr.as_ref() {
2819                    Expr::Exists { subquery, negated } => Some((subquery.as_ref(), !*negated)),
2820                    _ => None,
2821                },
2822                _ => None,
2823            };
2824            if let Some((subquery, neg)) = parsed {
2825                // v7.34.2 first chose `[NOT] IN (SELECT k FROM t)` first
2826                // because the `mailrs_prod_not_exists` 250 k probe
2827                // dropped 178 ms (LEFT JOIN + IS NULL form) → 74 ms
2828                // (NOT IN form). But that win was from the OUTER ORDER
2829                // BY id DESC LIMIT N walker fast path
2830                // (`try_pk_walk_top_n`), which only the InList shape
2831                // exposes (early-stop on first N survivors). For
2832                // shapes WITHOUT an outer LIMIT (e.g. `SELECT
2833                // COUNT(*) FROM messages WHERE NOT EXISTS …`) the IN
2834                // form has to materialise the entire 12.5 k inner
2835                // value set as `Vec<Expr::Literal>` before HashSet
2836                // build — pure overhead that the LEFT ANTI JOIN
2837                // executor skips by hashing the inner table directly.
2838                // v7.37.x (docker-fair NOTEX) — branch on outer
2839                // LIMIT presence: with LIMIT, prefer InList (walker
2840                // benefit); without LIMIT, prefer LEFT ANTI JOIN
2841                // (streaming build, no Expr::Literal Vec roundtrip).
2842                let outer_has_limit = stmt.limit.is_some();
2843                let try_in_first = outer_has_limit;
2844                let mut consumed = false;
2845                if try_in_first
2846                    && let Some(rewritten) =
2847                        self.try_pull_up_exists_as_in(subquery, neg, &outer_aliases)
2848                {
2849                    survivors.push(rewritten);
2850                    consumed = true;
2851                }
2852                if !consumed
2853                    && let Some((join, residual)) = self.try_pull_up_exists_sublink(
2854                        subquery,
2855                        neg,
2856                        &outer_aliases,
2857                        &outer_tables,
2858                        new_joins.len(),
2859                    )
2860                {
2861                    new_joins.push(join);
2862                    if let Some(r) = residual {
2863                        survivors.push(r);
2864                    }
2865                    consumed = true;
2866                }
2867                if !consumed
2868                    && !try_in_first
2869                    && let Some(rewritten) =
2870                        self.try_pull_up_exists_as_in(subquery, neg, &outer_aliases)
2871                {
2872                    // Fallback when LEFT ANTI JOIN refused (e.g. inner
2873                    // shape too complex) — IN form is the next best.
2874                    survivors.push(rewritten);
2875                    consumed = true;
2876                }
2877                if consumed {
2878                    rewrote_any = true;
2879                    continue;
2880                }
2881            }
2882            survivors.push(c.clone());
2883        }
2884        if !rewrote_any {
2885            stmt.where_ = Some(where_expr);
2886            return false;
2887        }
2888        EXISTS_PULLUP_FIRE_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2889        if !new_joins.is_empty() {
2890            stmt.from
2891                .as_mut()
2892                .expect("from present")
2893                .joins
2894                .extend(new_joins);
2895        }
2896        stmt.where_ = survivors.into_iter().reduce(|a, b| Expr::Binary {
2897            lhs: alloc::boxed::Box::new(a),
2898            op: BinOp::And,
2899            rhs: alloc::boxed::Box::new(b),
2900        });
2901        true
2902    }
2903
2904    /// v7.34.3 — emit the EXISTS conjunct as `outer.col IN (SELECT
2905    /// inner.k FROM inner.table)` (or its negated form). Eligibility
2906    /// mirrors `try_pull_up_exists_sublink` — single plain-table FROM,
2907    /// no shape-breaking clauses, exactly one `inner.k = outer.col`
2908    /// correlation plus optional all-inner predicates — except no
2909    /// uniqueness check is needed (IN handles duplicate inner.k
2910    /// fine). For the NEGATED case we ALSO require inner.k to be
2911    /// declared NOT NULL: `outer.col NOT IN (set with NULL)` returns
2912    /// UNKNOWN for every outer row in SQL three-valued logic, which
2913    /// differs from NOT EXISTS semantics. None on ineligible →
2914    /// caller falls back to the LEFT JOIN + IS NULL injection or
2915    /// the legacy per-row resolver.
2916    fn try_pull_up_exists_as_in(
2917        &self,
2918        inner: &SelectStatement,
2919        negated: bool,
2920        outer_aliases: &alloc::collections::BTreeSet<String>,
2921    ) -> Option<Expr> {
2922        if !inner.ctes.is_empty()
2923            || !inner.unions.is_empty()
2924            || inner.group_by.is_some()
2925            || inner.having.is_some()
2926            || inner.distinct
2927            || !inner.order_by.is_empty()
2928            || inner.limit.is_some()
2929            || inner.offset.is_some()
2930        {
2931            return None;
2932        }
2933        let from = inner.from.as_ref()?;
2934        if !from.joins.is_empty()
2935            || from.primary.lateral_subquery.is_some()
2936            || from.primary.unnest_expr.is_some()
2937            || from.primary.generate_series_args.is_some()
2938            || from.primary.as_of_segment.is_some()
2939        {
2940            return None;
2941        }
2942        let inner_table = from.primary.name.clone();
2943        let inner_alias = from
2944            .primary
2945            .alias
2946            .clone()
2947            .unwrap_or_else(|| inner_table.clone());
2948        let is_inner = |c: &ColumnName| -> bool {
2949            c.qualifier
2950                .as_deref()
2951                .is_some_and(|q| q.eq_ignore_ascii_case(&inner_alias))
2952        };
2953        let is_outer = |c: &ColumnName| -> bool {
2954            c.qualifier
2955                .as_deref()
2956                .is_some_and(|q| outer_aliases.contains(&q.to_ascii_lowercase()))
2957        };
2958        let w = inner.where_.as_ref()?;
2959        let mut corr: Option<(String, ColumnName)> = None;
2960        let mut rest: Vec<Expr> = Vec::new();
2961        for c in reorder::split_and_conjunctions(w) {
2962            if let Expr::Binary {
2963                lhs,
2964                op: BinOp::Eq,
2965                rhs,
2966            } = c
2967                && let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref())
2968            {
2969                let pair = if is_inner(a) && is_outer(b) {
2970                    Some((a.name.clone(), b.clone()))
2971                } else if is_inner(b) && is_outer(a) {
2972                    Some((b.name.clone(), a.clone()))
2973                } else {
2974                    None
2975                };
2976                if let Some(p) = pair {
2977                    if corr.is_some() {
2978                        return None;
2979                    }
2980                    corr = Some(p);
2981                    continue;
2982                }
2983            }
2984            if !expr_is_all_inner(c, &inner_alias) {
2985                return None;
2986            }
2987            rest.push(c.clone());
2988        }
2989        let (inner_key, outer_col) = corr?;
2990        if negated && !self.column_is_not_null(&inner_table, &inner_key) {
2991            return None;
2992        }
2993        // Build the rewritten inner SELECT: `SELECT inner.k FROM
2994        // inner.table [WHERE rest]`. The correlation conjunct is
2995        // dropped — IN-subquery handles equality membership. All-inner
2996        // residual predicates ride into the new WHERE.
2997        let mut rewritten = inner.clone();
2998        rewritten.limit = None;
2999        rewritten.offset = None;
3000        rewritten.order_by = Vec::new();
3001        rewritten.distinct = false;
3002        rewritten.where_ = rest.into_iter().reduce(|a, b| Expr::Binary {
3003            lhs: alloc::boxed::Box::new(a),
3004            op: BinOp::And,
3005            rhs: alloc::boxed::Box::new(b),
3006        });
3007        rewritten.items = alloc::vec![SelectItem::Expr {
3008            expr: Expr::Column(ColumnName {
3009                qualifier: Some(inner_alias),
3010                name: inner_key,
3011            }),
3012            alias: None,
3013        }];
3014        Some(Expr::InSubquery {
3015            expr: alloc::boxed::Box::new(Expr::Column(outer_col)),
3016            subquery: alloc::boxed::Box::new(rewritten),
3017            negated,
3018        })
3019    }
3020
3021    fn try_pull_up_exists_sublink(
3022        &self,
3023        inner: &SelectStatement,
3024        negated: bool,
3025        outer_aliases: &alloc::collections::BTreeSet<String>,
3026        outer_tables: &alloc::collections::BTreeMap<String, String>,
3027        alias_n: usize,
3028    ) -> Option<(FromJoin, Option<Expr>)> {
3029        EXISTS_PULLUP_CANDIDATE_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3030        if !inner.ctes.is_empty()
3031            || !inner.unions.is_empty()
3032            || inner.group_by.is_some()
3033            || inner.having.is_some()
3034            || inner.distinct
3035            || !inner.order_by.is_empty()
3036            || inner.limit.is_some()
3037            || inner.offset.is_some()
3038        {
3039            EXISTS_PULLUP_BAIL_INNER_SHAPE.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3040            return None;
3041        }
3042        let from = inner.from.as_ref()?;
3043        if !from.joins.is_empty()
3044            || from.primary.lateral_subquery.is_some()
3045            || from.primary.unnest_expr.is_some()
3046            || from.primary.generate_series_args.is_some()
3047            || from.primary.as_of_segment.is_some()
3048        {
3049            EXISTS_PULLUP_BAIL_INNER_FROM.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3050            return None;
3051        }
3052        let inner_table = from.primary.name.clone();
3053        let inner_alias = from
3054            .primary
3055            .alias
3056            .clone()
3057            .unwrap_or_else(|| inner_table.clone());
3058        let is_inner = |c: &ColumnName| -> bool {
3059            c.qualifier
3060                .as_deref()
3061                .is_some_and(|q| q.eq_ignore_ascii_case(&inner_alias))
3062        };
3063        let is_outer = |c: &ColumnName| -> bool {
3064            c.qualifier
3065                .as_deref()
3066                .is_some_and(|q| outer_aliases.contains(&q.to_ascii_lowercase()))
3067        };
3068        let Some(w) = inner.where_.as_ref() else {
3069            EXISTS_PULLUP_BAIL_NO_WHERE.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3070            return None;
3071        };
3072        // v7.37.4 A'' (mailrs prod /api/conversations 2-col anti-join) —
3073        // accept multi-column correlation. Today's single-pair restriction
3074        // forced mailrs's
3075        //   NOT EXISTS (SELECT 1 FROM sc WHERE sc.thread_id = m.thread_id
3076        //                                  AND sc.account_address = mb.user_address
3077        //                                  AND sc.snoozed_until > 0)
3078        // to fall back to the batch `try_batch_correlated_exists` path,
3079        // which builds the inner set fine but then pays a per-row host-
3080        // expression clone + AST walk + eval to splice each EXISTS node
3081        // into a Bool literal (line 194-211 above). 100k join survivors ×
3082        // ~1.5 µs per splice = ~150 ms on the mini cold bench. Pulling
3083        // multi-col is the same shape SPG / PG / MySQL / MariaDB plan a
3084        // multi-key anti-join: LEFT JOIN sc ON (sc.thread_id = m.thread_id
3085        //   AND sc.account_address = mb.user_address [AND inner preds])
3086        // + WHERE sc.<first key> IS NULL. NULL semantics: a NULL on any
3087        // join key means no match, identical to NOT EXISTS three-valued
3088        // logic (the IS NULL probe matches the pad row).
3089        // v7.39 (round 721) — the outer half of a correlation pair is an
3090        // EXPRESSION now (a plain column rides as Expr::Column). What
3091        // widened it: `WHERE b.id = a.id + 500000` bailed to the per-row
3092        // correlated executor (~208 ms on the panel's 500k anti-join)
3093        // because only column=column pairs were recognised. A computed
3094        // outer half is admitted for the ANTI join when it is integer-only
3095        // over outer columns AND the inner column is integer-family — the
3096        // exact shape the round-720 mirror hash lane executes; anything
3097        // wider would pull up into a nested-loop join and be SLOWER than
3098        // the correlated executor it replaces.
3099        let inner_col_is_int = |name: &str| -> bool {
3100            self.active_catalog().get(&inner_table).is_some_and(|t| {
3101                t.schema().columns.iter().any(|cs| {
3102                    cs.name.eq_ignore_ascii_case(name)
3103                        && matches!(
3104                            cs.ty,
3105                            spg_storage::DataType::Int
3106                                | spg_storage::DataType::BigInt
3107                                | spg_storage::DataType::SmallInt
3108                        )
3109                })
3110            })
3111        };
3112        // v7.39 (round 752) — the inner half of a correlation pair is not
3113        // always a bare column any more: `WHERE a.id = b.id + 1` (outer
3114        // column = inner-only integer expression) was the round-721
3115        // ledger's second entry and ran the per-row correlated executor.
3116        // It is the round-719 lane's exact shape once pulled up
3117        // (`ON <fresh int expr> = <outer int column>`), so the pair's
3118        // inner half widens to carry it.
3119        enum InnerHalf {
3120            Col(String),
3121            IntExpr(Expr),
3122        }
3123        // Integer-only over the inner alias — the mirror of
3124        // `outer_int_only_expr`, with the same operator set as the join
3125        // lane's `int_only_key_expr` (Add/Sub/Mul, int-family columns,
3126        // integer literals) so an admitted pair is one the i64 lane
3127        // executes rather than a nested loop.
3128        fn inner_int_only_expr(
3129            e: &Expr,
3130            is_inner: &dyn Fn(&ColumnName) -> bool,
3131            inner_col_is_int: &dyn Fn(&str) -> bool,
3132        ) -> bool {
3133            match e {
3134                Expr::Column(c) => is_inner(c) && inner_col_is_int(&c.name),
3135                Expr::Literal(spg_sql::ast::Literal::Integer(_)) => true,
3136                Expr::Binary { lhs, op, rhs } => {
3137                    matches!(op, BinOp::Add | BinOp::Sub | BinOp::Mul)
3138                        && inner_int_only_expr(lhs, is_inner, inner_col_is_int)
3139                        && inner_int_only_expr(rhs, is_inner, inner_col_is_int)
3140                }
3141                _ => false,
3142            }
3143        }
3144        fn first_inner_col(e: &Expr) -> Option<String> {
3145            match e {
3146                Expr::Column(c) => Some(c.name.clone()),
3147                Expr::Binary { lhs, rhs, .. } => {
3148                    first_inner_col(lhs).or_else(|| first_inner_col(rhs))
3149                }
3150                _ => None,
3151            }
3152        }
3153        let mut corr_pairs: Vec<(InnerHalf, Expr)> = Vec::new();
3154        let mut rest: Vec<Expr> = Vec::new();
3155        for c in reorder::split_and_conjunctions(w) {
3156            if let Expr::Binary {
3157                lhs,
3158                op: BinOp::Eq,
3159                rhs,
3160            } = c
3161            {
3162                let pair = match (lhs.as_ref(), rhs.as_ref()) {
3163                    (Expr::Column(a), Expr::Column(b)) if is_inner(a) && is_outer(b) => {
3164                        Some((InnerHalf::Col(a.name.clone()), Expr::Column(b.clone())))
3165                    }
3166                    (Expr::Column(a), Expr::Column(b)) if is_inner(b) && is_outer(a) => {
3167                        Some((InnerHalf::Col(b.name.clone()), Expr::Column(a.clone())))
3168                    }
3169                    // v7.39 (round 725) — the `negated`-only restriction is
3170                    // gone: positive EXISTS pulls up as a true SEMI join now,
3171                    // so a computed key no longer risks row multiplication.
3172                    (Expr::Column(a), e)
3173                        if is_inner(a)
3174                            && !matches!(e, Expr::Column(_))
3175                            && inner_col_is_int(&a.name)
3176                            && outer_int_only_expr(e, outer_aliases, outer_tables, self) =>
3177                    {
3178                        Some((InnerHalf::Col(a.name.clone()), e.clone()))
3179                    }
3180                    (e, Expr::Column(a))
3181                        if is_inner(a)
3182                            && !matches!(e, Expr::Column(_))
3183                            && inner_col_is_int(&a.name)
3184                            && outer_int_only_expr(e, outer_aliases, outer_tables, self) =>
3185                    {
3186                        Some((InnerHalf::Col(a.name.clone()), e.clone()))
3187                    }
3188                    // v7.39 (round 752) — the reverse: outer bare column =
3189                    // inner-only integer expression. The outer column must
3190                    // be integer-family too (checked through the alias→table
3191                    // map by `outer_int_only_expr` on the lone column) or
3192                    // the i64 lane cannot key it, and the expression must
3193                    // mention at least one inner column — a column-free
3194                    // `a.id = 5` is not a correlation.
3195                    (Expr::Column(o), e)
3196                        if is_outer(o)
3197                            && !matches!(e, Expr::Column(_))
3198                            && outer_int_only_expr(
3199                                &Expr::Column(o.clone()),
3200                                outer_aliases,
3201                                outer_tables,
3202                                self,
3203                            )
3204                            && inner_int_only_expr(e, &is_inner, &inner_col_is_int)
3205                            && first_inner_col(e).is_some() =>
3206                    {
3207                        Some((InnerHalf::IntExpr(e.clone()), Expr::Column(o.clone())))
3208                    }
3209                    (e, Expr::Column(o))
3210                        if is_outer(o)
3211                            && !matches!(e, Expr::Column(_))
3212                            && outer_int_only_expr(
3213                                &Expr::Column(o.clone()),
3214                                outer_aliases,
3215                                outer_tables,
3216                                self,
3217                            )
3218                            && inner_int_only_expr(e, &is_inner, &inner_col_is_int)
3219                            && first_inner_col(e).is_some() =>
3220                    {
3221                        Some((InnerHalf::IntExpr(e.clone()), Expr::Column(o.clone())))
3222                    }
3223                    _ => None,
3224                };
3225                if let Some(p) = pair {
3226                    corr_pairs.push(p);
3227                    continue;
3228                }
3229            }
3230            if !expr_is_all_inner(c, &inner_alias) {
3231                EXISTS_PULLUP_BAIL_RESIDUAL_NOT_INNER
3232                    .fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3233                return None;
3234            }
3235            rest.push(c.clone());
3236        }
3237        if corr_pairs.is_empty() {
3238            EXISTS_PULLUP_BAIL_NO_CORR.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3239            return None;
3240        }
3241        // Differential knob — refuse the multi-col case under test so
3242        // the baseline path (batch resolver) runs and its result can
3243        // be compared against the pullup-on path. Single-col stays on.
3244        if corr_pairs.len() > 1
3245            && EXISTS_PULLUP_MULTICOL_DISABLE.load(core::sync::atomic::Ordering::Relaxed)
3246        {
3247            EXISTS_PULLUP_BAIL_MULTICOL_DISABLED
3248                .fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3249            return None;
3250        }
3251        // v7.39 (round 725) — EXISTS pulls up as a true SEMI join now
3252        // (each outer row keeps at most one pairing), so the uniqueness
3253        // gate that guarded the old INNER-join form is gone: an INNER
3254        // join multiplies outer rows on duplicate inner matches, a semi
3255        // join cannot. That gate was the round-721 ledger's first entry
3256        // — the panel's positive-EXISTS cell bailed on it (7.44×, the
3257        // inner column carries no declared UNIQUE) and ran the per-row
3258        // correlated executor. NOT EXISTS keeps LEFT + IS NULL.
3259        let fresh = alloc::format!("__exsj_{alias_n}");
3260        // Build the ON conjunction: every (inner_key = outer_col) pair
3261        // joined by AND, then folded with the all-inner residual.
3262        let inner_half_expr = |ih: &InnerHalf| -> Expr {
3263            match ih {
3264                InnerHalf::Col(name) => Expr::Column(ColumnName {
3265                    qualifier: Some(fresh.clone()),
3266                    name: name.clone(),
3267                }),
3268                InnerHalf::IntExpr(e) => {
3269                    let mut e = e.clone();
3270                    rename_qualifier(&mut e, &inner_alias, &fresh);
3271                    e
3272                }
3273            }
3274        };
3275        let mut on_iter = corr_pairs.iter().map(|(ik, oc)| Expr::Binary {
3276            lhs: alloc::boxed::Box::new(inner_half_expr(ik)),
3277            op: BinOp::Eq,
3278            rhs: alloc::boxed::Box::new(oc.clone()),
3279        });
3280        let first_key_eq = on_iter
3281            .next()
3282            .expect("corr_pairs non-empty post `is_empty()` gate");
3283        let on = rest
3284            .into_iter()
3285            .map(|mut e| {
3286                rename_qualifier(&mut e, &inner_alias, &fresh);
3287                e
3288            })
3289            .chain(on_iter)
3290            .fold(first_key_eq, |acc, pred| Expr::Binary {
3291                lhs: alloc::boxed::Box::new(acc),
3292                op: BinOp::And,
3293                rhs: alloc::boxed::Box::new(pred),
3294            });
3295        let join = FromJoin {
3296            kind: if negated {
3297                JoinKind::Left
3298            } else {
3299                JoinKind::Semi
3300            },
3301            table: TableRef {
3302                name: inner_table,
3303                alias: Some(fresh.clone()),
3304                only: false,
3305                as_of_segment: None,
3306                unnest_expr: None,
3307                unnest_column_aliases: Vec::new(),
3308                with_ordinality: false,
3309                generate_series_args: None,
3310                lateral_subquery: None,
3311                jsonb_each_text_arg: None,
3312                table_fn_call: None,
3313                rows_from: None,
3314                json_table: None,
3315                scalar_fn_item: false,
3316            },
3317            on: Some(on),
3318            using_cols: None,
3319            natural: false,
3320        };
3321        let residual = if negated {
3322            // anti-join: pick the FIRST inner key as the IS NULL probe.
3323            // Any IS NULL on a joined-side column is sufficient — the
3324            // LEFT-JOIN pad row sets ALL inner columns to NULL atomically,
3325            // so a single column witnesses "no match". For an IntExpr
3326            // inner half the probe is the first column INSIDE it: a pair
3327            // only matches when its Eq is TRUE, which needs the whole
3328            // Add/Sub/Mul expression non-NULL, which needs every column
3329            // in it non-NULL — so that column is a valid witness, and a
3330            // bare column is what the anti-join fast path recognises.
3331            let probe_key = match &corr_pairs[0].0 {
3332                InnerHalf::Col(name) => name.clone(),
3333                InnerHalf::IntExpr(e) => {
3334                    first_inner_col(e).expect("IntExpr admitted only with an inner column")
3335                }
3336            };
3337            Some(Expr::IsNull {
3338                expr: alloc::boxed::Box::new(Expr::Column(ColumnName {
3339                    qualifier: Some(fresh),
3340                    name: probe_key,
3341                })),
3342                negated: false,
3343            })
3344        } else {
3345            None
3346        };
3347        Some((join, residual))
3348    }
3349
3350    /// v7.34.3 — true when `col` on `table` is declared NOT NULL (the
3351    /// `ColumnSchema.nullable` flag is `false`). Used to gate the
3352    /// `NOT EXISTS → NOT IN` rewrite, since SQL three-valued logic
3353    /// turns `outer.col NOT IN (set with NULL)` into UNKNOWN for every
3354    /// outer row, which would differ from the NOT EXISTS semantics.
3355    fn column_is_not_null(&self, table: &str, col: &str) -> bool {
3356        let Some(t) = self.active_catalog().get(table) else {
3357            return false;
3358        };
3359        let sch = t.schema();
3360        // Direct flag — cheap path. Covers explicit NOT NULL columns
3361        // and table-level PK constraints (ddl.rs line 1252).
3362        if sch
3363            .columns
3364            .iter()
3365            .find(|c| c.name.eq_ignore_ascii_case(col))
3366            .is_some_and(|c| !c.nullable)
3367        {
3368            return true;
3369        }
3370        // v7.34.3 — inline `PRIMARY KEY` on a column definition
3371        // (e.g. `id BIGSERIAL PRIMARY KEY`) does NOT currently flip
3372        // `ColumnSchema.nullable` to false in ddl.rs (only the
3373        // table-level `CONSTRAINT … PRIMARY KEY (col)` shape does).
3374        // PK semantically implies NOT NULL, so cross-check the
3375        // installed uniqueness constraints' `is_primary_key` flag too.
3376        let Some(pos) = sch
3377            .columns
3378            .iter()
3379            .position(|c| c.name.eq_ignore_ascii_case(col))
3380        else {
3381            return false;
3382        };
3383        sch.uniqueness_constraints
3384            .iter()
3385            .any(|u| u.is_primary_key && u.columns.as_slice() == [pos])
3386    }
3387
3388    /// True when `col` on `table` is covered by a single-column UNIQUE or
3389    /// PRIMARY KEY constraint (declared and engine-enforced), or a unique
3390    /// index — i.e. an equality on it matches at most one row.
3391    fn column_is_single_unique(&self, table: &str, col: &str) -> bool {
3392        let Some(t) = self.active_catalog().get(table) else {
3393            return false;
3394        };
3395        let sch = t.schema();
3396        let Some(pos) = sch
3397            .columns
3398            .iter()
3399            .position(|c| c.name.eq_ignore_ascii_case(col))
3400        else {
3401            return false;
3402        };
3403        if sch
3404            .uniqueness_constraints
3405            .iter()
3406            .any(|u| u.columns.as_slice() == [pos])
3407        {
3408            return true;
3409        }
3410        t.index_on(pos).is_some_and(|idx| idx.is_unique)
3411    }
3412}
3413
3414// ---- subquery free-fn helpers (lib.rs split 6) ----
3415
3416/// v7.33 — true when every column in `e` is qualified to `inner_alias`
3417/// and `e` contains no nested subquery. Used by the sublink pull-up to
3418/// confirm a non-correlation conjunct is purely inner (safe to carry into
3419/// the join ON after a qualifier rename).
3420/// v7.37.4 — refuse projection expressions that would dangle after
3421/// the LIMIT 1 pullup: aggregates / window calls / EXISTS / scalar
3422/// subqueries / outer-qualified columns (the pulled-up CTE body is
3423/// uncorrelated, so an outer reference inside the projection has no
3424/// scope to bind against). All-inner column references are fine.
3425fn proj_has_disqualifying_shape(
3426    e: &Expr,
3427    inner_alias: &str,
3428    outer_aliases: &alloc::collections::BTreeSet<String>,
3429) -> bool {
3430    match e {
3431        Expr::AggregateOrdered { .. }
3432        | Expr::WindowFunction { .. }
3433        | Expr::ScalarSubquery(_)
3434        | Expr::Exists { .. } => true,
3435        Expr::FunctionCall { name, args } => {
3436            if aggregate::is_aggregate_name(name) {
3437                return true;
3438            }
3439            args.iter()
3440                .any(|a| proj_has_disqualifying_shape(a, inner_alias, outer_aliases))
3441        }
3442        Expr::Column(c) => {
3443            // Reject outer-qualified columns inside the projection
3444            // (they'd dangle in the uncorrelated CTE body). Unqualified
3445            // columns are ambiguous in a multi-table inner — for the
3446            // phase-2 single-table gate they resolve to `inner_alias`
3447            // anyway, accept them. Qualified inner refs are OK.
3448            if let Some(q) = c.qualifier.as_deref() {
3449                outer_aliases.contains(&q.to_ascii_lowercase())
3450                    && !q.eq_ignore_ascii_case(inner_alias)
3451            } else {
3452                false
3453            }
3454        }
3455        Expr::Binary { lhs, rhs, .. } => {
3456            proj_has_disqualifying_shape(lhs, inner_alias, outer_aliases)
3457                || proj_has_disqualifying_shape(rhs, inner_alias, outer_aliases)
3458        }
3459        Expr::Unary { expr, .. }
3460        | Expr::Cast { expr, .. }
3461        | Expr::IsNull { expr, .. }
3462        | Expr::BoolTest { expr, .. }
3463        | Expr::FieldAccess { base: expr, .. } => {
3464            proj_has_disqualifying_shape(expr, inner_alias, outer_aliases)
3465        }
3466        Expr::Like { expr, pattern, .. } => {
3467            proj_has_disqualifying_shape(expr, inner_alias, outer_aliases)
3468                || proj_has_disqualifying_shape(pattern, inner_alias, outer_aliases)
3469        }
3470        Expr::InList { expr, list, .. } => {
3471            proj_has_disqualifying_shape(expr, inner_alias, outer_aliases)
3472                || list
3473                    .iter()
3474                    .any(|it| proj_has_disqualifying_shape(it, inner_alias, outer_aliases))
3475        }
3476        Expr::Case {
3477            operand,
3478            branches,
3479            else_branch,
3480        } => {
3481            operand
3482                .as_ref()
3483                .is_some_and(|o| proj_has_disqualifying_shape(o, inner_alias, outer_aliases))
3484                || branches.iter().any(|(w, t)| {
3485                    proj_has_disqualifying_shape(w, inner_alias, outer_aliases)
3486                        || proj_has_disqualifying_shape(t, inner_alias, outer_aliases)
3487                })
3488                || else_branch
3489                    .as_ref()
3490                    .is_some_and(|b| proj_has_disqualifying_shape(b, inner_alias, outer_aliases))
3491        }
3492        Expr::ArraySubscript { target, index } => {
3493            proj_has_disqualifying_shape(target, inner_alias, outer_aliases)
3494                || proj_has_disqualifying_shape(index, inner_alias, outer_aliases)
3495        }
3496        _ => false,
3497    }
3498}
3499
3500/// v7.37.4 A'' — walk every Expr field of a SelectStatement and
3501/// qualify any unqualified column whose name is in `owner`. Skips
3502/// nested subqueries' bodies (they own their own scope) but covers
3503/// SELECT items, WHERE, GROUP BY, HAVING, ORDER BY, and the
3504/// outer FROM clause's join ON predicates. Pulled-up join names
3505/// (`__exsj_*` / `__cl1_*` / `__plj_*`) are NOT in `owner`, so this
3506/// pass is idempotent under re-runs.
3507fn disambiguate_stmt_unqualified_columns(
3508    stmt: &mut SelectStatement,
3509    owner: &alloc::collections::BTreeMap<String, String>,
3510) {
3511    for item in &mut stmt.items {
3512        if let SelectItem::Expr { expr, .. } = item {
3513            disambiguate_expr_unqualified_columns(expr, owner);
3514        }
3515    }
3516    if let Some(from) = &mut stmt.from {
3517        for j in &mut from.joins {
3518            if let Some(on) = &mut j.on {
3519                disambiguate_expr_unqualified_columns(on, owner);
3520            }
3521        }
3522    }
3523    if let Some(g) = &mut stmt.group_by {
3524        for e in g.iter_mut() {
3525            disambiguate_expr_unqualified_columns(e, owner);
3526        }
3527    }
3528    if let Some(h) = &mut stmt.having {
3529        disambiguate_expr_unqualified_columns(h, owner);
3530    }
3531    for ob in &mut stmt.order_by {
3532        disambiguate_expr_unqualified_columns(&mut ob.expr, owner);
3533    }
3534}
3535
3536fn disambiguate_expr_unqualified_columns(
3537    e: &mut Expr,
3538    owner: &alloc::collections::BTreeMap<String, String>,
3539) {
3540    match e {
3541        Expr::Column(c) => {
3542            if c.qualifier.is_none()
3543                && let Some(alias) = owner.get(&c.name.to_ascii_lowercase())
3544            {
3545                c.qualifier = Some(alias.clone());
3546            }
3547        }
3548        Expr::Binary { lhs, rhs, .. } => {
3549            disambiguate_expr_unqualified_columns(lhs, owner);
3550            disambiguate_expr_unqualified_columns(rhs, owner);
3551        }
3552        Expr::Unary { expr, .. }
3553        | Expr::Cast { expr, .. }
3554        | Expr::IsNull { expr, .. }
3555        | Expr::BoolTest { expr, .. }
3556        | Expr::FieldAccess { base: expr, .. } => {
3557            disambiguate_expr_unqualified_columns(expr, owner);
3558        }
3559        Expr::FunctionCall { args, .. } => {
3560            for a in args.iter_mut() {
3561                disambiguate_expr_unqualified_columns(a, owner);
3562            }
3563        }
3564        Expr::AggregateOrdered {
3565            call,
3566            order_by,
3567            filter,
3568            ..
3569        } => {
3570            disambiguate_expr_unqualified_columns(call, owner);
3571            for ob in order_by.iter_mut() {
3572                disambiguate_expr_unqualified_columns(&mut ob.expr, owner);
3573            }
3574            if let Some(f) = filter {
3575                disambiguate_expr_unqualified_columns(f, owner);
3576            }
3577        }
3578        Expr::Like { expr, pattern, .. } => {
3579            disambiguate_expr_unqualified_columns(expr, owner);
3580            disambiguate_expr_unqualified_columns(pattern, owner);
3581        }
3582        Expr::InList { expr, list, .. } => {
3583            disambiguate_expr_unqualified_columns(expr, owner);
3584            for it in list.iter_mut() {
3585                disambiguate_expr_unqualified_columns(it, owner);
3586            }
3587        }
3588        Expr::Case {
3589            operand,
3590            branches,
3591            else_branch,
3592        } => {
3593            if let Some(o) = operand {
3594                disambiguate_expr_unqualified_columns(o, owner);
3595            }
3596            for (w, t) in branches.iter_mut() {
3597                disambiguate_expr_unqualified_columns(w, owner);
3598                disambiguate_expr_unqualified_columns(t, owner);
3599            }
3600            if let Some(eb) = else_branch {
3601                disambiguate_expr_unqualified_columns(eb, owner);
3602            }
3603        }
3604        Expr::ArraySubscript { target, index } => {
3605            disambiguate_expr_unqualified_columns(target, owner);
3606            disambiguate_expr_unqualified_columns(index, owner);
3607        }
3608        // Subquery bodies own their own scope — leave untouched.
3609        _ => {}
3610    }
3611}
3612
3613/// v7.39 (round 721) — integer-only over the OUTER side: every column
3614/// belongs to an outer alias whose stored table types it integer-family,
3615/// every literal a plain integer, operators closed over the integers.
3616/// The admission bar for a computed correlation half: exactly what the
3617/// round-720 mirror hash lane executes.
3618fn outer_int_only_expr(
3619    e: &Expr,
3620    outer_aliases: &alloc::collections::BTreeSet<String>,
3621    outer_tables: &alloc::collections::BTreeMap<String, String>,
3622    engine: &Engine,
3623) -> bool {
3624    match e {
3625        Expr::Column(c) => {
3626            let Some(q) = c.qualifier.as_deref() else {
3627                return false;
3628            };
3629            let q = q.to_ascii_lowercase();
3630            if !outer_aliases.contains(&q) {
3631                return false;
3632            }
3633            let Some(tname) = outer_tables.get(&q) else {
3634                return false;
3635            };
3636            engine.active_catalog().get(tname).is_some_and(|t| {
3637                t.schema().columns.iter().any(|cs| {
3638                    cs.name.eq_ignore_ascii_case(&c.name)
3639                        && matches!(
3640                            cs.ty,
3641                            spg_storage::DataType::Int
3642                                | spg_storage::DataType::BigInt
3643                                | spg_storage::DataType::SmallInt
3644                        )
3645                })
3646            })
3647        }
3648        Expr::Literal(spg_sql::ast::Literal::Integer(_)) => true,
3649        Expr::Binary { lhs, op, rhs } => {
3650            matches!(op, BinOp::Add | BinOp::Sub | BinOp::Mul)
3651                && outer_int_only_expr(lhs, outer_aliases, outer_tables, engine)
3652                && outer_int_only_expr(rhs, outer_aliases, outer_tables, engine)
3653        }
3654        _ => false,
3655    }
3656}
3657
3658fn expr_is_all_inner(e: &Expr, inner_alias: &str) -> bool {
3659    let mut cols: Vec<ColumnName> = Vec::new();
3660    let mut subs: Vec<&SelectStatement> = Vec::new();
3661    visit_expr_columns_and_subqueries(e, &mut |c| cols.push(c.clone()), &mut |s| subs.push(s));
3662    subs.is_empty()
3663        && cols.iter().all(|c| {
3664            c.qualifier
3665                .as_deref()
3666                .is_some_and(|q| q.eq_ignore_ascii_case(inner_alias))
3667        })
3668}
3669
3670/// v7.33 — rename every column qualifier equal to `from` into `to` in
3671/// place. Used to retarget an inner subquery's predicates from its
3672/// original table alias onto the fresh LEFT JOIN alias.
3673fn rename_qualifier(e: &mut Expr, from: &str, to: &str) {
3674    match e {
3675        Expr::Column(c) => {
3676            if c.qualifier
3677                .as_deref()
3678                .is_some_and(|q| q.eq_ignore_ascii_case(from))
3679            {
3680                c.qualifier = Some(to.into());
3681            }
3682        }
3683        Expr::Binary { lhs, rhs, .. } => {
3684            rename_qualifier(lhs, from, to);
3685            rename_qualifier(rhs, from, to);
3686        }
3687        Expr::Unary { expr, .. }
3688        | Expr::Cast { expr, .. }
3689        | Expr::IsNull { expr, .. }
3690        | Expr::BoolTest { expr, .. }
3691        | Expr::FieldAccess { base: expr, .. } => {
3692            rename_qualifier(expr, from, to);
3693        }
3694        Expr::FunctionCall { args, .. } => {
3695            for a in args.iter_mut() {
3696                rename_qualifier(a, from, to);
3697            }
3698        }
3699        Expr::Like { expr, pattern, .. } => {
3700            rename_qualifier(expr, from, to);
3701            rename_qualifier(pattern, from, to);
3702        }
3703        Expr::InList { expr, list, .. } => {
3704            rename_qualifier(expr, from, to);
3705            for it in list.iter_mut() {
3706                rename_qualifier(it, from, to);
3707            }
3708        }
3709        Expr::Case {
3710            operand,
3711            branches,
3712            else_branch,
3713        } => {
3714            if let Some(o) = operand {
3715                rename_qualifier(o, from, to);
3716            }
3717            for (w, t) in branches.iter_mut() {
3718                rename_qualifier(w, from, to);
3719                rename_qualifier(t, from, to);
3720            }
3721            if let Some(eb) = else_branch {
3722                rename_qualifier(eb, from, to);
3723            }
3724        }
3725        _ => {}
3726    }
3727}
3728
3729/// v4.23: recognise the engine errors that indicate the inner
3730/// SELECT couldn't be evaluated in isolation because it references
3731/// an outer column — used by `subquery_replacement` to skip
3732/// materialisation and let row-eval handle it instead.
3733fn is_correlation_error(e: &EngineError) -> bool {
3734    matches!(
3735        e,
3736        EngineError::Eval(
3737            eval::EvalError::ColumnNotFound { .. } | eval::EvalError::UnknownQualifier { .. }
3738        )
3739    )
3740}
3741
3742/// v7.32 (R30 memory) — cheap static correlation pre-check.
3743///
3744/// `subquery_replacement` distinguishes a correlated subquery from an
3745/// uncorrelated one by *optimistically executing* it and catching the
3746/// resulting `ColumnNotFound` / `UnknownQualifier`. For a join-bodied
3747/// correlated subquery that catch fires only AFTER the inner FROM is
3748/// materialised — and the deferred-join pipeline clones the whole
3749/// driving table to do it (the inbox `… JOIN messages m2 …` body
3750/// clones 960k × 10 KB ≈ 10 GB at prod scale, once per outer query,
3751/// purely to be thrown away). A correlated subquery is always handled
3752/// downstream by the per-row / post-LIMIT correlated path, so spotting
3753/// it up front lets us skip the wasted materialisation entirely.
3754///
3755/// Sound for the `true` answer: returns true only when a qualified
3756/// column at the statement's own level names a qualifier that is not
3757/// one of its own FROM aliases — exactly the reference the inner exec
3758/// would fail to resolve. Everything it can't reason about cleanly
3759/// (lateral / derived FROM entries) returns false and falls through to
3760/// the existing execute-and-catch path, so behaviour is unchanged.
3761/// v7.37.x (docker-fair SCALARSQ attack) — pre-analysed plan for the
3762/// `(SELECT COUNT(*) FROM T WHERE T.pk = outer.col)` correlated
3763/// scalar subquery shape. Computing the table + index + position
3764/// lookups once per query (instead of once per outer row) drops the
3765/// per-row work to a single column read + index probe.
3766#[derive(Debug, Clone)]
3767pub struct ScalarPkProbeFastPath {
3768    /// Position in the OUTER scan schema for the column that drives
3769    /// the equality. Per row we read `row.values[outer_pos]` directly.
3770    pub outer_pos: usize,
3771    /// Catalog-qualified name of the inner table (looked up per probe).
3772    pub inner_table_name: String,
3773    /// Column position of the inner-side PK on which we probe.
3774    pub inner_pos: usize,
3775    /// v7.37.42 (docker-fair SCALARSQ attack 1) — cached insertion-order
3776    /// index of `inner_table_name` in the active catalog at PREPARE time.
3777    /// The executor and prepare share a single engine `RwLock` read guard
3778    /// per query (see `pgwire.rs` simple-query path), so the catalog
3779    /// can't mutate mid-query — the cached index stays in sync with the
3780    /// string name. The per-row probe therefore skips the
3781    /// `BTreeMap<String, usize>` descent that `Catalog::get(&str)` would
3782    /// otherwise perform, saving ~300 ns × N outer rows.
3783    pub table_idx: usize,
3784}
3785
3786impl ScalarPkProbeFastPath {
3787    /// Per-row probe. Reads `row.values[self.outer_pos]`, looks up the
3788    /// inner table and PK index, and returns `Int(1)` on a hit or
3789    /// `Int(0)` on a miss / NULL outer key.
3790    pub fn probe(&self, row: &Row<'static>) -> Value<'static> {
3791        // The engine handle is needed to access the live catalog. The
3792        // probe is called from the run-loop with the engine in scope,
3793        // so we look up the catalog via a thread_local-cached
3794        // borrow. Simpler: defer to the engine helper that takes the
3795        // pre-analysed plan + the row. Kept here as a vtable-style
3796        // entry point so the run-loop's hot path is small.
3797        let outer_int = match row.values.get(self.outer_pos) {
3798            Some(Value::BigInt(n)) => *n,
3799            Some(Value::Int(n)) => i64::from(*n),
3800            Some(Value::SmallInt(n)) => i64::from(*n),
3801            Some(Value::Null) | None => return Value::BigInt(0),
3802            _ => return Value::BigInt(0),
3803        };
3804        SCALARSQ_PK_PROBE_PLAN_OUTER_INT.store(outer_int, core::sync::atomic::Ordering::Relaxed);
3805        SCALARSQ_PK_PROBE_PLAN_FIRED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3806        // The actual seek lives in `Engine::probe_with_pk_fast_path` —
3807        // we can't carry an engine borrow here without a lifetime
3808        // round-trip. Returning BigInt(0) as a placeholder would break
3809        // semantics; instead the run-loop calls
3810        // `engine.probe_with_pk_fast_path(&self, row)` directly so
3811        // the plan's `probe()` method is used only in tests where
3812        // the table data isn't load-bearing.
3813        Value::BigInt(0)
3814    }
3815}
3816
3817/// v7.37.x — per-row hit counter for the plan-cached fast path.
3818pub static SCALARSQ_PK_PROBE_PLAN_FIRED: core::sync::atomic::AtomicU64 =
3819    core::sync::atomic::AtomicU64::new(0);
3820pub static SCALARSQ_PK_PROBE_PLAN_OUTER_INT: core::sync::atomic::AtomicI64 =
3821    core::sync::atomic::AtomicI64::new(0);
3822
3823/// v7.37.x (docker-fair SCALARSQ attack) — direct PK probe for the
3824/// `(SELECT COUNT(*) FROM T WHERE T.pk = outer.col)` correlated
3825/// scalar subquery shape. Returns `Some(BigInt(0))` if the probe misses
3826/// or `Some(BigInt(1))` if it hits; `None` when the shape doesn't match
3827/// (caller falls back to per-row exec). Bypasses parse / resolve /
3828/// plan / aggregate; the SCALARSQ docker-fair bench drops from
3829/// per-row ~3 µs to per-row ~100 ns.
3830impl Engine {
3831    /// Run a pre-analysed PK probe against the live catalog. Used by
3832    /// the per-row projection fast path to avoid going through
3833    /// `eval_expr_with_correlated`.
3834    pub(crate) fn probe_with_pk_fast_path(
3835        &self,
3836        plan: &ScalarPkProbeFastPath,
3837        row: &Row<'static>,
3838    ) -> Value<'static> {
3839        let outer_int = match row.values.get(plan.outer_pos) {
3840            Some(Value::BigInt(n)) => *n,
3841            Some(Value::Int(n)) => i64::from(*n),
3842            Some(Value::SmallInt(n)) => i64::from(*n),
3843            Some(Value::Null) | None => return Value::BigInt(0),
3844            _ => return Value::BigInt(0),
3845        };
3846        // v7.37.42 attack 1 — bypass per-row `BTreeMap<String,usize>::get`
3847        // by going through the cached positional index. The prepare-time
3848        // analyser stores the index against the same catalog snapshot
3849        // the executor sees (same engine read guard), so the cached
3850        // index remains valid for the query's duration.
3851        let Some(inner_table) = self.active_catalog().tables_at(plan.table_idx) else {
3852            return Value::BigInt(0);
3853        };
3854        let Some(idx) = inner_table.index_on(plan.inner_pos) else {
3855            return Value::BigInt(0);
3856        };
3857        // r1039 — in the inner column's key space, not `BigInt`'s.
3858        let Some(key) = inner_table
3859            .schema()
3860            .columns
3861            .get(plan.inner_pos)
3862            .and_then(|c| {
3863                spg_storage::IndexKey::from_value_for_column(&Value::BigInt(outer_int), c.ty)
3864            })
3865        else {
3866            return Value::BigInt(0);
3867        };
3868        let hit = !idx.lookup_eq(&key).is_empty();
3869        SCALARSQ_PK_PROBE_FIRED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3870        Value::BigInt(i64::from(hit))
3871    }
3872
3873    /// Analyse a scalar subquery against the OUTER scan schema; return
3874    /// a `ScalarPkProbeFastPath` plan when the canonical shape is
3875    /// recognised, otherwise `None`. The outer alias and column-name
3876    /// resolution use the scan schema so the run-loop can read the
3877    /// outer value by position.
3878    pub(crate) fn analyse_scalar_count_pk_eq_probe(
3879        &self,
3880        inner: &SelectStatement,
3881        outer_schema: &[spg_storage::ColumnSchema],
3882        outer_alias: &str,
3883    ) -> Option<ScalarPkProbeFastPath> {
3884        use spg_sql::ast::{BinOp, ColumnName, SelectItem};
3885        if !inner.ctes.is_empty()
3886            || !inner.unions.is_empty()
3887            || inner.group_by.is_some()
3888            || inner.having.is_some()
3889            || inner.distinct
3890            || !inner.order_by.is_empty()
3891            || inner.limit.is_some()
3892            || inner.offset.is_some()
3893            || inner.items.len() != 1
3894        {
3895            return None;
3896        }
3897        let SelectItem::Expr { expr, .. } = &inner.items[0] else {
3898            return None;
3899        };
3900        let is_count_shape = match expr {
3901            Expr::FunctionCall { name, args } => {
3902                (name.eq_ignore_ascii_case("count_star") && args.is_empty())
3903                    || name.eq_ignore_ascii_case("count")
3904            }
3905            _ => false,
3906        };
3907        if !is_count_shape {
3908            return None;
3909        }
3910        let from = inner.from.as_ref()?;
3911        if !from.joins.is_empty()
3912            || from.primary.lateral_subquery.is_some()
3913            || from.primary.unnest_expr.is_some()
3914            || from.primary.generate_series_args.is_some()
3915            || from.primary.as_of_segment.is_some()
3916        {
3917            return None;
3918        }
3919        let inner_table_name = from.primary.name.clone();
3920        let inner_alias = from
3921            .primary
3922            .alias
3923            .as_deref()
3924            .unwrap_or(inner_table_name.as_str());
3925        let where_expr = inner.where_.as_ref()?;
3926        let Expr::Binary {
3927            lhs,
3928            op: BinOp::Eq,
3929            rhs,
3930        } = where_expr
3931        else {
3932            return None;
3933        };
3934        let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref()) else {
3935            return None;
3936        };
3937        let pick = |x: &ColumnName, y: &ColumnName| -> Option<(String, ColumnName)> {
3938            if x.qualifier
3939                .as_deref()
3940                .is_some_and(|q| q.eq_ignore_ascii_case(inner_alias))
3941            {
3942                Some((x.name.clone(), y.clone()))
3943            } else {
3944                None
3945            }
3946        };
3947        let (inner_col_name, outer_col) = pick(a, b).or_else(|| pick(b, a))?;
3948        // Outer column must be in the scan schema and qualified to
3949        // outer_alias (or unqualified).
3950        if let Some(q) = outer_col.qualifier.as_deref()
3951            && !q.eq_ignore_ascii_case(outer_alias)
3952        {
3953            return None;
3954        }
3955        let outer_pos = outer_schema
3956            .iter()
3957            .position(|c| c.name.eq_ignore_ascii_case(&outer_col.name))?;
3958        // Inner column must be a single-column PK on an integer family.
3959        // v7.37.42 attack 1 — resolve the inner table's positional index
3960        // alongside the table fetch so the per-row probe can skip the
3961        // `BTreeMap<String,usize>::get(&str)` descent.
3962        let catalog = self.active_catalog();
3963        let table_idx = catalog.tables_position_of(inner_table_name.as_str())?;
3964        let inner_table = catalog.tables_at(table_idx)?;
3965        let inner_schema_ref = inner_table.schema();
3966        let inner_pos = inner_schema_ref
3967            .columns
3968            .iter()
3969            .position(|c| c.name.eq_ignore_ascii_case(&inner_col_name))?;
3970        if !matches!(
3971            inner_schema_ref.columns[inner_pos].ty,
3972            spg_storage::DataType::BigInt
3973                | spg_storage::DataType::Int
3974                | spg_storage::DataType::SmallInt
3975        ) {
3976            return None;
3977        }
3978        if !inner_schema_ref
3979            .uniqueness_constraints
3980            .iter()
3981            .any(|u| u.is_primary_key && u.columns.as_slice() == [inner_pos])
3982        {
3983            return None;
3984        }
3985        Some(ScalarPkProbeFastPath {
3986            outer_pos,
3987            inner_table_name,
3988            inner_pos,
3989            table_idx,
3990        })
3991    }
3992
3993    pub(crate) fn try_scalar_count_pk_eq_probe(
3994        &self,
3995        inner: &SelectStatement,
3996        row: &Row<'static>,
3997        ctx: &EvalContext<'_>,
3998    ) -> Result<Option<Value<'static>>, EngineError> {
3999        use spg_sql::ast::{BinOp, ColumnName, SelectItem};
4000        if !inner.ctes.is_empty()
4001            || !inner.unions.is_empty()
4002            || inner.group_by.is_some()
4003            || inner.having.is_some()
4004            || inner.distinct
4005            || !inner.order_by.is_empty()
4006            || inner.limit.is_some()
4007            || inner.offset.is_some()
4008            || inner.items.len() != 1
4009        {
4010            return Ok(None);
4011        }
4012        let SelectItem::Expr { expr, .. } = &inner.items[0] else {
4013            return Ok(None);
4014        };
4015        let is_count_shape = match expr {
4016            Expr::FunctionCall { name, args } => {
4017                (name.eq_ignore_ascii_case("count_star") && args.is_empty())
4018                    || name.eq_ignore_ascii_case("count")
4019            }
4020            _ => false,
4021        };
4022        if !is_count_shape {
4023            return Ok(None);
4024        }
4025        let Some(from) = &inner.from else {
4026            return Ok(None);
4027        };
4028        if !from.joins.is_empty()
4029            || from.primary.lateral_subquery.is_some()
4030            || from.primary.unnest_expr.is_some()
4031            || from.primary.generate_series_args.is_some()
4032            || from.primary.as_of_segment.is_some()
4033        {
4034            return Ok(None);
4035        }
4036        let inner_table_name = from.primary.name.as_str();
4037        let inner_alias = from.primary.alias.as_deref().unwrap_or(inner_table_name);
4038        let Some(where_expr) = &inner.where_ else {
4039            return Ok(None);
4040        };
4041        let Expr::Binary {
4042            lhs,
4043            op: BinOp::Eq,
4044            rhs,
4045        } = where_expr
4046        else {
4047            return Ok(None);
4048        };
4049        let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref()) else {
4050            return Ok(None);
4051        };
4052        let pick = |x: &ColumnName, y: &ColumnName| -> Option<(String, ColumnName)> {
4053            if x.qualifier
4054                .as_deref()
4055                .is_some_and(|q| q.eq_ignore_ascii_case(inner_alias))
4056            {
4057                Some((x.name.clone(), y.clone()))
4058            } else {
4059                None
4060            }
4061        };
4062        let Some((inner_col_name, outer_col)) = pick(a, b).or_else(|| pick(b, a)) else {
4063            return Ok(None);
4064        };
4065        let catalog = self.active_catalog();
4066        let Some(inner_table) = catalog.get(inner_table_name) else {
4067            return Ok(None);
4068        };
4069        let inner_schema = inner_table.schema();
4070        let Some(inner_pos) = inner_schema
4071            .columns
4072            .iter()
4073            .position(|c| c.name.eq_ignore_ascii_case(&inner_col_name))
4074        else {
4075            return Ok(None);
4076        };
4077        if !matches!(
4078            inner_schema.columns[inner_pos].ty,
4079            spg_storage::DataType::BigInt
4080                | spg_storage::DataType::Int
4081                | spg_storage::DataType::SmallInt
4082        ) {
4083            return Ok(None);
4084        }
4085        if !inner_schema
4086            .uniqueness_constraints
4087            .iter()
4088            .any(|u| u.is_primary_key && u.columns.as_slice() == [inner_pos])
4089        {
4090            return Ok(None);
4091        }
4092        let outer_val = match eval::eval_expr(&Expr::Column(outer_col), row, ctx) {
4093            Ok(v) => v,
4094            Err(_) => return Ok(None),
4095        };
4096        let outer_int = match outer_val {
4097            Value::BigInt(n) => n,
4098            Value::Int(n) => i64::from(n),
4099            Value::SmallInt(n) => i64::from(n),
4100            Value::Null => return Ok(Some(Value::BigInt(0))),
4101            _ => return Ok(None),
4102        };
4103        let Some(idx) = inner_table.index_on(inner_pos) else {
4104            return Ok(None);
4105        };
4106        // r1039 — in the inner column's key space, not `BigInt`'s.
4107        let Some(key) = inner_schema.columns.get(inner_pos).and_then(|c| {
4108            spg_storage::IndexKey::from_value_for_column(&Value::BigInt(outer_int), c.ty)
4109        }) else {
4110            return Ok(None);
4111        };
4112        SCALARSQ_PK_PROBE_FIRED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
4113        let hit = !idx.lookup_eq(&key).is_empty();
4114        Ok(Some(Value::BigInt(i64::from(hit))))
4115    }
4116}
4117
4118pub static SCALARSQ_PK_PROBE_FIRED: core::sync::atomic::AtomicU64 =
4119    core::sync::atomic::AtomicU64::new(0);
4120
4121/// v7.37.x (docker-fair SCALARSQ attack) — return the SQL empty-set
4122/// default for a scalar subquery's output expression. PG semantics
4123/// distinguish `COUNT(*)` (0 over an empty set) from other aggregates
4124/// (NULL). Called by the batched ScalarSubquery resolver when a
4125/// per-outer-row probe finds no matching inner partition.
4126fn scalar_subquery_empty_default(inner: &SelectStatement) -> Value<'static> {
4127    use spg_sql::ast::SelectItem;
4128    if inner.items.len() != 1 {
4129        return Value::Null;
4130    }
4131    let SelectItem::Expr { expr, .. } = &inner.items[0] else {
4132        return Value::Null;
4133    };
4134    fn is_count(e: &Expr) -> bool {
4135        match e {
4136            // COUNT(*) parses as `count_star`; COUNT(col) as `count`.
4137            // Both have BIGINT-shaped empty-set default of 0.
4138            Expr::FunctionCall { name, .. } => {
4139                name.eq_ignore_ascii_case("count") || name.eq_ignore_ascii_case("count_star")
4140            }
4141            Expr::AggregateOrdered { call, .. } => is_count(call),
4142            _ => false,
4143        }
4144    }
4145    if is_count(expr) {
4146        // v7.39 (round 189) — count is BIGINT; the Int(0) default
4147        // leaked an integer-typed zero on the empty-set path.
4148        Value::BigInt(0)
4149    } else {
4150        Value::Null
4151    }
4152}
4153
4154/// v7.39 (round 545) — does this qualifier name that relation?
4155///
4156/// A catalog reference is rewritten to a synthetic name before it
4157/// reaches the engine (`pg_type` becomes `__spg_pg_type`), and the
4158/// rewrite happens in the FROM clause but not in the QUALIFIER a
4159/// correlated reference writes:
4160///
4161/// ```text
4162///     SELECT typname, (SELECT typarray FROM pg_type te
4163///                      WHERE te.oid = pg_type.typelem) FROM pg_type
4164///     PG18  answers      SPG  missing FROM-clause entry for "pg_type"
4165/// ```
4166///
4167/// which is how pg_dump asks whether a type is an array type. The
4168/// written name and the rewritten one are the same relation.
4169fn relation_name_matches(qualifier: &str, relation: &str) -> bool {
4170    if qualifier.eq_ignore_ascii_case(relation) {
4171        return true;
4172    }
4173    let rewritten = if let Some(bare) = qualifier
4174        .to_ascii_lowercase()
4175        .strip_prefix("pg_")
4176        .map(alloc::string::String::from)
4177    {
4178        alloc::format!("__spg_pg_{bare}")
4179    } else {
4180        alloc::format!("__spg_info_{}", qualifier.to_ascii_lowercase())
4181    };
4182    rewritten.eq_ignore_ascii_case(relation)
4183}
4184
4185/// v7.39 (round 545) — the column names this statement's own FROM
4186/// scope makes visible, or `None` when they cannot all be determined.
4187///
4188/// SQL resolves an unqualified name innermost-first and walks OUTWARD
4189/// when it is not there. SPG only ever looked inward, so every
4190/// correlated subquery written the ordinary way failed outright:
4191///
4192/// ```text
4193///     SELECT v, (SELECT w FROM ob WHERE bid = aid) FROM oa
4194///     PG18  x|B1, y|B2       SPG  ERROR: column "aid" does not exist
4195/// ```
4196///
4197/// Only the qualified spelling (`oa.aid`) worked — which is why the gap
4198/// survived: the catalog queries and the tests that exercised
4199/// correlation all wrote the qualifier.
4200///
4201/// A name in BOTH scopes belongs to the inner one, as in PG, so this
4202/// set is what decides — and it has to be COMPLETE to decide anything.
4203/// A FROM entry whose columns this cannot enumerate (a CTE, a view, a
4204/// set-returning function) makes the whole answer `None`, and a `None`
4205/// leaves bare names alone rather than guessing they are outer. Naming
4206/// an inner column as outer would splice the wrong row's value in,
4207/// which is silently wrong; leaving it alone is the behaviour that was
4208/// already there.
4209fn inner_scope_column_names(
4210    s: &SelectStatement,
4211    cat: &spg_storage::Catalog,
4212) -> Option<alloc::collections::BTreeSet<alloc::string::String>> {
4213    use spg_sql::ast::SelectItem;
4214    fn add_table(
4215        t: &spg_sql::ast::TableRef,
4216        cat: &spg_storage::Catalog,
4217        names: &mut alloc::collections::BTreeSet<alloc::string::String>,
4218    ) -> bool {
4219        if let Some(body) = &t.lateral_subquery {
4220            // A derived body publishes its own items; a `*` among them
4221            // republishes whatever it selected from, so recurse.
4222            //
4223            // A body with NO items is a VALUES list, whose column names
4224            // live in the alias rather than the statement — unknowable
4225            // here, and an empty set would read as "the inner scope
4226            // supplies nothing", which is the opposite of the truth.
4227            if body.items.is_empty() {
4228                return false;
4229            }
4230            for item in &body.items {
4231                match item {
4232                    SelectItem::Expr { alias: Some(a), .. } => {
4233                        names.insert(a.to_ascii_lowercase());
4234                    }
4235                    SelectItem::Expr {
4236                        expr: Expr::Column(c),
4237                        ..
4238                    } => {
4239                        names.insert(c.name.to_ascii_lowercase());
4240                    }
4241                    SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
4242                        let Some(inner) = inner_scope_column_names(body, cat) else {
4243                            return false;
4244                        };
4245                        names.extend(inner);
4246                    }
4247                    SelectItem::Expr { .. } => return false,
4248                }
4249            }
4250            return true;
4251        }
4252        // 7.38.1 S5.1 — a FROM-position table function's output
4253        // columns ARE knowable: the explicit alias column list wins,
4254        // and the builtins with a fixed shape publish it (pg_dump's
4255        // per-attribute pass needs `option_name` recognised as INNER
4256        // so the bare outer `attfdwoptions` next to it gets spliced).
4257        if let Some(call) = &t.table_fn_call {
4258            if !t.unnest_column_aliases.is_empty() {
4259                for c in &t.unnest_column_aliases {
4260                    names.insert(c.to_ascii_lowercase());
4261                }
4262                return true;
4263            }
4264            if call.0.eq_ignore_ascii_case("pg_options_to_table") {
4265                names.insert("option_name".into());
4266                names.insert("option_value".into());
4267                return true;
4268            }
4269            return false;
4270        }
4271        if t.unnest_expr.is_some() || t.generate_series_args.is_some() || t.name.is_empty() {
4272            return false;
4273        }
4274        let Some(tbl) = cat.get(&t.name) else {
4275            // A CTE, a view, or a name this catalog does not hold.
4276            return false;
4277        };
4278        for col in &tbl.schema().columns {
4279            names.insert(col.name.to_ascii_lowercase());
4280        }
4281        true
4282    }
4283    let mut names: alloc::collections::BTreeSet<alloc::string::String> =
4284        alloc::collections::BTreeSet::new();
4285    let from = s.from.as_ref()?;
4286    if !add_table(&from.primary, cat, &mut names) {
4287        return None;
4288    }
4289    for j in &from.joins {
4290        if !add_table(&j.table, cat, &mut names) {
4291            return None;
4292        }
4293    }
4294    // The statement's own output aliases are in scope for ORDER BY /
4295    // HAVING and must not be mistaken for outer references.
4296    for item in &s.items {
4297        if let SelectItem::Expr { alias: Some(a), .. } = item {
4298            names.insert(a.to_ascii_lowercase());
4299        }
4300    }
4301    // A CTE the statement defines is inner too, and its columns are not
4302    // in the catalog — so a WITH makes the answer unknowable here.
4303    if !s.ctes.is_empty() {
4304        return None;
4305    }
4306    Some(names)
4307}
4308
4309/// Is this bare name one the engine generated rather than the user?
4310fn is_synthetic_column_name(n: &str) -> bool {
4311    n.starts_with("__grp_") || n.starts_with("__agg_") || n.starts_with("__spg_")
4312}
4313
4314pub(crate) fn select_is_correlated(s: &SelectStatement) -> bool {
4315    use spg_sql::ast::SelectItem;
4316    let Some(from) = &s.from else {
4317        // No FROM: correlated iff some projected column is qualified
4318        // (a qualifier with nothing to bind to is necessarily outer).
4319        let mut qualified = false;
4320        for item in &s.items {
4321            if let SelectItem::Expr { expr, .. } = item {
4322                visit_expr_columns_and_subqueries(
4323                    expr,
4324                    &mut |c| {
4325                        if c.qualifier.is_some() {
4326                            qualified = true;
4327                        }
4328                    },
4329                    &mut |_| {},
4330                );
4331            }
4332        }
4333        return qualified;
4334    };
4335    // v7.39 (round 530) — a derived-table FROM entry used to answer "not
4336    // correlated" for the WHOLE subquery, on the grounds that its scope
4337    // was beyond this cheap check. The direction was backwards. An
4338    // uncorrelated subquery is evaluated ONCE and its answer reused for
4339    // every outer row, so a wrong "no" is silently wrong:
4340    //
4341    //   EXISTS(SELECT 1 FROM (SELECT 1 AS id) x WHERE t.id = x.id)
4342    //   PG18  true only for the matching row     SPG  true for EVERY row
4343    //
4344    // A wrong "yes" only costs a re-evaluation. So the derived entry's
4345    // alias joins the inner scope like any other name, and the ordinary
4346    // scan decides — plus the check below, since a derived body that is
4347    // itself correlated reaches outside its own scope.
4348    let mut inner: Vec<&str> = Vec::new();
4349    if let Some(a) = &from.primary.alias {
4350        inner.push(a.as_str());
4351    }
4352    if !from.primary.name.is_empty() {
4353        inner.push(from.primary.name.as_str());
4354    }
4355    for j in &from.joins {
4356        if let Some(a) = &j.table.alias {
4357            inner.push(a.as_str());
4358        }
4359        if !j.table.name.is_empty() {
4360            inner.push(j.table.name.as_str());
4361        }
4362    }
4363    // Gather every expression position that evaluates in this
4364    // statement's own scope (NOT inside nested subquery bodies — the
4365    // visitor reports those via the subquery callback, which we drop).
4366    let mut exprs: Vec<&Expr> = Vec::new();
4367    for item in &s.items {
4368        if let SelectItem::Expr { expr, .. } = item {
4369            exprs.push(expr);
4370        }
4371    }
4372    if let Some(w) = &s.where_ {
4373        exprs.push(w);
4374    }
4375    for j in &from.joins {
4376        if let Some(on) = &j.on {
4377            exprs.push(on);
4378        }
4379    }
4380    if let Some(gs) = &s.group_by {
4381        for g in gs {
4382            exprs.push(g);
4383        }
4384    }
4385    if let Some(h) = &s.having {
4386        exprs.push(h);
4387    }
4388    for o in &s.order_by {
4389        exprs.push(&o.expr);
4390    }
4391    let mut correlated = false;
4392    // v7.39 (round 545) — an UNQUALIFIED name that this statement's own
4393    // scope does not supply is an outer reference, which is how SQL
4394    // scoping works and how almost everyone writes a correlated
4395    // subquery. Only the qualified spelling was recognised before.
4396    for e in exprs {
4397        visit_expr_columns_and_subqueries(
4398            e,
4399            // v7.39 (round 545) — this stays the QUALIFIED-only test it
4400            // has always been. Teaching it about bare outer references
4401            // was tried and reverted: the runtime already routes such a
4402            // subquery to the per-row path (the pre-resolver hands it
4403            // back unreplaced), and claiming correlation up front pushed
4404            // shapes onto a path they are not resolved on —
4405            // `SELECT pg_typeof((SELECT count(*) FROM (VALUES (1)) b(y)))`
4406            // came back "subquery reached row eval". Nine tests said so.
4407            &mut |c| {
4408                if let Some(q) = &c.qualifier
4409                    && !inner.iter().any(|a| relation_name_matches(q, a))
4410                {
4411                    correlated = true;
4412                }
4413            },
4414            &mut |_| {},
4415        );
4416    }
4417    // A LATERAL body reads the row beside it, and its references never
4418    // appear in the expressions above — the visitor drops subquery
4419    // bodies. A body correlated against its OWN scope is reaching
4420    // further out, which is this statement's scope or beyond; either
4421    // way this statement has to be evaluated per row.
4422    if !correlated {
4423        for t in core::iter::once(&from.primary).chain(from.joins.iter().map(|j| &j.table)) {
4424            if let Some(body) = &t.lateral_subquery
4425                && select_is_correlated(body)
4426            {
4427                correlated = true;
4428                break;
4429            }
4430        }
4431    }
4432    correlated
4433}
4434
4435/// v7.29 (3c) — pre-order collection of SCALAR subquery nodes in a
4436/// host expression (no descent into subquery bodies). The splice
4437/// walk below uses the same order; the pair must stay in lockstep.
4438pub(crate) fn collect_scalar_subqueries<'a>(e: &'a Expr, out: &mut Vec<&'a SelectStatement>) {
4439    match e {
4440        Expr::ScalarSubquery(s) => out.push(s),
4441        Expr::Exists { .. }
4442        | Expr::InSubquery { .. }
4443        | Expr::RowInSubquery { .. }
4444        | Expr::RowCmpSubquery { .. } => {}
4445        Expr::Binary { lhs, rhs, .. } => {
4446            collect_scalar_subqueries(lhs, out);
4447            collect_scalar_subqueries(rhs, out);
4448        }
4449        Expr::Unary { expr, .. }
4450        | Expr::Cast { expr, .. }
4451        | Expr::IsNull { expr, .. }
4452        | Expr::BoolTest { expr, .. }
4453        | Expr::FieldAccess { base: expr, .. } => {
4454            collect_scalar_subqueries(expr, out);
4455        }
4456        Expr::Like { expr, pattern, .. } => {
4457            collect_scalar_subqueries(expr, out);
4458            collect_scalar_subqueries(pattern, out);
4459        }
4460        Expr::FunctionCall { args, .. } => {
4461            for a in args {
4462                collect_scalar_subqueries(a, out);
4463            }
4464        }
4465        Expr::AggregateOrdered { call, order_by, .. } => {
4466            collect_scalar_subqueries(call, out);
4467            for o in order_by {
4468                collect_scalar_subqueries(&o.expr, out);
4469            }
4470        }
4471        Expr::Case {
4472            operand,
4473            branches,
4474            else_branch,
4475        } => {
4476            if let Some(op) = operand {
4477                collect_scalar_subqueries(op, out);
4478            }
4479            for (w, t) in branches {
4480                collect_scalar_subqueries(w, out);
4481                collect_scalar_subqueries(t, out);
4482            }
4483            if let Some(eb) = else_branch {
4484                collect_scalar_subqueries(eb, out);
4485            }
4486        }
4487        Expr::ArraySubscript { target, index } => {
4488            collect_scalar_subqueries(target, out);
4489            collect_scalar_subqueries(index, out);
4490        }
4491        Expr::InList { expr, list, .. } => {
4492            collect_scalar_subqueries(expr, out);
4493            for item in list {
4494                collect_scalar_subqueries(item, out);
4495            }
4496        }
4497        _ => {}
4498    }
4499}
4500
4501/// v7.29 (3d) — empty every scalar-subquery BODY in a host
4502/// expression (node kept so the splice pre-order still matches).
4503fn hollow_scalar_subqueries(e: &mut Expr) {
4504    match e {
4505        Expr::ScalarSubquery(s) => {
4506            let hollow = SelectStatement {
4507                items: Vec::new(),
4508                ..SelectStatement::default()
4509            };
4510            **s = hollow;
4511        }
4512        Expr::Exists { .. }
4513        | Expr::InSubquery { .. }
4514        | Expr::RowInSubquery { .. }
4515        | Expr::RowCmpSubquery { .. } => {}
4516        Expr::Binary { lhs, rhs, .. } => {
4517            hollow_scalar_subqueries(lhs);
4518            hollow_scalar_subqueries(rhs);
4519        }
4520        Expr::Unary { expr, .. }
4521        | Expr::Cast { expr, .. }
4522        | Expr::IsNull { expr, .. }
4523        | Expr::BoolTest { expr, .. }
4524        | Expr::FieldAccess { base: expr, .. } => {
4525            hollow_scalar_subqueries(expr);
4526        }
4527        Expr::Like { expr, pattern, .. } => {
4528            hollow_scalar_subqueries(expr);
4529            hollow_scalar_subqueries(pattern);
4530        }
4531        Expr::FunctionCall { args, .. } => {
4532            for a in args.iter_mut() {
4533                hollow_scalar_subqueries(a);
4534            }
4535        }
4536        Expr::AggregateOrdered { call, order_by, .. } => {
4537            hollow_scalar_subqueries(call);
4538            for o in order_by.iter_mut() {
4539                hollow_scalar_subqueries(&mut o.expr);
4540            }
4541        }
4542        Expr::Case {
4543            operand,
4544            branches,
4545            else_branch,
4546        } => {
4547            if let Some(op) = operand {
4548                hollow_scalar_subqueries(op);
4549            }
4550            for (w, t) in branches.iter_mut() {
4551                hollow_scalar_subqueries(w);
4552                hollow_scalar_subqueries(t);
4553            }
4554            if let Some(eb) = else_branch {
4555                hollow_scalar_subqueries(eb);
4556            }
4557        }
4558        Expr::ArraySubscript { target, index } => {
4559            hollow_scalar_subqueries(target);
4560            hollow_scalar_subqueries(index);
4561        }
4562        Expr::InList { expr, list, .. } => {
4563            hollow_scalar_subqueries(expr);
4564            for item in list.iter_mut() {
4565                hollow_scalar_subqueries(item);
4566            }
4567        }
4568        _ => {}
4569    }
4570}
4571
4572/// v7.29 (3c) — splice the i-th scalar subquery's batched value into
4573/// the cloned tree (same pre-order as collect_scalar_subqueries).
4574/// Returns Ok(false) if a literal conversion fails (caller falls
4575/// back to the resolver path).
4576fn splice_planned_subqueries(
4577    e: &mut Expr,
4578    plan: &[Option<alloc::rc::Rc<memoize::GroupMap>>],
4579    idx: &mut usize,
4580    row: &Row<'static>,
4581    ctx: &EvalContext<'_>,
4582) -> Result<bool, EngineError> {
4583    match e {
4584        Expr::ScalarSubquery(_) => {
4585            let Some(Some(gm)) = plan.get(*idx) else {
4586                return Ok(false);
4587            };
4588            *idx += 1;
4589            // v7.37.x (docker-fair SCALARSQ attack) — empty_default is
4590            // carried on the GroupMap (PG empty-set semantics: COUNT = 0,
4591            // others = NULL). The inner here may be HOLLOWED by the
4592            // template-rewrite step, so re-introspecting it for the
4593            // aggregate kind doesn't work — the construction-time
4594            // value on the GroupMap is the source of truth.
4595            let (outer_col, map, empty_default) = gm.as_ref();
4596            let key_v = eval::eval_expr(&Expr::Column(outer_col.clone()), row, ctx)
4597                .map_err(EngineError::Eval)?;
4598            // v7.39 (round 620) — a NULL correlation key gives an EMPTY result
4599            // set, not a NULL result.
4600            //
4601            // `b.g = NULL` matches nothing, so the subquery runs over no rows —
4602            // which is the same situation as a non-NULL key that is absent from
4603            // the map, and the aggregate's own empty-set value decides it:
4604            // `count` answers 0, everything else answers NULL. This branch
4605            // answered NULL for every aggregate, so
4606            // `(SELECT count(*) FROM b WHERE b.g = a.g)` came back NULL on the
4607            // rows whose `a.g` is NULL where PG answers 0 — silently, and only
4608            // for the count family, which is why it survived: `sum` / `min` /
4609            // `array_agg` / `string_agg` / `bool_and` all have NULL as their
4610            // empty-set value and were right by accident.
4611            let v = map
4612                .get(&aggregate::encode_key(core::slice::from_ref(&key_v)))
4613                .cloned()
4614                .unwrap_or_else(|| empty_default.clone());
4615            *e = value_to_literal_expr(v)?;
4616            Ok(true)
4617        }
4618        Expr::Exists { .. }
4619        | Expr::InSubquery { .. }
4620        | Expr::RowInSubquery { .. }
4621        | Expr::RowCmpSubquery { .. } => Ok(true),
4622        Expr::Binary { lhs, rhs, .. } => Ok(splice_planned_subqueries(lhs, plan, idx, row, ctx)?
4623            && splice_planned_subqueries(rhs, plan, idx, row, ctx)?),
4624        Expr::Unary { expr, .. }
4625        | Expr::Cast { expr, .. }
4626        | Expr::IsNull { expr, .. }
4627        | Expr::BoolTest { expr, .. }
4628        | Expr::FieldAccess { base: expr, .. } => {
4629            splice_planned_subqueries(expr, plan, idx, row, ctx)
4630        }
4631        Expr::Like { expr, pattern, .. } => {
4632            Ok(splice_planned_subqueries(expr, plan, idx, row, ctx)?
4633                && splice_planned_subqueries(pattern, plan, idx, row, ctx)?)
4634        }
4635        Expr::FunctionCall { args, .. } => {
4636            for a in args.iter_mut() {
4637                if !splice_planned_subqueries(a, plan, idx, row, ctx)? {
4638                    return Ok(false);
4639                }
4640            }
4641            Ok(true)
4642        }
4643        Expr::AggregateOrdered { call, order_by, .. } => {
4644            if !splice_planned_subqueries(call, plan, idx, row, ctx)? {
4645                return Ok(false);
4646            }
4647            for o in order_by.iter_mut() {
4648                if !splice_planned_subqueries(&mut o.expr, plan, idx, row, ctx)? {
4649                    return Ok(false);
4650                }
4651            }
4652            Ok(true)
4653        }
4654        Expr::Case {
4655            operand,
4656            branches,
4657            else_branch,
4658        } => {
4659            if let Some(op) = operand {
4660                if !splice_planned_subqueries(op, plan, idx, row, ctx)? {
4661                    return Ok(false);
4662                }
4663            }
4664            for (w, t) in branches.iter_mut() {
4665                if !splice_planned_subqueries(w, plan, idx, row, ctx)?
4666                    || !splice_planned_subqueries(t, plan, idx, row, ctx)?
4667                {
4668                    return Ok(false);
4669                }
4670            }
4671            if let Some(eb) = else_branch {
4672                if !splice_planned_subqueries(eb, plan, idx, row, ctx)? {
4673                    return Ok(false);
4674                }
4675            }
4676            Ok(true)
4677        }
4678        Expr::ArraySubscript { target, index } => {
4679            Ok(splice_planned_subqueries(target, plan, idx, row, ctx)?
4680                && splice_planned_subqueries(index, plan, idx, row, ctx)?)
4681        }
4682        Expr::InList { expr, list, .. } => {
4683            if !splice_planned_subqueries(expr, plan, idx, row, ctx)? {
4684                return Ok(false);
4685            }
4686            for item in list.iter_mut() {
4687                if !splice_planned_subqueries(item, plan, idx, row, ctx)? {
4688                    return Ok(false);
4689                }
4690            }
4691            Ok(true)
4692        }
4693        _ => Ok(true),
4694    }
4695}
4696
4697/// v7.34.2 (EXISTS-FILTER baseline) — pre-order collect for EXISTS
4698/// subqueries. Mirrors `collect_scalar_subqueries` so the per-row
4699/// splice walker can re-traverse in the same order and pick the
4700/// matching planned set by ordinal index — no string repr, no
4701/// BTreeMap probe per row. ScalarSubquery / InSubquery nodes are
4702/// skipped here (they ride their own planners).
4703pub(crate) fn collect_exists_subqueries<'a>(e: &'a Expr, out: &mut Vec<&'a SelectStatement>) {
4704    match e {
4705        Expr::Exists { subquery, .. } => out.push(subquery.as_ref()),
4706        Expr::ScalarSubquery(_)
4707        | Expr::InSubquery { .. }
4708        | Expr::RowInSubquery { .. }
4709        | Expr::RowCmpSubquery { .. } => {}
4710        Expr::Binary { lhs, rhs, .. } => {
4711            collect_exists_subqueries(lhs, out);
4712            collect_exists_subqueries(rhs, out);
4713        }
4714        Expr::Unary { expr, .. }
4715        | Expr::Cast { expr, .. }
4716        | Expr::IsNull { expr, .. }
4717        | Expr::BoolTest { expr, .. }
4718        | Expr::FieldAccess { base: expr, .. } => {
4719            collect_exists_subqueries(expr, out);
4720        }
4721        Expr::Like { expr, pattern, .. } => {
4722            collect_exists_subqueries(expr, out);
4723            collect_exists_subqueries(pattern, out);
4724        }
4725        Expr::FunctionCall { args, .. } => {
4726            for a in args {
4727                collect_exists_subqueries(a, out);
4728            }
4729        }
4730        Expr::AggregateOrdered { call, order_by, .. } => {
4731            collect_exists_subqueries(call, out);
4732            for o in order_by {
4733                collect_exists_subqueries(&o.expr, out);
4734            }
4735        }
4736        Expr::Case {
4737            operand,
4738            branches,
4739            else_branch,
4740        } => {
4741            if let Some(op) = operand {
4742                collect_exists_subqueries(op, out);
4743            }
4744            for (w, t) in branches {
4745                collect_exists_subqueries(w, out);
4746                collect_exists_subqueries(t, out);
4747            }
4748            if let Some(eb) = else_branch {
4749                collect_exists_subqueries(eb, out);
4750            }
4751        }
4752        Expr::ArraySubscript { target, index } => {
4753            collect_exists_subqueries(target, out);
4754            collect_exists_subqueries(index, out);
4755        }
4756        Expr::InList { expr, list, .. } => {
4757            collect_exists_subqueries(expr, out);
4758            for item in list {
4759                collect_exists_subqueries(item, out);
4760            }
4761        }
4762        _ => {}
4763    }
4764}
4765/// v7.39 (round 616) — `EXISTS (…)` or `NOT EXISTS (…)` and nothing else.
4766/// Returns the node's own `negated` flag and whether a `NOT` wraps it.
4767fn bare_exists_shape(e: &Expr) -> Option<(bool, bool)> {
4768    match e {
4769        Expr::Exists { negated, .. } => Some((*negated, false)),
4770        Expr::Unary {
4771            op: spg_sql::ast::UnOp::Not,
4772            expr: inner,
4773        } => match inner.as_ref() {
4774            Expr::Exists { negated, .. } => Some((*negated, true)),
4775            _ => None,
4776        },
4777        _ => None,
4778    }
4779}
4780
4781/// v7.39 (round 616) — the verdict a planned EXISTS gives for one outer row.
4782///
4783/// Split out of the splice so the shape that IS a single EXISTS can be
4784/// answered without cloning anything: see the caller.
4785fn planned_exists_bit(
4786    es: &memoize::ExistsSet,
4787    negated: bool,
4788    row: &Row<'static>,
4789    ctx: &EvalContext<'_>,
4790) -> Result<bool, EngineError> {
4791    let (outer_cols, set) = es;
4792    let mut key_vals: Vec<Value<'static>> = Vec::with_capacity(outer_cols.len());
4793    let mut any_null = false;
4794    for oc in outer_cols {
4795        // v7.39 (round 596) — the outer side is an expression now, so this
4796        // evaluates it directly instead of rebuilding a column node per
4797        // outer row (which allocated, per row per key).
4798        let v = eval::eval_expr(oc, row, ctx).map_err(EngineError::Eval)?;
4799        if matches!(v, Value::Null) {
4800            any_null = true;
4801        }
4802        key_vals.push(v);
4803    }
4804    let present = !any_null && set.contains(&aggregate::encode_canonical_key(&key_vals));
4805    Ok(if negated { !present } else { present })
4806}
4807
4808/// v7.34.2 — per-row splice for the planned EXISTS sets. Walks the
4809/// (cloned) host expression in the SAME pre-order as
4810/// `collect_exists_subqueries`, increments `idx` past each EXISTS
4811/// node, and replaces it in place with `Bool(true/false)` derived
4812/// from the planned key-set + outer-row column values. Returns
4813/// `Ok(false)` when any encountered EXISTS lacks a planned set; the
4814/// caller falls back to the legacy per-row resolver path.
4815fn splice_planned_exists(
4816    e: &mut Expr,
4817    plan: &[Option<alloc::rc::Rc<memoize::ExistsSet>>],
4818    idx: &mut usize,
4819    row: &Row<'static>,
4820    ctx: &EvalContext<'_>,
4821) -> Result<bool, EngineError> {
4822    match e {
4823        Expr::Exists { negated, .. } => {
4824            let Some(Some(es)) = plan.get(*idx) else {
4825                return Ok(false);
4826            };
4827            *idx += 1;
4828            let bit = planned_exists_bit(es, *negated, row, ctx)?;
4829            *e = Expr::Literal(Literal::Bool(bit));
4830            Ok(true)
4831        }
4832        Expr::ScalarSubquery(_)
4833        | Expr::InSubquery { .. }
4834        | Expr::RowInSubquery { .. }
4835        | Expr::RowCmpSubquery { .. } => Ok(true),
4836        Expr::Binary { lhs, rhs, .. } => Ok(splice_planned_exists(lhs, plan, idx, row, ctx)?
4837            && splice_planned_exists(rhs, plan, idx, row, ctx)?),
4838        Expr::Unary { expr, .. }
4839        | Expr::Cast { expr, .. }
4840        | Expr::IsNull { expr, .. }
4841        | Expr::BoolTest { expr, .. }
4842        | Expr::FieldAccess { base: expr, .. } => splice_planned_exists(expr, plan, idx, row, ctx),
4843        Expr::Like { expr, pattern, .. } => Ok(splice_planned_exists(expr, plan, idx, row, ctx)?
4844            && splice_planned_exists(pattern, plan, idx, row, ctx)?),
4845        Expr::FunctionCall { args, .. } => {
4846            for a in args.iter_mut() {
4847                if !splice_planned_exists(a, plan, idx, row, ctx)? {
4848                    return Ok(false);
4849                }
4850            }
4851            Ok(true)
4852        }
4853        Expr::AggregateOrdered { call, order_by, .. } => {
4854            if !splice_planned_exists(call, plan, idx, row, ctx)? {
4855                return Ok(false);
4856            }
4857            for o in order_by.iter_mut() {
4858                if !splice_planned_exists(&mut o.expr, plan, idx, row, ctx)? {
4859                    return Ok(false);
4860                }
4861            }
4862            Ok(true)
4863        }
4864        Expr::Case {
4865            operand,
4866            branches,
4867            else_branch,
4868        } => {
4869            if let Some(op) = operand {
4870                if !splice_planned_exists(op, plan, idx, row, ctx)? {
4871                    return Ok(false);
4872                }
4873            }
4874            for (w, t) in branches.iter_mut() {
4875                if !splice_planned_exists(w, plan, idx, row, ctx)?
4876                    || !splice_planned_exists(t, plan, idx, row, ctx)?
4877                {
4878                    return Ok(false);
4879                }
4880            }
4881            if let Some(eb) = else_branch {
4882                if !splice_planned_exists(eb, plan, idx, row, ctx)? {
4883                    return Ok(false);
4884                }
4885            }
4886            Ok(true)
4887        }
4888        Expr::ArraySubscript { target, index } => {
4889            Ok(splice_planned_exists(target, plan, idx, row, ctx)?
4890                && splice_planned_exists(index, plan, idx, row, ctx)?)
4891        }
4892        Expr::InList { expr, list, .. } => {
4893            if !splice_planned_exists(expr, plan, idx, row, ctx)? {
4894                return Ok(false);
4895            }
4896            for item in list.iter_mut() {
4897                if !splice_planned_exists(item, plan, idx, row, ctx)? {
4898                    return Ok(false);
4899                }
4900            }
4901            Ok(true)
4902        }
4903        _ => Ok(true),
4904    }
4905}
4906
4907/// v7.30.2 (mailrs round-25) — minimum element count before an
4908/// all-literal `IN` list gets a per-query membership set. Below
4909/// this the linear scan wins on build cost.
4910const INLIST_SET_THRESHOLD: usize = 64;
4911
4912/// Cheap pre-check: is a set-eligible `IN` list reachable on the
4913/// AND spine of this expression? Anything else keeps the plain
4914/// `eval_expr` path untouched.
4915fn expr_may_use_in_set(e: &Expr) -> bool {
4916    match e {
4917        Expr::InList { list, .. } => list.len() >= INLIST_SET_THRESHOLD,
4918        Expr::Binary {
4919            lhs,
4920            op: BinOp::And,
4921            rhs,
4922        } => expr_may_use_in_set(lhs) || expr_may_use_in_set(rhs),
4923        _ => false,
4924    }
4925}
4926
4927/// v7.39 (round 275) — is this cast target one of the integer widths
4928/// whose values all live in the same `InListSet::Int`?
4929fn cast_target_is_integer(target: &spg_sql::ast::CastTarget) -> bool {
4930    use spg_sql::ast::CastTarget;
4931    match target {
4932        CastTarget::BigInt | CastTarget::Int => true,
4933        CastTarget::Named(n) => {
4934            matches!(
4935                n.to_ascii_lowercase().as_str(),
4936                "int2" | "int4" | "int8" | "smallint" | "integer" | "int" | "bigint"
4937            )
4938        }
4939        _ => false,
4940    }
4941}
4942
4943/// Analyse an `IN` list for set eligibility: every element a literal,
4944/// all of one family (integer or string, NULLs tracked separately).
4945pub(crate) fn build_in_list_set(list: &[Expr]) -> Option<memoize::InListSetEntry> {
4946    let mut has_null = false;
4947    let mut ints: hashbrown::HashSet<i64> = hashbrown::HashSet::with_capacity(list.len());
4948    let mut texts: hashbrown::HashSet<String> = hashbrown::HashSet::with_capacity(list.len());
4949    for item in list {
4950        // v7.39 (round 275) — see through the integer cast round 189
4951        // wraps a materialised BIGINT / SMALLINT subquery result in.
4952        // Before that round every element was a bare literal; after it
4953        // the elements of a pulled-up NOT EXISTS list are
4954        // `Expr::Cast { Literal::Integer, ::int8 }`, and requiring a
4955        // bare literal here silently dropped the whole set — the
4956        // membership probe fell back to an O(N x M) linear scan and the
4957        // mailrs content_worker shape went from 9 ms to 321 s.
4958        //
4959        // The set is keyed by VALUE, not by width: the probe side
4960        // already matches SmallInt / Int / BigInt against
4961        // `InListSet::Int`, so the cast carries nothing the set needs.
4962        let lit = match item {
4963            Expr::Literal(lit) => lit,
4964            Expr::Cast { expr, target } if cast_target_is_integer(target) => match expr.as_ref() {
4965                Expr::Literal(inner) => inner,
4966                _ => return None,
4967            },
4968            _ => return None,
4969        };
4970        match lit {
4971            Literal::Null => has_null = true,
4972            Literal::Integer(i) => {
4973                ints.insert(*i);
4974            }
4975            Literal::String(s) => {
4976                texts.insert(s.clone());
4977            }
4978            _ => return None,
4979        }
4980        if !ints.is_empty() && !texts.is_empty() {
4981            return None;
4982        }
4983    }
4984    let set = if !ints.is_empty() {
4985        memoize::InListSet::Int(ints)
4986    } else if !texts.is_empty() {
4987        memoize::InListSet::Text(texts)
4988    } else {
4989        return None;
4990    };
4991    Some(memoize::InListSetEntry { set, has_null })
4992}
4993
4994/// Subquery-free eval that serves large all-literal `IN` lists from
4995/// a per-query membership set (cached in the memo by node address).
4996/// Walks only the AND spine; every other node — and every needle
4997/// whose runtime family doesn't match the set — falls through to
4998/// `eval_expr`, so coercion and error semantics stay identical.
4999fn eval_with_in_sets(
5000    e: &Expr,
5001    row: &Row<'static>,
5002    ctx: &EvalContext<'_>,
5003    m: &mut memoize::MemoizeCache,
5004) -> Result<Value<'static>, EngineError> {
5005    match e {
5006        Expr::Binary {
5007            lhs,
5008            op: BinOp::And,
5009            rhs,
5010        } => {
5011            // Mirror eval_expr: both sides evaluate (no short
5012            // circuit), then SQL three-valued AND.
5013            let l = eval_with_in_sets(lhs, row, ctx, m)?;
5014            let r = eval_with_in_sets(rhs, row, ctx, m)?;
5015            eval::and_3vl(l, r).map_err(EngineError::Eval)
5016        }
5017        Expr::InList {
5018            expr: lhs,
5019            list,
5020            negated,
5021        } if list.len() >= INLIST_SET_THRESHOLD => {
5022            let key = core::ptr::from_ref::<Expr>(e) as usize;
5023            let Some(entry) = m
5024                .in_sets
5025                .entry(key)
5026                .or_insert_with(|| build_in_list_set(list))
5027            else {
5028                return eval::eval_expr(e, row, ctx).map_err(EngineError::Eval);
5029            };
5030            let needle = eval::eval_expr(lhs, row, ctx).map_err(EngineError::Eval)?;
5031            let contained = match (&needle, &entry.set) {
5032                // Non-empty list + NULL needle → NULL (negation of
5033                // NULL is still NULL).
5034                (Value::Null, _) => return Ok(Value::Null),
5035                (Value::SmallInt(n), memoize::InListSet::Int(s)) => s.contains(&i64::from(*n)),
5036                (Value::Int(n), memoize::InListSet::Int(s)) => s.contains(&i64::from(*n)),
5037                (Value::BigInt(n), memoize::InListSet::Int(s)) => s.contains(n),
5038                (Value::Text(t), memoize::InListSet::Text(s)) => s.contains(t.as_ref()),
5039                // Cross-family needle (e.g. Float vs integer list):
5040                // keep apply_binary's coercion / error behaviour.
5041                _ => return eval::eval_expr(e, row, ctx).map_err(EngineError::Eval),
5042            };
5043            let inner = if contained {
5044                Value::Bool(true)
5045            } else if entry.has_null {
5046                Value::Null
5047            } else {
5048                Value::Bool(false)
5049            };
5050            Ok(match (negated, inner) {
5051                (true, Value::Bool(b)) => Value::Bool(!b),
5052                (_, v) => v,
5053            })
5054        }
5055        _ => eval::eval_expr(e, row, ctx).map_err(EngineError::Eval),
5056    }
5057}
5058
5059fn substitute_outer_columns(
5060    stmt: &mut SelectStatement,
5061    row: &Row<'static>,
5062    ctx: &EvalContext<'_>,
5063    cat: &spg_storage::Catalog,
5064) {
5065    // v7.24 (round-16 B) — joined outer contexts carry no single
5066    // table alias; their schemas use composite "alias.column" names
5067    // instead. Pass an unmatchable alias and let the composite
5068    // lookup in substitute_in_expr do the work (a correlated EXISTS
5069    // under a JOIN previously skipped substitution entirely and
5070    // died with "unknown table qualifier").
5071    let outer_alias = ctx.table_alias.unwrap_or("");
5072    substitute_in_select(stmt, row, ctx, outer_alias, cat);
5073}
5074
5075fn substitute_in_select(
5076    stmt: &mut SelectStatement,
5077    row: &Row<'static>,
5078    ctx: &EvalContext<'_>,
5079    outer_alias: &str,
5080    cat: &spg_storage::Catalog,
5081) {
5082    // v7.39 (round 545) — what this statement's own scope supplies. A
5083    // bare name it does NOT supply is an outer reference and gets
5084    // spliced; one it does belongs to the inner relation, as in PG.
5085    let visible = inner_scope_column_names(stmt, cat);
5086    for item in &mut stmt.items {
5087        if let SelectItem::Expr { expr, .. } = item {
5088            substitute_in_expr(expr, row, ctx, outer_alias, cat, visible.as_ref());
5089        }
5090    }
5091    if let Some(w) = &mut stmt.where_ {
5092        substitute_in_expr(w, row, ctx, outer_alias, cat, visible.as_ref());
5093    }
5094    if let Some(gs) = &mut stmt.group_by {
5095        for g in gs {
5096            substitute_in_expr(g, row, ctx, outer_alias, cat, visible.as_ref());
5097        }
5098    }
5099    if let Some(h) = &mut stmt.having {
5100        substitute_in_expr(h, row, ctx, outer_alias, cat, visible.as_ref());
5101    }
5102    for o in &mut stmt.order_by {
5103        substitute_in_expr(&mut o.expr, row, ctx, outer_alias, cat, visible.as_ref());
5104    }
5105    for (_, peer) in &mut stmt.unions {
5106        substitute_in_select(peer, row, ctx, outer_alias, cat);
5107    }
5108    // v7.39 (round 532) — and the FROM clause. A correlated subquery is
5109    // run by splicing the outer row's values into it, and that walk
5110    // covered every clause EXCEPT this one — so an outer reference
5111    // inside a JOIN's ON, or inside a LATERAL body, survived
5112    // unsubstituted and died resolving:
5113    //
5114    //   SELECT (SELECT l.k FROM b, LATERAL (SELECT b.d + a.id AS k) l
5115    //           WHERE b.id = a.id) FROM a
5116    //   PG18  101, NULL      SPG  missing FROM-clause entry for "a"
5117    //
5118    // The same reference one clause over — in the subquery's own WHERE
5119    // — always worked, which is what made this look like a LATERAL
5120    // problem rather than a missing branch of the walk.
5121    //
5122    // A sibling name inside the FROM (`b.d` above) is not in the outer
5123    // schema, so it is left alone; only genuinely outer references are
5124    // spliced.
5125    if let Some(from) = &mut stmt.from {
5126        if let Some(body) = &mut from.primary.lateral_subquery {
5127            substitute_in_select(body, row, ctx, outer_alias, cat);
5128        }
5129        // 7.38.1 S5.1 — a FROM-position table function's ARGUMENTS can
5130        // reference the outer row too: pg_dump's per-attribute pass
5131        // runs `ARRAY(SELECT … FROM pg_options_to_table(attfdwoptions))`,
5132        // where `attfdwoptions` belongs to the OUTER pg_attribute row.
5133        // Same walk rule as the lateral bodies above.
5134        if let Some(call) = &mut from.primary.table_fn_call {
5135            for a in &mut call.1 {
5136                substitute_in_expr(a, row, ctx, outer_alias, cat, visible.as_ref());
5137            }
5138        }
5139        for j in &mut from.joins {
5140            if let Some(on) = &mut j.on {
5141                substitute_in_expr(on, row, ctx, outer_alias, cat, visible.as_ref());
5142            }
5143            if let Some(body) = &mut j.table.lateral_subquery {
5144                substitute_in_select(body, row, ctx, outer_alias, cat);
5145            }
5146            if let Some(call) = &mut j.table.table_fn_call {
5147                for a in &mut call.1 {
5148                    substitute_in_expr(a, row, ctx, outer_alias, cat, visible.as_ref());
5149                }
5150            }
5151        }
5152    }
5153}
5154
5155fn substitute_in_expr(
5156    e: &mut Expr,
5157    row: &Row<'static>,
5158    ctx: &EvalContext<'_>,
5159    outer_alias: &str,
5160    cat: &spg_storage::Catalog,
5161    visible: Option<&alloc::collections::BTreeSet<alloc::string::String>>,
5162) {
5163    // v7.25.2 (round-19 A) — bare synthetic columns. The aggregate
5164    // rewriter replaces group-key references INSIDE subquery bodies
5165    // with `__grp_N` so a correlated subquery in a GROUP BY select
5166    // list can resolve against the synthesised group row. The names
5167    // are engine-generated, so they can't shadow user columns.
5168    if let Expr::Column(c) = e
5169        && c.qualifier.is_none()
5170        && (c.name.starts_with("__grp_") || c.name.starts_with("__agg_"))
5171        && let Some(idx) = ctx.columns.iter().position(|sc| sc.name == c.name)
5172    {
5173        let v = row.values.get(idx).cloned().unwrap_or(Value::Null);
5174        if let Ok(lit) = value_to_literal_expr(v) {
5175            *e = lit;
5176            return;
5177        }
5178    }
5179    // v7.39 (round 545) — a bare name this statement's own scope does
5180    // not supply is an outer reference. SQL resolves innermost-first
5181    // and walks outward; SPG only ever looked inward, so the ordinary
5182    // spelling of a correlated subquery — `WHERE bid = aid` — died with
5183    // "column does not exist" while `WHERE bid = oa.aid` worked.
5184    if let Expr::Column(c) = e
5185        && c.qualifier.is_none()
5186        && c.name != "*"
5187        && !is_synthetic_column_name(&c.name.to_ascii_lowercase())
5188        && visible.is_some_and(|v| !v.contains(&c.name.to_ascii_lowercase()))
5189    {
5190        // 7.38.1 S5.1 — a JOINED outer publishes composite
5191        // "alias.column" names, so a bare outer reference must also
5192        // try the suffix — but only an UNAMBIGUOUS one (two joined
5193        // relations sharing the name would be PG's "ambiguous
5194        // column"). pg_dump's per-attribute pass hits this: bare
5195        // `attfdwoptions` under `FROM unnest(...) JOIN pg_attribute a`.
5196        let idx = ctx
5197            .columns
5198            .iter()
5199            .position(|sc| sc.name.eq_ignore_ascii_case(&c.name))
5200            .or_else(|| {
5201                let suffix = alloc::format!(".{}", c.name.to_ascii_lowercase());
5202                let mut hits = ctx
5203                    .columns
5204                    .iter()
5205                    .enumerate()
5206                    .filter(|(_, sc)| sc.name.to_ascii_lowercase().ends_with(&suffix));
5207                match (hits.next(), hits.next()) {
5208                    (Some((i, _)), None) => Some(i),
5209                    _ => None,
5210                }
5211            });
5212        if let Some(idx) = idx {
5213            let v = row.values.get(idx).cloned().unwrap_or(Value::Null);
5214            if let Ok(lit) = value_to_literal_expr(v) {
5215                *e = lit;
5216                return;
5217            }
5218        }
5219    }
5220    if let Expr::Column(c) = e
5221        && let Some(qual) = &c.qualifier
5222    {
5223        // Look up the column's index in the outer schema: plain name
5224        // when the qualifier is the outer table's alias, composite
5225        // "alias.column" for joined outer schemas (v7.24).
5226        let idx = if !outer_alias.is_empty() && relation_name_matches(qual, outer_alias) {
5227            ctx.columns
5228                .iter()
5229                .position(|sc| sc.name.eq_ignore_ascii_case(&c.name))
5230        } else {
5231            None
5232        }
5233        .or_else(|| {
5234            let composite = alloc::format!("{qual}.{name}", name = c.name);
5235            ctx.columns
5236                .iter()
5237                .position(|sc| sc.name.eq_ignore_ascii_case(&composite))
5238        });
5239        if let Some(idx) = idx {
5240            let v = row.values.get(idx).cloned().unwrap_or(Value::Null);
5241            if let Ok(lit) = value_to_literal_expr(v) {
5242                *e = lit;
5243                return;
5244            }
5245        }
5246    }
5247    match e {
5248        Expr::Collate { expr, .. } | Expr::NamedArg { expr, .. } => {
5249            substitute_in_expr(expr, row, ctx, outer_alias, cat, visible)
5250        }
5251        Expr::Variadic(expr) => substitute_in_expr(expr, row, ctx, outer_alias, cat, visible),
5252        Expr::AggregateOrdered { call, order_by, .. } => {
5253            substitute_in_expr(call, row, ctx, outer_alias, cat, visible);
5254            for o in order_by.iter_mut() {
5255                substitute_in_expr(&mut o.expr, row, ctx, outer_alias, cat, visible);
5256            }
5257        }
5258        Expr::Binary { lhs, rhs, .. } => {
5259            substitute_in_expr(lhs, row, ctx, outer_alias, cat, visible);
5260            substitute_in_expr(rhs, row, ctx, outer_alias, cat, visible);
5261        }
5262        Expr::Unary { expr, .. }
5263        | Expr::Cast { expr, .. }
5264        | Expr::IsNull { expr, .. }
5265        | Expr::BoolTest { expr, .. }
5266        | Expr::FieldAccess { base: expr, .. } => {
5267            substitute_in_expr(expr, row, ctx, outer_alias, cat, visible);
5268        }
5269        Expr::Like { expr, pattern, .. } => {
5270            substitute_in_expr(expr, row, ctx, outer_alias, cat, visible);
5271            substitute_in_expr(pattern, row, ctx, outer_alias, cat, visible);
5272        }
5273        Expr::FunctionCall { args, .. } => {
5274            for a in args {
5275                substitute_in_expr(a, row, ctx, outer_alias, cat, visible);
5276            }
5277        }
5278        Expr::Extract { source, .. } => {
5279            substitute_in_expr(source, row, ctx, outer_alias, cat, visible)
5280        }
5281        Expr::WindowFunction {
5282            args,
5283            partition_by,
5284            order_by,
5285            ..
5286        } => {
5287            for a in args {
5288                substitute_in_expr(a, row, ctx, outer_alias, cat, visible);
5289            }
5290            for p in partition_by {
5291                substitute_in_expr(p, row, ctx, outer_alias, cat, visible);
5292            }
5293            for (o, _, _) in order_by {
5294                substitute_in_expr(o, row, ctx, outer_alias, cat, visible);
5295            }
5296        }
5297        Expr::ScalarSubquery(s) => substitute_in_select(s, row, ctx, outer_alias, cat),
5298        Expr::Exists { subquery, .. } | Expr::InSubquery { subquery, .. } => {
5299            substitute_in_select(subquery, row, ctx, outer_alias, cat);
5300        }
5301        Expr::RowInSubquery {
5302            row: row_exprs,
5303            subquery,
5304            ..
5305        } => {
5306            for el in row_exprs.iter_mut() {
5307                substitute_in_expr(el, row, ctx, outer_alias, cat, visible);
5308            }
5309            substitute_in_select(subquery, row, ctx, outer_alias, cat);
5310        }
5311        Expr::RowCmpSubquery {
5312            row: row_exprs,
5313            subquery,
5314            ..
5315        } => {
5316            for el in row_exprs.iter_mut() {
5317                substitute_in_expr(el, row, ctx, outer_alias, cat, visible);
5318            }
5319            substitute_in_select(subquery, row, ctx, outer_alias, cat);
5320        }
5321        Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => {}
5322        Expr::Array(items) => {
5323            for elem in items {
5324                substitute_in_expr(elem, row, ctx, outer_alias, cat, visible);
5325            }
5326        }
5327        Expr::ArraySubscript { target, index } => {
5328            substitute_in_expr(target, row, ctx, outer_alias, cat, visible);
5329            substitute_in_expr(index, row, ctx, outer_alias, cat, visible);
5330        }
5331        Expr::ArraySlice { target, lo, hi } => {
5332            substitute_in_expr(target, row, ctx, outer_alias, cat, visible);
5333            if let Some(l) = lo {
5334                substitute_in_expr(l, row, ctx, outer_alias, cat, visible);
5335            }
5336            if let Some(h) = hi {
5337                substitute_in_expr(h, row, ctx, outer_alias, cat, visible);
5338            }
5339        }
5340        Expr::AnyAll { expr, array, .. } => {
5341            substitute_in_expr(expr, row, ctx, outer_alias, cat, visible);
5342            substitute_in_expr(array, row, ctx, outer_alias, cat, visible);
5343        }
5344        Expr::InList { expr, list, .. } => {
5345            substitute_in_expr(expr, row, ctx, outer_alias, cat, visible);
5346            for item in list {
5347                substitute_in_expr(item, row, ctx, outer_alias, cat, visible);
5348            }
5349        }
5350        Expr::Case {
5351            operand,
5352            branches,
5353            else_branch,
5354        } => {
5355            if let Some(o) = operand {
5356                substitute_in_expr(o, row, ctx, outer_alias, cat, visible);
5357            }
5358            for (w, t) in branches {
5359                substitute_in_expr(w, row, ctx, outer_alias, cat, visible);
5360                substitute_in_expr(t, row, ctx, outer_alias, cat, visible);
5361            }
5362            if let Some(e) = else_branch {
5363                substitute_in_expr(e, row, ctx, outer_alias, cat, visible);
5364            }
5365        }
5366    }
5367}
5368
5369/// Quick scan for any subquery-bearing node in a SELECT's WHERE /
5370/// projection / `order_by` — saves cloning the AST when there are
5371/// none (the common case).
5372pub fn expr_tree_has_subquery(stmt: &SelectStatement) -> bool {
5373    let mut any = false;
5374    for item in &stmt.items {
5375        if let SelectItem::Expr { expr, .. } = item {
5376            any = any || expr_has_subquery(expr);
5377        }
5378    }
5379    if let Some(w) = &stmt.where_ {
5380        any = any || expr_has_subquery(w);
5381    }
5382    if let Some(h) = &stmt.having {
5383        any = any || expr_has_subquery(h);
5384    }
5385    for o in &stmt.order_by {
5386        any = any || expr_has_subquery(&o.expr);
5387    }
5388    for (_, peer) in &stmt.unions {
5389        any = any || expr_tree_has_subquery(peer);
5390    }
5391    any
5392}
5393
5394pub(crate) fn expr_has_subquery(e: &Expr) -> bool {
5395    match e {
5396        Expr::Collate { expr, .. } | Expr::NamedArg { expr, .. } => expr_has_subquery(expr),
5397        Expr::Variadic(expr) => expr_has_subquery(expr),
5398        Expr::ScalarSubquery(_)
5399        | Expr::Exists { .. }
5400        | Expr::InSubquery { .. }
5401        | Expr::RowInSubquery { .. }
5402        | Expr::RowCmpSubquery { .. } => true,
5403        Expr::AggregateOrdered { call, order_by, .. } => {
5404            expr_has_subquery(call) || order_by.iter().any(|o| expr_has_subquery(&o.expr))
5405        }
5406        Expr::Binary { lhs, rhs, .. } => expr_has_subquery(lhs) || expr_has_subquery(rhs),
5407        Expr::Unary { expr, .. }
5408        | Expr::Cast { expr, .. }
5409        | Expr::IsNull { expr, .. }
5410        | Expr::BoolTest { expr, .. }
5411        | Expr::FieldAccess { base: expr, .. } => expr_has_subquery(expr),
5412        Expr::FunctionCall { args, .. } => args.iter().any(expr_has_subquery),
5413        Expr::Like { expr, pattern, .. } => expr_has_subquery(expr) || expr_has_subquery(pattern),
5414        Expr::Extract { source, .. } => expr_has_subquery(source),
5415        Expr::WindowFunction {
5416            args,
5417            partition_by,
5418            order_by,
5419            ..
5420        } => {
5421            args.iter().any(expr_has_subquery)
5422                || partition_by.iter().any(expr_has_subquery)
5423                || order_by.iter().any(|(e, _, _)| expr_has_subquery(e))
5424        }
5425        Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => false,
5426        Expr::Array(items) => items.iter().any(expr_has_subquery),
5427        Expr::ArraySubscript { target, index } => {
5428            expr_has_subquery(target) || expr_has_subquery(index)
5429        }
5430        Expr::ArraySlice { target, lo, hi } => {
5431            expr_has_subquery(target)
5432                || lo.as_deref().is_some_and(expr_has_subquery)
5433                || hi.as_deref().is_some_and(expr_has_subquery)
5434        }
5435        Expr::AnyAll { expr, array, .. } => expr_has_subquery(expr) || expr_has_subquery(array),
5436        Expr::InList { expr, list, .. } => {
5437            expr_has_subquery(expr) || list.iter().any(expr_has_subquery)
5438        }
5439        Expr::Case {
5440            operand,
5441            branches,
5442            else_branch,
5443        } => {
5444            operand.as_deref().is_some_and(expr_has_subquery)
5445                || branches
5446                    .iter()
5447                    .any(|(w, t)| expr_has_subquery(w) || expr_has_subquery(t))
5448                || else_branch.as_deref().is_some_and(expr_has_subquery)
5449        }
5450    }
5451}