Skip to main content

powdb_query/executor/plan_exec/
dispatch.rs

1//! The `execute_plan` dispatch match and materialized view operations.
2
3use crate::cancel::CancelCheck;
4use crate::result::{QueryError, QueryResult};
5use powdb_storage::row::{decode_row, RowLayout};
6use powdb_storage::types::*;
7use std::ops::ControlFlow;
8
9use crate::executor::compiled::*;
10use crate::executor::eval::*;
11use crate::executor::row_body_base;
12use crate::executor::{Engine, MAX_SORT_ROWS};
13use powdb_storage::view::ViewDef;
14
15use super::*;
16
17impl Engine {
18    pub fn execute_plan(&mut self, plan: &PlanNode) -> Result<QueryResult, QueryError> {
19        // Refuse any plan whose evaluable expressions still carry an aggregate
20        // FunctionCall the grouped-aggregate planner could not lower. Without
21        // this, such an aggregate would reach eval_expr and silently evaluate
22        // to Empty (a wrong answer). The outermost call validates the whole
23        // tree before any row is produced.
24        validate_no_stray_aggregates(plan)?;
25        validate_json_path_types(&self.catalog, plan)?;
26        match plan {
27            PlanNode::ExprIndexScan { .. }
28            | PlanNode::ExprRangeScan { .. }
29            | PlanNode::OrderedExprIndexScan { .. } => {
30                if let Some(result) = self.execute_expression_index_plan(plan, None)? {
31                    return Ok(result);
32                }
33                let fallback = expression_index_fallback(plan)
34                    .expect("expression-index branch always has a fallback");
35                self.execute_plan(&fallback)
36            }
37            PlanNode::SeqScan { table } => {
38                // Auto-refresh dirty materialized views on read.
39                if self.view_registry.is_dirty(table) {
40                    self.refresh_view(table)?;
41                }
42                let schema = self
43                    .catalog
44                    .schema(table)
45                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
46                    .clone();
47                let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
48                // Cooperative cancellation: a full-table scan of a huge table
49                // must stay stoppable.
50                let mut cancel = CancelCheck::new();
51                let mut rows: Vec<Vec<Value>> = Vec::new();
52                for (_, row) in self
53                    .catalog
54                    .scan(table)
55                    .map_err(|e| QueryError::StorageError(e.to_string()))?
56                {
57                    cancel.tick()?;
58                    rows.push(row);
59                }
60                Ok(QueryResult::Rows { columns, rows })
61            }
62
63            PlanNode::Filter { input, predicate } => {
64                // Materialize any IN-subqueries in the predicate before the
65                // scan loop — the closure can't call back into the engine.
66                // Correlated subqueries are left in place for per-row eval.
67                let materialized;
68                let predicate = if contains_subquery(predicate) {
69                    materialized = self.materialize_subqueries(predicate)?;
70                    &materialized
71                } else {
72                    predicate
73                };
74
75                // Correlated subquery path: per-row materialisation.
76                if contains_subquery(predicate) {
77                    let result = self.execute_plan(input)?;
78                    return match result {
79                        QueryResult::Rows { columns, rows } => {
80                            let mut filtered = Vec::new();
81                            // Cooperative cancellation: a subquery runs per outer
82                            // row, so a large outer scan must stay stoppable.
83                            let mut cancel = CancelCheck::new();
84                            for row in rows {
85                                cancel.tick()?;
86                                let row_pred =
87                                    self.materialize_correlated_for_row(predicate, &row, &columns)?;
88                                if eval_predicate(&row_pred, &row, &columns) {
89                                    filtered.push(row);
90                                }
91                            }
92                            Ok(QueryResult::Rows {
93                                columns,
94                                rows: filtered,
95                            })
96                        }
97                        _ => Err("filter requires row input".into()),
98                    };
99                }
100
101                // Lane A fast path: Filter over an equality-driven index scan.
102                // The index narrows the candidate rids; the residual is
103                // re-checked with a partial decode, full rows only for matches.
104                if matches!(
105                    input.as_ref(),
106                    PlanNode::IndexScan { .. } | PlanNode::ExprIndexScan { .. }
107                ) {
108                    if let Some(result) = self.try_filter_index_residual_fast(input, predicate)? {
109                        return Ok(result);
110                    }
111                }
112
113                // Fast path: fuse Filter + SeqScan into a zero-copy streaming
114                // loop. Uses decode_column() to evaluate the predicate on only
115                // the columns it references, avoiding heap allocations for
116                // String/Bytes columns that aren't part of the filter.
117                // Overflow safety (P0-4/P1): v2-capable tables fall through to
118                // the decoded general Filter path below — the raw fast path
119                // rehydrates to v1 and drops/mis-reads >= 64KB spilled values.
120                if let PlanNode::SeqScan { table } = input.as_ref() {
121                    if !self.catalog.table_has_overflow(table) {
122                        // Auto-refresh dirty materialized views.
123                        if self.view_registry.is_dirty(table) {
124                            self.refresh_view(table)?;
125                        }
126                        let schema = self
127                            .catalog
128                            .schema(table)
129                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
130                            .clone();
131                        let columns: Vec<String> =
132                            schema.columns.iter().map(|c| c.name.clone()).collect();
133                        let fast = FastLayout::new(&schema);
134                        let row_layout = RowLayout::new(&schema);
135                        // Mission F: pre-size to skip the first 4 Vec doublings
136                        // (4 → 8 → 16 → 32 → 64). On a 100K-row scan with 30%
137                        // selectivity that's ~4 fewer reallocations + memcpys.
138                        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
139
140                        // Try compiled predicate for the filter check (handles
141                        // int leaves, string-eq leaves, and And conjunctions).
142                        // Cooperative cancellation: a full-table compiled/
143                        // selective predicate scan must stay stoppable, so use
144                        // the early-terminating scan and break on cancel. The
145                        // captured error is surfaced after the scan returns.
146                        let mut cancel = CancelCheck::new();
147                        let mut cancel_err: Option<QueryError> = None;
148                        if let Some(compiled) =
149                            compile_predicate(predicate, &columns, &fast, &schema)
150                        {
151                            self.catalog
152                                .try_for_each_row_raw(table, |_rid, data| {
153                                    if let Err(e) = cancel.tick() {
154                                        cancel_err = Some(e);
155                                        return ControlFlow::Break(());
156                                    }
157                                    if compiled(data) {
158                                        rows.push(decode_row(&schema, data));
159                                    }
160                                    ControlFlow::Continue(())
161                                })
162                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
163                        } else {
164                            let pred_cols = predicate_column_indices_json(predicate, &columns);
165                            self.catalog
166                                .try_for_each_row_raw(table, |_rid, data| {
167                                    if let Err(e) = cancel.tick() {
168                                        cancel_err = Some(e);
169                                        return ControlFlow::Break(());
170                                    }
171                                    let pred_row =
172                                        decode_selective(&schema, &row_layout, data, &pred_cols);
173                                    if eval_predicate(predicate, &pred_row, &columns) {
174                                        rows.push(decode_row(&schema, data));
175                                    }
176                                    ControlFlow::Continue(())
177                                })
178                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
179                        }
180                        if let Some(e) = cancel_err {
181                            return Err(e);
182                        }
183
184                        return Ok(QueryResult::Rows { columns, rows });
185                    }
186                }
187
188                // General path: materialise then filter.
189                let result = self.execute_plan(input)?;
190                match result {
191                    QueryResult::Rows { columns, rows } => {
192                        let mut cancel = CancelCheck::new();
193                        let mut filtered: Vec<Vec<Value>> = Vec::new();
194                        for row in rows {
195                            cancel.tick()?;
196                            if eval_predicate(predicate, &row, &columns) {
197                                filtered.push(row);
198                            }
199                        }
200                        Ok(QueryResult::Rows {
201                            columns,
202                            rows: filtered,
203                        })
204                    }
205                    _ => Err("filter requires row input".into()),
206                }
207            }
208
209            PlanNode::Project { input, fields } => {
210                if matches!(
211                    input.as_ref(),
212                    PlanNode::ExprIndexScan { .. }
213                        | PlanNode::ExprRangeScan { .. }
214                        | PlanNode::OrderedExprIndexScan { .. }
215                ) {
216                    if let Some(result) = self.execute_expression_index_plan(input, Some(fields))? {
217                        return Ok(result);
218                    }
219                }
220                // Fast path: Project over IndexScan — decode only projected
221                // columns from raw bytes instead of full decode_row.
222                if let PlanNode::IndexScan { table, column, key } = input.as_ref() {
223                    let schema = self
224                        .catalog
225                        .schema(table)
226                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
227                        .clone();
228                    let all_columns: Vec<String> =
229                        schema.columns.iter().map(|c| c.name.clone()).collect();
230                    let key_value = literal_to_value(key)?;
231                    let tbl = self
232                        .catalog
233                        .get_table(table)
234                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
235
236                    let proj_columns: Vec<String> = fields
237                        .iter()
238                        .map(|f| {
239                            f.alias.clone().unwrap_or_else(|| match &f.expr {
240                                Expr::Field(name) => name.clone(),
241                                _ => "?".into(),
242                            })
243                        })
244                        .collect();
245
246                    // Determine which column indices the projection needs
247                    let proj_indices: Vec<usize> = fields
248                        .iter()
249                        .filter_map(|f| {
250                            if let Expr::Field(name) = &f.expr {
251                                all_columns.iter().position(|c| c == name)
252                            } else {
253                                None
254                            }
255                        })
256                        .collect();
257
258                    // Only serve plain-field projections here; a computed
259                    // projection (e.g. `length(.v)`) must fall through to the
260                    // generic expression-evaluating path — otherwise its column
261                    // is silently dropped (proj_indices only collects Fields).
262                    let all_plain_fields = fields.iter().all(|f| matches!(f.expr, Expr::Field(_)));
263                    if tbl.has_index(column) && all_plain_fields {
264                        let rids = tbl.index_lookup_all(column, &key_value);
265                        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(rids.len());
266                        let mut cancel = CancelCheck::new();
267                        for rid in rids {
268                            cancel.tick()?;
269                            // Overflow safety (P0-3/P0-4): `tbl.get` reassembles
270                            // spilled columns from their overflow chains. The old
271                            // `heap.get` + `decode_column` read raw v2 bytes and
272                            // returned Empty for a spilled column (or wrapped a
273                            // >= 64KB value).
274                            if let Some(full) = tbl.get(rid) {
275                                let row: Vec<Value> =
276                                    proj_indices.iter().map(|&ci| full[ci].clone()).collect();
277                                rows.push(row);
278                            }
279                        }
280                        return Ok(QueryResult::Rows {
281                            columns: proj_columns,
282                            rows,
283                        });
284                    }
285                }
286
287                // Fast path: Project(Limit(Sort(Filter(SeqScan)))) — bounded
288                // top-N heap. Decodes only the sort key + projected columns,
289                // keeps at most `limit` rows in a heap. Also handles the
290                // Project(Limit(Sort(SeqScan))) variant (no filter).
291                if let PlanNode::Limit {
292                    input: inner,
293                    count: limit_expr,
294                } = input.as_ref()
295                {
296                    if let PlanNode::Sort {
297                        input: sort_input,
298                        keys,
299                    } = inner.as_ref()
300                    {
301                        // Fast path only for single-key sorts
302                        if keys.len() == 1 {
303                            if let Expr::Field(sort_field) = &keys[0].expr {
304                                let descending = keys[0].descending;
305                                let limit = match limit_expr {
306                                    Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
307                                    _ => usize::MAX,
308                                };
309                                let (table_opt, pred_opt): (Option<&str>, Option<&Expr>) =
310                                    match sort_input.as_ref() {
311                                        PlanNode::SeqScan { table } => (Some(table.as_str()), None),
312                                        PlanNode::Filter {
313                                            input: fi,
314                                            predicate,
315                                        } => {
316                                            if let PlanNode::SeqScan { table } = fi.as_ref() {
317                                                (Some(table.as_str()), Some(predicate))
318                                            } else {
319                                                (None, None)
320                                            }
321                                        }
322                                        _ => (None, None),
323                                    };
324                                if let Some(table) = table_opt {
325                                    if let Some(result) = self.project_filter_sort_limit_fast(
326                                        table, fields, sort_field, descending, limit, pred_opt,
327                                    )? {
328                                        return Ok(result);
329                                    }
330                                }
331                            }
332                        }
333                    }
334                    // Fast path: Project(Limit(Filter(SeqScan))) — stream,
335                    // decode only projected columns, stop at limit.
336                    if let PlanNode::Filter {
337                        input: fi,
338                        predicate,
339                    } = inner.as_ref()
340                    {
341                        if let PlanNode::SeqScan { table } = fi.as_ref() {
342                            let limit = match limit_expr {
343                                Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
344                                _ => usize::MAX,
345                            };
346                            if let Some(result) = self.project_filter_limit_fast(
347                                table,
348                                fields,
349                                limit,
350                                Some(predicate),
351                            )? {
352                                return Ok(result);
353                            }
354                        }
355                    }
356                    // Fast path: Project(Limit(SeqScan)) — stream, no filter.
357                    if let PlanNode::SeqScan { table } = inner.as_ref() {
358                        let limit = match limit_expr {
359                            Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
360                            _ => usize::MAX,
361                        };
362                        if let Some(result) =
363                            self.project_filter_limit_fast(table, fields, limit, None)?
364                        {
365                            return Ok(result);
366                        }
367                    }
368                }
369
370                // Mission D4: Project(Filter(SeqScan)) without Limit. Reuses
371                // `project_filter_limit_fast` with limit = usize::MAX so the
372                // hot loop decodes only projected columns and uses the
373                // compiled predicate. Previously this fell through to the
374                // generic Filter branch which materialised every column via
375                // `decode_row` then re-projected — quadratic work.
376                //
377                // multi_col_and_filter (`U filter .age > 30 and .status =
378                // "active" { .name, .age }`) was 6.18ms (0.7x SQLite) and
379                // is the load-bearing workload for this fast path.
380                if let PlanNode::Filter {
381                    input: fi,
382                    predicate,
383                } = input.as_ref()
384                {
385                    if let PlanNode::SeqScan { table } = fi.as_ref() {
386                        if let Some(result) = self.project_filter_limit_fast(
387                            table,
388                            fields,
389                            usize::MAX,
390                            Some(predicate),
391                        )? {
392                            return Ok(result);
393                        }
394                    }
395                }
396
397                // Mission D4: Project(SeqScan) without Filter or Limit.
398                // Decode only projected columns; the previous fall-through
399                // built full Vec<Value> rows then re-projected.
400                if let PlanNode::SeqScan { table } = input.as_ref() {
401                    if let Some(result) =
402                        self.project_filter_limit_fast(table, fields, usize::MAX, None)?
403                    {
404                        return Ok(result);
405                    }
406                }
407
408                let result = self.execute_plan(input)?;
409                match result {
410                    QueryResult::Rows { columns, rows } => {
411                        let proj_columns: Vec<String> = fields
412                            .iter()
413                            .map(|f| {
414                                f.alias.clone().unwrap_or_else(|| match &f.expr {
415                                    Expr::Field(name) => name.clone(),
416                                    // Mission E1.2: `{ u.name }` projects as the
417                                    // qualified column name so callers can still
418                                    // disambiguate across the join output.
419                                    Expr::QualifiedField { qualifier, field } => {
420                                        format!("{qualifier}.{field}")
421                                    }
422                                    _ => "?".into(),
423                                })
424                            })
425                            .collect();
426                        let mut cancel = CancelCheck::new();
427                        let mut proj_rows: Vec<Vec<Value>> = Vec::with_capacity(rows.len());
428                        for row in &rows {
429                            cancel.tick()?;
430                            proj_rows.push(
431                                fields
432                                    .iter()
433                                    .map(|f| eval_expr(&f.expr, row, &columns))
434                                    .collect(),
435                            );
436                        }
437                        Ok(QueryResult::Rows {
438                            columns: proj_columns,
439                            rows: proj_rows,
440                        })
441                    }
442                    _ => Err("project requires row input".into()),
443                }
444            }
445
446            PlanNode::Sort { input, keys } => {
447                let result = self.execute_plan(input)?;
448                match result {
449                    QueryResult::Rows { columns, mut rows } => {
450                        // WS2: row-count cap is a cheap secondary guard; the
451                        // byte budget is the real OOM defense for the sort
452                        // buffer (a few very large rows pass the row cap).
453                        if rows.len() > MAX_SORT_ROWS {
454                            return Err(QueryError::SortLimitExceeded);
455                        }
456                        self.charge_rows(&rows)?;
457                        let key_specs: Vec<(Option<usize>, &Expr, bool)> = keys
458                            .iter()
459                            .map(|k| {
460                                let stored_name = match &k.expr {
461                                    Expr::Field(name) => Some(name.clone()),
462                                    Expr::QualifiedField { qualifier, field } => {
463                                        Some(format!("{qualifier}.{field}"))
464                                    }
465                                    _ => None,
466                                };
467                                let index = stored_name
468                                    .as_ref()
469                                    .and_then(|name| columns.iter().position(|c| c == name));
470                                if let Some(name) = stored_name {
471                                    if index.is_none() {
472                                        return Err(QueryError::ColumnNotFound {
473                                            table: String::new(),
474                                            column: name,
475                                        });
476                                    }
477                                }
478                                Ok((index, &k.expr, k.descending))
479                            })
480                            .collect::<Result<_, QueryError>>()?;
481                        cooperative_stable_sort_by(&mut rows, self.query_memory_limit, |a, b| {
482                            for &(col_idx, expr, descending) in &key_specs {
483                                let (left_value, right_value) = match col_idx {
484                                    Some(index) => (&a[index], &b[index]),
485                                    None => {
486                                        let left = eval_expr(expr, a, &columns);
487                                        let right = eval_expr(expr, b, &columns);
488                                        let cmp = compare_order_values(&left, &right, descending);
489                                        if cmp != std::cmp::Ordering::Equal {
490                                            return cmp;
491                                        }
492                                        continue;
493                                    }
494                                };
495                                let cmp = compare_order_values(left_value, right_value, descending);
496                                if cmp != std::cmp::Ordering::Equal {
497                                    return cmp;
498                                }
499                            }
500                            std::cmp::Ordering::Equal
501                        })?;
502                        Ok(QueryResult::Rows { columns, rows })
503                    }
504                    _ => Err("sort requires row input".into()),
505                }
506            }
507
508            PlanNode::Limit { input, count } => {
509                let result = self.execute_plan(input)?;
510                let n = match count {
511                    Expr::Literal(Literal::Int(v)) => *v as usize,
512                    _ => return Err("limit must be integer literal".into()),
513                };
514                match result {
515                    QueryResult::Rows { columns, rows } => {
516                        let mut cancel = CancelCheck::new();
517                        let mut limited = Vec::with_capacity(n.min(rows.len()));
518                        for row in rows.into_iter().take(n) {
519                            cancel.tick()?;
520                            limited.push(row);
521                        }
522                        Ok(QueryResult::Rows {
523                            columns,
524                            rows: limited,
525                        })
526                    }
527                    _ => Err("limit requires row input".into()),
528                }
529            }
530
531            PlanNode::Offset { input, count } => {
532                let result = self.execute_plan(input)?;
533                let n = match count {
534                    Expr::Literal(Literal::Int(v)) => *v as usize,
535                    _ => return Err("offset must be integer literal".into()),
536                };
537                match result {
538                    QueryResult::Rows { columns, rows } => {
539                        let mut cancel = CancelCheck::new();
540                        let mut offset = Vec::with_capacity(rows.len().saturating_sub(n));
541                        for (index, row) in rows.into_iter().enumerate() {
542                            cancel.tick()?;
543                            if index >= n {
544                                offset.push(row);
545                            }
546                        }
547                        Ok(QueryResult::Rows {
548                            columns,
549                            rows: offset,
550                        })
551                    }
552                    _ => Err("offset requires row input".into()),
553                }
554            }
555
556            PlanNode::Aggregate {
557                input,
558                function,
559                argument,
560                mode: _,
561                provenance_alias,
562            } => {
563                if let Some(provenance_alias) = provenance_alias {
564                    let input = self.materialize_rows_with_provenance(input)?;
565                    self.charge_rows(&input.rows)?;
566                    return aggregate_rows_with_provenance(
567                        *function,
568                        argument.as_ref(),
569                        &input,
570                        provenance_alias,
571                        self.query_memory_limit(),
572                    );
573                }
574                // Fast path: count() over SeqScan — count rows without any decode
575                if *function == AggFunc::Count {
576                    // Overflow safety (P0-4): the raw `for_each_row_raw` count
577                    // drops any row too large to re-inline (>= 64KB) and would
578                    // undercount; v2-capable tables use the decoded generic path.
579                    if let PlanNode::SeqScan { table } = input.as_ref() {
580                        if !self.catalog.table_has_overflow(table) {
581                            // Auto-refresh a dirty materialized view before
582                            // counting it — otherwise count(View) returns stale
583                            // data after an underlying mutation (F3).
584                            if self.view_registry.is_dirty(table) {
585                                self.refresh_view(table)?;
586                            }
587                            let mut count: i64 = 0;
588                            for_each_row_raw_cancellable(&self.catalog, table, |_rid, _data| {
589                                count += 1;
590                            })?;
591                            return Ok(QueryResult::Scalar(Value::Int(count)));
592                        }
593                    }
594                    // Fast path: count() over Filter(SeqScan) — try compiled
595                    // predicate first, fall back to decode_column path.
596                    // Skip a predicate carrying a subquery: the raw-bytes
597                    // evaluators here don't materialise subqueries, so
598                    // `count(T filter .x in (...))` would silently count 0
599                    // (F1). Falling through routes it to the generic path
600                    // that resolves the subquery correctly.
601                    if let PlanNode::Filter {
602                        input: inner,
603                        predicate,
604                    } = input.as_ref()
605                    {
606                        if let PlanNode::SeqScan { table } = inner.as_ref() {
607                            if self.view_registry.is_dirty(table) {
608                                self.refresh_view(table)?;
609                            }
610                        }
611                        if let (PlanNode::SeqScan { table }, false) =
612                            (inner.as_ref(), contains_subquery(predicate))
613                        {
614                            if !self.catalog.table_has_overflow(table) {
615                                let schema = self
616                                    .catalog
617                                    .schema(table)
618                                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
619                                    .clone();
620                                let columns: Vec<String> =
621                                    schema.columns.iter().map(|c| c.name.clone()).collect();
622                                let fast = FastLayout::new(&schema);
623                                let row_layout = RowLayout::new(&schema);
624
625                                // Try compiled predicate (zero-allocation hot path).
626                                // Handles int leaves, string-eq leaves, AND conjunctions.
627                                if let Some(compiled) =
628                                    compile_predicate(predicate, &columns, &fast, &schema)
629                                {
630                                    let mut count: i64 = 0;
631                                    for_each_row_raw_cancellable(
632                                        &self.catalog,
633                                        table,
634                                        |_rid, data| {
635                                            if compiled(data) {
636                                                count += 1;
637                                            }
638                                        },
639                                    )?;
640                                    return Ok(QueryResult::Scalar(Value::Int(count)));
641                                }
642
643                                // Fallback: decode predicate columns
644                                let pred_cols = predicate_column_indices_json(predicate, &columns);
645                                let mut count: i64 = 0;
646                                for_each_row_raw_cancellable(
647                                    &self.catalog,
648                                    table,
649                                    |_rid, data| {
650                                        let pred_row = decode_selective(
651                                            &schema,
652                                            &row_layout,
653                                            data,
654                                            &pred_cols,
655                                        );
656                                        if eval_predicate(predicate, &pred_row, &columns) {
657                                            count += 1;
658                                        }
659                                    },
660                                )?;
661
662                                return Ok(QueryResult::Scalar(Value::Int(count)));
663                            }
664                        }
665                    }
666                }
667
668                // Fast path: sum/avg/min/max over a single fixed-size int
669                // column with an optional compiled filter predicate. Walks
670                // raw row bytes, zero allocation per row.
671                if matches!(
672                    function,
673                    AggFunc::Sum
674                        | AggFunc::Avg
675                        | AggFunc::Min
676                        | AggFunc::Max
677                        | AggFunc::CountDistinct
678                ) {
679                    if let Some(Expr::Field(col)) = argument.as_ref() {
680                        // Shape: Aggregate(SeqScan) or Aggregate(Filter(SeqScan))
681                        let (table_opt, pred_opt): (Option<&str>, Option<&Expr>) =
682                            match input.as_ref() {
683                                PlanNode::SeqScan { table } => (Some(table.as_str()), None),
684                                PlanNode::Filter {
685                                    input: inner,
686                                    predicate,
687                                } => {
688                                    if let PlanNode::SeqScan { table } = inner.as_ref() {
689                                        (Some(table.as_str()), Some(predicate))
690                                    } else {
691                                        (None, None)
692                                    }
693                                }
694                                _ => (None, None),
695                            };
696                        if let Some(table) = table_opt {
697                            if let Some(result) =
698                                self.agg_single_col_fast(table, col, *function, pred_opt)?
699                            {
700                                return Ok(result);
701                            }
702                        }
703                    }
704                }
705
706                // Fast path: Project(Limit(Filter(SeqScan))) — stream, decode
707                // only projected columns, stop once we hit the limit.
708                // (Handled in the Project branch; this branch only fires when
709                // the aggregate is the outer node.)
710                let result = self.execute_plan(input)?;
711                match result {
712                    QueryResult::Rows { columns, rows } => {
713                        aggregate_rows(*function, argument.as_ref(), &columns, &rows)
714                    }
715                    _ => Err("aggregate requires row input".into()),
716                }
717            }
718
719            PlanNode::Insert {
720                table,
721                rows,
722                returning,
723            } => {
724                // Build + validate EVERY row before inserting any, so a bad
725                // row (unknown/missing/uncoercible field) aborts the whole
726                // statement without a partial write. The WAL fsync happens
727                // once at statement end, so N rows = N appends + 1 fsync.
728                let mut returning_columns: Vec<String> = Vec::new();
729                let all_values: Vec<Vec<Value>> = {
730                    let schema = self
731                        .catalog
732                        .schema(table)
733                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
734                    if *returning {
735                        returning_columns = schema.columns.iter().map(|c| c.name.clone()).collect();
736                    }
737                    let defaults = self.catalog.column_defaults(table).unwrap_or(&[]);
738                    let auto = self.catalog.auto_columns(table).unwrap_or(&[]);
739                    let mut all = Vec::with_capacity(rows.len());
740                    for assignments in rows {
741                        let mut values = vec![Value::Empty; schema.columns.len()];
742                        for a in assignments {
743                            let idx = schema.column_index(&a.field).ok_or_else(|| {
744                                QueryError::ColumnNotFound {
745                                    table: String::new(),
746                                    column: a.field.clone(),
747                                }
748                            })?;
749                            let raw = literal_to_value(&a.value)?;
750                            values[idx] = coerce_value(raw, &schema.columns[idx])?;
751                        }
752                        // Fill any column left unset by this row from its
753                        // declared default (applied before the required check,
754                        // so a default satisfies a required column).
755                        for (i, slot) in values.iter_mut().enumerate() {
756                            if slot.is_empty() {
757                                if let Some(Some(d)) = defaults.get(i) {
758                                    *slot = d.clone();
759                                }
760                            }
761                        }
762                        for col in &schema.columns {
763                            let pos = col.position as usize;
764                            // Auto columns are exempt from the required check —
765                            // they are filled from the sequence just below.
766                            let is_auto = auto.get(pos).copied().unwrap_or(false);
767                            if col.required && !is_auto && matches!(values[pos], Value::Empty) {
768                                return Err(QueryError::Execution(format!(
769                                    "column '{}' is required but no value was provided",
770                                    col.name
771                                )));
772                            }
773                        }
774                        all.push(values);
775                    }
776                    all
777                };
778                // Assign auto-increment columns now that the immutable
779                // schema/defaults/auto borrows are released. Done here (not in
780                // the build loop) so the assigned ids land in `all_values` and
781                // flow back through `returning`.
782                let mut all_values = all_values;
783                for values in all_values.iter_mut() {
784                    self.catalog.assign_auto_columns(table, values);
785                }
786                // Charge the materialized batch against the per-query memory
787                // budget before inserting — keeps multi-row insert consistent
788                // with every other full-materialization point (sort/join/group)
789                // and bounds embedded callers (the server also caps the query
790                // string at 1 MB, but embedded callers have no such limit).
791                self.charge_rows(&all_values)?;
792                let n = all_values.len() as u64;
793                for values in &all_values {
794                    self.catalog
795                        .insert(table, values)
796                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
797                }
798                self.view_registry.mark_dependents_dirty(table);
799                if *returning {
800                    Ok(QueryResult::Rows {
801                        columns: returning_columns,
802                        rows: all_values,
803                    })
804                } else {
805                    Ok(QueryResult::Modified(n))
806                }
807            }
808
809            PlanNode::Upsert {
810                table,
811                key_column,
812                assignments,
813                on_conflict,
814            } => {
815                let (values, key_idx) = {
816                    let schema = self
817                        .catalog
818                        .schema(table)
819                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
820                    let mut values = vec![Value::Empty; schema.columns.len()];
821                    for a in assignments {
822                        let idx = schema.column_index(&a.field).ok_or_else(|| {
823                            QueryError::ColumnNotFound {
824                                table: String::new(),
825                                column: a.field.clone(),
826                            }
827                        })?;
828                        let raw = literal_to_value(&a.value)?;
829                        values[idx] = coerce_value(raw, &schema.columns[idx])?;
830                    }
831                    // Apply column defaults for the insert path, same as a plain
832                    // insert (applied before the required-column check).
833                    let defaults = self.catalog.column_defaults(table).unwrap_or(&[]);
834                    for (i, slot) in values.iter_mut().enumerate() {
835                        if slot.is_empty() {
836                            if let Some(Some(d)) = defaults.get(i) {
837                                *slot = d.clone();
838                            }
839                        }
840                    }
841                    for col in &schema.columns {
842                        if col.required && matches!(values[col.position as usize], Value::Empty) {
843                            return Err(QueryError::Execution(format!(
844                                "column '{}' is required but no value was provided",
845                                col.name
846                            )));
847                        }
848                    }
849                    let key_idx = schema
850                        .column_index(key_column)
851                        .ok_or_else(|| format!("key column '{key_column}' not found"))?;
852                    (values, key_idx)
853                };
854
855                // Upsert requires the `on` column to be unique — otherwise
856                // there is no well-defined row to overwrite and a plain
857                // insert could silently create duplicate keys.
858                if self.catalog.is_index_unique(table, key_column) != Some(true) {
859                    return Err(QueryError::Execution(format!(
860                        "upsert on .{key_column} requires a unique column (declare it with \
861                         `unique {key_column}: <type>` or `alter {table} add unique .{key_column}`)"
862                    )));
863                }
864
865                let key_value = values[key_idx].clone();
866
867                // Probe the unique index for a conflict.
868                let existing = {
869                    let tbl = self
870                        .catalog
871                        .get_table(table)
872                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
873                    // The key column is guaranteed unique above, so this
874                    // returns at most one matching row.
875                    let rids = tbl.index_lookup_all(key_column, &key_value);
876                    // Overflow safety (P0-3): reassemble via `tbl.get` so an
877                    // upsert conflict row with a spilled column is read in full.
878                    rids.into_iter()
879                        .next()
880                        .and_then(|rid| tbl.get(rid).map(|row| (rid, row)))
881                };
882
883                if let Some((rid, mut existing_row)) = existing {
884                    // Conflict: apply on_conflict assignments (or all non-key if empty).
885                    let update_assignments = if on_conflict.is_empty() {
886                        assignments
887                    } else {
888                        on_conflict
889                    };
890                    let changed_cols: Vec<usize> = {
891                        let schema = self
892                            .catalog
893                            .schema(table)
894                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
895                        let mut indices = Vec::new();
896                        for a in update_assignments {
897                            let idx = schema.column_index(&a.field).ok_or_else(|| {
898                                QueryError::ColumnNotFound {
899                                    table: String::new(),
900                                    column: a.field.clone(),
901                                }
902                            })?;
903                            if idx != key_idx {
904                                // Coerce to the target column type, same as the
905                                // UPDATE and INSERT paths — an int→float literal
906                                // here would otherwise persist as raw i64 bits
907                                // (#118 corruption on the upsert conflict path).
908                                existing_row[idx] =
909                                    coerce_value(literal_to_value(&a.value)?, &schema.columns[idx])
910                                        .map_err(QueryError::TypeError)?;
911                                indices.push(idx);
912                            }
913                        }
914                        indices
915                    };
916                    self.catalog
917                        .update_hinted(table, rid, &existing_row, Some(&changed_cols))
918                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
919                    self.view_registry.mark_dependents_dirty(table);
920                    Ok(QueryResult::Modified(1))
921                } else {
922                    // No conflict: insert.
923                    self.catalog
924                        .insert(table, &values)
925                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
926                    self.view_registry.mark_dependents_dirty(table);
927                    Ok(QueryResult::Modified(1))
928                }
929            }
930
931            PlanNode::Update {
932                input,
933                table,
934                assignments,
935                returning,
936            } => {
937                // Mission C Phase 3: resolve assignments against a borrowed
938                // schema, then drop the borrow before the mutation loop.
939                // Try literal-only path first; fall back to per-row expression
940                // evaluation if any assignment contains a non-literal expression
941                // (e.g., `age := .age + 1`).
942                let (col_indices, literal_vals, target_cols): (
943                    Vec<usize>,
944                    Option<Vec<Value>>,
945                    Vec<ColumnDef>,
946                ) = {
947                    let schema_ref = self
948                        .catalog
949                        .schema(table)
950                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
951                    let indices: Vec<usize> = assignments
952                        .iter()
953                        .map(|a| {
954                            schema_ref.column_index(&a.field).ok_or_else(|| {
955                                QueryError::ColumnNotFound {
956                                    table: String::new(),
957                                    column: a.field.clone(),
958                                }
959                            })
960                        })
961                        .collect::<Result<_, _>>()?;
962                    // The target column defs (aligned with `assignments`), owned
963                    // so the per-row expression path can coerce without holding a
964                    // catalog borrow across the mutation loop.
965                    let target_cols: Vec<ColumnDef> = indices
966                        .iter()
967                        .map(|&idx| schema_ref.columns[idx].clone())
968                        .collect();
969                    // Resolve each assignment to a literal value. If any is a
970                    // non-literal expression, fall back (None) to the per-row
971                    // expression-eval path below.
972                    let raw_vals: Result<Vec<Value>, _> = assignments
973                        .iter()
974                        .map(|a| literal_to_value(&a.value))
975                        .collect();
976                    // Coerce each literal to its target column's declared type
977                    // before it can reach the byte-patch fast path (the same
978                    // coercion the INSERT path applies). Without this, an int
979                    // assigned to a float column is written as raw i64 bits
980                    // (#118 silent corruption) and a str assigned to a
981                    // fixed-size column reaches `unreachable!` and aborts the
982                    // whole server (#117 remote DoS). A genuine type mismatch
983                    // is a hard error to the client, not an expr-path fallback.
984                    let coerced = match raw_vals {
985                        Ok(raws) => {
986                            let mut out = Vec::with_capacity(raws.len());
987                            for (raw, &idx) in raws.into_iter().zip(indices.iter()) {
988                                out.push(
989                                    coerce_value(raw, &schema_ref.columns[idx])
990                                        .map_err(QueryError::TypeError)?,
991                                );
992                            }
993                            Some(out)
994                        }
995                        Err(_) => None,
996                    };
997                    (indices, coerced, target_cols)
998                };
999                let resolved_assignments: Option<Vec<(usize, Value)>> =
1000                    literal_vals.map(|vals| col_indices.iter().copied().zip(vals).collect());
1001
1002                // Mission C Phase 2: the hint Table::update_hinted needs to
1003                // decide whether to read the old row for index diff.
1004                let changed_cols: Vec<usize> = col_indices.clone();
1005
1006                // ── RETURNING path ──────────────────────────────────────
1007                // `returning` materializes the post-update row image, so the
1008                // byte-patch / fused fast paths (which never decode a row)
1009                // can't serve it. Take the generic decode→mutate→collect
1010                // route. Opt-in only: when `returning` is false every path
1011                // below is byte-for-byte unchanged.
1012                if *returning {
1013                    let columns: Vec<String> = {
1014                        let schema_ref = self
1015                            .catalog
1016                            .schema(table)
1017                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1018                        schema_ref.columns.iter().map(|c| c.name.clone()).collect()
1019                    };
1020                    let matching_rids = self.collect_rids_for_mutation(input, table)?;
1021                    let mut out_rows: Vec<Vec<Value>> = Vec::with_capacity(matching_rids.len());
1022                    // Cancellation is safe while collecting the target set, but
1023                    // once row writes start this executor has no statement-level
1024                    // savepoint. Check at the mutation boundary and then apply the
1025                    // full set without mid-loop cancellation; returning an error
1026                    // after a logged prefix would violate statement atomicity and
1027                    // is especially unsafe inside an explicit transaction.
1028                    crate::cancel::check()?;
1029                    for rid in matching_rids {
1030                        let mut row = match self.catalog.get(table, rid) {
1031                            Some(r) => r,
1032                            None => continue,
1033                        };
1034                        match &resolved_assignments {
1035                            // Literal path: apply the pre-coerced values.
1036                            Some(resolved) => {
1037                                for (idx, val) in resolved.iter() {
1038                                    row[*idx] = val.clone();
1039                                }
1040                            }
1041                            // Expression path: evaluate each RHS against the
1042                            // (progressively mutated) row, then coerce to the
1043                            // target column type before writing — same guard the
1044                            // literal path gets, matching the non-returning expr
1045                            // path exactly (#117/#118 on computed assignments).
1046                            None => {
1047                                for (i, asgn) in assignments.iter().enumerate() {
1048                                    let val = eval_expr(&asgn.value, &row, &columns);
1049                                    row[col_indices[i]] = coerce_value(val, &target_cols[i])
1050                                        .map_err(QueryError::TypeError)?;
1051                                }
1052                            }
1053                        }
1054                        self.catalog
1055                            .update_hinted(table, rid, &row, Some(&changed_cols))
1056                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
1057                        out_rows.push(row);
1058                    }
1059                    self.view_registry.mark_dependents_dirty(table);
1060                    return Ok(QueryResult::Rows {
1061                        columns,
1062                        rows: out_rows,
1063                    });
1064                }
1065
1066                // ── Fused scan+update for Update(Filter(SeqScan)) ────────
1067                // Perf sprint: instead of the two-pass collect-RIDs-then-loop
1068                // pattern (which pays one ensure_hot per matched row on the
1069                // second pass), fuse the predicate evaluation and in-place
1070                // byte-level mutation into a single heap walk. Same idea as
1071                // the fused scan_delete_matching path for deletes.
1072                if let Some(ref resolved_assignments) = resolved_assignments {
1073                    if let PlanNode::Filter {
1074                        input: inner,
1075                        predicate,
1076                    } = input.as_ref()
1077                    {
1078                        if let PlanNode::SeqScan { table: t } = inner.as_ref() {
1079                            if t == table {
1080                                // The fused primitive mutates during its scan and
1081                                // cannot roll back a cancelled prefix. Honor an
1082                                // already-triggered token before entering it, then
1083                                // let the primitive finish atomically from the
1084                                // query layer's perspective.
1085                                crate::cancel::check()?;
1086                                let fused_result = self.try_fused_scan_update(
1087                                    table,
1088                                    predicate,
1089                                    resolved_assignments,
1090                                    &changed_cols,
1091                                );
1092                                if let Some(result) = fused_result {
1093                                    return result;
1094                                }
1095                            }
1096                        }
1097                    }
1098                }
1099
1100                // Collect matching RowIds in a single pass.
1101                let matching_rids = self.collect_rids_for_mutation(input, table)?;
1102                // This is the last cancellable boundary before any row is
1103                // changed. Mutation loops below deliberately do not poll.
1104                crate::cancel::check()?;
1105
1106                // ── Literal-only fast paths ─────────────────────────────
1107                if let Some(ref resolved_assignments) = resolved_assignments {
1108                    // Mission C Phase 4: in-place byte-patch fast path. If every
1109                    // assignment targets a fixed-size non-null column AND none of
1110                    // them is indexed, we can skip decode_row / Vec<Value> /
1111                    // encode_row_into entirely and patch the row's raw bytes on
1112                    // the hot page.
1113                    let fast_patch: Option<Vec<FastPatch>> = {
1114                        let tbl = self
1115                            .catalog
1116                            .get_table(table)
1117                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1118                        let schema = tbl.schema();
1119                        // Overflow safety (P0): byte-patching a v2 row with v1
1120                        // offsets corrupts it. Overflow tables take the generic
1121                        // reassembling `get` + `update_hinted` path below.
1122                        let all_fixed_nonnull = !tbl.has_overflow_rows()
1123                            && resolved_assignments.iter().all(|(idx, val)| {
1124                                is_fixed_size(schema.columns[*idx].type_id) && !val.is_empty()
1125                            });
1126                        let no_indexed = !resolved_assignments
1127                            .iter()
1128                            .any(|(idx, _)| tbl.has_indexed_col(*idx));
1129
1130                        if all_fixed_nonnull && no_indexed {
1131                            let layout = RowLayout::new(schema);
1132                            let bitmap_size = layout.bitmap_size();
1133                            let patches: Vec<FastPatch> = resolved_assignments
1134                                .iter()
1135                                .map(|(idx, val)| {
1136                                    let fixed_off = layout
1137                                        .fixed_offset(*idx)
1138                                        .expect("is_fixed_size already checked");
1139                                    let field_off = 2 + bitmap_size + fixed_off;
1140                                    let bytes: FixedBytes = match val {
1141                                        Value::Int(v) => FixedBytes::I64(v.to_le_bytes()),
1142                                        Value::Float(v) => FixedBytes::F64(v.to_le_bytes()),
1143                                        Value::Bool(v) => FixedBytes::Bool(if *v { 1 } else { 0 }),
1144                                        Value::DateTime(v) => FixedBytes::I64(v.to_le_bytes()),
1145                                        Value::Uuid(v) => FixedBytes::Uuid(*v),
1146                                        _ => unreachable!("all_fixed_nonnull guard lied"),
1147                                    };
1148                                    FastPatch {
1149                                        field_off,
1150                                        bitmap_byte_off: 2 + idx / 8,
1151                                        bit_mask: 1u8 << (idx % 8),
1152                                        bytes,
1153                                    }
1154                                })
1155                                .collect();
1156                            Some(patches)
1157                        } else {
1158                            None
1159                        }
1160                    };
1161
1162                    if let Some(patches) = fast_patch {
1163                        let mut count = 0u64;
1164                        let mut fallback_rids: Vec<RowId> = Vec::new();
1165                        for rid in &matching_rids {
1166                            // Mission B2: WAL-log every patch so crash
1167                            // recovery replays the update. Same mutation
1168                            // closure as before — the wrapper just sandwiches
1169                            // it between a hot-page read and a WAL append.
1170                            //
1171                            // A false return means the byte-patch was refused
1172                            // (e.g. a v2/overflow row whose in-place layout the
1173                            // fast path cannot compute, reachable on a legacy
1174                            // heap where has_overflow_rows() under-reports). Do
1175                            // NOT drop the row: push it to `fallback_rids` and
1176                            // let the reassembling get + update_hinted path
1177                            // apply it, mirroring the var-column fast path
1178                            // below. The fast path is thus a pure optimization
1179                            // that can never silently lose an update.
1180                            let ok = self
1181                                .catalog
1182                                .update_row_bytes_logged(table, *rid, |row| {
1183                                    let base = row_body_base(row);
1184                                    for p in &patches {
1185                                        row[base + p.bitmap_byte_off] &= !p.bit_mask;
1186                                        let field_bytes = p.bytes.as_slice();
1187                                        row[base + p.field_off
1188                                            ..base + p.field_off + field_bytes.len()]
1189                                            .copy_from_slice(field_bytes);
1190                                    }
1191                                })
1192                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
1193                            if ok {
1194                                count += 1;
1195                            } else {
1196                                fallback_rids.push(*rid);
1197                            }
1198                        }
1199                        for rid in fallback_rids {
1200                            let mut row = match self.catalog.get(table, rid) {
1201                                Some(r) => r,
1202                                None => continue,
1203                            };
1204                            for (idx, val) in resolved_assignments.iter() {
1205                                row[*idx] = val.clone();
1206                            }
1207                            self.catalog
1208                                .update_hinted(table, rid, &row, Some(&changed_cols))
1209                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
1210                            count += 1;
1211                        }
1212                        self.view_registry.mark_dependents_dirty(table);
1213                        return Ok(QueryResult::Modified(count));
1214                    }
1215
1216                    // Mission C Phase 10: var-column in-place shrink fast path.
1217                    let var_fast: Option<(usize, Option<Vec<u8>>)> = {
1218                        let tbl = self
1219                            .catalog
1220                            .get_table(table)
1221                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1222                        let schema = tbl.schema();
1223                        // Overflow safety (P0/P0-2): the in-place var shrink
1224                        // patch computes v1 offsets — never on a v2-capable
1225                        // table. Falls through to the reassembling path.
1226                        let is_single = resolved_assignments.len() == 1 && !tbl.has_overflow_rows();
1227                        let is_var_col = is_single
1228                            && !is_fixed_size(schema.columns[resolved_assignments[0].0].type_id);
1229                        let no_indexed = !resolved_assignments
1230                            .iter()
1231                            .any(|(idx, _)| tbl.has_indexed_col(*idx));
1232
1233                        if is_single && is_var_col && no_indexed {
1234                            let (idx, val) = &resolved_assignments[0];
1235                            let bytes_opt: Option<Vec<u8>> = match val {
1236                                Value::Str(s) => Some(s.as_bytes().to_vec()),
1237                                Value::Bytes(b) => Some(b.clone()),
1238                                // A json column stores its PJ1 bytes as the var
1239                                // payload (u32 length prefix + bytes, like Bytes),
1240                                // so the in-place patch writes them verbatim.
1241                                Value::Json(b) => Some(b.to_vec()),
1242                                Value::Empty => None,
1243                                _ => {
1244                                    return Err(QueryError::TypeError(format!(
1245                                        "cannot assign non-var value to var column '{}'",
1246                                        schema.columns[*idx].name
1247                                    )))
1248                                }
1249                            };
1250                            Some((*idx, bytes_opt))
1251                        } else {
1252                            None
1253                        }
1254                    };
1255
1256                    if let Some((col_idx, new_bytes_opt)) = var_fast {
1257                        let new_bytes_ref: Option<&[u8]> = new_bytes_opt.as_deref();
1258                        let mut count = 0u64;
1259                        let mut fallback_rids: Vec<RowId> = Vec::new();
1260                        for rid in &matching_rids {
1261                            // Mission B2: logged variant so crash recovery
1262                            // replays the shrink. On a false return (row
1263                            // would have to grow), the rid is pushed to
1264                            // `fallback_rids` and the slower `update_hinted`
1265                            // path — which is already WAL-logged — picks it up.
1266                            let ok = self
1267                                .catalog
1268                                .patch_var_col_logged(table, *rid, col_idx, new_bytes_ref)
1269                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
1270                            if ok {
1271                                count += 1;
1272                            } else {
1273                                fallback_rids.push(*rid);
1274                            }
1275                        }
1276                        for rid in fallback_rids {
1277                            let mut row = match self.catalog.get(table, rid) {
1278                                Some(r) => r,
1279                                None => continue,
1280                            };
1281                            for (idx, val) in resolved_assignments.iter() {
1282                                row[*idx] = val.clone();
1283                            }
1284                            self.catalog
1285                                .update_hinted(table, rid, &row, Some(&changed_cols))
1286                                .map_err(|e| QueryError::StorageError(e.to_string()))?;
1287                            count += 1;
1288                        }
1289                        self.view_registry.mark_dependents_dirty(table);
1290                        return Ok(QueryResult::Modified(count));
1291                    }
1292
1293                    // Generic literal path: decode row, apply literal values.
1294                    let mut count = 0u64;
1295                    for rid in matching_rids {
1296                        let mut row = match self.catalog.get(table, rid) {
1297                            Some(r) => r,
1298                            None => continue,
1299                        };
1300                        for (idx, val) in resolved_assignments.iter() {
1301                            row[*idx] = val.clone();
1302                        }
1303                        self.catalog
1304                            .update_hinted(table, rid, &row, Some(&changed_cols))
1305                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
1306                        count += 1;
1307                    }
1308                    self.view_registry.mark_dependents_dirty(table);
1309                    return Ok(QueryResult::Modified(count));
1310                } // end if let Some(resolved_assignments)
1311
1312                // ── Expression-based update path ────────────────────────
1313                // At least one assignment contains a non-literal expression
1314                // (e.g., `age := .age + 1`). Evaluate per-row.
1315                let col_names: Vec<String> = {
1316                    let schema_ref = self
1317                        .catalog
1318                        .schema(table)
1319                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1320                    schema_ref.columns.iter().map(|c| c.name.clone()).collect()
1321                };
1322                let mut count = 0u64;
1323                for rid in matching_rids {
1324                    let mut row = match self.catalog.get(table, rid) {
1325                        Some(r) => r,
1326                        None => continue,
1327                    };
1328                    for (i, asgn) in assignments.iter().enumerate() {
1329                        let val = eval_expr(&asgn.value, &row, &col_names);
1330                        // Coerce to the target column type before writing, so a
1331                        // computed int→float assignment stores f64 (not raw i64
1332                        // bits, #118) and a str→fixed-col assignment returns a
1333                        // typed error instead of hitting the encoder's
1334                        // `unreachable!` and aborting the process (#117).
1335                        row[col_indices[i]] =
1336                            coerce_value(val, &target_cols[i]).map_err(QueryError::TypeError)?;
1337                    }
1338                    self.catalog
1339                        .update_hinted(table, rid, &row, Some(&changed_cols))
1340                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1341                    count += 1;
1342                }
1343                self.view_registry.mark_dependents_dirty(table);
1344                Ok(QueryResult::Modified(count))
1345            }
1346
1347            PlanNode::Delete {
1348                input,
1349                table,
1350                returning,
1351            } => {
1352                // ── RETURNING path ──────────────────────────────────────
1353                // `returning` needs the pre-delete row image, so read each
1354                // matched row before removing it. The fused single-pass
1355                // delete primitives below never decode rows, so they can't
1356                // serve this. Opt-in only: when `returning` is false the
1357                // fast paths below are byte-for-byte unchanged.
1358                if *returning {
1359                    let columns: Vec<String> = {
1360                        let schema_ref = self
1361                            .catalog
1362                            .schema(table)
1363                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1364                        schema_ref.columns.iter().map(|c| c.name.clone()).collect()
1365                    };
1366                    let matching_rids = self.collect_rids_for_mutation(input, table)?;
1367                    let mut out_rows: Vec<Vec<Value>> = Vec::with_capacity(matching_rids.len());
1368                    // Cooperative cancellation of the pre-delete image read. The
1369                    // actual removal below is a single batched `delete_many`, so
1370                    // cancelling here happens before any row is deleted.
1371                    let mut cancel = CancelCheck::new();
1372                    for rid in &matching_rids {
1373                        cancel.tick()?;
1374                        if let Some(row) = self.catalog.get(table, *rid) {
1375                            out_rows.push(row);
1376                        }
1377                    }
1378                    crate::cancel::check()?;
1379                    self.catalog
1380                        .delete_many(table, &matching_rids)
1381                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1382                    self.view_registry.mark_dependents_dirty(table);
1383                    return Ok(QueryResult::Rows {
1384                        columns,
1385                        rows: out_rows,
1386                    });
1387                }
1388
1389                // Mission C Phase 3: no schema clone — collect_rids_for_mutation
1390                // looks up schema internally when it needs one, and the mutation
1391                // loop doesn't need the schema at all.
1392                //
1393                // Mission C Phase 12: route bulk deletes through
1394                // `Catalog::delete_many`, which batches the btree leaf
1395                // compaction and shares one `ensure_hot` per row between
1396                // the index-key extraction and the slot delete. On
1397                // `delete_by_filter` (100K fixture, ~20K matches) that
1398                // removes ~4ms of pure `Vec::remove` memmove from the btree
1399                // maintenance phase.
1400                //
1401                // Mission C Phase 16: for the common `delete where ...`
1402                // shape (Filter(SeqScan)) — and the rarer "delete
1403                // everything" shape (SeqScan) — skip the two-pass
1404                // `collect_rids_for_mutation` + `delete_many` flow entirely.
1405                // The fused `scan_delete_matching` primitive walks the
1406                // heap exactly once, paying one `ensure_hot` per page
1407                // instead of per-row. That closes the last major gap on
1408                // the bench's `delete_by_filter` workload.
1409                // Overflow safety (P1): a v2-capable table cannot take the fused
1410                // raw-byte delete — the compiled predicate mis-reads spilled
1411                // columns. Route it through the reassembling collect-rids path.
1412                let delete_overflow = self.catalog.table_has_overflow(table);
1413                if let PlanNode::Filter {
1414                    input: inner,
1415                    predicate,
1416                } = input.as_ref()
1417                {
1418                    if let PlanNode::SeqScan { table: t } = inner.as_ref() {
1419                        if t == table && !delete_overflow {
1420                            let schema = self
1421                                .catalog
1422                                .schema(table)
1423                                .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1424                            let columns: Vec<String> =
1425                                schema.columns.iter().map(|c| c.name.clone()).collect();
1426                            let fast = FastLayout::new(schema);
1427                            if let Some(compiled) =
1428                                compile_predicate(predicate, &columns, &fast, schema)
1429                            {
1430                                // Mission B2: logged variant so every
1431                                // matched rid hits the WAL during the
1432                                // single-pass scan. Structure of the
1433                                // fused scan is unchanged — only the
1434                                // hook closure now also appends.
1435                                crate::cancel::check()?;
1436                                let count = self
1437                                    .catalog
1438                                    .scan_delete_matching_logged(table, |data| compiled(data))
1439                                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
1440                                self.view_registry.mark_dependents_dirty(table);
1441                                return Ok(QueryResult::Modified(count));
1442                            }
1443                        }
1444                    }
1445                } else if let PlanNode::SeqScan { table: t } = input.as_ref() {
1446                    if t == table && !delete_overflow {
1447                        // `delete from T` with no predicate — every live
1448                        // row matches. One pass is still the right shape.
1449                        // Mission B2: logged variant — see above.
1450                        crate::cancel::check()?;
1451                        let count = self
1452                            .catalog
1453                            .scan_delete_matching_logged(table, |_| true)
1454                            .map_err(|e| QueryError::StorageError(e.to_string()))?;
1455                        self.view_registry.mark_dependents_dirty(table);
1456                        return Ok(QueryResult::Modified(count));
1457                    }
1458                }
1459
1460                let matching_rids = self.collect_rids_for_mutation(input, table)?;
1461                crate::cancel::check()?;
1462                let count = self
1463                    .catalog
1464                    .delete_many(table, &matching_rids)
1465                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
1466                self.view_registry.mark_dependents_dirty(table);
1467                Ok(QueryResult::Modified(count))
1468            }
1469
1470            PlanNode::NestedProject { input, fields } => {
1471                // Auto-refresh dirty materialized views among the child
1472                // tables (at every nesting level) before the read-only
1473                // assembly runs.
1474                let mut child_tables = Vec::new();
1475                for field in fields {
1476                    if let NestedProjectField::Nested(nested) = field {
1477                        nested.visit_tables(&mut |table| child_tables.push(table.to_string()));
1478                    }
1479                }
1480                for table in child_tables {
1481                    if self.view_registry.is_dirty(&table) {
1482                        self.refresh_view(&table)?;
1483                    }
1484                }
1485                let parent = self.execute_plan(input)?;
1486                self.execute_nested_project(parent, fields)
1487            }
1488
1489            PlanNode::AliasScan { table, alias } => {
1490                // Mission E1.2: scan `table` and rename every output column
1491                // to `alias.field`. Used as a join leaf so downstream
1492                // NestedLoopJoin + Filter + Project nodes can resolve
1493                // `Expr::QualifiedField` lookups by direct column-name match.
1494                //
1495                // We don't bother with a fused zero-copy loop here yet — the
1496                // whole join path is nested-loop and correctness-first
1497                // (Phase E1.3 will introduce hash join and at that point we
1498                // can revisit whether to specialise AliasScan).
1499                let schema = self
1500                    .catalog
1501                    .schema(table)
1502                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
1503                    .clone();
1504                let columns: Vec<String> = schema
1505                    .columns
1506                    .iter()
1507                    .map(|c| format!("{alias}.{}", c.name))
1508                    .collect();
1509                let mut cancel = CancelCheck::new();
1510                let mut rows: Vec<Vec<Value>> = Vec::new();
1511                for (_, row) in self
1512                    .catalog
1513                    .scan(table)
1514                    .map_err(|e| QueryError::StorageError(e.to_string()))?
1515                {
1516                    cancel.tick()?;
1517                    rows.push(row);
1518                }
1519                Ok(QueryResult::Rows { columns, rows })
1520            }
1521
1522            PlanNode::NestedLoopJoin {
1523                left,
1524                right,
1525                on,
1526                kind,
1527            } => {
1528                // Materialise both sides. The executor ships two strategies:
1529                //   1. Hash join (E1.3) — when the `on` predicate is a
1530                //      simple equi-predicate `left_col = right_col`, build a
1531                //      FxHashMap<Value, Vec<row_idx>> over the right side
1532                //      and probe with the left side. O(L + R) instead of
1533                //      O(L × R). Handles Inner and LeftOuter.
1534                //   2. Nested loop (E1.2) — fallback for Cross, non-equi
1535                //      predicates, or `on` expressions that reference
1536                //      either side with something more complex than a
1537                //      QualifiedField.
1538                let left_result = self.execute_plan(left)?;
1539                let right_result = self.execute_plan(right)?;
1540                let (left_columns, left_rows) = match left_result {
1541                    QueryResult::Rows { columns, rows } => (columns, rows),
1542                    _ => return Err("join left side must produce rows".into()),
1543                };
1544                let (right_columns, right_rows) = match right_result {
1545                    QueryResult::Rows { columns, rows } => (columns, rows),
1546                    _ => return Err("join right side must produce rows".into()),
1547                };
1548
1549                // WS2: byte-budget guard on the join build side. Charge both
1550                // materialized inputs before we build the hash table / probe;
1551                // the output is row-capped by check_join_limit below.
1552                self.charge_rows(&left_rows)?;
1553                self.charge_rows(&right_rows)?;
1554
1555                execute_materialized_join(
1556                    left_columns,
1557                    left_rows,
1558                    right_columns,
1559                    right_rows,
1560                    on.as_ref(),
1561                    *kind,
1562                    self.nested_loop_pair_limit,
1563                )
1564            }
1565
1566            PlanNode::Distinct { input } => {
1567                let result = self.execute_plan(input)?;
1568                match result {
1569                    QueryResult::Rows { columns, rows } => {
1570                        let mut seen = std::collections::HashSet::new();
1571                        let mut unique_rows = Vec::new();
1572                        let mut cancel = CancelCheck::new();
1573                        for row in rows {
1574                            cancel.tick()?;
1575                            if seen.insert(row.clone()) {
1576                                unique_rows.push(row);
1577                            }
1578                        }
1579                        Ok(QueryResult::Rows {
1580                            columns,
1581                            rows: unique_rows,
1582                        })
1583                    }
1584                    other => Ok(other),
1585                }
1586            }
1587
1588            PlanNode::GroupBy {
1589                input,
1590                keys,
1591                aggregates,
1592                having,
1593            } => {
1594                if aggregates
1595                    .iter()
1596                    .any(|aggregate| aggregate.provenance_alias.is_some())
1597                {
1598                    let input = self.materialize_rows_with_provenance(input)?;
1599                    self.charge_rows(&input.rows)?;
1600                    return exec_group_by_with_provenance(
1601                        input,
1602                        keys,
1603                        aggregates,
1604                        having,
1605                        self.query_memory_limit(),
1606                    );
1607                }
1608                let result = self.execute_plan(input)?;
1609                match result {
1610                    QueryResult::Rows { columns, rows } => {
1611                        // WS2: byte-budget guard on the GROUP BY input buffer
1612                        // (the hash table is bounded by the input it groups).
1613                        self.charge_rows(&rows)?;
1614                        exec_group_by(columns, rows, keys, aggregates, having)
1615                    }
1616                    _ => Err("group by requires row input".into()),
1617                }
1618            }
1619
1620            PlanNode::CreateTable {
1621                name,
1622                fields,
1623                if_not_exists,
1624            } => {
1625                // Idempotency: a re-declared type is a clean no-op under
1626                // `if not exists`, and otherwise a PowQL-flavored error that
1627                // names the type (not the storage layer's generic "table").
1628                if self.catalog.schema(name).is_some() {
1629                    if *if_not_exists {
1630                        return Ok(QueryResult::Executed {
1631                            message: format!("type '{name}' already exists (skipped)"),
1632                        });
1633                    }
1634                    // "cannot" prefix keeps this on the server's
1635                    // safe-to-forward allowlist (SAFE_ERROR_PREFIXES).
1636                    return Err(QueryError::Execution(format!(
1637                        "cannot create type '{name}': it already exists"
1638                    )));
1639                }
1640                let columns: Vec<ColumnDef> = fields
1641                    .iter()
1642                    .enumerate()
1643                    .map(|(i, f)| -> Result<ColumnDef, QueryError> {
1644                        Ok(ColumnDef {
1645                            name: f.name.clone(),
1646                            type_id: type_name_to_id(&f.type_name)
1647                                .map_err(QueryError::TypeError)?,
1648                            required: f.required,
1649                            position: i as u16,
1650                        })
1651                    })
1652                    .collect::<Result<Vec<_>, _>>()?;
1653                // Coerce each literal default to its column's type now, so a
1654                // type mismatch (`count: int default "x"`) is rejected at DDL
1655                // time and the stored default is ready to drop into inserts.
1656                let mut defaults: Vec<Option<Value>> = vec![None; columns.len()];
1657                let mut auto_cols: Vec<bool> = vec![false; columns.len()];
1658                for (i, f) in fields.iter().enumerate() {
1659                    if let Some(lit) = &f.default {
1660                        let raw = literal_value_from(lit);
1661                        defaults[i] = Some(coerce_value(raw, &columns[i])?);
1662                    }
1663                    if f.auto {
1664                        // Auto-increment only makes sense on an integer column,
1665                        // and combining it with a literal default is
1666                        // contradictory (both want to supply the value).
1667                        if columns[i].type_id != TypeId::Int {
1668                            return Err(QueryError::TypeError(format!(
1669                                "auto column '{}' must be of type int",
1670                                f.name
1671                            )));
1672                        }
1673                        if f.default.is_some() {
1674                            return Err(QueryError::TypeError(format!(
1675                                "auto column '{}' cannot also declare a default",
1676                                f.name
1677                            )));
1678                        }
1679                        auto_cols[i] = true;
1680                    }
1681                }
1682                let schema = Schema {
1683                    table_name: name.clone(),
1684                    columns,
1685                };
1686                self.catalog
1687                    .create_table_full(schema, defaults, auto_cols)
1688                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
1689                // Declaring a field `unique` auto-creates a unique B+tree
1690                // index, which is where uniqueness is enforced on writes.
1691                for f in fields.iter().filter(|f| f.unique) {
1692                    self.catalog
1693                        .create_index_unique(name, &f.name, true)
1694                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1695                }
1696                Ok(QueryResult::Created(name.clone()))
1697            }
1698
1699            PlanNode::AlterTable { table, action } => match action {
1700                AlterAction::AddColumn {
1701                    name,
1702                    type_name,
1703                    required,
1704                } => {
1705                    let position = self
1706                        .catalog
1707                        .schema(table)
1708                        .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
1709                        .columns
1710                        .len() as u16;
1711                    let col = ColumnDef {
1712                        name: name.clone(),
1713                        type_id: type_name_to_id(type_name).map_err(QueryError::TypeError)?,
1714                        required: *required,
1715                        position,
1716                    };
1717                    self.catalog
1718                        .alter_table_add_column(table, col)
1719                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1720                    Ok(QueryResult::Executed {
1721                        message: format!("column '{name}' added to '{table}'"),
1722                    })
1723                }
1724                AlterAction::DropColumn { name, if_exists } => {
1725                    // `if exists`: a missing column (or missing table) is a
1726                    // no-op instead of an error.
1727                    if *if_exists {
1728                        let present = self
1729                            .catalog
1730                            .schema(table)
1731                            .map(|s| s.column_index(name).is_some())
1732                            .unwrap_or(false);
1733                        if !present {
1734                            return Ok(QueryResult::Executed {
1735                                message: format!(
1736                                    "column '{name}' does not exist on '{table}' (skipped)"
1737                                ),
1738                            });
1739                        }
1740                    }
1741                    self.catalog
1742                        .alter_table_drop_column(table, name)
1743                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1744                    Ok(QueryResult::Executed {
1745                        message: format!("column '{name}' dropped from '{table}'"),
1746                    })
1747                }
1748                AlterAction::AddIndex {
1749                    target,
1750                    if_not_exists: _,
1751                } => {
1752                    let IndexTarget::Column(column) = target else {
1753                        let IndexTarget::JsonPath(path) = target else {
1754                            unreachable!("index target variants are exhaustive")
1755                        };
1756                        if let Some(existing) = resolve_expression_index(&self.catalog, table, path)
1757                        {
1758                            return Ok(QueryResult::Executed {
1759                                message: format!(
1760                                    "expression index {} on '{}' already exists (skipped)",
1761                                    existing.index_id, table
1762                                ),
1763                            });
1764                        }
1765                        crate::cancel::check()?;
1766                        let index_id = self
1767                            .catalog
1768                            .create_expression_index_metadata(
1769                                table,
1770                                1,
1771                                path.canonical_text(),
1772                                path.clone(),
1773                                false,
1774                            )
1775                            .map_err(|error| QueryError::StorageError(error.to_string()))?;
1776                        return Ok(QueryResult::Executed {
1777                            message: format!("expression index {index_id} on '{}' created", table),
1778                        });
1779                    };
1780                    // `add index` is already idempotent (no-op if the index
1781                    // exists), so `if not exists` is accepted for symmetry but
1782                    // does not change behavior.
1783                    crate::cancel::check()?;
1784                    self.catalog
1785                        .create_index(table, column)
1786                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1787                    Ok(QueryResult::Executed {
1788                        message: format!("index on '{table}.{column}' created"),
1789                    })
1790                }
1791                AlterAction::AddUnique {
1792                    target,
1793                    if_not_exists,
1794                } => {
1795                    let IndexTarget::Column(column) = target else {
1796                        let IndexTarget::JsonPath(path) = target else {
1797                            unreachable!("index target variants are exhaustive")
1798                        };
1799                        if let Some(existing) = resolve_expression_index(&self.catalog, table, path)
1800                        {
1801                            if *if_not_exists {
1802                                return Ok(QueryResult::Executed {
1803                                    message: format!(
1804                                        "expression index {} on '{}' already exists (skipped)",
1805                                        existing.index_id, table
1806                                    ),
1807                                });
1808                            }
1809                            return Err(QueryError::Execution(format!(
1810                                "cannot add unique expression index on {}: path already indexed",
1811                                table
1812                            )));
1813                        }
1814                        crate::cancel::check()?;
1815                        let index_id = self
1816                            .catalog
1817                            .create_expression_index_metadata(
1818                                table,
1819                                1,
1820                                path.canonical_text(),
1821                                path.clone(),
1822                                true,
1823                            )
1824                            .map_err(|error| QueryError::StorageError(error.to_string()))?;
1825                        return Ok(QueryResult::Executed {
1826                            message: format!(
1827                                "unique expression index {index_id} on '{}' created",
1828                                table
1829                            ),
1830                        });
1831                    };
1832                    // `if not exists`: an already-indexed column is a no-op
1833                    // rather than the (default) "already indexed" error.
1834                    if self.catalog.has_index(table, column) {
1835                        if *if_not_exists {
1836                            return Ok(QueryResult::Executed {
1837                                message: format!(
1838                                    "index on '{table}.{column}' already exists (skipped)"
1839                                ),
1840                            });
1841                        }
1842                        // Upgrading an existing non-unique index in place is
1843                        // intentionally rejected.
1844                        return Err(QueryError::Execution(format!(
1845                            "cannot add unique on {table}.{column}: column already indexed"
1846                        )));
1847                    }
1848                    // Scan existing rows for duplicate (non-null) values
1849                    // before creating the unique index.
1850                    {
1851                        let tbl = self
1852                            .catalog
1853                            .get_table(table)
1854                            .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1855                        let col_idx = tbl.schema().column_index(column).ok_or_else(|| {
1856                            QueryError::ColumnNotFound {
1857                                table: table.to_string(),
1858                                column: column.clone(),
1859                            }
1860                        })?;
1861                        let mut seen = std::collections::HashSet::new();
1862                        let mut cancel = CancelCheck::new();
1863                        for (_, row) in tbl.scan() {
1864                            cancel.tick()?;
1865                            let v = &row[col_idx];
1866                            if v.is_empty() {
1867                                continue;
1868                            }
1869                            if !seen.insert(v.clone()) {
1870                                return Err(QueryError::Execution(format!(
1871                                    "cannot add unique on {table}.{column}: \
1872                                     duplicate value {v:?} exists"
1873                                )));
1874                            }
1875                        }
1876                    }
1877                    crate::cancel::check()?;
1878                    self.catalog
1879                        .create_index_unique(table, column, true)
1880                        .map_err(|e| QueryError::StorageError(e.to_string()))?;
1881                    Ok(QueryResult::Executed {
1882                        message: format!("unique index on '{table}.{column}' created"),
1883                    })
1884                }
1885                AlterAction::DropIndex { target, if_exists } => {
1886                    let IndexTarget::JsonPath(path) = target else {
1887                        return Err(QueryError::Execution(
1888                            "dropping stored-column indexes is not supported".to_string(),
1889                        ));
1890                    };
1891                    let Some(existing) = resolve_expression_index(&self.catalog, table, path)
1892                    else {
1893                        if *if_exists {
1894                            return Ok(QueryResult::Executed {
1895                                message: format!(
1896                                    "expression index on '{}' does not exist (skipped)",
1897                                    table
1898                                ),
1899                            });
1900                        }
1901                        return Err(QueryError::Execution(format!(
1902                            "expression index on '{}' does not exist",
1903                            table
1904                        )));
1905                    };
1906                    crate::cancel::check()?;
1907                    self.catalog
1908                        .drop_expression_index(table, existing.index_id)
1909                        .map_err(|error| QueryError::StorageError(error.to_string()))?;
1910                    Ok(QueryResult::Executed {
1911                        message: format!(
1912                            "expression index {} on '{}' dropped",
1913                            existing.index_id, table
1914                        ),
1915                    })
1916                }
1917            },
1918
1919            PlanNode::DropTable { name, if_exists } => {
1920                if *if_exists && self.catalog.schema(name).is_none() {
1921                    return Ok(QueryResult::Executed {
1922                        message: format!("type '{name}' does not exist (skipped)"),
1923                    });
1924                }
1925                self.catalog
1926                    .drop_table(name)
1927                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
1928                Ok(QueryResult::Executed {
1929                    message: format!("table '{name}' dropped"),
1930                })
1931            }
1932
1933            PlanNode::ListTypes => self.introspect_list_types(),
1934
1935            PlanNode::Describe { table } => self.introspect_describe(table),
1936
1937            PlanNode::CreateView { name, query_text } => {
1938                self.create_view(name, query_text)?;
1939                Ok(QueryResult::Executed {
1940                    message: format!("materialized view '{name}' created"),
1941                })
1942            }
1943
1944            PlanNode::RefreshView { name } => {
1945                self.refresh_view(name)?;
1946                Ok(QueryResult::Executed {
1947                    message: format!("materialized view '{name}' refreshed"),
1948                })
1949            }
1950
1951            PlanNode::DropView { name, if_exists } => {
1952                if *if_exists && !self.view_registry.is_view(name) {
1953                    return Ok(QueryResult::Executed {
1954                        message: format!("view '{name}' does not exist (skipped)"),
1955                    });
1956                }
1957                self.drop_view(name)?;
1958                Ok(QueryResult::Executed {
1959                    message: format!("materialized view '{name}' dropped"),
1960                })
1961            }
1962
1963            PlanNode::Window { input, windows } => {
1964                let result = self.execute_plan(input)?;
1965                execute_window(result, windows, self.query_memory_limit)
1966            }
1967
1968            PlanNode::Union { left, right, all } => {
1969                let left_result = self.execute_plan(left)?;
1970                let right_result = self.execute_plan(right)?;
1971                let (left_cols, left_rows) = match left_result {
1972                    QueryResult::Rows { columns, rows } => (columns, rows),
1973                    _ => return Err("UNION requires query results on left side".into()),
1974                };
1975                let (_, right_rows) = match right_result {
1976                    QueryResult::Rows { columns, rows } => (columns, rows),
1977                    _ => return Err("UNION requires query results on right side".into()),
1978                };
1979                let mut combined = left_rows;
1980                let mut cancel = CancelCheck::new();
1981                if *all {
1982                    // UNION ALL — just concatenate.
1983                    for row in right_rows {
1984                        cancel.tick()?;
1985                        combined.push(row);
1986                    }
1987                } else {
1988                    // UNION — deduplicate using the same HashSet approach
1989                    // as DISTINCT. Value already implements Hash + Eq.
1990                    let mut seen = std::collections::HashSet::new();
1991                    for row in &combined {
1992                        cancel.tick()?;
1993                        seen.insert(row.clone());
1994                    }
1995                    for row in right_rows {
1996                        cancel.tick()?;
1997                        if seen.insert(row.clone()) {
1998                            combined.push(row);
1999                        }
2000                    }
2001                }
2002                Ok(QueryResult::Rows {
2003                    columns: left_cols,
2004                    rows: combined,
2005                })
2006            }
2007
2008            PlanNode::Explain { input } => {
2009                // Every execute entry point runs lower_unindexed_scans before
2010                // dispatch and lowering recurses into Explain, so `input` is
2011                // already the plan that will actually run.
2012                let text = format_plan_tree(&self.catalog, input, 0);
2013                Ok(QueryResult::Rows {
2014                    columns: vec!["plan".to_string()],
2015                    rows: text
2016                        .lines()
2017                        .map(|line| vec![Value::Str(line.to_string())])
2018                        .collect(),
2019                })
2020            }
2021
2022            PlanNode::Begin => {
2023                if self.in_transaction {
2024                    return Err(QueryError::Execution(
2025                        "already in a transaction (nested transactions not supported)".into(),
2026                    ));
2027                }
2028                self.catalog
2029                    .begin_transaction()
2030                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
2031                self.in_transaction = true;
2032                Ok(QueryResult::Executed {
2033                    message: "transaction started".to_string(),
2034                })
2035            }
2036
2037            PlanNode::Commit => {
2038                if !self.in_transaction {
2039                    return Err(QueryError::Execution(
2040                        "no active transaction to commit".into(),
2041                    ));
2042                }
2043                self.catalog
2044                    .commit_transaction()
2045                    .map_err(|e| QueryError::StorageError(e.to_string()))?;
2046                self.in_transaction = false;
2047                Ok(QueryResult::Executed {
2048                    message: "transaction committed".to_string(),
2049                })
2050            }
2051
2052            PlanNode::Rollback => {
2053                if !self.in_transaction {
2054                    return Err(QueryError::Execution(
2055                        "no active transaction to roll back".into(),
2056                    ));
2057                }
2058                self.rollback_transaction_preserving_wal_archive()
2059            }
2060
2061            PlanNode::IndexScan { table, column, key } => {
2062                let key_value = literal_to_value(key)?;
2063                let tbl = self
2064                    .catalog
2065                    .get_table(table)
2066                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
2067                let columns: Vec<String> = tbl
2068                    .schema()
2069                    .columns
2070                    .iter()
2071                    .map(|c| c.name.clone())
2072                    .collect();
2073
2074                // Fast path: the table has a B-tree on this column.
2075                // Uses index_lookup_all to return ALL matching rows for
2076                // both unique and non-unique indexes.
2077                if tbl.has_index(column) {
2078                    let rids = tbl.index_lookup_all(column, &key_value);
2079                    let mut rows: Vec<Vec<Value>> = Vec::with_capacity(rids.len());
2080                    let mut cancel = CancelCheck::new();
2081                    for rid in rids {
2082                        cancel.tick()?;
2083                        // Overflow safety (P0-3/P0-4): `tbl.get` reassembles
2084                        // spilled columns; the old `heap.get` + `decode_row`
2085                        // returned Empty / wrapped a >= 64KB value.
2086                        if let Some(row) = tbl.get(rid) {
2087                            rows.push(row);
2088                        }
2089                    }
2090                    return Ok(QueryResult::Rows { columns, rows });
2091                }
2092
2093                // Fallback: no index on this column. The planner emits IndexScan
2094                // eagerly (it has no visibility into which columns are indexed
2095                // at plan time), so here we must behave like SeqScan+Filter on
2096                // `.col = literal`: return *all* matching rows, not just the
2097                // first one. A non-indexed column isn't necessarily unique.
2098                // We compile the eq predicate once and stream without any
2099                // per-row decode for non-matching rows.
2100                let schema = tbl.schema();
2101                let fast = FastLayout::new(schema);
2102                let synth_pred = Expr::BinaryOp(
2103                    Box::new(Expr::Field(column.clone())),
2104                    BinOp::Eq,
2105                    Box::new(key.clone()),
2106                );
2107                // Overflow safety (P0-4/P1): the raw compiled scan drops/mis-reads
2108                // spilled columns; a v2-capable table uses the decoded scan below.
2109                if !tbl.has_overflow_rows() {
2110                    if let Some(compiled) = compile_predicate(&synth_pred, &columns, &fast, schema)
2111                    {
2112                        // Mission F: skip the first 4 Vec doublings.
2113                        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
2114                        for_each_row_raw_cancellable(&self.catalog, table, |_rid, data| {
2115                            if compiled(data) {
2116                                rows.push(decode_row(schema, data));
2117                            }
2118                        })?;
2119                        return Ok(QueryResult::Rows { columns, rows });
2120                    }
2121                }
2122
2123                // Last resort: slow eq-check on materialised rows.
2124                let col_idx =
2125                    schema
2126                        .column_index(column)
2127                        .ok_or_else(|| QueryError::ColumnNotFound {
2128                            table: String::new(),
2129                            column: column.clone(),
2130                        })?;
2131                let mut cancel = CancelCheck::new();
2132                let mut rows: Vec<Vec<Value>> = Vec::new();
2133                for (_, row) in tbl.scan() {
2134                    cancel.tick()?;
2135                    if row[col_idx] == key_value {
2136                        rows.push(row);
2137                    }
2138                }
2139                Ok(QueryResult::Rows { columns, rows })
2140            }
2141
2142            PlanNode::RangeScan {
2143                table,
2144                column,
2145                start,
2146                end,
2147            } => {
2148                let tbl = self
2149                    .catalog
2150                    .get_table(table)
2151                    .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
2152                let columns: Vec<String> = tbl
2153                    .schema()
2154                    .columns
2155                    .iter()
2156                    .map(|c| c.name.clone())
2157                    .collect();
2158                let schema = tbl.schema();
2159
2160                let start_val = match start {
2161                    Some((expr, _)) => Some(literal_to_value(expr)?),
2162                    None => None,
2163                };
2164                let end_val = match end {
2165                    Some((expr, _)) => Some(literal_to_value(expr)?),
2166                    None => None,
2167                };
2168                let start_inclusive = start.as_ref().map(|(_, inc)| *inc).unwrap_or(true);
2169                let end_inclusive = end.as_ref().map(|(_, inc)| *inc).unwrap_or(true);
2170
2171                // Non-unique index: walk the composite (value, rid) leaf
2172                // chain between prefix bounds, fetch each row from the heap,
2173                // and recheck. The recheck enforces exclusive bounds
2174                // (range_rids is inclusive) and defensively skips any decoded
2175                // null (nulls are never indexed, so they must not match).
2176                if tbl.is_index_unique(column) == Some(false) {
2177                    if let Some(btree) = tbl.index(column) {
2178                        if start_val.is_some() || end_val.is_some() {
2179                            let col_idx = schema.column_index(column).ok_or_else(|| {
2180                                QueryError::ColumnNotFound {
2181                                    table: String::new(),
2182                                    column: column.clone(),
2183                                }
2184                            })?;
2185                            let rids = btree.range_rids(start_val.as_ref(), end_val.as_ref());
2186                            let mut rows: Vec<Vec<Value>> = Vec::with_capacity(rids.len());
2187                            let mut cancel = CancelCheck::new();
2188                            for rid in rids {
2189                                cancel.tick()?;
2190                                // Overflow safety (P0-3): reassemble spilled cols.
2191                                if let Some(row) = tbl.get(rid) {
2192                                    if !row[col_idx].is_empty()
2193                                        && range_matches(
2194                                            &row[col_idx],
2195                                            &start_val,
2196                                            start_inclusive,
2197                                            &end_val,
2198                                            end_inclusive,
2199                                        )
2200                                    {
2201                                        rows.push(row);
2202                                    }
2203                                }
2204                            }
2205                            return Ok(QueryResult::Rows { columns, rows });
2206                        }
2207                    }
2208                }
2209
2210                // Range scans use the btree fast path for unique indexes,
2211                // walking raw column-value keys directly.
2212                if tbl.is_index_unique(column) == Some(true) {
2213                    if let Some(btree) = tbl.index(column) {
2214                        let hits: Vec<(Value, RowId)> = match (&start_val, &end_val) {
2215                            (Some(s), Some(e)) => btree.range(s, e).collect(),
2216                            (Some(s), None) => btree.range_from(s),
2217                            (None, Some(e)) => btree.range_to(e),
2218                            (None, None) => {
2219                                let mut cancel = CancelCheck::new();
2220                                let mut rows: Vec<Vec<Value>> = Vec::new();
2221                                for (_, row) in tbl.scan() {
2222                                    cancel.tick()?;
2223                                    rows.push(row);
2224                                }
2225                                return Ok(QueryResult::Rows { columns, rows });
2226                            }
2227                        };
2228                        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(hits.len());
2229                        let mut cancel = CancelCheck::new();
2230                        for (key, rid) in hits {
2231                            cancel.tick()?;
2232                            if !start_inclusive {
2233                                if let Some(ref s) = start_val {
2234                                    if &key == s {
2235                                        continue;
2236                                    }
2237                                }
2238                            }
2239                            if !end_inclusive {
2240                                if let Some(ref e) = end_val {
2241                                    if &key == e {
2242                                        continue;
2243                                    }
2244                                }
2245                            }
2246                            // Overflow safety (P0-3): reassemble spilled cols.
2247                            if let Some(row) = tbl.get(rid) {
2248                                rows.push(row);
2249                            }
2250                        }
2251                        return Ok(QueryResult::Rows { columns, rows });
2252                    }
2253                }
2254
2255                // Fallback: no index — synthesize range predicate and scan.
2256                // Overflow safety (P0-4): v2-capable tables use the decoded
2257                // last-resort scan below.
2258                let fast = FastLayout::new(schema);
2259                let synth = synthesize_range_predicate(column, start, end);
2260                if !tbl.has_overflow_rows() {
2261                    if let Some(compiled) = compile_predicate(&synth, &columns, &fast, schema) {
2262                        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
2263                        for_each_row_raw_cancellable(&self.catalog, table, |_rid, data| {
2264                            if compiled(data) {
2265                                rows.push(decode_row(schema, data));
2266                            }
2267                        })?;
2268                        return Ok(QueryResult::Rows { columns, rows });
2269                    }
2270                }
2271
2272                let col_idx =
2273                    schema
2274                        .column_index(column)
2275                        .ok_or_else(|| QueryError::ColumnNotFound {
2276                            table: String::new(),
2277                            column: column.clone(),
2278                        })?;
2279                let mut cancel = CancelCheck::new();
2280                let mut rows: Vec<Vec<Value>> = Vec::new();
2281                for (_, row) in tbl.scan() {
2282                    cancel.tick()?;
2283                    if range_matches(
2284                        &row[col_idx],
2285                        &start_val,
2286                        start_inclusive,
2287                        &end_val,
2288                        end_inclusive,
2289                    ) {
2290                        rows.push(row);
2291                    }
2292                }
2293                Ok(QueryResult::Rows { columns, rows })
2294            }
2295        }
2296    }
2297
2298    // ─── Materialized view operations ──────────────────────────────────────
2299
2300    /// Create a materialized view: execute the source query, store results
2301    /// in a new backing table, and register the view.
2302    fn create_view(&mut self, name: &str, query_text: &str) -> Result<(), QueryError> {
2303        if self.view_registry.is_view(name) {
2304            return Err(QueryError::ViewError(format!(
2305                "materialized view '{name}' already exists"
2306            )));
2307        }
2308        // Execute the source query to get the result set.
2309        let result = self.execute_powql(query_text)?;
2310        let (columns, rows) = match result {
2311            QueryResult::Rows { columns, rows } => (columns, rows),
2312            _ => return Err("view source query must be a SELECT".into()),
2313        };
2314        // Derive a schema for the backing table from the query result columns.
2315        let schema = self.derive_view_schema(name, &columns, &rows);
2316        // Create the backing table and insert the result rows.
2317        crate::cancel::check()?;
2318        self.catalog
2319            .create_table(schema)
2320            .map_err(|e| QueryError::StorageError(e.to_string()))?;
2321        for row in &rows {
2322            self.catalog
2323                .insert(name, row)
2324                .map_err(|e| QueryError::StorageError(e.to_string()))?;
2325        }
2326        // Determine which base tables this view depends on by parsing the query.
2327        let depends_on = self.extract_view_deps(query_text);
2328        self.view_registry
2329            .register(ViewDef {
2330                name: name.to_string(),
2331                query: query_text.to_string(),
2332                depends_on,
2333                dirty: false,
2334            })
2335            .map_err(|e| QueryError::StorageError(e.to_string()))?;
2336        Ok(())
2337    }
2338
2339    /// Refresh a materialized view: re-execute its source query and replace
2340    /// the backing table's contents.
2341    fn refresh_view(&mut self, name: &str) -> Result<(), QueryError> {
2342        let def = self
2343            .view_registry
2344            .get(name)
2345            .ok_or_else(|| format!("materialized view '{name}' not found"))?;
2346        let query_text = def.query.clone();
2347        // Execute the source query.
2348        let result = self.execute_powql(&query_text)?;
2349        let (_columns, rows) = match result {
2350            QueryResult::Rows { columns, rows } => (columns, rows),
2351            _ => return Err("view source query must be a SELECT".into()),
2352        };
2353        // Clear old data and insert fresh results. Mission B2: logged
2354        // variant — view refreshes are a mutation and crash recovery
2355        // must see them.
2356        crate::cancel::check()?;
2357        self.catalog
2358            .scan_delete_matching_logged(name, |_| true)
2359            .map_err(|e| QueryError::StorageError(e.to_string()))?;
2360        for row in &rows {
2361            self.catalog
2362                .insert(name, row)
2363                .map_err(|e| QueryError::StorageError(e.to_string()))?;
2364        }
2365        self.view_registry.mark_clean(name);
2366        Ok(())
2367    }
2368
2369    /// Drop a materialized view: remove the backing table and unregister.
2370    fn drop_view(&mut self, name: &str) -> Result<(), QueryError> {
2371        if !self.view_registry.is_view(name) {
2372            return Err(QueryError::ViewError(format!(
2373                "materialized view '{name}' not found"
2374            )));
2375        }
2376        self.view_registry
2377            .unregister(name)
2378            .map_err(|e| QueryError::StorageError(e.to_string()))?;
2379        self.catalog
2380            .drop_table(name)
2381            .map_err(|e| QueryError::StorageError(e.to_string()))?;
2382        Ok(())
2383    }
2384
2385    /// Derive a storage `Schema` for a view's backing table from query
2386    /// result column names and the first row's types.
2387    fn derive_view_schema(&self, name: &str, columns: &[String], rows: &[Vec<Value>]) -> Schema {
2388        use powdb_storage::types::{ColumnDef, TypeId};
2389        let cols: Vec<ColumnDef> = columns
2390            .iter()
2391            .enumerate()
2392            .map(|(i, col_name)| {
2393                let type_id = rows
2394                    .first()
2395                    .and_then(|row| row.get(i))
2396                    .map(|v| v.type_id())
2397                    .unwrap_or(TypeId::Str);
2398                ColumnDef {
2399                    name: col_name.clone(),
2400                    type_id,
2401                    required: false,
2402                    position: i as u16,
2403                }
2404            })
2405            .collect();
2406        Schema {
2407            table_name: name.to_string(),
2408            columns: cols,
2409        }
2410    }
2411
2412    /// Extract base table dependencies from a view's source query by
2413    /// parsing it and collecting the source table name.
2414    fn extract_view_deps(&self, query_text: &str) -> Vec<String> {
2415        use crate::parser::parse;
2416        match parse(query_text) {
2417            Ok(Statement::Query(q)) => {
2418                let mut deps = vec![q.source.clone()];
2419                for j in &q.joins {
2420                    deps.push(j.source.clone());
2421                }
2422                deps
2423            }
2424            _ => Vec::new(),
2425        }
2426    }
2427
2428    /// Execute the projection layer of a `NestedProject`: plain fields
2429    /// evaluate against the parent rows like `Project`; each nested field is
2430    /// assembled bottom-up by [`Engine::assemble_nested_arrays`], one hash
2431    /// build pass per child table keyed by its correlation column. Shared by
2432    /// the mutable and read-only dispatches (assembly only reads).
2433    pub(crate) fn execute_nested_project(
2434        &self,
2435        parent: QueryResult,
2436        fields: &[NestedProjectField],
2437    ) -> Result<QueryResult, QueryError> {
2438        use rustc_hash::FxHashMap;
2439        let QueryResult::Rows {
2440            columns: parent_columns,
2441            rows: parent_rows,
2442        } = parent
2443        else {
2444            return Err("nested projection requires row input".into());
2445        };
2446        // Per nested field: the parent-side key column index and the
2447        // assembled correlation value -> JSON array text map.
2448        let mut builds: Vec<(usize, FxHashMap<Value, String>)> = Vec::new();
2449        for field in fields {
2450            let NestedProjectField::Nested(nested) = field else {
2451                continue;
2452            };
2453            let parent_idx = parent_columns
2454                .iter()
2455                .position(|c| c == &nested.parent_key)
2456                .ok_or_else(|| {
2457                    QueryError::Execution(format!(
2458                        "nested projection `{}` outer column `{}` not found",
2459                        nested.name, nested.parent_key
2460                    ))
2461                })?;
2462            builds.push((parent_idx, self.assemble_nested_arrays(nested)?));
2463        }
2464
2465        let columns: Vec<String> = fields
2466            .iter()
2467            .map(|field| match field {
2468                NestedProjectField::Plain(f) => f
2469                    .alias
2470                    .clone()
2471                    .unwrap_or_else(|| expression_output_name(&f.expr)),
2472                NestedProjectField::Nested(nested) => nested.name.clone(),
2473            })
2474            .collect();
2475        let mut cancel = CancelCheck::new();
2476        let mut rows: Vec<Vec<Value>> = Vec::with_capacity(parent_rows.len());
2477        for parent_row in &parent_rows {
2478            cancel.tick()?;
2479            let mut out = Vec::with_capacity(fields.len());
2480            let mut build_iter = builds.iter();
2481            for field in fields {
2482                match field {
2483                    NestedProjectField::Plain(f) => {
2484                        out.push(eval_expr(&f.expr, parent_row, &parent_columns));
2485                    }
2486                    NestedProjectField::Nested(nested) => {
2487                        let (parent_idx, build) =
2488                            build_iter.next().expect("one build side per nested field");
2489                        let array = build
2490                            .get(&parent_row[*parent_idx])
2491                            .map(String::as_str)
2492                            .unwrap_or("[]");
2493                        // Round-tripping through the text parser yields
2494                        // canonical PJ1 (sorted object keys) for free.
2495                        let doc = powdb_storage::pj1::parse_json_text(array).map_err(|e| {
2496                            QueryError::Execution(format!(
2497                                "nested projection `{}` produced invalid JSON: {e}",
2498                                nested.name
2499                            ))
2500                        })?;
2501                        out.push(Value::Json(doc.into()));
2502                    }
2503                }
2504            }
2505            rows.push(out);
2506        }
2507        Ok(QueryResult::Rows { columns, rows })
2508    }
2509
2510    /// Assemble one nested projection level bottom-up: recursively assemble
2511    /// its own nested children first, then scan the child table once, apply
2512    /// the residual filter, group rows by correlation value, order and
2513    /// truncate each parent's bucket, and serialize each bucket to JSON
2514    /// array text. Recursion depth is bounded by the parser's nesting guard.
2515    fn assemble_nested_arrays(
2516        &self,
2517        nested: &NestedProjection,
2518    ) -> Result<rustc_hash::FxHashMap<Value, String>, QueryError> {
2519        use rustc_hash::FxHashMap;
2520        let schema = self
2521            .catalog
2522            .schema(&nested.table)
2523            .ok_or_else(|| QueryError::TableNotFound(nested.table.clone()))?
2524            .clone();
2525        let column_index = |name: &str| {
2526            schema
2527                .columns
2528                .iter()
2529                .position(|c| c.name == name)
2530                .ok_or_else(|| QueryError::ColumnNotFound {
2531                    table: nested.table.clone(),
2532                    column: name.to_string(),
2533                })
2534        };
2535        let key_idx = column_index(&nested.child_key)?;
2536        // One value source per output field: a scalar column, or the
2537        // correlation column of a deeper level plus its assembled arrays.
2538        enum FieldSource {
2539            Column,
2540            Arrays(rustc_hash::FxHashMap<Value, String>),
2541        }
2542        let mut sources: Vec<(&str, usize, FieldSource)> = Vec::with_capacity(nested.fields.len());
2543        for field in &nested.fields {
2544            match field {
2545                NestedField::Scalar { key, column } => {
2546                    sources.push((key.as_str(), column_index(column)?, FieldSource::Column));
2547                }
2548                NestedField::Nested(inner) => {
2549                    sources.push((
2550                        inner.name.as_str(),
2551                        column_index(&inner.parent_key)?,
2552                        FieldSource::Arrays(self.assemble_nested_arrays(inner)?),
2553                    ));
2554                }
2555            }
2556        }
2557        let order_idxs = nested
2558            .order
2559            .iter()
2560            .map(|(column, descending)| Ok((column_index(column)?, *descending)))
2561            .collect::<Result<Vec<_>, QueryError>>()?;
2562        let bound = |expr: &Option<Expr>, what: &str| -> Result<Option<usize>, QueryError> {
2563            match expr {
2564                None => Ok(None),
2565                Some(Expr::Literal(Literal::Int(v))) if *v >= 0 => Ok(Some(*v as usize)),
2566                Some(_) => Err(QueryError::Execution(format!(
2567                    "nested projection `{}` {what} must be a non-negative integer literal",
2568                    nested.name
2569                ))),
2570            }
2571        };
2572        let limit = bound(&nested.limit, "limit")?;
2573        let offset = bound(&nested.offset, "offset")?;
2574        // Residual conditions reference bare child columns (rewritten by
2575        // the planner), so they evaluate against the full schema row.
2576        let schema_cols: Vec<String> = if nested.residual.is_some() {
2577            schema.columns.iter().map(|c| c.name.clone()).collect()
2578        } else {
2579            Vec::new()
2580        };
2581        // Materialize only the needed child columns (key first), charge
2582        // them against the query budget like a join build side, then fold
2583        // into per-parent buckets.
2584        let mut cancel = CancelCheck::new();
2585        let mut child_rows: Vec<Vec<Value>> = Vec::new();
2586        for (_, row) in self
2587            .catalog
2588            .scan(&nested.table)
2589            .map_err(|e| QueryError::StorageError(e.to_string()))?
2590        {
2591            cancel.tick()?;
2592            // A NULL correlation value never matches any parent.
2593            if row[key_idx] == Value::Empty {
2594                continue;
2595            }
2596            if let Some(residual) = &nested.residual {
2597                if !eval_predicate(residual, &row, &schema_cols) {
2598                    continue;
2599                }
2600            }
2601            let mut narrowed = Vec::with_capacity(1 + sources.len() + order_idxs.len());
2602            narrowed.push(row[key_idx].clone());
2603            for (_, idx, _) in &sources {
2604                narrowed.push(row[*idx].clone());
2605            }
2606            for (idx, _) in &order_idxs {
2607                narrowed.push(row[*idx].clone());
2608            }
2609            child_rows.push(narrowed);
2610        }
2611        self.charge_rows(&child_rows)?;
2612        // Bucket entries keep their per-parent sort key values (the
2613        // narrowed tail) until ordering and truncation are applied.
2614        let mut buckets: FxHashMap<Value, Vec<(Vec<Value>, String)>> =
2615            FxHashMap::with_capacity_and_hasher(child_rows.len(), Default::default());
2616        let sort_tail = 1 + sources.len();
2617        for mut child in child_rows {
2618            cancel.tick()?;
2619            let sort_values = child.split_off(sort_tail);
2620            let mut object = String::from("{");
2621            for (i, ((name, _, source), value)) in sources.iter().zip(&child[1..]).enumerate() {
2622                if i > 0 {
2623                    object.push(',');
2624                }
2625                push_json_string(&mut object, name);
2626                object.push(':');
2627                match source {
2628                    FieldSource::Column => push_json_value(&mut object, value),
2629                    FieldSource::Arrays(arrays) => {
2630                        object.push_str(arrays.get(value).map(String::as_str).unwrap_or("[]"));
2631                    }
2632                }
2633            }
2634            object.push('}');
2635            let key = child.swap_remove(0);
2636            buckets.entry(key).or_default().push((sort_values, object));
2637        }
2638        let mut build: FxHashMap<Value, String> =
2639            FxHashMap::with_capacity_and_hasher(buckets.len(), Default::default());
2640        for (key, mut bucket) in buckets {
2641            cancel.tick()?;
2642            if !order_idxs.is_empty() {
2643                // Stable sort: ties keep child scan order.
2644                bucket.sort_by(|(a, _), (b, _)| {
2645                    for (pos, (_, descending)) in order_idxs.iter().enumerate() {
2646                        let cmp = compare_order_values(&a[pos], &b[pos], *descending);
2647                        if cmp != std::cmp::Ordering::Equal {
2648                            return cmp;
2649                        }
2650                    }
2651                    std::cmp::Ordering::Equal
2652                });
2653            }
2654            let kept = bucket
2655                .iter()
2656                .skip(offset.unwrap_or(0))
2657                .take(limit.unwrap_or(usize::MAX));
2658            let mut array =
2659                String::with_capacity(2 + kept.clone().map(|(_, o)| o.len() + 1).sum::<usize>());
2660            array.push('[');
2661            for (i, (_, object)) in kept.enumerate() {
2662                if i > 0 {
2663                    array.push(',');
2664                }
2665                array.push_str(object);
2666            }
2667            array.push(']');
2668            build.insert(key, array);
2669        }
2670        Ok(build)
2671    }
2672}
2673
2674/// Append `s` to `out` as a JSON string literal with the required escapes.
2675fn push_json_string(out: &mut String, s: &str) {
2676    use std::fmt::Write;
2677    out.push('"');
2678    for ch in s.chars() {
2679        match ch {
2680            '"' => out.push_str("\\\""),
2681            '\\' => out.push_str("\\\\"),
2682            '\n' => out.push_str("\\n"),
2683            '\r' => out.push_str("\\r"),
2684            '\t' => out.push_str("\\t"),
2685            c if c <= '\u{1f}' => {
2686                let _ = write!(out, "\\u{:04x}", c as u32);
2687            }
2688            c => out.push(c),
2689        }
2690    }
2691    out.push('"');
2692}
2693
2694/// Append a child column value to `out` as a JSON value. Scalars map
2695/// naturally (int/float -> number, str -> string, bool -> bool, empty ->
2696/// null); JSON columns embed as sub-documents; the remaining types
2697/// (datetime, uuid, bytes) fall back to their wire text as a JSON string
2698/// (slice scope).
2699fn push_json_value(out: &mut String, value: &Value) {
2700    use std::fmt::Write;
2701    match value {
2702        Value::Empty => out.push_str("null"),
2703        Value::Int(v) => {
2704            let _ = write!(out, "{v}");
2705        }
2706        Value::Float(v) if v.is_finite() => {
2707            let _ = write!(out, "{v}");
2708        }
2709        // NaN/infinity have no JSON representation.
2710        Value::Float(_) => out.push_str("null"),
2711        Value::Bool(v) => out.push_str(if *v { "true" } else { "false" }),
2712        Value::Str(s) => push_json_string(out, s),
2713        Value::Json(doc) => {
2714            out.push_str(&powdb_storage::pj1::pj1_to_text(doc).unwrap_or_else(|_| "null".into()))
2715        }
2716        other => push_json_string(out, &other.to_wire_string()),
2717    }
2718}