powdb_query/executor/plan_exec.rs
1//! The execute_plan method and associated helpers.
2
3use crate::ast::*;
4use crate::plan::*;
5use crate::result::{QueryError, QueryResult};
6use powdb_storage::catalog::Catalog;
7use powdb_storage::row::{decode_column, decode_row, patch_var_column_in_place, RowLayout};
8use powdb_storage::types::*;
9use std::cmp::Reverse;
10use std::collections::BinaryHeap;
11
12use super::compiled::*;
13use super::eval::*;
14use super::row_body_base;
15use super::{check_join_limit, Engine, MAX_SORT_ROWS};
16use powdb_storage::view::{ViewDef, ViewRegistry};
17
18impl Engine {
19 pub fn execute_plan(&mut self, plan: &PlanNode) -> Result<QueryResult, QueryError> {
20 match plan {
21 PlanNode::SeqScan { table } => {
22 // Auto-refresh dirty materialized views on read.
23 if self.view_registry.is_dirty(table) {
24 self.refresh_view(table)?;
25 }
26 let schema = self
27 .catalog
28 .schema(table)
29 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
30 .clone();
31 let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
32 let rows: Vec<Vec<Value>> = self
33 .catalog
34 .scan(table)
35 .map_err(|e| QueryError::StorageError(e.to_string()))?
36 .map(|(_, row)| row)
37 .collect();
38 Ok(QueryResult::Rows { columns, rows })
39 }
40
41 PlanNode::Filter { input, predicate } => {
42 // Materialize any IN-subqueries in the predicate before the
43 // scan loop — the closure can't call back into the engine.
44 // Correlated subqueries are left in place for per-row eval.
45 let materialized;
46 let predicate = if contains_subquery(predicate) {
47 materialized = self.materialize_subqueries(predicate)?;
48 &materialized
49 } else {
50 predicate
51 };
52
53 // Correlated subquery path: per-row materialisation.
54 if contains_subquery(predicate) {
55 let result = self.execute_plan(input)?;
56 return match result {
57 QueryResult::Rows { columns, rows } => {
58 let mut filtered = Vec::new();
59 for row in rows {
60 let row_pred =
61 self.materialize_correlated_for_row(predicate, &row, &columns)?;
62 if eval_predicate(&row_pred, &row, &columns) {
63 filtered.push(row);
64 }
65 }
66 Ok(QueryResult::Rows {
67 columns,
68 rows: filtered,
69 })
70 }
71 _ => Err("filter requires row input".into()),
72 };
73 }
74
75 // Fast path: fuse Filter + SeqScan into a zero-copy streaming
76 // loop. Uses decode_column() to evaluate the predicate on only
77 // the columns it references, avoiding heap allocations for
78 // String/Bytes columns that aren't part of the filter.
79 if let PlanNode::SeqScan { table } = input.as_ref() {
80 // Auto-refresh dirty materialized views.
81 if self.view_registry.is_dirty(table) {
82 self.refresh_view(table)?;
83 }
84 let schema = self
85 .catalog
86 .schema(table)
87 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
88 .clone();
89 let columns: Vec<String> =
90 schema.columns.iter().map(|c| c.name.clone()).collect();
91 let fast = FastLayout::new(&schema);
92 let row_layout = RowLayout::new(&schema);
93 // Mission F: pre-size to skip the first 4 Vec doublings
94 // (4 → 8 → 16 → 32 → 64). On a 100K-row scan with 30%
95 // selectivity that's ~4 fewer reallocations + memcpys.
96 let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
97
98 // Try compiled predicate for the filter check (handles
99 // int leaves, string-eq leaves, and And conjunctions).
100 if let Some(compiled) = compile_predicate(predicate, &columns, &fast, &schema) {
101 self.catalog
102 .for_each_row_raw(table, |_rid, data| {
103 if compiled(data) {
104 rows.push(decode_row(&schema, data));
105 }
106 })
107 .map_err(|e| QueryError::StorageError(e.to_string()))?;
108 } else {
109 let pred_cols = predicate_column_indices(predicate, &columns);
110 self.catalog
111 .for_each_row_raw(table, |_rid, data| {
112 let pred_row =
113 decode_selective(&schema, &row_layout, data, &pred_cols);
114 if eval_predicate(predicate, &pred_row, &columns) {
115 rows.push(decode_row(&schema, data));
116 }
117 })
118 .map_err(|e| QueryError::StorageError(e.to_string()))?;
119 }
120
121 return Ok(QueryResult::Rows { columns, rows });
122 }
123
124 // General path: materialise then filter.
125 let result = self.execute_plan(input)?;
126 match result {
127 QueryResult::Rows { columns, rows } => {
128 let filtered: Vec<Vec<Value>> = rows
129 .into_iter()
130 .filter(|row| eval_predicate(predicate, row, &columns))
131 .collect();
132 Ok(QueryResult::Rows {
133 columns,
134 rows: filtered,
135 })
136 }
137 _ => Err("filter requires row input".into()),
138 }
139 }
140
141 PlanNode::Project { input, fields } => {
142 // Fast path: Project over IndexScan — decode only projected
143 // columns from raw bytes instead of full decode_row.
144 if let PlanNode::IndexScan { table, column, key } = input.as_ref() {
145 let schema = self
146 .catalog
147 .schema(table)
148 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
149 .clone();
150 let all_columns: Vec<String> =
151 schema.columns.iter().map(|c| c.name.clone()).collect();
152 let key_value = literal_to_value(key)?;
153 let tbl = self
154 .catalog
155 .get_table(table)
156 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
157
158 let proj_columns: Vec<String> = fields
159 .iter()
160 .map(|f| {
161 f.alias.clone().unwrap_or_else(|| match &f.expr {
162 Expr::Field(name) => name.clone(),
163 _ => "?".into(),
164 })
165 })
166 .collect();
167
168 // Determine which column indices the projection needs
169 let proj_indices: Vec<usize> = fields
170 .iter()
171 .filter_map(|f| {
172 if let Expr::Field(name) = &f.expr {
173 all_columns.iter().position(|c| c == name)
174 } else {
175 None
176 }
177 })
178 .collect();
179
180 if tbl.has_index(column) {
181 let layout = RowLayout::new(&schema);
182 let rids = tbl.index_lookup_all(column, &key_value);
183 let mut rows: Vec<Vec<Value>> = Vec::with_capacity(rids.len());
184 for rid in rids {
185 if let Some(data) = tbl.heap.get(rid) {
186 let row: Vec<Value> = proj_indices
187 .iter()
188 .map(|&ci| decode_column(&schema, &layout, &data, ci))
189 .collect();
190 rows.push(row);
191 }
192 }
193 return Ok(QueryResult::Rows {
194 columns: proj_columns,
195 rows,
196 });
197 }
198 }
199
200 // Fast path: Project(Limit(Sort(Filter(SeqScan)))) — bounded
201 // top-N heap. Decodes only the sort key + projected columns,
202 // keeps at most `limit` rows in a heap. Also handles the
203 // Project(Limit(Sort(SeqScan))) variant (no filter).
204 if let PlanNode::Limit {
205 input: inner,
206 count: limit_expr,
207 } = input.as_ref()
208 {
209 if let PlanNode::Sort {
210 input: sort_input,
211 keys,
212 } = inner.as_ref()
213 {
214 // Fast path only for single-key sorts
215 if keys.len() == 1 {
216 let sort_field = &keys[0].field;
217 let descending = keys[0].descending;
218 let limit = match limit_expr {
219 Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
220 _ => usize::MAX,
221 };
222 let (table_opt, pred_opt): (Option<&str>, Option<&Expr>) =
223 match sort_input.as_ref() {
224 PlanNode::SeqScan { table } => (Some(table.as_str()), None),
225 PlanNode::Filter {
226 input: fi,
227 predicate,
228 } => {
229 if let PlanNode::SeqScan { table } = fi.as_ref() {
230 (Some(table.as_str()), Some(predicate))
231 } else {
232 (None, None)
233 }
234 }
235 _ => (None, None),
236 };
237 if let Some(table) = table_opt {
238 if let Some(result) = self.project_filter_sort_limit_fast(
239 table, fields, sort_field, descending, limit, pred_opt,
240 )? {
241 return Ok(result);
242 }
243 }
244 }
245 }
246 // Fast path: Project(Limit(Filter(SeqScan))) — stream,
247 // decode only projected columns, stop at limit.
248 if let PlanNode::Filter {
249 input: fi,
250 predicate,
251 } = inner.as_ref()
252 {
253 if let PlanNode::SeqScan { table } = fi.as_ref() {
254 let limit = match limit_expr {
255 Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
256 _ => usize::MAX,
257 };
258 if let Some(result) = self.project_filter_limit_fast(
259 table,
260 fields,
261 limit,
262 Some(predicate),
263 )? {
264 return Ok(result);
265 }
266 }
267 }
268 // Fast path: Project(Limit(SeqScan)) — stream, no filter.
269 if let PlanNode::SeqScan { table } = inner.as_ref() {
270 let limit = match limit_expr {
271 Expr::Literal(Literal::Int(v)) if *v >= 0 => *v as usize,
272 _ => usize::MAX,
273 };
274 if let Some(result) =
275 self.project_filter_limit_fast(table, fields, limit, None)?
276 {
277 return Ok(result);
278 }
279 }
280 }
281
282 // Mission D4: Project(Filter(SeqScan)) without Limit. Reuses
283 // `project_filter_limit_fast` with limit = usize::MAX so the
284 // hot loop decodes only projected columns and uses the
285 // compiled predicate. Previously this fell through to the
286 // generic Filter branch which materialised every column via
287 // `decode_row` then re-projected — quadratic work.
288 //
289 // multi_col_and_filter (`U filter .age > 30 and .status =
290 // "active" { .name, .age }`) was 6.18ms (0.7x SQLite) and
291 // is the load-bearing workload for this fast path.
292 if let PlanNode::Filter {
293 input: fi,
294 predicate,
295 } = input.as_ref()
296 {
297 if let PlanNode::SeqScan { table } = fi.as_ref() {
298 if let Some(result) = self.project_filter_limit_fast(
299 table,
300 fields,
301 usize::MAX,
302 Some(predicate),
303 )? {
304 return Ok(result);
305 }
306 }
307 }
308
309 // Mission D4: Project(SeqScan) without Filter or Limit.
310 // Decode only projected columns; the previous fall-through
311 // built full Vec<Value> rows then re-projected.
312 if let PlanNode::SeqScan { table } = input.as_ref() {
313 if let Some(result) =
314 self.project_filter_limit_fast(table, fields, usize::MAX, None)?
315 {
316 return Ok(result);
317 }
318 }
319
320 let result = self.execute_plan(input)?;
321 match result {
322 QueryResult::Rows { columns, rows } => {
323 let proj_columns: Vec<String> = fields
324 .iter()
325 .map(|f| {
326 f.alias.clone().unwrap_or_else(|| match &f.expr {
327 Expr::Field(name) => name.clone(),
328 // Mission E1.2: `{ u.name }` projects as the
329 // qualified column name so callers can still
330 // disambiguate across the join output.
331 Expr::QualifiedField { qualifier, field } => {
332 format!("{qualifier}.{field}")
333 }
334 _ => "?".into(),
335 })
336 })
337 .collect();
338 let proj_rows: Vec<Vec<Value>> = rows
339 .iter()
340 .map(|row| {
341 fields
342 .iter()
343 .map(|f| eval_expr(&f.expr, row, &columns))
344 .collect()
345 })
346 .collect();
347 Ok(QueryResult::Rows {
348 columns: proj_columns,
349 rows: proj_rows,
350 })
351 }
352 _ => Err("project requires row input".into()),
353 }
354 }
355
356 PlanNode::Sort { input, keys } => {
357 let result = self.execute_plan(input)?;
358 match result {
359 QueryResult::Rows { columns, mut rows } => {
360 // WS2: row-count cap is a cheap secondary guard; the
361 // byte budget is the real OOM defense for the sort
362 // buffer (a few very large rows pass the row cap).
363 if rows.len() > MAX_SORT_ROWS {
364 return Err(QueryError::SortLimitExceeded);
365 }
366 self.charge_rows(&rows)?;
367 let key_indices: Vec<(usize, bool)> = keys
368 .iter()
369 .map(|k| {
370 columns
371 .iter()
372 .position(|c| c == &k.field)
373 .map(|idx| (idx, k.descending))
374 .ok_or_else(|| QueryError::ColumnNotFound {
375 table: String::new(),
376 column: k.field.clone(),
377 })
378 })
379 .collect::<Result<_, QueryError>>()?;
380 rows.sort_by(|a, b| {
381 for &(col_idx, descending) in &key_indices {
382 let cmp = a[col_idx].cmp(&b[col_idx]);
383 let cmp = if descending { cmp.reverse() } else { cmp };
384 if cmp != std::cmp::Ordering::Equal {
385 return cmp;
386 }
387 }
388 std::cmp::Ordering::Equal
389 });
390 Ok(QueryResult::Rows { columns, rows })
391 }
392 _ => Err("sort requires row input".into()),
393 }
394 }
395
396 PlanNode::Limit { input, count } => {
397 let result = self.execute_plan(input)?;
398 let n = match count {
399 Expr::Literal(Literal::Int(v)) => *v as usize,
400 _ => return Err("limit must be integer literal".into()),
401 };
402 match result {
403 QueryResult::Rows { columns, rows } => Ok(QueryResult::Rows {
404 columns,
405 rows: rows.into_iter().take(n).collect(),
406 }),
407 _ => Err("limit requires row input".into()),
408 }
409 }
410
411 PlanNode::Offset { input, count } => {
412 let result = self.execute_plan(input)?;
413 let n = match count {
414 Expr::Literal(Literal::Int(v)) => *v as usize,
415 _ => return Err("offset must be integer literal".into()),
416 };
417 match result {
418 QueryResult::Rows { columns, rows } => Ok(QueryResult::Rows {
419 columns,
420 rows: rows.into_iter().skip(n).collect(),
421 }),
422 _ => Err("offset requires row input".into()),
423 }
424 }
425
426 PlanNode::Aggregate {
427 input,
428 function,
429 field,
430 } => {
431 // Fast path: count() over SeqScan — count rows without any decode
432 if *function == AggFunc::Count {
433 if let PlanNode::SeqScan { table } = input.as_ref() {
434 // Auto-refresh a dirty materialized view before
435 // counting it — otherwise count(View) returns stale
436 // data after an underlying mutation (F3).
437 if self.view_registry.is_dirty(table) {
438 self.refresh_view(table)?;
439 }
440 let mut count: i64 = 0;
441 self.catalog
442 .for_each_row_raw(table, |_rid, _data| {
443 count += 1;
444 })
445 .map_err(|e| QueryError::StorageError(e.to_string()))?;
446 return Ok(QueryResult::Scalar(Value::Int(count)));
447 }
448 // Fast path: count() over Filter(SeqScan) — try compiled
449 // predicate first, fall back to decode_column path.
450 // Skip a predicate carrying a subquery: the raw-bytes
451 // evaluators here don't materialise subqueries, so
452 // `count(T filter .x in (...))` would silently count 0
453 // (F1). Falling through routes it to the generic path
454 // that resolves the subquery correctly.
455 if let PlanNode::Filter {
456 input: inner,
457 predicate,
458 } = input.as_ref()
459 {
460 if let PlanNode::SeqScan { table } = inner.as_ref() {
461 if self.view_registry.is_dirty(table) {
462 self.refresh_view(table)?;
463 }
464 }
465 if let (PlanNode::SeqScan { table }, false) =
466 (inner.as_ref(), contains_subquery(predicate))
467 {
468 let schema = self
469 .catalog
470 .schema(table)
471 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
472 .clone();
473 let columns: Vec<String> =
474 schema.columns.iter().map(|c| c.name.clone()).collect();
475 let fast = FastLayout::new(&schema);
476 let row_layout = RowLayout::new(&schema);
477
478 // Try compiled predicate (zero-allocation hot path).
479 // Handles int leaves, string-eq leaves, AND conjunctions.
480 if let Some(compiled) =
481 compile_predicate(predicate, &columns, &fast, &schema)
482 {
483 let mut count: i64 = 0;
484 self.catalog
485 .for_each_row_raw(table, |_rid, data| {
486 if compiled(data) {
487 count += 1;
488 }
489 })
490 .map_err(|e| QueryError::StorageError(e.to_string()))?;
491 return Ok(QueryResult::Scalar(Value::Int(count)));
492 }
493
494 // Fallback: decode predicate columns
495 let pred_cols = predicate_column_indices(predicate, &columns);
496 let mut count: i64 = 0;
497 self.catalog
498 .for_each_row_raw(table, |_rid, data| {
499 let pred_row =
500 decode_selective(&schema, &row_layout, data, &pred_cols);
501 if eval_predicate(predicate, &pred_row, &columns) {
502 count += 1;
503 }
504 })
505 .map_err(|e| QueryError::StorageError(e.to_string()))?;
506
507 return Ok(QueryResult::Scalar(Value::Int(count)));
508 }
509 }
510 }
511
512 // Fast path: sum/avg/min/max over a single fixed-size int
513 // column with an optional compiled filter predicate. Walks
514 // raw row bytes, zero allocation per row.
515 if matches!(
516 function,
517 AggFunc::Sum
518 | AggFunc::Avg
519 | AggFunc::Min
520 | AggFunc::Max
521 | AggFunc::CountDistinct
522 ) {
523 if let Some(col) = field.as_ref() {
524 // Shape: Aggregate(SeqScan) or Aggregate(Filter(SeqScan))
525 let (table_opt, pred_opt): (Option<&str>, Option<&Expr>) =
526 match input.as_ref() {
527 PlanNode::SeqScan { table } => (Some(table.as_str()), None),
528 PlanNode::Filter {
529 input: inner,
530 predicate,
531 } => {
532 if let PlanNode::SeqScan { table } = inner.as_ref() {
533 (Some(table.as_str()), Some(predicate))
534 } else {
535 (None, None)
536 }
537 }
538 _ => (None, None),
539 };
540 if let Some(table) = table_opt {
541 if let Some(result) =
542 self.agg_single_col_fast(table, col, *function, pred_opt)?
543 {
544 return Ok(result);
545 }
546 }
547 }
548 }
549
550 // Fast path: Project(Limit(Filter(SeqScan))) — stream, decode
551 // only projected columns, stop once we hit the limit.
552 // (Handled in the Project branch; this branch only fires when
553 // the aggregate is the outer node.)
554 let result = self.execute_plan(input)?;
555 match result {
556 QueryResult::Rows { columns, rows } => {
557 match function {
558 AggFunc::Count => {
559 Ok(QueryResult::Scalar(Value::Int(rows.len() as i64)))
560 }
561 AggFunc::CountDistinct => {
562 let col = field.as_ref().ok_or("count distinct requires field")?;
563 let idx = columns
564 .iter()
565 .position(|c| c == col)
566 .ok_or("col not found")?;
567 let mut seen = std::collections::HashSet::new();
568 for row in &rows {
569 let v = &row[idx];
570 if !v.is_empty() {
571 seen.insert(v.clone());
572 }
573 }
574 Ok(QueryResult::Scalar(Value::Int(seen.len() as i64)))
575 }
576 AggFunc::Avg => {
577 let col = field.as_ref().ok_or("avg requires field")?;
578 let idx = columns
579 .iter()
580 .position(|c| c == col)
581 .ok_or("col not found")?;
582 let mut count: u64 = 0;
583 let sum: f64 = rows
584 .iter()
585 .filter_map(|r| match &r[idx] {
586 Value::Int(v) => Some(*v as f64),
587 Value::Float(v) => Some(*v),
588 _ => None,
589 })
590 .inspect(|_| count += 1)
591 .sum();
592 if count == 0 {
593 Ok(QueryResult::Scalar(Value::Empty))
594 } else {
595 Ok(QueryResult::Scalar(Value::Float(sum / count as f64)))
596 }
597 }
598 AggFunc::Sum => {
599 let col = field.as_ref().ok_or("sum requires field")?;
600 let idx = columns
601 .iter()
602 .position(|c| c == col)
603 .ok_or("col not found")?;
604 // Track int and float contributions separately so
605 // Float columns (and mixed Int/Float rows) don't get
606 // silently dropped as they did in the Int-only
607 // version. If any Float is present, the whole sum
608 // promotes to Float — matching Avg's semantics.
609 let mut int_sum: i64 = 0;
610 let mut float_sum: f64 = 0.0;
611 let mut saw_float = false;
612 for r in &rows {
613 match &r[idx] {
614 Value::Int(v) => int_sum += *v,
615 Value::Float(v) => {
616 float_sum += *v;
617 saw_float = true;
618 }
619 _ => {}
620 }
621 }
622 let result = if saw_float {
623 Value::Float(float_sum + int_sum as f64)
624 } else {
625 Value::Int(int_sum)
626 };
627 Ok(QueryResult::Scalar(result))
628 }
629 AggFunc::Min | AggFunc::Max => {
630 let col = field.as_ref().ok_or("min/max requires field")?;
631 let idx = columns
632 .iter()
633 .position(|c| c == col)
634 .ok_or("col not found")?;
635 let vals: Vec<&Value> = rows.iter().map(|r| &r[idx]).collect();
636 let result = if *function == AggFunc::Min {
637 vals.into_iter().min().cloned()
638 } else {
639 vals.into_iter().max().cloned()
640 };
641 Ok(QueryResult::Scalar(result.unwrap_or(Value::Empty)))
642 }
643 }
644 }
645 _ => Err("aggregate requires row input".into()),
646 }
647 }
648
649 PlanNode::Insert {
650 table,
651 rows,
652 returning,
653 } => {
654 // Build + validate EVERY row before inserting any, so a bad
655 // row (unknown/missing/uncoercible field) aborts the whole
656 // statement without a partial write. The WAL fsync happens
657 // once at statement end, so N rows = N appends + 1 fsync.
658 let mut returning_columns: Vec<String> = Vec::new();
659 let all_values: Vec<Vec<Value>> = {
660 let schema = self
661 .catalog
662 .schema(table)
663 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
664 if *returning {
665 returning_columns = schema.columns.iter().map(|c| c.name.clone()).collect();
666 }
667 let defaults = self.catalog.column_defaults(table).unwrap_or(&[]);
668 let auto = self.catalog.auto_columns(table).unwrap_or(&[]);
669 let mut all = Vec::with_capacity(rows.len());
670 for assignments in rows {
671 let mut values = vec![Value::Empty; schema.columns.len()];
672 for a in assignments {
673 let idx = schema.column_index(&a.field).ok_or_else(|| {
674 QueryError::ColumnNotFound {
675 table: String::new(),
676 column: a.field.clone(),
677 }
678 })?;
679 let raw = literal_to_value(&a.value)?;
680 values[idx] = coerce_value(raw, &schema.columns[idx])?;
681 }
682 // Fill any column left unset by this row from its
683 // declared default (applied before the required check,
684 // so a default satisfies a required column).
685 for (i, slot) in values.iter_mut().enumerate() {
686 if slot.is_empty() {
687 if let Some(Some(d)) = defaults.get(i) {
688 *slot = d.clone();
689 }
690 }
691 }
692 for col in &schema.columns {
693 let pos = col.position as usize;
694 // Auto columns are exempt from the required check —
695 // they are filled from the sequence just below.
696 let is_auto = auto.get(pos).copied().unwrap_or(false);
697 if col.required && !is_auto && matches!(values[pos], Value::Empty) {
698 return Err(QueryError::Execution(format!(
699 "column '{}' is required but no value was provided",
700 col.name
701 )));
702 }
703 }
704 all.push(values);
705 }
706 all
707 };
708 // Assign auto-increment columns now that the immutable
709 // schema/defaults/auto borrows are released. Done here (not in
710 // the build loop) so the assigned ids land in `all_values` and
711 // flow back through `returning`.
712 let mut all_values = all_values;
713 for values in all_values.iter_mut() {
714 self.catalog.assign_auto_columns(table, values);
715 }
716 // Charge the materialized batch against the per-query memory
717 // budget before inserting — keeps multi-row insert consistent
718 // with every other full-materialization point (sort/join/group)
719 // and bounds embedded callers (the server also caps the query
720 // string at 1 MB, but embedded callers have no such limit).
721 self.charge_rows(&all_values)?;
722 let n = all_values.len() as u64;
723 for values in &all_values {
724 self.catalog
725 .insert(table, values)
726 .map_err(|e| QueryError::StorageError(e.to_string()))?;
727 }
728 self.view_registry.mark_dependents_dirty(table);
729 if *returning {
730 Ok(QueryResult::Rows {
731 columns: returning_columns,
732 rows: all_values,
733 })
734 } else {
735 Ok(QueryResult::Modified(n))
736 }
737 }
738
739 PlanNode::Upsert {
740 table,
741 key_column,
742 assignments,
743 on_conflict,
744 } => {
745 let (values, key_idx) = {
746 let schema = self
747 .catalog
748 .schema(table)
749 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
750 let mut values = vec![Value::Empty; schema.columns.len()];
751 for a in assignments {
752 let idx = schema.column_index(&a.field).ok_or_else(|| {
753 QueryError::ColumnNotFound {
754 table: String::new(),
755 column: a.field.clone(),
756 }
757 })?;
758 let raw = literal_to_value(&a.value)?;
759 values[idx] = coerce_value(raw, &schema.columns[idx])?;
760 }
761 // Apply column defaults for the insert path, same as a plain
762 // insert (applied before the required-column check).
763 let defaults = self.catalog.column_defaults(table).unwrap_or(&[]);
764 for (i, slot) in values.iter_mut().enumerate() {
765 if slot.is_empty() {
766 if let Some(Some(d)) = defaults.get(i) {
767 *slot = d.clone();
768 }
769 }
770 }
771 for col in &schema.columns {
772 if col.required && matches!(values[col.position as usize], Value::Empty) {
773 return Err(QueryError::Execution(format!(
774 "column '{}' is required but no value was provided",
775 col.name
776 )));
777 }
778 }
779 let key_idx = schema
780 .column_index(key_column)
781 .ok_or_else(|| format!("key column '{key_column}' not found"))?;
782 (values, key_idx)
783 };
784
785 // Upsert requires the `on` column to be unique — otherwise
786 // there is no well-defined row to overwrite and a plain
787 // insert could silently create duplicate keys.
788 if self.catalog.is_index_unique(table, key_column) != Some(true) {
789 return Err(QueryError::Execution(format!(
790 "upsert on .{key_column} requires a unique column (declare it with \
791 `unique {key_column}: <type>` or `alter {table} add unique .{key_column}`)"
792 )));
793 }
794
795 let key_value = values[key_idx].clone();
796
797 // Probe the unique index for a conflict.
798 let existing = {
799 let tbl = self
800 .catalog
801 .get_table(table)
802 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
803 // The key column is guaranteed unique above, so this
804 // returns at most one matching row.
805 let rids = tbl.index_lookup_all(key_column, &key_value);
806 rids.into_iter().next().and_then(|rid| {
807 tbl.heap
808 .get(rid)
809 .map(|data| (rid, decode_row(&tbl.schema, &data)))
810 })
811 };
812
813 if let Some((rid, mut existing_row)) = existing {
814 // Conflict: apply on_conflict assignments (or all non-key if empty).
815 let update_assignments = if on_conflict.is_empty() {
816 assignments
817 } else {
818 on_conflict
819 };
820 let changed_cols: Vec<usize> = {
821 let schema = self
822 .catalog
823 .schema(table)
824 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
825 let mut indices = Vec::new();
826 for a in update_assignments {
827 let idx = schema.column_index(&a.field).ok_or_else(|| {
828 QueryError::ColumnNotFound {
829 table: String::new(),
830 column: a.field.clone(),
831 }
832 })?;
833 if idx != key_idx {
834 // Coerce to the target column type, same as the
835 // UPDATE and INSERT paths — an int→float literal
836 // here would otherwise persist as raw i64 bits
837 // (#118 corruption on the upsert conflict path).
838 existing_row[idx] =
839 coerce_value(literal_to_value(&a.value)?, &schema.columns[idx])
840 .map_err(QueryError::TypeError)?;
841 indices.push(idx);
842 }
843 }
844 indices
845 };
846 self.catalog
847 .update_hinted(table, rid, &existing_row, Some(&changed_cols))
848 .map_err(|e| QueryError::StorageError(e.to_string()))?;
849 self.view_registry.mark_dependents_dirty(table);
850 Ok(QueryResult::Modified(1))
851 } else {
852 // No conflict: insert.
853 self.catalog
854 .insert(table, &values)
855 .map_err(|e| QueryError::StorageError(e.to_string()))?;
856 self.view_registry.mark_dependents_dirty(table);
857 Ok(QueryResult::Modified(1))
858 }
859 }
860
861 PlanNode::Update {
862 input,
863 table,
864 assignments,
865 returning,
866 } => {
867 // Mission C Phase 3: resolve assignments against a borrowed
868 // schema, then drop the borrow before the mutation loop.
869 // Try literal-only path first; fall back to per-row expression
870 // evaluation if any assignment contains a non-literal expression
871 // (e.g., `age := .age + 1`).
872 let (col_indices, literal_vals, target_cols): (
873 Vec<usize>,
874 Option<Vec<Value>>,
875 Vec<ColumnDef>,
876 ) = {
877 let schema_ref = self
878 .catalog
879 .schema(table)
880 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
881 let indices: Vec<usize> = assignments
882 .iter()
883 .map(|a| {
884 schema_ref.column_index(&a.field).ok_or_else(|| {
885 QueryError::ColumnNotFound {
886 table: String::new(),
887 column: a.field.clone(),
888 }
889 })
890 })
891 .collect::<Result<_, _>>()?;
892 // The target column defs (aligned with `assignments`), owned
893 // so the per-row expression path can coerce without holding a
894 // catalog borrow across the mutation loop.
895 let target_cols: Vec<ColumnDef> = indices
896 .iter()
897 .map(|&idx| schema_ref.columns[idx].clone())
898 .collect();
899 // Resolve each assignment to a literal value. If any is a
900 // non-literal expression, fall back (None) to the per-row
901 // expression-eval path below.
902 let raw_vals: Result<Vec<Value>, _> = assignments
903 .iter()
904 .map(|a| literal_to_value(&a.value))
905 .collect();
906 // Coerce each literal to its target column's declared type
907 // before it can reach the byte-patch fast path (the same
908 // coercion the INSERT path applies). Without this, an int
909 // assigned to a float column is written as raw i64 bits
910 // (#118 silent corruption) and a str assigned to a
911 // fixed-size column reaches `unreachable!` and aborts the
912 // whole server (#117 remote DoS). A genuine type mismatch
913 // is a hard error to the client, not an expr-path fallback.
914 let coerced = match raw_vals {
915 Ok(raws) => {
916 let mut out = Vec::with_capacity(raws.len());
917 for (raw, &idx) in raws.into_iter().zip(indices.iter()) {
918 out.push(
919 coerce_value(raw, &schema_ref.columns[idx])
920 .map_err(QueryError::TypeError)?,
921 );
922 }
923 Some(out)
924 }
925 Err(_) => None,
926 };
927 (indices, coerced, target_cols)
928 };
929 let resolved_assignments: Option<Vec<(usize, Value)>> =
930 literal_vals.map(|vals| col_indices.iter().copied().zip(vals).collect());
931
932 // Mission C Phase 2: the hint Table::update_hinted needs to
933 // decide whether to read the old row for index diff.
934 let changed_cols: Vec<usize> = col_indices.clone();
935
936 // ── RETURNING path ──────────────────────────────────────
937 // `returning` materializes the post-update row image, so the
938 // byte-patch / fused fast paths (which never decode a row)
939 // can't serve it. Take the generic decode→mutate→collect
940 // route. Opt-in only: when `returning` is false every path
941 // below is byte-for-byte unchanged.
942 if *returning {
943 let columns: Vec<String> = {
944 let schema_ref = self
945 .catalog
946 .schema(table)
947 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
948 schema_ref.columns.iter().map(|c| c.name.clone()).collect()
949 };
950 let matching_rids = self.collect_rids_for_mutation(input, table)?;
951 let mut out_rows: Vec<Vec<Value>> = Vec::with_capacity(matching_rids.len());
952 for rid in matching_rids {
953 let mut row = match self.catalog.get(table, rid) {
954 Some(r) => r,
955 None => continue,
956 };
957 match &resolved_assignments {
958 // Literal path: apply the pre-coerced values.
959 Some(resolved) => {
960 for (idx, val) in resolved.iter() {
961 row[*idx] = val.clone();
962 }
963 }
964 // Expression path: evaluate each RHS against the
965 // (progressively mutated) row, then coerce to the
966 // target column type before writing — same guard the
967 // literal path gets, matching the non-returning expr
968 // path exactly (#117/#118 on computed assignments).
969 None => {
970 for (i, asgn) in assignments.iter().enumerate() {
971 let val = eval_expr(&asgn.value, &row, &columns);
972 row[col_indices[i]] = coerce_value(val, &target_cols[i])
973 .map_err(QueryError::TypeError)?;
974 }
975 }
976 }
977 self.catalog
978 .update_hinted(table, rid, &row, Some(&changed_cols))
979 .map_err(|e| QueryError::StorageError(e.to_string()))?;
980 out_rows.push(row);
981 }
982 self.view_registry.mark_dependents_dirty(table);
983 return Ok(QueryResult::Rows {
984 columns,
985 rows: out_rows,
986 });
987 }
988
989 // ── Fused scan+update for Update(Filter(SeqScan)) ────────
990 // Perf sprint: instead of the two-pass collect-RIDs-then-loop
991 // pattern (which pays one ensure_hot per matched row on the
992 // second pass), fuse the predicate evaluation and in-place
993 // byte-level mutation into a single heap walk. Same idea as
994 // the fused scan_delete_matching path for deletes.
995 if let Some(ref resolved_assignments) = resolved_assignments {
996 if let PlanNode::Filter {
997 input: inner,
998 predicate,
999 } = input.as_ref()
1000 {
1001 if let PlanNode::SeqScan { table: t } = inner.as_ref() {
1002 if t == table {
1003 let fused_result = self.try_fused_scan_update(
1004 table,
1005 predicate,
1006 resolved_assignments,
1007 &changed_cols,
1008 );
1009 if let Some(result) = fused_result {
1010 return result;
1011 }
1012 }
1013 }
1014 }
1015 }
1016
1017 // Collect matching RowIds in a single pass.
1018 let matching_rids = self.collect_rids_for_mutation(input, table)?;
1019
1020 // ── Literal-only fast paths ─────────────────────────────
1021 if let Some(ref resolved_assignments) = resolved_assignments {
1022 // Mission C Phase 4: in-place byte-patch fast path. If every
1023 // assignment targets a fixed-size non-null column AND none of
1024 // them is indexed, we can skip decode_row / Vec<Value> /
1025 // encode_row_into entirely and patch the row's raw bytes on
1026 // the hot page.
1027 let fast_patch: Option<Vec<FastPatch>> = {
1028 let tbl = self
1029 .catalog
1030 .get_table(table)
1031 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1032 let schema = &tbl.schema;
1033 let all_fixed_nonnull = resolved_assignments.iter().all(|(idx, val)| {
1034 is_fixed_size(schema.columns[*idx].type_id) && !val.is_empty()
1035 });
1036 let no_indexed = !resolved_assignments
1037 .iter()
1038 .any(|(idx, _)| tbl.has_indexed_col(*idx));
1039
1040 if all_fixed_nonnull && no_indexed {
1041 let layout = RowLayout::new(schema);
1042 let bitmap_size = layout.bitmap_size();
1043 let patches: Vec<FastPatch> = resolved_assignments
1044 .iter()
1045 .map(|(idx, val)| {
1046 let fixed_off = layout
1047 .fixed_offset(*idx)
1048 .expect("is_fixed_size already checked");
1049 let field_off = 2 + bitmap_size + fixed_off;
1050 let bytes: FixedBytes = match val {
1051 Value::Int(v) => FixedBytes::I64(v.to_le_bytes()),
1052 Value::Float(v) => FixedBytes::F64(v.to_le_bytes()),
1053 Value::Bool(v) => FixedBytes::Bool(if *v { 1 } else { 0 }),
1054 Value::DateTime(v) => FixedBytes::I64(v.to_le_bytes()),
1055 Value::Uuid(v) => FixedBytes::Uuid(*v),
1056 _ => unreachable!("all_fixed_nonnull guard lied"),
1057 };
1058 FastPatch {
1059 field_off,
1060 bitmap_byte_off: 2 + idx / 8,
1061 bit_mask: 1u8 << (idx % 8),
1062 bytes,
1063 }
1064 })
1065 .collect();
1066 Some(patches)
1067 } else {
1068 None
1069 }
1070 };
1071
1072 if let Some(patches) = fast_patch {
1073 let mut count = 0u64;
1074 for rid in matching_rids {
1075 // Mission B2: WAL-log every patch so crash
1076 // recovery replays the update. Same mutation
1077 // closure as before — the wrapper just sandwiches
1078 // it between a hot-page read and a WAL append.
1079 let ok = self
1080 .catalog
1081 .update_row_bytes_logged(table, rid, |row| {
1082 let base = row_body_base(row);
1083 for p in &patches {
1084 row[base + p.bitmap_byte_off] &= !p.bit_mask;
1085 let field_bytes = p.bytes.as_slice();
1086 row[base + p.field_off
1087 ..base + p.field_off + field_bytes.len()]
1088 .copy_from_slice(field_bytes);
1089 }
1090 })
1091 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1092 if ok {
1093 count += 1;
1094 }
1095 }
1096 self.view_registry.mark_dependents_dirty(table);
1097 return Ok(QueryResult::Modified(count));
1098 }
1099
1100 // Mission C Phase 10: var-column in-place shrink fast path.
1101 let var_fast: Option<(usize, Option<Vec<u8>>)> = {
1102 let tbl = self
1103 .catalog
1104 .get_table(table)
1105 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1106 let schema = &tbl.schema;
1107 let is_single = resolved_assignments.len() == 1;
1108 let is_var_col = is_single
1109 && !is_fixed_size(schema.columns[resolved_assignments[0].0].type_id);
1110 let no_indexed = !resolved_assignments
1111 .iter()
1112 .any(|(idx, _)| tbl.has_indexed_col(*idx));
1113
1114 if is_single && is_var_col && no_indexed {
1115 let (idx, val) = &resolved_assignments[0];
1116 let bytes_opt: Option<Vec<u8>> = match val {
1117 Value::Str(s) => Some(s.as_bytes().to_vec()),
1118 Value::Bytes(b) => Some(b.clone()),
1119 Value::Empty => None,
1120 _ => {
1121 return Err(QueryError::TypeError(format!(
1122 "cannot assign non-var value to var column '{}'",
1123 schema.columns[*idx].name
1124 )))
1125 }
1126 };
1127 Some((*idx, bytes_opt))
1128 } else {
1129 None
1130 }
1131 };
1132
1133 if let Some((col_idx, new_bytes_opt)) = var_fast {
1134 let new_bytes_ref: Option<&[u8]> = new_bytes_opt.as_deref();
1135 let mut count = 0u64;
1136 let mut fallback_rids: Vec<RowId> = Vec::new();
1137 for rid in &matching_rids {
1138 // Mission B2: logged variant so crash recovery
1139 // replays the shrink. On a false return (row
1140 // would have to grow), the rid is pushed to
1141 // `fallback_rids` and the slower `update_hinted`
1142 // path — which is already WAL-logged — picks it up.
1143 let ok = self
1144 .catalog
1145 .patch_var_col_logged(table, *rid, col_idx, new_bytes_ref)
1146 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1147 if ok {
1148 count += 1;
1149 } else {
1150 fallback_rids.push(*rid);
1151 }
1152 }
1153 for rid in fallback_rids {
1154 let mut row = match self.catalog.get(table, rid) {
1155 Some(r) => r,
1156 None => continue,
1157 };
1158 for (idx, val) in resolved_assignments.iter() {
1159 row[*idx] = val.clone();
1160 }
1161 self.catalog
1162 .update_hinted(table, rid, &row, Some(&changed_cols))
1163 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1164 count += 1;
1165 }
1166 self.view_registry.mark_dependents_dirty(table);
1167 return Ok(QueryResult::Modified(count));
1168 }
1169
1170 // Generic literal path: decode row, apply literal values.
1171 let mut count = 0u64;
1172 for rid in matching_rids {
1173 let mut row = match self.catalog.get(table, rid) {
1174 Some(r) => r,
1175 None => continue,
1176 };
1177 for (idx, val) in resolved_assignments.iter() {
1178 row[*idx] = val.clone();
1179 }
1180 self.catalog
1181 .update_hinted(table, rid, &row, Some(&changed_cols))
1182 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1183 count += 1;
1184 }
1185 self.view_registry.mark_dependents_dirty(table);
1186 return Ok(QueryResult::Modified(count));
1187 } // end if let Some(resolved_assignments)
1188
1189 // ── Expression-based update path ────────────────────────
1190 // At least one assignment contains a non-literal expression
1191 // (e.g., `age := .age + 1`). Evaluate per-row.
1192 let col_names: Vec<String> = {
1193 let schema_ref = self
1194 .catalog
1195 .schema(table)
1196 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1197 schema_ref.columns.iter().map(|c| c.name.clone()).collect()
1198 };
1199 let mut count = 0u64;
1200 for rid in matching_rids {
1201 let mut row = match self.catalog.get(table, rid) {
1202 Some(r) => r,
1203 None => continue,
1204 };
1205 for (i, asgn) in assignments.iter().enumerate() {
1206 let val = eval_expr(&asgn.value, &row, &col_names);
1207 // Coerce to the target column type before writing, so a
1208 // computed int→float assignment stores f64 (not raw i64
1209 // bits, #118) and a str→fixed-col assignment returns a
1210 // typed error instead of hitting the encoder's
1211 // `unreachable!` and aborting the process (#117).
1212 row[col_indices[i]] =
1213 coerce_value(val, &target_cols[i]).map_err(QueryError::TypeError)?;
1214 }
1215 self.catalog
1216 .update_hinted(table, rid, &row, Some(&changed_cols))
1217 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1218 count += 1;
1219 }
1220 self.view_registry.mark_dependents_dirty(table);
1221 Ok(QueryResult::Modified(count))
1222 }
1223
1224 PlanNode::Delete {
1225 input,
1226 table,
1227 returning,
1228 } => {
1229 // ── RETURNING path ──────────────────────────────────────
1230 // `returning` needs the pre-delete row image, so read each
1231 // matched row before removing it. The fused single-pass
1232 // delete primitives below never decode rows, so they can't
1233 // serve this. Opt-in only: when `returning` is false the
1234 // fast paths below are byte-for-byte unchanged.
1235 if *returning {
1236 let columns: Vec<String> = {
1237 let schema_ref = self
1238 .catalog
1239 .schema(table)
1240 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1241 schema_ref.columns.iter().map(|c| c.name.clone()).collect()
1242 };
1243 let matching_rids = self.collect_rids_for_mutation(input, table)?;
1244 let mut out_rows: Vec<Vec<Value>> = Vec::with_capacity(matching_rids.len());
1245 for rid in &matching_rids {
1246 if let Some(row) = self.catalog.get(table, *rid) {
1247 out_rows.push(row);
1248 }
1249 }
1250 self.catalog
1251 .delete_many(table, &matching_rids)
1252 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1253 self.view_registry.mark_dependents_dirty(table);
1254 return Ok(QueryResult::Rows {
1255 columns,
1256 rows: out_rows,
1257 });
1258 }
1259
1260 // Mission C Phase 3: no schema clone — collect_rids_for_mutation
1261 // looks up schema internally when it needs one, and the mutation
1262 // loop doesn't need the schema at all.
1263 //
1264 // Mission C Phase 12: route bulk deletes through
1265 // `Catalog::delete_many`, which batches the btree leaf
1266 // compaction and shares one `ensure_hot` per row between
1267 // the index-key extraction and the slot delete. On
1268 // `delete_by_filter` (100K fixture, ~20K matches) that
1269 // removes ~4ms of pure `Vec::remove` memmove from the btree
1270 // maintenance phase.
1271 //
1272 // Mission C Phase 16: for the common `delete where ...`
1273 // shape (Filter(SeqScan)) — and the rarer "delete
1274 // everything" shape (SeqScan) — skip the two-pass
1275 // `collect_rids_for_mutation` + `delete_many` flow entirely.
1276 // The fused `scan_delete_matching` primitive walks the
1277 // heap exactly once, paying one `ensure_hot` per page
1278 // instead of per-row. That closes the last major gap on
1279 // the bench's `delete_by_filter` workload.
1280 if let PlanNode::Filter {
1281 input: inner,
1282 predicate,
1283 } = input.as_ref()
1284 {
1285 if let PlanNode::SeqScan { table: t } = inner.as_ref() {
1286 if t == table {
1287 let schema = self
1288 .catalog
1289 .schema(table)
1290 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1291 let columns: Vec<String> =
1292 schema.columns.iter().map(|c| c.name.clone()).collect();
1293 let fast = FastLayout::new(schema);
1294 if let Some(compiled) =
1295 compile_predicate(predicate, &columns, &fast, schema)
1296 {
1297 // Mission B2: logged variant so every
1298 // matched rid hits the WAL during the
1299 // single-pass scan. Structure of the
1300 // fused scan is unchanged — only the
1301 // hook closure now also appends.
1302 let count = self
1303 .catalog
1304 .scan_delete_matching_logged(table, |data| compiled(data))
1305 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1306 self.view_registry.mark_dependents_dirty(table);
1307 return Ok(QueryResult::Modified(count));
1308 }
1309 }
1310 }
1311 } else if let PlanNode::SeqScan { table: t } = input.as_ref() {
1312 if t == table {
1313 // `delete from T` with no predicate — every live
1314 // row matches. One pass is still the right shape.
1315 // Mission B2: logged variant — see above.
1316 let count = self
1317 .catalog
1318 .scan_delete_matching_logged(table, |_| true)
1319 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1320 self.view_registry.mark_dependents_dirty(table);
1321 return Ok(QueryResult::Modified(count));
1322 }
1323 }
1324
1325 let matching_rids = self.collect_rids_for_mutation(input, table)?;
1326 let count = self
1327 .catalog
1328 .delete_many(table, &matching_rids)
1329 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1330 self.view_registry.mark_dependents_dirty(table);
1331 Ok(QueryResult::Modified(count))
1332 }
1333
1334 PlanNode::AliasScan { table, alias } => {
1335 // Mission E1.2: scan `table` and rename every output column
1336 // to `alias.field`. Used as a join leaf so downstream
1337 // NestedLoopJoin + Filter + Project nodes can resolve
1338 // `Expr::QualifiedField` lookups by direct column-name match.
1339 //
1340 // We don't bother with a fused zero-copy loop here yet — the
1341 // whole join path is nested-loop and correctness-first
1342 // (Phase E1.3 will introduce hash join and at that point we
1343 // can revisit whether to specialise AliasScan).
1344 let schema = self
1345 .catalog
1346 .schema(table)
1347 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
1348 .clone();
1349 let columns: Vec<String> = schema
1350 .columns
1351 .iter()
1352 .map(|c| format!("{alias}.{}", c.name))
1353 .collect();
1354 let rows: Vec<Vec<Value>> = self
1355 .catalog
1356 .scan(table)
1357 .map_err(|e| QueryError::StorageError(e.to_string()))?
1358 .map(|(_, row)| row)
1359 .collect();
1360 Ok(QueryResult::Rows { columns, rows })
1361 }
1362
1363 PlanNode::NestedLoopJoin {
1364 left,
1365 right,
1366 on,
1367 kind,
1368 } => {
1369 // Materialise both sides. The executor ships two strategies:
1370 // 1. Hash join (E1.3) — when the `on` predicate is a
1371 // simple equi-predicate `left_col = right_col`, build a
1372 // FxHashMap<Value, Vec<row_idx>> over the right side
1373 // and probe with the left side. O(L + R) instead of
1374 // O(L × R). Handles Inner and LeftOuter.
1375 // 2. Nested loop (E1.2) — fallback for Cross, non-equi
1376 // predicates, or `on` expressions that reference
1377 // either side with something more complex than a
1378 // QualifiedField.
1379 let left_result = self.execute_plan(left)?;
1380 let right_result = self.execute_plan(right)?;
1381 let (left_columns, left_rows) = match left_result {
1382 QueryResult::Rows { columns, rows } => (columns, rows),
1383 _ => return Err("join left side must produce rows".into()),
1384 };
1385 let (right_columns, right_rows) = match right_result {
1386 QueryResult::Rows { columns, rows } => (columns, rows),
1387 _ => return Err("join right side must produce rows".into()),
1388 };
1389
1390 // WS2: byte-budget guard on the join build side. Charge both
1391 // materialized inputs before we build the hash table / probe;
1392 // the output is row-capped by check_join_limit below.
1393 self.charge_rows(&left_rows)?;
1394 self.charge_rows(&right_rows)?;
1395
1396 // Hash-join fast path.
1397 if !matches!(kind, JoinKind::Cross) {
1398 if let Some(pred) = on {
1399 if let Some((l_idx, r_idx)) =
1400 try_extract_equi_join_keys(pred, &left_columns, &right_columns)
1401 {
1402 let result = hash_join(
1403 left_columns,
1404 left_rows,
1405 right_columns,
1406 right_rows,
1407 l_idx,
1408 r_idx,
1409 *kind,
1410 );
1411 if let QueryResult::Rows { ref rows, .. } = result {
1412 check_join_limit(rows.len())?;
1413 }
1414 return Ok(result);
1415 }
1416 }
1417 }
1418
1419 // Nested-loop fallback.
1420 let n_left = left_columns.len();
1421 let n_right = right_columns.len();
1422 let mut columns = Vec::with_capacity(n_left + n_right);
1423 columns.extend(left_columns);
1424 columns.extend(right_columns);
1425
1426 let mut rows: Vec<Vec<Value>> = Vec::with_capacity(left_rows.len());
1427 let mut combined: Vec<Value> = Vec::with_capacity(n_left + n_right);
1428
1429 for left_row in &left_rows {
1430 let mut matched = false;
1431 for right_row in &right_rows {
1432 combined.clear();
1433 combined.extend_from_slice(left_row);
1434 combined.extend_from_slice(right_row);
1435 let keep = match kind {
1436 JoinKind::Cross => true,
1437 JoinKind::Inner | JoinKind::LeftOuter => match on {
1438 Some(pred) => eval_predicate(pred, &combined, &columns),
1439 // Missing `on` for non-cross joins is a
1440 // parser error, but if it slips through we
1441 // treat it as "match everything".
1442 None => true,
1443 },
1444 // RightOuter is rewritten to LeftOuter by the
1445 // planner, so we never see it here.
1446 JoinKind::RightOuter => {
1447 unreachable!("planner rewrites RightOuter to LeftOuter")
1448 }
1449 };
1450 if keep {
1451 rows.push(combined.clone());
1452 check_join_limit(rows.len())?;
1453 matched = true;
1454 }
1455 }
1456 if !matched && matches!(kind, JoinKind::LeftOuter) {
1457 let mut row = Vec::with_capacity(n_left + n_right);
1458 row.extend_from_slice(left_row);
1459 row.resize(n_left + n_right, Value::Empty);
1460 rows.push(row);
1461 check_join_limit(rows.len())?;
1462 }
1463 }
1464
1465 Ok(QueryResult::Rows { columns, rows })
1466 }
1467
1468 PlanNode::Distinct { input } => {
1469 let result = self.execute_plan(input)?;
1470 match result {
1471 QueryResult::Rows { columns, rows } => {
1472 let mut seen = std::collections::HashSet::new();
1473 let mut unique_rows = Vec::new();
1474 for row in rows {
1475 if seen.insert(row.clone()) {
1476 unique_rows.push(row);
1477 }
1478 }
1479 Ok(QueryResult::Rows {
1480 columns,
1481 rows: unique_rows,
1482 })
1483 }
1484 other => Ok(other),
1485 }
1486 }
1487
1488 PlanNode::GroupBy {
1489 input,
1490 keys,
1491 aggregates,
1492 having,
1493 } => {
1494 let result = self.execute_plan(input)?;
1495 match result {
1496 QueryResult::Rows { columns, rows } => {
1497 // WS2: byte-budget guard on the GROUP BY input buffer
1498 // (the hash table is bounded by the input it groups).
1499 self.charge_rows(&rows)?;
1500 // Resolve key column indices.
1501 let key_indices: Vec<usize> = keys
1502 .iter()
1503 .map(|k| {
1504 columns
1505 .iter()
1506 .position(|c| c == k)
1507 .ok_or_else(|| format!("group-by column '{k}' not found"))
1508 })
1509 .collect::<Result<Vec<_>, _>>()?;
1510
1511 // Resolve aggregate field indices. count(*) uses
1512 // sentinel usize::MAX — compute_group_aggregate
1513 // treats it as "count all rows in the group".
1514 let agg_field_indices: Vec<usize> = aggregates
1515 .iter()
1516 .map(|a| {
1517 if a.field == "*" {
1518 Ok(usize::MAX)
1519 } else {
1520 columns.iter().position(|c| c == &a.field).ok_or_else(|| {
1521 format!("aggregate column '{}' not found", a.field)
1522 })
1523 }
1524 })
1525 .collect::<Result<Vec<_>, _>>()?;
1526
1527 // Group rows by key values (preserving insertion order).
1528 let mut group_map: rustc_hash::FxHashMap<Vec<Value>, usize> =
1529 rustc_hash::FxHashMap::default();
1530 let mut groups: Vec<(Vec<Value>, Vec<usize>)> = Vec::new();
1531 for (ri, row) in rows.iter().enumerate() {
1532 let key: Vec<Value> =
1533 key_indices.iter().map(|&i| row[i].clone()).collect();
1534 match group_map.get(&key) {
1535 Some(&idx) => groups[idx].1.push(ri),
1536 None => {
1537 let idx = groups.len();
1538 group_map.insert(key.clone(), idx);
1539 groups.push((key, vec![ri]));
1540 }
1541 }
1542 }
1543
1544 // Build output column names: keys ++ aggregate output names.
1545 let mut out_columns: Vec<String> = keys.clone();
1546 for agg in aggregates.iter() {
1547 out_columns.push(agg.output_name.clone());
1548 }
1549
1550 // Compute aggregates per group.
1551 let mut out_rows: Vec<Vec<Value>> = Vec::with_capacity(groups.len());
1552 for (key_vals, row_indices) in &groups {
1553 let mut row = key_vals.clone();
1554 for (ai, agg) in aggregates.iter().enumerate() {
1555 let col_idx = agg_field_indices[ai];
1556 let val = compute_group_aggregate(
1557 agg.function,
1558 &rows,
1559 row_indices,
1560 col_idx,
1561 );
1562 row.push(val);
1563 }
1564 out_rows.push(row);
1565 }
1566
1567 // Apply HAVING filter.
1568 if let Some(having_expr) = having {
1569 out_rows.retain(|row| eval_predicate(having_expr, row, &out_columns));
1570 }
1571
1572 Ok(QueryResult::Rows {
1573 columns: out_columns,
1574 rows: out_rows,
1575 })
1576 }
1577 _ => Err("group by requires row input".into()),
1578 }
1579 }
1580
1581 PlanNode::CreateTable { name, fields } => {
1582 let columns: Vec<ColumnDef> = fields
1583 .iter()
1584 .enumerate()
1585 .map(|(i, f)| -> Result<ColumnDef, QueryError> {
1586 Ok(ColumnDef {
1587 name: f.name.clone(),
1588 type_id: type_name_to_id(&f.type_name)
1589 .map_err(QueryError::TypeError)?,
1590 required: f.required,
1591 position: i as u16,
1592 })
1593 })
1594 .collect::<Result<Vec<_>, _>>()?;
1595 // Coerce each literal default to its column's type now, so a
1596 // type mismatch (`count: int default "x"`) is rejected at DDL
1597 // time and the stored default is ready to drop into inserts.
1598 let mut defaults: Vec<Option<Value>> = vec![None; columns.len()];
1599 let mut auto_cols: Vec<bool> = vec![false; columns.len()];
1600 for (i, f) in fields.iter().enumerate() {
1601 if let Some(lit) = &f.default {
1602 let raw = literal_value_from(lit);
1603 defaults[i] = Some(coerce_value(raw, &columns[i])?);
1604 }
1605 if f.auto {
1606 // Auto-increment only makes sense on an integer column,
1607 // and combining it with a literal default is
1608 // contradictory (both want to supply the value).
1609 if columns[i].type_id != TypeId::Int {
1610 return Err(QueryError::TypeError(format!(
1611 "auto column '{}' must be of type int",
1612 f.name
1613 )));
1614 }
1615 if f.default.is_some() {
1616 return Err(QueryError::TypeError(format!(
1617 "auto column '{}' cannot also declare a default",
1618 f.name
1619 )));
1620 }
1621 auto_cols[i] = true;
1622 }
1623 }
1624 let schema = Schema {
1625 table_name: name.clone(),
1626 columns,
1627 };
1628 self.catalog
1629 .create_table_full(schema, defaults, auto_cols)
1630 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1631 // Declaring a field `unique` auto-creates a unique B+tree
1632 // index, which is where uniqueness is enforced on writes.
1633 for f in fields.iter().filter(|f| f.unique) {
1634 self.catalog
1635 .create_index_unique(name, &f.name, true)
1636 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1637 }
1638 Ok(QueryResult::Created(name.clone()))
1639 }
1640
1641 PlanNode::AlterTable { table, action } => match action {
1642 AlterAction::AddColumn {
1643 name,
1644 type_name,
1645 required,
1646 } => {
1647 let position = self
1648 .catalog
1649 .schema(table)
1650 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
1651 .columns
1652 .len() as u16;
1653 let col = ColumnDef {
1654 name: name.clone(),
1655 type_id: type_name_to_id(type_name).map_err(QueryError::TypeError)?,
1656 required: *required,
1657 position,
1658 };
1659 self.catalog
1660 .alter_table_add_column(table, col)
1661 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1662 Ok(QueryResult::Executed {
1663 message: format!("column '{name}' added to '{table}'"),
1664 })
1665 }
1666 AlterAction::DropColumn { name } => {
1667 self.catalog
1668 .alter_table_drop_column(table, name)
1669 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1670 Ok(QueryResult::Executed {
1671 message: format!("column '{name}' dropped from '{table}'"),
1672 })
1673 }
1674 AlterAction::AddIndex { column } => {
1675 self.catalog
1676 .create_index(table, column)
1677 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1678 Ok(QueryResult::Executed {
1679 message: format!("index on '{table}.{column}' created"),
1680 })
1681 }
1682 AlterAction::AddUnique { column } => {
1683 // No DropIndex exists, so we cannot upgrade an existing
1684 // non-unique index in place — reject it cleanly.
1685 if self.catalog.has_index(table, column) {
1686 return Err(QueryError::Execution(format!(
1687 "cannot add unique on {table}.{column}: column already indexed"
1688 )));
1689 }
1690 // Scan existing rows for duplicate (non-null) values
1691 // before creating the unique index.
1692 {
1693 let tbl = self
1694 .catalog
1695 .get_table(table)
1696 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1697 let col_idx = tbl.schema.column_index(column).ok_or_else(|| {
1698 QueryError::ColumnNotFound {
1699 table: table.to_string(),
1700 column: column.clone(),
1701 }
1702 })?;
1703 let mut seen = std::collections::HashSet::new();
1704 for (_, row) in tbl.scan() {
1705 let v = &row[col_idx];
1706 if v.is_empty() {
1707 continue;
1708 }
1709 if !seen.insert(v.clone()) {
1710 return Err(QueryError::Execution(format!(
1711 "cannot add unique on {table}.{column}: \
1712 duplicate value {v:?} exists"
1713 )));
1714 }
1715 }
1716 }
1717 self.catalog
1718 .create_index_unique(table, column, true)
1719 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1720 Ok(QueryResult::Executed {
1721 message: format!("unique index on '{table}.{column}' created"),
1722 })
1723 }
1724 },
1725
1726 PlanNode::DropTable { name } => {
1727 self.catalog
1728 .drop_table(name)
1729 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1730 Ok(QueryResult::Executed {
1731 message: format!("table '{name}' dropped"),
1732 })
1733 }
1734
1735 PlanNode::CreateView { name, query_text } => {
1736 self.create_view(name, query_text)?;
1737 Ok(QueryResult::Executed {
1738 message: format!("materialized view '{name}' created"),
1739 })
1740 }
1741
1742 PlanNode::RefreshView { name } => {
1743 self.refresh_view(name)?;
1744 Ok(QueryResult::Executed {
1745 message: format!("materialized view '{name}' refreshed"),
1746 })
1747 }
1748
1749 PlanNode::DropView { name } => {
1750 self.drop_view(name)?;
1751 Ok(QueryResult::Executed {
1752 message: format!("materialized view '{name}' dropped"),
1753 })
1754 }
1755
1756 PlanNode::Window { input, windows } => {
1757 let result = self.execute_plan(input)?;
1758 execute_window(result, windows)
1759 }
1760
1761 PlanNode::Union { left, right, all } => {
1762 let left_result = self.execute_plan(left)?;
1763 let right_result = self.execute_plan(right)?;
1764 let (left_cols, left_rows) = match left_result {
1765 QueryResult::Rows { columns, rows } => (columns, rows),
1766 _ => return Err("UNION requires query results on left side".into()),
1767 };
1768 let (_, right_rows) = match right_result {
1769 QueryResult::Rows { columns, rows } => (columns, rows),
1770 _ => return Err("UNION requires query results on right side".into()),
1771 };
1772 let mut combined = left_rows;
1773 if *all {
1774 // UNION ALL — just concatenate.
1775 combined.extend(right_rows);
1776 } else {
1777 // UNION — deduplicate using the same HashSet approach
1778 // as DISTINCT. Value already implements Hash + Eq.
1779 let mut seen = std::collections::HashSet::new();
1780 for row in &combined {
1781 seen.insert(row.clone());
1782 }
1783 for row in right_rows {
1784 if seen.insert(row.clone()) {
1785 combined.push(row);
1786 }
1787 }
1788 }
1789 Ok(QueryResult::Rows {
1790 columns: left_cols,
1791 rows: combined,
1792 })
1793 }
1794
1795 PlanNode::Explain { input } => {
1796 let text = format_plan_tree(input, 0);
1797 Ok(QueryResult::Rows {
1798 columns: vec!["plan".to_string()],
1799 rows: text
1800 .lines()
1801 .map(|line| vec![Value::Str(line.to_string())])
1802 .collect(),
1803 })
1804 }
1805
1806 PlanNode::Begin => {
1807 if self.in_transaction {
1808 return Err(QueryError::Execution(
1809 "already in a transaction (nested transactions not supported)".into(),
1810 ));
1811 }
1812 self.catalog
1813 .begin_transaction()
1814 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1815 self.in_transaction = true;
1816 Ok(QueryResult::Executed {
1817 message: "transaction started".to_string(),
1818 })
1819 }
1820
1821 PlanNode::Commit => {
1822 if !self.in_transaction {
1823 return Err(QueryError::Execution(
1824 "no active transaction to commit".into(),
1825 ));
1826 }
1827 self.catalog
1828 .commit_transaction()
1829 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1830 self.in_transaction = false;
1831 Ok(QueryResult::Executed {
1832 message: "transaction committed".to_string(),
1833 })
1834 }
1835
1836 PlanNode::Rollback => {
1837 if !self.in_transaction {
1838 return Err(QueryError::Execution(
1839 "no active transaction to roll back".into(),
1840 ));
1841 }
1842 self.in_transaction = false;
1843 self.catalog
1844 .rollback_to_last_sync()
1845 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1846 if let Ok(mut cache) = self.plan_cache.lock() {
1847 cache.clear();
1848 }
1849 self.view_registry = ViewRegistry::open(self.catalog.data_dir())
1850 .unwrap_or_else(|_| ViewRegistry::new(self.catalog.data_dir()));
1851 Ok(QueryResult::Executed {
1852 message: "transaction rolled back".to_string(),
1853 })
1854 }
1855
1856 PlanNode::IndexScan { table, column, key } => {
1857 let key_value = literal_to_value(key)?;
1858 let tbl = self
1859 .catalog
1860 .get_table(table)
1861 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1862 let columns: Vec<String> =
1863 tbl.schema.columns.iter().map(|c| c.name.clone()).collect();
1864
1865 // Fast path: the table has a B-tree on this column.
1866 // Uses index_lookup_all to return ALL matching rows for
1867 // both unique and non-unique indexes.
1868 if tbl.has_index(column) {
1869 let rids = tbl.index_lookup_all(column, &key_value);
1870 let mut rows: Vec<Vec<Value>> = Vec::with_capacity(rids.len());
1871 for rid in rids {
1872 if let Some(data) = tbl.heap.get(rid) {
1873 rows.push(decode_row(&tbl.schema, &data));
1874 }
1875 }
1876 return Ok(QueryResult::Rows { columns, rows });
1877 }
1878
1879 // Fallback: no index on this column. The planner emits IndexScan
1880 // eagerly (it has no visibility into which columns are indexed
1881 // at plan time), so here we must behave like SeqScan+Filter on
1882 // `.col = literal`: return *all* matching rows, not just the
1883 // first one. A non-indexed column isn't necessarily unique.
1884 // We compile the eq predicate once and stream without any
1885 // per-row decode for non-matching rows.
1886 let schema = &tbl.schema;
1887 let fast = FastLayout::new(schema);
1888 let synth_pred = Expr::BinaryOp(
1889 Box::new(Expr::Field(column.clone())),
1890 BinOp::Eq,
1891 Box::new(key.clone()),
1892 );
1893 if let Some(compiled) = compile_predicate(&synth_pred, &columns, &fast, schema) {
1894 // Mission F: skip the first 4 Vec doublings.
1895 let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
1896 self.catalog
1897 .for_each_row_raw(table, |_rid, data| {
1898 if compiled(data) {
1899 rows.push(decode_row(schema, data));
1900 }
1901 })
1902 .map_err(|e| QueryError::StorageError(e.to_string()))?;
1903 return Ok(QueryResult::Rows { columns, rows });
1904 }
1905
1906 // Last resort: slow eq-check on materialised rows.
1907 let col_idx =
1908 schema
1909 .column_index(column)
1910 .ok_or_else(|| QueryError::ColumnNotFound {
1911 table: String::new(),
1912 column: column.clone(),
1913 })?;
1914 let rows: Vec<Vec<Value>> = tbl
1915 .scan()
1916 .filter_map(|(_, row)| {
1917 if row[col_idx] == key_value {
1918 Some(row)
1919 } else {
1920 None
1921 }
1922 })
1923 .collect();
1924 Ok(QueryResult::Rows { columns, rows })
1925 }
1926
1927 PlanNode::RangeScan {
1928 table,
1929 column,
1930 start,
1931 end,
1932 } => {
1933 let tbl = self
1934 .catalog
1935 .get_table(table)
1936 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
1937 let columns: Vec<String> =
1938 tbl.schema.columns.iter().map(|c| c.name.clone()).collect();
1939 let schema = &tbl.schema;
1940
1941 let start_val = match start {
1942 Some((expr, _)) => Some(literal_to_value(expr)?),
1943 None => None,
1944 };
1945 let end_val = match end {
1946 Some((expr, _)) => Some(literal_to_value(expr)?),
1947 None => None,
1948 };
1949 let start_inclusive = start.as_ref().map(|(_, inc)| *inc).unwrap_or(true);
1950 let end_inclusive = end.as_ref().map(|(_, inc)| *inc).unwrap_or(true);
1951
1952 // Non-unique index: walk the composite (value, rid) leaf
1953 // chain between prefix bounds, fetch each row from the heap,
1954 // and recheck. The recheck enforces exclusive bounds
1955 // (range_rids is inclusive) and defensively skips any decoded
1956 // null (nulls are never indexed, so they must not match).
1957 if tbl.is_index_unique(column) == Some(false) {
1958 if let Some(btree) = tbl.index(column) {
1959 if start_val.is_some() || end_val.is_some() {
1960 let col_idx = schema.column_index(column).ok_or_else(|| {
1961 QueryError::ColumnNotFound {
1962 table: String::new(),
1963 column: column.clone(),
1964 }
1965 })?;
1966 let rids = btree.range_rids(start_val.as_ref(), end_val.as_ref());
1967 let mut rows: Vec<Vec<Value>> = Vec::with_capacity(rids.len());
1968 for rid in rids {
1969 if let Some(data) = tbl.heap.get(rid) {
1970 let row = decode_row(schema, &data);
1971 if !row[col_idx].is_empty()
1972 && range_matches(
1973 &row[col_idx],
1974 &start_val,
1975 start_inclusive,
1976 &end_val,
1977 end_inclusive,
1978 )
1979 {
1980 rows.push(row);
1981 }
1982 }
1983 }
1984 return Ok(QueryResult::Rows { columns, rows });
1985 }
1986 }
1987 }
1988
1989 // Range scans use the btree fast path for unique indexes,
1990 // walking raw column-value keys directly.
1991 if tbl.is_index_unique(column) == Some(true) {
1992 if let Some(btree) = tbl.index(column) {
1993 let hits: Vec<(Value, RowId)> = match (&start_val, &end_val) {
1994 (Some(s), Some(e)) => btree.range(s, e).collect(),
1995 (Some(s), None) => btree.range_from(s),
1996 (None, Some(e)) => btree.range_to(e),
1997 (None, None) => {
1998 let rows: Vec<Vec<Value>> =
1999 tbl.scan().map(|(_, row)| row).collect();
2000 return Ok(QueryResult::Rows { columns, rows });
2001 }
2002 };
2003 let mut rows: Vec<Vec<Value>> = Vec::with_capacity(hits.len());
2004 for (key, rid) in hits {
2005 if !start_inclusive {
2006 if let Some(ref s) = start_val {
2007 if &key == s {
2008 continue;
2009 }
2010 }
2011 }
2012 if !end_inclusive {
2013 if let Some(ref e) = end_val {
2014 if &key == e {
2015 continue;
2016 }
2017 }
2018 }
2019 if let Some(data) = tbl.heap.get(rid) {
2020 rows.push(decode_row(schema, &data));
2021 }
2022 }
2023 return Ok(QueryResult::Rows { columns, rows });
2024 }
2025 }
2026
2027 // Fallback: no index — synthesize range predicate and scan.
2028 let fast = FastLayout::new(schema);
2029 let synth = synthesize_range_predicate(column, start, end);
2030 if let Some(compiled) = compile_predicate(&synth, &columns, &fast, schema) {
2031 let mut rows: Vec<Vec<Value>> = Vec::with_capacity(64);
2032 self.catalog
2033 .for_each_row_raw(table, |_rid, data| {
2034 if compiled(data) {
2035 rows.push(decode_row(schema, data));
2036 }
2037 })
2038 .map_err(|e| QueryError::StorageError(e.to_string()))?;
2039 return Ok(QueryResult::Rows { columns, rows });
2040 }
2041
2042 let col_idx =
2043 schema
2044 .column_index(column)
2045 .ok_or_else(|| QueryError::ColumnNotFound {
2046 table: String::new(),
2047 column: column.clone(),
2048 })?;
2049 let rows: Vec<Vec<Value>> = tbl
2050 .scan()
2051 .filter(|(_, row)| {
2052 range_matches(
2053 &row[col_idx],
2054 &start_val,
2055 start_inclusive,
2056 &end_val,
2057 end_inclusive,
2058 )
2059 })
2060 .map(|(_, row)| row)
2061 .collect();
2062 Ok(QueryResult::Rows { columns, rows })
2063 }
2064 }
2065 }
2066
2067 // ─── Materialized view operations ──────────────────────────────────────
2068
2069 /// Create a materialized view: execute the source query, store results
2070 /// in a new backing table, and register the view.
2071 fn create_view(&mut self, name: &str, query_text: &str) -> Result<(), QueryError> {
2072 if self.view_registry.is_view(name) {
2073 return Err(QueryError::ViewError(format!(
2074 "materialized view '{name}' already exists"
2075 )));
2076 }
2077 // Execute the source query to get the result set.
2078 let result = self.execute_powql(query_text)?;
2079 let (columns, rows) = match result {
2080 QueryResult::Rows { columns, rows } => (columns, rows),
2081 _ => return Err("view source query must be a SELECT".into()),
2082 };
2083 // Derive a schema for the backing table from the query result columns.
2084 let schema = self.derive_view_schema(name, &columns, &rows);
2085 // Create the backing table and insert the result rows.
2086 self.catalog
2087 .create_table(schema)
2088 .map_err(|e| QueryError::StorageError(e.to_string()))?;
2089 for row in &rows {
2090 self.catalog
2091 .insert(name, row)
2092 .map_err(|e| QueryError::StorageError(e.to_string()))?;
2093 }
2094 // Determine which base tables this view depends on by parsing the query.
2095 let depends_on = self.extract_view_deps(query_text);
2096 self.view_registry
2097 .register(ViewDef {
2098 name: name.to_string(),
2099 query: query_text.to_string(),
2100 depends_on,
2101 dirty: false,
2102 })
2103 .map_err(|e| QueryError::StorageError(e.to_string()))?;
2104 Ok(())
2105 }
2106
2107 /// Refresh a materialized view: re-execute its source query and replace
2108 /// the backing table's contents.
2109 fn refresh_view(&mut self, name: &str) -> Result<(), QueryError> {
2110 let def = self
2111 .view_registry
2112 .get(name)
2113 .ok_or_else(|| format!("materialized view '{name}' not found"))?;
2114 let query_text = def.query.clone();
2115 // Execute the source query.
2116 let result = self.execute_powql(&query_text)?;
2117 let (_columns, rows) = match result {
2118 QueryResult::Rows { columns, rows } => (columns, rows),
2119 _ => return Err("view source query must be a SELECT".into()),
2120 };
2121 // Clear old data and insert fresh results. Mission B2: logged
2122 // variant — view refreshes are a mutation and crash recovery
2123 // must see them.
2124 self.catalog
2125 .scan_delete_matching_logged(name, |_| true)
2126 .map_err(|e| QueryError::StorageError(e.to_string()))?;
2127 for row in &rows {
2128 self.catalog
2129 .insert(name, row)
2130 .map_err(|e| QueryError::StorageError(e.to_string()))?;
2131 }
2132 self.view_registry.mark_clean(name);
2133 Ok(())
2134 }
2135
2136 /// Drop a materialized view: remove the backing table and unregister.
2137 fn drop_view(&mut self, name: &str) -> Result<(), QueryError> {
2138 if !self.view_registry.is_view(name) {
2139 return Err(QueryError::ViewError(format!(
2140 "materialized view '{name}' not found"
2141 )));
2142 }
2143 self.view_registry
2144 .unregister(name)
2145 .map_err(|e| QueryError::StorageError(e.to_string()))?;
2146 self.catalog
2147 .drop_table(name)
2148 .map_err(|e| QueryError::StorageError(e.to_string()))?;
2149 Ok(())
2150 }
2151
2152 /// Derive a storage `Schema` for a view's backing table from query
2153 /// result column names and the first row's types.
2154 fn derive_view_schema(&self, name: &str, columns: &[String], rows: &[Vec<Value>]) -> Schema {
2155 use powdb_storage::types::{ColumnDef, TypeId};
2156 let cols: Vec<ColumnDef> = columns
2157 .iter()
2158 .enumerate()
2159 .map(|(i, col_name)| {
2160 let type_id = rows
2161 .first()
2162 .and_then(|row| row.get(i))
2163 .map(|v| v.type_id())
2164 .unwrap_or(TypeId::Str);
2165 ColumnDef {
2166 name: col_name.clone(),
2167 type_id,
2168 required: false,
2169 position: i as u16,
2170 }
2171 })
2172 .collect();
2173 Schema {
2174 table_name: name.to_string(),
2175 columns: cols,
2176 }
2177 }
2178
2179 /// Extract base table dependencies from a view's source query by
2180 /// parsing it and collecting the source table name.
2181 fn extract_view_deps(&self, query_text: &str) -> Vec<String> {
2182 use crate::parser::parse;
2183 match parse(query_text) {
2184 Ok(Statement::Query(q)) => {
2185 let mut deps = vec![q.source.clone()];
2186 for j in &q.joins {
2187 deps.push(j.source.clone());
2188 }
2189 deps
2190 }
2191 _ => Vec::new(),
2192 }
2193 }
2194
2195 // ─── Specialized fast paths ─────────────────────────────────────────────
2196 //
2197 // These methods are helpers for the `execute_plan` match arms above.
2198 // Each returns `Ok(Some(result))` when the fast path fires, `Ok(None)`
2199 // when the shape isn't supported (caller falls back to generic code).
2200
2201 /// Aggregate sum/avg/min/max over a single fixed-size i64 column, with
2202 /// an optional compiled filter predicate. Walks raw row bytes — zero
2203 /// per-row allocation. Uses i128 accumulator for sum/avg overflow safety.
2204 pub(super) fn agg_single_col_fast(
2205 &self,
2206 table: &str,
2207 col: &str,
2208 function: AggFunc,
2209 predicate: Option<&Expr>,
2210 ) -> Result<Option<QueryResult>, QueryError> {
2211 let schema = self
2212 .catalog
2213 .schema(table)
2214 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
2215 .clone();
2216 let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
2217 let col_idx = match schema.column_index(col) {
2218 Some(i) => i,
2219 None => return Ok(None),
2220 };
2221 // Only fast-path fixed-size numeric columns (Int/Float) for
2222 // sum/avg/min/max/count. Mission D10: Float parity — prior version
2223 // bailed on Float columns, forcing them through the generic row-
2224 // decoding path that allocated a Vec<Value> per row and dispatched
2225 // on Value::cmp for every compare. f64 decode is structurally the
2226 // same as i64 (load 8 bytes, cast), so the fast path handles both.
2227 let col_type = schema.columns[col_idx].type_id;
2228 if col_type != TypeId::Int && col_type != TypeId::Float {
2229 return Ok(None);
2230 }
2231
2232 let fast = FastLayout::new(&schema);
2233 // Mission C Phase 20b: inline the numeric-column reader instead of
2234 // building a `Box<dyn Fn>`. Eliminates 100K vtable dispatches per
2235 // 100K-row agg scan — every reader call folds directly into the
2236 // hot loop below.
2237 let byte_offset = match fast.fixed_offsets[col_idx] {
2238 Some(o) => o,
2239 None => return Ok(None),
2240 };
2241 let bitmap_byte = col_idx / 8;
2242 let bitmap_bit = (col_idx % 8) as u32;
2243 let body_data_offset = 2 + fast.bitmap_size + byte_offset;
2244
2245 // Optional compiled filter.
2246 let compiled_pred: Option<CompiledPredicate> = match predicate {
2247 Some(pred) => match compile_predicate(pred, &columns, &fast, &schema) {
2248 Some(c) => Some(c),
2249 None => return Ok(None), // let generic path handle it
2250 },
2251 None => None,
2252 };
2253
2254 // Mission C Phase 20b: specialize the inner loop per aggregate
2255 // function. The previous version ran a `match function { ... }`
2256 // *inside* the closure, which kept LLVM from producing optimal
2257 // scalar code for each variant (agg_max regressed ~23% vs the
2258 // baseline Box<dyn Fn> version even though per-row vtable cost
2259 // should have been strictly lower). Pushing the match out of the
2260 // hot loop lets each specialized body fold cleanly into
2261 // `for_each_row_raw` and removes a captured `AggFunc` + match
2262 // dispatch per row.
2263 //
2264 // Mission D10: same specialisation applies to the Float branch.
2265 // For Min/Max we use `f64::total_cmp` so the result matches
2266 // `Value::Ord` — this is the same ordering ORDER BY and the
2267 // top-N sort fast path use, keeping semantics consistent across
2268 // read paths (NaN compares as greatest, -0.0 < +0.0 for
2269 // deterministic tie-breaking).
2270 //
2271 // Mission D11 Phase 1: each inner loop now splits on presence of
2272 // a predicate (`if let Some(pred) = &compiled_pred`) so the hot
2273 // body never re-tests `Option` per row, and reads column bytes
2274 // via `read_i64_unchecked` / `read_f64_unchecked` helpers that
2275 // drop two bounds checks per row (null bitmap byte + value
2276 // slice). Safety is carried by the `FastLayout` invariant that
2277 // `data_offset + 8 <= row_len` for any fixed-size column; see
2278 // the helper doc comments. Hot loops are macro-generated so the
2279 // with-pred / no-pred split can't drift between variants.
2280 let result = match col_type {
2281 TypeId::Int => match function {
2282 AggFunc::Sum | AggFunc::Avg => {
2283 let mut sum_i128: i128 = 0;
2284 let mut count: i64 = 0;
2285 agg_int_loop!(
2286 self,
2287 table,
2288 compiled_pred,
2289 bitmap_byte,
2290 bitmap_bit,
2291 body_data_offset,
2292 |v: i64| {
2293 count += 1;
2294 sum_i128 += v as i128;
2295 }
2296 );
2297 if matches!(function, AggFunc::Sum) {
2298 let clamped = sum_i128.clamp(i64::MIN as i128, i64::MAX as i128) as i64;
2299 QueryResult::Scalar(Value::Int(clamped))
2300 } else if count == 0 {
2301 QueryResult::Scalar(Value::Empty)
2302 } else {
2303 let avg = (sum_i128 as f64) / (count as f64);
2304 QueryResult::Scalar(Value::Float(avg))
2305 }
2306 }
2307 AggFunc::Min => {
2308 let mut min_v: Option<i64> = None;
2309 agg_int_loop!(
2310 self,
2311 table,
2312 compiled_pred,
2313 bitmap_byte,
2314 bitmap_bit,
2315 body_data_offset,
2316 |v: i64| {
2317 min_v = Some(match min_v {
2318 Some(m) => m.min(v),
2319 None => v,
2320 });
2321 }
2322 );
2323 QueryResult::Scalar(min_v.map(Value::Int).unwrap_or(Value::Empty))
2324 }
2325 AggFunc::Max => {
2326 let mut max_v: Option<i64> = None;
2327 agg_int_loop!(
2328 self,
2329 table,
2330 compiled_pred,
2331 bitmap_byte,
2332 bitmap_bit,
2333 body_data_offset,
2334 |v: i64| {
2335 max_v = Some(match max_v {
2336 Some(m) => m.max(v),
2337 None => v,
2338 });
2339 }
2340 );
2341 QueryResult::Scalar(max_v.map(Value::Int).unwrap_or(Value::Empty))
2342 }
2343 AggFunc::Count => {
2344 let mut count: i64 = 0;
2345 agg_int_loop!(
2346 self,
2347 table,
2348 compiled_pred,
2349 bitmap_byte,
2350 bitmap_bit,
2351 body_data_offset,
2352 |_v: i64| {
2353 count += 1;
2354 }
2355 );
2356 QueryResult::Scalar(Value::Int(count))
2357 }
2358 AggFunc::CountDistinct => {
2359 let mut seen = rustc_hash::FxHashSet::default();
2360 agg_int_loop!(
2361 self,
2362 table,
2363 compiled_pred,
2364 bitmap_byte,
2365 bitmap_bit,
2366 body_data_offset,
2367 |v: i64| {
2368 seen.insert(v);
2369 }
2370 );
2371 QueryResult::Scalar(Value::Int(seen.len() as i64))
2372 }
2373 },
2374 TypeId::Float => match function {
2375 AggFunc::Sum => {
2376 // Use a single f64 accumulator. Naive summation is
2377 // sufficient for MVP parity; if precision becomes an
2378 // issue on long scans we can upgrade to Kahan–Neumaier
2379 // compensated sum (~2x scalar cost, zero error growth).
2380 let mut sum: f64 = 0.0;
2381 agg_float_loop!(
2382 self,
2383 table,
2384 compiled_pred,
2385 bitmap_byte,
2386 bitmap_bit,
2387 body_data_offset,
2388 |v: f64| {
2389 sum += v;
2390 }
2391 );
2392 QueryResult::Scalar(Value::Float(sum))
2393 }
2394 AggFunc::Avg => {
2395 let mut sum: f64 = 0.0;
2396 let mut count: i64 = 0;
2397 agg_float_loop!(
2398 self,
2399 table,
2400 compiled_pred,
2401 bitmap_byte,
2402 bitmap_bit,
2403 body_data_offset,
2404 |v: f64| {
2405 sum += v;
2406 count += 1;
2407 }
2408 );
2409 if count == 0 {
2410 QueryResult::Scalar(Value::Empty)
2411 } else {
2412 QueryResult::Scalar(Value::Float(sum / count as f64))
2413 }
2414 }
2415 AggFunc::Min => {
2416 // `total_cmp` for deterministic NaN handling (matches
2417 // Value::Ord). NaN compares greatest, so Min will
2418 // correctly ignore it in favour of any finite value.
2419 let mut min_v: Option<f64> = None;
2420 agg_float_loop!(
2421 self,
2422 table,
2423 compiled_pred,
2424 bitmap_byte,
2425 bitmap_bit,
2426 body_data_offset,
2427 |v: f64| {
2428 min_v = Some(match min_v {
2429 Some(m) => {
2430 if v.total_cmp(&m).is_lt() {
2431 v
2432 } else {
2433 m
2434 }
2435 }
2436 None => v,
2437 });
2438 }
2439 );
2440 QueryResult::Scalar(min_v.map(Value::Float).unwrap_or(Value::Empty))
2441 }
2442 AggFunc::Max => {
2443 let mut max_v: Option<f64> = None;
2444 agg_float_loop!(
2445 self,
2446 table,
2447 compiled_pred,
2448 bitmap_byte,
2449 bitmap_bit,
2450 body_data_offset,
2451 |v: f64| {
2452 max_v = Some(match max_v {
2453 Some(m) => {
2454 if v.total_cmp(&m).is_gt() {
2455 v
2456 } else {
2457 m
2458 }
2459 }
2460 None => v,
2461 });
2462 }
2463 );
2464 QueryResult::Scalar(max_v.map(Value::Float).unwrap_or(Value::Empty))
2465 }
2466 AggFunc::Count => {
2467 let mut count: i64 = 0;
2468 agg_float_loop!(
2469 self,
2470 table,
2471 compiled_pred,
2472 bitmap_byte,
2473 bitmap_bit,
2474 body_data_offset,
2475 |_v: f64| {
2476 count += 1;
2477 }
2478 );
2479 QueryResult::Scalar(Value::Int(count))
2480 }
2481 AggFunc::CountDistinct => {
2482 // Hash on `f64::to_bits` — matches `Value::Hash`, so
2483 // distinct NaN bit patterns count as distinct and
2484 // -0.0/+0.0 count as distinct. Consistent with how
2485 // Float values are hashed in every other DISTINCT /
2486 // GROUP BY path.
2487 let mut seen = rustc_hash::FxHashSet::default();
2488 agg_float_loop!(
2489 self,
2490 table,
2491 compiled_pred,
2492 bitmap_byte,
2493 bitmap_bit,
2494 body_data_offset,
2495 |v: f64| {
2496 seen.insert(v.to_bits());
2497 }
2498 );
2499 QueryResult::Scalar(Value::Int(seen.len() as i64))
2500 }
2501 },
2502 _ => unreachable!("type guard above restricts to Int/Float"),
2503 };
2504 Ok(Some(result))
2505 }
2506
2507 /// `Project(Limit(Filter(SeqScan)))` and `Project(Limit(SeqScan))`.
2508 /// Streams rows, decodes only projected columns, stops at the limit.
2509 pub(super) fn project_filter_limit_fast(
2510 &self,
2511 table: &str,
2512 fields: &[ProjectField],
2513 limit: usize,
2514 predicate: Option<&Expr>,
2515 ) -> Result<Option<QueryResult>, QueryError> {
2516 let schema = self
2517 .catalog
2518 .schema(table)
2519 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
2520 .clone();
2521 let all_columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
2522
2523 // Each projection field must be a simple `.field` reference for this
2524 // fast path. Aliased or computed fields fall through.
2525 let mut proj_indices: Vec<usize> = Vec::with_capacity(fields.len());
2526 let mut proj_columns: Vec<String> = Vec::with_capacity(fields.len());
2527 for f in fields {
2528 let name = match &f.expr {
2529 Expr::Field(n) => n.clone(),
2530 _ => return Ok(None),
2531 };
2532 let idx = match all_columns.iter().position(|c| c == &name) {
2533 Some(i) => i,
2534 None => return Ok(None),
2535 };
2536 proj_indices.push(idx);
2537 proj_columns.push(f.alias.clone().unwrap_or(name));
2538 }
2539
2540 let fast = FastLayout::new(&schema);
2541 let row_layout = RowLayout::new(&schema);
2542
2543 let compiled_pred: Option<CompiledPredicate> = match predicate {
2544 Some(pred) => match compile_predicate(pred, &all_columns, &fast, &schema) {
2545 Some(c) => Some(c),
2546 None => return Ok(None),
2547 },
2548 None => None,
2549 };
2550
2551 let mut out: Vec<Vec<Value>> = Vec::with_capacity(limit.min(1024));
2552 // Mission D2: use try_for_each_row_raw to actually stop iterating
2553 // once the limit is reached. The previous `done` flag only short-
2554 // circuited the closure body, so a `limit 100` over 100K rows still
2555 // walked all 100K slots — burning ~30x SQLite on scan_filter_project_top100.
2556 self.catalog
2557 .try_for_each_row_raw(table, |_rid, data| {
2558 use std::ops::ControlFlow;
2559 if let Some(ref pred) = compiled_pred {
2560 if !pred(data) {
2561 return ControlFlow::Continue(());
2562 }
2563 }
2564 let row: Vec<Value> = proj_indices
2565 .iter()
2566 .map(|&ci| decode_column(&schema, &row_layout, data, ci))
2567 .collect();
2568 out.push(row);
2569 if out.len() >= limit {
2570 ControlFlow::Break(())
2571 } else {
2572 ControlFlow::Continue(())
2573 }
2574 })
2575 .map_err(|e| QueryError::StorageError(e.to_string()))?;
2576
2577 Ok(Some(QueryResult::Rows {
2578 columns: proj_columns,
2579 rows: out,
2580 }))
2581 }
2582
2583 /// `Project(Limit(Sort(Filter(SeqScan))))` and `Project(Limit(Sort(SeqScan)))`.
2584 /// Bounded top-N heap over the sort key. Only the sort key needs to be
2585 /// read per row; projected columns are decoded only for the final
2586 /// winning rows when the heap drains.
2587 pub(super) fn project_filter_sort_limit_fast(
2588 &self,
2589 table: &str,
2590 fields: &[ProjectField],
2591 sort_field: &str,
2592 descending: bool,
2593 limit: usize,
2594 predicate: Option<&Expr>,
2595 ) -> Result<Option<QueryResult>, QueryError> {
2596 if limit == 0 {
2597 // Degenerate case — empty result. Let the generic path handle it
2598 // for proper column naming.
2599 return Ok(None);
2600 }
2601 // The top-N heaps never hold more than `limit` rows, but `limit` is an
2602 // attacker-supplied literal (`order .x limit 99999999999`). Reserving
2603 // that capacity up front would allocate gigabytes and abort the
2604 // process before a single row is read. Cap the pre-allocation; the
2605 // heaps still grow on demand up to the true `limit`.
2606 const TOPN_PREALLOC_CAP: usize = 4096;
2607 let prealloc = limit.min(TOPN_PREALLOC_CAP);
2608 let schema = self
2609 .catalog
2610 .schema(table)
2611 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?
2612 .clone();
2613 let all_columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
2614
2615 // Sort key must be a fixed-size numeric column (Int or Float).
2616 // Mission D10: extended from Int-only. Float sort keys use a
2617 // sortable-u64 transform (see `f64_to_sortable_u64`) so the heap
2618 // path stays keyed on `u64` and the whole branch shape is
2619 // identical to the Int case — no new heap types, no `total_cmp`
2620 // closures in the hot loop.
2621 let sort_idx = match schema.column_index(sort_field) {
2622 Some(i) => i,
2623 None => return Ok(None),
2624 };
2625 let sort_col_type = schema.columns[sort_idx].type_id;
2626 if sort_col_type != TypeId::Int && sort_col_type != TypeId::Float {
2627 return Ok(None);
2628 }
2629
2630 // Each projection field must be a simple `.field`.
2631 let mut proj_indices: Vec<usize> = Vec::with_capacity(fields.len());
2632 let mut proj_columns: Vec<String> = Vec::with_capacity(fields.len());
2633 for f in fields {
2634 let name = match &f.expr {
2635 Expr::Field(n) => n.clone(),
2636 _ => return Ok(None),
2637 };
2638 let idx = match all_columns.iter().position(|c| c == &name) {
2639 Some(i) => i,
2640 None => return Ok(None),
2641 };
2642 proj_indices.push(idx);
2643 proj_columns.push(f.alias.clone().unwrap_or(name));
2644 }
2645
2646 let fast = FastLayout::new(&schema);
2647 let row_layout = RowLayout::new(&schema);
2648 // Mission C Phase 20b: inline numeric-column reader (no Box<dyn Fn>).
2649 let sort_byte_offset = match fast.fixed_offsets[sort_idx] {
2650 Some(o) => o,
2651 None => return Ok(None),
2652 };
2653 let sort_bitmap_byte = sort_idx / 8;
2654 let sort_bitmap_bit = (sort_idx % 8) as u32;
2655 let sort_body_data_offset = 2 + fast.bitmap_size + sort_byte_offset;
2656
2657 let compiled_pred: Option<CompiledPredicate> = match predicate {
2658 Some(pred) => match compile_predicate(pred, &all_columns, &fast, &schema) {
2659 Some(c) => Some(c),
2660 None => return Ok(None),
2661 },
2662 None => None,
2663 };
2664
2665 // Bounded top-N heap. For `order .x desc limit N`, we want the N
2666 // largest values — use a min-heap so the smallest is at the top and
2667 // can be popped when a better candidate arrives. For ascending, use
2668 // a max-heap. We tie-break with a monotonic `seq` counter so the
2669 // result is deterministic and stable.
2670 //
2671 // To keep this simple we maintain two typed heaps and pick by
2672 // direction.
2673 let drained: Vec<Vec<u8>> = match sort_col_type {
2674 TypeId::Int => {
2675 let mut seq: u64 = 0;
2676 let mut heap_desc: BinaryHeap<Reverse<(i64, u64, Vec<u8>)>> =
2677 BinaryHeap::with_capacity(prealloc);
2678 let mut heap_asc: BinaryHeap<(i64, u64, Vec<u8>)> =
2679 BinaryHeap::with_capacity(prealloc);
2680
2681 self.catalog
2682 .for_each_row_raw(table, |_rid, data| {
2683 if let Some(ref pred) = compiled_pred {
2684 if !pred(data) {
2685 return;
2686 }
2687 }
2688 // Inlined int-column reader: null check + i64 decode.
2689 let base = row_body_base(data);
2690 let sort_data_offset = base + sort_body_data_offset;
2691 if data.len() < sort_data_offset + 8
2692 || data.len() <= base + 2 + sort_bitmap_byte
2693 {
2694 return;
2695 }
2696 let is_null =
2697 (data[base + 2 + sort_bitmap_byte] >> sort_bitmap_bit) & 1 == 1;
2698 if is_null {
2699 return;
2700 }
2701 let key = i64::from_le_bytes(
2702 data[sort_data_offset..sort_data_offset + 8]
2703 .try_into()
2704 .unwrap_or_else(|_| unreachable!()),
2705 );
2706 let id = seq;
2707 seq += 1;
2708
2709 if descending {
2710 if heap_desc.len() < limit {
2711 heap_desc.push(Reverse((key, id, data.to_vec())));
2712 } else if let Some(Reverse((top_key, _, _))) = heap_desc.peek() {
2713 if key > *top_key {
2714 heap_desc.pop();
2715 heap_desc.push(Reverse((key, id, data.to_vec())));
2716 }
2717 }
2718 } else if heap_asc.len() < limit {
2719 heap_asc.push((key, id, data.to_vec()));
2720 } else if let Some((top_key, _, _)) = heap_asc.peek() {
2721 if key < *top_key {
2722 heap_asc.pop();
2723 heap_asc.push((key, id, data.to_vec()));
2724 }
2725 }
2726 })
2727 .map_err(|e| QueryError::StorageError(e.to_string()))?;
2728
2729 let mut drained: Vec<(i64, u64, Vec<u8>)> = if descending {
2730 heap_desc.into_iter().map(|Reverse(t)| t).collect()
2731 } else {
2732 heap_asc.into_iter().collect()
2733 };
2734 if descending {
2735 drained.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
2736 } else {
2737 drained.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
2738 }
2739 drained.into_iter().map(|(_, _, d)| d).collect()
2740 }
2741 TypeId::Float => {
2742 // Novel angle: rather than introducing a `TotalF64` newtype
2743 // with `Ord via total_cmp`, transform the f64 bit pattern
2744 // into a sortable `u64` so `BinaryHeap<u64>` orders exactly
2745 // like `f64::total_cmp` would. Classic trick: flip the sign
2746 // bit on positives, flip all bits on negatives. Result:
2747 // - NaN (sign=0) stays greatest, matching total_cmp
2748 // - -0.0 sorts before +0.0, matching total_cmp
2749 // - Hot loop is branch-cheap (one compare + one xor)
2750 let mut seq: u64 = 0;
2751 let mut heap_desc: BinaryHeap<Reverse<(u64, u64, Vec<u8>)>> =
2752 BinaryHeap::with_capacity(prealloc);
2753 let mut heap_asc: BinaryHeap<(u64, u64, Vec<u8>)> =
2754 BinaryHeap::with_capacity(prealloc);
2755
2756 self.catalog
2757 .for_each_row_raw(table, |_rid, data| {
2758 if let Some(ref pred) = compiled_pred {
2759 if !pred(data) {
2760 return;
2761 }
2762 }
2763 let base = row_body_base(data);
2764 let sort_data_offset = base + sort_body_data_offset;
2765 if data.len() < sort_data_offset + 8
2766 || data.len() <= base + 2 + sort_bitmap_byte
2767 {
2768 return;
2769 }
2770 let is_null =
2771 (data[base + 2 + sort_bitmap_byte] >> sort_bitmap_bit) & 1 == 1;
2772 if is_null {
2773 return;
2774 }
2775 let bits = u64::from_le_bytes(
2776 data[sort_data_offset..sort_data_offset + 8]
2777 .try_into()
2778 .unwrap_or_else(|_| unreachable!()),
2779 );
2780 let key = f64_bits_to_sortable_u64(bits);
2781 let id = seq;
2782 seq += 1;
2783
2784 if descending {
2785 if heap_desc.len() < limit {
2786 heap_desc.push(Reverse((key, id, data.to_vec())));
2787 } else if let Some(Reverse((top_key, _, _))) = heap_desc.peek() {
2788 if key > *top_key {
2789 heap_desc.pop();
2790 heap_desc.push(Reverse((key, id, data.to_vec())));
2791 }
2792 }
2793 } else if heap_asc.len() < limit {
2794 heap_asc.push((key, id, data.to_vec()));
2795 } else if let Some((top_key, _, _)) = heap_asc.peek() {
2796 if key < *top_key {
2797 heap_asc.pop();
2798 heap_asc.push((key, id, data.to_vec()));
2799 }
2800 }
2801 })
2802 .map_err(|e| QueryError::StorageError(e.to_string()))?;
2803
2804 let mut drained: Vec<(u64, u64, Vec<u8>)> = if descending {
2805 heap_desc.into_iter().map(|Reverse(t)| t).collect()
2806 } else {
2807 heap_asc.into_iter().collect()
2808 };
2809 if descending {
2810 drained.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
2811 } else {
2812 drained.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
2813 }
2814 drained.into_iter().map(|(_, _, d)| d).collect()
2815 }
2816 _ => unreachable!("type guard above restricts to Int/Float"),
2817 };
2818
2819 let rows: Vec<Vec<Value>> = drained
2820 .into_iter()
2821 .map(|data| {
2822 proj_indices
2823 .iter()
2824 .map(|&ci| decode_column(&schema, &row_layout, &data, ci))
2825 .collect()
2826 })
2827 .collect();
2828
2829 Ok(Some(QueryResult::Rows {
2830 columns: proj_columns,
2831 rows,
2832 }))
2833 }
2834
2835 /// Gather the RowIds that a mutation should operate on, without
2836 /// materialising the full row set. Handles the shapes the planner emits
2837 /// for update/delete: SeqScan, IndexScan, and Filter(SeqScan). Other
2838 /// shapes fall back to `generic_rid_match`.
2839 ///
2840 /// Perf sprint: try to fuse the predicate evaluation and in-place
2841 /// byte-level mutation into a single heap walk. Returns `Some(result)`
2842 /// if the fused path fired, `None` to fall through to the generic
2843 /// two-pass code.
2844 ///
2845 /// Covers two shapes:
2846 /// 1. Fixed-width non-null literal assignments on non-indexed columns
2847 /// → byte-patch every matched row in place (row length unchanged).
2848 /// 2. Single var-col literal assignment on a non-indexed column
2849 /// → `patch_var_column_in_place` on every matched row (may shrink);
2850 /// rows that can't be patched in place are collected for fallback.
2851 fn try_fused_scan_update(
2852 &mut self,
2853 table: &str,
2854 predicate: &Expr,
2855 resolved: &[(usize, Value)],
2856 changed_cols: &[usize],
2857 ) -> Option<Result<QueryResult, QueryError>> {
2858 // Build compiled predicate. Requires a schema borrow that must be
2859 // dropped before we call scan_patch_matching_logged.
2860 let compiled = {
2861 let schema = self.catalog.schema(table)?;
2862 let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
2863 let fast = FastLayout::new(schema);
2864 compile_predicate(predicate, &columns, &fast, schema)?
2865 };
2866
2867 // ── Path 1: fixed-width fast patch ──────────────────────────
2868 let fixed_patches: Option<Vec<FastPatch>> = {
2869 let tbl = self.catalog.get_table(table)?;
2870 let schema = &tbl.schema;
2871 let all_fixed_nonnull = resolved
2872 .iter()
2873 .all(|(idx, val)| is_fixed_size(schema.columns[*idx].type_id) && !val.is_empty());
2874 let no_indexed = !resolved.iter().any(|(idx, _)| tbl.has_indexed_col(*idx));
2875 if all_fixed_nonnull && no_indexed {
2876 let layout = RowLayout::new(schema);
2877 let bitmap_size = layout.bitmap_size();
2878 Some(
2879 resolved
2880 .iter()
2881 .map(|(idx, val)| {
2882 let fixed_off = layout
2883 .fixed_offset(*idx)
2884 .expect("is_fixed_size already checked");
2885 let field_off = 2 + bitmap_size + fixed_off;
2886 let bytes: FixedBytes = match val {
2887 Value::Int(v) => FixedBytes::I64(v.to_le_bytes()),
2888 Value::Float(v) => FixedBytes::F64(v.to_le_bytes()),
2889 Value::Bool(v) => FixedBytes::Bool(if *v { 1 } else { 0 }),
2890 Value::DateTime(v) => FixedBytes::I64(v.to_le_bytes()),
2891 Value::Uuid(v) => FixedBytes::Uuid(*v),
2892 _ => unreachable!("all_fixed_nonnull guard"),
2893 };
2894 FastPatch {
2895 field_off,
2896 bitmap_byte_off: 2 + idx / 8,
2897 bit_mask: 1u8 << (idx % 8),
2898 bytes,
2899 }
2900 })
2901 .collect(),
2902 )
2903 } else {
2904 None
2905 }
2906 };
2907 if let Some(patches) = fixed_patches {
2908 let result = self
2909 .catalog
2910 .scan_patch_matching_logged(table, compiled, |row| {
2911 let base = row_body_base(row);
2912 for p in &patches {
2913 row[base + p.bitmap_byte_off] &= !p.bit_mask;
2914 let field_bytes = p.bytes.as_slice();
2915 row[base + p.field_off..base + p.field_off + field_bytes.len()]
2916 .copy_from_slice(field_bytes);
2917 }
2918 Some(row.len() as u16)
2919 })
2920 .map_err(|e| e.to_string());
2921 match result {
2922 Ok((count, _)) => {
2923 self.view_registry.mark_dependents_dirty(table);
2924 return Some(Ok(QueryResult::Modified(count)));
2925 }
2926 Err(e) => return Some(Err(QueryError::Execution(e))),
2927 }
2928 }
2929
2930 // ── Path 2: single var-col shrink fast patch ────────────────
2931 let var_patch: Option<(usize, Option<Vec<u8>>)> = {
2932 let tbl = self.catalog.get_table(table)?;
2933 let schema = &tbl.schema;
2934 let is_single = resolved.len() == 1;
2935 let is_var = is_single && !is_fixed_size(schema.columns[resolved[0].0].type_id);
2936 let no_indexed = !resolved.iter().any(|(idx, _)| tbl.has_indexed_col(*idx));
2937 if is_single && is_var && no_indexed {
2938 let (idx, val) = &resolved[0];
2939 let bytes_opt = match val {
2940 Value::Str(s) => Some(s.as_bytes().to_vec()),
2941 Value::Bytes(b) => Some(b.clone()),
2942 Value::Empty => None,
2943 _ => return None, // type mismatch, fall through
2944 };
2945 Some((*idx, bytes_opt))
2946 } else {
2947 None
2948 }
2949 };
2950 if let Some((col_idx, ref new_bytes_opt)) = var_patch {
2951 // Build a fresh RowLayout before the mutable borrow.
2952 let layout = {
2953 let schema = self.catalog.schema(table)?;
2954 RowLayout::new(schema)
2955 };
2956 let new_bytes_ref: Option<&[u8]> = new_bytes_opt.as_deref();
2957 let result = self
2958 .catalog
2959 .scan_patch_matching_logged(table, compiled, |row| {
2960 patch_var_column_in_place(row, &layout, col_idx, new_bytes_ref)
2961 })
2962 .map_err(|e| e.to_string());
2963 match result {
2964 Ok((mut count, fallback_rids)) => {
2965 // Handle rows where in-place patch failed (new > old).
2966 for rid in fallback_rids {
2967 let mut row = match self.catalog.get(table, rid) {
2968 Some(r) => r,
2969 None => continue,
2970 };
2971 for (idx, val) in resolved.iter() {
2972 row[*idx] = val.clone();
2973 }
2974 if let Err(e) =
2975 self.catalog
2976 .update_hinted(table, rid, &row, Some(changed_cols))
2977 {
2978 return Some(Err(QueryError::StorageError(e.to_string())));
2979 }
2980 count += 1;
2981 }
2982 self.view_registry.mark_dependents_dirty(table);
2983 return Some(Ok(QueryResult::Modified(count)));
2984 }
2985 Err(e) => return Some(Err(QueryError::Execution(e))),
2986 }
2987 }
2988
2989 None // no fused path applicable — fall through
2990 }
2991
2992 /// Mission C Phase 3: schema is looked up via `self.catalog.schema(table)`
2993 /// inside the branches that actually need it. Previously the caller had
2994 /// to clone the full Schema (6+ String allocs) before every mutation just
2995 /// so this function could borrow it — a cost the update/delete hot path
2996 /// did not need.
2997 fn collect_rids_for_mutation(
2998 &mut self,
2999 input: &PlanNode,
3000 table: &str,
3001 ) -> Result<Vec<RowId>, QueryError> {
3002 match input {
3003 PlanNode::SeqScan { table: t } if t == table => {
3004 // "Update/delete everything" — rare but legal.
3005 let rids: Vec<RowId> = self
3006 .catalog
3007 .scan(table)
3008 .map_err(|e| QueryError::StorageError(e.to_string()))?
3009 .map(|(rid, _)| rid)
3010 .collect();
3011 Ok(rids)
3012 }
3013 PlanNode::IndexScan {
3014 table: t,
3015 column,
3016 key,
3017 } if t == table => {
3018 let key_value = literal_to_value(key)?;
3019
3020 // Indexed case: single lookup, 0 or 1 rows.
3021 // Mission D7: int-specialized fast path on int-keyed indexes
3022 // (primary keys, created_at, etc.) — the common case for
3023 // `update_by_pk` / `delete where id = ?`.
3024 //
3025 // Scope the `tbl` borrow so it's released before we fall
3026 // through to the scan-based paths below (which reborrow
3027 // `self.catalog`).
3028 {
3029 let tbl = self
3030 .catalog
3031 .get_table(table)
3032 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
3033 if tbl.has_index(column) {
3034 let rids = tbl.index_lookup_all(column, &key_value);
3035 return Ok(rids);
3036 }
3037 }
3038
3039 // No index: the planner folds `.col = literal` to IndexScan
3040 // regardless of whether the column is actually unique. When
3041 // there's no index we must behave like Filter(SeqScan) and
3042 // return *all* matching RIDs — not just the first one.
3043 let schema = self
3044 .catalog
3045 .schema(table)
3046 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
3047 let columns: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
3048 let fast = FastLayout::new(schema);
3049 let synth = Expr::BinaryOp(
3050 Box::new(Expr::Field(column.clone())),
3051 BinOp::Eq,
3052 Box::new(key.clone()),
3053 );
3054 if let Some(compiled) = compile_predicate(&synth, &columns, &fast, schema) {
3055 // Mission F: skip the first 4 Vec doublings.
3056 let mut rids: Vec<RowId> = Vec::with_capacity(64);
3057 self.catalog
3058 .for_each_row_raw(table, |rid, data| {
3059 if compiled(data) {
3060 rids.push(rid);
3061 }
3062 })
3063 .map_err(|e| QueryError::StorageError(e.to_string()))?;
3064 return Ok(rids);
3065 }
3066
3067 // Fallback: decode each row, compare values.
3068 let col_idx =
3069 schema
3070 .column_index(column)
3071 .ok_or_else(|| QueryError::ColumnNotFound {
3072 table: String::new(),
3073 column: column.clone(),
3074 })?;
3075 let rids: Vec<RowId> = self
3076 .catalog
3077 .scan(table)
3078 .map_err(|e| QueryError::StorageError(e.to_string()))?
3079 .filter_map(|(rid, row)| {
3080 if row[col_idx] == key_value {
3081 Some(rid)
3082 } else {
3083 None
3084 }
3085 })
3086 .collect();
3087 Ok(rids)
3088 }
3089 PlanNode::Filter {
3090 input: inner,
3091 predicate,
3092 } => {
3093 if let PlanNode::SeqScan { table: t } = inner.as_ref() {
3094 if t != table {
3095 return self.generic_rid_match(input, table);
3096 }
3097 let schema = self
3098 .catalog
3099 .schema(table)
3100 .ok_or_else(|| QueryError::TableNotFound(table.to_string()))?;
3101 let columns: Vec<String> =
3102 schema.columns.iter().map(|c| c.name.clone()).collect();
3103 let fast = FastLayout::new(schema);
3104 let row_layout = RowLayout::new(schema);
3105
3106 // Try compiled predicate first.
3107 if let Some(compiled) = compile_predicate(predicate, &columns, &fast, schema) {
3108 // Mission F: skip the first 4 Vec doublings.
3109 let mut rids: Vec<RowId> = Vec::with_capacity(64);
3110 self.catalog
3111 .for_each_row_raw(table, |rid, data| {
3112 if compiled(data) {
3113 rids.push(rid);
3114 }
3115 })
3116 .map_err(|e| QueryError::StorageError(e.to_string()))?;
3117 return Ok(rids);
3118 }
3119
3120 // Fallback: selective decode + eval.
3121 let pred_cols = predicate_column_indices(predicate, &columns);
3122 let mut rids: Vec<RowId> = Vec::with_capacity(64);
3123 self.catalog
3124 .for_each_row_raw(table, |rid, data| {
3125 let pred_row = decode_selective(schema, &row_layout, data, &pred_cols);
3126 if eval_predicate(predicate, &pred_row, &columns) {
3127 rids.push(rid);
3128 }
3129 })
3130 .map_err(|e| QueryError::StorageError(e.to_string()))?;
3131 return Ok(rids);
3132 }
3133 self.generic_rid_match(input, table)
3134 }
3135 _ => self.generic_rid_match(input, table),
3136 }
3137 }
3138
3139 /// Last-ditch generic match: execute the plan, collect matching rows,
3140 /// then find corresponding RowIds by value equality. This is the old
3141 /// O(N*M) code path; only used when the plan shape is something exotic.
3142 fn generic_rid_match(
3143 &mut self,
3144 input: &PlanNode,
3145 table: &str,
3146 ) -> Result<Vec<RowId>, QueryError> {
3147 let result = self.execute_plan(input)?;
3148 let rows = match result {
3149 QueryResult::Rows { rows, .. } => rows,
3150 _ => return Err("mutation source must be rows".into()),
3151 };
3152 let matching: Vec<RowId> = self
3153 .catalog
3154 .scan(table)
3155 .map_err(|e| QueryError::StorageError(e.to_string()))?
3156 .filter(|(_, row)| rows.iter().any(|r| r == row))
3157 .map(|(rid, _)| rid)
3158 .collect();
3159 Ok(matching)
3160 }
3161}
3162
3163pub(super) fn execute_window(
3164 result: QueryResult,
3165 windows: &[WindowDef],
3166) -> Result<QueryResult, QueryError> {
3167 let (mut columns, mut rows) = match result {
3168 QueryResult::Rows { columns, rows } => (columns, rows),
3169 _ => return Err("window function requires row input".into()),
3170 };
3171
3172 for wdef in windows {
3173 // Resolve partition/order column indices against current columns.
3174 let part_indices: Vec<usize> = wdef
3175 .partition_by
3176 .iter()
3177 .map(|name| {
3178 columns
3179 .iter()
3180 .position(|c| c == name)
3181 .ok_or_else(|| format!("window partition column '{name}' not found"))
3182 })
3183 .collect::<Result<Vec<_>, _>>()?;
3184
3185 let ord_indices: Vec<(usize, bool)> = wdef
3186 .order_by
3187 .iter()
3188 .map(|sk| {
3189 columns
3190 .iter()
3191 .position(|c| c == &sk.field)
3192 .map(|i| (i, sk.descending))
3193 .ok_or_else(|| format!("window order column '{}' not found", sk.field))
3194 })
3195 .collect::<Result<Vec<_>, _>>()?;
3196
3197 // Resolve the argument column index (for aggregate windows).
3198 let arg_col_idx: Option<usize> = if let Some(arg) = wdef.args.first() {
3199 match arg {
3200 Expr::Field(name) => {
3201 if name == "*" {
3202 None // count(*) style — no specific column
3203 } else {
3204 Some(
3205 columns
3206 .iter()
3207 .position(|c| c == name)
3208 .ok_or_else(|| format!("window arg column '{name}' not found"))?,
3209 )
3210 }
3211 }
3212 _ => None,
3213 }
3214 } else {
3215 None
3216 };
3217
3218 // Build a sort-index to sort rows by partition_by then order_by
3219 // without actually reordering the original Vec (we need original
3220 // order to write results back).
3221 let n = rows.len();
3222 let mut indices: Vec<usize> = (0..n).collect();
3223 indices.sort_by(|&a, &b| {
3224 // Compare partition keys first.
3225 for &pi in &part_indices {
3226 let cmp = rows[a][pi].cmp(&rows[b][pi]);
3227 if cmp != std::cmp::Ordering::Equal {
3228 return cmp;
3229 }
3230 }
3231 // Then order keys.
3232 for &(oi, desc) in &ord_indices {
3233 let cmp = rows[a][oi].cmp(&rows[b][oi]);
3234 if cmp != std::cmp::Ordering::Equal {
3235 return if desc { cmp.reverse() } else { cmp };
3236 }
3237 }
3238 std::cmp::Ordering::Equal
3239 });
3240
3241 // SQL window-frame semantics: with no `order` clause the frame for an
3242 // aggregate window is the ENTIRE partition, not the running prefix.
3243 // The loop below computes running values; for the no-order case we
3244 // back-fill every row of a partition with the partition's final
3245 // (i.e. complete) aggregate once its boundary is reached. Ranking
3246 // functions are untouched — row_number/rank/dense_rank are inherently
3247 // positional.
3248 let whole_partition_frame = wdef.order_by.is_empty()
3249 && matches!(
3250 wdef.function,
3251 WindowFunc::Sum
3252 | WindowFunc::Avg
3253 | WindowFunc::Count
3254 | WindowFunc::Min
3255 | WindowFunc::Max
3256 );
3257 // Original row indices of the partition currently being scanned
3258 // (only tracked when back-filling is needed).
3259 let mut partition_row_indices: Vec<usize> = Vec::new();
3260
3261 // Compute window values in sorted order, tracking partition boundaries.
3262 let mut win_values: Vec<Value> = vec![Value::Empty; n];
3263 let mut partition_start = 0usize;
3264 // Running state for aggregate windows:
3265 let mut running_count: i64 = 0;
3266 let mut running_int_sum: i64 = 0;
3267 let mut running_float_sum: f64 = 0.0;
3268 let mut running_saw_float = false;
3269 let mut running_min: Option<Value> = None;
3270 let mut running_max: Option<Value> = None;
3271 let mut rank_counter: i64 = 0;
3272 let mut dense_rank_counter: i64 = 0;
3273 let mut prev_order_key: Option<Vec<Value>> = None;
3274 let mut same_rank_count: i64 = 0;
3275
3276 for sorted_pos in 0..n {
3277 let row_idx = indices[sorted_pos];
3278
3279 // Detect partition boundary.
3280 let new_partition = if sorted_pos == 0 {
3281 true
3282 } else {
3283 let prev_row_idx = indices[sorted_pos - 1];
3284 part_indices
3285 .iter()
3286 .any(|&pi| rows[row_idx][pi] != rows[prev_row_idx][pi])
3287 };
3288
3289 if new_partition {
3290 // No-order aggregate frame: the partition that just ended is
3291 // complete, so its final running value IS the whole-partition
3292 // aggregate. Back-fill it onto every row of that partition.
3293 if whole_partition_frame && sorted_pos > 0 {
3294 let final_v = win_values[indices[sorted_pos - 1]].clone();
3295 for ri in partition_row_indices.drain(..) {
3296 win_values[ri] = final_v.clone();
3297 }
3298 }
3299 partition_start = sorted_pos;
3300 running_count = 0;
3301 running_int_sum = 0;
3302 running_float_sum = 0.0;
3303 running_saw_float = false;
3304 running_min = None;
3305 running_max = None;
3306 rank_counter = 0;
3307 dense_rank_counter = 0;
3308 prev_order_key = None;
3309 same_rank_count = 0;
3310 }
3311
3312 // Extract current order key for rank tracking.
3313 let current_order_key: Vec<Value> = ord_indices
3314 .iter()
3315 .map(|&(oi, _)| rows[row_idx][oi].clone())
3316 .collect();
3317 let same_as_prev = prev_order_key.as_ref() == Some(¤t_order_key);
3318
3319 let value = match wdef.function {
3320 WindowFunc::RowNumber => Value::Int((sorted_pos - partition_start + 1) as i64),
3321 WindowFunc::Rank => {
3322 if same_as_prev {
3323 same_rank_count += 1;
3324 } else {
3325 rank_counter += same_rank_count + 1;
3326 same_rank_count = 0;
3327 if rank_counter == 0 {
3328 rank_counter = 1;
3329 }
3330 }
3331 Value::Int(rank_counter)
3332 }
3333 WindowFunc::DenseRank => {
3334 if !same_as_prev {
3335 dense_rank_counter += 1;
3336 }
3337 Value::Int(dense_rank_counter)
3338 }
3339 WindowFunc::Sum => {
3340 if let Some(ci) = arg_col_idx {
3341 match &rows[row_idx][ci] {
3342 Value::Int(v) => running_int_sum += v,
3343 Value::Float(v) => {
3344 running_float_sum += v;
3345 running_saw_float = true;
3346 }
3347 _ => {}
3348 }
3349 }
3350 if running_saw_float {
3351 Value::Float(running_float_sum + running_int_sum as f64)
3352 } else {
3353 Value::Int(running_int_sum)
3354 }
3355 }
3356 WindowFunc::Avg => {
3357 if let Some(ci) = arg_col_idx {
3358 match &rows[row_idx][ci] {
3359 Value::Int(v) => {
3360 running_float_sum += *v as f64;
3361 running_count += 1;
3362 }
3363 Value::Float(v) => {
3364 running_float_sum += v;
3365 running_count += 1;
3366 }
3367 _ => {}
3368 }
3369 }
3370 if running_count == 0 {
3371 Value::Empty
3372 } else {
3373 Value::Float(running_float_sum / running_count as f64)
3374 }
3375 }
3376 WindowFunc::Count => {
3377 if let Some(ci) = arg_col_idx {
3378 if !rows[row_idx][ci].is_empty() {
3379 running_count += 1;
3380 }
3381 } else {
3382 // count(*) — count all rows
3383 running_count += 1;
3384 }
3385 Value::Int(running_count)
3386 }
3387 WindowFunc::Min => {
3388 if let Some(ci) = arg_col_idx {
3389 let v = &rows[row_idx][ci];
3390 if !v.is_empty() {
3391 running_min = Some(match &running_min {
3392 None => v.clone(),
3393 Some(cur) => {
3394 if v < cur {
3395 v.clone()
3396 } else {
3397 cur.clone()
3398 }
3399 }
3400 });
3401 }
3402 }
3403 running_min.clone().unwrap_or(Value::Empty)
3404 }
3405 WindowFunc::Max => {
3406 if let Some(ci) = arg_col_idx {
3407 let v = &rows[row_idx][ci];
3408 if !v.is_empty() {
3409 running_max = Some(match &running_max {
3410 None => v.clone(),
3411 Some(cur) => {
3412 if v > cur {
3413 v.clone()
3414 } else {
3415 cur.clone()
3416 }
3417 }
3418 });
3419 }
3420 }
3421 running_max.clone().unwrap_or(Value::Empty)
3422 }
3423 };
3424
3425 prev_order_key = Some(current_order_key);
3426 win_values[row_idx] = value;
3427 if whole_partition_frame {
3428 partition_row_indices.push(row_idx);
3429 }
3430 }
3431
3432 // Back-fill the final partition (the loop only flushes at boundaries).
3433 if whole_partition_frame && n > 0 {
3434 let final_v = win_values[indices[n - 1]].clone();
3435 for ri in partition_row_indices.drain(..) {
3436 win_values[ri] = final_v.clone();
3437 }
3438 }
3439
3440 // Append the computed window column to each row.
3441 for (ri, row) in rows.iter_mut().enumerate() {
3442 row.push(win_values[ri].clone());
3443 }
3444 columns.push(wdef.output_name.clone());
3445 }
3446
3447 Ok(QueryResult::Rows { columns, rows })
3448}
3449
3450/// Mission E2b: compute one aggregate over a set of rows in a group.
3451pub(super) fn compute_group_aggregate(
3452 func: AggFunc,
3453 all_rows: &[Vec<Value>],
3454 row_indices: &[usize],
3455 col_idx: usize,
3456) -> Value {
3457 match func {
3458 AggFunc::Count => {
3459 if col_idx == usize::MAX {
3460 // count(*) — count all rows in the group.
3461 return Value::Int(row_indices.len() as i64);
3462 }
3463 let count = row_indices
3464 .iter()
3465 .filter(|&&ri| !all_rows[ri][col_idx].is_empty())
3466 .count();
3467 Value::Int(count as i64)
3468 }
3469 AggFunc::CountDistinct => {
3470 let mut seen = std::collections::HashSet::new();
3471 for &ri in row_indices {
3472 let v = &all_rows[ri][col_idx];
3473 if !v.is_empty() {
3474 seen.insert(v.clone());
3475 }
3476 }
3477 Value::Int(seen.len() as i64)
3478 }
3479 AggFunc::Sum => {
3480 // Mirror the scalar Sum path: accumulate int and float
3481 // contributions separately and promote the final result to
3482 // Float if any Float row was observed. Prevents silent
3483 // drop of Float columns in GROUP BY aggregates.
3484 let mut int_sum: i64 = 0;
3485 let mut float_sum: f64 = 0.0;
3486 let mut saw_float = false;
3487 for &ri in row_indices {
3488 match &all_rows[ri][col_idx] {
3489 Value::Int(v) => int_sum += v,
3490 Value::Float(v) => {
3491 float_sum += *v;
3492 saw_float = true;
3493 }
3494 _ => {}
3495 }
3496 }
3497 if saw_float {
3498 Value::Float(float_sum + int_sum as f64)
3499 } else {
3500 Value::Int(int_sum)
3501 }
3502 }
3503 AggFunc::Avg => {
3504 let mut sum = 0.0f64;
3505 let mut count = 0usize;
3506 for &ri in row_indices {
3507 match &all_rows[ri][col_idx] {
3508 Value::Int(v) => {
3509 sum += *v as f64;
3510 count += 1;
3511 }
3512 Value::Float(v) => {
3513 sum += *v;
3514 count += 1;
3515 }
3516 _ => {}
3517 }
3518 }
3519 if count == 0 {
3520 Value::Empty
3521 } else {
3522 Value::Float(sum / count as f64)
3523 }
3524 }
3525 AggFunc::Min => row_indices
3526 .iter()
3527 .map(|&ri| &all_rows[ri][col_idx])
3528 .filter(|v| !v.is_empty())
3529 .min()
3530 .cloned()
3531 .unwrap_or(Value::Empty),
3532 AggFunc::Max => row_indices
3533 .iter()
3534 .map(|&ri| &all_rows[ri][col_idx])
3535 .filter(|v| !v.is_empty())
3536 .max()
3537 .cloned()
3538 .unwrap_or(Value::Empty),
3539 }
3540}
3541
3542/// Mission E1.3: try to extract equi-join key indices from a join `on`
3543/// predicate. Returns `Some((left_col_idx, right_col_idx))` when the
3544/// predicate is exactly `L = R` (or `R = L`) and both sides resolve
3545/// cleanly — `L` to the left subtree's column list and `R` to the right
3546/// subtree's column list.
3547///
3548/// This is deliberately narrow. We only recognise the two shapes:
3549/// * `QualifiedField = QualifiedField` (`u.id = o.user_id`)
3550/// * `Field = Field` (`.id = .user_id`, unqualified)
3551///
3552/// Anything else — conjunctions, constants, function calls, or predicates
3553/// that touch the same side on both halves — falls through to the
3554/// nested-loop path unchanged.
3555pub(super) fn try_extract_equi_join_keys(
3556 pred: &Expr,
3557 left_columns: &[String],
3558 right_columns: &[String],
3559) -> Option<(usize, usize)> {
3560 let (lhs, op, rhs) = match pred {
3561 Expr::BinaryOp(l, op, r) => (l.as_ref(), *op, r.as_ref()),
3562 _ => return None,
3563 };
3564 if op != BinOp::Eq {
3565 return None;
3566 }
3567 // Normal orientation: lhs in left, rhs in right.
3568 if let (Some(li), Some(ri)) = (
3569 resolve_side_column(lhs, left_columns),
3570 resolve_side_column(rhs, right_columns),
3571 ) {
3572 return Some((li, ri));
3573 }
3574 // Swapped: rhs in left, lhs in right. Both sides of `=` are
3575 // commutative so this is safe.
3576 if let (Some(li), Some(ri)) = (
3577 resolve_side_column(rhs, left_columns),
3578 resolve_side_column(lhs, right_columns),
3579 ) {
3580 return Some((li, ri));
3581 }
3582 None
3583}
3584
3585fn resolve_side_column(expr: &Expr, columns: &[String]) -> Option<usize> {
3586 match expr {
3587 Expr::QualifiedField { qualifier, field } => {
3588 // Byte-level match so we don't allocate a fresh `format!` on
3589 // every call — this runs once per plan, so allocation would be
3590 // cheap, but the match is trivial enough to keep inline with
3591 // the eval_expr version.
3592 let q = qualifier.as_bytes();
3593 let f = field.as_bytes();
3594 columns.iter().position(|c| {
3595 let b = c.as_bytes();
3596 b.len() == q.len() + 1 + f.len()
3597 && b[..q.len()] == *q
3598 && b[q.len()] == b'.'
3599 && b[q.len() + 1..] == *f
3600 })
3601 }
3602 Expr::Field(name) => columns.iter().position(|c| c == name),
3603 _ => None,
3604 }
3605}
3606
3607/// Mission E1.3: O(L + R) hash join. Builds a `FxHashMap<Value, Vec<usize>>`
3608/// over the right (inner) side's join keys, then streams the left (outer)
3609/// side and for each probe row emits every combined row whose right-side
3610/// key matches. For `JoinKind::LeftOuter`, unmatched left rows are emitted
3611/// padded with `Value::Empty` on the right side.
3612///
3613/// The right side is always the build side. That choice is forced for
3614/// LeftOuter (the left side must stream so we can detect orphans), and
3615/// for Inner it's a reasonable default — left-deep plans tend to grow the
3616/// left side with each join, so the un-joined right leaf is often the
3617/// smaller of the two at each level.
3618pub(super) fn hash_join(
3619 left_columns: Vec<String>,
3620 left_rows: Vec<Vec<Value>>,
3621 right_columns: Vec<String>,
3622 right_rows: Vec<Vec<Value>>,
3623 left_key_idx: usize,
3624 right_key_idx: usize,
3625 kind: JoinKind,
3626) -> QueryResult {
3627 use rustc_hash::FxHashMap;
3628
3629 let n_left = left_columns.len();
3630 let n_right = right_columns.len();
3631 let mut columns = Vec::with_capacity(n_left + n_right);
3632 columns.extend(left_columns);
3633 columns.extend(right_columns);
3634
3635 // Build: right_key -> list of right-row indices. Pre-size to the row
3636 // count so the map doesn't rehash mid-build.
3637 let mut build: FxHashMap<Value, Vec<usize>> =
3638 FxHashMap::with_capacity_and_hasher(right_rows.len(), Default::default());
3639 for (i, row) in right_rows.iter().enumerate() {
3640 // Skip Empty keys on the build side — they can never match under
3641 // SQL semantics (NULL ≠ NULL) and would collapse all nullables to
3642 // one bucket.
3643 if matches!(row[right_key_idx], Value::Empty) {
3644 continue;
3645 }
3646 build.entry(row[right_key_idx].clone()).or_default().push(i);
3647 }
3648
3649 // Reasonable starting capacity — inner joins produce ≥ left_rows.len()
3650 // rows in the common 1:1 case, left-outer always emits ≥ left_rows.len().
3651 let mut rows: Vec<Vec<Value>> = Vec::with_capacity(left_rows.len());
3652
3653 for left_row in &left_rows {
3654 let key = &left_row[left_key_idx];
3655 let matched = if matches!(key, Value::Empty) {
3656 None
3657 } else {
3658 build.get(key)
3659 };
3660 match matched {
3661 Some(matches) if !matches.is_empty() => {
3662 for &ri in matches {
3663 let right_row = &right_rows[ri];
3664 let mut combined = Vec::with_capacity(n_left + n_right);
3665 combined.extend_from_slice(left_row);
3666 combined.extend_from_slice(right_row);
3667 rows.push(combined);
3668 }
3669 }
3670 _ => {
3671 if matches!(kind, JoinKind::LeftOuter) {
3672 let mut row = Vec::with_capacity(n_left + n_right);
3673 row.extend_from_slice(left_row);
3674 row.resize(n_left + n_right, Value::Empty);
3675 rows.push(row);
3676 }
3677 }
3678 }
3679 }
3680
3681 QueryResult::Rows { columns, rows }
3682}
3683
3684/// Lower unindexed `RangeScan` and `IndexScan` nodes to `Filter(SeqScan)`
3685/// so that all downstream fast paths (count, project+limit, sort+limit,
3686/// agg, update, delete) continue to fire.
3687///
3688/// The planner emits `RangeScan` (for `.age > 30`) and `IndexScan` (for
3689/// `.email = lit`) speculatively because it has no catalog access. When
3690/// the column has a B-tree index, those plans are correct. When it
3691/// doesn't, the executor's fallbacks materialise every matching row with
3692/// full `decode_row` — bypassing the compiled-predicate fast paths that
3693/// `Filter(SeqScan)` would trigger. Lowering both speculative leaf kinds
3694/// also keeps EXPLAIN honest: it prints the plan that actually runs.
3695///
3696/// This pass runs once per query, before execution.
3697pub(super) fn lower_unindexed_scans(catalog: &Catalog, plan: &PlanNode) -> PlanNode {
3698 match plan {
3699 PlanNode::RangeScan {
3700 table,
3701 column,
3702 start,
3703 end,
3704 } => {
3705 if let Some(tbl) = catalog.get_table(table) {
3706 // Keep RangeScan whenever ANY index exists on the column:
3707 // unique indexes store raw column values, non-unique indexes
3708 // store composite (value, rid) keys that the executor walks
3709 // natively via BTree::range_rids. Only lower to Filter(SeqScan)
3710 // when the column is unindexed.
3711 if tbl.has_index(column) {
3712 return plan.clone();
3713 }
3714 }
3715 let pred = synthesize_range_predicate(column, start, end);
3716 PlanNode::Filter {
3717 input: Box::new(PlanNode::SeqScan {
3718 table: table.clone(),
3719 }),
3720 predicate: pred,
3721 }
3722 }
3723 PlanNode::Filter { input, predicate } => PlanNode::Filter {
3724 input: Box::new(lower_unindexed_scans(catalog, input)),
3725 predicate: predicate.clone(),
3726 },
3727 PlanNode::Project { input, fields } => PlanNode::Project {
3728 input: Box::new(lower_unindexed_scans(catalog, input)),
3729 fields: fields.clone(),
3730 },
3731 PlanNode::Sort { input, keys } => PlanNode::Sort {
3732 input: Box::new(lower_unindexed_scans(catalog, input)),
3733 keys: keys.clone(),
3734 },
3735 PlanNode::Limit { input, count } => PlanNode::Limit {
3736 input: Box::new(lower_unindexed_scans(catalog, input)),
3737 count: count.clone(),
3738 },
3739 PlanNode::Offset { input, count } => PlanNode::Offset {
3740 input: Box::new(lower_unindexed_scans(catalog, input)),
3741 count: count.clone(),
3742 },
3743 PlanNode::Aggregate {
3744 input,
3745 function,
3746 field,
3747 } => PlanNode::Aggregate {
3748 input: Box::new(lower_unindexed_scans(catalog, input)),
3749 function: *function,
3750 field: field.clone(),
3751 },
3752 PlanNode::Distinct { input } => PlanNode::Distinct {
3753 input: Box::new(lower_unindexed_scans(catalog, input)),
3754 },
3755 PlanNode::GroupBy {
3756 input,
3757 keys,
3758 aggregates,
3759 having,
3760 } => PlanNode::GroupBy {
3761 input: Box::new(lower_unindexed_scans(catalog, input)),
3762 keys: keys.clone(),
3763 aggregates: aggregates.clone(),
3764 having: having.clone(),
3765 },
3766 PlanNode::Update {
3767 input,
3768 table,
3769 assignments,
3770 returning,
3771 } => PlanNode::Update {
3772 input: Box::new(lower_unindexed_scans(catalog, input)),
3773 table: table.clone(),
3774 assignments: assignments.clone(),
3775 returning: *returning,
3776 },
3777 PlanNode::Delete {
3778 input,
3779 table,
3780 returning,
3781 } => PlanNode::Delete {
3782 input: Box::new(lower_unindexed_scans(catalog, input)),
3783 table: table.clone(),
3784 returning: *returning,
3785 },
3786 PlanNode::Window { input, windows } => PlanNode::Window {
3787 input: Box::new(lower_unindexed_scans(catalog, input)),
3788 windows: windows.clone(),
3789 },
3790 PlanNode::Union { left, right, all } => PlanNode::Union {
3791 left: Box::new(lower_unindexed_scans(catalog, left)),
3792 right: Box::new(lower_unindexed_scans(catalog, right)),
3793 all: *all,
3794 },
3795 PlanNode::Explain { input } => PlanNode::Explain {
3796 input: Box::new(lower_unindexed_scans(catalog, input)),
3797 },
3798 PlanNode::NestedLoopJoin {
3799 left,
3800 right,
3801 on,
3802 kind,
3803 } => PlanNode::NestedLoopJoin {
3804 left: Box::new(lower_unindexed_scans(catalog, left)),
3805 right: Box::new(lower_unindexed_scans(catalog, right)),
3806 on: on.clone(),
3807 kind: *kind,
3808 },
3809 PlanNode::IndexScan { table, column, key } => {
3810 if let Some(tbl) = catalog.get_table(table) {
3811 if tbl.has_index(column) {
3812 return plan.clone();
3813 }
3814 }
3815 PlanNode::Filter {
3816 input: Box::new(PlanNode::SeqScan {
3817 table: table.clone(),
3818 }),
3819 predicate: Expr::BinaryOp(
3820 Box::new(Expr::Field(column.clone())),
3821 BinOp::Eq,
3822 Box::new(key.clone()),
3823 ),
3824 }
3825 }
3826 // Leaf nodes: no children to recurse into.
3827 _ => plan.clone(),
3828 }
3829}
3830
3831/// Synthesize a range predicate from RangeScan bounds for the fallback path.
3832pub(super) fn synthesize_range_predicate(
3833 column: &str,
3834 start: &Option<(Expr, bool)>,
3835 end: &Option<(Expr, bool)>,
3836) -> Expr {
3837 let lower = start.as_ref().map(|(expr, inclusive)| {
3838 let op = if *inclusive { BinOp::Gte } else { BinOp::Gt };
3839 Expr::BinaryOp(
3840 Box::new(Expr::Field(column.to_string())),
3841 op,
3842 Box::new(expr.clone()),
3843 )
3844 });
3845 let upper = end.as_ref().map(|(expr, inclusive)| {
3846 let op = if *inclusive { BinOp::Lte } else { BinOp::Lt };
3847 Expr::BinaryOp(
3848 Box::new(Expr::Field(column.to_string())),
3849 op,
3850 Box::new(expr.clone()),
3851 )
3852 });
3853 match (lower, upper) {
3854 (Some(l), Some(u)) => Expr::BinaryOp(Box::new(l), BinOp::And, Box::new(u)),
3855 (Some(l), None) => l,
3856 (None, Some(u)) => u,
3857 (None, None) => Expr::Literal(Literal::Bool(true)),
3858 }
3859}
3860
3861/// Check if a value falls within a range (used in last-resort decoded-row eval).
3862pub(super) fn range_matches(
3863 val: &Value,
3864 start: &Option<Value>,
3865 start_inc: bool,
3866 end: &Option<Value>,
3867 end_inc: bool,
3868) -> bool {
3869 if let Some(ref s) = start {
3870 if start_inc {
3871 if val < s {
3872 return false;
3873 }
3874 } else if val <= s {
3875 return false;
3876 }
3877 }
3878 if let Some(ref e) = end {
3879 if end_inc {
3880 if val > e {
3881 return false;
3882 }
3883 } else if val >= e {
3884 return false;
3885 }
3886 }
3887 true
3888}
3889
3890/// Format a `PlanNode` tree as a human-readable, indented text
3891/// representation. Used by the `EXPLAIN` command.
3892pub(super) fn format_plan_tree(plan: &PlanNode, depth: usize) -> String {
3893 let indent = " ".repeat(depth);
3894 match plan {
3895 PlanNode::SeqScan { table } => format!("{indent}SeqScan table={table}"),
3896 PlanNode::AliasScan { table, alias } => {
3897 format!("{indent}AliasScan table={table} alias={alias}")
3898 }
3899 PlanNode::IndexScan { table, column, key } => {
3900 format!("{indent}IndexScan table={table} column={column} key={key:?}")
3901 }
3902 PlanNode::RangeScan {
3903 table,
3904 column,
3905 start,
3906 end,
3907 } => {
3908 let s = match start {
3909 Some((expr, inc)) => {
3910 let op = if *inc { ">=" } else { ">" };
3911 format!("{op}{expr:?}")
3912 }
3913 None => "unbounded".to_string(),
3914 };
3915 let e = match end {
3916 Some((expr, inc)) => {
3917 let op = if *inc { "<=" } else { "<" };
3918 format!("{op}{expr:?}")
3919 }
3920 None => "unbounded".to_string(),
3921 };
3922 format!("{indent}RangeScan table={table} column={column} [{s}, {e}]")
3923 }
3924 PlanNode::Filter { input, predicate } => {
3925 let child = format_plan_tree(input, depth + 1);
3926 format!("{indent}Filter predicate={predicate:?}\n{child}")
3927 }
3928 PlanNode::Project { input, fields } => {
3929 let names: Vec<String> = fields
3930 .iter()
3931 .map(|f| match &f.alias {
3932 Some(a) => format!("{a}: {:?}", f.expr),
3933 None => format!("{:?}", f.expr),
3934 })
3935 .collect();
3936 let child = format_plan_tree(input, depth + 1);
3937 format!("{indent}Project fields=[{}]\n{child}", names.join(", "))
3938 }
3939 PlanNode::Sort { input, keys } => {
3940 let ks: Vec<String> = keys
3941 .iter()
3942 .map(|k| {
3943 if k.descending {
3944 format!("{} desc", k.field)
3945 } else {
3946 k.field.clone()
3947 }
3948 })
3949 .collect();
3950 let child = format_plan_tree(input, depth + 1);
3951 format!("{indent}Sort keys=[{}]\n{child}", ks.join(", "))
3952 }
3953 PlanNode::Limit { input, count } => {
3954 let child = format_plan_tree(input, depth + 1);
3955 format!("{indent}Limit count={count:?}\n{child}")
3956 }
3957 PlanNode::Offset { input, count } => {
3958 let child = format_plan_tree(input, depth + 1);
3959 format!("{indent}Offset count={count:?}\n{child}")
3960 }
3961 PlanNode::Aggregate {
3962 input,
3963 function,
3964 field,
3965 } => {
3966 let f = field.as_deref().unwrap_or("*");
3967 let child = format_plan_tree(input, depth + 1);
3968 format!("{indent}Aggregate fn={function:?} field={f}\n{child}")
3969 }
3970 PlanNode::NestedLoopJoin {
3971 left,
3972 right,
3973 on,
3974 kind,
3975 } => {
3976 let left_child = format_plan_tree(left, depth + 1);
3977 let right_child = format_plan_tree(right, depth + 1);
3978 let on_str = match on {
3979 Some(pred) => format!("{pred:?}"),
3980 None => "none".to_string(),
3981 };
3982 format!("{indent}NestedLoopJoin kind={kind:?} on={on_str}\n{left_child}\n{right_child}")
3983 }
3984 PlanNode::Distinct { input } => {
3985 let child = format_plan_tree(input, depth + 1);
3986 format!("{indent}Distinct\n{child}")
3987 }
3988 PlanNode::GroupBy {
3989 input,
3990 keys,
3991 aggregates,
3992 having,
3993 } => {
3994 let agg_strs: Vec<String> = aggregates
3995 .iter()
3996 .map(|a| format!("{:?}({}) as {}", a.function, a.field, a.output_name))
3997 .collect();
3998 let having_str = match having {
3999 Some(h) => format!(" having={h:?}"),
4000 None => String::new(),
4001 };
4002 let child = format_plan_tree(input, depth + 1);
4003 format!(
4004 "{indent}GroupBy keys=[{}] aggs=[{}]{having_str}\n{child}",
4005 keys.join(", "),
4006 agg_strs.join(", "),
4007 )
4008 }
4009 PlanNode::Insert { table, rows, .. } => {
4010 let cols: Vec<&str> = rows
4011 .first()
4012 .map(|r| r.iter().map(|a| a.field.as_str()).collect())
4013 .unwrap_or_default();
4014 format!(
4015 "{indent}Insert table={table} rows={} cols=[{}]",
4016 rows.len(),
4017 cols.join(", ")
4018 )
4019 }
4020 PlanNode::Upsert {
4021 table,
4022 key_column,
4023 assignments,
4024 on_conflict,
4025 } => {
4026 let cols: Vec<&str> = assignments.iter().map(|a| a.field.as_str()).collect();
4027 let conflict_cols: Vec<&str> = on_conflict.iter().map(|a| a.field.as_str()).collect();
4028 if conflict_cols.is_empty() {
4029 format!(
4030 "{indent}Upsert table={table} key={key_column} cols=[{}]",
4031 cols.join(", ")
4032 )
4033 } else {
4034 format!(
4035 "{indent}Upsert table={table} key={key_column} cols=[{}] on_conflict=[{}]",
4036 cols.join(", "),
4037 conflict_cols.join(", ")
4038 )
4039 }
4040 }
4041 PlanNode::Update {
4042 input,
4043 table,
4044 assignments,
4045 returning,
4046 } => {
4047 let cols: Vec<&str> = assignments.iter().map(|a| a.field.as_str()).collect();
4048 let child = format_plan_tree(input, depth + 1);
4049 let ret = if *returning { " returning" } else { "" };
4050 format!(
4051 "{indent}Update table={table} set=[{}]{ret}\n{child}",
4052 cols.join(", ")
4053 )
4054 }
4055 PlanNode::Delete {
4056 input,
4057 table,
4058 returning,
4059 } => {
4060 let child = format_plan_tree(input, depth + 1);
4061 let ret = if *returning { " returning" } else { "" };
4062 format!("{indent}Delete table={table}{ret}\n{child}")
4063 }
4064 PlanNode::CreateTable { name, fields } => {
4065 let fs: Vec<String> = fields
4066 .iter()
4067 .map(|f| {
4068 let mut mods = String::new();
4069 if f.required {
4070 mods.push_str(" required");
4071 }
4072 if f.unique {
4073 mods.push_str(" unique");
4074 }
4075 format!("{}: {}{mods}", f.name, f.type_name)
4076 })
4077 .collect();
4078 format!("{indent}CreateTable name={name} fields=[{}]", fs.join(", "))
4079 }
4080 PlanNode::AlterTable { table, action } => {
4081 format!("{indent}AlterTable table={table} action={action:?}")
4082 }
4083 PlanNode::DropTable { name } => format!("{indent}DropTable name={name}"),
4084 PlanNode::CreateView { name, .. } => format!("{indent}CreateView name={name}"),
4085 PlanNode::RefreshView { name } => format!("{indent}RefreshView name={name}"),
4086 PlanNode::DropView { name } => format!("{indent}DropView name={name}"),
4087 PlanNode::Window { input, windows } => {
4088 let ws: Vec<String> = windows
4089 .iter()
4090 .map(|w| format!("{:?} as {}", w.function, w.output_name))
4091 .collect();
4092 let child = format_plan_tree(input, depth + 1);
4093 format!("{indent}Window fns=[{}]\n{child}", ws.join(", "))
4094 }
4095 PlanNode::Union { left, right, all } => {
4096 let kind = if *all { "UNION ALL" } else { "UNION" };
4097 let left_child = format_plan_tree(left, depth + 1);
4098 let right_child = format_plan_tree(right, depth + 1);
4099 format!("{indent}{kind}\n{left_child}\n{right_child}")
4100 }
4101 PlanNode::Explain { input } => {
4102 let child = format_plan_tree(input, depth + 1);
4103 format!("{indent}Explain\n{child}")
4104 }
4105 PlanNode::Begin => format!("{indent}Begin"),
4106 PlanNode::Commit => format!("{indent}Commit"),
4107 PlanNode::Rollback => format!("{indent}Rollback"),
4108 }
4109}