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