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