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