Skip to main content

powdb_query/executor/
plan_exec.rs

1//! The execute_plan method and associated helpers.
2
3use crate::ast::*;
4use crate::cancel::CancelCheck;
5use crate::plan::*;
6use crate::result::{QueryError, QueryResult};
7use powdb_storage::catalog::{Catalog, ExpressionIndexMeta, IndexOrderDirection};
8use powdb_storage::row::{decode_column, decode_row, patch_var_column_in_place, RowLayout};
9use powdb_storage::stored_json_path::StoredJsonPathV1;
10use powdb_storage::types::*;
11use std::cmp::Reverse;
12use std::collections::{BinaryHeap, HashSet};
13use std::ops::ControlFlow;
14
15use super::compiled::*;
16use super::eval::*;
17use super::row_body_base;
18use super::{check_join_limit, mem_budget, Engine, MAX_SORT_ROWS};
19use powdb_storage::view::ViewDef;
20
21/// Maximum number of elements sorted by the standard-library stable sort
22/// without an intervening cancellation checkpoint. Larger inputs are sorted
23/// as bounded stable runs and cooperatively merged below.
24const CANCELLABLE_SORT_RUN: usize = 2_048;
25
26/// Compare ORDER BY values with the engine-wide `NULLS LAST` contract.
27///
28/// `Value::Empty` represents both SQL/PowQL null and a missing JSON path.
29/// Direction only reverses non-null values, so nulls remain last for both
30/// ascending and descending order.
31pub(super) fn compare_order_values(
32    left: &Value,
33    right: &Value,
34    descending: bool,
35) -> std::cmp::Ordering {
36    use std::cmp::Ordering;
37
38    match (left, right) {
39        (Value::Empty, Value::Empty) => Ordering::Equal,
40        (Value::Empty, _) => Ordering::Greater,
41        (_, Value::Empty) => Ordering::Less,
42        _ if descending => left.cmp(right).reverse(),
43        _ => left.cmp(right),
44    }
45}
46
47/// Stable sort with cooperative cancellation throughout the sort itself.
48///
49/// We sort original positions rather than moving potentially large rows on
50/// every merge pass. Each bounded run uses Rust's stable sort, with immediate
51/// checks on both sides; bottom-up merges poll while emitting positions and
52/// prefer the left run on equality, preserving global stability. The final
53/// permutation is applied in place and also polls. Two `usize` arrays are the
54/// only scratch allocation and are charged to the query memory budget.
55pub(super) fn cooperative_stable_sort_by<T, F>(
56    values: &mut [T],
57    memory_limit: usize,
58    compare: F,
59) -> Result<(), QueryError>
60where
61    F: Fn(&T, &T) -> std::cmp::Ordering,
62{
63    crate::cancel::check()?;
64    let len = values.len();
65    if len < 2 {
66        return Ok(());
67    }
68
69    let scratch_bytes = len
70        .saturating_mul(std::mem::size_of::<usize>())
71        .saturating_mul(2);
72    mem_budget::charge(scratch_bytes, memory_limit)?;
73
74    let mut order: Vec<usize> = (0..len).collect();
75    let mut scratch = vec![0usize; len];
76
77    for run in order.chunks_mut(CANCELLABLE_SORT_RUN) {
78        crate::cancel::check()?;
79        run.sort_by(|&a, &b| compare(&values[a], &values[b]));
80        crate::cancel::check()?;
81    }
82
83    let mut cancel = CancelCheck::new();
84    let mut width = CANCELLABLE_SORT_RUN;
85    while width < len {
86        let step = width.saturating_mul(2);
87        let mut start = 0usize;
88        while start < len {
89            let mid = start.saturating_add(width).min(len);
90            let end = start.saturating_add(step).min(len);
91            let (mut left, mut right, mut out) = (start, mid, start);
92
93            while left < mid && right < end {
94                cancel.tick()?;
95                if compare(&values[order[left]], &values[order[right]])
96                    != std::cmp::Ordering::Greater
97                {
98                    scratch[out] = order[left];
99                    left += 1;
100                } else {
101                    scratch[out] = order[right];
102                    right += 1;
103                }
104                out += 1;
105            }
106            while left < mid {
107                cancel.tick()?;
108                scratch[out] = order[left];
109                left += 1;
110                out += 1;
111            }
112            while right < end {
113                cancel.tick()?;
114                scratch[out] = order[right];
115                right += 1;
116                out += 1;
117            }
118            start = start.saturating_add(step);
119        }
120        std::mem::swap(&mut order, &mut scratch);
121        width = step;
122    }
123
124    // `order[new_position] = old_position`; invert it to a destination for
125    // each item, then apply permutation cycles without cloning row payloads.
126    for (new_position, &old_position) in order.iter().enumerate() {
127        cancel.tick()?;
128        scratch[old_position] = new_position;
129    }
130    drop(order);
131    for position in 0..len {
132        while scratch[position] != position {
133            cancel.tick()?;
134            let destination = scratch[position];
135            values.swap(position, destination);
136            scratch.swap(position, destination);
137        }
138    }
139    Ok(())
140}
141
142/// Run a raw table scan with cooperative cancellation while preserving the
143/// existing allocation-free callback shape used by hot read paths.
144pub(super) fn for_each_row_raw_cancellable(
145    catalog: &Catalog,
146    table: &str,
147    mut f: impl FnMut(RowId, &[u8]),
148) -> Result<(), QueryError> {
149    // Embedded/direct execution normally has no cancellation token. Preserve
150    // the original zero-poll raw-scan hot path in that case instead of paying
151    // a counter increment and mask branch for every row. The active-install
152    // flag is a process-wide hint, so a true result still enters the
153    // authoritative thread-local polling path below.
154    if !crate::cancel::has_active_install() {
155        return catalog
156            .for_each_row_raw(table, f)
157            .map_err(|err| QueryError::StorageError(err.to_string()));
158    }
159
160    let mut cancel = CancelCheck::new();
161    let mut cancel_err: Option<QueryError> = None;
162    catalog
163        .try_for_each_row_raw(table, |rid, data| {
164            if let Err(err) = cancel.tick() {
165                cancel_err = Some(err);
166                return ControlFlow::Break(());
167            }
168            f(rid, data);
169            ControlFlow::Continue(())
170        })
171        .map_err(|err| QueryError::StorageError(err.to_string()))?;
172    match cancel_err {
173        Some(err) => Err(err),
174        None => Ok(()),
175    }
176}
177
178fn resolve_expression_index(
179    catalog: &Catalog,
180    table: &str,
181    path: &StoredJsonPathV1,
182) -> Option<ExpressionIndexMeta> {
183    catalog
184        .expression_index_metadata(table)?
185        .into_iter()
186        .find(|metadata| metadata.canonical_version == 1 && metadata.json_path == *path)
187}
188
189fn expression_index_fallback(plan: &PlanNode) -> Option<PlanNode> {
190    match plan {
191        PlanNode::ExprIndexScan { table, path, key } => Some(PlanNode::Filter {
192            input: Box::new(PlanNode::SeqScan {
193                table: table.clone(),
194            }),
195            predicate: Expr::BinaryOp(
196                Box::new(stored_json_path_expr(path)),
197                BinOp::Eq,
198                Box::new(key.clone()),
199            ),
200        }),
201        PlanNode::ExprRangeScan {
202            table,
203            path,
204            start,
205            end,
206        } => Some(PlanNode::Filter {
207            input: Box::new(PlanNode::SeqScan {
208                table: table.clone(),
209            }),
210            predicate: synthesize_expr_range_predicate(path, start, end),
211        }),
212        PlanNode::OrderedExprIndexScan {
213            table,
214            path,
215            descending,
216            limit,
217            offset,
218        } => {
219            let sorted = PlanNode::Sort {
220                input: Box::new(PlanNode::SeqScan {
221                    table: table.clone(),
222                }),
223                keys: vec![SortKey {
224                    expr: stored_json_path_expr(path),
225                    descending: *descending,
226                }],
227            };
228            let sliced = match offset {
229                Some(count) => PlanNode::Offset {
230                    input: Box::new(sorted),
231                    count: count.clone(),
232                },
233                None => sorted,
234            };
235            Some(PlanNode::Limit {
236                input: Box::new(sliced),
237                count: limit.clone(),
238            })
239        }
240        _ => None,
241    }
242}
243
244#[derive(Debug)]
245pub(super) struct ProvenanceRows {
246    pub(super) columns: Vec<String>,
247    pub(super) rows: Vec<Vec<Value>>,
248    source_aliases: Vec<String>,
249    provenance: Vec<Vec<Option<RowId>>>,
250}
251
252impl ProvenanceRows {
253    fn source_index(&self, alias: &str) -> Option<usize> {
254        self.source_aliases
255            .iter()
256            .position(|source| source == alias)
257    }
258}
259
260impl Engine {
261    pub(super) fn execute_expression_index_plan(
262        &self,
263        plan: &PlanNode,
264        projected_fields: Option<&[ProjectField]>,
265    ) -> Result<Option<QueryResult>, QueryError> {
266        let (table, path) = match plan {
267            PlanNode::ExprIndexScan { table, path, .. }
268            | PlanNode::ExprRangeScan { table, path, .. }
269            | PlanNode::OrderedExprIndexScan { table, path, .. } => (table, path),
270            _ => return Ok(None),
271        };
272        let Some(index) = resolve_expression_index(&self.catalog, table, path) else {
273            return Ok(None);
274        };
275        let schema = self
276            .catalog
277            .schema(table)
278            .ok_or_else(|| QueryError::TableNotFound(table.clone()))?
279            .clone();
280        let all_columns: Vec<String> = schema
281            .columns
282            .iter()
283            .map(|column| column.name.clone())
284            .collect();
285
286        let projection = match projected_fields {
287            Some(fields) => {
288                if !fields
289                    .iter()
290                    .all(|field| matches!(field.expr, Expr::Field(_)))
291                {
292                    return Ok(None);
293                }
294                let mut indices = Vec::with_capacity(fields.len());
295                let mut columns = Vec::with_capacity(fields.len());
296                for field in fields {
297                    let Expr::Field(name) = &field.expr else {
298                        unreachable!("plain-field projection checked above")
299                    };
300                    let index =
301                        schema
302                            .column_index(name)
303                            .ok_or_else(|| QueryError::ColumnNotFound {
304                                table: table.clone(),
305                                column: name.clone(),
306                            })?;
307                    indices.push(index);
308                    columns.push(field.alias.clone().unwrap_or_else(|| name.clone()));
309                }
310                Some((indices, columns))
311            }
312            None => None,
313        };
314
315        let (rids, range) = match plan {
316            PlanNode::ExprIndexScan { key, .. } => {
317                let key = literal_to_value(key)?;
318                let rids = if key.is_empty() {
319                    self.catalog
320                        .expression_index_btree(table, index.index_id)
321                        .ok_or_else(|| {
322                            QueryError::Execution("expression index disappeared".to_string())
323                        })?
324                        .empty_rids()
325                        .to_vec()
326                } else {
327                    self.catalog
328                        .expression_index_lookup_all(table, index.index_id, &key)
329                        .map_err(|error| QueryError::StorageError(error.to_string()))?
330                };
331                (rids, None)
332            }
333            PlanNode::ExprRangeScan { start, end, .. } => {
334                let start_value = start
335                    .as_ref()
336                    .map(|(expr, _)| literal_to_value(expr))
337                    .transpose()?;
338                let end_value = end
339                    .as_ref()
340                    .map(|(expr, _)| literal_to_value(expr))
341                    .transpose()?;
342                let rids = self
343                    .catalog
344                    .expression_index_range_rids(
345                        table,
346                        index.index_id,
347                        start_value.as_ref(),
348                        end_value.as_ref(),
349                    )
350                    .map_err(|error| QueryError::StorageError(error.to_string()))?;
351                (
352                    rids,
353                    Some((
354                        start_value,
355                        start.as_ref().is_none_or(|(_, inclusive)| *inclusive),
356                        end_value,
357                        end.as_ref().is_none_or(|(_, inclusive)| *inclusive),
358                    )),
359                )
360            }
361            PlanNode::OrderedExprIndexScan {
362                descending,
363                limit,
364                offset,
365                ..
366            } => {
367                let Expr::Literal(Literal::Int(limit)) = limit else {
368                    return Err(QueryError::Execution(
369                        "expression-index limit must be a non-negative integer".to_string(),
370                    ));
371                };
372                let offset = match offset {
373                    Some(Expr::Literal(Literal::Int(offset))) if *offset >= 0 => *offset as usize,
374                    None => 0,
375                    _ => {
376                        return Err(QueryError::Execution(
377                            "expression-index offset must be a non-negative integer".to_string(),
378                        ));
379                    }
380                };
381                if *limit < 0 {
382                    return Err(QueryError::Execution(
383                        "expression-index limit must be a non-negative integer".to_string(),
384                    ));
385                }
386                let rids = self
387                    .catalog
388                    .expression_index_ordered_rids_bounded(
389                        table,
390                        index.index_id,
391                        if *descending {
392                            IndexOrderDirection::Desc
393                        } else {
394                            IndexOrderDirection::Asc
395                        },
396                        offset,
397                        *limit as usize,
398                    )
399                    .map_err(|error| QueryError::StorageError(error.to_string()))?;
400                (rids, None)
401            }
402            _ => unreachable!("expression-index plan checked above"),
403        };
404
405        let root_index =
406            schema
407                .column_index(&path.column)
408                .ok_or_else(|| QueryError::ColumnNotFound {
409                    table: table.clone(),
410                    column: path.column.clone(),
411                })?;
412        let path_expr = stored_json_path_expr(path);
413        let mut rows = Vec::with_capacity(rids.len());
414        let mut cancel = CancelCheck::new();
415        for rid in rids {
416            cancel.tick()?;
417            match &projection {
418                Some((projected_indices, _)) => {
419                    let mut fetch_indices = projected_indices.clone();
420                    let root_position = fetch_indices.iter().position(|index| *index == root_index);
421                    let root_position = match root_position {
422                        Some(position) => position,
423                        None => {
424                            fetch_indices.push(root_index);
425                            fetch_indices.len() - 1
426                        }
427                    };
428                    let Some(mut fetched) = self
429                        .catalog
430                        .get_projected(table, rid, &fetch_indices)
431                        .map_err(|error| QueryError::StorageError(error.to_string()))?
432                    else {
433                        continue;
434                    };
435                    if let Some((start, start_inclusive, end, end_inclusive)) = &range {
436                        let value = eval_expr(
437                            &path_expr,
438                            std::slice::from_ref(&fetched[root_position]),
439                            std::slice::from_ref(&path.column),
440                        );
441                        if value.is_empty()
442                            || !range_matches(&value, start, *start_inclusive, end, *end_inclusive)
443                        {
444                            continue;
445                        }
446                    }
447                    fetched.truncate(projected_indices.len());
448                    rows.push(fetched);
449                }
450                None => {
451                    let Some(row) = self.catalog.get(table, rid) else {
452                        continue;
453                    };
454                    if let Some((start, start_inclusive, end, end_inclusive)) = &range {
455                        let value = eval_expr(&path_expr, &row, &all_columns);
456                        if value.is_empty()
457                            || !range_matches(&value, start, *start_inclusive, end, *end_inclusive)
458                        {
459                            continue;
460                        }
461                    }
462                    rows.push(row);
463                }
464            }
465        }
466
467        let columns = projection
468            .map(|(_, columns)| columns)
469            .unwrap_or(all_columns);
470        Ok(Some(QueryResult::Rows { columns, rows }))
471    }
472
473    fn charge_provenance(&self, rows: &ProvenanceRows) -> Result<(), QueryError> {
474        let aliases =
475            rows.source_aliases
476                .iter()
477                .fold(std::mem::size_of::<Vec<String>>(), |total, alias| {
478                    total
479                        .saturating_add(std::mem::size_of::<String>())
480                        .saturating_add(alias.capacity())
481                });
482        let per_row = std::mem::size_of::<Vec<Option<RowId>>>().saturating_add(
483            rows.source_aliases
484                .len()
485                .saturating_mul(std::mem::size_of::<Option<RowId>>()),
486        );
487        mem_budget::charge(
488            aliases.saturating_add(rows.provenance.len().saturating_mul(per_row)),
489            self.query_memory_limit(),
490        )
491    }
492
493    fn provenance_scan(
494        &self,
495        table: &str,
496        alias: &str,
497        qualify_columns: bool,
498    ) -> Result<ProvenanceRows, QueryError> {
499        let schema = self
500            .catalog
501            .schema(table)
502            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
503            .clone();
504        let columns = schema
505            .columns
506            .iter()
507            .map(|column| {
508                if qualify_columns {
509                    format!("{alias}.{}", column.name)
510                } else {
511                    column.name.clone()
512                }
513            })
514            .collect();
515        let mut rows = Vec::new();
516        let mut provenance = Vec::new();
517        let mut cancel = CancelCheck::new();
518        for (rid, row) in self
519            .catalog
520            .scan(table)
521            .map_err(|error| QueryError::StorageError(error.to_string()))?
522        {
523            cancel.tick()?;
524            rows.push(row);
525            provenance.push(vec![Some(rid)]);
526        }
527        let result = ProvenanceRows {
528            columns,
529            rows,
530            source_aliases: vec![alias.to_string()],
531            provenance,
532        };
533        Ok(result)
534    }
535
536    pub(super) fn materialize_rows_with_provenance(
537        &self,
538        plan: &PlanNode,
539    ) -> Result<ProvenanceRows, QueryError> {
540        let result = match plan {
541            PlanNode::SeqScan { table } => self.provenance_scan(table, table, false)?,
542            PlanNode::AliasScan { table, alias } => self.provenance_scan(table, alias, true)?,
543            PlanNode::IndexScan { table, column, key } => {
544                let fallback = PlanNode::Filter {
545                    input: Box::new(PlanNode::SeqScan {
546                        table: table.clone(),
547                    }),
548                    predicate: Expr::BinaryOp(
549                        Box::new(Expr::Field(column.clone())),
550                        BinOp::Eq,
551                        Box::new(key.clone()),
552                    ),
553                };
554                self.materialize_rows_with_provenance(&fallback)?
555            }
556            PlanNode::RangeScan {
557                table,
558                column,
559                start,
560                end,
561            } => {
562                let fallback = PlanNode::Filter {
563                    input: Box::new(PlanNode::SeqScan {
564                        table: table.clone(),
565                    }),
566                    predicate: synthesize_range_predicate(column, start, end),
567                };
568                self.materialize_rows_with_provenance(&fallback)?
569            }
570            PlanNode::ExprIndexScan { .. }
571            | PlanNode::ExprRangeScan { .. }
572            | PlanNode::OrderedExprIndexScan { .. } => {
573                let fallback = expression_index_fallback(plan)
574                    .expect("expression-index branch always has a fallback");
575                self.materialize_rows_with_provenance(&fallback)?
576            }
577            PlanNode::Filter { input, predicate } => {
578                if contains_subquery(predicate) {
579                    return Err(QueryError::Execution(
580                        "symmetric aggregation over a subquery filter is not supported; use raw"
581                            .to_string(),
582                    ));
583                }
584                let input = self.materialize_rows_with_provenance(input)?;
585                let mut rows = Vec::new();
586                let mut provenance = Vec::new();
587                let mut cancel = CancelCheck::new();
588                for (row, row_provenance) in input.rows.into_iter().zip(input.provenance) {
589                    cancel.tick()?;
590                    if eval_predicate(predicate, &row, &input.columns) {
591                        rows.push(row);
592                        provenance.push(row_provenance);
593                    }
594                }
595                ProvenanceRows {
596                    columns: input.columns,
597                    rows,
598                    source_aliases: input.source_aliases,
599                    provenance,
600                }
601            }
602            PlanNode::Project { input, fields } => {
603                let input = self.materialize_rows_with_provenance(input)?;
604                let columns = fields
605                    .iter()
606                    .map(|field| {
607                        field.alias.clone().unwrap_or_else(|| match &field.expr {
608                            Expr::Field(name) => name.clone(),
609                            Expr::QualifiedField { qualifier, field } => {
610                                format!("{qualifier}.{field}")
611                            }
612                            _ => expression_output_name(&field.expr),
613                        })
614                    })
615                    .collect();
616                let mut rows = Vec::with_capacity(input.rows.len());
617                let mut cancel = CancelCheck::new();
618                for row in &input.rows {
619                    cancel.tick()?;
620                    rows.push(
621                        fields
622                            .iter()
623                            .map(|field| eval_expr(&field.expr, row, &input.columns))
624                            .collect(),
625                    );
626                }
627                ProvenanceRows {
628                    columns,
629                    rows,
630                    source_aliases: input.source_aliases,
631                    provenance: input.provenance,
632                }
633            }
634            PlanNode::Sort { input, keys } => {
635                let input = self.materialize_rows_with_provenance(input)?;
636                if input.rows.len() > MAX_SORT_ROWS {
637                    return Err(QueryError::SortLimitExceeded);
638                }
639                self.charge_rows(&input.rows)?;
640                let mut paired: Vec<_> = input.rows.into_iter().zip(input.provenance).collect();
641                cooperative_stable_sort_by(
642                    &mut paired,
643                    self.query_memory_limit(),
644                    |(left, _), (right, _)| {
645                        for key in keys {
646                            let left_value = eval_expr(&key.expr, left, &input.columns);
647                            let right_value = eval_expr(&key.expr, right, &input.columns);
648                            let comparison =
649                                compare_order_values(&left_value, &right_value, key.descending);
650                            if comparison != std::cmp::Ordering::Equal {
651                                return comparison;
652                            }
653                        }
654                        std::cmp::Ordering::Equal
655                    },
656                )?;
657                let (rows, provenance) = paired.into_iter().unzip();
658                ProvenanceRows {
659                    columns: input.columns,
660                    rows,
661                    source_aliases: input.source_aliases,
662                    provenance,
663                }
664            }
665            PlanNode::Limit { input, count } | PlanNode::Offset { input, count } => {
666                let input_rows = self.materialize_rows_with_provenance(input)?;
667                let Expr::Literal(Literal::Int(count)) = count else {
668                    return Err(QueryError::Execution(
669                        "limit/offset must be an integer literal".to_string(),
670                    ));
671                };
672                let count = *count as usize;
673                let is_limit = matches!(plan, PlanNode::Limit { .. });
674                let iterator = input_rows.rows.into_iter().zip(input_rows.provenance);
675                let (rows, provenance) = if is_limit {
676                    iterator.take(count).unzip()
677                } else {
678                    iterator.skip(count).unzip()
679                };
680                ProvenanceRows {
681                    columns: input_rows.columns,
682                    rows,
683                    source_aliases: input_rows.source_aliases,
684                    provenance,
685                }
686            }
687            PlanNode::Distinct { input } => {
688                let input = self.materialize_rows_with_provenance(input)?;
689                let mut seen = HashSet::new();
690                let mut rows = Vec::new();
691                let mut provenance = Vec::new();
692                let mut cancel = CancelCheck::new();
693                for (row, row_provenance) in input.rows.into_iter().zip(input.provenance) {
694                    cancel.tick()?;
695                    if seen.insert(row.clone()) {
696                        rows.push(row);
697                        provenance.push(row_provenance);
698                    }
699                }
700                ProvenanceRows {
701                    columns: input.columns,
702                    rows,
703                    source_aliases: input.source_aliases,
704                    provenance,
705                }
706            }
707            PlanNode::Union { left, right, all } => {
708                let mut left_rows = self.materialize_rows_with_provenance(left)?;
709                let right_rows = self.materialize_rows_with_provenance(right)?;
710                if left_rows.columns.len() != right_rows.columns.len() {
711                    return Err(QueryError::Execution(
712                        "union sides must have the same number of columns".to_string(),
713                    ));
714                }
715                if left_rows.source_aliases != right_rows.source_aliases {
716                    return Err(QueryError::Execution(
717                        "symmetric aggregation over union requires matching source aliases; use raw"
718                            .to_string(),
719                    ));
720                }
721                left_rows.rows.extend(right_rows.rows);
722                left_rows.provenance.extend(right_rows.provenance);
723                if !all {
724                    let mut seen = HashSet::new();
725                    let mut rows = Vec::new();
726                    let mut provenance = Vec::new();
727                    for (row, row_provenance) in
728                        left_rows.rows.into_iter().zip(left_rows.provenance)
729                    {
730                        if seen.insert(row.clone()) {
731                            rows.push(row);
732                            provenance.push(row_provenance);
733                        }
734                    }
735                    left_rows.rows = rows;
736                    left_rows.provenance = provenance;
737                }
738                left_rows
739            }
740            PlanNode::NestedLoopJoin {
741                left,
742                right,
743                on,
744                kind,
745            } => {
746                let left = self.materialize_rows_with_provenance(left)?;
747                let right = self.materialize_rows_with_provenance(right)?;
748                execute_provenance_join(
749                    left,
750                    right,
751                    on.as_ref(),
752                    *kind,
753                    self.nested_loop_pair_limit,
754                )?
755            }
756            _ => {
757                return Err(QueryError::Execution(
758                    "symmetric aggregation input shape is not supported; use raw".to_string(),
759                ));
760            }
761        };
762        self.charge_provenance(&result)?;
763        Ok(result)
764    }
765
766    /// `schema` — one result row per type: name + column count. Read-only;
767    /// reads live catalog state, so a cached plan can never serve a stale list.
768    pub(super) fn introspect_list_types(&self) -> Result<QueryResult, QueryError> {
769        let rows: Vec<Vec<Value>> = self
770            .catalog
771            .list_tables()
772            .iter()
773            .map(|name| {
774                let cols = self
775                    .catalog
776                    .schema(name)
777                    .map(|s| s.columns.len())
778                    .unwrap_or(0) as i64;
779                vec![Value::Str((*name).to_string()), Value::Int(cols)]
780            })
781            .collect();
782        Ok(QueryResult::Rows {
783            columns: vec!["name".to_string(), "columns".to_string()],
784            rows,
785        })
786    }
787
788    /// `describe <Type>` — one result row per column: name, type, nullability,
789    /// and index kind (`unique` / `index` / empty). Read-only.
790    pub(super) fn introspect_describe(&self, table: &str) -> Result<QueryResult, QueryError> {
791        let schema = self
792            .catalog
793            .schema(table)
794            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
795        let rows: Vec<Vec<Value>> = schema
796            .columns
797            .iter()
798            .map(|c| {
799                let index = if self.catalog.has_index(table, &c.name) {
800                    match self.catalog.is_index_unique(table, &c.name) {
801                        Some(true) => "unique",
802                        _ => "index",
803                    }
804                } else {
805                    ""
806                };
807                vec![
808                    Value::Str(c.name.clone()),
809                    Value::Str(type_id_to_name(c.type_id).to_string()),
810                    Value::Bool(!c.required),
811                    Value::Str(index.to_string()),
812                ]
813            })
814            .collect();
815        Ok(QueryResult::Rows {
816            columns: vec![
817                "column".to_string(),
818                "type".to_string(),
819                "nullable".to_string(),
820                "index".to_string(),
821            ],
822            rows,
823        })
824    }
825
826    pub fn execute_plan(&mut self, plan: &PlanNode) -> Result<QueryResult, QueryError> {
827        // Refuse any plan whose evaluable expressions still carry an aggregate
828        // FunctionCall the grouped-aggregate planner could not lower. Without
829        // this, such an aggregate would reach eval_expr and silently evaluate
830        // to Empty (a wrong answer). The outermost call validates the whole
831        // tree before any row is produced.
832        validate_no_stray_aggregates(plan)?;
833        validate_json_path_types(&self.catalog, plan)?;
834        match plan {
835            PlanNode::ExprIndexScan { .. }
836            | PlanNode::ExprRangeScan { .. }
837            | PlanNode::OrderedExprIndexScan { .. } => {
838                if let Some(result) = self.execute_expression_index_plan(plan, None)? {
839                    return Ok(result);
840                }
841                let fallback = expression_index_fallback(plan)
842                    .expect("expression-index branch always has a fallback");
843                self.execute_plan(&fallback)
844            }
845            PlanNode::SeqScan { table } => {
846                // Auto-refresh dirty materialized views on read.
847                if self.view_registry.is_dirty(table) {
848                    self.refresh_view(table)?;
849                }
850                let schema = self
851                    .catalog
852                    .schema(table)
853                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
854                    .clone();
855                let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
856                // Cooperative cancellation: a full-table scan of a huge table
857                // must stay stoppable.
858                let mut cancel = CancelCheck::new();
859                let mut rows: Vec<Vec<Value>> = Vec::new();
860                for (_, row) in self
861                    .catalog
862                    .scan(table)
863                    .map_err(|e| QueryError::StorageError(e.to_string()))?
864                {
865                    cancel.tick()?;
866                    rows.push(row);
867                }
868                Ok(QueryResult::Rows { columns, rows })
869            }
870
871            PlanNode::Filter { input, predicate } => {
872                // Materialize any IN-subqueries in the predicate before the
873                // scan loop — the closure can't call back into the engine.
874                // Correlated subqueries are left in place for per-row eval.
875                let materialized;
876                let predicate = if contains_subquery(predicate) {
877                    materialized = self.materialize_subqueries(predicate)?;
878                    &materialized
879                } else {
880                    predicate
881                };
882
883                // Correlated subquery path: per-row materialisation.
884                if contains_subquery(predicate) {
885                    let result = self.execute_plan(input)?;
886                    return match result {
887                        QueryResult::Rows { columns, rows } => {
888                            let mut filtered = Vec::new();
889                            // Cooperative cancellation: a subquery runs per outer
890                            // row, so a large outer scan must stay stoppable.
891                            let mut cancel = CancelCheck::new();
892                            for row in rows {
893                                cancel.tick()?;
894                                let row_pred =
895                                    self.materialize_correlated_for_row(predicate, &row, &columns)?;
896                                if eval_predicate(&row_pred, &row, &columns) {
897                                    filtered.push(row);
898                                }
899                            }
900                            Ok(QueryResult::Rows {
901                                columns,
902                                rows: filtered,
903                            })
904                        }
905                        _ => Err("filter requires row input".into()),
906                    };
907                }
908
909                // Fast path: fuse Filter + SeqScan into a zero-copy streaming
910                // loop. Uses decode_column() to evaluate the predicate on only
911                // the columns it references, avoiding heap allocations for
912                // String/Bytes columns that aren't part of the filter.
913                // Overflow safety (P0-4/P1): v2-capable tables fall through to
914                // the decoded general Filter path below — the raw fast path
915                // rehydrates to v1 and drops/mis-reads >= 64KB spilled values.
916                if let PlanNode::SeqScan { table } = input.as_ref() {
917                    if !self.catalog.table_has_overflow(table) {
918                        // Auto-refresh dirty materialized views.
919                        if self.view_registry.is_dirty(table) {
920                            self.refresh_view(table)?;
921                        }
922                        let schema = self
923                            .catalog
924                            .schema(table)
925                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
926                            .clone();
927                        let columns: Vec<String> =
928                            schema.columns.iter().map(|c| c.name.clone()).collect();
929                        let fast = FastLayout::new(&schema);
930                        let row_layout = RowLayout::new(&schema);
931                        // Mission F: pre-size to skip the first 4 Vec doublings
932                        // (4 → 8 → 16 → 32 → 64). On a 100K-row scan with 30%
933                        // selectivity that's ~4 fewer reallocations + memcpys.
934                        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
935
936                        // Try compiled predicate for the filter check (handles
937                        // int leaves, string-eq leaves, and And conjunctions).
938                        // Cooperative cancellation: a full-table compiled/
939                        // selective predicate scan must stay stoppable, so use
940                        // the early-terminating scan and break on cancel. The
941                        // captured error is surfaced after the scan returns.
942                        let mut cancel = CancelCheck::new();
943                        let mut cancel_err: Option<QueryError> = None;
944                        if let Some(compiled) =
945                            compile_predicate(predicate, &columns, &fast, &schema)
946                        {
947                            self.catalog
948                                .try_for_each_row_raw(table, |_rid, data| {
949                                    if let Err(e) = cancel.tick() {
950                                        cancel_err = Some(e);
951                                        return ControlFlow::Break(());
952                                    }
953                                    if compiled(data) {
954                                        rows.push(decode_row(&schema, data));
955                                    }
956                                    ControlFlow::Continue(())
957                                })
958                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
959                        } else {
960                            let pred_cols = predicate_column_indices_json(predicate, &columns);
961                            self.catalog
962                                .try_for_each_row_raw(table, |_rid, data| {
963                                    if let Err(e) = cancel.tick() {
964                                        cancel_err = Some(e);
965                                        return ControlFlow::Break(());
966                                    }
967                                    let pred_row =
968                                        decode_selective(&schema, &row_layout, data, &pred_cols);
969                                    if eval_predicate(predicate, &pred_row, &columns) {
970                                        rows.push(decode_row(&schema, data));
971                                    }
972                                    ControlFlow::Continue(())
973                                })
974                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
975                        }
976                        if let Some(e) = cancel_err {
977                            return Err(e);
978                        }
979
980                        return Ok(QueryResult::Rows { columns, rows });
981                    }
982                }
983
984                // General path: materialise then filter.
985                let result = self.execute_plan(input)?;
986                match result {
987                    QueryResult::Rows { columns, rows } => {
988                        let mut cancel = CancelCheck::new();
989                        let mut filtered: Vec<Vec<Value>> = Vec::new();
990                        for row in rows {
991                            cancel.tick()?;
992                            if eval_predicate(predicate, &row, &columns) {
993                                filtered.push(row);
994                            }
995                        }
996                        Ok(QueryResult::Rows {
997                            columns,
998                            rows: filtered,
999                        })
1000                    }
1001                    _ => Err("filter requires row input".into()),
1002                }
1003            }
1004
1005            PlanNode::Project { input, fields } => {
1006                if matches!(
1007                    input.as_ref(),
1008                    PlanNode::ExprIndexScan { .. }
1009                        | PlanNode::ExprRangeScan { .. }
1010                        | PlanNode::OrderedExprIndexScan { .. }
1011                ) {
1012                    if let Some(result) = self.execute_expression_index_plan(input, Some(fields))? {
1013                        return Ok(result);
1014                    }
1015                }
1016                // Fast path: Project over IndexScan — decode only projected
1017                // columns from raw bytes instead of full decode_row.
1018                if let PlanNode::IndexScan { table, column, key } = input.as_ref() {
1019                    let schema = self
1020                        .catalog
1021                        .schema(table)
1022                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
1023                        .clone();
1024                    let all_columns: Vec<String> =
1025                        schema.columns.iter().map(|c| c.name.clone()).collect();
1026                    let key_value = literal_to_value(key)?;
1027                    let tbl = self
1028                        .catalog
1029                        .get_table(table)
1030                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1031
1032                    let proj_columns: Vec<String> = fields
1033                        .iter()
1034                        .map(|f| {
1035                            f.alias.clone().unwrap_or_else(|| match &f.expr {
1036                                Expr::Field(name) => name.clone(),
1037                                _ => "?".into(),
1038                            })
1039                        })
1040                        .collect();
1041
1042                    // Determine which column indices the projection needs
1043                    let proj_indices: Vec<usize> = fields
1044                        .iter()
1045                        .filter_map(|f| {
1046                            if let Expr::Field(name) = &f.expr {
1047                                all_columns.iter().position(|c| c == name)
1048                            } else {
1049                                None
1050                            }
1051                        })
1052                        .collect();
1053
1054                    // Only serve plain-field projections here; a computed
1055                    // projection (e.g. `length(.v)`) must fall through to the
1056                    // generic expression-evaluating path — otherwise its column
1057                    // is silently dropped (proj_indices only collects Fields).
1058                    let all_plain_fields = fields.iter().all(|f| matches!(f.expr, Expr::Field(_)));
1059                    if tbl.has_index(column) && all_plain_fields {
1060                        let rids = tbl.index_lookup_all(column, &key_value);
1061                        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(rids.len());
1062                        let mut cancel = CancelCheck::new();
1063                        for rid in rids {
1064                            cancel.tick()?;
1065                            // Overflow safety (P0-3/P0-4): `tbl.get` reassembles
1066                            // spilled columns from their overflow chains. The old
1067                            // `heap.get` + `decode_column` read raw v2 bytes and
1068                            // returned Empty for a spilled column (or wrapped a
1069                            // >= 64KB value).
1070                            if let Some(full) = tbl.get(rid) {
1071                                let row: Vec<Value> =
1072                                    proj_indices.iter().map(|&ci| full[ci].clone()).collect();
1073                                rows.push(row);
1074                            }
1075                        }
1076                        return Ok(QueryResult::Rows {
1077                            columns: proj_columns,
1078                            rows,
1079                        });
1080                    }
1081                }
1082
1083                // Fast path: Project(Limit(Sort(Filter(SeqScan)))) — bounded
1084                // top-N heap. Decodes only the sort key + projected columns,
1085                // keeps at most `limit` rows in a heap. Also handles the
1086                // Project(Limit(Sort(SeqScan))) variant (no filter).
1087                if let PlanNode::Limit {
1088                    input: inner,
1089                    count: limit_expr,
1090                } = input.as_ref()
1091                {
1092                    if let PlanNode::Sort {
1093                        input: sort_input,
1094                        keys,
1095                    } = inner.as_ref()
1096                    {
1097                        // Fast path only for single-key sorts
1098                        if keys.len() == 1 {
1099                            if let Expr::Field(sort_field) = &keys[0].expr {
1100                                let descending = keys[0].descending;
1101                                let limit = match limit_expr {
1102                                    Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
1103                                    _ => usize::MAX,
1104                                };
1105                                let (table_opt, pred_opt): (Option<&str>, Option<&Expr>) =
1106                                    match sort_input.as_ref() {
1107                                        PlanNode::SeqScan { table } => (Some(table.as_str()), None),
1108                                        PlanNode::Filter {
1109                                            input: fi,
1110                                            predicate,
1111                                        } => {
1112                                            if let PlanNode::SeqScan { table } = fi.as_ref() {
1113                                                (Some(table.as_str()), Some(predicate))
1114                                            } else {
1115                                                (None, None)
1116                                            }
1117                                        }
1118                                        _ => (None, None),
1119                                    };
1120                                if let Some(table) = table_opt {
1121                                    if let Some(result) = self.project_filter_sort_limit_fast(
1122                                        table, fields, sort_field, descending, limit, pred_opt,
1123                                    )? {
1124                                        return Ok(result);
1125                                    }
1126                                }
1127                            }
1128                        }
1129                    }
1130                    // Fast path: Project(Limit(Filter(SeqScan))) — stream,
1131                    // decode only projected columns, stop at limit.
1132                    if let PlanNode::Filter {
1133                        input: fi,
1134                        predicate,
1135                    } = inner.as_ref()
1136                    {
1137                        if let PlanNode::SeqScan { table } = fi.as_ref() {
1138                            let limit = match limit_expr {
1139                                Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
1140                                _ => usize::MAX,
1141                            };
1142                            if let Some(result) = self.project_filter_limit_fast(
1143                                table,
1144                                fields,
1145                                limit,
1146                                Some(predicate),
1147                            )? {
1148                                return Ok(result);
1149                            }
1150                        }
1151                    }
1152                    // Fast path: Project(Limit(SeqScan)) — stream, no filter.
1153                    if let PlanNode::SeqScan { table } = inner.as_ref() {
1154                        let limit = match limit_expr {
1155                            Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
1156                            _ => usize::MAX,
1157                        };
1158                        if let Some(result) =
1159                            self.project_filter_limit_fast(table, fields, limit, None)?
1160                        {
1161                            return Ok(result);
1162                        }
1163                    }
1164                }
1165
1166                // Mission D4: Project(Filter(SeqScan)) without Limit. Reuses
1167                // `project_filter_limit_fast` with limit = usize::MAX so the
1168                // hot loop decodes only projected columns and uses the
1169                // compiled predicate. Previously this fell through to the
1170                // generic Filter branch which materialised every column via
1171                // `decode_row` then re-projected — quadratic work.
1172                //
1173                // multi_col_and_filter (`U filter .age > 30 and .status =
1174                // "active" { .name, .age }`) was 6.18ms (0.7x SQLite) and
1175                // is the load-bearing workload for this fast path.
1176                if let PlanNode::Filter {
1177                    input: fi,
1178                    predicate,
1179                } = input.as_ref()
1180                {
1181                    if let PlanNode::SeqScan { table } = fi.as_ref() {
1182                        if let Some(result) = self.project_filter_limit_fast(
1183                            table,
1184                            fields,
1185                            usize::MAX,
1186                            Some(predicate),
1187                        )? {
1188                            return Ok(result);
1189                        }
1190                    }
1191                }
1192
1193                // Mission D4: Project(SeqScan) without Filter or Limit.
1194                // Decode only projected columns; the previous fall-through
1195                // built full Vec<Value> rows then re-projected.
1196                if let PlanNode::SeqScan { table } = input.as_ref() {
1197                    if let Some(result) =
1198                        self.project_filter_limit_fast(table, fields, usize::MAX, None)?
1199                    {
1200                        return Ok(result);
1201                    }
1202                }
1203
1204                let result = self.execute_plan(input)?;
1205                match result {
1206                    QueryResult::Rows { columns, rows } => {
1207                        let proj_columns: Vec<String> = fields
1208                            .iter()
1209                            .map(|f| {
1210                                f.alias.clone().unwrap_or_else(|| match &f.expr {
1211                                    Expr::Field(name) => name.clone(),
1212                                    // Mission E1.2: `{ u.name }` projects as the
1213                                    // qualified column name so callers can still
1214                                    // disambiguate across the join output.
1215                                    Expr::QualifiedField { qualifier, field } => {
1216                                        format!("{qualifier}.{field}")
1217                                    }
1218                                    _ => "?".into(),
1219                                })
1220                            })
1221                            .collect();
1222                        let mut cancel = CancelCheck::new();
1223                        let mut proj_rows: Vec<Vec<Value>> = Vec::with_capacity(rows.len());
1224                        for row in &rows {
1225                            cancel.tick()?;
1226                            proj_rows.push(
1227                                fields
1228                                    .iter()
1229                                    .map(|f| eval_expr(&f.expr, row, &columns))
1230                                    .collect(),
1231                            );
1232                        }
1233                        Ok(QueryResult::Rows {
1234                            columns: proj_columns,
1235                            rows: proj_rows,
1236                        })
1237                    }
1238                    _ => Err("project requires row input".into()),
1239                }
1240            }
1241
1242            PlanNode::Sort { input, keys } => {
1243                let result = self.execute_plan(input)?;
1244                match result {
1245                    QueryResult::Rows { columns, mut rows } => {
1246                        // WS2: row-count cap is a cheap secondary guard; the
1247                        // byte budget is the real OOM defense for the sort
1248                        // buffer (a few very large rows pass the row cap).
1249                        if rows.len() > MAX_SORT_ROWS {
1250                            return Err(QueryError::SortLimitExceeded);
1251                        }
1252                        self.charge_rows(&rows)?;
1253                        let key_specs: Vec<(Option<usize>, &Expr, bool)> = keys
1254                            .iter()
1255                            .map(|k| {
1256                                let stored_name = match &k.expr {
1257                                    Expr::Field(name) => Some(name.clone()),
1258                                    Expr::QualifiedField { qualifier, field } => {
1259                                        Some(format!("{qualifier}.{field}"))
1260                                    }
1261                                    _ => None,
1262                                };
1263                                let index = stored_name
1264                                    .as_ref()
1265                                    .and_then(|name| columns.iter().position(|c| c == name));
1266                                if let Some(name) = stored_name {
1267                                    if index.is_none() {
1268                                        return Err(QueryError::ColumnNotFound {
1269                                            table: String::new(),
1270                                            column: name,
1271                                        });
1272                                    }
1273                                }
1274                                Ok((index, &k.expr, k.descending))
1275                            })
1276                            .collect::<Result<_, QueryError>>()?;
1277                        cooperative_stable_sort_by(&mut rows, self.query_memory_limit, |a, b| {
1278                            for &(col_idx, expr, descending) in &key_specs {
1279                                let (left_value, right_value) = match col_idx {
1280                                    Some(index) => (&a[index], &b[index]),
1281                                    None => {
1282                                        let left = eval_expr(expr, a, &columns);
1283                                        let right = eval_expr(expr, b, &columns);
1284                                        let cmp = compare_order_values(&left, &right, descending);
1285                                        if cmp != std::cmp::Ordering::Equal {
1286                                            return cmp;
1287                                        }
1288                                        continue;
1289                                    }
1290                                };
1291                                let cmp = compare_order_values(left_value, right_value, descending);
1292                                if cmp != std::cmp::Ordering::Equal {
1293                                    return cmp;
1294                                }
1295                            }
1296                            std::cmp::Ordering::Equal
1297                        })?;
1298                        Ok(QueryResult::Rows { columns, rows })
1299                    }
1300                    _ => Err("sort requires row input".into()),
1301                }
1302            }
1303
1304            PlanNode::Limit { input, count } => {
1305                let result = self.execute_plan(input)?;
1306                let n = match count {
1307                    Expr::Literal(Literal::Int(v)) => *v as usize,
1308                    _ => return Err("limit must be integer literal".into()),
1309                };
1310                match result {
1311                    QueryResult::Rows { columns, rows } => {
1312                        let mut cancel = CancelCheck::new();
1313                        let mut limited = Vec::with_capacity(n.min(rows.len()));
1314                        for row in rows.into_iter().take(n) {
1315                            cancel.tick()?;
1316                            limited.push(row);
1317                        }
1318                        Ok(QueryResult::Rows {
1319                            columns,
1320                            rows: limited,
1321                        })
1322                    }
1323                    _ => Err("limit requires row input".into()),
1324                }
1325            }
1326
1327            PlanNode::Offset { input, count } => {
1328                let result = self.execute_plan(input)?;
1329                let n = match count {
1330                    Expr::Literal(Literal::Int(v)) => *v as usize,
1331                    _ => return Err("offset must be integer literal".into()),
1332                };
1333                match result {
1334                    QueryResult::Rows { columns, rows } => {
1335                        let mut cancel = CancelCheck::new();
1336                        let mut offset = Vec::with_capacity(rows.len().saturating_sub(n));
1337                        for (index, row) in rows.into_iter().enumerate() {
1338                            cancel.tick()?;
1339                            if index >= n {
1340                                offset.push(row);
1341                            }
1342                        }
1343                        Ok(QueryResult::Rows {
1344                            columns,
1345                            rows: offset,
1346                        })
1347                    }
1348                    _ => Err("offset requires row input".into()),
1349                }
1350            }
1351
1352            PlanNode::Aggregate {
1353                input,
1354                function,
1355                argument,
1356                mode: _,
1357                provenance_alias,
1358            } => {
1359                if let Some(provenance_alias) = provenance_alias {
1360                    let input = self.materialize_rows_with_provenance(input)?;
1361                    self.charge_rows(&input.rows)?;
1362                    return aggregate_rows_with_provenance(
1363                        *function,
1364                        argument.as_ref(),
1365                        &input,
1366                        provenance_alias,
1367                        self.query_memory_limit(),
1368                    );
1369                }
1370                // Fast path: count() over SeqScan — count rows without any decode
1371                if *function == AggFunc::Count {
1372                    // Overflow safety (P0-4): the raw `for_each_row_raw` count
1373                    // drops any row too large to re-inline (>= 64KB) and would
1374                    // undercount; v2-capable tables use the decoded generic path.
1375                    if let PlanNode::SeqScan { table } = input.as_ref() {
1376                        if !self.catalog.table_has_overflow(table) {
1377                            // Auto-refresh a dirty materialized view before
1378                            // counting it — otherwise count(View) returns stale
1379                            // data after an underlying mutation (F3).
1380                            if self.view_registry.is_dirty(table) {
1381                                self.refresh_view(table)?;
1382                            }
1383                            let mut count: i64 = 0;
1384                            for_each_row_raw_cancellable(&self.catalog, table, |_rid, _data| {
1385                                count += 1;
1386                            })?;
1387                            return Ok(QueryResult::Scalar(Value::Int(count)));
1388                        }
1389                    }
1390                    // Fast path: count() over Filter(SeqScan) — try compiled
1391                    // predicate first, fall back to decode_column path.
1392                    // Skip a predicate carrying a subquery: the raw-bytes
1393                    // evaluators here don't materialise subqueries, so
1394                    // `count(T filter .x in (...))` would silently count 0
1395                    // (F1). Falling through routes it to the generic path
1396                    // that resolves the subquery correctly.
1397                    if let PlanNode::Filter {
1398                        input: inner,
1399                        predicate,
1400                    } = input.as_ref()
1401                    {
1402                        if let PlanNode::SeqScan { table } = inner.as_ref() {
1403                            if self.view_registry.is_dirty(table) {
1404                                self.refresh_view(table)?;
1405                            }
1406                        }
1407                        if let (PlanNode::SeqScan { table }, false) =
1408                            (inner.as_ref(), contains_subquery(predicate))
1409                        {
1410                            if !self.catalog.table_has_overflow(table) {
1411                                let schema = self
1412                                    .catalog
1413                                    .schema(table)
1414                                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
1415                                    .clone();
1416                                let columns: Vec<String> =
1417                                    schema.columns.iter().map(|c| c.name.clone()).collect();
1418                                let fast = FastLayout::new(&schema);
1419                                let row_layout = RowLayout::new(&schema);
1420
1421                                // Try compiled predicate (zero-allocation hot path).
1422                                // Handles int leaves, string-eq leaves, AND conjunctions.
1423                                if let Some(compiled) =
1424                                    compile_predicate(predicate, &columns, &fast, &schema)
1425                                {
1426                                    let mut count: i64 = 0;
1427                                    for_each_row_raw_cancellable(
1428                                        &self.catalog,
1429                                        table,
1430                                        |_rid, data| {
1431                                            if compiled(data) {
1432                                                count += 1;
1433                                            }
1434                                        },
1435                                    )?;
1436                                    return Ok(QueryResult::Scalar(Value::Int(count)));
1437                                }
1438
1439                                // Fallback: decode predicate columns
1440                                let pred_cols = predicate_column_indices_json(predicate, &columns);
1441                                let mut count: i64 = 0;
1442                                for_each_row_raw_cancellable(
1443                                    &self.catalog,
1444                                    table,
1445                                    |_rid, data| {
1446                                        let pred_row = decode_selective(
1447                                            &schema,
1448                                            &row_layout,
1449                                            data,
1450                                            &pred_cols,
1451                                        );
1452                                        if eval_predicate(predicate, &pred_row, &columns) {
1453                                            count += 1;
1454                                        }
1455                                    },
1456                                )?;
1457
1458                                return Ok(QueryResult::Scalar(Value::Int(count)));
1459                            }
1460                        }
1461                    }
1462                }
1463
1464                // Fast path: sum/avg/min/max over a single fixed-size int
1465                // column with an optional compiled filter predicate. Walks
1466                // raw row bytes, zero allocation per row.
1467                if matches!(
1468                    function,
1469                    AggFunc::Sum
1470                        | AggFunc::Avg
1471                        | AggFunc::Min
1472                        | AggFunc::Max
1473                        | AggFunc::CountDistinct
1474                ) {
1475                    if let Some(Expr::Field(col)) = argument.as_ref() {
1476                        // Shape: Aggregate(SeqScan) or Aggregate(Filter(SeqScan))
1477                        let (table_opt, pred_opt): (Option<&str>, Option<&Expr>) =
1478                            match input.as_ref() {
1479                                PlanNode::SeqScan { table } => (Some(table.as_str()), None),
1480                                PlanNode::Filter {
1481                                    input: inner,
1482                                    predicate,
1483                                } => {
1484                                    if let PlanNode::SeqScan { table } = inner.as_ref() {
1485                                        (Some(table.as_str()), Some(predicate))
1486                                    } else {
1487                                        (None, None)
1488                                    }
1489                                }
1490                                _ => (None, None),
1491                            };
1492                        if let Some(table) = table_opt {
1493                            if let Some(result) =
1494                                self.agg_single_col_fast(table, col, *function, pred_opt)?
1495                            {
1496                                return Ok(result);
1497                            }
1498                        }
1499                    }
1500                }
1501
1502                // Fast path: Project(Limit(Filter(SeqScan))) — stream, decode
1503                // only projected columns, stop once we hit the limit.
1504                // (Handled in the Project branch; this branch only fires when
1505                // the aggregate is the outer node.)
1506                let result = self.execute_plan(input)?;
1507                match result {
1508                    QueryResult::Rows { columns, rows } => {
1509                        aggregate_rows(*function, argument.as_ref(), &columns, &rows)
1510                    }
1511                    _ => Err("aggregate requires row input".into()),
1512                }
1513            }
1514
1515            PlanNode::Insert {
1516                table,
1517                rows,
1518                returning,
1519            } => {
1520                // Build + validate EVERY row before inserting any, so a bad
1521                // row (unknown/missing/uncoercible field) aborts the whole
1522                // statement without a partial write. The WAL fsync happens
1523                // once at statement end, so N rows = N appends + 1 fsync.
1524                let mut returning_columns: Vec<String> = Vec::new();
1525                let all_values: Vec<Vec<Value>> = {
1526                    let schema = self
1527                        .catalog
1528                        .schema(table)
1529                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1530                    if *returning {
1531                        returning_columns = schema.columns.iter().map(|c| c.name.clone()).collect();
1532                    }
1533                    let defaults = self.catalog.column_defaults(table).unwrap_or(&[]);
1534                    let auto = self.catalog.auto_columns(table).unwrap_or(&[]);
1535                    let mut all = Vec::with_capacity(rows.len());
1536                    for assignments in rows {
1537                        let mut values = vec![Value::Empty; schema.columns.len()];
1538                        for a in assignments {
1539                            let idx = schema.column_index(&a.field).ok_or_else(|| {
1540                                QueryError::ColumnNotFound {
1541                                    table: String::new(),
1542                                    column: a.field.clone(),
1543                                }
1544                            })?;
1545                            let raw = literal_to_value(&a.value)?;
1546                            values[idx] = coerce_value(raw, &schema.columns[idx])?;
1547                        }
1548                        // Fill any column left unset by this row from its
1549                        // declared default (applied before the required check,
1550                        // so a default satisfies a required column).
1551                        for (i, slot) in values.iter_mut().enumerate() {
1552                            if slot.is_empty() {
1553                                if let Some(Some(d)) = defaults.get(i) {
1554                                    *slot = d.clone();
1555                                }
1556                            }
1557                        }
1558                        for col in &schema.columns {
1559                            let pos = col.position as usize;
1560                            // Auto columns are exempt from the required check —
1561                            // they are filled from the sequence just below.
1562                            let is_auto = auto.get(pos).copied().unwrap_or(false);
1563                            if col.required && !is_auto && matches!(values[pos], Value::Empty) {
1564                                return Err(QueryError::Execution(format!(
1565                                    "column '{}' is required but no value was provided",
1566                                    col.name
1567                                )));
1568                            }
1569                        }
1570                        all.push(values);
1571                    }
1572                    all
1573                };
1574                // Assign auto-increment columns now that the immutable
1575                // schema/defaults/auto borrows are released. Done here (not in
1576                // the build loop) so the assigned ids land in `all_values` and
1577                // flow back through `returning`.
1578                let mut all_values = all_values;
1579                for values in all_values.iter_mut() {
1580                    self.catalog.assign_auto_columns(table, values);
1581                }
1582                // Charge the materialized batch against the per-query memory
1583                // budget before inserting — keeps multi-row insert consistent
1584                // with every other full-materialization point (sort/join/group)
1585                // and bounds embedded callers (the server also caps the query
1586                // string at 1 MB, but embedded callers have no such limit).
1587                self.charge_rows(&all_values)?;
1588                let n = all_values.len() as u64;
1589                for values in &all_values {
1590                    self.catalog
1591                        .insert(table, values)
1592                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1593                }
1594                self.view_registry.mark_dependents_dirty(table);
1595                if *returning {
1596                    Ok(QueryResult::Rows {
1597                        columns: returning_columns,
1598                        rows: all_values,
1599                    })
1600                } else {
1601                    Ok(QueryResult::Modified(n))
1602                }
1603            }
1604
1605            PlanNode::Upsert {
1606                table,
1607                key_column,
1608                assignments,
1609                on_conflict,
1610            } => {
1611                let (values, key_idx) = {
1612                    let schema = self
1613                        .catalog
1614                        .schema(table)
1615                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1616                    let mut values = vec![Value::Empty; schema.columns.len()];
1617                    for a in assignments {
1618                        let idx = schema.column_index(&a.field).ok_or_else(|| {
1619                            QueryError::ColumnNotFound {
1620                                table: String::new(),
1621                                column: a.field.clone(),
1622                            }
1623                        })?;
1624                        let raw = literal_to_value(&a.value)?;
1625                        values[idx] = coerce_value(raw, &schema.columns[idx])?;
1626                    }
1627                    // Apply column defaults for the insert path, same as a plain
1628                    // insert (applied before the required-column check).
1629                    let defaults = self.catalog.column_defaults(table).unwrap_or(&[]);
1630                    for (i, slot) in values.iter_mut().enumerate() {
1631                        if slot.is_empty() {
1632                            if let Some(Some(d)) = defaults.get(i) {
1633                                *slot = d.clone();
1634                            }
1635                        }
1636                    }
1637                    for col in &schema.columns {
1638                        if col.required && matches!(values[col.position as usize], Value::Empty) {
1639                            return Err(QueryError::Execution(format!(
1640                                "column '{}' is required but no value was provided",
1641                                col.name
1642                            )));
1643                        }
1644                    }
1645                    let key_idx = schema
1646                        .column_index(key_column)
1647                        .ok_or_else(|| format!("key column '{key_column}' not found"))?;
1648                    (values, key_idx)
1649                };
1650
1651                // Upsert requires the `on` column to be unique — otherwise
1652                // there is no well-defined row to overwrite and a plain
1653                // insert could silently create duplicate keys.
1654                if self.catalog.is_index_unique(table, key_column) != Some(true) {
1655                    return Err(QueryError::Execution(format!(
1656                        "upsert on .{key_column} requires a unique column (declare it with \
1657                         `unique {key_column}: <type>` or `alter {table} add unique .{key_column}`)"
1658                    )));
1659                }
1660
1661                let key_value = values[key_idx].clone();
1662
1663                // Probe the unique index for a conflict.
1664                let existing = {
1665                    let tbl = self
1666                        .catalog
1667                        .get_table(table)
1668                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1669                    // The key column is guaranteed unique above, so this
1670                    // returns at most one matching row.
1671                    let rids = tbl.index_lookup_all(key_column, &key_value);
1672                    // Overflow safety (P0-3): reassemble via `tbl.get` so an
1673                    // upsert conflict row with a spilled column is read in full.
1674                    rids.into_iter()
1675                        .next()
1676                        .and_then(|rid| tbl.get(rid).map(|row| (rid, row)))
1677                };
1678
1679                if let Some((rid, mut existing_row)) = existing {
1680                    // Conflict: apply on_conflict assignments (or all non-key if empty).
1681                    let update_assignments = if on_conflict.is_empty() {
1682                        assignments
1683                    } else {
1684                        on_conflict
1685                    };
1686                    let changed_cols: Vec<usize> = {
1687                        let schema = self
1688                            .catalog
1689                            .schema(table)
1690                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1691                        let mut indices = Vec::new();
1692                        for a in update_assignments {
1693                            let idx = schema.column_index(&a.field).ok_or_else(|| {
1694                                QueryError::ColumnNotFound {
1695                                    table: String::new(),
1696                                    column: a.field.clone(),
1697                                }
1698                            })?;
1699                            if idx != key_idx {
1700                                // Coerce to the target column type, same as the
1701                                // UPDATE and INSERT paths — an int→float literal
1702                                // here would otherwise persist as raw i64 bits
1703                                // (#118 corruption on the upsert conflict path).
1704                                existing_row[idx] =
1705                                    coerce_value(literal_to_value(&a.value)?, &schema.columns[idx])
1706                                        .map_err(QueryError::TypeError)?;
1707                                indices.push(idx);
1708                            }
1709                        }
1710                        indices
1711                    };
1712                    self.catalog
1713                        .update_hinted(table, rid, &existing_row, Some(&changed_cols))
1714                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1715                    self.view_registry.mark_dependents_dirty(table);
1716                    Ok(QueryResult::Modified(1))
1717                } else {
1718                    // No conflict: insert.
1719                    self.catalog
1720                        .insert(table, &values)
1721                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1722                    self.view_registry.mark_dependents_dirty(table);
1723                    Ok(QueryResult::Modified(1))
1724                }
1725            }
1726
1727            PlanNode::Update {
1728                input,
1729                table,
1730                assignments,
1731                returning,
1732            } => {
1733                // Mission C Phase 3: resolve assignments against a borrowed
1734                // schema, then drop the borrow before the mutation loop.
1735                // Try literal-only path first; fall back to per-row expression
1736                // evaluation if any assignment contains a non-literal expression
1737                // (e.g., `age := .age + 1`).
1738                let (col_indices, literal_vals, target_cols): (
1739                    Vec<usize>,
1740                    Option<Vec<Value>>,
1741                    Vec<ColumnDef>,
1742                ) = {
1743                    let schema_ref = self
1744                        .catalog
1745                        .schema(table)
1746                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1747                    let indices: Vec<usize> = assignments
1748                        .iter()
1749                        .map(|a| {
1750                            schema_ref.column_index(&a.field).ok_or_else(|| {
1751                                QueryError::ColumnNotFound {
1752                                    table: String::new(),
1753                                    column: a.field.clone(),
1754                                }
1755                            })
1756                        })
1757                        .collect::<Result<_, _>>()?;
1758                    // The target column defs (aligned with `assignments`), owned
1759                    // so the per-row expression path can coerce without holding a
1760                    // catalog borrow across the mutation loop.
1761                    let target_cols: Vec<ColumnDef> = indices
1762                        .iter()
1763                        .map(|&idx| schema_ref.columns[idx].clone())
1764                        .collect();
1765                    // Resolve each assignment to a literal value. If any is a
1766                    // non-literal expression, fall back (None) to the per-row
1767                    // expression-eval path below.
1768                    let raw_vals: Result<Vec<Value>, _> = assignments
1769                        .iter()
1770                        .map(|a| literal_to_value(&a.value))
1771                        .collect();
1772                    // Coerce each literal to its target column's declared type
1773                    // before it can reach the byte-patch fast path (the same
1774                    // coercion the INSERT path applies). Without this, an int
1775                    // assigned to a float column is written as raw i64 bits
1776                    // (#118 silent corruption) and a str assigned to a
1777                    // fixed-size column reaches `unreachable!` and aborts the
1778                    // whole server (#117 remote DoS). A genuine type mismatch
1779                    // is a hard error to the client, not an expr-path fallback.
1780                    let coerced = match raw_vals {
1781                        Ok(raws) => {
1782                            let mut out = Vec::with_capacity(raws.len());
1783                            for (raw, &idx) in raws.into_iter().zip(indices.iter()) {
1784                                out.push(
1785                                    coerce_value(raw, &schema_ref.columns[idx])
1786                                        .map_err(QueryError::TypeError)?,
1787                                );
1788                            }
1789                            Some(out)
1790                        }
1791                        Err(_) => None,
1792                    };
1793                    (indices, coerced, target_cols)
1794                };
1795                let resolved_assignments: Option<Vec<(usize, Value)>> =
1796                    literal_vals.map(|vals| col_indices.iter().copied().zip(vals).collect());
1797
1798                // Mission C Phase 2: the hint Table::update_hinted needs to
1799                // decide whether to read the old row for index diff.
1800                let changed_cols: Vec<usize> = col_indices.clone();
1801
1802                // ── RETURNING path ──────────────────────────────────────
1803                // `returning` materializes the post-update row image, so the
1804                // byte-patch / fused fast paths (which never decode a row)
1805                // can't serve it. Take the generic decode→mutate→collect
1806                // route. Opt-in only: when `returning` is false every path
1807                // below is byte-for-byte unchanged.
1808                if *returning {
1809                    let columns: Vec<String> = {
1810                        let schema_ref = self
1811                            .catalog
1812                            .schema(table)
1813                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1814                        schema_ref.columns.iter().map(|c| c.name.clone()).collect()
1815                    };
1816                    let matching_rids = self.collect_rids_for_mutation(input, table)?;
1817                    let mut out_rows: Vec<Vec<Value>> = Vec::with_capacity(matching_rids.len());
1818                    // Cancellation is safe while collecting the target set, but
1819                    // once row writes start this executor has no statement-level
1820                    // savepoint. Check at the mutation boundary and then apply the
1821                    // full set without mid-loop cancellation; returning an error
1822                    // after a logged prefix would violate statement atomicity and
1823                    // is especially unsafe inside an explicit transaction.
1824                    crate::cancel::check()?;
1825                    for rid in matching_rids {
1826                        let mut row = match self.catalog.get(table, rid) {
1827                            Some(r) => r,
1828                            None => continue,
1829                        };
1830                        match &resolved_assignments {
1831                            // Literal path: apply the pre-coerced values.
1832                            Some(resolved) => {
1833                                for (idx, val) in resolved.iter() {
1834                                    row[*idx] = val.clone();
1835                                }
1836                            }
1837                            // Expression path: evaluate each RHS against the
1838                            // (progressively mutated) row, then coerce to the
1839                            // target column type before writing — same guard the
1840                            // literal path gets, matching the non-returning expr
1841                            // path exactly (#117/#118 on computed assignments).
1842                            None => {
1843                                for (i, asgn) in assignments.iter().enumerate() {
1844                                    let val = eval_expr(&asgn.value, &row, &columns);
1845                                    row[col_indices[i]] = coerce_value(val, &target_cols[i])
1846                                        .map_err(QueryError::TypeError)?;
1847                                }
1848                            }
1849                        }
1850                        self.catalog
1851                            .update_hinted(table, rid, &row, Some(&changed_cols))
1852                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
1853                        out_rows.push(row);
1854                    }
1855                    self.view_registry.mark_dependents_dirty(table);
1856                    return Ok(QueryResult::Rows {
1857                        columns,
1858                        rows: out_rows,
1859                    });
1860                }
1861
1862                // ── Fused scan+update for Update(Filter(SeqScan)) ────────
1863                // Perf sprint: instead of the two-pass collect-RIDs-then-loop
1864                // pattern (which pays one ensure_hot per matched row on the
1865                // second pass), fuse the predicate evaluation and in-place
1866                // byte-level mutation into a single heap walk. Same idea as
1867                // the fused scan_delete_matching path for deletes.
1868                if let Some(ref resolved_assignments) = resolved_assignments {
1869                    if let PlanNode::Filter {
1870                        input: inner,
1871                        predicate,
1872                    } = input.as_ref()
1873                    {
1874                        if let PlanNode::SeqScan { table: t } = inner.as_ref() {
1875                            if t == table {
1876                                // The fused primitive mutates during its scan and
1877                                // cannot roll back a cancelled prefix. Honor an
1878                                // already-triggered token before entering it, then
1879                                // let the primitive finish atomically from the
1880                                // query layer's perspective.
1881                                crate::cancel::check()?;
1882                                let fused_result = self.try_fused_scan_update(
1883                                    table,
1884                                    predicate,
1885                                    resolved_assignments,
1886                                    &changed_cols,
1887                                );
1888                                if let Some(result) = fused_result {
1889                                    return result;
1890                                }
1891                            }
1892                        }
1893                    }
1894                }
1895
1896                // Collect matching RowIds in a single pass.
1897                let matching_rids = self.collect_rids_for_mutation(input, table)?;
1898                // This is the last cancellable boundary before any row is
1899                // changed. Mutation loops below deliberately do not poll.
1900                crate::cancel::check()?;
1901
1902                // ── Literal-only fast paths ─────────────────────────────
1903                if let Some(ref resolved_assignments) = resolved_assignments {
1904                    // Mission C Phase 4: in-place byte-patch fast path. If every
1905                    // assignment targets a fixed-size non-null column AND none of
1906                    // them is indexed, we can skip decode_row / Vec<Value> /
1907                    // encode_row_into entirely and patch the row's raw bytes on
1908                    // the hot page.
1909                    let fast_patch: Option<Vec<FastPatch>> = {
1910                        let tbl = self
1911                            .catalog
1912                            .get_table(table)
1913                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1914                        let schema = tbl.schema();
1915                        // Overflow safety (P0): byte-patching a v2 row with v1
1916                        // offsets corrupts it. Overflow tables take the generic
1917                        // reassembling `get` + `update_hinted` path below.
1918                        let all_fixed_nonnull = !tbl.has_overflow_rows()
1919                            && resolved_assignments.iter().all(|(idx, val)| {
1920                                is_fixed_size(schema.columns[*idx].type_id) && !val.is_empty()
1921                            });
1922                        let no_indexed = !resolved_assignments
1923                            .iter()
1924                            .any(|(idx, _)| tbl.has_indexed_col(*idx));
1925
1926                        if all_fixed_nonnull && no_indexed {
1927                            let layout = RowLayout::new(schema);
1928                            let bitmap_size = layout.bitmap_size();
1929                            let patches: Vec<FastPatch> = resolved_assignments
1930                                .iter()
1931                                .map(|(idx, val)| {
1932                                    let fixed_off = layout
1933                                        .fixed_offset(*idx)
1934                                        .expect("is_fixed_size already checked");
1935                                    let field_off = 2 + bitmap_size + fixed_off;
1936                                    let bytes: FixedBytes = match val {
1937                                        Value::Int(v) => FixedBytes::I64(v.to_le_bytes()),
1938                                        Value::Float(v) => FixedBytes::F64(v.to_le_bytes()),
1939                                        Value::Bool(v) => FixedBytes::Bool(if *v { 1 } else { 0 }),
1940                                        Value::DateTime(v) => FixedBytes::I64(v.to_le_bytes()),
1941                                        Value::Uuid(v) => FixedBytes::Uuid(*v),
1942                                        _ => unreachable!("all_fixed_nonnull guard lied"),
1943                                    };
1944                                    FastPatch {
1945                                        field_off,
1946                                        bitmap_byte_off: 2 + idx / 8,
1947                                        bit_mask: 1u8 << (idx % 8),
1948                                        bytes,
1949                                    }
1950                                })
1951                                .collect();
1952                            Some(patches)
1953                        } else {
1954                            None
1955                        }
1956                    };
1957
1958                    if let Some(patches) = fast_patch {
1959                        let mut count = 0u64;
1960                        let mut fallback_rids: Vec<RowId> = Vec::new();
1961                        for rid in &matching_rids {
1962                            // Mission B2: WAL-log every patch so crash
1963                            // recovery replays the update. Same mutation
1964                            // closure as before — the wrapper just sandwiches
1965                            // it between a hot-page read and a WAL append.
1966                            //
1967                            // A false return means the byte-patch was refused
1968                            // (e.g. a v2/overflow row whose in-place layout the
1969                            // fast path cannot compute, reachable on a legacy
1970                            // heap where has_overflow_rows() under-reports). Do
1971                            // NOT drop the row: push it to `fallback_rids` and
1972                            // let the reassembling get + update_hinted path
1973                            // apply it, mirroring the var-column fast path
1974                            // below. The fast path is thus a pure optimization
1975                            // that can never silently lose an update.
1976                            let ok = self
1977                                .catalog
1978                                .update_row_bytes_logged(table, *rid, |row| {
1979                                    let base = row_body_base(row);
1980                                    for p in &patches {
1981                                        row[base + p.bitmap_byte_off] &= !p.bit_mask;
1982                                        let field_bytes = p.bytes.as_slice();
1983                                        row[base + p.field_off
1984                                            ..base + p.field_off + field_bytes.len()]
1985                                            .copy_from_slice(field_bytes);
1986                                    }
1987                                })
1988                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
1989                            if ok {
1990                                count += 1;
1991                            } else {
1992                                fallback_rids.push(*rid);
1993                            }
1994                        }
1995                        for rid in fallback_rids {
1996                            let mut row = match self.catalog.get(table, rid) {
1997                                Some(r) => r,
1998                                None => continue,
1999                            };
2000                            for (idx, val) in resolved_assignments.iter() {
2001                                row[*idx] = val.clone();
2002                            }
2003                            self.catalog
2004                                .update_hinted(table, rid, &row, Some(&changed_cols))
2005                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
2006                            count += 1;
2007                        }
2008                        self.view_registry.mark_dependents_dirty(table);
2009                        return Ok(QueryResult::Modified(count));
2010                    }
2011
2012                    // Mission C Phase 10: var-column in-place shrink fast path.
2013                    let var_fast: Option<(usize, Option<Vec<u8>>)> = {
2014                        let tbl = self
2015                            .catalog
2016                            .get_table(table)
2017                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
2018                        let schema = tbl.schema();
2019                        // Overflow safety (P0/P0-2): the in-place var shrink
2020                        // patch computes v1 offsets — never on a v2-capable
2021                        // table. Falls through to the reassembling path.
2022                        let is_single = resolved_assignments.len() == 1 && !tbl.has_overflow_rows();
2023                        let is_var_col = is_single
2024                            && !is_fixed_size(schema.columns[resolved_assignments[0].0].type_id);
2025                        let no_indexed = !resolved_assignments
2026                            .iter()
2027                            .any(|(idx, _)| tbl.has_indexed_col(*idx));
2028
2029                        if is_single && is_var_col && no_indexed {
2030                            let (idx, val) = &resolved_assignments[0];
2031                            let bytes_opt: Option<Vec<u8>> = match val {
2032                                Value::Str(s) => Some(s.as_bytes().to_vec()),
2033                                Value::Bytes(b) => Some(b.clone()),
2034                                // A json column stores its PJ1 bytes as the var
2035                                // payload (u32 length prefix + bytes, like Bytes),
2036                                // so the in-place patch writes them verbatim.
2037                                Value::Json(b) => Some(b.to_vec()),
2038                                Value::Empty => None,
2039                                _ => {
2040                                    return Err(QueryError::TypeError(format!(
2041                                        "cannot assign non-var value to var column '{}'",
2042                                        schema.columns[*idx].name
2043                                    )))
2044                                }
2045                            };
2046                            Some((*idx, bytes_opt))
2047                        } else {
2048                            None
2049                        }
2050                    };
2051
2052                    if let Some((col_idx, new_bytes_opt)) = var_fast {
2053                        let new_bytes_ref: Option<&[u8]> = new_bytes_opt.as_deref();
2054                        let mut count = 0u64;
2055                        let mut fallback_rids: Vec<RowId> = Vec::new();
2056                        for rid in &matching_rids {
2057                            // Mission B2: logged variant so crash recovery
2058                            // replays the shrink. On a false return (row
2059                            // would have to grow), the rid is pushed to
2060                            // `fallback_rids` and the slower `update_hinted`
2061                            // path — which is already WAL-logged — picks it up.
2062                            let ok = self
2063                                .catalog
2064                                .patch_var_col_logged(table, *rid, col_idx, new_bytes_ref)
2065                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
2066                            if ok {
2067                                count += 1;
2068                            } else {
2069                                fallback_rids.push(*rid);
2070                            }
2071                        }
2072                        for rid in fallback_rids {
2073                            let mut row = match self.catalog.get(table, rid) {
2074                                Some(r) => r,
2075                                None => continue,
2076                            };
2077                            for (idx, val) in resolved_assignments.iter() {
2078                                row[*idx] = val.clone();
2079                            }
2080                            self.catalog
2081                                .update_hinted(table, rid, &row, Some(&changed_cols))
2082                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
2083                            count += 1;
2084                        }
2085                        self.view_registry.mark_dependents_dirty(table);
2086                        return Ok(QueryResult::Modified(count));
2087                    }
2088
2089                    // Generic literal path: decode row, apply literal values.
2090                    let mut count = 0u64;
2091                    for rid in matching_rids {
2092                        let mut row = match self.catalog.get(table, rid) {
2093                            Some(r) => r,
2094                            None => continue,
2095                        };
2096                        for (idx, val) in resolved_assignments.iter() {
2097                            row[*idx] = val.clone();
2098                        }
2099                        self.catalog
2100                            .update_hinted(table, rid, &row, Some(&changed_cols))
2101                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
2102                        count += 1;
2103                    }
2104                    self.view_registry.mark_dependents_dirty(table);
2105                    return Ok(QueryResult::Modified(count));
2106                } // end if let Some(resolved_assignments)
2107
2108                // ── Expression-based update path ────────────────────────
2109                // At least one assignment contains a non-literal expression
2110                // (e.g., `age := .age + 1`). Evaluate per-row.
2111                let col_names: Vec<String> = {
2112                    let schema_ref = self
2113                        .catalog
2114                        .schema(table)
2115                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
2116                    schema_ref.columns.iter().map(|c| c.name.clone()).collect()
2117                };
2118                let mut count = 0u64;
2119                for rid in matching_rids {
2120                    let mut row = match self.catalog.get(table, rid) {
2121                        Some(r) => r,
2122                        None => continue,
2123                    };
2124                    for (i, asgn) in assignments.iter().enumerate() {
2125                        let val = eval_expr(&asgn.value, &row, &col_names);
2126                        // Coerce to the target column type before writing, so a
2127                        // computed int→float assignment stores f64 (not raw i64
2128                        // bits, #118) and a str→fixed-col assignment returns a
2129                        // typed error instead of hitting the encoder's
2130                        // `unreachable!` and aborting the process (#117).
2131                        row[col_indices[i]] =
2132                            coerce_value(val, &target_cols[i]).map_err(QueryError::TypeError)?;
2133                    }
2134                    self.catalog
2135                        .update_hinted(table, rid, &row, Some(&changed_cols))
2136                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
2137                    count += 1;
2138                }
2139                self.view_registry.mark_dependents_dirty(table);
2140                Ok(QueryResult::Modified(count))
2141            }
2142
2143            PlanNode::Delete {
2144                input,
2145                table,
2146                returning,
2147            } => {
2148                // ── RETURNING path ──────────────────────────────────────
2149                // `returning` needs the pre-delete row image, so read each
2150                // matched row before removing it. The fused single-pass
2151                // delete primitives below never decode rows, so they can't
2152                // serve this. Opt-in only: when `returning` is false the
2153                // fast paths below are byte-for-byte unchanged.
2154                if *returning {
2155                    let columns: Vec<String> = {
2156                        let schema_ref = self
2157                            .catalog
2158                            .schema(table)
2159                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
2160                        schema_ref.columns.iter().map(|c| c.name.clone()).collect()
2161                    };
2162                    let matching_rids = self.collect_rids_for_mutation(input, table)?;
2163                    let mut out_rows: Vec<Vec<Value>> = Vec::with_capacity(matching_rids.len());
2164                    // Cooperative cancellation of the pre-delete image read. The
2165                    // actual removal below is a single batched `delete_many`, so
2166                    // cancelling here happens before any row is deleted.
2167                    let mut cancel = CancelCheck::new();
2168                    for rid in &matching_rids {
2169                        cancel.tick()?;
2170                        if let Some(row) = self.catalog.get(table, *rid) {
2171                            out_rows.push(row);
2172                        }
2173                    }
2174                    crate::cancel::check()?;
2175                    self.catalog
2176                        .delete_many(table, &matching_rids)
2177                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
2178                    self.view_registry.mark_dependents_dirty(table);
2179                    return Ok(QueryResult::Rows {
2180                        columns,
2181                        rows: out_rows,
2182                    });
2183                }
2184
2185                // Mission C Phase 3: no schema clone — collect_rids_for_mutation
2186                // looks up schema internally when it needs one, and the mutation
2187                // loop doesn't need the schema at all.
2188                //
2189                // Mission C Phase 12: route bulk deletes through
2190                // `Catalog::delete_many`, which batches the btree leaf
2191                // compaction and shares one `ensure_hot` per row between
2192                // the index-key extraction and the slot delete. On
2193                // `delete_by_filter` (100K fixture, ~20K matches) that
2194                // removes ~4ms of pure `Vec::remove` memmove from the btree
2195                // maintenance phase.
2196                //
2197                // Mission C Phase 16: for the common `delete where ...`
2198                // shape (Filter(SeqScan)) — and the rarer "delete
2199                // everything" shape (SeqScan) — skip the two-pass
2200                // `collect_rids_for_mutation` + `delete_many` flow entirely.
2201                // The fused `scan_delete_matching` primitive walks the
2202                // heap exactly once, paying one `ensure_hot` per page
2203                // instead of per-row. That closes the last major gap on
2204                // the bench's `delete_by_filter` workload.
2205                // Overflow safety (P1): a v2-capable table cannot take the fused
2206                // raw-byte delete — the compiled predicate mis-reads spilled
2207                // columns. Route it through the reassembling collect-rids path.
2208                let delete_overflow = self.catalog.table_has_overflow(table);
2209                if let PlanNode::Filter {
2210                    input: inner,
2211                    predicate,
2212                } = input.as_ref()
2213                {
2214                    if let PlanNode::SeqScan { table: t } = inner.as_ref() {
2215                        if t == table && !delete_overflow {
2216                            let schema = self
2217                                .catalog
2218                                .schema(table)
2219                                .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
2220                            let columns: Vec<String> =
2221                                schema.columns.iter().map(|c| c.name.clone()).collect();
2222                            let fast = FastLayout::new(schema);
2223                            if let Some(compiled) =
2224                                compile_predicate(predicate, &columns, &fast, schema)
2225                            {
2226                                // Mission B2: logged variant so every
2227                                // matched rid hits the WAL during the
2228                                // single-pass scan. Structure of the
2229                                // fused scan is unchanged — only the
2230                                // hook closure now also appends.
2231                                crate::cancel::check()?;
2232                                let count = self
2233                                    .catalog
2234                                    .scan_delete_matching_logged(table, |data| compiled(data))
2235                                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
2236                                self.view_registry.mark_dependents_dirty(table);
2237                                return Ok(QueryResult::Modified(count));
2238                            }
2239                        }
2240                    }
2241                } else if let PlanNode::SeqScan { table: t } = input.as_ref() {
2242                    if t == table && !delete_overflow {
2243                        // `delete from T` with no predicate — every live
2244                        // row matches. One pass is still the right shape.
2245                        // Mission B2: logged variant — see above.
2246                        crate::cancel::check()?;
2247                        let count = self
2248                            .catalog
2249                            .scan_delete_matching_logged(table, |_| true)
2250                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
2251                        self.view_registry.mark_dependents_dirty(table);
2252                        return Ok(QueryResult::Modified(count));
2253                    }
2254                }
2255
2256                let matching_rids = self.collect_rids_for_mutation(input, table)?;
2257                crate::cancel::check()?;
2258                let count = self
2259                    .catalog
2260                    .delete_many(table, &matching_rids)
2261                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
2262                self.view_registry.mark_dependents_dirty(table);
2263                Ok(QueryResult::Modified(count))
2264            }
2265
2266            PlanNode::AliasScan { table, alias } => {
2267                // Mission E1.2: scan `table` and rename every output column
2268                // to `alias.field`. Used as a join leaf so downstream
2269                // NestedLoopJoin + Filter + Project nodes can resolve
2270                // `Expr::QualifiedField` lookups by direct column-name match.
2271                //
2272                // We don't bother with a fused zero-copy loop here yet — the
2273                // whole join path is nested-loop and correctness-first
2274                // (Phase E1.3 will introduce hash join and at that point we
2275                // can revisit whether to specialise AliasScan).
2276                let schema = self
2277                    .catalog
2278                    .schema(table)
2279                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
2280                    .clone();
2281                let columns: Vec<String> = schema
2282                    .columns
2283                    .iter()
2284                    .map(|c| format!("{alias}.{}", c.name))
2285                    .collect();
2286                let mut cancel = CancelCheck::new();
2287                let mut rows: Vec<Vec<Value>> = Vec::new();
2288                for (_, row) in self
2289                    .catalog
2290                    .scan(table)
2291                    .map_err(|e| QueryError::StorageError(e.to_string()))?
2292                {
2293                    cancel.tick()?;
2294                    rows.push(row);
2295                }
2296                Ok(QueryResult::Rows { columns, rows })
2297            }
2298
2299            PlanNode::NestedLoopJoin {
2300                left,
2301                right,
2302                on,
2303                kind,
2304            } => {
2305                // Materialise both sides. The executor ships two strategies:
2306                //   1. Hash join (E1.3) — when the `on` predicate is a
2307                //      simple equi-predicate `left_col = right_col`, build a
2308                //      FxHashMap<Value, Vec<row_idx>> over the right side
2309                //      and probe with the left side. O(L + R) instead of
2310                //      O(L × R). Handles Inner and LeftOuter.
2311                //   2. Nested loop (E1.2) — fallback for Cross, non-equi
2312                //      predicates, or `on` expressions that reference
2313                //      either side with something more complex than a
2314                //      QualifiedField.
2315                let left_result = self.execute_plan(left)?;
2316                let right_result = self.execute_plan(right)?;
2317                let (left_columns, left_rows) = match left_result {
2318                    QueryResult::Rows { columns, rows } => (columns, rows),
2319                    _ => return Err("join left side must produce rows".into()),
2320                };
2321                let (right_columns, right_rows) = match right_result {
2322                    QueryResult::Rows { columns, rows } => (columns, rows),
2323                    _ => return Err("join right side must produce rows".into()),
2324                };
2325
2326                // WS2: byte-budget guard on the join build side. Charge both
2327                // materialized inputs before we build the hash table / probe;
2328                // the output is row-capped by check_join_limit below.
2329                self.charge_rows(&left_rows)?;
2330                self.charge_rows(&right_rows)?;
2331
2332                execute_materialized_join(
2333                    left_columns,
2334                    left_rows,
2335                    right_columns,
2336                    right_rows,
2337                    on.as_ref(),
2338                    *kind,
2339                    self.nested_loop_pair_limit,
2340                )
2341            }
2342
2343            PlanNode::Distinct { input } => {
2344                let result = self.execute_plan(input)?;
2345                match result {
2346                    QueryResult::Rows { columns, rows } => {
2347                        let mut seen = std::collections::HashSet::new();
2348                        let mut unique_rows = Vec::new();
2349                        let mut cancel = CancelCheck::new();
2350                        for row in rows {
2351                            cancel.tick()?;
2352                            if seen.insert(row.clone()) {
2353                                unique_rows.push(row);
2354                            }
2355                        }
2356                        Ok(QueryResult::Rows {
2357                            columns,
2358                            rows: unique_rows,
2359                        })
2360                    }
2361                    other => Ok(other),
2362                }
2363            }
2364
2365            PlanNode::GroupBy {
2366                input,
2367                keys,
2368                aggregates,
2369                having,
2370            } => {
2371                if aggregates
2372                    .iter()
2373                    .any(|aggregate| aggregate.provenance_alias.is_some())
2374                {
2375                    let input = self.materialize_rows_with_provenance(input)?;
2376                    self.charge_rows(&input.rows)?;
2377                    return exec_group_by_with_provenance(
2378                        input,
2379                        keys,
2380                        aggregates,
2381                        having,
2382                        self.query_memory_limit(),
2383                    );
2384                }
2385                let result = self.execute_plan(input)?;
2386                match result {
2387                    QueryResult::Rows { columns, rows } => {
2388                        // WS2: byte-budget guard on the GROUP BY input buffer
2389                        // (the hash table is bounded by the input it groups).
2390                        self.charge_rows(&rows)?;
2391                        exec_group_by(columns, rows, keys, aggregates, having)
2392                    }
2393                    _ => Err("group by requires row input".into()),
2394                }
2395            }
2396
2397            PlanNode::CreateTable {
2398                name,
2399                fields,
2400                if_not_exists,
2401            } => {
2402                // Idempotency: a re-declared type is a clean no-op under
2403                // `if not exists`, and otherwise a PowQL-flavored error that
2404                // names the type (not the storage layer's generic "table").
2405                if self.catalog.schema(name).is_some() {
2406                    if *if_not_exists {
2407                        return Ok(QueryResult::Executed {
2408                            message: format!("type '{name}' already exists (skipped)"),
2409                        });
2410                    }
2411                    // "cannot" prefix keeps this on the server's
2412                    // safe-to-forward allowlist (SAFE_ERROR_PREFIXES).
2413                    return Err(QueryError::Execution(format!(
2414                        "cannot create type '{name}': it already exists"
2415                    )));
2416                }
2417                let columns: Vec<ColumnDef> = fields
2418                    .iter()
2419                    .enumerate()
2420                    .map(|(i, f)| -> Result<ColumnDef, QueryError> {
2421                        Ok(ColumnDef {
2422                            name: f.name.clone(),
2423                            type_id: type_name_to_id(&f.type_name)
2424                                .map_err(QueryError::TypeError)?,
2425                            required: f.required,
2426                            position: i as u16,
2427                        })
2428                    })
2429                    .collect::<Result<Vec<_>, _>>()?;
2430                // Coerce each literal default to its column's type now, so a
2431                // type mismatch (`count: int default "x"`) is rejected at DDL
2432                // time and the stored default is ready to drop into inserts.
2433                let mut defaults: Vec<Option<Value>> = vec![None; columns.len()];
2434                let mut auto_cols: Vec<bool> = vec![false; columns.len()];
2435                for (i, f) in fields.iter().enumerate() {
2436                    if let Some(lit) = &f.default {
2437                        let raw = literal_value_from(lit);
2438                        defaults[i] = Some(coerce_value(raw, &columns[i])?);
2439                    }
2440                    if f.auto {
2441                        // Auto-increment only makes sense on an integer column,
2442                        // and combining it with a literal default is
2443                        // contradictory (both want to supply the value).
2444                        if columns[i].type_id != TypeId::Int {
2445                            return Err(QueryError::TypeError(format!(
2446                                "auto column '{}' must be of type int",
2447                                f.name
2448                            )));
2449                        }
2450                        if f.default.is_some() {
2451                            return Err(QueryError::TypeError(format!(
2452                                "auto column '{}' cannot also declare a default",
2453                                f.name
2454                            )));
2455                        }
2456                        auto_cols[i] = true;
2457                    }
2458                }
2459                let schema = Schema {
2460                    table_name: name.clone(),
2461                    columns,
2462                };
2463                self.catalog
2464                    .create_table_full(schema, defaults, auto_cols)
2465                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
2466                // Declaring a field `unique` auto-creates a unique B+tree
2467                // index, which is where uniqueness is enforced on writes.
2468                for f in fields.iter().filter(|f| f.unique) {
2469                    self.catalog
2470                        .create_index_unique(name, &f.name, true)
2471                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
2472                }
2473                Ok(QueryResult::Created(name.clone()))
2474            }
2475
2476            PlanNode::AlterTable { table, action } => match action {
2477                AlterAction::AddColumn {
2478                    name,
2479                    type_name,
2480                    required,
2481                } => {
2482                    let position = self
2483                        .catalog
2484                        .schema(table)
2485                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
2486                        .columns
2487                        .len() as u16;
2488                    let col = ColumnDef {
2489                        name: name.clone(),
2490                        type_id: type_name_to_id(type_name).map_err(QueryError::TypeError)?,
2491                        required: *required,
2492                        position,
2493                    };
2494                    self.catalog
2495                        .alter_table_add_column(table, col)
2496                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
2497                    Ok(QueryResult::Executed {
2498                        message: format!("column '{name}' added to '{table}'"),
2499                    })
2500                }
2501                AlterAction::DropColumn { name, if_exists } => {
2502                    // `if exists`: a missing column (or missing table) is a
2503                    // no-op instead of an error.
2504                    if *if_exists {
2505                        let present = self
2506                            .catalog
2507                            .schema(table)
2508                            .map(|s| s.column_index(name).is_some())
2509                            .unwrap_or(false);
2510                        if !present {
2511                            return Ok(QueryResult::Executed {
2512                                message: format!(
2513                                    "column '{name}' does not exist on '{table}' (skipped)"
2514                                ),
2515                            });
2516                        }
2517                    }
2518                    self.catalog
2519                        .alter_table_drop_column(table, name)
2520                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
2521                    Ok(QueryResult::Executed {
2522                        message: format!("column '{name}' dropped from '{table}'"),
2523                    })
2524                }
2525                AlterAction::AddIndex {
2526                    target,
2527                    if_not_exists: _,
2528                } => {
2529                    let IndexTarget::Column(column) = target else {
2530                        let IndexTarget::JsonPath(path) = target else {
2531                            unreachable!("index target variants are exhaustive")
2532                        };
2533                        if let Some(existing) = resolve_expression_index(&self.catalog, table, path)
2534                        {
2535                            return Ok(QueryResult::Executed {
2536                                message: format!(
2537                                    "expression index {} on '{}' already exists (skipped)",
2538                                    existing.index_id, table
2539                                ),
2540                            });
2541                        }
2542                        crate::cancel::check()?;
2543                        let index_id = self
2544                            .catalog
2545                            .create_expression_index_metadata(
2546                                table,
2547                                1,
2548                                path.canonical_text(),
2549                                path.clone(),
2550                                false,
2551                            )
2552                            .map_err(|error| QueryError::StorageError(error.to_string()))?;
2553                        return Ok(QueryResult::Executed {
2554                            message: format!("expression index {index_id} on '{}' created", table),
2555                        });
2556                    };
2557                    // `add index` is already idempotent (no-op if the index
2558                    // exists), so `if not exists` is accepted for symmetry but
2559                    // does not change behavior.
2560                    crate::cancel::check()?;
2561                    self.catalog
2562                        .create_index(table, column)
2563                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
2564                    Ok(QueryResult::Executed {
2565                        message: format!("index on '{table}.{column}' created"),
2566                    })
2567                }
2568                AlterAction::AddUnique {
2569                    target,
2570                    if_not_exists,
2571                } => {
2572                    let IndexTarget::Column(column) = target else {
2573                        let IndexTarget::JsonPath(path) = target else {
2574                            unreachable!("index target variants are exhaustive")
2575                        };
2576                        if let Some(existing) = resolve_expression_index(&self.catalog, table, path)
2577                        {
2578                            if *if_not_exists {
2579                                return Ok(QueryResult::Executed {
2580                                    message: format!(
2581                                        "expression index {} on '{}' already exists (skipped)",
2582                                        existing.index_id, table
2583                                    ),
2584                                });
2585                            }
2586                            return Err(QueryError::Execution(format!(
2587                                "cannot add unique expression index on {}: path already indexed",
2588                                table
2589                            )));
2590                        }
2591                        crate::cancel::check()?;
2592                        let index_id = self
2593                            .catalog
2594                            .create_expression_index_metadata(
2595                                table,
2596                                1,
2597                                path.canonical_text(),
2598                                path.clone(),
2599                                true,
2600                            )
2601                            .map_err(|error| QueryError::StorageError(error.to_string()))?;
2602                        return Ok(QueryResult::Executed {
2603                            message: format!(
2604                                "unique expression index {index_id} on '{}' created",
2605                                table
2606                            ),
2607                        });
2608                    };
2609                    // `if not exists`: an already-indexed column is a no-op
2610                    // rather than the (default) "already indexed" error.
2611                    if self.catalog.has_index(table, column) {
2612                        if *if_not_exists {
2613                            return Ok(QueryResult::Executed {
2614                                message: format!(
2615                                    "index on '{table}.{column}' already exists (skipped)"
2616                                ),
2617                            });
2618                        }
2619                        // Upgrading an existing non-unique index in place is
2620                        // intentionally rejected.
2621                        return Err(QueryError::Execution(format!(
2622                            "cannot add unique on {table}.{column}: column already indexed"
2623                        )));
2624                    }
2625                    // Scan existing rows for duplicate (non-null) values
2626                    // before creating the unique index.
2627                    {
2628                        let tbl = self
2629                            .catalog
2630                            .get_table(table)
2631                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
2632                        let col_idx = tbl.schema().column_index(column).ok_or_else(|| {
2633                            QueryError::ColumnNotFound {
2634                                table: table.to_string(),
2635                                column: column.clone(),
2636                            }
2637                        })?;
2638                        let mut seen = std::collections::HashSet::new();
2639                        let mut cancel = CancelCheck::new();
2640                        for (_, row) in tbl.scan() {
2641                            cancel.tick()?;
2642                            let v = &row[col_idx];
2643                            if v.is_empty() {
2644                                continue;
2645                            }
2646                            if !seen.insert(v.clone()) {
2647                                return Err(QueryError::Execution(format!(
2648                                    "cannot add unique on {table}.{column}: \
2649                                     duplicate value {v:?} exists"
2650                                )));
2651                            }
2652                        }
2653                    }
2654                    crate::cancel::check()?;
2655                    self.catalog
2656                        .create_index_unique(table, column, true)
2657                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
2658                    Ok(QueryResult::Executed {
2659                        message: format!("unique index on '{table}.{column}' created"),
2660                    })
2661                }
2662                AlterAction::DropIndex { target, if_exists } => {
2663                    let IndexTarget::JsonPath(path) = target else {
2664                        return Err(QueryError::Execution(
2665                            "dropping stored-column indexes is not supported".to_string(),
2666                        ));
2667                    };
2668                    let Some(existing) = resolve_expression_index(&self.catalog, table, path)
2669                    else {
2670                        if *if_exists {
2671                            return Ok(QueryResult::Executed {
2672                                message: format!(
2673                                    "expression index on '{}' does not exist (skipped)",
2674                                    table
2675                                ),
2676                            });
2677                        }
2678                        return Err(QueryError::Execution(format!(
2679                            "expression index on '{}' does not exist",
2680                            table
2681                        )));
2682                    };
2683                    crate::cancel::check()?;
2684                    self.catalog
2685                        .drop_expression_index(table, existing.index_id)
2686                        .map_err(|error| QueryError::StorageError(error.to_string()))?;
2687                    Ok(QueryResult::Executed {
2688                        message: format!(
2689                            "expression index {} on '{}' dropped",
2690                            existing.index_id, table
2691                        ),
2692                    })
2693                }
2694            },
2695
2696            PlanNode::DropTable { name, if_exists } => {
2697                if *if_exists && self.catalog.schema(name).is_none() {
2698                    return Ok(QueryResult::Executed {
2699                        message: format!("type '{name}' does not exist (skipped)"),
2700                    });
2701                }
2702                self.catalog
2703                    .drop_table(name)
2704                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
2705                Ok(QueryResult::Executed {
2706                    message: format!("table '{name}' dropped"),
2707                })
2708            }
2709
2710            PlanNode::ListTypes => self.introspect_list_types(),
2711
2712            PlanNode::Describe { table } => self.introspect_describe(table),
2713
2714            PlanNode::CreateView { name, query_text } => {
2715                self.create_view(name, query_text)?;
2716                Ok(QueryResult::Executed {
2717                    message: format!("materialized view '{name}' created"),
2718                })
2719            }
2720
2721            PlanNode::RefreshView { name } => {
2722                self.refresh_view(name)?;
2723                Ok(QueryResult::Executed {
2724                    message: format!("materialized view '{name}' refreshed"),
2725                })
2726            }
2727
2728            PlanNode::DropView { name, if_exists } => {
2729                if *if_exists && !self.view_registry.is_view(name) {
2730                    return Ok(QueryResult::Executed {
2731                        message: format!("view '{name}' does not exist (skipped)"),
2732                    });
2733                }
2734                self.drop_view(name)?;
2735                Ok(QueryResult::Executed {
2736                    message: format!("materialized view '{name}' dropped"),
2737                })
2738            }
2739
2740            PlanNode::Window { input, windows } => {
2741                let result = self.execute_plan(input)?;
2742                execute_window(result, windows, self.query_memory_limit)
2743            }
2744
2745            PlanNode::Union { left, right, all } => {
2746                let left_result = self.execute_plan(left)?;
2747                let right_result = self.execute_plan(right)?;
2748                let (left_cols, left_rows) = match left_result {
2749                    QueryResult::Rows { columns, rows } => (columns, rows),
2750                    _ => return Err("UNION requires query results on left side".into()),
2751                };
2752                let (_, right_rows) = match right_result {
2753                    QueryResult::Rows { columns, rows } => (columns, rows),
2754                    _ => return Err("UNION requires query results on right side".into()),
2755                };
2756                let mut combined = left_rows;
2757                let mut cancel = CancelCheck::new();
2758                if *all {
2759                    // UNION ALL — just concatenate.
2760                    for row in right_rows {
2761                        cancel.tick()?;
2762                        combined.push(row);
2763                    }
2764                } else {
2765                    // UNION — deduplicate using the same HashSet approach
2766                    // as DISTINCT. Value already implements Hash + Eq.
2767                    let mut seen = std::collections::HashSet::new();
2768                    for row in &combined {
2769                        cancel.tick()?;
2770                        seen.insert(row.clone());
2771                    }
2772                    for row in right_rows {
2773                        cancel.tick()?;
2774                        if seen.insert(row.clone()) {
2775                            combined.push(row);
2776                        }
2777                    }
2778                }
2779                Ok(QueryResult::Rows {
2780                    columns: left_cols,
2781                    rows: combined,
2782                })
2783            }
2784
2785            PlanNode::Explain { input } => {
2786                let text = format_plan_tree(&self.catalog, input, 0);
2787                Ok(QueryResult::Rows {
2788                    columns: vec!["plan".to_string()],
2789                    rows: text
2790                        .lines()
2791                        .map(|line| vec![Value::Str(line.to_string())])
2792                        .collect(),
2793                })
2794            }
2795
2796            PlanNode::Begin => {
2797                if self.in_transaction {
2798                    return Err(QueryError::Execution(
2799                        "already in a transaction (nested transactions not supported)".into(),
2800                    ));
2801                }
2802                self.catalog
2803                    .begin_transaction()
2804                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
2805                self.in_transaction = true;
2806                Ok(QueryResult::Executed {
2807                    message: "transaction started".to_string(),
2808                })
2809            }
2810
2811            PlanNode::Commit => {
2812                if !self.in_transaction {
2813                    return Err(QueryError::Execution(
2814                        "no active transaction to commit".into(),
2815                    ));
2816                }
2817                self.catalog
2818                    .commit_transaction()
2819                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
2820                self.in_transaction = false;
2821                Ok(QueryResult::Executed {
2822                    message: "transaction committed".to_string(),
2823                })
2824            }
2825
2826            PlanNode::Rollback => {
2827                if !self.in_transaction {
2828                    return Err(QueryError::Execution(
2829                        "no active transaction to roll back".into(),
2830                    ));
2831                }
2832                self.rollback_transaction_preserving_wal_archive()
2833            }
2834
2835            PlanNode::IndexScan { table, column, key } => {
2836                let key_value = literal_to_value(key)?;
2837                let tbl = self
2838                    .catalog
2839                    .get_table(table)
2840                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
2841                let columns: Vec<String> = tbl
2842                    .schema()
2843                    .columns
2844                    .iter()
2845                    .map(|c| c.name.clone())
2846                    .collect();
2847
2848                // Fast path: the table has a B-tree on this column.
2849                // Uses index_lookup_all to return ALL matching rows for
2850                // both unique and non-unique indexes.
2851                if tbl.has_index(column) {
2852                    let rids = tbl.index_lookup_all(column, &key_value);
2853                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(rids.len());
2854                    let mut cancel = CancelCheck::new();
2855                    for rid in rids {
2856                        cancel.tick()?;
2857                        // Overflow safety (P0-3/P0-4): `tbl.get` reassembles
2858                        // spilled columns; the old `heap.get` + `decode_row`
2859                        // returned Empty / wrapped a >= 64KB value.
2860                        if let Some(row) = tbl.get(rid) {
2861                            rows.push(row);
2862                        }
2863                    }
2864                    return Ok(QueryResult::Rows { columns, rows });
2865                }
2866
2867                // Fallback: no index on this column. The planner emits IndexScan
2868                // eagerly (it has no visibility into which columns are indexed
2869                // at plan time), so here we must behave like SeqScan+Filter on
2870                // `.col = literal`: return *all* matching rows, not just the
2871                // first one. A non-indexed column isn't necessarily unique.
2872                // We compile the eq predicate once and stream without any
2873                // per-row decode for non-matching rows.
2874                let schema = tbl.schema();
2875                let fast = FastLayout::new(schema);
2876                let synth_pred = Expr::BinaryOp(
2877                    Box::new(Expr::Field(column.clone())),
2878                    BinOp::Eq,
2879                    Box::new(key.clone()),
2880                );
2881                // Overflow safety (P0-4/P1): the raw compiled scan drops/mis-reads
2882                // spilled columns; a v2-capable table uses the decoded scan below.
2883                if !tbl.has_overflow_rows() {
2884                    if let Some(compiled) = compile_predicate(&synth_pred, &columns, &fast, schema)
2885                    {
2886                        // Mission F: skip the first 4 Vec doublings.
2887                        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
2888                        for_each_row_raw_cancellable(&self.catalog, table, |_rid, data| {
2889                            if compiled(data) {
2890                                rows.push(decode_row(schema, data));
2891                            }
2892                        })?;
2893                        return Ok(QueryResult::Rows { columns, rows });
2894                    }
2895                }
2896
2897                // Last resort: slow eq-check on materialised rows.
2898                let col_idx =
2899                    schema
2900                        .column_index(column)
2901                        .ok_or_else(|| QueryError::ColumnNotFound {
2902                            table: String::new(),
2903                            column: column.clone(),
2904                        })?;
2905                let mut cancel = CancelCheck::new();
2906                let mut rows: Vec<Vec<Value>> = Vec::new();
2907                for (_, row) in tbl.scan() {
2908                    cancel.tick()?;
2909                    if row[col_idx] == key_value {
2910                        rows.push(row);
2911                    }
2912                }
2913                Ok(QueryResult::Rows { columns, rows })
2914            }
2915
2916            PlanNode::RangeScan {
2917                table,
2918                column,
2919                start,
2920                end,
2921            } => {
2922                let tbl = self
2923                    .catalog
2924                    .get_table(table)
2925                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
2926                let columns: Vec<String> = tbl
2927                    .schema()
2928                    .columns
2929                    .iter()
2930                    .map(|c| c.name.clone())
2931                    .collect();
2932                let schema = tbl.schema();
2933
2934                let start_val = match start {
2935                    Some((expr, _)) => Some(literal_to_value(expr)?),
2936                    None => None,
2937                };
2938                let end_val = match end {
2939                    Some((expr, _)) => Some(literal_to_value(expr)?),
2940                    None => None,
2941                };
2942                let start_inclusive = start.as_ref().map(|(_, inc)| *inc).unwrap_or(true);
2943                let end_inclusive = end.as_ref().map(|(_, inc)| *inc).unwrap_or(true);
2944
2945                // Non-unique index: walk the composite (value, rid) leaf
2946                // chain between prefix bounds, fetch each row from the heap,
2947                // and recheck. The recheck enforces exclusive bounds
2948                // (range_rids is inclusive) and defensively skips any decoded
2949                // null (nulls are never indexed, so they must not match).
2950                if tbl.is_index_unique(column) == Some(false) {
2951                    if let Some(btree) = tbl.index(column) {
2952                        if start_val.is_some() || end_val.is_some() {
2953                            let col_idx = schema.column_index(column).ok_or_else(|| {
2954                                QueryError::ColumnNotFound {
2955                                    table: String::new(),
2956                                    column: column.clone(),
2957                                }
2958                            })?;
2959                            let rids = btree.range_rids(start_val.as_ref(), end_val.as_ref());
2960                            let mut rows: Vec<Vec<Value>> = Vec::with_capacity(rids.len());
2961                            let mut cancel = CancelCheck::new();
2962                            for rid in rids {
2963                                cancel.tick()?;
2964                                // Overflow safety (P0-3): reassemble spilled cols.
2965                                if let Some(row) = tbl.get(rid) {
2966                                    if !row[col_idx].is_empty()
2967                                        && range_matches(
2968                                            &row[col_idx],
2969                                            &start_val,
2970                                            start_inclusive,
2971                                            &end_val,
2972                                            end_inclusive,
2973                                        )
2974                                    {
2975                                        rows.push(row);
2976                                    }
2977                                }
2978                            }
2979                            return Ok(QueryResult::Rows { columns, rows });
2980                        }
2981                    }
2982                }
2983
2984                // Range scans use the btree fast path for unique indexes,
2985                // walking raw column-value keys directly.
2986                if tbl.is_index_unique(column) == Some(true) {
2987                    if let Some(btree) = tbl.index(column) {
2988                        let hits: Vec<(Value, RowId)> = match (&start_val, &end_val) {
2989                            (Some(s), Some(e)) => btree.range(s, e).collect(),
2990                            (Some(s), None) => btree.range_from(s),
2991                            (None, Some(e)) => btree.range_to(e),
2992                            (None, None) => {
2993                                let mut cancel = CancelCheck::new();
2994                                let mut rows: Vec<Vec<Value>> = Vec::new();
2995                                for (_, row) in tbl.scan() {
2996                                    cancel.tick()?;
2997                                    rows.push(row);
2998                                }
2999                                return Ok(QueryResult::Rows { columns, rows });
3000                            }
3001                        };
3002                        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(hits.len());
3003                        let mut cancel = CancelCheck::new();
3004                        for (key, rid) in hits {
3005                            cancel.tick()?;
3006                            if !start_inclusive {
3007                                if let Some(ref s) = start_val {
3008                                    if &key == s {
3009                                        continue;
3010                                    }
3011                                }
3012                            }
3013                            if !end_inclusive {
3014                                if let Some(ref e) = end_val {
3015                                    if &key == e {
3016                                        continue;
3017                                    }
3018                                }
3019                            }
3020                            // Overflow safety (P0-3): reassemble spilled cols.
3021                            if let Some(row) = tbl.get(rid) {
3022                                rows.push(row);
3023                            }
3024                        }
3025                        return Ok(QueryResult::Rows { columns, rows });
3026                    }
3027                }
3028
3029                // Fallback: no index — synthesize range predicate and scan.
3030                // Overflow safety (P0-4): v2-capable tables use the decoded
3031                // last-resort scan below.
3032                let fast = FastLayout::new(schema);
3033                let synth = synthesize_range_predicate(column, start, end);
3034                if !tbl.has_overflow_rows() {
3035                    if let Some(compiled) = compile_predicate(&synth, &columns, &fast, schema) {
3036                        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
3037                        for_each_row_raw_cancellable(&self.catalog, table, |_rid, data| {
3038                            if compiled(data) {
3039                                rows.push(decode_row(schema, data));
3040                            }
3041                        })?;
3042                        return Ok(QueryResult::Rows { columns, rows });
3043                    }
3044                }
3045
3046                let col_idx =
3047                    schema
3048                        .column_index(column)
3049                        .ok_or_else(|| QueryError::ColumnNotFound {
3050                            table: String::new(),
3051                            column: column.clone(),
3052                        })?;
3053                let mut cancel = CancelCheck::new();
3054                let mut rows: Vec<Vec<Value>> = Vec::new();
3055                for (_, row) in tbl.scan() {
3056                    cancel.tick()?;
3057                    if range_matches(
3058                        &row[col_idx],
3059                        &start_val,
3060                        start_inclusive,
3061                        &end_val,
3062                        end_inclusive,
3063                    ) {
3064                        rows.push(row);
3065                    }
3066                }
3067                Ok(QueryResult::Rows { columns, rows })
3068            }
3069        }
3070    }
3071
3072    // ─── Materialized view operations ──────────────────────────────────────
3073
3074    /// Create a materialized view: execute the source query, store results
3075    /// in a new backing table, and register the view.
3076    fn create_view(&mut self, name: &str, query_text: &str) -> Result<(), QueryError> {
3077        if self.view_registry.is_view(name) {
3078            return Err(QueryError::ViewError(format!(
3079                "materialized view '{name}' already exists"
3080            )));
3081        }
3082        // Execute the source query to get the result set.
3083        let result = self.execute_powql(query_text)?;
3084        let (columns, rows) = match result {
3085            QueryResult::Rows { columns, rows } => (columns, rows),
3086            _ => return Err("view source query must be a SELECT".into()),
3087        };
3088        // Derive a schema for the backing table from the query result columns.
3089        let schema = self.derive_view_schema(name, &columns, &rows);
3090        // Create the backing table and insert the result rows.
3091        crate::cancel::check()?;
3092        self.catalog
3093            .create_table(schema)
3094            .map_err(|e| QueryError::StorageError(e.to_string()))?;
3095        for row in &rows {
3096            self.catalog
3097                .insert(name, row)
3098                .map_err(|e| QueryError::StorageError(e.to_string()))?;
3099        }
3100        // Determine which base tables this view depends on by parsing the query.
3101        let depends_on = self.extract_view_deps(query_text);
3102        self.view_registry
3103            .register(ViewDef {
3104                name: name.to_string(),
3105                query: query_text.to_string(),
3106                depends_on,
3107                dirty: false,
3108            })
3109            .map_err(|e| QueryError::StorageError(e.to_string()))?;
3110        Ok(())
3111    }
3112
3113    /// Refresh a materialized view: re-execute its source query and replace
3114    /// the backing table's contents.
3115    fn refresh_view(&mut self, name: &str) -> Result<(), QueryError> {
3116        let def = self
3117            .view_registry
3118            .get(name)
3119            .ok_or_else(|| format!("materialized view '{name}' not found"))?;
3120        let query_text = def.query.clone();
3121        // Execute the source query.
3122        let result = self.execute_powql(&query_text)?;
3123        let (_columns, rows) = match result {
3124            QueryResult::Rows { columns, rows } => (columns, rows),
3125            _ => return Err("view source query must be a SELECT".into()),
3126        };
3127        // Clear old data and insert fresh results. Mission B2: logged
3128        // variant — view refreshes are a mutation and crash recovery
3129        // must see them.
3130        crate::cancel::check()?;
3131        self.catalog
3132            .scan_delete_matching_logged(name, |_| true)
3133            .map_err(|e| QueryError::StorageError(e.to_string()))?;
3134        for row in &rows {
3135            self.catalog
3136                .insert(name, row)
3137                .map_err(|e| QueryError::StorageError(e.to_string()))?;
3138        }
3139        self.view_registry.mark_clean(name);
3140        Ok(())
3141    }
3142
3143    /// Drop a materialized view: remove the backing table and unregister.
3144    fn drop_view(&mut self, name: &str) -> Result<(), QueryError> {
3145        if !self.view_registry.is_view(name) {
3146            return Err(QueryError::ViewError(format!(
3147                "materialized view '{name}' not found"
3148            )));
3149        }
3150        self.view_registry
3151            .unregister(name)
3152            .map_err(|e| QueryError::StorageError(e.to_string()))?;
3153        self.catalog
3154            .drop_table(name)
3155            .map_err(|e| QueryError::StorageError(e.to_string()))?;
3156        Ok(())
3157    }
3158
3159    /// Derive a storage `Schema` for a view's backing table from query
3160    /// result column names and the first row's types.
3161    fn derive_view_schema(&self, name: &str, columns: &[String], rows: &[Vec<Value>]) -> Schema {
3162        use powdb_storage::types::{ColumnDef, TypeId};
3163        let cols: Vec<ColumnDef> = columns
3164            .iter()
3165            .enumerate()
3166            .map(|(i, col_name)| {
3167                let type_id = rows
3168                    .first()
3169                    .and_then(|row| row.get(i))
3170                    .map(|v| v.type_id())
3171                    .unwrap_or(TypeId::Str);
3172                ColumnDef {
3173                    name: col_name.clone(),
3174                    type_id,
3175                    required: false,
3176                    position: i as u16,
3177                }
3178            })
3179            .collect();
3180        Schema {
3181            table_name: name.to_string(),
3182            columns: cols,
3183        }
3184    }
3185
3186    /// Extract base table dependencies from a view's source query by
3187    /// parsing it and collecting the source table name.
3188    fn extract_view_deps(&self, query_text: &str) -> Vec<String> {
3189        use crate::parser::parse;
3190        match parse(query_text) {
3191            Ok(Statement::Query(q)) => {
3192                let mut deps = vec![q.source.clone()];
3193                for j in &q.joins {
3194                    deps.push(j.source.clone());
3195                }
3196                deps
3197            }
3198            _ => Vec::new(),
3199        }
3200    }
3201
3202    // ─── Specialized fast paths ─────────────────────────────────────────────
3203    //
3204    // These methods are helpers for the `execute_plan` match arms above.
3205    // Each returns `Ok(Some(result))` when the fast path fires, `Ok(None)`
3206    // when the shape isn't supported (caller falls back to generic code).
3207
3208    /// Aggregate sum/avg/min/max over a single fixed-size i64 column, with
3209    /// an optional compiled filter predicate. Walks raw row bytes — zero
3210    /// per-row allocation. Uses i128 accumulator for sum/avg overflow safety.
3211    pub(super) fn agg_single_col_fast(
3212        &self,
3213        table: &str,
3214        col: &str,
3215        function: AggFunc,
3216        predicate: Option<&Expr>,
3217    ) -> Result<Option<QueryResult>, QueryError> {
3218        // Overflow safety (P0-4): this walks raw rehydrated bytes and would
3219        // silently drop any row carrying a value too large to re-inline
3220        // (>= 64KB), undercounting the aggregate. Fall back to the decoded path.
3221        if self.catalog.table_has_overflow(table) {
3222            return Ok(None);
3223        }
3224        let schema = self
3225            .catalog
3226            .schema(table)
3227            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
3228            .clone();
3229        let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
3230        let col_idx = match schema.column_index(col) {
3231            Some(i) => i,
3232            None => return Ok(None),
3233        };
3234        // Only fast-path fixed-size numeric columns (Int/Float) for
3235        // sum/avg/min/max/count. Mission D10: Float parity — prior version
3236        // bailed on Float columns, forcing them through the generic row-
3237        // decoding path that allocated a Vec<Value> per row and dispatched
3238        // on Value::cmp for every compare. f64 decode is structurally the
3239        // same as i64 (load 8 bytes, cast), so the fast path handles both.
3240        let col_type = schema.columns[col_idx].type_id;
3241        if col_type != TypeId::Int && col_type != TypeId::Float {
3242            return Ok(None);
3243        }
3244
3245        let fast = FastLayout::new(&schema);
3246        // Mission C Phase 20b: inline the numeric-column reader instead of
3247        // building a `Box<dyn Fn>`. Eliminates 100K vtable dispatches per
3248        // 100K-row agg scan — every reader call folds directly into the
3249        // hot loop below.
3250        let byte_offset = match fast.fixed_offsets[col_idx] {
3251            Some(o) => o,
3252            None => return Ok(None),
3253        };
3254        let bitmap_byte = col_idx / 8;
3255        let bitmap_bit = (col_idx % 8) as u32;
3256        let body_data_offset = 2 + fast.bitmap_size + byte_offset;
3257
3258        // Optional compiled filter.
3259        let compiled_pred: Option<CompiledPredicate> = match predicate {
3260            Some(pred) => match compile_predicate(pred, &columns, &fast, &schema) {
3261                Some(c) => Some(c),
3262                None => return Ok(None), // let generic path handle it
3263            },
3264            None => None,
3265        };
3266
3267        // Mission C Phase 20b: specialize the inner loop per aggregate
3268        // function. The previous version ran a `match function { ... }`
3269        // *inside* the closure, which kept LLVM from producing optimal
3270        // scalar code for each variant (agg_max regressed ~23% vs the
3271        // baseline Box<dyn Fn> version even though per-row vtable cost
3272        // should have been strictly lower). Pushing the match out of the
3273        // hot loop lets each specialized body fold cleanly into
3274        // `for_each_row_raw` and removes a captured `AggFunc` + match
3275        // dispatch per row.
3276        //
3277        // Mission D10: same specialisation applies to the Float branch.
3278        // For Min/Max we use `f64::total_cmp` so the result matches
3279        // `Value::Ord` — this is the same ordering ORDER BY and the
3280        // top-N sort fast path use, keeping semantics consistent across
3281        // read paths (NaN compares as greatest, -0.0 < +0.0 for
3282        // deterministic tie-breaking).
3283        //
3284        // Mission D11 Phase 1: each inner loop now splits on presence of
3285        // a predicate (`if let Some(pred) = &compiled_pred`) so the hot
3286        // body never re-tests `Option` per row, and reads column bytes
3287        // via `read_i64_unchecked` / `read_f64_unchecked` helpers that
3288        // drop two bounds checks per row (null bitmap byte + value
3289        // slice). Safety is carried by the `FastLayout` invariant that
3290        // `data_offset + 8 <= row_len` for any fixed-size column; see
3291        // the helper doc comments. Hot loops are macro-generated so the
3292        // with-pred / no-pred split can't drift between variants.
3293        let result = match col_type {
3294            TypeId::Int => match function {
3295                AggFunc::Sum | AggFunc::Avg => {
3296                    let mut sum_i128: i128 = 0;
3297                    let mut count: i64 = 0;
3298                    agg_int_loop!(
3299                        self,
3300                        table,
3301                        compiled_pred,
3302                        bitmap_byte,
3303                        bitmap_bit,
3304                        body_data_offset,
3305                        |v: i64| {
3306                            count += 1;
3307                            sum_i128 += v as i128;
3308                        }
3309                    );
3310                    if matches!(function, AggFunc::Sum) {
3311                        let clamped = sum_i128.clamp(i64::MIN as i128, i64::MAX as i128) as i64;
3312                        QueryResult::Scalar(Value::Int(clamped))
3313                    } else if count == 0 {
3314                        QueryResult::Scalar(Value::Empty)
3315                    } else {
3316                        let avg = (sum_i128 as f64) / (count as f64);
3317                        QueryResult::Scalar(Value::Float(avg))
3318                    }
3319                }
3320                AggFunc::Min => {
3321                    let mut min_v: Option<i64> = None;
3322                    agg_int_loop!(
3323                        self,
3324                        table,
3325                        compiled_pred,
3326                        bitmap_byte,
3327                        bitmap_bit,
3328                        body_data_offset,
3329                        |v: i64| {
3330                            min_v = Some(match min_v {
3331                                Some(m) => m.min(v),
3332                                None => v,
3333                            });
3334                        }
3335                    );
3336                    QueryResult::Scalar(min_v.map(Value::Int).unwrap_or(Value::Empty))
3337                }
3338                AggFunc::Max => {
3339                    let mut max_v: Option<i64> = None;
3340                    agg_int_loop!(
3341                        self,
3342                        table,
3343                        compiled_pred,
3344                        bitmap_byte,
3345                        bitmap_bit,
3346                        body_data_offset,
3347                        |v: i64| {
3348                            max_v = Some(match max_v {
3349                                Some(m) => m.max(v),
3350                                None => v,
3351                            });
3352                        }
3353                    );
3354                    QueryResult::Scalar(max_v.map(Value::Int).unwrap_or(Value::Empty))
3355                }
3356                AggFunc::Count => {
3357                    let mut count: i64 = 0;
3358                    agg_int_loop!(
3359                        self,
3360                        table,
3361                        compiled_pred,
3362                        bitmap_byte,
3363                        bitmap_bit,
3364                        body_data_offset,
3365                        |_v: i64| {
3366                            count += 1;
3367                        }
3368                    );
3369                    QueryResult::Scalar(Value::Int(count))
3370                }
3371                AggFunc::CountDistinct => {
3372                    let mut seen = rustc_hash::FxHashSet::default();
3373                    agg_int_loop!(
3374                        self,
3375                        table,
3376                        compiled_pred,
3377                        bitmap_byte,
3378                        bitmap_bit,
3379                        body_data_offset,
3380                        |v: i64| {
3381                            seen.insert(v);
3382                        }
3383                    );
3384                    QueryResult::Scalar(Value::Int(seen.len() as i64))
3385                }
3386            },
3387            TypeId::Float => match function {
3388                AggFunc::Sum => {
3389                    // Use a single f64 accumulator. Naive summation is
3390                    // sufficient for MVP parity; if precision becomes an
3391                    // issue on long scans we can upgrade to Kahan–Neumaier
3392                    // compensated sum (~2x scalar cost, zero error growth).
3393                    let mut sum: f64 = 0.0;
3394                    agg_float_loop!(
3395                        self,
3396                        table,
3397                        compiled_pred,
3398                        bitmap_byte,
3399                        bitmap_bit,
3400                        body_data_offset,
3401                        |v: f64| {
3402                            sum += v;
3403                        }
3404                    );
3405                    QueryResult::Scalar(Value::Float(sum))
3406                }
3407                AggFunc::Avg => {
3408                    let mut sum: f64 = 0.0;
3409                    let mut count: i64 = 0;
3410                    agg_float_loop!(
3411                        self,
3412                        table,
3413                        compiled_pred,
3414                        bitmap_byte,
3415                        bitmap_bit,
3416                        body_data_offset,
3417                        |v: f64| {
3418                            sum += v;
3419                            count += 1;
3420                        }
3421                    );
3422                    if count == 0 {
3423                        QueryResult::Scalar(Value::Empty)
3424                    } else {
3425                        QueryResult::Scalar(Value::Float(sum / count as f64))
3426                    }
3427                }
3428                AggFunc::Min => {
3429                    // `total_cmp` for deterministic NaN handling (matches
3430                    // Value::Ord). NaN compares greatest, so Min will
3431                    // correctly ignore it in favour of any finite value.
3432                    let mut min_v: Option<f64> = None;
3433                    agg_float_loop!(
3434                        self,
3435                        table,
3436                        compiled_pred,
3437                        bitmap_byte,
3438                        bitmap_bit,
3439                        body_data_offset,
3440                        |v: f64| {
3441                            min_v = Some(match min_v {
3442                                Some(m) => {
3443                                    if v.total_cmp(&m).is_lt() {
3444                                        v
3445                                    } else {
3446                                        m
3447                                    }
3448                                }
3449                                None => v,
3450                            });
3451                        }
3452                    );
3453                    QueryResult::Scalar(min_v.map(Value::Float).unwrap_or(Value::Empty))
3454                }
3455                AggFunc::Max => {
3456                    let mut max_v: Option<f64> = None;
3457                    agg_float_loop!(
3458                        self,
3459                        table,
3460                        compiled_pred,
3461                        bitmap_byte,
3462                        bitmap_bit,
3463                        body_data_offset,
3464                        |v: f64| {
3465                            max_v = Some(match max_v {
3466                                Some(m) => {
3467                                    if v.total_cmp(&m).is_gt() {
3468                                        v
3469                                    } else {
3470                                        m
3471                                    }
3472                                }
3473                                None => v,
3474                            });
3475                        }
3476                    );
3477                    QueryResult::Scalar(max_v.map(Value::Float).unwrap_or(Value::Empty))
3478                }
3479                AggFunc::Count => {
3480                    let mut count: i64 = 0;
3481                    agg_float_loop!(
3482                        self,
3483                        table,
3484                        compiled_pred,
3485                        bitmap_byte,
3486                        bitmap_bit,
3487                        body_data_offset,
3488                        |_v: f64| {
3489                            count += 1;
3490                        }
3491                    );
3492                    QueryResult::Scalar(Value::Int(count))
3493                }
3494                AggFunc::CountDistinct => {
3495                    // Hash on `f64::to_bits` — matches `Value::Hash`, so
3496                    // distinct NaN bit patterns count as distinct and
3497                    // -0.0/+0.0 count as distinct. Consistent with how
3498                    // Float values are hashed in every other DISTINCT /
3499                    // GROUP BY path.
3500                    let mut seen = rustc_hash::FxHashSet::default();
3501                    agg_float_loop!(
3502                        self,
3503                        table,
3504                        compiled_pred,
3505                        bitmap_byte,
3506                        bitmap_bit,
3507                        body_data_offset,
3508                        |v: f64| {
3509                            seen.insert(v.to_bits());
3510                        }
3511                    );
3512                    QueryResult::Scalar(Value::Int(seen.len() as i64))
3513                }
3514            },
3515            _ => unreachable!("type guard above restricts to Int/Float"),
3516        };
3517        Ok(Some(result))
3518    }
3519
3520    /// `Project(Limit(Filter(SeqScan)))` and `Project(Limit(SeqScan))`.
3521    /// Streams rows, decodes only projected columns, stops at the limit.
3522    pub(super) fn project_filter_limit_fast(
3523        &self,
3524        table: &str,
3525        fields: &[ProjectField],
3526        limit: usize,
3527        predicate: Option<&Expr>,
3528    ) -> Result<Option<QueryResult>, QueryError> {
3529        // Overflow safety (P0-4): raw-byte projection over rehydrated rows
3530        // drops any row with a value too large to re-inline (>= 64KB) and
3531        // cannot return such a value; fall back to the decoded generic path.
3532        if self.catalog.table_has_overflow(table) {
3533            return Ok(None);
3534        }
3535        let schema = self
3536            .catalog
3537            .schema(table)
3538            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
3539            .clone();
3540        let all_columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
3541
3542        // Each projection field must be a simple `.field` reference for this
3543        // fast path. Aliased or computed fields fall through.
3544        let mut proj_indices: Vec<usize> = Vec::with_capacity(fields.len());
3545        let mut proj_columns: Vec<String> = Vec::with_capacity(fields.len());
3546        for f in fields {
3547            let name = match &f.expr {
3548                Expr::Field(n) => n.clone(),
3549                _ => return Ok(None),
3550            };
3551            let idx = match all_columns.iter().position(|c| c == &name) {
3552                Some(i) => i,
3553                None => return Ok(None),
3554            };
3555            proj_indices.push(idx);
3556            proj_columns.push(f.alias.clone().unwrap_or(name));
3557        }
3558
3559        let fast = FastLayout::new(&schema);
3560        let row_layout = RowLayout::new(&schema);
3561
3562        let compiled_pred: Option<CompiledPredicate> = match predicate {
3563            Some(pred) => match compile_predicate(pred, &all_columns, &fast, &schema) {
3564                Some(c) => Some(c),
3565                None => return Ok(None),
3566            },
3567            None => None,
3568        };
3569
3570        let mut out: Vec<Vec<Value>> = Vec::with_capacity(limit.min(1024));
3571        // Mission D2: use try_for_each_row_raw to actually stop iterating
3572        // once the limit is reached. The previous `done` flag only short-
3573        // circuited the closure body, so a `limit 100` over 100K rows still
3574        // walked all 100K slots — burning ~30x SQLite on scan_filter_project_top100.
3575        // Cooperative cancellation: an unbounded (limit == usize::MAX) projected
3576        // scan over a huge table must stay stoppable.
3577        let mut cancel = CancelCheck::new();
3578        let mut cancel_err: Option<QueryError> = None;
3579        self.catalog
3580            .try_for_each_row_raw(table, |_rid, data| {
3581                if let Err(e) = cancel.tick() {
3582                    cancel_err = Some(e);
3583                    return ControlFlow::Break(());
3584                }
3585                if let Some(ref pred) = compiled_pred {
3586                    if !pred(data) {
3587                        return ControlFlow::Continue(());
3588                    }
3589                }
3590                let row: Vec<Value> = proj_indices
3591                    .iter()
3592                    .map(|&ci| decode_column(&schema, &row_layout, data, ci))
3593                    .collect();
3594                out.push(row);
3595                if out.len() >= limit {
3596                    ControlFlow::Break(())
3597                } else {
3598                    ControlFlow::Continue(())
3599                }
3600            })
3601            .map_err(|e| QueryError::StorageError(e.to_string()))?;
3602        if let Some(e) = cancel_err {
3603            return Err(e);
3604        }
3605
3606        Ok(Some(QueryResult::Rows {
3607            columns: proj_columns,
3608            rows: out,
3609        }))
3610    }
3611
3612    /// `Project(Limit(Sort(Filter(SeqScan))))` and `Project(Limit(Sort(SeqScan)))`.
3613    /// Bounded top-N heap over the sort key. Only the sort key needs to be
3614    /// read per row; projected columns are decoded only for the final
3615    /// winning rows when the heap drains.
3616    pub(super) fn project_filter_sort_limit_fast(
3617        &self,
3618        table: &str,
3619        fields: &[ProjectField],
3620        sort_field: &str,
3621        descending: bool,
3622        limit: usize,
3623        predicate: Option<&Expr>,
3624    ) -> Result<Option<QueryResult>, QueryError> {
3625        // Overflow safety (P0-4): raw-byte scan drops/wraps >= 64KB values;
3626        // let the decoded generic path handle v2-capable tables.
3627        if self.catalog.table_has_overflow(table) {
3628            return Ok(None);
3629        }
3630        if limit == 0 {
3631            // Degenerate case — empty result. Let the generic path handle it
3632            // for proper column naming.
3633            return Ok(None);
3634        }
3635        // The top-N heaps never hold more than `limit` rows, but `limit` is an
3636        // attacker-supplied literal (`order .x limit 99999999999`). Reserving
3637        // that capacity up front would allocate gigabytes and abort the
3638        // process before a single row is read. Cap the pre-allocation; the
3639        // heaps still grow on demand up to the true `limit`.
3640        const TOPN_PREALLOC_CAP: usize = 4096;
3641        let prealloc = limit.min(TOPN_PREALLOC_CAP);
3642        let schema = self
3643            .catalog
3644            .schema(table)
3645            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
3646            .clone();
3647        let all_columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
3648
3649        // Sort key must be a fixed-size numeric column (Int or Float).
3650        // Mission D10: extended from Int-only. Float sort keys use a
3651        // sortable-u64 transform (see `f64_to_sortable_u64`) so the heap
3652        // path stays keyed on `u64` and the whole branch shape is
3653        // identical to the Int case — no new heap types, no `total_cmp`
3654        // closures in the hot loop.
3655        let sort_idx = match schema.column_index(sort_field) {
3656            Some(i) => i,
3657            None => return Ok(None),
3658        };
3659        let sort_col_type = schema.columns[sort_idx].type_id;
3660        if sort_col_type != TypeId::Int && sort_col_type != TypeId::Float {
3661            return Ok(None);
3662        }
3663
3664        // Each projection field must be a simple `.field`.
3665        let mut proj_indices: Vec<usize> = Vec::with_capacity(fields.len());
3666        let mut proj_columns: Vec<String> = Vec::with_capacity(fields.len());
3667        for f in fields {
3668            let name = match &f.expr {
3669                Expr::Field(n) => n.clone(),
3670                _ => return Ok(None),
3671            };
3672            let idx = match all_columns.iter().position(|c| c == &name) {
3673                Some(i) => i,
3674                None => return Ok(None),
3675            };
3676            proj_indices.push(idx);
3677            proj_columns.push(f.alias.clone().unwrap_or(name));
3678        }
3679
3680        let fast = FastLayout::new(&schema);
3681        let row_layout = RowLayout::new(&schema);
3682        // Mission C Phase 20b: inline numeric-column reader (no Box<dyn Fn>).
3683        let sort_byte_offset = match fast.fixed_offsets[sort_idx] {
3684            Some(o) => o,
3685            None => return Ok(None),
3686        };
3687        let sort_bitmap_byte = sort_idx / 8;
3688        let sort_bitmap_bit = (sort_idx % 8) as u32;
3689        let sort_body_data_offset = 2 + fast.bitmap_size + sort_byte_offset;
3690
3691        let compiled_pred: Option<CompiledPredicate> = match predicate {
3692            Some(pred) => match compile_predicate(pred, &all_columns, &fast, &schema) {
3693                Some(c) => Some(c),
3694                None => return Ok(None),
3695            },
3696            None => None,
3697        };
3698
3699        // Bounded top-N heap. For `order .x desc limit N`, we want the N
3700        // largest values — use a min-heap so the smallest is at the top and
3701        // can be popped when a better candidate arrives. For ascending, use
3702        // a max-heap. We tie-break with a monotonic `seq` counter so the
3703        // result is deterministic and stable.
3704        //
3705        // To keep this simple we maintain two typed heaps and pick by
3706        // direction.
3707        let drained: Vec<Vec<u8>> = match sort_col_type {
3708            TypeId::Int => {
3709                let mut seq: u64 = 0;
3710                let mut heap_desc: BinaryHeap<Reverse<(i64, u64, Vec<u8>)>> =
3711                    BinaryHeap::with_capacity(prealloc);
3712                let mut heap_asc: BinaryHeap<(i64, u64, Vec<u8>)> =
3713                    BinaryHeap::with_capacity(prealloc);
3714                let mut null_rows: Vec<Vec<u8>> = Vec::with_capacity(prealloc);
3715
3716                for_each_row_raw_cancellable(&self.catalog, table, |_rid, data| {
3717                    if let Some(ref pred) = compiled_pred {
3718                        if !pred(data) {
3719                            return;
3720                        }
3721                    }
3722                    // Inlined int-column reader: null check + i64 decode.
3723                    let base = row_body_base(data);
3724                    let sort_data_offset = base + sort_body_data_offset;
3725                    if data.len() < sort_data_offset + 8
3726                        || data.len() <= base + 2 + sort_bitmap_byte
3727                    {
3728                        return;
3729                    }
3730                    let is_null = (data[base + 2 + sort_bitmap_byte] >> sort_bitmap_bit) & 1 == 1;
3731                    let id = seq;
3732                    seq += 1;
3733                    if is_null {
3734                        if null_rows.len() < limit {
3735                            null_rows.push(data.to_vec());
3736                        }
3737                        return;
3738                    }
3739                    let key = i64::from_le_bytes(
3740                        data[sort_data_offset..sort_data_offset + 8]
3741                            .try_into()
3742                            .unwrap_or_else(|_| unreachable!()),
3743                    );
3744                    if descending {
3745                        if heap_desc.len() < limit {
3746                            heap_desc.push(Reverse((key, id, data.to_vec())));
3747                        } else if let Some(Reverse((top_key, _, _))) = heap_desc.peek() {
3748                            if key > *top_key {
3749                                heap_desc.pop();
3750                                heap_desc.push(Reverse((key, id, data.to_vec())));
3751                            }
3752                        }
3753                    } else if heap_asc.len() < limit {
3754                        heap_asc.push((key, id, data.to_vec()));
3755                    } else if let Some((top_key, _, _)) = heap_asc.peek() {
3756                        if key < *top_key {
3757                            heap_asc.pop();
3758                            heap_asc.push((key, id, data.to_vec()));
3759                        }
3760                    }
3761                })?;
3762
3763                let mut drained: Vec<(i64, u64, Vec<u8>)> = if descending {
3764                    heap_desc.into_iter().map(|Reverse(t)| t).collect()
3765                } else {
3766                    heap_asc.into_iter().collect()
3767                };
3768                if descending {
3769                    cooperative_stable_sort_by(&mut drained, self.query_memory_limit, |a, b| {
3770                        b.0.cmp(&a.0).then(a.1.cmp(&b.1))
3771                    })?;
3772                } else {
3773                    cooperative_stable_sort_by(&mut drained, self.query_memory_limit, |a, b| {
3774                        a.0.cmp(&b.0).then(a.1.cmp(&b.1))
3775                    })?;
3776                }
3777                let mut rows: Vec<Vec<u8>> = drained.into_iter().map(|(_, _, d)| d).collect();
3778                rows.extend(null_rows.into_iter().take(limit.saturating_sub(rows.len())));
3779                rows
3780            }
3781            TypeId::Float => {
3782                // Novel angle: rather than introducing a `TotalF64` newtype
3783                // with `Ord via total_cmp`, transform the f64 bit pattern
3784                // into a sortable `u64` so `BinaryHeap<u64>` orders exactly
3785                // like `f64::total_cmp` would. Classic trick: flip the sign
3786                // bit on positives, flip all bits on negatives. Result:
3787                // - NaN (sign=0) stays greatest, matching total_cmp
3788                // - -0.0 sorts before +0.0, matching total_cmp
3789                // - Hot loop is branch-cheap (one compare + one xor)
3790                let mut seq: u64 = 0;
3791                let mut heap_desc: BinaryHeap<Reverse<(u64, u64, Vec<u8>)>> =
3792                    BinaryHeap::with_capacity(prealloc);
3793                let mut heap_asc: BinaryHeap<(u64, u64, Vec<u8>)> =
3794                    BinaryHeap::with_capacity(prealloc);
3795                let mut null_rows: Vec<Vec<u8>> = Vec::with_capacity(prealloc);
3796
3797                for_each_row_raw_cancellable(&self.catalog, table, |_rid, data| {
3798                    if let Some(ref pred) = compiled_pred {
3799                        if !pred(data) {
3800                            return;
3801                        }
3802                    }
3803                    let base = row_body_base(data);
3804                    let sort_data_offset = base + sort_body_data_offset;
3805                    if data.len() < sort_data_offset + 8
3806                        || data.len() <= base + 2 + sort_bitmap_byte
3807                    {
3808                        return;
3809                    }
3810                    let is_null = (data[base + 2 + sort_bitmap_byte] >> sort_bitmap_bit) & 1 == 1;
3811                    let id = seq;
3812                    seq += 1;
3813                    if is_null {
3814                        if null_rows.len() < limit {
3815                            null_rows.push(data.to_vec());
3816                        }
3817                        return;
3818                    }
3819                    let bits = u64::from_le_bytes(
3820                        data[sort_data_offset..sort_data_offset + 8]
3821                            .try_into()
3822                            .unwrap_or_else(|_| unreachable!()),
3823                    );
3824                    let key = f64_bits_to_sortable_u64(bits);
3825                    if descending {
3826                        if heap_desc.len() < limit {
3827                            heap_desc.push(Reverse((key, id, data.to_vec())));
3828                        } else if let Some(Reverse((top_key, _, _))) = heap_desc.peek() {
3829                            if key > *top_key {
3830                                heap_desc.pop();
3831                                heap_desc.push(Reverse((key, id, data.to_vec())));
3832                            }
3833                        }
3834                    } else if heap_asc.len() < limit {
3835                        heap_asc.push((key, id, data.to_vec()));
3836                    } else if let Some((top_key, _, _)) = heap_asc.peek() {
3837                        if key < *top_key {
3838                            heap_asc.pop();
3839                            heap_asc.push((key, id, data.to_vec()));
3840                        }
3841                    }
3842                })?;
3843
3844                let mut drained: Vec<(u64, u64, Vec<u8>)> = if descending {
3845                    heap_desc.into_iter().map(|Reverse(t)| t).collect()
3846                } else {
3847                    heap_asc.into_iter().collect()
3848                };
3849                if descending {
3850                    cooperative_stable_sort_by(&mut drained, self.query_memory_limit, |a, b| {
3851                        b.0.cmp(&a.0).then(a.1.cmp(&b.1))
3852                    })?;
3853                } else {
3854                    cooperative_stable_sort_by(&mut drained, self.query_memory_limit, |a, b| {
3855                        a.0.cmp(&b.0).then(a.1.cmp(&b.1))
3856                    })?;
3857                }
3858                let mut rows: Vec<Vec<u8>> = drained.into_iter().map(|(_, _, d)| d).collect();
3859                rows.extend(null_rows.into_iter().take(limit.saturating_sub(rows.len())));
3860                rows
3861            }
3862            _ => unreachable!("type guard above restricts to Int/Float"),
3863        };
3864
3865        let mut cancel = CancelCheck::new();
3866        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(drained.len());
3867        for data in drained {
3868            cancel.tick()?;
3869            rows.push(
3870                proj_indices
3871                    .iter()
3872                    .map(|&ci| decode_column(&schema, &row_layout, &data, ci))
3873                    .collect(),
3874            );
3875        }
3876
3877        Ok(Some(QueryResult::Rows {
3878            columns: proj_columns,
3879            rows,
3880        }))
3881    }
3882
3883    /// Gather the RowIds that a mutation should operate on, without
3884    /// materialising the full row set. Handles the shapes the planner emits
3885    /// for update/delete: SeqScan, IndexScan, and Filter(SeqScan). Other
3886    /// shapes fall back to `generic_rid_match`.
3887    ///
3888    /// Perf sprint: try to fuse the predicate evaluation and in-place
3889    /// byte-level mutation into a single heap walk. Returns `Some(result)`
3890    /// if the fused path fired, `None` to fall through to the generic
3891    /// two-pass code.
3892    ///
3893    /// Covers two shapes:
3894    /// 1. Fixed-width non-null literal assignments on non-indexed columns
3895    ///    → byte-patch every matched row in place (row length unchanged).
3896    /// 2. Single var-col literal assignment on a non-indexed column
3897    ///    → `patch_var_column_in_place` on every matched row (may shrink);
3898    ///    rows that can't be patched in place are collected for fallback.
3899    fn try_fused_scan_update(
3900        &mut self,
3901        table: &str,
3902        predicate: &Expr,
3903        resolved: &[(usize, Value)],
3904        changed_cols: &[usize],
3905    ) -> Option<Result<QueryResult, QueryError>> {
3906        // Overflow safety (P0/P1): a table that may hold v2 rows can never take
3907        // the byte-patch fast paths — patching computes v1 offsets and would
3908        // corrupt a spilled row, and the compiled predicate over raw bytes
3909        // mis-evaluates a spilled column. Fall through to the reassembling
3910        // collect-rids + get/update_hinted path.
3911        if self.catalog.table_has_overflow(table) {
3912            return None;
3913        }
3914        // Build compiled predicate. Requires a schema borrow that must be
3915        // dropped before we call scan_patch_matching_logged.
3916        let compiled = {
3917            let schema = self.catalog.schema(table)?;
3918            let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
3919            let fast = FastLayout::new(schema);
3920            compile_predicate(predicate, &columns, &fast, schema)?
3921        };
3922
3923        // ── Path 1: fixed-width fast patch ──────────────────────────
3924        let fixed_patches: Option<Vec<FastPatch>> = {
3925            let tbl = self.catalog.get_table(table)?;
3926            let schema = tbl.schema();
3927            let all_fixed_nonnull = resolved
3928                .iter()
3929                .all(|(idx, val)| is_fixed_size(schema.columns[*idx].type_id) && !val.is_empty());
3930            let no_indexed = !resolved.iter().any(|(idx, _)| tbl.has_indexed_col(*idx));
3931            if all_fixed_nonnull && no_indexed {
3932                let layout = RowLayout::new(schema);
3933                let bitmap_size = layout.bitmap_size();
3934                Some(
3935                    resolved
3936                        .iter()
3937                        .map(|(idx, val)| {
3938                            let fixed_off = layout
3939                                .fixed_offset(*idx)
3940                                .expect("is_fixed_size already checked");
3941                            let field_off = 2 + bitmap_size + fixed_off;
3942                            let bytes: FixedBytes = match val {
3943                                Value::Int(v) => FixedBytes::I64(v.to_le_bytes()),
3944                                Value::Float(v) => FixedBytes::F64(v.to_le_bytes()),
3945                                Value::Bool(v) => FixedBytes::Bool(if *v { 1 } else { 0 }),
3946                                Value::DateTime(v) => FixedBytes::I64(v.to_le_bytes()),
3947                                Value::Uuid(v) => FixedBytes::Uuid(*v),
3948                                _ => unreachable!("all_fixed_nonnull guard"),
3949                            };
3950                            FastPatch {
3951                                field_off,
3952                                bitmap_byte_off: 2 + idx / 8,
3953                                bit_mask: 1u8 << (idx % 8),
3954                                bytes,
3955                            }
3956                        })
3957                        .collect(),
3958                )
3959            } else {
3960                None
3961            }
3962        };
3963        if let Some(patches) = fixed_patches {
3964            let result = self
3965                .catalog
3966                .scan_patch_matching_logged(table, compiled, |row| {
3967                    let base = row_body_base(row);
3968                    for p in &patches {
3969                        row[base + p.bitmap_byte_off] &= !p.bit_mask;
3970                        let field_bytes = p.bytes.as_slice();
3971                        row[base + p.field_off..base + p.field_off + field_bytes.len()]
3972                            .copy_from_slice(field_bytes);
3973                    }
3974                    Some(row.len() as u16)
3975                })
3976                .map_err(|e| e.to_string());
3977            match result {
3978                Ok((count, _)) => {
3979                    self.view_registry.mark_dependents_dirty(table);
3980                    return Some(Ok(QueryResult::Modified(count)));
3981                }
3982                Err(e) => return Some(Err(QueryError::Execution(e))),
3983            }
3984        }
3985
3986        // ── Path 2: single var-col shrink fast patch ────────────────
3987        let var_patch: Option<(usize, Option<Vec<u8>>)> = {
3988            let tbl = self.catalog.get_table(table)?;
3989            let schema = tbl.schema();
3990            let is_single = resolved.len() == 1;
3991            let is_var = is_single && !is_fixed_size(schema.columns[resolved[0].0].type_id);
3992            let no_indexed = !resolved.iter().any(|(idx, _)| tbl.has_indexed_col(*idx));
3993            if is_single && is_var && no_indexed {
3994                let (idx, val) = &resolved[0];
3995                let bytes_opt = match val {
3996                    Value::Str(s) => Some(s.as_bytes().to_vec()),
3997                    Value::Bytes(b) => Some(b.clone()),
3998                    Value::Empty => None,
3999                    _ => return None, // type mismatch, fall through
4000                };
4001                Some((*idx, bytes_opt))
4002            } else {
4003                None
4004            }
4005        };
4006        if let Some((col_idx, ref new_bytes_opt)) = var_patch {
4007            // Build a fresh RowLayout before the mutable borrow.
4008            let layout = {
4009                let schema = self.catalog.schema(table)?;
4010                RowLayout::new(schema)
4011            };
4012            let new_bytes_ref: Option<&[u8]> = new_bytes_opt.as_deref();
4013            let result = self
4014                .catalog
4015                .scan_patch_matching_logged(table, compiled, |row| {
4016                    patch_var_column_in_place(row, &layout, col_idx, new_bytes_ref)
4017                })
4018                .map_err(|e| e.to_string());
4019            match result {
4020                Ok((mut count, fallback_rids)) => {
4021                    // Handle rows where in-place patch failed (new > old).
4022                    for rid in fallback_rids {
4023                        let mut row = match self.catalog.get(table, rid) {
4024                            Some(r) => r,
4025                            None => continue,
4026                        };
4027                        for (idx, val) in resolved.iter() {
4028                            row[*idx] = val.clone();
4029                        }
4030                        if let Err(e) =
4031                            self.catalog
4032                                .update_hinted(table, rid, &row, Some(changed_cols))
4033                        {
4034                            return Some(Err(QueryError::StorageError(e.to_string())));
4035                        }
4036                        count += 1;
4037                    }
4038                    self.view_registry.mark_dependents_dirty(table);
4039                    return Some(Ok(QueryResult::Modified(count)));
4040                }
4041                Err(e) => return Some(Err(QueryError::Execution(e))),
4042            }
4043        }
4044
4045        None // no fused path applicable — fall through
4046    }
4047
4048    /// Mission C Phase 3: schema is looked up via `self.catalog.schema(table)`
4049    /// inside the branches that actually need it. Previously the caller had
4050    /// to clone the full Schema (6+ String allocs) before every mutation just
4051    /// so this function could borrow it — a cost the update/delete hot path
4052    /// did not need.
4053    fn collect_rids_for_mutation(
4054        &mut self,
4055        input: &PlanNode,
4056        table: &str,
4057    ) -> Result<Vec<RowId>, QueryError> {
4058        // Overflow safety (P1/P0-4): the raw-byte fast paths below stream
4059        // through `for_each_row_raw`, which rehydrates v2 rows to v1 and SKIPS
4060        // any row carrying a value too large to re-inline (>= 64KB). For a
4061        // v2-capable table, evaluate the predicate over fully decoded rows
4062        // instead so no matching row is missed or mis-judged on a spilled
4063        // column. Exact index lookups (value-size independent) still fall
4064        // through to the normal path.
4065        if self.catalog.table_has_overflow(table) {
4066            if let Some(rids) = self.collect_rids_decoded(input, table)? {
4067                return Ok(rids);
4068            }
4069        }
4070        match input {
4071            PlanNode::SeqScan { table: t } if t == table => {
4072                // "Update/delete everything" — rare but legal.
4073                let mut cancel = CancelCheck::new();
4074                let mut rids: Vec<RowId> = Vec::new();
4075                for (rid, _) in self
4076                    .catalog
4077                    .scan(table)
4078                    .map_err(|e| QueryError::StorageError(e.to_string()))?
4079                {
4080                    cancel.tick()?;
4081                    rids.push(rid);
4082                }
4083                Ok(rids)
4084            }
4085            PlanNode::IndexScan {
4086                table: t,
4087                column,
4088                key,
4089            } if t == table => {
4090                let key_value = literal_to_value(key)?;
4091
4092                // Indexed case: single lookup, 0 or 1 rows.
4093                // Mission D7: int-specialized fast path on int-keyed indexes
4094                // (primary keys, created_at, etc.) — the common case for
4095                // `update_by_pk` / `delete where id = ?`.
4096                //
4097                // Scope the `tbl` borrow so it's released before we fall
4098                // through to the scan-based paths below (which reborrow
4099                // `self.catalog`).
4100                {
4101                    let tbl = self
4102                        .catalog
4103                        .get_table(table)
4104                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
4105                    if tbl.has_index(column) {
4106                        let rids = tbl.index_lookup_all(column, &key_value);
4107                        return Ok(rids);
4108                    }
4109                }
4110
4111                // No index: the planner folds `.col = literal` to IndexScan
4112                // regardless of whether the column is actually unique. When
4113                // there's no index we must behave like Filter(SeqScan) and
4114                // return *all* matching RIDs — not just the first one.
4115                let schema = self
4116                    .catalog
4117                    .schema(table)
4118                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
4119                let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
4120                let fast = FastLayout::new(schema);
4121                let synth = Expr::BinaryOp(
4122                    Box::new(Expr::Field(column.clone())),
4123                    BinOp::Eq,
4124                    Box::new(key.clone()),
4125                );
4126                if let Some(compiled) = compile_predicate(&synth, &columns, &fast, schema) {
4127                    // Mission F: skip the first 4 Vec doublings.
4128                    let mut rids: Vec<RowId> = Vec::with_capacity(64);
4129                    let mut cancel = CancelCheck::new();
4130                    let mut cancel_err: Option<QueryError> = None;
4131                    self.catalog
4132                        .try_for_each_row_raw(table, |rid, data| {
4133                            if let Err(e) = cancel.tick() {
4134                                cancel_err = Some(e);
4135                                return ControlFlow::Break(());
4136                            }
4137                            if compiled(data) {
4138                                rids.push(rid);
4139                            }
4140                            ControlFlow::Continue(())
4141                        })
4142                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
4143                    if let Some(e) = cancel_err {
4144                        return Err(e);
4145                    }
4146                    return Ok(rids);
4147                }
4148
4149                // Fallback: decode each row, compare values.
4150                let col_idx =
4151                    schema
4152                        .column_index(column)
4153                        .ok_or_else(|| QueryError::ColumnNotFound {
4154                            table: String::new(),
4155                            column: column.clone(),
4156                        })?;
4157                let mut cancel = CancelCheck::new();
4158                let mut rids: Vec<RowId> = Vec::new();
4159                for (rid, row) in self
4160                    .catalog
4161                    .scan(table)
4162                    .map_err(|e| QueryError::StorageError(e.to_string()))?
4163                {
4164                    cancel.tick()?;
4165                    if row[col_idx] == key_value {
4166                        rids.push(rid);
4167                    }
4168                }
4169                Ok(rids)
4170            }
4171            PlanNode::Filter {
4172                input: inner,
4173                predicate,
4174            } => {
4175                if let PlanNode::SeqScan { table: t } = inner.as_ref() {
4176                    if t != table {
4177                        return self.generic_rid_match(input, table);
4178                    }
4179                    let schema = self
4180                        .catalog
4181                        .schema(table)
4182                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
4183                    let columns: Vec<String> =
4184                        schema.columns.iter().map(|c| c.name.clone()).collect();
4185                    let fast = FastLayout::new(schema);
4186                    let row_layout = RowLayout::new(schema);
4187
4188                    // Cooperative cancellation: the rid-collection scan that
4189                    // backs `update/delete filter <unindexed pred>` walks the
4190                    // whole table, so it must stay stoppable.
4191                    let mut cancel = CancelCheck::new();
4192                    let mut cancel_err: Option<QueryError> = None;
4193                    // Try compiled predicate first.
4194                    if let Some(compiled) = compile_predicate(predicate, &columns, &fast, schema) {
4195                        // Mission F: skip the first 4 Vec doublings.
4196                        let mut rids: Vec<RowId> = Vec::with_capacity(64);
4197                        self.catalog
4198                            .try_for_each_row_raw(table, |rid, data| {
4199                                if let Err(e) = cancel.tick() {
4200                                    cancel_err = Some(e);
4201                                    return ControlFlow::Break(());
4202                                }
4203                                if compiled(data) {
4204                                    rids.push(rid);
4205                                }
4206                                ControlFlow::Continue(())
4207                            })
4208                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
4209                        if let Some(e) = cancel_err {
4210                            return Err(e);
4211                        }
4212                        return Ok(rids);
4213                    }
4214
4215                    // Fallback: selective decode + eval.
4216                    let pred_cols = predicate_column_indices_json(predicate, &columns);
4217                    let mut rids: Vec<RowId> = Vec::with_capacity(64);
4218                    self.catalog
4219                        .try_for_each_row_raw(table, |rid, data| {
4220                            if let Err(e) = cancel.tick() {
4221                                cancel_err = Some(e);
4222                                return ControlFlow::Break(());
4223                            }
4224                            let pred_row = decode_selective(schema, &row_layout, data, &pred_cols);
4225                            if eval_predicate(predicate, &pred_row, &columns) {
4226                                rids.push(rid);
4227                            }
4228                            ControlFlow::Continue(())
4229                        })
4230                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
4231                    if let Some(e) = cancel_err {
4232                        return Err(e);
4233                    }
4234                    return Ok(rids);
4235                }
4236                self.generic_rid_match(input, table)
4237            }
4238            _ => self.generic_rid_match(input, table),
4239        }
4240    }
4241
4242    /// Decode-based rid collection for v2-capable tables (see the guard in
4243    /// [`Self::collect_rids_for_mutation`]). Scans fully reassembled rows via
4244    /// `Catalog::scan` (`decode_row_v2`, chain fetch, correct for any value
4245    /// size) and evaluates the predicate on decoded `Value`s. Returns `None`
4246    /// for shapes it does not special-case (indexed `IndexScan`, or anything
4247    /// exotic) so the caller falls through to the normal path.
4248    fn collect_rids_decoded(
4249        &mut self,
4250        input: &PlanNode,
4251        table: &str,
4252    ) -> Result<Option<Vec<RowId>>, QueryError> {
4253        // Determine the per-row predicate (None = match every row).
4254        let pred: Option<Expr> = match input {
4255            PlanNode::SeqScan { table: t } if t == table => None,
4256            PlanNode::Filter {
4257                input: inner,
4258                predicate,
4259            } => match inner.as_ref() {
4260                PlanNode::SeqScan { table: t } if t == table => Some(predicate.clone()),
4261                _ => return Ok(None),
4262            },
4263            PlanNode::IndexScan {
4264                table: t,
4265                column,
4266                key,
4267            } if t == table => {
4268                // A real index makes the lookup exact and value-size
4269                // independent — let the normal IndexScan path handle it.
4270                let indexed = self
4271                    .catalog
4272                    .get_table(table)
4273                    .map(|tb| tb.has_index(column))
4274                    .unwrap_or(false);
4275                if indexed {
4276                    return Ok(None);
4277                }
4278                Some(Expr::BinaryOp(
4279                    Box::new(Expr::Field(column.clone())),
4280                    BinOp::Eq,
4281                    Box::new(key.clone()),
4282                ))
4283            }
4284            _ => return Ok(None),
4285        };
4286
4287        let columns: Vec<String> = {
4288            let schema = self
4289                .catalog
4290                .schema(table)
4291                .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
4292            schema.columns.iter().map(|c| c.name.clone()).collect()
4293        };
4294        let mut rids: Vec<RowId> = Vec::new();
4295        let mut cancel = CancelCheck::new();
4296        for (rid, row) in self
4297            .catalog
4298            .scan(table)
4299            .map_err(|e| QueryError::StorageError(e.to_string()))?
4300        {
4301            cancel.tick()?;
4302            let keep = match &pred {
4303                None => true,
4304                Some(p) => eval_predicate(p, &row, &columns),
4305            };
4306            if keep {
4307                rids.push(rid);
4308            }
4309        }
4310        Ok(Some(rids))
4311    }
4312
4313    /// Last-ditch generic match: execute the plan, collect matching rows,
4314    /// then find corresponding RowIds by value equality. This is the old
4315    /// O(N*M) code path; only used when the plan shape is something exotic.
4316    fn generic_rid_match(
4317        &mut self,
4318        input: &PlanNode,
4319        table: &str,
4320    ) -> Result<Vec<RowId>, QueryError> {
4321        let result = self.execute_plan(input)?;
4322        let rows = match result {
4323            QueryResult::Rows { rows, .. } => rows,
4324            _ => return Err("mutation source must be rows".into()),
4325        };
4326        let mut matching: Vec<RowId> = Vec::new();
4327        let mut cancel = CancelCheck::new();
4328        for (rid, row) in self
4329            .catalog
4330            .scan(table)
4331            .map_err(|e| QueryError::StorageError(e.to_string()))?
4332        {
4333            cancel.tick()?;
4334            let mut matched = false;
4335            for candidate in &rows {
4336                cancel.tick()?;
4337                if candidate == &row {
4338                    matched = true;
4339                    break;
4340                }
4341            }
4342            if matched {
4343                matching.push(rid);
4344            }
4345        }
4346        Ok(matching)
4347    }
4348}
4349
4350pub(super) fn execute_window(
4351    result: QueryResult,
4352    windows: &[WindowDef],
4353    memory_limit: usize,
4354) -> Result<QueryResult, QueryError> {
4355    let (mut columns, mut rows) = match result {
4356        QueryResult::Rows { columns, rows } => (columns, rows),
4357        _ => return Err("window function requires row input".into()),
4358    };
4359
4360    let mut cancel = CancelCheck::new();
4361    for wdef in windows {
4362        cancel.tick()?;
4363        // Stored fields resolve once; expression-valued window keys use the
4364        // common evaluator without changing the original row order.
4365        let part_indices: Vec<Option<usize>> = wdef
4366            .partition_by
4367            .iter()
4368            .map(|expr| resolve_direct_group_expr(expr, &columns))
4369            .collect::<Result<Vec<_>, _>>()?;
4370
4371        let ord_indices: Vec<(Option<usize>, &Expr, bool)> = wdef
4372            .order_by
4373            .iter()
4374            .map(|sk| {
4375                resolve_direct_group_expr(&sk.expr, &columns)
4376                    .map(|index| (index, &sk.expr, sk.descending))
4377            })
4378            .collect::<Result<Vec<_>, _>>()?;
4379
4380        let arg_expr = wdef.args.first();
4381        let arg_col_idx = arg_expr
4382            .map(|expr| resolve_direct_group_expr(expr, &columns))
4383            .transpose()?
4384            .flatten();
4385
4386        // Build a sort-index to sort rows by partition_by then order_by
4387        // without actually reordering the original Vec (we need original
4388        // order to write results back).
4389        let n = rows.len();
4390        let mut indices: Vec<usize> = (0..n).collect();
4391        cooperative_stable_sort_by(&mut indices, memory_limit, |&a, &b| {
4392            // Compare partition keys first.
4393            for (expr, index) in wdef.partition_by.iter().zip(&part_indices) {
4394                let av = index
4395                    .map(|i| rows[a][i].clone())
4396                    .unwrap_or_else(|| eval_expr(expr, &rows[a], &columns));
4397                let bv = index
4398                    .map(|i| rows[b][i].clone())
4399                    .unwrap_or_else(|| eval_expr(expr, &rows[b], &columns));
4400                let cmp = av.cmp(&bv);
4401                if cmp != std::cmp::Ordering::Equal {
4402                    return cmp;
4403                }
4404            }
4405            // Then order keys.
4406            for &(index, expr, desc) in &ord_indices {
4407                let av = index
4408                    .map(|i| rows[a][i].clone())
4409                    .unwrap_or_else(|| eval_expr(expr, &rows[a], &columns));
4410                let bv = index
4411                    .map(|i| rows[b][i].clone())
4412                    .unwrap_or_else(|| eval_expr(expr, &rows[b], &columns));
4413                let cmp = compare_order_values(&av, &bv, desc);
4414                if cmp != std::cmp::Ordering::Equal {
4415                    return cmp;
4416                }
4417            }
4418            std::cmp::Ordering::Equal
4419        })?;
4420
4421        // SQL window-frame semantics: with no `order` clause the frame for an
4422        // aggregate window is the ENTIRE partition, not the running prefix.
4423        // The loop below computes running values; for the no-order case we
4424        // back-fill every row of a partition with the partition's final
4425        // (i.e. complete) aggregate once its boundary is reached. Ranking
4426        // functions are untouched — row_number/rank/dense_rank are inherently
4427        // positional.
4428        let whole_partition_frame = wdef.order_by.is_empty()
4429            && matches!(
4430                wdef.function,
4431                WindowFunc::Sum
4432                    | WindowFunc::Avg
4433                    | WindowFunc::Count
4434                    | WindowFunc::Min
4435                    | WindowFunc::Max
4436            );
4437        // Original row indices of the partition currently being scanned
4438        // (only tracked when back-filling is needed).
4439        let mut partition_row_indices: Vec<usize> = Vec::new();
4440
4441        // Compute window values in sorted order, tracking partition boundaries.
4442        let mut win_values: Vec<Value> = vec![Value::Empty; n];
4443        let mut partition_start = 0usize;
4444        // Running state for aggregate windows:
4445        let mut running_count: i64 = 0;
4446        let mut running_int_sum: i64 = 0;
4447        let mut running_float_sum: f64 = 0.0;
4448        let mut running_saw_float = false;
4449        let mut running_min: Option<Value> = None;
4450        let mut running_max: Option<Value> = None;
4451        let mut rank_counter: i64 = 0;
4452        let mut dense_rank_counter: i64 = 0;
4453        let mut prev_order_key: Option<Vec<Value>> = None;
4454        let mut same_rank_count: i64 = 0;
4455
4456        for sorted_pos in 0..n {
4457            cancel.tick()?;
4458            let row_idx = indices[sorted_pos];
4459
4460            // Detect partition boundary.
4461            let new_partition = if sorted_pos == 0 {
4462                true
4463            } else {
4464                let prev_row_idx = indices[sorted_pos - 1];
4465                wdef.partition_by
4466                    .iter()
4467                    .zip(&part_indices)
4468                    .any(|(expr, index)| {
4469                        let current = index
4470                            .map(|i| rows[row_idx][i].clone())
4471                            .unwrap_or_else(|| eval_expr(expr, &rows[row_idx], &columns));
4472                        let previous = index
4473                            .map(|i| rows[prev_row_idx][i].clone())
4474                            .unwrap_or_else(|| eval_expr(expr, &rows[prev_row_idx], &columns));
4475                        current != previous
4476                    })
4477            };
4478
4479            if new_partition {
4480                // No-order aggregate frame: the partition that just ended is
4481                // complete, so its final running value IS the whole-partition
4482                // aggregate. Back-fill it onto every row of that partition.
4483                if whole_partition_frame && sorted_pos > 0 {
4484                    let final_v = win_values[indices[sorted_pos - 1]].clone();
4485                    for ri in partition_row_indices.drain(..) {
4486                        cancel.tick()?;
4487                        win_values[ri] = final_v.clone();
4488                    }
4489                }
4490                partition_start = sorted_pos;
4491                running_count = 0;
4492                running_int_sum = 0;
4493                running_float_sum = 0.0;
4494                running_saw_float = false;
4495                running_min = None;
4496                running_max = None;
4497                rank_counter = 0;
4498                dense_rank_counter = 0;
4499                prev_order_key = None;
4500                same_rank_count = 0;
4501            }
4502
4503            // Extract current order key for rank tracking.
4504            let current_order_key: Vec<Value> = ord_indices
4505                .iter()
4506                .map(|&(index, expr, _)| {
4507                    index
4508                        .map(|i| rows[row_idx][i].clone())
4509                        .unwrap_or_else(|| eval_expr(expr, &rows[row_idx], &columns))
4510                })
4511                .collect();
4512            let same_as_prev = prev_order_key.as_ref() == Some(&current_order_key);
4513            let current_arg = || {
4514                arg_expr.map(|expr| {
4515                    arg_col_idx
4516                        .map(|index| rows[row_idx][index].clone())
4517                        .unwrap_or_else(|| eval_expr(expr, &rows[row_idx], &columns))
4518                })
4519            };
4520            let count_all =
4521                arg_expr.is_none() || matches!(arg_expr, Some(Expr::Field(name)) if name == "*");
4522
4523            let value = match wdef.function {
4524                WindowFunc::RowNumber => Value::Int((sorted_pos - partition_start + 1) as i64),
4525                WindowFunc::Rank => {
4526                    if same_as_prev {
4527                        same_rank_count += 1;
4528                    } else {
4529                        rank_counter += same_rank_count + 1;
4530                        same_rank_count = 0;
4531                        if rank_counter == 0 {
4532                            rank_counter = 1;
4533                        }
4534                    }
4535                    Value::Int(rank_counter)
4536                }
4537                WindowFunc::DenseRank => {
4538                    if !same_as_prev {
4539                        dense_rank_counter += 1;
4540                    }
4541                    Value::Int(dense_rank_counter)
4542                }
4543                WindowFunc::Sum => {
4544                    if let Some(value) = current_arg() {
4545                        match value {
4546                            Value::Int(v) => running_int_sum += v,
4547                            Value::Float(v) => {
4548                                running_float_sum += v;
4549                                running_saw_float = true;
4550                            }
4551                            _ => {}
4552                        }
4553                    }
4554                    if running_saw_float {
4555                        Value::Float(running_float_sum + running_int_sum as f64)
4556                    } else {
4557                        Value::Int(running_int_sum)
4558                    }
4559                }
4560                WindowFunc::Avg => {
4561                    if let Some(value) = current_arg() {
4562                        match value {
4563                            Value::Int(v) => {
4564                                running_float_sum += v as f64;
4565                                running_count += 1;
4566                            }
4567                            Value::Float(v) => {
4568                                running_float_sum += v;
4569                                running_count += 1;
4570                            }
4571                            _ => {}
4572                        }
4573                    }
4574                    if running_count == 0 {
4575                        Value::Empty
4576                    } else {
4577                        Value::Float(running_float_sum / running_count as f64)
4578                    }
4579                }
4580                WindowFunc::Count => {
4581                    if count_all {
4582                        running_count += 1;
4583                    } else if let Some(value) = current_arg() {
4584                        if !value.is_empty() {
4585                            running_count += 1;
4586                        }
4587                    }
4588                    Value::Int(running_count)
4589                }
4590                WindowFunc::Min => {
4591                    if let Some(v) = current_arg() {
4592                        if !v.is_empty() {
4593                            running_min = Some(match &running_min {
4594                                None => v,
4595                                Some(cur) => {
4596                                    if v < *cur {
4597                                        v
4598                                    } else {
4599                                        cur.clone()
4600                                    }
4601                                }
4602                            });
4603                        }
4604                    }
4605                    running_min.clone().unwrap_or(Value::Empty)
4606                }
4607                WindowFunc::Max => {
4608                    if let Some(v) = current_arg() {
4609                        if !v.is_empty() {
4610                            running_max = Some(match &running_max {
4611                                None => v,
4612                                Some(cur) => {
4613                                    if v > *cur {
4614                                        v
4615                                    } else {
4616                                        cur.clone()
4617                                    }
4618                                }
4619                            });
4620                        }
4621                    }
4622                    running_max.clone().unwrap_or(Value::Empty)
4623                }
4624            };
4625
4626            prev_order_key = Some(current_order_key);
4627            win_values[row_idx] = value;
4628            if whole_partition_frame {
4629                partition_row_indices.push(row_idx);
4630            }
4631        }
4632
4633        // Back-fill the final partition (the loop only flushes at boundaries).
4634        if whole_partition_frame && n > 0 {
4635            let final_v = win_values[indices[n - 1]].clone();
4636            for ri in partition_row_indices.drain(..) {
4637                cancel.tick()?;
4638                win_values[ri] = final_v.clone();
4639            }
4640        }
4641
4642        // Append the computed window column to each row.
4643        for (ri, row) in rows.iter_mut().enumerate() {
4644            cancel.tick()?;
4645            row.push(win_values[ri].clone());
4646        }
4647        columns.push(wdef.output_name.clone());
4648    }
4649
4650    Ok(QueryResult::Rows { columns, rows })
4651}
4652
4653/// Resolve a group-by key or aggregate argument name against the input
4654/// columns of a `GroupBy` node.
4655///
4656/// Single-table inputs have bare column names (`status`); join inputs have
4657/// `alias.field` names. Resolution rules:
4658///   1. Exact match first. Single-table keys and fully qualified
4659///      `alias.field` references hit here, preserving existing behavior.
4660///   2. A qualified reference (one containing `.`) only ever matches exactly;
4661///      if the exact column is absent it is genuinely missing.
4662///   3. An unqualified name falls back to a unique `.field` suffix match over
4663///      the join output columns. Zero matches is a column-not-found error;
4664///      more than one is an ambiguity error naming the candidates.
4665pub(super) fn resolve_group_column(name: &str, columns: &[String]) -> Result<usize, QueryError> {
4666    if let Some(i) = columns.iter().position(|c| c == name) {
4667        return Ok(i);
4668    }
4669    if name.contains('.') {
4670        return Err(QueryError::ColumnNotFound {
4671            table: String::new(),
4672            column: name.to_string(),
4673        });
4674    }
4675    let suffix = format!(".{name}");
4676    let mut matches = columns
4677        .iter()
4678        .enumerate()
4679        .filter(|(_, c)| c.ends_with(&suffix));
4680    match matches.next() {
4681        None => Err(QueryError::ColumnNotFound {
4682            table: String::new(),
4683            column: name.to_string(),
4684        }),
4685        Some((first_idx, _)) => {
4686            let rest: Vec<&str> = matches.map(|(_, c)| c.as_str()).collect();
4687            if rest.is_empty() {
4688                Ok(first_idx)
4689            } else {
4690                // Rebuild the full candidate list (the consumed first match
4691                // plus the rest) so the message names every ambiguous column.
4692                let candidates: Vec<&str> = columns
4693                    .iter()
4694                    .filter(|c| c.ends_with(&suffix))
4695                    .map(|c| c.as_str())
4696                    .collect();
4697                Err(QueryError::Execution(format!(
4698                    "cannot group by ambiguous column '{name}'; candidates: {}",
4699                    candidates.join(", ")
4700                )))
4701            }
4702        }
4703    }
4704}
4705
4706/// Mission E2b: execute a `GroupBy` plan node over already-materialized input
4707/// rows. Shared by the mutable (`execute_plan`) and read-only
4708/// (`execute_plan_readonly`) executors so key/argument resolution and the
4709/// output-column naming stay identical on both paths.
4710pub(super) fn exec_group_by(
4711    columns: Vec<String>,
4712    rows: Vec<Vec<Value>>,
4713    keys: &[GroupKey],
4714    aggregates: &[GroupAgg],
4715    having: &Option<Expr>,
4716) -> Result<QueryResult, QueryError> {
4717    exec_group_by_internal(columns, rows, None, keys, aggregates, having)
4718}
4719
4720pub(super) fn exec_group_by_with_provenance(
4721    input: ProvenanceRows,
4722    keys: &[GroupKey],
4723    aggregates: &[GroupAgg],
4724    having: &Option<Expr>,
4725    memory_limit: usize,
4726) -> Result<QueryResult, QueryError> {
4727    let ProvenanceRows {
4728        columns,
4729        rows,
4730        source_aliases,
4731        provenance,
4732    } = input;
4733    exec_group_by_internal(
4734        columns,
4735        rows,
4736        Some(GroupProvenance {
4737            source_aliases,
4738            rows: provenance,
4739            memory_limit,
4740        }),
4741        keys,
4742        aggregates,
4743        having,
4744    )
4745}
4746
4747struct GroupProvenance {
4748    source_aliases: Vec<String>,
4749    rows: Vec<Vec<Option<RowId>>>,
4750    memory_limit: usize,
4751}
4752
4753fn exec_group_by_internal(
4754    columns: Vec<String>,
4755    rows: Vec<Vec<Value>>,
4756    provenance: Option<GroupProvenance>,
4757    keys: &[GroupKey],
4758    aggregates: &[GroupAgg],
4759    having: &Option<Expr>,
4760) -> Result<QueryResult, QueryError> {
4761    // Stored fields resolve once and read directly. Expression-valued keys
4762    // (including JSON paths) use the common expression evaluator per row.
4763    let key_indices: Vec<Option<usize>> = keys
4764        .iter()
4765        .map(|k| resolve_direct_group_expr(&k.expr, &columns))
4766        .collect::<Result<Vec<_>, _>>()?;
4767
4768    let agg_field_indices: Vec<Option<usize>> = aggregates
4769        .iter()
4770        .map(|a| resolve_direct_group_expr(&a.argument, &columns))
4771        .collect::<Result<Vec<_>, _>>()?;
4772    let agg_source_indices: Vec<Option<usize>> = aggregates
4773        .iter()
4774        .map(|aggregate| {
4775            aggregate
4776                .provenance_alias
4777                .as_ref()
4778                .map(|alias| {
4779                    provenance
4780                        .as_ref()
4781                        .and_then(|provenance| {
4782                            provenance
4783                                .source_aliases
4784                                .iter()
4785                                .position(|source| source == alias)
4786                        })
4787                        .ok_or_else(|| {
4788                            QueryError::Execution(format!(
4789                                "symmetric aggregate source alias '{alias}' is not present in its input"
4790                            ))
4791                        })
4792                })
4793                .transpose()
4794        })
4795        .collect::<Result<Vec<_>, _>>()?;
4796
4797    // Group rows by key values (preserving insertion order).
4798    let mut group_map: rustc_hash::FxHashMap<Vec<Value>, usize> = rustc_hash::FxHashMap::default();
4799    let mut groups: Vec<(Vec<Value>, Vec<usize>)> = Vec::new();
4800    let mut cancel = CancelCheck::new();
4801    for (ri, row) in rows.iter().enumerate() {
4802        cancel.tick()?;
4803        let key: Vec<Value> = keys
4804            .iter()
4805            .zip(&key_indices)
4806            .map(|(key, index)| match index {
4807                Some(index) => row[*index].clone(),
4808                None => eval_expr(&key.expr, row, &columns),
4809            })
4810            .collect();
4811        match group_map.get(&key) {
4812            Some(&idx) => groups[idx].1.push(ri),
4813            None => {
4814                let idx = groups.len();
4815                group_map.insert(key.clone(), idx);
4816                groups.push((key, vec![ri]));
4817            }
4818        }
4819    }
4820
4821    // Output columns: key display names ++ aggregate output names. Qualified
4822    // keys are emitted as `alias.field` so a qualified HAVING reference and
4823    // downstream projections resolve against them.
4824    let mut out_columns: Vec<String> = keys.iter().map(|k| k.output_name()).collect();
4825    for agg in aggregates.iter() {
4826        out_columns.push(agg.output_name.clone());
4827    }
4828
4829    // Compute aggregates per group.
4830    let mut out_rows: Vec<Vec<Value>> = Vec::with_capacity(groups.len());
4831    for (key_vals, row_indices) in &groups {
4832        cancel.tick()?;
4833        let mut row = key_vals.clone();
4834        for (ai, agg) in aggregates.iter().enumerate() {
4835            let val = compute_group_aggregate(
4836                agg.function,
4837                &agg.argument,
4838                agg_field_indices[ai],
4839                GroupAggregateContext {
4840                    columns: &columns,
4841                    all_rows: &rows,
4842                    row_indices,
4843                    source_index: agg_source_indices[ai],
4844                    provenance: provenance
4845                        .as_ref()
4846                        .map(|provenance| (provenance.rows.as_slice(), provenance.memory_limit)),
4847                },
4848            )?;
4849            row.push(val);
4850        }
4851        out_rows.push(row);
4852    }
4853
4854    // Apply HAVING filter.
4855    if let Some(having_expr) = having {
4856        let mut filtered = Vec::with_capacity(out_rows.len());
4857        for row in out_rows {
4858            cancel.tick()?;
4859            if eval_predicate(having_expr, &row, &out_columns) {
4860                filtered.push(row);
4861            }
4862        }
4863        out_rows = filtered;
4864    }
4865
4866    Ok(QueryResult::Rows {
4867        columns: out_columns,
4868        rows: out_rows,
4869    })
4870}
4871
4872fn resolve_direct_group_expr(expr: &Expr, columns: &[String]) -> Result<Option<usize>, QueryError> {
4873    match expr {
4874        Expr::Field(name) if name == "*" => Ok(None),
4875        Expr::Field(name) => resolve_group_column(name, columns).map(Some),
4876        Expr::QualifiedField { qualifier, field } => {
4877            resolve_group_column(&format!("{qualifier}.{field}"), columns).map(Some)
4878        }
4879        _ => Ok(None),
4880    }
4881}
4882
4883/// Reject any aggregate `FunctionCall` that survives planning into an
4884/// evaluable position (a projection field, a filter predicate, or a HAVING
4885/// clause). The grouped-aggregate planner rewrites every supported aggregate
4886/// into a `Field` reference to a computed column, so a surviving
4887/// `FunctionCall` means the aggregate sits somewhere the engine cannot
4888/// evaluate it. `eval_expr` would otherwise silently produce `Empty` there (a
4889/// wrong answer); this turns that into a typed error before any row is
4890/// evaluated. Walks the whole plan so fused fast paths cannot bypass it.
4891/// Column indices a predicate reads, INCLUDING the json columns that JSON `->`
4892/// path bases decode from. The compiled walker `predicate_column_indices`
4893/// (`collect_field_indices`) does not descend into `Expr::JsonPath`, so on its
4894/// own it would leave the json column undecoded and every path evaluate to the
4895/// empty set. This augments it with each path's base column so `decode_selective`
4896/// materializes the value the path walks.
4897pub(super) fn predicate_column_indices_json(expr: &Expr, columns: &[String]) -> Vec<usize> {
4898    let mut indices = predicate_column_indices(expr, columns);
4899    collect_json_path_base_indices(expr, columns, &mut indices);
4900    indices.sort_unstable();
4901    indices.dedup();
4902    indices
4903}
4904
4905/// Add the column index of every `JsonPath` base reachable from `expr`.
4906fn collect_json_path_base_indices(expr: &Expr, columns: &[String], out: &mut Vec<usize>) {
4907    match expr {
4908        Expr::JsonPath { base, .. } => {
4909            let name = match base.as_ref() {
4910                Expr::Field(n) => n.clone(),
4911                Expr::QualifiedField { qualifier, field } => format!("{qualifier}.{field}"),
4912                other => {
4913                    collect_json_path_base_indices(other, columns, out);
4914                    return;
4915                }
4916            };
4917            if let Some(idx) = columns.iter().position(|c| *c == name) {
4918                out.push(idx);
4919            }
4920        }
4921        Expr::BinaryOp(l, _, r) | Expr::Coalesce(l, r) => {
4922            collect_json_path_base_indices(l, columns, out);
4923            collect_json_path_base_indices(r, columns, out);
4924        }
4925        Expr::UnaryOp(_, i) | Expr::FunctionCall(_, i, _) | Expr::Cast(i, _) => {
4926            collect_json_path_base_indices(i, columns, out);
4927        }
4928        Expr::ScalarFunc(_, args) => {
4929            for a in args {
4930                collect_json_path_base_indices(a, columns, out);
4931            }
4932        }
4933        Expr::InList { expr, list, .. } => {
4934            collect_json_path_base_indices(expr, columns, out);
4935            for item in list {
4936                collect_json_path_base_indices(item, columns, out);
4937            }
4938        }
4939        Expr::InSubquery { expr, .. } => collect_json_path_base_indices(expr, columns, out),
4940        Expr::Case { whens, else_expr } => {
4941            for (c, r) in whens {
4942                collect_json_path_base_indices(c, columns, out);
4943                collect_json_path_base_indices(r, columns, out);
4944            }
4945            if let Some(e) = else_expr {
4946                collect_json_path_base_indices(e, columns, out);
4947            }
4948        }
4949        _ => {}
4950    }
4951}
4952
4953/// Reject a JSON `->` path whose base column is not of type `json` (e.g.
4954/// `.age->x` on an int column) before any row is produced, so a mistyped path
4955/// never silently evaluates to the empty set on every row (the "database must
4956/// not have silent-wrong-answer paths" rule).
4957///
4958/// Resolution is deliberately conservative to never reject a VALID query: a
4959/// base name a `Project` node could redefine (shadowing the scan column), a
4960/// base that resolves to more than one type across joined tables, or a base
4961/// that resolves to no scan column at all, is skipped. Such paths fall through
4962/// to the generic evaluator, which safely yields `Empty` for a non-JSON base.
4963pub(super) fn validate_json_path_types(
4964    catalog: &Catalog,
4965    plan: &PlanNode,
4966) -> Result<(), QueryError> {
4967    let mut scope: Vec<(String, TypeId)> = Vec::new();
4968    collect_scan_columns(catalog, plan, &mut scope);
4969    let mut shadowed: std::collections::HashSet<String> = std::collections::HashSet::new();
4970    collect_projected_names(plan, &mut shadowed);
4971    check_plan_json_paths(plan, &scope, &shadowed)
4972}
4973
4974/// Gather the output column names and types of every scan leaf reachable from
4975/// `plan`. `SeqScan`/`IndexScan`/`RangeScan` contribute bare column names;
4976/// `AliasScan` contributes `alias.field` names (the join output shape).
4977fn collect_scan_columns(catalog: &Catalog, plan: &PlanNode, out: &mut Vec<(String, TypeId)>) {
4978    match plan {
4979        PlanNode::SeqScan { table }
4980        | PlanNode::IndexScan { table, .. }
4981        | PlanNode::RangeScan { table, .. } => {
4982            if let Some(schema) = catalog.schema(table) {
4983                for c in &schema.columns {
4984                    out.push((c.name.clone(), c.type_id));
4985                }
4986            }
4987        }
4988        PlanNode::AliasScan { table, alias } => {
4989            if let Some(schema) = catalog.schema(table) {
4990                for c in &schema.columns {
4991                    out.push((format!("{alias}.{}", c.name), c.type_id));
4992                }
4993            }
4994        }
4995        PlanNode::Filter { input, .. }
4996        | PlanNode::Project { input, .. }
4997        | PlanNode::Sort { input, .. }
4998        | PlanNode::Limit { input, .. }
4999        | PlanNode::Offset { input, .. }
5000        | PlanNode::Aggregate { input, .. }
5001        | PlanNode::Distinct { input }
5002        | PlanNode::GroupBy { input, .. }
5003        | PlanNode::Window { input, .. }
5004        | PlanNode::Update { input, .. }
5005        | PlanNode::Delete { input, .. }
5006        | PlanNode::Explain { input } => collect_scan_columns(catalog, input, out),
5007        PlanNode::NestedLoopJoin { left, right, .. } | PlanNode::Union { left, right, .. } => {
5008            collect_scan_columns(catalog, left, out);
5009            collect_scan_columns(catalog, right, out);
5010        }
5011        _ => {}
5012    }
5013}
5014
5015/// Collect the output names produced by every `Project` node, so a base name a
5016/// projection could rebind to a different type is left unvalidated.
5017fn collect_projected_names(plan: &PlanNode, out: &mut std::collections::HashSet<String>) {
5018    if let PlanNode::Project { fields, .. } = plan {
5019        for f in fields {
5020            if let Some(a) = &f.alias {
5021                out.insert(a.clone());
5022            } else {
5023                match &f.expr {
5024                    Expr::Field(n) => {
5025                        out.insert(n.clone());
5026                    }
5027                    Expr::QualifiedField { qualifier, field } => {
5028                        out.insert(format!("{qualifier}.{field}"));
5029                    }
5030                    _ => {}
5031                }
5032            }
5033        }
5034    }
5035    match plan {
5036        PlanNode::Filter { input, .. }
5037        | PlanNode::Project { input, .. }
5038        | PlanNode::Sort { input, .. }
5039        | PlanNode::Limit { input, .. }
5040        | PlanNode::Offset { input, .. }
5041        | PlanNode::Aggregate { input, .. }
5042        | PlanNode::Distinct { input }
5043        | PlanNode::GroupBy { input, .. }
5044        | PlanNode::Window { input, .. }
5045        | PlanNode::Update { input, .. }
5046        | PlanNode::Delete { input, .. }
5047        | PlanNode::Explain { input } => collect_projected_names(input, out),
5048        PlanNode::NestedLoopJoin { left, right, .. } | PlanNode::Union { left, right, .. } => {
5049            collect_projected_names(left, out);
5050            collect_projected_names(right, out);
5051        }
5052        _ => {}
5053    }
5054}
5055
5056/// Resolve `name` in `scope` to a single column type. Returns `None` when the
5057/// name is absent or resolves to more than one distinct type (ambiguous across
5058/// joined tables) — both cases are skipped by the caller.
5059fn resolve_scan_type(name: &str, scope: &[(String, TypeId)]) -> Option<TypeId> {
5060    let mut found: Option<TypeId> = None;
5061    for (n, t) in scope {
5062        if n == name {
5063            match found {
5064                None => found = Some(*t),
5065                Some(prev) if prev == *t => {}
5066                Some(_) => return None, // ambiguous
5067            }
5068        }
5069    }
5070    found
5071}
5072
5073/// If `base` (the base of a `JsonPath`) resolves to a non-`json` scan column,
5074/// return a typed error message; otherwise `None`.
5075fn json_path_base_error(
5076    base: &Expr,
5077    scope: &[(String, TypeId)],
5078    shadowed: &std::collections::HashSet<String>,
5079) -> Option<String> {
5080    let name = match base {
5081        Expr::Field(n) => n.clone(),
5082        Expr::QualifiedField { qualifier, field } => format!("{qualifier}.{field}"),
5083        // The parser flattens nested paths, so a JsonPath base is always a
5084        // Field/QualifiedField; anything else is left to the generic evaluator.
5085        _ => return None,
5086    };
5087    if shadowed.contains(&name) {
5088        return None;
5089    }
5090    match resolve_scan_type(&name, scope) {
5091        Some(TypeId::Json) | None => None,
5092        Some(other) => Some(format!(
5093            "'{}' is a {} column, not json: the '->' path operator requires a json column",
5094            name,
5095            type_id_to_name(other)
5096        )),
5097    }
5098}
5099
5100/// Walk `expr`, validating the base of every `JsonPath` it contains.
5101fn check_expr_json_paths(
5102    expr: &Expr,
5103    scope: &[(String, TypeId)],
5104    shadowed: &std::collections::HashSet<String>,
5105) -> Result<(), QueryError> {
5106    match expr {
5107        Expr::JsonPath { base, .. } => {
5108            if let Some(msg) = json_path_base_error(base, scope, shadowed) {
5109                return Err(QueryError::TypeError(msg));
5110            }
5111            check_expr_json_paths(base, scope, shadowed)
5112        }
5113        Expr::BinaryOp(l, _, r) | Expr::Coalesce(l, r) => {
5114            check_expr_json_paths(l, scope, shadowed)?;
5115            check_expr_json_paths(r, scope, shadowed)
5116        }
5117        Expr::UnaryOp(_, inner) | Expr::FunctionCall(_, inner, _) | Expr::Cast(inner, _) => {
5118            check_expr_json_paths(inner, scope, shadowed)
5119        }
5120        Expr::ScalarFunc(_, args) => {
5121            for a in args {
5122                check_expr_json_paths(a, scope, shadowed)?;
5123            }
5124            Ok(())
5125        }
5126        Expr::Window {
5127            args,
5128            partition_by,
5129            order_by,
5130            ..
5131        } => {
5132            for expr in args.iter().chain(partition_by) {
5133                check_expr_json_paths(expr, scope, shadowed)?;
5134            }
5135            for key in order_by {
5136                check_expr_json_paths(&key.expr, scope, shadowed)?;
5137            }
5138            Ok(())
5139        }
5140        Expr::InList { expr, list, .. } => {
5141            check_expr_json_paths(expr, scope, shadowed)?;
5142            for item in list {
5143                check_expr_json_paths(item, scope, shadowed)?;
5144            }
5145            Ok(())
5146        }
5147        Expr::Case { whens, else_expr } => {
5148            for (c, r) in whens {
5149                check_expr_json_paths(c, scope, shadowed)?;
5150                check_expr_json_paths(r, scope, shadowed)?;
5151            }
5152            if let Some(e) = else_expr {
5153                check_expr_json_paths(e, scope, shadowed)?;
5154            }
5155            Ok(())
5156        }
5157        // Subquery operands validate their own paths on their own plan; only
5158        // the outer operand is on this plan's scope.
5159        Expr::InSubquery { expr, .. } => check_expr_json_paths(expr, scope, shadowed),
5160        _ => Ok(()),
5161    }
5162}
5163
5164/// Recurse `plan`, validating JSON paths in every expression-bearing field.
5165fn check_plan_json_paths(
5166    plan: &PlanNode,
5167    scope: &[(String, TypeId)],
5168    shadowed: &std::collections::HashSet<String>,
5169) -> Result<(), QueryError> {
5170    match plan {
5171        PlanNode::Filter { input, predicate } => {
5172            check_expr_json_paths(predicate, scope, shadowed)?;
5173            check_plan_json_paths(input, scope, shadowed)
5174        }
5175        PlanNode::Project { input, fields } => {
5176            for f in fields {
5177                check_expr_json_paths(&f.expr, scope, shadowed)?;
5178            }
5179            check_plan_json_paths(input, scope, shadowed)
5180        }
5181        PlanNode::GroupBy {
5182            input,
5183            keys,
5184            aggregates,
5185            having,
5186        } => {
5187            for key in keys {
5188                check_expr_json_paths(&key.expr, scope, shadowed)?;
5189            }
5190            for aggregate in aggregates {
5191                check_expr_json_paths(&aggregate.argument, scope, shadowed)?;
5192            }
5193            if let Some(h) = having {
5194                check_expr_json_paths(h, scope, shadowed)?;
5195            }
5196            check_plan_json_paths(input, scope, shadowed)
5197        }
5198        PlanNode::NestedLoopJoin {
5199            left, right, on, ..
5200        } => {
5201            if let Some(on) = on {
5202                check_expr_json_paths(on, scope, shadowed)?;
5203            }
5204            check_plan_json_paths(left, scope, shadowed)?;
5205            check_plan_json_paths(right, scope, shadowed)
5206        }
5207        PlanNode::Union { left, right, .. } => {
5208            check_plan_json_paths(left, scope, shadowed)?;
5209            check_plan_json_paths(right, scope, shadowed)
5210        }
5211        PlanNode::Sort { input, keys } => {
5212            for key in keys {
5213                check_expr_json_paths(&key.expr, scope, shadowed)?;
5214            }
5215            check_plan_json_paths(input, scope, shadowed)
5216        }
5217        PlanNode::Aggregate {
5218            input, argument, ..
5219        } => {
5220            if let Some(argument) = argument {
5221                check_expr_json_paths(argument, scope, shadowed)?;
5222            }
5223            check_plan_json_paths(input, scope, shadowed)
5224        }
5225        PlanNode::Window { input, windows } => {
5226            for window in windows {
5227                for expr in window.args.iter().chain(&window.partition_by) {
5228                    check_expr_json_paths(expr, scope, shadowed)?;
5229                }
5230                for key in &window.order_by {
5231                    check_expr_json_paths(&key.expr, scope, shadowed)?;
5232                }
5233            }
5234            check_plan_json_paths(input, scope, shadowed)
5235        }
5236        PlanNode::Limit { input, .. }
5237        | PlanNode::Offset { input, .. }
5238        | PlanNode::Distinct { input }
5239        | PlanNode::Update { input, .. }
5240        | PlanNode::Delete { input, .. }
5241        | PlanNode::Explain { input } => check_plan_json_paths(input, scope, shadowed),
5242        _ => Ok(()),
5243    }
5244}
5245
5246pub(super) fn validate_no_stray_aggregates(plan: &PlanNode) -> Result<(), QueryError> {
5247    match plan {
5248        PlanNode::Project { input, fields } => {
5249            for f in fields {
5250                check_expr_no_aggregate(&f.expr)?;
5251            }
5252            validate_no_stray_aggregates(input)?;
5253        }
5254        PlanNode::Filter { input, predicate } => {
5255            check_expr_no_aggregate(predicate)?;
5256            validate_no_stray_aggregates(input)?;
5257        }
5258        PlanNode::GroupBy {
5259            input,
5260            keys,
5261            aggregates,
5262            having,
5263        } => {
5264            for key in keys {
5265                check_expr_no_aggregate(&key.expr)?;
5266            }
5267            for aggregate in aggregates {
5268                check_expr_no_aggregate(&aggregate.argument)?;
5269            }
5270            if let Some(h) = having {
5271                check_expr_no_aggregate(h)?;
5272            }
5273            validate_no_stray_aggregates(input)?;
5274        }
5275        PlanNode::NestedLoopJoin {
5276            left, right, on, ..
5277        } => {
5278            if let Some(on) = on {
5279                check_expr_no_aggregate(on)?;
5280            }
5281            validate_no_stray_aggregates(left)?;
5282            validate_no_stray_aggregates(right)?;
5283        }
5284        PlanNode::Union { left, right, .. } => {
5285            validate_no_stray_aggregates(left)?;
5286            validate_no_stray_aggregates(right)?;
5287        }
5288        PlanNode::Sort { input, keys } => {
5289            for key in keys {
5290                check_expr_no_aggregate(&key.expr)?;
5291            }
5292            validate_no_stray_aggregates(input)?;
5293        }
5294        PlanNode::Aggregate {
5295            input, argument, ..
5296        } => {
5297            if let Some(argument) = argument {
5298                check_expr_no_aggregate(argument)?;
5299            }
5300            validate_no_stray_aggregates(input)?;
5301        }
5302        PlanNode::Window { input, windows } => {
5303            for window in windows {
5304                for expr in window.args.iter().chain(&window.partition_by) {
5305                    check_expr_no_aggregate(expr)?;
5306                }
5307                for key in &window.order_by {
5308                    check_expr_no_aggregate(&key.expr)?;
5309                }
5310            }
5311            validate_no_stray_aggregates(input)?;
5312        }
5313        PlanNode::Limit { input, .. }
5314        | PlanNode::Offset { input, .. }
5315        | PlanNode::Distinct { input }
5316        | PlanNode::Update { input, .. }
5317        | PlanNode::Delete { input, .. }
5318        | PlanNode::Explain { input } => {
5319            validate_no_stray_aggregates(input)?;
5320        }
5321        _ => {}
5322    }
5323    Ok(())
5324}
5325
5326/// Recurse an expression tree, rejecting any aggregate `FunctionCall`. Does
5327/// not descend into subquery `QueryExpr`s (they are materialized and
5328/// evaluated on their own path), only their outer operand expression.
5329fn check_expr_no_aggregate(expr: &Expr) -> Result<(), QueryError> {
5330    match expr {
5331        Expr::FunctionCall(..) => Err(QueryError::Execution(
5332            "invalid query: aggregate function in an unsupported position".to_string(),
5333        )),
5334        Expr::BinaryOp(l, _, r) | Expr::Coalesce(l, r) => {
5335            check_expr_no_aggregate(l)?;
5336            check_expr_no_aggregate(r)
5337        }
5338        Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) | Expr::JsonPath { base: inner, .. } => {
5339            check_expr_no_aggregate(inner)
5340        }
5341        Expr::ScalarFunc(_, args) => {
5342            for a in args {
5343                check_expr_no_aggregate(a)?;
5344            }
5345            Ok(())
5346        }
5347        Expr::InList { expr: e, list, .. } => {
5348            check_expr_no_aggregate(e)?;
5349            for item in list {
5350                check_expr_no_aggregate(item)?;
5351            }
5352            Ok(())
5353        }
5354        Expr::InSubquery { expr: e, .. } => check_expr_no_aggregate(e),
5355        Expr::Case { whens, else_expr } => {
5356            for (c, r) in whens {
5357                check_expr_no_aggregate(c)?;
5358                check_expr_no_aggregate(r)?;
5359            }
5360            if let Some(e) = else_expr {
5361                check_expr_no_aggregate(e)?;
5362            }
5363            Ok(())
5364        }
5365        Expr::Window {
5366            args,
5367            partition_by,
5368            order_by,
5369            ..
5370        } => {
5371            for expr in args.iter().chain(partition_by) {
5372                check_expr_no_aggregate(expr)?;
5373            }
5374            for key in order_by {
5375                check_expr_no_aggregate(&key.expr)?;
5376            }
5377            Ok(())
5378        }
5379        _ => Ok(()),
5380    }
5381}
5382
5383/// Evaluate a scalar aggregate over already materialized rows. Stored-field
5384/// aggregates retain their raw-column fast path in the caller; this generic
5385/// path is also able to aggregate arbitrary expressions such as JSON paths.
5386pub(super) fn aggregate_rows(
5387    func: AggFunc,
5388    argument: Option<&Expr>,
5389    columns: &[String],
5390    rows: &[Vec<Value>],
5391) -> Result<QueryResult, QueryError> {
5392    let mut cancel = CancelCheck::new();
5393    if func == AggFunc::Count && argument.is_none() {
5394        return Ok(QueryResult::Scalar(Value::Int(rows.len() as i64)));
5395    }
5396    let argument = argument.ok_or_else(|| {
5397        QueryError::Execution(format!(
5398            "{} requires an argument",
5399            format!("{func:?}").to_lowercase()
5400        ))
5401    })?;
5402
5403    let mut values = Vec::with_capacity(rows.len());
5404    for row in rows {
5405        cancel.tick()?;
5406        values.push(eval_expr(argument, row, columns));
5407    }
5408
5409    let value = match func {
5410        AggFunc::Count => Value::Int(values.iter().filter(|v| !v.is_empty()).count() as i64),
5411        AggFunc::CountDistinct => {
5412            let seen: std::collections::HashSet<Value> =
5413                values.into_iter().filter(|v| !v.is_empty()).collect();
5414            Value::Int(seen.len() as i64)
5415        }
5416        AggFunc::Avg => {
5417            let mut sum = 0.0;
5418            let mut count = 0_u64;
5419            for value in values {
5420                match value {
5421                    Value::Int(v) => {
5422                        sum += v as f64;
5423                        count += 1;
5424                    }
5425                    Value::Float(v) => {
5426                        sum += v;
5427                        count += 1;
5428                    }
5429                    _ => {}
5430                }
5431            }
5432            if count == 0 {
5433                Value::Empty
5434            } else {
5435                Value::Float(sum / count as f64)
5436            }
5437        }
5438        AggFunc::Sum => {
5439            let mut int_sum = 0_i64;
5440            let mut float_sum = 0.0;
5441            let mut saw_float = false;
5442            for value in values {
5443                match value {
5444                    Value::Int(v) => int_sum += v,
5445                    Value::Float(v) => {
5446                        float_sum += v;
5447                        saw_float = true;
5448                    }
5449                    _ => {}
5450                }
5451            }
5452            if saw_float {
5453                Value::Float(float_sum + int_sum as f64)
5454            } else {
5455                Value::Int(int_sum)
5456            }
5457        }
5458        AggFunc::Min | AggFunc::Max => {
5459            let mut result: Option<Value> = None;
5460            for value in values.into_iter().filter(|v| !v.is_empty()) {
5461                let replace = match &result {
5462                    None => true,
5463                    Some(current) if func == AggFunc::Min => value < *current,
5464                    Some(current) => value > *current,
5465                };
5466                if replace {
5467                    result = Some(value);
5468                }
5469            }
5470            result.unwrap_or(Value::Empty)
5471        }
5472    };
5473    Ok(QueryResult::Scalar(value))
5474}
5475
5476const SYMMETRIC_RID_SET_ENTRY_BYTES: usize =
5477    std::mem::size_of::<RowId>() + 2 * std::mem::size_of::<usize>();
5478
5479pub(super) fn aggregate_rows_with_provenance(
5480    func: AggFunc,
5481    argument: Option<&Expr>,
5482    input: &ProvenanceRows,
5483    provenance_alias: &str,
5484    memory_limit: usize,
5485) -> Result<QueryResult, QueryError> {
5486    if matches!(func, AggFunc::Min | AggFunc::Max | AggFunc::CountDistinct) {
5487        return aggregate_rows(func, argument, &input.columns, &input.rows);
5488    }
5489    let argument = argument.ok_or_else(|| {
5490        QueryError::Execution(
5491            "symmetric aggregate requires a source-valued argument; use raw".to_string(),
5492        )
5493    })?;
5494    let source_index = input.source_index(provenance_alias).ok_or_else(|| {
5495        QueryError::Execution(format!(
5496            "symmetric aggregate source alias '{provenance_alias}' is not present in its input"
5497        ))
5498    })?;
5499    let mut seen = HashSet::new();
5500    let mut int_sum = 0_i64;
5501    let mut float_sum = 0.0_f64;
5502    let mut saw_float = false;
5503    let mut count = 0_u64;
5504    let mut cancel = CancelCheck::new();
5505    for (row, row_provenance) in input.rows.iter().zip(&input.provenance) {
5506        cancel.tick()?;
5507        let value = eval_expr(argument, row, &input.columns);
5508        if value.is_empty() {
5509            continue;
5510        }
5511        let Some(rid) = row_provenance[source_index] else {
5512            continue;
5513        };
5514        if !seen.insert(rid) {
5515            continue;
5516        }
5517        mem_budget::charge(SYMMETRIC_RID_SET_ENTRY_BYTES, memory_limit)?;
5518        match func {
5519            AggFunc::Count => count += 1,
5520            AggFunc::Sum | AggFunc::Avg => match value {
5521                Value::Int(value) => {
5522                    int_sum += value;
5523                    count += 1;
5524                }
5525                Value::Float(value) => {
5526                    float_sum += value;
5527                    saw_float = true;
5528                    count += 1;
5529                }
5530                _ => {}
5531            },
5532            AggFunc::CountDistinct | AggFunc::Min | AggFunc::Max => unreachable!(),
5533        }
5534    }
5535    let value = match func {
5536        AggFunc::Count => Value::Int(count as i64),
5537        AggFunc::Sum if saw_float => Value::Float(float_sum + int_sum as f64),
5538        AggFunc::Sum => Value::Int(int_sum),
5539        AggFunc::Avg if count == 0 => Value::Empty,
5540        AggFunc::Avg => Value::Float((float_sum + int_sum as f64) / count as f64),
5541        AggFunc::CountDistinct | AggFunc::Min | AggFunc::Max => unreachable!(),
5542    };
5543    Ok(QueryResult::Scalar(value))
5544}
5545
5546/// Mission E2b: compute one aggregate over a set of rows in a group.
5547pub(super) struct GroupAggregateContext<'a> {
5548    pub(super) columns: &'a [String],
5549    pub(super) all_rows: &'a [Vec<Value>],
5550    pub(super) row_indices: &'a [usize],
5551    pub(super) source_index: Option<usize>,
5552    pub(super) provenance: Option<(&'a [Vec<Option<RowId>>], usize)>,
5553}
5554
5555pub(super) fn compute_group_aggregate(
5556    func: AggFunc,
5557    argument: &Expr,
5558    direct_index: Option<usize>,
5559    context: GroupAggregateContext<'_>,
5560) -> Result<Value, QueryError> {
5561    let GroupAggregateContext {
5562        columns,
5563        all_rows,
5564        row_indices,
5565        source_index,
5566        provenance,
5567    } = context;
5568    let count_all = matches!(argument, Expr::Field(name) if name == "*");
5569    let value_at = |ri: usize| match direct_index {
5570        Some(index) => all_rows[ri][index].clone(),
5571        None => eval_expr(argument, &all_rows[ri], columns),
5572    };
5573    let mut cancel = CancelCheck::new();
5574    let mut seen_rids = HashSet::new();
5575    match func {
5576        AggFunc::Count => {
5577            if count_all {
5578                // count(*) — count all rows in the group.
5579                return Ok(Value::Int(row_indices.len() as i64));
5580            }
5581            let mut count = 0usize;
5582            for &ri in row_indices {
5583                cancel.tick()?;
5584                let value = value_at(ri);
5585                if !value.is_empty()
5586                    && accept_symmetric_contribution(ri, source_index, provenance, &mut seen_rids)?
5587                {
5588                    count += 1;
5589                }
5590            }
5591            Ok(Value::Int(count as i64))
5592        }
5593        AggFunc::CountDistinct => {
5594            let mut seen = std::collections::HashSet::new();
5595            for &ri in row_indices {
5596                cancel.tick()?;
5597                let v = value_at(ri);
5598                if !v.is_empty() {
5599                    seen.insert(v);
5600                }
5601            }
5602            Ok(Value::Int(seen.len() as i64))
5603        }
5604        AggFunc::Sum => {
5605            // Mirror the scalar Sum path: accumulate int and float
5606            // contributions separately and promote the final result to
5607            // Float if any Float row was observed. Prevents silent
5608            // drop of Float columns in GROUP BY aggregates.
5609            let mut int_sum: i64 = 0;
5610            let mut float_sum: f64 = 0.0;
5611            let mut saw_float = false;
5612            for &ri in row_indices {
5613                cancel.tick()?;
5614                let value = value_at(ri);
5615                if value.is_empty()
5616                    || !accept_symmetric_contribution(ri, source_index, provenance, &mut seen_rids)?
5617                {
5618                    continue;
5619                }
5620                match value {
5621                    Value::Int(v) => int_sum += v,
5622                    Value::Float(v) => {
5623                        float_sum += v;
5624                        saw_float = true;
5625                    }
5626                    _ => {}
5627                }
5628            }
5629            if saw_float {
5630                Ok(Value::Float(float_sum + int_sum as f64))
5631            } else {
5632                Ok(Value::Int(int_sum))
5633            }
5634        }
5635        AggFunc::Avg => {
5636            let mut sum = 0.0f64;
5637            let mut count = 0usize;
5638            for &ri in row_indices {
5639                cancel.tick()?;
5640                let value = value_at(ri);
5641                if value.is_empty()
5642                    || !accept_symmetric_contribution(ri, source_index, provenance, &mut seen_rids)?
5643                {
5644                    continue;
5645                }
5646                match value {
5647                    Value::Int(v) => {
5648                        sum += v as f64;
5649                        count += 1;
5650                    }
5651                    Value::Float(v) => {
5652                        sum += v;
5653                        count += 1;
5654                    }
5655                    _ => {}
5656                }
5657            }
5658            if count == 0 {
5659                Ok(Value::Empty)
5660            } else {
5661                Ok(Value::Float(sum / count as f64))
5662            }
5663        }
5664        AggFunc::Min | AggFunc::Max => {
5665            let mut result: Option<Value> = None;
5666            for &ri in row_indices {
5667                cancel.tick()?;
5668                let value = value_at(ri);
5669                if value.is_empty() {
5670                    continue;
5671                }
5672                let replace = match &result {
5673                    None => true,
5674                    Some(current) if func == AggFunc::Min => value < *current,
5675                    Some(current) => value > *current,
5676                };
5677                if replace {
5678                    result = Some(value);
5679                }
5680            }
5681            Ok(result.unwrap_or(Value::Empty))
5682        }
5683    }
5684}
5685
5686fn accept_symmetric_contribution(
5687    row_index: usize,
5688    source_index: Option<usize>,
5689    provenance: Option<(&[Vec<Option<RowId>>], usize)>,
5690    seen: &mut HashSet<RowId>,
5691) -> Result<bool, QueryError> {
5692    let Some(source_index) = source_index else {
5693        return Ok(true);
5694    };
5695    let Some((provenance, memory_limit)) = provenance else {
5696        return Err(QueryError::Execution(
5697            "symmetric aggregate provenance is unavailable; use raw".to_string(),
5698        ));
5699    };
5700    let Some(rid) = provenance[row_index][source_index] else {
5701        return Ok(false);
5702    };
5703    if !seen.insert(rid) {
5704        return Ok(false);
5705    }
5706    mem_budget::charge(SYMMETRIC_RID_SET_ENTRY_BYTES, memory_limit)?;
5707    Ok(true)
5708}
5709
5710struct HashJoinSpec<'a> {
5711    left_key_idx: usize,
5712    right_key_idx: usize,
5713    residuals: Vec<&'a Expr>,
5714}
5715
5716struct MaterializedJoinInputs {
5717    left_columns: Vec<String>,
5718    left_rows: Vec<Vec<Value>>,
5719    right_columns: Vec<String>,
5720    right_rows: Vec<Vec<Value>>,
5721}
5722
5723fn flatten_conjunctions<'a>(expr: &'a Expr, out: &mut Vec<&'a Expr>) {
5724    match expr {
5725        Expr::BinaryOp(left, BinOp::And, right) => {
5726            flatten_conjunctions(left, out);
5727            flatten_conjunctions(right, out);
5728        }
5729        _ => out.push(expr),
5730    }
5731}
5732
5733/// Extract one cross-side equality from an arbitrary AND conjunction. The
5734/// chosen equality becomes the hash key and every other conjunct remains a
5735/// residual predicate evaluated only inside the matching hash bucket.
5736fn try_extract_hash_join<'a>(
5737    pred: &'a Expr,
5738    left_columns: &[String],
5739    right_columns: &[String],
5740) -> Option<HashJoinSpec<'a>> {
5741    let mut conjuncts = Vec::new();
5742    flatten_conjunctions(pred, &mut conjuncts);
5743    for (key_position, conjunct) in conjuncts.iter().enumerate() {
5744        let Some((left_key_idx, right_key_idx)) =
5745            try_extract_equi_join_keys(conjunct, left_columns, right_columns)
5746        else {
5747            continue;
5748        };
5749        let residuals = conjuncts
5750            .iter()
5751            .enumerate()
5752            .filter_map(|(position, residual)| (position != key_position).then_some(*residual))
5753            .collect();
5754        return Some(HashJoinSpec {
5755            left_key_idx,
5756            right_key_idx,
5757            residuals,
5758        });
5759    }
5760    None
5761}
5762
5763/// Resolve a single cross-side equality, accepting either operand orientation.
5764pub(super) fn try_extract_equi_join_keys(
5765    pred: &Expr,
5766    left_columns: &[String],
5767    right_columns: &[String],
5768) -> Option<(usize, usize)> {
5769    let (lhs, op, rhs) = match pred {
5770        Expr::BinaryOp(l, op, r) => (l.as_ref(), *op, r.as_ref()),
5771        _ => return None,
5772    };
5773    if op != BinOp::Eq {
5774        return None;
5775    }
5776    // Normal orientation: lhs in left, rhs in right.
5777    if let (Some(li), Some(ri)) = (
5778        resolve_side_column(lhs, left_columns),
5779        resolve_side_column(rhs, right_columns),
5780    ) {
5781        return Some((li, ri));
5782    }
5783    // Swapped: rhs in left, lhs in right. Both sides of `=` are
5784    // commutative so this is safe.
5785    if let (Some(li), Some(ri)) = (
5786        resolve_side_column(rhs, left_columns),
5787        resolve_side_column(lhs, right_columns),
5788    ) {
5789        return Some((li, ri));
5790    }
5791    None
5792}
5793
5794fn resolve_side_column(expr: &Expr, columns: &[String]) -> Option<usize> {
5795    match expr {
5796        Expr::QualifiedField { qualifier, field } => {
5797            // Byte-level match so we don't allocate a fresh `format!` on
5798            // every call — this runs once per plan, so allocation would be
5799            // cheap, but the match is trivial enough to keep inline with
5800            // the eval_expr version.
5801            let q = qualifier.as_bytes();
5802            let f = field.as_bytes();
5803            columns.iter().position(|c| {
5804                let b = c.as_bytes();
5805                b.len() == q.len() + 1 + f.len()
5806                    && b[..q.len()] == *q
5807                    && b[q.len()] == b'.'
5808                    && b[q.len() + 1..] == *f
5809            })
5810        }
5811        Expr::Field(name) => columns.iter().position(|c| c == name),
5812        _ => None,
5813    }
5814}
5815
5816/// O(L + R + matching bucket candidates) hash join. Residual predicates are
5817/// evaluated only after the equi-key probe has found a candidate. For
5818/// `JoinKind::LeftOuter`, a left row is padded with `Value::Empty` when there
5819/// is no key bucket or when every candidate in its bucket fails a residual.
5820///
5821/// The right side is always the build side. That choice is forced for
5822/// LeftOuter (the left side must stream so we can detect orphans), and
5823/// for Inner it's a reasonable default — left-deep plans tend to grow the
5824/// left side with each join, so the un-joined right leaf is often the
5825/// smaller of the two at each level.
5826fn hash_join(
5827    inputs: MaterializedJoinInputs,
5828    left_key_idx: usize,
5829    right_key_idx: usize,
5830    kind: JoinKind,
5831    residuals: &[&Expr],
5832) -> Result<QueryResult, QueryError> {
5833    use rustc_hash::FxHashMap;
5834
5835    let MaterializedJoinInputs {
5836        left_columns,
5837        left_rows,
5838        right_columns,
5839        right_rows,
5840    } = inputs;
5841
5842    let n_left = left_columns.len();
5843    let n_right = right_columns.len();
5844    let mut columns = Vec::with_capacity(n_left + n_right);
5845    columns.extend(left_columns);
5846    columns.extend(right_columns);
5847
5848    // Cooperative cancellation: build and probe both walk the full input, so
5849    // poll the deadline in each so a huge-input join can be timed out / freed.
5850    let mut cancel = CancelCheck::new();
5851
5852    // Build: right_key -> list of right-row indices. Pre-size to the row
5853    // count so the map doesn't rehash mid-build.
5854    let mut build: FxHashMap<Value, Vec<usize>> =
5855        FxHashMap::with_capacity_and_hasher(right_rows.len(), Default::default());
5856    for (i, row) in right_rows.iter().enumerate() {
5857        cancel.tick()?;
5858        // PowQL equality is direct Value equality, including Empty = Empty.
5859        // Hash joins must preserve the same semantics as the nested-loop
5860        // evaluator rather than silently dropping nullable-key matches.
5861        build.entry(row[right_key_idx].clone()).or_default().push(i);
5862    }
5863
5864    // Reasonable starting capacity — inner joins produce ≥ left_rows.len()
5865    // rows in the common 1:1 case, left-outer always emits ≥ left_rows.len().
5866    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(left_rows.len());
5867
5868    crate::cancel::check()?;
5869    for left_row in &left_rows {
5870        cancel.tick()?;
5871        let key = &left_row[left_key_idx];
5872        let candidates = build.get(key);
5873        let mut matched = false;
5874        match candidates {
5875            Some(matches) if !matches.is_empty() => {
5876                for &ri in matches {
5877                    cancel.tick()?;
5878                    let right_row = &right_rows[ri];
5879                    let mut combined = Vec::with_capacity(n_left + n_right);
5880                    combined.extend_from_slice(left_row);
5881                    combined.extend_from_slice(right_row);
5882                    if residuals
5883                        .iter()
5884                        .all(|residual| eval_predicate(residual, &combined, &columns))
5885                    {
5886                        rows.push(combined);
5887                        check_join_limit(rows.len())?;
5888                        matched = true;
5889                    }
5890                }
5891            }
5892            _ => {}
5893        }
5894        if !matched && matches!(kind, JoinKind::LeftOuter) {
5895            let mut row = Vec::with_capacity(n_left + n_right);
5896            row.extend_from_slice(left_row);
5897            row.resize(n_left + n_right, Value::Empty);
5898            rows.push(row);
5899            check_join_limit(rows.len())?;
5900        }
5901    }
5902
5903    Ok(QueryResult::Rows { columns, rows })
5904}
5905
5906#[inline]
5907pub(super) fn check_nested_loop_pair_limit(
5908    left_rows: usize,
5909    right_rows: usize,
5910    pair_limit: usize,
5911) -> Result<usize, QueryError> {
5912    let candidate_pairs =
5913        left_rows
5914            .checked_mul(right_rows)
5915            .ok_or(QueryError::NestedLoopPairLimitExceeded {
5916                left_rows,
5917                right_rows,
5918                limit: pair_limit,
5919            })?;
5920    if candidate_pairs > pair_limit {
5921        return Err(QueryError::NestedLoopPairLimitExceeded {
5922            left_rows,
5923            right_rows,
5924            limit: pair_limit,
5925        });
5926    }
5927    Ok(candidate_pairs)
5928}
5929
5930/// Execute a join over already materialized inputs. Runtime column resolution
5931/// decides whether a cross-side equality is usable as a hash key; otherwise the
5932/// checked and cancellation-aware nested loop remains the compatibility path.
5933pub(super) fn execute_materialized_join(
5934    left_columns: Vec<String>,
5935    left_rows: Vec<Vec<Value>>,
5936    right_columns: Vec<String>,
5937    right_rows: Vec<Vec<Value>>,
5938    on: Option<&Expr>,
5939    kind: JoinKind,
5940    pair_limit: usize,
5941) -> Result<QueryResult, QueryError> {
5942    crate::cancel::check()?;
5943    if !matches!(kind, JoinKind::Cross) {
5944        if let Some(pred) = on {
5945            if let Some(spec) = try_extract_hash_join(pred, &left_columns, &right_columns) {
5946                return hash_join(
5947                    MaterializedJoinInputs {
5948                        left_columns,
5949                        left_rows,
5950                        right_columns,
5951                        right_rows,
5952                    },
5953                    spec.left_key_idx,
5954                    spec.right_key_idx,
5955                    kind,
5956                    &spec.residuals,
5957                );
5958            }
5959        }
5960    }
5961
5962    check_nested_loop_pair_limit(left_rows.len(), right_rows.len(), pair_limit)?;
5963    let n_left = left_columns.len();
5964    let n_right = right_columns.len();
5965    let mut columns = Vec::with_capacity(n_left + n_right);
5966    columns.extend(left_columns);
5967    columns.extend(right_columns);
5968
5969    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(left_rows.len());
5970    let mut combined: Vec<Value> = Vec::with_capacity(n_left + n_right);
5971    let mut cancel = CancelCheck::new();
5972    for left_row in &left_rows {
5973        let mut matched = false;
5974        for right_row in &right_rows {
5975            cancel.tick()?;
5976            combined.clear();
5977            combined.extend_from_slice(left_row);
5978            combined.extend_from_slice(right_row);
5979            let keep = match kind {
5980                JoinKind::Cross => true,
5981                JoinKind::Inner | JoinKind::LeftOuter => {
5982                    on.is_none_or(|pred| eval_predicate(pred, &combined, &columns))
5983                }
5984                JoinKind::RightOuter => {
5985                    unreachable!("planner rewrites RightOuter to LeftOuter")
5986                }
5987            };
5988            if keep {
5989                rows.push(combined.clone());
5990                check_join_limit(rows.len())?;
5991                matched = true;
5992            }
5993        }
5994        if !matched && matches!(kind, JoinKind::LeftOuter) {
5995            let mut row = Vec::with_capacity(n_left + n_right);
5996            row.extend_from_slice(left_row);
5997            row.resize(n_left + n_right, Value::Empty);
5998            rows.push(row);
5999            check_join_limit(rows.len())?;
6000        }
6001    }
6002    Ok(QueryResult::Rows { columns, rows })
6003}
6004
6005fn execute_provenance_join(
6006    left: ProvenanceRows,
6007    right: ProvenanceRows,
6008    on: Option<&Expr>,
6009    kind: JoinKind,
6010    pair_limit: usize,
6011) -> Result<ProvenanceRows, QueryError> {
6012    let left_width = left.columns.len();
6013    let right_width = right.columns.len();
6014    let right_source_count = right.source_aliases.len();
6015    let mut columns = left.columns.clone();
6016    columns.extend(right.columns.clone());
6017    let mut source_aliases = left.source_aliases.clone();
6018    source_aliases.extend(right.source_aliases.clone());
6019    let mut rows = Vec::new();
6020    let mut provenance = Vec::new();
6021    let mut cancel = CancelCheck::new();
6022
6023    if !matches!(kind, JoinKind::Cross) {
6024        if let Some(predicate) = on {
6025            if let Some(spec) = try_extract_hash_join(predicate, &left.columns, &right.columns) {
6026                let mut build: rustc_hash::FxHashMap<Value, Vec<usize>> =
6027                    rustc_hash::FxHashMap::default();
6028                for (index, row) in right.rows.iter().enumerate() {
6029                    cancel.tick()?;
6030                    let key = &row[spec.right_key_idx];
6031                    build.entry(key.clone()).or_default().push(index);
6032                }
6033                for (left_index, left_row) in left.rows.iter().enumerate() {
6034                    cancel.tick()?;
6035                    let key = &left_row[spec.left_key_idx];
6036                    let candidates = build.get(key);
6037                    let mut matched = false;
6038                    if let Some(candidates) = candidates {
6039                        for &right_index in candidates {
6040                            cancel.tick()?;
6041                            let mut row = Vec::with_capacity(left_width + right_width);
6042                            row.extend_from_slice(left_row);
6043                            row.extend_from_slice(&right.rows[right_index]);
6044                            if spec
6045                                .residuals
6046                                .iter()
6047                                .all(|residual| eval_predicate(residual, &row, &columns))
6048                            {
6049                                let mut row_provenance = left.provenance[left_index].clone();
6050                                row_provenance.extend_from_slice(&right.provenance[right_index]);
6051                                rows.push(row);
6052                                provenance.push(row_provenance);
6053                                check_join_limit(rows.len())?;
6054                                matched = true;
6055                            }
6056                        }
6057                    }
6058                    if !matched && matches!(kind, JoinKind::LeftOuter) {
6059                        let mut row = left_row.clone();
6060                        row.resize(left_width + right_width, Value::Empty);
6061                        let mut row_provenance = left.provenance[left_index].clone();
6062                        row_provenance.extend(std::iter::repeat_n(None, right_source_count));
6063                        rows.push(row);
6064                        provenance.push(row_provenance);
6065                        check_join_limit(rows.len())?;
6066                    }
6067                }
6068                return Ok(ProvenanceRows {
6069                    columns,
6070                    rows,
6071                    source_aliases,
6072                    provenance,
6073                });
6074            }
6075        }
6076    }
6077
6078    check_nested_loop_pair_limit(left.rows.len(), right.rows.len(), pair_limit)?;
6079    for (left_index, left_row) in left.rows.iter().enumerate() {
6080        let mut matched = false;
6081        for (right_index, right_row) in right.rows.iter().enumerate() {
6082            cancel.tick()?;
6083            let mut row = Vec::with_capacity(left_width + right_width);
6084            row.extend_from_slice(left_row);
6085            row.extend_from_slice(right_row);
6086            let keep = match kind {
6087                JoinKind::Cross => true,
6088                JoinKind::Inner | JoinKind::LeftOuter => {
6089                    on.is_none_or(|predicate| eval_predicate(predicate, &row, &columns))
6090                }
6091                JoinKind::RightOuter => {
6092                    unreachable!("planner rewrites RightOuter to LeftOuter")
6093                }
6094            };
6095            if keep {
6096                let mut row_provenance = left.provenance[left_index].clone();
6097                row_provenance.extend_from_slice(&right.provenance[right_index]);
6098                rows.push(row);
6099                provenance.push(row_provenance);
6100                check_join_limit(rows.len())?;
6101                matched = true;
6102            }
6103        }
6104        if !matched && matches!(kind, JoinKind::LeftOuter) {
6105            let mut row = left_row.clone();
6106            row.resize(left_width + right_width, Value::Empty);
6107            let mut row_provenance = left.provenance[left_index].clone();
6108            row_provenance.extend(std::iter::repeat_n(None, right_source_count));
6109            rows.push(row);
6110            provenance.push(row_provenance);
6111            check_join_limit(rows.len())?;
6112        }
6113    }
6114    Ok(ProvenanceRows {
6115        columns,
6116        rows,
6117        source_aliases,
6118        provenance,
6119    })
6120}
6121
6122/// Lower unindexed `RangeScan` and `IndexScan` nodes to `Filter(SeqScan)`
6123/// so that all downstream fast paths (count, project+limit, sort+limit,
6124/// agg, update, delete) continue to fire.
6125///
6126/// The planner emits `RangeScan` (for `.age > 30`) and `IndexScan` (for
6127/// `.email = lit`) speculatively because it has no catalog access. When
6128/// the column has a B-tree index, those plans are correct. When it
6129/// doesn't, the executor's fallbacks materialise every matching row with
6130/// full `decode_row` — bypassing the compiled-predicate fast paths that
6131/// `Filter(SeqScan)` would trigger. Lowering both speculative leaf kinds
6132/// also keeps EXPLAIN honest: it prints the plan that actually runs.
6133///
6134/// This pass runs once per query, before execution.
6135pub(super) fn lower_unindexed_scans(catalog: &Catalog, plan: &PlanNode) -> PlanNode {
6136    match plan {
6137        PlanNode::ExprIndexScan { table, path, .. }
6138        | PlanNode::ExprRangeScan { table, path, .. }
6139        | PlanNode::OrderedExprIndexScan { table, path, .. } => {
6140            if resolve_expression_index(catalog, table, path).is_some() {
6141                plan.clone()
6142            } else {
6143                expression_index_fallback(plan)
6144                    .expect("expression-index branch always has a fallback")
6145            }
6146        }
6147        PlanNode::RangeScan {
6148            table,
6149            column,
6150            start,
6151            end,
6152        } => {
6153            if let Some(tbl) = catalog.get_table(table) {
6154                // Keep RangeScan whenever ANY index exists on the column:
6155                // unique indexes store raw column values, non-unique indexes
6156                // store composite (value, rid) keys that the executor walks
6157                // natively via BTree::range_rids. Only lower to Filter(SeqScan)
6158                // when the column is unindexed.
6159                if tbl.has_index(column) {
6160                    return plan.clone();
6161                }
6162            }
6163            let pred = synthesize_range_predicate(column, start, end);
6164            PlanNode::Filter {
6165                input: Box::new(PlanNode::SeqScan {
6166                    table: table.clone(),
6167                }),
6168                predicate: pred,
6169            }
6170        }
6171        PlanNode::Filter { input, predicate } => PlanNode::Filter {
6172            input: Box::new(lower_unindexed_scans(catalog, input)),
6173            predicate: predicate.clone(),
6174        },
6175        PlanNode::Project { input, fields } => PlanNode::Project {
6176            input: Box::new(lower_unindexed_scans(catalog, input)),
6177            fields: fields.clone(),
6178        },
6179        PlanNode::Sort { input, keys } => PlanNode::Sort {
6180            input: Box::new(lower_unindexed_scans(catalog, input)),
6181            keys: keys.clone(),
6182        },
6183        PlanNode::Limit { input, count } => PlanNode::Limit {
6184            input: Box::new(lower_unindexed_scans(catalog, input)),
6185            count: count.clone(),
6186        },
6187        PlanNode::Offset { input, count } => PlanNode::Offset {
6188            input: Box::new(lower_unindexed_scans(catalog, input)),
6189            count: count.clone(),
6190        },
6191        PlanNode::Aggregate {
6192            input,
6193            function,
6194            argument,
6195            mode,
6196            provenance_alias,
6197        } => PlanNode::Aggregate {
6198            input: Box::new(lower_unindexed_scans(catalog, input)),
6199            function: *function,
6200            argument: argument.clone(),
6201            mode: *mode,
6202            provenance_alias: provenance_alias.clone(),
6203        },
6204        PlanNode::Distinct { input } => PlanNode::Distinct {
6205            input: Box::new(lower_unindexed_scans(catalog, input)),
6206        },
6207        PlanNode::GroupBy {
6208            input,
6209            keys,
6210            aggregates,
6211            having,
6212        } => PlanNode::GroupBy {
6213            input: Box::new(lower_unindexed_scans(catalog, input)),
6214            keys: keys.clone(),
6215            aggregates: aggregates.clone(),
6216            having: having.clone(),
6217        },
6218        PlanNode::Update {
6219            input,
6220            table,
6221            assignments,
6222            returning,
6223        } => PlanNode::Update {
6224            input: Box::new(lower_unindexed_scans(catalog, input)),
6225            table: table.clone(),
6226            assignments: assignments.clone(),
6227            returning: *returning,
6228        },
6229        PlanNode::Delete {
6230            input,
6231            table,
6232            returning,
6233        } => PlanNode::Delete {
6234            input: Box::new(lower_unindexed_scans(catalog, input)),
6235            table: table.clone(),
6236            returning: *returning,
6237        },
6238        PlanNode::Window { input, windows } => PlanNode::Window {
6239            input: Box::new(lower_unindexed_scans(catalog, input)),
6240            windows: windows.clone(),
6241        },
6242        PlanNode::Union { left, right, all } => PlanNode::Union {
6243            left: Box::new(lower_unindexed_scans(catalog, left)),
6244            right: Box::new(lower_unindexed_scans(catalog, right)),
6245            all: *all,
6246        },
6247        PlanNode::Explain { input } => PlanNode::Explain {
6248            input: Box::new(lower_unindexed_scans(catalog, input)),
6249        },
6250        PlanNode::NestedLoopJoin {
6251            left,
6252            right,
6253            on,
6254            kind,
6255        } => PlanNode::NestedLoopJoin {
6256            left: Box::new(lower_unindexed_scans(catalog, left)),
6257            right: Box::new(lower_unindexed_scans(catalog, right)),
6258            on: on.clone(),
6259            kind: *kind,
6260        },
6261        PlanNode::IndexScan { table, column, key } => {
6262            if let Some(tbl) = catalog.get_table(table) {
6263                if tbl.has_index(column) {
6264                    return plan.clone();
6265                }
6266            }
6267            PlanNode::Filter {
6268                input: Box::new(PlanNode::SeqScan {
6269                    table: table.clone(),
6270                }),
6271                predicate: Expr::BinaryOp(
6272                    Box::new(Expr::Field(column.clone())),
6273                    BinOp::Eq,
6274                    Box::new(key.clone()),
6275                ),
6276            }
6277        }
6278        // Leaf nodes: no children to recurse into.
6279        _ => plan.clone(),
6280    }
6281}
6282
6283fn stored_json_path_expr(path: &powdb_storage::stored_json_path::StoredJsonPathV1) -> Expr {
6284    use powdb_storage::stored_json_path::StoredJsonPathSegmentV1;
6285
6286    Expr::JsonPath {
6287        base: Box::new(Expr::Field(path.column.clone())),
6288        segments: path
6289            .segments
6290            .iter()
6291            .map(|segment| match segment {
6292                StoredJsonPathSegmentV1::Key(key) => PathSeg::Key(key.clone()),
6293                StoredJsonPathSegmentV1::Index(index) => PathSeg::Index(*index),
6294            })
6295            .collect(),
6296    }
6297}
6298
6299fn synthesize_expr_range_predicate(
6300    path: &powdb_storage::stored_json_path::StoredJsonPathV1,
6301    start: &Option<(Expr, bool)>,
6302    end: &Option<(Expr, bool)>,
6303) -> Expr {
6304    let lower = start.as_ref().map(|(expr, inclusive)| {
6305        Expr::BinaryOp(
6306            Box::new(stored_json_path_expr(path)),
6307            if *inclusive { BinOp::Gte } else { BinOp::Gt },
6308            Box::new(expr.clone()),
6309        )
6310    });
6311    let upper = end.as_ref().map(|(expr, inclusive)| {
6312        Expr::BinaryOp(
6313            Box::new(stored_json_path_expr(path)),
6314            if *inclusive { BinOp::Lte } else { BinOp::Lt },
6315            Box::new(expr.clone()),
6316        )
6317    });
6318    match (lower, upper) {
6319        (Some(lower), Some(upper)) => Expr::BinaryOp(Box::new(lower), BinOp::And, Box::new(upper)),
6320        (Some(lower), None) => lower,
6321        (None, Some(upper)) => upper,
6322        (None, None) => Expr::Literal(Literal::Bool(true)),
6323    }
6324}
6325
6326/// Synthesize a range predicate from RangeScan bounds for the fallback path.
6327pub(super) fn synthesize_range_predicate(
6328    column: &str,
6329    start: &Option<(Expr, bool)>,
6330    end: &Option<(Expr, bool)>,
6331) -> Expr {
6332    let lower = start.as_ref().map(|(expr, inclusive)| {
6333        let op = if *inclusive { BinOp::Gte } else { BinOp::Gt };
6334        Expr::BinaryOp(
6335            Box::new(Expr::Field(column.to_string())),
6336            op,
6337            Box::new(expr.clone()),
6338        )
6339    });
6340    let upper = end.as_ref().map(|(expr, inclusive)| {
6341        let op = if *inclusive { BinOp::Lte } else { BinOp::Lt };
6342        Expr::BinaryOp(
6343            Box::new(Expr::Field(column.to_string())),
6344            op,
6345            Box::new(expr.clone()),
6346        )
6347    });
6348    match (lower, upper) {
6349        (Some(l), Some(u)) => Expr::BinaryOp(Box::new(l), BinOp::And, Box::new(u)),
6350        (Some(l), None) => l,
6351        (None, Some(u)) => u,
6352        (None, None) => Expr::Literal(Literal::Bool(true)),
6353    }
6354}
6355
6356/// Check if a value falls within a range (used in last-resort decoded-row eval).
6357pub(super) fn range_matches(
6358    val: &Value,
6359    start: &Option<Value>,
6360    start_inc: bool,
6361    end: &Option<Value>,
6362    end_inc: bool,
6363) -> bool {
6364    if let Some(ref s) = start {
6365        if start_inc {
6366            if val < s {
6367                return false;
6368            }
6369        } else if val <= s {
6370            return false;
6371        }
6372    }
6373    if let Some(ref e) = end {
6374        if end_inc {
6375            if val > e {
6376                return false;
6377            }
6378        } else if val >= e {
6379            return false;
6380        }
6381    }
6382    true
6383}
6384
6385fn collect_plan_qualifiers(plan: &PlanNode, qualifiers: &mut HashSet<String>) {
6386    match plan {
6387        PlanNode::SeqScan { table }
6388        | PlanNode::IndexScan { table, .. }
6389        | PlanNode::RangeScan { table, .. }
6390        | PlanNode::ExprIndexScan { table, .. }
6391        | PlanNode::ExprRangeScan { table, .. }
6392        | PlanNode::OrderedExprIndexScan { table, .. } => {
6393            qualifiers.insert(table.clone());
6394        }
6395        PlanNode::AliasScan { alias, .. } => {
6396            qualifiers.insert(alias.clone());
6397        }
6398        PlanNode::Filter { input, .. }
6399        | PlanNode::Project { input, .. }
6400        | PlanNode::Sort { input, .. }
6401        | PlanNode::Limit { input, .. }
6402        | PlanNode::Offset { input, .. }
6403        | PlanNode::Aggregate { input, .. }
6404        | PlanNode::Distinct { input }
6405        | PlanNode::GroupBy { input, .. }
6406        | PlanNode::Update { input, .. }
6407        | PlanNode::Delete { input, .. }
6408        | PlanNode::Window { input, .. }
6409        | PlanNode::Explain { input } => collect_plan_qualifiers(input, qualifiers),
6410        PlanNode::NestedLoopJoin { left, right, .. } | PlanNode::Union { left, right, .. } => {
6411            collect_plan_qualifiers(left, qualifiers);
6412            collect_plan_qualifiers(right, qualifiers);
6413        }
6414        _ => {}
6415    }
6416}
6417
6418fn qualified_ref(expr: &Expr) -> Option<&str> {
6419    match expr {
6420        Expr::QualifiedField { qualifier, .. } => Some(qualifier),
6421        _ => None,
6422    }
6423}
6424
6425fn explain_join_strategy(
6426    left: &PlanNode,
6427    right: &PlanNode,
6428    on: Option<&Expr>,
6429    kind: JoinKind,
6430) -> &'static str {
6431    if matches!(kind, JoinKind::Cross) {
6432        return "nested-loop-bounded";
6433    }
6434    let Some(predicate) = on else {
6435        return "nested-loop-bounded";
6436    };
6437    let mut conjunctions = Vec::new();
6438    flatten_conjunctions(predicate, &mut conjunctions);
6439    let mut left_qualifiers = HashSet::new();
6440    let mut right_qualifiers = HashSet::new();
6441    collect_plan_qualifiers(left, &mut left_qualifiers);
6442    collect_plan_qualifiers(right, &mut right_qualifiers);
6443
6444    let has_cross_side_equi = conjunctions.iter().any(|expr| {
6445        let Expr::BinaryOp(lhs, BinOp::Eq, rhs) = expr else {
6446            return false;
6447        };
6448        let (Some(lhs_q), Some(rhs_q)) = (qualified_ref(lhs), qualified_ref(rhs)) else {
6449            return false;
6450        };
6451        (left_qualifiers.contains(lhs_q) && right_qualifiers.contains(rhs_q))
6452            || (left_qualifiers.contains(rhs_q) && right_qualifiers.contains(lhs_q))
6453    });
6454    if has_cross_side_equi {
6455        if conjunctions.len() > 1 {
6456            "hash+residual"
6457        } else {
6458            "hash"
6459        }
6460    } else {
6461        "nested-loop-bounded"
6462    }
6463}
6464
6465/// Format a `PlanNode` tree as a human-readable, indented text
6466/// representation. Used by the `EXPLAIN` command.
6467pub(super) fn format_plan_tree(catalog: &Catalog, plan: &PlanNode, depth: usize) -> String {
6468    let indent = "  ".repeat(depth);
6469    match plan {
6470        PlanNode::SeqScan { table } => format!("{indent}SeqScan table={table}"),
6471        PlanNode::AliasScan { table, alias } => {
6472            format!("{indent}AliasScan table={table} alias={alias}")
6473        }
6474        PlanNode::IndexScan { table, column, key } => {
6475            format!("{indent}IndexScan table={table} column={column} key={key:?}")
6476        }
6477        PlanNode::RangeScan {
6478            table,
6479            column,
6480            start,
6481            end,
6482        } => {
6483            let s = match start {
6484                Some((expr, inc)) => {
6485                    let op = if *inc { ">=" } else { ">" };
6486                    format!("{op}{expr:?}")
6487                }
6488                None => "unbounded".to_string(),
6489            };
6490            let e = match end {
6491                Some((expr, inc)) => {
6492                    let op = if *inc { "<=" } else { "<" };
6493                    format!("{op}{expr:?}")
6494                }
6495                None => "unbounded".to_string(),
6496            };
6497            format!("{indent}RangeScan table={table} column={column} [{s}, {e}]")
6498        }
6499        PlanNode::ExprIndexScan { table, path, key } => {
6500            let index_id = resolve_expression_index(catalog, table, path)
6501                .map(|metadata| metadata.index_id.to_string())
6502                .unwrap_or_else(|| "unresolved".to_string());
6503            format!(
6504                "{indent}ExprIndexScan table={table} path={} index_id={index_id} key={key:?}",
6505                path.canonical_text()
6506            )
6507        }
6508        PlanNode::ExprRangeScan {
6509            table,
6510            path,
6511            start,
6512            end,
6513        } => {
6514            let index_id = resolve_expression_index(catalog, table, path)
6515                .map(|metadata| metadata.index_id.to_string())
6516                .unwrap_or_else(|| "unresolved".to_string());
6517            format!(
6518                "{indent}ExprRangeScan table={table} path={} index_id={index_id} start={start:?} end={end:?}",
6519                path.canonical_text()
6520            )
6521        }
6522        PlanNode::OrderedExprIndexScan {
6523            table,
6524            path,
6525            descending,
6526            limit,
6527            offset,
6528        } => {
6529            let index_id = resolve_expression_index(catalog, table, path)
6530                .map(|metadata| metadata.index_id.to_string())
6531                .unwrap_or_else(|| "unresolved".to_string());
6532            format!(
6533                "{indent}OrderedExprIndexScan table={table} path={} index_id={index_id} descending={descending} limit={limit:?} offset={offset:?}",
6534                path.canonical_text()
6535            )
6536        }
6537        PlanNode::Filter { input, predicate } => {
6538            let child = format_plan_tree(catalog, input, depth + 1);
6539            format!("{indent}Filter predicate={predicate:?}\n{child}")
6540        }
6541        PlanNode::Project { input, fields } => {
6542            let names: Vec<String> = fields
6543                .iter()
6544                .map(|f| match &f.alias {
6545                    Some(a) => format!("{a}: {:?}", f.expr),
6546                    None => format!("{:?}", f.expr),
6547                })
6548                .collect();
6549            let child = format_plan_tree(catalog, input, depth + 1);
6550            format!("{indent}Project fields=[{}]\n{child}", names.join(", "))
6551        }
6552        PlanNode::Sort { input, keys } => {
6553            let ks: Vec<String> = keys
6554                .iter()
6555                .map(|k| {
6556                    let expr = expression_output_name(&k.expr);
6557                    if k.descending {
6558                        format!("{expr} desc")
6559                    } else {
6560                        expr
6561                    }
6562                })
6563                .collect();
6564            let child = format_plan_tree(catalog, input, depth + 1);
6565            format!("{indent}Sort keys=[{}]\n{child}", ks.join(", "))
6566        }
6567        PlanNode::Limit { input, count } => {
6568            let child = format_plan_tree(catalog, input, depth + 1);
6569            format!("{indent}Limit count={count:?}\n{child}")
6570        }
6571        PlanNode::Offset { input, count } => {
6572            let child = format_plan_tree(catalog, input, depth + 1);
6573            format!("{indent}Offset count={count:?}\n{child}")
6574        }
6575        PlanNode::Aggregate {
6576            input,
6577            function,
6578            argument,
6579            mode,
6580            provenance_alias: _,
6581        } => {
6582            let argument = argument
6583                .as_ref()
6584                .map(expression_output_name)
6585                .unwrap_or_else(|| "*".to_string());
6586            let child = format_plan_tree(catalog, input, depth + 1);
6587            format!("{indent}Aggregate fn={function:?} mode={mode:?} argument={argument}\n{child}")
6588        }
6589        PlanNode::NestedLoopJoin {
6590            left,
6591            right,
6592            on,
6593            kind,
6594        } => {
6595            let left_child = format_plan_tree(catalog, left, depth + 1);
6596            let right_child = format_plan_tree(catalog, right, depth + 1);
6597            let on_str = match on {
6598                Some(pred) => format!("{pred:?}"),
6599                None => "none".to_string(),
6600            };
6601            let strategy = explain_join_strategy(left, right, on.as_ref(), *kind);
6602            format!(
6603                "{indent}NestedLoopJoin kind={kind:?} strategy={strategy} on={on_str}\n{left_child}\n{right_child}"
6604            )
6605        }
6606        PlanNode::Distinct { input } => {
6607            let child = format_plan_tree(catalog, input, depth + 1);
6608            format!("{indent}Distinct\n{child}")
6609        }
6610        PlanNode::GroupBy {
6611            input,
6612            keys,
6613            aggregates,
6614            having,
6615        } => {
6616            let agg_strs: Vec<String> = aggregates
6617                .iter()
6618                .map(|a| {
6619                    format!(
6620                        "{:?}({}) mode={:?} as {}",
6621                        a.function,
6622                        expression_output_name(&a.argument),
6623                        a.mode,
6624                        a.output_name
6625                    )
6626                })
6627                .collect();
6628            let having_str = match having {
6629                Some(h) => format!(" having={h:?}"),
6630                None => String::new(),
6631            };
6632            let key_strs: Vec<String> = keys.iter().map(|k| k.output_name()).collect();
6633            let child = format_plan_tree(catalog, input, depth + 1);
6634            format!(
6635                "{indent}GroupBy keys=[{}] aggs=[{}]{having_str}\n{child}",
6636                key_strs.join(", "),
6637                agg_strs.join(", "),
6638            )
6639        }
6640        PlanNode::Insert { table, rows, .. } => {
6641            let cols: Vec<&str> = rows
6642                .first()
6643                .map(|r| r.iter().map(|a| a.field.as_str()).collect())
6644                .unwrap_or_default();
6645            format!(
6646                "{indent}Insert table={table} rows={} cols=[{}]",
6647                rows.len(),
6648                cols.join(", ")
6649            )
6650        }
6651        PlanNode::Upsert {
6652            table,
6653            key_column,
6654            assignments,
6655            on_conflict,
6656        } => {
6657            let cols: Vec<&str> = assignments.iter().map(|a| a.field.as_str()).collect();
6658            let conflict_cols: Vec<&str> = on_conflict.iter().map(|a| a.field.as_str()).collect();
6659            if conflict_cols.is_empty() {
6660                format!(
6661                    "{indent}Upsert table={table} key={key_column} cols=[{}]",
6662                    cols.join(", ")
6663                )
6664            } else {
6665                format!(
6666                    "{indent}Upsert table={table} key={key_column} cols=[{}] on_conflict=[{}]",
6667                    cols.join(", "),
6668                    conflict_cols.join(", ")
6669                )
6670            }
6671        }
6672        PlanNode::Update {
6673            input,
6674            table,
6675            assignments,
6676            returning,
6677        } => {
6678            let cols: Vec<&str> = assignments.iter().map(|a| a.field.as_str()).collect();
6679            let child = format_plan_tree(catalog, input, depth + 1);
6680            let ret = if *returning { " returning" } else { "" };
6681            format!(
6682                "{indent}Update table={table} set=[{}]{ret}\n{child}",
6683                cols.join(", ")
6684            )
6685        }
6686        PlanNode::Delete {
6687            input,
6688            table,
6689            returning,
6690        } => {
6691            let child = format_plan_tree(catalog, input, depth + 1);
6692            let ret = if *returning { " returning" } else { "" };
6693            format!("{indent}Delete table={table}{ret}\n{child}")
6694        }
6695        PlanNode::CreateTable { name, fields, .. } => {
6696            let fs: Vec<String> = fields
6697                .iter()
6698                .map(|f| {
6699                    let mut mods = String::new();
6700                    if f.required {
6701                        mods.push_str(" required");
6702                    }
6703                    if f.unique {
6704                        mods.push_str(" unique");
6705                    }
6706                    format!("{}: {}{mods}", f.name, f.type_name)
6707                })
6708                .collect();
6709            format!("{indent}CreateTable name={name} fields=[{}]", fs.join(", "))
6710        }
6711        PlanNode::AlterTable { table, action } => {
6712            format!("{indent}AlterTable table={table} action={action:?}")
6713        }
6714        PlanNode::DropTable { name, .. } => format!("{indent}DropTable name={name}"),
6715        PlanNode::CreateView { name, .. } => format!("{indent}CreateView name={name}"),
6716        PlanNode::RefreshView { name } => format!("{indent}RefreshView name={name}"),
6717        PlanNode::DropView { name, .. } => format!("{indent}DropView name={name}"),
6718        PlanNode::ListTypes => format!("{indent}ListTypes"),
6719        PlanNode::Describe { table } => format!("{indent}Describe table={table}"),
6720        PlanNode::Window { input, windows } => {
6721            let ws: Vec<String> = windows
6722                .iter()
6723                .map(|w| format!("{:?} as {}", w.function, w.output_name))
6724                .collect();
6725            let child = format_plan_tree(catalog, input, depth + 1);
6726            format!("{indent}Window fns=[{}]\n{child}", ws.join(", "))
6727        }
6728        PlanNode::Union { left, right, all } => {
6729            let kind = if *all { "UNION ALL" } else { "UNION" };
6730            let left_child = format_plan_tree(catalog, left, depth + 1);
6731            let right_child = format_plan_tree(catalog, right, depth + 1);
6732            format!("{indent}{kind}\n{left_child}\n{right_child}")
6733        }
6734        PlanNode::Explain { input } => {
6735            let child = format_plan_tree(catalog, input, depth + 1);
6736            format!("{indent}Explain\n{child}")
6737        }
6738        PlanNode::Begin => format!("{indent}Begin"),
6739        PlanNode::Commit => format!("{indent}Commit"),
6740        PlanNode::Rollback => format!("{indent}Rollback"),
6741    }
6742}