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