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