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