Skip to main content

mongreldb_kit/
query.rs

1//! Query execution for kit statements.
2//!
3//! The implementation is intentionally simple: it materializes every visible row
4//! from the target table(s), evaluates predicates in Rust, then sorts, groups,
5//! joins, and reduces in memory. This keeps the crate independent of MongrelDB
6//! core's native query primitives while remaining correct for the supported
7//! subset.
8//!
9//! ponytail: every operator here is computed in-memory after a full scan; there
10//! is no predicate/projection pushdown into MongrelDB native conditions on the
11//! Rust path. That is the deliberate non-pushdown ceiling — the async SQL engine
12//! path is out of scope for this crate.
13
14use crate::error::{KitError, Result};
15use crate::schema::{core_row_to_json, Row};
16use mongreldb_core::memtable::Row as CoreRow;
17use mongreldb_core::query::Condition;
18use mongreldb_kit_core::query::{
19    AggFunc, Aggregate, AggregateQuery, Direction, Expr, JoinKind, JoinQuery, Literal, OrderBy,
20    Query, Select,
21};
22use mongreldb_kit_core::schema::{Schema as KitSchema, Table as KitTable};
23use serde_json::{Map, Value};
24use std::cmp::Ordering;
25use std::collections::{HashMap, HashSet};
26
27/// A combined join row: a JSON object keyed by table alias whose values are the
28/// matched source rows (or JSON `null` for an unmatched right side of a `LEFT`
29/// join). See [`JoinQuery`] for the documented shape.
30pub type JoinRow = Map<String, Value>;
31
32/// Fetches the visible rows for a real (non-CTE) table at a read snapshot.
33/// When `Some(conditions)` is provided, the fetcher resolves them via native
34/// indexes and returns only matching rows (Kit Priority 1 pushdown).
35type TableFetch<'a> = Box<dyn Fn(&str, Option<&[Condition]>) -> Result<Vec<Row>> + 'a>;
36
37/// Resolves the base rows for a table name and carries materialized CTEs.
38///
39/// CTE materializations shadow real tables, so a later query can read a `with`
40/// result by name. Subqueries and joins evaluate against the same context, which
41/// is how cross-table reads stay consistent at the transaction's read snapshot.
42pub(crate) struct ExecCtx<'a> {
43    schema: Option<&'a KitSchema>,
44    fetch: TableFetch<'a>,
45    ctes: HashMap<String, Vec<Row>>,
46}
47
48impl<'a> ExecCtx<'a> {
49    pub(crate) fn new(
50        schema: Option<&'a KitSchema>,
51        fetch: impl Fn(&str, Option<&[Condition]>) -> Result<Vec<Row>> + 'a,
52    ) -> Self {
53        Self {
54            schema,
55            fetch: Box::new(fetch),
56            ctes: HashMap::new(),
57        }
58    }
59
60    pub(crate) fn add_cte(&mut self, name: String, rows: Vec<Row>) {
61        self.ctes.insert(name, rows);
62    }
63
64    fn table_rows(&self, name: &str) -> Result<Vec<Row>> {
65        if let Some(rows) = self.ctes.get(name) {
66            return Ok(rows.clone());
67        }
68        (self.fetch)(name, None)
69    }
70
71    /// Fetch rows for `name` with native `conditions` pushed to the engine.
72    /// Falls back to an unfiltered fetch for CTEs (which are already
73    /// materialized) or empty conditions.
74    fn table_rows_filtered(&self, name: &str, conditions: &[Condition]) -> Result<Vec<Row>> {
75        if conditions.is_empty() || self.ctes.contains_key(name) {
76            return self.table_rows(name);
77        }
78        (self.fetch)(name, Some(conditions))
79    }
80
81    fn table_def(&self, name: &str) -> Option<&KitTable> {
82        self.schema.and_then(|s| s.table(name))
83    }
84}
85
86/// A name-resolution scope for predicate/value evaluation.
87trait Scope {
88    fn get(&self, name: &str) -> Value;
89}
90
91/// Resolves bare column names against a single flat row.
92struct FlatScope<'a>(&'a Map<String, Value>);
93
94impl Scope for FlatScope<'_> {
95    fn get(&self, name: &str) -> Value {
96        self.0.get(name).cloned().unwrap_or(Value::Null)
97    }
98}
99
100/// Resolves `alias.column` (and, as a fallback, bare column) names against a
101/// combined join row keyed by table alias.
102struct JoinScope<'a>(&'a Map<String, Value>);
103
104impl Scope for JoinScope<'_> {
105    fn get(&self, name: &str) -> Value {
106        if let Some((alias, col)) = name.split_once('.') {
107            return self
108                .0
109                .get(alias)
110                .and_then(|t| t.as_object())
111                .and_then(|o| o.get(col))
112                .cloned()
113                .unwrap_or(Value::Null);
114        }
115        for value in self.0.values() {
116            if let Some(obj) = value.as_object() {
117                if let Some(v) = obj.get(name) {
118                    return v.clone();
119                }
120            }
121        }
122        Value::Null
123    }
124}
125
126// ── public entry points ─────────────────────────────────────────────────────
127
128/// Execute a kit [`Query::Select`] against the supplied visible rows.
129///
130/// `rows` must be the newest visible version of every non-deleted row in the
131/// target table at the transaction's read snapshot. This standalone form cannot
132/// resolve other tables, so subqueries/joins referencing a different table fail.
133pub fn execute_select(
134    table: &KitTable,
135    visible_rows: Vec<CoreRow>,
136    select: &Select,
137) -> Result<Vec<Row>> {
138    let rows: Vec<Row> = visible_rows
139        .into_iter()
140        .map(|r| core_row_to_json(&r, table))
141        .collect::<Result<Vec<_>>>()?;
142    let mut ctx = ExecCtx::new(None, |name: &str, _conds: Option<&[Condition]>| {
143        Err(KitError::Validation(format!(
144            "table {name} is not available outside a transaction context"
145        )))
146    });
147    ctx.add_cte(table.name.clone(), rows);
148    run_select(&ctx, select)
149}
150
151/// Execute any supported kit query statement against visible rows.
152pub fn execute_query(
153    table: &KitTable,
154    visible_rows: Vec<CoreRow>,
155    query: &Query,
156) -> Result<Vec<Row>> {
157    match query {
158        Query::Select(select) => execute_select(table, visible_rows, select),
159        _ => Err(KitError::Validation(
160            "only SELECT queries are supported by execute_query".into(),
161        )),
162    }
163}
164
165// ── select ──────────────────────────────────────────────────────────────────
166
167pub(crate) fn run_select(ctx: &ExecCtx, select: &Select) -> Result<Vec<Row>> {
168    // Kit Priority 1 pushdown: translate the filter into native Conditions so
169    // the engine resolves them via indexes (HOT/bitmap/range) instead of a
170    // full scan. Core conditions return a superset, so the original filter is
171    // re-applied in Rust when the translation was partial.
172    let (mut rows, residual_needed) = match &select.filter {
173        Some(filter) => {
174            let pushed = ctx
175                .table_def(&select.table)
176                .and_then(|t| crate::pushdown::translate_predicate(t, filter));
177            match pushed {
178                Some(plan) if plan.can_push() => {
179                    let fetched = ctx.table_rows_filtered(&select.table, &plan.conditions)?;
180                    (fetched, !plan.fully_translated)
181                }
182                _ => (ctx.table_rows(&select.table)?, true),
183            }
184        }
185        None => (ctx.table_rows(&select.table)?, false),
186    };
187
188    // Residual Rust evaluation for partially-translated predicates.
189    if residual_needed {
190        if let Some(filter) = &select.filter {
191            let mut kept = Vec::with_capacity(rows.len());
192            for r in rows {
193                if eval_pred(filter, &FlatScope(&r.values), ctx)? {
194                    kept.push(r);
195                }
196            }
197            rows = kept;
198        }
199    }
200
201    for order in &select.order_by {
202        sort_rows(ctx, &mut rows, &select.table, order)?;
203    }
204
205    apply_limit_offset(&mut rows, select.limit, select.offset);
206    Ok(rows)
207}
208
209/// Project to `select.columns` (when given) then drop duplicate rows, preserving
210/// first-seen order. Used by `select_distinct`.
211pub(crate) fn project_distinct(select: &Select, rows: Vec<Row>) -> Vec<Row> {
212    let cols: Vec<String> = select
213        .columns
214        .iter()
215        .filter_map(|e| match e {
216            Expr::Column(n) => Some(n.clone()),
217            _ => None,
218        })
219        .collect();
220    let mut seen = HashSet::new();
221    let mut out = Vec::new();
222    for r in rows {
223        let values = if cols.is_empty() {
224            r.values
225        } else {
226            let mut m = Map::new();
227            for c in &cols {
228                m.insert(c.clone(), r.values.get(c).cloned().unwrap_or(Value::Null));
229            }
230            m
231        };
232        let key = serde_json::to_string(&values).unwrap_or_default();
233        if seen.insert(key) {
234            out.push(Row { row_id: 0, values });
235        }
236    }
237    out
238}
239
240// ── aggregates / group by / having ──────────────────────────────────────────
241
242pub(crate) fn run_aggregate(ctx: &ExecCtx, query: &AggregateQuery) -> Result<Vec<Row>> {
243    // Kit Priority 1 pushdown: same strategy as run_select.
244    let (mut rows, residual_needed) = match &query.filter {
245        Some(filter) => {
246            let pushed = ctx
247                .table_def(&query.table)
248                .and_then(|t| crate::pushdown::translate_predicate(t, filter));
249            match pushed {
250                Some(plan) if plan.can_push() => {
251                    let fetched = ctx.table_rows_filtered(&query.table, &plan.conditions)?;
252                    (fetched, !plan.fully_translated)
253                }
254                _ => (ctx.table_rows(&query.table)?, true),
255            }
256        }
257        None => (ctx.table_rows(&query.table)?, false),
258    };
259
260    if residual_needed {
261        if let Some(filter) = &query.filter {
262            let mut kept = Vec::with_capacity(rows.len());
263            for r in rows {
264                if eval_pred(filter, &FlatScope(&r.values), ctx)? {
265                    kept.push(r);
266                }
267            }
268            rows = kept;
269        }
270    }
271
272    // Group rows by the key columns, preserving first-seen group order.
273    let mut groups: Vec<(Vec<Value>, Vec<Row>)> = Vec::new();
274    if query.group_by.is_empty() {
275        groups.push((Vec::new(), rows));
276    } else {
277        let mut index: HashMap<String, usize> = HashMap::new();
278        for r in rows {
279            let key_vals: Vec<Value> = query
280                .group_by
281                .iter()
282                .map(|c| r.values.get(c).cloned().unwrap_or(Value::Null))
283                .collect();
284            let key_str = serde_json::to_string(&key_vals).unwrap_or_default();
285            match index.get(&key_str) {
286                Some(&i) => groups[i].1.push(r),
287                None => {
288                    index.insert(key_str, groups.len());
289                    groups.push((key_vals, vec![r]));
290                }
291            }
292        }
293    }
294
295    let mut out = Vec::with_capacity(groups.len());
296    for (key_vals, group_rows) in groups {
297        let mut values = Map::new();
298        for (col, val) in query.group_by.iter().zip(key_vals.iter()) {
299            values.insert(col.clone(), val.clone());
300        }
301        for agg in &query.aggregates {
302            values.insert(agg.alias.clone(), compute_aggregate(agg, &group_rows)?);
303        }
304        if let Some(having) = &query.having {
305            if !eval_pred(having, &FlatScope(&values), ctx)? {
306                continue;
307            }
308        }
309        out.push(Row { row_id: 0, values });
310    }
311    Ok(out)
312}
313
314fn compute_aggregate(agg: &Aggregate, rows: &[Row]) -> Result<Value> {
315    match agg.func {
316        AggFunc::Count => {
317            let n = match (&agg.column, agg.distinct) {
318                // COUNT(*) — DISTINCT is meaningless without a column.
319                (None, _) => rows.len(),
320                // COUNT(DISTINCT col): unique non-null values.
321                (Some(col), true) => distinct_non_null(rows, col).len(),
322                // COUNT(col): non-null values.
323                (Some(col), false) => rows
324                    .iter()
325                    .filter(|r| !r.values.get(col).map(Value::is_null).unwrap_or(true))
326                    .count(),
327            };
328            Ok(Value::Number((n as i64).into()))
329        }
330        AggFunc::Sum | AggFunc::Avg => {
331            let col = require_agg_column(agg)?;
332            // DISTINCT ⇒ aggregate over the distinct non-null values.
333            let (nums, all_int): (Vec<f64>, bool) = if agg.distinct {
334                let vals = distinct_non_null(rows, col);
335                let all_int = vals.iter().all(|v| v.as_i64().is_some());
336                (vals.iter().filter_map(|&v| num_of(v)).collect(), all_int)
337            } else {
338                let all_int = rows
339                    .iter()
340                    .filter_map(|r| r.values.get(col))
341                    .filter(|v| !v.is_null())
342                    .all(|v| v.as_i64().is_some());
343                (numeric_values(rows, col), all_int)
344            };
345            if nums.is_empty() {
346                return Ok(Value::Null);
347            }
348            let sum: f64 = nums.iter().sum();
349            if matches!(agg.func, AggFunc::Avg) {
350                return Ok(number_value(sum / nums.len() as f64));
351            }
352            // Preserve integer-ness when every summand was an integer.
353            if all_int {
354                Ok(Value::Number((sum as i64).into()))
355            } else {
356                Ok(number_value(sum))
357            }
358        }
359        AggFunc::Min | AggFunc::Max => {
360            let col = require_agg_column(agg)?;
361            let mut best: Option<&Value> = None;
362            for r in rows {
363                let Some(v) = r.values.get(col) else { continue };
364                if v.is_null() {
365                    continue;
366                }
367                best = Some(match best {
368                    None => v,
369                    Some(cur) => {
370                        let take = matches!(
371                            (json_cmp(v, cur), agg.func),
372                            (Some(Ordering::Less), AggFunc::Min)
373                                | (Some(Ordering::Greater), AggFunc::Max)
374                        );
375                        if take {
376                            v
377                        } else {
378                            cur
379                        }
380                    }
381                });
382            }
383            Ok(best.cloned().unwrap_or(Value::Null))
384        }
385    }
386}
387
388fn require_agg_column(agg: &Aggregate) -> Result<&String> {
389    agg.column
390        .as_ref()
391        .ok_or_else(|| KitError::Validation(format!("aggregate {:?} requires a column", agg.func)))
392}
393
394fn numeric_values(rows: &[Row], col: &str) -> Vec<f64> {
395    rows.iter()
396        .filter_map(|r| r.values.get(col))
397        .filter_map(num_of)
398        .collect()
399}
400
401fn num_of(v: &Value) -> Option<f64> {
402    match v {
403        Value::Number(n) => n.as_f64(),
404        _ => None,
405    }
406}
407
408fn number_value(f: f64) -> Value {
409    serde_json::Number::from_f64(f)
410        .map(Value::Number)
411        .unwrap_or(Value::Null)
412}
413
414/// Distinct non-null values of `col` across `rows`, deduplicated by their
415/// canonical JSON form — the same value identity GROUP BY keys use. Backs the
416/// `DISTINCT` aggregates (`COUNT`/`SUM`/`AVG`).
417fn distinct_non_null<'a>(rows: &'a [Row], col: &str) -> Vec<&'a Value> {
418    let mut seen = std::collections::HashSet::new();
419    let mut out = Vec::new();
420    for r in rows {
421        if let Some(v) = r.values.get(col) {
422            if v.is_null() {
423                continue;
424            }
425            if seen.insert(serde_json::to_string(v).unwrap_or_default()) {
426                out.push(v);
427            }
428        }
429    }
430    out
431}
432
433// ── joins ───────────────────────────────────────────────────────────────────
434
435/// When a join `ON` is `right.col = left.col` (column = column) and the right
436/// column has a declared bitmap index, build a `BitmapIn` over the distinct
437/// left-side key values so the right table is fetched by an index probe instead
438/// of a full scan. `eval_pred` still re-checks the predicate for every combined
439/// row, so this only shrinks the candidate set — the result is identical.
440/// Returns `None` (→ full scan) for any non-FK-equality shape, a right column
441/// without a bitmap index, or an empty probe set.
442fn fk_join_condition(
443    right_table: &KitTable,
444    right_alias: &str,
445    on: &Expr,
446    left_rows: &[JoinRow],
447) -> Option<Condition> {
448    let Expr::Eq(a, b) = on else { return None };
449    let (Expr::Column(ca), Expr::Column(cb)) = (a.as_ref(), b.as_ref()) else {
450        return None;
451    };
452    let prefix = format!("{right_alias}.");
453    // Exactly one side must reference the right (joined) alias.
454    let (right_qual, left_qual) = match (ca.starts_with(&prefix), cb.starts_with(&prefix)) {
455        (true, false) => (ca.as_str(), cb.as_str()),
456        (false, true) => (cb.as_str(), ca.as_str()),
457        _ => return None,
458    };
459    let right_col = right_qual.strip_prefix(&prefix)?;
460    if !crate::pushdown::has_declared_bitmap_index(right_table, right_col) {
461        return None;
462    }
463    let col = right_table.column(right_col)?;
464    let (left_alias, left_col) = left_qual.split_once('.')?;
465
466    let mut seen = std::collections::HashSet::new();
467    let mut values = Vec::new();
468    for row in left_rows {
469        let key = row
470            .get(left_alias)
471            .and_then(|t| t.as_object())
472            .and_then(|o| o.get(left_col))
473            .and_then(|v| crate::pushdown::value_index_key(v, col.storage_type));
474        if let Some(key) = key {
475            if seen.insert(key.clone()) {
476                values.push(key);
477            }
478        }
479    }
480    if values.is_empty() {
481        return None;
482    }
483    Some(Condition::BitmapIn {
484        column_id: col.id as u16,
485        values,
486    })
487}
488
489/// Flatten an `Expr::And` chain into its conjuncts. A non-And expr yields a
490/// single-element vec.
491fn conjuncts(expr: &Expr) -> Vec<&Expr> {
492    match expr {
493        Expr::And(parts) => parts.iter().flat_map(conjuncts).collect(),
494        other => vec![other],
495    }
496}
497
498/// Collect every `alias.column` alias referenced in `expr`. Bare column names
499/// contribute no alias (they're ambiguous in a join context).
500fn collect_aliases(expr: &Expr, out: &mut HashSet<String>) {
501    match expr {
502        Expr::Column(name) => {
503            if let Some((alias, _)) = name.split_once('.') {
504                out.insert(alias.to_string());
505            }
506        }
507        Expr::And(parts) | Expr::Or(parts) => {
508            for p in parts {
509                collect_aliases(p, out);
510            }
511        }
512        Expr::Not(e) => collect_aliases(e, out),
513        Expr::Eq(a, b)
514        | Expr::Ne(a, b)
515        | Expr::Gt(a, b)
516        | Expr::Gte(a, b)
517        | Expr::Lt(a, b)
518        | Expr::Lte(a, b) => {
519            collect_aliases(a, out);
520            collect_aliases(b, out);
521        }
522        Expr::In(a, _)
523        | Expr::NotIn(a, _)
524        | Expr::Like(a, _)
525        | Expr::Contains(a, _)
526        | Expr::BytesPrefix(a, _)
527        | Expr::IsNull(a)
528        | Expr::IsNotNull(a) => collect_aliases(a, out),
529        Expr::InSubquery(a, _) => collect_aliases(a, out),
530        Expr::Exists(_) | Expr::NotExists(_) => {
531            out.insert("__subquery__".to_string());
532        }
533        Expr::Literal(_) => {}
534    };
535}
536
537/// If `expr` references columns from exactly one alias (all `alias.column`
538/// for the same alias), return that alias. Conjuncts with bare column names
539/// or subqueries return `None` (conservative — left as post-join residuals).
540fn single_alias(expr: &Expr) -> Option<String> {
541    let mut set = HashSet::new();
542    collect_aliases(expr, &mut set);
543    if set.len() == 1 {
544        return set.into_iter().next();
545    }
546    None
547}
548
549/// Rewrite `alias.column` → `column` in a cloned expression tree, so the
550/// conjunct can be translated against a flat table row / `translate_predicate`.
551fn strip_alias_prefix(expr: &Expr, alias: &str) -> Expr {
552    match expr {
553        Expr::Column(name) => match name.split_once('.') {
554            Some((a, col)) if a == alias => Expr::Column(col.to_string()),
555            _ => Expr::Column(name.clone()),
556        },
557        Expr::And(parts) => Expr::And(parts.iter().map(|p| strip_alias_prefix(p, alias)).collect()),
558        Expr::Or(parts) => Expr::Or(parts.iter().map(|p| strip_alias_prefix(p, alias)).collect()),
559        Expr::Not(e) => Expr::Not(Box::new(strip_alias_prefix(e, alias))),
560        Expr::Eq(a, b) => Expr::Eq(
561            Box::new(strip_alias_prefix(a, alias)),
562            Box::new(strip_alias_prefix(b, alias)),
563        ),
564        Expr::Ne(a, b) => Expr::Ne(
565            Box::new(strip_alias_prefix(a, alias)),
566            Box::new(strip_alias_prefix(b, alias)),
567        ),
568        Expr::Gt(a, b) => Expr::Gt(
569            Box::new(strip_alias_prefix(a, alias)),
570            Box::new(strip_alias_prefix(b, alias)),
571        ),
572        Expr::Gte(a, b) => Expr::Gte(
573            Box::new(strip_alias_prefix(a, alias)),
574            Box::new(strip_alias_prefix(b, alias)),
575        ),
576        Expr::Lt(a, b) => Expr::Lt(
577            Box::new(strip_alias_prefix(a, alias)),
578            Box::new(strip_alias_prefix(b, alias)),
579        ),
580        Expr::Lte(a, b) => Expr::Lte(
581            Box::new(strip_alias_prefix(a, alias)),
582            Box::new(strip_alias_prefix(b, alias)),
583        ),
584        Expr::In(a, list) => Expr::In(Box::new(strip_alias_prefix(a, alias)), list.clone()),
585        Expr::NotIn(a, list) => Expr::NotIn(Box::new(strip_alias_prefix(a, alias)), list.clone()),
586        Expr::IsNull(a) => Expr::IsNull(Box::new(strip_alias_prefix(a, alias))),
587        Expr::IsNotNull(a) => Expr::IsNotNull(Box::new(strip_alias_prefix(a, alias))),
588        Expr::Like(a, pat) => Expr::Like(Box::new(strip_alias_prefix(a, alias)), pat.clone()),
589        Expr::Contains(a, needle) => {
590            Expr::Contains(Box::new(strip_alias_prefix(a, alias)), needle.clone())
591        }
592        Expr::BytesPrefix(a, prefix) => {
593            Expr::BytesPrefix(Box::new(strip_alias_prefix(a, alias)), prefix.clone())
594        }
595        Expr::InSubquery(a, sub) => {
596            Expr::InSubquery(Box::new(strip_alias_prefix(a, alias)), sub.clone())
597        }
598        other => other.clone(),
599    }
600}
601
602/// Split a join filter into per-alias groups. Returns:
603/// - `side_filters`: conjuncts pushable to a single alias (stripped of prefix).
604/// - `residual`: conjuncts that span multiple aliases (or have bare refs /
605///   subqueries) — must be evaluated post-join.
606fn split_join_filter(filter: Option<&Expr>) -> (HashMap<String, Vec<Expr>>, Vec<&Expr>) {
607    let mut side_filters: HashMap<String, Vec<Expr>> = HashMap::new();
608    let mut residual: Vec<&Expr> = Vec::new();
609    let parts = match filter {
610        Some(f) => conjuncts(f),
611        None => return (side_filters, residual),
612    };
613    for c in parts {
614        match single_alias(c) {
615            Some(alias) => {
616                let stripped = strip_alias_prefix(c, &alias);
617                side_filters.entry(alias).or_default().push(stripped);
618            }
619            None => residual.push(c),
620        }
621    }
622    (side_filters, residual)
623}
624
625/// Translate a group of stripped conjuncts for `table` into native Conditions
626/// (for engine pushdown). Returns the combined conditions (may be partial —
627/// the caller should still re-evaluate the conjuncts in Rust).
628fn side_conditions(ctx: &ExecCtx, table: &str, conjuncts: &[Expr]) -> Vec<Condition> {
629    let Some(t) = ctx.table_def(table) else {
630        return Vec::new();
631    };
632    conjuncts
633        .iter()
634        .filter_map(|c| crate::pushdown::translate_predicate(t, c))
635        .flat_map(|p| p.conditions)
636        .collect()
637}
638
639/// Apply a group of stripped conjuncts in Rust against a flat row.
640fn passes_side_filter(conjuncts: &[Expr], row: &Map<String, Value>, ctx: &ExecCtx) -> Result<bool> {
641    if conjuncts.is_empty() {
642        return Ok(true);
643    }
644    let scope = FlatScope(row);
645    for c in conjuncts {
646        if !eval_pred(c, &scope, ctx)? {
647            return Ok(false);
648        }
649    }
650    Ok(true)
651}
652
653pub(crate) fn run_join(ctx: &ExecCtx, query: &JoinQuery) -> Result<Vec<JoinRow>> {
654    let base_alias = query.alias.clone().unwrap_or_else(|| query.table.clone());
655
656    // P5: split the post-join filter into per-alias groups. Single-alias
657    // conjuncts are pushed to the engine (via Conditions) AND re-applied in
658    // Rust on the fetched rows; multi-alias conjuncts stay as post-join
659    // residuals. This avoids materializing rows that the filter would drop.
660    let (side_filters, residual) = split_join_filter(query.filter.as_ref());
661
662    // ── base side ─────────────────────────────────────────────────────────
663    let base_conjuncts = side_filters.get(&base_alias).cloned().unwrap_or_default();
664    let base_conds = side_conditions(ctx, &query.table, &base_conjuncts);
665    let base_rows = if base_conds.is_empty() {
666        ctx.table_rows(&query.table)?
667    } else {
668        ctx.table_rows_filtered(&query.table, &base_conds)?
669    };
670    let mut acc: Vec<JoinRow> = Vec::with_capacity(base_rows.len());
671    for r in base_rows {
672        if passes_side_filter(&base_conjuncts, &r.values, ctx)? {
673            let mut m = Map::new();
674            m.insert(base_alias.clone(), Value::Object(r.values));
675            acc.push(m);
676        }
677    }
678
679    for join in &query.joins {
680        let right_alias = join.alias.clone().unwrap_or_else(|| join.table.clone());
681        // P5: push right-side filter conjuncts alongside the FK-equality probe.
682        let right_conjuncts = side_filters.get(&right_alias).cloned().unwrap_or_default();
683        let mut right_conds = side_conditions(ctx, &join.table, &right_conjuncts);
684        // For an FK-equality ON with a bitmap-indexed right column, probe the
685        // right table by `BitmapIn` over the accumulated left keys instead of a
686        // full scan; otherwise fall back to the full scan.
687        let fk_cond = match (join.kind, join.on.as_ref()) {
688            (JoinKind::Cross, _) | (_, None) => None,
689            (_, Some(on)) => ctx
690                .table_def(&join.table)
691                .and_then(|t| fk_join_condition(t, &right_alias, on, &acc)),
692        };
693        if let Some(c) = fk_cond {
694            right_conds.push(c);
695        }
696        let right_rows_fetched = if right_conds.is_empty() {
697            ctx.table_rows(&join.table)?
698        } else {
699            ctx.table_rows_filtered(&join.table, &right_conds)?
700        };
701        // Apply right-side conjuncts in Rust (handles partial translation).
702        let mut right_rows: Vec<Row> = Vec::with_capacity(right_rows_fetched.len());
703        for r in right_rows_fetched {
704            if passes_side_filter(&right_conjuncts, &r.values, ctx)? {
705                right_rows.push(r);
706            }
707        }
708        let mut next = Vec::new();
709        for left in acc {
710            let mut matched = false;
711            for rr in &right_rows {
712                let mut combined = left.clone();
713                combined.insert(right_alias.clone(), Value::Object(rr.values.clone()));
714                let keep = match join.kind {
715                    JoinKind::Cross => true,
716                    _ => match &join.on {
717                        Some(on) => eval_pred(on, &JoinScope(&combined), ctx)?,
718                        None => true,
719                    },
720                };
721                if keep {
722                    matched = true;
723                    next.push(combined);
724                }
725            }
726            if !matched && join.kind == JoinKind::Left {
727                let mut combined = left;
728                combined.insert(right_alias.clone(), Value::Null);
729                next.push(combined);
730            }
731        }
732        acc = next;
733    }
734
735    // P5: only multi-alias conjuncts remain as post-join residuals.
736    if !residual.is_empty() {
737        let mut kept = Vec::with_capacity(acc.len());
738        for row in acc {
739            let ok = residual.iter().try_fold(true, |acc, c| {
740                eval_pred(c, &JoinScope(&row), ctx).map(|v| acc && v)
741            })?;
742            if ok {
743                kept.push(row);
744            }
745        }
746        acc = kept;
747    }
748
749    for order in &query.order_by {
750        let key = match &order.expr {
751            Expr::Column(n) => n.clone(),
752            other => {
753                return Err(KitError::Validation(format!(
754                    "unsupported order by: {other:?}"
755                )))
756            }
757        };
758        acc.sort_by(|a, b| {
759            let av = JoinScope(a).get(&key);
760            let bv = JoinScope(b).get(&key);
761            let ord = json_cmp(&av, &bv).unwrap_or(Ordering::Equal);
762            match order.direction {
763                Direction::Asc => ord,
764                Direction::Desc => ord.reverse(),
765            }
766        });
767    }
768
769    apply_limit_offset(&mut acc, query.limit, query.offset);
770    Ok(acc)
771}
772
773// ── expression evaluation ───────────────────────────────────────────────────
774
775fn eval_pred<S: Scope>(expr: &Expr, scope: &S, ctx: &ExecCtx) -> Result<bool> {
776    Ok(match expr {
777        Expr::Column(name) => truthy(&scope.get(name)),
778        Expr::Literal(lit) => truthy(&literal_to_value(lit)),
779        Expr::And(parts) => {
780            for p in parts {
781                if !eval_pred(p, scope, ctx)? {
782                    return Ok(false);
783                }
784            }
785            true
786        }
787        Expr::Or(parts) => {
788            for p in parts {
789                if eval_pred(p, scope, ctx)? {
790                    return Ok(true);
791                }
792            }
793            false
794        }
795        Expr::Not(inner) => !eval_pred(inner, scope, ctx)?,
796        Expr::Eq(a, b) => cmp(a, b, scope, ctx)? == Some(Ordering::Equal),
797        Expr::Ne(a, b) => cmp(a, b, scope, ctx)? != Some(Ordering::Equal),
798        Expr::Gt(a, b) => cmp(a, b, scope, ctx)? == Some(Ordering::Greater),
799        Expr::Gte(a, b) => cmp(a, b, scope, ctx)?.is_some_and(|o| o != Ordering::Less),
800        Expr::Lt(a, b) => cmp(a, b, scope, ctx)? == Some(Ordering::Less),
801        Expr::Lte(a, b) => cmp(a, b, scope, ctx)?.is_some_and(|o| o != Ordering::Greater),
802        Expr::In(col, list) => {
803            let v = eval_val(col, scope, ctx)?;
804            list.iter().any(|lit| v == literal_to_value(lit))
805        }
806        Expr::NotIn(col, list) => {
807            let v = eval_val(col, scope, ctx)?;
808            list.iter().all(|lit| v != literal_to_value(lit))
809        }
810        Expr::IsNull(inner) => eval_val(inner, scope, ctx)?.is_null(),
811        Expr::IsNotNull(inner) => !eval_val(inner, scope, ctx)?.is_null(),
812        Expr::Like(col, pattern) => match eval_val(col, scope, ctx)? {
813            Value::String(s) => like(&s, pattern),
814            _ => false,
815        },
816        Expr::Contains(col, needle) => match eval_val(col, scope, ctx)? {
817            Value::String(s) => s.contains(needle.as_str()),
818            _ => false,
819        },
820        Expr::BytesPrefix(col, prefix) => match eval_val(col, scope, ctx)? {
821            // Bytes columns arrive from the engine as a JSON array of byte
822            // numbers; text columns as a string. Treat both as UTF-8 bytes for
823            // the anchored prefix check. (Pushdown is exact in the engine; this
824            // residual only runs when pushdown is bypassed — e.g. a CTE source
825            // — or when the column has no bitmap index.)
826            Value::String(s) => s.as_bytes().starts_with(prefix.as_bytes()),
827            Value::Array(elems) => {
828                bytes_from_json_array(&elems).is_some_and(|b| b.starts_with(prefix.as_bytes()))
829            }
830            _ => false,
831        },
832        Expr::InSubquery(col, sub) => {
833            // ponytail: subqueries are uncorrelated — the sub-SELECT is evaluated
834            // once against the same snapshot/CTEs and cannot reference the outer
835            // row. That covers `id IN (SELECT ...)` and EXISTS-of-a-condition; a
836            // correlated executor is the deferred ceiling.
837            let v = eval_val(col, scope, ctx)?;
838            let key = subquery_column(sub);
839            run_select(ctx, sub)?
840                .iter()
841                .any(|r| subquery_value(r, key.as_deref()) == v)
842        }
843        Expr::Exists(sub) => !run_select(ctx, sub)?.is_empty(),
844        Expr::NotExists(sub) => run_select(ctx, sub)?.is_empty(),
845    })
846}
847
848fn eval_val<S: Scope>(expr: &Expr, scope: &S, _ctx: &ExecCtx) -> Result<Value> {
849    Ok(match expr {
850        Expr::Column(name) => scope.get(name),
851        Expr::Literal(lit) => literal_to_value(lit),
852        other => {
853            return Err(KitError::Validation(format!(
854                "expression {other:?} cannot be used as a scalar value"
855            )))
856        }
857    })
858}
859
860fn cmp<S: Scope>(a: &Expr, b: &Expr, scope: &S, ctx: &ExecCtx) -> Result<Option<Ordering>> {
861    Ok(json_cmp(
862        &eval_val(a, scope, ctx)?,
863        &eval_val(b, scope, ctx)?,
864    ))
865}
866
867/// The column name a subquery exposes for `IN`: the first projected `Column`, or
868/// `None` to fall back to the first value of each result row.
869fn subquery_column(select: &Select) -> Option<String> {
870    select.columns.iter().find_map(|e| match e {
871        Expr::Column(n) => Some(n.clone()),
872        _ => None,
873    })
874}
875
876fn subquery_value(row: &Row, key: Option<&str>) -> Value {
877    match key {
878        Some(k) => row.values.get(k).cloned().unwrap_or(Value::Null),
879        None => row.values.values().next().cloned().unwrap_or(Value::Null),
880    }
881}
882
883fn literal_to_value(lit: &Literal) -> Value {
884    match lit {
885        Literal::Null => Value::Null,
886        Literal::Bool(b) => Value::Bool(*b),
887        Literal::Int(i) => Value::Number((*i).into()),
888        Literal::Float(f) => serde_json::to_value(*f).unwrap_or(Value::Null),
889        Literal::Text(s) => Value::String(s.clone()),
890        Literal::Json(v) => v.clone(),
891    }
892}
893
894fn json_cmp(a: &Value, b: &Value) -> Option<Ordering> {
895    match (a, b) {
896        (Value::Null, Value::Null) => Some(Ordering::Equal),
897        (Value::Null, _) | (_, Value::Null) => None,
898        (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
899        (Value::Number(a), Value::Number(b)) => {
900            if let (Some(ai), Some(bi)) = (a.as_i64(), b.as_i64()) {
901                Some(ai.cmp(&bi))
902            } else {
903                let af = a.as_f64().unwrap_or(f64::NAN);
904                let bf = b.as_f64().unwrap_or(f64::NAN);
905                af.partial_cmp(&bf)
906            }
907        }
908        (Value::String(a), Value::String(b)) => Some(a.cmp(b)),
909        (Value::Array(a), Value::Array(b)) => compare_arrays(a, b),
910        (Value::Object(a), Value::Object(b)) => compare_objects(a, b),
911        _ => None,
912    }
913}
914
915fn compare_arrays(a: &[Value], b: &[Value]) -> Option<Ordering> {
916    let len_cmp = a.len().partial_cmp(&b.len())?;
917    if len_cmp != Ordering::Equal {
918        return Some(len_cmp);
919    }
920    for (x, y) in a.iter().zip(b.iter()) {
921        match json_cmp(x, y) {
922            Some(Ordering::Equal) => {}
923            other => return other,
924        }
925    }
926    Some(Ordering::Equal)
927}
928
929fn compare_objects(a: &Map<String, Value>, b: &Map<String, Value>) -> Option<Ordering> {
930    let len_cmp = a.len().partial_cmp(&b.len())?;
931    if len_cmp != Ordering::Equal {
932        return Some(len_cmp);
933    }
934    let mut a_keys: Vec<&String> = a.keys().collect();
935    let mut b_keys: Vec<&String> = b.keys().collect();
936    a_keys.sort();
937    b_keys.sort();
938    for (ak, bk) in a_keys.iter().zip(b_keys.iter()) {
939        match ak.cmp(bk) {
940            Ordering::Equal => {}
941            other => return Some(other),
942        }
943        let av = a.get(*ak).unwrap();
944        let bv = b.get(*bk).unwrap();
945        match json_cmp(av, bv) {
946            Some(Ordering::Equal) => {}
947            other => return other,
948        }
949    }
950    Some(Ordering::Equal)
951}
952
953fn truthy(v: &Value) -> bool {
954    match v {
955        Value::Null => false,
956        Value::Bool(b) => *b,
957        Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
958        Value::String(s) => !s.is_empty(),
959        Value::Array(a) => !a.is_empty(),
960        Value::Object(o) => !o.is_empty(),
961    }
962}
963
964fn sort_rows(ctx: &ExecCtx, rows: &mut [Row], table: &str, order: &OrderBy) -> Result<()> {
965    let col_name = match &order.expr {
966        Expr::Column(name) => name.clone(),
967        other => {
968            return Err(KitError::Validation(format!(
969                "unsupported order by: {other:?}"
970            )))
971        }
972    };
973    // Validate against the schema when the table is known; CTE/virtual tables are
974    // not in the schema and are sorted leniently (missing values sort as null).
975    if let Some(t) = ctx.table_def(table) {
976        if t.column(&col_name).is_none() {
977            return Err(KitError::Validation(format!(
978                "unknown order column {col_name}"
979            )));
980        }
981    }
982
983    rows.sort_by(|a, b| {
984        let av = a.values.get(&col_name).cloned().unwrap_or(Value::Null);
985        let bv = b.values.get(&col_name).cloned().unwrap_or(Value::Null);
986        let ord = json_cmp(&av, &bv).unwrap_or(Ordering::Equal);
987        match order.direction {
988            Direction::Asc => ord,
989            Direction::Desc => ord.reverse(),
990        }
991    });
992    Ok(())
993}
994
995fn apply_limit_offset<T>(rows: &mut Vec<T>, limit: Option<usize>, offset: Option<usize>) {
996    let offset = offset.unwrap_or(0);
997    if offset > 0 || limit.is_some() {
998        let start = offset.min(rows.len());
999        let end = limit
1000            .map(|l| start + l)
1001            .unwrap_or(rows.len())
1002            .min(rows.len());
1003        *rows = rows.drain(start..end).collect();
1004    }
1005}
1006
1007fn like(text: &str, pattern: &str) -> bool {
1008    let regex = match regex_like(pattern) {
1009        Ok(re) => re,
1010        Err(_) => return false,
1011    };
1012    regex.is_match(text)
1013}
1014
1015/// Decode a bytes column value from its JSON array form (the inverse of
1016/// `to_core_value`'s `ColumnType::Bytes` branch): each element is a byte
1017/// number in 0..=255. Returns `None` on a malformed array.
1018fn bytes_from_json_array(elems: &[Value]) -> Option<Vec<u8>> {
1019    let mut out = Vec::with_capacity(elems.len());
1020    for v in elems {
1021        match v {
1022            Value::Number(n) => out.push(n.as_u64()? as u8),
1023            _ => return None,
1024        }
1025    }
1026    Some(out)
1027}
1028
1029fn regex_like(pattern: &str) -> Result<regex::Regex> {
1030    let mut out = String::with_capacity(pattern.len() * 2);
1031    out.push('^');
1032    for ch in pattern.chars() {
1033        match ch {
1034            '%' => out.push_str(".*"),
1035            '_' => out.push('.'),
1036            c => {
1037                if regex::escape(&c.to_string()).len() > 1 {
1038                    out.push_str(&regex::escape(&c.to_string()));
1039                } else {
1040                    out.push(c);
1041                }
1042            }
1043        }
1044    }
1045    out.push('$');
1046    regex::Regex::new(&out).map_err(|e| KitError::Validation(format!("invalid LIKE pattern: {e}")))
1047}