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