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