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