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