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