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
100impl Engine {
101    /// v4.23: per-row eval that handles correlated subqueries.
102    /// Equivalent to `eval::eval_expr` when the expression has no
103    /// subqueries; otherwise clones the expression, substitutes
104    /// outer-row columns into each surviving subquery node, runs
105    /// the inner SELECT, and replaces the node with the literal
106    /// result. Only the WHERE-filter call sites use this path so
107    /// the uncorrelated fast path is preserved everywhere else.
108    pub(crate) fn eval_expr_with_correlated(
109        &self,
110        expr: &Expr,
111        row: &Row<'static>,
112        ctx: &EvalContext<'_>,
113        cancel: CancelToken<'_>,
114        mut memo: Option<&mut memoize::MemoizeCache>,
115    ) -> Result<Value<'static>, EngineError> {
116        // v7.30.2 (mailrs round-25) — the has-subquery walk is
117        // O(tree) and a materialised `IN (…)` list makes the tree
118        // huge; cache the answer per expression address so the
119        // per-row dispatch stops re-walking 24k list elements.
120        let has_subq = if let Some(m) = memo.as_deref_mut() {
121            let key = core::ptr::from_ref::<Expr>(expr) as usize;
122            match m.has_subquery.get(&key) {
123                Some(b) => *b,
124                None => {
125                    let b = expr_has_subquery(expr);
126                    m.has_subquery.insert(key, b);
127                    b
128                }
129            }
130        } else {
131            expr_has_subquery(expr)
132        };
133        if !has_subq {
134            // A large materialised `IN (…)` list inside the WHERE
135            // makes the plain eval O(rows × list); route through the
136            // per-query membership set (built once, keyed by node
137            // address) when one is reachable on the AND spine.
138            if let Some(m) = memo.as_deref_mut()
139                && expr_may_use_in_set(expr)
140            {
141                return eval_with_in_sets(expr, row, ctx, m);
142            }
143            return eval::eval_expr(expr, row, ctx).map_err(EngineError::Eval);
144        }
145        // v7.29 (3c) - per-expression plan: the batch maps for this
146        // host expression's scalar subqueries are looked up by the
147        // expression's ADDRESS (stable across the row loop), so the
148        // hot path does zero AST formatting. Building the plan (and
149        // its Display-keyed group maps) happens once per expression.
150        if let Some(m) = memo.as_deref_mut() {
151            let key = core::ptr::from_ref::<Expr>(expr) as usize;
152            // Plan hit: skip the collection walk entirely (it ran
153            // once per group otherwise - 70k walks per inbox query).
154            // The memo is per-query and host expressions outlive it,
155            // so an address that hit once stays valid.
156            let plan_hit = m.expr_plans.contains_key(&key);
157            let exists_plan_hit = m.exists_plans.contains_key(&key);
158            let mut subs: Vec<&SelectStatement> = Vec::new();
159            let mut exists_subs: Vec<&SelectStatement> = Vec::new();
160            if !plan_hit {
161                collect_scalar_subqueries(expr, &mut subs);
162            }
163            if !exists_plan_hit {
164                collect_exists_subqueries(expr, &mut exists_subs);
165            }
166            if !plan_hit && !subs.is_empty() {
167                let mut plan: Vec<Option<alloc::rc::Rc<memoize::GroupMap>>> =
168                    Vec::with_capacity(subs.len());
169                for sub in &subs {
170                    let repr = alloc::format!("{sub}");
171                    if !m.group_maps.contains_key(&repr) {
172                        let built = self
173                            .try_batch_correlated_scalar(sub, None, cancel)?
174                            .map(alloc::rc::Rc::new);
175                        m.group_maps.insert(repr.clone(), built);
176                    }
177                    plan.push(m.group_maps.get(&repr).cloned().flatten());
178                }
179                let mut template = expr.clone();
180                hollow_scalar_subqueries(&mut template);
181                m.expr_plans.insert(key, (subs.len(), plan, template));
182            }
183            // v7.34.2 — parallel EXISTS plan. Walk host ONCE in pre-order,
184            // build a decorrelated key-set for each EXISTS subquery via
185            // `try_batch_correlated_exists`, and cache the vec by host_ptr.
186            // Per-row dispatch below uses `splice_planned_exists` which
187            // increments an ordinal cursor — no `alloc::format!` per row.
188            if !exists_plan_hit && !exists_subs.is_empty() {
189                let mut eplan: Vec<Option<alloc::rc::Rc<memoize::ExistsSet>>> =
190                    Vec::with_capacity(exists_subs.len());
191                for sub in &exists_subs {
192                    let built = self
193                        .try_batch_correlated_exists(sub, cancel)?
194                        .map(alloc::rc::Rc::new);
195                    if built.is_some() {
196                        EXISTS_BATCH_FIRE_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
197                    } else {
198                        EXISTS_BATCH_FALL_THROUGH_COUNT
199                            .fetch_add(1, core::sync::atomic::Ordering::Relaxed);
200                    }
201                    eplan.push(built);
202                }
203                m.exists_plans.insert(key, eplan);
204            }
205            // Fast-path gate: take it if we have a planned scalar set, a
206            // planned EXISTS set, or both — anything that lets us skip
207            // the per-row `expr.clone()` + `resolve_correlated_in_expr`
208            // dispatch for the corresponding subquery class.
209            let scalar_ready = m
210                .expr_plans
211                .get(&key)
212                .map(|(_, plan, _)| !plan.is_empty() && plan.iter().all(|p| p.is_some()))
213                .unwrap_or(false);
214            let exists_ready = m
215                .exists_plans
216                .get(&key)
217                .map(|plan| !plan.is_empty() && plan.iter().all(|p| p.is_some()))
218                .unwrap_or(false);
219            if scalar_ready || exists_ready {
220                // Fast path: every planned subquery resolves via its
221                // map; clone the (hollowed-where-scalar) template,
222                // splice map values, eval. EXISTS bodies are NOT
223                // hollowed (we don't traverse into them during splice —
224                // `splice_planned_exists` consumes the EXISTS node
225                // wholesale), so cloning the original `expr` works for
226                // the EXISTS-only path.
227                let scalar_plan = m
228                    .expr_plans
229                    .get(&key)
230                    .map(|(_, plan, template)| (plan.clone(), template.clone()));
231                let exists_plan = m.exists_plans.get(&key).cloned();
232                let mut e = match &scalar_plan {
233                    Some((_, template)) => template.clone(),
234                    None => expr.clone(),
235                };
236                let mut all_ok = true;
237                if let Some((plan, _)) = &scalar_plan {
238                    let mut idx = 0usize;
239                    all_ok &= splice_planned_subqueries(&mut e, plan, &mut idx, row, ctx)?;
240                }
241                if all_ok && let Some(plan) = &exists_plan {
242                    let mut idx = 0usize;
243                    all_ok &= splice_planned_exists(&mut e, plan, &mut idx, row, ctx)?;
244                }
245                if all_ok {
246                    if expr_has_subquery(&e) {
247                        self.resolve_correlated_in_expr(&mut e, row, ctx, cancel, memo)?;
248                    }
249                    return eval::eval_expr(&e, row, ctx).map_err(EngineError::Eval);
250                }
251            }
252        }
253        let mut e = expr.clone();
254        self.resolve_correlated_in_expr(&mut e, row, ctx, cancel, memo)?;
255        eval::eval_expr(&e, row, ctx).map_err(EngineError::Eval)
256    }
257
258    fn resolve_correlated_in_expr(
259        &self,
260        e: &mut Expr,
261        row: &Row<'static>,
262        ctx: &EvalContext<'_>,
263        cancel: CancelToken<'_>,
264        mut memo: Option<&mut memoize::MemoizeCache>,
265    ) -> Result<(), EngineError> {
266        match e {
267            Expr::AggregateOrdered { call, order_by, .. } => {
268                self.resolve_correlated_in_expr(call, row, ctx, cancel, memo.as_deref_mut())?;
269                for o in order_by.iter_mut() {
270                    self.resolve_correlated_in_expr(
271                        &mut o.expr,
272                        row,
273                        ctx,
274                        cancel,
275                        memo.as_deref_mut(),
276                    )?;
277                }
278            }
279            Expr::ScalarSubquery(inner) => {
280                // v7.29 (round-22 phase 3) — batch path first: a
281                // correlated scalar of the `inner_col = outer_col
282                // [ORDER BY … LIMIT 1]` shape evaluates ONCE as a
283                // grouped scan; per-row resolution becomes a map
284                // lookup. 23.5k per-group executions (~900 ms) became
285                // one scan + lookups.
286                // v7.37.x (docker-fair SCALARSQ attack) — pointer-keyed
287                // fast cache. The inner SelectStatement is stable for
288                // the duration of the query, so its address makes a
289                // unique key that costs nothing to compute (vs
290                // `alloc::format!("{}", inner)` ~ 500 ns × N outer
291                // rows of pure repr churn).
292                if memo.is_some() {
293                    let ptr_key = core::ptr::from_ref::<SelectStatement>(&**inner) as usize;
294                    let entry_known = memo
295                        .as_ref()
296                        .is_some_and(|m| m.group_maps_by_ptr.contains_key(&ptr_key));
297                    if !entry_known {
298                        let built = self
299                            .try_batch_correlated_scalar(inner, None, cancel)?
300                            .map(alloc::rc::Rc::new);
301                        if let Some(m) = memo.as_deref_mut() {
302                            m.group_maps_by_ptr.insert(ptr_key, built);
303                        }
304                    }
305                    if let Some(m) = memo.as_deref_mut()
306                        && let Some(Some(gm)) = m.group_maps_by_ptr.get(&ptr_key)
307                    {
308                        let (outer_col, map, empty_default) = gm.as_ref();
309                        let key_v = eval::eval_expr(&Expr::Column(outer_col.clone()), row, ctx)
310                            .map_err(EngineError::Eval)?;
311                        // v7.37.x — scalar subquery empty-set semantics:
312                        // `COUNT(*)` / `COUNT(col)` over no rows = 0,
313                        // every other aggregate = NULL. The batched
314                        // GroupMap omits keys whose inner-table partition
315                        // was empty; treat such misses as the per-
316                        // aggregate empty-default.
317                        let v = if matches!(key_v, Value::Null) {
318                            Value::Null
319                        } else {
320                            map.get(&aggregate::encode_key(core::slice::from_ref(&key_v)))
321                                .cloned()
322                                .unwrap_or_else(|| empty_default.clone())
323                        };
324                        *e = value_to_literal_expr(v)?;
325                        return Ok(());
326                    }
327                }
328                // v6.2.6 — Memoize: build the cache key from the
329                // pre-substitution subquery repr + the outer row's
330                // values. Two outer rows with identical correlated
331                // values hit the same entry.
332                let cache_key = memo.as_ref().map(|_| memoize::CacheKey {
333                    subquery_repr: alloc::format!("{}", **inner),
334                    outer_values: row.values.iter().cloned().map(Value::into_owned).collect(),
335                });
336                if let (Some(cache), Some(k)) = (memo.as_deref_mut(), cache_key.as_ref())
337                    && let Some(cached) = cache.get(k)
338                {
339                    *e = value_to_literal_expr(cached)?;
340                    return Ok(());
341                }
342                // v7.37.x (docker-fair SCALARSQ attack) — direct PK probe
343                // fast path. The shape
344                //   (SELECT COUNT(*) FROM T WHERE T.pk = outer.col)
345                // — common SCALARSQ shape and what the docker-fair
346                // SCALARSQ benchmark exercises — is a 1-bit lookup:
347                // the probe either finds 1 row or 0. Skip
348                // `exec_select_cancel`'s parse / resolve / plan /
349                // aggregate roundtrip; do an index seek on T.pk
350                // directly and return `Int(0)` or `Int(1)`. PG with a
351                // cached prepared plan does roughly this; SCALARSQ
352                // drops from per-row ~3 µs to per-row ~100 ns.
353                if let Some(v) = self.try_scalar_count_pk_eq_probe(inner, row, ctx)? {
354                    *e = value_to_literal_expr(v)?;
355                    return Ok(());
356                }
357                let mut s = (**inner).clone();
358                substitute_outer_columns(&mut s, row, ctx);
359                let r = self.exec_select_cancel(&s, cancel)?;
360                let QueryResult::Rows { rows, .. } = r else {
361                    return Err(EngineError::Unsupported(
362                        "scalar subquery: inner did not return rows".into(),
363                    ));
364                };
365                let value = match rows.as_slice() {
366                    [] => Value::Null,
367                    [r0] => r0.values.first().cloned().unwrap_or(Value::Null),
368                    _ => {
369                        return Err(EngineError::Unsupported(alloc::format!(
370                            "scalar subquery returned {} rows; expected 0 or 1",
371                            rows.len()
372                        )));
373                    }
374                };
375                if let (Some(cache), Some(k)) = (memo.as_deref_mut(), cache_key) {
376                    cache.insert(k, value.clone());
377                }
378                *e = value_to_literal_expr(value)?;
379            }
380            Expr::Exists { subquery, negated } => {
381                // v7.34 (mailrs conn-pool P0) — semi/anti-join batch path
382                // first: a correlated `[NOT] EXISTS` of the
383                // `inner.k = outer.col [AND inner-preds]` shape builds its
384                // inner key-set ONCE (keyed by repr in the per-query memo);
385                // per-row resolution becomes a membership test. 24k per-row
386                // inner executions became one scan + 24k lookups.
387                if memo.is_some() {
388                    let repr = alloc::format!("{}", **subquery);
389                    let known = memo
390                        .as_ref()
391                        .is_some_and(|m| m.exists_sets.contains_key(&repr));
392                    if !known {
393                        let built = self
394                            .try_batch_correlated_exists(subquery, cancel)?
395                            .map(alloc::rc::Rc::new);
396                        if let Some(m) = memo.as_deref_mut() {
397                            m.exists_sets.insert(repr.clone(), built);
398                        }
399                    }
400                    if let Some(m) = memo.as_deref_mut()
401                        && let Some(Some(es)) = m.exists_sets.get(&repr)
402                    {
403                        let (outer_cols, set) = es.as_ref();
404                        let mut key_vals: Vec<Value<'static>> =
405                            Vec::with_capacity(outer_cols.len());
406                        let mut any_null = false;
407                        for oc in outer_cols {
408                            let v = eval::eval_expr(&Expr::Column(oc.clone()), row, ctx)
409                                .map_err(EngineError::Eval)?;
410                            if matches!(v, Value::Null) {
411                                any_null = true;
412                            }
413                            key_vals.push(v);
414                        }
415                        // NULL key component → never matches → not present.
416                        let present = !any_null && set.contains(&aggregate::encode_key(&key_vals));
417                        let bit = if *negated { !present } else { present };
418                        *e = Expr::Literal(Literal::Bool(bit));
419                        return Ok(());
420                    }
421                }
422                let mut s = (**subquery).clone();
423                substitute_outer_columns(&mut s, row, ctx);
424                let r = self.exec_select_cancel(&s, cancel)?;
425                let exists = matches!(r, QueryResult::Rows { rows, .. } if !rows.is_empty());
426                let bit = if *negated { !exists } else { exists };
427                *e = Expr::Literal(Literal::Bool(bit));
428            }
429            Expr::InSubquery {
430                expr: lhs,
431                subquery,
432                negated,
433            } => {
434                self.resolve_correlated_in_expr(lhs, row, ctx, cancel, memo.as_deref_mut())?;
435                let lhs_val = eval::eval_expr(lhs, row, ctx).map_err(EngineError::Eval)?;
436                let mut s = (**subquery).clone();
437                substitute_outer_columns(&mut s, row, ctx);
438                let r = self.exec_select_cancel(&s, cancel)?;
439                let QueryResult::Rows { columns, rows, .. } = r else {
440                    return Err(EngineError::Unsupported(
441                        "IN-subquery: inner did not return rows".into(),
442                    ));
443                };
444                if columns.len() != 1 {
445                    return Err(EngineError::Unsupported(alloc::format!(
446                        "IN-subquery must project exactly one column; got {}",
447                        columns.len()
448                    )));
449                }
450                let mut found = false;
451                let mut any_null = false;
452                for r0 in rows {
453                    let v = r0.values.into_iter().next().unwrap_or(Value::Null);
454                    if v.is_null() {
455                        any_null = true;
456                        continue;
457                    }
458                    if value_cmp(&v, &lhs_val) == core::cmp::Ordering::Equal {
459                        found = true;
460                        break;
461                    }
462                }
463                let bit = if found {
464                    !*negated
465                } else if any_null {
466                    return Err(EngineError::Unsupported(
467                        "IN-subquery with NULL in result and no match: NULL semantics not yet implemented".into(),
468                    ));
469                } else {
470                    *negated
471                };
472                *e = Expr::Literal(Literal::Bool(bit));
473            }
474            Expr::Binary { lhs, rhs, .. } => {
475                self.resolve_correlated_in_expr(lhs, row, ctx, cancel, memo.as_deref_mut())?;
476                self.resolve_correlated_in_expr(rhs, row, ctx, cancel, memo.as_deref_mut())?;
477            }
478            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
479                self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
480            }
481            Expr::Like { expr, pattern, .. } => {
482                self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
483                self.resolve_correlated_in_expr(pattern, row, ctx, cancel, memo.as_deref_mut())?;
484            }
485            Expr::FunctionCall { args, .. } => {
486                for a in args {
487                    self.resolve_correlated_in_expr(a, row, ctx, cancel, memo.as_deref_mut())?;
488                }
489            }
490            Expr::Extract { source, .. } => {
491                self.resolve_correlated_in_expr(source, row, ctx, cancel, memo.as_deref_mut())?;
492            }
493            Expr::WindowFunction { .. }
494            | Expr::Literal(_)
495            | Expr::Placeholder(_)
496            | Expr::Column(_) => {}
497            // v7.10.10 — recurse children.
498            Expr::Array(items) => {
499                for elem in items {
500                    self.resolve_correlated_in_expr(elem, row, ctx, cancel, memo.as_deref_mut())?;
501                }
502            }
503            Expr::ArraySubscript { target, index } => {
504                self.resolve_correlated_in_expr(target, row, ctx, cancel, memo.as_deref_mut())?;
505                self.resolve_correlated_in_expr(index, row, ctx, cancel, memo.as_deref_mut())?;
506            }
507            Expr::AnyAll { expr, array, .. } => {
508                self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
509                self.resolve_correlated_in_expr(array, row, ctx, cancel, memo.as_deref_mut())?;
510            }
511            Expr::InList { expr, list, .. } => {
512                self.resolve_correlated_in_expr(expr, row, ctx, cancel, memo.as_deref_mut())?;
513                for item in list {
514                    self.resolve_correlated_in_expr(item, row, ctx, cancel, memo.as_deref_mut())?;
515                }
516            }
517            Expr::Case {
518                operand,
519                branches,
520                else_branch,
521            } => {
522                if let Some(o) = operand {
523                    self.resolve_correlated_in_expr(o, row, ctx, cancel, memo.as_deref_mut())?;
524                }
525                for (w, t) in branches {
526                    self.resolve_correlated_in_expr(w, row, ctx, cancel, memo.as_deref_mut())?;
527                    self.resolve_correlated_in_expr(t, row, ctx, cancel, memo.as_deref_mut())?;
528                }
529                if let Some(e) = else_branch {
530                    self.resolve_correlated_in_expr(e, row, ctx, cancel, memo.as_deref_mut())?;
531                }
532            }
533        }
534        Ok(())
535    }
536
537    /// v4.10: pre-walk the WHERE / projection / etc. of a SELECT and
538    /// replace every subquery node with a materialised literal. SPG
539    /// only supports uncorrelated subqueries — the inner SELECT does
540    /// not see outer-row columns, so the result is the same for every
541    /// outer row and can be evaluated once.
542    ///
543    /// Returns the rewritten statement; the caller passes this to the
544    /// regular row-loop executor which no longer sees Subquery nodes
545    /// in its tree.
546    pub(crate) fn subquery_replacement(
547        &self,
548        e: &Expr,
549        cancel: CancelToken<'_>,
550    ) -> Result<Option<Expr>, EngineError> {
551        match e {
552            Expr::ScalarSubquery(inner) => {
553                // v7.32 (R30) — a correlated subquery is resolved by
554                // the per-row / post-LIMIT correlated path; executing
555                // it here only to catch the correlation error first
556                // materialises (and discards) its whole inner FROM.
557                if select_is_correlated(inner) {
558                    return Ok(None);
559                }
560                let mut s = (**inner).clone();
561                // Recurse into the inner SELECT first so nested
562                // subqueries materialise bottom-up.
563                self.resolve_select_subqueries(&mut s, cancel)?;
564                let r = match self.exec_bare_select_cancel(&s, cancel) {
565                    Ok(r) => r,
566                    Err(e) if is_correlation_error(&e) => return Ok(None),
567                    Err(e) => return Err(e),
568                };
569                let QueryResult::Rows { rows, .. } = r else {
570                    return Err(EngineError::Unsupported(
571                        "scalar subquery: inner statement did not return rows".into(),
572                    ));
573                };
574                let value = match rows.as_slice() {
575                    [] => Value::Null,
576                    [row] => row.values.first().cloned().unwrap_or(Value::Null),
577                    _ => {
578                        return Err(EngineError::Unsupported(alloc::format!(
579                            "scalar subquery returned {} rows; expected 0 or 1",
580                            rows.len()
581                        )));
582                    }
583                };
584                Ok(Some(value_to_literal_expr(value)?))
585            }
586            Expr::Exists { subquery, negated } => {
587                if select_is_correlated(subquery) {
588                    return Ok(None);
589                }
590                let mut s = (**subquery).clone();
591                self.resolve_select_subqueries(&mut s, cancel)?;
592                let r = match self.exec_bare_select_cancel(&s, cancel) {
593                    Ok(r) => r,
594                    Err(e) if is_correlation_error(&e) => return Ok(None),
595                    Err(e) => return Err(e),
596                };
597                let exists = match r {
598                    QueryResult::Rows { rows, .. } => !rows.is_empty(),
599                    QueryResult::CommandOk { .. } => false,
600                };
601                let bit = if *negated { !exists } else { exists };
602                Ok(Some(Expr::Literal(Literal::Bool(bit))))
603            }
604            Expr::InSubquery {
605                expr,
606                subquery,
607                negated,
608            } => {
609                if select_is_correlated(subquery) {
610                    return Ok(None);
611                }
612                let mut s = (**subquery).clone();
613                self.resolve_select_subqueries(&mut s, cancel)?;
614                let r = match self.exec_bare_select_cancel(&s, cancel) {
615                    Ok(r) => r,
616                    Err(e) if is_correlation_error(&e) => return Ok(None),
617                    Err(e) => return Err(e),
618                };
619                let QueryResult::Rows { columns, rows, .. } = r else {
620                    return Err(EngineError::Unsupported(
621                        "IN-subquery: inner statement did not return rows".into(),
622                    ));
623                };
624                if columns.len() != 1 {
625                    return Err(EngineError::Unsupported(alloc::format!(
626                        "IN-subquery must project exactly one column; got {}",
627                        columns.len()
628                    )));
629                }
630                // v7.30.2 (mailrs round-25) — flat InList, NOT an OR-Eq
631                // chain: chain depth scaled with the inner result's ROW
632                // COUNT, so one 24k-match search overflowed the worker
633                // stack (recursive eval + recursive Box drop) and
634                // aborted the embedding host process.
635                let mut list: Vec<Expr> = Vec::with_capacity(rows.len());
636                for row in rows {
637                    let v = row.values.into_iter().next().unwrap_or(Value::Null);
638                    list.push(value_to_literal_expr(v)?);
639                }
640                Ok(Some(Expr::InList {
641                    expr: expr.clone(),
642                    list,
643                    negated: *negated,
644                }))
645            }
646            _ => Ok(None),
647        }
648    }
649}
650
651impl Engine {
652    /// v7.29 (round-22 phase 3) — try to batch-evaluate a correlated
653    /// scalar subquery of the shape
654    ///   (SELECT expr FROM … WHERE inner_preds AND inner_col = outer_col
655    ///    [ORDER BY o [DESC]] [LIMIT 1])
656    /// by running the subquery ONCE without the correlation and
657    /// folding rows into a key→value map (group top-1 when ordered).
658    /// Returns None when the shape doesn't qualify; correctness then
659    /// falls back to per-row execution.
660    pub(crate) fn try_batch_correlated_scalar(
661        &self,
662        inner: &SelectStatement,
663        restrict: Option<(&[Row<'static>], &EvalContext<'_>)>,
664        cancel: CancelToken<'_>,
665    ) -> Result<Option<memoize::GroupMap>, EngineError> {
666        use spg_sql::ast::{BinOp, SelectItem as SI};
667        if !inner.ctes.is_empty()
668            || !inner.unions.is_empty()
669            || inner.group_by.is_some()
670            || inner.having.is_some()
671            || inner.distinct
672            || inner.items.len() != 1
673            || inner.order_by.len() > 1
674            || inner.offset.is_some()
675        {
676            return Ok(None);
677        }
678        // LIMIT must be absent or literally 1 (top-1 semantics).
679        if let Some(le) = inner.limit
680            && le.as_literal() != Some(1)
681        {
682            return Ok(None);
683        }
684        let Some(from) = &inner.from else {
685            return Ok(None);
686        };
687        if from.primary.lateral_subquery.is_some() || from.primary.unnest_expr.is_some() {
688            return Ok(None);
689        }
690        // Inner alias set.
691        let mut inner_aliases: Vec<String> = Vec::new();
692        inner_aliases.push(
693            from.primary
694                .alias
695                .clone()
696                .unwrap_or_else(|| from.primary.name.clone()),
697        );
698        for j in &from.joins {
699            if j.table.lateral_subquery.is_some() || j.table.unnest_expr.is_some() {
700                return Ok(None);
701            }
702            inner_aliases.push(
703                j.table
704                    .alias
705                    .clone()
706                    .unwrap_or_else(|| j.table.name.clone()),
707            );
708        }
709        let is_inner = |c: &spg_sql::ast::ColumnName| -> bool {
710            match &c.qualifier {
711                Some(q) => inner_aliases.iter().any(|a| a.eq_ignore_ascii_case(q)),
712                None => false,
713            }
714        };
715        let is_outer = |c: &spg_sql::ast::ColumnName| -> bool {
716            match &c.qualifier {
717                Some(q) => !inner_aliases.iter().any(|a| a.eq_ignore_ascii_case(q)),
718                // Synthetic group columns arrive bare after the
719                // aggregate rewrite.
720                None => c.name.starts_with("__grp_") || c.name.starts_with("__agg_"),
721            }
722        };
723        // Every expression OTHER than the correlation conjunct must be
724        // fully inner (qualified to inner aliases).
725        let all_inner = |e: &Expr| -> bool {
726            let mut cols: Vec<spg_sql::ast::ColumnName> = Vec::new();
727            let mut subs: Vec<&SelectStatement> = Vec::new();
728            visit_expr_columns_and_subqueries(e, &mut |c| cols.push(c.clone()), &mut |sub| {
729                subs.push(sub)
730            });
731            subs.is_empty() && cols.iter().all(|c| is_inner(c) && !c.name.is_empty())
732        };
733        let Some(w) = &inner.where_ else {
734            return Ok(None);
735        };
736        let conjuncts = reorder::split_and_conjunctions(w);
737        let mut corr: Option<(spg_sql::ast::ColumnName, spg_sql::ast::ColumnName)> = None; // (inner, outer)
738        let mut rest: Vec<&Expr> = Vec::new();
739        for c in conjuncts {
740            if let Expr::Binary {
741                lhs,
742                op: BinOp::Eq,
743                rhs,
744            } = c
745                && let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref())
746            {
747                let pair = if is_inner(a) && is_outer(b) {
748                    Some((a.clone(), b.clone()))
749                } else if is_inner(b) && is_outer(a) {
750                    Some((b.clone(), a.clone()))
751                } else {
752                    None
753                };
754                if let Some(p) = pair {
755                    if corr.is_some() {
756                        return Ok(None); // more than one correlation
757                    }
758                    corr = Some(p);
759                    continue;
760                }
761            }
762            if !all_inner(c) {
763                return Ok(None);
764            }
765            rest.push(c);
766        }
767        let Some((inner_col, outer_col)) = corr else {
768            return Ok(None);
769        };
770        let SI::Expr { expr: out_expr, .. } = &inner.items[0] else {
771            return Ok(None);
772        };
773        if !all_inner(out_expr) {
774            return Ok(None);
775        }
776        let order = inner.order_by.first();
777        if let Some(o) = order
778            && !all_inner(&o.expr)
779        {
780            return Ok(None);
781        }
782        // Build the batch statement: SELECT inner_col, [order], expr
783        // FROM … WHERE rest — no correlation, no order, no limit.
784        let mut batch = inner.clone();
785        batch.limit = None;
786        batch.offset = None;
787        batch.order_by = Vec::new();
788        batch.where_ = rest
789            .iter()
790            .map(|e| (*e).clone())
791            .reduce(|a, b| Expr::Binary {
792                lhs: alloc::boxed::Box::new(a),
793                op: BinOp::And,
794                rhs: alloc::boxed::Box::new(b),
795            });
796        let mut items: Vec<SI> = alloc::vec![SI::Expr {
797            expr: Expr::Column(inner_col.clone()),
798            alias: None,
799        }];
800        if let Some(o) = order {
801            items.push(SI::Expr {
802                expr: o.expr.clone(),
803                alias: None,
804            });
805        }
806        items.push(SI::Expr {
807            expr: out_expr.clone(),
808            alias: None,
809        });
810        batch.items = items;
811        // v7.37.x (docker-fair SCALARSQ-aggregate path) — when the
812        // inner output expression is an aggregate (e.g. `COUNT(*)`
813        // for the `(SELECT COUNT(*) FROM inner WHERE inner.k =
814        // outer.k)` scalar subquery shape), the batch query
815        // `SELECT inner.k, COUNT(*) FROM inner` is invalid SQL
816        // without `GROUP BY inner.k`. Inject the GROUP BY so the
817        // aggregate executor produces (key → count) pairs, matching
818        // the per-key scalar-subquery semantics. Pre-7.37.x this
819        // case mis-executed as a single anonymous group and either
820        // returned a wrong total or surfaced an `UnknownQualifier`
821        // (when the rewriter couldn't bind the bare column ref).
822        if aggregate::contains_aggregate(out_expr) {
823            batch.group_by = Some(alloc::vec![Expr::Column(inner_col.clone())]);
824        }
825        // v7.32 (architecture v2 P3) — keyed index-probe. When the
826        // caller hands a restriction set (the ≤LIMIT surviving outer
827        // rows of a post-LIMIT deferred subquery) AND the correlation
828        // column is backed by an index, evaluate only the surviving
829        // correlation keys via per-key index seek instead of scanning
830        // the whole inner relation. This is PG's SubPlan with an index
831        // scan: 50 seeks of ~µs each vs a 24k-row all-keys batch
832        // (~16 ms). The grouping below is shared — keyed result ≡
833        // full-batch result for the covered keys, so semantics are
834        // identical.
835        //
836        // The inner relation may itself be a join. The correlation
837        // column names the *driving* table; PG, MySQL and MariaDB all
838        // plan a correlated join subquery the same way — seek the
839        // correlation index, then index-nested-loop to the joined
840        // table. We promote that table to drive `batch` (an all-INNER
841        // chain only) so the per-key `inner_col = <lit>` predicate
842        // becomes a primary index seek and the existing INL path joins
843        // the rest. A correlation column without a usable index, or a
844        // join the promotion can't safely reorder, returns None and
845        // the caller falls back to the lazy all-keys batch (no
846        // regression).
847        let keyed: Option<(&[Row<'static>], &EvalContext<'_>)> =
848            restrict.and_then(|(rows, rctx)| {
849                // Resolve the table that owns the correlation column.
850                let driver_name: &str = if from.joins.is_empty() {
851                    from.primary.name.as_str()
852                } else {
853                    let q = inner_col.qualifier.as_deref()?;
854                    let primary_alias = from
855                        .primary
856                        .alias
857                        .as_deref()
858                        .unwrap_or(from.primary.name.as_str());
859                    if primary_alias.eq_ignore_ascii_case(q) {
860                        from.primary.name.as_str()
861                    } else {
862                        from.joins
863                            .iter()
864                            .find(|j| {
865                                j.table
866                                    .alias
867                                    .as_deref()
868                                    .unwrap_or(j.table.name.as_str())
869                                    .eq_ignore_ascii_case(q)
870                            })
871                            .map(|j| j.table.name.as_str())?
872                    }
873                };
874                let table = self.active_catalog().get(driver_name)?;
875                let pos = table
876                    .schema()
877                    .columns
878                    .iter()
879                    .position(|c| c.name.eq_ignore_ascii_case(&inner_col.name))?;
880                table.index_on(pos)?;
881                // v7.33 (mailrs 7.32.1) — cost guard. The keyed path runs one
882                // index seek (a full `exec_select_cancel` round trip) per
883                // surviving correlation key. That wins when few keys survive
884                // (a tight outer LIMIT leaves a handful), but a *correlated
885                // select-list subquery with no outer LIMIT* leaves every group
886                // alive — `restrict` is then all ~N groups, and N seeks dwarf
887                // a single grouped all-keys scan of the same driver. Reproduced
888                // on the conversation aggregation (`get_conversations_by_thread_ids`,
889                // no LIMIT): 24k per-key seeks took 78–155 ms vs ~one scan.
890                // Fall through to the all-keys batch (`keyed = None` → the
891                // `else` arm below) when the survivor set is large relative to
892                // the driver; the batch's group map ⊇ the keyed map for every
893                // covered key, so the result is identical. Crossover ~rows/4
894                // (measured per-seek exec overhead vs per-row scan cost).
895                if rows.len().saturating_mul(4) >= table.row_count() {
896                    return None;
897                }
898                // For a join inner, drive the seek from the correlation
899                // table so `inner_col = <lit>` lands as a primary index
900                // seek (else the source-order primary scans the full
901                // relation and the join hash-builds the whole peer — the
902                // 12 GB all-keys hog R30 hit at prod scale).
903                if !from.joins.is_empty() {
904                    let driver_alias = inner_col.qualifier.as_deref()?;
905                    if !reorder::drive_from(&mut batch, driver_alias) {
906                        return None;
907                    }
908                }
909                Some((rows, rctx))
910            });
911        let rows = if let Some((restrict_rows, rctx)) = keyed {
912            BATCHED_SCALAR_KEYED_FIRE_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
913            // v7.37.4 A' — collect the deduped surviving correlation
914            // keys, then issue ONE `inner.k IN (lit1, …, litN)` probe
915            // instead of N separate `inner.k = lit` probes. The v7.34.3
916            // IN-list seek path treats the literal list as a bitmap-
917            // style index sweep (single index lookup per literal,
918            // unioned), so the total cost is O(N seeks + matched rows)
919            // — same asymptotic as the N-probe loop but without N
920            // rounds of stmt clone + plan + executor stack overhead.
921            //
922            // Per-probe overhead measured on mailrs prod 100k:
923            //   - sequential: 50 probes × ~1.7 ms = ~85 ms per subq
924            //   - 3 subqueries × ~85 ms = ~255 ms of the 388 ms total
925            // IN-list batched probe is one stmt + N IN-list literals,
926            // amortising the plan + setup over all keys.
927            let mut seen: alloc::collections::BTreeSet<String> =
928                alloc::collections::BTreeSet::new();
929            let mut key_lits: Vec<Expr> = Vec::new();
930            for srow in restrict_rows {
931                cancel.check()?;
932                let kv = eval::eval_expr(&Expr::Column(outer_col.clone()), srow, rctx)
933                    .map_err(EngineError::Eval)?;
934                if matches!(kv, Value::Null) {
935                    continue;
936                }
937                if !seen.insert(aggregate::encode_key(core::slice::from_ref(&kv))) {
938                    continue;
939                }
940                key_lits.push(value_to_literal_expr(kv)?);
941            }
942            if key_lits.is_empty() {
943                Vec::new()
944            } else {
945                let in_pred = Expr::InList {
946                    expr: alloc::boxed::Box::new(Expr::Column(inner_col.clone())),
947                    list: key_lits,
948                    negated: false,
949                };
950                let mut probe = batch.clone();
951                probe.where_ = Some(match probe.where_.take() {
952                    Some(w) => Expr::Binary {
953                        lhs: alloc::boxed::Box::new(w),
954                        op: BinOp::And,
955                        rhs: alloc::boxed::Box::new(in_pred),
956                    },
957                    None => in_pred,
958                });
959                BATCHED_SCALAR_KEYED_PROBE_COUNT
960                    .fetch_add(1, core::sync::atomic::Ordering::Relaxed);
961                if let QueryResult::Rows { rows, .. } = self.exec_select_cancel(&probe, cancel)? {
962                    rows
963                } else {
964                    Vec::new()
965                }
966            }
967        } else {
968            BATCHED_SCALAR_FALL_THROUGH_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
969            let r = self.exec_select_cancel(&batch, cancel)?;
970            let QueryResult::Rows { rows, .. } = r else {
971                return Ok(None);
972            };
973            rows
974        };
975        let has_order = order.is_some();
976        let (desc, nf) = order
977            .map(|o| (o.desc, o.nulls_first))
978            .unwrap_or((false, None));
979        let mut best: alloc::collections::BTreeMap<String, (Option<Value>, Value)> =
980            alloc::collections::BTreeMap::new();
981        for row in rows {
982            let key_v = row.values.first().cloned().unwrap_or(Value::Null);
983            if matches!(key_v, Value::Null) {
984                continue;
985            }
986            let key = aggregate::encode_key(core::slice::from_ref(&key_v));
987            let (ord_v, out_v) = if has_order {
988                (
989                    Some(row.values.get(1).cloned().unwrap_or(Value::Null)),
990                    row.values.get(2).cloned().unwrap_or(Value::Null),
991                )
992            } else {
993                (None, row.values.get(1).cloned().unwrap_or(Value::Null))
994            };
995            match best.get(&key) {
996                None => {
997                    best.insert(key, (ord_v, out_v));
998                }
999                Some((cur_ord, _)) if has_order => {
1000                    // The sorted-first row wins: candidate beats the
1001                    // incumbent when it compares LESS under the key's
1002                    // ordering.
1003                    let cand = ord_v.clone().unwrap_or(Value::Null);
1004                    let cur = cur_ord.clone().unwrap_or(Value::Null);
1005                    if order_by_value_cmp(desc, nf, &cand, &cur) == core::cmp::Ordering::Less {
1006                        best.insert(key, (ord_v, out_v));
1007                    }
1008                }
1009                Some(_) => {} // unordered: first row stands (any row is valid)
1010            }
1011        }
1012        let map = best.into_iter().map(|(k, (_, v))| (k, v)).collect();
1013        // v7.37.x (docker-fair SCALARSQ attack) — empty-default per
1014        // PG scalar-subquery aggregate semantics. Captured here so the
1015        // splice path doesn't have to re-introspect a possibly-hollowed
1016        // inner template.
1017        let empty_default = scalar_subquery_empty_default(inner);
1018        Ok(Some((outer_col, map, empty_default)))
1019    }
1020}
1021
1022impl Engine {
1023    /// v7.34 (mailrs conn-pool-exhaustion P0) — decorrelate a correlated
1024    /// `[NOT] EXISTS` into a hash semi/anti-join. Recognise
1025    ///   EXISTS (SELECT … FROM t [joins]
1026    ///           WHERE k1 = o1 AND … AND kN = oN AND <inner-preds>)
1027    /// run the inner ONCE without the correlation, collect the set of
1028    /// inner key-tuples `(k1,…,kN)` that satisfy the inner-preds; an outer
1029    /// row's EXISTS then reduces to a membership test on `(o1,…,oN)`. The
1030    /// reported `count_unseen` ran two correlated `NOT EXISTS` per ~24k
1031    /// join survivors (~48k inner executions, 98.7% of a 1.4 s query);
1032    /// this turns each into one scan + 24k lookups.
1033    ///
1034    /// Multi-column correlation is supported (the prod `snoozed` anti-join
1035    /// correlates on both `thread_id` and `account_address`). NULL is
1036    /// exact: an outer key with any NULL component is never present
1037    /// (`NULL = k` is never true), so EXISTS=false / NOT EXISTS=true,
1038    /// identical to the per-row resolver. Returns None when the shape
1039    /// doesn't qualify — the caller falls back to per-row execution, so
1040    /// there is no regression.
1041    pub(crate) fn try_batch_correlated_exists(
1042        &self,
1043        inner: &SelectStatement,
1044        cancel: CancelToken<'_>,
1045    ) -> Result<Option<memoize::ExistsSet>, EngineError> {
1046        use spg_sql::ast::SelectItem as SI;
1047        if !inner.ctes.is_empty()
1048            || !inner.unions.is_empty()
1049            || inner.group_by.is_some()
1050            || inner.having.is_some()
1051            || inner.distinct
1052        {
1053            return Ok(None);
1054        }
1055        let Some(from) = &inner.from else {
1056            return Ok(None);
1057        };
1058        if from.primary.lateral_subquery.is_some()
1059            || from.primary.unnest_expr.is_some()
1060            || from.primary.generate_series_args.is_some()
1061            || from.primary.as_of_segment.is_some()
1062        {
1063            return Ok(None);
1064        }
1065        let mut inner_aliases: Vec<String> = Vec::new();
1066        inner_aliases.push(
1067            from.primary
1068                .alias
1069                .clone()
1070                .unwrap_or_else(|| from.primary.name.clone()),
1071        );
1072        for j in &from.joins {
1073            if j.table.lateral_subquery.is_some() || j.table.unnest_expr.is_some() {
1074                return Ok(None);
1075            }
1076            inner_aliases.push(
1077                j.table
1078                    .alias
1079                    .clone()
1080                    .unwrap_or_else(|| j.table.name.clone()),
1081            );
1082        }
1083        let is_inner = |c: &spg_sql::ast::ColumnName| -> bool {
1084            match &c.qualifier {
1085                Some(q) => inner_aliases.iter().any(|a| a.eq_ignore_ascii_case(q)),
1086                None => false,
1087            }
1088        };
1089        let is_outer = |c: &spg_sql::ast::ColumnName| -> bool {
1090            match &c.qualifier {
1091                Some(q) => !inner_aliases.iter().any(|a| a.eq_ignore_ascii_case(q)),
1092                None => c.name.starts_with("__grp_") || c.name.starts_with("__agg_"),
1093            }
1094        };
1095        let all_inner = |e: &Expr| -> bool {
1096            let mut cols: Vec<spg_sql::ast::ColumnName> = Vec::new();
1097            let mut subs: Vec<&SelectStatement> = Vec::new();
1098            visit_expr_columns_and_subqueries(e, &mut |c| cols.push(c.clone()), &mut |sub| {
1099                subs.push(sub)
1100            });
1101            subs.is_empty() && cols.iter().all(|c| is_inner(c) && !c.name.is_empty())
1102        };
1103        let Some(w) = &inner.where_ else {
1104            return Ok(None);
1105        };
1106        let conjuncts = reorder::split_and_conjunctions(w);
1107        let mut inner_keys: Vec<spg_sql::ast::ColumnName> = Vec::new();
1108        let mut outer_cols: Vec<spg_sql::ast::ColumnName> = Vec::new();
1109        let mut rest: Vec<&Expr> = Vec::new();
1110        for c in conjuncts {
1111            if let Expr::Binary {
1112                lhs,
1113                op: BinOp::Eq,
1114                rhs,
1115            } = c
1116                && let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref())
1117            {
1118                let pair = if is_inner(a) && is_outer(b) {
1119                    Some((a.clone(), b.clone()))
1120                } else if is_inner(b) && is_outer(a) {
1121                    Some((b.clone(), a.clone()))
1122                } else {
1123                    None
1124                };
1125                if let Some((ic, oc)) = pair {
1126                    inner_keys.push(ic);
1127                    outer_cols.push(oc);
1128                    continue;
1129                }
1130            }
1131            // A non-correlation conjunct must be purely inner (carried
1132            // into the build scan). Anything else (outer-only filter,
1133            // mixed expression) is beyond this rewrite.
1134            if !all_inner(c) {
1135                return Ok(None);
1136            }
1137            rest.push(c);
1138        }
1139        if inner_keys.is_empty() {
1140            return Ok(None); // uncorrelated — materialised elsewhere
1141        }
1142        // Build: SELECT k1,…,kN FROM <inner from> WHERE <rest> — no
1143        // correlation, no order/limit. The inner relation may be a join;
1144        // exec handles it.
1145        let mut batch = inner.clone();
1146        batch.limit = None;
1147        batch.offset = None;
1148        batch.order_by = Vec::new();
1149        batch.distinct = false;
1150        batch.where_ = rest
1151            .iter()
1152            .map(|e| (*e).clone())
1153            .reduce(|a, b| Expr::Binary {
1154                lhs: alloc::boxed::Box::new(a),
1155                op: BinOp::And,
1156                rhs: alloc::boxed::Box::new(b),
1157            });
1158        batch.items = inner_keys
1159            .iter()
1160            .map(|c| SI::Expr {
1161                expr: Expr::Column(c.clone()),
1162                alias: None,
1163            })
1164            .collect();
1165        let r = self.exec_select_cancel(&batch, cancel)?;
1166        let QueryResult::Rows { rows, .. } = r else {
1167            return Ok(None);
1168        };
1169        let n = inner_keys.len();
1170        let mut set: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
1171        for row in rows {
1172            let keys = row.values.get(..n).unwrap_or(&row.values);
1173            // A NULL key component can never satisfy `k = outer`, so the
1174            // tuple matches no outer row — drop it from the set.
1175            if keys.iter().any(|v| matches!(v, Value::Null)) {
1176                continue;
1177            }
1178            set.insert(aggregate::encode_key(keys));
1179        }
1180        Ok(Some((outer_cols, set)))
1181    }
1182}
1183
1184impl Engine {
1185    /// v7.33 (mailrs 7.32.1) — sublink pull-up for aggregate-wrapped
1186    /// correlated scalar subqueries. Rewrite
1187    ///   AGG( (SELECT j_col FROM t j WHERE j.key = outer.col [AND inner preds]) )
1188    /// into a LEFT JOIN plus a plain column reference:
1189    ///   AGG(j.j_col) … LEFT JOIN t AS j ON j.key = outer.col [AND inner preds]
1190    /// when `t.key` carries a single-column UNIQUE / PRIMARY KEY constraint.
1191    /// That constraint guarantees the join matches AT MOST ONE inner row
1192    /// per outer row, which is exactly the scalar subquery's at-most-one
1193    /// contract (NULL on no match), so the aggregate folds an identical
1194    /// per-row value stream — only now the executor streams one join
1195    /// instead of splicing a per-row subplan (the R31 path cloned a hollow
1196    /// template per outer row: ~24k clones for the mailrs conversation
1197    /// aggregation).
1198    ///
1199    /// Scoped tightly for safety: the subquery must sit inside an aggregate
1200    /// argument (so the joined column is always folded, never a bare
1201    /// select-list column a GROUP BY would reject); the inner must be a
1202    /// single plain-table scan projecting one inner column with exactly one
1203    /// `inner.key = outer.col` correlation (both qualified) plus optional
1204    /// all-inner predicates; and the select list must have no bare wildcard
1205    /// (a join would widen `*`). Anything else is left for the existing
1206    /// per-row / batch resolver. Returns true when it rewrote at least one.
1207    /// v7.37.4 (A — correlated LIMIT 1 ORDER BY DESC subquery pullup) —
1208    /// plan-time rewrite of the "per-key latest" select-list scalar
1209    /// subquery pattern:
1210    ///
1211    /// ```sql
1212    /// SELECT outer.k,
1213    ///        (SELECT proj_expr FROM inner
1214    ///          WHERE inner.k = outer.k AND <non_corr_preds>
1215    ///          ORDER BY sort_key DESC LIMIT 1) AS latest_proj
1216    ///   FROM outer
1217    /// ```
1218    ///
1219    /// becomes (semantically equivalent, executor-friendly):
1220    ///
1221    /// ```sql
1222    /// WITH __cl1_N AS (
1223    ///   SELECT inner.k AS jk,
1224    ///          (array_agg(proj_expr ORDER BY sort_key DESC NULLS LAST))[1] AS pj
1225    ///     FROM <inner.from>
1226    ///    WHERE <non_corr_preds>
1227    ///    GROUP BY inner.k
1228    /// )
1229    /// SELECT outer.k, MAX(__cl1_N.pj) AS latest_proj
1230    ///   FROM outer LEFT JOIN __cl1_N ON __cl1_N.jk = outer.k
1231    /// ```
1232    ///
1233    /// The CTE materialises once for the whole outer scan; LEFT JOIN
1234    /// on the GROUP-BY-unique `jk` column never multiplies outer rows.
1235    /// The `array_agg(... ORDER BY ...)[1]` form reuses the v7.33
1236    /// `first_ordered` argmax executor (per-group keep the first row,
1237    /// no array build).
1238    ///
1239    /// Common shape across inbox / feed / timeline applications:
1240    /// thread latest message, user latest transaction, device latest
1241    /// heartbeat. **Not a mailrs-specific patch** — any client query
1242    /// in this shape gets the rewrite.
1243    ///
1244    /// Acceptance (`try_pull_up_limit_one`):
1245    /// - inner: single SELECT, LIMIT 1 + ORDER BY <expr>, no GROUP BY /
1246    ///   HAVING / DISTINCT / CTE / UNION / OFFSET, single projection
1247    /// - inner FROM: may contain JOINs (INNER) over plain tables; no
1248    ///   LATERAL / UNNEST / generate_series / AS OF; no outer reference
1249    ///   inside join ON
1250    /// - WHERE: exactly one `inner.k = outer.col` (qualified columns)
1251    ///   + non-correlated all-inner predicates
1252    /// - projection: scalar expression, no aggregates / windows
1253    /// - outer: SelectStatement with FROM, no wildcards
1254    ///
1255    /// Returns true when at least one ScalarSubquery was rewritten.
1256    /// Returns false (no-op) when nothing in the statement matches —
1257    /// the existing per-row resolver then handles whatever's left.
1258    pub(crate) fn pull_up_correlated_limit_one_subqueries(
1259        &self,
1260        stmt: &mut SelectStatement,
1261    ) -> bool {
1262        // Phase 5 differential knob: an `AtomicBool` switch will land
1263        // alongside the byte-equal differential e2e (no_std rules out
1264        // std::env::var here). Production keeps the pass default-on.
1265        //
1266        // Outer FROM required (no FROM → nothing to JOIN against);
1267        // outer wildcards (`SELECT *`) widen the projection and would
1268        // surface the joined CTE's columns — refuse for safety.
1269        if stmt.from.is_none() || stmt.items.iter().any(|i| matches!(i, SelectItem::Wildcard)) {
1270            return false;
1271        }
1272        // Aliases an outer-correlation column may qualify to. Same
1273        // collection rule as `pull_up_unique_correlated_agg_subqueries`.
1274        let outer_aliases: alloc::collections::BTreeSet<String> = {
1275            let from = stmt.from.as_ref().expect("from present");
1276            let mut s = alloc::collections::BTreeSet::new();
1277            let push = |s: &mut alloc::collections::BTreeSet<String>, t: &TableRef| {
1278                s.insert(
1279                    t.alias
1280                        .clone()
1281                        .unwrap_or_else(|| t.name.clone())
1282                        .to_ascii_lowercase(),
1283                );
1284            };
1285            push(&mut s, &from.primary);
1286            for j in &from.joins {
1287                push(&mut s, &j.table);
1288            }
1289            s
1290        };
1291        let outer_has_group_by = stmt.group_by.is_some() || stmt.group_by_all;
1292        let mut new_ctes: Vec<Cte> = Vec::new();
1293        let mut new_joins: Vec<FromJoin> = Vec::new();
1294        let cte_seed = stmt.ctes.len();
1295        for item in &mut stmt.items {
1296            if let SelectItem::Expr { expr, .. } = item {
1297                self.pull_up_walk_limit_one(
1298                    expr,
1299                    false,
1300                    &outer_aliases,
1301                    outer_has_group_by,
1302                    cte_seed,
1303                    &mut new_ctes,
1304                    &mut new_joins,
1305                );
1306            }
1307        }
1308        if new_ctes.is_empty() {
1309            return false;
1310        }
1311        PULLUP_LIMIT1_FIRE_COUNT
1312            .fetch_add(new_ctes.len() as u64, core::sync::atomic::Ordering::Relaxed);
1313        stmt.ctes.extend(new_ctes);
1314        stmt.from
1315            .as_mut()
1316            .expect("from present")
1317            .joins
1318            .extend(new_joins);
1319        true
1320    }
1321
1322    /// v7.37.4 — recursive mutable walk over a select-list expression
1323    /// for the LIMIT 1 pullup. Tracks `in_agg` so a ScalarSubquery
1324    /// already inside an aggregate doesn't get a redundant MAX wrapper
1325    /// (the outer aggregate folds whatever cell value the join supplies).
1326    #[allow(clippy::too_many_arguments)]
1327    fn pull_up_walk_limit_one(
1328        &self,
1329        e: &mut Expr,
1330        in_agg: bool,
1331        outer_aliases: &alloc::collections::BTreeSet<String>,
1332        outer_has_group_by: bool,
1333        cte_seed: usize,
1334        ctes_out: &mut Vec<Cte>,
1335        joins_out: &mut Vec<FromJoin>,
1336    ) {
1337        match e {
1338            Expr::ScalarSubquery(inner) => {
1339                if let Some((cte, join, cte_col)) =
1340                    self.try_pull_up_limit_one(inner, outer_aliases, cte_seed + ctes_out.len())
1341                {
1342                    ctes_out.push(cte);
1343                    joins_out.push(join);
1344                    // Outer needs a single scalar per outer row. With a
1345                    // LEFT JOIN against the CTE (sq.jk UNIQUE by GROUP
1346                    // BY), sq.pj is functionally a single value per
1347                    // join key — but a strict GROUP BY checker won't
1348                    // know that. When the outer query has its own
1349                    // GROUP BY and this position isn't already wrapped
1350                    // in an aggregate, wrap in MAX(sq.pj) so the
1351                    // checker sees an aggregate; MAX over a single
1352                    // value equals the value (any aggregate would).
1353                    let col_expr = Expr::Column(cte_col);
1354                    *e = if outer_has_group_by && !in_agg {
1355                        Expr::FunctionCall {
1356                            name: "max".into(),
1357                            args: alloc::vec![col_expr],
1358                        }
1359                    } else {
1360                        col_expr
1361                    };
1362                }
1363                // Otherwise leave for the existing per-row resolver.
1364                // The subquery body is a separate scope — don't descend.
1365            }
1366            Expr::FunctionCall { name, args } => {
1367                let child = in_agg || aggregate::is_aggregate_name(name);
1368                for a in args.iter_mut() {
1369                    self.pull_up_walk_limit_one(
1370                        a,
1371                        child,
1372                        outer_aliases,
1373                        outer_has_group_by,
1374                        cte_seed,
1375                        ctes_out,
1376                        joins_out,
1377                    );
1378                }
1379            }
1380            Expr::AggregateOrdered {
1381                call,
1382                order_by,
1383                filter,
1384                ..
1385            } => {
1386                self.pull_up_walk_limit_one(
1387                    call,
1388                    true,
1389                    outer_aliases,
1390                    outer_has_group_by,
1391                    cte_seed,
1392                    ctes_out,
1393                    joins_out,
1394                );
1395                for o in order_by.iter_mut() {
1396                    self.pull_up_walk_limit_one(
1397                        &mut o.expr,
1398                        true,
1399                        outer_aliases,
1400                        outer_has_group_by,
1401                        cte_seed,
1402                        ctes_out,
1403                        joins_out,
1404                    );
1405                }
1406                if let Some(f) = filter {
1407                    self.pull_up_walk_limit_one(
1408                        f,
1409                        true,
1410                        outer_aliases,
1411                        outer_has_group_by,
1412                        cte_seed,
1413                        ctes_out,
1414                        joins_out,
1415                    );
1416                }
1417            }
1418            Expr::Binary { lhs, rhs, .. } => {
1419                self.pull_up_walk_limit_one(
1420                    lhs,
1421                    in_agg,
1422                    outer_aliases,
1423                    outer_has_group_by,
1424                    cte_seed,
1425                    ctes_out,
1426                    joins_out,
1427                );
1428                self.pull_up_walk_limit_one(
1429                    rhs,
1430                    in_agg,
1431                    outer_aliases,
1432                    outer_has_group_by,
1433                    cte_seed,
1434                    ctes_out,
1435                    joins_out,
1436                );
1437            }
1438            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
1439                self.pull_up_walk_limit_one(
1440                    expr,
1441                    in_agg,
1442                    outer_aliases,
1443                    outer_has_group_by,
1444                    cte_seed,
1445                    ctes_out,
1446                    joins_out,
1447                );
1448            }
1449            Expr::Like { expr, pattern, .. } => {
1450                self.pull_up_walk_limit_one(
1451                    expr,
1452                    in_agg,
1453                    outer_aliases,
1454                    outer_has_group_by,
1455                    cte_seed,
1456                    ctes_out,
1457                    joins_out,
1458                );
1459                self.pull_up_walk_limit_one(
1460                    pattern,
1461                    in_agg,
1462                    outer_aliases,
1463                    outer_has_group_by,
1464                    cte_seed,
1465                    ctes_out,
1466                    joins_out,
1467                );
1468            }
1469            Expr::InList { expr, list, .. } => {
1470                self.pull_up_walk_limit_one(
1471                    expr,
1472                    in_agg,
1473                    outer_aliases,
1474                    outer_has_group_by,
1475                    cte_seed,
1476                    ctes_out,
1477                    joins_out,
1478                );
1479                for it in list.iter_mut() {
1480                    self.pull_up_walk_limit_one(
1481                        it,
1482                        in_agg,
1483                        outer_aliases,
1484                        outer_has_group_by,
1485                        cte_seed,
1486                        ctes_out,
1487                        joins_out,
1488                    );
1489                }
1490            }
1491            Expr::Case {
1492                operand,
1493                branches,
1494                else_branch,
1495            } => {
1496                if let Some(o) = operand {
1497                    self.pull_up_walk_limit_one(
1498                        o,
1499                        in_agg,
1500                        outer_aliases,
1501                        outer_has_group_by,
1502                        cte_seed,
1503                        ctes_out,
1504                        joins_out,
1505                    );
1506                }
1507                for (w, t) in branches.iter_mut() {
1508                    self.pull_up_walk_limit_one(
1509                        w,
1510                        in_agg,
1511                        outer_aliases,
1512                        outer_has_group_by,
1513                        cte_seed,
1514                        ctes_out,
1515                        joins_out,
1516                    );
1517                    self.pull_up_walk_limit_one(
1518                        t,
1519                        in_agg,
1520                        outer_aliases,
1521                        outer_has_group_by,
1522                        cte_seed,
1523                        ctes_out,
1524                        joins_out,
1525                    );
1526                }
1527                if let Some(eb) = else_branch {
1528                    self.pull_up_walk_limit_one(
1529                        eb,
1530                        in_agg,
1531                        outer_aliases,
1532                        outer_has_group_by,
1533                        cte_seed,
1534                        ctes_out,
1535                        joins_out,
1536                    );
1537                }
1538            }
1539            // Same boundary policy as `pull_up_walk` — don't descend
1540            // into window calls, EXISTS, etc.
1541            _ => {}
1542        }
1543    }
1544
1545    /// v7.37.4 — decide whether a correlated scalar subquery qualifies
1546    /// for the LIMIT 1 → CTE pullup. Returns the CTE to add to outer
1547    /// `WITH`, the LEFT JOIN to append, and the (qualified) column
1548    /// that replaces the subquery node. None means: leave it for the
1549    /// per-row resolver.
1550    fn try_pull_up_limit_one(
1551        &self,
1552        inner: &SelectStatement,
1553        outer_aliases: &alloc::collections::BTreeSet<String>,
1554        alias_n: usize,
1555    ) -> Option<(Cte, FromJoin, ColumnName)> {
1556        // v7.37.4 A phase-2 finding (2026-06-19): the CTE rewrite
1557        // fires correctly on the mailrs prod subq 3 shape (verified
1558        // via PULLUP_LIMIT1_FIRE_COUNT in `pullup_fires_on_mailrs_subq3_shape`)
1559        // but PRODUCES A REGRESSION on the full prod SQL — mini cold
1560        // 100k SPGE 388.5 → 523.8 ms (+35%). Root cause:
1561        //   1. SPG's existing `try_batch_correlated_scalar` already
1562        //      handles the LIMIT 1 + ORDER BY 1 shape via post-LIMIT
1563        //      defer + keyed index seek (~ µs per surfaced outer key).
1564        //   2. The CTE form forces a full inner-table GROUP BY scan
1565        //      (~ 100 ms for 100k messages), then exec_with_ctes
1566        //      strips ctes + re-enters the body — extra catalog
1567        //      clone + double scan.
1568        //   3. Outer LIMIT 50 + GROUP BY thread_id means only ~50
1569        //      outer keys ultimately matter; CTE pre-aggregates ALL
1570        //      keys eagerly, wasting work for the unsurfaced 99 %.
1571        //
1572        // The CTE rewrite is right shape FOR the wrong root cause.
1573        // Real ceiling-first target is to make the existing batch
1574        // resolver's keyed-restriction path fire for the mailrs
1575        // GROUP BY + LIMIT shape, not to bypass it with a CTE.
1576        //
1577        // Keep the implementation dormant — the walker + gate
1578        // analysis stays as reference; turning this back on requires
1579        // a cost gate that proves CTE materialise + LEFT JOIN beats
1580        // the batch resolver for the SHAPE AT HAND (rare in practice).
1581        return None;
1582        #[allow(unreachable_code)]
1583        // Inner shape gates.
1584        if !inner.ctes.is_empty()
1585            || !inner.unions.is_empty()
1586            || inner.group_by.is_some()
1587            || inner.group_by_all
1588            || inner.having.is_some()
1589            || inner.distinct
1590            || inner.offset.is_some()
1591            || inner.items.len() != 1
1592            || inner.order_by.is_empty()
1593        {
1594            return None;
1595        }
1596        // LIMIT must be the literal 1 (placeholders bind late; we
1597        // can't guarantee the value here).
1598        match inner.limit {
1599            Some(LimitExpr::Literal(1)) => {}
1600            _ => return None,
1601        }
1602        let from = inner.from.as_ref()?;
1603        // Phase 2: single plain-table inner. Phase 3 lifts this gate
1604        // to allow inner INNER JOINs whose ON clauses are all-inner.
1605        if !from.joins.is_empty()
1606            || from.primary.lateral_subquery.is_some()
1607            || from.primary.unnest_expr.is_some()
1608            || from.primary.generate_series_args.is_some()
1609            || from.primary.as_of_segment.is_some()
1610        {
1611            return None;
1612        }
1613        let inner_table = from.primary.name.clone();
1614        let inner_alias = from
1615            .primary
1616            .alias
1617            .clone()
1618            .unwrap_or_else(|| inner_table.clone());
1619        let is_inner = |c: &ColumnName| -> bool {
1620            c.qualifier
1621                .as_deref()
1622                .is_some_and(|q| q.eq_ignore_ascii_case(&inner_alias))
1623        };
1624        let is_outer = |c: &ColumnName| -> bool {
1625            c.qualifier
1626                .as_deref()
1627                .is_some_and(|q| outer_aliases.contains(&q.to_ascii_lowercase()))
1628        };
1629        // Projection: scalar expression; reject aggregates / windows /
1630        // nested subqueries / outer references (the pulled-up SELECT
1631        // is uncorrelated GROUP BY — an outer column reference would
1632        // dangle).
1633        let SelectItem::Expr {
1634            expr: proj_expr,
1635            alias: _,
1636        } = &inner.items[0]
1637        else {
1638            return None;
1639        };
1640        if proj_has_disqualifying_shape(proj_expr, &inner_alias, outer_aliases) {
1641            return None;
1642        }
1643        // WHERE: exactly one `inner.k = outer.col`, plus all-inner
1644        // residual predicates.
1645        let where_ = inner.where_.as_ref()?;
1646        let mut corr: Option<(String, ColumnName)> = None;
1647        let mut non_corr: Vec<Expr> = Vec::new();
1648        for c in reorder::split_and_conjunctions(where_) {
1649            if let Expr::Binary {
1650                lhs,
1651                op: BinOp::Eq,
1652                rhs,
1653            } = c
1654                && let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref())
1655            {
1656                let pair = if is_inner(a) && is_outer(b) {
1657                    Some((a.name.clone(), b.clone()))
1658                } else if is_inner(b) && is_outer(a) {
1659                    Some((b.name.clone(), a.clone()))
1660                } else {
1661                    None
1662                };
1663                if let Some(p) = pair {
1664                    if corr.is_some() {
1665                        return None; // more than one correlation key
1666                    }
1667                    corr = Some(p);
1668                    continue;
1669                }
1670            }
1671            if !expr_is_all_inner(c, &inner_alias) {
1672                return None;
1673            }
1674            non_corr.push(c.clone());
1675        }
1676        let (inner_key, outer_col) = corr?;
1677        // ORDER BY: every key must be all-inner. Outer-referencing
1678        // sort keys would dangle after pullup.
1679        for ob in &inner.order_by {
1680            if !expr_is_all_inner(&ob.expr, &inner_alias) {
1681                return None;
1682            }
1683        }
1684        // Proj must also be all-inner (uncorrelated CTE body).
1685        if !expr_is_all_inner(proj_expr, &inner_alias) {
1686            return None;
1687        }
1688        // Build the CTE body:
1689        //   SELECT <inner.k> AS jk,
1690        //          (array_agg(<proj> ORDER BY <sort_keys>))[1] AS pj
1691        //     FROM <inner.from> WHERE <non_corr_AND_chain>
1692        //    GROUP BY <inner.k>
1693        let cte_name = alloc::format!("__cl1_{alias_n}");
1694        let jk_expr = Expr::Column(ColumnName {
1695            qualifier: Some(inner_alias.clone()),
1696            name: inner_key.clone(),
1697        });
1698        let argmax = Expr::ArraySubscript {
1699            target: alloc::boxed::Box::new(Expr::AggregateOrdered {
1700                call: alloc::boxed::Box::new(Expr::FunctionCall {
1701                    name: "array_agg".into(),
1702                    args: alloc::vec![proj_expr.clone()],
1703                }),
1704                order_by: inner.order_by.clone(),
1705                distinct: false,
1706                filter: None,
1707            }),
1708            index: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(1))),
1709        };
1710        let body_where = if non_corr.is_empty() {
1711            None
1712        } else {
1713            let mut iter = non_corr.into_iter();
1714            let head = iter.next().expect("non_corr nonempty in this branch");
1715            Some(iter.fold(head, |acc, p| Expr::Binary {
1716                lhs: alloc::boxed::Box::new(acc),
1717                op: BinOp::And,
1718                rhs: alloc::boxed::Box::new(p),
1719            }))
1720        };
1721        let body = SelectStatement {
1722            ctes: Vec::new(),
1723            distinct: false,
1724            items: alloc::vec![
1725                SelectItem::Expr {
1726                    expr: jk_expr.clone(),
1727                    alias: Some("jk".into()),
1728                },
1729                SelectItem::Expr {
1730                    expr: argmax,
1731                    alias: Some("pj".into()),
1732                },
1733            ],
1734            from: Some(from.clone()),
1735            where_: body_where,
1736            group_by: Some(alloc::vec![jk_expr]),
1737            group_by_all: false,
1738            having: None,
1739            unions: Vec::new(),
1740            order_by: Vec::new(),
1741            limit: None,
1742            offset: None,
1743            limit_with_ties: false,
1744        };
1745        let cte = Cte {
1746            name: cte_name.clone(),
1747            body: spg_sql::ast::CteBody::Select(body),
1748            recursive: false,
1749            column_overrides: Vec::new(),
1750        };
1751        // LEFT JOIN __cl1_N ON __cl1_N.jk = <outer_col>
1752        let join = FromJoin {
1753            kind: JoinKind::Left,
1754            table: TableRef {
1755                name: cte_name.clone(),
1756                alias: None,
1757                as_of_segment: None,
1758                unnest_expr: None,
1759                unnest_column_aliases: Vec::new(),
1760                generate_series_args: None,
1761                lateral_subquery: None,
1762                jsonb_each_text_arg: None,
1763            },
1764            on: Some(Expr::Binary {
1765                lhs: alloc::boxed::Box::new(Expr::Column(ColumnName {
1766                    qualifier: Some(cte_name.clone()),
1767                    name: "jk".into(),
1768                })),
1769                op: BinOp::Eq,
1770                rhs: alloc::boxed::Box::new(Expr::Column(outer_col)),
1771            }),
1772        };
1773        let repl = ColumnName {
1774            qualifier: Some(cte_name),
1775            name: "pj".into(),
1776        };
1777        Some((cte, join, repl))
1778    }
1779
1780    pub(crate) fn pull_up_unique_correlated_agg_subqueries(
1781        &self,
1782        stmt: &mut SelectStatement,
1783    ) -> bool {
1784        if stmt.from.is_none() || stmt.items.iter().any(|i| matches!(i, SelectItem::Wildcard)) {
1785            return false;
1786        }
1787        // Aliases an outer-correlation column may qualify to.
1788        let outer_aliases: alloc::collections::BTreeSet<String> = {
1789            let from = stmt.from.as_ref().expect("from present");
1790            let mut s = alloc::collections::BTreeSet::new();
1791            let push = |s: &mut alloc::collections::BTreeSet<String>, t: &TableRef| {
1792                s.insert(
1793                    t.alias
1794                        .clone()
1795                        .unwrap_or_else(|| t.name.clone())
1796                        .to_ascii_lowercase(),
1797                );
1798            };
1799            push(&mut s, &from.primary);
1800            for j in &from.joins {
1801                push(&mut s, &j.table);
1802            }
1803            s
1804        };
1805        let mut new_joins: Vec<FromJoin> = Vec::new();
1806        for item in &mut stmt.items {
1807            if let SelectItem::Expr { expr, .. } = item {
1808                self.pull_up_walk(expr, false, &outer_aliases, &mut new_joins);
1809            }
1810        }
1811        if new_joins.is_empty() {
1812            return false;
1813        }
1814        stmt.from
1815            .as_mut()
1816            .expect("from present")
1817            .joins
1818            .extend(new_joins);
1819        true
1820    }
1821
1822    /// Recursive mutable walk over an expression tracking whether we are
1823    /// inside an aggregate argument. A correlated scalar subquery found in
1824    /// aggregate context that `try_pull_up_join` accepts is replaced in
1825    /// place by the joined column; the join is queued in `joins_out`.
1826    fn pull_up_walk(
1827        &self,
1828        e: &mut Expr,
1829        in_agg: bool,
1830        outer_aliases: &alloc::collections::BTreeSet<String>,
1831        joins_out: &mut Vec<FromJoin>,
1832    ) {
1833        match e {
1834            Expr::ScalarSubquery(inner) => {
1835                if in_agg
1836                    && let Some((join, col)) =
1837                        self.try_pull_up_join(inner, outer_aliases, joins_out.len())
1838                {
1839                    joins_out.push(join);
1840                    *e = Expr::Column(col);
1841                }
1842                // Otherwise leave for the existing resolver; the subquery
1843                // body is a separate scope, so don't descend into it.
1844            }
1845            Expr::FunctionCall { name, args } => {
1846                let child = in_agg || aggregate::is_aggregate_name(name);
1847                for a in args.iter_mut() {
1848                    self.pull_up_walk(a, child, outer_aliases, joins_out);
1849                }
1850            }
1851            Expr::AggregateOrdered {
1852                call,
1853                order_by,
1854                filter,
1855                ..
1856            } => {
1857                self.pull_up_walk(call, true, outer_aliases, joins_out);
1858                for o in order_by.iter_mut() {
1859                    self.pull_up_walk(&mut o.expr, true, outer_aliases, joins_out);
1860                }
1861                if let Some(f) = filter {
1862                    self.pull_up_walk(f, true, outer_aliases, joins_out);
1863                }
1864            }
1865            Expr::Binary { lhs, rhs, .. } => {
1866                self.pull_up_walk(lhs, in_agg, outer_aliases, joins_out);
1867                self.pull_up_walk(rhs, in_agg, outer_aliases, joins_out);
1868            }
1869            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
1870                self.pull_up_walk(expr, in_agg, outer_aliases, joins_out);
1871            }
1872            Expr::Like { expr, pattern, .. } => {
1873                self.pull_up_walk(expr, in_agg, outer_aliases, joins_out);
1874                self.pull_up_walk(pattern, in_agg, outer_aliases, joins_out);
1875            }
1876            Expr::InList { expr, list, .. } => {
1877                self.pull_up_walk(expr, in_agg, outer_aliases, joins_out);
1878                for it in list.iter_mut() {
1879                    self.pull_up_walk(it, in_agg, outer_aliases, joins_out);
1880                }
1881            }
1882            Expr::Case {
1883                operand,
1884                branches,
1885                else_branch,
1886            } => {
1887                if let Some(o) = operand {
1888                    self.pull_up_walk(o, in_agg, outer_aliases, joins_out);
1889                }
1890                for (w, t) in branches.iter_mut() {
1891                    self.pull_up_walk(w, in_agg, outer_aliases, joins_out);
1892                    self.pull_up_walk(t, in_agg, outer_aliases, joins_out);
1893                }
1894                if let Some(eb) = else_branch {
1895                    self.pull_up_walk(eb, in_agg, outer_aliases, joins_out);
1896                }
1897            }
1898            // Window functions, EXISTS / IN subqueries, and other variants
1899            // are intentionally not descended for this rewrite — the
1900            // common aggregate-arg shapes above cover the reported load and
1901            // anything missed simply keeps its existing evaluation.
1902            _ => {}
1903        }
1904    }
1905
1906    /// Decide whether a correlated scalar subquery qualifies for the
1907    /// unique-key LEFT JOIN pull-up. Returns the join to append and the
1908    /// column that replaces the subquery node, or None to leave it alone.
1909    fn try_pull_up_join(
1910        &self,
1911        inner: &SelectStatement,
1912        outer_aliases: &alloc::collections::BTreeSet<String>,
1913        alias_n: usize,
1914    ) -> Option<(FromJoin, ColumnName)> {
1915        // Inner must be a single plain-table scan with one projected
1916        // column and none of the shape-breaking clauses.
1917        if !inner.ctes.is_empty()
1918            || !inner.unions.is_empty()
1919            || inner.group_by.is_some()
1920            || inner.having.is_some()
1921            || inner.distinct
1922            || !inner.order_by.is_empty()
1923            || inner.limit.is_some()
1924            || inner.offset.is_some()
1925            || inner.items.len() != 1
1926        {
1927            return None;
1928        }
1929        let from = inner.from.as_ref()?;
1930        if !from.joins.is_empty()
1931            || from.primary.lateral_subquery.is_some()
1932            || from.primary.unnest_expr.is_some()
1933            || from.primary.generate_series_args.is_some()
1934            || from.primary.as_of_segment.is_some()
1935        {
1936            return None;
1937        }
1938        let inner_table = from.primary.name.clone();
1939        let inner_alias = from
1940            .primary
1941            .alias
1942            .clone()
1943            .unwrap_or_else(|| inner_table.clone());
1944        let is_inner = |c: &ColumnName| -> bool {
1945            c.qualifier
1946                .as_deref()
1947                .is_some_and(|q| q.eq_ignore_ascii_case(&inner_alias))
1948        };
1949        let is_outer = |c: &ColumnName| -> bool {
1950            c.qualifier
1951                .as_deref()
1952                .is_some_and(|q| outer_aliases.contains(&q.to_ascii_lowercase()))
1953        };
1954        // Projected column: a single inner-qualified column.
1955        let SelectItem::Expr { expr: out_expr, .. } = &inner.items[0] else {
1956            return None;
1957        };
1958        let Expr::Column(out_col) = out_expr else {
1959            return None;
1960        };
1961        if !is_inner(out_col) {
1962            return None;
1963        }
1964        // WHERE: exactly one `inner.key = outer.col`, rest all-inner.
1965        let w = inner.where_.as_ref()?;
1966        let mut corr: Option<(String, ColumnName)> = None;
1967        let mut rest: Vec<Expr> = Vec::new();
1968        for c in reorder::split_and_conjunctions(w) {
1969            if let Expr::Binary {
1970                lhs,
1971                op: BinOp::Eq,
1972                rhs,
1973            } = c
1974                && let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref())
1975            {
1976                let pair = if is_inner(a) && is_outer(b) {
1977                    Some((a.name.clone(), b.clone()))
1978                } else if is_inner(b) && is_outer(a) {
1979                    Some((b.name.clone(), a.clone()))
1980                } else {
1981                    None
1982                };
1983                if let Some(p) = pair {
1984                    if corr.is_some() {
1985                        return None; // more than one correlation
1986                    }
1987                    corr = Some(p);
1988                    continue;
1989                }
1990            }
1991            if !expr_is_all_inner(c, &inner_alias) {
1992                return None;
1993            }
1994            rest.push(c.clone());
1995        }
1996        let (inner_key, outer_col) = corr?;
1997        // Safety gate: the correlation key must be UNIQUE / PRIMARY KEY on
1998        // the inner table so the join can't multiply outer rows.
1999        if !self.column_is_single_unique(&inner_table, &inner_key) {
2000            return None;
2001        }
2002        // Build the LEFT JOIN against a fresh alias.
2003        let fresh = alloc::format!("__plj_{alias_n}");
2004        let key_eq = Expr::Binary {
2005            lhs: alloc::boxed::Box::new(Expr::Column(ColumnName {
2006                qualifier: Some(fresh.clone()),
2007                name: inner_key,
2008            })),
2009            op: BinOp::Eq,
2010            rhs: alloc::boxed::Box::new(Expr::Column(outer_col)),
2011        };
2012        let on = rest
2013            .into_iter()
2014            .map(|mut e| {
2015                rename_qualifier(&mut e, &inner_alias, &fresh);
2016                e
2017            })
2018            .fold(key_eq, |acc, pred| Expr::Binary {
2019                lhs: alloc::boxed::Box::new(acc),
2020                op: BinOp::And,
2021                rhs: alloc::boxed::Box::new(pred),
2022            });
2023        let join = FromJoin {
2024            kind: JoinKind::Left,
2025            table: TableRef {
2026                name: inner_table,
2027                alias: Some(fresh.clone()),
2028                as_of_segment: None,
2029                unnest_expr: None,
2030                unnest_column_aliases: Vec::new(),
2031                generate_series_args: None,
2032                lateral_subquery: None,
2033                jsonb_each_text_arg: None,
2034            },
2035            on: Some(on),
2036        };
2037        let repl = ColumnName {
2038            qualifier: Some(fresh),
2039            name: out_col.name.clone(),
2040        };
2041        Some((join, repl))
2042    }
2043
2044    /// v7.34.2 (mailrs prod NOT EXISTS hot-path) — plan-time EXISTS /
2045    /// NOT EXISTS sublink pull-up to semi/anti-join. PostgreSQL's
2046    /// `convert_EXISTS_sublink_to_join`-flavoured rewrite: a correlated
2047    /// `[NOT] EXISTS (SELECT … FROM t WHERE t.k = outer.col [AND inner])`
2048    /// in the WHERE-AND spine collapses to a real JOIN against `t`. The
2049    /// per-row dispatch (clone host expr × 25 k + splice + eval) goes
2050    /// away entirely — the executor streams one tight join loop the
2051    /// same way it would for a hand-written JOIN.
2052    ///
2053    /// Shape rules:
2054    ///   * NOT EXISTS  → LEFT JOIN t AS __exsj_N ON t.k = outer.col [AND …]
2055    ///                   AND a survivor `__exsj_N.k IS NULL` conjunct
2056    ///                   stays in WHERE. Safe regardless of uniqueness:
2057    ///                   IS-NULL only fires on the LEFT-JOIN pad row,
2058    ///                   so duplicate inner matches collapse cleanly
2059    ///                   (any match drops the outer row; only no-match
2060    ///                   outer rows survive).
2061    ///   * EXISTS      → INNER JOIN. Safe only when inner.k is single-
2062    ///                   column UNIQUE / PRIMARY KEY (otherwise INNER
2063    ///                   would multiply outer rows). Gated by
2064    ///                   `column_is_single_unique`. No survivor needed
2065    ///                   in WHERE — the join itself encodes EXISTS=true.
2066    ///
2067    /// Eligible inner: single plain-table FROM, no nested JOIN / CTE /
2068    /// UNION / GROUP / HAVING / DISTINCT / ORDER / LIMIT / OFFSET, and
2069    /// WHERE = exactly one `inner.k = outer.col` correlation plus
2070    /// optional all-inner predicates that ride into the ON clause.
2071    /// Anything else is left for the per-row resolver.
2072    ///
2073    /// Returns true when at least one conjunct was pulled up.
2074    pub(crate) fn pull_up_exists_sublinks(&self, stmt: &mut SelectStatement) -> bool {
2075        if stmt.from.is_none() {
2076            return false;
2077        }
2078        let Some(where_expr) = stmt.where_.take() else {
2079            return false;
2080        };
2081        // v7.37.4 A'' — pre-disambiguate outer unqualified column refs
2082        // whose name would collide with a future pulled-up inner
2083        // table's columns. mailrs `/api/conversations` uses bare
2084        // `thread_id != ''` in outer WHERE; once we add
2085        // `__exsj_0 LEFT JOIN snoozed_conversations` (also with a
2086        // `thread_id` column), the resolver raises "ambiguous column".
2087        // Conservative: scan EXISTS / NOT EXISTS subqueries in the
2088        // WHERE we just took out, look up each inner plain-table's
2089        // column set, and for every collision column that exists in
2090        // exactly one outer table, pre-qualify it to that owning alias.
2091        let mut collision_names: alloc::collections::BTreeSet<String> =
2092            alloc::collections::BTreeSet::new();
2093        for c in reorder::split_and_conjunctions(&where_expr) {
2094            let inner_subq: Option<&SelectStatement> = match c {
2095                Expr::Exists { subquery, .. } => Some(subquery.as_ref()),
2096                Expr::Unary {
2097                    op: UnOp::Not,
2098                    expr,
2099                } => match expr.as_ref() {
2100                    Expr::Exists { subquery, .. } => Some(subquery.as_ref()),
2101                    _ => None,
2102                },
2103                _ => None,
2104            };
2105            let Some(inner) = inner_subq else { continue };
2106            let Some(from) = &inner.from else { continue };
2107            if !from.joins.is_empty() {
2108                continue;
2109            }
2110            let Some(t) = self.active_catalog().get(&from.primary.name) else {
2111                continue;
2112            };
2113            for col in &t.schema().columns {
2114                collision_names.insert(col.name.to_ascii_lowercase());
2115            }
2116        }
2117        let mut where_expr = where_expr;
2118        if !collision_names.is_empty() {
2119            let from = stmt.from.as_ref().expect("from present");
2120            let outer_tables: Vec<(String, String)> = {
2121                let mut v = Vec::new();
2122                let collect = |v: &mut Vec<(String, String)>, t: &TableRef| {
2123                    let alias = t.alias.clone().unwrap_or_else(|| t.name.clone());
2124                    v.push((alias, t.name.clone()));
2125                };
2126                collect(&mut v, &from.primary);
2127                for j in &from.joins {
2128                    collect(&mut v, &j.table);
2129                }
2130                v
2131            };
2132            let mut owner: alloc::collections::BTreeMap<String, String> =
2133                alloc::collections::BTreeMap::new();
2134            for col_lc in &collision_names {
2135                let mut matches: Vec<String> = Vec::new();
2136                for (alias, tname) in &outer_tables {
2137                    let Some(t) = self.active_catalog().get(tname) else {
2138                        continue;
2139                    };
2140                    if t.schema()
2141                        .columns
2142                        .iter()
2143                        .any(|c| c.name.eq_ignore_ascii_case(col_lc))
2144                    {
2145                        matches.push(alias.clone());
2146                    }
2147                }
2148                if matches.len() == 1 {
2149                    owner.insert(col_lc.clone(), matches.remove(0));
2150                }
2151            }
2152            if !owner.is_empty() {
2153                disambiguate_stmt_unqualified_columns(stmt, &owner);
2154                disambiguate_expr_unqualified_columns(&mut where_expr, &owner);
2155            }
2156        }
2157        let outer_aliases: alloc::collections::BTreeSet<String> = {
2158            let from = stmt.from.as_ref().expect("from present");
2159            let mut s = alloc::collections::BTreeSet::new();
2160            let push = |s: &mut alloc::collections::BTreeSet<String>, t: &TableRef| {
2161                s.insert(
2162                    t.alias
2163                        .clone()
2164                        .unwrap_or_else(|| t.name.clone())
2165                        .to_ascii_lowercase(),
2166                );
2167            };
2168            push(&mut s, &from.primary);
2169            for j in &from.joins {
2170                push(&mut s, &j.table);
2171            }
2172            s
2173        };
2174        let conjuncts = reorder::split_and_conjunctions(&where_expr);
2175        let mut survivors: Vec<Expr> = Vec::new();
2176        let mut new_joins: Vec<FromJoin> = Vec::new();
2177        let mut rewrote_any = false;
2178        for c in conjuncts {
2179            // v7.34.3 — the parser emits `NOT EXISTS(...)` as
2180            // `Expr::Unary{Not, Exists{negated:false, …}}`, NOT as
2181            // `Exists{negated:true}`. Match both shapes so the
2182            // pull-up handles both `EXISTS` and `NOT EXISTS`.
2183            let parsed: Option<(&SelectStatement, bool)> = match c {
2184                Expr::Exists { subquery, negated } => Some((subquery.as_ref(), *negated)),
2185                Expr::Unary {
2186                    op: UnOp::Not,
2187                    expr,
2188                } => match expr.as_ref() {
2189                    Expr::Exists { subquery, negated } => Some((subquery.as_ref(), !*negated)),
2190                    _ => None,
2191                },
2192                _ => None,
2193            };
2194            if let Some((subquery, neg)) = parsed {
2195                // v7.34.2 first chose `[NOT] IN (SELECT k FROM t)` first
2196                // because the `mailrs_prod_not_exists` 250 k probe
2197                // dropped 178 ms (LEFT JOIN + IS NULL form) → 74 ms
2198                // (NOT IN form). But that win was from the OUTER ORDER
2199                // BY id DESC LIMIT N walker fast path
2200                // (`try_pk_walk_top_n`), which only the InList shape
2201                // exposes (early-stop on first N survivors). For
2202                // shapes WITHOUT an outer LIMIT (e.g. `SELECT
2203                // COUNT(*) FROM messages WHERE NOT EXISTS …`) the IN
2204                // form has to materialise the entire 12.5 k inner
2205                // value set as `Vec<Expr::Literal>` before HashSet
2206                // build — pure overhead that the LEFT ANTI JOIN
2207                // executor skips by hashing the inner table directly.
2208                // v7.37.x (docker-fair NOTEX) — branch on outer
2209                // LIMIT presence: with LIMIT, prefer InList (walker
2210                // benefit); without LIMIT, prefer LEFT ANTI JOIN
2211                // (streaming build, no Expr::Literal Vec roundtrip).
2212                let outer_has_limit = stmt.limit.is_some();
2213                let try_in_first = outer_has_limit;
2214                let mut consumed = false;
2215                if try_in_first
2216                    && let Some(rewritten) =
2217                        self.try_pull_up_exists_as_in(subquery, neg, &outer_aliases)
2218                {
2219                    survivors.push(rewritten);
2220                    consumed = true;
2221                }
2222                if !consumed
2223                    && let Some((join, residual)) = self.try_pull_up_exists_sublink(
2224                        subquery,
2225                        neg,
2226                        &outer_aliases,
2227                        new_joins.len(),
2228                    )
2229                {
2230                    new_joins.push(join);
2231                    if let Some(r) = residual {
2232                        survivors.push(r);
2233                    }
2234                    consumed = true;
2235                }
2236                if !consumed
2237                    && !try_in_first
2238                    && let Some(rewritten) =
2239                        self.try_pull_up_exists_as_in(subquery, neg, &outer_aliases)
2240                {
2241                    // Fallback when LEFT ANTI JOIN refused (e.g. inner
2242                    // shape too complex) — IN form is the next best.
2243                    survivors.push(rewritten);
2244                    consumed = true;
2245                }
2246                if consumed {
2247                    rewrote_any = true;
2248                    continue;
2249                }
2250            }
2251            survivors.push(c.clone());
2252        }
2253        if !rewrote_any {
2254            stmt.where_ = Some(where_expr);
2255            return false;
2256        }
2257        EXISTS_PULLUP_FIRE_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2258        if !new_joins.is_empty() {
2259            stmt.from
2260                .as_mut()
2261                .expect("from present")
2262                .joins
2263                .extend(new_joins);
2264        }
2265        stmt.where_ = survivors.into_iter().reduce(|a, b| Expr::Binary {
2266            lhs: alloc::boxed::Box::new(a),
2267            op: BinOp::And,
2268            rhs: alloc::boxed::Box::new(b),
2269        });
2270        true
2271    }
2272
2273    /// v7.34.3 — emit the EXISTS conjunct as `outer.col IN (SELECT
2274    /// inner.k FROM inner.table)` (or its negated form). Eligibility
2275    /// mirrors `try_pull_up_exists_sublink` — single plain-table FROM,
2276    /// no shape-breaking clauses, exactly one `inner.k = outer.col`
2277    /// correlation plus optional all-inner predicates — except no
2278    /// uniqueness check is needed (IN handles duplicate inner.k
2279    /// fine). For the NEGATED case we ALSO require inner.k to be
2280    /// declared NOT NULL: `outer.col NOT IN (set with NULL)` returns
2281    /// UNKNOWN for every outer row in SQL three-valued logic, which
2282    /// differs from NOT EXISTS semantics. None on ineligible →
2283    /// caller falls back to the LEFT JOIN + IS NULL injection or
2284    /// the legacy per-row resolver.
2285    fn try_pull_up_exists_as_in(
2286        &self,
2287        inner: &SelectStatement,
2288        negated: bool,
2289        outer_aliases: &alloc::collections::BTreeSet<String>,
2290    ) -> Option<Expr> {
2291        if !inner.ctes.is_empty()
2292            || !inner.unions.is_empty()
2293            || inner.group_by.is_some()
2294            || inner.having.is_some()
2295            || inner.distinct
2296            || !inner.order_by.is_empty()
2297            || inner.limit.is_some()
2298            || inner.offset.is_some()
2299        {
2300            return None;
2301        }
2302        let from = inner.from.as_ref()?;
2303        if !from.joins.is_empty()
2304            || from.primary.lateral_subquery.is_some()
2305            || from.primary.unnest_expr.is_some()
2306            || from.primary.generate_series_args.is_some()
2307            || from.primary.as_of_segment.is_some()
2308        {
2309            return None;
2310        }
2311        let inner_table = from.primary.name.clone();
2312        let inner_alias = from
2313            .primary
2314            .alias
2315            .clone()
2316            .unwrap_or_else(|| inner_table.clone());
2317        let is_inner = |c: &ColumnName| -> bool {
2318            c.qualifier
2319                .as_deref()
2320                .is_some_and(|q| q.eq_ignore_ascii_case(&inner_alias))
2321        };
2322        let is_outer = |c: &ColumnName| -> bool {
2323            c.qualifier
2324                .as_deref()
2325                .is_some_and(|q| outer_aliases.contains(&q.to_ascii_lowercase()))
2326        };
2327        let w = inner.where_.as_ref()?;
2328        let mut corr: Option<(String, ColumnName)> = None;
2329        let mut rest: Vec<Expr> = Vec::new();
2330        for c in reorder::split_and_conjunctions(w) {
2331            if let Expr::Binary {
2332                lhs,
2333                op: BinOp::Eq,
2334                rhs,
2335            } = c
2336                && let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref())
2337            {
2338                let pair = if is_inner(a) && is_outer(b) {
2339                    Some((a.name.clone(), b.clone()))
2340                } else if is_inner(b) && is_outer(a) {
2341                    Some((b.name.clone(), a.clone()))
2342                } else {
2343                    None
2344                };
2345                if let Some(p) = pair {
2346                    if corr.is_some() {
2347                        return None;
2348                    }
2349                    corr = Some(p);
2350                    continue;
2351                }
2352            }
2353            if !expr_is_all_inner(c, &inner_alias) {
2354                return None;
2355            }
2356            rest.push(c.clone());
2357        }
2358        let (inner_key, outer_col) = corr?;
2359        if negated && !self.column_is_not_null(&inner_table, &inner_key) {
2360            return None;
2361        }
2362        // Build the rewritten inner SELECT: `SELECT inner.k FROM
2363        // inner.table [WHERE rest]`. The correlation conjunct is
2364        // dropped — IN-subquery handles equality membership. All-inner
2365        // residual predicates ride into the new WHERE.
2366        let mut rewritten = inner.clone();
2367        rewritten.limit = None;
2368        rewritten.offset = None;
2369        rewritten.order_by = Vec::new();
2370        rewritten.distinct = false;
2371        rewritten.where_ = rest.into_iter().reduce(|a, b| Expr::Binary {
2372            lhs: alloc::boxed::Box::new(a),
2373            op: BinOp::And,
2374            rhs: alloc::boxed::Box::new(b),
2375        });
2376        rewritten.items = alloc::vec![SelectItem::Expr {
2377            expr: Expr::Column(ColumnName {
2378                qualifier: Some(inner_alias),
2379                name: inner_key,
2380            }),
2381            alias: None,
2382        }];
2383        Some(Expr::InSubquery {
2384            expr: alloc::boxed::Box::new(Expr::Column(outer_col)),
2385            subquery: alloc::boxed::Box::new(rewritten),
2386            negated,
2387        })
2388    }
2389
2390    fn try_pull_up_exists_sublink(
2391        &self,
2392        inner: &SelectStatement,
2393        negated: bool,
2394        outer_aliases: &alloc::collections::BTreeSet<String>,
2395        alias_n: usize,
2396    ) -> Option<(FromJoin, Option<Expr>)> {
2397        EXISTS_PULLUP_CANDIDATE_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2398        if !inner.ctes.is_empty()
2399            || !inner.unions.is_empty()
2400            || inner.group_by.is_some()
2401            || inner.having.is_some()
2402            || inner.distinct
2403            || !inner.order_by.is_empty()
2404            || inner.limit.is_some()
2405            || inner.offset.is_some()
2406        {
2407            EXISTS_PULLUP_BAIL_INNER_SHAPE.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2408            return None;
2409        }
2410        let from = inner.from.as_ref()?;
2411        if !from.joins.is_empty()
2412            || from.primary.lateral_subquery.is_some()
2413            || from.primary.unnest_expr.is_some()
2414            || from.primary.generate_series_args.is_some()
2415            || from.primary.as_of_segment.is_some()
2416        {
2417            EXISTS_PULLUP_BAIL_INNER_FROM.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2418            return None;
2419        }
2420        let inner_table = from.primary.name.clone();
2421        let inner_alias = from
2422            .primary
2423            .alias
2424            .clone()
2425            .unwrap_or_else(|| inner_table.clone());
2426        let is_inner = |c: &ColumnName| -> bool {
2427            c.qualifier
2428                .as_deref()
2429                .is_some_and(|q| q.eq_ignore_ascii_case(&inner_alias))
2430        };
2431        let is_outer = |c: &ColumnName| -> bool {
2432            c.qualifier
2433                .as_deref()
2434                .is_some_and(|q| outer_aliases.contains(&q.to_ascii_lowercase()))
2435        };
2436        let Some(w) = inner.where_.as_ref() else {
2437            EXISTS_PULLUP_BAIL_NO_WHERE.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2438            return None;
2439        };
2440        // v7.37.4 A'' (mailrs prod /api/conversations 2-col anti-join) —
2441        // accept multi-column correlation. Today's single-pair restriction
2442        // forced mailrs's
2443        //   NOT EXISTS (SELECT 1 FROM sc WHERE sc.thread_id = m.thread_id
2444        //                                  AND sc.account_address = mb.user_address
2445        //                                  AND sc.snoozed_until > 0)
2446        // to fall back to the batch `try_batch_correlated_exists` path,
2447        // which builds the inner set fine but then pays a per-row host-
2448        // expression clone + AST walk + eval to splice each EXISTS node
2449        // into a Bool literal (line 194-211 above). 100k join survivors ×
2450        // ~1.5 µs per splice = ~150 ms on the mini cold bench. Pulling
2451        // multi-col is the same shape SPG / PG / MySQL / MariaDB plan a
2452        // multi-key anti-join: LEFT JOIN sc ON (sc.thread_id = m.thread_id
2453        //   AND sc.account_address = mb.user_address [AND inner preds])
2454        // + WHERE sc.<first key> IS NULL. NULL semantics: a NULL on any
2455        // join key means no match, identical to NOT EXISTS three-valued
2456        // logic (the IS NULL probe matches the pad row).
2457        let mut corr_pairs: Vec<(String, ColumnName)> = Vec::new();
2458        let mut rest: Vec<Expr> = Vec::new();
2459        for c in reorder::split_and_conjunctions(w) {
2460            if let Expr::Binary {
2461                lhs,
2462                op: BinOp::Eq,
2463                rhs,
2464            } = c
2465                && let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref())
2466            {
2467                let pair = if is_inner(a) && is_outer(b) {
2468                    Some((a.name.clone(), b.clone()))
2469                } else if is_inner(b) && is_outer(a) {
2470                    Some((b.name.clone(), a.clone()))
2471                } else {
2472                    None
2473                };
2474                if let Some(p) = pair {
2475                    corr_pairs.push(p);
2476                    continue;
2477                }
2478            }
2479            if !expr_is_all_inner(c, &inner_alias) {
2480                EXISTS_PULLUP_BAIL_RESIDUAL_NOT_INNER
2481                    .fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2482                return None;
2483            }
2484            rest.push(c.clone());
2485        }
2486        if corr_pairs.is_empty() {
2487            EXISTS_PULLUP_BAIL_NO_CORR.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2488            return None;
2489        }
2490        // Differential knob — refuse the multi-col case under test so
2491        // the baseline path (batch resolver) runs and its result can
2492        // be compared against the pullup-on path. Single-col stays on.
2493        if corr_pairs.len() > 1
2494            && EXISTS_PULLUP_MULTICOL_DISABLE.load(core::sync::atomic::Ordering::Relaxed)
2495        {
2496            EXISTS_PULLUP_BAIL_MULTICOL_DISABLED
2497                .fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2498            return None;
2499        }
2500        // EXISTS (semi-join) requires uniqueness on EVERY inner key so
2501        // the INNER JOIN can't multiply outer rows when more than one
2502        // inner row matches the tuple. NOT EXISTS (anti-join) uses
2503        // LEFT + IS NULL and is safe regardless of inner key uniqueness:
2504        // duplicate inner matches collapse into "matched" for the
2505        // anti-join probe.
2506        if !negated {
2507            // For multi-col EXISTS today we conservatively require each
2508            // inner column to carry a single-column UNIQUE / PRIMARY KEY
2509            // — the join cardinality guarantee is per-column. A truer
2510            // composite-unique gate could relax this; the prod hot
2511            // path (mailrs) is negated so deferring is safe.
2512            for (k, _) in &corr_pairs {
2513                if !self.column_is_single_unique(&inner_table, k) {
2514                    EXISTS_PULLUP_BAIL_UNIQUE_KEY_MISSING
2515                        .fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2516                    return None;
2517                }
2518            }
2519        }
2520        let fresh = alloc::format!("__exsj_{alias_n}");
2521        // Build the ON conjunction: every (inner_key = outer_col) pair
2522        // joined by AND, then folded with the all-inner residual.
2523        let mut on_iter = corr_pairs.iter().map(|(ik, oc)| Expr::Binary {
2524            lhs: alloc::boxed::Box::new(Expr::Column(ColumnName {
2525                qualifier: Some(fresh.clone()),
2526                name: ik.clone(),
2527            })),
2528            op: BinOp::Eq,
2529            rhs: alloc::boxed::Box::new(Expr::Column(oc.clone())),
2530        });
2531        let first_key_eq = on_iter
2532            .next()
2533            .expect("corr_pairs non-empty post `is_empty()` gate");
2534        let on = rest
2535            .into_iter()
2536            .map(|mut e| {
2537                rename_qualifier(&mut e, &inner_alias, &fresh);
2538                e
2539            })
2540            .chain(on_iter)
2541            .fold(first_key_eq, |acc, pred| Expr::Binary {
2542                lhs: alloc::boxed::Box::new(acc),
2543                op: BinOp::And,
2544                rhs: alloc::boxed::Box::new(pred),
2545            });
2546        let join = FromJoin {
2547            kind: if negated {
2548                JoinKind::Left
2549            } else {
2550                JoinKind::Inner
2551            },
2552            table: TableRef {
2553                name: inner_table,
2554                alias: Some(fresh.clone()),
2555                as_of_segment: None,
2556                unnest_expr: None,
2557                unnest_column_aliases: Vec::new(),
2558                generate_series_args: None,
2559                lateral_subquery: None,
2560                jsonb_each_text_arg: None,
2561            },
2562            on: Some(on),
2563        };
2564        let residual = if negated {
2565            // anti-join: pick the FIRST inner key as the IS NULL probe.
2566            // Any IS NULL on a joined-side column is sufficient — the
2567            // LEFT-JOIN pad row sets ALL inner columns to NULL atomically,
2568            // so a single column witnesses "no match".
2569            let probe_key = corr_pairs[0].0.clone();
2570            Some(Expr::IsNull {
2571                expr: alloc::boxed::Box::new(Expr::Column(ColumnName {
2572                    qualifier: Some(fresh),
2573                    name: probe_key,
2574                })),
2575                negated: false,
2576            })
2577        } else {
2578            None
2579        };
2580        Some((join, residual))
2581    }
2582
2583    /// v7.34.3 — true when `col` on `table` is declared NOT NULL (the
2584    /// `ColumnSchema.nullable` flag is `false`). Used to gate the
2585    /// `NOT EXISTS → NOT IN` rewrite, since SQL three-valued logic
2586    /// turns `outer.col NOT IN (set with NULL)` into UNKNOWN for every
2587    /// outer row, which would differ from the NOT EXISTS semantics.
2588    fn column_is_not_null(&self, table: &str, col: &str) -> bool {
2589        let Some(t) = self.active_catalog().get(table) else {
2590            return false;
2591        };
2592        let sch = t.schema();
2593        // Direct flag — cheap path. Covers explicit NOT NULL columns
2594        // and table-level PK constraints (ddl.rs line 1252).
2595        if sch
2596            .columns
2597            .iter()
2598            .find(|c| c.name.eq_ignore_ascii_case(col))
2599            .is_some_and(|c| !c.nullable)
2600        {
2601            return true;
2602        }
2603        // v7.34.3 — inline `PRIMARY KEY` on a column definition
2604        // (e.g. `id BIGSERIAL PRIMARY KEY`) does NOT currently flip
2605        // `ColumnSchema.nullable` to false in ddl.rs (only the
2606        // table-level `CONSTRAINT … PRIMARY KEY (col)` shape does).
2607        // PK semantically implies NOT NULL, so cross-check the
2608        // installed uniqueness constraints' `is_primary_key` flag too.
2609        let Some(pos) = sch
2610            .columns
2611            .iter()
2612            .position(|c| c.name.eq_ignore_ascii_case(col))
2613        else {
2614            return false;
2615        };
2616        sch.uniqueness_constraints
2617            .iter()
2618            .any(|u| u.is_primary_key && u.columns.as_slice() == [pos])
2619    }
2620
2621    /// True when `col` on `table` is covered by a single-column UNIQUE or
2622    /// PRIMARY KEY constraint (declared and engine-enforced), or a unique
2623    /// index — i.e. an equality on it matches at most one row.
2624    fn column_is_single_unique(&self, table: &str, col: &str) -> bool {
2625        let Some(t) = self.active_catalog().get(table) else {
2626            return false;
2627        };
2628        let sch = t.schema();
2629        let Some(pos) = sch
2630            .columns
2631            .iter()
2632            .position(|c| c.name.eq_ignore_ascii_case(col))
2633        else {
2634            return false;
2635        };
2636        if sch
2637            .uniqueness_constraints
2638            .iter()
2639            .any(|u| u.columns.as_slice() == [pos])
2640        {
2641            return true;
2642        }
2643        t.index_on(pos).is_some_and(|idx| idx.is_unique)
2644    }
2645}
2646
2647// ---- subquery free-fn helpers (lib.rs split 6) ----
2648
2649/// v7.33 — true when every column in `e` is qualified to `inner_alias`
2650/// and `e` contains no nested subquery. Used by the sublink pull-up to
2651/// confirm a non-correlation conjunct is purely inner (safe to carry into
2652/// the join ON after a qualifier rename).
2653/// v7.37.4 — refuse projection expressions that would dangle after
2654/// the LIMIT 1 pullup: aggregates / window calls / EXISTS / scalar
2655/// subqueries / outer-qualified columns (the pulled-up CTE body is
2656/// uncorrelated, so an outer reference inside the projection has no
2657/// scope to bind against). All-inner column references are fine.
2658fn proj_has_disqualifying_shape(
2659    e: &Expr,
2660    inner_alias: &str,
2661    outer_aliases: &alloc::collections::BTreeSet<String>,
2662) -> bool {
2663    match e {
2664        Expr::AggregateOrdered { .. }
2665        | Expr::WindowFunction { .. }
2666        | Expr::ScalarSubquery(_)
2667        | Expr::Exists { .. } => true,
2668        Expr::FunctionCall { name, args } => {
2669            if aggregate::is_aggregate_name(name) {
2670                return true;
2671            }
2672            args.iter()
2673                .any(|a| proj_has_disqualifying_shape(a, inner_alias, outer_aliases))
2674        }
2675        Expr::Column(c) => {
2676            // Reject outer-qualified columns inside the projection
2677            // (they'd dangle in the uncorrelated CTE body). Unqualified
2678            // columns are ambiguous in a multi-table inner — for the
2679            // phase-2 single-table gate they resolve to `inner_alias`
2680            // anyway, accept them. Qualified inner refs are OK.
2681            if let Some(q) = c.qualifier.as_deref() {
2682                outer_aliases.contains(&q.to_ascii_lowercase())
2683                    && !q.eq_ignore_ascii_case(inner_alias)
2684            } else {
2685                false
2686            }
2687        }
2688        Expr::Binary { lhs, rhs, .. } => {
2689            proj_has_disqualifying_shape(lhs, inner_alias, outer_aliases)
2690                || proj_has_disqualifying_shape(rhs, inner_alias, outer_aliases)
2691        }
2692        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
2693            proj_has_disqualifying_shape(expr, inner_alias, outer_aliases)
2694        }
2695        Expr::Like { expr, pattern, .. } => {
2696            proj_has_disqualifying_shape(expr, inner_alias, outer_aliases)
2697                || proj_has_disqualifying_shape(pattern, inner_alias, outer_aliases)
2698        }
2699        Expr::InList { expr, list, .. } => {
2700            proj_has_disqualifying_shape(expr, inner_alias, outer_aliases)
2701                || list
2702                    .iter()
2703                    .any(|it| proj_has_disqualifying_shape(it, inner_alias, outer_aliases))
2704        }
2705        Expr::Case {
2706            operand,
2707            branches,
2708            else_branch,
2709        } => {
2710            operand
2711                .as_ref()
2712                .is_some_and(|o| proj_has_disqualifying_shape(o, inner_alias, outer_aliases))
2713                || branches.iter().any(|(w, t)| {
2714                    proj_has_disqualifying_shape(w, inner_alias, outer_aliases)
2715                        || proj_has_disqualifying_shape(t, inner_alias, outer_aliases)
2716                })
2717                || else_branch
2718                    .as_ref()
2719                    .is_some_and(|b| proj_has_disqualifying_shape(b, inner_alias, outer_aliases))
2720        }
2721        Expr::ArraySubscript { target, index } => {
2722            proj_has_disqualifying_shape(target, inner_alias, outer_aliases)
2723                || proj_has_disqualifying_shape(index, inner_alias, outer_aliases)
2724        }
2725        _ => false,
2726    }
2727}
2728
2729/// v7.37.4 A'' — walk every Expr field of a SelectStatement and
2730/// qualify any unqualified column whose name is in `owner`. Skips
2731/// nested subqueries' bodies (they own their own scope) but covers
2732/// SELECT items, WHERE, GROUP BY, HAVING, ORDER BY, and the
2733/// outer FROM clause's join ON predicates. Pulled-up join names
2734/// (`__exsj_*` / `__cl1_*` / `__plj_*`) are NOT in `owner`, so this
2735/// pass is idempotent under re-runs.
2736fn disambiguate_stmt_unqualified_columns(
2737    stmt: &mut SelectStatement,
2738    owner: &alloc::collections::BTreeMap<String, String>,
2739) {
2740    for item in &mut stmt.items {
2741        if let SelectItem::Expr { expr, .. } = item {
2742            disambiguate_expr_unqualified_columns(expr, owner);
2743        }
2744    }
2745    if let Some(from) = &mut stmt.from {
2746        for j in &mut from.joins {
2747            if let Some(on) = &mut j.on {
2748                disambiguate_expr_unqualified_columns(on, owner);
2749            }
2750        }
2751    }
2752    if let Some(g) = &mut stmt.group_by {
2753        for e in g.iter_mut() {
2754            disambiguate_expr_unqualified_columns(e, owner);
2755        }
2756    }
2757    if let Some(h) = &mut stmt.having {
2758        disambiguate_expr_unqualified_columns(h, owner);
2759    }
2760    for ob in &mut stmt.order_by {
2761        disambiguate_expr_unqualified_columns(&mut ob.expr, owner);
2762    }
2763}
2764
2765fn disambiguate_expr_unqualified_columns(
2766    e: &mut Expr,
2767    owner: &alloc::collections::BTreeMap<String, String>,
2768) {
2769    match e {
2770        Expr::Column(c) => {
2771            if c.qualifier.is_none()
2772                && let Some(alias) = owner.get(&c.name.to_ascii_lowercase())
2773            {
2774                c.qualifier = Some(alias.clone());
2775            }
2776        }
2777        Expr::Binary { lhs, rhs, .. } => {
2778            disambiguate_expr_unqualified_columns(lhs, owner);
2779            disambiguate_expr_unqualified_columns(rhs, owner);
2780        }
2781        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
2782            disambiguate_expr_unqualified_columns(expr, owner);
2783        }
2784        Expr::FunctionCall { args, .. } => {
2785            for a in args.iter_mut() {
2786                disambiguate_expr_unqualified_columns(a, owner);
2787            }
2788        }
2789        Expr::AggregateOrdered {
2790            call,
2791            order_by,
2792            filter,
2793            ..
2794        } => {
2795            disambiguate_expr_unqualified_columns(call, owner);
2796            for ob in order_by.iter_mut() {
2797                disambiguate_expr_unqualified_columns(&mut ob.expr, owner);
2798            }
2799            if let Some(f) = filter {
2800                disambiguate_expr_unqualified_columns(f, owner);
2801            }
2802        }
2803        Expr::Like { expr, pattern, .. } => {
2804            disambiguate_expr_unqualified_columns(expr, owner);
2805            disambiguate_expr_unqualified_columns(pattern, owner);
2806        }
2807        Expr::InList { expr, list, .. } => {
2808            disambiguate_expr_unqualified_columns(expr, owner);
2809            for it in list.iter_mut() {
2810                disambiguate_expr_unqualified_columns(it, owner);
2811            }
2812        }
2813        Expr::Case {
2814            operand,
2815            branches,
2816            else_branch,
2817        } => {
2818            if let Some(o) = operand {
2819                disambiguate_expr_unqualified_columns(o, owner);
2820            }
2821            for (w, t) in branches.iter_mut() {
2822                disambiguate_expr_unqualified_columns(w, owner);
2823                disambiguate_expr_unqualified_columns(t, owner);
2824            }
2825            if let Some(eb) = else_branch {
2826                disambiguate_expr_unqualified_columns(eb, owner);
2827            }
2828        }
2829        Expr::ArraySubscript { target, index } => {
2830            disambiguate_expr_unqualified_columns(target, owner);
2831            disambiguate_expr_unqualified_columns(index, owner);
2832        }
2833        // Subquery bodies own their own scope — leave untouched.
2834        _ => {}
2835    }
2836}
2837
2838fn expr_is_all_inner(e: &Expr, inner_alias: &str) -> bool {
2839    let mut cols: Vec<ColumnName> = Vec::new();
2840    let mut subs: Vec<&SelectStatement> = Vec::new();
2841    visit_expr_columns_and_subqueries(e, &mut |c| cols.push(c.clone()), &mut |s| subs.push(s));
2842    subs.is_empty()
2843        && cols.iter().all(|c| {
2844            c.qualifier
2845                .as_deref()
2846                .is_some_and(|q| q.eq_ignore_ascii_case(inner_alias))
2847        })
2848}
2849
2850/// v7.33 — rename every column qualifier equal to `from` into `to` in
2851/// place. Used to retarget an inner subquery's predicates from its
2852/// original table alias onto the fresh LEFT JOIN alias.
2853fn rename_qualifier(e: &mut Expr, from: &str, to: &str) {
2854    match e {
2855        Expr::Column(c) => {
2856            if c.qualifier
2857                .as_deref()
2858                .is_some_and(|q| q.eq_ignore_ascii_case(from))
2859            {
2860                c.qualifier = Some(to.into());
2861            }
2862        }
2863        Expr::Binary { lhs, rhs, .. } => {
2864            rename_qualifier(lhs, from, to);
2865            rename_qualifier(rhs, from, to);
2866        }
2867        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
2868            rename_qualifier(expr, from, to);
2869        }
2870        Expr::FunctionCall { args, .. } => {
2871            for a in args.iter_mut() {
2872                rename_qualifier(a, from, to);
2873            }
2874        }
2875        Expr::Like { expr, pattern, .. } => {
2876            rename_qualifier(expr, from, to);
2877            rename_qualifier(pattern, from, to);
2878        }
2879        Expr::InList { expr, list, .. } => {
2880            rename_qualifier(expr, from, to);
2881            for it in list.iter_mut() {
2882                rename_qualifier(it, from, to);
2883            }
2884        }
2885        Expr::Case {
2886            operand,
2887            branches,
2888            else_branch,
2889        } => {
2890            if let Some(o) = operand {
2891                rename_qualifier(o, from, to);
2892            }
2893            for (w, t) in branches.iter_mut() {
2894                rename_qualifier(w, from, to);
2895                rename_qualifier(t, from, to);
2896            }
2897            if let Some(eb) = else_branch {
2898                rename_qualifier(eb, from, to);
2899            }
2900        }
2901        _ => {}
2902    }
2903}
2904
2905/// v4.23: recognise the engine errors that indicate the inner
2906/// SELECT couldn't be evaluated in isolation because it references
2907/// an outer column — used by `subquery_replacement` to skip
2908/// materialisation and let row-eval handle it instead.
2909fn is_correlation_error(e: &EngineError) -> bool {
2910    matches!(
2911        e,
2912        EngineError::Eval(
2913            eval::EvalError::ColumnNotFound { .. } | eval::EvalError::UnknownQualifier { .. }
2914        )
2915    )
2916}
2917
2918/// v7.32 (R30 memory) — cheap static correlation pre-check.
2919///
2920/// `subquery_replacement` distinguishes a correlated subquery from an
2921/// uncorrelated one by *optimistically executing* it and catching the
2922/// resulting `ColumnNotFound` / `UnknownQualifier`. For a join-bodied
2923/// correlated subquery that catch fires only AFTER the inner FROM is
2924/// materialised — and the deferred-join pipeline clones the whole
2925/// driving table to do it (the inbox `… JOIN messages m2 …` body
2926/// clones 960k × 10 KB ≈ 10 GB at prod scale, once per outer query,
2927/// purely to be thrown away). A correlated subquery is always handled
2928/// downstream by the per-row / post-LIMIT correlated path, so spotting
2929/// it up front lets us skip the wasted materialisation entirely.
2930///
2931/// Sound for the `true` answer: returns true only when a qualified
2932/// column at the statement's own level names a qualifier that is not
2933/// one of its own FROM aliases — exactly the reference the inner exec
2934/// would fail to resolve. Everything it can't reason about cleanly
2935/// (lateral / derived FROM entries) returns false and falls through to
2936/// the existing execute-and-catch path, so behaviour is unchanged.
2937/// v7.37.x (docker-fair SCALARSQ attack) — pre-analysed plan for the
2938/// `(SELECT COUNT(*) FROM T WHERE T.pk = outer.col)` correlated
2939/// scalar subquery shape. Computing the table + index + position
2940/// lookups once per query (instead of once per outer row) drops the
2941/// per-row work to a single column read + index probe.
2942#[derive(Debug, Clone)]
2943pub struct ScalarPkProbeFastPath {
2944    /// Position in the OUTER scan schema for the column that drives
2945    /// the equality. Per row we read `row.values[outer_pos]` directly.
2946    pub outer_pos: usize,
2947    /// Catalog-qualified name of the inner table (looked up per probe).
2948    pub inner_table_name: String,
2949    /// Column position of the inner-side PK on which we probe.
2950    pub inner_pos: usize,
2951    /// v7.37.42 (docker-fair SCALARSQ attack 1) — cached insertion-order
2952    /// index of `inner_table_name` in the active catalog at PREPARE time.
2953    /// The executor and prepare share a single engine `RwLock` read guard
2954    /// per query (see `pgwire.rs` simple-query path), so the catalog
2955    /// can't mutate mid-query — the cached index stays in sync with the
2956    /// string name. The per-row probe therefore skips the
2957    /// `BTreeMap<String, usize>` descent that `Catalog::get(&str)` would
2958    /// otherwise perform, saving ~300 ns × N outer rows.
2959    pub table_idx: usize,
2960}
2961
2962impl ScalarPkProbeFastPath {
2963    /// Per-row probe. Reads `row.values[self.outer_pos]`, looks up the
2964    /// inner table and PK index, and returns `Int(1)` on a hit or
2965    /// `Int(0)` on a miss / NULL outer key.
2966    pub fn probe(&self, row: &Row<'static>) -> Value<'static> {
2967        // The engine handle is needed to access the live catalog. The
2968        // probe is called from the run-loop with the engine in scope,
2969        // so we look up the catalog via a thread_local-cached
2970        // borrow. Simpler: defer to the engine helper that takes the
2971        // pre-analysed plan + the row. Kept here as a vtable-style
2972        // entry point so the run-loop's hot path is small.
2973        let outer_int = match row.values.get(self.outer_pos) {
2974            Some(Value::BigInt(n)) => *n,
2975            Some(Value::Int(n)) => i64::from(*n),
2976            Some(Value::SmallInt(n)) => i64::from(*n),
2977            Some(Value::Null) | None => return Value::Int(0),
2978            _ => return Value::Int(0),
2979        };
2980        SCALARSQ_PK_PROBE_PLAN_OUTER_INT.store(outer_int, core::sync::atomic::Ordering::Relaxed);
2981        SCALARSQ_PK_PROBE_PLAN_FIRED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
2982        // The actual seek lives in `Engine::probe_with_pk_fast_path` —
2983        // we can't carry an engine borrow here without a lifetime
2984        // round-trip. Returning Int(0) as a placeholder would break
2985        // semantics; instead the run-loop calls
2986        // `engine.probe_with_pk_fast_path(&self, row)` directly so
2987        // the plan's `probe()` method is used only in tests where
2988        // the table data isn't load-bearing.
2989        Value::Int(0)
2990    }
2991}
2992
2993/// v7.37.x — per-row hit counter for the plan-cached fast path.
2994pub static SCALARSQ_PK_PROBE_PLAN_FIRED: core::sync::atomic::AtomicU64 =
2995    core::sync::atomic::AtomicU64::new(0);
2996pub static SCALARSQ_PK_PROBE_PLAN_OUTER_INT: core::sync::atomic::AtomicI64 =
2997    core::sync::atomic::AtomicI64::new(0);
2998
2999/// v7.37.x (docker-fair SCALARSQ attack) — direct PK probe for the
3000/// `(SELECT COUNT(*) FROM T WHERE T.pk = outer.col)` correlated
3001/// scalar subquery shape. Returns `Some(Int(0))` if the probe misses
3002/// or `Some(Int(1))` if it hits; `None` when the shape doesn't match
3003/// (caller falls back to per-row exec). Bypasses parse / resolve /
3004/// plan / aggregate; the SCALARSQ docker-fair bench drops from
3005/// per-row ~3 µs to per-row ~100 ns.
3006impl Engine {
3007    /// Run a pre-analysed PK probe against the live catalog. Used by
3008    /// the per-row projection fast path to avoid going through
3009    /// `eval_expr_with_correlated`.
3010    pub(crate) fn probe_with_pk_fast_path(
3011        &self,
3012        plan: &ScalarPkProbeFastPath,
3013        row: &Row<'static>,
3014    ) -> Value<'static> {
3015        let outer_int = match row.values.get(plan.outer_pos) {
3016            Some(Value::BigInt(n)) => *n,
3017            Some(Value::Int(n)) => i64::from(*n),
3018            Some(Value::SmallInt(n)) => i64::from(*n),
3019            Some(Value::Null) | None => return Value::Int(0),
3020            _ => return Value::Int(0),
3021        };
3022        // v7.37.42 attack 1 — bypass per-row `BTreeMap<String,usize>::get`
3023        // by going through the cached positional index. The prepare-time
3024        // analyser stores the index against the same catalog snapshot
3025        // the executor sees (same engine read guard), so the cached
3026        // index remains valid for the query's duration.
3027        let Some(inner_table) = self.active_catalog().tables_at(plan.table_idx) else {
3028            return Value::Int(0);
3029        };
3030        let Some(idx) = inner_table.index_on(plan.inner_pos) else {
3031            return Value::Int(0);
3032        };
3033        let Some(key) = spg_storage::IndexKey::from_value(&Value::BigInt(outer_int)) else {
3034            return Value::Int(0);
3035        };
3036        let hit = !idx.lookup_eq(&key).is_empty();
3037        SCALARSQ_PK_PROBE_FIRED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3038        Value::Int(i32::from(hit))
3039    }
3040
3041    /// Analyse a scalar subquery against the OUTER scan schema; return
3042    /// a `ScalarPkProbeFastPath` plan when the canonical shape is
3043    /// recognised, otherwise `None`. The outer alias and column-name
3044    /// resolution use the scan schema so the run-loop can read the
3045    /// outer value by position.
3046    pub(crate) fn analyse_scalar_count_pk_eq_probe(
3047        &self,
3048        inner: &SelectStatement,
3049        outer_schema: &[spg_storage::ColumnSchema],
3050        outer_alias: &str,
3051    ) -> Option<ScalarPkProbeFastPath> {
3052        use spg_sql::ast::{BinOp, ColumnName, SelectItem};
3053        if !inner.ctes.is_empty()
3054            || !inner.unions.is_empty()
3055            || inner.group_by.is_some()
3056            || inner.having.is_some()
3057            || inner.distinct
3058            || !inner.order_by.is_empty()
3059            || inner.limit.is_some()
3060            || inner.offset.is_some()
3061            || inner.items.len() != 1
3062        {
3063            return None;
3064        }
3065        let SelectItem::Expr { expr, .. } = &inner.items[0] else {
3066            return None;
3067        };
3068        let is_count_shape = match expr {
3069            Expr::FunctionCall { name, args } => {
3070                (name.eq_ignore_ascii_case("count_star") && args.is_empty())
3071                    || name.eq_ignore_ascii_case("count")
3072            }
3073            _ => false,
3074        };
3075        if !is_count_shape {
3076            return None;
3077        }
3078        let from = inner.from.as_ref()?;
3079        if !from.joins.is_empty()
3080            || from.primary.lateral_subquery.is_some()
3081            || from.primary.unnest_expr.is_some()
3082            || from.primary.generate_series_args.is_some()
3083            || from.primary.as_of_segment.is_some()
3084        {
3085            return None;
3086        }
3087        let inner_table_name = from.primary.name.clone();
3088        let inner_alias = from
3089            .primary
3090            .alias
3091            .as_deref()
3092            .unwrap_or(inner_table_name.as_str());
3093        let where_expr = inner.where_.as_ref()?;
3094        let Expr::Binary {
3095            lhs,
3096            op: BinOp::Eq,
3097            rhs,
3098        } = where_expr
3099        else {
3100            return None;
3101        };
3102        let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref()) else {
3103            return None;
3104        };
3105        let pick = |x: &ColumnName, y: &ColumnName| -> Option<(String, ColumnName)> {
3106            if x.qualifier
3107                .as_deref()
3108                .is_some_and(|q| q.eq_ignore_ascii_case(inner_alias))
3109            {
3110                Some((x.name.clone(), y.clone()))
3111            } else {
3112                None
3113            }
3114        };
3115        let (inner_col_name, outer_col) = pick(a, b).or_else(|| pick(b, a))?;
3116        // Outer column must be in the scan schema and qualified to
3117        // outer_alias (or unqualified).
3118        if let Some(q) = outer_col.qualifier.as_deref()
3119            && !q.eq_ignore_ascii_case(outer_alias)
3120        {
3121            return None;
3122        }
3123        let outer_pos = outer_schema
3124            .iter()
3125            .position(|c| c.name.eq_ignore_ascii_case(&outer_col.name))?;
3126        // Inner column must be a single-column PK on an integer family.
3127        // v7.37.42 attack 1 — resolve the inner table's positional index
3128        // alongside the table fetch so the per-row probe can skip the
3129        // `BTreeMap<String,usize>::get(&str)` descent.
3130        let catalog = self.active_catalog();
3131        let table_idx = catalog.tables_position_of(inner_table_name.as_str())?;
3132        let inner_table = catalog.tables_at(table_idx)?;
3133        let inner_schema_ref = inner_table.schema();
3134        let inner_pos = inner_schema_ref
3135            .columns
3136            .iter()
3137            .position(|c| c.name.eq_ignore_ascii_case(&inner_col_name))?;
3138        if !matches!(
3139            inner_schema_ref.columns[inner_pos].ty,
3140            spg_storage::DataType::BigInt
3141                | spg_storage::DataType::Int
3142                | spg_storage::DataType::SmallInt
3143        ) {
3144            return None;
3145        }
3146        if !inner_schema_ref
3147            .uniqueness_constraints
3148            .iter()
3149            .any(|u| u.is_primary_key && u.columns.as_slice() == [inner_pos])
3150        {
3151            return None;
3152        }
3153        Some(ScalarPkProbeFastPath {
3154            outer_pos,
3155            inner_table_name,
3156            inner_pos,
3157            table_idx,
3158        })
3159    }
3160
3161    pub(crate) fn try_scalar_count_pk_eq_probe(
3162        &self,
3163        inner: &SelectStatement,
3164        row: &Row<'static>,
3165        ctx: &EvalContext<'_>,
3166    ) -> Result<Option<Value<'static>>, EngineError> {
3167        use spg_sql::ast::{BinOp, ColumnName, SelectItem};
3168        if !inner.ctes.is_empty()
3169            || !inner.unions.is_empty()
3170            || inner.group_by.is_some()
3171            || inner.having.is_some()
3172            || inner.distinct
3173            || !inner.order_by.is_empty()
3174            || inner.limit.is_some()
3175            || inner.offset.is_some()
3176            || inner.items.len() != 1
3177        {
3178            return Ok(None);
3179        }
3180        let SelectItem::Expr { expr, .. } = &inner.items[0] else {
3181            return Ok(None);
3182        };
3183        let is_count_shape = match expr {
3184            Expr::FunctionCall { name, args } => {
3185                (name.eq_ignore_ascii_case("count_star") && args.is_empty())
3186                    || name.eq_ignore_ascii_case("count")
3187            }
3188            _ => false,
3189        };
3190        if !is_count_shape {
3191            return Ok(None);
3192        }
3193        let Some(from) = &inner.from else {
3194            return Ok(None);
3195        };
3196        if !from.joins.is_empty()
3197            || from.primary.lateral_subquery.is_some()
3198            || from.primary.unnest_expr.is_some()
3199            || from.primary.generate_series_args.is_some()
3200            || from.primary.as_of_segment.is_some()
3201        {
3202            return Ok(None);
3203        }
3204        let inner_table_name = from.primary.name.as_str();
3205        let inner_alias = from.primary.alias.as_deref().unwrap_or(inner_table_name);
3206        let Some(where_expr) = &inner.where_ else {
3207            return Ok(None);
3208        };
3209        let Expr::Binary {
3210            lhs,
3211            op: BinOp::Eq,
3212            rhs,
3213        } = where_expr
3214        else {
3215            return Ok(None);
3216        };
3217        let (Expr::Column(a), Expr::Column(b)) = (lhs.as_ref(), rhs.as_ref()) else {
3218            return Ok(None);
3219        };
3220        let pick = |x: &ColumnName, y: &ColumnName| -> Option<(String, ColumnName)> {
3221            if x.qualifier
3222                .as_deref()
3223                .is_some_and(|q| q.eq_ignore_ascii_case(inner_alias))
3224            {
3225                Some((x.name.clone(), y.clone()))
3226            } else {
3227                None
3228            }
3229        };
3230        let Some((inner_col_name, outer_col)) = pick(a, b).or_else(|| pick(b, a)) else {
3231            return Ok(None);
3232        };
3233        let catalog = self.active_catalog();
3234        let Some(inner_table) = catalog.get(inner_table_name) else {
3235            return Ok(None);
3236        };
3237        let inner_schema = inner_table.schema();
3238        let Some(inner_pos) = inner_schema
3239            .columns
3240            .iter()
3241            .position(|c| c.name.eq_ignore_ascii_case(&inner_col_name))
3242        else {
3243            return Ok(None);
3244        };
3245        if !matches!(
3246            inner_schema.columns[inner_pos].ty,
3247            spg_storage::DataType::BigInt
3248                | spg_storage::DataType::Int
3249                | spg_storage::DataType::SmallInt
3250        ) {
3251            return Ok(None);
3252        }
3253        if !inner_schema
3254            .uniqueness_constraints
3255            .iter()
3256            .any(|u| u.is_primary_key && u.columns.as_slice() == [inner_pos])
3257        {
3258            return Ok(None);
3259        }
3260        let outer_val = match eval::eval_expr(&Expr::Column(outer_col), row, ctx) {
3261            Ok(v) => v,
3262            Err(_) => return Ok(None),
3263        };
3264        let outer_int = match outer_val {
3265            Value::BigInt(n) => n,
3266            Value::Int(n) => i64::from(n),
3267            Value::SmallInt(n) => i64::from(n),
3268            Value::Null => return Ok(Some(Value::Int(0))),
3269            _ => return Ok(None),
3270        };
3271        let Some(idx) = inner_table.index_on(inner_pos) else {
3272            return Ok(None);
3273        };
3274        let Some(key) = spg_storage::IndexKey::from_value(&Value::BigInt(outer_int)) else {
3275            return Ok(None);
3276        };
3277        SCALARSQ_PK_PROBE_FIRED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3278        let hit = !idx.lookup_eq(&key).is_empty();
3279        Ok(Some(Value::Int(i32::from(hit))))
3280    }
3281}
3282
3283pub static SCALARSQ_PK_PROBE_FIRED: core::sync::atomic::AtomicU64 =
3284    core::sync::atomic::AtomicU64::new(0);
3285
3286/// v7.37.x (docker-fair SCALARSQ attack) — return the SQL empty-set
3287/// default for a scalar subquery's output expression. PG semantics
3288/// distinguish `COUNT(*)` (0 over an empty set) from other aggregates
3289/// (NULL). Called by the batched ScalarSubquery resolver when a
3290/// per-outer-row probe finds no matching inner partition.
3291fn scalar_subquery_empty_default(inner: &SelectStatement) -> Value<'static> {
3292    use spg_sql::ast::SelectItem;
3293    if inner.items.len() != 1 {
3294        return Value::Null;
3295    }
3296    let SelectItem::Expr { expr, .. } = &inner.items[0] else {
3297        return Value::Null;
3298    };
3299    fn is_count(e: &Expr) -> bool {
3300        match e {
3301            // COUNT(*) parses as `count_star`; COUNT(col) as `count`.
3302            // Both have BIGINT-shaped empty-set default of 0.
3303            Expr::FunctionCall { name, .. } => {
3304                name.eq_ignore_ascii_case("count") || name.eq_ignore_ascii_case("count_star")
3305            }
3306            Expr::AggregateOrdered { call, .. } => is_count(call),
3307            _ => false,
3308        }
3309    }
3310    if is_count(expr) {
3311        Value::Int(0)
3312    } else {
3313        Value::Null
3314    }
3315}
3316
3317pub(crate) fn select_is_correlated(s: &SelectStatement) -> bool {
3318    use spg_sql::ast::SelectItem;
3319    let Some(from) = &s.from else {
3320        // No FROM: correlated iff some projected column is qualified
3321        // (a qualifier with nothing to bind to is necessarily outer).
3322        let mut qualified = false;
3323        for item in &s.items {
3324            if let SelectItem::Expr { expr, .. } = item {
3325                visit_expr_columns_and_subqueries(
3326                    expr,
3327                    &mut |c| {
3328                        if c.qualifier.is_some() {
3329                            qualified = true;
3330                        }
3331                    },
3332                    &mut |_| {},
3333                );
3334            }
3335        }
3336        return qualified;
3337    };
3338    // Lateral / derived FROM entries put scope resolution beyond this
3339    // cheap check — defer to execute-and-catch.
3340    if from.primary.lateral_subquery.is_some() {
3341        return false;
3342    }
3343    let mut inner: Vec<&str> = Vec::new();
3344    if let Some(a) = &from.primary.alias {
3345        inner.push(a.as_str());
3346    }
3347    if !from.primary.name.is_empty() {
3348        inner.push(from.primary.name.as_str());
3349    }
3350    for j in &from.joins {
3351        if j.table.lateral_subquery.is_some() {
3352            return false;
3353        }
3354        if let Some(a) = &j.table.alias {
3355            inner.push(a.as_str());
3356        }
3357        if !j.table.name.is_empty() {
3358            inner.push(j.table.name.as_str());
3359        }
3360    }
3361    // Gather every expression position that evaluates in this
3362    // statement's own scope (NOT inside nested subquery bodies — the
3363    // visitor reports those via the subquery callback, which we drop).
3364    let mut exprs: Vec<&Expr> = Vec::new();
3365    for item in &s.items {
3366        if let SelectItem::Expr { expr, .. } = item {
3367            exprs.push(expr);
3368        }
3369    }
3370    if let Some(w) = &s.where_ {
3371        exprs.push(w);
3372    }
3373    for j in &from.joins {
3374        if let Some(on) = &j.on {
3375            exprs.push(on);
3376        }
3377    }
3378    if let Some(gs) = &s.group_by {
3379        for g in gs {
3380            exprs.push(g);
3381        }
3382    }
3383    if let Some(h) = &s.having {
3384        exprs.push(h);
3385    }
3386    for o in &s.order_by {
3387        exprs.push(&o.expr);
3388    }
3389    let mut correlated = false;
3390    for e in exprs {
3391        visit_expr_columns_and_subqueries(
3392            e,
3393            &mut |c| {
3394                if let Some(q) = &c.qualifier
3395                    && !inner.iter().any(|a| a.eq_ignore_ascii_case(q))
3396                {
3397                    correlated = true;
3398                }
3399            },
3400            &mut |_| {},
3401        );
3402    }
3403    correlated
3404}
3405
3406/// v7.29 (3c) — pre-order collection of SCALAR subquery nodes in a
3407/// host expression (no descent into subquery bodies). The splice
3408/// walk below uses the same order; the pair must stay in lockstep.
3409pub(crate) fn collect_scalar_subqueries<'a>(e: &'a Expr, out: &mut Vec<&'a SelectStatement>) {
3410    match e {
3411        Expr::ScalarSubquery(s) => out.push(s),
3412        Expr::Exists { .. } | Expr::InSubquery { .. } => {}
3413        Expr::Binary { lhs, rhs, .. } => {
3414            collect_scalar_subqueries(lhs, out);
3415            collect_scalar_subqueries(rhs, out);
3416        }
3417        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
3418            collect_scalar_subqueries(expr, out);
3419        }
3420        Expr::Like { expr, pattern, .. } => {
3421            collect_scalar_subqueries(expr, out);
3422            collect_scalar_subqueries(pattern, out);
3423        }
3424        Expr::FunctionCall { args, .. } => {
3425            for a in args {
3426                collect_scalar_subqueries(a, out);
3427            }
3428        }
3429        Expr::AggregateOrdered { call, order_by, .. } => {
3430            collect_scalar_subqueries(call, out);
3431            for o in order_by {
3432                collect_scalar_subqueries(&o.expr, out);
3433            }
3434        }
3435        Expr::Case {
3436            operand,
3437            branches,
3438            else_branch,
3439        } => {
3440            if let Some(op) = operand {
3441                collect_scalar_subqueries(op, out);
3442            }
3443            for (w, t) in branches {
3444                collect_scalar_subqueries(w, out);
3445                collect_scalar_subqueries(t, out);
3446            }
3447            if let Some(eb) = else_branch {
3448                collect_scalar_subqueries(eb, out);
3449            }
3450        }
3451        Expr::ArraySubscript { target, index } => {
3452            collect_scalar_subqueries(target, out);
3453            collect_scalar_subqueries(index, out);
3454        }
3455        Expr::InList { expr, list, .. } => {
3456            collect_scalar_subqueries(expr, out);
3457            for item in list {
3458                collect_scalar_subqueries(item, out);
3459            }
3460        }
3461        _ => {}
3462    }
3463}
3464
3465/// v7.29 (3d) — empty every scalar-subquery BODY in a host
3466/// expression (node kept so the splice pre-order still matches).
3467fn hollow_scalar_subqueries(e: &mut Expr) {
3468    match e {
3469        Expr::ScalarSubquery(s) => {
3470            let hollow = SelectStatement {
3471                items: Vec::new(),
3472                ..SelectStatement::default()
3473            };
3474            **s = hollow;
3475        }
3476        Expr::Exists { .. } | Expr::InSubquery { .. } => {}
3477        Expr::Binary { lhs, rhs, .. } => {
3478            hollow_scalar_subqueries(lhs);
3479            hollow_scalar_subqueries(rhs);
3480        }
3481        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
3482            hollow_scalar_subqueries(expr);
3483        }
3484        Expr::Like { expr, pattern, .. } => {
3485            hollow_scalar_subqueries(expr);
3486            hollow_scalar_subqueries(pattern);
3487        }
3488        Expr::FunctionCall { args, .. } => {
3489            for a in args.iter_mut() {
3490                hollow_scalar_subqueries(a);
3491            }
3492        }
3493        Expr::AggregateOrdered { call, order_by, .. } => {
3494            hollow_scalar_subqueries(call);
3495            for o in order_by.iter_mut() {
3496                hollow_scalar_subqueries(&mut o.expr);
3497            }
3498        }
3499        Expr::Case {
3500            operand,
3501            branches,
3502            else_branch,
3503        } => {
3504            if let Some(op) = operand {
3505                hollow_scalar_subqueries(op);
3506            }
3507            for (w, t) in branches.iter_mut() {
3508                hollow_scalar_subqueries(w);
3509                hollow_scalar_subqueries(t);
3510            }
3511            if let Some(eb) = else_branch {
3512                hollow_scalar_subqueries(eb);
3513            }
3514        }
3515        Expr::ArraySubscript { target, index } => {
3516            hollow_scalar_subqueries(target);
3517            hollow_scalar_subqueries(index);
3518        }
3519        Expr::InList { expr, list, .. } => {
3520            hollow_scalar_subqueries(expr);
3521            for item in list.iter_mut() {
3522                hollow_scalar_subqueries(item);
3523            }
3524        }
3525        _ => {}
3526    }
3527}
3528
3529/// v7.29 (3c) — splice the i-th scalar subquery's batched value into
3530/// the cloned tree (same pre-order as collect_scalar_subqueries).
3531/// Returns Ok(false) if a literal conversion fails (caller falls
3532/// back to the resolver path).
3533fn splice_planned_subqueries(
3534    e: &mut Expr,
3535    plan: &[Option<alloc::rc::Rc<memoize::GroupMap>>],
3536    idx: &mut usize,
3537    row: &Row<'static>,
3538    ctx: &EvalContext<'_>,
3539) -> Result<bool, EngineError> {
3540    match e {
3541        Expr::ScalarSubquery(_) => {
3542            let Some(Some(gm)) = plan.get(*idx) else {
3543                return Ok(false);
3544            };
3545            *idx += 1;
3546            // v7.37.x (docker-fair SCALARSQ attack) — empty_default is
3547            // carried on the GroupMap (PG empty-set semantics: COUNT = 0,
3548            // others = NULL). The inner here may be HOLLOWED by the
3549            // template-rewrite step, so re-introspecting it for the
3550            // aggregate kind doesn't work — the construction-time
3551            // value on the GroupMap is the source of truth.
3552            let (outer_col, map, empty_default) = gm.as_ref();
3553            let key_v = eval::eval_expr(&Expr::Column(outer_col.clone()), row, ctx)
3554                .map_err(EngineError::Eval)?;
3555            let v = if matches!(key_v, Value::Null) {
3556                Value::Null
3557            } else {
3558                map.get(&aggregate::encode_key(core::slice::from_ref(&key_v)))
3559                    .cloned()
3560                    .unwrap_or_else(|| empty_default.clone())
3561            };
3562            *e = value_to_literal_expr(v)?;
3563            Ok(true)
3564        }
3565        Expr::Exists { .. } | Expr::InSubquery { .. } => Ok(true),
3566        Expr::Binary { lhs, rhs, .. } => Ok(splice_planned_subqueries(lhs, plan, idx, row, ctx)?
3567            && splice_planned_subqueries(rhs, plan, idx, row, ctx)?),
3568        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
3569            splice_planned_subqueries(expr, plan, idx, row, ctx)
3570        }
3571        Expr::Like { expr, pattern, .. } => {
3572            Ok(splice_planned_subqueries(expr, plan, idx, row, ctx)?
3573                && splice_planned_subqueries(pattern, plan, idx, row, ctx)?)
3574        }
3575        Expr::FunctionCall { args, .. } => {
3576            for a in args.iter_mut() {
3577                if !splice_planned_subqueries(a, plan, idx, row, ctx)? {
3578                    return Ok(false);
3579                }
3580            }
3581            Ok(true)
3582        }
3583        Expr::AggregateOrdered { call, order_by, .. } => {
3584            if !splice_planned_subqueries(call, plan, idx, row, ctx)? {
3585                return Ok(false);
3586            }
3587            for o in order_by.iter_mut() {
3588                if !splice_planned_subqueries(&mut o.expr, plan, idx, row, ctx)? {
3589                    return Ok(false);
3590                }
3591            }
3592            Ok(true)
3593        }
3594        Expr::Case {
3595            operand,
3596            branches,
3597            else_branch,
3598        } => {
3599            if let Some(op) = operand {
3600                if !splice_planned_subqueries(op, plan, idx, row, ctx)? {
3601                    return Ok(false);
3602                }
3603            }
3604            for (w, t) in branches.iter_mut() {
3605                if !splice_planned_subqueries(w, plan, idx, row, ctx)?
3606                    || !splice_planned_subqueries(t, plan, idx, row, ctx)?
3607                {
3608                    return Ok(false);
3609                }
3610            }
3611            if let Some(eb) = else_branch {
3612                if !splice_planned_subqueries(eb, plan, idx, row, ctx)? {
3613                    return Ok(false);
3614                }
3615            }
3616            Ok(true)
3617        }
3618        Expr::ArraySubscript { target, index } => {
3619            Ok(splice_planned_subqueries(target, plan, idx, row, ctx)?
3620                && splice_planned_subqueries(index, plan, idx, row, ctx)?)
3621        }
3622        Expr::InList { expr, list, .. } => {
3623            if !splice_planned_subqueries(expr, plan, idx, row, ctx)? {
3624                return Ok(false);
3625            }
3626            for item in list.iter_mut() {
3627                if !splice_planned_subqueries(item, plan, idx, row, ctx)? {
3628                    return Ok(false);
3629                }
3630            }
3631            Ok(true)
3632        }
3633        _ => Ok(true),
3634    }
3635}
3636
3637/// v7.34.2 (EXISTS-FILTER baseline) — pre-order collect for EXISTS
3638/// subqueries. Mirrors `collect_scalar_subqueries` so the per-row
3639/// splice walker can re-traverse in the same order and pick the
3640/// matching planned set by ordinal index — no string repr, no
3641/// BTreeMap probe per row. ScalarSubquery / InSubquery nodes are
3642/// skipped here (they ride their own planners).
3643pub(crate) fn collect_exists_subqueries<'a>(e: &'a Expr, out: &mut Vec<&'a SelectStatement>) {
3644    match e {
3645        Expr::Exists { subquery, .. } => out.push(subquery.as_ref()),
3646        Expr::ScalarSubquery(_) | Expr::InSubquery { .. } => {}
3647        Expr::Binary { lhs, rhs, .. } => {
3648            collect_exists_subqueries(lhs, out);
3649            collect_exists_subqueries(rhs, out);
3650        }
3651        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
3652            collect_exists_subqueries(expr, out);
3653        }
3654        Expr::Like { expr, pattern, .. } => {
3655            collect_exists_subqueries(expr, out);
3656            collect_exists_subqueries(pattern, out);
3657        }
3658        Expr::FunctionCall { args, .. } => {
3659            for a in args {
3660                collect_exists_subqueries(a, out);
3661            }
3662        }
3663        Expr::AggregateOrdered { call, order_by, .. } => {
3664            collect_exists_subqueries(call, out);
3665            for o in order_by {
3666                collect_exists_subqueries(&o.expr, out);
3667            }
3668        }
3669        Expr::Case {
3670            operand,
3671            branches,
3672            else_branch,
3673        } => {
3674            if let Some(op) = operand {
3675                collect_exists_subqueries(op, out);
3676            }
3677            for (w, t) in branches {
3678                collect_exists_subqueries(w, out);
3679                collect_exists_subqueries(t, out);
3680            }
3681            if let Some(eb) = else_branch {
3682                collect_exists_subqueries(eb, out);
3683            }
3684        }
3685        Expr::ArraySubscript { target, index } => {
3686            collect_exists_subqueries(target, out);
3687            collect_exists_subqueries(index, out);
3688        }
3689        Expr::InList { expr, list, .. } => {
3690            collect_exists_subqueries(expr, out);
3691            for item in list {
3692                collect_exists_subqueries(item, out);
3693            }
3694        }
3695        _ => {}
3696    }
3697}
3698
3699/// v7.34.2 — per-row splice for the planned EXISTS sets. Walks the
3700/// (cloned) host expression in the SAME pre-order as
3701/// `collect_exists_subqueries`, increments `idx` past each EXISTS
3702/// node, and replaces it in place with `Bool(true/false)` derived
3703/// from the planned key-set + outer-row column values. Returns
3704/// `Ok(false)` when any encountered EXISTS lacks a planned set; the
3705/// caller falls back to the legacy per-row resolver path.
3706fn splice_planned_exists(
3707    e: &mut Expr,
3708    plan: &[Option<alloc::rc::Rc<memoize::ExistsSet>>],
3709    idx: &mut usize,
3710    row: &Row<'static>,
3711    ctx: &EvalContext<'_>,
3712) -> Result<bool, EngineError> {
3713    match e {
3714        Expr::Exists { negated, .. } => {
3715            let Some(Some(es)) = plan.get(*idx) else {
3716                return Ok(false);
3717            };
3718            *idx += 1;
3719            let (outer_cols, set) = es.as_ref();
3720            let mut key_vals: Vec<Value<'static>> = Vec::with_capacity(outer_cols.len());
3721            let mut any_null = false;
3722            for oc in outer_cols {
3723                let v = eval::eval_expr(&Expr::Column(oc.clone()), row, ctx)
3724                    .map_err(EngineError::Eval)?;
3725                if matches!(v, Value::Null) {
3726                    any_null = true;
3727                }
3728                key_vals.push(v);
3729            }
3730            let present = !any_null && set.contains(&aggregate::encode_key(&key_vals));
3731            let bit = if *negated { !present } else { present };
3732            *e = Expr::Literal(Literal::Bool(bit));
3733            Ok(true)
3734        }
3735        Expr::ScalarSubquery(_) | Expr::InSubquery { .. } => Ok(true),
3736        Expr::Binary { lhs, rhs, .. } => Ok(splice_planned_exists(lhs, plan, idx, row, ctx)?
3737            && splice_planned_exists(rhs, plan, idx, row, ctx)?),
3738        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
3739            splice_planned_exists(expr, plan, idx, row, ctx)
3740        }
3741        Expr::Like { expr, pattern, .. } => Ok(splice_planned_exists(expr, plan, idx, row, ctx)?
3742            && splice_planned_exists(pattern, plan, idx, row, ctx)?),
3743        Expr::FunctionCall { args, .. } => {
3744            for a in args.iter_mut() {
3745                if !splice_planned_exists(a, plan, idx, row, ctx)? {
3746                    return Ok(false);
3747                }
3748            }
3749            Ok(true)
3750        }
3751        Expr::AggregateOrdered { call, order_by, .. } => {
3752            if !splice_planned_exists(call, plan, idx, row, ctx)? {
3753                return Ok(false);
3754            }
3755            for o in order_by.iter_mut() {
3756                if !splice_planned_exists(&mut o.expr, plan, idx, row, ctx)? {
3757                    return Ok(false);
3758                }
3759            }
3760            Ok(true)
3761        }
3762        Expr::Case {
3763            operand,
3764            branches,
3765            else_branch,
3766        } => {
3767            if let Some(op) = operand {
3768                if !splice_planned_exists(op, plan, idx, row, ctx)? {
3769                    return Ok(false);
3770                }
3771            }
3772            for (w, t) in branches.iter_mut() {
3773                if !splice_planned_exists(w, plan, idx, row, ctx)?
3774                    || !splice_planned_exists(t, plan, idx, row, ctx)?
3775                {
3776                    return Ok(false);
3777                }
3778            }
3779            if let Some(eb) = else_branch {
3780                if !splice_planned_exists(eb, plan, idx, row, ctx)? {
3781                    return Ok(false);
3782                }
3783            }
3784            Ok(true)
3785        }
3786        Expr::ArraySubscript { target, index } => {
3787            Ok(splice_planned_exists(target, plan, idx, row, ctx)?
3788                && splice_planned_exists(index, plan, idx, row, ctx)?)
3789        }
3790        Expr::InList { expr, list, .. } => {
3791            if !splice_planned_exists(expr, plan, idx, row, ctx)? {
3792                return Ok(false);
3793            }
3794            for item in list.iter_mut() {
3795                if !splice_planned_exists(item, plan, idx, row, ctx)? {
3796                    return Ok(false);
3797                }
3798            }
3799            Ok(true)
3800        }
3801        _ => Ok(true),
3802    }
3803}
3804
3805/// v7.30.2 (mailrs round-25) — minimum element count before an
3806/// all-literal `IN` list gets a per-query membership set. Below
3807/// this the linear scan wins on build cost.
3808const INLIST_SET_THRESHOLD: usize = 64;
3809
3810/// Cheap pre-check: is a set-eligible `IN` list reachable on the
3811/// AND spine of this expression? Anything else keeps the plain
3812/// `eval_expr` path untouched.
3813fn expr_may_use_in_set(e: &Expr) -> bool {
3814    match e {
3815        Expr::InList { list, .. } => list.len() >= INLIST_SET_THRESHOLD,
3816        Expr::Binary {
3817            lhs,
3818            op: BinOp::And,
3819            rhs,
3820        } => expr_may_use_in_set(lhs) || expr_may_use_in_set(rhs),
3821        _ => false,
3822    }
3823}
3824
3825/// Analyse an `IN` list for set eligibility: every element a literal,
3826/// all of one family (integer or string, NULLs tracked separately).
3827pub(crate) fn build_in_list_set(list: &[Expr]) -> Option<memoize::InListSetEntry> {
3828    let mut has_null = false;
3829    let mut ints: hashbrown::HashSet<i64> = hashbrown::HashSet::with_capacity(list.len());
3830    let mut texts: hashbrown::HashSet<String> = hashbrown::HashSet::with_capacity(list.len());
3831    for item in list {
3832        let Expr::Literal(lit) = item else {
3833            return None;
3834        };
3835        match lit {
3836            Literal::Null => has_null = true,
3837            Literal::Integer(i) => {
3838                ints.insert(*i);
3839            }
3840            Literal::String(s) => {
3841                texts.insert(s.clone());
3842            }
3843            _ => return None,
3844        }
3845        if !ints.is_empty() && !texts.is_empty() {
3846            return None;
3847        }
3848    }
3849    let set = if !ints.is_empty() {
3850        memoize::InListSet::Int(ints)
3851    } else if !texts.is_empty() {
3852        memoize::InListSet::Text(texts)
3853    } else {
3854        return None;
3855    };
3856    Some(memoize::InListSetEntry { set, has_null })
3857}
3858
3859/// Subquery-free eval that serves large all-literal `IN` lists from
3860/// a per-query membership set (cached in the memo by node address).
3861/// Walks only the AND spine; every other node — and every needle
3862/// whose runtime family doesn't match the set — falls through to
3863/// `eval_expr`, so coercion and error semantics stay identical.
3864fn eval_with_in_sets(
3865    e: &Expr,
3866    row: &Row<'static>,
3867    ctx: &EvalContext<'_>,
3868    m: &mut memoize::MemoizeCache,
3869) -> Result<Value<'static>, EngineError> {
3870    match e {
3871        Expr::Binary {
3872            lhs,
3873            op: BinOp::And,
3874            rhs,
3875        } => {
3876            // Mirror eval_expr: both sides evaluate (no short
3877            // circuit), then SQL three-valued AND.
3878            let l = eval_with_in_sets(lhs, row, ctx, m)?;
3879            let r = eval_with_in_sets(rhs, row, ctx, m)?;
3880            eval::and_3vl(l, r).map_err(EngineError::Eval)
3881        }
3882        Expr::InList {
3883            expr: lhs,
3884            list,
3885            negated,
3886        } if list.len() >= INLIST_SET_THRESHOLD => {
3887            let key = core::ptr::from_ref::<Expr>(e) as usize;
3888            let Some(entry) = m
3889                .in_sets
3890                .entry(key)
3891                .or_insert_with(|| build_in_list_set(list))
3892            else {
3893                return eval::eval_expr(e, row, ctx).map_err(EngineError::Eval);
3894            };
3895            let needle = eval::eval_expr(lhs, row, ctx).map_err(EngineError::Eval)?;
3896            let contained = match (&needle, &entry.set) {
3897                // Non-empty list + NULL needle → NULL (negation of
3898                // NULL is still NULL).
3899                (Value::Null, _) => return Ok(Value::Null),
3900                (Value::SmallInt(n), memoize::InListSet::Int(s)) => s.contains(&i64::from(*n)),
3901                (Value::Int(n), memoize::InListSet::Int(s)) => s.contains(&i64::from(*n)),
3902                (Value::BigInt(n), memoize::InListSet::Int(s)) => s.contains(n),
3903                (Value::Text(t), memoize::InListSet::Text(s)) => s.contains(t.as_ref()),
3904                // Cross-family needle (e.g. Float vs integer list):
3905                // keep apply_binary's coercion / error behaviour.
3906                _ => return eval::eval_expr(e, row, ctx).map_err(EngineError::Eval),
3907            };
3908            let inner = if contained {
3909                Value::Bool(true)
3910            } else if entry.has_null {
3911                Value::Null
3912            } else {
3913                Value::Bool(false)
3914            };
3915            Ok(match (negated, inner) {
3916                (true, Value::Bool(b)) => Value::Bool(!b),
3917                (_, v) => v,
3918            })
3919        }
3920        _ => eval::eval_expr(e, row, ctx).map_err(EngineError::Eval),
3921    }
3922}
3923
3924fn substitute_outer_columns(stmt: &mut SelectStatement, row: &Row<'static>, ctx: &EvalContext<'_>) {
3925    // v7.24 (round-16 B) — joined outer contexts carry no single
3926    // table alias; their schemas use composite "alias.column" names
3927    // instead. Pass an unmatchable alias and let the composite
3928    // lookup in substitute_in_expr do the work (a correlated EXISTS
3929    // under a JOIN previously skipped substitution entirely and
3930    // died with "unknown table qualifier").
3931    let outer_alias = ctx.table_alias.unwrap_or("");
3932    substitute_in_select(stmt, row, ctx, outer_alias);
3933}
3934
3935fn substitute_in_select(
3936    stmt: &mut SelectStatement,
3937    row: &Row<'static>,
3938    ctx: &EvalContext<'_>,
3939    outer_alias: &str,
3940) {
3941    for item in &mut stmt.items {
3942        if let SelectItem::Expr { expr, .. } = item {
3943            substitute_in_expr(expr, row, ctx, outer_alias);
3944        }
3945    }
3946    if let Some(w) = &mut stmt.where_ {
3947        substitute_in_expr(w, row, ctx, outer_alias);
3948    }
3949    if let Some(gs) = &mut stmt.group_by {
3950        for g in gs {
3951            substitute_in_expr(g, row, ctx, outer_alias);
3952        }
3953    }
3954    if let Some(h) = &mut stmt.having {
3955        substitute_in_expr(h, row, ctx, outer_alias);
3956    }
3957    for o in &mut stmt.order_by {
3958        substitute_in_expr(&mut o.expr, row, ctx, outer_alias);
3959    }
3960    for (_, peer) in &mut stmt.unions {
3961        substitute_in_select(peer, row, ctx, outer_alias);
3962    }
3963}
3964
3965fn substitute_in_expr(e: &mut Expr, row: &Row<'static>, ctx: &EvalContext<'_>, outer_alias: &str) {
3966    // v7.25.2 (round-19 A) — bare synthetic columns. The aggregate
3967    // rewriter replaces group-key references INSIDE subquery bodies
3968    // with `__grp_N` so a correlated subquery in a GROUP BY select
3969    // list can resolve against the synthesised group row. The names
3970    // are engine-generated, so they can't shadow user columns.
3971    if let Expr::Column(c) = e
3972        && c.qualifier.is_none()
3973        && (c.name.starts_with("__grp_") || c.name.starts_with("__agg_"))
3974        && let Some(idx) = ctx.columns.iter().position(|sc| sc.name == c.name)
3975    {
3976        let v = row.values.get(idx).cloned().unwrap_or(Value::Null);
3977        if let Ok(lit) = value_to_literal_expr(v) {
3978            *e = lit;
3979            return;
3980        }
3981    }
3982    if let Expr::Column(c) = e
3983        && let Some(qual) = &c.qualifier
3984    {
3985        // Look up the column's index in the outer schema: plain name
3986        // when the qualifier is the outer table's alias, composite
3987        // "alias.column" for joined outer schemas (v7.24).
3988        let idx = if !outer_alias.is_empty() && qual.eq_ignore_ascii_case(outer_alias) {
3989            ctx.columns
3990                .iter()
3991                .position(|sc| sc.name.eq_ignore_ascii_case(&c.name))
3992        } else {
3993            None
3994        }
3995        .or_else(|| {
3996            let composite = alloc::format!("{qual}.{name}", name = c.name);
3997            ctx.columns
3998                .iter()
3999                .position(|sc| sc.name.eq_ignore_ascii_case(&composite))
4000        });
4001        if let Some(idx) = idx {
4002            let v = row.values.get(idx).cloned().unwrap_or(Value::Null);
4003            if let Ok(lit) = value_to_literal_expr(v) {
4004                *e = lit;
4005                return;
4006            }
4007        }
4008    }
4009    match e {
4010        Expr::AggregateOrdered { call, order_by, .. } => {
4011            substitute_in_expr(call, row, ctx, outer_alias);
4012            for o in order_by.iter_mut() {
4013                substitute_in_expr(&mut o.expr, row, ctx, outer_alias);
4014            }
4015        }
4016        Expr::Binary { lhs, rhs, .. } => {
4017            substitute_in_expr(lhs, row, ctx, outer_alias);
4018            substitute_in_expr(rhs, row, ctx, outer_alias);
4019        }
4020        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
4021            substitute_in_expr(expr, row, ctx, outer_alias);
4022        }
4023        Expr::Like { expr, pattern, .. } => {
4024            substitute_in_expr(expr, row, ctx, outer_alias);
4025            substitute_in_expr(pattern, row, ctx, outer_alias);
4026        }
4027        Expr::FunctionCall { args, .. } => {
4028            for a in args {
4029                substitute_in_expr(a, row, ctx, outer_alias);
4030            }
4031        }
4032        Expr::Extract { source, .. } => substitute_in_expr(source, row, ctx, outer_alias),
4033        Expr::WindowFunction {
4034            args,
4035            partition_by,
4036            order_by,
4037            ..
4038        } => {
4039            for a in args {
4040                substitute_in_expr(a, row, ctx, outer_alias);
4041            }
4042            for p in partition_by {
4043                substitute_in_expr(p, row, ctx, outer_alias);
4044            }
4045            for (o, _, _) in order_by {
4046                substitute_in_expr(o, row, ctx, outer_alias);
4047            }
4048        }
4049        Expr::ScalarSubquery(s) => substitute_in_select(s, row, ctx, outer_alias),
4050        Expr::Exists { subquery, .. } | Expr::InSubquery { subquery, .. } => {
4051            substitute_in_select(subquery, row, ctx, outer_alias);
4052        }
4053        Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => {}
4054        Expr::Array(items) => {
4055            for elem in items {
4056                substitute_in_expr(elem, row, ctx, outer_alias);
4057            }
4058        }
4059        Expr::ArraySubscript { target, index } => {
4060            substitute_in_expr(target, row, ctx, outer_alias);
4061            substitute_in_expr(index, row, ctx, outer_alias);
4062        }
4063        Expr::AnyAll { expr, array, .. } => {
4064            substitute_in_expr(expr, row, ctx, outer_alias);
4065            substitute_in_expr(array, row, ctx, outer_alias);
4066        }
4067        Expr::InList { expr, list, .. } => {
4068            substitute_in_expr(expr, row, ctx, outer_alias);
4069            for item in list {
4070                substitute_in_expr(item, row, ctx, outer_alias);
4071            }
4072        }
4073        Expr::Case {
4074            operand,
4075            branches,
4076            else_branch,
4077        } => {
4078            if let Some(o) = operand {
4079                substitute_in_expr(o, row, ctx, outer_alias);
4080            }
4081            for (w, t) in branches {
4082                substitute_in_expr(w, row, ctx, outer_alias);
4083                substitute_in_expr(t, row, ctx, outer_alias);
4084            }
4085            if let Some(e) = else_branch {
4086                substitute_in_expr(e, row, ctx, outer_alias);
4087            }
4088        }
4089    }
4090}
4091
4092/// Quick scan for any subquery-bearing node in a SELECT's WHERE /
4093/// projection / `order_by` — saves cloning the AST when there are
4094/// none (the common case).
4095pub fn expr_tree_has_subquery(stmt: &SelectStatement) -> bool {
4096    let mut any = false;
4097    for item in &stmt.items {
4098        if let SelectItem::Expr { expr, .. } = item {
4099            any = any || expr_has_subquery(expr);
4100        }
4101    }
4102    if let Some(w) = &stmt.where_ {
4103        any = any || expr_has_subquery(w);
4104    }
4105    if let Some(h) = &stmt.having {
4106        any = any || expr_has_subquery(h);
4107    }
4108    for o in &stmt.order_by {
4109        any = any || expr_has_subquery(&o.expr);
4110    }
4111    for (_, peer) in &stmt.unions {
4112        any = any || expr_tree_has_subquery(peer);
4113    }
4114    any
4115}
4116
4117pub(crate) fn expr_has_subquery(e: &Expr) -> bool {
4118    match e {
4119        Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. } => true,
4120        Expr::AggregateOrdered { call, order_by, .. } => {
4121            expr_has_subquery(call) || order_by.iter().any(|o| expr_has_subquery(&o.expr))
4122        }
4123        Expr::Binary { lhs, rhs, .. } => expr_has_subquery(lhs) || expr_has_subquery(rhs),
4124        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
4125            expr_has_subquery(expr)
4126        }
4127        Expr::FunctionCall { args, .. } => args.iter().any(expr_has_subquery),
4128        Expr::Like { expr, pattern, .. } => expr_has_subquery(expr) || expr_has_subquery(pattern),
4129        Expr::Extract { source, .. } => expr_has_subquery(source),
4130        Expr::WindowFunction {
4131            args,
4132            partition_by,
4133            order_by,
4134            ..
4135        } => {
4136            args.iter().any(expr_has_subquery)
4137                || partition_by.iter().any(expr_has_subquery)
4138                || order_by.iter().any(|(e, _, _)| expr_has_subquery(e))
4139        }
4140        Expr::Literal(_) | Expr::Placeholder(_) | Expr::Column(_) => false,
4141        Expr::Array(items) => items.iter().any(expr_has_subquery),
4142        Expr::ArraySubscript { target, index } => {
4143            expr_has_subquery(target) || expr_has_subquery(index)
4144        }
4145        Expr::AnyAll { expr, array, .. } => expr_has_subquery(expr) || expr_has_subquery(array),
4146        Expr::InList { expr, list, .. } => {
4147            expr_has_subquery(expr) || list.iter().any(expr_has_subquery)
4148        }
4149        Expr::Case {
4150            operand,
4151            branches,
4152            else_branch,
4153        } => {
4154            operand.as_deref().is_some_and(expr_has_subquery)
4155                || branches
4156                    .iter()
4157                    .any(|(w, t)| expr_has_subquery(w) || expr_has_subquery(t))
4158                || else_branch.as_deref().is_some_and(expr_has_subquery)
4159        }
4160    }
4161}