Skip to main content

powdb_query/executor/
plan_exec.rs

1//! The execute_plan method and associated helpers.
2
3use crate::ast::*;
4use crate::plan::*;
5use crate::result::{QueryError, QueryResult};
6use powdb_storage::catalog::Catalog;
7use powdb_storage::row::{decode_column, decode_row, patch_var_column_in_place, RowLayout};
8use powdb_storage::types::*;
9use std::cmp::Reverse;
10use std::collections::BinaryHeap;
11
12use super::compiled::*;
13use super::eval::*;
14use super::row_body_base;
15use super::{check_join_limit, Engine, MAX_SORT_ROWS};
16use powdb_storage::view::{ViewDef, ViewRegistry};
17
18impl Engine {
19    pub fn execute_plan(&mut self, plan: &PlanNode) -> Result<QueryResult, QueryError> {
20        match plan {
21            PlanNode::SeqScan { table } => {
22                // Auto-refresh dirty materialized views on read.
23                if self.view_registry.is_dirty(table) {
24                    self.refresh_view(table)?;
25                }
26                let schema = self
27                    .catalog
28                    .schema(table)
29                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
30                    .clone();
31                let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
32                let rows: Vec<Vec<Value>> = self
33                    .catalog
34                    .scan(table)
35                    .map_err(|e| QueryError::StorageError(e.to_string()))?
36                    .map(|(_, row)| row)
37                    .collect();
38                Ok(QueryResult::Rows { columns, rows })
39            }
40
41            PlanNode::Filter { input, predicate } => {
42                // Materialize any IN-subqueries in the predicate before the
43                // scan loop — the closure can't call back into the engine.
44                // Correlated subqueries are left in place for per-row eval.
45                let materialized;
46                let predicate = if contains_subquery(predicate) {
47                    materialized = self.materialize_subqueries(predicate)?;
48                    &materialized
49                } else {
50                    predicate
51                };
52
53                // Correlated subquery path: per-row materialisation.
54                if contains_subquery(predicate) {
55                    let result = self.execute_plan(input)?;
56                    return match result {
57                        QueryResult::Rows { columns, rows } => {
58                            let mut filtered = Vec::new();
59                            for row in rows {
60                                let row_pred =
61                                    self.materialize_correlated_for_row(predicate, &row, &columns)?;
62                                if eval_predicate(&row_pred, &row, &columns) {
63                                    filtered.push(row);
64                                }
65                            }
66                            Ok(QueryResult::Rows {
67                                columns,
68                                rows: filtered,
69                            })
70                        }
71                        _ => Err("filter requires row input".into()),
72                    };
73                }
74
75                // Fast path: fuse Filter + SeqScan into a zero-copy streaming
76                // loop. Uses decode_column() to evaluate the predicate on only
77                // the columns it references, avoiding heap allocations for
78                // String/Bytes columns that aren't part of the filter.
79                if let PlanNode::SeqScan { table } = input.as_ref() {
80                    // Auto-refresh dirty materialized views.
81                    if self.view_registry.is_dirty(table) {
82                        self.refresh_view(table)?;
83                    }
84                    let schema = self
85                        .catalog
86                        .schema(table)
87                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
88                        .clone();
89                    let columns: Vec<String> =
90                        schema.columns.iter().map(|c| c.name.clone()).collect();
91                    let fast = FastLayout::new(&schema);
92                    let row_layout = RowLayout::new(&schema);
93                    // Mission F: pre-size to skip the first 4 Vec doublings
94                    // (4 → 8 → 16 → 32 → 64). On a 100K-row scan with 30%
95                    // selectivity that's ~4 fewer reallocations + memcpys.
96                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
97
98                    // Try compiled predicate for the filter check (handles
99                    // int leaves, string-eq leaves, and And conjunctions).
100                    if let Some(compiled) = compile_predicate(predicate, &columns, &fast, &schema) {
101                        self.catalog
102                            .for_each_row_raw(table, |_rid, data| {
103                                if compiled(data) {
104                                    rows.push(decode_row(&schema, data));
105                                }
106                            })
107                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
108                    } else {
109                        let pred_cols = predicate_column_indices(predicate, &columns);
110                        self.catalog
111                            .for_each_row_raw(table, |_rid, data| {
112                                let pred_row =
113                                    decode_selective(&schema, &row_layout, data, &pred_cols);
114                                if eval_predicate(predicate, &pred_row, &columns) {
115                                    rows.push(decode_row(&schema, data));
116                                }
117                            })
118                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
119                    }
120
121                    return Ok(QueryResult::Rows { columns, rows });
122                }
123
124                // General path: materialise then filter.
125                let result = self.execute_plan(input)?;
126                match result {
127                    QueryResult::Rows { columns, rows } => {
128                        let filtered: Vec<Vec<Value>> = rows
129                            .into_iter()
130                            .filter(|row| eval_predicate(predicate, row, &columns))
131                            .collect();
132                        Ok(QueryResult::Rows {
133                            columns,
134                            rows: filtered,
135                        })
136                    }
137                    _ => Err("filter requires row input".into()),
138                }
139            }
140
141            PlanNode::Project { input, fields } => {
142                // Fast path: Project over IndexScan — decode only projected
143                // columns from raw bytes instead of full decode_row.
144                if let PlanNode::IndexScan { table, column, key } = input.as_ref() {
145                    let schema = self
146                        .catalog
147                        .schema(table)
148                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
149                        .clone();
150                    let all_columns: Vec<String> =
151                        schema.columns.iter().map(|c| c.name.clone()).collect();
152                    let key_value = literal_to_value(key)?;
153                    let tbl = self
154                        .catalog
155                        .get_table(table)
156                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
157
158                    let proj_columns: Vec<String> = fields
159                        .iter()
160                        .map(|f| {
161                            f.alias.clone().unwrap_or_else(|| match &f.expr {
162                                Expr::Field(name) => name.clone(),
163                                _ => "?".into(),
164                            })
165                        })
166                        .collect();
167
168                    // Determine which column indices the projection needs
169                    let proj_indices: Vec<usize> = fields
170                        .iter()
171                        .filter_map(|f| {
172                            if let Expr::Field(name) = &f.expr {
173                                all_columns.iter().position(|c| c == name)
174                            } else {
175                                None
176                            }
177                        })
178                        .collect();
179
180                    if tbl.has_index(column) {
181                        let layout = RowLayout::new(&schema);
182                        let rids = tbl.index_lookup_all(column, &key_value);
183                        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(rids.len());
184                        for rid in rids {
185                            if let Some(data) = tbl.heap.get(rid) {
186                                let row: Vec<Value> = proj_indices
187                                    .iter()
188                                    .map(|&ci| decode_column(&schema, &layout, &data, ci))
189                                    .collect();
190                                rows.push(row);
191                            }
192                        }
193                        return Ok(QueryResult::Rows {
194                            columns: proj_columns,
195                            rows,
196                        });
197                    }
198                }
199
200                // Fast path: Project(Limit(Sort(Filter(SeqScan)))) — bounded
201                // top-N heap. Decodes only the sort key + projected columns,
202                // keeps at most `limit` rows in a heap. Also handles the
203                // Project(Limit(Sort(SeqScan))) variant (no filter).
204                if let PlanNode::Limit {
205                    input: inner,
206                    count: limit_expr,
207                } = input.as_ref()
208                {
209                    if let PlanNode::Sort {
210                        input: sort_input,
211                        keys,
212                    } = inner.as_ref()
213                    {
214                        // Fast path only for single-key sorts
215                        if keys.len() == 1 {
216                            let sort_field = &keys[0].field;
217                            let descending = keys[0].descending;
218                            let limit = match limit_expr {
219                                Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
220                                _ => usize::MAX,
221                            };
222                            let (table_opt, pred_opt): (Option<&str>, Option<&Expr>) =
223                                match sort_input.as_ref() {
224                                    PlanNode::SeqScan { table } => (Some(table.as_str()), None),
225                                    PlanNode::Filter {
226                                        input: fi,
227                                        predicate,
228                                    } => {
229                                        if let PlanNode::SeqScan { table } = fi.as_ref() {
230                                            (Some(table.as_str()), Some(predicate))
231                                        } else {
232                                            (None, None)
233                                        }
234                                    }
235                                    _ => (None, None),
236                                };
237                            if let Some(table) = table_opt {
238                                if let Some(result) = self.project_filter_sort_limit_fast(
239                                    table, fields, sort_field, descending, limit, pred_opt,
240                                )? {
241                                    return Ok(result);
242                                }
243                            }
244                        }
245                    }
246                    // Fast path: Project(Limit(Filter(SeqScan))) — stream,
247                    // decode only projected columns, stop at limit.
248                    if let PlanNode::Filter {
249                        input: fi,
250                        predicate,
251                    } = inner.as_ref()
252                    {
253                        if let PlanNode::SeqScan { table } = fi.as_ref() {
254                            let limit = match limit_expr {
255                                Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
256                                _ => usize::MAX,
257                            };
258                            if let Some(result) = self.project_filter_limit_fast(
259                                table,
260                                fields,
261                                limit,
262                                Some(predicate),
263                            )? {
264                                return Ok(result);
265                            }
266                        }
267                    }
268                    // Fast path: Project(Limit(SeqScan)) — stream, no filter.
269                    if let PlanNode::SeqScan { table } = inner.as_ref() {
270                        let limit = match limit_expr {
271                            Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
272                            _ => usize::MAX,
273                        };
274                        if let Some(result) =
275                            self.project_filter_limit_fast(table, fields, limit, None)?
276                        {
277                            return Ok(result);
278                        }
279                    }
280                }
281
282                // Mission D4: Project(Filter(SeqScan)) without Limit. Reuses
283                // `project_filter_limit_fast` with limit = usize::MAX so the
284                // hot loop decodes only projected columns and uses the
285                // compiled predicate. Previously this fell through to the
286                // generic Filter branch which materialised every column via
287                // `decode_row` then re-projected — quadratic work.
288                //
289                // multi_col_and_filter (`U filter .age > 30 and .status =
290                // "active" { .name, .age }`) was 6.18ms (0.7x SQLite) and
291                // is the load-bearing workload for this fast path.
292                if let PlanNode::Filter {
293                    input: fi,
294                    predicate,
295                } = input.as_ref()
296                {
297                    if let PlanNode::SeqScan { table } = fi.as_ref() {
298                        if let Some(result) = self.project_filter_limit_fast(
299                            table,
300                            fields,
301                            usize::MAX,
302                            Some(predicate),
303                        )? {
304                            return Ok(result);
305                        }
306                    }
307                }
308
309                // Mission D4: Project(SeqScan) without Filter or Limit.
310                // Decode only projected columns; the previous fall-through
311                // built full Vec<Value> rows then re-projected.
312                if let PlanNode::SeqScan { table } = input.as_ref() {
313                    if let Some(result) =
314                        self.project_filter_limit_fast(table, fields, usize::MAX, None)?
315                    {
316                        return Ok(result);
317                    }
318                }
319
320                let result = self.execute_plan(input)?;
321                match result {
322                    QueryResult::Rows { columns, rows } => {
323                        let proj_columns: Vec<String> = fields
324                            .iter()
325                            .map(|f| {
326                                f.alias.clone().unwrap_or_else(|| match &f.expr {
327                                    Expr::Field(name) => name.clone(),
328                                    // Mission E1.2: `{ u.name }` projects as the
329                                    // qualified column name so callers can still
330                                    // disambiguate across the join output.
331                                    Expr::QualifiedField { qualifier, field } => {
332                                        format!("{qualifier}.{field}")
333                                    }
334                                    _ => "?".into(),
335                                })
336                            })
337                            .collect();
338                        let proj_rows: Vec<Vec<Value>> = rows
339                            .iter()
340                            .map(|row| {
341                                fields
342                                    .iter()
343                                    .map(|f| eval_expr(&f.expr, row, &columns))
344                                    .collect()
345                            })
346                            .collect();
347                        Ok(QueryResult::Rows {
348                            columns: proj_columns,
349                            rows: proj_rows,
350                        })
351                    }
352                    _ => Err("project requires row input".into()),
353                }
354            }
355
356            PlanNode::Sort { input, keys } => {
357                let result = self.execute_plan(input)?;
358                match result {
359                    QueryResult::Rows { columns, mut rows } => {
360                        // WS2: row-count cap is a cheap secondary guard; the
361                        // byte budget is the real OOM defense for the sort
362                        // buffer (a few very large rows pass the row cap).
363                        if rows.len() > MAX_SORT_ROWS {
364                            return Err(QueryError::SortLimitExceeded);
365                        }
366                        self.charge_rows(&rows)?;
367                        let key_indices: Vec<(usize, bool)> = keys
368                            .iter()
369                            .map(|k| {
370                                columns
371                                    .iter()
372                                    .position(|c| c == &k.field)
373                                    .map(|idx| (idx, k.descending))
374                                    .ok_or_else(|| QueryError::ColumnNotFound {
375                                        table: String::new(),
376                                        column: k.field.clone(),
377                                    })
378                            })
379                            .collect::<Result<_, QueryError>>()?;
380                        rows.sort_by(|a, b| {
381                            for &(col_idx, descending) in &key_indices {
382                                let cmp = a[col_idx].cmp(&b[col_idx]);
383                                let cmp = if descending { cmp.reverse() } else { cmp };
384                                if cmp != std::cmp::Ordering::Equal {
385                                    return cmp;
386                                }
387                            }
388                            std::cmp::Ordering::Equal
389                        });
390                        Ok(QueryResult::Rows { columns, rows })
391                    }
392                    _ => Err("sort requires row input".into()),
393                }
394            }
395
396            PlanNode::Limit { input, count } => {
397                let result = self.execute_plan(input)?;
398                let n = match count {
399                    Expr::Literal(Literal::Int(v)) => *v as usize,
400                    _ => return Err("limit must be integer literal".into()),
401                };
402                match result {
403                    QueryResult::Rows { columns, rows } => Ok(QueryResult::Rows {
404                        columns,
405                        rows: rows.into_iter().take(n).collect(),
406                    }),
407                    _ => Err("limit requires row input".into()),
408                }
409            }
410
411            PlanNode::Offset { input, count } => {
412                let result = self.execute_plan(input)?;
413                let n = match count {
414                    Expr::Literal(Literal::Int(v)) => *v as usize,
415                    _ => return Err("offset must be integer literal".into()),
416                };
417                match result {
418                    QueryResult::Rows { columns, rows } => Ok(QueryResult::Rows {
419                        columns,
420                        rows: rows.into_iter().skip(n).collect(),
421                    }),
422                    _ => Err("offset requires row input".into()),
423                }
424            }
425
426            PlanNode::Aggregate {
427                input,
428                function,
429                field,
430            } => {
431                // Fast path: count() over SeqScan — count rows without any decode
432                if *function == AggFunc::Count {
433                    if let PlanNode::SeqScan { table } = input.as_ref() {
434                        // Auto-refresh a dirty materialized view before
435                        // counting it — otherwise count(View) returns stale
436                        // data after an underlying mutation (F3).
437                        if self.view_registry.is_dirty(table) {
438                            self.refresh_view(table)?;
439                        }
440                        let mut count: i64 = 0;
441                        self.catalog
442                            .for_each_row_raw(table, |_rid, _data| {
443                                count += 1;
444                            })
445                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
446                        return Ok(QueryResult::Scalar(Value::Int(count)));
447                    }
448                    // Fast path: count() over Filter(SeqScan) — try compiled
449                    // predicate first, fall back to decode_column path.
450                    // Skip a predicate carrying a subquery: the raw-bytes
451                    // evaluators here don't materialise subqueries, so
452                    // `count(T filter .x in (...))` would silently count 0
453                    // (F1). Falling through routes it to the generic path
454                    // that resolves the subquery correctly.
455                    if let PlanNode::Filter {
456                        input: inner,
457                        predicate,
458                    } = input.as_ref()
459                    {
460                        if let PlanNode::SeqScan { table } = inner.as_ref() {
461                            if self.view_registry.is_dirty(table) {
462                                self.refresh_view(table)?;
463                            }
464                        }
465                        if let (PlanNode::SeqScan { table }, false) =
466                            (inner.as_ref(), contains_subquery(predicate))
467                        {
468                            let schema = self
469                                .catalog
470                                .schema(table)
471                                .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
472                                .clone();
473                            let columns: Vec<String> =
474                                schema.columns.iter().map(|c| c.name.clone()).collect();
475                            let fast = FastLayout::new(&schema);
476                            let row_layout = RowLayout::new(&schema);
477
478                            // Try compiled predicate (zero-allocation hot path).
479                            // Handles int leaves, string-eq leaves, AND conjunctions.
480                            if let Some(compiled) =
481                                compile_predicate(predicate, &columns, &fast, &schema)
482                            {
483                                let mut count: i64 = 0;
484                                self.catalog
485                                    .for_each_row_raw(table, |_rid, data| {
486                                        if compiled(data) {
487                                            count += 1;
488                                        }
489                                    })
490                                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
491                                return Ok(QueryResult::Scalar(Value::Int(count)));
492                            }
493
494                            // Fallback: decode predicate columns
495                            let pred_cols = predicate_column_indices(predicate, &columns);
496                            let mut count: i64 = 0;
497                            self.catalog
498                                .for_each_row_raw(table, |_rid, data| {
499                                    let pred_row =
500                                        decode_selective(&schema, &row_layout, data, &pred_cols);
501                                    if eval_predicate(predicate, &pred_row, &columns) {
502                                        count += 1;
503                                    }
504                                })
505                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
506
507                            return Ok(QueryResult::Scalar(Value::Int(count)));
508                        }
509                    }
510                }
511
512                // Fast path: sum/avg/min/max over a single fixed-size int
513                // column with an optional compiled filter predicate. Walks
514                // raw row bytes, zero allocation per row.
515                if matches!(
516                    function,
517                    AggFunc::Sum
518                        | AggFunc::Avg
519                        | AggFunc::Min
520                        | AggFunc::Max
521                        | AggFunc::CountDistinct
522                ) {
523                    if let Some(col) = field.as_ref() {
524                        // Shape: Aggregate(SeqScan) or Aggregate(Filter(SeqScan))
525                        let (table_opt, pred_opt): (Option<&str>, Option<&Expr>) =
526                            match input.as_ref() {
527                                PlanNode::SeqScan { table } => (Some(table.as_str()), None),
528                                PlanNode::Filter {
529                                    input: inner,
530                                    predicate,
531                                } => {
532                                    if let PlanNode::SeqScan { table } = inner.as_ref() {
533                                        (Some(table.as_str()), Some(predicate))
534                                    } else {
535                                        (None, None)
536                                    }
537                                }
538                                _ => (None, None),
539                            };
540                        if let Some(table) = table_opt {
541                            if let Some(result) =
542                                self.agg_single_col_fast(table, col, *function, pred_opt)?
543                            {
544                                return Ok(result);
545                            }
546                        }
547                    }
548                }
549
550                // Fast path: Project(Limit(Filter(SeqScan))) — stream, decode
551                // only projected columns, stop once we hit the limit.
552                // (Handled in the Project branch; this branch only fires when
553                // the aggregate is the outer node.)
554                let result = self.execute_plan(input)?;
555                match result {
556                    QueryResult::Rows { columns, rows } => {
557                        match function {
558                            AggFunc::Count => {
559                                Ok(QueryResult::Scalar(Value::Int(rows.len() as i64)))
560                            }
561                            AggFunc::CountDistinct => {
562                                let col = field.as_ref().ok_or("count distinct requires field")?;
563                                let idx = columns
564                                    .iter()
565                                    .position(|c| c == col)
566                                    .ok_or("col not found")?;
567                                let mut seen = std::collections::HashSet::new();
568                                for row in &rows {
569                                    let v = &row[idx];
570                                    if !v.is_empty() {
571                                        seen.insert(v.clone());
572                                    }
573                                }
574                                Ok(QueryResult::Scalar(Value::Int(seen.len() as i64)))
575                            }
576                            AggFunc::Avg => {
577                                let col = field.as_ref().ok_or("avg requires field")?;
578                                let idx = columns
579                                    .iter()
580                                    .position(|c| c == col)
581                                    .ok_or("col not found")?;
582                                let mut count: u64 = 0;
583                                let sum: f64 = rows
584                                    .iter()
585                                    .filter_map(|r| match &r[idx] {
586                                        Value::Int(v) => Some(*v as f64),
587                                        Value::Float(v) => Some(*v),
588                                        _ => None,
589                                    })
590                                    .inspect(|_| count += 1)
591                                    .sum();
592                                if count == 0 {
593                                    Ok(QueryResult::Scalar(Value::Empty))
594                                } else {
595                                    Ok(QueryResult::Scalar(Value::Float(sum / count as f64)))
596                                }
597                            }
598                            AggFunc::Sum => {
599                                let col = field.as_ref().ok_or("sum requires field")?;
600                                let idx = columns
601                                    .iter()
602                                    .position(|c| c == col)
603                                    .ok_or("col not found")?;
604                                // Track int and float contributions separately so
605                                // Float columns (and mixed Int/Float rows) don't get
606                                // silently dropped as they did in the Int-only
607                                // version. If any Float is present, the whole sum
608                                // promotes to Float — matching Avg's semantics.
609                                let mut int_sum: i64 = 0;
610                                let mut float_sum: f64 = 0.0;
611                                let mut saw_float = false;
612                                for r in &rows {
613                                    match &r[idx] {
614                                        Value::Int(v) => int_sum += *v,
615                                        Value::Float(v) => {
616                                            float_sum += *v;
617                                            saw_float = true;
618                                        }
619                                        _ => {}
620                                    }
621                                }
622                                let result = if saw_float {
623                                    Value::Float(float_sum + int_sum as f64)
624                                } else {
625                                    Value::Int(int_sum)
626                                };
627                                Ok(QueryResult::Scalar(result))
628                            }
629                            AggFunc::Min | AggFunc::Max => {
630                                let col = field.as_ref().ok_or("min/max requires field")?;
631                                let idx = columns
632                                    .iter()
633                                    .position(|c| c == col)
634                                    .ok_or("col not found")?;
635                                let vals: Vec<&Value> = rows.iter().map(|r| &r[idx]).collect();
636                                let result = if *function == AggFunc::Min {
637                                    vals.into_iter().min().cloned()
638                                } else {
639                                    vals.into_iter().max().cloned()
640                                };
641                                Ok(QueryResult::Scalar(result.unwrap_or(Value::Empty)))
642                            }
643                        }
644                    }
645                    _ => Err("aggregate requires row input".into()),
646                }
647            }
648
649            PlanNode::Insert { table, rows } => {
650                // Build + validate EVERY row before inserting any, so a bad
651                // row (unknown/missing/uncoercible field) aborts the whole
652                // statement without a partial write. The WAL fsync happens
653                // once at statement end, so N rows = N appends + 1 fsync.
654                let all_values: Vec<Vec<Value>> = {
655                    let schema = self
656                        .catalog
657                        .schema(table)
658                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
659                    let mut all = Vec::with_capacity(rows.len());
660                    for assignments in rows {
661                        let mut values = vec![Value::Empty; schema.columns.len()];
662                        for a in assignments {
663                            let idx = schema.column_index(&a.field).ok_or_else(|| {
664                                QueryError::ColumnNotFound {
665                                    table: String::new(),
666                                    column: a.field.clone(),
667                                }
668                            })?;
669                            let raw = literal_to_value(&a.value)?;
670                            values[idx] = coerce_value(raw, &schema.columns[idx])?;
671                        }
672                        for col in &schema.columns {
673                            if col.required && matches!(values[col.position as usize], Value::Empty)
674                            {
675                                return Err(QueryError::Execution(format!(
676                                    "column '{}' is required but no value was provided",
677                                    col.name
678                                )));
679                            }
680                        }
681                        all.push(values);
682                    }
683                    all
684                };
685                // Charge the materialized batch against the per-query memory
686                // budget before inserting — keeps multi-row insert consistent
687                // with every other full-materialization point (sort/join/group)
688                // and bounds embedded callers (the server also caps the query
689                // string at 1 MB, but embedded callers have no such limit).
690                self.charge_rows(&all_values)?;
691                let n = all_values.len() as u64;
692                for values in &all_values {
693                    self.catalog
694                        .insert(table, values)
695                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
696                }
697                self.view_registry.mark_dependents_dirty(table);
698                Ok(QueryResult::Modified(n))
699            }
700
701            PlanNode::Upsert {
702                table,
703                key_column,
704                assignments,
705                on_conflict,
706            } => {
707                let (values, key_idx) = {
708                    let schema = self
709                        .catalog
710                        .schema(table)
711                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
712                    let mut values = vec![Value::Empty; schema.columns.len()];
713                    for a in assignments {
714                        let idx = schema.column_index(&a.field).ok_or_else(|| {
715                            QueryError::ColumnNotFound {
716                                table: String::new(),
717                                column: a.field.clone(),
718                            }
719                        })?;
720                        let raw = literal_to_value(&a.value)?;
721                        values[idx] = coerce_value(raw, &schema.columns[idx])?;
722                    }
723                    for col in &schema.columns {
724                        if col.required && matches!(values[col.position as usize], Value::Empty) {
725                            return Err(QueryError::Execution(format!(
726                                "column '{}' is required but no value was provided",
727                                col.name
728                            )));
729                        }
730                    }
731                    let key_idx = schema
732                        .column_index(key_column)
733                        .ok_or_else(|| format!("key column '{key_column}' not found"))?;
734                    (values, key_idx)
735                };
736
737                // Upsert requires the `on` column to be unique — otherwise
738                // there is no well-defined row to overwrite and a plain
739                // insert could silently create duplicate keys.
740                if self.catalog.is_index_unique(table, key_column) != Some(true) {
741                    return Err(QueryError::Execution(format!(
742                        "upsert on .{key_column} requires a unique column (declare it with \
743                         `unique {key_column}: <type>` or `alter {table} add unique .{key_column}`)"
744                    )));
745                }
746
747                let key_value = values[key_idx].clone();
748
749                // Probe the unique index for a conflict.
750                let existing = {
751                    let tbl = self
752                        .catalog
753                        .get_table(table)
754                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
755                    // The key column is guaranteed unique above, so this
756                    // returns at most one matching row.
757                    let rids = tbl.index_lookup_all(key_column, &key_value);
758                    rids.into_iter().next().and_then(|rid| {
759                        tbl.heap
760                            .get(rid)
761                            .map(|data| (rid, decode_row(&tbl.schema, &data)))
762                    })
763                };
764
765                if let Some((rid, mut existing_row)) = existing {
766                    // Conflict: apply on_conflict assignments (or all non-key if empty).
767                    let update_assignments = if on_conflict.is_empty() {
768                        assignments
769                    } else {
770                        on_conflict
771                    };
772                    let changed_cols: Vec<usize> = {
773                        let schema = self
774                            .catalog
775                            .schema(table)
776                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
777                        let mut indices = Vec::new();
778                        for a in update_assignments {
779                            let idx = schema.column_index(&a.field).ok_or_else(|| {
780                                QueryError::ColumnNotFound {
781                                    table: String::new(),
782                                    column: a.field.clone(),
783                                }
784                            })?;
785                            if idx != key_idx {
786                                existing_row[idx] = literal_to_value(&a.value)?;
787                                indices.push(idx);
788                            }
789                        }
790                        indices
791                    };
792                    self.catalog
793                        .update_hinted(table, rid, &existing_row, Some(&changed_cols))
794                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
795                    self.view_registry.mark_dependents_dirty(table);
796                    Ok(QueryResult::Modified(1))
797                } else {
798                    // No conflict: insert.
799                    self.catalog
800                        .insert(table, &values)
801                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
802                    self.view_registry.mark_dependents_dirty(table);
803                    Ok(QueryResult::Modified(1))
804                }
805            }
806
807            PlanNode::Update {
808                input,
809                table,
810                assignments,
811            } => {
812                // Mission C Phase 3: resolve assignments against a borrowed
813                // schema, then drop the borrow before the mutation loop.
814                // Try literal-only path first; fall back to per-row expression
815                // evaluation if any assignment contains a non-literal expression
816                // (e.g., `age := .age + 1`).
817                let (col_indices, literal_vals): (Vec<usize>, Option<Vec<Value>>) = {
818                    let schema_ref = self
819                        .catalog
820                        .schema(table)
821                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
822                    let indices: Vec<usize> = assignments
823                        .iter()
824                        .map(|a| {
825                            schema_ref.column_index(&a.field).ok_or_else(|| {
826                                QueryError::ColumnNotFound {
827                                    table: String::new(),
828                                    column: a.field.clone(),
829                                }
830                            })
831                        })
832                        .collect::<Result<_, _>>()?;
833                    let vals: Result<Vec<Value>, _> = assignments
834                        .iter()
835                        .map(|a| literal_to_value(&a.value))
836                        .collect();
837                    (indices, vals.ok())
838                };
839                let resolved_assignments: Option<Vec<(usize, Value)>> =
840                    literal_vals.map(|vals| col_indices.iter().copied().zip(vals).collect());
841
842                // Mission C Phase 2: the hint Table::update_hinted needs to
843                // decide whether to read the old row for index diff.
844                let changed_cols: Vec<usize> = col_indices.clone();
845
846                // ── Fused scan+update for Update(Filter(SeqScan)) ────────
847                // Perf sprint: instead of the two-pass collect-RIDs-then-loop
848                // pattern (which pays one ensure_hot per matched row on the
849                // second pass), fuse the predicate evaluation and in-place
850                // byte-level mutation into a single heap walk. Same idea as
851                // the fused scan_delete_matching path for deletes.
852                if let Some(ref resolved_assignments) = resolved_assignments {
853                    if let PlanNode::Filter {
854                        input: inner,
855                        predicate,
856                    } = input.as_ref()
857                    {
858                        if let PlanNode::SeqScan { table: t } = inner.as_ref() {
859                            if t == table {
860                                let fused_result = self.try_fused_scan_update(
861                                    table,
862                                    predicate,
863                                    resolved_assignments,
864                                    &changed_cols,
865                                );
866                                if let Some(result) = fused_result {
867                                    return result;
868                                }
869                            }
870                        }
871                    }
872                }
873
874                // Collect matching RowIds in a single pass.
875                let matching_rids = self.collect_rids_for_mutation(input, table)?;
876
877                // ── Literal-only fast paths ─────────────────────────────
878                if let Some(ref resolved_assignments) = resolved_assignments {
879                    // Mission C Phase 4: in-place byte-patch fast path. If every
880                    // assignment targets a fixed-size non-null column AND none of
881                    // them is indexed, we can skip decode_row / Vec<Value> /
882                    // encode_row_into entirely and patch the row's raw bytes on
883                    // the hot page.
884                    let fast_patch: Option<Vec<FastPatch>> = {
885                        let tbl = self
886                            .catalog
887                            .get_table(table)
888                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
889                        let schema = &tbl.schema;
890                        let all_fixed_nonnull = resolved_assignments.iter().all(|(idx, val)| {
891                            is_fixed_size(schema.columns[*idx].type_id) && !val.is_empty()
892                        });
893                        let no_indexed = !resolved_assignments
894                            .iter()
895                            .any(|(idx, _)| tbl.has_indexed_col(*idx));
896
897                        if all_fixed_nonnull && no_indexed {
898                            let layout = RowLayout::new(schema);
899                            let bitmap_size = layout.bitmap_size();
900                            let patches: Vec<FastPatch> = resolved_assignments
901                                .iter()
902                                .map(|(idx, val)| {
903                                    let fixed_off = layout
904                                        .fixed_offset(*idx)
905                                        .expect("is_fixed_size already checked");
906                                    let field_off = 2 + bitmap_size + fixed_off;
907                                    let bytes: FixedBytes = match val {
908                                        Value::Int(v) => FixedBytes::I64(v.to_le_bytes()),
909                                        Value::Float(v) => FixedBytes::F64(v.to_le_bytes()),
910                                        Value::Bool(v) => FixedBytes::Bool(if *v { 1 } else { 0 }),
911                                        Value::DateTime(v) => FixedBytes::I64(v.to_le_bytes()),
912                                        Value::Uuid(v) => FixedBytes::Uuid(*v),
913                                        _ => unreachable!("all_fixed_nonnull guard lied"),
914                                    };
915                                    FastPatch {
916                                        field_off,
917                                        bitmap_byte_off: 2 + idx / 8,
918                                        bit_mask: 1u8 << (idx % 8),
919                                        bytes,
920                                    }
921                                })
922                                .collect();
923                            Some(patches)
924                        } else {
925                            None
926                        }
927                    };
928
929                    if let Some(patches) = fast_patch {
930                        let mut count = 0u64;
931                        for rid in matching_rids {
932                            // Mission B2: WAL-log every patch so crash
933                            // recovery replays the update. Same mutation
934                            // closure as before — the wrapper just sandwiches
935                            // it between a hot-page read and a WAL append.
936                            let ok = self
937                                .catalog
938                                .update_row_bytes_logged(table, rid, |row| {
939                                    let base = row_body_base(row);
940                                    for p in &patches {
941                                        row[base + p.bitmap_byte_off] &= !p.bit_mask;
942                                        let field_bytes = p.bytes.as_slice();
943                                        row[base + p.field_off
944                                            ..base + p.field_off + field_bytes.len()]
945                                            .copy_from_slice(field_bytes);
946                                    }
947                                })
948                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
949                            if ok {
950                                count += 1;
951                            }
952                        }
953                        self.view_registry.mark_dependents_dirty(table);
954                        return Ok(QueryResult::Modified(count));
955                    }
956
957                    // Mission C Phase 10: var-column in-place shrink fast path.
958                    let var_fast: Option<(usize, Option<Vec<u8>>)> = {
959                        let tbl = self
960                            .catalog
961                            .get_table(table)
962                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
963                        let schema = &tbl.schema;
964                        let is_single = resolved_assignments.len() == 1;
965                        let is_var_col = is_single
966                            && !is_fixed_size(schema.columns[resolved_assignments[0].0].type_id);
967                        let no_indexed = !resolved_assignments
968                            .iter()
969                            .any(|(idx, _)| tbl.has_indexed_col(*idx));
970
971                        if is_single && is_var_col && no_indexed {
972                            let (idx, val) = &resolved_assignments[0];
973                            let bytes_opt: Option<Vec<u8>> = match val {
974                                Value::Str(s) => Some(s.as_bytes().to_vec()),
975                                Value::Bytes(b) => Some(b.clone()),
976                                Value::Empty => None,
977                                _ => {
978                                    return Err(QueryError::TypeError(format!(
979                                        "cannot assign non-var value to var column '{}'",
980                                        schema.columns[*idx].name
981                                    )))
982                                }
983                            };
984                            Some((*idx, bytes_opt))
985                        } else {
986                            None
987                        }
988                    };
989
990                    if let Some((col_idx, new_bytes_opt)) = var_fast {
991                        let new_bytes_ref: Option<&[u8]> = new_bytes_opt.as_deref();
992                        let mut count = 0u64;
993                        let mut fallback_rids: Vec<RowId> = Vec::new();
994                        for rid in &matching_rids {
995                            // Mission B2: logged variant so crash recovery
996                            // replays the shrink. On a false return (row
997                            // would have to grow), the rid is pushed to
998                            // `fallback_rids` and the slower `update_hinted`
999                            // path — which is already WAL-logged — picks it up.
1000                            let ok = self
1001                                .catalog
1002                                .patch_var_col_logged(table, *rid, col_idx, new_bytes_ref)
1003                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
1004                            if ok {
1005                                count += 1;
1006                            } else {
1007                                fallback_rids.push(*rid);
1008                            }
1009                        }
1010                        for rid in fallback_rids {
1011                            let mut row = match self.catalog.get(table, rid) {
1012                                Some(r) => r,
1013                                None => continue,
1014                            };
1015                            for (idx, val) in resolved_assignments.iter() {
1016                                row[*idx] = val.clone();
1017                            }
1018                            self.catalog
1019                                .update_hinted(table, rid, &row, Some(&changed_cols))
1020                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
1021                            count += 1;
1022                        }
1023                        self.view_registry.mark_dependents_dirty(table);
1024                        return Ok(QueryResult::Modified(count));
1025                    }
1026
1027                    // Generic literal path: decode row, apply literal values.
1028                    let mut count = 0u64;
1029                    for rid in matching_rids {
1030                        let mut row = match self.catalog.get(table, rid) {
1031                            Some(r) => r,
1032                            None => continue,
1033                        };
1034                        for (idx, val) in resolved_assignments.iter() {
1035                            row[*idx] = val.clone();
1036                        }
1037                        self.catalog
1038                            .update_hinted(table, rid, &row, Some(&changed_cols))
1039                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
1040                        count += 1;
1041                    }
1042                    self.view_registry.mark_dependents_dirty(table);
1043                    return Ok(QueryResult::Modified(count));
1044                } // end if let Some(resolved_assignments)
1045
1046                // ── Expression-based update path ────────────────────────
1047                // At least one assignment contains a non-literal expression
1048                // (e.g., `age := .age + 1`). Evaluate per-row.
1049                let col_names: Vec<String> = {
1050                    let schema_ref = self
1051                        .catalog
1052                        .schema(table)
1053                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1054                    schema_ref.columns.iter().map(|c| c.name.clone()).collect()
1055                };
1056                let mut count = 0u64;
1057                for rid in matching_rids {
1058                    let mut row = match self.catalog.get(table, rid) {
1059                        Some(r) => r,
1060                        None => continue,
1061                    };
1062                    for (i, asgn) in assignments.iter().enumerate() {
1063                        let val = eval_expr(&asgn.value, &row, &col_names);
1064                        row[col_indices[i]] = val;
1065                    }
1066                    self.catalog
1067                        .update_hinted(table, rid, &row, Some(&changed_cols))
1068                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1069                    count += 1;
1070                }
1071                self.view_registry.mark_dependents_dirty(table);
1072                Ok(QueryResult::Modified(count))
1073            }
1074
1075            PlanNode::Delete { input, table } => {
1076                // Mission C Phase 3: no schema clone — collect_rids_for_mutation
1077                // looks up schema internally when it needs one, and the mutation
1078                // loop doesn't need the schema at all.
1079                //
1080                // Mission C Phase 12: route bulk deletes through
1081                // `Catalog::delete_many`, which batches the btree leaf
1082                // compaction and shares one `ensure_hot` per row between
1083                // the index-key extraction and the slot delete. On
1084                // `delete_by_filter` (100K fixture, ~20K matches) that
1085                // removes ~4ms of pure `Vec::remove` memmove from the btree
1086                // maintenance phase.
1087                //
1088                // Mission C Phase 16: for the common `delete where ...`
1089                // shape (Filter(SeqScan)) — and the rarer "delete
1090                // everything" shape (SeqScan) — skip the two-pass
1091                // `collect_rids_for_mutation` + `delete_many` flow entirely.
1092                // The fused `scan_delete_matching` primitive walks the
1093                // heap exactly once, paying one `ensure_hot` per page
1094                // instead of per-row. That closes the last major gap on
1095                // the bench's `delete_by_filter` workload.
1096                if let PlanNode::Filter {
1097                    input: inner,
1098                    predicate,
1099                } = input.as_ref()
1100                {
1101                    if let PlanNode::SeqScan { table: t } = inner.as_ref() {
1102                        if t == table {
1103                            let schema = self
1104                                .catalog
1105                                .schema(table)
1106                                .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1107                            let columns: Vec<String> =
1108                                schema.columns.iter().map(|c| c.name.clone()).collect();
1109                            let fast = FastLayout::new(schema);
1110                            if let Some(compiled) =
1111                                compile_predicate(predicate, &columns, &fast, schema)
1112                            {
1113                                // Mission B2: logged variant so every
1114                                // matched rid hits the WAL during the
1115                                // single-pass scan. Structure of the
1116                                // fused scan is unchanged — only the
1117                                // hook closure now also appends.
1118                                let count = self
1119                                    .catalog
1120                                    .scan_delete_matching_logged(table, |data| compiled(data))
1121                                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
1122                                self.view_registry.mark_dependents_dirty(table);
1123                                return Ok(QueryResult::Modified(count));
1124                            }
1125                        }
1126                    }
1127                } else if let PlanNode::SeqScan { table: t } = input.as_ref() {
1128                    if t == table {
1129                        // `delete from T` with no predicate — every live
1130                        // row matches. One pass is still the right shape.
1131                        // Mission B2: logged variant — see above.
1132                        let count = self
1133                            .catalog
1134                            .scan_delete_matching_logged(table, |_| true)
1135                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
1136                        self.view_registry.mark_dependents_dirty(table);
1137                        return Ok(QueryResult::Modified(count));
1138                    }
1139                }
1140
1141                let matching_rids = self.collect_rids_for_mutation(input, table)?;
1142                let count = self
1143                    .catalog
1144                    .delete_many(table, &matching_rids)
1145                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
1146                self.view_registry.mark_dependents_dirty(table);
1147                Ok(QueryResult::Modified(count))
1148            }
1149
1150            PlanNode::AliasScan { table, alias } => {
1151                // Mission E1.2: scan `table` and rename every output column
1152                // to `alias.field`. Used as a join leaf so downstream
1153                // NestedLoopJoin + Filter + Project nodes can resolve
1154                // `Expr::QualifiedField` lookups by direct column-name match.
1155                //
1156                // We don't bother with a fused zero-copy loop here yet — the
1157                // whole join path is nested-loop and correctness-first
1158                // (Phase E1.3 will introduce hash join and at that point we
1159                // can revisit whether to specialise AliasScan).
1160                let schema = self
1161                    .catalog
1162                    .schema(table)
1163                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
1164                    .clone();
1165                let columns: Vec<String> = schema
1166                    .columns
1167                    .iter()
1168                    .map(|c| format!("{alias}.{}", c.name))
1169                    .collect();
1170                let rows: Vec<Vec<Value>> = self
1171                    .catalog
1172                    .scan(table)
1173                    .map_err(|e| QueryError::StorageError(e.to_string()))?
1174                    .map(|(_, row)| row)
1175                    .collect();
1176                Ok(QueryResult::Rows { columns, rows })
1177            }
1178
1179            PlanNode::NestedLoopJoin {
1180                left,
1181                right,
1182                on,
1183                kind,
1184            } => {
1185                // Materialise both sides. The executor ships two strategies:
1186                //   1. Hash join (E1.3) — when the `on` predicate is a
1187                //      simple equi-predicate `left_col = right_col`, build a
1188                //      FxHashMap<Value, Vec<row_idx>> over the right side
1189                //      and probe with the left side. O(L + R) instead of
1190                //      O(L × R). Handles Inner and LeftOuter.
1191                //   2. Nested loop (E1.2) — fallback for Cross, non-equi
1192                //      predicates, or `on` expressions that reference
1193                //      either side with something more complex than a
1194                //      QualifiedField.
1195                let left_result = self.execute_plan(left)?;
1196                let right_result = self.execute_plan(right)?;
1197                let (left_columns, left_rows) = match left_result {
1198                    QueryResult::Rows { columns, rows } => (columns, rows),
1199                    _ => return Err("join left side must produce rows".into()),
1200                };
1201                let (right_columns, right_rows) = match right_result {
1202                    QueryResult::Rows { columns, rows } => (columns, rows),
1203                    _ => return Err("join right side must produce rows".into()),
1204                };
1205
1206                // WS2: byte-budget guard on the join build side. Charge both
1207                // materialized inputs before we build the hash table / probe;
1208                // the output is row-capped by check_join_limit below.
1209                self.charge_rows(&left_rows)?;
1210                self.charge_rows(&right_rows)?;
1211
1212                // Hash-join fast path.
1213                if !matches!(kind, JoinKind::Cross) {
1214                    if let Some(pred) = on {
1215                        if let Some((l_idx, r_idx)) =
1216                            try_extract_equi_join_keys(pred, &left_columns, &right_columns)
1217                        {
1218                            let result = hash_join(
1219                                left_columns,
1220                                left_rows,
1221                                right_columns,
1222                                right_rows,
1223                                l_idx,
1224                                r_idx,
1225                                *kind,
1226                            );
1227                            if let QueryResult::Rows { ref rows, .. } = result {
1228                                check_join_limit(rows.len())?;
1229                            }
1230                            return Ok(result);
1231                        }
1232                    }
1233                }
1234
1235                // Nested-loop fallback.
1236                let n_left = left_columns.len();
1237                let n_right = right_columns.len();
1238                let mut columns = Vec::with_capacity(n_left + n_right);
1239                columns.extend(left_columns);
1240                columns.extend(right_columns);
1241
1242                let mut rows: Vec<Vec<Value>> = Vec::with_capacity(left_rows.len());
1243                let mut combined: Vec<Value> = Vec::with_capacity(n_left + n_right);
1244
1245                for left_row in &left_rows {
1246                    let mut matched = false;
1247                    for right_row in &right_rows {
1248                        combined.clear();
1249                        combined.extend_from_slice(left_row);
1250                        combined.extend_from_slice(right_row);
1251                        let keep = match kind {
1252                            JoinKind::Cross => true,
1253                            JoinKind::Inner | JoinKind::LeftOuter => match on {
1254                                Some(pred) => eval_predicate(pred, &combined, &columns),
1255                                // Missing `on` for non-cross joins is a
1256                                // parser error, but if it slips through we
1257                                // treat it as "match everything".
1258                                None => true,
1259                            },
1260                            // RightOuter is rewritten to LeftOuter by the
1261                            // planner, so we never see it here.
1262                            JoinKind::RightOuter => {
1263                                unreachable!("planner rewrites RightOuter to LeftOuter")
1264                            }
1265                        };
1266                        if keep {
1267                            rows.push(combined.clone());
1268                            check_join_limit(rows.len())?;
1269                            matched = true;
1270                        }
1271                    }
1272                    if !matched && matches!(kind, JoinKind::LeftOuter) {
1273                        let mut row = Vec::with_capacity(n_left + n_right);
1274                        row.extend_from_slice(left_row);
1275                        row.resize(n_left + n_right, Value::Empty);
1276                        rows.push(row);
1277                        check_join_limit(rows.len())?;
1278                    }
1279                }
1280
1281                Ok(QueryResult::Rows { columns, rows })
1282            }
1283
1284            PlanNode::Distinct { input } => {
1285                let result = self.execute_plan(input)?;
1286                match result {
1287                    QueryResult::Rows { columns, rows } => {
1288                        let mut seen = std::collections::HashSet::new();
1289                        let mut unique_rows = Vec::new();
1290                        for row in rows {
1291                            if seen.insert(row.clone()) {
1292                                unique_rows.push(row);
1293                            }
1294                        }
1295                        Ok(QueryResult::Rows {
1296                            columns,
1297                            rows: unique_rows,
1298                        })
1299                    }
1300                    other => Ok(other),
1301                }
1302            }
1303
1304            PlanNode::GroupBy {
1305                input,
1306                keys,
1307                aggregates,
1308                having,
1309            } => {
1310                let result = self.execute_plan(input)?;
1311                match result {
1312                    QueryResult::Rows { columns, rows } => {
1313                        // WS2: byte-budget guard on the GROUP BY input buffer
1314                        // (the hash table is bounded by the input it groups).
1315                        self.charge_rows(&rows)?;
1316                        // Resolve key column indices.
1317                        let key_indices: Vec<usize> = keys
1318                            .iter()
1319                            .map(|k| {
1320                                columns
1321                                    .iter()
1322                                    .position(|c| c == k)
1323                                    .ok_or_else(|| format!("group-by column '{k}' not found"))
1324                            })
1325                            .collect::<Result<Vec<_>, _>>()?;
1326
1327                        // Resolve aggregate field indices. count(*) uses
1328                        // sentinel usize::MAX — compute_group_aggregate
1329                        // treats it as "count all rows in the group".
1330                        let agg_field_indices: Vec<usize> = aggregates
1331                            .iter()
1332                            .map(|a| {
1333                                if a.field == "*" {
1334                                    Ok(usize::MAX)
1335                                } else {
1336                                    columns.iter().position(|c| c == &a.field).ok_or_else(|| {
1337                                        format!("aggregate column '{}' not found", a.field)
1338                                    })
1339                                }
1340                            })
1341                            .collect::<Result<Vec<_>, _>>()?;
1342
1343                        // Group rows by key values (preserving insertion order).
1344                        let mut group_map: rustc_hash::FxHashMap<Vec<Value>, usize> =
1345                            rustc_hash::FxHashMap::default();
1346                        let mut groups: Vec<(Vec<Value>, Vec<usize>)> = Vec::new();
1347                        for (ri, row) in rows.iter().enumerate() {
1348                            let key: Vec<Value> =
1349                                key_indices.iter().map(|&i| row[i].clone()).collect();
1350                            match group_map.get(&key) {
1351                                Some(&idx) => groups[idx].1.push(ri),
1352                                None => {
1353                                    let idx = groups.len();
1354                                    group_map.insert(key.clone(), idx);
1355                                    groups.push((key, vec![ri]));
1356                                }
1357                            }
1358                        }
1359
1360                        // Build output column names: keys ++ aggregate output names.
1361                        let mut out_columns: Vec<String> = keys.clone();
1362                        for agg in aggregates.iter() {
1363                            out_columns.push(agg.output_name.clone());
1364                        }
1365
1366                        // Compute aggregates per group.
1367                        let mut out_rows: Vec<Vec<Value>> = Vec::with_capacity(groups.len());
1368                        for (key_vals, row_indices) in &groups {
1369                            let mut row = key_vals.clone();
1370                            for (ai, agg) in aggregates.iter().enumerate() {
1371                                let col_idx = agg_field_indices[ai];
1372                                let val = compute_group_aggregate(
1373                                    agg.function,
1374                                    &rows,
1375                                    row_indices,
1376                                    col_idx,
1377                                );
1378                                row.push(val);
1379                            }
1380                            out_rows.push(row);
1381                        }
1382
1383                        // Apply HAVING filter.
1384                        if let Some(having_expr) = having {
1385                            out_rows.retain(|row| eval_predicate(having_expr, row, &out_columns));
1386                        }
1387
1388                        Ok(QueryResult::Rows {
1389                            columns: out_columns,
1390                            rows: out_rows,
1391                        })
1392                    }
1393                    _ => Err("group by requires row input".into()),
1394                }
1395            }
1396
1397            PlanNode::CreateTable { name, fields } => {
1398                let columns: Vec<ColumnDef> = fields
1399                    .iter()
1400                    .enumerate()
1401                    .map(|(i, f)| -> Result<ColumnDef, QueryError> {
1402                        Ok(ColumnDef {
1403                            name: f.name.clone(),
1404                            type_id: type_name_to_id(&f.type_name)
1405                                .map_err(QueryError::TypeError)?,
1406                            required: f.required,
1407                            position: i as u16,
1408                        })
1409                    })
1410                    .collect::<Result<Vec<_>, _>>()?;
1411                let schema = Schema {
1412                    table_name: name.clone(),
1413                    columns,
1414                };
1415                self.catalog
1416                    .create_table(schema)
1417                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
1418                // Declaring a field `unique` auto-creates a unique B+tree
1419                // index, which is where uniqueness is enforced on writes.
1420                for f in fields.iter().filter(|f| f.unique) {
1421                    self.catalog
1422                        .create_index_unique(name, &f.name, true)
1423                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1424                }
1425                Ok(QueryResult::Created(name.clone()))
1426            }
1427
1428            PlanNode::AlterTable { table, action } => match action {
1429                AlterAction::AddColumn {
1430                    name,
1431                    type_name,
1432                    required,
1433                } => {
1434                    let position = self
1435                        .catalog
1436                        .schema(table)
1437                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
1438                        .columns
1439                        .len() as u16;
1440                    let col = ColumnDef {
1441                        name: name.clone(),
1442                        type_id: type_name_to_id(type_name).map_err(QueryError::TypeError)?,
1443                        required: *required,
1444                        position,
1445                    };
1446                    self.catalog
1447                        .alter_table_add_column(table, col)
1448                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1449                    Ok(QueryResult::Executed {
1450                        message: format!("column '{name}' added to '{table}'"),
1451                    })
1452                }
1453                AlterAction::DropColumn { name } => {
1454                    self.catalog
1455                        .alter_table_drop_column(table, name)
1456                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1457                    Ok(QueryResult::Executed {
1458                        message: format!("column '{name}' dropped from '{table}'"),
1459                    })
1460                }
1461                AlterAction::AddIndex { column } => {
1462                    self.catalog
1463                        .create_index(table, column)
1464                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1465                    Ok(QueryResult::Executed {
1466                        message: format!("index on '{table}.{column}' created"),
1467                    })
1468                }
1469                AlterAction::AddUnique { column } => {
1470                    // No DropIndex exists, so we cannot upgrade an existing
1471                    // non-unique index in place — reject it cleanly.
1472                    if self.catalog.has_index(table, column) {
1473                        return Err(QueryError::Execution(format!(
1474                            "cannot add unique on {table}.{column}: column already indexed"
1475                        )));
1476                    }
1477                    // Scan existing rows for duplicate (non-null) values
1478                    // before creating the unique index.
1479                    {
1480                        let tbl = self
1481                            .catalog
1482                            .get_table(table)
1483                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1484                        let col_idx = tbl.schema.column_index(column).ok_or_else(|| {
1485                            QueryError::ColumnNotFound {
1486                                table: table.to_string(),
1487                                column: column.clone(),
1488                            }
1489                        })?;
1490                        let mut seen = std::collections::HashSet::new();
1491                        for (_, row) in tbl.scan() {
1492                            let v = &row[col_idx];
1493                            if v.is_empty() {
1494                                continue;
1495                            }
1496                            if !seen.insert(v.clone()) {
1497                                return Err(QueryError::Execution(format!(
1498                                    "cannot add unique on {table}.{column}: \
1499                                     duplicate value {v:?} exists"
1500                                )));
1501                            }
1502                        }
1503                    }
1504                    self.catalog
1505                        .create_index_unique(table, column, true)
1506                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1507                    Ok(QueryResult::Executed {
1508                        message: format!("unique index on '{table}.{column}' created"),
1509                    })
1510                }
1511            },
1512
1513            PlanNode::DropTable { name } => {
1514                self.catalog
1515                    .drop_table(name)
1516                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
1517                Ok(QueryResult::Executed {
1518                    message: format!("table '{name}' dropped"),
1519                })
1520            }
1521
1522            PlanNode::CreateView { name, query_text } => {
1523                self.create_view(name, query_text)?;
1524                Ok(QueryResult::Executed {
1525                    message: format!("materialized view '{name}' created"),
1526                })
1527            }
1528
1529            PlanNode::RefreshView { name } => {
1530                self.refresh_view(name)?;
1531                Ok(QueryResult::Executed {
1532                    message: format!("materialized view '{name}' refreshed"),
1533                })
1534            }
1535
1536            PlanNode::DropView { name } => {
1537                self.drop_view(name)?;
1538                Ok(QueryResult::Executed {
1539                    message: format!("materialized view '{name}' dropped"),
1540                })
1541            }
1542
1543            PlanNode::Window { input, windows } => {
1544                let result = self.execute_plan(input)?;
1545                execute_window(result, windows)
1546            }
1547
1548            PlanNode::Union { left, right, all } => {
1549                let left_result = self.execute_plan(left)?;
1550                let right_result = self.execute_plan(right)?;
1551                let (left_cols, left_rows) = match left_result {
1552                    QueryResult::Rows { columns, rows } => (columns, rows),
1553                    _ => return Err("UNION requires query results on left side".into()),
1554                };
1555                let (_, right_rows) = match right_result {
1556                    QueryResult::Rows { columns, rows } => (columns, rows),
1557                    _ => return Err("UNION requires query results on right side".into()),
1558                };
1559                let mut combined = left_rows;
1560                if *all {
1561                    // UNION ALL — just concatenate.
1562                    combined.extend(right_rows);
1563                } else {
1564                    // UNION — deduplicate using the same HashSet approach
1565                    // as DISTINCT. Value already implements Hash + Eq.
1566                    let mut seen = std::collections::HashSet::new();
1567                    for row in &combined {
1568                        seen.insert(row.clone());
1569                    }
1570                    for row in right_rows {
1571                        if seen.insert(row.clone()) {
1572                            combined.push(row);
1573                        }
1574                    }
1575                }
1576                Ok(QueryResult::Rows {
1577                    columns: left_cols,
1578                    rows: combined,
1579                })
1580            }
1581
1582            PlanNode::Explain { input } => {
1583                let text = format_plan_tree(input, 0);
1584                Ok(QueryResult::Rows {
1585                    columns: vec!["plan".to_string()],
1586                    rows: text
1587                        .lines()
1588                        .map(|line| vec![Value::Str(line.to_string())])
1589                        .collect(),
1590                })
1591            }
1592
1593            PlanNode::Begin => {
1594                if self.in_transaction {
1595                    return Err(QueryError::Execution(
1596                        "already in a transaction (nested transactions not supported)".into(),
1597                    ));
1598                }
1599                self.catalog
1600                    .begin_transaction()
1601                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
1602                self.in_transaction = true;
1603                Ok(QueryResult::Executed {
1604                    message: "transaction started".to_string(),
1605                })
1606            }
1607
1608            PlanNode::Commit => {
1609                if !self.in_transaction {
1610                    return Err(QueryError::Execution(
1611                        "no active transaction to commit".into(),
1612                    ));
1613                }
1614                self.catalog
1615                    .commit_transaction()
1616                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
1617                self.in_transaction = false;
1618                Ok(QueryResult::Executed {
1619                    message: "transaction committed".to_string(),
1620                })
1621            }
1622
1623            PlanNode::Rollback => {
1624                if !self.in_transaction {
1625                    return Err(QueryError::Execution(
1626                        "no active transaction to roll back".into(),
1627                    ));
1628                }
1629                self.in_transaction = false;
1630                self.catalog
1631                    .rollback_to_last_sync()
1632                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
1633                if let Ok(mut cache) = self.plan_cache.lock() {
1634                    cache.clear();
1635                }
1636                self.view_registry = ViewRegistry::open(self.catalog.data_dir())
1637                    .unwrap_or_else(|_| ViewRegistry::new(self.catalog.data_dir()));
1638                Ok(QueryResult::Executed {
1639                    message: "transaction rolled back".to_string(),
1640                })
1641            }
1642
1643            PlanNode::IndexScan { table, column, key } => {
1644                let key_value = literal_to_value(key)?;
1645                let tbl = self
1646                    .catalog
1647                    .get_table(table)
1648                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1649                let columns: Vec<String> =
1650                    tbl.schema.columns.iter().map(|c| c.name.clone()).collect();
1651
1652                // Fast path: the table has a B-tree on this column.
1653                // Uses index_lookup_all to return ALL matching rows for
1654                // both unique and non-unique indexes.
1655                if tbl.has_index(column) {
1656                    let rids = tbl.index_lookup_all(column, &key_value);
1657                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(rids.len());
1658                    for rid in rids {
1659                        if let Some(data) = tbl.heap.get(rid) {
1660                            rows.push(decode_row(&tbl.schema, &data));
1661                        }
1662                    }
1663                    return Ok(QueryResult::Rows { columns, rows });
1664                }
1665
1666                // Fallback: no index on this column. The planner emits IndexScan
1667                // eagerly (it has no visibility into which columns are indexed
1668                // at plan time), so here we must behave like SeqScan+Filter on
1669                // `.col = literal`: return *all* matching rows, not just the
1670                // first one. A non-indexed column isn't necessarily unique.
1671                // We compile the eq predicate once and stream without any
1672                // per-row decode for non-matching rows.
1673                let schema = &tbl.schema;
1674                let fast = FastLayout::new(schema);
1675                let synth_pred = Expr::BinaryOp(
1676                    Box::new(Expr::Field(column.clone())),
1677                    BinOp::Eq,
1678                    Box::new(key.clone()),
1679                );
1680                if let Some(compiled) = compile_predicate(&synth_pred, &columns, &fast, schema) {
1681                    // Mission F: skip the first 4 Vec doublings.
1682                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
1683                    self.catalog
1684                        .for_each_row_raw(table, |_rid, data| {
1685                            if compiled(data) {
1686                                rows.push(decode_row(schema, data));
1687                            }
1688                        })
1689                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1690                    return Ok(QueryResult::Rows { columns, rows });
1691                }
1692
1693                // Last resort: slow eq-check on materialised rows.
1694                let col_idx =
1695                    schema
1696                        .column_index(column)
1697                        .ok_or_else(|| QueryError::ColumnNotFound {
1698                            table: String::new(),
1699                            column: column.clone(),
1700                        })?;
1701                let rows: Vec<Vec<Value>> = tbl
1702                    .scan()
1703                    .filter_map(|(_, row)| {
1704                        if row[col_idx] == key_value {
1705                            Some(row)
1706                        } else {
1707                            None
1708                        }
1709                    })
1710                    .collect();
1711                Ok(QueryResult::Rows { columns, rows })
1712            }
1713
1714            PlanNode::RangeScan {
1715                table,
1716                column,
1717                start,
1718                end,
1719            } => {
1720                let tbl = self
1721                    .catalog
1722                    .get_table(table)
1723                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1724                let columns: Vec<String> =
1725                    tbl.schema.columns.iter().map(|c| c.name.clone()).collect();
1726                let schema = &tbl.schema;
1727
1728                let start_val = match start {
1729                    Some((expr, _)) => Some(literal_to_value(expr)?),
1730                    None => None,
1731                };
1732                let end_val = match end {
1733                    Some((expr, _)) => Some(literal_to_value(expr)?),
1734                    None => None,
1735                };
1736                let start_inclusive = start.as_ref().map(|(_, inc)| *inc).unwrap_or(true);
1737                let end_inclusive = end.as_ref().map(|(_, inc)| *inc).unwrap_or(true);
1738
1739                // Non-unique index: walk the composite (value, rid) leaf
1740                // chain between prefix bounds, fetch each row from the heap,
1741                // and recheck. The recheck enforces exclusive bounds
1742                // (range_rids is inclusive) and defensively skips any decoded
1743                // null (nulls are never indexed, so they must not match).
1744                if tbl.is_index_unique(column) == Some(false) {
1745                    if let Some(btree) = tbl.index(column) {
1746                        if start_val.is_some() || end_val.is_some() {
1747                            let col_idx = schema.column_index(column).ok_or_else(|| {
1748                                QueryError::ColumnNotFound {
1749                                    table: String::new(),
1750                                    column: column.clone(),
1751                                }
1752                            })?;
1753                            let rids = btree.range_rids(start_val.as_ref(), end_val.as_ref());
1754                            let mut rows: Vec<Vec<Value>> = Vec::with_capacity(rids.len());
1755                            for rid in rids {
1756                                if let Some(data) = tbl.heap.get(rid) {
1757                                    let row = decode_row(schema, &data);
1758                                    if !row[col_idx].is_empty()
1759                                        && range_matches(
1760                                            &row[col_idx],
1761                                            &start_val,
1762                                            start_inclusive,
1763                                            &end_val,
1764                                            end_inclusive,
1765                                        )
1766                                    {
1767                                        rows.push(row);
1768                                    }
1769                                }
1770                            }
1771                            return Ok(QueryResult::Rows { columns, rows });
1772                        }
1773                    }
1774                }
1775
1776                // Range scans use the btree fast path for unique indexes,
1777                // walking raw column-value keys directly.
1778                if tbl.is_index_unique(column) == Some(true) {
1779                    if let Some(btree) = tbl.index(column) {
1780                        let hits: Vec<(Value, RowId)> = match (&start_val, &end_val) {
1781                            (Some(s), Some(e)) => btree.range(s, e).collect(),
1782                            (Some(s), None) => btree.range_from(s),
1783                            (None, Some(e)) => btree.range_to(e),
1784                            (None, None) => {
1785                                let rows: Vec<Vec<Value>> =
1786                                    tbl.scan().map(|(_, row)| row).collect();
1787                                return Ok(QueryResult::Rows { columns, rows });
1788                            }
1789                        };
1790                        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(hits.len());
1791                        for (key, rid) in hits {
1792                            if !start_inclusive {
1793                                if let Some(ref s) = start_val {
1794                                    if &key == s {
1795                                        continue;
1796                                    }
1797                                }
1798                            }
1799                            if !end_inclusive {
1800                                if let Some(ref e) = end_val {
1801                                    if &key == e {
1802                                        continue;
1803                                    }
1804                                }
1805                            }
1806                            if let Some(data) = tbl.heap.get(rid) {
1807                                rows.push(decode_row(schema, &data));
1808                            }
1809                        }
1810                        return Ok(QueryResult::Rows { columns, rows });
1811                    }
1812                }
1813
1814                // Fallback: no index — synthesize range predicate and scan.
1815                let fast = FastLayout::new(schema);
1816                let synth = synthesize_range_predicate(column, start, end);
1817                if let Some(compiled) = compile_predicate(&synth, &columns, &fast, schema) {
1818                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
1819                    self.catalog
1820                        .for_each_row_raw(table, |_rid, data| {
1821                            if compiled(data) {
1822                                rows.push(decode_row(schema, data));
1823                            }
1824                        })
1825                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1826                    return Ok(QueryResult::Rows { columns, rows });
1827                }
1828
1829                let col_idx =
1830                    schema
1831                        .column_index(column)
1832                        .ok_or_else(|| QueryError::ColumnNotFound {
1833                            table: String::new(),
1834                            column: column.clone(),
1835                        })?;
1836                let rows: Vec<Vec<Value>> = tbl
1837                    .scan()
1838                    .filter(|(_, row)| {
1839                        range_matches(
1840                            &row[col_idx],
1841                            &start_val,
1842                            start_inclusive,
1843                            &end_val,
1844                            end_inclusive,
1845                        )
1846                    })
1847                    .map(|(_, row)| row)
1848                    .collect();
1849                Ok(QueryResult::Rows { columns, rows })
1850            }
1851        }
1852    }
1853
1854    // ─── Materialized view operations ──────────────────────────────────────
1855
1856    /// Create a materialized view: execute the source query, store results
1857    /// in a new backing table, and register the view.
1858    fn create_view(&mut self, name: &str, query_text: &str) -> Result<(), QueryError> {
1859        if self.view_registry.is_view(name) {
1860            return Err(QueryError::ViewError(format!(
1861                "materialized view '{name}' already exists"
1862            )));
1863        }
1864        // Execute the source query to get the result set.
1865        let result = self.execute_powql(query_text)?;
1866        let (columns, rows) = match result {
1867            QueryResult::Rows { columns, rows } => (columns, rows),
1868            _ => return Err("view source query must be a SELECT".into()),
1869        };
1870        // Derive a schema for the backing table from the query result columns.
1871        let schema = self.derive_view_schema(name, &columns, &rows);
1872        // Create the backing table and insert the result rows.
1873        self.catalog
1874            .create_table(schema)
1875            .map_err(|e| QueryError::StorageError(e.to_string()))?;
1876        for row in &rows {
1877            self.catalog
1878                .insert(name, row)
1879                .map_err(|e| QueryError::StorageError(e.to_string()))?;
1880        }
1881        // Determine which base tables this view depends on by parsing the query.
1882        let depends_on = self.extract_view_deps(query_text);
1883        self.view_registry
1884            .register(ViewDef {
1885                name: name.to_string(),
1886                query: query_text.to_string(),
1887                depends_on,
1888                dirty: false,
1889            })
1890            .map_err(|e| QueryError::StorageError(e.to_string()))?;
1891        Ok(())
1892    }
1893
1894    /// Refresh a materialized view: re-execute its source query and replace
1895    /// the backing table's contents.
1896    fn refresh_view(&mut self, name: &str) -> Result<(), QueryError> {
1897        let def = self
1898            .view_registry
1899            .get(name)
1900            .ok_or_else(|| format!("materialized view '{name}' not found"))?;
1901        let query_text = def.query.clone();
1902        // Execute the source query.
1903        let result = self.execute_powql(&query_text)?;
1904        let (_columns, rows) = match result {
1905            QueryResult::Rows { columns, rows } => (columns, rows),
1906            _ => return Err("view source query must be a SELECT".into()),
1907        };
1908        // Clear old data and insert fresh results. Mission B2: logged
1909        // variant — view refreshes are a mutation and crash recovery
1910        // must see them.
1911        self.catalog
1912            .scan_delete_matching_logged(name, |_| true)
1913            .map_err(|e| QueryError::StorageError(e.to_string()))?;
1914        for row in &rows {
1915            self.catalog
1916                .insert(name, row)
1917                .map_err(|e| QueryError::StorageError(e.to_string()))?;
1918        }
1919        self.view_registry.mark_clean(name);
1920        Ok(())
1921    }
1922
1923    /// Drop a materialized view: remove the backing table and unregister.
1924    fn drop_view(&mut self, name: &str) -> Result<(), QueryError> {
1925        if !self.view_registry.is_view(name) {
1926            return Err(QueryError::ViewError(format!(
1927                "materialized view '{name}' not found"
1928            )));
1929        }
1930        self.view_registry
1931            .unregister(name)
1932            .map_err(|e| QueryError::StorageError(e.to_string()))?;
1933        self.catalog
1934            .drop_table(name)
1935            .map_err(|e| QueryError::StorageError(e.to_string()))?;
1936        Ok(())
1937    }
1938
1939    /// Derive a storage `Schema` for a view's backing table from query
1940    /// result column names and the first row's types.
1941    fn derive_view_schema(&self, name: &str, columns: &[String], rows: &[Vec<Value>]) -> Schema {
1942        use powdb_storage::types::{ColumnDef, TypeId};
1943        let cols: Vec<ColumnDef> = columns
1944            .iter()
1945            .enumerate()
1946            .map(|(i, col_name)| {
1947                let type_id = rows
1948                    .first()
1949                    .and_then(|row| row.get(i))
1950                    .map(|v| v.type_id())
1951                    .unwrap_or(TypeId::Str);
1952                ColumnDef {
1953                    name: col_name.clone(),
1954                    type_id,
1955                    required: false,
1956                    position: i as u16,
1957                }
1958            })
1959            .collect();
1960        Schema {
1961            table_name: name.to_string(),
1962            columns: cols,
1963        }
1964    }
1965
1966    /// Extract base table dependencies from a view's source query by
1967    /// parsing it and collecting the source table name.
1968    fn extract_view_deps(&self, query_text: &str) -> Vec<String> {
1969        use crate::parser::parse;
1970        match parse(query_text) {
1971            Ok(Statement::Query(q)) => {
1972                let mut deps = vec![q.source.clone()];
1973                for j in &q.joins {
1974                    deps.push(j.source.clone());
1975                }
1976                deps
1977            }
1978            _ => Vec::new(),
1979        }
1980    }
1981
1982    // ─── Specialized fast paths ─────────────────────────────────────────────
1983    //
1984    // These methods are helpers for the `execute_plan` match arms above.
1985    // Each returns `Ok(Some(result))` when the fast path fires, `Ok(None)`
1986    // when the shape isn't supported (caller falls back to generic code).
1987
1988    /// Aggregate sum/avg/min/max over a single fixed-size i64 column, with
1989    /// an optional compiled filter predicate. Walks raw row bytes — zero
1990    /// per-row allocation. Uses i128 accumulator for sum/avg overflow safety.
1991    pub(super) fn agg_single_col_fast(
1992        &self,
1993        table: &str,
1994        col: &str,
1995        function: AggFunc,
1996        predicate: Option<&Expr>,
1997    ) -> Result<Option<QueryResult>, QueryError> {
1998        let schema = self
1999            .catalog
2000            .schema(table)
2001            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
2002            .clone();
2003        let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
2004        let col_idx = match schema.column_index(col) {
2005            Some(i) => i,
2006            None => return Ok(None),
2007        };
2008        // Only fast-path fixed-size numeric columns (Int/Float) for
2009        // sum/avg/min/max/count. Mission D10: Float parity — prior version
2010        // bailed on Float columns, forcing them through the generic row-
2011        // decoding path that allocated a Vec<Value> per row and dispatched
2012        // on Value::cmp for every compare. f64 decode is structurally the
2013        // same as i64 (load 8 bytes, cast), so the fast path handles both.
2014        let col_type = schema.columns[col_idx].type_id;
2015        if col_type != TypeId::Int && col_type != TypeId::Float {
2016            return Ok(None);
2017        }
2018
2019        let fast = FastLayout::new(&schema);
2020        // Mission C Phase 20b: inline the numeric-column reader instead of
2021        // building a `Box<dyn Fn>`. Eliminates 100K vtable dispatches per
2022        // 100K-row agg scan — every reader call folds directly into the
2023        // hot loop below.
2024        let byte_offset = match fast.fixed_offsets[col_idx] {
2025            Some(o) => o,
2026            None => return Ok(None),
2027        };
2028        let bitmap_byte = col_idx / 8;
2029        let bitmap_bit = (col_idx % 8) as u32;
2030        let body_data_offset = 2 + fast.bitmap_size + byte_offset;
2031
2032        // Optional compiled filter.
2033        let compiled_pred: Option<CompiledPredicate> = match predicate {
2034            Some(pred) => match compile_predicate(pred, &columns, &fast, &schema) {
2035                Some(c) => Some(c),
2036                None => return Ok(None), // let generic path handle it
2037            },
2038            None => None,
2039        };
2040
2041        // Mission C Phase 20b: specialize the inner loop per aggregate
2042        // function. The previous version ran a `match function { ... }`
2043        // *inside* the closure, which kept LLVM from producing optimal
2044        // scalar code for each variant (agg_max regressed ~23% vs the
2045        // baseline Box<dyn Fn> version even though per-row vtable cost
2046        // should have been strictly lower). Pushing the match out of the
2047        // hot loop lets each specialized body fold cleanly into
2048        // `for_each_row_raw` and removes a captured `AggFunc` + match
2049        // dispatch per row.
2050        //
2051        // Mission D10: same specialisation applies to the Float branch.
2052        // For Min/Max we use `f64::total_cmp` so the result matches
2053        // `Value::Ord` — this is the same ordering ORDER BY and the
2054        // top-N sort fast path use, keeping semantics consistent across
2055        // read paths (NaN compares as greatest, -0.0 < +0.0 for
2056        // deterministic tie-breaking).
2057        //
2058        // Mission D11 Phase 1: each inner loop now splits on presence of
2059        // a predicate (`if let Some(pred) = &compiled_pred`) so the hot
2060        // body never re-tests `Option` per row, and reads column bytes
2061        // via `read_i64_unchecked` / `read_f64_unchecked` helpers that
2062        // drop two bounds checks per row (null bitmap byte + value
2063        // slice). Safety is carried by the `FastLayout` invariant that
2064        // `data_offset + 8 <= row_len` for any fixed-size column; see
2065        // the helper doc comments. Hot loops are macro-generated so the
2066        // with-pred / no-pred split can't drift between variants.
2067        let result = match col_type {
2068            TypeId::Int => match function {
2069                AggFunc::Sum | AggFunc::Avg => {
2070                    let mut sum_i128: i128 = 0;
2071                    let mut count: i64 = 0;
2072                    agg_int_loop!(
2073                        self,
2074                        table,
2075                        compiled_pred,
2076                        bitmap_byte,
2077                        bitmap_bit,
2078                        body_data_offset,
2079                        |v: i64| {
2080                            count += 1;
2081                            sum_i128 += v as i128;
2082                        }
2083                    );
2084                    if matches!(function, AggFunc::Sum) {
2085                        let clamped = sum_i128.clamp(i64::MIN as i128, i64::MAX as i128) as i64;
2086                        QueryResult::Scalar(Value::Int(clamped))
2087                    } else if count == 0 {
2088                        QueryResult::Scalar(Value::Empty)
2089                    } else {
2090                        let avg = (sum_i128 as f64) / (count as f64);
2091                        QueryResult::Scalar(Value::Float(avg))
2092                    }
2093                }
2094                AggFunc::Min => {
2095                    let mut min_v: Option<i64> = None;
2096                    agg_int_loop!(
2097                        self,
2098                        table,
2099                        compiled_pred,
2100                        bitmap_byte,
2101                        bitmap_bit,
2102                        body_data_offset,
2103                        |v: i64| {
2104                            min_v = Some(match min_v {
2105                                Some(m) => m.min(v),
2106                                None => v,
2107                            });
2108                        }
2109                    );
2110                    QueryResult::Scalar(min_v.map(Value::Int).unwrap_or(Value::Empty))
2111                }
2112                AggFunc::Max => {
2113                    let mut max_v: Option<i64> = None;
2114                    agg_int_loop!(
2115                        self,
2116                        table,
2117                        compiled_pred,
2118                        bitmap_byte,
2119                        bitmap_bit,
2120                        body_data_offset,
2121                        |v: i64| {
2122                            max_v = Some(match max_v {
2123                                Some(m) => m.max(v),
2124                                None => v,
2125                            });
2126                        }
2127                    );
2128                    QueryResult::Scalar(max_v.map(Value::Int).unwrap_or(Value::Empty))
2129                }
2130                AggFunc::Count => {
2131                    let mut count: i64 = 0;
2132                    agg_int_loop!(
2133                        self,
2134                        table,
2135                        compiled_pred,
2136                        bitmap_byte,
2137                        bitmap_bit,
2138                        body_data_offset,
2139                        |_v: i64| {
2140                            count += 1;
2141                        }
2142                    );
2143                    QueryResult::Scalar(Value::Int(count))
2144                }
2145                AggFunc::CountDistinct => {
2146                    let mut seen = rustc_hash::FxHashSet::default();
2147                    agg_int_loop!(
2148                        self,
2149                        table,
2150                        compiled_pred,
2151                        bitmap_byte,
2152                        bitmap_bit,
2153                        body_data_offset,
2154                        |v: i64| {
2155                            seen.insert(v);
2156                        }
2157                    );
2158                    QueryResult::Scalar(Value::Int(seen.len() as i64))
2159                }
2160            },
2161            TypeId::Float => match function {
2162                AggFunc::Sum => {
2163                    // Use a single f64 accumulator. Naive summation is
2164                    // sufficient for MVP parity; if precision becomes an
2165                    // issue on long scans we can upgrade to Kahan–Neumaier
2166                    // compensated sum (~2x scalar cost, zero error growth).
2167                    let mut sum: f64 = 0.0;
2168                    agg_float_loop!(
2169                        self,
2170                        table,
2171                        compiled_pred,
2172                        bitmap_byte,
2173                        bitmap_bit,
2174                        body_data_offset,
2175                        |v: f64| {
2176                            sum += v;
2177                        }
2178                    );
2179                    QueryResult::Scalar(Value::Float(sum))
2180                }
2181                AggFunc::Avg => {
2182                    let mut sum: f64 = 0.0;
2183                    let mut count: i64 = 0;
2184                    agg_float_loop!(
2185                        self,
2186                        table,
2187                        compiled_pred,
2188                        bitmap_byte,
2189                        bitmap_bit,
2190                        body_data_offset,
2191                        |v: f64| {
2192                            sum += v;
2193                            count += 1;
2194                        }
2195                    );
2196                    if count == 0 {
2197                        QueryResult::Scalar(Value::Empty)
2198                    } else {
2199                        QueryResult::Scalar(Value::Float(sum / count as f64))
2200                    }
2201                }
2202                AggFunc::Min => {
2203                    // `total_cmp` for deterministic NaN handling (matches
2204                    // Value::Ord). NaN compares greatest, so Min will
2205                    // correctly ignore it in favour of any finite value.
2206                    let mut min_v: Option<f64> = None;
2207                    agg_float_loop!(
2208                        self,
2209                        table,
2210                        compiled_pred,
2211                        bitmap_byte,
2212                        bitmap_bit,
2213                        body_data_offset,
2214                        |v: f64| {
2215                            min_v = Some(match min_v {
2216                                Some(m) => {
2217                                    if v.total_cmp(&m).is_lt() {
2218                                        v
2219                                    } else {
2220                                        m
2221                                    }
2222                                }
2223                                None => v,
2224                            });
2225                        }
2226                    );
2227                    QueryResult::Scalar(min_v.map(Value::Float).unwrap_or(Value::Empty))
2228                }
2229                AggFunc::Max => {
2230                    let mut max_v: Option<f64> = None;
2231                    agg_float_loop!(
2232                        self,
2233                        table,
2234                        compiled_pred,
2235                        bitmap_byte,
2236                        bitmap_bit,
2237                        body_data_offset,
2238                        |v: f64| {
2239                            max_v = Some(match max_v {
2240                                Some(m) => {
2241                                    if v.total_cmp(&m).is_gt() {
2242                                        v
2243                                    } else {
2244                                        m
2245                                    }
2246                                }
2247                                None => v,
2248                            });
2249                        }
2250                    );
2251                    QueryResult::Scalar(max_v.map(Value::Float).unwrap_or(Value::Empty))
2252                }
2253                AggFunc::Count => {
2254                    let mut count: i64 = 0;
2255                    agg_float_loop!(
2256                        self,
2257                        table,
2258                        compiled_pred,
2259                        bitmap_byte,
2260                        bitmap_bit,
2261                        body_data_offset,
2262                        |_v: f64| {
2263                            count += 1;
2264                        }
2265                    );
2266                    QueryResult::Scalar(Value::Int(count))
2267                }
2268                AggFunc::CountDistinct => {
2269                    // Hash on `f64::to_bits` — matches `Value::Hash`, so
2270                    // distinct NaN bit patterns count as distinct and
2271                    // -0.0/+0.0 count as distinct. Consistent with how
2272                    // Float values are hashed in every other DISTINCT /
2273                    // GROUP BY path.
2274                    let mut seen = rustc_hash::FxHashSet::default();
2275                    agg_float_loop!(
2276                        self,
2277                        table,
2278                        compiled_pred,
2279                        bitmap_byte,
2280                        bitmap_bit,
2281                        body_data_offset,
2282                        |v: f64| {
2283                            seen.insert(v.to_bits());
2284                        }
2285                    );
2286                    QueryResult::Scalar(Value::Int(seen.len() as i64))
2287                }
2288            },
2289            _ => unreachable!("type guard above restricts to Int/Float"),
2290        };
2291        Ok(Some(result))
2292    }
2293
2294    /// `Project(Limit(Filter(SeqScan)))` and `Project(Limit(SeqScan))`.
2295    /// Streams rows, decodes only projected columns, stops at the limit.
2296    pub(super) fn project_filter_limit_fast(
2297        &self,
2298        table: &str,
2299        fields: &[ProjectField],
2300        limit: usize,
2301        predicate: Option<&Expr>,
2302    ) -> Result<Option<QueryResult>, QueryError> {
2303        let schema = self
2304            .catalog
2305            .schema(table)
2306            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
2307            .clone();
2308        let all_columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
2309
2310        // Each projection field must be a simple `.field` reference for this
2311        // fast path. Aliased or computed fields fall through.
2312        let mut proj_indices: Vec<usize> = Vec::with_capacity(fields.len());
2313        let mut proj_columns: Vec<String> = Vec::with_capacity(fields.len());
2314        for f in fields {
2315            let name = match &f.expr {
2316                Expr::Field(n) => n.clone(),
2317                _ => return Ok(None),
2318            };
2319            let idx = match all_columns.iter().position(|c| c == &name) {
2320                Some(i) => i,
2321                None => return Ok(None),
2322            };
2323            proj_indices.push(idx);
2324            proj_columns.push(f.alias.clone().unwrap_or(name));
2325        }
2326
2327        let fast = FastLayout::new(&schema);
2328        let row_layout = RowLayout::new(&schema);
2329
2330        let compiled_pred: Option<CompiledPredicate> = match predicate {
2331            Some(pred) => match compile_predicate(pred, &all_columns, &fast, &schema) {
2332                Some(c) => Some(c),
2333                None => return Ok(None),
2334            },
2335            None => None,
2336        };
2337
2338        let mut out: Vec<Vec<Value>> = Vec::with_capacity(limit.min(1024));
2339        // Mission D2: use try_for_each_row_raw to actually stop iterating
2340        // once the limit is reached. The previous `done` flag only short-
2341        // circuited the closure body, so a `limit 100` over 100K rows still
2342        // walked all 100K slots — burning ~30x SQLite on scan_filter_project_top100.
2343        self.catalog
2344            .try_for_each_row_raw(table, |_rid, data| {
2345                use std::ops::ControlFlow;
2346                if let Some(ref pred) = compiled_pred {
2347                    if !pred(data) {
2348                        return ControlFlow::Continue(());
2349                    }
2350                }
2351                let row: Vec<Value> = proj_indices
2352                    .iter()
2353                    .map(|&ci| decode_column(&schema, &row_layout, data, ci))
2354                    .collect();
2355                out.push(row);
2356                if out.len() >= limit {
2357                    ControlFlow::Break(())
2358                } else {
2359                    ControlFlow::Continue(())
2360                }
2361            })
2362            .map_err(|e| QueryError::StorageError(e.to_string()))?;
2363
2364        Ok(Some(QueryResult::Rows {
2365            columns: proj_columns,
2366            rows: out,
2367        }))
2368    }
2369
2370    /// `Project(Limit(Sort(Filter(SeqScan))))` and `Project(Limit(Sort(SeqScan)))`.
2371    /// Bounded top-N heap over the sort key. Only the sort key needs to be
2372    /// read per row; projected columns are decoded only for the final
2373    /// winning rows when the heap drains.
2374    pub(super) fn project_filter_sort_limit_fast(
2375        &self,
2376        table: &str,
2377        fields: &[ProjectField],
2378        sort_field: &str,
2379        descending: bool,
2380        limit: usize,
2381        predicate: Option<&Expr>,
2382    ) -> Result<Option<QueryResult>, QueryError> {
2383        if limit == 0 {
2384            // Degenerate case — empty result. Let the generic path handle it
2385            // for proper column naming.
2386            return Ok(None);
2387        }
2388        // The top-N heaps never hold more than `limit` rows, but `limit` is an
2389        // attacker-supplied literal (`order .x limit 99999999999`). Reserving
2390        // that capacity up front would allocate gigabytes and abort the
2391        // process before a single row is read. Cap the pre-allocation; the
2392        // heaps still grow on demand up to the true `limit`.
2393        const TOPN_PREALLOC_CAP: usize = 4096;
2394        let prealloc = limit.min(TOPN_PREALLOC_CAP);
2395        let schema = self
2396            .catalog
2397            .schema(table)
2398            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
2399            .clone();
2400        let all_columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
2401
2402        // Sort key must be a fixed-size numeric column (Int or Float).
2403        // Mission D10: extended from Int-only. Float sort keys use a
2404        // sortable-u64 transform (see `f64_to_sortable_u64`) so the heap
2405        // path stays keyed on `u64` and the whole branch shape is
2406        // identical to the Int case — no new heap types, no `total_cmp`
2407        // closures in the hot loop.
2408        let sort_idx = match schema.column_index(sort_field) {
2409            Some(i) => i,
2410            None => return Ok(None),
2411        };
2412        let sort_col_type = schema.columns[sort_idx].type_id;
2413        if sort_col_type != TypeId::Int && sort_col_type != TypeId::Float {
2414            return Ok(None);
2415        }
2416
2417        // Each projection field must be a simple `.field`.
2418        let mut proj_indices: Vec<usize> = Vec::with_capacity(fields.len());
2419        let mut proj_columns: Vec<String> = Vec::with_capacity(fields.len());
2420        for f in fields {
2421            let name = match &f.expr {
2422                Expr::Field(n) => n.clone(),
2423                _ => return Ok(None),
2424            };
2425            let idx = match all_columns.iter().position(|c| c == &name) {
2426                Some(i) => i,
2427                None => return Ok(None),
2428            };
2429            proj_indices.push(idx);
2430            proj_columns.push(f.alias.clone().unwrap_or(name));
2431        }
2432
2433        let fast = FastLayout::new(&schema);
2434        let row_layout = RowLayout::new(&schema);
2435        // Mission C Phase 20b: inline numeric-column reader (no Box<dyn Fn>).
2436        let sort_byte_offset = match fast.fixed_offsets[sort_idx] {
2437            Some(o) => o,
2438            None => return Ok(None),
2439        };
2440        let sort_bitmap_byte = sort_idx / 8;
2441        let sort_bitmap_bit = (sort_idx % 8) as u32;
2442        let sort_body_data_offset = 2 + fast.bitmap_size + sort_byte_offset;
2443
2444        let compiled_pred: Option<CompiledPredicate> = match predicate {
2445            Some(pred) => match compile_predicate(pred, &all_columns, &fast, &schema) {
2446                Some(c) => Some(c),
2447                None => return Ok(None),
2448            },
2449            None => None,
2450        };
2451
2452        // Bounded top-N heap. For `order .x desc limit N`, we want the N
2453        // largest values — use a min-heap so the smallest is at the top and
2454        // can be popped when a better candidate arrives. For ascending, use
2455        // a max-heap. We tie-break with a monotonic `seq` counter so the
2456        // result is deterministic and stable.
2457        //
2458        // To keep this simple we maintain two typed heaps and pick by
2459        // direction.
2460        let drained: Vec<Vec<u8>> = match sort_col_type {
2461            TypeId::Int => {
2462                let mut seq: u64 = 0;
2463                let mut heap_desc: BinaryHeap<Reverse<(i64, u64, Vec<u8>)>> =
2464                    BinaryHeap::with_capacity(prealloc);
2465                let mut heap_asc: BinaryHeap<(i64, u64, Vec<u8>)> =
2466                    BinaryHeap::with_capacity(prealloc);
2467
2468                self.catalog
2469                    .for_each_row_raw(table, |_rid, data| {
2470                        if let Some(ref pred) = compiled_pred {
2471                            if !pred(data) {
2472                                return;
2473                            }
2474                        }
2475                        // Inlined int-column reader: null check + i64 decode.
2476                        let base = row_body_base(data);
2477                        let sort_data_offset = base + sort_body_data_offset;
2478                        if data.len() < sort_data_offset + 8
2479                            || data.len() <= base + 2 + sort_bitmap_byte
2480                        {
2481                            return;
2482                        }
2483                        let is_null =
2484                            (data[base + 2 + sort_bitmap_byte] >> sort_bitmap_bit) & 1 == 1;
2485                        if is_null {
2486                            return;
2487                        }
2488                        let key = i64::from_le_bytes(
2489                            data[sort_data_offset..sort_data_offset + 8]
2490                                .try_into()
2491                                .unwrap_or_else(|_| unreachable!()),
2492                        );
2493                        let id = seq;
2494                        seq += 1;
2495
2496                        if descending {
2497                            if heap_desc.len() < limit {
2498                                heap_desc.push(Reverse((key, id, data.to_vec())));
2499                            } else if let Some(Reverse((top_key, _, _))) = heap_desc.peek() {
2500                                if key > *top_key {
2501                                    heap_desc.pop();
2502                                    heap_desc.push(Reverse((key, id, data.to_vec())));
2503                                }
2504                            }
2505                        } else if heap_asc.len() < limit {
2506                            heap_asc.push((key, id, data.to_vec()));
2507                        } else if let Some((top_key, _, _)) = heap_asc.peek() {
2508                            if key < *top_key {
2509                                heap_asc.pop();
2510                                heap_asc.push((key, id, data.to_vec()));
2511                            }
2512                        }
2513                    })
2514                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
2515
2516                let mut drained: Vec<(i64, u64, Vec<u8>)> = if descending {
2517                    heap_desc.into_iter().map(|Reverse(t)| t).collect()
2518                } else {
2519                    heap_asc.into_iter().collect()
2520                };
2521                if descending {
2522                    drained.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
2523                } else {
2524                    drained.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
2525                }
2526                drained.into_iter().map(|(_, _, d)| d).collect()
2527            }
2528            TypeId::Float => {
2529                // Novel angle: rather than introducing a `TotalF64` newtype
2530                // with `Ord via total_cmp`, transform the f64 bit pattern
2531                // into a sortable `u64` so `BinaryHeap<u64>` orders exactly
2532                // like `f64::total_cmp` would. Classic trick: flip the sign
2533                // bit on positives, flip all bits on negatives. Result:
2534                // - NaN (sign=0) stays greatest, matching total_cmp
2535                // - -0.0 sorts before +0.0, matching total_cmp
2536                // - Hot loop is branch-cheap (one compare + one xor)
2537                let mut seq: u64 = 0;
2538                let mut heap_desc: BinaryHeap<Reverse<(u64, u64, Vec<u8>)>> =
2539                    BinaryHeap::with_capacity(prealloc);
2540                let mut heap_asc: BinaryHeap<(u64, u64, Vec<u8>)> =
2541                    BinaryHeap::with_capacity(prealloc);
2542
2543                self.catalog
2544                    .for_each_row_raw(table, |_rid, data| {
2545                        if let Some(ref pred) = compiled_pred {
2546                            if !pred(data) {
2547                                return;
2548                            }
2549                        }
2550                        let base = row_body_base(data);
2551                        let sort_data_offset = base + sort_body_data_offset;
2552                        if data.len() < sort_data_offset + 8
2553                            || data.len() <= base + 2 + sort_bitmap_byte
2554                        {
2555                            return;
2556                        }
2557                        let is_null =
2558                            (data[base + 2 + sort_bitmap_byte] >> sort_bitmap_bit) & 1 == 1;
2559                        if is_null {
2560                            return;
2561                        }
2562                        let bits = u64::from_le_bytes(
2563                            data[sort_data_offset..sort_data_offset + 8]
2564                                .try_into()
2565                                .unwrap_or_else(|_| unreachable!()),
2566                        );
2567                        let key = f64_bits_to_sortable_u64(bits);
2568                        let id = seq;
2569                        seq += 1;
2570
2571                        if descending {
2572                            if heap_desc.len() < limit {
2573                                heap_desc.push(Reverse((key, id, data.to_vec())));
2574                            } else if let Some(Reverse((top_key, _, _))) = heap_desc.peek() {
2575                                if key > *top_key {
2576                                    heap_desc.pop();
2577                                    heap_desc.push(Reverse((key, id, data.to_vec())));
2578                                }
2579                            }
2580                        } else if heap_asc.len() < limit {
2581                            heap_asc.push((key, id, data.to_vec()));
2582                        } else if let Some((top_key, _, _)) = heap_asc.peek() {
2583                            if key < *top_key {
2584                                heap_asc.pop();
2585                                heap_asc.push((key, id, data.to_vec()));
2586                            }
2587                        }
2588                    })
2589                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
2590
2591                let mut drained: Vec<(u64, u64, Vec<u8>)> = if descending {
2592                    heap_desc.into_iter().map(|Reverse(t)| t).collect()
2593                } else {
2594                    heap_asc.into_iter().collect()
2595                };
2596                if descending {
2597                    drained.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
2598                } else {
2599                    drained.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
2600                }
2601                drained.into_iter().map(|(_, _, d)| d).collect()
2602            }
2603            _ => unreachable!("type guard above restricts to Int/Float"),
2604        };
2605
2606        let rows: Vec<Vec<Value>> = drained
2607            .into_iter()
2608            .map(|data| {
2609                proj_indices
2610                    .iter()
2611                    .map(|&ci| decode_column(&schema, &row_layout, &data, ci))
2612                    .collect()
2613            })
2614            .collect();
2615
2616        Ok(Some(QueryResult::Rows {
2617            columns: proj_columns,
2618            rows,
2619        }))
2620    }
2621
2622    /// Gather the RowIds that a mutation should operate on, without
2623    /// materialising the full row set. Handles the shapes the planner emits
2624    /// for update/delete: SeqScan, IndexScan, and Filter(SeqScan). Other
2625    /// shapes fall back to `generic_rid_match`.
2626    ///
2627    /// Perf sprint: try to fuse the predicate evaluation and in-place
2628    /// byte-level mutation into a single heap walk. Returns `Some(result)`
2629    /// if the fused path fired, `None` to fall through to the generic
2630    /// two-pass code.
2631    ///
2632    /// Covers two shapes:
2633    /// 1. Fixed-width non-null literal assignments on non-indexed columns
2634    ///    → byte-patch every matched row in place (row length unchanged).
2635    /// 2. Single var-col literal assignment on a non-indexed column
2636    ///    → `patch_var_column_in_place` on every matched row (may shrink);
2637    ///    rows that can't be patched in place are collected for fallback.
2638    fn try_fused_scan_update(
2639        &mut self,
2640        table: &str,
2641        predicate: &Expr,
2642        resolved: &[(usize, Value)],
2643        changed_cols: &[usize],
2644    ) -> Option<Result<QueryResult, QueryError>> {
2645        // Build compiled predicate. Requires a schema borrow that must be
2646        // dropped before we call scan_patch_matching_logged.
2647        let compiled = {
2648            let schema = self.catalog.schema(table)?;
2649            let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
2650            let fast = FastLayout::new(schema);
2651            compile_predicate(predicate, &columns, &fast, schema)?
2652        };
2653
2654        // ── Path 1: fixed-width fast patch ──────────────────────────
2655        let fixed_patches: Option<Vec<FastPatch>> = {
2656            let tbl = self.catalog.get_table(table)?;
2657            let schema = &tbl.schema;
2658            let all_fixed_nonnull = resolved
2659                .iter()
2660                .all(|(idx, val)| is_fixed_size(schema.columns[*idx].type_id) && !val.is_empty());
2661            let no_indexed = !resolved.iter().any(|(idx, _)| tbl.has_indexed_col(*idx));
2662            if all_fixed_nonnull && no_indexed {
2663                let layout = RowLayout::new(schema);
2664                let bitmap_size = layout.bitmap_size();
2665                Some(
2666                    resolved
2667                        .iter()
2668                        .map(|(idx, val)| {
2669                            let fixed_off = layout
2670                                .fixed_offset(*idx)
2671                                .expect("is_fixed_size already checked");
2672                            let field_off = 2 + bitmap_size + fixed_off;
2673                            let bytes: FixedBytes = match val {
2674                                Value::Int(v) => FixedBytes::I64(v.to_le_bytes()),
2675                                Value::Float(v) => FixedBytes::F64(v.to_le_bytes()),
2676                                Value::Bool(v) => FixedBytes::Bool(if *v { 1 } else { 0 }),
2677                                Value::DateTime(v) => FixedBytes::I64(v.to_le_bytes()),
2678                                Value::Uuid(v) => FixedBytes::Uuid(*v),
2679                                _ => unreachable!("all_fixed_nonnull guard"),
2680                            };
2681                            FastPatch {
2682                                field_off,
2683                                bitmap_byte_off: 2 + idx / 8,
2684                                bit_mask: 1u8 << (idx % 8),
2685                                bytes,
2686                            }
2687                        })
2688                        .collect(),
2689                )
2690            } else {
2691                None
2692            }
2693        };
2694        if let Some(patches) = fixed_patches {
2695            let result = self
2696                .catalog
2697                .scan_patch_matching_logged(table, compiled, |row| {
2698                    let base = row_body_base(row);
2699                    for p in &patches {
2700                        row[base + p.bitmap_byte_off] &= !p.bit_mask;
2701                        let field_bytes = p.bytes.as_slice();
2702                        row[base + p.field_off..base + p.field_off + field_bytes.len()]
2703                            .copy_from_slice(field_bytes);
2704                    }
2705                    Some(row.len() as u16)
2706                })
2707                .map_err(|e| e.to_string());
2708            match result {
2709                Ok((count, _)) => {
2710                    self.view_registry.mark_dependents_dirty(table);
2711                    return Some(Ok(QueryResult::Modified(count)));
2712                }
2713                Err(e) => return Some(Err(QueryError::Execution(e))),
2714            }
2715        }
2716
2717        // ── Path 2: single var-col shrink fast patch ────────────────
2718        let var_patch: Option<(usize, Option<Vec<u8>>)> = {
2719            let tbl = self.catalog.get_table(table)?;
2720            let schema = &tbl.schema;
2721            let is_single = resolved.len() == 1;
2722            let is_var = is_single && !is_fixed_size(schema.columns[resolved[0].0].type_id);
2723            let no_indexed = !resolved.iter().any(|(idx, _)| tbl.has_indexed_col(*idx));
2724            if is_single && is_var && no_indexed {
2725                let (idx, val) = &resolved[0];
2726                let bytes_opt = match val {
2727                    Value::Str(s) => Some(s.as_bytes().to_vec()),
2728                    Value::Bytes(b) => Some(b.clone()),
2729                    Value::Empty => None,
2730                    _ => return None, // type mismatch, fall through
2731                };
2732                Some((*idx, bytes_opt))
2733            } else {
2734                None
2735            }
2736        };
2737        if let Some((col_idx, ref new_bytes_opt)) = var_patch {
2738            // Build a fresh RowLayout before the mutable borrow.
2739            let layout = {
2740                let schema = self.catalog.schema(table)?;
2741                RowLayout::new(schema)
2742            };
2743            let new_bytes_ref: Option<&[u8]> = new_bytes_opt.as_deref();
2744            let result = self
2745                .catalog
2746                .scan_patch_matching_logged(table, compiled, |row| {
2747                    patch_var_column_in_place(row, &layout, col_idx, new_bytes_ref)
2748                })
2749                .map_err(|e| e.to_string());
2750            match result {
2751                Ok((mut count, fallback_rids)) => {
2752                    // Handle rows where in-place patch failed (new > old).
2753                    for rid in fallback_rids {
2754                        let mut row = match self.catalog.get(table, rid) {
2755                            Some(r) => r,
2756                            None => continue,
2757                        };
2758                        for (idx, val) in resolved.iter() {
2759                            row[*idx] = val.clone();
2760                        }
2761                        if let Err(e) =
2762                            self.catalog
2763                                .update_hinted(table, rid, &row, Some(changed_cols))
2764                        {
2765                            return Some(Err(QueryError::StorageError(e.to_string())));
2766                        }
2767                        count += 1;
2768                    }
2769                    self.view_registry.mark_dependents_dirty(table);
2770                    return Some(Ok(QueryResult::Modified(count)));
2771                }
2772                Err(e) => return Some(Err(QueryError::Execution(e))),
2773            }
2774        }
2775
2776        None // no fused path applicable — fall through
2777    }
2778
2779    /// Mission C Phase 3: schema is looked up via `self.catalog.schema(table)`
2780    /// inside the branches that actually need it. Previously the caller had
2781    /// to clone the full Schema (6+ String allocs) before every mutation just
2782    /// so this function could borrow it — a cost the update/delete hot path
2783    /// did not need.
2784    fn collect_rids_for_mutation(
2785        &mut self,
2786        input: &PlanNode,
2787        table: &str,
2788    ) -> Result<Vec<RowId>, QueryError> {
2789        match input {
2790            PlanNode::SeqScan { table: t } if t == table => {
2791                // "Update/delete everything" — rare but legal.
2792                let rids: Vec<RowId> = self
2793                    .catalog
2794                    .scan(table)
2795                    .map_err(|e| QueryError::StorageError(e.to_string()))?
2796                    .map(|(rid, _)| rid)
2797                    .collect();
2798                Ok(rids)
2799            }
2800            PlanNode::IndexScan {
2801                table: t,
2802                column,
2803                key,
2804            } if t == table => {
2805                let key_value = literal_to_value(key)?;
2806
2807                // Indexed case: single lookup, 0 or 1 rows.
2808                // Mission D7: int-specialized fast path on int-keyed indexes
2809                // (primary keys, created_at, etc.) — the common case for
2810                // `update_by_pk` / `delete where id = ?`.
2811                //
2812                // Scope the `tbl` borrow so it's released before we fall
2813                // through to the scan-based paths below (which reborrow
2814                // `self.catalog`).
2815                {
2816                    let tbl = self
2817                        .catalog
2818                        .get_table(table)
2819                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
2820                    if tbl.has_index(column) {
2821                        let rids = tbl.index_lookup_all(column, &key_value);
2822                        return Ok(rids);
2823                    }
2824                }
2825
2826                // No index: the planner folds `.col = literal` to IndexScan
2827                // regardless of whether the column is actually unique. When
2828                // there's no index we must behave like Filter(SeqScan) and
2829                // return *all* matching RIDs — not just the first one.
2830                let schema = self
2831                    .catalog
2832                    .schema(table)
2833                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
2834                let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
2835                let fast = FastLayout::new(schema);
2836                let synth = Expr::BinaryOp(
2837                    Box::new(Expr::Field(column.clone())),
2838                    BinOp::Eq,
2839                    Box::new(key.clone()),
2840                );
2841                if let Some(compiled) = compile_predicate(&synth, &columns, &fast, schema) {
2842                    // Mission F: skip the first 4 Vec doublings.
2843                    let mut rids: Vec<RowId> = Vec::with_capacity(64);
2844                    self.catalog
2845                        .for_each_row_raw(table, |rid, data| {
2846                            if compiled(data) {
2847                                rids.push(rid);
2848                            }
2849                        })
2850                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
2851                    return Ok(rids);
2852                }
2853
2854                // Fallback: decode each row, compare values.
2855                let col_idx =
2856                    schema
2857                        .column_index(column)
2858                        .ok_or_else(|| QueryError::ColumnNotFound {
2859                            table: String::new(),
2860                            column: column.clone(),
2861                        })?;
2862                let rids: Vec<RowId> = self
2863                    .catalog
2864                    .scan(table)
2865                    .map_err(|e| QueryError::StorageError(e.to_string()))?
2866                    .filter_map(|(rid, row)| {
2867                        if row[col_idx] == key_value {
2868                            Some(rid)
2869                        } else {
2870                            None
2871                        }
2872                    })
2873                    .collect();
2874                Ok(rids)
2875            }
2876            PlanNode::Filter {
2877                input: inner,
2878                predicate,
2879            } => {
2880                if let PlanNode::SeqScan { table: t } = inner.as_ref() {
2881                    if t != table {
2882                        return self.generic_rid_match(input, table);
2883                    }
2884                    let schema = self
2885                        .catalog
2886                        .schema(table)
2887                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
2888                    let columns: Vec<String> =
2889                        schema.columns.iter().map(|c| c.name.clone()).collect();
2890                    let fast = FastLayout::new(schema);
2891                    let row_layout = RowLayout::new(schema);
2892
2893                    // Try compiled predicate first.
2894                    if let Some(compiled) = compile_predicate(predicate, &columns, &fast, schema) {
2895                        // Mission F: skip the first 4 Vec doublings.
2896                        let mut rids: Vec<RowId> = Vec::with_capacity(64);
2897                        self.catalog
2898                            .for_each_row_raw(table, |rid, data| {
2899                                if compiled(data) {
2900                                    rids.push(rid);
2901                                }
2902                            })
2903                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
2904                        return Ok(rids);
2905                    }
2906
2907                    // Fallback: selective decode + eval.
2908                    let pred_cols = predicate_column_indices(predicate, &columns);
2909                    let mut rids: Vec<RowId> = Vec::with_capacity(64);
2910                    self.catalog
2911                        .for_each_row_raw(table, |rid, data| {
2912                            let pred_row = decode_selective(schema, &row_layout, data, &pred_cols);
2913                            if eval_predicate(predicate, &pred_row, &columns) {
2914                                rids.push(rid);
2915                            }
2916                        })
2917                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
2918                    return Ok(rids);
2919                }
2920                self.generic_rid_match(input, table)
2921            }
2922            _ => self.generic_rid_match(input, table),
2923        }
2924    }
2925
2926    /// Last-ditch generic match: execute the plan, collect matching rows,
2927    /// then find corresponding RowIds by value equality. This is the old
2928    /// O(N*M) code path; only used when the plan shape is something exotic.
2929    fn generic_rid_match(
2930        &mut self,
2931        input: &PlanNode,
2932        table: &str,
2933    ) -> Result<Vec<RowId>, QueryError> {
2934        let result = self.execute_plan(input)?;
2935        let rows = match result {
2936            QueryResult::Rows { rows, .. } => rows,
2937            _ => return Err("mutation source must be rows".into()),
2938        };
2939        let matching: Vec<RowId> = self
2940            .catalog
2941            .scan(table)
2942            .map_err(|e| QueryError::StorageError(e.to_string()))?
2943            .filter(|(_, row)| rows.iter().any(|r| r == row))
2944            .map(|(rid, _)| rid)
2945            .collect();
2946        Ok(matching)
2947    }
2948}
2949
2950pub(super) fn execute_window(
2951    result: QueryResult,
2952    windows: &[WindowDef],
2953) -> Result<QueryResult, QueryError> {
2954    let (mut columns, mut rows) = match result {
2955        QueryResult::Rows { columns, rows } => (columns, rows),
2956        _ => return Err("window function requires row input".into()),
2957    };
2958
2959    for wdef in windows {
2960        // Resolve partition/order column indices against current columns.
2961        let part_indices: Vec<usize> = wdef
2962            .partition_by
2963            .iter()
2964            .map(|name| {
2965                columns
2966                    .iter()
2967                    .position(|c| c == name)
2968                    .ok_or_else(|| format!("window partition column '{name}' not found"))
2969            })
2970            .collect::<Result<Vec<_>, _>>()?;
2971
2972        let ord_indices: Vec<(usize, bool)> = wdef
2973            .order_by
2974            .iter()
2975            .map(|sk| {
2976                columns
2977                    .iter()
2978                    .position(|c| c == &sk.field)
2979                    .map(|i| (i, sk.descending))
2980                    .ok_or_else(|| format!("window order column '{}' not found", sk.field))
2981            })
2982            .collect::<Result<Vec<_>, _>>()?;
2983
2984        // Resolve the argument column index (for aggregate windows).
2985        let arg_col_idx: Option<usize> = if let Some(arg) = wdef.args.first() {
2986            match arg {
2987                Expr::Field(name) => {
2988                    if name == "*" {
2989                        None // count(*) style — no specific column
2990                    } else {
2991                        Some(
2992                            columns
2993                                .iter()
2994                                .position(|c| c == name)
2995                                .ok_or_else(|| format!("window arg column '{name}' not found"))?,
2996                        )
2997                    }
2998                }
2999                _ => None,
3000            }
3001        } else {
3002            None
3003        };
3004
3005        // Build a sort-index to sort rows by partition_by then order_by
3006        // without actually reordering the original Vec (we need original
3007        // order to write results back).
3008        let n = rows.len();
3009        let mut indices: Vec<usize> = (0..n).collect();
3010        indices.sort_by(|&a, &b| {
3011            // Compare partition keys first.
3012            for &pi in &part_indices {
3013                let cmp = rows[a][pi].cmp(&rows[b][pi]);
3014                if cmp != std::cmp::Ordering::Equal {
3015                    return cmp;
3016                }
3017            }
3018            // Then order keys.
3019            for &(oi, desc) in &ord_indices {
3020                let cmp = rows[a][oi].cmp(&rows[b][oi]);
3021                if cmp != std::cmp::Ordering::Equal {
3022                    return if desc { cmp.reverse() } else { cmp };
3023                }
3024            }
3025            std::cmp::Ordering::Equal
3026        });
3027
3028        // SQL window-frame semantics: with no `order` clause the frame for an
3029        // aggregate window is the ENTIRE partition, not the running prefix.
3030        // The loop below computes running values; for the no-order case we
3031        // back-fill every row of a partition with the partition's final
3032        // (i.e. complete) aggregate once its boundary is reached. Ranking
3033        // functions are untouched — row_number/rank/dense_rank are inherently
3034        // positional.
3035        let whole_partition_frame = wdef.order_by.is_empty()
3036            && matches!(
3037                wdef.function,
3038                WindowFunc::Sum
3039                    | WindowFunc::Avg
3040                    | WindowFunc::Count
3041                    | WindowFunc::Min
3042                    | WindowFunc::Max
3043            );
3044        // Original row indices of the partition currently being scanned
3045        // (only tracked when back-filling is needed).
3046        let mut partition_row_indices: Vec<usize> = Vec::new();
3047
3048        // Compute window values in sorted order, tracking partition boundaries.
3049        let mut win_values: Vec<Value> = vec![Value::Empty; n];
3050        let mut partition_start = 0usize;
3051        // Running state for aggregate windows:
3052        let mut running_count: i64 = 0;
3053        let mut running_int_sum: i64 = 0;
3054        let mut running_float_sum: f64 = 0.0;
3055        let mut running_saw_float = false;
3056        let mut running_min: Option<Value> = None;
3057        let mut running_max: Option<Value> = None;
3058        let mut rank_counter: i64 = 0;
3059        let mut dense_rank_counter: i64 = 0;
3060        let mut prev_order_key: Option<Vec<Value>> = None;
3061        let mut same_rank_count: i64 = 0;
3062
3063        for sorted_pos in 0..n {
3064            let row_idx = indices[sorted_pos];
3065
3066            // Detect partition boundary.
3067            let new_partition = if sorted_pos == 0 {
3068                true
3069            } else {
3070                let prev_row_idx = indices[sorted_pos - 1];
3071                part_indices
3072                    .iter()
3073                    .any(|&pi| rows[row_idx][pi] != rows[prev_row_idx][pi])
3074            };
3075
3076            if new_partition {
3077                // No-order aggregate frame: the partition that just ended is
3078                // complete, so its final running value IS the whole-partition
3079                // aggregate. Back-fill it onto every row of that partition.
3080                if whole_partition_frame && sorted_pos > 0 {
3081                    let final_v = win_values[indices[sorted_pos - 1]].clone();
3082                    for ri in partition_row_indices.drain(..) {
3083                        win_values[ri] = final_v.clone();
3084                    }
3085                }
3086                partition_start = sorted_pos;
3087                running_count = 0;
3088                running_int_sum = 0;
3089                running_float_sum = 0.0;
3090                running_saw_float = false;
3091                running_min = None;
3092                running_max = None;
3093                rank_counter = 0;
3094                dense_rank_counter = 0;
3095                prev_order_key = None;
3096                same_rank_count = 0;
3097            }
3098
3099            // Extract current order key for rank tracking.
3100            let current_order_key: Vec<Value> = ord_indices
3101                .iter()
3102                .map(|&(oi, _)| rows[row_idx][oi].clone())
3103                .collect();
3104            let same_as_prev = prev_order_key.as_ref() == Some(&current_order_key);
3105
3106            let value = match wdef.function {
3107                WindowFunc::RowNumber => Value::Int((sorted_pos - partition_start + 1) as i64),
3108                WindowFunc::Rank => {
3109                    if same_as_prev {
3110                        same_rank_count += 1;
3111                    } else {
3112                        rank_counter += same_rank_count + 1;
3113                        same_rank_count = 0;
3114                        if rank_counter == 0 {
3115                            rank_counter = 1;
3116                        }
3117                    }
3118                    Value::Int(rank_counter)
3119                }
3120                WindowFunc::DenseRank => {
3121                    if !same_as_prev {
3122                        dense_rank_counter += 1;
3123                    }
3124                    Value::Int(dense_rank_counter)
3125                }
3126                WindowFunc::Sum => {
3127                    if let Some(ci) = arg_col_idx {
3128                        match &rows[row_idx][ci] {
3129                            Value::Int(v) => running_int_sum += v,
3130                            Value::Float(v) => {
3131                                running_float_sum += v;
3132                                running_saw_float = true;
3133                            }
3134                            _ => {}
3135                        }
3136                    }
3137                    if running_saw_float {
3138                        Value::Float(running_float_sum + running_int_sum as f64)
3139                    } else {
3140                        Value::Int(running_int_sum)
3141                    }
3142                }
3143                WindowFunc::Avg => {
3144                    if let Some(ci) = arg_col_idx {
3145                        match &rows[row_idx][ci] {
3146                            Value::Int(v) => {
3147                                running_float_sum += *v as f64;
3148                                running_count += 1;
3149                            }
3150                            Value::Float(v) => {
3151                                running_float_sum += v;
3152                                running_count += 1;
3153                            }
3154                            _ => {}
3155                        }
3156                    }
3157                    if running_count == 0 {
3158                        Value::Empty
3159                    } else {
3160                        Value::Float(running_float_sum / running_count as f64)
3161                    }
3162                }
3163                WindowFunc::Count => {
3164                    if let Some(ci) = arg_col_idx {
3165                        if !rows[row_idx][ci].is_empty() {
3166                            running_count += 1;
3167                        }
3168                    } else {
3169                        // count(*) — count all rows
3170                        running_count += 1;
3171                    }
3172                    Value::Int(running_count)
3173                }
3174                WindowFunc::Min => {
3175                    if let Some(ci) = arg_col_idx {
3176                        let v = &rows[row_idx][ci];
3177                        if !v.is_empty() {
3178                            running_min = Some(match &running_min {
3179                                None => v.clone(),
3180                                Some(cur) => {
3181                                    if v < cur {
3182                                        v.clone()
3183                                    } else {
3184                                        cur.clone()
3185                                    }
3186                                }
3187                            });
3188                        }
3189                    }
3190                    running_min.clone().unwrap_or(Value::Empty)
3191                }
3192                WindowFunc::Max => {
3193                    if let Some(ci) = arg_col_idx {
3194                        let v = &rows[row_idx][ci];
3195                        if !v.is_empty() {
3196                            running_max = Some(match &running_max {
3197                                None => v.clone(),
3198                                Some(cur) => {
3199                                    if v > cur {
3200                                        v.clone()
3201                                    } else {
3202                                        cur.clone()
3203                                    }
3204                                }
3205                            });
3206                        }
3207                    }
3208                    running_max.clone().unwrap_or(Value::Empty)
3209                }
3210            };
3211
3212            prev_order_key = Some(current_order_key);
3213            win_values[row_idx] = value;
3214            if whole_partition_frame {
3215                partition_row_indices.push(row_idx);
3216            }
3217        }
3218
3219        // Back-fill the final partition (the loop only flushes at boundaries).
3220        if whole_partition_frame && n > 0 {
3221            let final_v = win_values[indices[n - 1]].clone();
3222            for ri in partition_row_indices.drain(..) {
3223                win_values[ri] = final_v.clone();
3224            }
3225        }
3226
3227        // Append the computed window column to each row.
3228        for (ri, row) in rows.iter_mut().enumerate() {
3229            row.push(win_values[ri].clone());
3230        }
3231        columns.push(wdef.output_name.clone());
3232    }
3233
3234    Ok(QueryResult::Rows { columns, rows })
3235}
3236
3237/// Mission E2b: compute one aggregate over a set of rows in a group.
3238pub(super) fn compute_group_aggregate(
3239    func: AggFunc,
3240    all_rows: &[Vec<Value>],
3241    row_indices: &[usize],
3242    col_idx: usize,
3243) -> Value {
3244    match func {
3245        AggFunc::Count => {
3246            if col_idx == usize::MAX {
3247                // count(*) — count all rows in the group.
3248                return Value::Int(row_indices.len() as i64);
3249            }
3250            let count = row_indices
3251                .iter()
3252                .filter(|&&ri| !all_rows[ri][col_idx].is_empty())
3253                .count();
3254            Value::Int(count as i64)
3255        }
3256        AggFunc::CountDistinct => {
3257            let mut seen = std::collections::HashSet::new();
3258            for &ri in row_indices {
3259                let v = &all_rows[ri][col_idx];
3260                if !v.is_empty() {
3261                    seen.insert(v.clone());
3262                }
3263            }
3264            Value::Int(seen.len() as i64)
3265        }
3266        AggFunc::Sum => {
3267            // Mirror the scalar Sum path: accumulate int and float
3268            // contributions separately and promote the final result to
3269            // Float if any Float row was observed. Prevents silent
3270            // drop of Float columns in GROUP BY aggregates.
3271            let mut int_sum: i64 = 0;
3272            let mut float_sum: f64 = 0.0;
3273            let mut saw_float = false;
3274            for &ri in row_indices {
3275                match &all_rows[ri][col_idx] {
3276                    Value::Int(v) => int_sum += v,
3277                    Value::Float(v) => {
3278                        float_sum += *v;
3279                        saw_float = true;
3280                    }
3281                    _ => {}
3282                }
3283            }
3284            if saw_float {
3285                Value::Float(float_sum + int_sum as f64)
3286            } else {
3287                Value::Int(int_sum)
3288            }
3289        }
3290        AggFunc::Avg => {
3291            let mut sum = 0.0f64;
3292            let mut count = 0usize;
3293            for &ri in row_indices {
3294                match &all_rows[ri][col_idx] {
3295                    Value::Int(v) => {
3296                        sum += *v as f64;
3297                        count += 1;
3298                    }
3299                    Value::Float(v) => {
3300                        sum += *v;
3301                        count += 1;
3302                    }
3303                    _ => {}
3304                }
3305            }
3306            if count == 0 {
3307                Value::Empty
3308            } else {
3309                Value::Float(sum / count as f64)
3310            }
3311        }
3312        AggFunc::Min => row_indices
3313            .iter()
3314            .map(|&ri| &all_rows[ri][col_idx])
3315            .filter(|v| !v.is_empty())
3316            .min()
3317            .cloned()
3318            .unwrap_or(Value::Empty),
3319        AggFunc::Max => row_indices
3320            .iter()
3321            .map(|&ri| &all_rows[ri][col_idx])
3322            .filter(|v| !v.is_empty())
3323            .max()
3324            .cloned()
3325            .unwrap_or(Value::Empty),
3326    }
3327}
3328
3329/// Mission E1.3: try to extract equi-join key indices from a join `on`
3330/// predicate. Returns `Some((left_col_idx, right_col_idx))` when the
3331/// predicate is exactly `L = R` (or `R = L`) and both sides resolve
3332/// cleanly — `L` to the left subtree's column list and `R` to the right
3333/// subtree's column list.
3334///
3335/// This is deliberately narrow. We only recognise the two shapes:
3336///   * `QualifiedField = QualifiedField`  (`u.id = o.user_id`)
3337///   * `Field = Field`                    (`.id = .user_id`, unqualified)
3338///
3339/// Anything else — conjunctions, constants, function calls, or predicates
3340/// that touch the same side on both halves — falls through to the
3341/// nested-loop path unchanged.
3342pub(super) fn try_extract_equi_join_keys(
3343    pred: &Expr,
3344    left_columns: &[String],
3345    right_columns: &[String],
3346) -> Option<(usize, usize)> {
3347    let (lhs, op, rhs) = match pred {
3348        Expr::BinaryOp(l, op, r) => (l.as_ref(), *op, r.as_ref()),
3349        _ => return None,
3350    };
3351    if op != BinOp::Eq {
3352        return None;
3353    }
3354    // Normal orientation: lhs in left, rhs in right.
3355    if let (Some(li), Some(ri)) = (
3356        resolve_side_column(lhs, left_columns),
3357        resolve_side_column(rhs, right_columns),
3358    ) {
3359        return Some((li, ri));
3360    }
3361    // Swapped: rhs in left, lhs in right. Both sides of `=` are
3362    // commutative so this is safe.
3363    if let (Some(li), Some(ri)) = (
3364        resolve_side_column(rhs, left_columns),
3365        resolve_side_column(lhs, right_columns),
3366    ) {
3367        return Some((li, ri));
3368    }
3369    None
3370}
3371
3372fn resolve_side_column(expr: &Expr, columns: &[String]) -> Option<usize> {
3373    match expr {
3374        Expr::QualifiedField { qualifier, field } => {
3375            // Byte-level match so we don't allocate a fresh `format!` on
3376            // every call — this runs once per plan, so allocation would be
3377            // cheap, but the match is trivial enough to keep inline with
3378            // the eval_expr version.
3379            let q = qualifier.as_bytes();
3380            let f = field.as_bytes();
3381            columns.iter().position(|c| {
3382                let b = c.as_bytes();
3383                b.len() == q.len() + 1 + f.len()
3384                    && b[..q.len()] == *q
3385                    && b[q.len()] == b'.'
3386                    && b[q.len() + 1..] == *f
3387            })
3388        }
3389        Expr::Field(name) => columns.iter().position(|c| c == name),
3390        _ => None,
3391    }
3392}
3393
3394/// Mission E1.3: O(L + R) hash join. Builds a `FxHashMap<Value, Vec<usize>>`
3395/// over the right (inner) side's join keys, then streams the left (outer)
3396/// side and for each probe row emits every combined row whose right-side
3397/// key matches. For `JoinKind::LeftOuter`, unmatched left rows are emitted
3398/// padded with `Value::Empty` on the right side.
3399///
3400/// The right side is always the build side. That choice is forced for
3401/// LeftOuter (the left side must stream so we can detect orphans), and
3402/// for Inner it's a reasonable default — left-deep plans tend to grow the
3403/// left side with each join, so the un-joined right leaf is often the
3404/// smaller of the two at each level.
3405pub(super) fn hash_join(
3406    left_columns: Vec<String>,
3407    left_rows: Vec<Vec<Value>>,
3408    right_columns: Vec<String>,
3409    right_rows: Vec<Vec<Value>>,
3410    left_key_idx: usize,
3411    right_key_idx: usize,
3412    kind: JoinKind,
3413) -> QueryResult {
3414    use rustc_hash::FxHashMap;
3415
3416    let n_left = left_columns.len();
3417    let n_right = right_columns.len();
3418    let mut columns = Vec::with_capacity(n_left + n_right);
3419    columns.extend(left_columns);
3420    columns.extend(right_columns);
3421
3422    // Build: right_key -> list of right-row indices. Pre-size to the row
3423    // count so the map doesn't rehash mid-build.
3424    let mut build: FxHashMap<Value, Vec<usize>> =
3425        FxHashMap::with_capacity_and_hasher(right_rows.len(), Default::default());
3426    for (i, row) in right_rows.iter().enumerate() {
3427        // Skip Empty keys on the build side — they can never match under
3428        // SQL semantics (NULL ≠ NULL) and would collapse all nullables to
3429        // one bucket.
3430        if matches!(row[right_key_idx], Value::Empty) {
3431            continue;
3432        }
3433        build.entry(row[right_key_idx].clone()).or_default().push(i);
3434    }
3435
3436    // Reasonable starting capacity — inner joins produce ≥ left_rows.len()
3437    // rows in the common 1:1 case, left-outer always emits ≥ left_rows.len().
3438    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(left_rows.len());
3439
3440    for left_row in &left_rows {
3441        let key = &left_row[left_key_idx];
3442        let matched = if matches!(key, Value::Empty) {
3443            None
3444        } else {
3445            build.get(key)
3446        };
3447        match matched {
3448            Some(matches) if !matches.is_empty() => {
3449                for &ri in matches {
3450                    let right_row = &right_rows[ri];
3451                    let mut combined = Vec::with_capacity(n_left + n_right);
3452                    combined.extend_from_slice(left_row);
3453                    combined.extend_from_slice(right_row);
3454                    rows.push(combined);
3455                }
3456            }
3457            _ => {
3458                if matches!(kind, JoinKind::LeftOuter) {
3459                    let mut row = Vec::with_capacity(n_left + n_right);
3460                    row.extend_from_slice(left_row);
3461                    row.resize(n_left + n_right, Value::Empty);
3462                    rows.push(row);
3463                }
3464            }
3465        }
3466    }
3467
3468    QueryResult::Rows { columns, rows }
3469}
3470
3471/// Lower unindexed `RangeScan` and `IndexScan` nodes to `Filter(SeqScan)`
3472/// so that all downstream fast paths (count, project+limit, sort+limit,
3473/// agg, update, delete) continue to fire.
3474///
3475/// The planner emits `RangeScan` (for `.age > 30`) and `IndexScan` (for
3476/// `.email = lit`) speculatively because it has no catalog access. When
3477/// the column has a B-tree index, those plans are correct. When it
3478/// doesn't, the executor's fallbacks materialise every matching row with
3479/// full `decode_row` — bypassing the compiled-predicate fast paths that
3480/// `Filter(SeqScan)` would trigger. Lowering both speculative leaf kinds
3481/// also keeps EXPLAIN honest: it prints the plan that actually runs.
3482///
3483/// This pass runs once per query, before execution.
3484pub(super) fn lower_unindexed_scans(catalog: &Catalog, plan: &PlanNode) -> PlanNode {
3485    match plan {
3486        PlanNode::RangeScan {
3487            table,
3488            column,
3489            start,
3490            end,
3491        } => {
3492            if let Some(tbl) = catalog.get_table(table) {
3493                // Keep RangeScan whenever ANY index exists on the column:
3494                // unique indexes store raw column values, non-unique indexes
3495                // store composite (value, rid) keys that the executor walks
3496                // natively via BTree::range_rids. Only lower to Filter(SeqScan)
3497                // when the column is unindexed.
3498                if tbl.has_index(column) {
3499                    return plan.clone();
3500                }
3501            }
3502            let pred = synthesize_range_predicate(column, start, end);
3503            PlanNode::Filter {
3504                input: Box::new(PlanNode::SeqScan {
3505                    table: table.clone(),
3506                }),
3507                predicate: pred,
3508            }
3509        }
3510        PlanNode::Filter { input, predicate } => PlanNode::Filter {
3511            input: Box::new(lower_unindexed_scans(catalog, input)),
3512            predicate: predicate.clone(),
3513        },
3514        PlanNode::Project { input, fields } => PlanNode::Project {
3515            input: Box::new(lower_unindexed_scans(catalog, input)),
3516            fields: fields.clone(),
3517        },
3518        PlanNode::Sort { input, keys } => PlanNode::Sort {
3519            input: Box::new(lower_unindexed_scans(catalog, input)),
3520            keys: keys.clone(),
3521        },
3522        PlanNode::Limit { input, count } => PlanNode::Limit {
3523            input: Box::new(lower_unindexed_scans(catalog, input)),
3524            count: count.clone(),
3525        },
3526        PlanNode::Offset { input, count } => PlanNode::Offset {
3527            input: Box::new(lower_unindexed_scans(catalog, input)),
3528            count: count.clone(),
3529        },
3530        PlanNode::Aggregate {
3531            input,
3532            function,
3533            field,
3534        } => PlanNode::Aggregate {
3535            input: Box::new(lower_unindexed_scans(catalog, input)),
3536            function: *function,
3537            field: field.clone(),
3538        },
3539        PlanNode::Distinct { input } => PlanNode::Distinct {
3540            input: Box::new(lower_unindexed_scans(catalog, input)),
3541        },
3542        PlanNode::GroupBy {
3543            input,
3544            keys,
3545            aggregates,
3546            having,
3547        } => PlanNode::GroupBy {
3548            input: Box::new(lower_unindexed_scans(catalog, input)),
3549            keys: keys.clone(),
3550            aggregates: aggregates.clone(),
3551            having: having.clone(),
3552        },
3553        PlanNode::Update {
3554            input,
3555            table,
3556            assignments,
3557        } => PlanNode::Update {
3558            input: Box::new(lower_unindexed_scans(catalog, input)),
3559            table: table.clone(),
3560            assignments: assignments.clone(),
3561        },
3562        PlanNode::Delete { input, table } => PlanNode::Delete {
3563            input: Box::new(lower_unindexed_scans(catalog, input)),
3564            table: table.clone(),
3565        },
3566        PlanNode::Window { input, windows } => PlanNode::Window {
3567            input: Box::new(lower_unindexed_scans(catalog, input)),
3568            windows: windows.clone(),
3569        },
3570        PlanNode::Union { left, right, all } => PlanNode::Union {
3571            left: Box::new(lower_unindexed_scans(catalog, left)),
3572            right: Box::new(lower_unindexed_scans(catalog, right)),
3573            all: *all,
3574        },
3575        PlanNode::Explain { input } => PlanNode::Explain {
3576            input: Box::new(lower_unindexed_scans(catalog, input)),
3577        },
3578        PlanNode::NestedLoopJoin {
3579            left,
3580            right,
3581            on,
3582            kind,
3583        } => PlanNode::NestedLoopJoin {
3584            left: Box::new(lower_unindexed_scans(catalog, left)),
3585            right: Box::new(lower_unindexed_scans(catalog, right)),
3586            on: on.clone(),
3587            kind: *kind,
3588        },
3589        PlanNode::IndexScan { table, column, key } => {
3590            if let Some(tbl) = catalog.get_table(table) {
3591                if tbl.has_index(column) {
3592                    return plan.clone();
3593                }
3594            }
3595            PlanNode::Filter {
3596                input: Box::new(PlanNode::SeqScan {
3597                    table: table.clone(),
3598                }),
3599                predicate: Expr::BinaryOp(
3600                    Box::new(Expr::Field(column.clone())),
3601                    BinOp::Eq,
3602                    Box::new(key.clone()),
3603                ),
3604            }
3605        }
3606        // Leaf nodes: no children to recurse into.
3607        _ => plan.clone(),
3608    }
3609}
3610
3611/// Synthesize a range predicate from RangeScan bounds for the fallback path.
3612pub(super) fn synthesize_range_predicate(
3613    column: &str,
3614    start: &Option<(Expr, bool)>,
3615    end: &Option<(Expr, bool)>,
3616) -> Expr {
3617    let lower = start.as_ref().map(|(expr, inclusive)| {
3618        let op = if *inclusive { BinOp::Gte } else { BinOp::Gt };
3619        Expr::BinaryOp(
3620            Box::new(Expr::Field(column.to_string())),
3621            op,
3622            Box::new(expr.clone()),
3623        )
3624    });
3625    let upper = end.as_ref().map(|(expr, inclusive)| {
3626        let op = if *inclusive { BinOp::Lte } else { BinOp::Lt };
3627        Expr::BinaryOp(
3628            Box::new(Expr::Field(column.to_string())),
3629            op,
3630            Box::new(expr.clone()),
3631        )
3632    });
3633    match (lower, upper) {
3634        (Some(l), Some(u)) => Expr::BinaryOp(Box::new(l), BinOp::And, Box::new(u)),
3635        (Some(l), None) => l,
3636        (None, Some(u)) => u,
3637        (None, None) => Expr::Literal(Literal::Bool(true)),
3638    }
3639}
3640
3641/// Check if a value falls within a range (used in last-resort decoded-row eval).
3642pub(super) fn range_matches(
3643    val: &Value,
3644    start: &Option<Value>,
3645    start_inc: bool,
3646    end: &Option<Value>,
3647    end_inc: bool,
3648) -> bool {
3649    if let Some(ref s) = start {
3650        if start_inc {
3651            if val < s {
3652                return false;
3653            }
3654        } else if val <= s {
3655            return false;
3656        }
3657    }
3658    if let Some(ref e) = end {
3659        if end_inc {
3660            if val > e {
3661                return false;
3662            }
3663        } else if val >= e {
3664            return false;
3665        }
3666    }
3667    true
3668}
3669
3670/// Format a `PlanNode` tree as a human-readable, indented text
3671/// representation. Used by the `EXPLAIN` command.
3672pub(super) fn format_plan_tree(plan: &PlanNode, depth: usize) -> String {
3673    let indent = "  ".repeat(depth);
3674    match plan {
3675        PlanNode::SeqScan { table } => format!("{indent}SeqScan table={table}"),
3676        PlanNode::AliasScan { table, alias } => {
3677            format!("{indent}AliasScan table={table} alias={alias}")
3678        }
3679        PlanNode::IndexScan { table, column, key } => {
3680            format!("{indent}IndexScan table={table} column={column} key={key:?}")
3681        }
3682        PlanNode::RangeScan {
3683            table,
3684            column,
3685            start,
3686            end,
3687        } => {
3688            let s = match start {
3689                Some((expr, inc)) => {
3690                    let op = if *inc { ">=" } else { ">" };
3691                    format!("{op}{expr:?}")
3692                }
3693                None => "unbounded".to_string(),
3694            };
3695            let e = match end {
3696                Some((expr, inc)) => {
3697                    let op = if *inc { "<=" } else { "<" };
3698                    format!("{op}{expr:?}")
3699                }
3700                None => "unbounded".to_string(),
3701            };
3702            format!("{indent}RangeScan table={table} column={column} [{s}, {e}]")
3703        }
3704        PlanNode::Filter { input, predicate } => {
3705            let child = format_plan_tree(input, depth + 1);
3706            format!("{indent}Filter predicate={predicate:?}\n{child}")
3707        }
3708        PlanNode::Project { input, fields } => {
3709            let names: Vec<String> = fields
3710                .iter()
3711                .map(|f| match &f.alias {
3712                    Some(a) => format!("{a}: {:?}", f.expr),
3713                    None => format!("{:?}", f.expr),
3714                })
3715                .collect();
3716            let child = format_plan_tree(input, depth + 1);
3717            format!("{indent}Project fields=[{}]\n{child}", names.join(", "))
3718        }
3719        PlanNode::Sort { input, keys } => {
3720            let ks: Vec<String> = keys
3721                .iter()
3722                .map(|k| {
3723                    if k.descending {
3724                        format!("{} desc", k.field)
3725                    } else {
3726                        k.field.clone()
3727                    }
3728                })
3729                .collect();
3730            let child = format_plan_tree(input, depth + 1);
3731            format!("{indent}Sort keys=[{}]\n{child}", ks.join(", "))
3732        }
3733        PlanNode::Limit { input, count } => {
3734            let child = format_plan_tree(input, depth + 1);
3735            format!("{indent}Limit count={count:?}\n{child}")
3736        }
3737        PlanNode::Offset { input, count } => {
3738            let child = format_plan_tree(input, depth + 1);
3739            format!("{indent}Offset count={count:?}\n{child}")
3740        }
3741        PlanNode::Aggregate {
3742            input,
3743            function,
3744            field,
3745        } => {
3746            let f = field.as_deref().unwrap_or("*");
3747            let child = format_plan_tree(input, depth + 1);
3748            format!("{indent}Aggregate fn={function:?} field={f}\n{child}")
3749        }
3750        PlanNode::NestedLoopJoin {
3751            left,
3752            right,
3753            on,
3754            kind,
3755        } => {
3756            let left_child = format_plan_tree(left, depth + 1);
3757            let right_child = format_plan_tree(right, depth + 1);
3758            let on_str = match on {
3759                Some(pred) => format!("{pred:?}"),
3760                None => "none".to_string(),
3761            };
3762            format!("{indent}NestedLoopJoin kind={kind:?} on={on_str}\n{left_child}\n{right_child}")
3763        }
3764        PlanNode::Distinct { input } => {
3765            let child = format_plan_tree(input, depth + 1);
3766            format!("{indent}Distinct\n{child}")
3767        }
3768        PlanNode::GroupBy {
3769            input,
3770            keys,
3771            aggregates,
3772            having,
3773        } => {
3774            let agg_strs: Vec<String> = aggregates
3775                .iter()
3776                .map(|a| format!("{:?}({}) as {}", a.function, a.field, a.output_name))
3777                .collect();
3778            let having_str = match having {
3779                Some(h) => format!(" having={h:?}"),
3780                None => String::new(),
3781            };
3782            let child = format_plan_tree(input, depth + 1);
3783            format!(
3784                "{indent}GroupBy keys=[{}] aggs=[{}]{having_str}\n{child}",
3785                keys.join(", "),
3786                agg_strs.join(", "),
3787            )
3788        }
3789        PlanNode::Insert { table, rows } => {
3790            let cols: Vec<&str> = rows
3791                .first()
3792                .map(|r| r.iter().map(|a| a.field.as_str()).collect())
3793                .unwrap_or_default();
3794            format!(
3795                "{indent}Insert table={table} rows={} cols=[{}]",
3796                rows.len(),
3797                cols.join(", ")
3798            )
3799        }
3800        PlanNode::Upsert {
3801            table,
3802            key_column,
3803            assignments,
3804            on_conflict,
3805        } => {
3806            let cols: Vec<&str> = assignments.iter().map(|a| a.field.as_str()).collect();
3807            let conflict_cols: Vec<&str> = on_conflict.iter().map(|a| a.field.as_str()).collect();
3808            if conflict_cols.is_empty() {
3809                format!(
3810                    "{indent}Upsert table={table} key={key_column} cols=[{}]",
3811                    cols.join(", ")
3812                )
3813            } else {
3814                format!(
3815                    "{indent}Upsert table={table} key={key_column} cols=[{}] on_conflict=[{}]",
3816                    cols.join(", "),
3817                    conflict_cols.join(", ")
3818                )
3819            }
3820        }
3821        PlanNode::Update {
3822            input,
3823            table,
3824            assignments,
3825        } => {
3826            let cols: Vec<&str> = assignments.iter().map(|a| a.field.as_str()).collect();
3827            let child = format_plan_tree(input, depth + 1);
3828            format!(
3829                "{indent}Update table={table} set=[{}]\n{child}",
3830                cols.join(", ")
3831            )
3832        }
3833        PlanNode::Delete { input, table } => {
3834            let child = format_plan_tree(input, depth + 1);
3835            format!("{indent}Delete table={table}\n{child}")
3836        }
3837        PlanNode::CreateTable { name, fields } => {
3838            let fs: Vec<String> = fields
3839                .iter()
3840                .map(|f| {
3841                    let mut mods = String::new();
3842                    if f.required {
3843                        mods.push_str(" required");
3844                    }
3845                    if f.unique {
3846                        mods.push_str(" unique");
3847                    }
3848                    format!("{}: {}{mods}", f.name, f.type_name)
3849                })
3850                .collect();
3851            format!("{indent}CreateTable name={name} fields=[{}]", fs.join(", "))
3852        }
3853        PlanNode::AlterTable { table, action } => {
3854            format!("{indent}AlterTable table={table} action={action:?}")
3855        }
3856        PlanNode::DropTable { name } => format!("{indent}DropTable name={name}"),
3857        PlanNode::CreateView { name, .. } => format!("{indent}CreateView name={name}"),
3858        PlanNode::RefreshView { name } => format!("{indent}RefreshView name={name}"),
3859        PlanNode::DropView { name } => format!("{indent}DropView name={name}"),
3860        PlanNode::Window { input, windows } => {
3861            let ws: Vec<String> = windows
3862                .iter()
3863                .map(|w| format!("{:?} as {}", w.function, w.output_name))
3864                .collect();
3865            let child = format_plan_tree(input, depth + 1);
3866            format!("{indent}Window fns=[{}]\n{child}", ws.join(", "))
3867        }
3868        PlanNode::Union { left, right, all } => {
3869            let kind = if *all { "UNION ALL" } else { "UNION" };
3870            let left_child = format_plan_tree(left, depth + 1);
3871            let right_child = format_plan_tree(right, depth + 1);
3872            format!("{indent}{kind}\n{left_child}\n{right_child}")
3873        }
3874        PlanNode::Explain { input } => {
3875            let child = format_plan_tree(input, depth + 1);
3876            format!("{indent}Explain\n{child}")
3877        }
3878        PlanNode::Begin => format!("{indent}Begin"),
3879        PlanNode::Commit => format!("{indent}Commit"),
3880        PlanNode::Rollback => format!("{indent}Rollback"),
3881    }
3882}