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