Skip to main content

powdb_query/
executor.rs

1use crate::ast::*;
2use crate::canonicalize::canonicalize;
3use crate::plan::*;
4use crate::plan_cache::PlanCache;
5use crate::planner;
6use crate::result::QueryResult;
7use powdb_storage::catalog::Catalog;
8use powdb_storage::row::{decode_column, decode_row, patch_var_column_in_place, RowLayout};
9use powdb_storage::types::*;
10use powdb_storage::view::{ViewDef, ViewRegistry};
11use std::cmp::Reverse;
12use std::collections::BinaryHeap;
13use std::io;
14use std::path::Path;
15use std::sync::Mutex;
16use std::time::Instant;
17use tracing::{error, info, Level};
18
19/// Sentinel error returned by `Engine::execute_powql_readonly` when the
20/// query touches a materialized view whose backing table is dirty. The
21/// read path holds only `&self`, so it can't refresh the view — the caller
22/// is expected to recognise this prefix and retry with the write lock.
23///
24/// Mission infra-1: this is the escalation hook between the RwLock reader
25/// fast path and the generic write path. Handlers match on it verbatim.
26pub const READONLY_NEEDS_WRITE: &str = "__POWDB_READONLY_NEEDS_WRITE__";
27
28/// Plan cache capacity. Bench workloads fill ~15 slots; real apps will sit
29/// comfortably in 256. Lookup is O(1), collisions clear the cache (see
30/// `plan_cache::PlanCache::insert`).
31const PLAN_CACHE_CAPACITY: usize = 256;
32
33/// Maximum number of rows a join may produce before the executor aborts.
34/// Prevents Cartesian-product blowups (e.g. `T cross join T` on 10K rows
35/// would produce 100M rows in memory without this cap).
36const MAX_JOIN_ROWS: usize = 1_000_000;
37
38/// Maximum number of rows that may be materialized for sorting.
39/// Queries that exceed this should add a LIMIT clause to narrow the input
40/// before sorting.
41const MAX_SORT_ROWS: usize = 10_000_000;
42
43#[inline]
44fn check_join_limit(row_count: usize) -> Result<(), String> {
45    if row_count > MAX_JOIN_ROWS {
46        return Err(format!("join result exceeds {} row limit", MAX_JOIN_ROWS));
47    }
48    Ok(())
49}
50
51// ─── Mission D11 Phase 1: scalar hot-loop helpers ─────────────────────────
52//
53// These macros expand into the scan body of `agg_single_col_fast` and sit
54// inside the `for_each_row_raw` closure. They exist to:
55//
56//   1. Split the loop on presence of a predicate *outside* the hot body,
57//      so the no-predicate path (agg_sum/agg_min/agg_max bench workloads)
58//      never pays the `Option<CompiledPredicate>` branch per row.
59//   2. Drop two bounds checks per row by reading the null bitmap byte
60//      and the 8-byte value via raw pointer casts.
61//
62// SAFETY (shared across every call site below):
63//
64//   - `$bmp_byte` is `col_idx / 8` where `col_idx < n_cols`, and the row
65//     encoding stores `bitmap_size = n_cols.div_ceil(8)` bytes of bitmap
66//     starting at offset 2. So `2 + $bmp_byte < 2 + bitmap_size ≤ row_len`
67//     and `get_unchecked(2 + $bmp_byte)` is inside the row slice.
68//   - `$off = 2 + bitmap_size + fixed_offsets[col_idx]` for a fixed-size
69//     column. Every fixed-size column contributes `fixed_size(type_id)`
70//     bytes to the fixed region, so the row always has `[$off .. $off+8]`
71//     available for any i64/f64 column — enforced by the row encoder
72//     (`storage/src/row.rs`) and the schema invariant that a row with a
73//     given schema has `row_len ≥ 2 + bitmap_size + fixed_region_size`.
74//   - Both macros are only invoked from `agg_single_col_fast`, which
75//     early-returns if the column isn't Int/Float (8-byte fixed) and
76//     early-returns if `fast.fixed_offsets[col_idx]` is `None`.
77macro_rules! agg_int_loop {
78    (
79        $self:expr, $table:expr, $pred:expr,
80        $bmp_byte:expr, $bmp_bit:expr, $off:expr,
81        |$v:ident : i64| $body:block
82    ) => {{
83        let bmp_byte = $bmp_byte;
84        let bmp_bit = $bmp_bit;
85        let off = $off;
86        if let Some(pred) = &$pred {
87            $self
88                .catalog
89                .for_each_row_raw($table, |_rid, data| {
90                    if !pred(data) {
91                        return;
92                    }
93                    // SAFETY: see module-level comment on agg_int_loop!.
94                    let bmp = unsafe { *data.get_unchecked(2 + bmp_byte) };
95                    if (bmp >> bmp_bit) & 1 == 1 {
96                        return;
97                    }
98                    let $v: i64 =
99                        unsafe { i64::from_le_bytes(*(data.as_ptr().add(off) as *const [u8; 8])) };
100                    $body
101                })
102                .map_err(|e| e.to_string())?;
103        } else {
104            $self
105                .catalog
106                .for_each_row_raw($table, |_rid, data| {
107                    // SAFETY: see module-level comment on agg_int_loop!.
108                    let bmp = unsafe { *data.get_unchecked(2 + bmp_byte) };
109                    if (bmp >> bmp_bit) & 1 == 1 {
110                        return;
111                    }
112                    let $v: i64 =
113                        unsafe { i64::from_le_bytes(*(data.as_ptr().add(off) as *const [u8; 8])) };
114                    $body
115                })
116                .map_err(|e| e.to_string())?;
117        }
118    }};
119}
120
121macro_rules! agg_float_loop {
122    (
123        $self:expr, $table:expr, $pred:expr,
124        $bmp_byte:expr, $bmp_bit:expr, $off:expr,
125        |$v:ident : f64| $body:block
126    ) => {{
127        let bmp_byte = $bmp_byte;
128        let bmp_bit = $bmp_bit;
129        let off = $off;
130        if let Some(pred) = &$pred {
131            $self
132                .catalog
133                .for_each_row_raw($table, |_rid, data| {
134                    if !pred(data) {
135                        return;
136                    }
137                    // SAFETY: see module-level comment on agg_float_loop!.
138                    let bmp = unsafe { *data.get_unchecked(2 + bmp_byte) };
139                    if (bmp >> bmp_bit) & 1 == 1 {
140                        return;
141                    }
142                    let $v: f64 =
143                        unsafe { f64::from_le_bytes(*(data.as_ptr().add(off) as *const [u8; 8])) };
144                    $body
145                })
146                .map_err(|e| e.to_string())?;
147        } else {
148            $self
149                .catalog
150                .for_each_row_raw($table, |_rid, data| {
151                    // SAFETY: see module-level comment on agg_float_loop!.
152                    let bmp = unsafe { *data.get_unchecked(2 + bmp_byte) };
153                    if (bmp >> bmp_bit) & 1 == 1 {
154                        return;
155                    }
156                    let $v: f64 =
157                        unsafe { f64::from_le_bytes(*(data.as_ptr().add(off) as *const [u8; 8])) };
158                    $body
159                })
160                .map_err(|e| e.to_string())?;
161        }
162    }};
163}
164
165/// Mission infra-1: classify a parsed statement as read-only vs. mutating.
166/// Used by [`Engine::execute_powql_readonly`] and by the server handler
167/// to decide between the RwLock reader and writer sides. `Union` recurses
168/// because each side can independently be read/write (though in practice
169/// both sides are reads — the parser only builds Union from query shapes).
170pub fn is_read_only_statement(stmt: &Statement) -> bool {
171    match stmt {
172        Statement::Query(_) => true,
173        Statement::Union(u) => is_read_only_statement(&u.left) && is_read_only_statement(&u.right),
174        Statement::Insert(_)
175        | Statement::Upsert(_)
176        | Statement::UpdateQuery(_)
177        | Statement::DeleteQuery(_)
178        | Statement::CreateType(_)
179        | Statement::AlterTable(_)
180        | Statement::DropTable(_)
181        | Statement::CreateView(_)
182        | Statement::RefreshView(_)
183        | Statement::DropView(_) => false,
184        Statement::Explain(inner) => is_read_only_statement(inner),
185    }
186}
187
188pub struct Engine {
189    catalog: Catalog,
190    /// Mission D9 — cached parsed+planned query trees keyed by canonical
191    /// hash. Saves the ~3μs parse+plan cost on repeat queries that differ
192    /// only in literal values.
193    ///
194    /// Mission infra-1: wrapped in `Mutex` so the read path can be driven
195    /// by `&self`. The critical section is extremely short — a single
196    /// hashmap lookup + plan clone on a hit, or a single insert on a miss.
197    /// A full `RwLock` would be over-engineered here; the contention window
198    /// is smaller than the read-path scan work it gates.
199    plan_cache: Mutex<PlanCache>,
200    /// Mission C Phase 13: reusable `Vec<Value>` scratch buffer for the
201    /// prepared-insert fast path. `execute_prepared` used to allocate a
202    /// fresh `vec![Value::Empty; n_cols]` on every insert; recycling this
203    /// buffer shaves one heap alloc per row on `insert_batch_1k`.
204    insert_values_scratch: Vec<Value>,
205    /// Materialized view registry: tracks view definitions, dependencies,
206    /// and dirty state. Views are backed by regular catalog tables; this
207    /// registry adds the lifecycle metadata.
208    view_registry: ViewRegistry,
209}
210
211/// Mission C Phase 5: a pre-parsed, pre-planned query. The caller holds
212/// one of these and repeatedly executes it with fresh literal values via
213/// [`Engine::execute_prepared`]. This is PowDB's equivalent of SQLite's
214/// `prepare_cached` — the parse + plan cost is paid exactly once, and
215/// every subsequent execution skips the lexer, the canonicalise hash,
216/// and the plan-cache hashmap lookup.
217///
218/// The template plan still contains the literal values from the original
219/// query string. They're overwritten on every call. See `execute_prepared`
220/// for the substitution walk order.
221///
222/// For `PlanNode::Insert` templates whose assignment values are all plain
223/// literals (the common case — `insert T { id := 1, name := "a" }`), we
224/// additionally resolve the column indices at prepare time and stash them
225/// in `insert_col_indices`. That lets `execute_prepared` skip the
226/// plan-clone + substitute walk entirely and build the row directly from
227/// the caller's literal slice — the fastest possible insert through the
228/// query layer.
229#[derive(Clone)]
230pub struct PreparedQuery {
231    plan_template: PlanNode,
232    /// Total number of `Expr::Literal` slots reachable from the plan.
233    /// Callers must supply exactly this many literals per execution.
234    pub param_count: usize,
235    /// Fast-path metadata for `PlanNode::Insert`. `Some` when:
236    ///   * the template is an Insert, and
237    ///   * every assignment RHS is `Expr::Literal(_)` (no computed exprs),
238    ///     which means param_count == assignments.len() and the caller's
239    ///     literal slice maps 1:1 to schema column indices.
240    ///
241    /// Mission C Phase 15: upgraded from a bare `Vec<usize>` to a
242    /// dedicated [`InsertFast`] struct so the execute path can skip the
243    /// second `catalog.schema(table)` HashMap lookup just to read
244    /// `n_cols`, and can dispatch through `get_table_mut` + `tbl.insert`
245    /// instead of going via the generic `catalog.insert` wrapper.
246    insert_fast: Option<InsertFast>,
247    /// Mission C Phase 14: fast-path metadata for point updates by primary
248    /// key — `T filter .pk = <lit> update { col := <lit> }` where `pk` is
249    /// an indexed column and `col` is fixed-size and not indexed. At
250    /// execute time we skip plan clone, substitute walk, schema re-lookup,
251    /// `resolved_assignments` + `FastPatch` + `matching_rids` Vec allocs,
252    /// and the whole `PlanNode::Update` arm. Just a btree lookup and a
253    /// byte patch.
254    update_pk_fast: Option<UpdatePkFast>,
255}
256
257/// Mission C Phase 15: precomputed insert fast-path metadata. Built once
258/// in [`Engine::prepare`] from a `PlanNode::Insert` template whose every
259/// assignment RHS is a raw literal. The execute path reads `n_cols` and
260/// `col_indices` directly — no catalog schema lookup needed.
261#[derive(Clone)]
262struct InsertFast {
263    /// Mission C Phase 18: cached slot index into `Catalog::tables`.
264    /// Resolved once at `prepare` time and stable for the lifetime of
265    /// the catalog (PowDB has no DROP TABLE). Lets the hot path dispatch
266    /// through `catalog.table_by_slot_mut(slot)` — a pure Vec index,
267    /// no hash, no bucket walk, no string compare.
268    table_slot: usize,
269    /// Schema column index for each positional literal, in the order the
270    /// caller passes them.
271    col_indices: Vec<usize>,
272    /// Total number of schema columns — the size `insert_values_scratch`
273    /// must be resized to before filling positions via `col_indices`.
274    /// Cached here so the hot loop skips `catalog.schema(table)` entirely.
275    n_cols: usize,
276}
277
278/// Mission C Phase 14: precomputed fast-path for `update_by_pk` shaped
279/// prepared queries. Built once in [`Engine::prepare`] and reused on every
280/// `execute_prepared` call.
281#[derive(Clone)]
282struct UpdatePkFast {
283    /// Mission C Phase 18: cached slot index into `Catalog::tables`.
284    /// Resolved once at `prepare` time and stable for the lifetime of
285    /// the catalog. At a 52ns total budget the swap from FxHashMap
286    /// probe to a Vec index is measurable.
287    table_slot: usize,
288    /// Name of the key column (the `.id = ?` side). We look this up in
289    /// the owning table's `indexed_cols` at execute time rather than
290    /// caching a raw `&BTree` — the engine owns the catalog and can't
291    /// hand out long-lived borrows anyway, and the n≤5 linear scan is
292    /// a handful of ns.
293    key_col: String,
294    /// Byte offset of the target fixed column in the row encoding:
295    /// `2 + bitmap_size + layout.fixed_offsets[target_col]`.
296    field_off: usize,
297    /// Byte offset of the bitmap byte containing the target column's null
298    /// bit (`2 + target_col / 8`).
299    bitmap_byte_off: usize,
300    /// Bit mask for the target column's null bit.
301    bit_mask: u8,
302    /// Type of the target fixed column — drives the literal-to-bytes
303    /// encoding at execute time.
304    target_type: TypeId,
305    /// Index into the caller's `literals` slice that holds the filter key.
306    /// Always 0 today (filter literal is visited before the assignment
307    /// RHS), but stored explicitly so the contract is obvious.
308    key_literal_idx: usize,
309    /// Index into the caller's `literals` slice that holds the new value.
310    value_literal_idx: usize,
311}
312
313impl Engine {
314    pub fn new(data_dir: &Path) -> io::Result<Self> {
315        std::fs::create_dir_all(data_dir)?;
316        // Try to reopen an existing database first; only create a fresh
317        // catalog when there isn't one already on disk.
318        let catalog = match Catalog::open(data_dir) {
319            Ok(c) => {
320                info!(data_dir = %data_dir.display(), "engine reopened existing database");
321                c
322            }
323            Err(e) if e.kind() == io::ErrorKind::NotFound => {
324                info!(data_dir = %data_dir.display(), "engine initialized fresh database");
325                Catalog::create(data_dir)?
326            }
327            Err(e) => return Err(e),
328        };
329        let view_registry =
330            ViewRegistry::open(data_dir).unwrap_or_else(|_| ViewRegistry::new(data_dir));
331        Ok(Engine {
332            catalog,
333            plan_cache: Mutex::new(PlanCache::new(PLAN_CACHE_CAPACITY)),
334            insert_values_scratch: Vec::new(),
335            view_registry,
336        })
337    }
338
339    /// Parse + plan + execute a PowQL query.
340    ///
341    /// Mission D6 — tracing collapse: the previous implementation ran 4
342    /// `Instant::now()` + 3 `elapsed().as_micros()` calls + formatted an
343    /// `info!` span on every query, even when tracing was disabled. On a
344    /// sub-microsecond `point_lookup_indexed` call that overhead was
345    /// 100-200ns — 20%+ of the whole query. We now measure time only when
346    /// INFO is actually enabled via `tracing::enabled!`, and we moved the
347    /// noisy `debug!(?plan)` line behind the same gate so the Debug
348    /// formatter can't run unconditionally either.
349    ///
350    /// Mission D9 — plan cache: on the hot path we canonicalise the query
351    /// text (lex + FNV-1a hash with literal values stripped), check the
352    /// cache, and on a hit substitute the new literals into a clone of the
353    /// cached plan. This skips re-lexing, re-parsing, and re-planning —
354    /// around 3μs per call on bench workloads. On a miss we plan as before
355    /// and insert the plan under its canonical hash.
356    pub fn execute_powql(&mut self, input: &str) -> Result<QueryResult, String> {
357        // Hot path: tracing disabled. Zero syscalls, zero formatting.
358        if !tracing::enabled!(Level::INFO) {
359            // D9: try the plan cache first. Canonicalisation lexes the
360            // query once; on a hit we skip the parser and planner entirely.
361            if let Ok((hash, literals)) = canonicalize(input) {
362                let cached = self
363                    .plan_cache
364                    .lock()
365                    .map_err(|e| format!("plan cache lock poisoned: {e}"))?
366                    .get_with_substitution(hash, &literals);
367                if let Some(plan) = cached {
368                    let plan = lower_unindexed_range_scans(&self.catalog, &plan);
369                    let result = self.execute_plan(&plan);
370                    // Mission B (post-review): statement-boundary WAL
371                    // group commit. Catalog::wal_log now only appends;
372                    // the fsync happens here exactly once per statement.
373                    // `sync_wal` is a no-op when nothing was buffered
374                    // (pure reads pay zero fsync).
375                    self.catalog.sync_wal().map_err(|e| e.to_string())?;
376                    return result;
377                }
378                // Miss — plan, insert, execute.
379                return match planner::plan(input) {
380                    Ok(plan) => {
381                        self.plan_cache
382                            .lock()
383                            .map_err(|e| format!("plan cache lock poisoned: {e}"))?
384                            .insert(hash, plan.clone());
385                        let plan = lower_unindexed_range_scans(&self.catalog, &plan);
386                        let result = self.execute_plan(&plan);
387                        self.catalog.sync_wal().map_err(|e| e.to_string())?;
388                        result
389                    }
390                    Err(e) => Err(e.to_string()),
391                };
392            }
393            // Lex error — fall through to the planner so the caller gets a
394            // consistent error shape.
395            return match planner::plan(input) {
396                Ok(plan) => {
397                    let plan = lower_unindexed_range_scans(&self.catalog, &plan);
398                    let result = self.execute_plan(&plan);
399                    self.catalog.sync_wal().map_err(|e| e.to_string())?;
400                    result
401                }
402                Err(e) => Err(e.to_string()),
403            };
404        }
405
406        // Instrumented path — only taken under explicit tracing subscribers.
407        let total_start = Instant::now();
408        let plan_start = Instant::now();
409        let plan = planner::plan(input).map_err(|e| {
410            error!(query = %input, error = %e.to_string(), "query plan failed");
411            e.to_string()
412        })?;
413        let plan_us = plan_start.elapsed().as_micros();
414
415        let exec_start = Instant::now();
416        let plan = lower_unindexed_range_scans(&self.catalog, &plan);
417        let result = self.execute_plan(&plan);
418        // Mission B (post-review): statement-boundary WAL flush.
419        let _ = self.catalog.sync_wal();
420        let exec_us = exec_start.elapsed().as_micros();
421
422        let total_us = total_start.elapsed().as_micros();
423        match &result {
424            Ok(r) => {
425                info!(
426                    query = %input,
427                    plan_us = plan_us,
428                    exec_us = exec_us,
429                    total_us = total_us,
430                    rows = r.row_count(),
431                    "query ok"
432                );
433            }
434            Err(e) => {
435                error!(
436                    query = %input,
437                    plan_us = plan_us,
438                    exec_us = exec_us,
439                    error = %e,
440                    "query failed"
441                );
442            }
443        }
444        result
445    }
446
447    /// Plan cache stats — useful for benches and debugging.
448    pub fn plan_cache_stats(&self) -> (u64, u64, usize) {
449        let cache = self.plan_cache.lock().unwrap();
450        (cache.hits, cache.misses, cache.len())
451    }
452
453    /// Mission infra-1: read-only entry point.
454    ///
455    /// Parses + plans + executes a PowQL query using only a shared borrow
456    /// on the engine. Rejects any statement that would mutate state
457    /// (Insert/Update/Delete/CreateTable/AlterTable/DropTable/CreateView/
458    /// RefreshView/DropView) by returning [`READONLY_NEEDS_WRITE`] so the
459    /// caller can escalate to the write lock.
460    ///
461    /// Also returns [`READONLY_NEEDS_WRITE`] if a materialized view in the
462    /// query is dirty — refreshing one requires `&mut self`, so the caller
463    /// must retake the write lock for the first refresh.
464    ///
465    /// This method is the concurrent-read fast path behind
466    /// `Arc<RwLock<Engine>>`: multiple threads can call it simultaneously
467    /// under a shared `.read()` lock and each will scan independently.
468    pub fn execute_powql_readonly(&self, input: &str) -> Result<QueryResult, String> {
469        // Parse the statement first so we can classify read vs. write
470        // without touching the catalog. This is the same lex+parse cost
471        // the hot path would pay anyway.
472        let stmt = crate::parser::parse(input).map_err(|e| e.to_string())?;
473        if !is_read_only_statement(&stmt) {
474            return Err(READONLY_NEEDS_WRITE.to_string());
475        }
476
477        // Try the plan cache first — identical hash scheme to
478        // `execute_powql` so both paths share cache state. The mutex
479        // section is just a hashmap lookup + plan clone.
480        if let Ok((hash, literals)) = canonicalize(input) {
481            let cached = self
482                .plan_cache
483                .lock()
484                .map_err(|e| format!("plan cache lock poisoned: {e}"))?
485                .get_with_substitution(hash, &literals);
486            if let Some(plan) = cached {
487                let plan = lower_unindexed_range_scans(&self.catalog, &plan);
488                return self.execute_plan_readonly(&plan);
489            }
490            // Miss: plan + insert + execute. The planner is pure, so this
491            // is safe from `&self`.
492            let plan = crate::planner::plan_statement(stmt).map_err(|e| e.to_string())?;
493            self.plan_cache
494                .lock()
495                .map_err(|e| format!("plan cache lock poisoned: {e}"))?
496                .insert(hash, plan.clone());
497            let plan = lower_unindexed_range_scans(&self.catalog, &plan);
498            return self.execute_plan_readonly(&plan);
499        }
500        // Lex error — fall through to the planner for a consistent error
501        // shape (though `parse` above would usually have caught it).
502        let plan = crate::planner::plan_statement(stmt).map_err(|e| e.to_string())?;
503        let plan = lower_unindexed_range_scans(&self.catalog, &plan);
504        self.execute_plan_readonly(&plan)
505    }
506
507    /// Read-only version of [`Engine::execute_plan`]. Dispatches the
508    /// read-path plan variants by calling `&self` helpers and errors with
509    /// [`READONLY_NEEDS_WRITE`] on any write variant. This is the
510    /// recursion target for composite read plans under the RwLock reader.
511    ///
512    /// The dispatch mirrors `execute_plan` for the read branches but does
513    /// not carry any of the fast-paths that need `&mut self` (e.g. plan-
514    /// cache mutation on inner subqueries is handled via the shared mutex
515    /// in [`Engine::execute_powql_readonly`]; in-flight subquery
516    /// materialisation uses [`Engine::materialize_subqueries_readonly`]).
517    fn execute_plan_readonly(&self, plan: &PlanNode) -> Result<QueryResult, String> {
518        match plan {
519            PlanNode::SeqScan { table } => {
520                // Dirty view means we'd need to refresh it — can't do that
521                // under `&self`. Escalate to the write path.
522                if self.view_registry.is_dirty(table) {
523                    return Err(READONLY_NEEDS_WRITE.to_string());
524                }
525                let schema = self
526                    .catalog
527                    .schema(table)
528                    .ok_or_else(|| format!("table '{table}' not found"))?
529                    .clone();
530                let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
531                let rows: Vec<Vec<Value>> = self
532                    .catalog
533                    .scan(table)
534                    .map_err(|e| e.to_string())?
535                    .map(|(_, row)| row)
536                    .collect();
537                Ok(QueryResult::Rows { columns, rows })
538            }
539
540            PlanNode::AliasScan { table, alias } => {
541                let schema = self
542                    .catalog
543                    .schema(table)
544                    .ok_or_else(|| format!("table '{table}' not found"))?
545                    .clone();
546                let columns: Vec<String> = schema
547                    .columns
548                    .iter()
549                    .map(|c| format!("{alias}.{}", c.name))
550                    .collect();
551                let rows: Vec<Vec<Value>> = self
552                    .catalog
553                    .scan(table)
554                    .map_err(|e| e.to_string())?
555                    .map(|(_, row)| row)
556                    .collect();
557                Ok(QueryResult::Rows { columns, rows })
558            }
559
560            PlanNode::IndexScan { table, column, key } => {
561                let schema = self
562                    .catalog
563                    .schema(table)
564                    .ok_or_else(|| format!("table '{table}' not found"))?
565                    .clone();
566                let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
567                let key_value = literal_to_value(key)?;
568                let tbl = self
569                    .catalog
570                    .get_table(table)
571                    .ok_or_else(|| format!("table '{table}' not found"))?;
572
573                if let Some(btree) = tbl.index(column) {
574                    let hit = match &key_value {
575                        Value::Int(k) => btree.lookup_int(*k),
576                        other => btree.lookup(other),
577                    };
578                    let rows = match hit {
579                        Some(rid) => match tbl.heap.get(rid) {
580                            Some(data) => vec![decode_row(&tbl.schema, &data)],
581                            None => Vec::new(),
582                        },
583                        None => Vec::new(),
584                    };
585                    return Ok(QueryResult::Rows { columns, rows });
586                }
587
588                // No index: synthetic eq predicate + compiled scan.
589                let fast = FastLayout::new(&schema);
590                let synth_pred = Expr::BinaryOp(
591                    Box::new(Expr::Field(column.clone())),
592                    BinOp::Eq,
593                    Box::new(key.clone()),
594                );
595                if let Some(compiled) = compile_predicate(&synth_pred, &columns, &fast, &schema) {
596                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
597                    self.catalog
598                        .for_each_row_raw(table, |_rid, data| {
599                            if compiled(data) {
600                                rows.push(decode_row(&schema, data));
601                            }
602                        })
603                        .map_err(|e| e.to_string())?;
604                    return Ok(QueryResult::Rows { columns, rows });
605                }
606
607                // Last resort: slow eq-check.
608                let col_idx = schema
609                    .column_index(column)
610                    .ok_or_else(|| format!("column '{column}' not found"))?;
611                let rows: Vec<Vec<Value>> = tbl
612                    .scan()
613                    .filter_map(|(_, row)| {
614                        if row[col_idx] == key_value {
615                            Some(row)
616                        } else {
617                            None
618                        }
619                    })
620                    .collect();
621                Ok(QueryResult::Rows { columns, rows })
622            }
623
624            PlanNode::RangeScan {
625                table,
626                column,
627                start,
628                end,
629            } => {
630                let tbl = self
631                    .catalog
632                    .get_table(table)
633                    .ok_or_else(|| format!("table '{table}' not found"))?;
634                let columns: Vec<String> =
635                    tbl.schema.columns.iter().map(|c| c.name.clone()).collect();
636                let schema = tbl.schema.clone();
637
638                let start_val = match start {
639                    Some((expr, _)) => Some(literal_to_value(expr)?),
640                    None => None,
641                };
642                let end_val = match end {
643                    Some((expr, _)) => Some(literal_to_value(expr)?),
644                    None => None,
645                };
646                let start_inclusive = start.as_ref().map(|(_, inc)| *inc).unwrap_or(true);
647                let end_inclusive = end.as_ref().map(|(_, inc)| *inc).unwrap_or(true);
648
649                if let Some(btree) = tbl.index(column) {
650                    let hits: Vec<(Value, RowId)> = match (&start_val, &end_val) {
651                        (Some(s), Some(e)) => btree.range(s, e).collect(),
652                        (Some(s), None) => btree.range_from(s),
653                        (None, Some(e)) => btree.range_to(e),
654                        (None, None) => {
655                            // Unbounded both sides — equivalent to seq scan.
656                            let rows: Vec<Vec<Value>> = tbl.scan().map(|(_, row)| row).collect();
657                            return Ok(QueryResult::Rows { columns, rows });
658                        }
659                    };
660                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(hits.len());
661                    for (key, rid) in hits {
662                        // Filter for exclusive bounds.
663                        if !start_inclusive {
664                            if let Some(ref s) = start_val {
665                                if &key == s {
666                                    continue;
667                                }
668                            }
669                        }
670                        if !end_inclusive {
671                            if let Some(ref e) = end_val {
672                                if &key == e {
673                                    continue;
674                                }
675                            }
676                        }
677                        if let Some(data) = tbl.heap.get(rid) {
678                            rows.push(decode_row(&schema, &data));
679                        }
680                    }
681                    return Ok(QueryResult::Rows { columns, rows });
682                }
683
684                // Fallback: no index — synthesize the range predicate and scan.
685                let fast = FastLayout::new(&schema);
686                let synth = synthesize_range_predicate(column, start, end);
687                if let Some(compiled) = compile_predicate(&synth, &columns, &fast, &schema) {
688                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
689                    self.catalog
690                        .for_each_row_raw(table, |_rid, data| {
691                            if compiled(data) {
692                                rows.push(decode_row(&schema, data));
693                            }
694                        })
695                        .map_err(|e| e.to_string())?;
696                    return Ok(QueryResult::Rows { columns, rows });
697                }
698
699                // Last resort: decoded row eval.
700                let col_idx = schema
701                    .column_index(column)
702                    .ok_or_else(|| format!("column '{column}' not found"))?;
703                let rows: Vec<Vec<Value>> = tbl
704                    .scan()
705                    .filter(|(_, row)| {
706                        range_matches(
707                            &row[col_idx],
708                            &start_val,
709                            start_inclusive,
710                            &end_val,
711                            end_inclusive,
712                        )
713                    })
714                    .map(|(_, row)| row)
715                    .collect();
716                Ok(QueryResult::Rows { columns, rows })
717            }
718
719            PlanNode::Filter { input, predicate } => {
720                // Materialise subqueries using the `&self` variant.
721                // Uncorrelated subqueries are replaced with InList/Bool;
722                // correlated ones are left as InSubquery/ExistsSubquery
723                // for per-row materialisation below.
724                let materialized;
725                let predicate = if contains_subquery(predicate) {
726                    materialized = self.materialize_subqueries_readonly(predicate)?;
727                    &materialized
728                } else {
729                    predicate
730                };
731
732                // Correlated subquery path: per-row materialisation.
733                if contains_subquery(predicate) {
734                    let result = self.execute_plan_readonly(input)?;
735                    return match result {
736                        QueryResult::Rows { columns, rows } => {
737                            let mut filtered = Vec::new();
738                            for row in rows {
739                                let row_pred = self.materialize_correlated_for_row_readonly(
740                                    predicate, &row, &columns,
741                                )?;
742                                if eval_predicate(&row_pred, &row, &columns) {
743                                    filtered.push(row);
744                                }
745                            }
746                            Ok(QueryResult::Rows {
747                                columns,
748                                rows: filtered,
749                            })
750                        }
751                        _ => Err("filter requires row input".into()),
752                    };
753                }
754
755                // Fused Filter+SeqScan fast path.
756                if let PlanNode::SeqScan { table } = input.as_ref() {
757                    if self.view_registry.is_dirty(table) {
758                        return Err(READONLY_NEEDS_WRITE.to_string());
759                    }
760                    let schema = self
761                        .catalog
762                        .schema(table)
763                        .ok_or_else(|| format!("table '{table}' not found"))?
764                        .clone();
765                    let columns: Vec<String> =
766                        schema.columns.iter().map(|c| c.name.clone()).collect();
767                    let fast = FastLayout::new(&schema);
768                    let row_layout = RowLayout::new(&schema);
769                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
770
771                    if let Some(compiled) = compile_predicate(predicate, &columns, &fast, &schema) {
772                        self.catalog
773                            .for_each_row_raw(table, |_rid, data| {
774                                if compiled(data) {
775                                    rows.push(decode_row(&schema, data));
776                                }
777                            })
778                            .map_err(|e| e.to_string())?;
779                    } else {
780                        let pred_cols = predicate_column_indices(predicate, &columns);
781                        self.catalog
782                            .for_each_row_raw(table, |_rid, data| {
783                                let pred_row =
784                                    decode_selective(&schema, &row_layout, data, &pred_cols);
785                                if eval_predicate(predicate, &pred_row, &columns) {
786                                    rows.push(decode_row(&schema, data));
787                                }
788                            })
789                            .map_err(|e| e.to_string())?;
790                    }
791
792                    return Ok(QueryResult::Rows { columns, rows });
793                }
794
795                // General path.
796                let result = self.execute_plan_readonly(input)?;
797                match result {
798                    QueryResult::Rows { columns, rows } => {
799                        let filtered: Vec<Vec<Value>> = rows
800                            .into_iter()
801                            .filter(|row| eval_predicate(predicate, row, &columns))
802                            .collect();
803                        Ok(QueryResult::Rows {
804                            columns,
805                            rows: filtered,
806                        })
807                    }
808                    _ => Err("filter requires row input".into()),
809                }
810            }
811
812            PlanNode::Project { input, fields } => {
813                // Fast path: Project over IndexScan. Avoids full-row decode
814                // by calling decode_column only for projected fields.
815                if let PlanNode::IndexScan { table, column, key } = input.as_ref() {
816                    let key_value = literal_to_value(key)?;
817                    let tbl = self
818                        .catalog
819                        .get_table(table)
820                        .ok_or_else(|| format!("table '{table}' not found"))?;
821                    let schema = &tbl.schema;
822                    let layout = tbl.row_layout();
823
824                    let proj_columns: Vec<String> = fields
825                        .iter()
826                        .map(|f| {
827                            f.alias.clone().unwrap_or_else(|| match &f.expr {
828                                Expr::Field(name) => name.clone(),
829                                _ => "?".into(),
830                            })
831                        })
832                        .collect();
833
834                    let proj_indices: Vec<usize> = fields
835                        .iter()
836                        .filter_map(|f| {
837                            if let Expr::Field(name) = &f.expr {
838                                schema.column_index(name)
839                            } else {
840                                None
841                            }
842                        })
843                        .collect();
844
845                    if let Some(btree) = tbl.index(column) {
846                        let lookup_result = match &key_value {
847                            Value::Int(k) => btree.lookup_int(*k),
848                            other => btree.lookup(other),
849                        };
850                        let rows = match lookup_result {
851                            Some(rid) => match tbl.heap.get(rid) {
852                                Some(data) => {
853                                    let row: Vec<Value> = proj_indices
854                                        .iter()
855                                        .map(|&ci| decode_column(schema, layout, &data, ci))
856                                        .collect();
857                                    vec![row]
858                                }
859                                None => Vec::new(),
860                            },
861                            None => Vec::new(),
862                        };
863                        return Ok(QueryResult::Rows {
864                            columns: proj_columns,
865                            rows,
866                        });
867                    }
868                }
869
870                // Fast paths over Limit(Sort(...)) / Limit(Filter(...)) / Limit(SeqScan).
871                if let PlanNode::Limit {
872                    input: inner,
873                    count: limit_expr,
874                } = input.as_ref()
875                {
876                    if let PlanNode::Sort {
877                        input: sort_input,
878                        keys,
879                    } = inner.as_ref()
880                    {
881                        if keys.len() == 1 {
882                            let sort_field = &keys[0].field;
883                            let descending = keys[0].descending;
884                            let limit = match limit_expr {
885                                Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
886                                _ => usize::MAX,
887                            };
888                            let (table_opt, pred_opt): (Option<&str>, Option<&Expr>) =
889                                match sort_input.as_ref() {
890                                    PlanNode::SeqScan { table } => (Some(table.as_str()), None),
891                                    PlanNode::Filter {
892                                        input: fi,
893                                        predicate,
894                                    } => {
895                                        if let PlanNode::SeqScan { table } = fi.as_ref() {
896                                            (Some(table.as_str()), Some(predicate))
897                                        } else {
898                                            (None, None)
899                                        }
900                                    }
901                                    _ => (None, None),
902                                };
903                            if let Some(table) = table_opt {
904                                if let Some(result) = self.project_filter_sort_limit_fast(
905                                    table, fields, sort_field, descending, limit, pred_opt,
906                                )? {
907                                    return Ok(result);
908                                }
909                            }
910                        }
911                    }
912                    if let PlanNode::Filter {
913                        input: fi,
914                        predicate,
915                    } = inner.as_ref()
916                    {
917                        if let PlanNode::SeqScan { table } = fi.as_ref() {
918                            let limit = match limit_expr {
919                                Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
920                                _ => usize::MAX,
921                            };
922                            if let Some(result) = self.project_filter_limit_fast(
923                                table,
924                                fields,
925                                limit,
926                                Some(predicate),
927                            )? {
928                                return Ok(result);
929                            }
930                        }
931                    }
932                    if let PlanNode::SeqScan { table } = inner.as_ref() {
933                        let limit = match limit_expr {
934                            Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
935                            _ => usize::MAX,
936                        };
937                        if let Some(result) =
938                            self.project_filter_limit_fast(table, fields, limit, None)?
939                        {
940                            return Ok(result);
941                        }
942                    }
943                }
944
945                // Project(Filter(SeqScan)) without Limit.
946                if let PlanNode::Filter {
947                    input: fi,
948                    predicate,
949                } = input.as_ref()
950                {
951                    if let PlanNode::SeqScan { table } = fi.as_ref() {
952                        if let Some(result) = self.project_filter_limit_fast(
953                            table,
954                            fields,
955                            usize::MAX,
956                            Some(predicate),
957                        )? {
958                            return Ok(result);
959                        }
960                    }
961                }
962
963                // Project(SeqScan) without Filter or Limit.
964                if let PlanNode::SeqScan { table } = input.as_ref() {
965                    if let Some(result) =
966                        self.project_filter_limit_fast(table, fields, usize::MAX, None)?
967                    {
968                        return Ok(result);
969                    }
970                }
971
972                // Generic path.
973                let result = self.execute_plan_readonly(input)?;
974                match result {
975                    QueryResult::Rows { columns, rows } => {
976                        let proj_columns: Vec<String> = fields
977                            .iter()
978                            .map(|f| {
979                                f.alias.clone().unwrap_or_else(|| match &f.expr {
980                                    Expr::Field(name) => name.clone(),
981                                    Expr::QualifiedField { qualifier, field } => {
982                                        format!("{qualifier}.{field}")
983                                    }
984                                    _ => "?".into(),
985                                })
986                            })
987                            .collect();
988                        let proj_rows: Vec<Vec<Value>> = rows
989                            .iter()
990                            .map(|row| {
991                                fields
992                                    .iter()
993                                    .map(|f| eval_expr(&f.expr, row, &columns))
994                                    .collect()
995                            })
996                            .collect();
997                        Ok(QueryResult::Rows {
998                            columns: proj_columns,
999                            rows: proj_rows,
1000                        })
1001                    }
1002                    _ => Err("project requires row input".into()),
1003                }
1004            }
1005
1006            PlanNode::Sort { input, keys } => {
1007                let result = self.execute_plan_readonly(input)?;
1008                match result {
1009                    QueryResult::Rows { columns, mut rows } => {
1010                        if rows.len() > MAX_SORT_ROWS {
1011                            return Err(format!(
1012                                "sort input exceeds {} row limit — add a LIMIT clause",
1013                                MAX_SORT_ROWS
1014                            ));
1015                        }
1016                        let key_indices: Vec<(usize, bool)> = keys
1017                            .iter()
1018                            .map(|k| {
1019                                columns
1020                                    .iter()
1021                                    .position(|c| c == &k.field)
1022                                    .map(|idx| (idx, k.descending))
1023                                    .ok_or_else(|| format!("column '{}' not found", k.field))
1024                            })
1025                            .collect::<Result<_, String>>()?;
1026                        rows.sort_by(|a, b| {
1027                            for &(col_idx, descending) in &key_indices {
1028                                let cmp = a[col_idx].cmp(&b[col_idx]);
1029                                let cmp = if descending { cmp.reverse() } else { cmp };
1030                                if cmp != std::cmp::Ordering::Equal {
1031                                    return cmp;
1032                                }
1033                            }
1034                            std::cmp::Ordering::Equal
1035                        });
1036                        Ok(QueryResult::Rows { columns, rows })
1037                    }
1038                    _ => Err("sort requires row input".into()),
1039                }
1040            }
1041
1042            PlanNode::Limit { input, count } => {
1043                let result = self.execute_plan_readonly(input)?;
1044                let n = match count {
1045                    Expr::Literal(Literal::Int(v)) => *v as usize,
1046                    _ => return Err("limit must be integer literal".into()),
1047                };
1048                match result {
1049                    QueryResult::Rows { columns, rows } => Ok(QueryResult::Rows {
1050                        columns,
1051                        rows: rows.into_iter().take(n).collect(),
1052                    }),
1053                    _ => Err("limit requires row input".into()),
1054                }
1055            }
1056
1057            PlanNode::Offset { input, count } => {
1058                let result = self.execute_plan_readonly(input)?;
1059                let n = match count {
1060                    Expr::Literal(Literal::Int(v)) => *v as usize,
1061                    _ => return Err("offset must be integer literal".into()),
1062                };
1063                match result {
1064                    QueryResult::Rows { columns, rows } => Ok(QueryResult::Rows {
1065                        columns,
1066                        rows: rows.into_iter().skip(n).collect(),
1067                    }),
1068                    _ => Err("offset requires row input".into()),
1069                }
1070            }
1071
1072            PlanNode::Aggregate {
1073                input,
1074                function,
1075                field,
1076            } => {
1077                // Fast path: count() over SeqScan.
1078                if *function == AggFunc::Count {
1079                    if let PlanNode::SeqScan { table } = input.as_ref() {
1080                        let mut count: i64 = 0;
1081                        self.catalog
1082                            .for_each_row_raw(table, |_rid, _data| {
1083                                count += 1;
1084                            })
1085                            .map_err(|e| e.to_string())?;
1086                        return Ok(QueryResult::Scalar(Value::Int(count)));
1087                    }
1088                    if let PlanNode::Filter {
1089                        input: inner,
1090                        predicate,
1091                    } = input.as_ref()
1092                    {
1093                        if let PlanNode::SeqScan { table } = inner.as_ref() {
1094                            let schema = self
1095                                .catalog
1096                                .schema(table)
1097                                .ok_or_else(|| format!("table '{table}' not found"))?
1098                                .clone();
1099                            let columns: Vec<String> =
1100                                schema.columns.iter().map(|c| c.name.clone()).collect();
1101                            let fast = FastLayout::new(&schema);
1102                            let row_layout = RowLayout::new(&schema);
1103
1104                            if let Some(compiled) =
1105                                compile_predicate(predicate, &columns, &fast, &schema)
1106                            {
1107                                let mut count: i64 = 0;
1108                                self.catalog
1109                                    .for_each_row_raw(table, |_rid, data| {
1110                                        if compiled(data) {
1111                                            count += 1;
1112                                        }
1113                                    })
1114                                    .map_err(|e| e.to_string())?;
1115                                return Ok(QueryResult::Scalar(Value::Int(count)));
1116                            }
1117
1118                            let pred_cols = predicate_column_indices(predicate, &columns);
1119                            let mut count: i64 = 0;
1120                            self.catalog
1121                                .for_each_row_raw(table, |_rid, data| {
1122                                    let pred_row =
1123                                        decode_selective(&schema, &row_layout, data, &pred_cols);
1124                                    if eval_predicate(predicate, &pred_row, &columns) {
1125                                        count += 1;
1126                                    }
1127                                })
1128                                .map_err(|e| e.to_string())?;
1129                            return Ok(QueryResult::Scalar(Value::Int(count)));
1130                        }
1131                    }
1132                }
1133
1134                // Fast path: sum/avg/min/max over single fixed-size numeric.
1135                if matches!(
1136                    function,
1137                    AggFunc::Sum
1138                        | AggFunc::Avg
1139                        | AggFunc::Min
1140                        | AggFunc::Max
1141                        | AggFunc::CountDistinct
1142                ) {
1143                    if let Some(col) = field.as_ref() {
1144                        let (table_opt, pred_opt): (Option<&str>, Option<&Expr>) =
1145                            match input.as_ref() {
1146                                PlanNode::SeqScan { table } => (Some(table.as_str()), None),
1147                                PlanNode::Filter {
1148                                    input: inner,
1149                                    predicate,
1150                                } => {
1151                                    if let PlanNode::SeqScan { table } = inner.as_ref() {
1152                                        (Some(table.as_str()), Some(predicate))
1153                                    } else {
1154                                        (None, None)
1155                                    }
1156                                }
1157                                _ => (None, None),
1158                            };
1159                        if let Some(table) = table_opt {
1160                            if let Some(result) =
1161                                self.agg_single_col_fast(table, col, *function, pred_opt)?
1162                            {
1163                                return Ok(result);
1164                            }
1165                        }
1166                    }
1167                }
1168
1169                // Generic path.
1170                let result = self.execute_plan_readonly(input)?;
1171                match result {
1172                    QueryResult::Rows { columns, rows } => match function {
1173                        AggFunc::Count => Ok(QueryResult::Scalar(Value::Int(rows.len() as i64))),
1174                        AggFunc::CountDistinct => {
1175                            let col = field.as_ref().ok_or("count distinct requires field")?;
1176                            let idx = columns
1177                                .iter()
1178                                .position(|c| c == col)
1179                                .ok_or("col not found")?;
1180                            let mut seen = std::collections::HashSet::new();
1181                            for row in &rows {
1182                                let v = &row[idx];
1183                                if !v.is_empty() {
1184                                    seen.insert(v.clone());
1185                                }
1186                            }
1187                            Ok(QueryResult::Scalar(Value::Int(seen.len() as i64)))
1188                        }
1189                        AggFunc::Avg => {
1190                            let col = field.as_ref().ok_or("avg requires field")?;
1191                            let idx = columns
1192                                .iter()
1193                                .position(|c| c == col)
1194                                .ok_or("col not found")?;
1195                            let sum: f64 = rows
1196                                .iter()
1197                                .filter_map(|r| match &r[idx] {
1198                                    Value::Int(v) => Some(*v as f64),
1199                                    Value::Float(v) => Some(*v),
1200                                    _ => None,
1201                                })
1202                                .sum();
1203                            let count = rows.len() as f64;
1204                            Ok(QueryResult::Scalar(Value::Float(sum / count)))
1205                        }
1206                        AggFunc::Sum => {
1207                            let col = field.as_ref().ok_or("sum requires field")?;
1208                            let idx = columns
1209                                .iter()
1210                                .position(|c| c == col)
1211                                .ok_or("col not found")?;
1212                            let mut int_sum: i64 = 0;
1213                            let mut float_sum: f64 = 0.0;
1214                            let mut saw_float = false;
1215                            for r in &rows {
1216                                match &r[idx] {
1217                                    Value::Int(v) => int_sum += *v,
1218                                    Value::Float(v) => {
1219                                        float_sum += *v;
1220                                        saw_float = true;
1221                                    }
1222                                    _ => {}
1223                                }
1224                            }
1225                            let result = if saw_float {
1226                                Value::Float(float_sum + int_sum as f64)
1227                            } else {
1228                                Value::Int(int_sum)
1229                            };
1230                            Ok(QueryResult::Scalar(result))
1231                        }
1232                        AggFunc::Min | AggFunc::Max => {
1233                            let col = field.as_ref().ok_or("min/max requires field")?;
1234                            let idx = columns
1235                                .iter()
1236                                .position(|c| c == col)
1237                                .ok_or("col not found")?;
1238                            let vals: Vec<&Value> = rows.iter().map(|r| &r[idx]).collect();
1239                            let result = if *function == AggFunc::Min {
1240                                vals.into_iter().min().cloned()
1241                            } else {
1242                                vals.into_iter().max().cloned()
1243                            };
1244                            Ok(QueryResult::Scalar(result.unwrap_or(Value::Empty)))
1245                        }
1246                    },
1247                    _ => Err("aggregate requires row input".into()),
1248                }
1249            }
1250
1251            PlanNode::Distinct { input } => {
1252                let result = self.execute_plan_readonly(input)?;
1253                match result {
1254                    QueryResult::Rows { columns, rows } => {
1255                        let mut seen = std::collections::HashSet::new();
1256                        let mut unique_rows = Vec::new();
1257                        for row in rows {
1258                            if seen.insert(row.clone()) {
1259                                unique_rows.push(row);
1260                            }
1261                        }
1262                        Ok(QueryResult::Rows {
1263                            columns,
1264                            rows: unique_rows,
1265                        })
1266                    }
1267                    other => Ok(other),
1268                }
1269            }
1270
1271            PlanNode::GroupBy {
1272                input,
1273                keys,
1274                aggregates,
1275                having,
1276            } => {
1277                let result = self.execute_plan_readonly(input)?;
1278                match result {
1279                    QueryResult::Rows { columns, rows } => {
1280                        let key_indices: Vec<usize> = keys
1281                            .iter()
1282                            .map(|k| {
1283                                columns
1284                                    .iter()
1285                                    .position(|c| c == k)
1286                                    .ok_or_else(|| format!("group-by column '{k}' not found"))
1287                            })
1288                            .collect::<Result<Vec<_>, _>>()?;
1289
1290                        let agg_field_indices: Vec<usize> = aggregates
1291                            .iter()
1292                            .map(|a| {
1293                                if a.field == "*" {
1294                                    Ok(usize::MAX)
1295                                } else {
1296                                    columns.iter().position(|c| c == &a.field).ok_or_else(|| {
1297                                        format!("aggregate column '{}' not found", a.field)
1298                                    })
1299                                }
1300                            })
1301                            .collect::<Result<Vec<_>, _>>()?;
1302
1303                        let mut group_map: rustc_hash::FxHashMap<Vec<Value>, usize> =
1304                            rustc_hash::FxHashMap::default();
1305                        let mut groups: Vec<(Vec<Value>, Vec<usize>)> = Vec::new();
1306                        for (ri, row) in rows.iter().enumerate() {
1307                            let key: Vec<Value> =
1308                                key_indices.iter().map(|&i| row[i].clone()).collect();
1309                            match group_map.get(&key) {
1310                                Some(&idx) => groups[idx].1.push(ri),
1311                                None => {
1312                                    let idx = groups.len();
1313                                    group_map.insert(key.clone(), idx);
1314                                    groups.push((key, vec![ri]));
1315                                }
1316                            }
1317                        }
1318
1319                        let mut out_columns: Vec<String> = keys.clone();
1320                        for agg in aggregates.iter() {
1321                            out_columns.push(agg.output_name.clone());
1322                        }
1323
1324                        let mut out_rows: Vec<Vec<Value>> = Vec::with_capacity(groups.len());
1325                        for (key_vals, row_indices) in &groups {
1326                            let mut row = key_vals.clone();
1327                            for (ai, agg) in aggregates.iter().enumerate() {
1328                                let col_idx = agg_field_indices[ai];
1329                                let val = compute_group_aggregate(
1330                                    agg.function,
1331                                    &rows,
1332                                    row_indices,
1333                                    col_idx,
1334                                );
1335                                row.push(val);
1336                            }
1337                            out_rows.push(row);
1338                        }
1339
1340                        if let Some(having_expr) = having {
1341                            out_rows.retain(|row| eval_predicate(having_expr, row, &out_columns));
1342                        }
1343
1344                        Ok(QueryResult::Rows {
1345                            columns: out_columns,
1346                            rows: out_rows,
1347                        })
1348                    }
1349                    _ => Err("group by requires row input".into()),
1350                }
1351            }
1352
1353            PlanNode::NestedLoopJoin {
1354                left,
1355                right,
1356                on,
1357                kind,
1358            } => {
1359                let left_result = self.execute_plan_readonly(left)?;
1360                let right_result = self.execute_plan_readonly(right)?;
1361                let (left_columns, left_rows) = match left_result {
1362                    QueryResult::Rows { columns, rows } => (columns, rows),
1363                    _ => return Err("join left side must produce rows".into()),
1364                };
1365                let (right_columns, right_rows) = match right_result {
1366                    QueryResult::Rows { columns, rows } => (columns, rows),
1367                    _ => return Err("join right side must produce rows".into()),
1368                };
1369
1370                if !matches!(kind, JoinKind::Cross) {
1371                    if let Some(pred) = on {
1372                        if let Some((l_idx, r_idx)) =
1373                            try_extract_equi_join_keys(pred, &left_columns, &right_columns)
1374                        {
1375                            let result = hash_join(
1376                                left_columns,
1377                                left_rows,
1378                                right_columns,
1379                                right_rows,
1380                                l_idx,
1381                                r_idx,
1382                                *kind,
1383                            );
1384                            if let QueryResult::Rows { ref rows, .. } = result {
1385                                check_join_limit(rows.len())?;
1386                            }
1387                            return Ok(result);
1388                        }
1389                    }
1390                }
1391
1392                let n_left = left_columns.len();
1393                let n_right = right_columns.len();
1394                let mut columns = Vec::with_capacity(n_left + n_right);
1395                columns.extend(left_columns);
1396                columns.extend(right_columns);
1397
1398                let mut rows: Vec<Vec<Value>> = Vec::with_capacity(left_rows.len());
1399                let mut combined: Vec<Value> = Vec::with_capacity(n_left + n_right);
1400
1401                for left_row in &left_rows {
1402                    let mut matched = false;
1403                    for right_row in &right_rows {
1404                        combined.clear();
1405                        combined.extend_from_slice(left_row);
1406                        combined.extend_from_slice(right_row);
1407                        let keep = match kind {
1408                            JoinKind::Cross => true,
1409                            JoinKind::Inner | JoinKind::LeftOuter => match on {
1410                                Some(pred) => eval_predicate(pred, &combined, &columns),
1411                                None => true,
1412                            },
1413                            JoinKind::RightOuter => {
1414                                unreachable!("planner rewrites RightOuter to LeftOuter")
1415                            }
1416                        };
1417                        if keep {
1418                            rows.push(combined.clone());
1419                            check_join_limit(rows.len())?;
1420                            matched = true;
1421                        }
1422                    }
1423                    if !matched && matches!(kind, JoinKind::LeftOuter) {
1424                        let mut row = Vec::with_capacity(n_left + n_right);
1425                        row.extend_from_slice(left_row);
1426                        row.resize(n_left + n_right, Value::Empty);
1427                        rows.push(row);
1428                        check_join_limit(rows.len())?;
1429                    }
1430                }
1431
1432                Ok(QueryResult::Rows { columns, rows })
1433            }
1434
1435            PlanNode::Window { input, windows } => {
1436                let result = self.execute_plan_readonly(input)?;
1437                execute_window(result, windows)
1438            }
1439
1440            PlanNode::Union { left, right, all } => {
1441                let left_result = self.execute_plan_readonly(left)?;
1442                let right_result = self.execute_plan_readonly(right)?;
1443                let (left_cols, left_rows) = match left_result {
1444                    QueryResult::Rows { columns, rows } => (columns, rows),
1445                    _ => return Err("UNION requires query results on left side".into()),
1446                };
1447                let (_, right_rows) = match right_result {
1448                    QueryResult::Rows { columns, rows } => (columns, rows),
1449                    _ => return Err("UNION requires query results on right side".into()),
1450                };
1451                let mut combined = left_rows;
1452                if *all {
1453                    combined.extend(right_rows);
1454                } else {
1455                    let mut seen = std::collections::HashSet::new();
1456                    for row in &combined {
1457                        seen.insert(row.clone());
1458                    }
1459                    for row in right_rows {
1460                        if seen.insert(row.clone()) {
1461                            combined.push(row);
1462                        }
1463                    }
1464                }
1465                Ok(QueryResult::Rows {
1466                    columns: left_cols,
1467                    rows: combined,
1468                })
1469            }
1470
1471            PlanNode::Explain { input } => {
1472                let text = format_plan_tree(input, 0);
1473                Ok(QueryResult::Rows {
1474                    columns: vec!["plan".to_string()],
1475                    rows: text
1476                        .lines()
1477                        .map(|line| vec![Value::Str(line.to_string())])
1478                        .collect(),
1479                })
1480            }
1481
1482            // All write variants — caller must escalate to the write lock.
1483            PlanNode::Insert { .. }
1484            | PlanNode::Update { .. }
1485            | PlanNode::Delete { .. }
1486            | PlanNode::Upsert { .. }
1487            | PlanNode::CreateTable { .. }
1488            | PlanNode::AlterTable { .. }
1489            | PlanNode::DropTable { .. }
1490            | PlanNode::CreateView { .. }
1491            | PlanNode::RefreshView { .. }
1492            | PlanNode::DropView { .. } => Err(READONLY_NEEDS_WRITE.to_string()),
1493        }
1494    }
1495
1496    /// `&self` variant of [`Engine::materialize_subqueries`]. Used by the
1497    /// read path so `Filter` predicates with `InSubquery`/`ExistsSubquery`
1498    /// children can evaluate their inner queries without taking the write
1499    /// lock. Inner queries that would themselves need a write (e.g. dirty
1500    /// view) escalate via [`READONLY_NEEDS_WRITE`] just like the top-level
1501    /// read path does.
1502    fn materialize_subqueries_readonly(&self, expr: &Expr) -> Result<Expr, String> {
1503        match expr {
1504            Expr::InSubquery {
1505                expr: inner,
1506                subquery,
1507                negated,
1508            } => {
1509                if is_correlated_subquery(subquery, &self.catalog) {
1510                    // Pass through — will be materialized per-row in the
1511                    // Filter handler's correlated subquery path.
1512                    let inner = self.materialize_subqueries_readonly(inner)?;
1513                    return Ok(Expr::InSubquery {
1514                        expr: Box::new(inner),
1515                        subquery: subquery.clone(),
1516                        negated: *negated,
1517                    });
1518                }
1519                let inner = self.materialize_subqueries_readonly(inner)?;
1520                let sub_plan = crate::planner::plan_statement(Statement::Query(*subquery.clone()))
1521                    .map_err(|e| e.to_string())?;
1522                let result = self.execute_plan_readonly(&sub_plan)?;
1523                let values = match result {
1524                    QueryResult::Rows { rows, .. } => rows
1525                        .into_iter()
1526                        .filter_map(|mut row| {
1527                            if row.is_empty() {
1528                                None
1529                            } else {
1530                                Some(value_to_expr(row.swap_remove(0)))
1531                            }
1532                        })
1533                        .collect(),
1534                    _ => Vec::new(),
1535                };
1536                Ok(Expr::InList {
1537                    expr: Box::new(inner),
1538                    list: values,
1539                    negated: *negated,
1540                })
1541            }
1542            Expr::ExistsSubquery { subquery, negated } => {
1543                if is_correlated_subquery(subquery, &self.catalog) {
1544                    return Ok(expr.clone());
1545                }
1546                let sub_plan = crate::planner::plan_statement(Statement::Query(*subquery.clone()))
1547                    .map_err(|e| e.to_string())?;
1548                let result = self.execute_plan_readonly(&sub_plan)?;
1549                let has_rows = match result {
1550                    QueryResult::Rows { rows, .. } => !rows.is_empty(),
1551                    _ => false,
1552                };
1553                let truth = if *negated { !has_rows } else { has_rows };
1554                Ok(Expr::Literal(Literal::Bool(truth)))
1555            }
1556            Expr::BinaryOp(l, op, r) => {
1557                let l = self.materialize_subqueries_readonly(l)?;
1558                let r = self.materialize_subqueries_readonly(r)?;
1559                Ok(Expr::BinaryOp(Box::new(l), *op, Box::new(r)))
1560            }
1561            Expr::UnaryOp(op, inner) => {
1562                let inner = self.materialize_subqueries_readonly(inner)?;
1563                Ok(Expr::UnaryOp(*op, Box::new(inner)))
1564            }
1565            Expr::Case { whens, else_expr } => {
1566                let whens = whens
1567                    .iter()
1568                    .map(|(c, r)| {
1569                        let c = self.materialize_subqueries_readonly(c)?;
1570                        let r = self.materialize_subqueries_readonly(r)?;
1571                        Ok((Box::new(c), Box::new(r)))
1572                    })
1573                    .collect::<Result<Vec<_>, String>>()?;
1574                let else_expr = match else_expr {
1575                    Some(e) => Some(Box::new(self.materialize_subqueries_readonly(e)?)),
1576                    None => None,
1577                };
1578                Ok(Expr::Case { whens, else_expr })
1579            }
1580            other => Ok(other.clone()),
1581        }
1582    }
1583
1584    /// Per-row materialisation of correlated subqueries. For each row in the
1585    /// outer query, substitute outer column references in the subquery's
1586    /// filter with the current row's literal values, execute the modified
1587    /// subquery, and return the result as an InList or Bool literal.
1588    fn materialize_correlated_for_row_readonly(
1589        &self,
1590        expr: &Expr,
1591        outer_row: &[Value],
1592        outer_columns: &[String],
1593    ) -> Result<Expr, String> {
1594        match expr {
1595            Expr::InSubquery {
1596                expr: inner,
1597                subquery,
1598                negated,
1599            } => {
1600                let inner =
1601                    self.materialize_correlated_for_row_readonly(inner, outer_row, outer_columns)?;
1602                let mut sub = *subquery.clone();
1603                if let Some(ref filter) = sub.filter {
1604                    sub.filter = Some(substitute_outer_refs(
1605                        filter,
1606                        &sub.source,
1607                        &self.catalog,
1608                        outer_row,
1609                        outer_columns,
1610                    ));
1611                }
1612                let sub_plan = crate::planner::plan_statement(Statement::Query(sub))
1613                    .map_err(|e| e.to_string())?;
1614                let result = self.execute_plan_readonly(&sub_plan)?;
1615                let values = match result {
1616                    QueryResult::Rows { rows, .. } => rows
1617                        .into_iter()
1618                        .filter_map(|mut row| {
1619                            if row.is_empty() {
1620                                None
1621                            } else {
1622                                Some(value_to_expr(row.swap_remove(0)))
1623                            }
1624                        })
1625                        .collect(),
1626                    _ => Vec::new(),
1627                };
1628                Ok(Expr::InList {
1629                    expr: Box::new(inner),
1630                    list: values,
1631                    negated: *negated,
1632                })
1633            }
1634            Expr::ExistsSubquery { subquery, negated } => {
1635                let mut sub = *subquery.clone();
1636                if let Some(ref filter) = sub.filter {
1637                    sub.filter = Some(substitute_outer_refs(
1638                        filter,
1639                        &sub.source,
1640                        &self.catalog,
1641                        outer_row,
1642                        outer_columns,
1643                    ));
1644                }
1645                let sub_plan = crate::planner::plan_statement(Statement::Query(sub))
1646                    .map_err(|e| e.to_string())?;
1647                let result = self.execute_plan_readonly(&sub_plan)?;
1648                let has_rows = match result {
1649                    QueryResult::Rows { rows, .. } => !rows.is_empty(),
1650                    _ => false,
1651                };
1652                let truth = if *negated { !has_rows } else { has_rows };
1653                Ok(Expr::Literal(Literal::Bool(truth)))
1654            }
1655            Expr::BinaryOp(l, op, r) => {
1656                let l =
1657                    self.materialize_correlated_for_row_readonly(l, outer_row, outer_columns)?;
1658                let r =
1659                    self.materialize_correlated_for_row_readonly(r, outer_row, outer_columns)?;
1660                Ok(Expr::BinaryOp(Box::new(l), *op, Box::new(r)))
1661            }
1662            Expr::UnaryOp(op, inner) => {
1663                let inner =
1664                    self.materialize_correlated_for_row_readonly(inner, outer_row, outer_columns)?;
1665                Ok(Expr::UnaryOp(*op, Box::new(inner)))
1666            }
1667            other => Ok(other.clone()),
1668        }
1669    }
1670
1671    /// Parse and plan a query once, returning a [`PreparedQuery`] handle
1672    /// the caller can execute repeatedly with fresh literal values.
1673    ///
1674    /// Mission C Phase 5: the plan cache already short-circuits repeat
1675    /// queries that share a shape, but every call still pays for
1676    /// `canonicalize` (lex + FNV hash) and a hashmap lookup. For a tight
1677    /// insert loop that's ~500-800ns of pure overhead per call on top of
1678    /// the caller's `format!()` cost. Prepared statements skip the lex,
1679    /// skip the hash, skip the format, and skip the cache lookup — the
1680    /// caller holds the plan template directly and hands us the new
1681    /// literals as a slice.
1682    ///
1683    /// The plan template holds whatever literal values the original query
1684    /// string contained; those are overwritten on every `execute_prepared`
1685    /// call, same way the plan cache does on a cache hit.
1686    ///
1687    /// The returned `param_count` matches the total number of
1688    /// `Expr::Literal` slots reachable from the plan, in the deterministic
1689    /// walk order used by `canonicalize` and the cache. Callers must pass
1690    /// exactly that many literals to `execute_prepared`, in the same order
1691    /// they appear in the source text.
1692    pub fn prepare(&mut self, query: &str) -> Result<PreparedQuery, String> {
1693        let plan = planner::plan(query).map_err(|e| e.to_string())?;
1694        let param_count = crate::plan_cache::count_literal_slots(&plan);
1695
1696        // Insert fast path: if the template is Insert and every assignment
1697        // RHS is a literal, resolve column indices once here and store
1698        // them. execute_prepared will skip the plan-clone + substitute
1699        // walk on this path.
1700        //
1701        // Mission C Phase 15: also cache `n_cols` and the target table
1702        // name so execute_prepared doesn't need a second HashMap lookup
1703        // on `self.catalog.schema(table)` just to size the scratch Vec.
1704        let insert_fast = match &plan {
1705            PlanNode::Insert { table, assignments }
1706                if assignments
1707                    .iter()
1708                    .all(|a| matches!(a.value, Expr::Literal(_)))
1709                    && param_count == assignments.len() =>
1710            {
1711                let table_slot = self
1712                    .catalog
1713                    .table_slot(table)
1714                    .ok_or_else(|| format!("table '{table}' not found"))?;
1715                let schema = &self.catalog.table_by_slot(table_slot).schema;
1716                let n_cols = schema.columns.len();
1717                let indices: Result<Vec<usize>, String> = assignments
1718                    .iter()
1719                    .map(|a| {
1720                        schema
1721                            .column_index(&a.field)
1722                            .ok_or_else(|| format!("column '{}' not found", a.field))
1723                    })
1724                    .collect();
1725                Some(InsertFast {
1726                    table_slot,
1727                    col_indices: indices?,
1728                    n_cols,
1729                })
1730            }
1731            _ => None,
1732        };
1733
1734        // Mission C Phase 14: update-by-pk fast path. Match on the shape
1735        // planner::plan_update builds for `T filter .pk = ? update
1736        // { col := ? }` — `Update { input: IndexScan(pk), assignments:
1737        // [{col, Literal}] }` — and only if every precondition holds:
1738        //   * `pk` is an indexed column (so the executor would take the
1739        //     btree.lookup path at run time regardless)
1740        //   * there's exactly one assignment
1741        //   * the assigned column is fixed-size and *not* indexed (so we
1742        //     don't have to maintain any secondary index on write)
1743        //   * both literal slots are already `Expr::Literal` (no computed
1744        //     expressions)
1745        // If any of these fail we fall through to the standard substitute
1746        // + execute path.
1747        let update_pk_fast = Self::try_build_update_pk_fast(&self.catalog, &plan);
1748
1749        Ok(PreparedQuery {
1750            plan_template: plan,
1751            param_count,
1752            insert_fast,
1753            update_pk_fast,
1754        })
1755    }
1756
1757    /// Mission C Phase 14: inspect a planned tree and, if it matches the
1758    /// `update_by_pk` fast-path shape, return the precomputed byte-patch
1759    /// metadata. Returns `None` on any mismatch — the caller falls through
1760    /// to the substitute-and-execute path, which is always correct.
1761    fn try_build_update_pk_fast(catalog: &Catalog, plan: &PlanNode) -> Option<UpdatePkFast> {
1762        // Top level must be `Update { input: IndexScan(...), ... }`.
1763        let (table, input, assignments) = match plan {
1764            PlanNode::Update {
1765                table,
1766                input,
1767                assignments,
1768            } => (table, input.as_ref(), assignments),
1769            _ => return None,
1770        };
1771        // Exactly one assignment — the bench hot path and the only case
1772        // where a single byte-patch covers the whole mutation.
1773        if assignments.len() != 1 {
1774            return None;
1775        }
1776        let assn = &assignments[0];
1777        // Assignment RHS must be a raw literal, not a computed expr.
1778        if !matches!(assn.value, Expr::Literal(_)) {
1779            return None;
1780        }
1781        // Input must be an IndexScan on the same table with a literal key.
1782        let (key_col, key_table) = match input {
1783            PlanNode::IndexScan {
1784                table: t,
1785                column,
1786                key: Expr::Literal(_),
1787            } => (column.clone(), t.clone()),
1788            _ => return None,
1789        };
1790        if &key_table != table {
1791            return None;
1792        }
1793
1794        // Look up schema + index state from the live catalog, caching
1795        // the slot so the execute path skips the name probe.
1796        let table_slot = catalog.table_slot(table)?;
1797        let tbl = catalog.table_by_slot(table_slot);
1798        let schema = &tbl.schema;
1799
1800        // Key column must have an index (the btree.lookup path is what
1801        // makes the fast path worth building).
1802        if !tbl.has_index(&key_col) {
1803            return None;
1804        }
1805
1806        // Target column must exist, be fixed-size, and NOT be indexed (so
1807        // we don't have to maintain any secondary index here).
1808        let target_col_idx = schema.column_index(&assn.field)?;
1809        let target_type = schema.columns[target_col_idx].type_id;
1810        if !is_fixed_size(target_type) {
1811            return None;
1812        }
1813        if tbl.has_indexed_col(target_col_idx) {
1814            return None;
1815        }
1816
1817        // Precompute byte offsets from the cached row layout.
1818        let layout = tbl.row_layout();
1819        let fixed_off = layout.fixed_offset(target_col_idx)?;
1820        let bitmap_size = layout.bitmap_size();
1821        let field_off = 2 + bitmap_size + fixed_off;
1822        let bitmap_byte_off = 2 + target_col_idx / 8;
1823        let bit_mask = 1u8 << (target_col_idx % 8);
1824
1825        // Literal walk order for `Update { IndexScan(key), [{value}] }`
1826        // (see `plan_cache::substitute_plan` — input first, then the
1827        // assignments). The filter key is literal 0, the assignment RHS
1828        // is literal 1.
1829        Some(UpdatePkFast {
1830            table_slot,
1831            key_col,
1832            field_off,
1833            bitmap_byte_off,
1834            bit_mask,
1835            target_type,
1836            key_literal_idx: 0,
1837            value_literal_idx: 1,
1838        })
1839    }
1840
1841    /// Execute a [`PreparedQuery`] with the given literal values.
1842    ///
1843    /// The literals are substituted into a clone of the template plan in
1844    /// the same deterministic walk order that [`crate::canonicalize`]
1845    /// produces (filter predicate first, then projection, then assignment
1846    /// RHS, and so on). Substitution errors here mean the caller passed
1847    /// the wrong number of literals for this query shape.
1848    pub fn execute_prepared(
1849        &mut self,
1850        prep: &PreparedQuery,
1851        literals: &[Literal],
1852    ) -> Result<QueryResult, String> {
1853        if literals.len() != prep.param_count {
1854            return Err(format!(
1855                "prepared query expects {} literal(s), got {}",
1856                prep.param_count,
1857                literals.len(),
1858            ));
1859        }
1860
1861        // Mission C Phase 14: update-by-pk fast path. Skip plan clone,
1862        // substitute walk, resolved_assignments, FastPatch, Vec<RowId>,
1863        // RowLayout::new — straight to btree.lookup_int + byte patch.
1864        // On rare mismatches (wrong literal type, index dropped after
1865        // prepare) the helper returns `Ok(None)` and we fall through to
1866        // the generic substitute-and-execute path below.
1867        if let Some(fast) = &prep.update_pk_fast {
1868            if let Some(result) = self.try_execute_update_pk_fast(fast, literals)? {
1869                // Mark dependent views dirty for prepared update fast path.
1870                if let PlanNode::Update { table, .. } = &prep.plan_template {
1871                    self.view_registry.mark_dependents_dirty(table);
1872                }
1873                // Mission B (post-review): statement-boundary WAL group
1874                // commit. The fast path appended an Update record but did
1875                // not flush — flush it now so the executor's contract is
1876                // "WAL is on disk before this returns".
1877                self.catalog.sync_wal().map_err(|e| e.to_string())?;
1878                return Ok(result);
1879            }
1880        }
1881
1882        // Insert fast path: skip plan-clone + substitute walk + PlanNode::Insert
1883        // arm's column-index resolution. Build the Row directly from the
1884        // caller's literal slice using indices we resolved at prepare time.
1885        // Saves ~300-500ns per insert on the bench.
1886        //
1887        // Mission C Phase 13: the scratch `Vec<Value>` is reused across
1888        // calls — no fresh allocation per insert. We split the borrow
1889        // between `self.catalog` and `self.insert_values_scratch` by
1890        // moving the scratch into a local, filling it, passing to the
1891        // catalog, and putting it back.
1892        //
1893        // Mission C Phase 15: the cached `InsertFast` carries `n_cols`
1894        // and the table name, so the hot path makes exactly one catalog
1895        // HashMap lookup (`get_table_mut`) and dispatches straight into
1896        // `tbl.insert` — no intermediate schema lookup, no generic
1897        // `Catalog::insert` wrapper.
1898        if let Some(fast) = &prep.insert_fast {
1899            let mut values = std::mem::take(&mut self.insert_values_scratch);
1900            values.clear();
1901            values.resize(fast.n_cols, Value::Empty);
1902            for (pos, lit) in literals.iter().enumerate() {
1903                values[fast.col_indices[pos]] = literal_value_from(lit);
1904            }
1905            // Mission C Phase 18: direct O(1) slot index — no
1906            // catalog hash probe. Slot was resolved at prepare time.
1907            let tbl = self.catalog.table_by_slot_mut(fast.table_slot);
1908            let res = tbl.insert(&values).map_err(|e| e.to_string());
1909            // Clear strings before returning the scratch — don't keep
1910            // dangling allocations from the previous row alive across
1911            // calls. `clear()` drops the Value::Str entries.
1912            values.clear();
1913            self.insert_values_scratch = values;
1914            res?;
1915            // Mark dependent views dirty for prepared insert fast path.
1916            if let PlanNode::Insert { table, .. } = &prep.plan_template {
1917                self.view_registry.mark_dependents_dirty(table);
1918            }
1919            // Mission B (post-review): statement-boundary WAL group commit.
1920            self.catalog.sync_wal().map_err(|e| e.to_string())?;
1921            return Ok(QueryResult::Modified(1));
1922        }
1923
1924        let mut plan = prep.plan_template.clone();
1925        let mut idx = 0usize;
1926        crate::plan_cache::substitute_plan(&mut plan, literals, &mut idx);
1927        debug_assert_eq!(idx, literals.len());
1928        let result = self.execute_plan(&plan);
1929        // Mission B (post-review): statement-boundary WAL group commit.
1930        // No-op when nothing was buffered (read-only plans).
1931        self.catalog.sync_wal().map_err(|e| e.to_string())?;
1932        result
1933    }
1934
1935    /// Mission C Phase 14: point-update fast path for prepared
1936    /// `T filter .pk = ? update { col := ? }` queries. The caller has
1937    /// already verified this is an int-indexed pk with a fixed-size,
1938    /// non-indexed target column; all we do here is pluck the two
1939    /// literals out of the caller's slice, run one `btree.lookup_int`,
1940    /// and patch 1–8 bytes of the row. No plan clone, no allocations.
1941    ///
1942    /// Returns:
1943    ///   * `Ok(Some(result))` — fast path took the mutation.
1944    ///   * `Ok(None)` — can't take the fast path this call (wrong
1945    ///     literal type, index dropped since prepare, etc.). Caller
1946    ///     falls through to the generic substitute-and-execute path.
1947    ///   * `Err(_)` — real error (table gone, I/O, etc.).
1948    #[inline]
1949    fn try_execute_update_pk_fast(
1950        &mut self,
1951        fast: &UpdatePkFast,
1952        literals: &[Literal],
1953    ) -> Result<Option<QueryResult>, String> {
1954        // 1) Extract the key literal. The fast path is only built for
1955        //    int key columns; any other literal type means the caller
1956        //    is violating the prepared-query contract or the schema
1957        //    changed — either way, fall back.
1958        let key_int = match &literals[fast.key_literal_idx] {
1959            Literal::Int(v) => *v,
1960            _ => return Ok(None),
1961        };
1962
1963        // 2) Encode the new value as little-endian bytes matching the
1964        //    target column's fixed encoding.
1965        let bytes: FixedBytes = match (fast.target_type, &literals[fast.value_literal_idx]) {
1966            (TypeId::Int, Literal::Int(v)) => FixedBytes::I64(v.to_le_bytes()),
1967            (TypeId::DateTime, Literal::Int(v)) => FixedBytes::I64(v.to_le_bytes()),
1968            (TypeId::Float, Literal::Float(v)) => FixedBytes::F64(v.to_le_bytes()),
1969            (TypeId::Bool, Literal::Bool(v)) => FixedBytes::Bool(if *v { 1 } else { 0 }),
1970            // Type mismatch — fall back to the generic path for a
1971            // consistent error shape.
1972            _ => return Ok(None),
1973        };
1974
1975        // 3) Look up the table + btree, do the int lookup, patch the row
1976        //    in place. Phase 18: table dispatch is a direct slot index;
1977        //    the btree lookup is the linear scan over `indexed_cols`.
1978        //    Single btree.lookup_int + one `with_row_bytes_mut` call.
1979        //    No Vec allocations at all.
1980        //
1981        // Mission B2: route the in-place patch through the catalog's
1982        // WAL-logged wrapper so crash recovery sees the update. The
1983        // extra cost is one WAL append + fsync per query — the hot
1984        // loop structure is unchanged.
1985        let tbl = self.catalog.table_by_slot_mut(fast.table_slot);
1986        let Some(btree) = tbl.index(&fast.key_col) else {
1987            // Index dropped since prepare — bail to the generic path.
1988            return Ok(None);
1989        };
1990        let Some(rid) = btree.lookup_int(key_int) else {
1991            return Ok(Some(QueryResult::Modified(0)));
1992        };
1993
1994        let fast_table_slot = fast.table_slot;
1995        let bitmap_byte_off = fast.bitmap_byte_off;
1996        let bit_mask = fast.bit_mask;
1997        let field_off = fast.field_off;
1998        let ok = self
1999            .catalog
2000            .update_row_bytes_logged_by_slot(fast_table_slot, rid, |row| {
2001                // Idempotent null-bit clear — safe even when the column was
2002                // already non-null (the overwhelmingly common case).
2003                row[bitmap_byte_off] &= !bit_mask;
2004                let field_bytes = bytes.as_slice();
2005                row[field_off..field_off + field_bytes.len()].copy_from_slice(field_bytes);
2006            })
2007            .map_err(|e| e.to_string())?;
2008
2009        Ok(Some(QueryResult::Modified(if ok { 1 } else { 0 })))
2010    }
2011
2012    /// Mission C Phase 13: moving variant of [`Engine::execute_prepared`]
2013    /// for the insert fast path. Takes `literals` by mutable reference
2014    /// so that each `Literal::String` can be consumed via `mem::take`
2015    /// instead of cloned into a `Value::Str`. On `insert_batch_1k` that
2016    /// removes three per-row heap allocations (name, status, email),
2017    /// bringing the workload over the line vs SQLite's amortized
2018    /// prepare+execute loop.
2019    ///
2020    /// The caller's `Literal::String` entries are replaced with empty
2021    /// strings on successful inserts — the `literals` slice is *not*
2022    /// left in a valid-for-reuse state except for `Int`/`Float`/`Bool`
2023    /// values. Non-insert templates fall through to the standard
2024    /// substitute-and-execute path.
2025    pub fn execute_prepared_take(
2026        &mut self,
2027        prep: &PreparedQuery,
2028        literals: &mut [Literal],
2029    ) -> Result<QueryResult, String> {
2030        if literals.len() != prep.param_count {
2031            return Err(format!(
2032                "prepared query expects {} literal(s), got {}",
2033                prep.param_count,
2034                literals.len(),
2035            ));
2036        }
2037
2038        if let Some(fast) = &prep.insert_fast {
2039            let mut values = std::mem::take(&mut self.insert_values_scratch);
2040            values.clear();
2041            values.resize(fast.n_cols, Value::Empty);
2042            for (pos, lit) in literals.iter_mut().enumerate() {
2043                values[fast.col_indices[pos]] = literal_value_take(lit);
2044            }
2045            // Mission C Phase 18: direct O(1) slot index — see
2046            // `execute_prepared` for rationale. This is the hot path
2047            // for `insert_batch_1k`.
2048            let tbl = self.catalog.table_by_slot_mut(fast.table_slot);
2049            let res = tbl.insert(&values).map_err(|e| e.to_string());
2050            values.clear();
2051            self.insert_values_scratch = values;
2052            res?;
2053            // Mission B (post-review): statement-boundary WAL group commit.
2054            self.catalog.sync_wal().map_err(|e| e.to_string())?;
2055            return Ok(QueryResult::Modified(1));
2056        }
2057
2058        // Non-insert templates — fall back to the standard path. We
2059        // can't usefully move the literals because `substitute_plan`
2060        // still expects an immutable slice, and the non-insert hot
2061        // paths are dominated by plan walks anyway.
2062        self.execute_prepared(prep, literals)
2063    }
2064
2065    /// Walk an expression tree and replace every `InSubquery` node with
2066    /// an `InList` by executing the subquery and collecting its first
2067    /// column as literal values. This must be called before entering
2068    /// the row-by-row scan loop because the scan closure can't call back
2069    /// into the engine.
2070    fn materialize_subqueries(&mut self, expr: &Expr) -> Result<Expr, String> {
2071        match expr {
2072            Expr::InSubquery {
2073                expr: inner,
2074                subquery,
2075                negated,
2076            } => {
2077                if is_correlated_subquery(subquery, &self.catalog) {
2078                    let inner = self.materialize_subqueries(inner)?;
2079                    return Ok(Expr::InSubquery {
2080                        expr: Box::new(inner),
2081                        subquery: subquery.clone(),
2082                        negated: *negated,
2083                    });
2084                }
2085                let inner = self.materialize_subqueries(inner)?;
2086                // Plan and execute the subquery.
2087                let sub_plan = crate::planner::plan_statement(Statement::Query(*subquery.clone()))
2088                    .map_err(|e| e.to_string())?;
2089                let result = self.execute_plan(&sub_plan)?;
2090                let values = match result {
2091                    QueryResult::Rows { rows, .. } => rows
2092                        .into_iter()
2093                        .filter_map(|mut row| {
2094                            if row.is_empty() {
2095                                None
2096                            } else {
2097                                Some(value_to_expr(row.swap_remove(0)))
2098                            }
2099                        })
2100                        .collect(),
2101                    _ => Vec::new(),
2102                };
2103                Ok(Expr::InList {
2104                    expr: Box::new(inner),
2105                    list: values,
2106                    negated: *negated,
2107                })
2108            }
2109            Expr::ExistsSubquery { subquery, negated } => {
2110                if is_correlated_subquery(subquery, &self.catalog) {
2111                    return Ok(expr.clone());
2112                }
2113                // Uncorrelated EXISTS: run the subquery once and collapse
2114                // into a Bool literal.
2115                let sub_plan = crate::planner::plan_statement(Statement::Query(*subquery.clone()))
2116                    .map_err(|e| e.to_string())?;
2117                let result = self.execute_plan(&sub_plan)?;
2118                let has_rows = match result {
2119                    QueryResult::Rows { rows, .. } => !rows.is_empty(),
2120                    _ => false,
2121                };
2122                let truth = if *negated { !has_rows } else { has_rows };
2123                Ok(Expr::Literal(Literal::Bool(truth)))
2124            }
2125            Expr::BinaryOp(l, op, r) => {
2126                let l = self.materialize_subqueries(l)?;
2127                let r = self.materialize_subqueries(r)?;
2128                Ok(Expr::BinaryOp(Box::new(l), *op, Box::new(r)))
2129            }
2130            Expr::UnaryOp(op, inner) => {
2131                let inner = self.materialize_subqueries(inner)?;
2132                Ok(Expr::UnaryOp(*op, Box::new(inner)))
2133            }
2134            Expr::Case { whens, else_expr } => {
2135                let whens = whens
2136                    .iter()
2137                    .map(|(c, r)| {
2138                        let c = self.materialize_subqueries(c)?;
2139                        let r = self.materialize_subqueries(r)?;
2140                        Ok((Box::new(c), Box::new(r)))
2141                    })
2142                    .collect::<Result<Vec<_>, String>>()?;
2143                let else_expr = match else_expr {
2144                    Some(e) => Some(Box::new(self.materialize_subqueries(e)?)),
2145                    None => None,
2146                };
2147                Ok(Expr::Case { whens, else_expr })
2148            }
2149            // Leaf nodes: no subqueries possible.
2150            other => Ok(other.clone()),
2151        }
2152    }
2153
2154    /// Write-path per-row materialisation of correlated subqueries.
2155    fn materialize_correlated_for_row(
2156        &mut self,
2157        expr: &Expr,
2158        outer_row: &[Value],
2159        outer_columns: &[String],
2160    ) -> Result<Expr, String> {
2161        match expr {
2162            Expr::InSubquery {
2163                expr: inner,
2164                subquery,
2165                negated,
2166            } => {
2167                let inner = self.materialize_correlated_for_row(inner, outer_row, outer_columns)?;
2168                let mut sub = *subquery.clone();
2169                if let Some(ref filter) = sub.filter {
2170                    sub.filter = Some(substitute_outer_refs(
2171                        filter,
2172                        &sub.source,
2173                        &self.catalog,
2174                        outer_row,
2175                        outer_columns,
2176                    ));
2177                }
2178                let sub_plan = crate::planner::plan_statement(Statement::Query(sub))
2179                    .map_err(|e| e.to_string())?;
2180                let result = self.execute_plan(&sub_plan)?;
2181                let values = match result {
2182                    QueryResult::Rows { rows, .. } => rows
2183                        .into_iter()
2184                        .filter_map(|mut row| {
2185                            if row.is_empty() {
2186                                None
2187                            } else {
2188                                Some(value_to_expr(row.swap_remove(0)))
2189                            }
2190                        })
2191                        .collect(),
2192                    _ => Vec::new(),
2193                };
2194                Ok(Expr::InList {
2195                    expr: Box::new(inner),
2196                    list: values,
2197                    negated: *negated,
2198                })
2199            }
2200            Expr::ExistsSubquery { subquery, negated } => {
2201                let mut sub = *subquery.clone();
2202                if let Some(ref filter) = sub.filter {
2203                    sub.filter = Some(substitute_outer_refs(
2204                        filter,
2205                        &sub.source,
2206                        &self.catalog,
2207                        outer_row,
2208                        outer_columns,
2209                    ));
2210                }
2211                let sub_plan = crate::planner::plan_statement(Statement::Query(sub))
2212                    .map_err(|e| e.to_string())?;
2213                let result = self.execute_plan(&sub_plan)?;
2214                let has_rows = match result {
2215                    QueryResult::Rows { rows, .. } => !rows.is_empty(),
2216                    _ => false,
2217                };
2218                let truth = if *negated { !has_rows } else { has_rows };
2219                Ok(Expr::Literal(Literal::Bool(truth)))
2220            }
2221            Expr::BinaryOp(l, op, r) => {
2222                let l = self.materialize_correlated_for_row(l, outer_row, outer_columns)?;
2223                let r = self.materialize_correlated_for_row(r, outer_row, outer_columns)?;
2224                Ok(Expr::BinaryOp(Box::new(l), *op, Box::new(r)))
2225            }
2226            Expr::UnaryOp(op, inner) => {
2227                let inner = self.materialize_correlated_for_row(inner, outer_row, outer_columns)?;
2228                Ok(Expr::UnaryOp(*op, Box::new(inner)))
2229            }
2230            other => Ok(other.clone()),
2231        }
2232    }
2233
2234    pub fn execute_plan(&mut self, plan: &PlanNode) -> Result<QueryResult, String> {
2235        match plan {
2236            PlanNode::SeqScan { table } => {
2237                // Auto-refresh dirty materialized views on read.
2238                if self.view_registry.is_dirty(table) {
2239                    self.refresh_view(table)?;
2240                }
2241                let schema = self
2242                    .catalog
2243                    .schema(table)
2244                    .ok_or_else(|| format!("table '{table}' not found"))?
2245                    .clone();
2246                let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
2247                let rows: Vec<Vec<Value>> = self
2248                    .catalog
2249                    .scan(table)
2250                    .map_err(|e| e.to_string())?
2251                    .map(|(_, row)| row)
2252                    .collect();
2253                Ok(QueryResult::Rows { columns, rows })
2254            }
2255
2256            PlanNode::Filter { input, predicate } => {
2257                // Materialize any IN-subqueries in the predicate before the
2258                // scan loop — the closure can't call back into the engine.
2259                // Correlated subqueries are left in place for per-row eval.
2260                let materialized;
2261                let predicate = if contains_subquery(predicate) {
2262                    materialized = self.materialize_subqueries(predicate)?;
2263                    &materialized
2264                } else {
2265                    predicate
2266                };
2267
2268                // Correlated subquery path: per-row materialisation.
2269                if contains_subquery(predicate) {
2270                    let result = self.execute_plan(input)?;
2271                    return match result {
2272                        QueryResult::Rows { columns, rows } => {
2273                            let mut filtered = Vec::new();
2274                            for row in rows {
2275                                let row_pred =
2276                                    self.materialize_correlated_for_row(predicate, &row, &columns)?;
2277                                if eval_predicate(&row_pred, &row, &columns) {
2278                                    filtered.push(row);
2279                                }
2280                            }
2281                            Ok(QueryResult::Rows {
2282                                columns,
2283                                rows: filtered,
2284                            })
2285                        }
2286                        _ => Err("filter requires row input".into()),
2287                    };
2288                }
2289
2290                // Fast path: fuse Filter + SeqScan into a zero-copy streaming
2291                // loop. Uses decode_column() to evaluate the predicate on only
2292                // the columns it references, avoiding heap allocations for
2293                // String/Bytes columns that aren't part of the filter.
2294                if let PlanNode::SeqScan { table } = input.as_ref() {
2295                    // Auto-refresh dirty materialized views.
2296                    if self.view_registry.is_dirty(table) {
2297                        self.refresh_view(table)?;
2298                    }
2299                    let schema = self
2300                        .catalog
2301                        .schema(table)
2302                        .ok_or_else(|| format!("table '{table}' not found"))?
2303                        .clone();
2304                    let columns: Vec<String> =
2305                        schema.columns.iter().map(|c| c.name.clone()).collect();
2306                    let fast = FastLayout::new(&schema);
2307                    let row_layout = RowLayout::new(&schema);
2308                    // Mission F: pre-size to skip the first 4 Vec doublings
2309                    // (4 → 8 → 16 → 32 → 64). On a 100K-row scan with 30%
2310                    // selectivity that's ~4 fewer reallocations + memcpys.
2311                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
2312
2313                    // Try compiled predicate for the filter check (handles
2314                    // int leaves, string-eq leaves, and And conjunctions).
2315                    if let Some(compiled) = compile_predicate(predicate, &columns, &fast, &schema) {
2316                        self.catalog
2317                            .for_each_row_raw(table, |_rid, data| {
2318                                if compiled(data) {
2319                                    rows.push(decode_row(&schema, data));
2320                                }
2321                            })
2322                            .map_err(|e| e.to_string())?;
2323                    } else {
2324                        let pred_cols = predicate_column_indices(predicate, &columns);
2325                        self.catalog
2326                            .for_each_row_raw(table, |_rid, data| {
2327                                let pred_row =
2328                                    decode_selective(&schema, &row_layout, data, &pred_cols);
2329                                if eval_predicate(predicate, &pred_row, &columns) {
2330                                    rows.push(decode_row(&schema, data));
2331                                }
2332                            })
2333                            .map_err(|e| e.to_string())?;
2334                    }
2335
2336                    return Ok(QueryResult::Rows { columns, rows });
2337                }
2338
2339                // General path: materialise then filter.
2340                let result = self.execute_plan(input)?;
2341                match result {
2342                    QueryResult::Rows { columns, rows } => {
2343                        let filtered: Vec<Vec<Value>> = rows
2344                            .into_iter()
2345                            .filter(|row| eval_predicate(predicate, row, &columns))
2346                            .collect();
2347                        Ok(QueryResult::Rows {
2348                            columns,
2349                            rows: filtered,
2350                        })
2351                    }
2352                    _ => Err("filter requires row input".into()),
2353                }
2354            }
2355
2356            PlanNode::Project { input, fields } => {
2357                // Fast path: Project over IndexScan — decode only projected
2358                // columns from raw bytes instead of full decode_row.
2359                if let PlanNode::IndexScan { table, column, key } = input.as_ref() {
2360                    let schema = self
2361                        .catalog
2362                        .schema(table)
2363                        .ok_or_else(|| format!("table '{table}' not found"))?
2364                        .clone();
2365                    let all_columns: Vec<String> =
2366                        schema.columns.iter().map(|c| c.name.clone()).collect();
2367                    let key_value = literal_to_value(key)?;
2368                    let tbl = self
2369                        .catalog
2370                        .get_table(table)
2371                        .ok_or_else(|| format!("table '{table}' not found"))?;
2372
2373                    let proj_columns: Vec<String> = fields
2374                        .iter()
2375                        .map(|f| {
2376                            f.alias.clone().unwrap_or_else(|| match &f.expr {
2377                                Expr::Field(name) => name.clone(),
2378                                _ => "?".into(),
2379                            })
2380                        })
2381                        .collect();
2382
2383                    // Determine which column indices the projection needs
2384                    let proj_indices: Vec<usize> = fields
2385                        .iter()
2386                        .filter_map(|f| {
2387                            if let Expr::Field(name) = &f.expr {
2388                                all_columns.iter().position(|c| c == name)
2389                            } else {
2390                                None
2391                            }
2392                        })
2393                        .collect();
2394
2395                    if let Some(btree) = tbl.index(column) {
2396                        let layout = RowLayout::new(&schema);
2397                        // Mission D7: int-specialized lookup skips the
2398                        // `<Value as Ord>::cmp` discriminant dispatch on
2399                        // int-keyed indexes (the vast majority).
2400                        let lookup_result = match &key_value {
2401                            Value::Int(k) => btree.lookup_int(*k),
2402                            other => btree.lookup(other),
2403                        };
2404                        let rows = match lookup_result {
2405                            Some(rid) => match tbl.heap.get(rid) {
2406                                Some(data) => {
2407                                    let row: Vec<Value> = proj_indices
2408                                        .iter()
2409                                        .map(|&ci| decode_column(&schema, &layout, &data, ci))
2410                                        .collect();
2411                                    vec![row]
2412                                }
2413                                None => Vec::new(),
2414                            },
2415                            None => Vec::new(),
2416                        };
2417                        return Ok(QueryResult::Rows {
2418                            columns: proj_columns,
2419                            rows,
2420                        });
2421                    }
2422                }
2423
2424                // Fast path: Project(Limit(Sort(Filter(SeqScan)))) — bounded
2425                // top-N heap. Decodes only the sort key + projected columns,
2426                // keeps at most `limit` rows in a heap. Also handles the
2427                // Project(Limit(Sort(SeqScan))) variant (no filter).
2428                if let PlanNode::Limit {
2429                    input: inner,
2430                    count: limit_expr,
2431                } = input.as_ref()
2432                {
2433                    if let PlanNode::Sort {
2434                        input: sort_input,
2435                        keys,
2436                    } = inner.as_ref()
2437                    {
2438                        // Fast path only for single-key sorts
2439                        if keys.len() == 1 {
2440                            let sort_field = &keys[0].field;
2441                            let descending = keys[0].descending;
2442                            let limit = match limit_expr {
2443                                Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
2444                                _ => usize::MAX,
2445                            };
2446                            let (table_opt, pred_opt): (Option<&str>, Option<&Expr>) =
2447                                match sort_input.as_ref() {
2448                                    PlanNode::SeqScan { table } => (Some(table.as_str()), None),
2449                                    PlanNode::Filter {
2450                                        input: fi,
2451                                        predicate,
2452                                    } => {
2453                                        if let PlanNode::SeqScan { table } = fi.as_ref() {
2454                                            (Some(table.as_str()), Some(predicate))
2455                                        } else {
2456                                            (None, None)
2457                                        }
2458                                    }
2459                                    _ => (None, None),
2460                                };
2461                            if let Some(table) = table_opt {
2462                                if let Some(result) = self.project_filter_sort_limit_fast(
2463                                    table, fields, sort_field, descending, limit, pred_opt,
2464                                )? {
2465                                    return Ok(result);
2466                                }
2467                            }
2468                        }
2469                    }
2470                    // Fast path: Project(Limit(Filter(SeqScan))) — stream,
2471                    // decode only projected columns, stop at limit.
2472                    if let PlanNode::Filter {
2473                        input: fi,
2474                        predicate,
2475                    } = inner.as_ref()
2476                    {
2477                        if let PlanNode::SeqScan { table } = fi.as_ref() {
2478                            let limit = match limit_expr {
2479                                Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
2480                                _ => usize::MAX,
2481                            };
2482                            if let Some(result) = self.project_filter_limit_fast(
2483                                table,
2484                                fields,
2485                                limit,
2486                                Some(predicate),
2487                            )? {
2488                                return Ok(result);
2489                            }
2490                        }
2491                    }
2492                    // Fast path: Project(Limit(SeqScan)) — stream, no filter.
2493                    if let PlanNode::SeqScan { table } = inner.as_ref() {
2494                        let limit = match limit_expr {
2495                            Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
2496                            _ => usize::MAX,
2497                        };
2498                        if let Some(result) =
2499                            self.project_filter_limit_fast(table, fields, limit, None)?
2500                        {
2501                            return Ok(result);
2502                        }
2503                    }
2504                }
2505
2506                // Mission D4: Project(Filter(SeqScan)) without Limit. Reuses
2507                // `project_filter_limit_fast` with limit = usize::MAX so the
2508                // hot loop decodes only projected columns and uses the
2509                // compiled predicate. Previously this fell through to the
2510                // generic Filter branch which materialised every column via
2511                // `decode_row` then re-projected — quadratic work.
2512                //
2513                // multi_col_and_filter (`U filter .age > 30 and .status =
2514                // "active" { .name, .age }`) was 6.18ms (0.7x SQLite) and
2515                // is the load-bearing workload for this fast path.
2516                if let PlanNode::Filter {
2517                    input: fi,
2518                    predicate,
2519                } = input.as_ref()
2520                {
2521                    if let PlanNode::SeqScan { table } = fi.as_ref() {
2522                        if let Some(result) = self.project_filter_limit_fast(
2523                            table,
2524                            fields,
2525                            usize::MAX,
2526                            Some(predicate),
2527                        )? {
2528                            return Ok(result);
2529                        }
2530                    }
2531                }
2532
2533                // Mission D4: Project(SeqScan) without Filter or Limit.
2534                // Decode only projected columns; the previous fall-through
2535                // built full Vec<Value> rows then re-projected.
2536                if let PlanNode::SeqScan { table } = input.as_ref() {
2537                    if let Some(result) =
2538                        self.project_filter_limit_fast(table, fields, usize::MAX, None)?
2539                    {
2540                        return Ok(result);
2541                    }
2542                }
2543
2544                let result = self.execute_plan(input)?;
2545                match result {
2546                    QueryResult::Rows { columns, rows } => {
2547                        let proj_columns: Vec<String> = fields
2548                            .iter()
2549                            .map(|f| {
2550                                f.alias.clone().unwrap_or_else(|| match &f.expr {
2551                                    Expr::Field(name) => name.clone(),
2552                                    // Mission E1.2: `{ u.name }` projects as the
2553                                    // qualified column name so callers can still
2554                                    // disambiguate across the join output.
2555                                    Expr::QualifiedField { qualifier, field } => {
2556                                        format!("{qualifier}.{field}")
2557                                    }
2558                                    _ => "?".into(),
2559                                })
2560                            })
2561                            .collect();
2562                        let proj_rows: Vec<Vec<Value>> = rows
2563                            .iter()
2564                            .map(|row| {
2565                                fields
2566                                    .iter()
2567                                    .map(|f| eval_expr(&f.expr, row, &columns))
2568                                    .collect()
2569                            })
2570                            .collect();
2571                        Ok(QueryResult::Rows {
2572                            columns: proj_columns,
2573                            rows: proj_rows,
2574                        })
2575                    }
2576                    _ => Err("project requires row input".into()),
2577                }
2578            }
2579
2580            PlanNode::Sort { input, keys } => {
2581                let result = self.execute_plan(input)?;
2582                match result {
2583                    QueryResult::Rows { columns, mut rows } => {
2584                        if rows.len() > MAX_SORT_ROWS {
2585                            return Err(format!(
2586                                "sort input exceeds {} row limit — add a LIMIT clause",
2587                                MAX_SORT_ROWS
2588                            ));
2589                        }
2590                        let key_indices: Vec<(usize, bool)> = keys
2591                            .iter()
2592                            .map(|k| {
2593                                columns
2594                                    .iter()
2595                                    .position(|c| c == &k.field)
2596                                    .map(|idx| (idx, k.descending))
2597                                    .ok_or_else(|| format!("column '{}' not found", k.field))
2598                            })
2599                            .collect::<Result<_, String>>()?;
2600                        rows.sort_by(|a, b| {
2601                            for &(col_idx, descending) in &key_indices {
2602                                let cmp = a[col_idx].cmp(&b[col_idx]);
2603                                let cmp = if descending { cmp.reverse() } else { cmp };
2604                                if cmp != std::cmp::Ordering::Equal {
2605                                    return cmp;
2606                                }
2607                            }
2608                            std::cmp::Ordering::Equal
2609                        });
2610                        Ok(QueryResult::Rows { columns, rows })
2611                    }
2612                    _ => Err("sort requires row input".into()),
2613                }
2614            }
2615
2616            PlanNode::Limit { input, count } => {
2617                let result = self.execute_plan(input)?;
2618                let n = match count {
2619                    Expr::Literal(Literal::Int(v)) => *v as usize,
2620                    _ => return Err("limit must be integer literal".into()),
2621                };
2622                match result {
2623                    QueryResult::Rows { columns, rows } => Ok(QueryResult::Rows {
2624                        columns,
2625                        rows: rows.into_iter().take(n).collect(),
2626                    }),
2627                    _ => Err("limit requires row input".into()),
2628                }
2629            }
2630
2631            PlanNode::Offset { input, count } => {
2632                let result = self.execute_plan(input)?;
2633                let n = match count {
2634                    Expr::Literal(Literal::Int(v)) => *v as usize,
2635                    _ => return Err("offset must be integer literal".into()),
2636                };
2637                match result {
2638                    QueryResult::Rows { columns, rows } => Ok(QueryResult::Rows {
2639                        columns,
2640                        rows: rows.into_iter().skip(n).collect(),
2641                    }),
2642                    _ => Err("offset requires row input".into()),
2643                }
2644            }
2645
2646            PlanNode::Aggregate {
2647                input,
2648                function,
2649                field,
2650            } => {
2651                // Fast path: count() over SeqScan — count rows without any decode
2652                if *function == AggFunc::Count {
2653                    if let PlanNode::SeqScan { table } = input.as_ref() {
2654                        let mut count: i64 = 0;
2655                        self.catalog
2656                            .for_each_row_raw(table, |_rid, _data| {
2657                                count += 1;
2658                            })
2659                            .map_err(|e| e.to_string())?;
2660                        return Ok(QueryResult::Scalar(Value::Int(count)));
2661                    }
2662                    // Fast path: count() over Filter(SeqScan) — try compiled
2663                    // predicate first, fall back to decode_column path.
2664                    if let PlanNode::Filter {
2665                        input: inner,
2666                        predicate,
2667                    } = input.as_ref()
2668                    {
2669                        if let PlanNode::SeqScan { table } = inner.as_ref() {
2670                            let schema = self
2671                                .catalog
2672                                .schema(table)
2673                                .ok_or_else(|| format!("table '{table}' not found"))?
2674                                .clone();
2675                            let columns: Vec<String> =
2676                                schema.columns.iter().map(|c| c.name.clone()).collect();
2677                            let fast = FastLayout::new(&schema);
2678                            let row_layout = RowLayout::new(&schema);
2679
2680                            // Try compiled predicate (zero-allocation hot path).
2681                            // Handles int leaves, string-eq leaves, AND conjunctions.
2682                            if let Some(compiled) =
2683                                compile_predicate(predicate, &columns, &fast, &schema)
2684                            {
2685                                let mut count: i64 = 0;
2686                                self.catalog
2687                                    .for_each_row_raw(table, |_rid, data| {
2688                                        if compiled(data) {
2689                                            count += 1;
2690                                        }
2691                                    })
2692                                    .map_err(|e| e.to_string())?;
2693                                return Ok(QueryResult::Scalar(Value::Int(count)));
2694                            }
2695
2696                            // Fallback: decode predicate columns
2697                            let pred_cols = predicate_column_indices(predicate, &columns);
2698                            let mut count: i64 = 0;
2699                            self.catalog
2700                                .for_each_row_raw(table, |_rid, data| {
2701                                    let pred_row =
2702                                        decode_selective(&schema, &row_layout, data, &pred_cols);
2703                                    if eval_predicate(predicate, &pred_row, &columns) {
2704                                        count += 1;
2705                                    }
2706                                })
2707                                .map_err(|e| e.to_string())?;
2708
2709                            return Ok(QueryResult::Scalar(Value::Int(count)));
2710                        }
2711                    }
2712                }
2713
2714                // Fast path: sum/avg/min/max over a single fixed-size int
2715                // column with an optional compiled filter predicate. Walks
2716                // raw row bytes, zero allocation per row.
2717                if matches!(
2718                    function,
2719                    AggFunc::Sum
2720                        | AggFunc::Avg
2721                        | AggFunc::Min
2722                        | AggFunc::Max
2723                        | AggFunc::CountDistinct
2724                ) {
2725                    if let Some(col) = field.as_ref() {
2726                        // Shape: Aggregate(SeqScan) or Aggregate(Filter(SeqScan))
2727                        let (table_opt, pred_opt): (Option<&str>, Option<&Expr>) =
2728                            match input.as_ref() {
2729                                PlanNode::SeqScan { table } => (Some(table.as_str()), None),
2730                                PlanNode::Filter {
2731                                    input: inner,
2732                                    predicate,
2733                                } => {
2734                                    if let PlanNode::SeqScan { table } = inner.as_ref() {
2735                                        (Some(table.as_str()), Some(predicate))
2736                                    } else {
2737                                        (None, None)
2738                                    }
2739                                }
2740                                _ => (None, None),
2741                            };
2742                        if let Some(table) = table_opt {
2743                            if let Some(result) =
2744                                self.agg_single_col_fast(table, col, *function, pred_opt)?
2745                            {
2746                                return Ok(result);
2747                            }
2748                        }
2749                    }
2750                }
2751
2752                // Fast path: Project(Limit(Filter(SeqScan))) — stream, decode
2753                // only projected columns, stop once we hit the limit.
2754                // (Handled in the Project branch; this branch only fires when
2755                // the aggregate is the outer node.)
2756                let result = self.execute_plan(input)?;
2757                match result {
2758                    QueryResult::Rows { columns, rows } => {
2759                        match function {
2760                            AggFunc::Count => {
2761                                Ok(QueryResult::Scalar(Value::Int(rows.len() as i64)))
2762                            }
2763                            AggFunc::CountDistinct => {
2764                                let col = field.as_ref().ok_or("count distinct requires field")?;
2765                                let idx = columns
2766                                    .iter()
2767                                    .position(|c| c == col)
2768                                    .ok_or("col not found")?;
2769                                let mut seen = std::collections::HashSet::new();
2770                                for row in &rows {
2771                                    let v = &row[idx];
2772                                    if !v.is_empty() {
2773                                        seen.insert(v.clone());
2774                                    }
2775                                }
2776                                Ok(QueryResult::Scalar(Value::Int(seen.len() as i64)))
2777                            }
2778                            AggFunc::Avg => {
2779                                let col = field.as_ref().ok_or("avg requires field")?;
2780                                let idx = columns
2781                                    .iter()
2782                                    .position(|c| c == col)
2783                                    .ok_or("col not found")?;
2784                                let sum: f64 = rows
2785                                    .iter()
2786                                    .filter_map(|r| match &r[idx] {
2787                                        Value::Int(v) => Some(*v as f64),
2788                                        Value::Float(v) => Some(*v),
2789                                        _ => None,
2790                                    })
2791                                    .sum();
2792                                let count = rows.len() as f64;
2793                                Ok(QueryResult::Scalar(Value::Float(sum / count)))
2794                            }
2795                            AggFunc::Sum => {
2796                                let col = field.as_ref().ok_or("sum requires field")?;
2797                                let idx = columns
2798                                    .iter()
2799                                    .position(|c| c == col)
2800                                    .ok_or("col not found")?;
2801                                // Track int and float contributions separately so
2802                                // Float columns (and mixed Int/Float rows) don't get
2803                                // silently dropped as they did in the Int-only
2804                                // version. If any Float is present, the whole sum
2805                                // promotes to Float — matching Avg's semantics.
2806                                let mut int_sum: i64 = 0;
2807                                let mut float_sum: f64 = 0.0;
2808                                let mut saw_float = false;
2809                                for r in &rows {
2810                                    match &r[idx] {
2811                                        Value::Int(v) => int_sum += *v,
2812                                        Value::Float(v) => {
2813                                            float_sum += *v;
2814                                            saw_float = true;
2815                                        }
2816                                        _ => {}
2817                                    }
2818                                }
2819                                let result = if saw_float {
2820                                    Value::Float(float_sum + int_sum as f64)
2821                                } else {
2822                                    Value::Int(int_sum)
2823                                };
2824                                Ok(QueryResult::Scalar(result))
2825                            }
2826                            AggFunc::Min | AggFunc::Max => {
2827                                let col = field.as_ref().ok_or("min/max requires field")?;
2828                                let idx = columns
2829                                    .iter()
2830                                    .position(|c| c == col)
2831                                    .ok_or("col not found")?;
2832                                let vals: Vec<&Value> = rows.iter().map(|r| &r[idx]).collect();
2833                                let result = if *function == AggFunc::Min {
2834                                    vals.into_iter().min().cloned()
2835                                } else {
2836                                    vals.into_iter().max().cloned()
2837                                };
2838                                Ok(QueryResult::Scalar(result.unwrap_or(Value::Empty)))
2839                            }
2840                        }
2841                    }
2842                    _ => Err("aggregate requires row input".into()),
2843                }
2844            }
2845
2846            PlanNode::Insert { table, assignments } => {
2847                // Mission C Phase 3: resolve column indices + literals under
2848                // a short-lived shared borrow on the catalog, then release
2849                // it before calling insert(). The previous code cloned the
2850                // full Schema (6+ String allocations on User) just to dodge
2851                // the borrow checker — a measurable 200-400ns on every
2852                // insert_single call in the bench.
2853                let values = {
2854                    let schema = self
2855                        .catalog
2856                        .schema(table)
2857                        .ok_or_else(|| format!("table '{table}' not found"))?;
2858                    let mut values = vec![Value::Empty; schema.columns.len()];
2859                    for a in assignments {
2860                        let idx = schema
2861                            .column_index(&a.field)
2862                            .ok_or_else(|| format!("column '{}' not found", a.field))?;
2863                        values[idx] = literal_to_value(&a.value)?;
2864                    }
2865                    values
2866                };
2867                self.catalog
2868                    .insert(table, &values)
2869                    .map_err(|e| e.to_string())?;
2870                self.view_registry.mark_dependents_dirty(table);
2871                Ok(QueryResult::Modified(1))
2872            }
2873
2874            PlanNode::Upsert {
2875                table,
2876                key_column,
2877                assignments,
2878                on_conflict,
2879            } => {
2880                // Build the insert values from assignments.
2881                let (values, key_idx) = {
2882                    let schema = self
2883                        .catalog
2884                        .schema(table)
2885                        .ok_or_else(|| format!("table '{table}' not found"))?;
2886                    let mut values = vec![Value::Empty; schema.columns.len()];
2887                    for a in assignments {
2888                        let idx = schema
2889                            .column_index(&a.field)
2890                            .ok_or_else(|| format!("column '{}' not found", a.field))?;
2891                        values[idx] = literal_to_value(&a.value)?;
2892                    }
2893                    let key_idx = schema
2894                        .column_index(key_column)
2895                        .ok_or_else(|| format!("key column '{key_column}' not found"))?;
2896                    (values, key_idx)
2897                };
2898
2899                let key_value = values[key_idx].clone();
2900
2901                // Probe the index for a conflict.
2902                let existing = {
2903                    let tbl = self
2904                        .catalog
2905                        .get_table(table)
2906                        .ok_or_else(|| format!("table '{table}' not found"))?;
2907                    if let Some(btree) = tbl.index(key_column) {
2908                        let hit = match &key_value {
2909                            Value::Int(k) => btree.lookup_int(*k),
2910                            other => btree.lookup(other),
2911                        };
2912                        hit.and_then(|rid| {
2913                            tbl.heap
2914                                .get(rid)
2915                                .map(|data| (rid, decode_row(&tbl.schema, &data)))
2916                        })
2917                    } else {
2918                        // No index — linear scan for the key.
2919                        let mut found = None;
2920                        for (rid, row) in tbl.scan() {
2921                            if row[key_idx] == key_value {
2922                                found = Some((rid, row));
2923                                break;
2924                            }
2925                        }
2926                        found
2927                    }
2928                };
2929
2930                if let Some((rid, mut existing_row)) = existing {
2931                    // Conflict: apply on_conflict assignments (or all non-key if empty).
2932                    let update_assignments = if on_conflict.is_empty() {
2933                        assignments
2934                    } else {
2935                        on_conflict
2936                    };
2937                    let changed_cols: Vec<usize> = {
2938                        let schema = self
2939                            .catalog
2940                            .schema(table)
2941                            .ok_or_else(|| format!("table '{table}' not found"))?;
2942                        let mut indices = Vec::new();
2943                        for a in update_assignments {
2944                            let idx = schema
2945                                .column_index(&a.field)
2946                                .ok_or_else(|| format!("column '{}' not found", a.field))?;
2947                            if idx != key_idx {
2948                                existing_row[idx] = literal_to_value(&a.value)?;
2949                                indices.push(idx);
2950                            }
2951                        }
2952                        indices
2953                    };
2954                    self.catalog
2955                        .update_hinted(table, rid, &existing_row, Some(&changed_cols))
2956                        .map_err(|e| e.to_string())?;
2957                    self.view_registry.mark_dependents_dirty(table);
2958                    Ok(QueryResult::Modified(1))
2959                } else {
2960                    // No conflict: insert.
2961                    self.catalog
2962                        .insert(table, &values)
2963                        .map_err(|e| e.to_string())?;
2964                    self.view_registry.mark_dependents_dirty(table);
2965                    Ok(QueryResult::Modified(1))
2966                }
2967            }
2968
2969            PlanNode::Update {
2970                input,
2971                table,
2972                assignments,
2973            } => {
2974                // Mission C Phase 3: resolve assignments against a borrowed
2975                // schema, then drop the borrow before the mutation loop.
2976                // Try literal-only path first; fall back to per-row expression
2977                // evaluation if any assignment contains a non-literal expression
2978                // (e.g., `age := .age + 1`).
2979                let (col_indices, literal_vals): (Vec<usize>, Option<Vec<Value>>) = {
2980                    let schema_ref = self
2981                        .catalog
2982                        .schema(table)
2983                        .ok_or_else(|| format!("table '{table}' not found"))?;
2984                    let indices: Vec<usize> = assignments
2985                        .iter()
2986                        .map(|a| {
2987                            schema_ref
2988                                .column_index(&a.field)
2989                                .ok_or_else(|| format!("column '{}' not found", a.field))
2990                        })
2991                        .collect::<Result<_, _>>()?;
2992                    let vals: Result<Vec<Value>, _> = assignments
2993                        .iter()
2994                        .map(|a| literal_to_value(&a.value))
2995                        .collect();
2996                    (indices, vals.ok())
2997                };
2998                let resolved_assignments: Option<Vec<(usize, Value)>> =
2999                    literal_vals.map(|vals| col_indices.iter().copied().zip(vals).collect());
3000
3001                // Mission C Phase 2: the hint Table::update_hinted needs to
3002                // decide whether to read the old row for index diff.
3003                let changed_cols: Vec<usize> = col_indices.clone();
3004
3005                // ── Fused scan+update for Update(Filter(SeqScan)) ────────
3006                // Perf sprint: instead of the two-pass collect-RIDs-then-loop
3007                // pattern (which pays one ensure_hot per matched row on the
3008                // second pass), fuse the predicate evaluation and in-place
3009                // byte-level mutation into a single heap walk. Same idea as
3010                // the fused scan_delete_matching path for deletes.
3011                if let Some(ref resolved_assignments) = resolved_assignments {
3012                    if let PlanNode::Filter {
3013                        input: inner,
3014                        predicate,
3015                    } = input.as_ref()
3016                    {
3017                        if let PlanNode::SeqScan { table: t } = inner.as_ref() {
3018                            if t == table {
3019                                let fused_result = self.try_fused_scan_update(
3020                                    table,
3021                                    predicate,
3022                                    resolved_assignments,
3023                                    &changed_cols,
3024                                );
3025                                if let Some(result) = fused_result {
3026                                    return result;
3027                                }
3028                            }
3029                        }
3030                    }
3031                }
3032
3033                // Collect matching RowIds in a single pass.
3034                let matching_rids = self.collect_rids_for_mutation(input, table)?;
3035
3036                // ── Literal-only fast paths ─────────────────────────────
3037                if let Some(ref resolved_assignments) = resolved_assignments {
3038                    // Mission C Phase 4: in-place byte-patch fast path. If every
3039                    // assignment targets a fixed-size non-null column AND none of
3040                    // them is indexed, we can skip decode_row / Vec<Value> /
3041                    // encode_row_into entirely and patch the row's raw bytes on
3042                    // the hot page.
3043                    let fast_patch: Option<Vec<FastPatch>> = {
3044                        let tbl = self
3045                            .catalog
3046                            .get_table(table)
3047                            .ok_or_else(|| format!("table '{table}' not found"))?;
3048                        let schema = &tbl.schema;
3049                        let all_fixed_nonnull = resolved_assignments.iter().all(|(idx, val)| {
3050                            is_fixed_size(schema.columns[*idx].type_id) && !val.is_empty()
3051                        });
3052                        let no_indexed = !resolved_assignments
3053                            .iter()
3054                            .any(|(idx, _)| tbl.has_indexed_col(*idx));
3055
3056                        if all_fixed_nonnull && no_indexed {
3057                            let layout = RowLayout::new(schema);
3058                            let bitmap_size = layout.bitmap_size();
3059                            let patches: Vec<FastPatch> = resolved_assignments
3060                                .iter()
3061                                .map(|(idx, val)| {
3062                                    let fixed_off = layout
3063                                        .fixed_offset(*idx)
3064                                        .expect("is_fixed_size already checked");
3065                                    let field_off = 2 + bitmap_size + fixed_off;
3066                                    let bytes: FixedBytes = match val {
3067                                        Value::Int(v) => FixedBytes::I64(v.to_le_bytes()),
3068                                        Value::Float(v) => FixedBytes::F64(v.to_le_bytes()),
3069                                        Value::Bool(v) => FixedBytes::Bool(if *v { 1 } else { 0 }),
3070                                        Value::DateTime(v) => FixedBytes::I64(v.to_le_bytes()),
3071                                        Value::Uuid(v) => FixedBytes::Uuid(*v),
3072                                        _ => unreachable!("all_fixed_nonnull guard lied"),
3073                                    };
3074                                    FastPatch {
3075                                        field_off,
3076                                        bitmap_byte_off: 2 + idx / 8,
3077                                        bit_mask: 1u8 << (idx % 8),
3078                                        bytes,
3079                                    }
3080                                })
3081                                .collect();
3082                            Some(patches)
3083                        } else {
3084                            None
3085                        }
3086                    };
3087
3088                    if let Some(patches) = fast_patch {
3089                        let mut count = 0u64;
3090                        for rid in matching_rids {
3091                            // Mission B2: WAL-log every patch so crash
3092                            // recovery replays the update. Same mutation
3093                            // closure as before — the wrapper just sandwiches
3094                            // it between a hot-page read and a WAL append.
3095                            let ok = self
3096                                .catalog
3097                                .update_row_bytes_logged(table, rid, |row| {
3098                                    for p in &patches {
3099                                        row[p.bitmap_byte_off] &= !p.bit_mask;
3100                                        let field_bytes = p.bytes.as_slice();
3101                                        row[p.field_off..p.field_off + field_bytes.len()]
3102                                            .copy_from_slice(field_bytes);
3103                                    }
3104                                })
3105                                .map_err(|e| e.to_string())?;
3106                            if ok {
3107                                count += 1;
3108                            }
3109                        }
3110                        self.view_registry.mark_dependents_dirty(table);
3111                        return Ok(QueryResult::Modified(count));
3112                    }
3113
3114                    // Mission C Phase 10: var-column in-place shrink fast path.
3115                    let var_fast: Option<(usize, Option<Vec<u8>>)> = {
3116                        let tbl = self
3117                            .catalog
3118                            .get_table(table)
3119                            .ok_or_else(|| format!("table '{table}' not found"))?;
3120                        let schema = &tbl.schema;
3121                        let is_single = resolved_assignments.len() == 1;
3122                        let is_var_col = is_single
3123                            && !is_fixed_size(schema.columns[resolved_assignments[0].0].type_id);
3124                        let no_indexed = !resolved_assignments
3125                            .iter()
3126                            .any(|(idx, _)| tbl.has_indexed_col(*idx));
3127
3128                        if is_single && is_var_col && no_indexed {
3129                            let (idx, val) = &resolved_assignments[0];
3130                            let bytes_opt: Option<Vec<u8>> = match val {
3131                                Value::Str(s) => Some(s.as_bytes().to_vec()),
3132                                Value::Bytes(b) => Some(b.clone()),
3133                                Value::Empty => None,
3134                                _ => {
3135                                    return Err(format!(
3136                                "type mismatch: cannot assign non-var value to var column '{}'",
3137                                schema.columns[*idx].name
3138                            ))
3139                                }
3140                            };
3141                            Some((*idx, bytes_opt))
3142                        } else {
3143                            None
3144                        }
3145                    };
3146
3147                    if let Some((col_idx, new_bytes_opt)) = var_fast {
3148                        let new_bytes_ref: Option<&[u8]> = new_bytes_opt.as_deref();
3149                        let mut count = 0u64;
3150                        let mut fallback_rids: Vec<RowId> = Vec::new();
3151                        for rid in &matching_rids {
3152                            // Mission B2: logged variant so crash recovery
3153                            // replays the shrink. On a false return (row
3154                            // would have to grow), the rid is pushed to
3155                            // `fallback_rids` and the slower `update_hinted`
3156                            // path — which is already WAL-logged — picks it up.
3157                            let ok = self
3158                                .catalog
3159                                .patch_var_col_logged(table, *rid, col_idx, new_bytes_ref)
3160                                .map_err(|e| e.to_string())?;
3161                            if ok {
3162                                count += 1;
3163                            } else {
3164                                fallback_rids.push(*rid);
3165                            }
3166                        }
3167                        for rid in fallback_rids {
3168                            let mut row = match self.catalog.get(table, rid) {
3169                                Some(r) => r,
3170                                None => continue,
3171                            };
3172                            for (idx, val) in resolved_assignments.iter() {
3173                                row[*idx] = val.clone();
3174                            }
3175                            self.catalog
3176                                .update_hinted(table, rid, &row, Some(&changed_cols))
3177                                .map_err(|e| e.to_string())?;
3178                            count += 1;
3179                        }
3180                        self.view_registry.mark_dependents_dirty(table);
3181                        return Ok(QueryResult::Modified(count));
3182                    }
3183
3184                    // Generic literal path: decode row, apply literal values.
3185                    let mut count = 0u64;
3186                    for rid in matching_rids {
3187                        let mut row = match self.catalog.get(table, rid) {
3188                            Some(r) => r,
3189                            None => continue,
3190                        };
3191                        for (idx, val) in resolved_assignments.iter() {
3192                            row[*idx] = val.clone();
3193                        }
3194                        self.catalog
3195                            .update_hinted(table, rid, &row, Some(&changed_cols))
3196                            .map_err(|e| e.to_string())?;
3197                        count += 1;
3198                    }
3199                    self.view_registry.mark_dependents_dirty(table);
3200                    return Ok(QueryResult::Modified(count));
3201                } // end if let Some(resolved_assignments)
3202
3203                // ── Expression-based update path ────────────────────────
3204                // At least one assignment contains a non-literal expression
3205                // (e.g., `age := .age + 1`). Evaluate per-row.
3206                let col_names: Vec<String> = {
3207                    let schema_ref = self
3208                        .catalog
3209                        .schema(table)
3210                        .ok_or_else(|| format!("table '{table}' not found"))?;
3211                    schema_ref.columns.iter().map(|c| c.name.clone()).collect()
3212                };
3213                let mut count = 0u64;
3214                for rid in matching_rids {
3215                    let mut row = match self.catalog.get(table, rid) {
3216                        Some(r) => r,
3217                        None => continue,
3218                    };
3219                    for (i, asgn) in assignments.iter().enumerate() {
3220                        let val = eval_expr(&asgn.value, &row, &col_names);
3221                        row[col_indices[i]] = val;
3222                    }
3223                    self.catalog
3224                        .update_hinted(table, rid, &row, Some(&changed_cols))
3225                        .map_err(|e| e.to_string())?;
3226                    count += 1;
3227                }
3228                self.view_registry.mark_dependents_dirty(table);
3229                Ok(QueryResult::Modified(count))
3230            }
3231
3232            PlanNode::Delete { input, table } => {
3233                // Mission C Phase 3: no schema clone — collect_rids_for_mutation
3234                // looks up schema internally when it needs one, and the mutation
3235                // loop doesn't need the schema at all.
3236                //
3237                // Mission C Phase 12: route bulk deletes through
3238                // `Catalog::delete_many`, which batches the btree leaf
3239                // compaction and shares one `ensure_hot` per row between
3240                // the index-key extraction and the slot delete. On
3241                // `delete_by_filter` (100K fixture, ~20K matches) that
3242                // removes ~4ms of pure `Vec::remove` memmove from the btree
3243                // maintenance phase.
3244                //
3245                // Mission C Phase 16: for the common `delete where ...`
3246                // shape (Filter(SeqScan)) — and the rarer "delete
3247                // everything" shape (SeqScan) — skip the two-pass
3248                // `collect_rids_for_mutation` + `delete_many` flow entirely.
3249                // The fused `scan_delete_matching` primitive walks the
3250                // heap exactly once, paying one `ensure_hot` per page
3251                // instead of per-row. That closes the last major gap on
3252                // the bench's `delete_by_filter` workload.
3253                if let PlanNode::Filter {
3254                    input: inner,
3255                    predicate,
3256                } = input.as_ref()
3257                {
3258                    if let PlanNode::SeqScan { table: t } = inner.as_ref() {
3259                        if t == table {
3260                            let schema = self
3261                                .catalog
3262                                .schema(table)
3263                                .ok_or_else(|| format!("table '{table}' not found"))?;
3264                            let columns: Vec<String> =
3265                                schema.columns.iter().map(|c| c.name.clone()).collect();
3266                            let fast = FastLayout::new(schema);
3267                            if let Some(compiled) =
3268                                compile_predicate(predicate, &columns, &fast, schema)
3269                            {
3270                                // Mission B2: logged variant so every
3271                                // matched rid hits the WAL during the
3272                                // single-pass scan. Structure of the
3273                                // fused scan is unchanged — only the
3274                                // hook closure now also appends.
3275                                let count = self
3276                                    .catalog
3277                                    .scan_delete_matching_logged(table, |data| compiled(data))
3278                                    .map_err(|e| e.to_string())?;
3279                                self.view_registry.mark_dependents_dirty(table);
3280                                return Ok(QueryResult::Modified(count));
3281                            }
3282                        }
3283                    }
3284                } else if let PlanNode::SeqScan { table: t } = input.as_ref() {
3285                    if t == table {
3286                        // `delete from T` with no predicate — every live
3287                        // row matches. One pass is still the right shape.
3288                        // Mission B2: logged variant — see above.
3289                        let count = self
3290                            .catalog
3291                            .scan_delete_matching_logged(table, |_| true)
3292                            .map_err(|e| e.to_string())?;
3293                        self.view_registry.mark_dependents_dirty(table);
3294                        return Ok(QueryResult::Modified(count));
3295                    }
3296                }
3297
3298                let matching_rids = self.collect_rids_for_mutation(input, table)?;
3299                let count = self
3300                    .catalog
3301                    .delete_many(table, &matching_rids)
3302                    .map_err(|e| e.to_string())?;
3303                self.view_registry.mark_dependents_dirty(table);
3304                Ok(QueryResult::Modified(count))
3305            }
3306
3307            PlanNode::AliasScan { table, alias } => {
3308                // Mission E1.2: scan `table` and rename every output column
3309                // to `alias.field`. Used as a join leaf so downstream
3310                // NestedLoopJoin + Filter + Project nodes can resolve
3311                // `Expr::QualifiedField` lookups by direct column-name match.
3312                //
3313                // We don't bother with a fused zero-copy loop here yet — the
3314                // whole join path is nested-loop and correctness-first
3315                // (Phase E1.3 will introduce hash join and at that point we
3316                // can revisit whether to specialise AliasScan).
3317                let schema = self
3318                    .catalog
3319                    .schema(table)
3320                    .ok_or_else(|| format!("table '{table}' not found"))?
3321                    .clone();
3322                let columns: Vec<String> = schema
3323                    .columns
3324                    .iter()
3325                    .map(|c| format!("{alias}.{}", c.name))
3326                    .collect();
3327                let rows: Vec<Vec<Value>> = self
3328                    .catalog
3329                    .scan(table)
3330                    .map_err(|e| e.to_string())?
3331                    .map(|(_, row)| row)
3332                    .collect();
3333                Ok(QueryResult::Rows { columns, rows })
3334            }
3335
3336            PlanNode::NestedLoopJoin {
3337                left,
3338                right,
3339                on,
3340                kind,
3341            } => {
3342                // Materialise both sides. The executor ships two strategies:
3343                //   1. Hash join (E1.3) — when the `on` predicate is a
3344                //      simple equi-predicate `left_col = right_col`, build a
3345                //      FxHashMap<Value, Vec<row_idx>> over the right side
3346                //      and probe with the left side. O(L + R) instead of
3347                //      O(L × R). Handles Inner and LeftOuter.
3348                //   2. Nested loop (E1.2) — fallback for Cross, non-equi
3349                //      predicates, or `on` expressions that reference
3350                //      either side with something more complex than a
3351                //      QualifiedField.
3352                let left_result = self.execute_plan(left)?;
3353                let right_result = self.execute_plan(right)?;
3354                let (left_columns, left_rows) = match left_result {
3355                    QueryResult::Rows { columns, rows } => (columns, rows),
3356                    _ => return Err("join left side must produce rows".into()),
3357                };
3358                let (right_columns, right_rows) = match right_result {
3359                    QueryResult::Rows { columns, rows } => (columns, rows),
3360                    _ => return Err("join right side must produce rows".into()),
3361                };
3362
3363                // Hash-join fast path.
3364                if !matches!(kind, JoinKind::Cross) {
3365                    if let Some(pred) = on {
3366                        if let Some((l_idx, r_idx)) =
3367                            try_extract_equi_join_keys(pred, &left_columns, &right_columns)
3368                        {
3369                            let result = hash_join(
3370                                left_columns,
3371                                left_rows,
3372                                right_columns,
3373                                right_rows,
3374                                l_idx,
3375                                r_idx,
3376                                *kind,
3377                            );
3378                            if let QueryResult::Rows { ref rows, .. } = result {
3379                                check_join_limit(rows.len())?;
3380                            }
3381                            return Ok(result);
3382                        }
3383                    }
3384                }
3385
3386                // Nested-loop fallback.
3387                let n_left = left_columns.len();
3388                let n_right = right_columns.len();
3389                let mut columns = Vec::with_capacity(n_left + n_right);
3390                columns.extend(left_columns);
3391                columns.extend(right_columns);
3392
3393                let mut rows: Vec<Vec<Value>> = Vec::with_capacity(left_rows.len());
3394                let mut combined: Vec<Value> = Vec::with_capacity(n_left + n_right);
3395
3396                for left_row in &left_rows {
3397                    let mut matched = false;
3398                    for right_row in &right_rows {
3399                        combined.clear();
3400                        combined.extend_from_slice(left_row);
3401                        combined.extend_from_slice(right_row);
3402                        let keep = match kind {
3403                            JoinKind::Cross => true,
3404                            JoinKind::Inner | JoinKind::LeftOuter => match on {
3405                                Some(pred) => eval_predicate(pred, &combined, &columns),
3406                                // Missing `on` for non-cross joins is a
3407                                // parser error, but if it slips through we
3408                                // treat it as "match everything".
3409                                None => true,
3410                            },
3411                            // RightOuter is rewritten to LeftOuter by the
3412                            // planner, so we never see it here.
3413                            JoinKind::RightOuter => {
3414                                unreachable!("planner rewrites RightOuter to LeftOuter")
3415                            }
3416                        };
3417                        if keep {
3418                            rows.push(combined.clone());
3419                            check_join_limit(rows.len())?;
3420                            matched = true;
3421                        }
3422                    }
3423                    if !matched && matches!(kind, JoinKind::LeftOuter) {
3424                        let mut row = Vec::with_capacity(n_left + n_right);
3425                        row.extend_from_slice(left_row);
3426                        row.resize(n_left + n_right, Value::Empty);
3427                        rows.push(row);
3428                        check_join_limit(rows.len())?;
3429                    }
3430                }
3431
3432                Ok(QueryResult::Rows { columns, rows })
3433            }
3434
3435            PlanNode::Distinct { input } => {
3436                let result = self.execute_plan(input)?;
3437                match result {
3438                    QueryResult::Rows { columns, rows } => {
3439                        let mut seen = std::collections::HashSet::new();
3440                        let mut unique_rows = Vec::new();
3441                        for row in rows {
3442                            if seen.insert(row.clone()) {
3443                                unique_rows.push(row);
3444                            }
3445                        }
3446                        Ok(QueryResult::Rows {
3447                            columns,
3448                            rows: unique_rows,
3449                        })
3450                    }
3451                    other => Ok(other),
3452                }
3453            }
3454
3455            PlanNode::GroupBy {
3456                input,
3457                keys,
3458                aggregates,
3459                having,
3460            } => {
3461                let result = self.execute_plan(input)?;
3462                match result {
3463                    QueryResult::Rows { columns, rows } => {
3464                        // Resolve key column indices.
3465                        let key_indices: Vec<usize> = keys
3466                            .iter()
3467                            .map(|k| {
3468                                columns
3469                                    .iter()
3470                                    .position(|c| c == k)
3471                                    .ok_or_else(|| format!("group-by column '{k}' not found"))
3472                            })
3473                            .collect::<Result<Vec<_>, _>>()?;
3474
3475                        // Resolve aggregate field indices. count(*) uses
3476                        // sentinel usize::MAX — compute_group_aggregate
3477                        // treats it as "count all rows in the group".
3478                        let agg_field_indices: Vec<usize> = aggregates
3479                            .iter()
3480                            .map(|a| {
3481                                if a.field == "*" {
3482                                    Ok(usize::MAX)
3483                                } else {
3484                                    columns.iter().position(|c| c == &a.field).ok_or_else(|| {
3485                                        format!("aggregate column '{}' not found", a.field)
3486                                    })
3487                                }
3488                            })
3489                            .collect::<Result<Vec<_>, _>>()?;
3490
3491                        // Group rows by key values (preserving insertion order).
3492                        let mut group_map: rustc_hash::FxHashMap<Vec<Value>, usize> =
3493                            rustc_hash::FxHashMap::default();
3494                        let mut groups: Vec<(Vec<Value>, Vec<usize>)> = Vec::new();
3495                        for (ri, row) in rows.iter().enumerate() {
3496                            let key: Vec<Value> =
3497                                key_indices.iter().map(|&i| row[i].clone()).collect();
3498                            match group_map.get(&key) {
3499                                Some(&idx) => groups[idx].1.push(ri),
3500                                None => {
3501                                    let idx = groups.len();
3502                                    group_map.insert(key.clone(), idx);
3503                                    groups.push((key, vec![ri]));
3504                                }
3505                            }
3506                        }
3507
3508                        // Build output column names: keys ++ aggregate output names.
3509                        let mut out_columns: Vec<String> = keys.clone();
3510                        for agg in aggregates.iter() {
3511                            out_columns.push(agg.output_name.clone());
3512                        }
3513
3514                        // Compute aggregates per group.
3515                        let mut out_rows: Vec<Vec<Value>> = Vec::with_capacity(groups.len());
3516                        for (key_vals, row_indices) in &groups {
3517                            let mut row = key_vals.clone();
3518                            for (ai, agg) in aggregates.iter().enumerate() {
3519                                let col_idx = agg_field_indices[ai];
3520                                let val = compute_group_aggregate(
3521                                    agg.function,
3522                                    &rows,
3523                                    row_indices,
3524                                    col_idx,
3525                                );
3526                                row.push(val);
3527                            }
3528                            out_rows.push(row);
3529                        }
3530
3531                        // Apply HAVING filter.
3532                        if let Some(having_expr) = having {
3533                            out_rows.retain(|row| eval_predicate(having_expr, row, &out_columns));
3534                        }
3535
3536                        Ok(QueryResult::Rows {
3537                            columns: out_columns,
3538                            rows: out_rows,
3539                        })
3540                    }
3541                    _ => Err("group by requires row input".into()),
3542                }
3543            }
3544
3545            PlanNode::CreateTable { name, fields } => {
3546                let columns: Vec<ColumnDef> = fields
3547                    .iter()
3548                    .enumerate()
3549                    .map(|(i, (fname, tname, req))| ColumnDef {
3550                        name: fname.clone(),
3551                        type_id: type_name_to_id(tname),
3552                        required: *req,
3553                        position: i as u16,
3554                    })
3555                    .collect();
3556                let schema = Schema {
3557                    table_name: name.clone(),
3558                    columns,
3559                };
3560                self.catalog
3561                    .create_table(schema)
3562                    .map_err(|e| e.to_string())?;
3563                Ok(QueryResult::Created(name.clone()))
3564            }
3565
3566            PlanNode::AlterTable { table, action } => match action {
3567                AlterAction::AddColumn {
3568                    name,
3569                    type_name,
3570                    required,
3571                } => {
3572                    let position = self
3573                        .catalog
3574                        .schema(table)
3575                        .ok_or_else(|| format!("table '{table}' not found"))?
3576                        .columns
3577                        .len() as u16;
3578                    let col = ColumnDef {
3579                        name: name.clone(),
3580                        type_id: type_name_to_id(type_name),
3581                        required: *required,
3582                        position,
3583                    };
3584                    self.catalog
3585                        .alter_table_add_column(table, col)
3586                        .map_err(|e| e.to_string())?;
3587                    Ok(QueryResult::Executed {
3588                        message: format!("column '{name}' added to '{table}'"),
3589                    })
3590                }
3591                AlterAction::DropColumn { name } => {
3592                    self.catalog
3593                        .alter_table_drop_column(table, name)
3594                        .map_err(|e| e.to_string())?;
3595                    Ok(QueryResult::Executed {
3596                        message: format!("column '{name}' dropped from '{table}'"),
3597                    })
3598                }
3599                AlterAction::AddIndex { column } => {
3600                    self.catalog
3601                        .create_index(table, column)
3602                        .map_err(|e| e.to_string())?;
3603                    Ok(QueryResult::Executed {
3604                        message: format!("index on '{table}.{column}' created"),
3605                    })
3606                }
3607            },
3608
3609            PlanNode::DropTable { name } => {
3610                self.catalog.drop_table(name).map_err(|e| e.to_string())?;
3611                Ok(QueryResult::Executed {
3612                    message: format!("table '{name}' dropped"),
3613                })
3614            }
3615
3616            PlanNode::CreateView { name, query_text } => {
3617                self.create_view(name, query_text)?;
3618                Ok(QueryResult::Executed {
3619                    message: format!("materialized view '{name}' created"),
3620                })
3621            }
3622
3623            PlanNode::RefreshView { name } => {
3624                self.refresh_view(name)?;
3625                Ok(QueryResult::Executed {
3626                    message: format!("materialized view '{name}' refreshed"),
3627                })
3628            }
3629
3630            PlanNode::DropView { name } => {
3631                self.drop_view(name)?;
3632                Ok(QueryResult::Executed {
3633                    message: format!("materialized view '{name}' dropped"),
3634                })
3635            }
3636
3637            PlanNode::Window { input, windows } => {
3638                let result = self.execute_plan(input)?;
3639                execute_window(result, windows)
3640            }
3641
3642            PlanNode::Union { left, right, all } => {
3643                let left_result = self.execute_plan(left)?;
3644                let right_result = self.execute_plan(right)?;
3645                let (left_cols, left_rows) = match left_result {
3646                    QueryResult::Rows { columns, rows } => (columns, rows),
3647                    _ => return Err("UNION requires query results on left side".into()),
3648                };
3649                let (_, right_rows) = match right_result {
3650                    QueryResult::Rows { columns, rows } => (columns, rows),
3651                    _ => return Err("UNION requires query results on right side".into()),
3652                };
3653                let mut combined = left_rows;
3654                if *all {
3655                    // UNION ALL — just concatenate.
3656                    combined.extend(right_rows);
3657                } else {
3658                    // UNION — deduplicate using the same HashSet approach
3659                    // as DISTINCT. Value already implements Hash + Eq.
3660                    let mut seen = std::collections::HashSet::new();
3661                    for row in &combined {
3662                        seen.insert(row.clone());
3663                    }
3664                    for row in right_rows {
3665                        if seen.insert(row.clone()) {
3666                            combined.push(row);
3667                        }
3668                    }
3669                }
3670                Ok(QueryResult::Rows {
3671                    columns: left_cols,
3672                    rows: combined,
3673                })
3674            }
3675
3676            PlanNode::Explain { input } => {
3677                let text = format_plan_tree(input, 0);
3678                Ok(QueryResult::Rows {
3679                    columns: vec!["plan".to_string()],
3680                    rows: text
3681                        .lines()
3682                        .map(|line| vec![Value::Str(line.to_string())])
3683                        .collect(),
3684                })
3685            }
3686
3687            PlanNode::IndexScan { table, column, key } => {
3688                let key_value = literal_to_value(key)?;
3689                let tbl = self
3690                    .catalog
3691                    .get_table(table)
3692                    .ok_or_else(|| format!("table '{table}' not found"))?;
3693                let columns: Vec<String> =
3694                    tbl.schema.columns.iter().map(|c| c.name.clone()).collect();
3695
3696                // Fast path: the table has a B-tree on this column. A single
3697                // point lookup returns 0 or 1 rows — this is the whole reason
3698                // the planner bothers emitting IndexScan.
3699                //
3700                // Mission D7: use `lookup_int` on int-keyed indexes to skip
3701                // the Value enum dispatch in the inner binary search. The
3702                // generic `tbl.index_lookup` helper can't do this without
3703                // lying about the key type, so we inline the index+heap
3704                // touch here.
3705                if let Some(btree) = tbl.index(column) {
3706                    let hit = match &key_value {
3707                        Value::Int(k) => btree.lookup_int(*k),
3708                        other => btree.lookup(other),
3709                    };
3710                    let rows = match hit {
3711                        Some(rid) => match tbl.heap.get(rid) {
3712                            Some(data) => vec![decode_row(&tbl.schema, &data)],
3713                            None => Vec::new(),
3714                        },
3715                        None => Vec::new(),
3716                    };
3717                    return Ok(QueryResult::Rows { columns, rows });
3718                }
3719
3720                // Fallback: no index on this column. The planner emits IndexScan
3721                // eagerly (it has no visibility into which columns are indexed
3722                // at plan time), so here we must behave like SeqScan+Filter on
3723                // `.col = literal`: return *all* matching rows, not just the
3724                // first one. A non-indexed column isn't necessarily unique.
3725                // We compile the eq predicate once and stream without any
3726                // per-row decode for non-matching rows.
3727                let schema = &tbl.schema;
3728                let fast = FastLayout::new(schema);
3729                let synth_pred = Expr::BinaryOp(
3730                    Box::new(Expr::Field(column.clone())),
3731                    BinOp::Eq,
3732                    Box::new(key.clone()),
3733                );
3734                if let Some(compiled) = compile_predicate(&synth_pred, &columns, &fast, schema) {
3735                    // Mission F: skip the first 4 Vec doublings.
3736                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
3737                    self.catalog
3738                        .for_each_row_raw(table, |_rid, data| {
3739                            if compiled(data) {
3740                                rows.push(decode_row(schema, data));
3741                            }
3742                        })
3743                        .map_err(|e| e.to_string())?;
3744                    return Ok(QueryResult::Rows { columns, rows });
3745                }
3746
3747                // Last resort: slow eq-check on materialised rows.
3748                let col_idx = schema
3749                    .column_index(column)
3750                    .ok_or_else(|| format!("column '{column}' not found"))?;
3751                let rows: Vec<Vec<Value>> = tbl
3752                    .scan()
3753                    .filter_map(|(_, row)| {
3754                        if row[col_idx] == key_value {
3755                            Some(row)
3756                        } else {
3757                            None
3758                        }
3759                    })
3760                    .collect();
3761                Ok(QueryResult::Rows { columns, rows })
3762            }
3763
3764            PlanNode::RangeScan {
3765                table,
3766                column,
3767                start,
3768                end,
3769            } => {
3770                let tbl = self
3771                    .catalog
3772                    .get_table(table)
3773                    .ok_or_else(|| format!("table '{table}' not found"))?;
3774                let columns: Vec<String> =
3775                    tbl.schema.columns.iter().map(|c| c.name.clone()).collect();
3776                let schema = &tbl.schema;
3777
3778                let start_val = match start {
3779                    Some((expr, _)) => Some(literal_to_value(expr)?),
3780                    None => None,
3781                };
3782                let end_val = match end {
3783                    Some((expr, _)) => Some(literal_to_value(expr)?),
3784                    None => None,
3785                };
3786                let start_inclusive = start.as_ref().map(|(_, inc)| *inc).unwrap_or(true);
3787                let end_inclusive = end.as_ref().map(|(_, inc)| *inc).unwrap_or(true);
3788
3789                if let Some(btree) = tbl.index(column) {
3790                    let hits: Vec<(Value, RowId)> = match (&start_val, &end_val) {
3791                        (Some(s), Some(e)) => btree.range(s, e).collect(),
3792                        (Some(s), None) => btree.range_from(s),
3793                        (None, Some(e)) => btree.range_to(e),
3794                        (None, None) => {
3795                            let rows: Vec<Vec<Value>> = tbl.scan().map(|(_, row)| row).collect();
3796                            return Ok(QueryResult::Rows { columns, rows });
3797                        }
3798                    };
3799                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(hits.len());
3800                    for (key, rid) in hits {
3801                        if !start_inclusive {
3802                            if let Some(ref s) = start_val {
3803                                if &key == s {
3804                                    continue;
3805                                }
3806                            }
3807                        }
3808                        if !end_inclusive {
3809                            if let Some(ref e) = end_val {
3810                                if &key == e {
3811                                    continue;
3812                                }
3813                            }
3814                        }
3815                        if let Some(data) = tbl.heap.get(rid) {
3816                            rows.push(decode_row(schema, &data));
3817                        }
3818                    }
3819                    return Ok(QueryResult::Rows { columns, rows });
3820                }
3821
3822                // Fallback: no index — synthesize range predicate and scan.
3823                let fast = FastLayout::new(schema);
3824                let synth = synthesize_range_predicate(column, start, end);
3825                if let Some(compiled) = compile_predicate(&synth, &columns, &fast, schema) {
3826                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
3827                    self.catalog
3828                        .for_each_row_raw(table, |_rid, data| {
3829                            if compiled(data) {
3830                                rows.push(decode_row(schema, data));
3831                            }
3832                        })
3833                        .map_err(|e| e.to_string())?;
3834                    return Ok(QueryResult::Rows { columns, rows });
3835                }
3836
3837                let col_idx = schema
3838                    .column_index(column)
3839                    .ok_or_else(|| format!("column '{column}' not found"))?;
3840                let rows: Vec<Vec<Value>> = tbl
3841                    .scan()
3842                    .filter(|(_, row)| {
3843                        range_matches(
3844                            &row[col_idx],
3845                            &start_val,
3846                            start_inclusive,
3847                            &end_val,
3848                            end_inclusive,
3849                        )
3850                    })
3851                    .map(|(_, row)| row)
3852                    .collect();
3853                Ok(QueryResult::Rows { columns, rows })
3854            }
3855        }
3856    }
3857
3858    // ─── Materialized view operations ──────────────────────────────────────
3859
3860    /// Create a materialized view: execute the source query, store results
3861    /// in a new backing table, and register the view.
3862    fn create_view(&mut self, name: &str, query_text: &str) -> Result<(), String> {
3863        if self.view_registry.is_view(name) {
3864            return Err(format!("materialized view '{name}' already exists"));
3865        }
3866        // Execute the source query to get the result set.
3867        let result = self.execute_powql(query_text)?;
3868        let (columns, rows) = match result {
3869            QueryResult::Rows { columns, rows } => (columns, rows),
3870            _ => return Err("view source query must be a SELECT".into()),
3871        };
3872        // Derive a schema for the backing table from the query result columns.
3873        let schema = self.derive_view_schema(name, &columns, &rows);
3874        // Create the backing table and insert the result rows.
3875        self.catalog
3876            .create_table(schema)
3877            .map_err(|e| e.to_string())?;
3878        for row in &rows {
3879            self.catalog.insert(name, row).map_err(|e| e.to_string())?;
3880        }
3881        // Determine which base tables this view depends on by parsing the query.
3882        let depends_on = self.extract_view_deps(query_text);
3883        self.view_registry
3884            .register(ViewDef {
3885                name: name.to_string(),
3886                query: query_text.to_string(),
3887                depends_on,
3888                dirty: false,
3889            })
3890            .map_err(|e| e.to_string())?;
3891        Ok(())
3892    }
3893
3894    /// Refresh a materialized view: re-execute its source query and replace
3895    /// the backing table's contents.
3896    fn refresh_view(&mut self, name: &str) -> Result<(), String> {
3897        let def = self
3898            .view_registry
3899            .get(name)
3900            .ok_or_else(|| format!("materialized view '{name}' not found"))?;
3901        let query_text = def.query.clone();
3902        // Execute the source query.
3903        let result = self.execute_powql(&query_text)?;
3904        let (_columns, rows) = match result {
3905            QueryResult::Rows { columns, rows } => (columns, rows),
3906            _ => return Err("view source query must be a SELECT".into()),
3907        };
3908        // Clear old data and insert fresh results. Mission B2: logged
3909        // variant — view refreshes are a mutation and crash recovery
3910        // must see them.
3911        self.catalog
3912            .scan_delete_matching_logged(name, |_| true)
3913            .map_err(|e| e.to_string())?;
3914        for row in &rows {
3915            self.catalog.insert(name, row).map_err(|e| e.to_string())?;
3916        }
3917        self.view_registry.mark_clean(name);
3918        Ok(())
3919    }
3920
3921    /// Drop a materialized view: remove the backing table and unregister.
3922    fn drop_view(&mut self, name: &str) -> Result<(), String> {
3923        if !self.view_registry.is_view(name) {
3924            return Err(format!("materialized view '{name}' not found"));
3925        }
3926        self.view_registry
3927            .unregister(name)
3928            .map_err(|e| e.to_string())?;
3929        self.catalog.drop_table(name).map_err(|e| e.to_string())?;
3930        Ok(())
3931    }
3932
3933    /// Derive a storage `Schema` for a view's backing table from query
3934    /// result column names and the first row's types.
3935    fn derive_view_schema(&self, name: &str, columns: &[String], rows: &[Vec<Value>]) -> Schema {
3936        use powdb_storage::types::{ColumnDef, TypeId};
3937        let cols: Vec<ColumnDef> = columns
3938            .iter()
3939            .enumerate()
3940            .map(|(i, col_name)| {
3941                let type_id = rows
3942                    .first()
3943                    .and_then(|row| row.get(i))
3944                    .map(|v| v.type_id())
3945                    .unwrap_or(TypeId::Str);
3946                ColumnDef {
3947                    name: col_name.clone(),
3948                    type_id,
3949                    required: false,
3950                    position: i as u16,
3951                }
3952            })
3953            .collect();
3954        Schema {
3955            table_name: name.to_string(),
3956            columns: cols,
3957        }
3958    }
3959
3960    /// Extract base table dependencies from a view's source query by
3961    /// parsing it and collecting the source table name.
3962    fn extract_view_deps(&self, query_text: &str) -> Vec<String> {
3963        use crate::parser::parse;
3964        match parse(query_text) {
3965            Ok(Statement::Query(q)) => {
3966                let mut deps = vec![q.source.clone()];
3967                for j in &q.joins {
3968                    deps.push(j.source.clone());
3969                }
3970                deps
3971            }
3972            _ => Vec::new(),
3973        }
3974    }
3975
3976    // ─── Specialized fast paths ─────────────────────────────────────────────
3977    //
3978    // These methods are helpers for the `execute_plan` match arms above.
3979    // Each returns `Ok(Some(result))` when the fast path fires, `Ok(None)`
3980    // when the shape isn't supported (caller falls back to generic code).
3981
3982    /// Aggregate sum/avg/min/max over a single fixed-size i64 column, with
3983    /// an optional compiled filter predicate. Walks raw row bytes — zero
3984    /// per-row allocation. Uses i128 accumulator for sum/avg overflow safety.
3985    fn agg_single_col_fast(
3986        &self,
3987        table: &str,
3988        col: &str,
3989        function: AggFunc,
3990        predicate: Option<&Expr>,
3991    ) -> Result<Option<QueryResult>, String> {
3992        let schema = self
3993            .catalog
3994            .schema(table)
3995            .ok_or_else(|| format!("table '{table}' not found"))?
3996            .clone();
3997        let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
3998        let col_idx = match schema.column_index(col) {
3999            Some(i) => i,
4000            None => return Ok(None),
4001        };
4002        // Only fast-path fixed-size numeric columns (Int/Float) for
4003        // sum/avg/min/max/count. Mission D10: Float parity — prior version
4004        // bailed on Float columns, forcing them through the generic row-
4005        // decoding path that allocated a Vec<Value> per row and dispatched
4006        // on Value::cmp for every compare. f64 decode is structurally the
4007        // same as i64 (load 8 bytes, cast), so the fast path handles both.
4008        let col_type = schema.columns[col_idx].type_id;
4009        if col_type != TypeId::Int && col_type != TypeId::Float {
4010            return Ok(None);
4011        }
4012
4013        let fast = FastLayout::new(&schema);
4014        // Mission C Phase 20b: inline the numeric-column reader instead of
4015        // building a `Box<dyn Fn>`. Eliminates 100K vtable dispatches per
4016        // 100K-row agg scan — every reader call folds directly into the
4017        // hot loop below.
4018        let byte_offset = match fast.fixed_offsets[col_idx] {
4019            Some(o) => o,
4020            None => return Ok(None),
4021        };
4022        let bitmap_byte = col_idx / 8;
4023        let bitmap_bit = (col_idx % 8) as u32;
4024        let data_offset = 2 + fast.bitmap_size + byte_offset;
4025
4026        // Optional compiled filter.
4027        let compiled_pred: Option<CompiledPredicate> = match predicate {
4028            Some(pred) => match compile_predicate(pred, &columns, &fast, &schema) {
4029                Some(c) => Some(c),
4030                None => return Ok(None), // let generic path handle it
4031            },
4032            None => None,
4033        };
4034
4035        // Mission C Phase 20b: specialize the inner loop per aggregate
4036        // function. The previous version ran a `match function { ... }`
4037        // *inside* the closure, which kept LLVM from producing optimal
4038        // scalar code for each variant (agg_max regressed ~23% vs the
4039        // baseline Box<dyn Fn> version even though per-row vtable cost
4040        // should have been strictly lower). Pushing the match out of the
4041        // hot loop lets each specialized body fold cleanly into
4042        // `for_each_row_raw` and removes a captured `AggFunc` + match
4043        // dispatch per row.
4044        //
4045        // Mission D10: same specialisation applies to the Float branch.
4046        // For Min/Max we use `f64::total_cmp` so the result matches
4047        // `Value::Ord` — this is the same ordering ORDER BY and the
4048        // top-N sort fast path use, keeping semantics consistent across
4049        // read paths (NaN compares as greatest, -0.0 < +0.0 for
4050        // deterministic tie-breaking).
4051        //
4052        // Mission D11 Phase 1: each inner loop now splits on presence of
4053        // a predicate (`if let Some(pred) = &compiled_pred`) so the hot
4054        // body never re-tests `Option` per row, and reads column bytes
4055        // via `read_i64_unchecked` / `read_f64_unchecked` helpers that
4056        // drop two bounds checks per row (null bitmap byte + value
4057        // slice). Safety is carried by the `FastLayout` invariant that
4058        // `data_offset + 8 <= row_len` for any fixed-size column; see
4059        // the helper doc comments. Hot loops are macro-generated so the
4060        // with-pred / no-pred split can't drift between variants.
4061        let result = match col_type {
4062            TypeId::Int => match function {
4063                AggFunc::Sum | AggFunc::Avg => {
4064                    let mut sum_i128: i128 = 0;
4065                    let mut count: i64 = 0;
4066                    agg_int_loop!(
4067                        self,
4068                        table,
4069                        compiled_pred,
4070                        bitmap_byte,
4071                        bitmap_bit,
4072                        data_offset,
4073                        |v: i64| {
4074                            count += 1;
4075                            sum_i128 += v as i128;
4076                        }
4077                    );
4078                    if matches!(function, AggFunc::Sum) {
4079                        let clamped = sum_i128.clamp(i64::MIN as i128, i64::MAX as i128) as i64;
4080                        QueryResult::Scalar(Value::Int(clamped))
4081                    } else if count == 0 {
4082                        QueryResult::Scalar(Value::Empty)
4083                    } else {
4084                        let avg = (sum_i128 as f64) / (count as f64);
4085                        QueryResult::Scalar(Value::Float(avg))
4086                    }
4087                }
4088                AggFunc::Min => {
4089                    let mut min_v: Option<i64> = None;
4090                    agg_int_loop!(
4091                        self,
4092                        table,
4093                        compiled_pred,
4094                        bitmap_byte,
4095                        bitmap_bit,
4096                        data_offset,
4097                        |v: i64| {
4098                            min_v = Some(match min_v {
4099                                Some(m) => m.min(v),
4100                                None => v,
4101                            });
4102                        }
4103                    );
4104                    QueryResult::Scalar(min_v.map(Value::Int).unwrap_or(Value::Empty))
4105                }
4106                AggFunc::Max => {
4107                    let mut max_v: Option<i64> = None;
4108                    agg_int_loop!(
4109                        self,
4110                        table,
4111                        compiled_pred,
4112                        bitmap_byte,
4113                        bitmap_bit,
4114                        data_offset,
4115                        |v: i64| {
4116                            max_v = Some(match max_v {
4117                                Some(m) => m.max(v),
4118                                None => v,
4119                            });
4120                        }
4121                    );
4122                    QueryResult::Scalar(max_v.map(Value::Int).unwrap_or(Value::Empty))
4123                }
4124                AggFunc::Count => {
4125                    let mut count: i64 = 0;
4126                    agg_int_loop!(
4127                        self,
4128                        table,
4129                        compiled_pred,
4130                        bitmap_byte,
4131                        bitmap_bit,
4132                        data_offset,
4133                        |_v: i64| {
4134                            count += 1;
4135                        }
4136                    );
4137                    QueryResult::Scalar(Value::Int(count))
4138                }
4139                AggFunc::CountDistinct => {
4140                    let mut seen = rustc_hash::FxHashSet::default();
4141                    agg_int_loop!(
4142                        self,
4143                        table,
4144                        compiled_pred,
4145                        bitmap_byte,
4146                        bitmap_bit,
4147                        data_offset,
4148                        |v: i64| {
4149                            seen.insert(v);
4150                        }
4151                    );
4152                    QueryResult::Scalar(Value::Int(seen.len() as i64))
4153                }
4154            },
4155            TypeId::Float => match function {
4156                AggFunc::Sum => {
4157                    // Use a single f64 accumulator. Naive summation is
4158                    // sufficient for MVP parity; if precision becomes an
4159                    // issue on long scans we can upgrade to Kahan–Neumaier
4160                    // compensated sum (~2x scalar cost, zero error growth).
4161                    let mut sum: f64 = 0.0;
4162                    agg_float_loop!(
4163                        self,
4164                        table,
4165                        compiled_pred,
4166                        bitmap_byte,
4167                        bitmap_bit,
4168                        data_offset,
4169                        |v: f64| {
4170                            sum += v;
4171                        }
4172                    );
4173                    QueryResult::Scalar(Value::Float(sum))
4174                }
4175                AggFunc::Avg => {
4176                    let mut sum: f64 = 0.0;
4177                    let mut count: i64 = 0;
4178                    agg_float_loop!(
4179                        self,
4180                        table,
4181                        compiled_pred,
4182                        bitmap_byte,
4183                        bitmap_bit,
4184                        data_offset,
4185                        |v: f64| {
4186                            sum += v;
4187                            count += 1;
4188                        }
4189                    );
4190                    if count == 0 {
4191                        QueryResult::Scalar(Value::Empty)
4192                    } else {
4193                        QueryResult::Scalar(Value::Float(sum / count as f64))
4194                    }
4195                }
4196                AggFunc::Min => {
4197                    // `total_cmp` for deterministic NaN handling (matches
4198                    // Value::Ord). NaN compares greatest, so Min will
4199                    // correctly ignore it in favour of any finite value.
4200                    let mut min_v: Option<f64> = None;
4201                    agg_float_loop!(
4202                        self,
4203                        table,
4204                        compiled_pred,
4205                        bitmap_byte,
4206                        bitmap_bit,
4207                        data_offset,
4208                        |v: f64| {
4209                            min_v = Some(match min_v {
4210                                Some(m) => {
4211                                    if v.total_cmp(&m).is_lt() {
4212                                        v
4213                                    } else {
4214                                        m
4215                                    }
4216                                }
4217                                None => v,
4218                            });
4219                        }
4220                    );
4221                    QueryResult::Scalar(min_v.map(Value::Float).unwrap_or(Value::Empty))
4222                }
4223                AggFunc::Max => {
4224                    let mut max_v: Option<f64> = None;
4225                    agg_float_loop!(
4226                        self,
4227                        table,
4228                        compiled_pred,
4229                        bitmap_byte,
4230                        bitmap_bit,
4231                        data_offset,
4232                        |v: f64| {
4233                            max_v = Some(match max_v {
4234                                Some(m) => {
4235                                    if v.total_cmp(&m).is_gt() {
4236                                        v
4237                                    } else {
4238                                        m
4239                                    }
4240                                }
4241                                None => v,
4242                            });
4243                        }
4244                    );
4245                    QueryResult::Scalar(max_v.map(Value::Float).unwrap_or(Value::Empty))
4246                }
4247                AggFunc::Count => {
4248                    let mut count: i64 = 0;
4249                    agg_float_loop!(
4250                        self,
4251                        table,
4252                        compiled_pred,
4253                        bitmap_byte,
4254                        bitmap_bit,
4255                        data_offset,
4256                        |_v: f64| {
4257                            count += 1;
4258                        }
4259                    );
4260                    QueryResult::Scalar(Value::Int(count))
4261                }
4262                AggFunc::CountDistinct => {
4263                    // Hash on `f64::to_bits` — matches `Value::Hash`, so
4264                    // distinct NaN bit patterns count as distinct and
4265                    // -0.0/+0.0 count as distinct. Consistent with how
4266                    // Float values are hashed in every other DISTINCT /
4267                    // GROUP BY path.
4268                    let mut seen = rustc_hash::FxHashSet::default();
4269                    agg_float_loop!(
4270                        self,
4271                        table,
4272                        compiled_pred,
4273                        bitmap_byte,
4274                        bitmap_bit,
4275                        data_offset,
4276                        |v: f64| {
4277                            seen.insert(v.to_bits());
4278                        }
4279                    );
4280                    QueryResult::Scalar(Value::Int(seen.len() as i64))
4281                }
4282            },
4283            _ => unreachable!("type guard above restricts to Int/Float"),
4284        };
4285        Ok(Some(result))
4286    }
4287
4288    /// `Project(Limit(Filter(SeqScan)))` and `Project(Limit(SeqScan))`.
4289    /// Streams rows, decodes only projected columns, stops at the limit.
4290    fn project_filter_limit_fast(
4291        &self,
4292        table: &str,
4293        fields: &[ProjectField],
4294        limit: usize,
4295        predicate: Option<&Expr>,
4296    ) -> Result<Option<QueryResult>, String> {
4297        let schema = self
4298            .catalog
4299            .schema(table)
4300            .ok_or_else(|| format!("table '{table}' not found"))?
4301            .clone();
4302        let all_columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
4303
4304        // Each projection field must be a simple `.field` reference for this
4305        // fast path. Aliased or computed fields fall through.
4306        let mut proj_indices: Vec<usize> = Vec::with_capacity(fields.len());
4307        let mut proj_columns: Vec<String> = Vec::with_capacity(fields.len());
4308        for f in fields {
4309            let name = match &f.expr {
4310                Expr::Field(n) => n.clone(),
4311                _ => return Ok(None),
4312            };
4313            let idx = match all_columns.iter().position(|c| c == &name) {
4314                Some(i) => i,
4315                None => return Ok(None),
4316            };
4317            proj_indices.push(idx);
4318            proj_columns.push(f.alias.clone().unwrap_or(name));
4319        }
4320
4321        let fast = FastLayout::new(&schema);
4322        let row_layout = RowLayout::new(&schema);
4323
4324        let compiled_pred: Option<CompiledPredicate> = match predicate {
4325            Some(pred) => match compile_predicate(pred, &all_columns, &fast, &schema) {
4326                Some(c) => Some(c),
4327                None => return Ok(None),
4328            },
4329            None => None,
4330        };
4331
4332        let mut out: Vec<Vec<Value>> = Vec::with_capacity(limit.min(1024));
4333        // Mission D2: use try_for_each_row_raw to actually stop iterating
4334        // once the limit is reached. The previous `done` flag only short-
4335        // circuited the closure body, so a `limit 100` over 100K rows still
4336        // walked all 100K slots — burning ~30x SQLite on scan_filter_project_top100.
4337        self.catalog
4338            .try_for_each_row_raw(table, |_rid, data| {
4339                use std::ops::ControlFlow;
4340                if let Some(ref pred) = compiled_pred {
4341                    if !pred(data) {
4342                        return ControlFlow::Continue(());
4343                    }
4344                }
4345                let row: Vec<Value> = proj_indices
4346                    .iter()
4347                    .map(|&ci| decode_column(&schema, &row_layout, data, ci))
4348                    .collect();
4349                out.push(row);
4350                if out.len() >= limit {
4351                    ControlFlow::Break(())
4352                } else {
4353                    ControlFlow::Continue(())
4354                }
4355            })
4356            .map_err(|e| e.to_string())?;
4357
4358        Ok(Some(QueryResult::Rows {
4359            columns: proj_columns,
4360            rows: out,
4361        }))
4362    }
4363
4364    /// `Project(Limit(Sort(Filter(SeqScan))))` and `Project(Limit(Sort(SeqScan)))`.
4365    /// Bounded top-N heap over the sort key. Only the sort key needs to be
4366    /// read per row; projected columns are decoded only for the final
4367    /// winning rows when the heap drains.
4368    fn project_filter_sort_limit_fast(
4369        &self,
4370        table: &str,
4371        fields: &[ProjectField],
4372        sort_field: &str,
4373        descending: bool,
4374        limit: usize,
4375        predicate: Option<&Expr>,
4376    ) -> Result<Option<QueryResult>, String> {
4377        if limit == 0 {
4378            // Degenerate case — empty result. Let the generic path handle it
4379            // for proper column naming.
4380            return Ok(None);
4381        }
4382        let schema = self
4383            .catalog
4384            .schema(table)
4385            .ok_or_else(|| format!("table '{table}' not found"))?
4386            .clone();
4387        let all_columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
4388
4389        // Sort key must be a fixed-size numeric column (Int or Float).
4390        // Mission D10: extended from Int-only. Float sort keys use a
4391        // sortable-u64 transform (see `f64_to_sortable_u64`) so the heap
4392        // path stays keyed on `u64` and the whole branch shape is
4393        // identical to the Int case — no new heap types, no `total_cmp`
4394        // closures in the hot loop.
4395        let sort_idx = match schema.column_index(sort_field) {
4396            Some(i) => i,
4397            None => return Ok(None),
4398        };
4399        let sort_col_type = schema.columns[sort_idx].type_id;
4400        if sort_col_type != TypeId::Int && sort_col_type != TypeId::Float {
4401            return Ok(None);
4402        }
4403
4404        // Each projection field must be a simple `.field`.
4405        let mut proj_indices: Vec<usize> = Vec::with_capacity(fields.len());
4406        let mut proj_columns: Vec<String> = Vec::with_capacity(fields.len());
4407        for f in fields {
4408            let name = match &f.expr {
4409                Expr::Field(n) => n.clone(),
4410                _ => return Ok(None),
4411            };
4412            let idx = match all_columns.iter().position(|c| c == &name) {
4413                Some(i) => i,
4414                None => return Ok(None),
4415            };
4416            proj_indices.push(idx);
4417            proj_columns.push(f.alias.clone().unwrap_or(name));
4418        }
4419
4420        let fast = FastLayout::new(&schema);
4421        let row_layout = RowLayout::new(&schema);
4422        // Mission C Phase 20b: inline numeric-column reader (no Box<dyn Fn>).
4423        let sort_byte_offset = match fast.fixed_offsets[sort_idx] {
4424            Some(o) => o,
4425            None => return Ok(None),
4426        };
4427        let sort_bitmap_byte = sort_idx / 8;
4428        let sort_bitmap_bit = (sort_idx % 8) as u32;
4429        let sort_data_offset = 2 + fast.bitmap_size + sort_byte_offset;
4430
4431        let compiled_pred: Option<CompiledPredicate> = match predicate {
4432            Some(pred) => match compile_predicate(pred, &all_columns, &fast, &schema) {
4433                Some(c) => Some(c),
4434                None => return Ok(None),
4435            },
4436            None => None,
4437        };
4438
4439        // Bounded top-N heap. For `order .x desc limit N`, we want the N
4440        // largest values — use a min-heap so the smallest is at the top and
4441        // can be popped when a better candidate arrives. For ascending, use
4442        // a max-heap. We tie-break with a monotonic `seq` counter so the
4443        // result is deterministic and stable.
4444        //
4445        // To keep this simple we maintain two typed heaps and pick by
4446        // direction.
4447        let drained: Vec<Vec<u8>> = match sort_col_type {
4448            TypeId::Int => {
4449                let mut seq: u64 = 0;
4450                let mut heap_desc: BinaryHeap<Reverse<(i64, u64, Vec<u8>)>> =
4451                    BinaryHeap::with_capacity(limit);
4452                let mut heap_asc: BinaryHeap<(i64, u64, Vec<u8>)> =
4453                    BinaryHeap::with_capacity(limit);
4454
4455                self.catalog
4456                    .for_each_row_raw(table, |_rid, data| {
4457                        if let Some(ref pred) = compiled_pred {
4458                            if !pred(data) {
4459                                return;
4460                            }
4461                        }
4462                        // Inlined int-column reader: null check + i64 decode.
4463                        let is_null = (data[2 + sort_bitmap_byte] >> sort_bitmap_bit) & 1 == 1;
4464                        if is_null {
4465                            return;
4466                        }
4467                        let key = i64::from_le_bytes(
4468                            data[sort_data_offset..sort_data_offset + 8]
4469                                .try_into()
4470                                .unwrap(),
4471                        );
4472                        let id = seq;
4473                        seq += 1;
4474
4475                        if descending {
4476                            if heap_desc.len() < limit {
4477                                heap_desc.push(Reverse((key, id, data.to_vec())));
4478                            } else if let Some(Reverse((top_key, _, _))) = heap_desc.peek() {
4479                                if key > *top_key {
4480                                    heap_desc.pop();
4481                                    heap_desc.push(Reverse((key, id, data.to_vec())));
4482                                }
4483                            }
4484                        } else if heap_asc.len() < limit {
4485                            heap_asc.push((key, id, data.to_vec()));
4486                        } else if let Some((top_key, _, _)) = heap_asc.peek() {
4487                            if key < *top_key {
4488                                heap_asc.pop();
4489                                heap_asc.push((key, id, data.to_vec()));
4490                            }
4491                        }
4492                    })
4493                    .map_err(|e| e.to_string())?;
4494
4495                let mut drained: Vec<(i64, u64, Vec<u8>)> = if descending {
4496                    heap_desc.into_iter().map(|Reverse(t)| t).collect()
4497                } else {
4498                    heap_asc.into_iter().collect()
4499                };
4500                if descending {
4501                    drained.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
4502                } else {
4503                    drained.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
4504                }
4505                drained.into_iter().map(|(_, _, d)| d).collect()
4506            }
4507            TypeId::Float => {
4508                // Novel angle: rather than introducing a `TotalF64` newtype
4509                // with `Ord via total_cmp`, transform the f64 bit pattern
4510                // into a sortable `u64` so `BinaryHeap<u64>` orders exactly
4511                // like `f64::total_cmp` would. Classic trick: flip the sign
4512                // bit on positives, flip all bits on negatives. Result:
4513                // - NaN (sign=0) stays greatest, matching total_cmp
4514                // - -0.0 sorts before +0.0, matching total_cmp
4515                // - Hot loop is branch-cheap (one compare + one xor)
4516                let mut seq: u64 = 0;
4517                let mut heap_desc: BinaryHeap<Reverse<(u64, u64, Vec<u8>)>> =
4518                    BinaryHeap::with_capacity(limit);
4519                let mut heap_asc: BinaryHeap<(u64, u64, Vec<u8>)> =
4520                    BinaryHeap::with_capacity(limit);
4521
4522                self.catalog
4523                    .for_each_row_raw(table, |_rid, data| {
4524                        if let Some(ref pred) = compiled_pred {
4525                            if !pred(data) {
4526                                return;
4527                            }
4528                        }
4529                        let is_null = (data[2 + sort_bitmap_byte] >> sort_bitmap_bit) & 1 == 1;
4530                        if is_null {
4531                            return;
4532                        }
4533                        let bits = u64::from_le_bytes(
4534                            data[sort_data_offset..sort_data_offset + 8]
4535                                .try_into()
4536                                .unwrap(),
4537                        );
4538                        let key = f64_bits_to_sortable_u64(bits);
4539                        let id = seq;
4540                        seq += 1;
4541
4542                        if descending {
4543                            if heap_desc.len() < limit {
4544                                heap_desc.push(Reverse((key, id, data.to_vec())));
4545                            } else if let Some(Reverse((top_key, _, _))) = heap_desc.peek() {
4546                                if key > *top_key {
4547                                    heap_desc.pop();
4548                                    heap_desc.push(Reverse((key, id, data.to_vec())));
4549                                }
4550                            }
4551                        } else if heap_asc.len() < limit {
4552                            heap_asc.push((key, id, data.to_vec()));
4553                        } else if let Some((top_key, _, _)) = heap_asc.peek() {
4554                            if key < *top_key {
4555                                heap_asc.pop();
4556                                heap_asc.push((key, id, data.to_vec()));
4557                            }
4558                        }
4559                    })
4560                    .map_err(|e| e.to_string())?;
4561
4562                let mut drained: Vec<(u64, u64, Vec<u8>)> = if descending {
4563                    heap_desc.into_iter().map(|Reverse(t)| t).collect()
4564                } else {
4565                    heap_asc.into_iter().collect()
4566                };
4567                if descending {
4568                    drained.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
4569                } else {
4570                    drained.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
4571                }
4572                drained.into_iter().map(|(_, _, d)| d).collect()
4573            }
4574            _ => unreachable!("type guard above restricts to Int/Float"),
4575        };
4576
4577        let rows: Vec<Vec<Value>> = drained
4578            .into_iter()
4579            .map(|data| {
4580                proj_indices
4581                    .iter()
4582                    .map(|&ci| decode_column(&schema, &row_layout, &data, ci))
4583                    .collect()
4584            })
4585            .collect();
4586
4587        Ok(Some(QueryResult::Rows {
4588            columns: proj_columns,
4589            rows,
4590        }))
4591    }
4592
4593    /// Gather the RowIds that a mutation should operate on, without
4594    /// materialising the full row set. Handles the shapes the planner emits
4595    /// for update/delete: SeqScan, IndexScan, and Filter(SeqScan). Other
4596    /// shapes fall back to `generic_rid_match`.
4597    ///
4598    /// Perf sprint: try to fuse the predicate evaluation and in-place
4599    /// byte-level mutation into a single heap walk. Returns `Some(result)`
4600    /// if the fused path fired, `None` to fall through to the generic
4601    /// two-pass code.
4602    ///
4603    /// Covers two shapes:
4604    /// 1. Fixed-width non-null literal assignments on non-indexed columns
4605    ///    → byte-patch every matched row in place (row length unchanged).
4606    /// 2. Single var-col literal assignment on a non-indexed column
4607    ///    → `patch_var_column_in_place` on every matched row (may shrink);
4608    ///    rows that can't be patched in place are collected for fallback.
4609    fn try_fused_scan_update(
4610        &mut self,
4611        table: &str,
4612        predicate: &Expr,
4613        resolved: &[(usize, Value)],
4614        changed_cols: &[usize],
4615    ) -> Option<Result<QueryResult, String>> {
4616        // Build compiled predicate. Requires a schema borrow that must be
4617        // dropped before we call scan_patch_matching_logged.
4618        let compiled = {
4619            let schema = self.catalog.schema(table)?;
4620            let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
4621            let fast = FastLayout::new(schema);
4622            compile_predicate(predicate, &columns, &fast, schema)?
4623        };
4624
4625        // ── Path 1: fixed-width fast patch ──────────────────────────
4626        let fixed_patches: Option<Vec<FastPatch>> = {
4627            let tbl = self.catalog.get_table(table)?;
4628            let schema = &tbl.schema;
4629            let all_fixed_nonnull = resolved
4630                .iter()
4631                .all(|(idx, val)| is_fixed_size(schema.columns[*idx].type_id) && !val.is_empty());
4632            let no_indexed = !resolved.iter().any(|(idx, _)| tbl.has_indexed_col(*idx));
4633            if all_fixed_nonnull && no_indexed {
4634                let layout = RowLayout::new(schema);
4635                let bitmap_size = layout.bitmap_size();
4636                Some(
4637                    resolved
4638                        .iter()
4639                        .map(|(idx, val)| {
4640                            let fixed_off = layout
4641                                .fixed_offset(*idx)
4642                                .expect("is_fixed_size already checked");
4643                            let field_off = 2 + bitmap_size + fixed_off;
4644                            let bytes: FixedBytes = match val {
4645                                Value::Int(v) => FixedBytes::I64(v.to_le_bytes()),
4646                                Value::Float(v) => FixedBytes::F64(v.to_le_bytes()),
4647                                Value::Bool(v) => FixedBytes::Bool(if *v { 1 } else { 0 }),
4648                                Value::DateTime(v) => FixedBytes::I64(v.to_le_bytes()),
4649                                Value::Uuid(v) => FixedBytes::Uuid(*v),
4650                                _ => unreachable!("all_fixed_nonnull guard"),
4651                            };
4652                            FastPatch {
4653                                field_off,
4654                                bitmap_byte_off: 2 + idx / 8,
4655                                bit_mask: 1u8 << (idx % 8),
4656                                bytes,
4657                            }
4658                        })
4659                        .collect(),
4660                )
4661            } else {
4662                None
4663            }
4664        };
4665        if let Some(patches) = fixed_patches {
4666            let result = self
4667                .catalog
4668                .scan_patch_matching_logged(table, compiled, |row| {
4669                    for p in &patches {
4670                        row[p.bitmap_byte_off] &= !p.bit_mask;
4671                        let field_bytes = p.bytes.as_slice();
4672                        row[p.field_off..p.field_off + field_bytes.len()]
4673                            .copy_from_slice(field_bytes);
4674                    }
4675                    Some(row.len() as u16)
4676                })
4677                .map_err(|e| e.to_string());
4678            match result {
4679                Ok((count, _)) => {
4680                    self.view_registry.mark_dependents_dirty(table);
4681                    return Some(Ok(QueryResult::Modified(count)));
4682                }
4683                Err(e) => return Some(Err(e)),
4684            }
4685        }
4686
4687        // ── Path 2: single var-col shrink fast patch ────────────────
4688        let var_patch: Option<(usize, Option<Vec<u8>>)> = {
4689            let tbl = self.catalog.get_table(table)?;
4690            let schema = &tbl.schema;
4691            let is_single = resolved.len() == 1;
4692            let is_var = is_single && !is_fixed_size(schema.columns[resolved[0].0].type_id);
4693            let no_indexed = !resolved.iter().any(|(idx, _)| tbl.has_indexed_col(*idx));
4694            if is_single && is_var && no_indexed {
4695                let (idx, val) = &resolved[0];
4696                let bytes_opt = match val {
4697                    Value::Str(s) => Some(s.as_bytes().to_vec()),
4698                    Value::Bytes(b) => Some(b.clone()),
4699                    Value::Empty => None,
4700                    _ => return None, // type mismatch, fall through
4701                };
4702                Some((*idx, bytes_opt))
4703            } else {
4704                None
4705            }
4706        };
4707        if let Some((col_idx, ref new_bytes_opt)) = var_patch {
4708            // Build a fresh RowLayout before the mutable borrow.
4709            let layout = {
4710                let schema = self.catalog.schema(table)?;
4711                RowLayout::new(schema)
4712            };
4713            let new_bytes_ref: Option<&[u8]> = new_bytes_opt.as_deref();
4714            let result = self
4715                .catalog
4716                .scan_patch_matching_logged(table, compiled, |row| {
4717                    patch_var_column_in_place(row, &layout, col_idx, new_bytes_ref)
4718                })
4719                .map_err(|e| e.to_string());
4720            match result {
4721                Ok((mut count, fallback_rids)) => {
4722                    // Handle rows where in-place patch failed (new > old).
4723                    for rid in fallback_rids {
4724                        let mut row = match self.catalog.get(table, rid) {
4725                            Some(r) => r,
4726                            None => continue,
4727                        };
4728                        for (idx, val) in resolved.iter() {
4729                            row[*idx] = val.clone();
4730                        }
4731                        self.catalog
4732                            .update_hinted(table, rid, &row, Some(changed_cols))
4733                            .map_err(|e| e.to_string())
4734                            .ok();
4735                        count += 1;
4736                    }
4737                    self.view_registry.mark_dependents_dirty(table);
4738                    return Some(Ok(QueryResult::Modified(count)));
4739                }
4740                Err(e) => return Some(Err(e)),
4741            }
4742        }
4743
4744        None // no fused path applicable — fall through
4745    }
4746
4747    /// Mission C Phase 3: schema is looked up via `self.catalog.schema(table)`
4748    /// inside the branches that actually need it. Previously the caller had
4749    /// to clone the full Schema (6+ String allocs) before every mutation just
4750    /// so this function could borrow it — a cost the update/delete hot path
4751    /// did not need.
4752    fn collect_rids_for_mutation(
4753        &mut self,
4754        input: &PlanNode,
4755        table: &str,
4756    ) -> Result<Vec<RowId>, String> {
4757        match input {
4758            PlanNode::SeqScan { table: t } if t == table => {
4759                // "Update/delete everything" — rare but legal.
4760                let rids: Vec<RowId> = self
4761                    .catalog
4762                    .scan(table)
4763                    .map_err(|e| e.to_string())?
4764                    .map(|(rid, _)| rid)
4765                    .collect();
4766                Ok(rids)
4767            }
4768            PlanNode::IndexScan {
4769                table: t,
4770                column,
4771                key,
4772            } if t == table => {
4773                let key_value = literal_to_value(key)?;
4774
4775                // Indexed case: single lookup, 0 or 1 rows.
4776                // Mission D7: int-specialized fast path on int-keyed indexes
4777                // (primary keys, created_at, etc.) — the common case for
4778                // `update_by_pk` / `delete where id = ?`.
4779                //
4780                // Scope the `tbl` borrow so it's released before we fall
4781                // through to the scan-based paths below (which reborrow
4782                // `self.catalog`).
4783                {
4784                    let tbl = self
4785                        .catalog
4786                        .get_table(table)
4787                        .ok_or_else(|| format!("table '{table}' not found"))?;
4788                    if let Some(btree) = tbl.index(column) {
4789                        let hit = match &key_value {
4790                            Value::Int(k) => btree.lookup_int(*k),
4791                            other => btree.lookup(other),
4792                        };
4793                        return Ok(match hit {
4794                            Some(rid) => vec![rid],
4795                            None => Vec::new(),
4796                        });
4797                    }
4798                }
4799
4800                // No index: the planner folds `.col = literal` to IndexScan
4801                // regardless of whether the column is actually unique. When
4802                // there's no index we must behave like Filter(SeqScan) and
4803                // return *all* matching RIDs — not just the first one.
4804                let schema = self
4805                    .catalog
4806                    .schema(table)
4807                    .ok_or_else(|| format!("table '{table}' not found"))?;
4808                let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
4809                let fast = FastLayout::new(schema);
4810                let synth = Expr::BinaryOp(
4811                    Box::new(Expr::Field(column.clone())),
4812                    BinOp::Eq,
4813                    Box::new(key.clone()),
4814                );
4815                if let Some(compiled) = compile_predicate(&synth, &columns, &fast, schema) {
4816                    // Mission F: skip the first 4 Vec doublings.
4817                    let mut rids: Vec<RowId> = Vec::with_capacity(64);
4818                    self.catalog
4819                        .for_each_row_raw(table, |rid, data| {
4820                            if compiled(data) {
4821                                rids.push(rid);
4822                            }
4823                        })
4824                        .map_err(|e| e.to_string())?;
4825                    return Ok(rids);
4826                }
4827
4828                // Fallback: decode each row, compare values.
4829                let col_idx = schema
4830                    .column_index(column)
4831                    .ok_or_else(|| format!("column '{column}' not found"))?;
4832                let rids: Vec<RowId> = self
4833                    .catalog
4834                    .scan(table)
4835                    .map_err(|e| e.to_string())?
4836                    .filter_map(|(rid, row)| {
4837                        if row[col_idx] == key_value {
4838                            Some(rid)
4839                        } else {
4840                            None
4841                        }
4842                    })
4843                    .collect();
4844                Ok(rids)
4845            }
4846            PlanNode::Filter {
4847                input: inner,
4848                predicate,
4849            } => {
4850                if let PlanNode::SeqScan { table: t } = inner.as_ref() {
4851                    if t != table {
4852                        return self.generic_rid_match(input, table);
4853                    }
4854                    let schema = self
4855                        .catalog
4856                        .schema(table)
4857                        .ok_or_else(|| format!("table '{table}' not found"))?;
4858                    let columns: Vec<String> =
4859                        schema.columns.iter().map(|c| c.name.clone()).collect();
4860                    let fast = FastLayout::new(schema);
4861                    let row_layout = RowLayout::new(schema);
4862
4863                    // Try compiled predicate first.
4864                    if let Some(compiled) = compile_predicate(predicate, &columns, &fast, schema) {
4865                        // Mission F: skip the first 4 Vec doublings.
4866                        let mut rids: Vec<RowId> = Vec::with_capacity(64);
4867                        self.catalog
4868                            .for_each_row_raw(table, |rid, data| {
4869                                if compiled(data) {
4870                                    rids.push(rid);
4871                                }
4872                            })
4873                            .map_err(|e| e.to_string())?;
4874                        return Ok(rids);
4875                    }
4876
4877                    // Fallback: selective decode + eval.
4878                    let pred_cols = predicate_column_indices(predicate, &columns);
4879                    let mut rids: Vec<RowId> = Vec::with_capacity(64);
4880                    self.catalog
4881                        .for_each_row_raw(table, |rid, data| {
4882                            let pred_row = decode_selective(schema, &row_layout, data, &pred_cols);
4883                            if eval_predicate(predicate, &pred_row, &columns) {
4884                                rids.push(rid);
4885                            }
4886                        })
4887                        .map_err(|e| e.to_string())?;
4888                    return Ok(rids);
4889                }
4890                self.generic_rid_match(input, table)
4891            }
4892            _ => self.generic_rid_match(input, table),
4893        }
4894    }
4895
4896    /// Last-ditch generic match: execute the plan, collect matching rows,
4897    /// then find corresponding RowIds by value equality. This is the old
4898    /// O(N*M) code path; only used when the plan shape is something exotic.
4899    fn generic_rid_match(&mut self, input: &PlanNode, table: &str) -> Result<Vec<RowId>, String> {
4900        let result = self.execute_plan(input)?;
4901        let rows = match result {
4902            QueryResult::Rows { rows, .. } => rows,
4903            _ => return Err("mutation source must be rows".into()),
4904        };
4905        let matching: Vec<RowId> = self
4906            .catalog
4907            .scan(table)
4908            .map_err(|e| e.to_string())?
4909            .filter(|(_, row)| rows.iter().any(|r| r == row))
4910            .map(|(rid, _)| rid)
4911            .collect();
4912        Ok(matching)
4913    }
4914
4915    pub fn catalog(&self) -> &Catalog {
4916        &self.catalog
4917    }
4918
4919    pub fn catalog_mut(&mut self) -> &mut Catalog {
4920        &mut self.catalog
4921    }
4922}
4923
4924/// Mission C Phase 4: precomputed byte-patch for the in-place update fast
4925/// path. Built once per `Update` query (outside the rid loop) and reused on
4926/// every matching row.
4927#[derive(Clone, Copy)]
4928struct FastPatch {
4929    /// Byte offset of the fixed column within the row encoding:
4930    /// `2 + bitmap_size + layout.fixed_offsets[col]`.
4931    field_off: usize,
4932    /// Byte offset of the bitmap byte containing this column's null bit
4933    /// (`2 + col/8`). We read-modify-write this byte to force the column
4934    /// non-null, so the idempotent clear is safe for already-non-null rows.
4935    bitmap_byte_off: usize,
4936    /// Bit mask for this column's null bit within `bitmap_byte_off`.
4937    bit_mask: u8,
4938    /// The new fixed-width value encoded as little-endian bytes.
4939    bytes: FixedBytes,
4940}
4941
4942#[derive(Clone, Copy)]
4943enum FixedBytes {
4944    I64([u8; 8]),
4945    F64([u8; 8]),
4946    Bool(u8),
4947    Uuid([u8; 16]),
4948}
4949
4950impl FixedBytes {
4951    #[inline]
4952    fn as_slice(&self) -> &[u8] {
4953        match self {
4954            FixedBytes::I64(b) => b.as_slice(),
4955            FixedBytes::F64(b) => b.as_slice(),
4956            FixedBytes::Bool(b) => std::slice::from_ref(b),
4957            FixedBytes::Uuid(b) => b.as_slice(),
4958        }
4959    }
4960}
4961
4962fn type_name_to_id(name: &str) -> TypeId {
4963    match name {
4964        "str" => TypeId::Str,
4965        "int" => TypeId::Int,
4966        "float" => TypeId::Float,
4967        "bool" => TypeId::Bool,
4968        "datetime" => TypeId::DateTime,
4969        "uuid" => TypeId::Uuid,
4970        "bytes" => TypeId::Bytes,
4971        _ => TypeId::Str,
4972    }
4973}
4974
4975/// Convert a runtime `Value` back into an `Expr::Literal` for InSubquery
4976/// materialization. Non-literal-representable values become `Literal::Int(0)`
4977/// (shouldn't happen in practice — subqueries return primitive columns).
4978/// Check if an expression tree contains any `InSubquery` nodes.
4979/// Collect all `Expr::Field` names referenced by an expression tree.
4980fn collect_field_refs(expr: &Expr, out: &mut Vec<String>) {
4981    match expr {
4982        Expr::Field(name) => out.push(name.clone()),
4983        Expr::QualifiedField { qualifier, field } => {
4984            out.push(format!("{qualifier}.{field}"));
4985        }
4986        Expr::BinaryOp(l, _, r) => {
4987            collect_field_refs(l, out);
4988            collect_field_refs(r, out);
4989        }
4990        Expr::UnaryOp(_, inner) => collect_field_refs(inner, out),
4991        Expr::FunctionCall(_, inner) => collect_field_refs(inner, out),
4992        Expr::Coalesce(l, r) => {
4993            collect_field_refs(l, out);
4994            collect_field_refs(r, out);
4995        }
4996        Expr::InList { expr, list, .. } => {
4997            collect_field_refs(expr, out);
4998            for item in list {
4999                collect_field_refs(item, out);
5000            }
5001        }
5002        Expr::ScalarFunc(_, args) => {
5003            for a in args {
5004                collect_field_refs(a, out);
5005            }
5006        }
5007        Expr::Cast(inner, _) => {
5008            collect_field_refs(inner, out);
5009        }
5010        Expr::Case { whens, else_expr } => {
5011            for (c, r) in whens {
5012                collect_field_refs(c, out);
5013                collect_field_refs(r, out);
5014            }
5015            if let Some(e) = else_expr {
5016                collect_field_refs(e, out);
5017            }
5018        }
5019        _ => {}
5020    }
5021}
5022
5023/// Detect whether a subquery is correlated: any `Expr::Field` reference in
5024/// the subquery's filter that doesn't match a column in the subquery's
5025/// source table indicates a reference to an outer scope.
5026/// Replace outer-scope field references in a correlated subquery's filter
5027/// with literal values from the current outer row. Fields that belong to
5028/// the subquery's own source table are left unchanged.
5029fn substitute_outer_refs(
5030    expr: &Expr,
5031    subquery_source: &str,
5032    catalog: &Catalog,
5033    outer_row: &[Value],
5034    outer_columns: &[String],
5035) -> Expr {
5036    let sub_cols: Vec<String> = catalog
5037        .schema(subquery_source)
5038        .map(|s| s.columns.iter().map(|c| c.name.clone()).collect())
5039        .unwrap_or_default();
5040    substitute_outer_refs_inner(expr, &sub_cols, outer_row, outer_columns)
5041}
5042
5043fn substitute_outer_refs_inner(
5044    expr: &Expr,
5045    sub_cols: &[String],
5046    outer_row: &[Value],
5047    outer_columns: &[String],
5048) -> Expr {
5049    match expr {
5050        Expr::Field(name) => {
5051            if sub_cols.iter().any(|c| c == name) {
5052                expr.clone()
5053            } else if let Some(i) = outer_columns.iter().position(|c| c == name) {
5054                value_to_expr(outer_row[i].clone())
5055            } else {
5056                expr.clone()
5057            }
5058        }
5059        Expr::BinaryOp(l, op, r) => {
5060            let l = substitute_outer_refs_inner(l, sub_cols, outer_row, outer_columns);
5061            let r = substitute_outer_refs_inner(r, sub_cols, outer_row, outer_columns);
5062            Expr::BinaryOp(Box::new(l), *op, Box::new(r))
5063        }
5064        Expr::UnaryOp(op, inner) => {
5065            let inner = substitute_outer_refs_inner(inner, sub_cols, outer_row, outer_columns);
5066            Expr::UnaryOp(*op, Box::new(inner))
5067        }
5068        Expr::InList {
5069            expr: e,
5070            list,
5071            negated,
5072        } => {
5073            let e = substitute_outer_refs_inner(e, sub_cols, outer_row, outer_columns);
5074            let list = list
5075                .iter()
5076                .map(|item| substitute_outer_refs_inner(item, sub_cols, outer_row, outer_columns))
5077                .collect();
5078            Expr::InList {
5079                expr: Box::new(e),
5080                list,
5081                negated: *negated,
5082            }
5083        }
5084        Expr::Coalesce(l, r) => {
5085            let l = substitute_outer_refs_inner(l, sub_cols, outer_row, outer_columns);
5086            let r = substitute_outer_refs_inner(r, sub_cols, outer_row, outer_columns);
5087            Expr::Coalesce(Box::new(l), Box::new(r))
5088        }
5089        other => other.clone(),
5090    }
5091}
5092
5093fn is_correlated_subquery(subquery: &QueryExpr, catalog: &Catalog) -> bool {
5094    let filter = match &subquery.filter {
5095        Some(f) => f,
5096        None => return false,
5097    };
5098    let schema = match catalog.schema(&subquery.source) {
5099        Some(s) => s,
5100        None => return false, // table not found — not correlation, just an error
5101    };
5102    let table_cols: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
5103    let mut refs = Vec::new();
5104    collect_field_refs(filter, &mut refs);
5105    // If any referenced field doesn't exist in the subquery's source table,
5106    // it's (probably) a reference to an outer scope — i.e., correlated.
5107    refs.iter().any(|r| {
5108        // Skip qualified references (alias.field) — they unambiguously
5109        // target a specific source and will only match the subquery's own
5110        // source if they share the alias.
5111        if r.contains('.') {
5112            let alias = subquery.alias.as_deref().unwrap_or(&subquery.source);
5113            !r.starts_with(alias)
5114        } else {
5115            !table_cols.iter().any(|c| c == r)
5116        }
5117    })
5118}
5119
5120fn contains_subquery(expr: &Expr) -> bool {
5121    match expr {
5122        Expr::InSubquery { .. } => true,
5123        Expr::ExistsSubquery { .. } => true,
5124        Expr::BinaryOp(l, _, r) => contains_subquery(l) || contains_subquery(r),
5125        Expr::UnaryOp(_, inner) => contains_subquery(inner),
5126        Expr::InList { expr, list, .. } => {
5127            contains_subquery(expr) || list.iter().any(contains_subquery)
5128        }
5129        Expr::Case { whens, else_expr } => {
5130            whens
5131                .iter()
5132                .any(|(c, r)| contains_subquery(c) || contains_subquery(r))
5133                || else_expr.as_ref().is_some_and(|e| contains_subquery(e))
5134        }
5135        Expr::ScalarFunc(_, args) => args.iter().any(contains_subquery),
5136        Expr::Cast(inner, _) => contains_subquery(inner),
5137        Expr::FunctionCall(_, inner) => contains_subquery(inner),
5138        Expr::Coalesce(l, r) => contains_subquery(l) || contains_subquery(r),
5139        _ => false,
5140    }
5141}
5142
5143fn value_to_expr(val: Value) -> Expr {
5144    match val {
5145        Value::Int(v) => Expr::Literal(Literal::Int(v)),
5146        Value::Float(v) => Expr::Literal(Literal::Float(v)),
5147        Value::Str(v) => Expr::Literal(Literal::String(v)),
5148        Value::Bool(v) => Expr::Literal(Literal::Bool(v)),
5149        _ => Expr::Literal(Literal::Int(0)),
5150    }
5151}
5152
5153fn literal_to_value(expr: &Expr) -> Result<Value, String> {
5154    match expr {
5155        Expr::Literal(Literal::Int(v)) => Ok(Value::Int(*v)),
5156        Expr::Literal(Literal::Float(v)) => Ok(Value::Float(*v)),
5157        Expr::Literal(Literal::String(v)) => Ok(Value::Str(v.clone())),
5158        Expr::Literal(Literal::Bool(v)) => Ok(Value::Bool(*v)),
5159        _ => Err("expected literal value".into()),
5160    }
5161}
5162
5163/// Mission C Phase 5: direct Literal→Value conversion used by the
5164/// prepared-statement Insert fast path. Skips the `Expr::Literal` unwrap
5165/// and the `Result` plumbing of [`literal_to_value`]. String literals
5166/// still clone because the row needs an owned `Value::Str`.
5167#[inline]
5168fn literal_value_from(lit: &Literal) -> Value {
5169    match lit {
5170        Literal::Int(v) => Value::Int(*v),
5171        Literal::Float(v) => Value::Float(*v),
5172        Literal::String(v) => Value::Str(v.clone()),
5173        Literal::Bool(v) => Value::Bool(*v),
5174    }
5175}
5176
5177/// Mission C Phase 13: moving companion to [`literal_value_from`] used
5178/// by [`Engine::execute_prepared_take`]. Pulls the `String` out of a
5179/// `Literal::String` via `mem::take`, leaving an empty string behind
5180/// so the caller's slice remains valid (but with blanked-out strings).
5181/// On the insert fast path this removes one heap alloc per string
5182/// column per row.
5183#[inline]
5184fn literal_value_take(lit: &mut Literal) -> Value {
5185    match lit {
5186        Literal::Int(v) => Value::Int(*v),
5187        Literal::Float(v) => Value::Float(*v),
5188        Literal::String(v) => Value::Str(std::mem::take(v)),
5189        Literal::Bool(v) => Value::Bool(*v),
5190    }
5191}
5192
5193fn eval_expr(expr: &Expr, row: &[Value], columns: &[String]) -> Value {
5194    match expr {
5195        Expr::Field(name) => columns
5196            .iter()
5197            .position(|c| c == name)
5198            .map(|i| row[i].clone())
5199            .unwrap_or(Value::Empty),
5200        Expr::QualifiedField { qualifier, field } => {
5201            // Mission E1.2: join queries emit columns named `alias.field`,
5202            // so the lookup is a direct prefix+tail match. We compare in
5203            // pieces to avoid allocating a fresh `format!("{q}.{f}")` on
5204            // every row — the join loop can evaluate this tens of thousands
5205            // of times per query.
5206            let q = qualifier.as_bytes();
5207            let f = field.as_bytes();
5208            let idx = columns.iter().position(|c| {
5209                let b = c.as_bytes();
5210                b.len() == q.len() + 1 + f.len()
5211                    && b[..q.len()] == *q
5212                    && b[q.len()] == b'.'
5213                    && b[q.len() + 1..] == *f
5214            });
5215            idx.map(|i| row[i].clone()).unwrap_or(Value::Empty)
5216        }
5217        Expr::Literal(lit) => match lit {
5218            Literal::Int(v) => Value::Int(*v),
5219            Literal::Float(v) => Value::Float(*v),
5220            Literal::String(v) => Value::Str(v.clone()),
5221            Literal::Bool(v) => Value::Bool(*v),
5222        },
5223        Expr::BinaryOp(left, op, right) => {
5224            let l = eval_expr(left, row, columns);
5225            let r = eval_expr(right, row, columns);
5226            eval_binop(&l, *op, &r)
5227        }
5228        Expr::Coalesce(left, right) => {
5229            let l = eval_expr(left, row, columns);
5230            if l.is_empty() {
5231                eval_expr(right, row, columns)
5232            } else {
5233                l
5234            }
5235        }
5236        Expr::InList {
5237            expr,
5238            list,
5239            negated,
5240        } => {
5241            let val = eval_expr(expr, row, columns);
5242            let found = list.iter().any(|item| {
5243                let iv = eval_expr(item, row, columns);
5244                val == iv
5245            });
5246            Value::Bool(if *negated { !found } else { found })
5247        }
5248        Expr::InSubquery { .. } => {
5249            // Should have been materialized into InList before eval_expr.
5250            Value::Empty
5251        }
5252        Expr::ExistsSubquery { .. } => {
5253            // Should have been materialized into a Bool literal before
5254            // eval_expr (see materialize_subqueries).
5255            Value::Empty
5256        }
5257        Expr::UnaryOp(op, inner) => {
5258            let v = eval_expr(inner, row, columns);
5259            match op {
5260                UnaryOp::Not => match v {
5261                    Value::Bool(b) => Value::Bool(!b),
5262                    _ => Value::Empty,
5263                },
5264                UnaryOp::Exists => Value::Bool(!v.is_empty()),
5265                UnaryOp::NotExists => Value::Bool(v.is_empty()),
5266                UnaryOp::IsNull => Value::Bool(v.is_empty()),
5267                UnaryOp::IsNotNull => Value::Bool(!v.is_empty()),
5268            }
5269        }
5270        Expr::ScalarFunc(func, args) => {
5271            let vals: Vec<Value> = args.iter().map(|a| eval_expr(a, row, columns)).collect();
5272            eval_scalar_func(*func, &vals)
5273        }
5274        Expr::Case { whens, else_expr } => {
5275            for (condition, result) in whens {
5276                if eval_predicate(condition, row, columns) {
5277                    return eval_expr(result, row, columns);
5278                }
5279            }
5280            match else_expr {
5281                Some(e) => eval_expr(e, row, columns),
5282                None => Value::Empty,
5283            }
5284        }
5285        Expr::Cast(inner, cast_type) => {
5286            let val = eval_expr(inner, row, columns);
5287            eval_cast(val, *cast_type)
5288        }
5289        Expr::FunctionCall(_, _) | Expr::Param(_) | Expr::Window { .. } => Value::Empty,
5290    }
5291}
5292
5293fn eval_predicate(expr: &Expr, row: &[Value], columns: &[String]) -> bool {
5294    match eval_expr(expr, row, columns) {
5295        Value::Bool(b) => b,
5296        _ => false,
5297    }
5298}
5299
5300fn eval_scalar_func(func: ScalarFn, args: &[Value]) -> Value {
5301    match func {
5302        ScalarFn::Upper => match args.first() {
5303            Some(Value::Str(s)) => Value::Str(s.to_uppercase()),
5304            _ => Value::Empty,
5305        },
5306        ScalarFn::Lower => match args.first() {
5307            Some(Value::Str(s)) => Value::Str(s.to_lowercase()),
5308            _ => Value::Empty,
5309        },
5310        ScalarFn::Length => match args.first() {
5311            Some(Value::Str(s)) => Value::Int(s.len() as i64),
5312            _ => Value::Empty,
5313        },
5314        ScalarFn::Trim => match args.first() {
5315            Some(Value::Str(s)) => Value::Str(s.trim().to_string()),
5316            _ => Value::Empty,
5317        },
5318        ScalarFn::Substring => {
5319            if args.len() < 3 {
5320                return Value::Empty;
5321            }
5322            match (&args[0], &args[1], &args[2]) {
5323                (Value::Str(s), Value::Int(start), Value::Int(len)) => {
5324                    let start = (*start as usize).saturating_sub(1); // 1-indexed
5325                    let len = *len as usize;
5326                    let sub: String = s.chars().skip(start).take(len).collect();
5327                    Value::Str(sub)
5328                }
5329                _ => Value::Empty,
5330            }
5331        }
5332        ScalarFn::Concat => {
5333            let mut result = String::new();
5334            for v in args {
5335                match v {
5336                    Value::Str(s) => result.push_str(s),
5337                    Value::Int(n) => result.push_str(&n.to_string()),
5338                    Value::Float(f) => result.push_str(&f.to_string()),
5339                    Value::Bool(b) => result.push_str(if *b { "true" } else { "false" }),
5340                    _ => {}
5341                }
5342            }
5343            Value::Str(result)
5344        }
5345        // Math functions
5346        ScalarFn::Abs => match args.first() {
5347            Some(Value::Int(n)) => Value::Int(n.abs()),
5348            Some(Value::Float(f)) => Value::Float(f.abs()),
5349            _ => Value::Empty,
5350        },
5351        ScalarFn::Round => {
5352            let decimals = match args.get(1) {
5353                Some(Value::Int(d)) => *d as i32,
5354                _ => 0,
5355            };
5356            match args.first() {
5357                Some(Value::Float(f)) => {
5358                    let factor = 10_f64.powi(decimals);
5359                    Value::Float((f * factor).round() / factor)
5360                }
5361                Some(Value::Int(n)) => Value::Int(*n),
5362                _ => Value::Empty,
5363            }
5364        }
5365        ScalarFn::Ceil => match args.first() {
5366            Some(Value::Float(f)) => Value::Float(f.ceil()),
5367            Some(Value::Int(n)) => Value::Int(*n),
5368            _ => Value::Empty,
5369        },
5370        ScalarFn::Floor => match args.first() {
5371            Some(Value::Float(f)) => Value::Float(f.floor()),
5372            Some(Value::Int(n)) => Value::Int(*n),
5373            _ => Value::Empty,
5374        },
5375        ScalarFn::Sqrt => match args.first() {
5376            Some(Value::Float(f)) if *f >= 0.0 => Value::Float(f.sqrt()),
5377            Some(Value::Int(n)) if *n >= 0 => Value::Float((*n as f64).sqrt()),
5378            _ => Value::Empty,
5379        },
5380        ScalarFn::Pow => match (args.first(), args.get(1)) {
5381            (Some(Value::Float(base)), Some(Value::Float(exp))) => Value::Float(base.powf(*exp)),
5382            (Some(Value::Float(base)), Some(Value::Int(exp))) => {
5383                Value::Float(base.powi(*exp as i32))
5384            }
5385            (Some(Value::Int(base)), Some(Value::Int(exp))) => {
5386                if *exp >= 0 && *exp <= u32::MAX as i64 {
5387                    match base.checked_pow(*exp as u32) {
5388                        Some(v) => Value::Int(v),
5389                        None => Value::Float((*base as f64).powi(*exp as i32)),
5390                    }
5391                } else {
5392                    Value::Float((*base as f64).powi(*exp as i32))
5393                }
5394            }
5395            (Some(Value::Int(base)), Some(Value::Float(exp))) => {
5396                Value::Float((*base as f64).powf(*exp))
5397            }
5398            _ => Value::Empty,
5399        },
5400        // Date/time functions
5401        ScalarFn::Now => {
5402            use std::time::{SystemTime, UNIX_EPOCH};
5403            let micros = SystemTime::now()
5404                .duration_since(UNIX_EPOCH)
5405                .unwrap_or_default()
5406                .as_micros() as i64;
5407            Value::DateTime(micros)
5408        }
5409        ScalarFn::Extract => {
5410            // extract("part", datetime_expr)
5411            let part = match args.first() {
5412                Some(Value::Str(s)) => s.as_str(),
5413                _ => return Value::Empty,
5414            };
5415            let micros = match args.get(1) {
5416                Some(Value::DateTime(m)) => *m,
5417                Some(Value::Int(m)) => *m, // treat raw int as micros
5418                _ => return Value::Empty,
5419            };
5420            datetime_extract(part, micros)
5421        }
5422        ScalarFn::DateAdd => {
5423            // date_add(datetime_expr, amount, "unit")
5424            let micros = match args.first() {
5425                Some(Value::DateTime(m)) => *m,
5426                Some(Value::Int(m)) => *m,
5427                _ => return Value::Empty,
5428            };
5429            let amount = match args.get(1) {
5430                Some(Value::Int(n)) => *n,
5431                _ => return Value::Empty,
5432            };
5433            let unit = match args.get(2) {
5434                Some(Value::Str(s)) => s.as_str(),
5435                _ => return Value::Empty,
5436            };
5437            let delta_micros = match unit {
5438                "microsecond" | "microseconds" | "us" => amount,
5439                "millisecond" | "milliseconds" | "ms" => amount * 1_000,
5440                "second" | "seconds" | "s" => amount * 1_000_000,
5441                "minute" | "minutes" | "m" => amount * 60_000_000,
5442                "hour" | "hours" | "h" => amount * 3_600_000_000,
5443                "day" | "days" | "d" => amount * 86_400_000_000,
5444                _ => return Value::Empty,
5445            };
5446            Value::DateTime(micros + delta_micros)
5447        }
5448        ScalarFn::DateDiff => {
5449            // date_diff(dt1, dt2, "unit")
5450            let m1 = match args.first() {
5451                Some(Value::DateTime(m)) => *m,
5452                Some(Value::Int(m)) => *m,
5453                _ => return Value::Empty,
5454            };
5455            let m2 = match args.get(1) {
5456                Some(Value::DateTime(m)) => *m,
5457                Some(Value::Int(m)) => *m,
5458                _ => return Value::Empty,
5459            };
5460            let unit = match args.get(2) {
5461                Some(Value::Str(s)) => s.as_str(),
5462                _ => return Value::Empty,
5463            };
5464            let diff = m1 - m2;
5465            let result = match unit {
5466                "microsecond" | "microseconds" | "us" => diff,
5467                "millisecond" | "milliseconds" | "ms" => diff / 1_000,
5468                "second" | "seconds" | "s" => diff / 1_000_000,
5469                "minute" | "minutes" | "m" => diff / 60_000_000,
5470                "hour" | "hours" | "h" => diff / 3_600_000_000,
5471                "day" | "days" | "d" => diff / 86_400_000_000,
5472                _ => return Value::Empty,
5473            };
5474            Value::Int(result)
5475        }
5476    }
5477}
5478
5479/// Extract a component from a DateTime value (microseconds since epoch).
5480fn datetime_extract(part: &str, micros: i64) -> Value {
5481    // Convert micros to seconds + remainder for calendar calculations
5482    let total_secs = micros / 1_000_000;
5483    let micro_rem = micros % 1_000_000;
5484
5485    // Simple civil calendar from Unix timestamp (no TZ — UTC assumed)
5486    let days_since_epoch = if total_secs >= 0 {
5487        total_secs / 86400
5488    } else {
5489        (total_secs - 86399) / 86400
5490    };
5491    let secs_of_day = total_secs - days_since_epoch * 86400;
5492
5493    match part {
5494        "hour" => Value::Int(secs_of_day / 3600),
5495        "minute" => Value::Int((secs_of_day % 3600) / 60),
5496        "second" => Value::Int(secs_of_day % 60),
5497        "millisecond" => Value::Int(micro_rem / 1000),
5498        "microsecond" => Value::Int(micro_rem),
5499        "epoch" => Value::Int(total_secs),
5500        "year" | "month" | "day" => {
5501            // Civil date from days since 1970-01-01 (algorithm from Howard Hinnant)
5502            let z = days_since_epoch + 719468;
5503            let era = if z >= 0 { z } else { z - 146096 } / 146097;
5504            let doe = (z - era * 146097) as u32;
5505            let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
5506            let y = (yoe as i64) + era * 400;
5507            let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
5508            let mp = (5 * doy + 2) / 153;
5509            let d = doy - (153 * mp + 2) / 5 + 1;
5510            let m = if mp < 10 { mp + 3 } else { mp - 9 };
5511            let y = if m <= 2 { y + 1 } else { y };
5512            match part {
5513                "year" => Value::Int(y),
5514                "month" => Value::Int(m as i64),
5515                "day" => Value::Int(d as i64),
5516                _ => unreachable!(),
5517            }
5518        }
5519        _ => Value::Empty,
5520    }
5521}
5522
5523/// Evaluate a CAST expression.
5524fn eval_cast(val: Value, target: CastType) -> Value {
5525    match target {
5526        CastType::Int => match val {
5527            Value::Int(n) => Value::Int(n),
5528            Value::Float(f) => Value::Int(f as i64),
5529            Value::Bool(b) => Value::Int(if b { 1 } else { 0 }),
5530            Value::Str(s) => s.parse::<i64>().map(Value::Int).unwrap_or(Value::Empty),
5531            Value::DateTime(m) => Value::Int(m),
5532            _ => Value::Empty,
5533        },
5534        CastType::Float => match val {
5535            Value::Float(f) => Value::Float(f),
5536            Value::Int(n) => Value::Float(n as f64),
5537            Value::Str(s) => s.parse::<f64>().map(Value::Float).unwrap_or(Value::Empty),
5538            Value::Bool(b) => Value::Float(if b { 1.0 } else { 0.0 }),
5539            _ => Value::Empty,
5540        },
5541        CastType::Str => match val {
5542            Value::Str(s) => Value::Str(s),
5543            Value::Int(n) => Value::Str(n.to_string()),
5544            Value::Float(f) => Value::Str(f.to_string()),
5545            Value::Bool(b) => Value::Str(b.to_string()),
5546            Value::DateTime(m) => Value::Str(m.to_string()),
5547            _ => Value::Empty,
5548        },
5549        CastType::Bool => match val {
5550            Value::Bool(b) => Value::Bool(b),
5551            Value::Int(n) => Value::Bool(n != 0),
5552            Value::Str(s) => match s.as_str() {
5553                "true" | "1" | "yes" => Value::Bool(true),
5554                "false" | "0" | "no" => Value::Bool(false),
5555                _ => Value::Empty,
5556            },
5557            _ => Value::Empty,
5558        },
5559        CastType::DateTime => match val {
5560            Value::DateTime(m) => Value::DateTime(m),
5561            Value::Int(m) => Value::DateTime(m),
5562            _ => Value::Empty,
5563        },
5564    }
5565}
5566
5567/// Execute window function computations. Shared by both read and write paths.
5568///
5569/// For each `WindowDef`:
5570///   1. Sort rows by (partition_by keys, order_by keys).
5571///   2. Walk sorted rows, detecting partition boundaries.
5572///   3. Compute the window value per row (running aggregates reset at
5573///      partition boundaries).
5574///   4. Append the computed column to each row and register the column name.
5575///
5576/// All computed columns are appended to the original row data; the
5577/// downstream `Project` node plucks the ones the user asked for.
5578fn execute_window(result: QueryResult, windows: &[WindowDef]) -> Result<QueryResult, String> {
5579    let (mut columns, mut rows) = match result {
5580        QueryResult::Rows { columns, rows } => (columns, rows),
5581        _ => return Err("window function requires row input".into()),
5582    };
5583
5584    for wdef in windows {
5585        // Resolve partition/order column indices against current columns.
5586        let part_indices: Vec<usize> = wdef
5587            .partition_by
5588            .iter()
5589            .map(|name| {
5590                columns
5591                    .iter()
5592                    .position(|c| c == name)
5593                    .ok_or_else(|| format!("window partition column '{name}' not found"))
5594            })
5595            .collect::<Result<Vec<_>, _>>()?;
5596
5597        let ord_indices: Vec<(usize, bool)> = wdef
5598            .order_by
5599            .iter()
5600            .map(|sk| {
5601                columns
5602                    .iter()
5603                    .position(|c| c == &sk.field)
5604                    .map(|i| (i, sk.descending))
5605                    .ok_or_else(|| format!("window order column '{}' not found", sk.field))
5606            })
5607            .collect::<Result<Vec<_>, _>>()?;
5608
5609        // Resolve the argument column index (for aggregate windows).
5610        let arg_col_idx: Option<usize> = if let Some(arg) = wdef.args.first() {
5611            match arg {
5612                Expr::Field(name) => {
5613                    if name == "*" {
5614                        None // count(*) style — no specific column
5615                    } else {
5616                        Some(
5617                            columns
5618                                .iter()
5619                                .position(|c| c == name)
5620                                .ok_or_else(|| format!("window arg column '{name}' not found"))?,
5621                        )
5622                    }
5623                }
5624                _ => None,
5625            }
5626        } else {
5627            None
5628        };
5629
5630        // Build a sort-index to sort rows by partition_by then order_by
5631        // without actually reordering the original Vec (we need original
5632        // order to write results back).
5633        let n = rows.len();
5634        let mut indices: Vec<usize> = (0..n).collect();
5635        indices.sort_by(|&a, &b| {
5636            // Compare partition keys first.
5637            for &pi in &part_indices {
5638                let cmp = rows[a][pi].cmp(&rows[b][pi]);
5639                if cmp != std::cmp::Ordering::Equal {
5640                    return cmp;
5641                }
5642            }
5643            // Then order keys.
5644            for &(oi, desc) in &ord_indices {
5645                let cmp = rows[a][oi].cmp(&rows[b][oi]);
5646                if cmp != std::cmp::Ordering::Equal {
5647                    return if desc { cmp.reverse() } else { cmp };
5648                }
5649            }
5650            std::cmp::Ordering::Equal
5651        });
5652
5653        // Compute window values in sorted order, tracking partition boundaries.
5654        let mut win_values: Vec<Value> = vec![Value::Empty; n];
5655        let mut partition_start = 0usize;
5656        // Running state for aggregate windows:
5657        let mut running_count: i64 = 0;
5658        let mut running_int_sum: i64 = 0;
5659        let mut running_float_sum: f64 = 0.0;
5660        let mut running_saw_float = false;
5661        let mut running_min: Option<Value> = None;
5662        let mut running_max: Option<Value> = None;
5663        let mut rank_counter: i64 = 0;
5664        let mut dense_rank_counter: i64 = 0;
5665        let mut prev_order_key: Option<Vec<Value>> = None;
5666        let mut same_rank_count: i64 = 0;
5667
5668        for sorted_pos in 0..n {
5669            let row_idx = indices[sorted_pos];
5670
5671            // Detect partition boundary.
5672            let new_partition = if sorted_pos == 0 {
5673                true
5674            } else {
5675                let prev_row_idx = indices[sorted_pos - 1];
5676                part_indices
5677                    .iter()
5678                    .any(|&pi| rows[row_idx][pi] != rows[prev_row_idx][pi])
5679            };
5680
5681            if new_partition {
5682                partition_start = sorted_pos;
5683                running_count = 0;
5684                running_int_sum = 0;
5685                running_float_sum = 0.0;
5686                running_saw_float = false;
5687                running_min = None;
5688                running_max = None;
5689                rank_counter = 0;
5690                dense_rank_counter = 0;
5691                prev_order_key = None;
5692                same_rank_count = 0;
5693            }
5694
5695            // Extract current order key for rank tracking.
5696            let current_order_key: Vec<Value> = ord_indices
5697                .iter()
5698                .map(|&(oi, _)| rows[row_idx][oi].clone())
5699                .collect();
5700            let same_as_prev = prev_order_key.as_ref() == Some(&current_order_key);
5701
5702            let value = match wdef.function {
5703                WindowFunc::RowNumber => Value::Int((sorted_pos - partition_start + 1) as i64),
5704                WindowFunc::Rank => {
5705                    if same_as_prev {
5706                        same_rank_count += 1;
5707                    } else {
5708                        rank_counter += same_rank_count + 1;
5709                        same_rank_count = 0;
5710                        if rank_counter == 0 {
5711                            rank_counter = 1;
5712                        }
5713                    }
5714                    Value::Int(rank_counter)
5715                }
5716                WindowFunc::DenseRank => {
5717                    if !same_as_prev {
5718                        dense_rank_counter += 1;
5719                    }
5720                    Value::Int(dense_rank_counter)
5721                }
5722                WindowFunc::Sum => {
5723                    if let Some(ci) = arg_col_idx {
5724                        match &rows[row_idx][ci] {
5725                            Value::Int(v) => running_int_sum += v,
5726                            Value::Float(v) => {
5727                                running_float_sum += v;
5728                                running_saw_float = true;
5729                            }
5730                            _ => {}
5731                        }
5732                    }
5733                    if running_saw_float {
5734                        Value::Float(running_float_sum + running_int_sum as f64)
5735                    } else {
5736                        Value::Int(running_int_sum)
5737                    }
5738                }
5739                WindowFunc::Avg => {
5740                    if let Some(ci) = arg_col_idx {
5741                        match &rows[row_idx][ci] {
5742                            Value::Int(v) => {
5743                                running_float_sum += *v as f64;
5744                                running_count += 1;
5745                            }
5746                            Value::Float(v) => {
5747                                running_float_sum += v;
5748                                running_count += 1;
5749                            }
5750                            _ => {}
5751                        }
5752                    }
5753                    if running_count == 0 {
5754                        Value::Empty
5755                    } else {
5756                        Value::Float(running_float_sum / running_count as f64)
5757                    }
5758                }
5759                WindowFunc::Count => {
5760                    if let Some(ci) = arg_col_idx {
5761                        if !rows[row_idx][ci].is_empty() {
5762                            running_count += 1;
5763                        }
5764                    } else {
5765                        // count(*) — count all rows
5766                        running_count += 1;
5767                    }
5768                    Value::Int(running_count)
5769                }
5770                WindowFunc::Min => {
5771                    if let Some(ci) = arg_col_idx {
5772                        let v = &rows[row_idx][ci];
5773                        if !v.is_empty() {
5774                            running_min = Some(match &running_min {
5775                                None => v.clone(),
5776                                Some(cur) => {
5777                                    if v < cur {
5778                                        v.clone()
5779                                    } else {
5780                                        cur.clone()
5781                                    }
5782                                }
5783                            });
5784                        }
5785                    }
5786                    running_min.clone().unwrap_or(Value::Empty)
5787                }
5788                WindowFunc::Max => {
5789                    if let Some(ci) = arg_col_idx {
5790                        let v = &rows[row_idx][ci];
5791                        if !v.is_empty() {
5792                            running_max = Some(match &running_max {
5793                                None => v.clone(),
5794                                Some(cur) => {
5795                                    if v > cur {
5796                                        v.clone()
5797                                    } else {
5798                                        cur.clone()
5799                                    }
5800                                }
5801                            });
5802                        }
5803                    }
5804                    running_max.clone().unwrap_or(Value::Empty)
5805                }
5806            };
5807
5808            prev_order_key = Some(current_order_key);
5809            win_values[row_idx] = value;
5810        }
5811
5812        // Append the computed window column to each row.
5813        for (ri, row) in rows.iter_mut().enumerate() {
5814            row.push(win_values[ri].clone());
5815        }
5816        columns.push(wdef.output_name.clone());
5817    }
5818
5819    Ok(QueryResult::Rows { columns, rows })
5820}
5821
5822/// Mission E2b: compute one aggregate over a set of rows in a group.
5823fn compute_group_aggregate(
5824    func: AggFunc,
5825    all_rows: &[Vec<Value>],
5826    row_indices: &[usize],
5827    col_idx: usize,
5828) -> Value {
5829    match func {
5830        AggFunc::Count => {
5831            if col_idx == usize::MAX {
5832                // count(*) — count all rows in the group.
5833                return Value::Int(row_indices.len() as i64);
5834            }
5835            let count = row_indices
5836                .iter()
5837                .filter(|&&ri| !all_rows[ri][col_idx].is_empty())
5838                .count();
5839            Value::Int(count as i64)
5840        }
5841        AggFunc::CountDistinct => {
5842            let mut seen = std::collections::HashSet::new();
5843            for &ri in row_indices {
5844                let v = &all_rows[ri][col_idx];
5845                if !v.is_empty() {
5846                    seen.insert(v.clone());
5847                }
5848            }
5849            Value::Int(seen.len() as i64)
5850        }
5851        AggFunc::Sum => {
5852            // Mirror the scalar Sum path: accumulate int and float
5853            // contributions separately and promote the final result to
5854            // Float if any Float row was observed. Prevents silent
5855            // drop of Float columns in GROUP BY aggregates.
5856            let mut int_sum: i64 = 0;
5857            let mut float_sum: f64 = 0.0;
5858            let mut saw_float = false;
5859            for &ri in row_indices {
5860                match &all_rows[ri][col_idx] {
5861                    Value::Int(v) => int_sum += v,
5862                    Value::Float(v) => {
5863                        float_sum += *v;
5864                        saw_float = true;
5865                    }
5866                    _ => {}
5867                }
5868            }
5869            if saw_float {
5870                Value::Float(float_sum + int_sum as f64)
5871            } else {
5872                Value::Int(int_sum)
5873            }
5874        }
5875        AggFunc::Avg => {
5876            let mut sum = 0.0f64;
5877            let mut count = 0usize;
5878            for &ri in row_indices {
5879                match &all_rows[ri][col_idx] {
5880                    Value::Int(v) => {
5881                        sum += *v as f64;
5882                        count += 1;
5883                    }
5884                    Value::Float(v) => {
5885                        sum += *v;
5886                        count += 1;
5887                    }
5888                    _ => {}
5889                }
5890            }
5891            if count == 0 {
5892                Value::Empty
5893            } else {
5894                Value::Float(sum / count as f64)
5895            }
5896        }
5897        AggFunc::Min => row_indices
5898            .iter()
5899            .map(|&ri| &all_rows[ri][col_idx])
5900            .filter(|v| !v.is_empty())
5901            .min()
5902            .cloned()
5903            .unwrap_or(Value::Empty),
5904        AggFunc::Max => row_indices
5905            .iter()
5906            .map(|&ri| &all_rows[ri][col_idx])
5907            .filter(|v| !v.is_empty())
5908            .max()
5909            .cloned()
5910            .unwrap_or(Value::Empty),
5911    }
5912}
5913
5914/// Mission E1.3: try to extract equi-join key indices from a join `on`
5915/// predicate. Returns `Some((left_col_idx, right_col_idx))` when the
5916/// predicate is exactly `L = R` (or `R = L`) and both sides resolve
5917/// cleanly — `L` to the left subtree's column list and `R` to the right
5918/// subtree's column list.
5919///
5920/// This is deliberately narrow. We only recognise the two shapes:
5921///   * `QualifiedField = QualifiedField`  (`u.id = o.user_id`)
5922///   * `Field = Field`                    (`.id = .user_id`, unqualified)
5923///
5924/// Anything else — conjunctions, constants, function calls, or predicates
5925/// that touch the same side on both halves — falls through to the
5926/// nested-loop path unchanged.
5927fn try_extract_equi_join_keys(
5928    pred: &Expr,
5929    left_columns: &[String],
5930    right_columns: &[String],
5931) -> Option<(usize, usize)> {
5932    let (lhs, op, rhs) = match pred {
5933        Expr::BinaryOp(l, op, r) => (l.as_ref(), *op, r.as_ref()),
5934        _ => return None,
5935    };
5936    if op != BinOp::Eq {
5937        return None;
5938    }
5939    // Normal orientation: lhs in left, rhs in right.
5940    if let (Some(li), Some(ri)) = (
5941        resolve_side_column(lhs, left_columns),
5942        resolve_side_column(rhs, right_columns),
5943    ) {
5944        return Some((li, ri));
5945    }
5946    // Swapped: rhs in left, lhs in right. Both sides of `=` are
5947    // commutative so this is safe.
5948    if let (Some(li), Some(ri)) = (
5949        resolve_side_column(rhs, left_columns),
5950        resolve_side_column(lhs, right_columns),
5951    ) {
5952        return Some((li, ri));
5953    }
5954    None
5955}
5956
5957fn resolve_side_column(expr: &Expr, columns: &[String]) -> Option<usize> {
5958    match expr {
5959        Expr::QualifiedField { qualifier, field } => {
5960            // Byte-level match so we don't allocate a fresh `format!` on
5961            // every call — this runs once per plan, so allocation would be
5962            // cheap, but the match is trivial enough to keep inline with
5963            // the eval_expr version.
5964            let q = qualifier.as_bytes();
5965            let f = field.as_bytes();
5966            columns.iter().position(|c| {
5967                let b = c.as_bytes();
5968                b.len() == q.len() + 1 + f.len()
5969                    && b[..q.len()] == *q
5970                    && b[q.len()] == b'.'
5971                    && b[q.len() + 1..] == *f
5972            })
5973        }
5974        Expr::Field(name) => columns.iter().position(|c| c == name),
5975        _ => None,
5976    }
5977}
5978
5979/// Mission E1.3: O(L + R) hash join. Builds a `FxHashMap<Value, Vec<usize>>`
5980/// over the right (inner) side's join keys, then streams the left (outer)
5981/// side and for each probe row emits every combined row whose right-side
5982/// key matches. For `JoinKind::LeftOuter`, unmatched left rows are emitted
5983/// padded with `Value::Empty` on the right side.
5984///
5985/// The right side is always the build side. That choice is forced for
5986/// LeftOuter (the left side must stream so we can detect orphans), and
5987/// for Inner it's a reasonable default — left-deep plans tend to grow the
5988/// left side with each join, so the un-joined right leaf is often the
5989/// smaller of the two at each level.
5990fn hash_join(
5991    left_columns: Vec<String>,
5992    left_rows: Vec<Vec<Value>>,
5993    right_columns: Vec<String>,
5994    right_rows: Vec<Vec<Value>>,
5995    left_key_idx: usize,
5996    right_key_idx: usize,
5997    kind: JoinKind,
5998) -> QueryResult {
5999    use rustc_hash::FxHashMap;
6000
6001    let n_left = left_columns.len();
6002    let n_right = right_columns.len();
6003    let mut columns = Vec::with_capacity(n_left + n_right);
6004    columns.extend(left_columns);
6005    columns.extend(right_columns);
6006
6007    // Build: right_key -> list of right-row indices. Pre-size to the row
6008    // count so the map doesn't rehash mid-build.
6009    let mut build: FxHashMap<Value, Vec<usize>> =
6010        FxHashMap::with_capacity_and_hasher(right_rows.len(), Default::default());
6011    for (i, row) in right_rows.iter().enumerate() {
6012        // Skip Empty keys on the build side — they can never match under
6013        // SQL semantics (NULL ≠ NULL) and would collapse all nullables to
6014        // one bucket.
6015        if matches!(row[right_key_idx], Value::Empty) {
6016            continue;
6017        }
6018        build.entry(row[right_key_idx].clone()).or_default().push(i);
6019    }
6020
6021    // Reasonable starting capacity — inner joins produce ≥ left_rows.len()
6022    // rows in the common 1:1 case, left-outer always emits ≥ left_rows.len().
6023    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(left_rows.len());
6024
6025    for left_row in &left_rows {
6026        let key = &left_row[left_key_idx];
6027        let matched = if matches!(key, Value::Empty) {
6028            None
6029        } else {
6030            build.get(key)
6031        };
6032        match matched {
6033            Some(matches) if !matches.is_empty() => {
6034                for &ri in matches {
6035                    let right_row = &right_rows[ri];
6036                    let mut combined = Vec::with_capacity(n_left + n_right);
6037                    combined.extend_from_slice(left_row);
6038                    combined.extend_from_slice(right_row);
6039                    rows.push(combined);
6040                }
6041            }
6042            _ => {
6043                if matches!(kind, JoinKind::LeftOuter) {
6044                    let mut row = Vec::with_capacity(n_left + n_right);
6045                    row.extend_from_slice(left_row);
6046                    row.resize(n_left + n_right, Value::Empty);
6047                    rows.push(row);
6048                }
6049            }
6050        }
6051    }
6052
6053    QueryResult::Rows { columns, rows }
6054}
6055
6056/// Lower unindexed `RangeScan` nodes to `Filter(SeqScan)` so that all
6057/// downstream fast paths (count, project+limit, sort+limit, agg, update,
6058/// delete) continue to fire.
6059///
6060/// The planner emits `RangeScan` speculatively for every range inequality
6061/// (`.age > 30`) because it has no catalog access. When the column has a
6062/// B-tree index, `RangeScan` is the correct plan. When it doesn't, the
6063/// executor's `RangeScan` fallback materialises every matching row with
6064/// full `decode_row` — bypassing the compiled-predicate fast paths that
6065/// `Filter(SeqScan)` would trigger.
6066///
6067/// This pass runs once per query, before execution.
6068fn lower_unindexed_range_scans(catalog: &Catalog, plan: &PlanNode) -> PlanNode {
6069    match plan {
6070        PlanNode::RangeScan {
6071            table,
6072            column,
6073            start,
6074            end,
6075        } => {
6076            if let Some(tbl) = catalog.get_table(table) {
6077                if tbl.index(column).is_some() {
6078                    return plan.clone();
6079                }
6080            }
6081            let pred = synthesize_range_predicate(column, start, end);
6082            PlanNode::Filter {
6083                input: Box::new(PlanNode::SeqScan {
6084                    table: table.clone(),
6085                }),
6086                predicate: pred,
6087            }
6088        }
6089        PlanNode::Filter { input, predicate } => PlanNode::Filter {
6090            input: Box::new(lower_unindexed_range_scans(catalog, input)),
6091            predicate: predicate.clone(),
6092        },
6093        PlanNode::Project { input, fields } => PlanNode::Project {
6094            input: Box::new(lower_unindexed_range_scans(catalog, input)),
6095            fields: fields.clone(),
6096        },
6097        PlanNode::Sort { input, keys } => PlanNode::Sort {
6098            input: Box::new(lower_unindexed_range_scans(catalog, input)),
6099            keys: keys.clone(),
6100        },
6101        PlanNode::Limit { input, count } => PlanNode::Limit {
6102            input: Box::new(lower_unindexed_range_scans(catalog, input)),
6103            count: count.clone(),
6104        },
6105        PlanNode::Offset { input, count } => PlanNode::Offset {
6106            input: Box::new(lower_unindexed_range_scans(catalog, input)),
6107            count: count.clone(),
6108        },
6109        PlanNode::Aggregate {
6110            input,
6111            function,
6112            field,
6113        } => PlanNode::Aggregate {
6114            input: Box::new(lower_unindexed_range_scans(catalog, input)),
6115            function: *function,
6116            field: field.clone(),
6117        },
6118        PlanNode::Distinct { input } => PlanNode::Distinct {
6119            input: Box::new(lower_unindexed_range_scans(catalog, input)),
6120        },
6121        PlanNode::GroupBy {
6122            input,
6123            keys,
6124            aggregates,
6125            having,
6126        } => PlanNode::GroupBy {
6127            input: Box::new(lower_unindexed_range_scans(catalog, input)),
6128            keys: keys.clone(),
6129            aggregates: aggregates.clone(),
6130            having: having.clone(),
6131        },
6132        PlanNode::Update {
6133            input,
6134            table,
6135            assignments,
6136        } => PlanNode::Update {
6137            input: Box::new(lower_unindexed_range_scans(catalog, input)),
6138            table: table.clone(),
6139            assignments: assignments.clone(),
6140        },
6141        PlanNode::Delete { input, table } => PlanNode::Delete {
6142            input: Box::new(lower_unindexed_range_scans(catalog, input)),
6143            table: table.clone(),
6144        },
6145        PlanNode::Window { input, windows } => PlanNode::Window {
6146            input: Box::new(lower_unindexed_range_scans(catalog, input)),
6147            windows: windows.clone(),
6148        },
6149        PlanNode::Union { left, right, all } => PlanNode::Union {
6150            left: Box::new(lower_unindexed_range_scans(catalog, left)),
6151            right: Box::new(lower_unindexed_range_scans(catalog, right)),
6152            all: *all,
6153        },
6154        PlanNode::Explain { input } => PlanNode::Explain {
6155            input: Box::new(lower_unindexed_range_scans(catalog, input)),
6156        },
6157        PlanNode::NestedLoopJoin {
6158            left,
6159            right,
6160            on,
6161            kind,
6162        } => PlanNode::NestedLoopJoin {
6163            left: Box::new(lower_unindexed_range_scans(catalog, left)),
6164            right: Box::new(lower_unindexed_range_scans(catalog, right)),
6165            on: on.clone(),
6166            kind: *kind,
6167        },
6168        // Leaf nodes: no children to recurse into.
6169        _ => plan.clone(),
6170    }
6171}
6172
6173/// Synthesize a range predicate from RangeScan bounds for the fallback path.
6174fn synthesize_range_predicate(
6175    column: &str,
6176    start: &Option<(Expr, bool)>,
6177    end: &Option<(Expr, bool)>,
6178) -> Expr {
6179    let lower = start.as_ref().map(|(expr, inclusive)| {
6180        let op = if *inclusive { BinOp::Gte } else { BinOp::Gt };
6181        Expr::BinaryOp(
6182            Box::new(Expr::Field(column.to_string())),
6183            op,
6184            Box::new(expr.clone()),
6185        )
6186    });
6187    let upper = end.as_ref().map(|(expr, inclusive)| {
6188        let op = if *inclusive { BinOp::Lte } else { BinOp::Lt };
6189        Expr::BinaryOp(
6190            Box::new(Expr::Field(column.to_string())),
6191            op,
6192            Box::new(expr.clone()),
6193        )
6194    });
6195    match (lower, upper) {
6196        (Some(l), Some(u)) => Expr::BinaryOp(Box::new(l), BinOp::And, Box::new(u)),
6197        (Some(l), None) => l,
6198        (None, Some(u)) => u,
6199        (None, None) => Expr::Literal(Literal::Bool(true)),
6200    }
6201}
6202
6203/// Check if a value falls within a range (used in last-resort decoded-row eval).
6204fn range_matches(
6205    val: &Value,
6206    start: &Option<Value>,
6207    start_inc: bool,
6208    end: &Option<Value>,
6209    end_inc: bool,
6210) -> bool {
6211    if let Some(ref s) = start {
6212        if start_inc {
6213            if val < s {
6214                return false;
6215            }
6216        } else if val <= s {
6217            return false;
6218        }
6219    }
6220    if let Some(ref e) = end {
6221        if end_inc {
6222            if val > e {
6223                return false;
6224            }
6225        } else if val >= e {
6226            return false;
6227        }
6228    }
6229    true
6230}
6231
6232/// Format a `PlanNode` tree as a human-readable, indented text
6233/// representation. Used by the `EXPLAIN` command.
6234fn format_plan_tree(plan: &PlanNode, depth: usize) -> String {
6235    let indent = "  ".repeat(depth);
6236    match plan {
6237        PlanNode::SeqScan { table } => format!("{indent}SeqScan table={table}"),
6238        PlanNode::AliasScan { table, alias } => {
6239            format!("{indent}AliasScan table={table} alias={alias}")
6240        }
6241        PlanNode::IndexScan { table, column, key } => {
6242            format!("{indent}IndexScan table={table} column={column} key={key:?}")
6243        }
6244        PlanNode::RangeScan {
6245            table,
6246            column,
6247            start,
6248            end,
6249        } => {
6250            let s = match start {
6251                Some((expr, inc)) => {
6252                    let op = if *inc { ">=" } else { ">" };
6253                    format!("{op}{expr:?}")
6254                }
6255                None => "unbounded".to_string(),
6256            };
6257            let e = match end {
6258                Some((expr, inc)) => {
6259                    let op = if *inc { "<=" } else { "<" };
6260                    format!("{op}{expr:?}")
6261                }
6262                None => "unbounded".to_string(),
6263            };
6264            format!("{indent}RangeScan table={table} column={column} [{s}, {e}]")
6265        }
6266        PlanNode::Filter { input, predicate } => {
6267            let child = format_plan_tree(input, depth + 1);
6268            format!("{indent}Filter predicate={predicate:?}\n{child}")
6269        }
6270        PlanNode::Project { input, fields } => {
6271            let names: Vec<String> = fields
6272                .iter()
6273                .map(|f| match &f.alias {
6274                    Some(a) => format!("{a}: {:?}", f.expr),
6275                    None => format!("{:?}", f.expr),
6276                })
6277                .collect();
6278            let child = format_plan_tree(input, depth + 1);
6279            format!("{indent}Project fields=[{}]\n{child}", names.join(", "))
6280        }
6281        PlanNode::Sort { input, keys } => {
6282            let ks: Vec<String> = keys
6283                .iter()
6284                .map(|k| {
6285                    if k.descending {
6286                        format!("{} desc", k.field)
6287                    } else {
6288                        k.field.clone()
6289                    }
6290                })
6291                .collect();
6292            let child = format_plan_tree(input, depth + 1);
6293            format!("{indent}Sort keys=[{}]\n{child}", ks.join(", "))
6294        }
6295        PlanNode::Limit { input, count } => {
6296            let child = format_plan_tree(input, depth + 1);
6297            format!("{indent}Limit count={count:?}\n{child}")
6298        }
6299        PlanNode::Offset { input, count } => {
6300            let child = format_plan_tree(input, depth + 1);
6301            format!("{indent}Offset count={count:?}\n{child}")
6302        }
6303        PlanNode::Aggregate {
6304            input,
6305            function,
6306            field,
6307        } => {
6308            let f = field.as_deref().unwrap_or("*");
6309            let child = format_plan_tree(input, depth + 1);
6310            format!("{indent}Aggregate fn={function:?} field={f}\n{child}")
6311        }
6312        PlanNode::NestedLoopJoin {
6313            left,
6314            right,
6315            on,
6316            kind,
6317        } => {
6318            let left_child = format_plan_tree(left, depth + 1);
6319            let right_child = format_plan_tree(right, depth + 1);
6320            let on_str = match on {
6321                Some(pred) => format!("{pred:?}"),
6322                None => "none".to_string(),
6323            };
6324            format!("{indent}NestedLoopJoin kind={kind:?} on={on_str}\n{left_child}\n{right_child}")
6325        }
6326        PlanNode::Distinct { input } => {
6327            let child = format_plan_tree(input, depth + 1);
6328            format!("{indent}Distinct\n{child}")
6329        }
6330        PlanNode::GroupBy {
6331            input,
6332            keys,
6333            aggregates,
6334            having,
6335        } => {
6336            let agg_strs: Vec<String> = aggregates
6337                .iter()
6338                .map(|a| format!("{:?}({}) as {}", a.function, a.field, a.output_name))
6339                .collect();
6340            let having_str = match having {
6341                Some(h) => format!(" having={h:?}"),
6342                None => String::new(),
6343            };
6344            let child = format_plan_tree(input, depth + 1);
6345            format!(
6346                "{indent}GroupBy keys=[{}] aggs=[{}]{having_str}\n{child}",
6347                keys.join(", "),
6348                agg_strs.join(", "),
6349            )
6350        }
6351        PlanNode::Insert { table, assignments } => {
6352            let cols: Vec<&str> = assignments.iter().map(|a| a.field.as_str()).collect();
6353            format!("{indent}Insert table={table} cols=[{}]", cols.join(", "))
6354        }
6355        PlanNode::Upsert {
6356            table,
6357            key_column,
6358            assignments,
6359            on_conflict,
6360        } => {
6361            let cols: Vec<&str> = assignments.iter().map(|a| a.field.as_str()).collect();
6362            let conflict_cols: Vec<&str> = on_conflict.iter().map(|a| a.field.as_str()).collect();
6363            if conflict_cols.is_empty() {
6364                format!(
6365                    "{indent}Upsert table={table} key={key_column} cols=[{}]",
6366                    cols.join(", ")
6367                )
6368            } else {
6369                format!(
6370                    "{indent}Upsert table={table} key={key_column} cols=[{}] on_conflict=[{}]",
6371                    cols.join(", "),
6372                    conflict_cols.join(", ")
6373                )
6374            }
6375        }
6376        PlanNode::Update {
6377            input,
6378            table,
6379            assignments,
6380        } => {
6381            let cols: Vec<&str> = assignments.iter().map(|a| a.field.as_str()).collect();
6382            let child = format_plan_tree(input, depth + 1);
6383            format!(
6384                "{indent}Update table={table} set=[{}]\n{child}",
6385                cols.join(", ")
6386            )
6387        }
6388        PlanNode::Delete { input, table } => {
6389            let child = format_plan_tree(input, depth + 1);
6390            format!("{indent}Delete table={table}\n{child}")
6391        }
6392        PlanNode::CreateTable { name, fields } => {
6393            let fs: Vec<String> = fields
6394                .iter()
6395                .map(|(n, t, r)| {
6396                    if *r {
6397                        format!("{n}: {t} required")
6398                    } else {
6399                        format!("{n}: {t}")
6400                    }
6401                })
6402                .collect();
6403            format!("{indent}CreateTable name={name} fields=[{}]", fs.join(", "))
6404        }
6405        PlanNode::AlterTable { table, action } => {
6406            format!("{indent}AlterTable table={table} action={action:?}")
6407        }
6408        PlanNode::DropTable { name } => format!("{indent}DropTable name={name}"),
6409        PlanNode::CreateView { name, .. } => format!("{indent}CreateView name={name}"),
6410        PlanNode::RefreshView { name } => format!("{indent}RefreshView name={name}"),
6411        PlanNode::DropView { name } => format!("{indent}DropView name={name}"),
6412        PlanNode::Window { input, windows } => {
6413            let ws: Vec<String> = windows
6414                .iter()
6415                .map(|w| format!("{:?} as {}", w.function, w.output_name))
6416                .collect();
6417            let child = format_plan_tree(input, depth + 1);
6418            format!("{indent}Window fns=[{}]\n{child}", ws.join(", "))
6419        }
6420        PlanNode::Union { left, right, all } => {
6421            let kind = if *all { "UNION ALL" } else { "UNION" };
6422            let left_child = format_plan_tree(left, depth + 1);
6423            let right_child = format_plan_tree(right, depth + 1);
6424            format!("{indent}{kind}\n{left_child}\n{right_child}")
6425        }
6426        PlanNode::Explain { input } => {
6427            let child = format_plan_tree(input, depth + 1);
6428            format!("{indent}Explain\n{child}")
6429        }
6430    }
6431}
6432
6433/// Executor-local row layout — computes the layout facts the compiled
6434/// predicates and column readers need without touching the storage crate's
6435/// private `RowLayout` internals.
6436///
6437/// The row format is:
6438///   [length: u16][null_bitmap][fixed cols packed][var offset table: (n_var+1) u16s][var data]
6439struct FastLayout {
6440    /// Null bitmap size in bytes.
6441    bitmap_size: usize,
6442    /// Byte offset within the fixed region for each column (None = var-length).
6443    fixed_offsets: Vec<Option<usize>>,
6444    /// Size of the fixed region in bytes.
6445    fixed_region_size: usize,
6446    /// For each column: its slot index in the var-offset table (None = fixed).
6447    var_indices: Vec<Option<usize>>,
6448    /// Total number of variable-length columns.
6449    n_var: usize,
6450}
6451
6452impl FastLayout {
6453    fn new(schema: &Schema) -> Self {
6454        let n_cols = schema.columns.len();
6455        let bitmap_size = n_cols.div_ceil(8);
6456        let mut fixed_offsets = vec![None; n_cols];
6457        let mut var_indices = vec![None; n_cols];
6458        let mut fixed_pos: usize = 0;
6459        let mut var_count: usize = 0;
6460
6461        for (i, col) in schema.columns.iter().enumerate() {
6462            if is_fixed_size(col.type_id) {
6463                fixed_offsets[i] = Some(fixed_pos);
6464                fixed_pos += fixed_size(col.type_id).unwrap();
6465            } else {
6466                var_indices[i] = Some(var_count);
6467                var_count += 1;
6468            }
6469        }
6470
6471        FastLayout {
6472            bitmap_size,
6473            fixed_offsets,
6474            fixed_region_size: fixed_pos,
6475            var_indices,
6476            n_var: var_count,
6477        }
6478    }
6479
6480    /// Where the var-offset table starts within `data`.
6481    #[inline]
6482    fn var_offset_table_start(&self) -> usize {
6483        2 + self.bitmap_size + self.fixed_region_size
6484    }
6485
6486    /// Where the var-data region starts within `data`.
6487    #[inline]
6488    fn var_data_start(&self) -> usize {
6489        self.var_offset_table_start() + (self.n_var + 1) * 2
6490    }
6491}
6492
6493type CompiledPredicate = Box<dyn Fn(&[u8]) -> bool>;
6494
6495/// Map an f64 bit pattern to a u64 that orders under unsigned integer
6496/// comparison the same way `f64::total_cmp` orders the floats. Classic
6497/// sortable-float transform:
6498///   - Positive floats (sign bit 0): flip the sign bit. This maps
6499///     [+0, +∞, +NaN] to [0x8000…, 0xFFF0…, 0xFFF8…] — increasing as u64.
6500///   - Negative floats (sign bit 1): flip every bit. This maps
6501///     [-∞, -0] to [0x000F…, 0x7FFF…] — increasing as u64, and placed
6502///     *below* the positive range so negatives < positives.
6503///
6504/// Used by Mission D10 Float fast paths so we can key heaps on `u64`
6505/// (branch-cheap, folds into LLVM xor/sar/xor) instead of a `TotalF64`
6506/// newtype with `Ord::cmp` calling `total_cmp`.
6507#[inline]
6508fn f64_bits_to_sortable_u64(bits: u64) -> u64 {
6509    // `((bits >> 63) as i64 * -1) as u64 | 0x8000_0000_0000_0000`
6510    // would also work; the branchless form below is equally good on
6511    // modern CPUs and easier to read.
6512    if bits & 0x8000_0000_0000_0000 == 0 {
6513        bits ^ 0x8000_0000_0000_0000
6514    } else {
6515        !bits
6516    }
6517}
6518
6519/// A single flattened predicate leaf — pure data, no closures, no allocation
6520/// per call. Mission D3: replaces recursive Box<dyn Fn> conjunctions with a
6521/// `Vec<CompiledLeaf>` so the inner scan loop becomes a tight match instead
6522/// of N+1 vtable indirect calls per row.
6523enum CompiledLeaf {
6524    /// `.field <op> literal_int` (or reversed)
6525    Int {
6526        data_offset: usize,
6527        bitmap_byte: usize,
6528        bitmap_bit: u8,
6529        op: BinOp,
6530        literal: i64,
6531    },
6532    /// `.field <op> literal_float` (or reversed), where `.field` is a
6533    /// Float column. Int literals that bound a Float column (e.g.
6534    /// `.price > 100` on `price: float`) are also routed here, promoted
6535    /// to `f64` at compile time so the hot loop only sees one shape.
6536    /// Comparisons use `f64::total_cmp` so NaN handling is deterministic
6537    /// and consistent with `Value::Ord` across every read path.
6538    Float {
6539        data_offset: usize,
6540        bitmap_byte: usize,
6541        bitmap_bit: u8,
6542        op: BinOp,
6543        literal: f64,
6544    },
6545    /// `.field is null` or `.field is not null`
6546    IsNull {
6547        bitmap_byte: usize,
6548        bitmap_bit: u8,
6549        want_null: bool,
6550    },
6551    /// `.field = string_literal` or `.field != string_literal`
6552    StrEq {
6553        var_offset_table_start: usize,
6554        var_data_start: usize,
6555        var_idx: usize,
6556        bitmap_byte: usize,
6557        bitmap_bit: u8,
6558        negate: bool,
6559        needle: Vec<u8>,
6560    },
6561}
6562
6563impl CompiledLeaf {
6564    /// Evaluate this leaf against a row's raw bytes. `#[inline]` so the
6565    /// match folds into the caller's tight loop with LTO.
6566    #[inline]
6567    fn eval(&self, data: &[u8]) -> bool {
6568        match self {
6569            CompiledLeaf::Int {
6570                data_offset,
6571                bitmap_byte,
6572                bitmap_bit,
6573                op,
6574                literal,
6575            } => {
6576                let is_null = (data[2 + bitmap_byte] >> bitmap_bit) & 1 == 1;
6577                if is_null {
6578                    return false;
6579                }
6580                let val =
6581                    i64::from_le_bytes(data[*data_offset..*data_offset + 8].try_into().unwrap());
6582                match op {
6583                    BinOp::Eq => val == *literal,
6584                    BinOp::Neq => val != *literal,
6585                    BinOp::Lt => val < *literal,
6586                    BinOp::Gt => val > *literal,
6587                    BinOp::Lte => val <= *literal,
6588                    BinOp::Gte => val >= *literal,
6589                    _ => false,
6590                }
6591            }
6592            CompiledLeaf::Float {
6593                data_offset,
6594                bitmap_byte,
6595                bitmap_bit,
6596                op,
6597                literal,
6598            } => {
6599                let is_null = (data[2 + bitmap_byte] >> bitmap_bit) & 1 == 1;
6600                if is_null {
6601                    return false;
6602                }
6603                let val =
6604                    f64::from_le_bytes(data[*data_offset..*data_offset + 8].try_into().unwrap());
6605                // `total_cmp` matches Value::Ord: NaN > everything,
6606                // -0.0 < +0.0, finite order as expected. Keeps compiled
6607                // WHERE identical in semantics to the generic row-decode
6608                // path (which calls Value::cmp directly).
6609                let ord = val.total_cmp(literal);
6610                match op {
6611                    BinOp::Eq => ord.is_eq(),
6612                    BinOp::Neq => !ord.is_eq(),
6613                    BinOp::Lt => ord.is_lt(),
6614                    BinOp::Gt => ord.is_gt(),
6615                    BinOp::Lte => !ord.is_gt(),
6616                    BinOp::Gte => !ord.is_lt(),
6617                    _ => false,
6618                }
6619            }
6620            CompiledLeaf::IsNull {
6621                bitmap_byte,
6622                bitmap_bit,
6623                want_null,
6624            } => {
6625                let is_null = (data[2 + bitmap_byte] >> bitmap_bit) & 1 == 1;
6626                if *want_null {
6627                    is_null
6628                } else {
6629                    !is_null
6630                }
6631            }
6632            CompiledLeaf::StrEq {
6633                var_offset_table_start,
6634                var_data_start,
6635                var_idx,
6636                bitmap_byte,
6637                bitmap_bit,
6638                negate,
6639                needle,
6640            } => {
6641                let is_null = (data[2 + bitmap_byte] >> bitmap_bit) & 1 == 1;
6642                if is_null {
6643                    return false;
6644                }
6645                let off_pos = var_offset_table_start + var_idx * 2;
6646                let next_pos = var_offset_table_start + (var_idx + 1) * 2;
6647                let start =
6648                    u16::from_le_bytes(data[off_pos..off_pos + 2].try_into().unwrap()) as usize;
6649                let end =
6650                    u16::from_le_bytes(data[next_pos..next_pos + 2].try_into().unwrap()) as usize;
6651                let slice = &data[var_data_start + start..var_data_start + end];
6652                let eq = slice == needle.as_slice();
6653                if *negate {
6654                    !eq
6655                } else {
6656                    eq
6657                }
6658            }
6659        }
6660    }
6661}
6662
6663/// Attempt to compile a predicate expression into a closure over raw row
6664/// bytes. Returns None if the predicate contains shapes we don't handle
6665/// (arithmetic, Or, Coalesce, non-literal comparands, etc.). Supported:
6666///   - `.field <op> literal_int` and its reversed form
6667///   - `.field = string_literal` / `string_literal = .field`
6668///   - `And` conjunctions of any number of the above
6669///
6670/// Mission D3: AND chains are flattened into a single `Vec<CompiledLeaf>`
6671/// closed over by ONE outer closure. The previous implementation built a
6672/// recursive `Box<Fn>` per AND combinator, costing N+1 indirect vtable
6673/// calls per row for an N-leaf conjunction. The flat version dispatches
6674/// each leaf via match (predictable branch, fully inlinable with LTO),
6675/// short-circuiting on the first failing leaf.
6676fn compile_predicate(
6677    expr: &Expr,
6678    columns: &[String],
6679    layout: &FastLayout,
6680    schema: &Schema,
6681) -> Option<CompiledPredicate> {
6682    let mut leaves: Vec<CompiledLeaf> = Vec::new();
6683    flatten_and_compile(expr, columns, layout, schema, &mut leaves)?;
6684    if leaves.is_empty() {
6685        return None;
6686    }
6687    if leaves.len() == 1 {
6688        // Single-leaf fast path: skip the Vec iteration entirely.
6689        let leaf = leaves.into_iter().next().unwrap();
6690        return Some(Box::new(move |data: &[u8]| leaf.eval(data)));
6691    }
6692    Some(Box::new(move |data: &[u8]| {
6693        // Tight short-circuit AND loop. With CompiledLeaf::eval marked
6694        // #[inline], LTO can fold the match arms into this loop body.
6695        for leaf in &leaves {
6696            if !leaf.eval(data) {
6697                return false;
6698            }
6699        }
6700        true
6701    }))
6702}
6703
6704/// Recursively walk an AND chain and push each leaf into `out`. Returns
6705/// `None` if any sub-expression isn't a supported leaf shape.
6706fn flatten_and_compile(
6707    expr: &Expr,
6708    columns: &[String],
6709    layout: &FastLayout,
6710    schema: &Schema,
6711    out: &mut Vec<CompiledLeaf>,
6712) -> Option<()> {
6713    match expr {
6714        Expr::BinaryOp(left, BinOp::And, right) => {
6715            flatten_and_compile(left, columns, layout, schema, out)?;
6716            flatten_and_compile(right, columns, layout, schema, out)?;
6717            Some(())
6718        }
6719        Expr::BinaryOp(left, op, right) => {
6720            if let Some(leaf) = build_int_leaf(left, *op, right, columns, layout, schema) {
6721                out.push(leaf);
6722                return Some(());
6723            }
6724            if let Some(leaf) = build_float_leaf(left, *op, right, columns, layout, schema) {
6725                out.push(leaf);
6726                return Some(());
6727            }
6728            if let Some(leaf) = build_str_eq_leaf(left, *op, right, columns, layout, schema) {
6729                out.push(leaf);
6730                return Some(());
6731            }
6732            None
6733        }
6734        Expr::UnaryOp(op, inner) if *op == UnaryOp::IsNull || *op == UnaryOp::IsNotNull => {
6735            if let Expr::Field(name) = inner.as_ref() {
6736                let col_idx = columns.iter().position(|c| c == name)?;
6737                let bitmap_byte = col_idx / 8;
6738                let bitmap_bit = (col_idx % 8) as u8;
6739                let want_null = *op == UnaryOp::IsNull;
6740                out.push(CompiledLeaf::IsNull {
6741                    bitmap_byte,
6742                    bitmap_bit,
6743                    want_null,
6744                });
6745                Some(())
6746            } else {
6747                None
6748            }
6749        }
6750        _ => None,
6751    }
6752}
6753
6754/// Build an `Int` leaf from `.field <op> literal_int` (or reversed).
6755///
6756/// Only fires for columns whose declared type is `TypeId::Int`. If the
6757/// column is a different numeric type (Float, DateTime) we return `None`
6758/// so the caller falls back to the generic `Value::cmp` evaluation path,
6759/// which correctly handles cross-type numeric comparison (e.g. Int literal
6760/// vs Float column in `BETWEEN 100 AND 500` on a `price: float` column).
6761/// Previously this function read 8 bytes of a Float column as little-endian
6762/// i64, producing nonsense comparisons.
6763fn build_int_leaf(
6764    left: &Expr,
6765    op: BinOp,
6766    right: &Expr,
6767    columns: &[String],
6768    layout: &FastLayout,
6769    schema: &Schema,
6770) -> Option<CompiledLeaf> {
6771    let (field_name, literal_val, op) = match (left, right) {
6772        (Expr::Field(name), Expr::Literal(Literal::Int(v))) => (name, *v, op),
6773        (Expr::Literal(Literal::Int(v)), Expr::Field(name)) => {
6774            let flipped = match op {
6775                BinOp::Lt => BinOp::Gt,
6776                BinOp::Gt => BinOp::Lt,
6777                BinOp::Lte => BinOp::Gte,
6778                BinOp::Gte => BinOp::Lte,
6779                other => other, // Eq, Neq are symmetric
6780            };
6781            (name, *v, flipped)
6782        }
6783        _ => return None,
6784    };
6785
6786    let col_idx = columns.iter().position(|c| c == field_name)?;
6787    // Guard: the compiled Int leaf reads the column's 8 bytes as i64.
6788    // Only valid when the column is actually an Int column.
6789    if schema.columns[col_idx].type_id != TypeId::Int {
6790        return None;
6791    }
6792    let byte_offset = layout.fixed_offsets[col_idx]?;
6793    let bitmap_byte = col_idx / 8;
6794    let bitmap_bit = (col_idx % 8) as u8;
6795    let data_offset = 2 + layout.bitmap_size + byte_offset;
6796
6797    Some(CompiledLeaf::Int {
6798        data_offset,
6799        bitmap_byte,
6800        bitmap_bit,
6801        op,
6802        literal: literal_val,
6803    })
6804}
6805
6806/// Build a `Float` leaf from `.field <op> literal` where `.field` is a
6807/// Float column and `literal` is numeric (Float or Int — Int literals are
6808/// promoted to `f64` at compile time so the hot loop only sees one shape).
6809///
6810/// Mission D10: adds the Float fast-path counterpart to `build_int_leaf`.
6811/// Without this, `WHERE .price > 100.0` on a `price: float` column falls
6812/// through `compile_predicate`, forcing the whole query to the generic
6813/// `decode_row → Value::cmp` path which allocates a `Vec<Value>` per row.
6814fn build_float_leaf(
6815    left: &Expr,
6816    op: BinOp,
6817    right: &Expr,
6818    columns: &[String],
6819    layout: &FastLayout,
6820    schema: &Schema,
6821) -> Option<CompiledLeaf> {
6822    // Accept either direction: field-op-literal or literal-op-field.
6823    // When the literal is on the left, flip the operator so the hot-loop
6824    // eval can assume the field is always the LHS.
6825    let (field_name, literal_val, op) = match (left, right) {
6826        (Expr::Field(name), Expr::Literal(Literal::Float(v))) => (name, *v, op),
6827        (Expr::Field(name), Expr::Literal(Literal::Int(v))) => (name, *v as f64, op),
6828        (Expr::Literal(Literal::Float(v)), Expr::Field(name)) => {
6829            let flipped = match op {
6830                BinOp::Lt => BinOp::Gt,
6831                BinOp::Gt => BinOp::Lt,
6832                BinOp::Lte => BinOp::Gte,
6833                BinOp::Gte => BinOp::Lte,
6834                other => other,
6835            };
6836            (name, *v, flipped)
6837        }
6838        (Expr::Literal(Literal::Int(v)), Expr::Field(name)) => {
6839            let flipped = match op {
6840                BinOp::Lt => BinOp::Gt,
6841                BinOp::Gt => BinOp::Lt,
6842                BinOp::Lte => BinOp::Gte,
6843                BinOp::Gte => BinOp::Lte,
6844                other => other,
6845            };
6846            (name, *v as f64, flipped)
6847        }
6848        _ => return None,
6849    };
6850
6851    let col_idx = columns.iter().position(|c| c == field_name)?;
6852    // Symmetric guard to build_int_leaf: only fire on Float columns. If
6853    // the column is Int but the literal was Float, we want the generic
6854    // path (which promotes Int → f64 via Value::cmp) — compiling a
6855    // Float leaf would read the i64 bytes as f64 and produce nonsense.
6856    if schema.columns[col_idx].type_id != TypeId::Float {
6857        return None;
6858    }
6859    let byte_offset = layout.fixed_offsets[col_idx]?;
6860    let bitmap_byte = col_idx / 8;
6861    let bitmap_bit = (col_idx % 8) as u8;
6862    let data_offset = 2 + layout.bitmap_size + byte_offset;
6863
6864    Some(CompiledLeaf::Float {
6865        data_offset,
6866        bitmap_byte,
6867        bitmap_bit,
6868        op,
6869        literal: literal_val,
6870    })
6871}
6872
6873/// Build a `StrEq` leaf from `.field = string_literal` (or reversed).
6874fn build_str_eq_leaf(
6875    left: &Expr,
6876    op: BinOp,
6877    right: &Expr,
6878    columns: &[String],
6879    layout: &FastLayout,
6880    schema: &Schema,
6881) -> Option<CompiledLeaf> {
6882    if op != BinOp::Eq && op != BinOp::Neq {
6883        return None;
6884    }
6885    let (field_name, literal_str) = match (left, right) {
6886        (Expr::Field(name), Expr::Literal(Literal::String(s))) => (name, s.clone()),
6887        (Expr::Literal(Literal::String(s)), Expr::Field(name)) => (name, s.clone()),
6888        _ => return None,
6889    };
6890
6891    let col_idx = columns.iter().position(|c| c == field_name)?;
6892    if schema.columns[col_idx].type_id != TypeId::Str {
6893        return None;
6894    }
6895    let var_idx = layout.var_indices[col_idx]?;
6896    let var_offset_table_start = layout.var_offset_table_start();
6897    let var_data_start = layout.var_data_start();
6898    let bitmap_byte = col_idx / 8;
6899    let bitmap_bit = (col_idx % 8) as u8;
6900    let negate = op == BinOp::Neq;
6901
6902    Some(CompiledLeaf::StrEq {
6903        var_offset_table_start,
6904        var_data_start,
6905        var_idx,
6906        bitmap_byte,
6907        bitmap_bit,
6908        negate,
6909        needle: literal_str.into_bytes(),
6910    })
6911}
6912
6913/// Collect the column indices referenced by a predicate expression.
6914fn predicate_column_indices(expr: &Expr, columns: &[String]) -> Vec<usize> {
6915    let mut indices = Vec::new();
6916    collect_field_indices(expr, columns, &mut indices);
6917    indices.sort_unstable();
6918    indices.dedup();
6919    indices
6920}
6921
6922fn collect_field_indices(expr: &Expr, columns: &[String], out: &mut Vec<usize>) {
6923    match expr {
6924        Expr::Field(name) => {
6925            if let Some(idx) = columns.iter().position(|c| c == name) {
6926                out.push(idx);
6927            }
6928        }
6929        Expr::BinaryOp(left, _, right) => {
6930            collect_field_indices(left, columns, out);
6931            collect_field_indices(right, columns, out);
6932        }
6933        Expr::Coalesce(left, right) => {
6934            collect_field_indices(left, columns, out);
6935            collect_field_indices(right, columns, out);
6936        }
6937        Expr::UnaryOp(_, inner) => {
6938            collect_field_indices(inner, columns, out);
6939        }
6940        Expr::FunctionCall(_, inner) => {
6941            collect_field_indices(inner, columns, out);
6942        }
6943        Expr::ScalarFunc(_, args) => {
6944            for arg in args {
6945                collect_field_indices(arg, columns, out);
6946            }
6947        }
6948        Expr::Cast(inner, _) => {
6949            collect_field_indices(inner, columns, out);
6950        }
6951        Expr::Case { whens, else_expr } => {
6952            for (cond, result) in whens {
6953                collect_field_indices(cond, columns, out);
6954                collect_field_indices(result, columns, out);
6955            }
6956            if let Some(e) = else_expr {
6957                collect_field_indices(e, columns, out);
6958            }
6959        }
6960        Expr::InList { expr, list, .. } => {
6961            collect_field_indices(expr, columns, out);
6962            for item in list {
6963                collect_field_indices(item, columns, out);
6964            }
6965        }
6966        Expr::InSubquery { expr, .. } => {
6967            collect_field_indices(expr, columns, out);
6968        }
6969        _ => {}
6970    }
6971}
6972
6973/// Decode only the specified columns from raw row bytes, filling the rest
6974/// with `Value::Empty`. This avoids heap allocations for String/Bytes
6975/// columns that the predicate doesn't reference.
6976fn decode_selective(
6977    schema: &Schema,
6978    layout: &RowLayout,
6979    data: &[u8],
6980    col_indices: &[usize],
6981) -> Vec<Value> {
6982    let n_cols = schema.columns.len();
6983    let mut values = vec![Value::Empty; n_cols];
6984    for &ci in col_indices {
6985        values[ci] = decode_column(schema, layout, data, ci);
6986    }
6987    values
6988}
6989
6990fn eval_binop(left: &Value, op: BinOp, right: &Value) -> Value {
6991    match op {
6992        BinOp::Eq => Value::Bool(left == right),
6993        BinOp::Neq => Value::Bool(left != right),
6994        BinOp::Lt => Value::Bool(left < right),
6995        BinOp::Gt => Value::Bool(left > right),
6996        BinOp::Lte => Value::Bool(left <= right),
6997        BinOp::Gte => Value::Bool(left >= right),
6998        BinOp::And => match (left, right) {
6999            (Value::Bool(a), Value::Bool(b)) => Value::Bool(*a && *b),
7000            _ => Value::Bool(false),
7001        },
7002        BinOp::Or => match (left, right) {
7003            (Value::Bool(a), Value::Bool(b)) => Value::Bool(*a || *b),
7004            _ => Value::Bool(false),
7005        },
7006        BinOp::Add => match (left, right) {
7007            (Value::Int(a), Value::Int(b)) => Value::Int(a.saturating_add(*b)),
7008            (Value::Float(a), Value::Float(b)) => Value::Float(a + b),
7009            (Value::Int(a), Value::Float(b)) => Value::Float(*a as f64 + b),
7010            (Value::Float(a), Value::Int(b)) => Value::Float(a + *b as f64),
7011            _ => Value::Empty,
7012        },
7013        BinOp::Sub => match (left, right) {
7014            (Value::Int(a), Value::Int(b)) => Value::Int(a.saturating_sub(*b)),
7015            (Value::Float(a), Value::Float(b)) => Value::Float(a - b),
7016            (Value::Int(a), Value::Float(b)) => Value::Float(*a as f64 - b),
7017            (Value::Float(a), Value::Int(b)) => Value::Float(a - *b as f64),
7018            _ => Value::Empty,
7019        },
7020        BinOp::Mul => match (left, right) {
7021            (Value::Int(a), Value::Int(b)) => Value::Int(a.saturating_mul(*b)),
7022            (Value::Float(a), Value::Float(b)) => Value::Float(a * b),
7023            (Value::Int(a), Value::Float(b)) => Value::Float(*a as f64 * b),
7024            (Value::Float(a), Value::Int(b)) => Value::Float(a * *b as f64),
7025            _ => Value::Empty,
7026        },
7027        BinOp::Div => match (left, right) {
7028            (Value::Int(a), Value::Int(b)) if *b != 0 => Value::Int(a / b),
7029            (Value::Float(a), Value::Float(b)) => Value::Float(a / b),
7030            (Value::Int(a), Value::Float(b)) => Value::Float(*a as f64 / b),
7031            (Value::Float(a), Value::Int(b)) => Value::Float(a / *b as f64),
7032            _ => Value::Empty,
7033        },
7034        BinOp::Like => match (left, right) {
7035            (Value::Str(text), Value::Str(pattern)) => Value::Bool(like_match(text, pattern)),
7036            _ => Value::Bool(false),
7037        },
7038    }
7039}
7040
7041/// SQL LIKE pattern match. `%` matches any sequence (including empty),
7042/// `_` matches exactly one character. No escape character for now.
7043fn like_match(text: &str, pattern: &str) -> bool {
7044    let t: Vec<char> = text.chars().collect();
7045    let p: Vec<char> = pattern.chars().collect();
7046    like_dp(&t, &p, 0, 0)
7047}
7048
7049fn like_dp(t: &[char], p: &[char], ti: usize, pi: usize) -> bool {
7050    if pi == p.len() {
7051        return ti == t.len();
7052    }
7053    if p[pi] == '%' {
7054        // '%' can match zero or more characters — try both.
7055        // Skip consecutive '%' to avoid exponential blowup.
7056        let mut pi2 = pi;
7057        while pi2 < p.len() && p[pi2] == '%' {
7058            pi2 += 1;
7059        }
7060        for i in ti..=t.len() {
7061            if like_dp(t, p, i, pi2) {
7062                return true;
7063            }
7064        }
7065        false
7066    } else if ti < t.len() && (p[pi] == '_' || p[pi] == t[ti]) {
7067        like_dp(t, p, ti + 1, pi + 1)
7068    } else {
7069        false
7070    }
7071}
7072
7073#[cfg(test)]
7074mod tests {
7075    use super::*;
7076    use std::sync::atomic::{AtomicU32, Ordering};
7077
7078    static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
7079
7080    fn test_engine() -> Engine {
7081        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
7082        let dir = std::env::temp_dir().join(format!("powdb_exec_{}_{}", std::process::id(), id));
7083        let mut engine = Engine::new(&dir).unwrap();
7084        engine
7085            .execute_powql("type User { required name: str, required email: str, age: int }")
7086            .unwrap();
7087        engine
7088            .execute_powql(r#"insert User { name := "Alice", email := "alice@ex.com", age := 30 }"#)
7089            .unwrap();
7090        engine
7091            .execute_powql(r#"insert User { name := "Bob", email := "bob@ex.com", age := 25 }"#)
7092            .unwrap();
7093        engine
7094            .execute_powql(
7095                r#"insert User { name := "Charlie", email := "charlie@ex.com", age := 35 }"#,
7096            )
7097            .unwrap();
7098        engine
7099    }
7100
7101    #[test]
7102    fn test_scan_all() {
7103        let mut engine = test_engine();
7104        let result = engine.execute_powql("User").unwrap();
7105        match result {
7106            QueryResult::Rows { rows, .. } => assert_eq!(rows.len(), 3),
7107            _ => panic!("expected rows"),
7108        }
7109    }
7110
7111    #[test]
7112    fn test_filter() {
7113        let mut engine = test_engine();
7114        let result = engine.execute_powql("User filter .age > 28").unwrap();
7115        match result {
7116            QueryResult::Rows { rows, .. } => {
7117                assert_eq!(rows.len(), 2); // Alice (30) and Charlie (35)
7118            }
7119            _ => panic!("expected rows"),
7120        }
7121    }
7122
7123    #[test]
7124    fn test_projection() {
7125        let mut engine = test_engine();
7126        let result = engine.execute_powql("User { name }").unwrap();
7127        match result {
7128            QueryResult::Rows { columns, rows } => {
7129                assert_eq!(columns, vec!["name"]);
7130                assert_eq!(rows.len(), 3);
7131            }
7132            _ => panic!("expected rows"),
7133        }
7134    }
7135
7136    #[test]
7137    fn test_insert_and_count() {
7138        let mut engine = test_engine();
7139        let result = engine.execute_powql("count(User)").unwrap();
7140        match result {
7141            QueryResult::Scalar(Value::Int(n)) => assert_eq!(n, 3),
7142            _ => panic!("expected scalar int"),
7143        }
7144    }
7145
7146    #[test]
7147    fn test_update() {
7148        let mut engine = test_engine();
7149        engine
7150            .execute_powql(r#"User filter .name = "Alice" update { age := 31 }"#)
7151            .unwrap();
7152        let result = engine
7153            .execute_powql(r#"User filter .name = "Alice" { name, age }"#)
7154            .unwrap();
7155        match result {
7156            QueryResult::Rows { rows, .. } => {
7157                assert_eq!(rows[0][1], Value::Int(31));
7158            }
7159            _ => panic!("expected rows"),
7160        }
7161    }
7162
7163    #[test]
7164    fn test_delete() {
7165        let mut engine = test_engine();
7166        engine
7167            .execute_powql(r#"User filter .name = "Bob" delete"#)
7168            .unwrap();
7169        let result = engine.execute_powql("count(User)").unwrap();
7170        match result {
7171            QueryResult::Scalar(Value::Int(n)) => assert_eq!(n, 2),
7172            _ => panic!("expected scalar int"),
7173        }
7174    }
7175
7176    #[test]
7177    fn test_order_limit() {
7178        let mut engine = test_engine();
7179        let result = engine
7180            .execute_powql("User order .age desc limit 2 { name, age }")
7181            .unwrap();
7182        match result {
7183            QueryResult::Rows { rows, .. } => {
7184                assert_eq!(rows.len(), 2);
7185                assert_eq!(rows[0][0], Value::Str("Charlie".into())); // age 35
7186                assert_eq!(rows[1][0], Value::Str("Alice".into())); // age 30
7187            }
7188            _ => panic!("expected rows"),
7189        }
7190    }
7191
7192    /// `order` by a non-existent column must surface an error, not panic.
7193    /// Regression for executor.rs:998 / :2560 where the Sort node called
7194    /// `unwrap_or_else(|| panic!(...))` — a malformed ORDER BY would crash
7195    /// the server thread instead of returning an error to the client.
7196    #[test]
7197    fn test_order_by_missing_column_errors() {
7198        let mut engine = test_engine();
7199        let err = engine
7200            .execute_powql("User order .nonexistent desc")
7201            .expect_err("sort on missing column must error, not panic");
7202        assert!(
7203            err.contains("nonexistent"),
7204            "error should name the missing column, got: {err}"
7205        );
7206    }
7207
7208    // ─── LIMIT / OFFSET combined semantics ──────────────────────────────────
7209    //
7210    // SQL/PowQL semantics: offset skips M rows first, then limit takes N rows.
7211    // `limit 3 offset 1` on 5 rows must return rows 1..4 (three rows), not
7212    // `N - M` rows. These regression tests pin the plan-shape ordering that
7213    // previously had Offset wrapping Limit (so Limit capped at N rows and
7214    // Offset then skipped M of those, yielding N - M).
7215
7216    /// 5-row Product fixture with an `id` column we can order on.
7217    fn product_engine() -> Engine {
7218        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
7219        let dir =
7220            std::env::temp_dir().join(format!("powdb_limit_offset_{}_{}", std::process::id(), id));
7221        let mut engine = Engine::new(&dir).unwrap();
7222        engine
7223            .execute_powql("type Product { required id: int, required name: str }")
7224            .unwrap();
7225        for i in 0..5i64 {
7226            let q = format!(r#"insert Product {{ id := {i}, name := "p{i}" }}"#);
7227            engine.execute_powql(&q).unwrap();
7228        }
7229        engine
7230    }
7231
7232    #[test]
7233    fn test_limit_offset_combined() {
7234        // 5 rows, `limit 3 offset 1` → exactly 3 rows, ids [1, 2, 3] when
7235        // ordered by id. We order by id to pin the row identity; without
7236        // an order by, insertion order is implementation-defined.
7237        let mut engine = product_engine();
7238        let result = engine
7239            .execute_powql("Product order .id limit 3 offset 1 { .id }")
7240            .unwrap();
7241        match result {
7242            QueryResult::Rows { rows, .. } => {
7243                assert_eq!(
7244                    rows.len(),
7245                    3,
7246                    "limit 3 offset 1 on 5 rows must return 3 rows"
7247                );
7248                assert_eq!(rows[0][0], Value::Int(1));
7249                assert_eq!(rows[1][0], Value::Int(2));
7250                assert_eq!(rows[2][0], Value::Int(3));
7251            }
7252            _ => panic!("expected rows"),
7253        }
7254
7255        // `limit 2 offset 1` → exactly 2 rows, ids [1, 2].
7256        let result = engine
7257            .execute_powql("Product order .id limit 2 offset 1 { .id }")
7258            .unwrap();
7259        match result {
7260            QueryResult::Rows { rows, .. } => {
7261                assert_eq!(
7262                    rows.len(),
7263                    2,
7264                    "limit 2 offset 1 on 5 rows must return 2 rows"
7265                );
7266                assert_eq!(rows[0][0], Value::Int(1));
7267                assert_eq!(rows[1][0], Value::Int(2));
7268            }
7269            _ => panic!("expected rows"),
7270        }
7271    }
7272
7273    #[test]
7274    fn test_limit_offset_combined_with_order() {
7275        // Same semantics but ordering on a string column. Names are p0..p4,
7276        // so sort order is identical to id order.
7277        let mut engine = product_engine();
7278        let result = engine
7279            .execute_powql("Product order .name limit 3 offset 1 { .name }")
7280            .unwrap();
7281        match result {
7282            QueryResult::Rows { rows, .. } => {
7283                assert_eq!(rows.len(), 3);
7284                assert_eq!(rows[0][0], Value::Str("p1".into()));
7285                assert_eq!(rows[1][0], Value::Str("p2".into()));
7286                assert_eq!(rows[2][0], Value::Str("p3".into()));
7287            }
7288            _ => panic!("expected rows"),
7289        }
7290    }
7291
7292    #[test]
7293    fn test_offset_then_limit_keyword_order() {
7294        // Parser accepts limit/offset in either order — verify the plan
7295        // semantics are identical regardless of keyword order.
7296        let mut engine = product_engine();
7297        let result = engine
7298            .execute_powql("Product order .id offset 1 limit 3 { .id }")
7299            .unwrap();
7300        match result {
7301            QueryResult::Rows { rows, .. } => {
7302                assert_eq!(rows.len(), 3);
7303                assert_eq!(rows[0][0], Value::Int(1));
7304                assert_eq!(rows[1][0], Value::Int(2));
7305                assert_eq!(rows[2][0], Value::Int(3));
7306            }
7307            _ => panic!("expected rows"),
7308        }
7309    }
7310
7311    // ─── Mission A fast-path tests ──────────────────────────────────────────
7312    //
7313    // Fixture: Mission A workload schema — the same User shape used by
7314    // crates/compare. Deterministic generator so expected values are
7315    // computable directly in the test without reimplementing the interpreter.
7316
7317    /// Build a Mission A User table with `n` rows and an index on id.
7318    /// Row i (0-indexed, id = i):
7319    ///   id        = i
7320    ///   name      = format!("user_{i}")
7321    ///   age       = 18 + (i % 60)
7322    ///   status    = ["active","inactive","pending"][i % 3]
7323    ///   email     = format!("user_{i}@example.com")
7324    ///   created_at= 1_700_000_000 + i
7325    fn mission_a_engine(n: i64) -> Engine {
7326        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
7327        let dir =
7328            std::env::temp_dir().join(format!("powdb_mission_a_{}_{}", std::process::id(), id));
7329        let mut engine = Engine::new(&dir).unwrap();
7330        engine
7331            .execute_powql(
7332                "type User { required id: int, required name: str, required age: int, \
7333             required status: str, required email: str, required created_at: int }",
7334            )
7335            .unwrap();
7336        engine.catalog_mut().create_index("User", "id").unwrap();
7337        let statuses = ["active", "inactive", "pending"];
7338        for i in 0..n {
7339            let age = 18 + (i % 60);
7340            let status = statuses[(i as usize) % 3];
7341            let created_at = 1_700_000_000_i64 + i;
7342            let q = format!(
7343                r#"insert User {{ id := {i}, name := "user_{i}", age := {age}, status := "{status}", email := "user_{i}@example.com", created_at := {created_at} }}"#
7344            );
7345            engine.execute_powql(&q).unwrap();
7346        }
7347        engine
7348    }
7349
7350    #[test]
7351    fn test_fastpath_point_lookup_nonindexed() {
7352        // `.email = literal` has no index — must short-circuit via compiled
7353        // predicate on the first match.
7354        let mut engine = mission_a_engine(50);
7355        let result = engine
7356            .execute_powql(r#"User filter .email = "user_17@example.com""#)
7357            .unwrap();
7358        match result {
7359            QueryResult::Rows { rows, .. } => {
7360                assert_eq!(rows.len(), 1);
7361                // id column is position 0
7362                assert_eq!(rows[0][0], Value::Int(17));
7363            }
7364            _ => panic!("expected rows"),
7365        }
7366    }
7367
7368    #[test]
7369    fn test_fastpath_scan_filter_project_top100() {
7370        // Project(Limit(Filter(SeqScan))) — stream, stop at 100.
7371        let mut engine = mission_a_engine(1000);
7372        let result = engine
7373            .execute_powql("User filter .age > 30 limit 100 { .id, .name }")
7374            .unwrap();
7375        match result {
7376            QueryResult::Rows { columns, rows } => {
7377                assert_eq!(columns, vec!["id", "name"]);
7378                assert_eq!(rows.len(), 100);
7379                // All rows must have age > 30 (age = 18 + (id % 60))
7380                // Verify via id: 18 + (id % 60) > 30  <=>  id % 60 > 12
7381                for row in &rows {
7382                    if let Value::Int(id) = row[0] {
7383                        assert!(18 + (id % 60) > 30, "id={id} has age={}", 18 + (id % 60));
7384                    } else {
7385                        panic!("expected int id");
7386                    }
7387                }
7388            }
7389            _ => panic!("expected rows"),
7390        }
7391    }
7392
7393    #[test]
7394    fn test_fastpath_scan_filter_sort_limit10_desc() {
7395        // Project(Limit(Sort(Filter(SeqScan)))) — bounded top-N heap desc.
7396        let mut engine = mission_a_engine(500);
7397        let result = engine
7398            .execute_powql(
7399                "User filter .age > 20 order .created_at desc limit 10 { .id, .created_at }",
7400            )
7401            .unwrap();
7402        match result {
7403            QueryResult::Rows { rows, .. } => {
7404                assert_eq!(rows.len(), 10);
7405                // Must be monotonically non-increasing in created_at.
7406                let keys: Vec<i64> = rows
7407                    .iter()
7408                    .map(|r| {
7409                        if let Value::Int(v) = r[1] {
7410                            v
7411                        } else {
7412                            panic!("expected int");
7413                        }
7414                    })
7415                    .collect();
7416                for w in keys.windows(2) {
7417                    assert!(w[0] >= w[1], "not desc sorted: {keys:?}");
7418                }
7419                // Highest created_at is id=499 (created_at=1_700_000_499),
7420                // age=18+(499%60)=37 which is > 20, so id=499 must be first.
7421                assert_eq!(rows[0][0], Value::Int(499));
7422            }
7423            _ => panic!("expected rows"),
7424        }
7425    }
7426
7427    #[test]
7428    fn test_fastpath_scan_filter_sort_limit10_asc() {
7429        let mut engine = mission_a_engine(500);
7430        let result = engine
7431            .execute_powql("User filter .age > 20 order .created_at limit 10 { .id, .created_at }")
7432            .unwrap();
7433        match result {
7434            QueryResult::Rows { rows, .. } => {
7435                assert_eq!(rows.len(), 10);
7436                let keys: Vec<i64> = rows
7437                    .iter()
7438                    .map(|r| {
7439                        if let Value::Int(v) = r[1] {
7440                            v
7441                        } else {
7442                            panic!("expected int");
7443                        }
7444                    })
7445                    .collect();
7446                for w in keys.windows(2) {
7447                    assert!(w[0] <= w[1], "not asc sorted: {keys:?}");
7448                }
7449            }
7450            _ => panic!("expected rows"),
7451        }
7452    }
7453
7454    #[test]
7455    fn test_fastpath_agg_sum() {
7456        // sum over all rows of the age column. Deterministic expected value.
7457        let n: i64 = 300;
7458        let mut engine = mission_a_engine(n);
7459        let result = engine.execute_powql("sum(User { .age })").unwrap();
7460        let expected: i64 = (0..n).map(|i| 18 + (i % 60)).sum();
7461        match result {
7462            QueryResult::Scalar(Value::Int(v)) => assert_eq!(v, expected),
7463            other => panic!("expected Int, got {other:?}"),
7464        }
7465    }
7466
7467    #[test]
7468    fn test_fastpath_agg_sum_with_filter() {
7469        let n: i64 = 300;
7470        let mut engine = mission_a_engine(n);
7471        let result = engine
7472            .execute_powql("sum(User filter .age > 30 { .age })")
7473            .unwrap();
7474        let expected: i64 = (0..n).map(|i| 18 + (i % 60)).filter(|a| *a > 30).sum();
7475        match result {
7476            QueryResult::Scalar(Value::Int(v)) => assert_eq!(v, expected),
7477            other => panic!("expected Int, got {other:?}"),
7478        }
7479    }
7480
7481    #[test]
7482    fn test_fastpath_agg_avg() {
7483        let n: i64 = 300;
7484        let mut engine = mission_a_engine(n);
7485        let result = engine.execute_powql("avg(User { .age })").unwrap();
7486        let total: f64 = (0..n).map(|i| (18 + (i % 60)) as f64).sum();
7487        let expected = total / n as f64;
7488        match result {
7489            QueryResult::Scalar(Value::Float(v)) => {
7490                assert!((v - expected).abs() < 1e-9, "expected {expected}, got {v}");
7491            }
7492            other => panic!("expected Float, got {other:?}"),
7493        }
7494    }
7495
7496    #[test]
7497    fn test_fastpath_agg_min_max() {
7498        let n: i64 = 300;
7499        let mut engine = mission_a_engine(n);
7500        // age = 18 + (i % 60), so min=18 and max=77 (18+59)
7501        let result_min = engine.execute_powql("min(User { .age })").unwrap();
7502        match result_min {
7503            QueryResult::Scalar(Value::Int(v)) => assert_eq!(v, 18),
7504            other => panic!("expected Int, got {other:?}"),
7505        }
7506        let result_max = engine.execute_powql("max(User { .age })").unwrap();
7507        match result_max {
7508            QueryResult::Scalar(Value::Int(v)) => assert_eq!(v, 77),
7509            other => panic!("expected Int, got {other:?}"),
7510        }
7511    }
7512
7513    #[test]
7514    fn test_fastpath_multi_col_and_filter() {
7515        // AND of int > and string = — both must be compiled into one closure.
7516        let n: i64 = 300;
7517        let mut engine = mission_a_engine(n);
7518        let result = engine
7519            .execute_powql(r#"count(User filter .age > 30 and .status = "active")"#)
7520            .unwrap();
7521        // Expected count via the same deterministic generator.
7522        let statuses = ["active", "inactive", "pending"];
7523        let expected = (0..n)
7524            .filter(|i| {
7525                let age = 18 + (i % 60);
7526                let status = statuses[(*i as usize) % 3];
7527                age > 30 && status == "active"
7528            })
7529            .count() as i64;
7530        match result {
7531            QueryResult::Scalar(Value::Int(v)) => assert_eq!(v, expected),
7532            other => panic!("expected Int, got {other:?}"),
7533        }
7534    }
7535
7536    #[test]
7537    fn test_fastpath_update_by_pk() {
7538        // Update(IndexScan) — single-row mutation via B-tree lookup.
7539        let mut engine = mission_a_engine(50);
7540        let result = engine
7541            .execute_powql("User filter .id = 25 update { age := 99 }")
7542            .unwrap();
7543        match result {
7544            QueryResult::Modified(n) => assert_eq!(n, 1),
7545            _ => panic!("expected Modified"),
7546        }
7547        // Verify the row has the new age.
7548        let lookup = engine
7549            .execute_powql("User filter .id = 25 { .age }")
7550            .unwrap();
7551        match lookup {
7552            QueryResult::Rows { rows, .. } => {
7553                assert_eq!(rows.len(), 1);
7554                assert_eq!(rows[0][0], Value::Int(99));
7555            }
7556            _ => panic!("expected rows"),
7557        }
7558        // Verify no neighbouring rows were touched.
7559        let neighbour = engine
7560            .execute_powql("User filter .id = 24 { .age }")
7561            .unwrap();
7562        if let QueryResult::Rows { rows, .. } = neighbour {
7563            assert_eq!(rows[0][0], Value::Int(42));
7564        }
7565    }
7566
7567    #[test]
7568    fn test_fastpath_update_by_filter_single_pass() {
7569        // Regression test for the O(N*M) bug: update by a range filter must
7570        // not take quadratic time. We can't directly assert timing, but we
7571        // can assert correctness and that the call completes for a
7572        // reasonably-sized table (the old path at N=2000 was ~40M row-eq
7573        // comparisons; the new path is O(N)).
7574        let n: i64 = 2000;
7575        let mut engine = mission_a_engine(n);
7576        let result = engine
7577            .execute_powql("User filter .age > 50 update { age := 5 }")
7578            .unwrap();
7579        let expected = (0..n).filter(|i| 18 + (i % 60) > 50).count() as u64;
7580        match result {
7581            QueryResult::Modified(nn) => assert_eq!(nn, expected),
7582            _ => panic!("expected Modified"),
7583        }
7584        // Every row that matched the filter now has age=5. We verify both
7585        // directions:
7586        //   (a) no rows remain with age > 50 (the filter predicate)
7587        //   (b) count(age = 5) equals the number of rows we updated
7588        // Note: the original generator never produces age=5, so count(age=5)
7589        // is exactly the number of updated rows.
7590        let check_zero = engine
7591            .execute_powql(r#"count(User filter .age > 50)"#)
7592            .unwrap();
7593        match check_zero {
7594            QueryResult::Scalar(Value::Int(v)) => assert_eq!(v, 0, "some rows still have age > 50"),
7595            _ => panic!("expected Int"),
7596        }
7597        let check_five = engine
7598            .execute_powql(r#"count(User filter .age = 5)"#)
7599            .unwrap();
7600        match check_five {
7601            QueryResult::Scalar(Value::Int(v)) => assert_eq!(v as u64, expected),
7602            _ => panic!("expected Int"),
7603        }
7604        // Total row count unchanged.
7605        let total = engine.execute_powql("count(User)").unwrap();
7606        match total {
7607            QueryResult::Scalar(Value::Int(v)) => assert_eq!(v, n),
7608            _ => panic!("expected Int"),
7609        }
7610    }
7611
7612    #[test]
7613    fn test_fastpath_delete_by_filter_single_pass() {
7614        let n: i64 = 2000;
7615        let mut engine = mission_a_engine(n);
7616        let to_delete = (0..n).filter(|i| 18 + (i % 60) > 60).count() as u64;
7617        let result = engine
7618            .execute_powql("User filter .age > 60 delete")
7619            .unwrap();
7620        match result {
7621            QueryResult::Modified(nn) => assert_eq!(nn, to_delete),
7622            _ => panic!("expected Modified"),
7623        }
7624        let count = engine.execute_powql("count(User)").unwrap();
7625        match count {
7626            QueryResult::Scalar(Value::Int(v)) => assert_eq!(v as u64, n as u64 - to_delete),
7627            _ => panic!("expected Int"),
7628        }
7629    }
7630
7631    #[test]
7632    fn test_fastpath_delete_by_pk() {
7633        let mut engine = mission_a_engine(30);
7634        let result = engine.execute_powql("User filter .id = 7 delete").unwrap();
7635        match result {
7636            QueryResult::Modified(n) => assert_eq!(n, 1),
7637            _ => panic!("expected Modified"),
7638        }
7639        // The deleted row must be gone.
7640        let lookup = engine.execute_powql("User filter .id = 7").unwrap();
7641        match lookup {
7642            QueryResult::Rows { rows, .. } => assert_eq!(rows.len(), 0),
7643            _ => panic!("expected rows"),
7644        }
7645        // Neighbours still present.
7646        let other = engine.execute_powql("User filter .id = 8 { .id }").unwrap();
7647        match other {
7648            QueryResult::Rows { rows, .. } => {
7649                assert_eq!(rows.len(), 1);
7650                assert_eq!(rows[0][0], Value::Int(8));
7651            }
7652            _ => panic!("expected rows"),
7653        }
7654    }
7655
7656    #[test]
7657    fn test_fastpath_update_by_filter_matches_generic() {
7658        // Cross-check: running the fast-path update and counting the
7659        // modified rows must agree with counting matching rows via a
7660        // separate query. This catches off-by-one bugs in rid collection.
7661        let n: i64 = 500;
7662        let mut engine = mission_a_engine(n);
7663        let count_before = engine
7664            .execute_powql(r#"count(User filter .status = "active")"#)
7665            .unwrap();
7666        let expected_count = match count_before {
7667            QueryResult::Scalar(Value::Int(v)) => v as u64,
7668            _ => panic!("expected Int"),
7669        };
7670
7671        let upd = engine
7672            .execute_powql(r#"User filter .status = "active" update { age := 42 }"#)
7673            .unwrap();
7674        match upd {
7675            QueryResult::Modified(n) => assert_eq!(n, expected_count),
7676            _ => panic!("expected Modified"),
7677        }
7678
7679        // All "active" rows now have age = 42.
7680        let count_after = engine
7681            .execute_powql(r#"count(User filter .age = 42)"#)
7682            .unwrap();
7683        match count_after {
7684            QueryResult::Scalar(Value::Int(v)) => {
7685                // Some non-active rows may also happen to have age = 42 from
7686                // the original schedule (age = 18 + (i % 60) == 42 when
7687                // i % 60 == 24). So we assert >= expected_count.
7688                assert!(v as u64 >= expected_count);
7689            }
7690            _ => panic!("expected Int"),
7691        }
7692    }
7693
7694    // ── Mission C Phase 5: prepared statements ────────────────────
7695
7696    #[test]
7697    fn test_prepared_insert_reuses_template() {
7698        let mut engine = test_engine();
7699        let prep = engine
7700            .prepare(r#"insert User { name := "seed", email := "seed@ex.com", age := 0 }"#)
7701            .expect("prepare");
7702        // The template has 3 literal slots: name, email, age.
7703        assert_eq!(prep.param_count, 3);
7704
7705        for i in 0..5 {
7706            engine
7707                .execute_prepared(
7708                    &prep,
7709                    &[
7710                        Literal::String(format!("user{i}")),
7711                        Literal::String(format!("u{i}@ex.com")),
7712                        Literal::Int(20 + i as i64),
7713                    ],
7714                )
7715                .expect("execute_prepared");
7716        }
7717
7718        // 3 seeded + 5 prepared inserts = 8 rows.
7719        let count = engine.execute_powql("count(User)").unwrap();
7720        match count {
7721            QueryResult::Scalar(Value::Int(n)) => assert_eq!(n, 8),
7722            _ => panic!("expected scalar"),
7723        }
7724    }
7725
7726    #[test]
7727    fn test_prepared_update_by_pk() {
7728        let mut engine = test_engine();
7729        let prep = engine
7730            .prepare(r#"User filter .name = "seed" update { age := 0 }"#)
7731            .expect("prepare");
7732        // Two slots: filter literal "seed" + assignment literal 0.
7733        assert_eq!(prep.param_count, 2);
7734
7735        engine
7736            .execute_prepared(&prep, &[Literal::String("Alice".into()), Literal::Int(99)])
7737            .expect("execute_prepared");
7738
7739        let result = engine
7740            .execute_powql(r#"User filter .name = "Alice" { age }"#)
7741            .unwrap();
7742        match result {
7743            QueryResult::Rows { rows, .. } => {
7744                assert_eq!(rows[0][0], Value::Int(99));
7745            }
7746            _ => panic!("expected rows"),
7747        }
7748    }
7749
7750    #[test]
7751    fn test_prepared_wrong_arity_errors() {
7752        let mut engine = test_engine();
7753        let prep = engine
7754            .prepare(r#"User filter .age > 0 { name }"#)
7755            .expect("prepare");
7756        assert_eq!(prep.param_count, 1);
7757        let err = engine.execute_prepared(&prep, &[]).unwrap_err();
7758        assert!(err.contains("expects 1 literal"));
7759    }
7760
7761    // ─── Mission E1.2 join executor tests ───────────────────────────────────
7762    //
7763    // Fixture: two-table User + Order schema. User has 3 rows; Order has 4
7764    // rows referencing users 1 and 2 (plus one orphan user_id 99 so we can
7765    // probe LEFT OUTER semantics). Charlie (user 3) has no orders.
7766
7767    fn join_engine() -> Engine {
7768        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
7769        let dir = std::env::temp_dir().join(format!("powdb_join_{}_{}", std::process::id(), id));
7770        let mut engine = Engine::new(&dir).unwrap();
7771        engine
7772            .execute_powql("type User { required id: int, required name: str }")
7773            .unwrap();
7774        engine
7775            .execute_powql(
7776                "type Order { required id: int, required user_id: int, required total: int }",
7777            )
7778            .unwrap();
7779        engine
7780            .execute_powql(r#"insert User { id := 1, name := "Alice" }"#)
7781            .unwrap();
7782        engine
7783            .execute_powql(r#"insert User { id := 2, name := "Bob" }"#)
7784            .unwrap();
7785        engine
7786            .execute_powql(r#"insert User { id := 3, name := "Charlie" }"#)
7787            .unwrap();
7788        engine
7789            .execute_powql(r#"insert Order { id := 10, user_id := 1, total := 100 }"#)
7790            .unwrap();
7791        engine
7792            .execute_powql(r#"insert Order { id := 11, user_id := 1, total := 200 }"#)
7793            .unwrap();
7794        engine
7795            .execute_powql(r#"insert Order { id := 12, user_id := 2, total := 50  }"#)
7796            .unwrap();
7797        engine
7798            .execute_powql(r#"insert Order { id := 13, user_id := 99, total := 999 }"#)
7799            .unwrap();
7800        engine
7801    }
7802
7803    #[test]
7804    fn test_inner_join_matches_rows() {
7805        let mut engine = join_engine();
7806        let result = engine
7807            .execute_powql("User as u join Order as o on u.id = o.user_id")
7808            .unwrap();
7809        match result {
7810            QueryResult::Rows { columns, rows } => {
7811                // 3 matches: Alice has 2 orders, Bob has 1. Charlie + orphan
7812                // are dropped under INNER semantics.
7813                assert_eq!(rows.len(), 3);
7814                // Columns are concatenated alias.field for both sides.
7815                assert!(columns.contains(&"u.id".to_string()));
7816                assert!(columns.contains(&"u.name".to_string()));
7817                assert!(columns.contains(&"o.id".to_string()));
7818                assert!(columns.contains(&"o.user_id".to_string()));
7819                assert!(columns.contains(&"o.total".to_string()));
7820            }
7821            _ => panic!("expected rows"),
7822        }
7823    }
7824
7825    #[test]
7826    fn test_inner_join_with_qualified_projection_and_filter() {
7827        let mut engine = join_engine();
7828        let result = engine
7829            .execute_powql(
7830                "User as u join Order as o on u.id = o.user_id \
7831             filter o.total > 75 { u.name, o.total }",
7832            )
7833            .unwrap();
7834        match result {
7835            QueryResult::Rows { columns, rows } => {
7836                assert_eq!(columns, vec!["u.name", "o.total"]);
7837                // Alice/100, Alice/200 (Bob's 50 filtered out).
7838                assert_eq!(rows.len(), 2);
7839                let names: Vec<_> = rows.iter().map(|r| r[0].clone()).collect();
7840                assert!(names
7841                    .iter()
7842                    .all(|v| matches!(v, Value::Str(s) if s == "Alice")));
7843            }
7844            _ => panic!("expected rows"),
7845        }
7846    }
7847
7848    #[test]
7849    fn test_join_projection_with_aliased_right_table_column() {
7850        // Regression: the TS client reported right-table projections being
7851        // silently dropped. Confirm that `{ u.name, tot: o.total }` emits
7852        // both columns (the right-table one under its explicit alias).
7853        let mut engine = join_engine();
7854        let result = engine
7855            .execute_powql("User as u join Order as o on u.id = o.user_id { u.name, tot: o.total }")
7856            .unwrap();
7857        match result {
7858            QueryResult::Rows { columns, rows } => {
7859                assert_eq!(columns, vec!["u.name", "tot"]);
7860                assert_eq!(rows.len(), 3);
7861                // Every row must have a populated `tot` value (not Empty).
7862                for row in &rows {
7863                    assert!(
7864                        matches!(row[1], Value::Int(_)),
7865                        "tot should be Int, got {:?}",
7866                        row[1]
7867                    );
7868                }
7869            }
7870            _ => panic!("expected rows"),
7871        }
7872    }
7873
7874    #[test]
7875    fn test_match_keyword_rejected_as_invalid_join() {
7876        // `match` is not a join keyword in PowQL — only `join`, `inner join`,
7877        // `left join`, `right join`, and `cross join` are recognised. With
7878        // the parser's EOF check in place, writing `match` produces a clean
7879        // error instead of silently dropping the rest of the query.
7880        let mut engine = join_engine();
7881        let err = engine
7882            .execute_powql("User match Order on u.id = o.user_id { u.name }")
7883            .unwrap_err();
7884        assert!(
7885            err.to_string().to_lowercase().contains("match")
7886                || err.to_string().to_lowercase().contains("trailing")
7887                || err.to_string().to_lowercase().contains("unexpected"),
7888            "expected parse error mentioning trailing/unexpected token, got: {err}"
7889        );
7890    }
7891
7892    #[test]
7893    fn test_left_outer_join_emits_orphan_left_rows() {
7894        let mut engine = join_engine();
7895        let result = engine
7896            .execute_powql("User as u left join Order as o on u.id = o.user_id")
7897            .unwrap();
7898        match result {
7899            QueryResult::Rows { rows, columns } => {
7900                // Alice(2) + Bob(1) + Charlie(padding) = 4 rows.
7901                assert_eq!(rows.len(), 4);
7902                // Find Charlie's row and verify the right-side columns are Empty.
7903                let u_name_idx = columns.iter().position(|c| c == "u.name").unwrap();
7904                let o_total_idx = columns.iter().position(|c| c == "o.total").unwrap();
7905                let charlie = rows
7906                    .iter()
7907                    .find(|r| matches!(&r[u_name_idx], Value::Str(s) if s == "Charlie"))
7908                    .expect("Charlie row present");
7909                assert_eq!(charlie[o_total_idx], Value::Empty);
7910            }
7911            _ => panic!("expected rows"),
7912        }
7913    }
7914
7915    #[test]
7916    fn test_right_outer_join_emits_orphan_right_rows() {
7917        let mut engine = join_engine();
7918        // The orphan order (user_id = 99) has no matching User; RIGHT OUTER
7919        // should still emit it with the left-side (User) columns as Empty.
7920        let result = engine
7921            .execute_powql("User as u right join Order as o on u.id = o.user_id")
7922            .unwrap();
7923        match result {
7924            QueryResult::Rows { rows, columns } => {
7925                // All 4 orders appear (3 matched + 1 orphan).
7926                assert_eq!(rows.len(), 4);
7927                let u_name_idx = columns.iter().position(|c| c == "u.name").unwrap();
7928                let o_total_idx = columns.iter().position(|c| c == "o.total").unwrap();
7929                let orphan = rows
7930                    .iter()
7931                    .find(|r| r[o_total_idx] == Value::Int(999))
7932                    .expect("orphan order row present");
7933                assert_eq!(orphan[u_name_idx], Value::Empty);
7934            }
7935            _ => panic!("expected rows"),
7936        }
7937    }
7938
7939    #[test]
7940    fn test_cross_join_emits_full_product() {
7941        let mut engine = join_engine();
7942        let result = engine
7943            .execute_powql("User as u cross join Order as o")
7944            .unwrap();
7945        match result {
7946            QueryResult::Rows { rows, .. } => {
7947                assert_eq!(rows.len(), 3 * 4);
7948            }
7949            _ => panic!("expected rows"),
7950        }
7951    }
7952
7953    #[test]
7954    fn test_hash_join_handles_swapped_predicate_orientation() {
7955        // `on o.user_id = u.id` should resolve the same as `u.id = o.user_id`
7956        // — exercises the swapped-orientation branch in
7957        // `try_extract_equi_join_keys`.
7958        let mut engine = join_engine();
7959        let result = engine
7960            .execute_powql("User as u join Order as o on o.user_id = u.id { u.name, o.total }")
7961            .unwrap();
7962        match result {
7963            QueryResult::Rows { rows, columns } => {
7964                assert_eq!(columns, vec!["u.name", "o.total"]);
7965                assert_eq!(rows.len(), 3);
7966            }
7967            _ => panic!("expected rows"),
7968        }
7969    }
7970
7971    #[test]
7972    fn test_non_equi_join_falls_back_to_nested_loop() {
7973        // `u.id < o.user_id` isn't an equi-join, so the executor must
7974        // drop into the nested-loop path and still return correct rows.
7975        let mut engine = join_engine();
7976        let result = engine
7977            .execute_powql("User as u join Order as o on u.id < o.user_id")
7978            .unwrap();
7979        match result {
7980            QueryResult::Rows { rows, columns } => {
7981                // Pairs where u.id < o.user_id:
7982                //   User 1 < orders 2,99 = 2 rows (o.user_id=2 twice? no, only one order for user 2)
7983                //   Actually: orders have user_ids [1,1,2,99].
7984                //   User 1 (id=1): 1<1 no, 1<1 no, 1<2 yes, 1<99 yes → 2
7985                //   User 2 (id=2): 2<1 no, 2<1 no, 2<2 no, 2<99 yes → 1
7986                //   User 3 (id=3): 3<1 no, 3<1 no, 3<2 no, 3<99 yes → 1
7987                // Total 4.
7988                assert_eq!(rows.len(), 4);
7989                let u_id_idx = columns.iter().position(|c| c == "u.id").unwrap();
7990                let o_uid_idx = columns.iter().position(|c| c == "o.user_id").unwrap();
7991                for row in &rows {
7992                    match (&row[u_id_idx], &row[o_uid_idx]) {
7993                        (Value::Int(u), Value::Int(o)) => assert!(u < o),
7994                        _ => panic!("expected int columns"),
7995                    }
7996                }
7997            }
7998            _ => panic!("expected rows"),
7999        }
8000    }
8001
8002    #[test]
8003    fn test_hash_join_with_string_key() {
8004        // Exercise the Value::Str hash path — plus verifies Hash impl for
8005        // Value works end to end via FxHashMap.
8006        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
8007        let dir = std::env::temp_dir().join(format!("powdb_strjoin_{}_{}", std::process::id(), id));
8008        let mut engine = Engine::new(&dir).unwrap();
8009        engine
8010            .execute_powql("type A { required code: str, required label: str }")
8011            .unwrap();
8012        engine
8013            .execute_powql("type B { required code: str, required score: int }")
8014            .unwrap();
8015        engine
8016            .execute_powql(r#"insert A { code := "x", label := "X-label" }"#)
8017            .unwrap();
8018        engine
8019            .execute_powql(r#"insert A { code := "y", label := "Y-label" }"#)
8020            .unwrap();
8021        engine
8022            .execute_powql(r#"insert B { code := "x", score := 100 }"#)
8023            .unwrap();
8024        engine
8025            .execute_powql(r#"insert B { code := "y", score := 200 }"#)
8026            .unwrap();
8027        engine
8028            .execute_powql(r#"insert B { code := "z", score := 300 }"#)
8029            .unwrap();
8030
8031        let result = engine
8032            .execute_powql("A as a join B as b on a.code = b.code { a.label, b.score }")
8033            .unwrap();
8034        match result {
8035            QueryResult::Rows { rows, .. } => {
8036                // x→100, y→200. z has no matching A.
8037                assert_eq!(rows.len(), 2);
8038            }
8039            _ => panic!("expected rows"),
8040        }
8041    }
8042
8043    #[test]
8044    fn test_multi_join_chain() {
8045        // Third source — verify left-deep chains compose correctly.
8046        let mut engine = join_engine();
8047        engine
8048            .execute_powql("type Product { required id: int, required name: str }")
8049            .unwrap();
8050        engine
8051            .execute_powql(r#"insert Product { id := 100, name := "Widget" }"#)
8052            .unwrap();
8053        engine
8054            .execute_powql(r#"insert Product { id := 200, name := "Gadget" }"#)
8055            .unwrap();
8056        // Re-create Orders with a product_id column wouldn't work without
8057        // table alter; instead we pick a test that exercises the shape only.
8058        let result = engine
8059            .execute_powql(
8060                "User as u join Order as o on u.id = o.user_id \
8061             cross join Product as p",
8062            )
8063            .unwrap();
8064        match result {
8065            QueryResult::Rows { rows, columns } => {
8066                // 3 inner matches × 2 products = 6 rows.
8067                assert_eq!(rows.len(), 6);
8068                assert!(columns.contains(&"u.name".to_string()));
8069                assert!(columns.contains(&"o.total".to_string()));
8070                assert!(columns.contains(&"p.name".to_string()));
8071            }
8072            _ => panic!("expected rows"),
8073        }
8074    }
8075
8076    // ---- Mission E2a: DISTINCT + IN-list + BETWEEN + LIKE -----------------
8077
8078    #[test]
8079    fn test_distinct_deduplicates_rows() {
8080        let mut engine = test_engine();
8081        // Insert a second Alice to create a duplicate name.
8082        engine
8083            .execute_powql(
8084                r#"insert User { name := "Alice", email := "alice2@ex.com", age := 25 }"#,
8085            )
8086            .unwrap();
8087        let result = engine.execute_powql("User distinct { .name }").unwrap();
8088        match result {
8089            QueryResult::Rows { rows, .. } => {
8090                let names: Vec<&Value> = rows.iter().map(|r| &r[0]).collect();
8091                // 4 rows in table (Alice×2, Bob, Charlie) but 3 distinct names.
8092                assert_eq!(names.len(), 3);
8093                let alice_count = names
8094                    .iter()
8095                    .filter(|v| matches!(v, Value::Str(s) if s == "Alice"))
8096                    .count();
8097                assert_eq!(alice_count, 1);
8098                assert!(names
8099                    .iter()
8100                    .any(|v| matches!(v, Value::Str(s) if s == "Bob")));
8101                assert!(names
8102                    .iter()
8103                    .any(|v| matches!(v, Value::Str(s) if s == "Charlie")));
8104            }
8105            _ => panic!("expected rows"),
8106        }
8107    }
8108
8109    #[test]
8110    fn test_in_list_filter() {
8111        let mut engine = test_engine();
8112        let result = engine
8113            .execute_powql(r#"User filter .name in ("Alice", "Bob") { .name }"#)
8114            .unwrap();
8115        match result {
8116            QueryResult::Rows { rows, .. } => {
8117                assert_eq!(rows.len(), 2);
8118            }
8119            _ => panic!("expected rows"),
8120        }
8121    }
8122
8123    #[test]
8124    fn test_not_in_list_filter() {
8125        let mut engine = test_engine();
8126        let result = engine
8127            .execute_powql(r#"User filter .name not in ("Alice") { .name }"#)
8128            .unwrap();
8129        match result {
8130            QueryResult::Rows { rows, .. } => {
8131                // Bob and Charlie survive.
8132                assert_eq!(rows.len(), 2);
8133            }
8134            _ => panic!("expected rows"),
8135        }
8136    }
8137
8138    #[test]
8139    fn test_between_filter() {
8140        let mut engine = test_engine();
8141        let result = engine
8142            .execute_powql("User filter .age between 25 and 30 { .name, .age }")
8143            .unwrap();
8144        match result {
8145            QueryResult::Rows { rows, .. } => {
8146                // Alice is 30 (inclusive), Bob is 25 (inclusive).
8147                assert_eq!(rows.len(), 2);
8148            }
8149            _ => panic!("expected rows"),
8150        }
8151    }
8152
8153    #[test]
8154    fn test_between_filter_float_column_int_literals() {
8155        // Regression for Value::Ord cross-type bug: BETWEEN on a Float column
8156        // with Int literals previously returned zero rows because Ord fell
8157        // through to TypeId discriminant comparison instead of promoting Int
8158        // to f64. Verifies the fix end-to-end through the query engine.
8159        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
8160        let dir = std::env::temp_dir().join(format!(
8161            "powdb_exec_between_float_{}_{}",
8162            std::process::id(),
8163            id
8164        ));
8165        let mut engine = Engine::new(&dir).unwrap();
8166        engine
8167            .execute_powql("type Product { required name: str, required price: float }")
8168            .unwrap();
8169        engine
8170            .execute_powql(r#"insert Product { name := "Cable",   price := 29.0 }"#)
8171            .unwrap();
8172        engine
8173            .execute_powql(r#"insert Product { name := "Speaker", price := 175.5 }"#)
8174            .unwrap();
8175        engine
8176            .execute_powql(r#"insert Product { name := "Monitor", price := 450.0 }"#)
8177            .unwrap();
8178        engine
8179            .execute_powql(r#"insert Product { name := "Laptop",  price := 1299.0 }"#)
8180            .unwrap();
8181
8182        let result = engine
8183            .execute_powql("Product filter .price between 100 and 500 { .name, .price }")
8184            .unwrap();
8185        match result {
8186            QueryResult::Rows { rows, .. } => {
8187                assert_eq!(
8188                    rows.len(),
8189                    2,
8190                    "expected 2 rows in [100, 500] range, got {}: {:?}",
8191                    rows.len(),
8192                    rows
8193                );
8194                // Sorted by insert order: Speaker (175.5), Monitor (450.0).
8195                let names: Vec<&str> = rows
8196                    .iter()
8197                    .map(|r| match &r[0] {
8198                        Value::Str(s) => s.as_str(),
8199                        _ => panic!("expected string name"),
8200                    })
8201                    .collect();
8202                assert!(names.contains(&"Speaker"));
8203                assert!(names.contains(&"Monitor"));
8204            }
8205            _ => panic!("expected rows"),
8206        }
8207    }
8208
8209    #[test]
8210    fn test_not_between_filter() {
8211        let mut engine = test_engine();
8212        let result = engine
8213            .execute_powql("User filter .age not between 26 and 29 { .name }")
8214            .unwrap();
8215        match result {
8216            QueryResult::Rows { rows, .. } => {
8217                // Alice (30), Bob (25), Charlie (35) all outside [26,29].
8218                assert_eq!(rows.len(), 3);
8219            }
8220            _ => panic!("expected rows"),
8221        }
8222    }
8223
8224    #[test]
8225    fn test_like_prefix_match() {
8226        let mut engine = test_engine();
8227        let result = engine
8228            .execute_powql(r#"User filter .name like "Ali%" { .name }"#)
8229            .unwrap();
8230        match result {
8231            QueryResult::Rows { rows, .. } => {
8232                assert_eq!(rows.len(), 1);
8233                assert!(matches!(&rows[0][0], Value::Str(s) if s == "Alice"));
8234            }
8235            _ => panic!("expected rows"),
8236        }
8237    }
8238
8239    #[test]
8240    fn test_like_wildcard_underscore() {
8241        let mut engine = test_engine();
8242        let result = engine
8243            .execute_powql(r#"User filter .name like "_ob" { .name }"#)
8244            .unwrap();
8245        match result {
8246            QueryResult::Rows { rows, .. } => {
8247                assert_eq!(rows.len(), 1);
8248                assert!(matches!(&rows[0][0], Value::Str(s) if s == "Bob"));
8249            }
8250            _ => panic!("expected rows"),
8251        }
8252    }
8253
8254    #[test]
8255    fn test_not_like_filter() {
8256        let mut engine = test_engine();
8257        let result = engine
8258            .execute_powql(r#"User filter .name not like "A%" { .name }"#)
8259            .unwrap();
8260        match result {
8261            QueryResult::Rows { rows, .. } => {
8262                // Bob and Charlie survive (don't start with A).
8263                assert_eq!(rows.len(), 2);
8264            }
8265            _ => panic!("expected rows"),
8266        }
8267    }
8268
8269    #[test]
8270    fn test_in_list_with_integers() {
8271        let mut engine = test_engine();
8272        let result = engine
8273            .execute_powql("User filter .age in (25, 30) { .name }")
8274            .unwrap();
8275        match result {
8276            QueryResult::Rows { rows, .. } => {
8277                assert_eq!(rows.len(), 2);
8278            }
8279            _ => panic!("expected rows"),
8280        }
8281    }
8282
8283    #[test]
8284    fn test_like_full_match() {
8285        let mut engine = test_engine();
8286        // Exact match (no wildcards).
8287        let result = engine
8288            .execute_powql(r#"User filter .name like "Alice" { .name }"#)
8289            .unwrap();
8290        match result {
8291            QueryResult::Rows { rows, .. } => {
8292                assert_eq!(rows.len(), 1);
8293            }
8294            _ => panic!("expected rows"),
8295        }
8296    }
8297
8298    // ─── Mission E2b: GROUP BY + HAVING ────────────────────────────────────
8299
8300    #[test]
8301    fn test_group_by_count() {
8302        // All 3 users share the same "age bucket" when we group by a
8303        // derived column, but we can at least group by a column with
8304        // distinct values. test_engine has 3 distinct names.
8305        let mut engine = test_engine();
8306        let result = engine
8307            .execute_powql("User group .name { .name, n: count(.name) }")
8308            .unwrap();
8309        match result {
8310            QueryResult::Rows { columns, rows } => {
8311                assert_eq!(columns, vec!["name", "n"]);
8312                assert_eq!(rows.len(), 3); // 3 distinct names
8313                                           // Each group has 1 row.
8314                for row in &rows {
8315                    assert_eq!(row[1], Value::Int(1));
8316                }
8317            }
8318            _ => panic!("expected rows"),
8319        }
8320    }
8321
8322    #[test]
8323    fn test_group_by_sum_avg() {
8324        // Group all rows into one bucket by a constant column.
8325        // We'll use the mission_a_engine with a known shape.
8326        let mut engine = test_engine();
8327        // All 3 users: ages 30, 25, 35 → sum=90, avg=30.0
8328        let result = engine
8329            .execute_powql("User group .email { .email, total_age: sum(.age) }")
8330            .unwrap();
8331        match result {
8332            QueryResult::Rows { rows, .. } => {
8333                // Each email is unique → 3 groups, each with sum of one age.
8334                assert_eq!(rows.len(), 3);
8335            }
8336            _ => panic!("expected rows"),
8337        }
8338    }
8339
8340    #[test]
8341    fn test_group_by_with_filter() {
8342        let mut engine = test_engine();
8343        // Filter first, then group.
8344        let result = engine
8345            .execute_powql("User filter .age >= 30 group .name { .name, n: count(.name) }")
8346            .unwrap();
8347        match result {
8348            QueryResult::Rows { rows, .. } => {
8349                // Alice (30) and Charlie (35) survive filter.
8350                assert_eq!(rows.len(), 2);
8351            }
8352            _ => panic!("expected rows"),
8353        }
8354    }
8355
8356    #[test]
8357    fn test_group_by_having() {
8358        // Use mission_a_engine so we have multiple rows per group.
8359        let mut engine = mission_a_engine(30);
8360        // 30 rows: statuses cycle active/inactive/pending → 10 each.
8361        // Group by status, HAVING count > 5.
8362        let result = engine
8363            .execute_powql(
8364                "User group .status having count(.name) > 5 { .status, n: count(.name) }",
8365            )
8366            .unwrap();
8367        match result {
8368            QueryResult::Rows { columns, rows } => {
8369                assert_eq!(columns, vec!["status", "n"]);
8370                // All 3 groups have 10 rows each, all > 5.
8371                assert_eq!(rows.len(), 3);
8372                for row in &rows {
8373                    assert_eq!(row[1], Value::Int(10));
8374                }
8375            }
8376            _ => panic!("expected rows"),
8377        }
8378    }
8379
8380    #[test]
8381    fn test_group_by_having_filters_groups() {
8382        let mut engine = mission_a_engine(30);
8383        // HAVING count > 100 → no groups survive.
8384        let result = engine
8385            .execute_powql("User group .status having count(.name) > 100 { .status }")
8386            .unwrap();
8387        match result {
8388            QueryResult::Rows { rows, .. } => {
8389                assert_eq!(rows.len(), 0);
8390            }
8391            _ => panic!("expected rows"),
8392        }
8393    }
8394
8395    #[test]
8396    fn test_group_by_having_with_aliased_projection_agg() {
8397        // Regression: TS client found that when the projection duplicates
8398        // the aggregate used by HAVING (with an alias), HAVING silently
8399        // failed to filter. This asserts the dedup path produces correct
8400        // filtering.
8401        let mut engine = mission_a_engine(30);
8402        // 3 statuses, 10 rows each. HAVING >= 11 should exclude all.
8403        let result = engine
8404            .execute_powql(
8405                "User group .status having count(.name) >= 11 { .status, cnt: count(.name) }",
8406            )
8407            .unwrap();
8408        match result {
8409            QueryResult::Rows { rows, .. } => {
8410                assert_eq!(rows.len(), 0, "HAVING >= 11 should filter all groups");
8411            }
8412            _ => panic!("expected rows"),
8413        }
8414        // HAVING >= 10 should include all three.
8415        let result = engine
8416            .execute_powql(
8417                "User group .status having count(.name) >= 10 { .status, cnt: count(.name) }",
8418            )
8419            .unwrap();
8420        match result {
8421            QueryResult::Rows { rows, .. } => {
8422                assert_eq!(rows.len(), 3);
8423                for row in &rows {
8424                    assert_eq!(row[1], Value::Int(10));
8425                }
8426            }
8427            _ => panic!("expected rows"),
8428        }
8429    }
8430
8431    #[test]
8432    fn test_group_by_having_post_projection() {
8433        // Regression: HAVING placed after the projection (`{ ... } having cnt >= N`,
8434        // referencing projection aliases) was silently dropped. This reproduces
8435        // the exact form the TS client used.
8436        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
8437        let dir =
8438            std::env::temp_dir().join(format!("powdb_having_post_{}_{}", std::process::id(), id));
8439        let mut engine = Engine::new(&dir).unwrap();
8440        engine
8441            .execute_powql("type Person { required name: str, required age: int, city: str }")
8442            .unwrap();
8443        for (name, age, city) in [
8444            ("Alice", 30, "NYC"),
8445            ("Bob", 24, "SF"),
8446            ("Carol", 41, "LA"),
8447            ("Dave", 28, "NYC"),
8448            ("Eve", 35, "Austin"),
8449        ] {
8450            engine
8451                .execute_powql(&format!(
8452                    r#"insert Person {{ name := "{name}", age := {age}, city := "{city}" }}"#
8453                ))
8454                .unwrap();
8455        }
8456        let result = engine
8457            .execute_powql("Person group .city { .city, cnt: count(.name) } having cnt >= 2")
8458            .unwrap();
8459        match result {
8460            QueryResult::Rows { rows, .. } => {
8461                assert_eq!(rows.len(), 1, "only NYC has >= 2 people, got: {rows:?}");
8462                assert_eq!(rows[0][0], Value::Str("NYC".into()));
8463                assert_eq!(rows[0][1], Value::Int(2));
8464            }
8465            _ => panic!("expected rows"),
8466        }
8467    }
8468
8469    #[test]
8470    fn test_having_without_group_by_errors() {
8471        let mut engine = test_engine();
8472        let err = engine.execute_powql("User { .name } having count(.name) > 1");
8473        assert!(
8474            err.is_err(),
8475            "HAVING without GROUP BY should be a parse error"
8476        );
8477    }
8478
8479    #[test]
8480    fn test_group_by_having_reproduces_ts_client_case() {
8481        // Exact reproduction of the TS client test that surfaced the bug:
8482        // 5 people across 4 cities, HAVING count >= 2 should keep only NYC.
8483        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
8484        let dir =
8485            std::env::temp_dir().join(format!("powdb_having_ts_{}_{}", std::process::id(), id));
8486        let mut engine = Engine::new(&dir).unwrap();
8487        engine
8488            .execute_powql("type Person { required name: str, required age: int, city: str }")
8489            .unwrap();
8490        for (name, age, city) in [
8491            ("Alice", 30, "NYC"),
8492            ("Bob", 24, "SF"),
8493            ("Carol", 41, "LA"),
8494            ("Dave", 28, "NYC"),
8495            ("Eve", 35, "Austin"),
8496        ] {
8497            engine
8498                .execute_powql(&format!(
8499                    r#"insert Person {{ name := "{name}", age := {age}, city := "{city}" }}"#
8500                ))
8501                .unwrap();
8502        }
8503        let result = engine
8504            .execute_powql(
8505                "Person group .city having count(.name) >= 2 { .city, cnt: count(.name) }",
8506            )
8507            .unwrap();
8508        match result {
8509            QueryResult::Rows { rows, .. } => {
8510                assert_eq!(rows.len(), 1, "only NYC has >= 2 people, got: {rows:?}");
8511                assert_eq!(rows[0][0], Value::Str("NYC".into()));
8512                assert_eq!(rows[0][1], Value::Int(2));
8513            }
8514            _ => panic!("expected rows"),
8515        }
8516    }
8517
8518    #[test]
8519    fn test_group_by_having_filters_some_groups() {
8520        // Skewed distribution — some groups pass HAVING, some don't.
8521        let mut engine = test_engine();
8522        // test_engine has 3 rows, all distinct names. Add duplicates for Alice.
8523        engine
8524            .execute_powql(r#"insert User { name := "Alice", email := "a2@ex.com", age := 31 }"#)
8525            .unwrap();
8526        engine
8527            .execute_powql(r#"insert User { name := "Alice", email := "a3@ex.com", age := 32 }"#)
8528            .unwrap();
8529        // Now: Alice ×3, Bob ×1, Charlie ×1. HAVING count >= 2 → only Alice.
8530        let result = engine
8531            .execute_powql("User group .name having count(.name) >= 2 { .name, cnt: count(.name) }")
8532            .unwrap();
8533        match result {
8534            QueryResult::Rows { rows, .. } => {
8535                assert_eq!(rows.len(), 1);
8536                assert_eq!(rows[0][0], Value::Str("Alice".into()));
8537                assert_eq!(rows[0][1], Value::Int(3));
8538            }
8539            _ => panic!("expected rows"),
8540        }
8541    }
8542
8543    #[test]
8544    fn test_group_by_min_max() {
8545        let mut engine = mission_a_engine(30);
8546        // 30 rows, ages = 18 + (i % 60) for i in 0..30, so ages 18..47.
8547        // Group by status (3 groups of 10 each).
8548        // status=active: i=0,3,6,9,12,15,18,21,24,27 → ages 18,21,24,27,30,33,36,39,42,45
8549        // min=18, max=45
8550        let result = engine.execute_powql(
8551            r#"User filter .status = "active" group .status { .status, lo: min(.age), hi: max(.age) }"#,
8552        ).unwrap();
8553        match result {
8554            QueryResult::Rows { columns, rows } => {
8555                assert_eq!(columns, vec!["status", "lo", "hi"]);
8556                assert_eq!(rows.len(), 1);
8557                assert_eq!(rows[0][0], Value::Str("active".into()));
8558                assert_eq!(rows[0][1], Value::Int(18));
8559                assert_eq!(rows[0][2], Value::Int(45));
8560            }
8561            _ => panic!("expected rows"),
8562        }
8563    }
8564
8565    #[test]
8566    fn test_group_by_avg() {
8567        let mut engine = mission_a_engine(6);
8568        // 6 rows: i=0..5
8569        // active (i=0,3): ages 18,21 → avg=19.5
8570        // inactive (i=1,4): ages 19,22 → avg=20.5
8571        // pending (i=2,5): ages 20,23 → avg=21.5
8572        let result = engine
8573            .execute_powql(
8574                r#"User filter .status = "active" group .status { .status, a: avg(.age) }"#,
8575            )
8576            .unwrap();
8577        match result {
8578            QueryResult::Rows { rows, .. } => {
8579                assert_eq!(rows.len(), 1);
8580                match &rows[0][1] {
8581                    Value::Float(v) => assert!((v - 19.5).abs() < 0.001),
8582                    other => panic!("expected float, got {other:?}"),
8583                }
8584            }
8585            _ => panic!("expected rows"),
8586        }
8587    }
8588
8589    // ─── IS NULL / IS NOT NULL tests ─────────────────────────────────────
8590
8591    #[test]
8592    fn test_is_null_filter() {
8593        let mut engine = test_engine();
8594        engine
8595            .execute_powql(r#"insert User { name := "Diana", email := "diana@ex.com" }"#)
8596            .unwrap();
8597        let result = engine
8598            .execute_powql("User filter .age is null { .name }")
8599            .unwrap();
8600        match result {
8601            QueryResult::Rows { rows, .. } => {
8602                assert_eq!(rows.len(), 1);
8603                assert_eq!(rows[0][0], Value::Str("Diana".into()));
8604            }
8605            _ => panic!("expected rows"),
8606        }
8607    }
8608
8609    #[test]
8610    fn test_is_not_null_filter() {
8611        let mut engine = test_engine();
8612        engine
8613            .execute_powql(r#"insert User { name := "Diana", email := "diana@ex.com" }"#)
8614            .unwrap();
8615        let result = engine
8616            .execute_powql("User filter .age is not null { .name }")
8617            .unwrap();
8618        match result {
8619            QueryResult::Rows { rows, .. } => {
8620                assert_eq!(rows.len(), 3);
8621            }
8622            _ => panic!("expected rows"),
8623        }
8624    }
8625
8626    #[test]
8627    fn test_is_null_count() {
8628        let mut engine = test_engine();
8629        engine
8630            .execute_powql(r#"insert User { name := "Diana", email := "diana@ex.com" }"#)
8631            .unwrap();
8632        let result = engine
8633            .execute_powql("count(User filter .age is null)")
8634            .unwrap();
8635        match result {
8636            QueryResult::Scalar(Value::Int(n)) => assert_eq!(n, 1),
8637            _ => panic!("expected scalar int"),
8638        }
8639    }
8640
8641    #[test]
8642    fn test_is_null_combined_with_and() {
8643        let mut engine = test_engine();
8644        engine
8645            .execute_powql(r#"insert User { name := "Diana", email := "diana@ex.com" }"#)
8646            .unwrap();
8647        engine
8648            .execute_powql(r#"insert User { name := "Eve", email := "eve@ex.com" }"#)
8649            .unwrap();
8650        let result = engine
8651            .execute_powql(r#"User filter .age is null and .name = "Diana" { .name }"#)
8652            .unwrap();
8653        match result {
8654            QueryResult::Rows { rows, .. } => {
8655                assert_eq!(rows.len(), 1);
8656                assert_eq!(rows[0][0], Value::Str("Diana".into()));
8657            }
8658            _ => panic!("expected rows"),
8659        }
8660    }
8661
8662    #[test]
8663    fn test_eq_null_matches_is_null() {
8664        let mut engine = test_engine();
8665        engine
8666            .execute_powql(r#"insert User { name := "Diana", email := "diana@ex.com" }"#)
8667            .unwrap();
8668        let result = engine
8669            .execute_powql("User filter .age = null { .name }")
8670            .unwrap();
8671        match result {
8672            QueryResult::Rows { rows, .. } => {
8673                assert_eq!(rows.len(), 1);
8674                assert_eq!(rows[0][0], Value::Str("Diana".into()));
8675            }
8676            _ => panic!("expected rows"),
8677        }
8678    }
8679
8680    #[test]
8681    fn test_neq_null_matches_is_not_null() {
8682        let mut engine = test_engine();
8683        engine
8684            .execute_powql(r#"insert User { name := "Diana", email := "diana@ex.com" }"#)
8685            .unwrap();
8686        let result = engine
8687            .execute_powql("User filter .age != null { .name }")
8688            .unwrap();
8689        match result {
8690            QueryResult::Rows { rows, .. } => {
8691                assert_eq!(rows.len(), 3);
8692            }
8693            _ => panic!("expected rows"),
8694        }
8695    }
8696
8697    // ─── String function tests ─────────────────────────────────────────────
8698
8699    #[test]
8700    fn test_upper_in_filter() {
8701        let mut engine = test_engine();
8702        let result = engine
8703            .execute_powql(r#"User filter upper(.name) = "ALICE""#)
8704            .unwrap();
8705        match result {
8706            QueryResult::Rows { rows, .. } => {
8707                assert_eq!(rows.len(), 1);
8708                assert_eq!(rows[0][0], Value::Str("Alice".into()));
8709            }
8710            _ => panic!("expected rows"),
8711        }
8712    }
8713
8714    #[test]
8715    fn test_lower_in_projection() {
8716        let mut engine = test_engine();
8717        let result = engine.execute_powql("User { low: lower(.email) }").unwrap();
8718        match result {
8719            QueryResult::Rows { columns, rows } => {
8720                assert_eq!(columns, vec!["low"]);
8721                assert_eq!(rows.len(), 3);
8722                assert_eq!(rows[0][0], Value::Str("alice@ex.com".into()));
8723            }
8724            _ => panic!("expected rows"),
8725        }
8726    }
8727
8728    #[test]
8729    fn test_length_in_projection() {
8730        let mut engine = test_engine();
8731        let result = engine
8732            .execute_powql("User { .name, len: length(.name) }")
8733            .unwrap();
8734        match result {
8735            QueryResult::Rows { columns, rows } => {
8736                assert_eq!(columns, vec!["name", "len"]);
8737                assert_eq!(rows[0][1], Value::Int(5));
8738                assert_eq!(rows[1][1], Value::Int(3));
8739                assert_eq!(rows[2][1], Value::Int(7));
8740            }
8741            _ => panic!("expected rows"),
8742        }
8743    }
8744
8745    #[test]
8746    fn test_substring_in_projection() {
8747        let mut engine = test_engine();
8748        let result = engine
8749            .execute_powql("User { sub: substring(.name, 1, 3) }")
8750            .unwrap();
8751        match result {
8752            QueryResult::Rows { rows, .. } => {
8753                assert_eq!(rows[0][0], Value::Str("Ali".into()));
8754                assert_eq!(rows[1][0], Value::Str("Bob".into()));
8755                assert_eq!(rows[2][0], Value::Str("Cha".into()));
8756            }
8757            _ => panic!("expected rows"),
8758        }
8759    }
8760
8761    #[test]
8762    fn test_concat_in_projection() {
8763        let mut engine = test_engine();
8764        let result = engine
8765            .execute_powql(r#"User { full: concat(.name, " - ", .email) }"#)
8766            .unwrap();
8767        match result {
8768            QueryResult::Rows { rows, .. } => {
8769                assert_eq!(rows[0][0], Value::Str("Alice - alice@ex.com".into()));
8770                assert_eq!(rows[1][0], Value::Str("Bob - bob@ex.com".into()));
8771                assert_eq!(rows[2][0], Value::Str("Charlie - charlie@ex.com".into()));
8772            }
8773            _ => panic!("expected rows"),
8774        }
8775    }
8776
8777    #[test]
8778    fn test_concat_coerces_int() {
8779        let mut engine = test_engine();
8780        let result = engine
8781            .execute_powql(r#"User { info: concat(.name, " age=", .age) }"#)
8782            .unwrap();
8783        match result {
8784            QueryResult::Rows { rows, .. } => {
8785                assert_eq!(rows[0][0], Value::Str("Alice age=30".into()));
8786            }
8787            _ => panic!("expected rows"),
8788        }
8789    }
8790
8791    // ─── CASE WHEN tests ───────────────────────────────────────────────
8792
8793    #[test]
8794    fn test_case_in_projection() {
8795        let mut engine = test_engine();
8796        let result = engine.execute_powql(
8797            r#"User { .name, label: case when .age > 30 then "senior" when .age >= 30 then "exactly30" else "young" end }"#
8798        ).unwrap();
8799        match result {
8800            QueryResult::Rows { columns, rows } => {
8801                assert_eq!(columns, vec!["name", "label"]);
8802                assert_eq!(rows.len(), 3);
8803                for row in &rows {
8804                    let name = &row[0];
8805                    let label = &row[1];
8806                    match name {
8807                        Value::Str(n) if n == "Alice" => {
8808                            assert_eq!(label, &Value::Str("exactly30".into()))
8809                        }
8810                        Value::Str(n) if n == "Bob" => {
8811                            assert_eq!(label, &Value::Str("young".into()))
8812                        }
8813                        Value::Str(n) if n == "Charlie" => {
8814                            assert_eq!(label, &Value::Str("senior".into()))
8815                        }
8816                        _ => panic!("unexpected name: {name:?}"),
8817                    }
8818                }
8819            }
8820            _ => panic!("expected rows"),
8821        }
8822    }
8823
8824    #[test]
8825    fn test_case_in_filter() {
8826        let mut engine = test_engine();
8827        let result = engine
8828            .execute_powql(r#"User filter case when .age > 30 then true else false end"#)
8829            .unwrap();
8830        match result {
8831            QueryResult::Rows { rows, .. } => {
8832                assert_eq!(rows.len(), 1);
8833                assert_eq!(rows[0][0], Value::Str("Charlie".into()));
8834            }
8835            _ => panic!("expected rows"),
8836        }
8837    }
8838
8839    #[test]
8840    fn test_case_without_else_returns_empty() {
8841        let mut engine = test_engine();
8842        let result = engine
8843            .execute_powql(r#"User { .name, label: case when .age > 100 then "old" end }"#)
8844            .unwrap();
8845        match result {
8846            QueryResult::Rows { rows, .. } => {
8847                for row in &rows {
8848                    assert_eq!(row[1], Value::Empty);
8849                }
8850            }
8851            _ => panic!("expected rows"),
8852        }
8853    }
8854
8855    // ─── Mul/Div expression tests (E2f) ───────────────────────────────
8856
8857    #[test]
8858    fn test_mul_in_projection() {
8859        let mut engine = test_engine();
8860        let result = engine
8861            .execute_powql("User { .name, double_age: .age * 2 }")
8862            .unwrap();
8863        match result {
8864            QueryResult::Rows { columns, rows } => {
8865                assert_eq!(columns, vec!["name", "double_age"]);
8866                // Alice age=30 → 60, Bob age=25 → 50, Charlie age=35 → 70
8867                let ages: Vec<_> = rows.iter().map(|r| &r[1]).collect();
8868                assert!(ages.contains(&&Value::Int(60)));
8869                assert!(ages.contains(&&Value::Int(50)));
8870                assert!(ages.contains(&&Value::Int(70)));
8871            }
8872            _ => panic!("expected rows"),
8873        }
8874    }
8875
8876    #[test]
8877    fn test_div_in_filter() {
8878        let mut engine = test_engine();
8879        let result = engine.execute_powql("User filter .age / 10 > 2").unwrap();
8880        match result {
8881            QueryResult::Rows { rows, .. } => {
8882                // 30/10=3>2 ✓, 25/10=2 ✗, 35/10=3>2 ✓
8883                assert_eq!(rows.len(), 2);
8884            }
8885            _ => panic!("expected rows"),
8886        }
8887    }
8888
8889    // ─── Multi-column ORDER BY tests (E2f) ────────────────────────────
8890
8891    #[test]
8892    fn test_multi_order_by() {
8893        let mut engine = test_engine();
8894        // Insert another 30-year-old so we can test tiebreaker
8895        engine
8896            .execute_powql(r#"insert User { name := "Dave", email := "dave@ex.com", age := 30 }"#)
8897            .unwrap();
8898        let result = engine
8899            .execute_powql("User order .age asc, .name asc { .name, .age }")
8900            .unwrap();
8901        match result {
8902            QueryResult::Rows { rows, .. } => {
8903                // Expected: Bob(25), Alice(30), Dave(30), Charlie(35)
8904                assert_eq!(rows[0][0], Value::Str("Bob".into()));
8905                assert_eq!(rows[1][0], Value::Str("Alice".into()));
8906                assert_eq!(rows[2][0], Value::Str("Dave".into()));
8907                assert_eq!(rows[3][0], Value::Str("Charlie".into()));
8908            }
8909            _ => panic!("expected rows"),
8910        }
8911    }
8912
8913    #[test]
8914    fn test_multi_order_mixed_direction() {
8915        let mut engine = test_engine();
8916        engine
8917            .execute_powql(r#"insert User { name := "Dave", email := "dave@ex.com", age := 30 }"#)
8918            .unwrap();
8919        let result = engine
8920            .execute_powql("User order .age asc, .name desc { .name, .age }")
8921            .unwrap();
8922        match result {
8923            QueryResult::Rows { rows, .. } => {
8924                // Expected: Bob(25), Dave(30), Alice(30), Charlie(35)
8925                assert_eq!(rows[0][0], Value::Str("Bob".into()));
8926                assert_eq!(rows[1][0], Value::Str("Dave".into()));
8927                assert_eq!(rows[2][0], Value::Str("Alice".into()));
8928                assert_eq!(rows[3][0], Value::Str("Charlie".into()));
8929            }
8930            _ => panic!("expected rows"),
8931        }
8932    }
8933
8934    // ─── ALTER TABLE / DROP TABLE tests (E2g) ─────────────────────────
8935
8936    #[test]
8937    fn test_alter_add_column() {
8938        let mut engine = test_engine();
8939        let result = engine
8940            .execute_powql("alter User add column status: str")
8941            .unwrap();
8942        match result {
8943            QueryResult::Executed { message } => {
8944                assert!(message.contains("status"));
8945                assert!(message.contains("User"));
8946            }
8947            other => panic!("expected Executed, got {other:?}"),
8948        }
8949        // Verify schema was updated — new inserts can use the new column
8950        engine.execute_powql(r#"insert User { name := "Eve", email := "eve@ex.com", age := 22, status := "active" }"#).unwrap();
8951        let result = engine
8952            .execute_powql(r#"User filter .name = "Eve" { .name, .status }"#)
8953            .unwrap();
8954        match result {
8955            QueryResult::Rows { columns, rows } => {
8956                assert_eq!(columns, vec!["name", "status"]);
8957                assert_eq!(rows.len(), 1);
8958                assert_eq!(rows[0][1], Value::Str("active".into()));
8959            }
8960            other => panic!("expected rows, got {other:?}"),
8961        }
8962    }
8963
8964    #[test]
8965    fn test_alter_add_column_reads_old_rows() {
8966        // Regression: before the catalog rewrite path existed, rows
8967        // inserted before `alter ... add column` were left on disk
8968        // with the pre-alter variable-offset-table layout. A bare
8969        // `Type` scan then walked `decode_row` which read
8970        // `n_var + 1` offsets using the NEW schema and panicked with
8971        // "range end index X out of range for slice of length Y".
8972        //
8973        // This test reproduces that exactly: insert, alter, bare scan.
8974        // Any panic or wrong row count means the rewrite regressed.
8975        let mut engine = test_engine();
8976        engine
8977            .execute_powql("alter User add column country: str")
8978            .unwrap();
8979        // Bare scan: NO filter, so the planner cannot skip old rows.
8980        let result = engine.execute_powql("User").unwrap();
8981        match result {
8982            QueryResult::Rows { columns, rows } => {
8983                assert!(columns.contains(&"country".to_string()));
8984                assert_eq!(rows.len(), 3, "three old rows must still be readable");
8985                let country_idx = columns
8986                    .iter()
8987                    .position(|c| c == "country")
8988                    .expect("country column");
8989                for row in &rows {
8990                    assert_eq!(
8991                        row[country_idx],
8992                        Value::Empty,
8993                        "backfilled column must be Empty"
8994                    );
8995                }
8996            }
8997            other => panic!("expected rows, got {other:?}"),
8998        }
8999    }
9000
9001    #[test]
9002    fn test_alter_add_required_column_fails() {
9003        // Adding a required column to a non-empty table has no
9004        // default value to backfill with, so storing `Empty` would
9005        // silently violate the required invariant. The catalog must
9006        // reject it.
9007        let mut engine = test_engine();
9008        let err = engine
9009            .execute_powql("alter User add column required country: str")
9010            .expect_err("required-column add on non-empty table must fail");
9011        let msg = err.to_string().to_lowercase();
9012        assert!(
9013            msg.contains("required") || msg.contains("backfill"),
9014            "error should mention required/backfill, got: {err}"
9015        );
9016        // And the schema must NOT have silently gained the column.
9017        let result = engine.execute_powql("User").unwrap();
9018        if let QueryResult::Rows { columns, .. } = result {
9019            assert!(
9020                !columns.contains(&"country".to_string()),
9021                "failed alter must not mutate the schema"
9022            );
9023        }
9024    }
9025
9026    #[test]
9027    fn test_alter_add_column_then_update_old_row() {
9028        // Regression-plus: after the rewrite path backfills Empty, an
9029        // UPDATE against an old row's new column must round-trip.
9030        // This exercises encode/decode with the new schema shape on a
9031        // row that was originally written with the old shape.
9032        let mut engine = test_engine();
9033        engine
9034            .execute_powql("alter User add column country: str")
9035            .unwrap();
9036        engine
9037            .execute_powql(r#"User filter .name = "Alice" update { country := "US" }"#)
9038            .unwrap();
9039
9040        let result = engine
9041            .execute_powql(r#"User filter .name = "Alice" { .name, .country }"#)
9042            .unwrap();
9043        match result {
9044            QueryResult::Rows { rows, .. } => {
9045                assert_eq!(rows.len(), 1);
9046                assert_eq!(rows[0][0], Value::Str("Alice".into()));
9047                assert_eq!(rows[0][1], Value::Str("US".into()));
9048            }
9049            other => panic!("expected rows, got {other:?}"),
9050        }
9051
9052        // The other two rows should still decode cleanly with Empty.
9053        let result = engine.execute_powql("User").unwrap();
9054        match result {
9055            QueryResult::Rows { columns, rows } => {
9056                assert_eq!(rows.len(), 3);
9057                let country_idx = columns
9058                    .iter()
9059                    .position(|c| c == "country")
9060                    .expect("country column");
9061                let empties = rows
9062                    .iter()
9063                    .filter(|r| r[country_idx] == Value::Empty)
9064                    .count();
9065                assert_eq!(
9066                    empties, 2,
9067                    "two unchanged old rows must still read as Empty"
9068                );
9069            }
9070            other => panic!("expected rows, got {other:?}"),
9071        }
9072    }
9073
9074    #[test]
9075    fn test_alter_drop_column() {
9076        let mut engine = test_engine();
9077        engine
9078            .execute_powql("alter User drop column email")
9079            .unwrap();
9080        let result = engine.execute_powql("User { .name, .age }").unwrap();
9081        match result {
9082            QueryResult::Rows { columns, rows } => {
9083                assert_eq!(columns, vec!["name", "age"]);
9084                assert_eq!(rows.len(), 3);
9085            }
9086            other => panic!("expected rows, got {other:?}"),
9087        }
9088    }
9089
9090    #[test]
9091    fn test_drop_table() {
9092        let mut engine = test_engine();
9093        let result = engine.execute_powql("drop User").unwrap();
9094        match result {
9095            QueryResult::Executed { message } => {
9096                assert!(message.contains("User"));
9097                assert!(message.contains("dropped"));
9098            }
9099            other => panic!("expected Executed, got {other:?}"),
9100        }
9101        // Querying the dropped table should fail
9102        assert!(engine.execute_powql("User").is_err());
9103    }
9104
9105    #[test]
9106    fn test_drop_nonexistent_table_errors() {
9107        let mut engine = test_engine();
9108        assert!(engine.execute_powql("drop NonExistent").is_err());
9109    }
9110
9111    #[test]
9112    fn test_alter_add_duplicate_column_errors() {
9113        let mut engine = test_engine();
9114        assert!(engine.execute_powql("alter User add name: str").is_err());
9115    }
9116
9117    #[test]
9118    fn test_alter_drop_nonexistent_column_errors() {
9119        let mut engine = test_engine();
9120        assert!(engine
9121            .execute_powql("alter User drop column nonexistent")
9122            .is_err());
9123    }
9124
9125    #[test]
9126    fn test_alter_add_index_creates_index() {
9127        let mut engine = test_engine();
9128        let result = engine.execute_powql("alter User add index .email").unwrap();
9129        match result {
9130            QueryResult::Executed { message } => {
9131                assert!(message.contains("User.email"), "message: {message}");
9132            }
9133            other => panic!("expected Executed, got {other:?}"),
9134        }
9135        // Equality lookup on the indexed column should still return results.
9136        let result = engine
9137            .execute_powql(r#"User filter .email = "alice@ex.com" { .name }"#)
9138            .unwrap();
9139        match result {
9140            QueryResult::Rows { rows, .. } => {
9141                assert_eq!(rows.len(), 1);
9142                assert_eq!(rows[0][0], Value::Str("Alice".into()));
9143            }
9144            other => panic!("expected rows, got {other:?}"),
9145        }
9146    }
9147
9148    #[test]
9149    fn test_parse_rejects_trailing_tokens() {
9150        // Previously `User create_index .email` silently succeeded as
9151        // `User` (ignoring the trailing unknown tokens). Now it's a
9152        // parse error so users know the syntax isn't recognized.
9153        let mut engine = test_engine();
9154        assert!(engine.execute_powql("User create_index .email").is_err());
9155        assert!(engine.execute_powql("User add_column score: int").is_err());
9156        assert!(engine.execute_powql("User drop_column email").is_err());
9157    }
9158
9159    // ─── IN subquery tests (E2h) ─────────────────────────────────────
9160
9161    #[test]
9162    fn test_in_subquery_basic() {
9163        let mut engine = test_engine();
9164        // Create a second table with a subset of user names
9165        engine
9166            .execute_powql("type VIP { required name: str }")
9167            .unwrap();
9168        engine
9169            .execute_powql(r#"insert VIP { name := "Alice" }"#)
9170            .unwrap();
9171        engine
9172            .execute_powql(r#"insert VIP { name := "Charlie" }"#)
9173            .unwrap();
9174
9175        let result = engine
9176            .execute_powql("User filter .name in (VIP { .name }) { .name, .age }")
9177            .unwrap();
9178        match result {
9179            QueryResult::Rows { rows, .. } => {
9180                assert_eq!(rows.len(), 2);
9181                let names: Vec<_> = rows.iter().map(|r| &r[0]).collect();
9182                assert!(names.contains(&&Value::Str("Alice".into())));
9183                assert!(names.contains(&&Value::Str("Charlie".into())));
9184            }
9185            _ => panic!("expected rows"),
9186        }
9187    }
9188
9189    #[test]
9190    fn test_not_in_subquery() {
9191        let mut engine = test_engine();
9192        engine
9193            .execute_powql("type VIP { required name: str }")
9194            .unwrap();
9195        engine
9196            .execute_powql(r#"insert VIP { name := "Alice" }"#)
9197            .unwrap();
9198        engine
9199            .execute_powql(r#"insert VIP { name := "Charlie" }"#)
9200            .unwrap();
9201
9202        let result = engine
9203            .execute_powql("User filter .name not in (VIP { .name }) { .name }")
9204            .unwrap();
9205        match result {
9206            QueryResult::Rows { rows, .. } => {
9207                assert_eq!(rows.len(), 1);
9208                assert_eq!(rows[0][0], Value::Str("Bob".into()));
9209            }
9210            _ => panic!("expected rows"),
9211        }
9212    }
9213
9214    #[test]
9215    fn test_in_subquery_with_filter() {
9216        let mut engine = test_engine();
9217        engine
9218            .execute_powql("type Score { required name: str, required points: int }")
9219            .unwrap();
9220        engine
9221            .execute_powql(r#"insert Score { name := "Alice", points := 100 }"#)
9222            .unwrap();
9223        engine
9224            .execute_powql(r#"insert Score { name := "Bob", points := 50 }"#)
9225            .unwrap();
9226        engine
9227            .execute_powql(r#"insert Score { name := "Charlie", points := 80 }"#)
9228            .unwrap();
9229
9230        // Find users whose names are in the high-scorers list (points > 70)
9231        let result = engine
9232            .execute_powql("User filter .name in (Score filter .points > 70 { .name }) { .name }")
9233            .unwrap();
9234        match result {
9235            QueryResult::Rows { rows, .. } => {
9236                assert_eq!(rows.len(), 2);
9237                let names: Vec<_> = rows.iter().map(|r| &r[0]).collect();
9238                assert!(names.contains(&&Value::Str("Alice".into())));
9239                assert!(names.contains(&&Value::Str("Charlie".into())));
9240            }
9241            _ => panic!("expected rows"),
9242        }
9243    }
9244
9245    // ─── EXISTS subquery tests (uncorrelated) ───────────────────────────
9246
9247    #[test]
9248    fn test_exists_subquery_uncorrelated_true() {
9249        let mut engine = test_engine();
9250        // A side table with at least one row → EXISTS(...) = true, so the
9251        // filter passes every User row through.
9252        engine
9253            .execute_powql("type VIP { required name: str }")
9254            .unwrap();
9255        engine
9256            .execute_powql(r#"insert VIP { name := "Alice" }"#)
9257            .unwrap();
9258
9259        let result = engine
9260            .execute_powql("User filter exists (VIP) { .name }")
9261            .unwrap();
9262        match result {
9263            QueryResult::Rows { rows, .. } => {
9264                assert_eq!(rows.len(), 3, "all users should pass when EXISTS is true");
9265            }
9266            _ => panic!("expected rows"),
9267        }
9268    }
9269
9270    #[test]
9271    fn test_exists_subquery_uncorrelated_false() {
9272        let mut engine = test_engine();
9273        // An empty side table → EXISTS(...) = false, so no User rows pass.
9274        engine
9275            .execute_powql("type VIP { required name: str }")
9276            .unwrap();
9277
9278        let result = engine
9279            .execute_powql("User filter exists (VIP) { .name }")
9280            .unwrap();
9281        match result {
9282            QueryResult::Rows { rows, .. } => {
9283                assert_eq!(rows.len(), 0, "no rows should pass when EXISTS is false");
9284            }
9285            _ => panic!("expected rows"),
9286        }
9287    }
9288
9289    #[test]
9290    fn test_not_exists_subquery() {
9291        let mut engine = test_engine();
9292        // NOT EXISTS over an empty table → true → all rows pass.
9293        engine
9294            .execute_powql("type VIP { required name: str }")
9295            .unwrap();
9296
9297        let result = engine
9298            .execute_powql("User filter not exists (VIP) { .name }")
9299            .unwrap();
9300        match result {
9301            QueryResult::Rows { rows, .. } => {
9302                assert_eq!(rows.len(), 3);
9303            }
9304            _ => panic!("expected rows"),
9305        }
9306
9307        // Now add a row — NOT EXISTS becomes false → no rows pass.
9308        engine
9309            .execute_powql(r#"insert VIP { name := "Alice" }"#)
9310            .unwrap();
9311        let result = engine
9312            .execute_powql("User filter not exists (VIP) { .name }")
9313            .unwrap();
9314        match result {
9315            QueryResult::Rows { rows, .. } => {
9316                assert_eq!(rows.len(), 0);
9317            }
9318            _ => panic!("expected rows"),
9319        }
9320    }
9321
9322    #[test]
9323    fn test_exists_subquery_with_inner_filter() {
9324        let mut engine = test_engine();
9325        // Subquery with its own filter: only rows matching the inner
9326        // predicate count toward EXISTS.
9327        engine
9328            .execute_powql("type Score { required name: str, required points: int }")
9329            .unwrap();
9330        engine
9331            .execute_powql(r#"insert Score { name := "Alice", points := 100 }"#)
9332            .unwrap();
9333
9334        // Inner filter matches → EXISTS true → all users pass.
9335        let result = engine
9336            .execute_powql("User filter exists (Score filter .points > 50) { .name }")
9337            .unwrap();
9338        match result {
9339            QueryResult::Rows { rows, .. } => assert_eq!(rows.len(), 3),
9340            _ => panic!("expected rows"),
9341        }
9342    }
9343
9344    #[test]
9345    fn test_exists_subquery_with_inner_filter_no_match() {
9346        // Fresh engine so the plan cache doesn't collide with the
9347        // `> 50` shape from the sibling test.
9348        let mut engine = test_engine();
9349        engine
9350            .execute_powql("type Score { required name: str, required points: int }")
9351            .unwrap();
9352        engine
9353            .execute_powql(r#"insert Score { name := "Alice", points := 100 }"#)
9354            .unwrap();
9355
9356        // Inner filter matches nothing → EXISTS false → no users pass.
9357        let result = engine
9358            .execute_powql("User filter exists (Score filter .points > 1000) { .name }")
9359            .unwrap();
9360        match result {
9361            QueryResult::Rows { rows, .. } => assert_eq!(rows.len(), 0),
9362            _ => panic!("expected rows"),
9363        }
9364    }
9365
9366    // ─── Materialized view tests ────────────────────────────────────────────
9367
9368    #[test]
9369    fn test_create_materialized_view() {
9370        let mut engine = test_engine();
9371        let result = engine
9372            .execute_powql(r#"materialize OldUsers as User filter .age > 28"#)
9373            .unwrap();
9374        match result {
9375            QueryResult::Executed { message } => {
9376                assert!(message.contains("OldUsers"));
9377            }
9378            _ => panic!("expected Executed"),
9379        }
9380        // Query the view like a table.
9381        let result = engine.execute_powql("OldUsers").unwrap();
9382        match result {
9383            QueryResult::Rows { rows, .. } => {
9384                assert_eq!(rows.len(), 2); // Alice (30) and Charlie (35)
9385            }
9386            _ => panic!("expected rows"),
9387        }
9388    }
9389
9390    #[test]
9391    fn test_view_auto_refresh_on_insert() {
9392        let mut engine = test_engine();
9393        engine
9394            .execute_powql(r#"materialize OldUsers as User filter .age > 28"#)
9395            .unwrap();
9396        // Insert a new qualifying row.
9397        engine
9398            .execute_powql(r#"insert User { name := "Dave", email := "dave@ex.com", age := 40 }"#)
9399            .unwrap();
9400        // The view should auto-refresh and include Dave.
9401        let result = engine.execute_powql("OldUsers").unwrap();
9402        match result {
9403            QueryResult::Rows { rows, .. } => {
9404                assert_eq!(rows.len(), 3); // Alice, Charlie, Dave
9405            }
9406            _ => panic!("expected rows"),
9407        }
9408    }
9409
9410    #[test]
9411    fn test_view_auto_refresh_on_delete() {
9412        let mut engine = test_engine();
9413        engine
9414            .execute_powql(r#"materialize OldUsers as User filter .age > 28"#)
9415            .unwrap();
9416        // Delete Alice (age 30) from the base table.
9417        engine
9418            .execute_powql(r#"User filter .name = "Alice" delete"#)
9419            .unwrap();
9420        // View should auto-refresh: only Charlie remains.
9421        let result = engine.execute_powql("OldUsers").unwrap();
9422        match result {
9423            QueryResult::Rows { rows, .. } => {
9424                assert_eq!(rows.len(), 1);
9425            }
9426            _ => panic!("expected rows"),
9427        }
9428    }
9429
9430    #[test]
9431    fn test_view_auto_refresh_on_update() {
9432        let mut engine = test_engine();
9433        engine
9434            .execute_powql(r#"materialize OldUsers as User filter .age > 28"#)
9435            .unwrap();
9436        // Update Bob's age to make him qualify.
9437        engine
9438            .execute_powql(r#"User filter .name = "Bob" update { age := 50 }"#)
9439            .unwrap();
9440        let result = engine.execute_powql("OldUsers").unwrap();
9441        match result {
9442            QueryResult::Rows { rows, .. } => {
9443                assert_eq!(rows.len(), 3); // Alice, Charlie, Bob
9444            }
9445            _ => panic!("expected rows"),
9446        }
9447    }
9448
9449    #[test]
9450    fn test_explicit_refresh() {
9451        let mut engine = test_engine();
9452        engine
9453            .execute_powql(r#"materialize OldUsers as User filter .age > 28"#)
9454            .unwrap();
9455        engine
9456            .execute_powql(r#"insert User { name := "Eve", email := "eve@ex.com", age := 55 }"#)
9457            .unwrap();
9458        // Explicit refresh.
9459        let result = engine.execute_powql("refresh OldUsers").unwrap();
9460        match result {
9461            QueryResult::Executed { message } => {
9462                assert!(message.contains("refreshed"));
9463            }
9464            _ => panic!("expected Executed"),
9465        }
9466        // Now query — should include Eve.
9467        let result = engine.execute_powql("OldUsers").unwrap();
9468        match result {
9469            QueryResult::Rows { rows, .. } => {
9470                assert_eq!(rows.len(), 3);
9471            }
9472            _ => panic!("expected rows"),
9473        }
9474    }
9475
9476    #[test]
9477    fn test_drop_view() {
9478        let mut engine = test_engine();
9479        engine
9480            .execute_powql(r#"materialize OldUsers as User filter .age > 28"#)
9481            .unwrap();
9482        let result = engine.execute_powql("drop view OldUsers").unwrap();
9483        match result {
9484            QueryResult::Executed { message } => {
9485                assert!(message.contains("dropped"));
9486            }
9487            _ => panic!("expected Executed"),
9488        }
9489        // Querying the dropped view should fail.
9490        let err = engine.execute_powql("OldUsers").unwrap_err();
9491        assert!(err.contains("not found"));
9492    }
9493
9494    #[test]
9495    fn test_view_with_projection() {
9496        let mut engine = test_engine();
9497        engine
9498            .execute_powql(r#"materialize UserNames as User { .name }"#)
9499            .unwrap();
9500        let result = engine.execute_powql("UserNames").unwrap();
9501        match result {
9502            QueryResult::Rows { columns, rows } => {
9503                assert_eq!(columns, vec!["name".to_string()]);
9504                assert_eq!(rows.len(), 3);
9505            }
9506            _ => panic!("expected rows"),
9507        }
9508    }
9509
9510    #[test]
9511    fn test_view_no_stale_reads() {
9512        let mut engine = test_engine();
9513        engine
9514            .execute_powql(r#"materialize AllUsers as User"#)
9515            .unwrap();
9516        // Verify initial state.
9517        let result = engine.execute_powql("AllUsers").unwrap();
9518        match &result {
9519            QueryResult::Rows { rows, .. } => assert_eq!(rows.len(), 3),
9520            _ => panic!("expected rows"),
9521        }
9522        // Insert two more.
9523        engine
9524            .execute_powql(r#"insert User { name := "D", email := "d@ex.com", age := 1 }"#)
9525            .unwrap();
9526        engine
9527            .execute_powql(r#"insert User { name := "E", email := "e@ex.com", age := 2 }"#)
9528            .unwrap();
9529        // First insert marks dirty, second stays dirty. Auto-refresh fires on read.
9530        let result = engine.execute_powql("AllUsers").unwrap();
9531        match result {
9532            QueryResult::Rows { rows, .. } => assert_eq!(rows.len(), 5),
9533            _ => panic!("expected rows"),
9534        }
9535    }
9536
9537    #[test]
9538    fn test_duplicate_view_creation_fails() {
9539        let mut engine = test_engine();
9540        engine.execute_powql(r#"materialize V as User"#).unwrap();
9541        let err = engine
9542            .execute_powql(r#"materialize V as User"#)
9543            .unwrap_err();
9544        assert!(err.contains("already exists"));
9545    }
9546
9547    #[test]
9548    fn test_drop_nonexistent_view_fails() {
9549        let mut engine = test_engine();
9550        let err = engine.execute_powql("drop view NoSuchView").unwrap_err();
9551        assert!(err.contains("not found"));
9552    }
9553
9554    // ── UNION / UNION ALL tests ────────────────────────────────
9555
9556    #[test]
9557    fn test_union_deduplicates() {
9558        let mut engine = test_engine();
9559        engine.execute_powql("type A { name: str }").unwrap();
9560        engine.execute_powql("type B { name: str }").unwrap();
9561        engine
9562            .execute_powql(r#"insert A { name := "alice" }"#)
9563            .unwrap();
9564        engine
9565            .execute_powql(r#"insert A { name := "bob" }"#)
9566            .unwrap();
9567        engine
9568            .execute_powql(r#"insert B { name := "bob" }"#)
9569            .unwrap();
9570        engine
9571            .execute_powql(r#"insert B { name := "carol" }"#)
9572            .unwrap();
9573        let result = engine.execute_powql("A union B").unwrap();
9574        let rows = match result {
9575            QueryResult::Rows { rows, .. } => rows,
9576            _ => panic!(),
9577        };
9578        // alice, bob, carol — bob deduped
9579        assert_eq!(rows.len(), 3);
9580    }
9581
9582    #[test]
9583    fn test_union_all_keeps_duplicates() {
9584        let mut engine = test_engine();
9585        engine.execute_powql("type X { val: int }").unwrap();
9586        engine.execute_powql("type Y { val: int }").unwrap();
9587        engine.execute_powql("insert X { val := 1 }").unwrap();
9588        engine.execute_powql("insert X { val := 2 }").unwrap();
9589        engine.execute_powql("insert Y { val := 2 }").unwrap();
9590        engine.execute_powql("insert Y { val := 3 }").unwrap();
9591        let result = engine.execute_powql("X union all Y").unwrap();
9592        let rows = match result {
9593            QueryResult::Rows { rows, .. } => rows,
9594            _ => panic!(),
9595        };
9596        // 1, 2, 2, 3 — no dedup
9597        assert_eq!(rows.len(), 4);
9598    }
9599
9600    #[test]
9601    fn test_union_with_filters() {
9602        let mut engine = test_engine();
9603        engine
9604            .execute_powql("type Emp { name: str, dept: str }")
9605            .unwrap();
9606        engine
9607            .execute_powql(r#"insert Emp { name := "alice", dept := "eng" }"#)
9608            .unwrap();
9609        engine
9610            .execute_powql(r#"insert Emp { name := "bob", dept := "sales" }"#)
9611            .unwrap();
9612        engine
9613            .execute_powql(r#"insert Emp { name := "carol", dept := "eng" }"#)
9614            .unwrap();
9615        let result = engine
9616            .execute_powql(r#"Emp filter .dept = "eng" union Emp filter .dept = "sales""#)
9617            .unwrap();
9618        let rows = match result {
9619            QueryResult::Rows { rows, .. } => rows,
9620            _ => panic!(),
9621        };
9622        assert_eq!(rows.len(), 3);
9623    }
9624
9625    #[test]
9626    fn test_union_chain_three_tables() {
9627        let mut engine = test_engine();
9628        engine.execute_powql("type T1 { v: int }").unwrap();
9629        engine.execute_powql("type T2 { v: int }").unwrap();
9630        engine.execute_powql("type T3 { v: int }").unwrap();
9631        engine.execute_powql("insert T1 { v := 1 }").unwrap();
9632        engine.execute_powql("insert T2 { v := 2 }").unwrap();
9633        engine.execute_powql("insert T3 { v := 3 }").unwrap();
9634        let result = engine.execute_powql("T1 union T2 union T3").unwrap();
9635        let rows = match result {
9636            QueryResult::Rows { rows, .. } => rows,
9637            _ => panic!(),
9638        };
9639        assert_eq!(rows.len(), 3);
9640    }
9641
9642    #[test]
9643    fn test_union_uses_left_side_columns() {
9644        let mut engine = test_engine();
9645        engine.execute_powql("type L { name: str }").unwrap();
9646        engine.execute_powql("type R { name: str }").unwrap();
9647        engine.execute_powql(r#"insert L { name := "a" }"#).unwrap();
9648        engine.execute_powql(r#"insert R { name := "b" }"#).unwrap();
9649        let result = engine.execute_powql("L union R").unwrap();
9650        match result {
9651            QueryResult::Rows { columns, rows } => {
9652                assert_eq!(columns, vec!["name".to_string()]);
9653                assert_eq!(rows.len(), 2);
9654            }
9655            _ => panic!("expected rows"),
9656        }
9657    }
9658
9659    // ── COUNT DISTINCT tests ───────────────────────────────────
9660
9661    #[test]
9662    fn test_count_distinct_standalone() {
9663        let mut engine = test_engine();
9664        engine.execute_powql("type Color { name: str }").unwrap();
9665        engine
9666            .execute_powql(r#"insert Color { name := "red" }"#)
9667            .unwrap();
9668        engine
9669            .execute_powql(r#"insert Color { name := "blue" }"#)
9670            .unwrap();
9671        engine
9672            .execute_powql(r#"insert Color { name := "red" }"#)
9673            .unwrap();
9674        engine
9675            .execute_powql(r#"insert Color { name := "green" }"#)
9676            .unwrap();
9677        let result = engine
9678            .execute_powql("count(distinct Color { .name })")
9679            .unwrap();
9680        match result {
9681            QueryResult::Scalar(Value::Int(n)) => assert_eq!(n, 3), // red, blue, green
9682            _ => panic!("expected scalar int"),
9683        }
9684    }
9685
9686    #[test]
9687    fn test_count_distinct_in_group_by() {
9688        let mut engine = test_engine();
9689        engine
9690            .execute_powql("type Sale { dept: str, item: str }")
9691            .unwrap();
9692        engine
9693            .execute_powql(r#"insert Sale { dept := "eng", item := "laptop" }"#)
9694            .unwrap();
9695        engine
9696            .execute_powql(r#"insert Sale { dept := "eng", item := "laptop" }"#)
9697            .unwrap();
9698        engine
9699            .execute_powql(r#"insert Sale { dept := "eng", item := "monitor" }"#)
9700            .unwrap();
9701        engine
9702            .execute_powql(r#"insert Sale { dept := "sales", item := "phone" }"#)
9703            .unwrap();
9704        let result = engine
9705            .execute_powql("Sale group .dept { .dept, count(distinct .item) }")
9706            .unwrap();
9707        let rows = match result {
9708            QueryResult::Rows { rows, .. } => rows,
9709            _ => panic!(),
9710        };
9711        // eng: 2 distinct items (laptop, monitor), sales: 1 (phone)
9712        let eng_row = rows
9713            .iter()
9714            .find(|r| r[0] == Value::Str("eng".into()))
9715            .unwrap();
9716        let sales_row = rows
9717            .iter()
9718            .find(|r| r[0] == Value::Str("sales".into()))
9719            .unwrap();
9720        assert_eq!(eng_row[1], Value::Int(2));
9721        assert_eq!(sales_row[1], Value::Int(1));
9722    }
9723
9724    #[test]
9725    fn test_count_distinct_with_filter() {
9726        let mut engine = test_engine();
9727        // Use test_engine which creates User with name, email, age
9728        engine
9729            .execute_powql(r#"insert User { name := "Dave", email := "d@e.com", age := 30 }"#)
9730            .unwrap();
9731        let result = engine
9732            .execute_powql("count(distinct User { .age })")
9733            .unwrap();
9734        match result {
9735            QueryResult::Scalar(Value::Int(n)) => {
9736                // 30(alice), 25(bob), 35(charlie), 30(dave) → 3 distinct
9737                assert_eq!(n, 3);
9738            }
9739            _ => panic!("expected scalar int"),
9740        }
9741    }
9742
9743    // ── UPDATE with expressions tests ──────────────────────────
9744
9745    #[test]
9746    fn test_update_with_arithmetic_expression() {
9747        let mut engine = test_engine();
9748        // Alice starts at age 30
9749        engine
9750            .execute_powql(r#"User filter .name = "Alice" update { age := .age + 5 }"#)
9751            .unwrap();
9752        let result = engine
9753            .execute_powql(r#"User filter .name = "Alice""#)
9754            .unwrap();
9755        let rows = match result {
9756            QueryResult::Rows { rows, .. } => rows,
9757            _ => panic!(),
9758        };
9759        assert_eq!(rows[0][2], Value::Int(35)); // 30 + 5 = 35
9760    }
9761
9762    #[test]
9763    fn test_update_with_multiply_expression() {
9764        let mut engine = test_engine();
9765        // Double everyone's age
9766        engine
9767            .execute_powql("User update { age := .age * 2 }")
9768            .unwrap();
9769        let result = engine.execute_powql("User").unwrap();
9770        let rows = match result {
9771            QueryResult::Rows { rows, .. } => rows,
9772            _ => panic!(),
9773        };
9774        let ages: Vec<i64> = rows
9775            .iter()
9776            .map(|r| match &r[2] {
9777                Value::Int(v) => *v,
9778                _ => 0,
9779            })
9780            .collect();
9781        assert!(ages.contains(&60)); // Alice: 30*2
9782        assert!(ages.contains(&50)); // Bob: 25*2
9783        assert!(ages.contains(&70)); // Charlie: 35*2
9784    }
9785
9786    #[test]
9787    fn test_update_expression_with_filter() {
9788        let mut engine = test_engine();
9789        // Increment age only for people over 28
9790        engine
9791            .execute_powql("User filter .age > 28 update { age := .age + 1 }")
9792            .unwrap();
9793        let result = engine
9794            .execute_powql(r#"User filter .name = "Alice""#)
9795            .unwrap();
9796        let rows = match result {
9797            QueryResult::Rows { rows, .. } => rows,
9798            _ => panic!(),
9799        };
9800        assert_eq!(rows[0][2], Value::Int(31)); // Alice was 30, now 31
9801        let result = engine
9802            .execute_powql(r#"User filter .name = "Bob""#)
9803            .unwrap();
9804        let rows = match result {
9805            QueryResult::Rows { rows, .. } => rows,
9806            _ => panic!(),
9807        };
9808        assert_eq!(rows[0][2], Value::Int(25)); // Bob was 25, unchanged
9809    }
9810
9811    #[test]
9812    fn test_update_literal_still_uses_fast_path() {
9813        // Verify the literal path still works after the refactor
9814        let mut engine = test_engine();
9815        engine
9816            .execute_powql(r#"User filter .name = "Alice" update { age := 99 }"#)
9817            .unwrap();
9818        let result = engine
9819            .execute_powql(r#"User filter .name = "Alice""#)
9820            .unwrap();
9821        let rows = match result {
9822            QueryResult::Rows { rows, .. } => rows,
9823            _ => panic!(),
9824        };
9825        assert_eq!(rows[0][2], Value::Int(99));
9826    }
9827
9828    // ── COUNT(*) in GROUP BY tests ─────────────────────────────
9829
9830    #[test]
9831    fn test_group_by_count_star() {
9832        let mut engine = test_engine();
9833        // test_engine has 3 users: Alice(30), Bob(25), Charlie(35)
9834        // Add another user with same age as Alice
9835        engine
9836            .execute_powql(r#"insert User { name := "Dave", email := "d@e.com", age := 30 }"#)
9837            .unwrap();
9838        let result = engine
9839            .execute_powql("User group .age { .age, count(*) }")
9840            .unwrap();
9841        let rows = match result {
9842            QueryResult::Rows { rows, .. } => rows,
9843            _ => panic!(),
9844        };
9845        let age30 = rows.iter().find(|r| r[0] == Value::Int(30)).unwrap();
9846        assert_eq!(age30[1], Value::Int(2)); // Alice + Dave
9847        let age25 = rows.iter().find(|r| r[0] == Value::Int(25)).unwrap();
9848        assert_eq!(age25[1], Value::Int(1)); // Bob only
9849    }
9850
9851    #[test]
9852    fn test_group_by_count_star_with_having() {
9853        let mut engine = test_engine();
9854        engine
9855            .execute_powql(r#"insert User { name := "Dave", email := "d@e.com", age := 30 }"#)
9856            .unwrap();
9857        let result = engine
9858            .execute_powql("User group .age having count(*) > 1 { .age, count(*) }")
9859            .unwrap();
9860        let rows = match result {
9861            QueryResult::Rows { rows, .. } => rows,
9862            _ => panic!(),
9863        };
9864        assert_eq!(rows.len(), 1);
9865        assert_eq!(rows[0][0], Value::Int(30)); // only age=30 has count > 1
9866    }
9867
9868    // ── Mixed-type arithmetic (Int <-> Float) regression tests ─────────
9869
9870    /// Engine with a Product type containing price:float + stock:int.
9871    /// Exercises mixed numeric promotion in `eval_binop`.
9872    fn product_mix_engine() -> Engine {
9873        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
9874        let dir =
9875            std::env::temp_dir().join(format!("powdb_product_mix_{}_{}", std::process::id(), id));
9876        let mut engine = Engine::new(&dir).unwrap();
9877        engine
9878            .execute_powql(
9879                "type Product { required name: str, required price: float, required stock: int }",
9880            )
9881            .unwrap();
9882        engine
9883            .execute_powql(r#"insert Product { name := "Apple",  price := 1.5, stock := 10 }"#)
9884            .unwrap();
9885        engine
9886            .execute_powql(r#"insert Product { name := "Banana", price := 0.25, stock := 4 }"#)
9887            .unwrap();
9888        engine
9889            .execute_powql(r#"insert Product { name := "Cherry", price := 2.0, stock := 3 }"#)
9890            .unwrap();
9891        engine
9892    }
9893
9894    fn as_float(v: &Value) -> f64 {
9895        match v {
9896            Value::Float(f) => *f,
9897            other => panic!("expected Float, got {other:?}"),
9898        }
9899    }
9900
9901    #[test]
9902    fn test_arith_float_times_int() {
9903        let mut engine = product_mix_engine();
9904        let result = engine
9905            .execute_powql("Product { .name, total: .price * .stock }")
9906            .unwrap();
9907        match result {
9908            QueryResult::Rows { columns, rows } => {
9909                assert_eq!(columns, vec!["name", "total"]);
9910                let mut by_name: std::collections::HashMap<String, f64> =
9911                    std::collections::HashMap::new();
9912                for row in &rows {
9913                    let name = match &row[0] {
9914                        Value::Str(s) => s.clone(),
9915                        _ => panic!(),
9916                    };
9917                    by_name.insert(name, as_float(&row[1]));
9918                }
9919                assert!((by_name["Apple"] - 15.0).abs() < 1e-9);
9920                assert!((by_name["Banana"] - 1.0).abs() < 1e-9);
9921                assert!((by_name["Cherry"] - 6.0).abs() < 1e-9);
9922            }
9923            _ => panic!("expected rows"),
9924        }
9925    }
9926
9927    #[test]
9928    fn test_arith_int_plus_float() {
9929        let mut engine = product_mix_engine();
9930        // stock:int + price:float → should promote to float
9931        let result = engine
9932            .execute_powql("Product { .name, bumped: .stock + .price }")
9933            .unwrap();
9934        match result {
9935            QueryResult::Rows { rows, .. } => {
9936                let mut by_name: std::collections::HashMap<String, f64> =
9937                    std::collections::HashMap::new();
9938                for row in &rows {
9939                    let name = match &row[0] {
9940                        Value::Str(s) => s.clone(),
9941                        _ => panic!(),
9942                    };
9943                    by_name.insert(name, as_float(&row[1]));
9944                }
9945                assert!((by_name["Apple"] - 11.5).abs() < 1e-9);
9946                assert!((by_name["Banana"] - 4.25).abs() < 1e-9);
9947                assert!((by_name["Cherry"] - 5.0).abs() < 1e-9);
9948            }
9949            _ => panic!("expected rows"),
9950        }
9951    }
9952
9953    #[test]
9954    fn test_arith_float_div_int() {
9955        let mut engine = product_mix_engine();
9956        let result = engine
9957            .execute_powql("Product { .name, unit: .price / .stock }")
9958            .unwrap();
9959        match result {
9960            QueryResult::Rows { rows, .. } => {
9961                let mut by_name: std::collections::HashMap<String, f64> =
9962                    std::collections::HashMap::new();
9963                for row in &rows {
9964                    let name = match &row[0] {
9965                        Value::Str(s) => s.clone(),
9966                        _ => panic!(),
9967                    };
9968                    by_name.insert(name, as_float(&row[1]));
9969                }
9970                assert!((by_name["Apple"] - 0.15).abs() < 1e-9);
9971                assert!((by_name["Banana"] - 0.0625).abs() < 1e-9);
9972                assert!((by_name["Cherry"] - (2.0 / 3.0)).abs() < 1e-9);
9973            }
9974            _ => panic!("expected rows"),
9975        }
9976    }
9977
9978    #[test]
9979    fn test_arith_int_minus_float() {
9980        let mut engine = product_mix_engine();
9981        let result = engine
9982            .execute_powql("Product { .name, delta: .stock - .price }")
9983            .unwrap();
9984        match result {
9985            QueryResult::Rows { rows, .. } => {
9986                let mut by_name: std::collections::HashMap<String, f64> =
9987                    std::collections::HashMap::new();
9988                for row in &rows {
9989                    let name = match &row[0] {
9990                        Value::Str(s) => s.clone(),
9991                        _ => panic!(),
9992                    };
9993                    by_name.insert(name, as_float(&row[1]));
9994                }
9995                assert!((by_name["Apple"] - 8.5).abs() < 1e-9);
9996                assert!((by_name["Banana"] - 3.75).abs() < 1e-9);
9997                assert!((by_name["Cherry"] - 1.0).abs() < 1e-9);
9998            }
9999            _ => panic!("expected rows"),
10000        }
10001    }
10002
10003    // Regression: sum() on a Float column must return the actual
10004    // floating-point sum, not Int(0). The old slow-path loops filtered
10005    // out Value::Float and only summed Ints, silently dropping every
10006    // value in a Float column.
10007    #[test]
10008    fn test_sum_float_scalar() {
10009        let mut engine = product_mix_engine();
10010        let result = engine.execute_powql("sum(Product { .price })").unwrap();
10011        match result {
10012            QueryResult::Scalar(v) => {
10013                // 1.5 + 0.25 + 2.0 = 3.75
10014                assert!(
10015                    (as_float(&v) - 3.75).abs() < 1e-9,
10016                    "expected 3.75, got {v:?}"
10017                );
10018            }
10019            _ => panic!("expected scalar result, got {result:?}"),
10020        }
10021    }
10022
10023    // Regression: sum() of a Float column inside a GROUP BY must work
10024    // the same way. compute_group_aggregate had the identical Int-only
10025    // bug as the scalar path.
10026    #[test]
10027    fn test_sum_float_group_by() {
10028        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
10029        let dir =
10030            std::env::temp_dir().join(format!("powdb_sum_float_gb_{}_{}", std::process::id(), id));
10031        let mut engine = Engine::new(&dir).unwrap();
10032        engine
10033            .execute_powql("type Sale { required region: str, required amount: float }")
10034            .unwrap();
10035        engine
10036            .execute_powql(r#"insert Sale { region := "E", amount := 1.5 }"#)
10037            .unwrap();
10038        engine
10039            .execute_powql(r#"insert Sale { region := "E", amount := 2.25 }"#)
10040            .unwrap();
10041        engine
10042            .execute_powql(r#"insert Sale { region := "W", amount := 4.0 }"#)
10043            .unwrap();
10044        engine
10045            .execute_powql(r#"insert Sale { region := "W", amount := 0.5 }"#)
10046            .unwrap();
10047
10048        let result = engine
10049            .execute_powql("Sale group .region { .region, total: sum(.amount) }")
10050            .unwrap();
10051        match result {
10052            QueryResult::Rows { columns, rows } => {
10053                assert_eq!(columns, vec!["region", "total"]);
10054                let mut by_region: std::collections::HashMap<String, f64> =
10055                    std::collections::HashMap::new();
10056                for row in &rows {
10057                    let region = match &row[0] {
10058                        Value::Str(s) => s.clone(),
10059                        _ => panic!(),
10060                    };
10061                    by_region.insert(region, as_float(&row[1]));
10062                }
10063                assert!(
10064                    (by_region["E"] - 3.75).abs() < 1e-9,
10065                    "E: {:?}",
10066                    by_region.get("E")
10067                );
10068                assert!(
10069                    (by_region["W"] - 4.5).abs() < 1e-9,
10070                    "W: {:?}",
10071                    by_region.get("W")
10072                );
10073            }
10074            _ => panic!("expected rows, got {result:?}"),
10075        }
10076    }
10077
10078    // ─── Mission D10: Float fast-path parity ─────────────────────────────
10079    //
10080    // Prior to D10, three hot paths in the executor bailed on Float columns:
10081    //   1. `agg_single_col_fast` — sum/avg/min/max/count fell through to the
10082    //      generic row-decoding path (allocates Vec<Value> per row).
10083    //   2. `project_filter_sort_limit_fast` — top-N by Float column fell
10084    //      through the generic sort path.
10085    //   3. `compile_predicate` / `build_int_leaf` — WHERE on Float columns
10086    //      couldn't compile, so the whole filter walked Value::cmp.
10087    //
10088    // These tests exercise each Float fast path end-to-end, including NaN
10089    // handling via `total_cmp` (which matches `Value::Ord` so semantics are
10090    // identical between fast-path and generic-path reads).
10091
10092    /// Engine with a Price table: price:float, qty:int. Eight rows with a
10093    /// deliberate spread of values, a NaN, a negative, -0.0, and a null.
10094    /// The null exercises the bitmap-skip branch; NaN and -0.0 exercise
10095    /// the `total_cmp` invariant.
10096    fn float_fast_engine() -> Engine {
10097        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
10098        let dir =
10099            std::env::temp_dir().join(format!("powdb_float_fast_{}_{}", std::process::id(), id));
10100        let mut engine = Engine::new(&dir).unwrap();
10101        engine
10102            .execute_powql("type Price { required name: str, price: float, required qty: int }")
10103            .unwrap();
10104        // Insertion order deliberately scrambled so top-N doesn't trivially
10105        // match insertion order.
10106        let rows = [
10107            ("a", "price := 1.5", "qty := 1"),
10108            ("b", "price := 0.25", "qty := 2"),
10109            ("c", "price := 2.0", "qty := 3"),
10110            ("d", "price := -3.5", "qty := 4"),
10111            ("e", "price := 10.0", "qty := 5"),
10112            ("f", "price := 0.5", "qty := 6"),
10113            ("g", "price := 100.0", "qty := 7"),
10114            ("h", "price := -0.0", "qty := 8"),
10115        ];
10116        for (name, price, qty) in rows {
10117            engine
10118                .execute_powql(&format!(
10119                    r#"insert Price {{ name := "{name}", {price}, {qty} }}"#
10120                ))
10121                .unwrap();
10122        }
10123        engine
10124    }
10125
10126    #[test]
10127    fn test_d10_agg_sum_float_fast_path() {
10128        let mut engine = float_fast_engine();
10129        let result = engine.execute_powql("sum(Price { .price })").unwrap();
10130        // 1.5 + 0.25 + 2.0 + -3.5 + 10.0 + 0.5 + 100.0 + -0.0 = 110.75
10131        match result {
10132            QueryResult::Scalar(v) => {
10133                assert!((as_float(&v) - 110.75).abs() < 1e-9, "got {v:?}");
10134            }
10135            _ => panic!("expected scalar, got {result:?}"),
10136        }
10137    }
10138
10139    #[test]
10140    fn test_d10_agg_avg_float_fast_path() {
10141        let mut engine = float_fast_engine();
10142        let result = engine.execute_powql("avg(Price { .price })").unwrap();
10143        // 110.75 / 8 = 13.84375
10144        match result {
10145            QueryResult::Scalar(v) => {
10146                assert!((as_float(&v) - 13.84375).abs() < 1e-9, "got {v:?}");
10147            }
10148            _ => panic!("expected scalar, got {result:?}"),
10149        }
10150    }
10151
10152    #[test]
10153    fn test_d10_agg_min_float_fast_path() {
10154        let mut engine = float_fast_engine();
10155        let result = engine.execute_powql("min(Price { .price })").unwrap();
10156        match result {
10157            QueryResult::Scalar(v) => {
10158                assert!((as_float(&v) - (-3.5)).abs() < 1e-9, "got {v:?}");
10159            }
10160            _ => panic!("expected scalar, got {result:?}"),
10161        }
10162    }
10163
10164    #[test]
10165    fn test_d10_agg_max_float_fast_path() {
10166        let mut engine = float_fast_engine();
10167        let result = engine.execute_powql("max(Price { .price })").unwrap();
10168        match result {
10169            QueryResult::Scalar(v) => {
10170                assert!((as_float(&v) - 100.0).abs() < 1e-9, "got {v:?}");
10171            }
10172            _ => panic!("expected scalar, got {result:?}"),
10173        }
10174    }
10175
10176    #[test]
10177    fn test_d10_agg_count_distinct_float_fast_path() {
10178        let mut engine = float_fast_engine();
10179        let result = engine
10180            .execute_powql("count(distinct Price { .price })")
10181            .unwrap();
10182        // All 8 prices are distinct (+0.0 isn't present; -0.0 is, and
10183        // distinct from every other value). Hash via to_bits so -0.0 and
10184        // +0.0 would count separately — matches Value::Hash.
10185        match result {
10186            QueryResult::Scalar(Value::Int(n)) => assert_eq!(n, 8, "got {n}"),
10187            _ => panic!("expected scalar int, got {result:?}"),
10188        }
10189    }
10190
10191    #[test]
10192    fn test_d10_agg_float_with_compiled_where() {
10193        // Exercises `build_float_leaf` — WHERE .price > 1.0 must compile,
10194        // and the Float fast path must use it to short-circuit rows.
10195        let mut engine = float_fast_engine();
10196        let result = engine
10197            .execute_powql("sum(Price filter .price > 1.0 { .price })")
10198            .unwrap();
10199        // Rows > 1.0: 1.5, 2.0, 10.0, 100.0 → sum = 113.5
10200        match result {
10201            QueryResult::Scalar(v) => {
10202                assert!((as_float(&v) - 113.5).abs() < 1e-9, "got {v:?}");
10203            }
10204            _ => panic!("expected scalar, got {result:?}"),
10205        }
10206    }
10207
10208    #[test]
10209    fn test_d10_agg_float_with_compiled_where_int_literal() {
10210        // Novel cross-type: WHERE .price > 1 (Int literal on Float column)
10211        // must still compile via build_float_leaf — the Int literal is
10212        // promoted to f64 at compile time so the hot loop only sees f64.
10213        let mut engine = float_fast_engine();
10214        let result = engine
10215            .execute_powql("sum(Price filter .price > 1 { .price })")
10216            .unwrap();
10217        match result {
10218            QueryResult::Scalar(v) => {
10219                assert!((as_float(&v) - 113.5).abs() < 1e-9, "got {v:?}");
10220            }
10221            _ => panic!("expected scalar, got {result:?}"),
10222        }
10223    }
10224
10225    #[test]
10226    fn test_d10_agg_float_with_reversed_literal() {
10227        // `100.0 > .price` (literal on LHS) must also compile. The
10228        // build_float_leaf flips the operator so the field is always LHS.
10229        let mut engine = float_fast_engine();
10230        let result = engine
10231            .execute_powql("count(Price filter 1.0 < .price { .price })")
10232            .unwrap();
10233        // Rows where 1.0 < .price: 1.5, 2.0, 10.0, 100.0 → count = 4
10234        match result {
10235            QueryResult::Scalar(Value::Int(n)) => assert_eq!(n, 4, "got {n}"),
10236            _ => panic!("expected scalar int, got {result:?}"),
10237        }
10238    }
10239
10240    #[test]
10241    fn test_d10_sort_float_desc_limit_fast_path() {
10242        // Top-3 by price descending — exercises the Float branch of
10243        // project_filter_sort_limit_fast with the sortable-u64 transform.
10244        let mut engine = float_fast_engine();
10245        let result = engine
10246            .execute_powql("Price order .price desc limit 3 { .name, .price }")
10247            .unwrap();
10248        match result {
10249            QueryResult::Rows { columns, rows } => {
10250                assert_eq!(columns, vec!["name", "price"]);
10251                assert_eq!(rows.len(), 3);
10252                assert_eq!(rows[0][0], Value::Str("g".into())); // 100.0
10253                assert!((as_float(&rows[0][1]) - 100.0).abs() < 1e-9);
10254                assert_eq!(rows[1][0], Value::Str("e".into())); // 10.0
10255                assert!((as_float(&rows[1][1]) - 10.0).abs() < 1e-9);
10256                assert_eq!(rows[2][0], Value::Str("c".into())); // 2.0
10257                assert!((as_float(&rows[2][1]) - 2.0).abs() < 1e-9);
10258            }
10259            _ => panic!("expected rows, got {result:?}"),
10260        }
10261    }
10262
10263    #[test]
10264    fn test_d10_sort_float_asc_limit_fast_path() {
10265        // Bottom-3 by price — negative and -0.0 must order correctly.
10266        let mut engine = float_fast_engine();
10267        let result = engine
10268            .execute_powql("Price order .price limit 3 { .name, .price }")
10269            .unwrap();
10270        match result {
10271            QueryResult::Rows { rows, .. } => {
10272                assert_eq!(rows.len(), 3);
10273                assert_eq!(rows[0][0], Value::Str("d".into())); // -3.5
10274                                                                // -0.0 must come before +0.25 under total_cmp ordering.
10275                assert_eq!(rows[1][0], Value::Str("h".into())); // -0.0
10276                assert_eq!(rows[2][0], Value::Str("b".into())); // 0.25
10277            }
10278            _ => panic!("expected rows, got {result:?}"),
10279        }
10280    }
10281
10282    #[test]
10283    fn test_d10_sort_float_with_compiled_filter() {
10284        // Filter + sort + limit all on Float column — every fast path
10285        // fires on the same query.
10286        let mut engine = float_fast_engine();
10287        let result = engine
10288            .execute_powql("Price filter .price > 0.0 order .price desc limit 2 { .name }")
10289            .unwrap();
10290        match result {
10291            QueryResult::Rows { rows, .. } => {
10292                assert_eq!(rows.len(), 2);
10293                assert_eq!(rows[0][0], Value::Str("g".into())); // 100.0
10294                assert_eq!(rows[1][0], Value::Str("e".into())); // 10.0
10295            }
10296            _ => panic!("expected rows, got {result:?}"),
10297        }
10298    }
10299
10300    #[test]
10301    fn test_f64_sortable_transform_monotonic() {
10302        // The sortable-u64 transform must preserve total_cmp ordering.
10303        // Regression guard against accidentally breaking the clever
10304        // sign-flip trick in `f64_bits_to_sortable_u64`.
10305        let samples: [f64; 11] = [
10306            f64::NEG_INFINITY,
10307            -1e100,
10308            -1.0,
10309            -f64::MIN_POSITIVE,
10310            -0.0,
10311            0.0,
10312            f64::MIN_POSITIVE,
10313            1.0,
10314            1e100,
10315            f64::INFINITY,
10316            f64::NAN, // total_cmp says NaN > +∞
10317        ];
10318        let mut sorted = samples;
10319        sorted.sort_by(|a, b| a.total_cmp(b));
10320
10321        let as_sortable: Vec<u64> = sorted
10322            .iter()
10323            .map(|f| f64_bits_to_sortable_u64(f.to_bits()))
10324            .collect();
10325
10326        // Each u64 must be strictly greater than its predecessor, because
10327        // `total_cmp` places every sample at a distinct total-order slot.
10328        for pair in as_sortable.windows(2) {
10329            assert!(
10330                pair[0] < pair[1],
10331                "sortable u64 not monotonic: {:#x} >= {:#x}",
10332                pair[0],
10333                pair[1]
10334            );
10335        }
10336    }
10337
10338    // ─── EXPLAIN tests ─────────────────────────────────────────────────
10339
10340    #[test]
10341    fn test_explain_simple_scan() {
10342        let mut engine = test_engine();
10343        let result = engine.execute_powql("explain User").unwrap();
10344        match result {
10345            QueryResult::Rows { columns, rows } => {
10346                assert_eq!(columns, vec!["plan"]);
10347                assert!(!rows.is_empty());
10348                assert!(matches!(&rows[0][0], Value::Str(s) if s.contains("SeqScan")));
10349            }
10350            _ => panic!("expected rows"),
10351        }
10352    }
10353
10354    #[test]
10355    fn test_explain_filter() {
10356        let mut engine = test_engine();
10357        let result = engine
10358            .execute_powql("explain User filter .age > 30")
10359            .unwrap();
10360        match result {
10361            QueryResult::Rows { rows, .. } => {
10362                let plan_text: String = rows
10363                    .iter()
10364                    .map(|r| match &r[0] {
10365                        Value::Str(s) => s.as_str(),
10366                        _ => "",
10367                    })
10368                    .collect::<Vec<_>>()
10369                    .join("\n");
10370                assert!(
10371                    plan_text.contains("Filter"),
10372                    "plan should show Filter(SeqScan) after lowering unindexed RangeScan"
10373                );
10374            }
10375            _ => panic!("expected rows"),
10376        }
10377    }
10378
10379    #[test]
10380    fn test_explain_does_not_execute() {
10381        let mut engine = test_engine();
10382        // EXPLAIN should NOT actually insert a row.
10383        let result = engine
10384            .execute_powql(r#"explain insert User { name := "Zara", age := 99 }"#)
10385            .unwrap();
10386        match result {
10387            QueryResult::Rows { rows, .. } => {
10388                let plan_text: String = rows
10389                    .iter()
10390                    .map(|r| match &r[0] {
10391                        Value::Str(s) => s.as_str(),
10392                        _ => "",
10393                    })
10394                    .collect::<Vec<_>>()
10395                    .join("\n");
10396                assert!(plan_text.contains("Insert"));
10397            }
10398            _ => panic!("expected rows"),
10399        }
10400        // Verify no row was actually inserted.
10401        let result = engine.execute_powql("User { .name }").unwrap();
10402        match result {
10403            QueryResult::Rows { rows, .. } => {
10404                assert_eq!(rows.len(), 3, "should still have original 3 users");
10405            }
10406            _ => panic!("expected rows"),
10407        }
10408    }
10409
10410    // ─── Correlated subquery tests ──────────────────────────────────────
10411
10412    #[test]
10413    fn test_correlated_in_subquery() {
10414        let mut engine = test_engine();
10415        // Create an orders table with user_name to correlate on.
10416        engine
10417            .execute_powql("type UserOrder { required user_name: str, required total: int }")
10418            .unwrap();
10419        engine
10420            .execute_powql(r#"insert UserOrder { user_name := "Alice", total := 100 }"#)
10421            .unwrap();
10422        engine
10423            .execute_powql(r#"insert UserOrder { user_name := "Alice", total := 200 }"#)
10424            .unwrap();
10425        engine
10426            .execute_powql(r#"insert UserOrder { user_name := "Bob", total := 50 }"#)
10427            .unwrap();
10428
10429        // Correlated: for each User row, find orders where user_name = outer .name
10430        // The subquery references .name which is a User column, not a UserOrder column.
10431        let result = engine.execute_powql(
10432            "User filter .name in (UserOrder filter .user_name = .name { .user_name }) { .name }"
10433        ).unwrap();
10434        match result {
10435            QueryResult::Rows { rows, .. } => {
10436                assert_eq!(rows.len(), 2, "Alice and Bob have orders");
10437                let names: Vec<_> = rows.iter().map(|r| &r[0]).collect();
10438                assert!(names.contains(&&Value::Str("Alice".into())));
10439                assert!(names.contains(&&Value::Str("Bob".into())));
10440            }
10441            _ => panic!("expected rows"),
10442        }
10443    }
10444
10445    #[test]
10446    fn test_correlated_exists_subquery() {
10447        let mut engine = test_engine();
10448        engine
10449            .execute_powql("type UserOrder { required user_name: str, required total: int }")
10450            .unwrap();
10451        engine
10452            .execute_powql(r#"insert UserOrder { user_name := "Alice", total := 100 }"#)
10453            .unwrap();
10454        engine
10455            .execute_powql(r#"insert UserOrder { user_name := "Bob", total := 50 }"#)
10456            .unwrap();
10457
10458        // Correlated EXISTS: only Users who have at least one order.
10459        // .name in the subquery filter refers to the outer User's name column.
10460        let result = engine
10461            .execute_powql("User filter exists (UserOrder filter .user_name = .name) { .name }")
10462            .unwrap();
10463        match result {
10464            QueryResult::Rows { rows, .. } => {
10465                assert_eq!(rows.len(), 2, "Alice and Bob have orders");
10466                let names: Vec<_> = rows.iter().map(|r| &r[0]).collect();
10467                assert!(names.contains(&&Value::Str("Alice".into())));
10468                assert!(names.contains(&&Value::Str("Bob".into())));
10469            }
10470            _ => panic!("expected rows"),
10471        }
10472    }
10473
10474    #[test]
10475    fn test_correlated_not_exists_subquery() {
10476        let mut engine = test_engine();
10477        engine
10478            .execute_powql("type UserOrder { required user_name: str, required total: int }")
10479            .unwrap();
10480        engine
10481            .execute_powql(r#"insert UserOrder { user_name := "Alice", total := 100 }"#)
10482            .unwrap();
10483
10484        // NOT EXISTS: Users without orders (Bob and Charlie).
10485        let result = engine
10486            .execute_powql("User filter not exists (UserOrder filter .user_name = .name) { .name }")
10487            .unwrap();
10488        match result {
10489            QueryResult::Rows { rows, .. } => {
10490                assert_eq!(rows.len(), 2, "Bob and Charlie have no orders");
10491                let names: Vec<_> = rows.iter().map(|r| &r[0]).collect();
10492                assert!(names.contains(&&Value::Str("Bob".into())));
10493                assert!(names.contains(&&Value::Str("Charlie".into())));
10494            }
10495            _ => panic!("expected rows"),
10496        }
10497    }
10498
10499    // ─── CAST tests ───────────────────────────────────────────────────
10500
10501    #[test]
10502    fn test_cast_int_to_str() {
10503        let mut engine = test_engine();
10504        let result = engine
10505            .execute_powql(r#"User { s: cast(.age, "str") }"#)
10506            .unwrap();
10507        match result {
10508            QueryResult::Rows { rows, .. } => {
10509                assert_eq!(rows[0][0], Value::Str("30".into()));
10510                assert_eq!(rows[1][0], Value::Str("25".into()));
10511            }
10512            _ => panic!("expected rows"),
10513        }
10514    }
10515
10516    #[test]
10517    fn test_cast_str_to_int() {
10518        let mut engine = test_engine();
10519        engine
10520            .execute_powql(r#"type Numbers { required val: str }"#)
10521            .unwrap();
10522        engine
10523            .execute_powql(r#"insert Numbers { val := "42" }"#)
10524            .unwrap();
10525        let result = engine
10526            .execute_powql(r#"Numbers { n: cast(.val, "int") }"#)
10527            .unwrap();
10528        match result {
10529            QueryResult::Rows { rows, .. } => {
10530                assert_eq!(rows[0][0], Value::Int(42));
10531            }
10532            _ => panic!("expected rows"),
10533        }
10534    }
10535
10536    #[test]
10537    fn test_cast_float_to_int() {
10538        let mut engine = test_engine();
10539        engine
10540            .execute_powql("type Floats { required val: float }")
10541            .unwrap();
10542        engine
10543            .execute_powql("insert Floats { val := 3.7 }")
10544            .unwrap();
10545        let result = engine
10546            .execute_powql(r#"Floats { n: cast(.val, "int") }"#)
10547            .unwrap();
10548        match result {
10549            QueryResult::Rows { rows, .. } => {
10550                assert_eq!(rows[0][0], Value::Int(3));
10551            }
10552            _ => panic!("expected rows"),
10553        }
10554    }
10555
10556    #[test]
10557    fn test_cast_int_to_float() {
10558        let mut engine = test_engine();
10559        let result = engine
10560            .execute_powql(r#"User { f: cast(.age, "float") }"#)
10561            .unwrap();
10562        match result {
10563            QueryResult::Rows { rows, .. } => {
10564                assert_eq!(rows[0][0], Value::Float(30.0));
10565            }
10566            _ => panic!("expected rows"),
10567        }
10568    }
10569
10570    #[test]
10571    fn test_cast_int_to_bool() {
10572        let mut engine = test_engine();
10573        let result = engine
10574            .execute_powql(r#"User { b: cast(.age, "bool") }"#)
10575            .unwrap();
10576        match result {
10577            QueryResult::Rows { rows, .. } => {
10578                // age=30 -> true (non-zero)
10579                assert_eq!(rows[0][0], Value::Bool(true));
10580            }
10581            _ => panic!("expected rows"),
10582        }
10583    }
10584
10585    // ─── Math function tests ──────────────────────────────────────────
10586
10587    #[test]
10588    fn test_abs() {
10589        let mut engine = test_engine();
10590        engine
10591            .execute_powql("type Nums { required val: int }")
10592            .unwrap();
10593        engine.execute_powql("insert Nums { val := -42 }").unwrap();
10594        let result = engine.execute_powql("Nums { a: abs(.val) }").unwrap();
10595        match result {
10596            QueryResult::Rows { rows, .. } => {
10597                assert_eq!(rows[0][0], Value::Int(42));
10598            }
10599            _ => panic!("expected rows"),
10600        }
10601    }
10602
10603    #[test]
10604    fn test_round() {
10605        let mut engine = test_engine();
10606        engine
10607            .execute_powql("type Floats { required val: float }")
10608            .unwrap();
10609        engine
10610            .execute_powql("insert Floats { val := 7.56789 }")
10611            .unwrap();
10612        let result = engine
10613            .execute_powql("Floats { r: round(.val, 2) }")
10614            .unwrap();
10615        match result {
10616            QueryResult::Rows { rows, .. } => {
10617                assert_eq!(rows[0][0], Value::Float(7.57));
10618            }
10619            _ => panic!("expected rows"),
10620        }
10621    }
10622
10623    #[test]
10624    fn test_ceil_floor() {
10625        let mut engine = test_engine();
10626        engine
10627            .execute_powql("type Floats { required val: float }")
10628            .unwrap();
10629        engine
10630            .execute_powql("insert Floats { val := 3.2 }")
10631            .unwrap();
10632        let c = engine.execute_powql("Floats { c: ceil(.val) }").unwrap();
10633        let f = engine.execute_powql("Floats { f: floor(.val) }").unwrap();
10634        match (c, f) {
10635            (QueryResult::Rows { rows: cr, .. }, QueryResult::Rows { rows: fr, .. }) => {
10636                assert_eq!(cr[0][0], Value::Float(4.0));
10637                assert_eq!(fr[0][0], Value::Float(3.0));
10638            }
10639            _ => panic!("expected rows"),
10640        }
10641    }
10642
10643    #[test]
10644    fn test_sqrt() {
10645        let mut engine = test_engine();
10646        engine
10647            .execute_powql("type Nums { required val: int }")
10648            .unwrap();
10649        engine.execute_powql("insert Nums { val := 144 }").unwrap();
10650        let result = engine.execute_powql("Nums { s: sqrt(.val) }").unwrap();
10651        match result {
10652            QueryResult::Rows { rows, .. } => {
10653                assert_eq!(rows[0][0], Value::Float(12.0));
10654            }
10655            _ => panic!("expected rows"),
10656        }
10657    }
10658
10659    #[test]
10660    fn test_pow() {
10661        let mut engine = test_engine();
10662        engine
10663            .execute_powql("type Nums { required val: int }")
10664            .unwrap();
10665        engine.execute_powql("insert Nums { val := 3 }").unwrap();
10666        let result = engine.execute_powql("Nums { p: pow(.val, 4) }").unwrap();
10667        match result {
10668            QueryResult::Rows { rows, .. } => {
10669                assert_eq!(rows[0][0], Value::Int(81));
10670            }
10671            _ => panic!("expected rows"),
10672        }
10673    }
10674
10675    // ─── Date/time function tests ─────────────────────────────────────
10676
10677    #[test]
10678    fn test_now_returns_datetime() {
10679        let mut engine = test_engine();
10680        engine
10681            .execute_powql("type Events { required name: str }")
10682            .unwrap();
10683        engine
10684            .execute_powql(r#"insert Events { name := "test" }"#)
10685            .unwrap();
10686        let result = engine.execute_powql("Events { ts: now() }").unwrap();
10687        match result {
10688            QueryResult::Rows { rows, .. } => match &rows[0][0] {
10689                Value::DateTime(m) => assert!(*m > 0, "now() should return positive timestamp"),
10690                other => panic!("expected DateTime, got {other:?}"),
10691            },
10692            _ => panic!("expected rows"),
10693        }
10694    }
10695
10696    #[test]
10697    fn test_extract_from_datetime() {
10698        let mut engine = test_engine();
10699        engine
10700            .execute_powql("type Events { required ts: datetime }")
10701            .unwrap();
10702        // 2024-01-15 12:30:45 UTC in microseconds
10703        // 2024-01-15 = 19737 days since epoch
10704        // 19737 * 86400 = 1705276800 seconds + 12*3600 + 30*60 + 45 = 1705321845
10705        // * 1_000_000 = 1705321845000000
10706        engine
10707            .execute_powql("insert Events { ts := 1705321845000000 }")
10708            .unwrap();
10709        let result = engine.execute_powql(r#"Events { y: extract("year", .ts), m: extract("month", .ts), d: extract("day", .ts), h: extract("hour", .ts) }"#).unwrap();
10710        match result {
10711            QueryResult::Rows { rows, .. } => {
10712                assert_eq!(rows[0][0], Value::Int(2024));
10713                assert_eq!(rows[0][1], Value::Int(1));
10714                assert_eq!(rows[0][2], Value::Int(15));
10715                assert_eq!(rows[0][3], Value::Int(12));
10716            }
10717            _ => panic!("expected rows"),
10718        }
10719    }
10720
10721    #[test]
10722    fn test_date_add() {
10723        let mut engine = test_engine();
10724        engine
10725            .execute_powql("type Events { required ts: datetime }")
10726            .unwrap();
10727        let base = 1705321845000000_i64; // 2024-01-15 12:30:45 UTC
10728        engine
10729            .execute_powql(&format!("insert Events {{ ts := {base} }}"))
10730            .unwrap();
10731        let result = engine
10732            .execute_powql(r#"Events { later: date_add(.ts, 2, "hours") }"#)
10733            .unwrap();
10734        match result {
10735            QueryResult::Rows { rows, .. } => {
10736                assert_eq!(rows[0][0], Value::DateTime(base + 2 * 3_600_000_000));
10737            }
10738            _ => panic!("expected rows"),
10739        }
10740    }
10741
10742    #[test]
10743    fn test_date_diff() {
10744        let mut engine = test_engine();
10745        engine
10746            .execute_powql("type Events { required start_ts: datetime, required end_ts: datetime }")
10747            .unwrap();
10748        let t1 = 1705321845000000_i64; // 2024-01-15 12:30:45 UTC
10749        let t2 = t1 + 3 * 86_400_000_000; // +3 days
10750        engine
10751            .execute_powql(&format!(
10752                "insert Events {{ start_ts := {t1}, end_ts := {t2} }}"
10753            ))
10754            .unwrap();
10755        let result = engine
10756            .execute_powql(r#"Events { diff: date_diff(.end_ts, .start_ts, "days") }"#)
10757            .unwrap();
10758        match result {
10759            QueryResult::Rows { rows, .. } => {
10760                assert_eq!(rows[0][0], Value::Int(3));
10761            }
10762            _ => panic!("expected rows"),
10763        }
10764    }
10765}