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