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