From bc26faf0e3feba74ff5d419769cfe584dbcc74bb Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:38 +0800 Subject: [PATCH 01/62] feat: reorder inner joins greedily using estimated row counts --- AGENT.md | 7 + src/db.rs | 5 + src/db/prepared.rs | 3 +- src/optimizer/heuristic/batch.rs | 38 +- src/optimizer/heuristic/optimizer.rs | 349 ++++++++---- .../rule/implementation/ddl/add_column.rs | 8 +- .../rule/implementation/ddl/change_column.rs | 8 +- .../rule/implementation/ddl/create_table.rs | 8 +- .../rule/implementation/ddl/drop_column.rs | 8 +- .../rule/implementation/ddl/drop_table.rs | 8 +- .../rule/implementation/ddl/truncate.rs | 8 +- .../rule/implementation/dml/analyze.rs | 4 +- .../rule/implementation/dml/copy_from_file.rs | 8 +- .../rule/implementation/dml/copy_to_file.rs | 8 +- .../rule/implementation/dml/delete.rs | 4 +- .../rule/implementation/dml/insert.rs | 4 +- .../rule/implementation/dml/update.rs | 4 +- .../rule/implementation/dql/aggregate.rs | 14 +- .../rule/implementation/dql/dummy.rs | 4 +- .../rule/implementation/dql/filter.rs | 8 +- .../rule/implementation/dql/function_scan.rs | 8 +- src/optimizer/rule/implementation/dql/join.rs | 8 +- .../rule/implementation/dql/limit.rs | 4 +- .../rule/implementation/dql/mark_apply.rs | 8 +- .../rule/implementation/dql/projection.rs | 8 +- .../implementation/dql/scalar_query_init.rs | 8 +- .../implementation/dql/scalar_subquery.rs | 8 +- src/optimizer/rule/implementation/dql/sort.rs | 3 +- .../rule/implementation/dql/table_scan.rs | 14 +- .../rule/implementation/dql/top_k.rs | 3 +- .../rule/implementation/dql/values.rs | 4 +- .../rule/implementation/dql/window.rs | 4 +- .../rule/normalization/elimination.rs | 64 ++- .../rule/normalization/join_reorder.rs | 524 ++++++++++++++++++ src/optimizer/rule/normalization/mod.rs | 7 +- .../rule/normalization/parameterized_index.rs | 18 +- src/planner/mod.rs | 43 +- src/planner/operator/mod.rs | 19 +- src/planner/operator/visitor_mut.rs | 13 +- tests/slt/join.slt | 83 +++ tests/slt/where_by_index_explain.slt | 2 +- 41 files changed, 1164 insertions(+), 197 deletions(-) create mode 100644 src/optimizer/rule/normalization/join_reorder.rs diff --git a/AGENT.md b/AGENT.md index 8073a5ff..0276a327 100644 --- a/AGENT.md +++ b/AGENT.md @@ -134,6 +134,13 @@ If code looks similar in multiple places, it probably wants to be reused. - Avoid exposing internal details - Keep modules small, focused, and cohesive +### 4.3 Rust Imports + +- Declare `use` imports at module scope, not inside functions or methods. +- Import functions and types before using them; do not write fully qualified paths + such as `crate::optimizer::rule::normalization::reorder_joins(...)` in function bodies. +- Use import aliases when names conflict; keep test imports at the test module scope. + --- ## 5. Dependency Policy diff --git a/src/db.rs b/src/db.rs index f1db4e06..7afe3c9c 100644 --- a/src/db.rs +++ b/src/db.rs @@ -451,6 +451,11 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::EliminateIndexFilter], ) + .after_batch( + "Greedy Join Reorder".to_string(), + HepBatchStrategy::once_bottomup(), + vec![NormalizationRuleImpl::GreedyJoinReorder], + ) .after_batch( "Expression Remapper".to_string(), HepBatchStrategy::once_topdown(), diff --git a/src/db/prepared.rs b/src/db/prepared.rs index 405639bd..e731bd66 100644 --- a/src/db/prepared.rs +++ b/src/db/prepared.rs @@ -1,7 +1,7 @@ use super::*; // GRCOV_EXCL_START #[cfg(test)] -use crate::planner::operator::{PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{PhysicalOption, PlanImpl, RowCountOption, SortOption}; // GRCOV_EXCL_STOP use crate::planner::{ExprRef, ParamArena, PlanInput}; // GRCOV_EXCL_START @@ -296,6 +296,7 @@ mod tests { scan.physical_option = Some(PhysicalOption::new( PlanImpl::IndexScan(Box::new(index.clone())), sort.clone(), + RowCountOption::Unknown, )); let filter = FilterOperator { predicate, diff --git a/src/optimizer/heuristic/batch.rs b/src/optimizer/heuristic/batch.rs index 25efa273..13b5b8c5 100644 --- a/src/optimizer/heuristic/batch.rs +++ b/src/optimizer/heuristic/batch.rs @@ -124,28 +124,48 @@ impl HepBatch { } } +#[derive(Clone, Copy)] +pub enum HepMatchOrder { + TopDown, + BottomUp, +} + #[derive(Clone, Copy)] pub enum HepBatchStrategy { - /// An execution_ap strategy for rules that indicates the maximum number of executions. If the - /// execution_ap reaches fix point (i.e. converge) before maxIterations, it will stop. - /// - /// Fix Point means that plan tree not changed after applying all rules. - MaxTimes(usize), + /// Stop at a fix point or after the configured number of full-tree iterations. + MaxTimes { + max_iteration: usize, + order: HepMatchOrder, + }, #[allow(dead_code)] - LoopIfApplied, + LoopIfApplied(HepMatchOrder), } impl HepBatchStrategy { pub fn once_topdown() -> Self { - HepBatchStrategy::MaxTimes(1) + Self::fix_point_topdown(1) + } + + pub fn once_bottomup() -> Self { + Self::fix_point_bottomup(1) } pub fn fix_point_topdown(max_iteration: usize) -> Self { - HepBatchStrategy::MaxTimes(max_iteration) + Self::MaxTimes { + max_iteration, + order: HepMatchOrder::TopDown, + } + } + + pub fn fix_point_bottomup(max_iteration: usize) -> Self { + Self::MaxTimes { + max_iteration, + order: HepMatchOrder::BottomUp, + } } #[allow(dead_code)] pub fn loop_if_applied() -> Self { - HepBatchStrategy::LoopIfApplied + Self::LoopIfApplied(HepMatchOrder::TopDown) } } diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index 8b853407..e9287a81 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -18,7 +18,7 @@ use crate::optimizer::core::rule::{ }; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::heuristic::batch::{ - HepBatch, HepBatchStep, HepBatchStrategy, HepLocalRewriteBatch, HepWholeTreePass, + HepBatch, HepBatchStep, HepBatchStrategy, HepLocalRewriteBatch, HepMatchOrder, HepWholeTreePass, }; use crate::optimizer::rule::implementation::{ImplementationRuleImpl, ImplementationRuleRootTag}; use crate::optimizer::rule::normalization::{ @@ -26,7 +26,7 @@ use crate::optimizer::rule::normalization::{ evaluator_bind_current, NormalizationRuleImpl, OrderHintKind, ScanOrderHint, WholeTreePassKind, }; use crate::planner::operator::table_scan::TableScanOperator; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::planner::{Childrens, LogicalPlan, PlanArena}; use std::array; use std::ops::Not; @@ -38,7 +38,6 @@ pub struct HepOptimizer<'a> { before_batches: &'a [HepBatch], after_batches: &'a [HepBatch], implementation_index: &'a ImplementationRuleIndex, - max_local_rules_len: usize, plan: LogicalPlan, } @@ -48,13 +47,11 @@ impl<'a> HepOptimizer<'a> { before_batches: &'a [HepBatch], after_batches: &'a [HepBatch], implementation_index: &'a ImplementationRuleIndex, - max_local_rules_len: usize, ) -> Self { Self { before_batches, after_batches, implementation_index, - max_local_rules_len, plan, } } @@ -64,13 +61,7 @@ impl<'a> HepOptimizer<'a> { loader: Option<&StatisticMetaLoader<'_>>, arena: &mut PlanArena, ) -> Result { - let mut applied_rules = Vec::with_capacity(self.max_local_rules_len); - Self::apply_batches( - &mut self.plan, - self.before_batches, - &mut applied_rules, - arena, - )?; + Self::apply_batches(&mut self.plan, self.before_batches, arena)?; if let Some(loader) = loader { if self.implementation_index.is_empty().not() { @@ -88,12 +79,7 @@ impl<'a> HepOptimizer<'a> { )?; } } - Self::apply_batches( - &mut self.plan, - self.after_batches, - &mut applied_rules, - arena, - )?; + Self::apply_batches(&mut self.plan, self.after_batches, arena)?; Ok(self.plan) } @@ -102,20 +88,22 @@ impl<'a> HepOptimizer<'a> { fn apply_batches( plan: &mut LogicalPlan, batches: &[HepBatch], - applied_rules: &mut Vec, arena: &mut PlanArena, ) -> Result<(), DatabaseError> { for batch in batches { match batch.strategy { - HepBatchStrategy::MaxTimes(max_iteration) => { + HepBatchStrategy::MaxTimes { + max_iteration, + order, + } => { for _ in 0..max_iteration { - if !Self::apply_batch(plan, batch, applied_rules, arena)? { + if !Self::apply_batch(plan, batch, order, arena)? { break; } } } - HepBatchStrategy::LoopIfApplied => { - while Self::apply_batch(plan, batch, applied_rules, arena)? {} + HepBatchStrategy::LoopIfApplied(order) => { + while Self::apply_batch(plan, batch, order, arena)? {} } } } @@ -126,7 +114,7 @@ impl<'a> HepOptimizer<'a> { fn apply_batch( plan: &mut LogicalPlan, batch: &HepBatch, - applied_rules: &mut Vec, + order: HepMatchOrder, arena: &mut PlanArena, ) -> Result { let mut applied = false; @@ -138,7 +126,7 @@ impl<'a> HepOptimizer<'a> { } } HepBatchStep::LocalRewrite(rules) => { - if Self::apply_local_rules(plan, rules, applied_rules, arena)? { + if Self::apply_local_rules(plan, rules, order, arena)? { applied = true; } } @@ -395,59 +383,48 @@ impl<'a> HepOptimizer<'a> { fn apply_local_rules( plan: &mut LogicalPlan, rules: &HepLocalRewriteBatch, - applied_rules: &mut Vec, + order: HepMatchOrder, arena: &mut PlanArena, ) -> Result { - applied_rules.clear(); - applied_rules.resize(rules.len(), false); - Self::apply_local_rules_inner(plan, rules, applied_rules, arena) + let mut applied = false; + if matches!(order, HepMatchOrder::TopDown) { + applied |= Self::apply_local_rules_current(plan, rules, arena)?; + } + + let child_applied = match plan.childrens.as_mut() { + Childrens::Only(child) => Self::apply_local_rules(child, rules, order, arena)?, + Childrens::Twins { left, right } => { + let left_applied = Self::apply_local_rules(left, rules, order, arena)?; + let right_applied = Self::apply_local_rules(right, rules, order, arena)?; + left_applied || right_applied + } + Childrens::None => false, + }; + if child_applied { + plan.reset_output_schema_cache(); + } + applied |= child_applied; + + if matches!(order, HepMatchOrder::BottomUp) { + applied |= Self::apply_local_rules_current(plan, rules, arena)?; + } + Ok(applied) } - fn apply_local_rules_inner( + fn apply_local_rules_current( plan: &mut LogicalPlan, rules: &HepLocalRewriteBatch, - applied_rules: &mut [bool], arena: &mut PlanArena, ) -> Result { let mut applied = false; let mut next_rule_idx = 0; - while let Some(idx) = rules.next_matching_rule_index_from(&plan.operator, next_rule_idx) { - let rule = rules.rules[idx]; next_rule_idx = idx + 1; - if applied_rules[idx] { - continue; - } - let applied_rule = rule.apply(plan, arena)?; - if applied_rule { - applied_rules[idx] = true; + if rules.rules[idx].apply(plan, arena)? { applied = true; plan.reset_output_schema_cache_recursive(); } } - - match plan.childrens.as_mut() { - Childrens::Only(child) => { - let child_applied = - Self::apply_local_rules_inner(child, rules, applied_rules, arena)?; - applied |= child_applied; - if child_applied { - plan.reset_output_schema_cache(); - } - } - Childrens::Twins { left, right } => { - let left_applied = - Self::apply_local_rules_inner(left, rules, applied_rules, arena)?; - let right_applied = - Self::apply_local_rules_inner(right, rules, applied_rules, arena)?; - applied |= left_applied || right_applied; - if left_applied || right_applied { - plan.reset_output_schema_cache(); - } - } - Childrens::None => {} - } - Ok(applied) } } @@ -483,6 +460,7 @@ impl ImplementationRuleIndex { Some(PhysicalOption::new( PlanImpl::HashAggregate, SortOption::None, + RowCountOption::Unknown, )) } Operator::Aggregate(op) @@ -492,25 +470,42 @@ impl ImplementationRuleIndex { Some(PhysicalOption::new( PlanImpl::SimpleAggregate, SortOption::None, + RowCountOption::Estimated(1), )) } - Operator::Dummy if self.contains(ImplementationRuleImpl::Dummy) => { - Some(PhysicalOption::new(PlanImpl::Dummy, SortOption::None)) - } + Operator::Dummy if self.contains(ImplementationRuleImpl::Dummy) => Some( + PhysicalOption::new(PlanImpl::Dummy, SortOption::None, RowCountOption::Unknown), + ), Operator::Filter(_) if self.contains(ImplementationRuleImpl::Filter) => { - Some(PhysicalOption::new(PlanImpl::Filter, SortOption::Follow)) + Some(PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Unknown, + )) } Operator::Join(join_op) if self.contains(ImplementationRuleImpl::HashJoin) => { - Some(PhysicalOption::new(join_op.plan_impl(), SortOption::None)) - } - Operator::Limit(_) if self.contains(ImplementationRuleImpl::Limit) => { - Some(PhysicalOption::new(PlanImpl::Limit, SortOption::Follow)) + Some(PhysicalOption::new( + join_op.plan_impl(), + SortOption::None, + RowCountOption::Unknown, + )) } + Operator::Limit(_) if self.contains(ImplementationRuleImpl::Limit) => Some( + PhysicalOption::new(PlanImpl::Limit, SortOption::Follow, RowCountOption::Unknown), + ), Operator::MarkApply(_) if self.contains(ImplementationRuleImpl::MarkApply) => { - Some(PhysicalOption::new(PlanImpl::MarkApply, SortOption::Follow)) + Some(PhysicalOption::new( + PlanImpl::MarkApply, + SortOption::Follow, + RowCountOption::Unknown, + )) } Operator::Project(_) if self.contains(ImplementationRuleImpl::Projection) => { - Some(PhysicalOption::new(PlanImpl::Project, SortOption::Follow)) + Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )) } Operator::ScalarQueryInit(_) if self.contains(ImplementationRuleImpl::ScalarQueryInit) => @@ -518,6 +513,7 @@ impl ImplementationRuleIndex { Some(PhysicalOption::new( PlanImpl::ScalarQueryInit, SortOption::Follow, + RowCountOption::Follow, )) } Operator::ScalarSubquery(_) @@ -526,12 +522,14 @@ impl ImplementationRuleIndex { Some(PhysicalOption::new( PlanImpl::ScalarSubquery, SortOption::Follow, + RowCountOption::Follow, )) } Operator::FunctionScan(_) if self.contains(ImplementationRuleImpl::FunctionScan) => { Some(PhysicalOption::new( PlanImpl::FunctionScan, SortOption::None, + RowCountOption::Unknown, )) } Operator::Sort(op) if self.contains(ImplementationRuleImpl::Sort) => { @@ -541,6 +539,7 @@ impl ImplementationRuleIndex { fields: op.sort_fields.clone(), ignore_prefix_len: 0, }, + RowCountOption::Follow, )) } Operator::TopK(op) if self.contains(ImplementationRuleImpl::TopK) => { @@ -550,57 +549,88 @@ impl ImplementationRuleIndex { fields: op.sort_fields.clone(), ignore_prefix_len: 0, }, + RowCountOption::Unknown, )) } - Operator::Values(_) if self.contains(ImplementationRuleImpl::Values) => { - Some(PhysicalOption::new(PlanImpl::Values, SortOption::None)) - } - Operator::Window(op) if self.contains(ImplementationRuleImpl::Window) => { - Some(PhysicalOption::new(PlanImpl::Window, op.sort_option())) - } - Operator::Analyze(_) if self.contains(ImplementationRuleImpl::Analyze) => { - Some(PhysicalOption::new(PlanImpl::Analyze, SortOption::None)) + Operator::Values(op) if self.contains(ImplementationRuleImpl::Values) => { + Some(PhysicalOption::new( + PlanImpl::Values, + SortOption::None, + RowCountOption::Estimated(op.rows.len() / op.schema_ref.len()), + )) } + Operator::Window(op) if self.contains(ImplementationRuleImpl::Window) => Some( + PhysicalOption::new(PlanImpl::Window, op.sort_option(), RowCountOption::Follow), + ), + Operator::Analyze(_) if self.contains(ImplementationRuleImpl::Analyze) => Some( + PhysicalOption::new(PlanImpl::Analyze, SortOption::None, RowCountOption::Unknown), + ), #[cfg(feature = "copy")] Operator::CopyFromFile(_) if self.contains(ImplementationRuleImpl::CopyFromFile) => { Some(PhysicalOption::new( PlanImpl::CopyFromFile, SortOption::None, + RowCountOption::Unknown, )) } #[cfg(feature = "copy")] Operator::CopyToFile(_) if self.contains(ImplementationRuleImpl::CopyToFile) => { - Some(PhysicalOption::new(PlanImpl::CopyToFile, SortOption::None)) - } - Operator::Delete(_) if self.contains(ImplementationRuleImpl::Delete) => { - Some(PhysicalOption::new(PlanImpl::Delete, SortOption::None)) - } - Operator::Insert(_) if self.contains(ImplementationRuleImpl::Insert) => { - Some(PhysicalOption::new(PlanImpl::Insert, SortOption::None)) - } - Operator::Update(_) if self.contains(ImplementationRuleImpl::Update) => { - Some(PhysicalOption::new(PlanImpl::Update, SortOption::None)) + Some(PhysicalOption::new( + PlanImpl::CopyToFile, + SortOption::None, + RowCountOption::Unknown, + )) } + Operator::Delete(_) if self.contains(ImplementationRuleImpl::Delete) => Some( + PhysicalOption::new(PlanImpl::Delete, SortOption::None, RowCountOption::Unknown), + ), + Operator::Insert(_) if self.contains(ImplementationRuleImpl::Insert) => Some( + PhysicalOption::new(PlanImpl::Insert, SortOption::None, RowCountOption::Unknown), + ), + Operator::Update(_) if self.contains(ImplementationRuleImpl::Update) => Some( + PhysicalOption::new(PlanImpl::Update, SortOption::None, RowCountOption::Unknown), + ), Operator::AddColumn(_) if self.contains(ImplementationRuleImpl::AddColumn) => { - Some(PhysicalOption::new(PlanImpl::AddColumn, SortOption::None)) + Some(PhysicalOption::new( + PlanImpl::AddColumn, + SortOption::None, + RowCountOption::Unknown, + )) } Operator::ChangeColumn(_) if self.contains(ImplementationRuleImpl::ChangeColumn) => { Some(PhysicalOption::new( PlanImpl::ChangeColumn, SortOption::None, + RowCountOption::Unknown, )) } Operator::CreateTable(_) if self.contains(ImplementationRuleImpl::CreateTable) => { - Some(PhysicalOption::new(PlanImpl::CreateTable, SortOption::None)) + Some(PhysicalOption::new( + PlanImpl::CreateTable, + SortOption::None, + RowCountOption::Unknown, + )) } Operator::DropColumn(_) if self.contains(ImplementationRuleImpl::DropColumn) => { - Some(PhysicalOption::new(PlanImpl::DropColumn, SortOption::None)) + Some(PhysicalOption::new( + PlanImpl::DropColumn, + SortOption::None, + RowCountOption::Unknown, + )) } Operator::DropTable(_) if self.contains(ImplementationRuleImpl::DropTable) => { - Some(PhysicalOption::new(PlanImpl::DropTable, SortOption::None)) + Some(PhysicalOption::new( + PlanImpl::DropTable, + SortOption::None, + RowCountOption::Unknown, + )) } Operator::Truncate(_) if self.contains(ImplementationRuleImpl::Truncate) => { - Some(PhysicalOption::new(PlanImpl::Truncate, SortOption::None)) + Some(PhysicalOption::new( + PlanImpl::Truncate, + SortOption::None, + RowCountOption::Unknown, + )) } _ => None, } @@ -626,18 +656,129 @@ mod tests { use crate::expression::range_detacher::Range; use crate::expression::ScalarExpression; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; - use crate::optimizer::heuristic::batch::HepBatchStrategy; - use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; + use crate::optimizer::heuristic::batch::{HepBatch, HepBatchStrategy, HepMatchOrder}; + use crate::optimizer::heuristic::optimizer::{HepOptimizer, HepOptimizerPipeline}; use crate::optimizer::rule::implementation::ImplementationRuleImpl; use crate::optimizer::rule::normalization::NormalizationRuleImpl; + use crate::planner::operator::filter::FilterOperator; + use crate::planner::operator::limit::LimitOperator; + use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::sort::SortField; use crate::planner::operator::{Operator, PlanImpl, SortOption}; + use crate::planner::{Childrens, LogicalPlan, PlanArena, TableArenaCell}; use crate::storage::{Storage, Transaction}; use crate::types::index::IndexLookup; use crate::types::value::DataValue; use std::ops::Bound; use tempfile::TempDir; + fn filter_chain(length: usize, arena: &mut PlanArena) -> LogicalPlan { + let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::None); + for value in 0..length { + let predicate = + arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(value as i32))); + plan = FilterOperator::build(predicate, plan, false); + } + plan + } + + #[test] + fn local_rule_order_and_sibling_application() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let batch = HepBatch::new( + "Combine Filters".into(), + HepBatchStrategy::once_bottomup(), + vec![NormalizationRuleImpl::CombineFilter], + ); + let mut topdown = filter_chain(3, &mut arena); + assert!(HepOptimizer::apply_batch( + &mut topdown, + &batch, + HepMatchOrder::TopDown, + &mut arena + )?); + assert!(matches!( + topdown.childrens.only().operator, + Operator::Filter(_) + )); + let mut bottomup = filter_chain(3, &mut arena); + assert!(HepOptimizer::apply_batch( + &mut bottomup, + &batch, + HepMatchOrder::BottomUp, + &mut arena + )?); + assert!(matches!( + bottomup.childrens.only().operator, + Operator::Dummy + )); + assert!(!HepOptimizer::apply_batch( + &mut bottomup, + &batch, + HepMatchOrder::BottomUp, + &mut arena + )?); + + for strategy in [ + HepBatchStrategy::once_topdown(), + HepBatchStrategy::once_bottomup(), + ] { + let plan = LogicalPlan::new( + Operator::Dummy, + Childrens::Twins { + left: Box::new(filter_chain(2, &mut arena)), + right: Box::new(filter_chain(2, &mut arena)), + }, + ); + let pipeline = HepOptimizerPipeline::builder() + .before_batch( + "Combine Filters".into(), + strategy, + vec![NormalizationRuleImpl::CombineFilter], + ) + .build(); + let result = pipeline.instantiate(plan).find_best(None, &mut arena)?; + let (left, right) = result.childrens.twins(); + assert!(matches!(left.operator, Operator::Filter(_))); + assert!(matches!(right.operator, Operator::Filter(_))); + assert!(matches!(left.childrens.only().operator, Operator::Dummy)); + assert!(matches!(right.childrens.only().operator, Operator::Dummy)); + } + Ok(()) + } + + #[test] + fn bottomup_fix_point_respects_iteration_limit() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + for (iterations, expected_depth) in [(0, 0), (1, 1), (2, 2), (30, 3)] { + let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::None); + for _ in 0..3 { + plan = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: Vec::new() }), + Childrens::Only(Box::new(plan)), + ); + } + plan = LimitOperator::build(None, Some(5), plan); + let pipeline = HepOptimizerPipeline::builder() + .before_batch( + "Limit Project Transpose".into(), + HepBatchStrategy::fix_point_bottomup(iterations), + vec![NormalizationRuleImpl::LimitProjectTranspose], + ) + .build(); + let result = pipeline.instantiate(plan).find_best(None, &mut arena)?; + let mut cursor = &result; + for _ in 0..expected_depth { + assert!(matches!(cursor.operator, Operator::Project(_))); + cursor = cursor.childrens.only(); + } + assert!(matches!(cursor.operator, Operator::Limit(_))); + } + Ok(()) + } + #[test] fn test_find_best_selects_cheapest_scan() -> Result<(), DatabaseError> { let temp_dir = TempDir::new().expect("unable to create temporary working directory"); @@ -763,7 +904,6 @@ pub struct HepOptimizerPipeline { before_batches: Vec, after_batches: Vec, implementation_index: ImplementationRuleIndex, - max_local_rules_len: usize, } impl HepOptimizerPipeline { @@ -780,22 +920,10 @@ impl HepOptimizerPipeline { after_batches: Vec, implementations: Vec, ) -> Self { - let max_local_rules_len = before_batches - .iter() - .chain(after_batches.iter()) - .flat_map(|batch| batch.steps.iter()) - .filter_map(|step| match step { - HepBatchStep::LocalRewrite(rules) => Some(rules.len()), - HepBatchStep::WholeTree(_) => None, - }) - .max() - .unwrap_or(0); - Self { before_batches, after_batches, implementation_index: ImplementationRuleIndex::new(implementations), - max_local_rules_len, } } @@ -805,7 +933,6 @@ impl HepOptimizerPipeline { &self.before_batches, &self.after_batches, &self.implementation_index, - self.max_local_rules_len, ) } } diff --git a/src/optimizer/rule/implementation/ddl/add_column.rs b/src/optimizer/rule/implementation/ddl/add_column.rs index d2f3bf17..0a82b516 100644 --- a/src/optimizer/rule/implementation/ddl/add_column.rs +++ b/src/optimizer/rule/implementation/ddl/add_column.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct AddColumnImplementation; single_mapping!( AddColumnImplementation, ADD_COLUMN_PATTERN, - PhysicalOption::new(PlanImpl::AddColumn, SortOption::None) + PhysicalOption::new( + PlanImpl::AddColumn, + SortOption::None, + RowCountOption::Unknown + ) ); diff --git a/src/optimizer/rule/implementation/ddl/change_column.rs b/src/optimizer/rule/implementation/ddl/change_column.rs index e9613793..7bc31c6e 100644 --- a/src/optimizer/rule/implementation/ddl/change_column.rs +++ b/src/optimizer/rule/implementation/ddl/change_column.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct ChangeColumnImplementation; single_mapping!( ChangeColumnImplementation, CHANGE_COLUMN_PATTERN, - PhysicalOption::new(PlanImpl::ChangeColumn, SortOption::None) + PhysicalOption::new( + PlanImpl::ChangeColumn, + SortOption::None, + RowCountOption::Unknown + ) ); diff --git a/src/optimizer/rule/implementation/ddl/create_table.rs b/src/optimizer/rule/implementation/ddl/create_table.rs index c50fc810..f3b0ec41 100644 --- a/src/optimizer/rule/implementation/ddl/create_table.rs +++ b/src/optimizer/rule/implementation/ddl/create_table.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct CreateTableImplementation; single_mapping!( CreateTableImplementation, CREATE_TABLE_PATTERN, - PhysicalOption::new(PlanImpl::CreateTable, SortOption::None) + PhysicalOption::new( + PlanImpl::CreateTable, + SortOption::None, + RowCountOption::Unknown + ) ); diff --git a/src/optimizer/rule/implementation/ddl/drop_column.rs b/src/optimizer/rule/implementation/ddl/drop_column.rs index a8b86ab7..d4684982 100644 --- a/src/optimizer/rule/implementation/ddl/drop_column.rs +++ b/src/optimizer/rule/implementation/ddl/drop_column.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct DropColumnImplementation; single_mapping!( DropColumnImplementation, DROP_COLUMN_PATTERN, - PhysicalOption::new(PlanImpl::DropColumn, SortOption::None) + PhysicalOption::new( + PlanImpl::DropColumn, + SortOption::None, + RowCountOption::Unknown + ) ); diff --git a/src/optimizer/rule/implementation/ddl/drop_table.rs b/src/optimizer/rule/implementation/ddl/drop_table.rs index 351663d4..26e09322 100644 --- a/src/optimizer/rule/implementation/ddl/drop_table.rs +++ b/src/optimizer/rule/implementation/ddl/drop_table.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct DropTableImplementation; single_mapping!( DropTableImplementation, DROP_TABLE_PATTERN, - PhysicalOption::new(PlanImpl::DropTable, SortOption::None) + PhysicalOption::new( + PlanImpl::DropTable, + SortOption::None, + RowCountOption::Unknown + ) ); diff --git a/src/optimizer/rule/implementation/ddl/truncate.rs b/src/optimizer/rule/implementation/ddl/truncate.rs index c9cb9248..6e3ff66c 100644 --- a/src/optimizer/rule/implementation/ddl/truncate.rs +++ b/src/optimizer/rule/implementation/ddl/truncate.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct TruncateImplementation; single_mapping!( TruncateImplementation, TRUNCATE_PATTERN, - PhysicalOption::new(PlanImpl::Truncate, SortOption::None) + PhysicalOption::new( + PlanImpl::Truncate, + SortOption::None, + RowCountOption::Unknown + ) ); diff --git a/src/optimizer/rule/implementation/dml/analyze.rs b/src/optimizer/rule/implementation/dml/analyze.rs index 0f571861..6bcc6dc8 100644 --- a/src/optimizer/rule/implementation/dml/analyze.rs +++ b/src/optimizer/rule/implementation/dml/analyze.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,5 @@ pub struct AnalyzeImplementation; single_mapping!( AnalyzeImplementation, ANALYZE_PATTERN, - PhysicalOption::new(PlanImpl::Analyze, SortOption::None) + PhysicalOption::new(PlanImpl::Analyze, SortOption::None, RowCountOption::Unknown) ); diff --git a/src/optimizer/rule/implementation/dml/copy_from_file.rs b/src/optimizer/rule/implementation/dml/copy_from_file.rs index 593fc843..b1cf3cd0 100644 --- a/src/optimizer/rule/implementation/dml/copy_from_file.rs +++ b/src/optimizer/rule/implementation/dml/copy_from_file.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct CopyFromFileImplementation; single_mapping!( CopyFromFileImplementation, COPY_FROM_FILE_PATTERN, - PhysicalOption::new(PlanImpl::CopyFromFile, SortOption::None) + PhysicalOption::new( + PlanImpl::CopyFromFile, + SortOption::None, + RowCountOption::Unknown + ) ); diff --git a/src/optimizer/rule/implementation/dml/copy_to_file.rs b/src/optimizer/rule/implementation/dml/copy_to_file.rs index d1f9f234..f668c230 100644 --- a/src/optimizer/rule/implementation/dml/copy_to_file.rs +++ b/src/optimizer/rule/implementation/dml/copy_to_file.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct CopyToFileImplementation; single_mapping!( CopyToFileImplementation, COPY_TO_FILE_PATTERN, - PhysicalOption::new(PlanImpl::CopyToFile, SortOption::None) + PhysicalOption::new( + PlanImpl::CopyToFile, + SortOption::None, + RowCountOption::Unknown + ) ); diff --git a/src/optimizer/rule/implementation/dml/delete.rs b/src/optimizer/rule/implementation/dml/delete.rs index 80f3cb18..436b8d49 100644 --- a/src/optimizer/rule/implementation/dml/delete.rs +++ b/src/optimizer/rule/implementation/dml/delete.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,5 @@ pub struct DeleteImplementation; single_mapping!( DeleteImplementation, DELETE_PATTERN, - PhysicalOption::new(PlanImpl::Delete, SortOption::None) + PhysicalOption::new(PlanImpl::Delete, SortOption::None, RowCountOption::Unknown) ); diff --git a/src/optimizer/rule/implementation/dml/insert.rs b/src/optimizer/rule/implementation/dml/insert.rs index 5f71fa9a..0300c55a 100644 --- a/src/optimizer/rule/implementation/dml/insert.rs +++ b/src/optimizer/rule/implementation/dml/insert.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,5 @@ pub struct InsertImplementation; single_mapping!( InsertImplementation, INSERT_PATTERN, - PhysicalOption::new(PlanImpl::Insert, SortOption::None) + PhysicalOption::new(PlanImpl::Insert, SortOption::None, RowCountOption::Unknown) ); diff --git a/src/optimizer/rule/implementation/dml/update.rs b/src/optimizer/rule/implementation/dml/update.rs index df120c2b..075a4cdc 100644 --- a/src/optimizer/rule/implementation/dml/update.rs +++ b/src/optimizer/rule/implementation/dml/update.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,5 @@ pub struct UpdateImplementation; single_mapping!( UpdateImplementation, UPDATE_PATTERN, - PhysicalOption::new(PlanImpl::Update, SortOption::None) + PhysicalOption::new(PlanImpl::Update, SortOption::None, RowCountOption::Unknown) ); diff --git a/src/optimizer/rule/implementation/dql/aggregate.rs b/src/optimizer/rule/implementation/dql/aggregate.rs index 2466076f..e92dada3 100644 --- a/src/optimizer/rule/implementation/dql/aggregate.rs +++ b/src/optimizer/rule/implementation/dql/aggregate.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -45,7 +45,11 @@ pub struct GroupByAggregateImplementation; single_mapping!( GroupByAggregateImplementation, GROUP_BY_AGGREGATE_PATTERN, - PhysicalOption::new(PlanImpl::HashAggregate, SortOption::None) + PhysicalOption::new( + PlanImpl::HashAggregate, + SortOption::None, + RowCountOption::Unknown + ) ); pub struct SimpleAggregateImplementation; @@ -53,5 +57,9 @@ pub struct SimpleAggregateImplementation; single_mapping!( SimpleAggregateImplementation, SIMPLE_AGGREGATE_PATTERN, - PhysicalOption::new(PlanImpl::SimpleAggregate, SortOption::None) + PhysicalOption::new( + PlanImpl::SimpleAggregate, + SortOption::None, + RowCountOption::Estimated(1) + ) ); diff --git a/src/optimizer/rule/implementation/dql/dummy.rs b/src/optimizer/rule/implementation/dql/dummy.rs index 9fd3cdd7..5e83d9c8 100644 --- a/src/optimizer/rule/implementation/dql/dummy.rs +++ b/src/optimizer/rule/implementation/dql/dummy.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,5 @@ pub struct DummyImplementation; single_mapping!( DummyImplementation, DUMMY_PATTERN, - PhysicalOption::new(PlanImpl::Dummy, SortOption::None) + PhysicalOption::new(PlanImpl::Dummy, SortOption::None, RowCountOption::Unknown) ); diff --git a/src/optimizer/rule/implementation/dql/filter.rs b/src/optimizer/rule/implementation/dql/filter.rs index cd254bc2..ffa5073c 100644 --- a/src/optimizer/rule/implementation/dql/filter.rs +++ b/src/optimizer/rule/implementation/dql/filter.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct FilterImplementation; single_mapping!( FilterImplementation, FILTER_PATTERN, - PhysicalOption::new(PlanImpl::Filter, SortOption::Follow) + PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Unknown + ) ); diff --git a/src/optimizer/rule/implementation/dql/function_scan.rs b/src/optimizer/rule/implementation/dql/function_scan.rs index 1b7c7f92..bf59d42a 100644 --- a/src/optimizer/rule/implementation/dql/function_scan.rs +++ b/src/optimizer/rule/implementation/dql/function_scan.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct FunctionScanImplementation; single_mapping!( FunctionScanImplementation, FUNCTION_SCAN_PATTERN, - PhysicalOption::new(PlanImpl::FunctionScan, SortOption::None) + PhysicalOption::new( + PlanImpl::FunctionScan, + SortOption::None, + RowCountOption::Unknown + ) ); diff --git a/src/optimizer/rule/implementation/dql/join.rs b/src/optimizer/rule/implementation/dql/join.rs index 6e703ae7..510e0fec 100644 --- a/src/optimizer/rule/implementation/dql/join.rs +++ b/src/optimizer/rule/implementation/dql/join.rs @@ -16,7 +16,7 @@ use crate::errors::DatabaseError; use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, RowCountOption, SortOption}; use std::sync::LazyLock; static JOIN_PATTERN: LazyLock = LazyLock::new(|| Pattern { @@ -43,7 +43,11 @@ impl ImplementationRule for JoinImplementation { let Operator::Join(operator) = op else { unreachable!() }; - let physical_option = PhysicalOption::new(operator.plan_impl(), SortOption::None); + let physical_option = PhysicalOption::new( + operator.plan_impl(), + SortOption::None, + RowCountOption::Unknown, + ); crate::optimizer::core::rule::keep_best_physical_option( best_physical_option, physical_option, diff --git a/src/optimizer/rule/implementation/dql/limit.rs b/src/optimizer/rule/implementation/dql/limit.rs index 10e17b6b..6654468b 100644 --- a/src/optimizer/rule/implementation/dql/limit.rs +++ b/src/optimizer/rule/implementation/dql/limit.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,5 @@ pub struct LimitImplementation; single_mapping!( LimitImplementation, LIMIT_PATTERN, - PhysicalOption::new(PlanImpl::Limit, SortOption::Follow) + PhysicalOption::new(PlanImpl::Limit, SortOption::Follow, RowCountOption::Unknown) ); diff --git a/src/optimizer/rule/implementation/dql/mark_apply.rs b/src/optimizer/rule/implementation/dql/mark_apply.rs index c79d3dc4..871d6c1c 100644 --- a/src/optimizer/rule/implementation/dql/mark_apply.rs +++ b/src/optimizer/rule/implementation/dql/mark_apply.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct MarkApplyImplementation; single_mapping!( MarkApplyImplementation, MARK_APPLY_PATTERN, - PhysicalOption::new(PlanImpl::MarkApply, SortOption::Follow) + PhysicalOption::new( + PlanImpl::MarkApply, + SortOption::Follow, + RowCountOption::Unknown + ) ); diff --git a/src/optimizer/rule/implementation/dql/projection.rs b/src/optimizer/rule/implementation/dql/projection.rs index 02806e9a..cb0e7d32 100644 --- a/src/optimizer/rule/implementation/dql/projection.rs +++ b/src/optimizer/rule/implementation/dql/projection.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct ProjectionImplementation; single_mapping!( ProjectionImplementation, PROJECTION_PATTERN, - PhysicalOption::new(PlanImpl::Project, SortOption::Follow) + PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow + ) ); diff --git a/src/optimizer/rule/implementation/dql/scalar_query_init.rs b/src/optimizer/rule/implementation/dql/scalar_query_init.rs index 9be962ba..a612ff44 100644 --- a/src/optimizer/rule/implementation/dql/scalar_query_init.rs +++ b/src/optimizer/rule/implementation/dql/scalar_query_init.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct ScalarQueryInitImplementation; single_mapping!( ScalarQueryInitImplementation, SCALAR_APPLY_PATTERN, - PhysicalOption::new(PlanImpl::ScalarQueryInit, SortOption::Follow) + PhysicalOption::new( + PlanImpl::ScalarQueryInit, + SortOption::Follow, + RowCountOption::Follow + ) ); diff --git a/src/optimizer/rule/implementation/dql/scalar_subquery.rs b/src/optimizer/rule/implementation/dql/scalar_subquery.rs index dcaaf88a..ba61d91f 100644 --- a/src/optimizer/rule/implementation/dql/scalar_subquery.rs +++ b/src/optimizer/rule/implementation/dql/scalar_subquery.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,9 @@ pub struct ScalarSubqueryImplementation; single_mapping!( ScalarSubqueryImplementation, SCALAR_SUBQUERY_PATTERN, - PhysicalOption::new(PlanImpl::ScalarSubquery, SortOption::Follow) + PhysicalOption::new( + PlanImpl::ScalarSubquery, + SortOption::Follow, + RowCountOption::Follow + ) ); diff --git a/src/optimizer/rule/implementation/dql/sort.rs b/src/optimizer/rule/implementation/dql/sort.rs index 6ea317e2..13493a32 100644 --- a/src/optimizer/rule/implementation/dql/sort.rs +++ b/src/optimizer/rule/implementation/dql/sort.rs @@ -16,7 +16,7 @@ use crate::errors::DatabaseError; use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use std::sync::LazyLock; static SORT_PATTERN: LazyLock = LazyLock::new(|| Pattern { @@ -49,6 +49,7 @@ impl ImplementationRule for SortImplementation { fields: op.sort_fields.clone(), ignore_prefix_len: 0, }, + RowCountOption::Follow, ), None, ); diff --git a/src/optimizer/rule/implementation/dql/table_scan.rs b/src/optimizer/rule/implementation/dql/table_scan.rs index e40663af..e7d46521 100644 --- a/src/optimizer/rule/implementation/dql/table_scan.rs +++ b/src/optimizer/rule/implementation/dql/table_scan.rs @@ -17,7 +17,7 @@ use crate::expression::range_detacher::Range; use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::types::index::{IndexLookup, IndexType}; use std::sync::LazyLock; @@ -59,7 +59,12 @@ impl ImplementationRule for SeqScanImplementation { crate::optimizer::core::rule::keep_best_physical_option( best_physical_option, - PhysicalOption::new(PlanImpl::SeqScan, SortOption::None), + PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + cost.map(RowCountOption::Estimated) + .unwrap_or(RowCountOption::Unknown), + ), cost, ); Ok(()) @@ -91,12 +96,14 @@ impl ImplementationRule for IndexScanImplementation { continue; }; let mut cost = None; + let mut estimated_rows = None; let index_meta = arena.index(index_info.meta); if let Some(mut row_count) = loader.collect_count(&scan_op.table_name, index_meta.id, range)? { row_count = adjust_index_row_count(index_meta.ty, range, row_count); + estimated_rows = Some(row_count); if index_info.covered_deserializers.is_none() && !matches!(index_meta.ty, IndexType::PrimaryKey { .. }) { @@ -147,6 +154,9 @@ impl ImplementationRule for IndexScanImplementation { PhysicalOption::new( PlanImpl::IndexScan(Box::new(index_info.clone())), index_info.sort_option.clone(), + estimated_rows + .map(RowCountOption::Estimated) + .unwrap_or(RowCountOption::Unknown), ), cost, ); diff --git a/src/optimizer/rule/implementation/dql/top_k.rs b/src/optimizer/rule/implementation/dql/top_k.rs index 5c024221..4dc99cf9 100644 --- a/src/optimizer/rule/implementation/dql/top_k.rs +++ b/src/optimizer/rule/implementation/dql/top_k.rs @@ -16,7 +16,7 @@ use crate::errors::DatabaseError; use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use std::sync::LazyLock; static TOPK_PATTERN: LazyLock = LazyLock::new(|| Pattern { @@ -49,6 +49,7 @@ impl ImplementationRule for TopKImplementation { fields: op.sort_fields.clone(), ignore_prefix_len: 0, }, + RowCountOption::Unknown, ), None, ); diff --git a/src/optimizer/rule/implementation/dql/values.rs b/src/optimizer/rule/implementation/dql/values.rs index 99ec57f0..badfcd2f 100644 --- a/src/optimizer/rule/implementation/dql/values.rs +++ b/src/optimizer/rule/implementation/dql/values.rs @@ -17,7 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::single_mapping; use std::sync::LazyLock; @@ -31,5 +31,5 @@ pub struct ValuesImplementation; single_mapping!( ValuesImplementation, VALUES_PATTERN, - PhysicalOption::new(PlanImpl::Values, SortOption::None) + PhysicalOption::new(PlanImpl::Values, SortOption::None, RowCountOption::Unknown) ); diff --git a/src/optimizer/rule/implementation/dql/window.rs b/src/optimizer/rule/implementation/dql/window.rs index ab234f8d..532b63c5 100644 --- a/src/optimizer/rule/implementation/dql/window.rs +++ b/src/optimizer/rule/implementation/dql/window.rs @@ -16,7 +16,7 @@ use crate::errors::DatabaseError; use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption}; use std::sync::LazyLock; static WINDOW_PATTERN: LazyLock = LazyLock::new(|| Pattern { @@ -43,7 +43,7 @@ impl ImplementationRule for WindowImplementation { if let Operator::Window(op) = op { crate::optimizer::core::rule::keep_best_physical_option( best_physical_option, - PhysicalOption::new(PlanImpl::Window, op.sort_option()), + PhysicalOption::new(PlanImpl::Window, op.sort_option(), RowCountOption::Follow), None, ); } diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index 2e483454..992f88fc 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -19,7 +19,7 @@ use crate::optimizer::plan_utils::{only_child_mut, replace_with_only_child, wrap use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::table_scan::TableScanOperator; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::planner::{Childrens, ExprRef, LogicalPlan}; use crate::types::index::{IndexLookup, IndexOrderHint}; @@ -56,7 +56,11 @@ impl NormalizationRule for EliminateRedundantSort { offset, limit: Some(limit), }); - plan.physical_option = Some(PhysicalOption::new(PlanImpl::Limit, SortOption::Follow)); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::Limit, + SortOption::Follow, + RowCountOption::Unknown, + )); return Ok(true); } @@ -296,7 +300,11 @@ impl NormalizationRule for UseStreamAggregate { return Ok(false); } - plan.physical_option = Some(PhysicalOption::new(implementation, SortOption::Follow)); + plan.physical_option = Some(PhysicalOption::new( + implementation, + SortOption::Follow, + RowCountOption::Unknown, + )); Ok(true) } } @@ -341,8 +349,16 @@ impl NormalizationRule for ForceSpillAggregate { return Ok(false); } let sort = only_child_mut(plan).expect("aggregate child was wrapped with sort"); - sort.physical_option = Some(PhysicalOption::new(PlanImpl::Sort, sort_option)); - plan.physical_option = Some(PhysicalOption::new(implementation, SortOption::Follow)); + sort.physical_option = Some(PhysicalOption::new( + PlanImpl::Sort, + sort_option, + RowCountOption::Follow, + )); + plan.physical_option = Some(PhysicalOption::new( + implementation, + SortOption::Follow, + RowCountOption::Unknown, + )); Ok(true) } } @@ -376,6 +392,7 @@ fn ensure_order( if let Some(PhysicalOption { plan: PlanImpl::IndexScan(index_info), sort_option, + .. }) = plan.physical_option.as_mut() { if covers(required, &index_info.sort_option, |required, provided| { @@ -507,7 +524,9 @@ mod tests { use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::top_k::TopKOperator; - use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; + use crate::planner::operator::{ + Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption, + }; use crate::planner::{Childrens, ExprRef, LogicalPlan}; use crate::types::index::{IndexInfo, IndexLookup, IndexMeta, IndexType}; use crate::types::value::DataValue; @@ -544,6 +563,7 @@ mod tests { leaf.physical_option = Some(PhysicalOption::new( PlanImpl::IndexScan(Box::new(index_info)), index_sort_option, + RowCountOption::Unknown, )); let predicate = @@ -556,7 +576,11 @@ mod tests { }), Childrens::Only(Box::new(leaf)), ); - filter.physical_option = Some(PhysicalOption::new(PlanImpl::Filter, SortOption::Follow)); + filter.physical_option = Some(PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Unknown, + )); LogicalPlan::new( Operator::Sort(SortOperator { @@ -632,6 +656,7 @@ mod tests { scan.physical_option = Some(PhysicalOption::new( PlanImpl::IndexScan(Box::new(index_info)), sort_option, + RowCountOption::Unknown, )); let mut filter = LogicalPlan::new( @@ -642,7 +667,11 @@ mod tests { }), Childrens::Only(Box::new(scan)), ); - filter.physical_option = Some(PhysicalOption::new(PlanImpl::Filter, SortOption::Follow)); + filter.physical_option = Some(PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Unknown, + )); filter } @@ -894,7 +923,11 @@ mod tests { offset: None, limit: Some(3), }); - filter.physical_option = Some(PhysicalOption::new(PlanImpl::Limit, SortOption::Follow)); + filter.physical_option = Some(PhysicalOption::new( + PlanImpl::Limit, + SortOption::Follow, + RowCountOption::Unknown, + )); assert!(!EliminateRedundantSort.apply(&mut plan, &mut arena)?); assert!(!scan_is_reverse(&plan)); @@ -1063,12 +1096,14 @@ mod tests { child.physical_option = Some(PhysicalOption::new( PlanImpl::IndexScan(Box::new(index_info)), sort_option, + RowCountOption::Unknown, )); } } plan.physical_option = Some(PhysicalOption::new( PlanImpl::HashAggregate, SortOption::None, + RowCountOption::Unknown, )); let rule = UseStreamAggregate; @@ -1102,10 +1137,12 @@ mod tests { child.physical_option = Some(PhysicalOption::new( PlanImpl::IndexScan(Box::new(scan_op.index_infos[0].clone())), sort_option, + RowCountOption::Unknown, )); plan.physical_option = Some(PhysicalOption::new( PlanImpl::HashAggregate, SortOption::None, + RowCountOption::Unknown, )); let rule = UseStreamAggregate; @@ -1132,6 +1169,7 @@ mod tests { unordered.physical_option = Some(PhysicalOption::new( PlanImpl::HashAggregate, SortOption::None, + RowCountOption::Unknown, )); assert!(!rule.apply(&mut unordered, &mut arena)?); assert!(matches!( @@ -1158,6 +1196,7 @@ mod tests { plan.physical_option = Some(PhysicalOption::new( PlanImpl::HashAggregate, SortOption::None, + RowCountOption::Unknown, )); let rule = ForceSpillAggregate; @@ -1238,6 +1277,7 @@ mod tests { scan_plan.physical_option = Some(PhysicalOption::new( PlanImpl::IndexScan(Box::new(index_info.clone())), index_info.sort_option, + RowCountOption::Unknown, )); } @@ -1250,7 +1290,11 @@ mod tests { }), Childrens::Only(Box::new(scan_plan)), ); - filter.physical_option = Some(PhysicalOption::new(PlanImpl::Filter, SortOption::Follow)); + filter.physical_option = Some(PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Unknown, + )); let mut plan = LogicalPlan::new( Operator::Sort(SortOperator { diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs new file mode 100644 index 00000000..3258b4b6 --- /dev/null +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -0,0 +1,524 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::catalog::ColumnRef; +use crate::errors::DatabaseError; +use crate::expression::visitor_mut::ExprVisitorMut; +use crate::expression::ScalarExpression; +use crate::optimizer::core::rule::NormalizationRule; +use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; +use crate::planner::operator::project::ProjectOperator; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; +use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; +use crate::types::tuple::Schema; + +pub(crate) struct GreedyJoinReorder; + +fn estimate_join_rows(left: usize, right: usize) -> usize { + // TODO: For INNER equijoins, use left_rows * right_rows / max(left_ndv, right_ndv) + // and cap the estimate with join-key uniqueness. Uniqueness bounds matches; + // it does not guarantee every input row finds a match. + left.saturating_mul(right) +} + +fn reorderable(plan: &LogicalPlan) -> bool { + matches!(&plan.operator, Operator::Join(JoinOperator { + join_type: JoinType::Inner, + on: JoinCondition::On { on, filter: None }, + force_nested_loop: false, + limit_pushed: false, + }) if !on.is_empty()) +} + +fn inspect<'plan>( + plan: &'plan mut LogicalPlan, + schemas: &mut Vec<&'plan Schema>, + keys: &mut Vec<(ExprRef, ExprRef)>, + arena: &mut PlanArena, +) -> Option { + if reorderable(plan) { + let ( + Operator::Join(JoinOperator { + on: JoinCondition::On { on, .. }, + .. + }), + Childrens::Twins { left, right }, + ) = (&plan.operator, plan.childrens.as_mut()) + else { + unreachable!() + }; + keys.extend_from_slice(on); + let left_rows = inspect(left, schemas, keys, arena)?; + let right_rows = inspect(right, schemas, keys, arena)?; + let rows = estimate_join_rows(left_rows, right_rows); + if let Some(option) = &mut plan.physical_option { + option.row_count = RowCountOption::Estimated(rows); + } + Some(rows) + } else { + let rows = plan.estimated_rows()?; + schemas.push(plan.output_schema(arena)); + Some(rows) + } +} + +fn flatten(plan: LogicalPlan, inputs: &mut Vec<(LogicalPlan, usize)>) { + if reorderable(&plan) { + let (left, right) = plan.childrens.pop_twins(); + flatten(left, inputs); + flatten(right, inputs); + } else { + // SAFETY: inspect has already verified every input's row estimate before detaching the region. + let rows = plan.estimated_rows().unwrap(); + inputs.push((plan, rows)); + } +} + +fn key_in_schema(expr: ExprRef, schema: &Schema, arena: &PlanArena) -> Result { + expr.all_referenced_columns(arena, |arena, column| { + schema + .iter() + .any(|candidate| arena.same_column(*candidate, *column)) + }) +} + +struct BindPosition<'a>(&'a Schema); + +impl ExprVisitorMut for BindPosition<'_> { + fn visit_column_ref( + &mut self, + column: &mut ColumnRef, + position: &mut usize, + arena: &mut (dyn MetaArena + '_), + ) -> Result<(), DatabaseError> { + *position = self + .0 + .iter() + .position(|candidate| arena.same_column(*candidate, *column)) + .ok_or(DatabaseError::SourceNotFound)?; + Ok(()) + } +} + +impl NormalizationRule for GreedyJoinReorder { + fn apply(&self, plan: &mut LogicalPlan, arena: &mut PlanArena) -> Result { + if !reorderable(plan) { + return Ok(false); + } + let original_schema = plan.output_schema(arena).clone(); + let mut schemas = Vec::new(); + let mut keys = Vec::new(); + if inspect(plan, &mut schemas, &mut keys, arena).is_none() || schemas.len() < 3 { + return Ok(false); + } + // Ambiguous column identities cannot be rebound to an individual input. + for (i, left) in schemas.iter().enumerate() { + for right in &schemas[i + 1..] { + if left + .iter() + .any(|l| right.iter().any(|r| arena.same_column(*l, *r))) + { + return Ok(false); + } + } + } + // Constants and keys spanning several inputs need separate predicate handling. + for &(left, right) in &keys { + let mut endpoints = [None, None]; + for (endpoint, expr) in endpoints.iter_mut().zip([left, right]) { + if !expr.any_referenced_column(arena, |_, _| true)? { + return Ok(false); + } + for (index, schema) in schemas.iter().enumerate() { + if key_in_schema(expr, schema, arena)? { + *endpoint = Some(index); + break; + } + } + if endpoint.is_none() { + return Ok(false); + } + } + if endpoints[0] == endpoints[1] { + return Ok(false); + } + } + let mut inputs = Vec::with_capacity(schemas.len()); + flatten(plan.take(), &mut inputs); + while inputs.len() > 1 { + let mut best = None; + for (i, (_, left)) in inputs.iter().enumerate() { + for (j, (_, right)) in inputs.iter().enumerate().skip(i + 1) { + let candidate = (estimate_join_rows(*left, *right), i, j); + if best.is_none_or(|previous| candidate < previous) { + best = Some(candidate); + } + } + } + let Some((product, i, j)) = best else { + return Ok(false); + }; + let (mut right, _) = inputs.remove(j); + let (mut left, _) = inputs.remove(i); + let left_schema = left.output_schema(arena); + let right_schema = right.output_schema(arena); + let mut on = Vec::new(); + let mut index = 0; + while index < keys.len() { + let (l, r) = keys[index]; + let key = if key_in_schema(l, left_schema, arena)? + && key_in_schema(r, right_schema, arena)? + { + Some((l, r)) + } else if key_in_schema(r, left_schema, arena)? + && key_in_schema(l, right_schema, arena)? + { + Some((r, l)) + } else { + None + }; + if let Some(key) = key { + keys.remove(index); + on.push(key); + } else { + index += 1; + } + } + for (l, r) in &mut on { + *l = l.clone_expression(arena)?; + *r = r.clone_expression(arena)?; + BindPosition(left_schema).visit(l, arena)?; + BindPosition(right_schema).visit(r, arena)?; + } + let (condition, join_type, implementation) = if on.is_empty() { + (JoinCondition::None, JoinType::Cross, PlanImpl::NestLoopJoin) + } else { + ( + JoinCondition::On { on, filter: None }, + JoinType::Inner, + PlanImpl::HashJoin, + ) + }; + let mut join = JoinOperator::build(left, right, condition, join_type, false); + join.physical_option = Some(PhysicalOption::new( + implementation, + SortOption::None, + RowCountOption::Estimated(product), + )); + inputs.insert(i, (join, product)); + } + // SAFETY: At this point, there should be exactly one plan left in the inputs vector, which is the final join result. + let (mut join, _) = inputs.pop().unwrap(); + let schema = join.output_schema(arena); + if &original_schema == schema { + *plan = join; + return Ok(true); + } + let exprs = original_schema + .iter() + .map(|column| { + let position = schema + .iter() + .position(|candidate| arena.same_column(*candidate, *column)) + .unwrap(); + arena.alloc_expression(ScalarExpression::column_expr(*column, position)) + }) + .collect(); + *plan = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs }), + Childrens::Only(Box::new(join)), + ); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )); + Ok(true) + } +} + +// GRCOV_EXCL_START +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::*; + use crate::catalog::{ColumnCatalog, ColumnDesc}; + use crate::planner::operator::filter::FilterOperator; + use crate::planner::operator::table_scan::TableScanOperator; + use crate::planner::TableArenaCell; + use crate::types::value::DataValue; + use crate::types::LogicalType; + + fn input(name: &str, rows: usize, arena: &mut PlanArena) -> (LogicalPlan, ExprRef) { + let column = arena.alloc_column(ColumnCatalog::new( + name.into(), + false, + ColumnDesc::new(LogicalType::Integer, None, false, None).unwrap(), + )); + let key = arena.alloc_expression(ScalarExpression::column_expr(column, 0)); + let mut plan = LogicalPlan::new( + Operator::TableScan(TableScanOperator { + table_name: name.into(), + columns: vec![column], + limit: (None, None), + index_infos: Vec::new(), + with_pk: false, + }), + Childrens::None, + ); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated(rows), + )); + (plan, key) + } + + fn three_inputs(arena: &mut PlanArena) -> LogicalPlan { + let (a, a_key) = input("a", 1000, arena); + let (b, b_key) = input("b", 10, arena); + let (c, c_key) = input("c", 20, arena); + let ab = JoinOperator::build( + a, + b, + JoinCondition::On { + on: vec![(a_key, b_key)], + filter: None, + }, + JoinType::Inner, + false, + ); + let b_column = b_key.output_column_ref(arena); + let b_key = arena.alloc_expression(ScalarExpression::column_expr(b_column, 1)); + JoinOperator::build( + ab, + c, + JoinCondition::On { + on: vec![(b_key, c_key)], + filter: None, + }, + JoinType::Inner, + false, + ) + } + + #[test] + fn smallest_connected_product_preserves_schema_and_key_positions() -> Result<(), DatabaseError> + { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let mut plan = three_inputs(&mut arena); + let schema = plan.output_schema(&mut arena).clone(); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + assert_eq!(plan.estimated_rows(), Some(200000)); + let (left, right) = plan.childrens.twins(); + assert_eq!(left.referenced_table(), vec!["a".into()]); + assert_eq!(right.referenced_table(), vec!["b".into(), "c".into()]); + assert_eq!(right.estimated_rows(), Some(200)); + let Operator::Join(op) = &right.operator else { + unreachable!() + }; + let JoinCondition::On { on, .. } = &op.on else { + unreachable!() + }; + for expr in [on[0].0, on[0].1] { + assert!(matches!( + arena.expression(expr), + ScalarExpression::ColumnRef { position: 0, .. } + )); + } + Ok(()) + } + + #[test] + fn nonadjacent_pair_restores_output_order_and_keeps_all_keys() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let (a, a_key) = input("a", 10, &mut arena); + let (b, b_key) = input("b", 1000, &mut arena); + let (c, c_key) = input("c", 20, &mut arena); + let ab = JoinOperator::build( + a, + b, + JoinCondition::On { + on: vec![(a_key, b_key)], + filter: None, + }, + JoinType::Inner, + false, + ); + let b_column = b_key.output_column_ref(&mut arena); + let b_key = arena.alloc_expression(ScalarExpression::column_expr(b_column, 1)); + let mut plan = JoinOperator::build( + ab, + c, + JoinCondition::On { + on: vec![(a_key, c_key), (b_key, c_key)], + filter: None, + }, + JoinType::Inner, + false, + ); + let schema = plan.output_schema(&mut arena).clone(); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + let Operator::Project(project) = &plan.operator else { + unreachable!() + }; + for (expr, expected) in project.exprs.iter().zip([0, 2, 1]) { + assert!( + matches!(arena.expression(*expr), ScalarExpression::ColumnRef { position, .. } if *position == expected) + ); + } + let join = plan.childrens.only(); + let (left, right) = join.childrens.twins(); + assert_eq!(left.referenced_table(), vec!["a".into(), "c".into()]); + assert_eq!(right.referenced_table(), vec!["b".into()]); + let Operator::Join(op) = &join.operator else { + unreachable!() + }; + let JoinCondition::On { on, .. } = &op.on else { + unreachable!() + }; + assert_eq!(on.len(), 2); + let Operator::Join(op) = &left.operator else { + unreachable!() + }; + let JoinCondition::On { on, .. } = &op.on else { + unreachable!() + }; + assert_eq!(on.len(), 1); + Ok(()) + } + + #[test] + fn smallest_pair_without_a_key_defers_both_conditions() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let mut plan = three_inputs(&mut arena); + if let Childrens::Twins { left, .. } = plan.childrens.as_mut() { + if let Childrens::Twins { left, right } = left.childrens.as_mut() { + left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated(10); + right.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated(1000); + } + } + let schema = plan.output_schema(&mut arena).clone(); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + let join = plan.childrens.only(); + let (left, right) = join.childrens.twins(); + assert_eq!(left.referenced_table(), vec!["a".into(), "c".into()]); + assert_eq!(right.referenced_table(), vec!["b".into()]); + assert!(matches!( + &left.operator, + Operator::Join(JoinOperator { + join_type: JoinType::Cross, + on: JoinCondition::None, + .. + }) + )); + assert_eq!( + left.physical_option.as_ref().unwrap().plan, + PlanImpl::NestLoopJoin + ); + let Operator::Join(op) = &join.operator else { + unreachable!() + }; + let JoinCondition::On { on, .. } = &op.on else { + unreachable!() + }; + assert_eq!(on.len(), 2); + Ok(()) + } + + #[test] + fn rebuilding_join_region_preserves_parent_and_input_subtrees() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let mut join = three_inputs(&mut arena); + let Childrens::Twins { right, .. } = join.childrens.as_mut() else { + unreachable!() + }; + let predicate = + arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(true))); + let mut filter = FilterOperator::build(predicate, right.take(), false); + filter.physical_option = Some(PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Estimated(20), + )); + **right = filter; + let schema = join.output_schema(&mut arena).clone(); + let exprs = schema + .iter() + .enumerate() + .map(|(position, column)| { + arena.alloc_expression(ScalarExpression::column_expr(*column, position)) + }) + .collect(); + let mut plan = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs }), + Childrens::Only(Box::new(join)), + ); + let original_parent = plan.operator.clone(); + let Childrens::Only(join) = plan.childrens.as_mut() else { + unreachable!() + }; + assert!(GreedyJoinReorder.apply(join, &mut arena)?); + assert_eq!(plan.operator, original_parent); + assert_eq!(plan.output_schema(&mut arena), &schema); + let (_, right) = plan.childrens.only().childrens.twins(); + let (_, filter) = right.childrens.twins(); + assert!(matches!(filter.operator, Operator::Filter(_))); + assert_eq!(filter.childrens.only().referenced_table(), vec!["c".into()]); + assert_eq!(filter.estimated_rows(), Some(20)); + Ok(()) + } + + #[test] + fn unknown_inputs_and_outer_joins_keep_original_order() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let mut plan = three_inputs(&mut arena); + if let Childrens::Twins { right, .. } = plan.childrens.as_mut() { + right.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; + } + let original = plan.clone(); + assert!(!GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert_eq!(plan, original); + if let Operator::Join(op) = &mut plan.operator { + op.join_type = JoinType::LeftOuter; + } + let original = plan.clone(); + assert!(!GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert_eq!(plan, original); + let mut plan = three_inputs(&mut arena); + if let Childrens::Twins { left, right } = plan.childrens.as_mut() { + right.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated(10); + if let Childrens::Twins { left, .. } = left.childrens.as_mut() { + left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated(10); + } + } + let schema = plan.output_schema(&mut arena).clone(); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + let (left, right) = plan.childrens.twins(); + assert_eq!(left.referenced_table(), vec!["a".into(), "b".into()]); + assert_eq!(right.referenced_table(), vec!["c".into()]); + assert_eq!(estimate_join_rows(usize::MAX, 2), usize::MAX); + assert_eq!(estimate_join_rows(0, 20), 0); + Ok(()) + } +} +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index 87eea173..04eab69a 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -40,7 +40,9 @@ mod column_pruning; mod combine_operators; mod compilation_in_advance; mod elimination; +mod join_reorder; mod min_max_top_k; +use join_reorder::GreedyJoinReorder; mod parameterized_index; mod pushdown_limit; mod pushdown_predicates; @@ -80,6 +82,7 @@ pub enum NormalizationRuleImpl { ParameterizeMarkApply, ParameterizeInnerJoin, EliminateIndexFilter, + GreedyJoinReorder, } #[derive(Debug, Copy, Clone, Eq, PartialEq)] @@ -184,7 +187,8 @@ impl NormalizationRuleImpl { NormalizationRuleImpl::EvaluatorBind => NormalizationRuleRootTag::Any, NormalizationRuleImpl::MinMaxToTopK => NormalizationRuleRootTag::Aggregate, NormalizationRuleImpl::EliminateIndexFilter => NormalizationRuleRootTag::Filter, - NormalizationRuleImpl::ParameterizeInnerJoin => NormalizationRuleRootTag::Join, + NormalizationRuleImpl::ParameterizeInnerJoin + | NormalizationRuleImpl::GreedyJoinReorder => NormalizationRuleRootTag::Join, NormalizationRuleImpl::ParameterizeMarkApply => NormalizationRuleRootTag::MarkApply, } } @@ -198,6 +202,7 @@ impl NormalizationRule for NormalizationRuleImpl { ) -> Result { match self { NormalizationRuleImpl::ColumnPruning => ColumnPruning.apply(plan, arena), + NormalizationRuleImpl::GreedyJoinReorder => GreedyJoinReorder.apply(plan, arena), NormalizationRuleImpl::CollapseProject => CollapseProject.apply(plan, arena), NormalizationRuleImpl::CollapseGroupByAgg => CollapseGroupByAgg.apply(plan, arena), NormalizationRuleImpl::CombineFilter => CombineFilter.apply(plan, arena), diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 8ee20085..296a1ec7 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -22,7 +22,7 @@ use crate::planner::operator::join::{JoinCondition, JoinType}; use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyOperator, MarkApplyQuantifier}; use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::table_scan::TableScanOperator; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; +use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::planner::MetaArena; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; use crate::types::index::{IndexLookup, IndexType}; @@ -178,6 +178,7 @@ fn parameterize_right_subtree( plan.physical_option = Some(PhysicalOption::new( PlanImpl::IndexScan(Box::new(index_info)), sort_option, + RowCountOption::Unknown, )); return true; } @@ -314,6 +315,7 @@ fn parameterize( plan.physical_option = Some(PhysicalOption::new( PlanImpl::IndexScan(Box::new(info.clone())), info.sort_option.clone(), + RowCountOption::Unknown, )); return Some(probe); } @@ -379,8 +381,11 @@ impl NormalizationRule for ParameterizeInnerJoin { Operator::Project(ProjectOperator { exprs }), Childrens::None, ); - project.physical_option = - Some(PhysicalOption::new(PlanImpl::Project, SortOption::Follow)); + project.physical_option = Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )); (Some(project), probe) }; @@ -422,7 +427,11 @@ impl NormalizationRule for ParameterizeInnerJoin { if let Some(filter) = filter { // The original join filter uses the original left/right output slots. *plan = FilterOperator::build(filter, plan.take(), false); - plan.physical_option = Some(PhysicalOption::new(PlanImpl::Filter, SortOption::Follow)); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Unknown, + )); } Ok(true) } @@ -731,6 +740,7 @@ mod inner_join_tests { right.physical_option = Some(PhysicalOption::new( PlanImpl::IndexScan(Box::new(info.clone())), SortOption::None, + RowCountOption::Unknown, )); let right = FilterOperator::build(predicate, right, false); let mut plan = join(left, right, vec![(l0, r1), (l1, r2)], None); diff --git a/src/planner/mod.rs b/src/planner/mod.rs index 612bcba5..14c7db99 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -25,7 +25,7 @@ use crate::planner::operator::set_membership::SetMembershipOperator; use crate::planner::operator::union::UnionOperator; use crate::planner::operator::values::ValuesOperator; use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; -use crate::planner::operator::{Operator, PhysicalOption}; +use crate::planner::operator::{Operator, PhysicalOption, RowCountOption}; use kite_sql_serde_macros::ReferenceSerialization; use std::fmt; use std::hash::{Hash, Hasher}; @@ -185,6 +185,18 @@ impl LogicalPlan { std::mem::replace(self, Self::new(Operator::Dummy, Childrens::None)) } + pub(crate) fn estimated_rows(&self) -> Option { + match &self.physical_option.as_ref()?.row_count { + RowCountOption::Estimated(rows) => Some(*rows), + RowCountOption::Unknown => None, + RowCountOption::Follow => match self.childrens.as_ref() { + Childrens::Only(child) => child.estimated_rows(), + Childrens::Twins { left, .. } => left.estimated_rows(), + Childrens::None => None, + }, + } + } + pub fn referenced_table(&self) -> Vec { fn collect_table(plan: &LogicalPlan, results: &mut Vec) { if let Operator::TableScan(op) = &plan.operator { @@ -560,6 +572,29 @@ mod tests { .collect() } + #[test] + fn row_count_follows_input_and_stops_at_unknown() { + let mut input = table_scan("users", Vec::new()); + input.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated(12), + )); + let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::Only(Box::new(input))); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )); + assert_eq!(plan.estimated_rows(), Some(12)); + plan.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; + assert_eq!(plan.estimated_rows(), None); + plan.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated(0); + assert_eq!(plan.estimated_rows(), Some(0)); + plan.physical_option = None; + assert_eq!(plan.estimated_rows(), None); + } + #[test] fn childrens_iterates_and_pops_expected_plans() { let left = LogicalPlan::new(Operator::ShowTable, Childrens::None); @@ -598,7 +633,11 @@ mod tests { let id = column("id", LogicalType::Integer, &mut arena); let scan = table_scan("users", vec![id]); let mut plan = LimitOperator::build(Some(2), Some(5), scan); - plan.physical_option = Some(PhysicalOption::new(PlanImpl::Limit, SortOption::Follow)); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::Limit, + SortOption::Follow, + RowCountOption::Unknown, + )); let tables = plan .referenced_table() diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 27d696ef..20ea6359 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -156,15 +156,27 @@ pub enum SortOption { None, } +#[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] +pub enum RowCountOption { + Follow, + Unknown, + Estimated(usize), +} + #[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] pub struct PhysicalOption { pub plan: PlanImpl, pub(crate) sort_option: SortOption, + pub(crate) row_count: RowCountOption, } impl PhysicalOption { - pub fn new(plan: PlanImpl, sort_option: SortOption) -> Self { - Self { plan, sort_option } + pub fn new(plan: PlanImpl, sort_option: SortOption, row_count: RowCountOption) -> Self { + Self { + plan, + sort_option, + row_count, + } } pub fn sort_option(&self) -> &SortOption { @@ -760,7 +772,8 @@ mod tests { assert_eq!(SortOption::Follow.explain(&arena).to_string(), "Follow"); assert_eq!(SortOption::None.explain(&arena).to_string(), "None"); - let physical = PhysicalOption::new(PlanImpl::TopK, sort_option.clone()); + let physical = + PhysicalOption::new(PlanImpl::TopK, sort_option.clone(), RowCountOption::Unknown); assert_eq!( physical.explain(&arena).to_string(), "TopK => (Sort Option: OrderBy: (1 Desc Nulls First) ignore_prefix_len: 2)" diff --git a/src/planner/operator/visitor_mut.rs b/src/planner/operator/visitor_mut.rs index 10dee0e5..7970f86b 100644 --- a/src/planner/operator/visitor_mut.rs +++ b/src/planner/operator/visitor_mut.rs @@ -463,6 +463,7 @@ pub fn walk_mut_operator<'a, V: OperatorVisitorMut<'a>>( mod tests { use super::*; use crate::planner::operator::visitor::tests::all_operators; + use crate::planner::operator::RowCountOption; use crate::types::value::DataValue; struct IncrementConstants(usize); @@ -504,9 +505,17 @@ mod tests { } let mut child = LogicalPlan::new(Operator::Dummy, Childrens::None); - child.physical_option = Some(PhysicalOption::new(PlanImpl::Dummy, SortOption::None)); + child.physical_option = Some(PhysicalOption::new( + PlanImpl::Dummy, + SortOption::None, + RowCountOption::Unknown, + )); let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::Only(Box::new(child))); - plan.physical_option = Some(PhysicalOption::new(PlanImpl::Dummy, SortOption::None)); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::Dummy, + SortOption::None, + RowCountOption::Unknown, + )); let mut counter = Counter { operators: 0, diff --git a/tests/slt/join.slt b/tests/slt/join.slt index c65945a9..d4536cf0 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -575,6 +575,89 @@ select * from full_using_l full join full_using_r using(x) 1 2 20 5 200 NULL 3 NULL 6 300 +# Greedy reorder must preserve output slots, duplicate matches and NULL equality. +statement ok +create table greedy_a(id int primary key, k int, v int) + +statement ok +create table greedy_b(id int primary key, k bigint, v int) + +statement ok +create table greedy_c(id int primary key, k int, v int) + +statement ok +insert into greedy_a values (1,1,10),(2,2,20),(3,3,30),(4,NULL,40),(5,5,50),(6,6,60) + +statement ok +insert into greedy_b values (1,1,100),(2,2,200) + +statement ok +insert into greedy_c values (1,1,1000),(2,1,1100),(3,2,2000) + +statement ok +insert into greedy_a select cast(number+100 as int),cast(-number-1000 as int),0 from table(numbers(300)) + +statement ok +insert into greedy_b select cast(number+100 as int),-number-2000,0 from table(numbers(100)) + +statement ok +insert into greedy_c select cast(number+100 as int),cast(-number-3000 as int),0 from table(numbers(200)) + +statement ok +analyze table greedy_a + +statement ok +analyze table greedy_b + +statement ok +analyze table greedy_c + +query T +explain select a.v,b.v,c.v from greedy_a a join greedy_b b on a.k=b.k join greedy_c c on b.k=c.k +---- +Projection [a.v, b.v, c.v] [Project => (Sort Option: Follow)] Inner Join On cast (a.k as Bigint) = b.k [HashJoin => (Sort Option: None)] Projection [(greedy_a.k) as (a.k), (greedy_a.v) as (a.v)] [Project => (Sort Option: Follow)] TableScan greedy_a -> [greedy_a.k, greedy_a.v] [SeqScan => (Sort Option: None)] Inner Join On b.k = cast (c.k as Bigint) [HashJoin => (Sort Option: None)] Projection [(greedy_b.k) as (b.k), (greedy_b.v) as (b.v)] [Project => (Sort Option: Follow)] TableScan greedy_b -> [greedy_b.k, greedy_b.v] [SeqScan => (Sort Option: None)] Projection [(greedy_c.k) as (c.k), (greedy_c.v) as (c.v)] [Project => (Sort Option: Follow)] TableScan greedy_c -> [greedy_c.k, greedy_c.v] [SeqScan => (Sort Option: None)] + +query III rowsort +select a.v,b.v,c.v from greedy_a a join greedy_b b on a.k=b.k join greedy_c c on b.k=c.k +---- +10 100 1000 +10 100 1100 +20 200 2000 + +# The smallest inputs b and c have no direct key; apply both keys when a joins. +query III rowsort +select a.v,b.v,c.v from greedy_a a join greedy_b b on a.k=b.k join greedy_c c on a.k=c.k +---- +10 100 1000 +10 100 1100 +20 200 2000 + +query III rowsort +select c.v,a.v,b.v from greedy_a a join greedy_b b on a.k=b.k join greedy_c c on b.k=c.k where a.v+c.v>1050 +---- +1100 10 100 +2000 20 200 + +query III rowsort +select a.v,b.v,c.v from greedy_a a left join greedy_b b on a.k=b.k left join greedy_c c on b.k=c.k where a.id<100 +---- +10 100 1000 +10 100 1100 +20 200 2000 +30 NULL NULL +40 NULL NULL +50 NULL NULL +60 NULL NULL + +statement ok +drop table greedy_a + +statement ok +drop table greedy_b + +statement ok +drop table greedy_c + statement ok drop table full_using_l diff --git a/tests/slt/where_by_index_explain.slt b/tests/slt/where_by_index_explain.slt index b24ada38..60d39041 100644 --- a/tests/slt/where_by_index_explain.slt +++ b/tests/slt/where_by_index_explain.slt @@ -23,7 +23,7 @@ analyze table t1; query T explain select a.c2, b.c2 from t1 a join t1 b on a.id + 3 = b.id where a.id = 3 and b.id = 6; ---- -Projection [a.c2, b.c2] [Project => (Sort Option: Follow)] Inner Join Where ((a.id + 3) = b.id) [NestLoopJoin => (Sort Option: None)] TableScan t1 -> [t1.id, t1.c2] [IndexScan By pk_index => 3 => (Sort Option: OrderBy: (t1.id Asc Nulls Last) ignore_prefix_len: 0)] Filter ((t1.id) as (b.id) = 6), Is Having: false [Filter => (Sort Option: Follow)] TableScan t1 -> [t1.id, t1.c2] [IndexScan By pk_index => 6 => (Sort Option: OrderBy: (t1.id Asc Nulls Last) ignore_prefix_len: 0)] +Projection [a.c2, b.c2] [Project => (Sort Option: Follow)] Inner Join Where ((a.id + 3) = b.id) [NestLoopJoin => (Sort Option: None)] TableScan t1 -> [t1.id, t1.c2] [IndexScan By pk_index => 3 => (Sort Option: OrderBy: (t1.id Asc Nulls Last) ignore_prefix_len: 0)] TableScan t1 -> [t1.id, t1.c2] [IndexScan By pk_index => 6 => (Sort Option: OrderBy: (t1.id Asc Nulls Last) ignore_prefix_len: 0)] query II select a.c2, b.c2 from t1 a join t1 b on a.id + 3 = b.id where a.id = 3 and b.id = 6; From 1143edddf9b4b4f851422b7588e3ef6554a7afc2 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:38 +0800 Subject: [PATCH 02/62] perf: avoid repeated greedy join reordering and prioritize equijoins --- src/db.rs | 16 +- src/optimizer/heuristic/batch.rs | 6 +- src/optimizer/heuristic/optimizer.rs | 172 ++++++++++++-- .../rule/implementation/dql/filter.rs | 7 +- src/optimizer/rule/implementation/mod.rs | 14 +- .../rule/normalization/join_reorder.rs | 209 +++++++++++++----- tests/slt/join.slt | 19 ++ 7 files changed, 348 insertions(+), 95 deletions(-) diff --git a/src/db.rs b/src/db.rs index 7afe3c9c..c83cb9fc 100644 --- a/src/db.rs +++ b/src/db.rs @@ -416,7 +416,7 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { ) .before_batch( "TopK".to_string(), - HepBatchStrategy::once_topdown(), + HepBatchStrategy::fix_point_topdown(1), vec![ NormalizationRuleImpl::MinMaxToTopK, NormalizationRuleImpl::TopK, @@ -437,8 +437,13 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { vec![NormalizationRuleImpl::PushPredicateIntoScan], ) .after_batch( - "Parameterize Mark Apply".to_string(), + "Greedy Join Reorder".to_string(), HepBatchStrategy::once_topdown(), + vec![NormalizationRuleImpl::GreedyJoinReorder], + ) + .after_batch( + "Parameterize Mark Apply".to_string(), + HepBatchStrategy::fix_point_topdown(1), vec![ NormalizationRuleImpl::ParameterizeMarkApply, NormalizationRuleImpl::ParameterizeInnerJoin, @@ -448,14 +453,9 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { // lookup. A Probe must not inherit residuals from a replaced static range. .after_batch( "Eliminate Index Filter".to_string(), - HepBatchStrategy::once_topdown(), + HepBatchStrategy::fix_point_topdown(1), vec![NormalizationRuleImpl::EliminateIndexFilter], ) - .after_batch( - "Greedy Join Reorder".to_string(), - HepBatchStrategy::once_bottomup(), - vec![NormalizationRuleImpl::GreedyJoinReorder], - ) .after_batch( "Expression Remapper".to_string(), HepBatchStrategy::once_topdown(), diff --git a/src/optimizer/heuristic/batch.rs b/src/optimizer/heuristic/batch.rs index 13b5b8c5..28b98971 100644 --- a/src/optimizer/heuristic/batch.rs +++ b/src/optimizer/heuristic/batch.rs @@ -132,6 +132,8 @@ pub enum HepMatchOrder { #[derive(Clone, Copy)] pub enum HepBatchStrategy { + /// Each local rule may succeed once across the tree. + Once(HepMatchOrder), /// Stop at a fix point or after the configured number of full-tree iterations. MaxTimes { max_iteration: usize, @@ -143,11 +145,11 @@ pub enum HepBatchStrategy { impl HepBatchStrategy { pub fn once_topdown() -> Self { - Self::fix_point_topdown(1) + Self::Once(HepMatchOrder::TopDown) } pub fn once_bottomup() -> Self { - Self::fix_point_bottomup(1) + Self::Once(HepMatchOrder::BottomUp) } pub fn fix_point_topdown(max_iteration: usize) -> Self { diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index e9287a81..c05b1108 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -61,7 +61,13 @@ impl<'a> HepOptimizer<'a> { loader: Option<&StatisticMetaLoader<'_>>, arena: &mut PlanArena, ) -> Result { - Self::apply_batches(&mut self.plan, self.before_batches, arena)?; + let mut applied_rules = Vec::new(); + Self::apply_batches( + &mut self.plan, + self.before_batches, + &mut applied_rules, + arena, + )?; if let Some(loader) = loader { if self.implementation_index.is_empty().not() { @@ -79,7 +85,12 @@ impl<'a> HepOptimizer<'a> { )?; } } - Self::apply_batches(&mut self.plan, self.after_batches, arena)?; + Self::apply_batches( + &mut self.plan, + self.after_batches, + &mut applied_rules, + arena, + )?; Ok(self.plan) } @@ -88,22 +99,26 @@ impl<'a> HepOptimizer<'a> { fn apply_batches( plan: &mut LogicalPlan, batches: &[HepBatch], + applied_rules: &mut Vec, arena: &mut PlanArena, ) -> Result<(), DatabaseError> { for batch in batches { match batch.strategy { + HepBatchStrategy::Once(order) => { + Self::apply_batch(plan, batch, order, applied_rules, arena)?; + } HepBatchStrategy::MaxTimes { max_iteration, order, } => { for _ in 0..max_iteration { - if !Self::apply_batch(plan, batch, order, arena)? { + if !Self::apply_batch(plan, batch, order, applied_rules, arena)? { break; } } } HepBatchStrategy::LoopIfApplied(order) => { - while Self::apply_batch(plan, batch, order, arena)? {} + while Self::apply_batch(plan, batch, order, applied_rules, arena)? {} } } } @@ -115,6 +130,7 @@ impl<'a> HepOptimizer<'a> { plan: &mut LogicalPlan, batch: &HepBatch, order: HepMatchOrder, + applied_rules: &mut Vec, arena: &mut PlanArena, ) -> Result { let mut applied = false; @@ -126,7 +142,11 @@ impl<'a> HepOptimizer<'a> { } } HepBatchStep::LocalRewrite(rules) => { - if Self::apply_local_rules(plan, rules, order, arena)? { + applied_rules.clear(); + if matches!(batch.strategy, HepBatchStrategy::Once(_)) { + applied_rules.resize(rules.len(), false); + } + if Self::apply_local_rules(plan, rules, order, applied_rules, arena)? { applied = true; } } @@ -384,18 +404,26 @@ impl<'a> HepOptimizer<'a> { plan: &mut LogicalPlan, rules: &HepLocalRewriteBatch, order: HepMatchOrder, + applied_rules: &mut [bool], arena: &mut PlanArena, ) -> Result { + if !applied_rules.is_empty() && applied_rules.iter().all(|done| *done) { + return Ok(false); + } let mut applied = false; if matches!(order, HepMatchOrder::TopDown) { - applied |= Self::apply_local_rules_current(plan, rules, arena)?; + applied |= Self::apply_local_rules_current(plan, rules, applied_rules, arena)?; } let child_applied = match plan.childrens.as_mut() { - Childrens::Only(child) => Self::apply_local_rules(child, rules, order, arena)?, + Childrens::Only(child) => { + Self::apply_local_rules(child, rules, order, applied_rules, arena)? + } Childrens::Twins { left, right } => { - let left_applied = Self::apply_local_rules(left, rules, order, arena)?; - let right_applied = Self::apply_local_rules(right, rules, order, arena)?; + let left_applied = + Self::apply_local_rules(left, rules, order, applied_rules, arena)?; + let right_applied = + Self::apply_local_rules(right, rules, order, applied_rules, arena)?; left_applied || right_applied } Childrens::None => false, @@ -406,7 +434,7 @@ impl<'a> HepOptimizer<'a> { applied |= child_applied; if matches!(order, HepMatchOrder::BottomUp) { - applied |= Self::apply_local_rules_current(plan, rules, arena)?; + applied |= Self::apply_local_rules_current(plan, rules, applied_rules, arena)?; } Ok(applied) } @@ -414,13 +442,20 @@ impl<'a> HepOptimizer<'a> { fn apply_local_rules_current( plan: &mut LogicalPlan, rules: &HepLocalRewriteBatch, + applied_rules: &mut [bool], arena: &mut PlanArena, ) -> Result { let mut applied = false; let mut next_rule_idx = 0; while let Some(idx) = rules.next_matching_rule_index_from(&plan.operator, next_rule_idx) { next_rule_idx = idx + 1; + if applied_rules.get(idx).copied().unwrap_or(false) { + continue; + } if rules.rules[idx].apply(plan, arena)? { + if let Some(done) = applied_rules.get_mut(idx) { + *done = true; + } applied = true; plan.reset_output_schema_cache_recursive(); } @@ -476,13 +511,9 @@ impl ImplementationRuleIndex { Operator::Dummy if self.contains(ImplementationRuleImpl::Dummy) => Some( PhysicalOption::new(PlanImpl::Dummy, SortOption::None, RowCountOption::Unknown), ), - Operator::Filter(_) if self.contains(ImplementationRuleImpl::Filter) => { - Some(PhysicalOption::new( - PlanImpl::Filter, - SortOption::Follow, - RowCountOption::Unknown, - )) - } + Operator::Filter(_) if self.contains(ImplementationRuleImpl::Filter) => Some( + PhysicalOption::new(PlanImpl::Filter, SortOption::Follow, RowCountOption::Follow), + ), Operator::Join(join_op) if self.contains(ImplementationRuleImpl::HashJoin) => { Some(PhysicalOption::new( join_op.plan_impl(), @@ -657,14 +688,18 @@ mod tests { use crate::expression::ScalarExpression; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::heuristic::batch::{HepBatch, HepBatchStrategy, HepMatchOrder}; - use crate::optimizer::heuristic::optimizer::{HepOptimizer, HepOptimizerPipeline}; + use crate::optimizer::heuristic::optimizer::{ + HepOptimizer, HepOptimizerPipeline, ImplementationRuleIndex, + }; use crate::optimizer::rule::implementation::ImplementationRuleImpl; use crate::optimizer::rule::normalization::NormalizationRuleImpl; use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::sort::SortField; - use crate::planner::operator::{Operator, PlanImpl, SortOption}; + use crate::planner::operator::{ + Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption, + }; use crate::planner::{Childrens, LogicalPlan, PlanArena, TableArenaCell}; use crate::storage::{Storage, Transaction}; use crate::types::index::IndexLookup; @@ -682,20 +717,44 @@ mod tests { plan } + #[test] + fn filter_fast_path_follows_input_row_count() { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let mut plan = filter_chain(1, &mut arena); + let Childrens::Only(child) = plan.childrens.as_mut() else { + unreachable!() + }; + child.physical_option = Some(PhysicalOption::new( + PlanImpl::Dummy, + SortOption::None, + RowCountOption::Estimated(12), + )); + let implementations = ImplementationRuleIndex::new(vec![ImplementationRuleImpl::Filter]); + plan.physical_option = implementations.direct_physical_option(&plan.operator); + assert_eq!( + plan.physical_option.as_ref().unwrap().row_count, + RowCountOption::Follow + ); + assert_eq!(plan.estimated_rows(), Some(12)); + } + #[test] fn local_rule_order_and_sibling_application() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); let batch = HepBatch::new( "Combine Filters".into(), - HepBatchStrategy::once_bottomup(), + HepBatchStrategy::fix_point_bottomup(1), vec![NormalizationRuleImpl::CombineFilter], ); + let mut applied_rules = Vec::new(); let mut topdown = filter_chain(3, &mut arena); assert!(HepOptimizer::apply_batch( &mut topdown, &batch, HepMatchOrder::TopDown, + &mut applied_rules, &mut arena )?); assert!(matches!( @@ -707,6 +766,7 @@ mod tests { &mut bottomup, &batch, HepMatchOrder::BottomUp, + &mut applied_rules, &mut arena )?); assert!(matches!( @@ -717,12 +777,13 @@ mod tests { &mut bottomup, &batch, HepMatchOrder::BottomUp, + &mut applied_rules, &mut arena )?); for strategy in [ - HepBatchStrategy::once_topdown(), - HepBatchStrategy::once_bottomup(), + HepBatchStrategy::fix_point_topdown(1), + HepBatchStrategy::fix_point_bottomup(1), ] { let plan = LogicalPlan::new( Operator::Dummy, @@ -748,6 +809,73 @@ mod tests { Ok(()) } + #[test] + fn once_stops_each_rule_after_its_first_success() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + for strategy in [ + HepBatchStrategy::once_topdown(), + HepBatchStrategy::once_bottomup(), + ] { + let plan = LogicalPlan::new( + Operator::Dummy, + Childrens::Twins { + left: Box::new(filter_chain(2, &mut arena)), + right: Box::new(filter_chain(2, &mut arena)), + }, + ); + let pipeline = HepOptimizerPipeline::builder() + .before_batch( + "Combine Filters".into(), + strategy, + vec![NormalizationRuleImpl::CombineFilter], + ) + .build(); + let result = pipeline.instantiate(plan).find_best(None, &mut arena)?; + let (left, right) = result.childrens.twins(); + assert!(matches!(left.childrens.only().operator, Operator::Dummy)); + assert!(matches!( + right.childrens.only().operator, + Operator::Filter(_) + )); + } + Ok(()) + } + + #[test] + fn once_does_not_reapply_a_rule_to_its_rewritten_children() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + for (strategy, expected_depth) in [ + (HepBatchStrategy::once_topdown(), 1), + (HepBatchStrategy::fix_point_topdown(1), 3), + ] { + let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::None); + for _ in 0..3 { + plan = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: Vec::new() }), + Childrens::Only(Box::new(plan)), + ); + } + plan = LimitOperator::build(None, Some(5), plan); + let pipeline = HepOptimizerPipeline::builder() + .before_batch( + "Limit Project Transpose".into(), + strategy, + vec![NormalizationRuleImpl::LimitProjectTranspose], + ) + .build(); + let result = pipeline.instantiate(plan).find_best(None, &mut arena)?; + let mut cursor = &result; + for _ in 0..expected_depth { + assert!(matches!(cursor.operator, Operator::Project(_))); + cursor = cursor.childrens.only(); + } + assert!(matches!(cursor.operator, Operator::Limit(_))); + } + Ok(()) + } + #[test] fn bottomup_fix_point_respects_iteration_limit() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); diff --git a/src/optimizer/rule/implementation/dql/filter.rs b/src/optimizer/rule/implementation/dql/filter.rs index ffa5073c..92a3021c 100644 --- a/src/optimizer/rule/implementation/dql/filter.rs +++ b/src/optimizer/rule/implementation/dql/filter.rs @@ -28,12 +28,9 @@ static FILTER_PATTERN: LazyLock = LazyLock::new(|| Pattern { #[derive(Clone)] pub struct FilterImplementation; +// Without a selectivity estimate, retain the input estimate rather than assuming a filtering benefit. single_mapping!( FilterImplementation, FILTER_PATTERN, - PhysicalOption::new( - PlanImpl::Filter, - SortOption::Follow, - RowCountOption::Unknown - ) + PhysicalOption::new(PlanImpl::Filter, SortOption::Follow, RowCountOption::Follow) ); diff --git a/src/optimizer/rule/implementation/mod.rs b/src/optimizer/rule/implementation/mod.rs index c1a5879f..65f82be1 100644 --- a/src/optimizer/rule/implementation/mod.rs +++ b/src/optimizer/rule/implementation/mod.rs @@ -324,7 +324,9 @@ mod tests { }; use crate::planner::operator::function_scan::FunctionScanOperator; use crate::planner::operator::top_k::TopKOperator; - use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; + use crate::planner::operator::{ + Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption, + }; use crate::planner::{Childrens, LogicalPlan, PlanArena, TableArenaCell}; use crate::storage::StatisticsMetaCache; use crate::types::value::DataValue; @@ -418,11 +420,15 @@ mod tests { |op| matches!(op, Operator::Dummy), PlanImpl::Dummy, )?; - assert_sql_rule( + with_operator_from_sql( "select * from t1 where c1 = 1", - ImplementationRuleImpl::Filter, |op| matches!(op, Operator::Filter(_)), - PlanImpl::Filter, + |filter, arena| { + let option = best_option(ImplementationRuleImpl::Filter, &filter, arena)?; + assert_eq!(option.plan, PlanImpl::Filter); + assert_eq!(option.row_count, RowCountOption::Follow); + Ok(()) + }, )?; assert_sql_rule( "select * from t1 limit 1", diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 3258b4b6..fa3cef55 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -32,13 +32,19 @@ fn estimate_join_rows(left: usize, right: usize) -> usize { left.saturating_mul(right) } -fn reorderable(plan: &LogicalPlan) -> bool { - matches!(&plan.operator, Operator::Join(JoinOperator { - join_type: JoinType::Inner, - on: JoinCondition::On { on, filter: None }, +fn can_expand(plan: &LogicalPlan) -> bool { + let Operator::Join(JoinOperator { + join_type, + on, force_nested_loop: false, limit_pushed: false, - }) if !on.is_empty()) + }) = &plan.operator + else { + return false; + }; + matches!((join_type, on), (JoinType::Cross, JoinCondition::None)) + || matches!((join_type, on), + (JoinType::Inner, JoinCondition::On { on, filter: None }) if !on.is_empty()) } fn inspect<'plan>( @@ -47,18 +53,15 @@ fn inspect<'plan>( keys: &mut Vec<(ExprRef, ExprRef)>, arena: &mut PlanArena, ) -> Option { - if reorderable(plan) { - let ( - Operator::Join(JoinOperator { - on: JoinCondition::On { on, .. }, - .. - }), - Childrens::Twins { left, right }, - ) = (&plan.operator, plan.childrens.as_mut()) + if can_expand(plan) { + let (Operator::Join(join), Childrens::Twins { left, right }) = + (&plan.operator, plan.childrens.as_mut()) else { unreachable!() }; - keys.extend_from_slice(on); + if let JoinCondition::On { on, .. } = &join.on { + keys.extend_from_slice(on); + } let left_rows = inspect(left, schemas, keys, arena)?; let right_rows = inspect(right, schemas, keys, arena)?; let rows = estimate_join_rows(left_rows, right_rows); @@ -74,7 +77,7 @@ fn inspect<'plan>( } fn flatten(plan: LogicalPlan, inputs: &mut Vec<(LogicalPlan, usize)>) { - if reorderable(&plan) { + if can_expand(&plan) { let (left, right) = plan.childrens.pop_twins(); flatten(left, inputs); flatten(right, inputs); @@ -93,6 +96,22 @@ fn key_in_schema(expr: ExprRef, schema: &Schema, arena: &PlanArena) -> Result Result, DatabaseError> { + if key_in_schema(left, left_schema, arena)? && key_in_schema(right, right_schema, arena)? { + Ok(Some((left, right))) + } else if key_in_schema(right, left_schema, arena)? && key_in_schema(left, right_schema, arena)? + { + Ok(Some((right, left))) + } else { + Ok(None) + } +} + struct BindPosition<'a>(&'a Schema); impl ExprVisitorMut for BindPosition<'_> { @@ -113,7 +132,9 @@ impl ExprVisitorMut for BindPosition<'_> { impl NormalizationRule for GreedyJoinReorder { fn apply(&self, plan: &mut LogicalPlan, arena: &mut PlanArena) -> Result { - if !reorderable(plan) { + if !matches!(&plan.operator, Operator::Join(join) if join.join_type == JoinType::Inner) + || !can_expand(plan) + { return Ok(false); } let original_schema = plan.output_schema(arena).clone(); @@ -158,15 +179,32 @@ impl NormalizationRule for GreedyJoinReorder { flatten(plan.take(), &mut inputs); while inputs.len() > 1 { let mut best = None; - for (i, (_, left)) in inputs.iter().enumerate() { - for (j, (_, right)) in inputs.iter().enumerate().skip(i + 1) { - let candidate = (estimate_join_rows(*left, *right), i, j); + for i in 0..inputs.len() { + let (prefix, suffix) = inputs.split_at_mut(i + 1); + let (left, left_rows) = &mut prefix[i]; + let left_schema = left.output_schema(arena); + for (offset, (right, right_rows)) in suffix.iter_mut().enumerate() { + let right_schema = right.output_schema(arena); + let mut connected = false; + for &key in &keys { + if key_between(key, left_schema, right_schema, arena)?.is_some() { + connected = true; + break; + } + } + // Apply available join predicates before forming Cartesian intermediates. + let candidate = ( + !connected, + estimate_join_rows(*left_rows, *right_rows), + i, + i + 1 + offset, + ); if best.is_none_or(|previous| candidate < previous) { best = Some(candidate); } } } - let Some((product, i, j)) = best else { + let Some((_, product, i, j)) = best else { return Ok(false); }; let (mut right, _) = inputs.remove(j); @@ -176,19 +214,7 @@ impl NormalizationRule for GreedyJoinReorder { let mut on = Vec::new(); let mut index = 0; while index < keys.len() { - let (l, r) = keys[index]; - let key = if key_in_schema(l, left_schema, arena)? - && key_in_schema(r, right_schema, arena)? - { - Some((l, r)) - } else if key_in_schema(r, left_schema, arena)? - && key_in_schema(l, right_schema, arena)? - { - Some((r, l)) - } else { - None - }; - if let Some(key) = key { + if let Some(key) = key_between(keys[index], left_schema, right_schema, arena)? { keys.remove(index); on.push(key); } else { @@ -403,7 +429,7 @@ mod tests { } #[test] - fn smallest_pair_without_a_key_defers_both_conditions() -> Result<(), DatabaseError> { + fn connected_pair_precedes_a_smaller_cartesian_product() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); let mut plan = three_inputs(&mut arena); @@ -416,29 +442,104 @@ mod tests { let schema = plan.output_schema(&mut arena).clone(); assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); assert_eq!(plan.output_schema(&mut arena), &schema); + let (left, right) = plan.childrens.twins(); + assert_eq!(left.referenced_table(), vec!["a".into(), "b".into()]); + assert_eq!(right.referenced_table(), vec!["c".into()]); + assert_eq!(left.estimated_rows(), Some(10000)); + for join in [&plan, left] { + let Operator::Join(op) = &join.operator else { + unreachable!() + }; + let JoinCondition::On { on, .. } = &op.on else { + unreachable!() + }; + assert_eq!(op.join_type, JoinType::Inner); + assert_eq!(on.len(), 1); + } + Ok(()) + } + + #[test] + fn inner_join_expands_cross_join_but_cross_join_is_not_an_entry() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let (a, a_key) = input("a", 1000, &mut arena); + let (b, b_key) = input("b", 10, &mut arena); + let (c, c_key) = input("c", 20, &mut arena); + let mut cross = JoinOperator::build(a, b, JoinCondition::None, JoinType::Cross, false); + let original = cross.clone(); + assert!(!GreedyJoinReorder.apply(&mut cross, &mut arena)?); + assert_eq!(cross, original); + let b_column = b_key.output_column_ref(&mut arena); + let b_key = arena.alloc_expression(ScalarExpression::column_expr(b_column, 1)); + let mut plan = JoinOperator::build( + cross, + c, + JoinCondition::On { + on: vec![(a_key, c_key), (b_key, c_key)], + filter: None, + }, + JoinType::Inner, + false, + ); + let schema = plan.output_schema(&mut arena).clone(); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + let (left, right) = plan.childrens.twins(); + assert_eq!(left.referenced_table(), vec!["a".into()]); + assert_eq!(right.referenced_table(), vec!["b".into(), "c".into()]); + assert_eq!(right.estimated_rows(), Some(200)); + for join in [&plan, right] { + let Operator::Join(op) = &join.operator else { + unreachable!() + }; + let JoinCondition::On { on, .. } = &op.on else { + unreachable!() + }; + assert_eq!(op.join_type, JoinType::Inner); + assert_eq!(on.len(), 1); + } + Ok(()) + } + + #[test] + fn disconnected_input_keeps_a_final_cross_join() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let (a, a_key) = input("a", 1000, &mut arena); + let (b, b_key) = input("b", 10, &mut arena); + let (c, c_key) = input("c", 20, &mut arena); + let (d, _) = input("d", 1, &mut arena); + let ab = JoinOperator::build(a, b, JoinCondition::None, JoinType::Cross, false); + let abd = JoinOperator::build(ab, d, JoinCondition::None, JoinType::Cross, false); + let b_column = b_key.output_column_ref(&mut arena); + let b_key = arena.alloc_expression(ScalarExpression::column_expr(b_column, 1)); + let mut plan = JoinOperator::build( + abd, + c, + JoinCondition::On { + on: vec![(a_key, c_key), (b_key, c_key)], + filter: None, + }, + JoinType::Inner, + false, + ); + let schema = plan.output_schema(&mut arena).clone(); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert_eq!(plan.output_schema(&mut arena), &schema); let join = plan.childrens.only(); + assert!( + matches!(&join.operator, Operator::Join(join) if join.join_type == JoinType::Cross) + ); let (left, right) = join.childrens.twins(); - assert_eq!(left.referenced_table(), vec!["a".into(), "c".into()]); - assert_eq!(right.referenced_table(), vec!["b".into()]); - assert!(matches!( - &left.operator, - Operator::Join(JoinOperator { - join_type: JoinType::Cross, - on: JoinCondition::None, - .. - }) - )); assert_eq!( - left.physical_option.as_ref().unwrap().plan, - PlanImpl::NestLoopJoin + left.referenced_table(), + vec!["a".into(), "b".into(), "c".into()] ); - let Operator::Join(op) = &join.operator else { - unreachable!() - }; - let JoinCondition::On { on, .. } = &op.on else { - unreachable!() - }; - assert_eq!(on.len(), 2); + assert_eq!(right.referenced_table(), vec!["d".into()]); + let (_, bc) = left.childrens.twins(); + assert_eq!(bc.referenced_table(), vec!["b".into(), "c".into()]); + assert_eq!(bc.estimated_rows(), Some(200)); Ok(()) } @@ -456,7 +557,7 @@ mod tests { filter.physical_option = Some(PhysicalOption::new( PlanImpl::Filter, SortOption::Follow, - RowCountOption::Estimated(20), + RowCountOption::Follow, )); **right = filter; let schema = join.output_schema(&mut arena).clone(); diff --git a/tests/slt/join.slt b/tests/slt/join.slt index d4536cf0..78c8283f 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -649,6 +649,25 @@ select a.v,b.v,c.v from greedy_a a left join greedy_b b on a.k=b.k left join gre 50 NULL NULL 60 NULL NULL +# A comma join initially contains a CrossJoin without a direct a/b key. +query III rowsort +select greedy_a.v,greedy_b.v,greedy_c.v from greedy_a,greedy_b,greedy_c where greedy_a.k=greedy_c.k and greedy_b.k=greedy_c.k +---- +10 100 1000 +10 100 1100 +20 200 2000 + +# Parameterization must still select an index probe after the join tree is reordered. +statement ok +create index greedy_b_k on greedy_b(k) + +query III rowsort +select greedy_a.v,greedy_b.v,greedy_c.v from greedy_a join greedy_b on greedy_a.k=greedy_b.k join greedy_c on greedy_b.k=greedy_c.k +---- +10 100 1000 +10 100 1100 +20 200 2000 + statement ok drop table greedy_a From a96efb4bf52421de6b247c80fb74e225720f98a4 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:38 +0800 Subject: [PATCH 03/62] perf: prune uncompetitive greedy join candidates --- .../rule/normalization/join_reorder.rs | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index fa3cef55..39ee7f80 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -25,6 +25,15 @@ use crate::types::tuple::Schema; pub(crate) struct GreedyJoinReorder; +// Prefer equijoins, then fewer rows, then the original input order. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct JoinCandidate { + cross_join: bool, + rows: usize, + left: usize, + right: usize, +} + fn estimate_join_rows(left: usize, right: usize) -> usize { // TODO: For INNER equijoins, use left_rows * right_rows / max(left_ndv, right_ndv) // and cap the estimate with join-key uniqueness. Uniqueness bounds matches; @@ -178,12 +187,20 @@ impl NormalizationRule for GreedyJoinReorder { let mut inputs = Vec::with_capacity(schemas.len()); flatten(plan.take(), &mut inputs); while inputs.len() > 1 { - let mut best = None; + let mut best: Option = None; for i in 0..inputs.len() { let (prefix, suffix) = inputs.split_at_mut(i + 1); let (left, left_rows) = &mut prefix[i]; let left_schema = left.output_schema(arena); for (offset, (right, right_rows)) in suffix.iter_mut().enumerate() { + let j = i + 1 + offset; + let product = estimate_join_rows(*left_rows, *right_rows); + if let Some(previous) = best { + // Equal row estimates retain the earlier pair in enumeration order. + if !previous.cross_join && product >= previous.rows { + continue; + } + } let right_schema = right.output_schema(arena); let mut connected = false; for &key in &keys { @@ -193,18 +210,24 @@ impl NormalizationRule for GreedyJoinReorder { } } // Apply available join predicates before forming Cartesian intermediates. - let candidate = ( - !connected, - estimate_join_rows(*left_rows, *right_rows), - i, - i + 1 + offset, - ); + let candidate = JoinCandidate { + cross_join: !connected, + rows: product, + left: i, + right: j, + }; if best.is_none_or(|previous| candidate < previous) { best = Some(candidate); } } } - let Some((_, product, i, j)) = best else { + let Some(JoinCandidate { + rows: product, + left: i, + right: j, + .. + }) = best + else { return Ok(false); }; let (mut right, _) = inputs.remove(j); From 9205ba2d47a8498094975ca54c43ed43c211bce1 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:38 +0800 Subject: [PATCH 04/62] perf: materialize reusable quantified subquery inputs --- src/db.rs | 6 + src/execution/dql/mark_apply.rs | 428 ++++++++++++------ src/optimizer/heuristic/optimizer.rs | 37 +- .../rule/implementation/dql/mark_apply.rs | 45 +- src/optimizer/rule/implementation/mod.rs | 17 +- .../normalization/materialize_mark_apply.rs | 159 +++++++ src/optimizer/rule/normalization/mod.rs | 7 +- src/planner/operator/mark_apply.rs | 4 + tests/slt/parameterized_subquery.slt | 20 +- tests/slt/subquery.slt | 19 + 10 files changed, 571 insertions(+), 171 deletions(-) create mode 100644 src/optimizer/rule/normalization/materialize_mark_apply.rs diff --git a/src/db.rs b/src/db.rs index c83cb9fc..01fedb51 100644 --- a/src/db.rs +++ b/src/db.rs @@ -456,6 +456,11 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { HepBatchStrategy::fix_point_topdown(1), vec![NormalizationRuleImpl::EliminateIndexFilter], ) + .after_batch( + "Materialize Mark Apply".to_string(), + HepBatchStrategy::fix_point_bottomup(1), + vec![NormalizationRuleImpl::MaterializeMarkApply], + ) .after_batch( "Expression Remapper".to_string(), HepBatchStrategy::once_topdown(), @@ -475,6 +480,7 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { ImplementationRuleImpl::HashJoin, ImplementationRuleImpl::Limit, ImplementationRuleImpl::Projection, + ImplementationRuleImpl::MarkApply, ImplementationRuleImpl::ScalarSubquery, ImplementationRuleImpl::SeqScan, ImplementationRuleImpl::IndexScan, diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index 761c1335..ceda357b 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -13,12 +13,13 @@ // limitations under the License. use crate::errors::DatabaseError; +#[cfg(feature = "spill")] +use crate::execution::spill::{SpillReader, SpillVec}; use crate::execution::{ build_read, ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor, }; use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyOperator, MarkApplyQuantifier}; -use crate::planner::LogicalPlan; -use crate::planner::{ExecMetaArena, MetaArena}; +use crate::planner::{ExecMetaArena, ExprRef, LogicalPlan, MetaArena}; use crate::storage::Transaction; use crate::types::index::RuntimeIndexProbe; use crate::types::tuple::{SplitTupleRef, Tuple}; @@ -32,11 +33,120 @@ enum QuantifiedPredicateOutcome { Skip, } +enum RightInput<'a> { + Rebuild { + plan: &'a LogicalPlan, + pos: ExecId, + }, + Materialize { + plan: &'a LogicalPlan, + pos: ExecId, + }, + Empty, + #[cfg(feature = "spill")] + Materialized(SpillReader), + #[cfg(not(feature = "spill"))] + Materialized(Vec), +} + +impl<'a> RightInput<'a> { + fn build( + &self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut (dyn MetaArena + 'a), + probe: Option, + ) -> ExecId { + let (Self::Rebuild { plan, pos } | Self::Materialize { plan, pos }) = self else { + unreachable!("materialized input no longer builds its RHS") + }; + arena.nodes.seek(*pos); + if let Some(probe) = probe { + arena.push_runtime_probe(probe); + } + build_read( + arena, + plan_arena, + plan, + arena.context(), + arena.transaction(), + ) + } + + // Return the first terminal predicate result, or None after exhausting the RHS. + // The caller supplies Mark semantics; this enum owns reconstruction and replay. + fn scan( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + probe: Option, + mut evaluate: impl FnMut(&Tuple, &ExecMetaArena) -> Result, DatabaseError>, + ) -> Result, DatabaseError> { + if matches!(self, Self::Materialize { .. }) { + let root = self.build(arena, plan_arena, None); + #[cfg(feature = "spill")] + { + let mut rows = SpillVec::new(); + while arena.next_tuple(root, plan_arena)? { + let _ = rows.push(arena.materialize_tuple())?; + } + // Fully flush before replaying through the existing reset API. + let _ = rows.flush()?; + *self = if rows.is_spilled() { + Self::Materialized(rows.into_iter()) + } else { + Self::Empty + }; + } + #[cfg(not(feature = "spill"))] + { + let mut rows = Vec::new(); + while arena.next_tuple(root, plan_arena)? { + rows.push(arena.materialize_tuple()); + } + *self = if rows.is_empty() { + Self::Empty + } else { + Self::Materialized(rows) + }; + } + } + match self { + Self::Rebuild { .. } => { + let root = self.build(arena, plan_arena, probe); + while arena.next_tuple(root, plan_arena)? { + if let Some(value) = evaluate(arena.result_tuple(), plan_arena)? { + return Ok(Some(value)); + } + } + } + #[cfg(feature = "spill")] + Self::Materialized(rows) => { + rows.reset()?; + for right in rows { + if let Some(value) = evaluate(&right?, plan_arena)? { + return Ok(Some(value)); + } + } + } + #[cfg(not(feature = "spill"))] + Self::Materialized(rows) => { + for right in rows { + if let Some(value) = evaluate(right, plan_arena)? { + return Ok(Some(value)); + } + } + } + Self::Empty => {} + Self::Materialize { .. } => unreachable!("RHS materialization is complete"), + } + Ok(None) + } +} + pub struct MarkApply<'a> { op: &'a MarkApplyOperator, - right_input_plan: &'a LogicalPlan, left_input: ExecId, - right_pos: ExecId, + right_input: RightInput<'a>, // Retain a streaming inner input across next_tuple calls, not its result rows. join_input: Option<(ExecId, Tuple)>, } @@ -52,13 +162,23 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for MarkApply<'a> { transaction: &T, ) -> ExecId { let left_input = build_read(arena, plan_arena, left_input, cache, transaction); - let right_pos = arena.nodes.position(); + let pos = arena.nodes.position(); build_read(arena, plan_arena, right_input, cache, transaction); + let right_input = if op.materialize { + RightInput::Materialize { + plan: right_input, + pos, + } + } else { + RightInput::Rebuild { + plan: right_input, + pos, + } + }; arena.push(ExecNode::MarkApply(Self { op, - right_input_plan: right_input, left_input, - right_pos, + right_input, join_input: None, })) } @@ -77,12 +197,10 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { arena.finish(); return Ok(()); } - - let mut left_tuple = arena.materialize_tuple(); - let marker = self.mark_value(arena, plan_arena, &left_tuple)?; - - left_tuple.values.push(marker); - arena.produce_tuple(left_tuple); + let mut left = arena.materialize_tuple(); + let marker = self.mark_value(arena, plan_arena, &left)?; + left.values.push(marker); + arena.produce_tuple(left); Ok(()) } } @@ -113,16 +231,17 @@ impl<'a> MarkApply<'a> { arena.finish(); return Ok(()); } - let left: Tuple = arena.materialize_tuple(); + let left = arena.materialize_tuple(); let value = self.parameterized_probe_value(&left, plan_arena)?; - let root = self.build_right_input(arena, plan_arena, value); + let root = self + .right_input + .build(arena, plan_arena, self.runtime_probe_for(value)); self.join_input = Some((root, left)); } } fn runtime_probe_for(&self, param_value: Option) -> Option { self.op.parameterized_probe()?; - match param_value { Some(value) => Some(RuntimeIndexProbe::Eq(value)), None if matches!( @@ -139,25 +258,6 @@ impl<'a> MarkApply<'a> { } } - fn build_right_input( - &self, - arena: &mut ExecArena<'a, T>, - plan_arena: &mut (dyn MetaArena + 'a), - param_value: Option, - ) -> ExecId { - arena.nodes.seek(self.right_pos); - if let Some(probe) = self.runtime_probe_for(param_value) { - arena.push_runtime_probe(probe); - } - build_read( - arena, - plan_arena, - self.right_input_plan, - arena.context(), - arena.transaction(), - ) - } - fn parameterized_probe_value( &self, left_tuple: &Tuple, @@ -175,7 +275,7 @@ impl<'a> MarkApply<'a> { } fn mark_value( - &self, + &mut self, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, left_tuple: &Tuple, @@ -184,123 +284,98 @@ impl<'a> MarkApply<'a> { match self.op.kind { MarkApplyKind::InnerJoin => unreachable!("inner join streams tuples"), MarkApplyKind::Exists => { - let right_input = self.build_right_input(arena, plan_arena, probe); - while arena.next_tuple(right_input, plan_arena)? { - let right_tuple = arena.result_tuple(); - if Self::predicates_matched( - self.op.predicates(), - left_tuple, - right_tuple, - plan_arena, - )? { - return Ok(DataValue::Boolean(true)); - } - } - Ok(DataValue::Boolean(false)) + let predicates = self.op.predicates(); + let runtime_probe = self.runtime_probe_for(probe); + Ok(self + .right_input + .scan(arena, plan_arena, runtime_probe, |right, meta| { + Ok( + Self::predicates_matched(predicates, left_tuple, right, meta)? + .then_some(DataValue::Boolean(true)), + ) + })? + .unwrap_or(DataValue::Boolean(false))) } - MarkApplyKind::Quantified(MarkApplyQuantifier::Any) => { - if let Some(probe_value) = probe { - if !probe_value.is_null() { - let right_input = - self.build_right_input(arena, plan_arena, Some(probe_value)); - while arena.next_tuple(right_input, plan_arena)? { - let right_tuple = arena.result_tuple(); - if self.quantified_predicate_outcome( - left_tuple, - right_tuple, + MarkApplyKind::Quantified(quantifier) => { + if matches!(quantifier, MarkApplyQuantifier::Any) { + if let Some(probe_value) = probe { + if !probe_value.is_null() { + let value = self.scan_quantified_right_input( + arena, plan_arena, - )? == QuantifiedPredicateOutcome::True - { - return Ok(DataValue::Boolean(true)); - } - } - - let right_input = - self.build_right_input(arena, plan_arena, Some(DataValue::Null)); - while arena.next_tuple(right_input, plan_arena)? { - let right_tuple = arena.result_tuple(); - if self.quantified_predicate_outcome( + Some(probe_value), + quantifier, left_tuple, - right_tuple, - plan_arena, - )? == QuantifiedPredicateOutcome::Null - { - return Ok(DataValue::Null); + )?; + if value == DataValue::Boolean(true) { + return Ok(value); } + let value = self.scan_quantified_right_input( + arena, + plan_arena, + Some(DataValue::Null), + quantifier, + left_tuple, + )?; + return Ok(if value.is_null() { + DataValue::Null + } else { + DataValue::Boolean(false) + }); } - - return Ok(DataValue::Boolean(false)); } } - - let right_input = self.build_right_input(arena, plan_arena, None); - self.scan_quantified_right_input( - arena, - plan_arena, - right_input, - MarkApplyQuantifier::Any, - left_tuple, - ) - } - MarkApplyKind::Quantified(MarkApplyQuantifier::All) => { - let right_input = self.build_right_input(arena, plan_arena, None); - self.scan_quantified_right_input( - arena, - plan_arena, - right_input, - MarkApplyQuantifier::All, - left_tuple, - ) + self.scan_quantified_right_input(arena, plan_arena, None, quantifier, left_tuple) } } } fn scan_quantified_right_input( - &self, + &mut self, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, - right_input: ExecId, + probe: Option, quantifier: MarkApplyQuantifier, left_tuple: &Tuple, ) -> Result { let mut saw_null = false; - - while arena.next_tuple(right_input, plan_arena)? { - let right_tuple = arena.result_tuple(); - match self.quantified_predicate_outcome(left_tuple, right_tuple, plan_arena)? { - QuantifiedPredicateOutcome::True => { - if matches!(quantifier, MarkApplyQuantifier::Any) { - return Ok(DataValue::Boolean(true)); + let predicates = self.op.predicates(); + let runtime_probe = self.runtime_probe_for(probe); + let value = self + .right_input + .scan(arena, plan_arena, runtime_probe, |right, meta| { + match Self::quantified_predicate_outcome(predicates, left_tuple, right, meta)? { + QuantifiedPredicateOutcome::True + if matches!(quantifier, MarkApplyQuantifier::Any) => + { + return Ok(Some(DataValue::Boolean(true))); } - } - QuantifiedPredicateOutcome::False => { - if matches!(quantifier, MarkApplyQuantifier::All) { - return Ok(DataValue::Boolean(false)); + QuantifiedPredicateOutcome::False + if matches!(quantifier, MarkApplyQuantifier::All) => + { + return Ok(Some(DataValue::Boolean(false))); } + QuantifiedPredicateOutcome::Null => saw_null = true, + _ => {} } - QuantifiedPredicateOutcome::Null => saw_null = true, - QuantifiedPredicateOutcome::Skip => {} + Ok(None) + })?; + Ok(value.unwrap_or_else(|| { + if saw_null { + DataValue::Null + } else { + DataValue::Boolean(matches!(quantifier, MarkApplyQuantifier::All)) } - } - - if saw_null { - Ok(DataValue::Null) - } else { - Ok(DataValue::Boolean(matches!( - quantifier, - MarkApplyQuantifier::All - ))) - } + })) } fn predicates_matched( - predicates: &[crate::planner::ExprRef], + predicates: &[ExprRef], left_tuple: &Tuple, right_tuple: &Tuple, plan_arena: &(dyn MetaArena + '_), ) -> Result { let values = SplitTupleRef::new(left_tuple, right_tuple); - for predicate in predicates { match *plan_arena .expression(*predicate) @@ -311,17 +386,16 @@ impl<'a> MarkApply<'a> { _ => return Err(DatabaseError::InvalidType), } } - Ok(true) } fn quantified_predicate_outcome( - &self, + predicates: &[ExprRef], left_tuple: &Tuple, right_tuple: &Tuple, plan_arena: &(dyn MetaArena + '_), ) -> Result { - match self.eval_predicates(left_tuple, right_tuple, plan_arena)? { + match Self::eval_predicates(predicates, left_tuple, right_tuple, plan_arena)? { Some(DataValue::Boolean(true)) => Ok(QuantifiedPredicateOutcome::True), Some(DataValue::Boolean(false)) => Ok(QuantifiedPredicateOutcome::False), Some(DataValue::Null) => Ok(QuantifiedPredicateOutcome::Null), @@ -331,19 +405,15 @@ impl<'a> MarkApply<'a> { } fn eval_predicates( - &self, + predicates: &[ExprRef], left_tuple: &Tuple, right_tuple: &Tuple, plan_arena: &(dyn MetaArena + '_), ) -> Result, DatabaseError> { let values = SplitTupleRef::new(left_tuple, right_tuple); // probe_predicate is in predicate, always first - let (probe_predicate, correlated_predicates) = self - .op - .predicates() - .split_first() - .ok_or(DatabaseError::InvalidType)?; - + let (probe_predicate, correlated_predicates) = + predicates.split_first().ok_or(DatabaseError::InvalidType)?; for predicate in correlated_predicates { match *plan_arena .expression(*predicate) @@ -354,7 +424,6 @@ impl<'a> MarkApply<'a> { _ => return Err(DatabaseError::InvalidType), } } - Ok(Some( plan_arena .expression(*probe_predicate) @@ -733,11 +802,13 @@ mod tests { &transaction, ); - let exec: MarkApply = MarkApply { + let mut exec: MarkApply = MarkApply { op: &op, - right_input_plan: &right, + right_input: RightInput::Rebuild { + plan: &right, + pos: arena.nodes.position(), + }, left_input: 0, - right_pos: arena.nodes.position(), join_input: None, }; let left_tuple = Tuple::new(None, vec![DataValue::Int32(2), DataValue::Int32(1)]); @@ -787,11 +858,13 @@ mod tests { &transaction, ); - let exec: MarkApply = MarkApply { + let mut exec: MarkApply = MarkApply { op: &op, - right_input_plan: &right, + right_input: RightInput::Rebuild { + plan: &right, + pos: arena.nodes.position(), + }, left_input: 0, - right_pos: arena.nodes.position(), join_input: None, }; let left_tuple = Tuple::new(None, vec![DataValue::Int32(2)]); @@ -841,11 +914,13 @@ mod tests { &transaction, ); - let exec: MarkApply = MarkApply { + let mut exec: MarkApply = MarkApply { op: &op, - right_input_plan: &right, + right_input: RightInput::Rebuild { + plan: &right, + pos: arena.nodes.position(), + }, left_input: 0, - right_pos: arena.nodes.position(), join_input: None, }; let left_tuple = Tuple::new(None, vec![DataValue::Null]); @@ -914,6 +989,73 @@ mod tests { Ok(()) } + #[test] + fn quantified_apply_materializes_once_and_replays_after_short_circuit( + ) -> Result<(), DatabaseError> { + let (table_cache, view_cache, meta_cache, _temp_dir, storage) = build_test_storage()?; + let transaction = storage.transaction()?; + let cache = crate::execution::empty_context(&table_cache, &view_cache, &meta_cache); + for quantifier in [MarkApplyQuantifier::Any, MarkApplyQuantifier::All] { + let table_arena = crate::planner::TableArenaCell::default(); + let mut meta = crate::planner::PlanArena::new(&table_arena); + let mut left = build_values(&mut meta, "left_key", vec![vec![DataValue::Int32(2)]; 8]); + let mut right = build_values( + &mut meta, + "right_key", + vec![vec![DataValue::Int32(2)], vec![DataValue::Null]], + ); + let left_column = left.output_schema(&mut meta)[0]; + let right_column = right.output_schema(&mut meta)[0]; + let predicate = build_equality_predicate(&mut meta, left_column, 0, right_column, 1)?; + let mut op = MarkApplyOperator::new_quantified( + quantifier, + build_marker_column(&mut meta), + vec![predicate], + ); + op.materialize = true; + let empty = build_values(&mut meta, "empty", vec![]); + let mut arena = ExecArena::with_capacity(0); + arena.init_context(cache, &transaction); + let left_input = build_read(&mut arena, &mut meta, &left, cache, &transaction); + let pos = arena.nodes.position(); + build_read(&mut arena, &mut meta, &right, cache, &transaction); + let mut exec = MarkApply { + op: &op, + right_input: RightInput::Materialize { plan: &right, pos }, + left_input, + join_input: None, + }; + let mut meta = ExecMetaArena::new(meta); + for value in [ + DataValue::Int32(2), + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(2), + ] { + let expected = match (quantifier, &value) { + (MarkApplyQuantifier::Any, DataValue::Int32(2)) => DataValue::Boolean(true), + (MarkApplyQuantifier::All, DataValue::Int32(1)) => DataValue::Boolean(false), + _ => DataValue::Null, + }; + let tuple = Tuple::new(None, vec![value]); + assert_eq!(exec.mark_value(&mut arena, &mut meta, &tuple)?, expected); + assert!(matches!(exec.right_input, RightInput::Materialized(_))); + } + // Empty RHS is a persistent state, including for a NULL left value. + exec.right_input = RightInput::Materialize { plan: &empty, pos }; + assert_eq!( + exec.mark_value( + &mut arena, + &mut meta, + &Tuple::new(None, vec![DataValue::Null]) + )?, + DataValue::Boolean(matches!(quantifier, MarkApplyQuantifier::All)) + ); + assert!(matches!(exec.right_input, RightInput::Empty)); + } + Ok(()) + } + #[test] fn mark_in_apply_treats_null_predicate_as_not_matched() -> Result<(), DatabaseError> { let table_arena = crate::planner::TableArenaCell::default(); diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index c05b1108..3c7a0cd9 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -25,6 +25,7 @@ use crate::optimizer::rule::normalization::{ apply_annotated_post_rules, apply_scan_order_hint, constant_calculation_current, evaluator_bind_current, NormalizationRuleImpl, OrderHintKind, ScanOrderHint, WholeTreePassKind, }; +use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::planner::{Childrens, LogicalPlan, PlanArena}; @@ -524,11 +525,16 @@ impl ImplementationRuleIndex { Operator::Limit(_) if self.contains(ImplementationRuleImpl::Limit) => Some( PhysicalOption::new(PlanImpl::Limit, SortOption::Follow, RowCountOption::Unknown), ), - Operator::MarkApply(_) if self.contains(ImplementationRuleImpl::MarkApply) => { + Operator::MarkApply(op) if self.contains(ImplementationRuleImpl::MarkApply) => { + let rows = if matches!(op.kind, MarkApplyKind::InnerJoin) { + RowCountOption::Unknown + } else { + RowCountOption::Follow + }; Some(PhysicalOption::new( PlanImpl::MarkApply, SortOption::Follow, - RowCountOption::Unknown, + rows, )) } Operator::Project(_) if self.contains(ImplementationRuleImpl::Projection) => { @@ -682,6 +688,7 @@ impl ImplementationRuleIndex { #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::binder::{Binder, BinderContext}; + use crate::catalog::{ColumnCatalog, ColumnDesc}; use crate::db::DataBaseBuilder; use crate::errors::DatabaseError; use crate::expression::range_detacher::Range; @@ -695,6 +702,7 @@ mod tests { use crate::optimizer::rule::normalization::NormalizationRuleImpl; use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::limit::LimitOperator; + use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyOperator}; use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::sort::SortField; use crate::planner::operator::{ @@ -704,6 +712,7 @@ mod tests { use crate::storage::{Storage, Transaction}; use crate::types::index::IndexLookup; use crate::types::value::DataValue; + use crate::types::LogicalType; use std::ops::Bound; use tempfile::TempDir; @@ -718,7 +727,7 @@ mod tests { } #[test] - fn filter_fast_path_follows_input_row_count() { + fn row_preserving_fast_paths_follow_input_row_count() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); let mut plan = filter_chain(1, &mut arena); @@ -737,6 +746,28 @@ mod tests { RowCountOption::Follow ); assert_eq!(plan.estimated_rows(), Some(12)); + + let marker = arena.alloc_column(ColumnCatalog::new( + "mark".to_string(), + true, + ColumnDesc::new(LogicalType::Boolean, None, false, None)?, + )); + let right = LogicalPlan::new(Operator::Dummy, Childrens::None); + let mut plan = MarkApplyOperator::build_exists(plan, right, marker, vec![]); + let implementations = ImplementationRuleIndex::new(vec![ImplementationRuleImpl::MarkApply]); + plan.physical_option = implementations.direct_physical_option(&plan.operator); + assert_eq!(plan.estimated_rows(), Some(12)); + let Operator::MarkApply(op) = &mut plan.operator else { + unreachable!() + }; + op.kind = MarkApplyKind::InnerJoin; + plan.physical_option = implementations.direct_physical_option(&plan.operator); + assert_eq!( + plan.physical_option.as_ref().unwrap().row_count, + RowCountOption::Unknown + ); + assert_eq!(plan.estimated_rows(), None); + Ok(()) } #[test] diff --git a/src/optimizer/rule/implementation/dql/mark_apply.rs b/src/optimizer/rule/implementation/dql/mark_apply.rs index 871d6c1c..7c6502ad 100644 --- a/src/optimizer/rule/implementation/dql/mark_apply.rs +++ b/src/optimizer/rule/implementation/dql/mark_apply.rs @@ -14,11 +14,12 @@ use crate::errors::DatabaseError; use crate::optimizer::core::pattern::Pattern; -use crate::optimizer::core::rule::BestPhysicalOption; +use crate::optimizer::core::rule::{keep_best_physical_option, BestPhysicalOption}; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; +use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; -use crate::single_mapping; +use crate::planner::PlanArena; use std::sync::LazyLock; static MARK_APPLY_PATTERN: LazyLock = LazyLock::new(|| Pattern { @@ -28,12 +29,34 @@ static MARK_APPLY_PATTERN: LazyLock = LazyLock::new(|| Pattern { #[derive(Clone)] pub struct MarkApplyImplementation; -single_mapping!( - MarkApplyImplementation, - MARK_APPLY_PATTERN, - PhysicalOption::new( - PlanImpl::MarkApply, - SortOption::Follow, - RowCountOption::Unknown - ) -); +impl MatchPattern for MarkApplyImplementation { + fn pattern(&self) -> &Pattern { + &MARK_APPLY_PATTERN + } +} + +impl ImplementationRule for MarkApplyImplementation { + fn update_best_option( + &self, + op: &Operator, + _: &PlanArena, + _: &StatisticMetaLoader<'_>, + best_physical_option: &mut BestPhysicalOption, + ) -> Result<(), DatabaseError> { + let Operator::MarkApply(op) = op else { + return Ok(()); + }; + // A mark adds one value to each left row; INNER JOIN may duplicate rows. + let rows = if matches!(op.kind, MarkApplyKind::InnerJoin) { + RowCountOption::Unknown + } else { + RowCountOption::Follow + }; + keep_best_physical_option( + best_physical_option, + PhysicalOption::new(PlanImpl::MarkApply, SortOption::Follow, rows), + None, + ); + Ok(()) + } +} diff --git a/src/optimizer/rule/implementation/mod.rs b/src/optimizer/rule/implementation/mod.rs index 65f82be1..1671bc1c 100644 --- a/src/optimizer/rule/implementation/mod.rs +++ b/src/optimizer/rule/implementation/mod.rs @@ -323,6 +323,7 @@ mod tests { ImplementationRuleImpl, ImplementationRuleRootTag, }; use crate::planner::operator::function_scan::FunctionScanOperator; + use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::top_k::TopKOperator; use crate::planner::operator::{ Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption, @@ -436,11 +437,21 @@ mod tests { |op| matches!(op, Operator::Limit(_)), PlanImpl::Limit, )?; - assert_sql_rule( + with_operator_from_sql( "select * from t1 where exists(select * from t2)", - ImplementationRuleImpl::MarkApply, |op| matches!(op, Operator::MarkApply(_)), - PlanImpl::MarkApply, + |mut operator, arena| { + let option = best_option(ImplementationRuleImpl::MarkApply, &operator, arena)?; + assert_eq!(option.plan, PlanImpl::MarkApply); + assert_eq!(option.row_count, RowCountOption::Follow); + let Operator::MarkApply(op) = &mut operator else { + unreachable!() + }; + op.kind = MarkApplyKind::InnerJoin; + let option = best_option(ImplementationRuleImpl::MarkApply, &operator, arena)?; + assert_eq!(option.row_count, RowCountOption::Unknown); + Ok(()) + }, )?; assert_sql_rule( "select c1 from t1", diff --git a/src/optimizer/rule/normalization/materialize_mark_apply.rs b/src/optimizer/rule/normalization/materialize_mark_apply.rs new file mode 100644 index 00000000..2bc131c6 --- /dev/null +++ b/src/optimizer/rule/normalization/materialize_mark_apply.rs @@ -0,0 +1,159 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::NormalizationRule; +use crate::errors::DatabaseError; +use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyOperator}; +use crate::planner::operator::Operator; +use crate::planner::{Childrens, LogicalPlan, PlanArena}; + +pub(crate) struct MaterializeMarkApply; + +impl MaterializeMarkApply { + fn can_materialize(op: &MarkApplyOperator) -> bool { + // The first predicate compares values; additional predicates bind outer rows. + matches!(op.kind, MarkApplyKind::Quantified(_)) + && op.parameterized_probe().is_none() + && op.predicates().len() == 1 + } +} + +impl NormalizationRule for MaterializeMarkApply { + fn apply(&self, plan: &mut LogicalPlan, _: &mut PlanArena) -> Result { + let (Operator::MarkApply(op), Childrens::Twins { left, right }) = + (&mut plan.operator, plan.childrens.as_ref()) + else { + return Ok(false); + }; + if op.materialize || !Self::can_materialize(op) { + return Ok(false); + } + let (Some(left_rows), Some(right_rows)) = (left.estimated_rows(), right.estimated_rows()) + else { + return Ok(false); + }; + // Prefer reuse when rebuilds are at least as numerous as retained rows, + // but avoid eager RHS evaluation when there is no repeated left input. + if left_rows <= 1 || left_rows < right_rows { + return Ok(false); + } + op.materialize = true; + Ok(true) + } +} + +// GRCOV_EXCL_START +#[cfg(test)] +mod tests { + use super::*; + use crate::catalog::{ColumnCatalog, ColumnDesc}; + use crate::expression::ScalarExpression; + use crate::planner::operator::mark_apply::MarkApplyQuantifier; + use crate::planner::operator::values::ValuesOperator; + use crate::planner::operator::{PhysicalOption, PlanImpl, RowCountOption, SortOption}; + use crate::planner::TableArenaCell; + use crate::types::value::DataValue; + use crate::types::LogicalType; + + #[test] + fn selects_materialization_without_changing_probe_or_correlated_paths( + ) -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let key = arena.alloc_column(ColumnCatalog::new( + "key".to_string(), + true, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, + )); + let marker = arena.alloc_column(ColumnCatalog::new( + "marker".to_string(), + true, + ColumnDesc::new(LogicalType::Boolean, None, false, None)?, + )); + let value = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(2))); + let left = LogicalPlan::new( + Operator::Values(ValuesOperator::new(vec![value; 8], 8, vec![key])), + Childrens::None, + ); + let right = LogicalPlan::new( + Operator::Values(ValuesOperator::new(vec![value; 2], 2, vec![key])), + Childrens::None, + ); + let predicate = + arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(true))); + let op = + MarkApplyOperator::new_quantified(MarkApplyQuantifier::Any, marker, vec![predicate]); + let mut plan = LogicalPlan::new( + Operator::MarkApply(op), + Childrens::Twins { + left: Box::new(left), + right: Box::new(right), + }, + ); + for (left_rows, right_rows, materialize) in [ + (0, 2, false), + (1, 0, false), + (2, 8, false), + (2, 2, true), + (8, 2, true), + ] { + let Operator::MarkApply(op) = &mut plan.operator else { + unreachable!() + }; + op.materialize = false; + let Childrens::Twins { left, right } = plan.childrens.as_mut() else { + unreachable!() + }; + left.physical_option = Some(PhysicalOption::new( + PlanImpl::Values, + SortOption::None, + RowCountOption::Estimated(left_rows), + )); + right.physical_option = Some(PhysicalOption::new( + PlanImpl::Values, + SortOption::None, + RowCountOption::Estimated(right_rows), + )); + assert_eq!( + MaterializeMarkApply.apply(&mut plan, &mut arena)?, + materialize + ); + } + assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena)?); + let Operator::MarkApply(op) = &mut plan.operator else { + unreachable!() + }; + assert!(op.materialize); + op.materialize = false; + op.set_parameterized_probe(Some(predicate)); + assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena)?); + let Operator::MarkApply(op) = &mut plan.operator else { + unreachable!() + }; + op.set_parameterized_probe(None); + op.predicates.push(predicate); + assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena)?); + let Operator::MarkApply(op) = &mut plan.operator else { + unreachable!() + }; + op.predicates.pop(); + let Childrens::Twins { right, .. } = plan.childrens.as_mut() else { + unreachable!() + }; + right.physical_option = None; + assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena)?); + Ok(()) + } +} +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index 04eab69a..e5a09dc9 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -41,8 +41,10 @@ mod combine_operators; mod compilation_in_advance; mod elimination; mod join_reorder; +mod materialize_mark_apply; mod min_max_top_k; use join_reorder::GreedyJoinReorder; +use materialize_mark_apply::MaterializeMarkApply; mod parameterized_index; mod pushdown_limit; mod pushdown_predicates; @@ -83,6 +85,7 @@ pub enum NormalizationRuleImpl { ParameterizeInnerJoin, EliminateIndexFilter, GreedyJoinReorder, + MaterializeMarkApply, } #[derive(Debug, Copy, Clone, Eq, PartialEq)] @@ -189,7 +192,8 @@ impl NormalizationRuleImpl { NormalizationRuleImpl::EliminateIndexFilter => NormalizationRuleRootTag::Filter, NormalizationRuleImpl::ParameterizeInnerJoin | NormalizationRuleImpl::GreedyJoinReorder => NormalizationRuleRootTag::Join, - NormalizationRuleImpl::ParameterizeMarkApply => NormalizationRuleRootTag::MarkApply, + NormalizationRuleImpl::ParameterizeMarkApply + | NormalizationRuleImpl::MaterializeMarkApply => NormalizationRuleRootTag::MarkApply, } } } @@ -232,6 +236,7 @@ impl NormalizationRule for NormalizationRuleImpl { NormalizationRuleImpl::ParameterizeMarkApply => { ParameterizeMarkApply.apply(plan, arena) } + NormalizationRuleImpl::MaterializeMarkApply => MaterializeMarkApply.apply(plan, arena), } } } diff --git a/src/planner/operator/mark_apply.rs b/src/planner/operator/mark_apply.rs index 32e056ac..b811550d 100644 --- a/src/planner/operator/mark_apply.rs +++ b/src/planner/operator/mark_apply.rs @@ -38,6 +38,7 @@ pub struct MarkApplyOperator { pub predicates: Vec, output_column: Option, pub parameterized_probe: Option, + pub(crate) materialize: bool, } impl MarkApplyOperator { @@ -47,6 +48,7 @@ impl MarkApplyOperator { predicates, output_column: Some(output_column), parameterized_probe: None, + materialize: false, } } @@ -79,6 +81,7 @@ impl MarkApplyOperator { predicates, output_column: Some(output_column), parameterized_probe: None, + materialize: false, } } @@ -137,6 +140,7 @@ impl MarkApplyOperator { predicates, output_column: None, parameterized_probe: Some(probe), + materialize: false, } } diff --git a/tests/slt/parameterized_subquery.slt b/tests/slt/parameterized_subquery.slt index 83c1c6e1..eaee110e 100644 --- a/tests/slt/parameterized_subquery.slt +++ b/tests/slt/parameterized_subquery.slt @@ -26,22 +26,22 @@ insert into in_inner_nn values (0, 2); query T explain select id from in_outer where a in (select v from in_inner where in_inner.v = in_outer.a); ---- -Projection [in_outer.id] [Project => (Sort Option: Follow)] Filter _temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply TableScan in_outer -> [in_outer.id, in_outer.a] [SeqScan => (Sort Option: None)] Projection [in_inner.v] [Project => (Sort Option: Follow)] TableScan in_inner -> [in_inner.v] [IndexScan By in_inner_v_index => Probe ? => (Sort Option: OrderBy: (in_inner.v Asc Nulls Last) ignore_prefix_len: 0)] +Projection [in_outer.id] [Project => (Sort Option: Follow)] Filter _temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply [MarkApply => (Sort Option: Follow)] TableScan in_outer -> [in_outer.id, in_outer.a] [SeqScan => (Sort Option: None)] Projection [in_inner.v] [Project => (Sort Option: Follow)] TableScan in_inner -> [in_inner.v] [IndexScan By in_inner_v_index => Probe ? => (Sort Option: OrderBy: (in_inner.v Asc Nulls Last) ignore_prefix_len: 0)] query T explain select id from in_outer where a not in (select v from in_inner where in_inner.v = in_outer.a); ---- -Projection [in_outer.id] [Project => (Sort Option: Follow)] Filter !_temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply TableScan in_outer -> [in_outer.id, in_outer.a] [SeqScan => (Sort Option: None)] Projection [in_inner.v] [Project => (Sort Option: Follow)] TableScan in_inner -> [in_inner.v] [IndexScan By in_inner_v_index => Probe ? => (Sort Option: OrderBy: (in_inner.v Asc Nulls Last) ignore_prefix_len: 0)] +Projection [in_outer.id] [Project => (Sort Option: Follow)] Filter !_temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply [MarkApply => (Sort Option: Follow)] TableScan in_outer -> [in_outer.id, in_outer.a] [SeqScan => (Sort Option: None)] Projection [in_inner.v] [Project => (Sort Option: Follow)] TableScan in_inner -> [in_inner.v] [IndexScan By in_inner_v_index => Probe ? => (Sort Option: OrderBy: (in_inner.v Asc Nulls Last) ignore_prefix_len: 0)] query T explain select id from in_outer where a in (select v from in_inner_nn where in_inner_nn.v = in_outer.a); ---- -Projection [in_outer.id] [Project => (Sort Option: Follow)] Filter _temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply TableScan in_outer -> [in_outer.id, in_outer.a] [SeqScan => (Sort Option: None)] Projection [in_inner_nn.v] [Project => (Sort Option: Follow)] TableScan in_inner_nn -> [in_inner_nn.v] [IndexScan By in_inner_nn_v_index => Probe ? => (Sort Option: OrderBy: (in_inner_nn.v Asc Nulls Last) ignore_prefix_len: 0)] +Projection [in_outer.id] [Project => (Sort Option: Follow)] Filter _temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply [MarkApply => (Sort Option: Follow)] TableScan in_outer -> [in_outer.id, in_outer.a] [SeqScan => (Sort Option: None)] Projection [in_inner_nn.v] [Project => (Sort Option: Follow)] TableScan in_inner_nn -> [in_inner_nn.v] [IndexScan By in_inner_nn_v_index => Probe ? => (Sort Option: OrderBy: (in_inner_nn.v Asc Nulls Last) ignore_prefix_len: 0)] query T explain select id from in_outer where a not in (select v from in_inner_nn where in_inner_nn.v = in_outer.a); ---- -Projection [in_outer.id] [Project => (Sort Option: Follow)] Filter !_temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply TableScan in_outer -> [in_outer.id, in_outer.a] [SeqScan => (Sort Option: None)] Projection [in_inner_nn.v] [Project => (Sort Option: Follow)] TableScan in_inner_nn -> [in_inner_nn.v] [IndexScan By in_inner_nn_v_index => Probe ? => (Sort Option: OrderBy: (in_inner_nn.v Asc Nulls Last) ignore_prefix_len: 0)] +Projection [in_outer.id] [Project => (Sort Option: Follow)] Filter !_temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply [MarkApply => (Sort Option: Follow)] TableScan in_outer -> [in_outer.id, in_outer.a] [SeqScan => (Sort Option: None)] Projection [in_inner_nn.v] [Project => (Sort Option: Follow)] TableScan in_inner_nn -> [in_inner_nn.v] [IndexScan By in_inner_nn_v_index => Probe ? => (Sort Option: OrderBy: (in_inner_nn.v Asc Nulls Last) ignore_prefix_len: 0)] query I rowsort select id from in_outer where a in (select v from in_inner where in_inner.v = in_outer.a) order by id; @@ -96,22 +96,22 @@ insert into in_inner_flag_nn values (0, 2, 1); query T explain select id from in_outer_flag where a in (select v from in_inner_flag where in_inner_flag.flag = in_outer_flag.b); ---- -Projection [in_outer_flag.id] [Project => (Sort Option: Follow)] Filter _temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply TableScan in_outer_flag -> [in_outer_flag.id, in_outer_flag.a, in_outer_flag.b] [SeqScan => (Sort Option: None)] Projection [in_inner_flag.v, in_inner_flag.flag] [Project => (Sort Option: Follow)] TableScan in_inner_flag -> [in_inner_flag.v, in_inner_flag.flag] [IndexScan By in_inner_flag_v_index => Probe ? => (Sort Option: OrderBy: (in_inner_flag.v Asc Nulls Last) ignore_prefix_len: 0)] +Projection [in_outer_flag.id] [Project => (Sort Option: Follow)] Filter _temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply [MarkApply => (Sort Option: Follow)] TableScan in_outer_flag -> [in_outer_flag.id, in_outer_flag.a, in_outer_flag.b] [SeqScan => (Sort Option: None)] Projection [in_inner_flag.v, in_inner_flag.flag] [Project => (Sort Option: Follow)] TableScan in_inner_flag -> [in_inner_flag.v, in_inner_flag.flag] [IndexScan By in_inner_flag_v_index => Probe ? => (Sort Option: OrderBy: (in_inner_flag.v Asc Nulls Last) ignore_prefix_len: 0)] query T explain select id from in_outer_flag where a not in (select v from in_inner_flag where in_inner_flag.flag = in_outer_flag.b); ---- -Projection [in_outer_flag.id] [Project => (Sort Option: Follow)] Filter !_temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply TableScan in_outer_flag -> [in_outer_flag.id, in_outer_flag.a, in_outer_flag.b] [SeqScan => (Sort Option: None)] Projection [in_inner_flag.v, in_inner_flag.flag] [Project => (Sort Option: Follow)] TableScan in_inner_flag -> [in_inner_flag.v, in_inner_flag.flag] [IndexScan By in_inner_flag_v_index => Probe ? => (Sort Option: OrderBy: (in_inner_flag.v Asc Nulls Last) ignore_prefix_len: 0)] +Projection [in_outer_flag.id] [Project => (Sort Option: Follow)] Filter !_temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply [MarkApply => (Sort Option: Follow)] TableScan in_outer_flag -> [in_outer_flag.id, in_outer_flag.a, in_outer_flag.b] [SeqScan => (Sort Option: None)] Projection [in_inner_flag.v, in_inner_flag.flag] [Project => (Sort Option: Follow)] TableScan in_inner_flag -> [in_inner_flag.v, in_inner_flag.flag] [IndexScan By in_inner_flag_v_index => Probe ? => (Sort Option: OrderBy: (in_inner_flag.v Asc Nulls Last) ignore_prefix_len: 0)] query T explain select id from in_outer_flag where a in (select v from in_inner_flag_nn where in_inner_flag_nn.flag = in_outer_flag.b); ---- -Projection [in_outer_flag.id] [Project => (Sort Option: Follow)] Filter _temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply TableScan in_outer_flag -> [in_outer_flag.id, in_outer_flag.a, in_outer_flag.b] [SeqScan => (Sort Option: None)] Projection [in_inner_flag_nn.v, in_inner_flag_nn.flag] [Project => (Sort Option: Follow)] TableScan in_inner_flag_nn -> [in_inner_flag_nn.v, in_inner_flag_nn.flag] [IndexScan By in_inner_flag_nn_v_index => Probe ? => (Sort Option: OrderBy: (in_inner_flag_nn.v Asc Nulls Last) ignore_prefix_len: 0)] +Projection [in_outer_flag.id] [Project => (Sort Option: Follow)] Filter _temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply [MarkApply => (Sort Option: Follow)] TableScan in_outer_flag -> [in_outer_flag.id, in_outer_flag.a, in_outer_flag.b] [SeqScan => (Sort Option: None)] Projection [in_inner_flag_nn.v, in_inner_flag_nn.flag] [Project => (Sort Option: Follow)] TableScan in_inner_flag_nn -> [in_inner_flag_nn.v, in_inner_flag_nn.flag] [IndexScan By in_inner_flag_nn_v_index => Probe ? => (Sort Option: OrderBy: (in_inner_flag_nn.v Asc Nulls Last) ignore_prefix_len: 0)] query T explain select id from in_outer_flag where a not in (select v from in_inner_flag_nn where in_inner_flag_nn.flag = in_outer_flag.b); ---- -Projection [in_outer_flag.id] [Project => (Sort Option: Follow)] Filter !_temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply TableScan in_outer_flag -> [in_outer_flag.id, in_outer_flag.a, in_outer_flag.b] [SeqScan => (Sort Option: None)] Projection [in_inner_flag_nn.v, in_inner_flag_nn.flag] [Project => (Sort Option: Follow)] TableScan in_inner_flag_nn -> [in_inner_flag_nn.v, in_inner_flag_nn.flag] [IndexScan By in_inner_flag_nn_v_index => Probe ? => (Sort Option: OrderBy: (in_inner_flag_nn.v Asc Nulls Last) ignore_prefix_len: 0)] +Projection [in_outer_flag.id] [Project => (Sort Option: Follow)] Filter !_temp_table_1_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkAnyApply [MarkApply => (Sort Option: Follow)] TableScan in_outer_flag -> [in_outer_flag.id, in_outer_flag.a, in_outer_flag.b] [SeqScan => (Sort Option: None)] Projection [in_inner_flag_nn.v, in_inner_flag_nn.flag] [Project => (Sort Option: Follow)] TableScan in_inner_flag_nn -> [in_inner_flag_nn.v, in_inner_flag_nn.flag] [IndexScan By in_inner_flag_nn_v_index => Probe ? => (Sort Option: OrderBy: (in_inner_flag_nn.v Asc Nulls Last) ignore_prefix_len: 0)] query I rowsort select id from in_outer_flag where a in (select v from in_inner_flag where in_inner_flag.flag = in_outer_flag.b) order by id; @@ -153,12 +153,12 @@ insert into exists_inner values (0, 1, 1), (1, 1, null), (2, 2, 1); query T explain select id from exists_outer where exists (select 1 from exists_inner where exists_inner.v = exists_outer.a and exists_inner.flag = exists_outer.b); ---- -Projection [exists_outer.id] [Project => (Sort Option: Follow)] Filter _temp_table_0_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkExistsApply TableScan exists_outer -> [exists_outer.id, exists_outer.a, exists_outer.b] [SeqScan => (Sort Option: None)] TableScan exists_inner -> [exists_inner.id, exists_inner.v, exists_inner.flag] [IndexScan By exists_inner_v_index => Probe ? => (Sort Option: OrderBy: (exists_inner.v Asc Nulls Last) ignore_prefix_len: 0)] +Projection [exists_outer.id] [Project => (Sort Option: Follow)] Filter _temp_table_0_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkExistsApply [MarkApply => (Sort Option: Follow)] TableScan exists_outer -> [exists_outer.id, exists_outer.a, exists_outer.b] [SeqScan => (Sort Option: None)] TableScan exists_inner -> [exists_inner.id, exists_inner.v, exists_inner.flag] [IndexScan By exists_inner_v_index => Probe ? => (Sort Option: OrderBy: (exists_inner.v Asc Nulls Last) ignore_prefix_len: 0)] query T explain select id from exists_outer where not exists (select 1 from exists_inner where exists_inner.v = exists_outer.a and exists_inner.flag = exists_outer.b); ---- -Projection [exists_outer.id] [Project => (Sort Option: Follow)] Filter !_temp_table_0_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkExistsApply TableScan exists_outer -> [exists_outer.id, exists_outer.a, exists_outer.b] [SeqScan => (Sort Option: None)] TableScan exists_inner -> [exists_inner.id, exists_inner.v, exists_inner.flag] [IndexScan By exists_inner_v_index => Probe ? => (Sort Option: OrderBy: (exists_inner.v Asc Nulls Last) ignore_prefix_len: 0)] +Projection [exists_outer.id] [Project => (Sort Option: Follow)] Filter !_temp_table_0_.true, Is Having: false [Filter => (Sort Option: Follow)] MarkExistsApply [MarkApply => (Sort Option: Follow)] TableScan exists_outer -> [exists_outer.id, exists_outer.a, exists_outer.b] [SeqScan => (Sort Option: None)] TableScan exists_inner -> [exists_inner.id, exists_inner.v, exists_inner.flag] [IndexScan By exists_inner_v_index => Probe ? => (Sort Option: OrderBy: (exists_inner.v Asc Nulls Last) ignore_prefix_len: 0)] query I rowsort select id from exists_outer where exists (select 1 from exists_inner where exists_inner.v = exists_outer.a and exists_inner.flag = exists_outer.b) order by id; diff --git a/tests/slt/subquery.slt b/tests/slt/subquery.slt index 0e3628a8..3f008708 100644 --- a/tests/slt/subquery.slt +++ b/tests/slt/subquery.slt @@ -1080,6 +1080,25 @@ select id,b in (select b from corr_rows where id=2) as matched,b not in (select 3 false false 4 NULL NULL +# Each row compares its own value with the same cached, nested RHS. +query ITTT +select id,b in (select b from corr_rows where b in (select b from corr_rows where id=2)),ball(select b from corr_rows where false) from corr_rows order by id; +---- +1 false true +2 false true +3 false true +4 false true +5 false true + statement error correlated quantified subqueries in SELECT are not supported yet select b in (select x.b from corr_rows x where x.id Date: Sun, 25 Oct 2026 12:34:38 +0800 Subject: [PATCH 05/62] perf: pause quantified input materialization on terminal matches --- src/execution/dql/mark_apply.rs | 400 +++++++++++++++++++++----------- src/execution/spill/mod.rs | 26 +++ 2 files changed, 296 insertions(+), 130 deletions(-) diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index ceda357b..82ae3a57 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -14,7 +14,7 @@ use crate::errors::DatabaseError; #[cfg(feature = "spill")] -use crate::execution::spill::{SpillReader, SpillVec}; +use crate::execution::spill::SpillVec; use crate::execution::{ build_read, ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor, }; @@ -33,6 +33,11 @@ enum QuantifiedPredicateOutcome { Skip, } +#[cfg(feature = "spill")] +type MaterializedRows = SpillVec<'static, Tuple>; +#[cfg(not(feature = "spill"))] +type MaterializedRows = Vec; + enum RightInput<'a> { Rebuild { plan: &'a LogicalPlan, @@ -42,11 +47,11 @@ enum RightInput<'a> { plan: &'a LogicalPlan, pos: ExecId, }, - Empty, - #[cfg(feature = "spill")] - Materialized(SpillReader), - #[cfg(not(feature = "spill"))] - Materialized(Vec), + Materializing { + root: ExecId, + rows: MaterializedRows, + }, + Materialized(MaterializedRows), } impl<'a> RightInput<'a> { @@ -75,78 +80,80 @@ impl<'a> RightInput<'a> { // Return the first terminal predicate result, or None after exhausting the RHS. // The caller supplies Mark semantics; this enum owns reconstruction and replay. fn scan( - &mut self, + self, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, probe: Option, mut evaluate: impl FnMut(&Tuple, &ExecMetaArena) -> Result, DatabaseError>, - ) -> Result, DatabaseError> { - if matches!(self, Self::Materialize { .. }) { - let root = self.build(arena, plan_arena, None); - #[cfg(feature = "spill")] - { - let mut rows = SpillVec::new(); + ) -> Result<(Self, Option), DatabaseError> { + match self { + input @ Self::Rebuild { .. } => { + let root = input.build(arena, plan_arena, probe); while arena.next_tuple(root, plan_arena)? { - let _ = rows.push(arena.materialize_tuple())?; + if let Some(value) = evaluate(arena.result_tuple(), plan_arena)? { + return Ok((input, Some(value))); + } } - // Fully flush before replaying through the existing reset API. - let _ = rows.flush()?; - *self = if rows.is_spilled() { - Self::Materialized(rows.into_iter()) - } else { - Self::Empty - }; + Ok((input, None)) } - #[cfg(not(feature = "spill"))] - { - let mut rows = Vec::new(); - while arena.next_tuple(root, plan_arena)? { - rows.push(arena.materialize_tuple()); + input @ Self::Materialize { .. } => { + let root = input.build(arena, plan_arena, None); + Self::Materializing { + root, + rows: MaterializedRows::new(), } - *self = if rows.is_empty() { - Self::Empty - } else { - Self::Materialized(rows) - }; + .scan(arena, plan_arena, None, evaluate) } - } - match self { - Self::Rebuild { .. } => { - let root = self.build(arena, plan_arena, probe); + Self::Materializing { root, mut rows } => { + if let Some(value) = Self::replay(&rows, plan_arena, &mut evaluate)? { + return Ok((Self::Materializing { root, rows }, Some(value))); + } while arena.next_tuple(root, plan_arena)? { - if let Some(value) = evaluate(arena.result_tuple(), plan_arena)? { - return Ok(Some(value)); + let right = arena.materialize_tuple(); + let result = evaluate(&right, plan_arena)?; + #[cfg(feature = "spill")] + let _ = rows.push(right)?; + #[cfg(not(feature = "spill"))] + rows.push(right); + // A terminal result pauses the executor; cached rows remain a prefix. + if result.is_some() { + return Ok((Self::Materializing { root, rows }, result)); } } + // Only EOF makes the prefix complete; move its cache into the final state. + Ok((Self::Materialized(rows), None)) } - #[cfg(feature = "spill")] Self::Materialized(rows) => { - rows.reset()?; - for right in rows { - if let Some(value) = evaluate(&right?, plan_arena)? { - return Ok(Some(value)); - } - } + let result = Self::replay(&rows, plan_arena, &mut evaluate)?; + Ok((Self::Materialized(rows), result)) } - #[cfg(not(feature = "spill"))] - Self::Materialized(rows) => { - for right in rows { - if let Some(value) = evaluate(right, plan_arena)? { - return Ok(Some(value)); - } + } + } + + fn replay( + rows: &MaterializedRows, + plan_arena: &ExecMetaArena, + mut evaluate: impl FnMut(&Tuple, &ExecMetaArena) -> Result, DatabaseError>, + ) -> Result, DatabaseError> { + #[cfg(feature = "spill")] + return rows.replay(|right| evaluate(right, plan_arena)); + #[cfg(not(feature = "spill"))] + { + for right in rows { + if let Some(value) = evaluate(right, plan_arena)? { + return Ok(Some(value)); } } - Self::Empty => {} - Self::Materialize { .. } => unreachable!("RHS materialization is complete"), + Ok(None) } - Ok(None) } } pub struct MarkApply<'a> { op: &'a MarkApplyOperator, left_input: ExecId, - right_input: RightInput<'a>, + // Temporarily None while scan owns and transitions the right input. + right_input: Option>, // Retain a streaming inner input across next_tuple calls, not its result rows. join_input: Option<(ExecId, Tuple)>, } @@ -178,7 +185,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for MarkApply<'a> { arena.push(ExecNode::MarkApply(Self { op, left_input, - right_input, + right_input: Some(right_input), join_input: None, })) } @@ -190,15 +197,22 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { + let Some(input) = self.right_input.take() else { + arena.finish(); + return Ok(()); + }; if matches!(self.op.kind, MarkApplyKind::InnerJoin) { - return self.next_join_tuple(arena, plan_arena); + self.next_join_tuple(&input, arena, plan_arena)?; + self.right_input = Some(input); + return Ok(()); } if !arena.next_tuple(self.left_input, plan_arena)? { arena.finish(); return Ok(()); } let mut left = arena.materialize_tuple(); - let marker = self.mark_value(arena, plan_arena, &left)?; + let (input, marker) = self.mark_value(input, arena, plan_arena, &left)?; + self.right_input = Some(input); left.values.push(marker); arena.produce_tuple(left); Ok(()) @@ -208,6 +222,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { impl<'a> MarkApply<'a> { fn next_join_tuple( &mut self, + input: &RightInput<'a>, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { @@ -233,9 +248,7 @@ impl<'a> MarkApply<'a> { } let left = arena.materialize_tuple(); let value = self.parameterized_probe_value(&left, plan_arena)?; - let root = self - .right_input - .build(arena, plan_arena, self.runtime_probe_for(value)); + let root = input.build(arena, plan_arena, self.runtime_probe_for(value)); self.join_input = Some((root, left)); } } @@ -275,32 +288,33 @@ impl<'a> MarkApply<'a> { } fn mark_value( - &mut self, + &self, + input: RightInput<'a>, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, left_tuple: &Tuple, - ) -> Result { + ) -> Result<(RightInput<'a>, DataValue), DatabaseError> { let probe = self.parameterized_probe_value(left_tuple, plan_arena)?; match self.op.kind { MarkApplyKind::InnerJoin => unreachable!("inner join streams tuples"), MarkApplyKind::Exists => { let predicates = self.op.predicates(); let runtime_probe = self.runtime_probe_for(probe); - Ok(self - .right_input - .scan(arena, plan_arena, runtime_probe, |right, meta| { + let (input, value) = + input.scan(arena, plan_arena, runtime_probe, |right, meta| { Ok( Self::predicates_matched(predicates, left_tuple, right, meta)? .then_some(DataValue::Boolean(true)), ) - })? - .unwrap_or(DataValue::Boolean(false))) + })?; + Ok((input, value.unwrap_or(DataValue::Boolean(false)))) } MarkApplyKind::Quantified(quantifier) => { if matches!(quantifier, MarkApplyQuantifier::Any) { if let Some(probe_value) = probe { if !probe_value.is_null() { - let value = self.scan_quantified_right_input( + let (input, value) = self.scan_quantified_right_input( + input, arena, plan_arena, Some(probe_value), @@ -308,65 +322,73 @@ impl<'a> MarkApply<'a> { left_tuple, )?; if value == DataValue::Boolean(true) { - return Ok(value); + return Ok((input, value)); } - let value = self.scan_quantified_right_input( + let (input, value) = self.scan_quantified_right_input( + input, arena, plan_arena, Some(DataValue::Null), quantifier, left_tuple, )?; - return Ok(if value.is_null() { - DataValue::Null - } else { - DataValue::Boolean(false) - }); + return Ok(( + input, + if value.is_null() { + DataValue::Null + } else { + DataValue::Boolean(false) + }, + )); } } } - self.scan_quantified_right_input(arena, plan_arena, None, quantifier, left_tuple) + self.scan_quantified_right_input( + input, arena, plan_arena, None, quantifier, left_tuple, + ) } } } fn scan_quantified_right_input( - &mut self, + &self, + input: RightInput<'a>, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, probe: Option, quantifier: MarkApplyQuantifier, left_tuple: &Tuple, - ) -> Result { + ) -> Result<(RightInput<'a>, DataValue), DatabaseError> { let mut saw_null = false; let predicates = self.op.predicates(); let runtime_probe = self.runtime_probe_for(probe); - let value = self - .right_input - .scan(arena, plan_arena, runtime_probe, |right, meta| { - match Self::quantified_predicate_outcome(predicates, left_tuple, right, meta)? { - QuantifiedPredicateOutcome::True - if matches!(quantifier, MarkApplyQuantifier::Any) => - { - return Ok(Some(DataValue::Boolean(true))); - } - QuantifiedPredicateOutcome::False - if matches!(quantifier, MarkApplyQuantifier::All) => - { - return Ok(Some(DataValue::Boolean(false))); - } - QuantifiedPredicateOutcome::Null => saw_null = true, - _ => {} + let (input, value) = input.scan(arena, plan_arena, runtime_probe, |right, meta| { + match Self::quantified_predicate_outcome(predicates, left_tuple, right, meta)? { + QuantifiedPredicateOutcome::True + if matches!(quantifier, MarkApplyQuantifier::Any) => + { + return Ok(Some(DataValue::Boolean(true))); } - Ok(None) - })?; - Ok(value.unwrap_or_else(|| { - if saw_null { - DataValue::Null - } else { - DataValue::Boolean(matches!(quantifier, MarkApplyQuantifier::All)) + QuantifiedPredicateOutcome::False + if matches!(quantifier, MarkApplyQuantifier::All) => + { + return Ok(Some(DataValue::Boolean(false))); + } + QuantifiedPredicateOutcome::Null => saw_null = true, + _ => {} } - })) + Ok(None) + })?; + Ok(( + input, + value.unwrap_or_else(|| { + if saw_null { + DataValue::Null + } else { + DataValue::Boolean(matches!(quantifier, MarkApplyQuantifier::All)) + } + }), + )) } fn predicates_matched( @@ -804,18 +826,20 @@ mod tests { let mut exec: MarkApply = MarkApply { op: &op, - right_input: RightInput::Rebuild { + right_input: Some(RightInput::Rebuild { plan: &right, pos: arena.nodes.position(), - }, + }), left_input: 0, join_input: None, }; let left_tuple = Tuple::new(None, vec![DataValue::Int32(2), DataValue::Int32(1)]); let mut plan_arena = ExecMetaArena::new(plan_arena); + let input = exec.right_input.take().unwrap(); assert_eq!( - exec.mark_value(&mut arena, &mut plan_arena, &left_tuple)?, + exec.mark_value(input, &mut arena, &mut plan_arena, &left_tuple)? + .1, DataValue::Boolean(true) ); assert_eq!( @@ -860,18 +884,20 @@ mod tests { let mut exec: MarkApply = MarkApply { op: &op, - right_input: RightInput::Rebuild { + right_input: Some(RightInput::Rebuild { plan: &right, pos: arena.nodes.position(), - }, + }), left_input: 0, join_input: None, }; let left_tuple = Tuple::new(None, vec![DataValue::Int32(2)]); let mut plan_arena = ExecMetaArena::new(plan_arena); + let input = exec.right_input.take().unwrap(); assert_eq!( - exec.mark_value(&mut arena, &mut plan_arena, &left_tuple)?, + exec.mark_value(input, &mut arena, &mut plan_arena, &left_tuple)? + .1, DataValue::Boolean(true) ); assert_eq!( @@ -916,18 +942,20 @@ mod tests { let mut exec: MarkApply = MarkApply { op: &op, - right_input: RightInput::Rebuild { + right_input: Some(RightInput::Rebuild { plan: &right, pos: arena.nodes.position(), - }, + }), left_input: 0, join_input: None, }; let left_tuple = Tuple::new(None, vec![DataValue::Null]); let mut plan_arena = ExecMetaArena::new(plan_arena); + let input = exec.right_input.take().unwrap(); assert_eq!( - exec.mark_value(&mut arena, &mut plan_arena, &left_tuple)?, + exec.mark_value(input, &mut arena, &mut plan_arena, &left_tuple)? + .1, DataValue::Null ); assert_eq!( @@ -995,15 +1023,19 @@ mod tests { let (table_cache, view_cache, meta_cache, _temp_dir, storage) = build_test_storage()?; let transaction = storage.transaction()?; let cache = crate::execution::empty_context(&table_cache, &view_cache, &meta_cache); - for quantifier in [MarkApplyQuantifier::Any, MarkApplyQuantifier::All] { + // Exercise both a single segment and the default 1024-row spill boundary. + for (quantifier, repeated_rows) in [ + (MarkApplyQuantifier::Any, 1), + (MarkApplyQuantifier::All, 1), + (MarkApplyQuantifier::Any, 1025), + (MarkApplyQuantifier::All, 1025), + ] { let table_arena = crate::planner::TableArenaCell::default(); let mut meta = crate::planner::PlanArena::new(&table_arena); let mut left = build_values(&mut meta, "left_key", vec![vec![DataValue::Int32(2)]; 8]); - let mut right = build_values( - &mut meta, - "right_key", - vec![vec![DataValue::Int32(2)], vec![DataValue::Null]], - ); + let mut right_rows = vec![vec![DataValue::Int32(2)]; repeated_rows]; + right_rows.push(vec![DataValue::Null]); + let mut right = build_values(&mut meta, "right_key", right_rows); let left_column = left.output_schema(&mut meta)[0]; let right_column = right.output_schema(&mut meta)[0]; let predicate = build_equality_predicate(&mut meta, left_column, 0, right_column, 1)?; @@ -1021,41 +1053,149 @@ mod tests { build_read(&mut arena, &mut meta, &right, cache, &transaction); let mut exec = MarkApply { op: &op, - right_input: RightInput::Materialize { plan: &right, pos }, + right_input: Some(RightInput::Materialize { plan: &right, pos }), left_input, join_input: None, }; let mut meta = ExecMetaArena::new(meta); - for value in [ + for (index, value) in [ DataValue::Int32(2), DataValue::Int32(1), DataValue::Null, DataValue::Int32(2), - ] { + ] + .into_iter() + .enumerate() + { let expected = match (quantifier, &value) { (MarkApplyQuantifier::Any, DataValue::Int32(2)) => DataValue::Boolean(true), (MarkApplyQuantifier::All, DataValue::Int32(1)) => DataValue::Boolean(false), _ => DataValue::Null, }; let tuple = Tuple::new(None, vec![value]); - assert_eq!(exec.mark_value(&mut arena, &mut meta, &tuple)?, expected); - assert!(matches!(exec.right_input, RightInput::Materialized(_))); + let input = exec.right_input.take().unwrap(); + let (input, value) = exec.mark_value(input, &mut arena, &mut meta, &tuple)?; + exec.right_input = Some(input); + assert_eq!(value, expected); + if index == 0 && matches!(quantifier, MarkApplyQuantifier::Any) { + let Some(RightInput::Materializing { rows, .. }) = &exec.right_input else { + panic!("first match must pause the RHS") + }; + #[cfg(feature = "spill")] + { + let mut cached = 0; + rows.replay::<()>(|_| { + cached += 1; + Ok(None) + })?; + assert_eq!(cached, 1); + } + #[cfg(not(feature = "spill"))] + assert_eq!(rows.len(), 1); + } else { + assert!(matches!( + exec.right_input, + Some(RightInput::Materialized(_)) + )); + } } - // Empty RHS is a persistent state, including for a NULL left value. - exec.right_input = RightInput::Materialize { plan: &empty, pos }; + // An empty RHS completes immediately, including for a NULL left value. + exec.right_input = Some(RightInput::Materialize { plan: &empty, pos }); + let input = exec.right_input.take().unwrap(); + let (input, value) = exec.mark_value( + input, + &mut arena, + &mut meta, + &Tuple::new(None, vec![DataValue::Null]), + )?; + exec.right_input = Some(input); assert_eq!( - exec.mark_value( - &mut arena, - &mut meta, - &Tuple::new(None, vec![DataValue::Null]) - )?, + value, DataValue::Boolean(matches!(quantifier, MarkApplyQuantifier::All)) ); - assert!(matches!(exec.right_input, RightInput::Empty)); + assert!(matches!( + exec.right_input, + Some(RightInput::Materialized(_)) + )); } Ok(()) } + #[test] + fn paused_rhs_survives_left_subquery_rebuilds() -> Result<(), DatabaseError> { + let tables = crate::planner::TableArenaCell::default(); + let mut meta = crate::planner::PlanArena::new(&tables); + let mut left = build_values( + &mut meta, + "left_key", + vec![ + vec![DataValue::Int32(1)], + vec![DataValue::Int32(2)], + vec![DataValue::Int32(99)], + vec![DataValue::Int32(1)], + ], + ); + let mut lookup = build_values( + &mut meta, + "lookup_key", + vec![vec![DataValue::Int32(1)], vec![DataValue::Int32(2)]], + ); + let left_column = left.output_schema(&mut meta)[0]; + let lookup_column = lookup.output_schema(&mut meta)[0]; + let predicate = build_equality_predicate(&mut meta, left_column, 0, lookup_column, 1)?; + let marker = build_marker_column(&mut meta); + let left = MarkApplyOperator::build_exists(left, lookup, marker, vec![predicate]); + let mut right = build_values( + &mut meta, + "right_key", + vec![ + vec![DataValue::Int32(1)], + vec![DataValue::Int32(2)], + vec![DataValue::Int32(3)], + ], + ); + let right_column = right.output_schema(&mut meta)[0]; + let predicate = build_equality_predicate(&mut meta, left_column, 0, right_column, 2)?; + let marker = build_marker_column(&mut meta); + let mut op = + MarkApplyOperator::new_quantified(MarkApplyQuantifier::Any, marker, vec![predicate]); + op.materialize = true; + let (table_cache, view_cache, meta_cache, _temp_dir, storage) = build_test_storage()?; + let transaction = storage.transaction()?; + let tuples = try_collect(execute_input::<_, MarkApply>( + (&op, &left, &right), + crate::execution::empty_context(&table_cache, &view_cache, &meta_cache), + meta, + &transaction, + ))?; + assert_eq!( + tuples.into_iter().map(|row| row.values).collect::>(), + vec![ + vec![ + DataValue::Int32(1), + DataValue::Boolean(true), + DataValue::Boolean(true) + ], + vec![ + DataValue::Int32(2), + DataValue::Boolean(true), + DataValue::Boolean(true) + ], + vec![ + DataValue::Int32(99), + DataValue::Boolean(false), + DataValue::Boolean(false) + ], + vec![ + DataValue::Int32(1), + DataValue::Boolean(true), + DataValue::Boolean(true) + ], + ] + ); + Ok(()) + } + #[test] fn mark_in_apply_treats_null_predicate_as_not_matched() -> Result<(), DatabaseError> { let table_arena = crate::planner::TableArenaCell::default(); diff --git a/src/execution/spill/mod.rs b/src/execution/spill/mod.rs index 1a293485..2b7b1076 100644 --- a/src/execution/spill/mod.rs +++ b/src/execution/spill/mod.rs @@ -115,6 +115,32 @@ impl<'on_flush, T: SpillCodec> SpillVec<'on_flush, T> { matches!(&self.writer, Ok(state) if state.file.is_some()) } + /// Replays the current prefix without consuming the writer or flushing its memory tail. + pub(crate) fn replay( + &self, + mut evaluate: impl FnMut(&T) -> Result, DatabaseError>, + ) -> Result, DatabaseError> { + let state = self.writer.as_ref().map_err(|_| { + DatabaseError::InvalidValue("cannot replay a failed SpillVec".to_string()) + })?; + if let Some(file) = &state.file { + let mut reader = SegmentReader::new(BufReader::new(File::open(&file.file_guard.path)?)); + while reader.start_next_segment()? { + for value in &mut reader { + if let Some(result) = evaluate(&value?)? { + return Ok(Some(result)); + } + } + } + } + for value in &state.buffer { + if let Some(result) = evaluate(value)? { + return Ok(Some(result)); + } + } + Ok(None) + } + pub(crate) fn flush(&mut self) -> Result, DatabaseError> { let state = self.writer.as_mut().map_err(|_| { DatabaseError::InvalidValue("cannot flush a failed SpillVec".to_string()) From 46eba36f5076c52242f55ede05ad1b63440f2ec9 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:38 +0800 Subject: [PATCH 06/62] perf: guard mark apply evaluation with safe boolean predicates --- src/db.rs | 5 + src/execution/dql/mark_apply.rs | 212 ++++++- .../rule/normalization/column_pruning.rs | 2 +- .../rule/normalization/guard_mark_apply.rs | 526 ++++++++++++++++++ src/optimizer/rule/normalization/mod.rs | 8 +- src/planner/operator/mark_apply.rs | 4 + src/planner/operator/mod.rs | 3 + src/planner/operator/visitor.rs | 3 + src/planner/operator/visitor_mut.rs | 3 + 9 files changed, 762 insertions(+), 4 deletions(-) create mode 100644 src/optimizer/rule/normalization/guard_mark_apply.rs diff --git a/src/db.rs b/src/db.rs index 01fedb51..f611207d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -436,6 +436,11 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { HepBatchStrategy::fix_point_topdown(10), vec![NormalizationRuleImpl::PushPredicateIntoScan], ) + .before_batch( + "Guard Mark Apply".to_string(), + HepBatchStrategy::fix_point_topdown(1), + vec![NormalizationRuleImpl::GuardMarkApply], + ) .after_batch( "Greedy Join Reorder".to_string(), HepBatchStrategy::once_topdown(), diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index 82ae3a57..47ec37c2 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -211,7 +211,15 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { return Ok(()); } let mut left = arena.materialize_tuple(); - let (input, marker) = self.mark_value(input, arena, plan_arena, &left)?; + let (input, marker) = match self.op.guard { + Some(guard) + if *plan_arena.expression(guard).eval(plan_arena, Some(&left))? + == DataValue::Boolean(false) => + { + (input, DataValue::Null) + } + _ => self.mark_value(input, arena, plan_arena, &left)?, + }; self.right_input = Some(input); left.values.push(marker); arena.produce_tuple(left); @@ -468,7 +476,7 @@ mod tests { use crate::planner::{Childrens, ExprRef, LogicalPlan}; use crate::storage::rocksdb::RocksStorage; use crate::storage::{StatisticsMetaCache, Storage, TableCache, ViewCache}; - use crate::types::evaluator::binary_create; + use crate::types::evaluator::{binary_create, cast_create}; use crate::types::index::RuntimeIndexProbe; use crate::types::tuple::Tuple; use crate::types::LogicalType; @@ -1017,6 +1025,206 @@ mod tests { Ok(()) } + #[test] + fn guard_false_skips_failing_rhs_but_true_and_null_evaluate_it() -> Result<(), DatabaseError> { + let (table_cache, view_cache, meta_cache, _temp_dir, storage) = build_test_storage()?; + let transaction = storage.transaction()?; + let cache = crate::execution::empty_context(&table_cache, &view_cache, &meta_cache); + for kind in [ + MarkApplyKind::Exists, + MarkApplyKind::Quantified(MarkApplyQuantifier::Any), + MarkApplyKind::Quantified(MarkApplyQuantifier::All), + ] { + for materialize in [false, true] { + for (flag, use_guard) in [ + (DataValue::Boolean(false), true), + (DataValue::Boolean(true), true), + (DataValue::Null, true), + (DataValue::Boolean(false), false), + ] { + let tables = crate::planner::TableArenaCell::default(); + let mut meta = crate::planner::PlanArena::new(&tables); + let mut left = build_values_with_schema( + &mut meta, + vec![ + ("key", LogicalType::Integer), + ("flag", LogicalType::Boolean), + ], + vec![vec![DataValue::Int32(1), flag.clone()]], + ); + let mut right = + build_values(&mut meta, "right_key", vec![vec![DataValue::Int32(1)]]); + let text = DataValue::from("not an integer".to_string()); + let evaluator = cast_create(&text.logical_type(), &LogicalType::Integer)?; + let expr = meta.alloc_expression(text.into()); + let failing = meta.alloc_expression(ScalarExpression::TypeCast { + expr, + ty: LogicalType::Integer, + evaluator: Some(evaluator), + }); + let Operator::Values(values) = &mut right.operator else { + unreachable!() + }; + values.rows[0] = failing; + let schema = left.output_schema(&mut meta); + let left_column = schema[0]; + let flag_column = schema[1]; + let right_column = right.output_schema(&mut meta)[0]; + let predicate = + build_equality_predicate(&mut meta, left_column, 0, right_column, 2)?; + let mut op = + MarkApplyOperator::new_in(build_marker_column(&mut meta), vec![predicate]); + op.kind = kind; + op.materialize = materialize && !matches!(kind, MarkApplyKind::Exists); + if use_guard { + op.guard = Some( + meta.alloc_expression(ScalarExpression::column_expr(flag_column, 1)), + ); + } + let mut arena = ExecArena::with_capacity(0); + arena.init_context(cache, &transaction); + let left_input = build_read(&mut arena, &mut meta, &left, cache, &transaction); + let pos = arena.nodes.position(); + build_read(&mut arena, &mut meta, &right, cache, &transaction); + let right_input = if op.materialize { + RightInput::Materialize { plan: &right, pos } + } else { + RightInput::Rebuild { plan: &right, pos } + }; + let mut exec = MarkApply { + op: &op, + left_input, + right_input: Some(right_input), + join_input: None, + }; + let mut meta = ExecMetaArena::new(meta); + let result = exec.next_tuple(&mut arena, &mut meta); + if use_guard && flag == DataValue::Boolean(false) { + result?; + assert_eq!( + arena.result_tuple().values, + vec![DataValue::Int32(1), flag, DataValue::Null] + ); + assert!(matches!( + exec.right_input, + Some(RightInput::Rebuild { .. } | RightInput::Materialize { .. }) + )); + exec.next_tuple(&mut arena, &mut meta)?; + assert!(exec.right_input.is_none()); + } else { + assert!(matches!(result, Err(DatabaseError::ParseInt(_)))); + } + } + } + } + Ok(()) + } + + #[test] + fn guard_skips_preserve_paused_and_completed_rhs() -> Result<(), DatabaseError> { + let (table_cache, view_cache, meta_cache, _temp_dir, storage) = build_test_storage()?; + let transaction = storage.transaction()?; + let cache = crate::execution::empty_context(&table_cache, &view_cache, &meta_cache); + let cases = [ + (99, DataValue::Boolean(false), DataValue::Null), + (1, DataValue::Boolean(true), DataValue::Boolean(true)), + (99, DataValue::Boolean(false), DataValue::Null), + (2, DataValue::Boolean(true), DataValue::Boolean(true)), + (99, DataValue::Null, DataValue::Null), + (2, DataValue::Boolean(false), DataValue::Null), + (1, DataValue::Boolean(true), DataValue::Boolean(true)), + ]; + for materialize in [false, true] { + let tables = crate::planner::TableArenaCell::default(); + let mut meta = crate::planner::PlanArena::new(&tables); + let mut left = build_values_with_schema( + &mut meta, + vec![ + ("key", LogicalType::Integer), + ("flag", LogicalType::Boolean), + ], + cases + .iter() + .map(|(key, flag, _)| vec![DataValue::Int32(*key), flag.clone()]) + .collect(), + ); + let mut right = build_values( + &mut meta, + "right_key", + vec![ + vec![DataValue::Int32(1)], + vec![DataValue::Int32(2)], + vec![DataValue::Null], + ], + ); + let schema = left.output_schema(&mut meta); + let left_column = schema[0]; + let flag_column = schema[1]; + let right_column = right.output_schema(&mut meta)[0]; + let predicate = build_equality_predicate(&mut meta, left_column, 0, right_column, 2)?; + let mut op = MarkApplyOperator::new_in(build_marker_column(&mut meta), vec![predicate]); + op.materialize = materialize; + op.guard = Some(meta.alloc_expression(ScalarExpression::column_expr(flag_column, 1))); + let mut arena = ExecArena::with_capacity(0); + arena.init_context(cache, &transaction); + let left_input = build_read(&mut arena, &mut meta, &left, cache, &transaction); + let pos = arena.nodes.position(); + build_read(&mut arena, &mut meta, &right, cache, &transaction); + let right_input = if materialize { + RightInput::Materialize { plan: &right, pos } + } else { + RightInput::Rebuild { plan: &right, pos } + }; + let mut exec = MarkApply { + op: &op, + left_input, + right_input: Some(right_input), + join_input: None, + }; + let mut meta = ExecMetaArena::new(meta); + for (index, (key, flag, marker)) in cases.iter().enumerate() { + exec.next_tuple(&mut arena, &mut meta)?; + assert_eq!( + arena.result_tuple().values, + vec![DataValue::Int32(*key), flag.clone(), marker.clone()] + ); + if materialize { + if index == 0 { + assert!(matches!( + exec.right_input, + Some(RightInput::Materialize { .. }) + )); + } else if index < 4 { + let Some(RightInput::Materializing { rows, .. }) = &exec.right_input else { + panic!("guard must retain the paused RHS") + }; + #[cfg(feature = "spill")] + { + let mut cached = 0; + rows.replay::<()>(|_| { + cached += 1; + Ok(None) + })?; + assert_eq!(cached, if index < 3 { 1 } else { 2 }); + } + #[cfg(not(feature = "spill"))] + assert_eq!(rows.len(), if index < 3 { 1 } else { 2 }); + } else { + assert!(matches!( + exec.right_input, + Some(RightInput::Materialized(_)) + )); + } + } else { + assert!(matches!(exec.right_input, Some(RightInput::Rebuild { .. }))); + } + } + exec.next_tuple(&mut arena, &mut meta)?; + assert!(exec.right_input.is_none()); + } + Ok(()) + } + #[test] fn quantified_apply_materializes_once_and_replays_after_short_circuit( ) -> Result<(), DatabaseError> { diff --git a/src/optimizer/rule/normalization/column_pruning.rs b/src/optimizer/rule/normalization/column_pruning.rs index 85fb9fa8..9d4ef677 100644 --- a/src/optimizer/rule/normalization/column_pruning.rs +++ b/src/optimizer/rule/normalization/column_pruning.rs @@ -672,7 +672,7 @@ impl ColumnPruning { let removed_positions = &outcome.removed_positions[left_removed_start..right_removed_end]; Self::remap_exprs_after_child_change( - op.predicates_mut().iter_mut(), + op.predicates.iter_mut().chain(op.guard.iter_mut()), removed_positions, &mut outcome.remapped_exprs, arena, diff --git a/src/optimizer/rule/normalization/guard_mark_apply.rs b/src/optimizer/rule/normalization/guard_mark_apply.rs new file mode 100644 index 00000000..36171460 --- /dev/null +++ b/src/optimizer/rule/normalization/guard_mark_apply.rs @@ -0,0 +1,526 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::strip_alias; +use crate::catalog::ColumnRef; +use crate::errors::DatabaseError; +use crate::expression::visitor::ExprVisitor; +use crate::expression::{AliasType, BinaryOperator, ScalarExpression, UnaryOperator}; +use crate::optimizer::core::rule::NormalizationRule; +use crate::planner::operator::mark_apply::MarkApplyKind; +use crate::planner::operator::Operator; +use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; +use crate::types::evaluator::BinaryEvaluatorRef; +use crate::types::value::DataValue; +use crate::types::LogicalType; + +pub struct GuardMarkApply; + +impl NormalizationRule for GuardMarkApply { + fn apply(&self, plan: &mut LogicalPlan, arena: &mut PlanArena) -> Result { + let (Operator::Project(project), Childrens::Only(filter)) = + (&plan.operator, plan.childrens.as_mut()) + else { + return Ok(false); + }; + let (Operator::Filter(filter_op), Childrens::Only(mark)) = + (&filter.operator, filter.childrens.as_mut()) + else { + return Ok(false); + }; + let (Operator::MarkApply(op), Childrens::Twins { left, .. }) = + (&mut mark.operator, mark.childrens.as_mut()) + else { + return Ok(false); + }; + if op.guard.is_some() || matches!(op.kind, MarkApplyKind::InnerJoin) { + return Ok(false); + } + let marker = *op.output_column(); + // The projection must hide the marker: downstream consumers need its real value otherwise. + for expr in &project.exprs { + if expr + .any_referenced_column(arena, |arena, column| arena.same_column(*column, marker))? + { + return Ok(false); + } + } + let mut visitor = GuardVisitor { + marker, + guard: None, + }; + visitor.visit(filter_op.predicate, arena)?; + let Some((guard, negated)) = visitor.guard else { + return Ok(false); + }; + let schema = left.output_schema(arena); + if !guard.all_referenced_columns(arena, |arena, column| { + schema + .iter() + .any(|candidate| arena.same_column(*candidate, *column)) + })? { + return Ok(false); + } + // The guard has its own mutable expression references, but uses the same left positions. + let guard = guard.clone_expression(arena)?; + op.guard = Some(if negated { + arena.alloc_expression(ScalarExpression::Unary { + op: UnaryOperator::Not, + expr: guard, + evaluator: None, + ty: LogicalType::Boolean, + }) + } else { + guard + }); + Ok(true) + } +} + +struct GuardVisitor { + marker: ColumnRef, + guard: Option<(ExprRef, bool)>, +} + +impl ExprVisitor> for GuardVisitor { + fn visit_expression_ref(&mut self, _: ExprRef, _: &PlanArena) -> Result { + Ok(self.guard.is_none()) + } + + fn visit_expression( + &mut self, + expr: &ScalarExpression, + _: &PlanArena, + ) -> Result { + Ok(matches!( + expr, + ScalarExpression::Binary { + op: BinaryOperator::And | BinaryOperator::Or, + .. + } | ScalarExpression::Unary { + op: UnaryOperator::Not, + .. + } | ScalarExpression::TypeCast { .. } + | ScalarExpression::Alias { .. } + )) + } + + fn visit_alias( + &mut self, + expr: ExprRef, + alias: &AliasType, + arena: &PlanArena, + ) -> Result<(), DatabaseError> { + self.visit( + match alias { + AliasType::Expr(expr) => *expr, + AliasType::Name(_) => expr, + }, + arena, + ) + } + + fn visit_binary( + &mut self, + op: &BinaryOperator, + left: ExprRef, + right: ExprRef, + _: Option<&BinaryEvaluatorRef>, + _: &LogicalType, + arena: &PlanArena, + ) -> Result<(), DatabaseError> { + let left_marker = left.any_referenced_column(arena, |arena, column| { + arena.same_column(*column, self.marker) + })?; + let right_marker = right.any_referenced_column(arena, |arena, column| { + arena.same_column(*column, self.marker) + })?; + let (branch, sibling) = match (left_marker, right_marker) { + (true, false) => (left, right), + (false, true) => (right, left), + _ => return Ok(()), + }; + let mut visitor = SimpleGuardVisitor { + valid: true, + null_operand: false, + }; + visitor.visit(sibling, arena)?; + if visitor.valid { + self.guard = Some((sibling, matches!(op, BinaryOperator::Or))); + return Ok(()); + } + self.visit(branch, arena) + } +} + +struct SimpleGuardVisitor { + valid: bool, + null_operand: bool, +} + +impl ExprVisitor> for SimpleGuardVisitor { + fn visit_expression_ref( + &mut self, + expr: ExprRef, + arena: &PlanArena, + ) -> Result { + if !self.valid { + return Ok(false); + } + let unaliased = strip_alias(expr, arena); + if unaliased != expr { + self.visit(unaliased, arena)?; + return Ok(false); + } + Ok(true) + } + + fn visit_expression( + &mut self, + expr: &ScalarExpression, + arena: &PlanArena, + ) -> Result { + self.valid = match expr { + ScalarExpression::Constant(value) => { + self.null_operand || matches!(value, DataValue::Boolean(_)) + } + ScalarExpression::ColumnRef { column, .. } => { + self.null_operand || arena.column(*column).datatype() == &LogicalType::Boolean + } + ScalarExpression::IsNull { .. } + | ScalarExpression::Unary { + op: UnaryOperator::Not, + .. + } => !self.null_operand, + // Do not move arbitrary functions, casts or potentially failing calculations earlier. + _ => false, + }; + Ok(self.valid) + } + + fn visit_is_null( + &mut self, + _: bool, + expr: ExprRef, + arena: &PlanArena, + ) -> Result<(), DatabaseError> { + self.null_operand = true; + self.visit(expr, arena)?; + self.null_operand = false; + Ok(()) + } +} + +// GRCOV_EXCL_START +#[cfg(test)] +mod tests { + use super::*; + use crate::catalog::{ColumnCatalog, ColumnDesc}; + use crate::optimizer::rule::normalization::compilation_in_advance::EvaluatorBind; + use crate::planner::operator::filter::FilterOperator; + use crate::planner::operator::mark_apply::MarkApplyOperator; + use crate::planner::operator::project::ProjectOperator; + use crate::planner::operator::values::ValuesOperator; + use crate::planner::TableArenaCell; + use crate::types::evaluator::cast_create; + use crate::types::tuple::Tuple; + + struct Fixture { + plan: LogicalPlan, + flag: ExprRef, + marker: ExprRef, + } + + impl Fixture { + fn new( + arena: &mut PlanArena, + op: BinaryOperator, + marker_first: bool, + ) -> Result { + let flag_column = arena.alloc_column(ColumnCatalog::new( + "flag".to_string(), + true, + ColumnDesc::new(LogicalType::Boolean, None, false, None)?, + )); + let marker_column = arena.alloc_column(ColumnCatalog::new( + "marker".to_string(), + true, + ColumnDesc::new(LogicalType::Boolean, None, false, None)?, + )); + let value = arena.alloc_expression(DataValue::Boolean(true).into()); + let left = LogicalPlan::new( + Operator::Values(ValuesOperator::new(vec![value], 1, vec![flag_column])), + Childrens::None, + ); + let right = LogicalPlan::new( + Operator::Values(ValuesOperator::new(vec![value], 1, vec![flag_column])), + Childrens::None, + ); + let mark = MarkApplyOperator::build_exists(left, right, marker_column, vec![]); + let flag = arena.alloc_expression(ScalarExpression::column_expr(flag_column, 0)); + let marker = arena.alloc_expression(ScalarExpression::column_expr(marker_column, 1)); + let (left_expr, right_expr) = if marker_first { + (marker, flag) + } else { + (flag, marker) + }; + let predicate = arena.alloc_expression(ScalarExpression::Binary { + op, + left_expr, + right_expr, + evaluator: None, + ty: LogicalType::Boolean, + }); + let filter = FilterOperator::build(predicate, mark, false); + let plan = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: vec![flag] }), + Childrens::Only(Box::new(filter)), + ); + Ok(Self { plan, flag, marker }) + } + + fn mark(&mut self) -> &mut MarkApplyOperator { + let Childrens::Only(filter) = self.plan.childrens.as_mut() else { + unreachable!() + }; + let Childrens::Only(mark) = filter.childrens.as_mut() else { + unreachable!() + }; + let Operator::MarkApply(op) = &mut mark.operator else { + unreachable!() + }; + op + } + + fn predicate(&mut self) -> ExprRef { + let filter = self.plan.childrens.only(); + let Operator::Filter(op) = &filter.operator else { + unreachable!() + }; + op.predicate + } + } + + #[test] + fn and_or_guards_preserve_three_valued_results() -> Result<(), DatabaseError> { + for op in [BinaryOperator::And, BinaryOperator::Or] { + for marker_first in [false, true] { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut fixture = Fixture::new(&mut arena, op, marker_first)?; + assert!(GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + let guard = fixture.mark().guard.unwrap(); + assert_ne!(guard, fixture.flag); + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + EvaluatorBind.apply(&mut fixture.plan, &mut arena)?; + let predicate = fixture.predicate(); + for flag in [ + DataValue::Boolean(false), + DataValue::Boolean(true), + DataValue::Null, + ] { + let left = Tuple::new(None, vec![flag.clone()]); + let guard_value = arena + .expression(guard) + .eval(&arena, Some(&left))? + .into_owned(); + let expected = match (&op, &flag) { + (BinaryOperator::Or, DataValue::Boolean(value)) => { + DataValue::Boolean(!value) + } + _ => flag.clone(), + }; + assert_eq!(guard_value, expected); + for marker in [ + DataValue::Boolean(false), + DataValue::Boolean(true), + DataValue::Null, + ] { + let original = Tuple::new(None, vec![flag.clone(), marker.clone()]); + let guarded = Tuple::new( + None, + vec![ + flag.clone(), + if guard_value == DataValue::Boolean(false) { + DataValue::Null + } else { + marker + }, + ], + ); + assert_eq!( + arena.expression(predicate).eval(&arena, Some(&original))?, + arena.expression(predicate).eval(&arena, Some(&guarded))?, + ); + } + } + } + } + Ok(()) + } + + #[test] + fn marker_needed_by_projection_or_another_branch_is_not_guarded() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut fixture = Fixture::new(&mut arena, BinaryOperator::Or, false)?; + let Operator::Project(project) = &mut fixture.plan.operator else { + unreachable!() + }; + project.exprs.push(fixture.marker); + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(fixture.mark().guard.is_none()); + let Operator::Project(project) = &mut fixture.plan.operator else { + unreachable!() + }; + project.exprs.pop(); + let guarded_branch = fixture.predicate(); + let predicate = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Or, + left_expr: guarded_branch, + right_expr: fixture.marker, + evaluator: None, + ty: LogicalType::Boolean, + }); + let Childrens::Only(filter) = fixture.plan.childrens.as_mut() else { + unreachable!() + }; + let Operator::Filter(op) = &mut filter.operator else { + unreachable!() + }; + op.predicate = predicate; + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(fixture.mark().guard.is_none()); + Ok(()) + } + + #[test] + fn unsafe_sibling_is_not_evaluated_before_a_matching_marker() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut fixture = Fixture::new(&mut arena, BinaryOperator::Or, true)?; + let value = DataValue::from("not a boolean".to_string()); + let evaluator = cast_create(&value.logical_type(), &LogicalType::Boolean)?; + let expr = arena.alloc_expression(value.into()); + let failing = arena.alloc_expression(ScalarExpression::TypeCast { + expr, + ty: LogicalType::Boolean, + evaluator: Some(evaluator), + }); + let predicate = fixture.predicate(); + let ScalarExpression::Binary { right_expr, .. } = &mut *arena.expression_mut(predicate) + else { + unreachable!() + }; + *right_expr = failing; + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(fixture.mark().guard.is_none()); + EvaluatorBind.apply(&mut fixture.plan, &mut arena)?; + let matched = Tuple::new( + None, + vec![DataValue::Boolean(false), DataValue::Boolean(true)], + ); + assert_eq!( + *arena.expression(predicate).eval(&arena, Some(&matched))?, + DataValue::Boolean(true) + ); + let unmatched = Tuple::new( + None, + vec![DataValue::Boolean(false), DataValue::Boolean(false)], + ); + assert!(matches!( + arena.expression(predicate).eval(&arena, Some(&unmatched)), + Err(DatabaseError::ParseBool(_)) + )); + Ok(()) + } + + #[test] + fn null_checks_and_aliases_keep_the_guard_whitelist() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut fixture = Fixture::new(&mut arena, BinaryOperator::And, false)?; + let number_column = arena.alloc_column(ColumnCatalog::new( + "number".to_string(), + true, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, + )); + let number = arena.alloc_expression(ScalarExpression::column_expr(number_column, 0)); + let is_null = arena.alloc_expression(ScalarExpression::IsNull { + negated: false, + expr: number, + }); + let is_not_null = arena.alloc_expression(ScalarExpression::IsNull { + negated: true, + expr: number, + }); + let not_flag = arena.alloc_expression(ScalarExpression::Unary { + op: UnaryOperator::Not, + expr: fixture.flag, + evaluator: None, + ty: LogicalType::Boolean, + }); + let alias = arena.alloc_expression(ScalarExpression::Alias { + expr: number, + alias: AliasType::Expr(fixture.flag), + }); + let not_number = arena.alloc_expression(ScalarExpression::Unary { + op: UnaryOperator::Not, + expr: number, + evaluator: None, + ty: LogicalType::Integer, + }); + let nested = arena.alloc_expression(ScalarExpression::IsNull { + negated: false, + expr: not_number, + }); + let assert_case = |expr, expected| -> Result<(), DatabaseError> { + let mut visitor = SimpleGuardVisitor { + valid: true, + null_operand: false, + }; + visitor.visit(expr, &arena)?; + assert_eq!(visitor.valid, expected); + Ok(()) + }; + assert_case(fixture.flag, true)?; + assert_case(number, false)?; + assert_case(is_null, true)?; + assert_case(is_not_null, true)?; + assert_case(not_flag, true)?; + assert_case(alias, true)?; + assert_case(nested, false)?; + // Passing the whitelist does not waive the rule's left-input dependency check. + let predicate = fixture.predicate(); + let ScalarExpression::Binary { left_expr, .. } = &mut *arena.expression_mut(predicate) + else { + unreachable!() + }; + *left_expr = alias; + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(fixture.mark().guard.is_none()); + let alias = arena.alloc_expression(ScalarExpression::Alias { + expr: fixture.flag, + alias: AliasType::Name("flag_alias".to_string()), + }); + let ScalarExpression::Binary { left_expr, .. } = &mut *arena.expression_mut(predicate) + else { + unreachable!() + }; + *left_expr = alias; + assert!(GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + Ok(()) + } +} +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index e5a09dc9..8f7928a5 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -40,9 +40,11 @@ mod column_pruning; mod combine_operators; mod compilation_in_advance; mod elimination; +mod guard_mark_apply; mod join_reorder; mod materialize_mark_apply; mod min_max_top_k; +use guard_mark_apply::GuardMarkApply; use join_reorder::GreedyJoinReorder; use materialize_mark_apply::MaterializeMarkApply; mod parameterized_index; @@ -86,6 +88,7 @@ pub enum NormalizationRuleImpl { EliminateIndexFilter, GreedyJoinReorder, MaterializeMarkApply, + GuardMarkApply, } #[derive(Debug, Copy, Clone, Eq, PartialEq)] @@ -175,7 +178,9 @@ impl NormalizationRuleImpl { pub fn root_tag(&self) -> NormalizationRuleRootTag { match self { NormalizationRuleImpl::ColumnPruning => NormalizationRuleRootTag::Any, - NormalizationRuleImpl::CollapseProject => NormalizationRuleRootTag::Project, + NormalizationRuleImpl::CollapseProject | NormalizationRuleImpl::GuardMarkApply => { + NormalizationRuleRootTag::Project + } NormalizationRuleImpl::CollapseGroupByAgg => NormalizationRuleRootTag::Aggregate, NormalizationRuleImpl::CombineFilter => NormalizationRuleRootTag::Filter, NormalizationRuleImpl::LimitProjectTranspose @@ -207,6 +212,7 @@ impl NormalizationRule for NormalizationRuleImpl { match self { NormalizationRuleImpl::ColumnPruning => ColumnPruning.apply(plan, arena), NormalizationRuleImpl::GreedyJoinReorder => GreedyJoinReorder.apply(plan, arena), + NormalizationRuleImpl::GuardMarkApply => GuardMarkApply.apply(plan, arena), NormalizationRuleImpl::CollapseProject => CollapseProject.apply(plan, arena), NormalizationRuleImpl::CollapseGroupByAgg => CollapseGroupByAgg.apply(plan, arena), NormalizationRuleImpl::CombineFilter => CombineFilter.apply(plan, arena), diff --git a/src/planner/operator/mark_apply.rs b/src/planner/operator/mark_apply.rs index b811550d..35217fb3 100644 --- a/src/planner/operator/mark_apply.rs +++ b/src/planner/operator/mark_apply.rs @@ -39,6 +39,7 @@ pub struct MarkApplyOperator { output_column: Option, pub parameterized_probe: Option, pub(crate) materialize: bool, + pub(crate) guard: Option, } impl MarkApplyOperator { @@ -49,6 +50,7 @@ impl MarkApplyOperator { output_column: Some(output_column), parameterized_probe: None, materialize: false, + guard: None, } } @@ -82,6 +84,7 @@ impl MarkApplyOperator { output_column: Some(output_column), parameterized_probe: None, materialize: false, + guard: None, } } @@ -141,6 +144,7 @@ impl MarkApplyOperator { output_column: None, parameterized_probe: Some(probe), materialize: false, + guard: None, } } diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 20ea6359..f205a748 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -400,6 +400,9 @@ impl Operator { if let Some(expr) = &op.parameterized_probe { ExprVisitor::visit(self, *expr, self.arena)?; } + if let Some(expr) = op.guard { + ExprVisitor::visit(self, expr, self.arena)?; + } Ok(()) } diff --git a/src/planner/operator/visitor.rs b/src/planner/operator/visitor.rs index 668f6768..f949c6e3 100644 --- a/src/planner/operator/visitor.rs +++ b/src/planner/operator/visitor.rs @@ -240,6 +240,9 @@ impl<'a, V: ExprVisitor, A: MetaArena + ?Sized> OperatorVisitor<'a> if let Some(expr) = &op.parameterized_probe { ExprVisitor::visit(self.visitor, *expr, self.arena)?; } + if let Some(expr) = op.guard { + ExprVisitor::visit(self.visitor, expr, self.arena)?; + } Ok(()) } diff --git a/src/planner/operator/visitor_mut.rs b/src/planner/operator/visitor_mut.rs index 7970f86b..12235428 100644 --- a/src/planner/operator/visitor_mut.rs +++ b/src/planner/operator/visitor_mut.rs @@ -294,6 +294,9 @@ impl<'a, V: ExprVisitorMut> OperatorVisitorMut<'a> for OperatorExprVisitorMut<'_ if let Some(expr) = &mut op.parameterized_probe { ExprVisitorMut::visit(self.visitor, expr, self.arena)?; } + if let Some(expr) = &mut op.guard { + ExprVisitorMut::visit(self.visitor, expr, self.arena)?; + } Ok(()) } From 41546f23cbc110c39f2fe32029fbfe90c11b99bf Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:38 +0800 Subject: [PATCH 07/62] fix: preserve composite unique bounds and continue missing index ranges --- .../rule/normalization/pushdown_predicates.rs | 110 ++++++++++++++++-- src/storage/mod.rs | 13 ++- tests/slt/create_index.slt | 104 +++++++++++++++++ 3 files changed, 212 insertions(+), 15 deletions(-) diff --git a/src/optimizer/rule/normalization/pushdown_predicates.rs b/src/optimizer/rule/normalization/pushdown_predicates.rs index 6bdef695..eda006b6 100644 --- a/src/optimizer/rule/normalization/pushdown_predicates.rs +++ b/src/optimizer/rule/normalization/pushdown_predicates.rs @@ -23,7 +23,7 @@ use crate::planner::operator::join::extract_join_keys; use crate::planner::operator::join::{JoinCondition, JoinType}; use crate::planner::operator::{Operator, SortOption}; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; -use crate::types::index::{IndexInfo, IndexLookup, IndexMetaRef, IndexType}; +use crate::types::index::{IndexInfo, IndexLookup, IndexMetaRef}; use crate::types::value::DataValue; use crate::types::LogicalType; use std::ops::Bound; @@ -338,16 +338,11 @@ impl NormalizationRule for PushPredicateIntoScan { else { return Err(DatabaseError::InvalidIndex); }; - let index_type = arena.index(*meta).ty; - let detached = match index_type { - IndexType::PrimaryKey { is_multiple: false } - | IndexType::Unique - | IndexType::Normal => { - RangeDetacher::for_index(*meta, 0, arena).detach(filter_op.predicate)? - } - IndexType::PrimaryKey { is_multiple: true } | IndexType::Composite => { - Self::composite_range(filter_op, *meta, ignore_prefix_len, arena)? - } + // Uniqueness does not determine key width: UNIQUE indexes can be composite too. + let detached = if arena.index(*meta).column_ids.len() > 1 { + Self::composite_range(filter_op, *meta, ignore_prefix_len, arena)? + } else { + RangeDetacher::for_index(*meta, 0, arena).detach(filter_op.predicate)? }; let Some(detached) = detached else { *lookup = None; @@ -690,6 +685,99 @@ mod tests { Ok(()) } + #[test] + fn test_unique_composite_index_uses_tuple_ranges() -> Result<(), DatabaseError> { + let table_state = build_t1_table()?; + let mut arena = PlanArena::new(&table_state.table_arena); + let table_name: TableName = "mock_table".to_string().into(); + let c1 = build_test_column(&mut arena, &table_name, 1, "c1")?; + let c2 = build_test_column(&mut arena, &table_name, 2, "c2")?; + let meta = arena.alloc_index(IndexMeta { + id: 0, + column_ids: vec![1, 2], + table_name: table_name.clone(), + pk_ty: LogicalType::Integer, + value_ty: LogicalType::Tuple(vec![LogicalType::Integer, LogicalType::Integer]), + name: "unique_c1_c2".to_string(), + ty: IndexType::Unique, + }); + let first_eq = cmp_predicate(&mut arena, BinaryOperator::Eq, c1, 0, 2); + let second_eq = cmp_predicate(&mut arena, BinaryOperator::Eq, c2, 1, 3); + let full_eq = and_predicate(&mut arena, first_eq, second_eq); + let first_range = cmp_predicate(&mut arena, BinaryOperator::GtEq, c1, 0, 2); + for (predicate, expected, prefix_len) in [ + ( + first_range, + Range::Scope { + min: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(2)])), + max: Bound::Unbounded, + }, + 0, + ), + ( + first_eq, + Range::Scope { + min: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(2)])), + max: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(2)])), + }, + 1, + ), + ( + full_eq, + Range::Eq(DataValue::Tuple(vec![ + DataValue::Int32(2), + DataValue::Int32(3), + ])), + 2, + ), + ] { + let scan = LogicalPlan::new( + Operator::TableScan(TableScanOperator { + table_name: table_name.clone(), + columns: vec![c1, c2], + limit: (None, None), + index_infos: vec![IndexInfo { + meta, + sort_option: SortOption::OrderBy { + fields: vec![], + ignore_prefix_len: 0, + }, + lookup: None, + residual_predicate: None, + covered_deserializers: None, + cover_mapping: None, + sort_elimination_hint: None, + stream_aggregate_hint: None, + }], + with_pk: false, + }), + Childrens::None, + ); + let plan = apply_pipeline( + FilterOperator::build(predicate, scan, false), + HepOptimizerPipeline::builder().before_batch( + "unique_composite_range".to_string(), + HepBatchStrategy::fix_point_topdown(1), + vec![NormalizationRuleImpl::PushPredicateIntoScan], + ), + &mut arena, + )?; + let scan = plan.childrens.pop_only(); + let Operator::TableScan(op) = scan.operator else { + unreachable!() + }; + assert_eq!( + op.index_infos[0].lookup, + Some(IndexLookup::Static(expected)) + ); + assert_eq!(op.index_infos[0].residual_predicate, None); + assert!(matches!(op.index_infos[0].sort_option, + SortOption::OrderBy { ignore_prefix_len, .. } if ignore_prefix_len == prefix_len)); + assert_eq!(arena.index(meta).ty, IndexType::Unique); + } + Ok(()) + } + #[test] fn test_composite_range_consumes_prefix_equalities_continuously() -> Result<(), DatabaseError> { let table_name: TableName = ::std::sync::Arc::from("mock_table"); diff --git a/src/storage/mod.rs b/src/storage/mod.rs index bcc537cc..8e65c650 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1694,7 +1694,7 @@ impl IndexImpl for UniqueIndexImpl { table_codec: &mut TableCodec, params: &IndexImplParams, value: &DataValue, - _: bool, + is_upper: bool, out: &mut Bytes, ) -> Result<(), DatabaseError> { let index = Index::new( @@ -1704,8 +1704,13 @@ impl IndexImpl for UniqueIndexImpl { ); table_codec.with_index(params.table_name.as_ref(), &index, None, |key, _| { - out.clear(); - out.extend_from_slice(key); + if index.values.len() < params.index_meta().column_ids.len() { + // A partial UNIQUE key bounds a prefix, not one exact index entry. + encode_bound_key(out, key, is_upper); + } else { + out.clear(); + out.extend_from_slice(key); + } Ok(()) }) } @@ -2147,7 +2152,7 @@ impl Iter for IndexIter<'_, T> { self.bounds.consume_limit(); return Ok(true); } - IndexResult::Miss => return Ok(false), + IndexResult::Miss => continue, IndexResult::Scope(iter) => { self.state = IndexIterState::Range(iter); } diff --git a/tests/slt/create_index.slt b/tests/slt/create_index.slt index 46667a43..b1febc51 100644 --- a/tests/slt/create_index.slt +++ b/tests/slt/create_index.slt @@ -85,6 +85,110 @@ select * from t; ---- 0 0 0 0 +# Multi-column UNIQUE keys must use tuple ranges after ANALYZE, including +# first-column ranges and partial/full equality prefixes. +statement ok +insert into t select cast(number+1 as int), cast(number+1 as int), +cast((number+1)%5 as int), cast(number+1 as int) from table(numbers(100)); + +statement ok +analyze table t; + +query I +select v3 from t where v2=2 and v3<=7 order by v3; +---- +2 +7 + +query I +select v3 from t where v2>=4 and v3<=14 order by v3; +---- +4 +9 +14 + +query I +select v3 from t where v2=2 order by v3 limit 3; +---- +2 +7 +12 + +query I +select v3 from t where v2=2 and v3=7; +---- +7 + +# Fetching a non-indexed column must use the same prefix bounds as covered reads. +query I +select id from t where v2=2 and v3<=7 order by id; +---- +2 +7 + +query I +select id from t where v2>=4 and v3<=14 order by id; +---- +4 +9 +14 + +query I +select id from t where v2>2 and v3<=4 order by id; +---- +3 +4 + +query I +select id from t where v2<=2 and v3<=7 order by id; +---- +0 +1 +2 +5 +6 +7 + +query I +select id from t where v2 in (2,4) and v3<=7 order by id; +---- +2 +4 +7 + +query I +select id from t where v2=2 and v3=7; +---- +7 + +# A missing equality key must not terminate the remaining index ranges. +query I +select id from t where id in (-1,2,7,101) order by id; +---- +2 +7 + +query I +select id from t where id in (-1,2,7,101) order by id limit 1 offset 1; +---- +7 + +query I +select id from t where id=-1 or id>=99 order by id; +---- +99 +100 + +query I +select id from t where v2=2 and v3 in (-1,2,3,7) order by id; +---- +2 +7 + +query I +select id from t where v2=2 and v3 in (3,4) order by id; +---- + statement ok drop index t.index_1 From 46539c20362aa0c81b3db70f8cfb9b353ac7e71d Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:38 +0800 Subject: [PATCH 08/62] perf: share safe guards across mark apply segments --- .../rule/normalization/guard_mark_apply.rs | 420 ++++++++++++++++-- .../rule/normalization/parameterized_index.rs | 205 +++++++-- tests/slt/subquery.slt | 78 ++++ 3 files changed, 618 insertions(+), 85 deletions(-) diff --git a/src/optimizer/rule/normalization/guard_mark_apply.rs b/src/optimizer/rule/normalization/guard_mark_apply.rs index 36171460..d00484a7 100644 --- a/src/optimizer/rule/normalization/guard_mark_apply.rs +++ b/src/optimizer/rule/normalization/guard_mark_apply.rs @@ -16,7 +16,7 @@ use super::strip_alias; use crate::catalog::ColumnRef; use crate::errors::DatabaseError; use crate::expression::visitor::ExprVisitor; -use crate::expression::{AliasType, BinaryOperator, ScalarExpression, UnaryOperator}; +use crate::expression::{AliasType, BinaryOperator, ScalarExpression, TypeCast, UnaryOperator}; use crate::optimizer::core::rule::NormalizationRule; use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::Operator; @@ -39,61 +39,83 @@ impl NormalizationRule for GuardMarkApply { else { return Ok(false); }; - let (Operator::MarkApply(op), Childrens::Twins { left, .. }) = - (&mut mark.operator, mark.childrens.as_mut()) - else { - return Ok(false); - }; - if op.guard.is_some() || matches!(op.kind, MarkApplyKind::InnerJoin) { + let mut markers = Vec::new(); + let mut input = mark.as_ref(); + while let (Operator::MarkApply(op), Childrens::Twins { left, .. }) = + (&input.operator, input.childrens.as_ref()) + { + if matches!(op.kind, MarkApplyKind::InnerJoin) { + break; + } + if op.guard.is_some() { + return Ok(false); + } + markers.push(*op.output_column()); + input = left; + } + if markers.is_empty() { return Ok(false); } - let marker = *op.output_column(); - // The projection must hide the marker: downstream consumers need its real value otherwise. + // No marker in this segment may escape through the projection. for expr in &project.exprs { - if expr - .any_referenced_column(arena, |arena, column| arena.same_column(*column, marker))? - { + if expr.any_referenced_column(arena, |arena, column| { + markers + .iter() + .any(|marker| arena.same_column(*column, *marker)) + })? { return Ok(false); } } let mut visitor = GuardVisitor { - marker, + markers: &markers, guard: None, }; visitor.visit(filter_op.predicate, arena)?; let Some((guard, negated)) = visitor.guard else { return Ok(false); }; - let schema = left.output_schema(arena); + let schema = mark.output_schema(arena); if !guard.all_referenced_columns(arena, |arena, column| { - schema + !markers .iter() - .any(|candidate| arena.same_column(*candidate, *column)) + .any(|marker| arena.same_column(*marker, *column)) + && schema + .iter() + .any(|candidate| arena.same_column(*candidate, *column)) })? { return Ok(false); } - // The guard has its own mutable expression references, but uses the same left positions. - let guard = guard.clone_expression(arena)?; - op.guard = Some(if negated { - arena.alloc_expression(ScalarExpression::Unary { - op: UnaryOperator::Not, - expr: guard, - evaluator: None, - ty: LogicalType::Boolean, - }) - } else { - guard - }); + let mut input = mark.as_mut(); + for _ in &markers { + let (Operator::MarkApply(op), Childrens::Twins { left, .. }) = + (&mut input.operator, input.childrens.as_mut()) + else { + unreachable!() + }; + // Share the logical predicate; isolate mutable expression positions. + let guard = guard.clone_expression(arena)?; + op.guard = Some(if negated { + arena.alloc_expression(ScalarExpression::Unary { + op: UnaryOperator::Not, + expr: guard, + evaluator: None, + ty: LogicalType::Boolean, + }) + } else { + guard + }); + input = left; + } Ok(true) } } -struct GuardVisitor { - marker: ColumnRef, +struct GuardVisitor<'a> { + markers: &'a [ColumnRef], guard: Option<(ExprRef, bool)>, } -impl ExprVisitor> for GuardVisitor { +impl ExprVisitor> for GuardVisitor<'_> { fn visit_expression_ref(&mut self, _: ExprRef, _: &PlanArena) -> Result { Ok(self.guard.is_none()) } @@ -141,10 +163,14 @@ impl ExprVisitor> for GuardVisitor { arena: &PlanArena, ) -> Result<(), DatabaseError> { let left_marker = left.any_referenced_column(arena, |arena, column| { - arena.same_column(*column, self.marker) + self.markers + .iter() + .any(|marker| arena.same_column(*column, *marker)) })?; let right_marker = right.any_referenced_column(arena, |arena, column| { - arena.same_column(*column, self.marker) + self.markers + .iter() + .any(|marker| arena.same_column(*column, *marker)) })?; let (branch, sibling) = match (left_marker, right_marker) { (true, false) => (left, right), @@ -153,7 +179,7 @@ impl ExprVisitor> for GuardVisitor { }; let mut visitor = SimpleGuardVisitor { valid: true, - null_operand: false, + context: GuardContext::Predicate, }; visitor.visit(sibling, arena)?; if visitor.valid { @@ -164,9 +190,16 @@ impl ExprVisitor> for GuardVisitor { } } +#[derive(Clone, Copy)] +enum GuardContext { + Predicate, + NullOperand, + NumericOperand, +} + struct SimpleGuardVisitor { valid: bool, - null_operand: bool, + context: GuardContext, } impl ExprVisitor> for SimpleGuardVisitor { @@ -191,19 +224,48 @@ impl ExprVisitor> for SimpleGuardVisitor { expr: &ScalarExpression, arena: &PlanArena, ) -> Result { - self.valid = match expr { - ScalarExpression::Constant(value) => { - self.null_operand || matches!(value, DataValue::Boolean(_)) + self.valid = match (self.context, expr) { + ( + GuardContext::NullOperand, + ScalarExpression::Constant(_) | ScalarExpression::ColumnRef { .. }, + ) => true, + (GuardContext::Predicate, ScalarExpression::Constant(DataValue::Boolean(_))) => true, + (GuardContext::Predicate, ScalarExpression::ColumnRef { column, .. }) => { + arena.column(*column).datatype() == &LogicalType::Boolean } - ScalarExpression::ColumnRef { column, .. } => { - self.null_operand || arena.column(*column).datatype() == &LogicalType::Boolean + ( + GuardContext::Predicate, + ScalarExpression::IsNull { .. } + | ScalarExpression::Unary { + op: UnaryOperator::Not, + .. + } + | ScalarExpression::Binary { + op: + BinaryOperator::Eq + | BinaryOperator::NotEq + | BinaryOperator::Gt + | BinaryOperator::GtEq + | BinaryOperator::Lt + | BinaryOperator::LtEq, + .. + }, + ) => true, + (GuardContext::NumericOperand, ScalarExpression::Constant(value)) => { + value.is_null() || value.logical_type().is_numeric() } - ScalarExpression::IsNull { .. } - | ScalarExpression::Unary { - op: UnaryOperator::Not, - .. - } => !self.null_operand, - // Do not move arbitrary functions, casts or potentially failing calculations earlier. + (GuardContext::NumericOperand, ScalarExpression::ColumnRef { column, .. }) => { + arena.column(*column).datatype().is_numeric() + } + (GuardContext::NumericOperand, ScalarExpression::TypeCast { expr, ty, .. }) => { + let from = expr.return_type(arena); + // Implicit numeric widening cannot fail; bounded Decimal and narrowing casts can. + (from.is_numeric() || *from == LogicalType::SqlNull) + && ty.is_numeric() + && !matches!(ty, LogicalType::Decimal(..)) + && LogicalType::can_implicit_cast(&from, ty) + } + // Do not move arbitrary functions or potentially failing calculations earlier. _ => false, }; Ok(self.valid) @@ -215,9 +277,27 @@ impl ExprVisitor> for SimpleGuardVisitor { expr: ExprRef, arena: &PlanArena, ) -> Result<(), DatabaseError> { - self.null_operand = true; + let previous = self.context; + self.context = GuardContext::NullOperand; self.visit(expr, arena)?; - self.null_operand = false; + self.context = previous; + Ok(()) + } + + fn visit_binary( + &mut self, + _: &BinaryOperator, + left: ExprRef, + right: ExprRef, + _: Option<&BinaryEvaluatorRef>, + _: &LogicalType, + arena: &PlanArena, + ) -> Result<(), DatabaseError> { + let previous = self.context; + self.context = GuardContext::NumericOperand; + self.visit(left, arena)?; + self.visit(right, arena)?; + self.context = previous; Ok(()) } } @@ -290,6 +370,55 @@ mod tests { Ok(Self { plan, flag, marker }) } + fn add_mark( + &mut self, + arena: &mut PlanArena, + op: BinaryOperator, + ) -> Result { + let column = arena.alloc_column(ColumnCatalog::new( + "upper_marker".into(), + true, + ColumnDesc::new(LogicalType::Boolean, None, false, None)?, + )); + let upper = arena.alloc_expression(ScalarExpression::column_expr(column, 2)); + let value = arena.alloc_expression(DataValue::Boolean(true).into()); + let right = LogicalPlan::new( + Operator::Values(ValuesOperator::new(vec![value], 1, vec![column])), + Childrens::None, + ); + let Childrens::Only(filter) = self.plan.childrens.as_mut() else { + unreachable!() + }; + let Childrens::Only(mark) = filter.childrens.as_mut() else { + unreachable!() + }; + let lower = mark.take(); + **mark = MarkApplyOperator::build_exists(lower, right, column, vec![self.marker]); + let markers = arena.alloc_expression(ScalarExpression::Binary { + op: if matches!(op, BinaryOperator::And) { + BinaryOperator::Or + } else { + BinaryOperator::And + }, + left_expr: self.marker, + right_expr: upper, + evaluator: None, + ty: LogicalType::Boolean, + }); + let predicate = arena.alloc_expression(ScalarExpression::Binary { + op, + left_expr: self.flag, + right_expr: markers, + evaluator: None, + ty: LogicalType::Boolean, + }); + let Operator::Filter(filter) = &mut filter.operator else { + unreachable!() + }; + filter.predicate = predicate; + Ok(upper) + } + fn mark(&mut self) -> &mut MarkApplyOperator { let Childrens::Only(filter) = self.plan.childrens.as_mut() else { unreachable!() @@ -312,6 +441,139 @@ mod tests { } } + #[test] + fn shared_segment_guard_preserves_all_marker_combinations() -> Result<(), DatabaseError> { + for op in [BinaryOperator::And, BinaryOperator::Or] { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut fixture = Fixture::new(&mut arena, op, false)?; + fixture.add_mark(&mut arena, op)?; + assert!(GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + let upper = fixture.mark().guard.unwrap(); + let Childrens::Only(filter) = fixture.plan.childrens.as_mut() else { + unreachable!() + }; + let Childrens::Only(mark) = filter.childrens.as_mut() else { + unreachable!() + }; + let Childrens::Twins { left, .. } = mark.childrens.as_mut() else { + unreachable!() + }; + let Operator::MarkApply(lower) = &left.operator else { + unreachable!() + }; + let lower = lower.guard.unwrap(); + assert_ne!(upper, lower); + let predicate = fixture.predicate(); + EvaluatorBind.apply(&mut fixture.plan, &mut arena)?; + for flag in [ + DataValue::Boolean(false), + DataValue::Boolean(true), + DataValue::Null, + ] { + let input = Tuple::new(None, vec![flag.clone()]); + let guard = arena + .expression(upper) + .eval(&arena, Some(&input))? + .into_owned(); + assert_eq!( + guard, + arena + .expression(lower) + .eval(&arena, Some(&input))? + .into_owned() + ); + for a in [ + DataValue::Boolean(false), + DataValue::Boolean(true), + DataValue::Null, + ] { + for b in [ + DataValue::Boolean(false), + DataValue::Boolean(true), + DataValue::Null, + ] { + let original = Tuple::new(None, vec![flag.clone(), a.clone(), b.clone()]); + let guarded = if guard == DataValue::Boolean(false) { + Tuple::new(None, vec![flag.clone(), DataValue::Null, DataValue::Null]) + } else { + original.clone() + }; + assert_eq!( + arena + .expression(predicate) + .eval(&arena, Some(&original))? + .into_owned(), + arena + .expression(predicate) + .eval(&arena, Some(&guarded))? + .into_owned() + ); + } + } + } + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + } + Ok(()) + } + + #[test] + fn segment_requires_common_protection_and_hidden_markers() -> Result<(), DatabaseError> { + for escaped in [0, 1, 2] { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut fixture = Fixture::new(&mut arena, BinaryOperator::And, false)?; + let upper = fixture.add_mark(&mut arena, BinaryOperator::And)?; + if escaped == 0 { + // (P AND lower) OR upper cannot skip both marks when P=false. + let protected = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::And, + left_expr: fixture.flag, + right_expr: fixture.marker, + evaluator: None, + ty: LogicalType::Boolean, + }); + let predicate = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Or, + left_expr: protected, + right_expr: upper, + evaluator: None, + ty: LogicalType::Boolean, + }); + let Childrens::Only(filter) = fixture.plan.childrens.as_mut() else { + unreachable!() + }; + let Operator::Filter(filter) = &mut filter.operator else { + unreachable!() + }; + filter.predicate = predicate; + } else { + let Operator::Project(project) = &mut fixture.plan.operator else { + unreachable!() + }; + project + .exprs + .push(if escaped == 1 { fixture.marker } else { upper }); + } + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(fixture.mark().guard.is_none()); + let Childrens::Only(filter) = fixture.plan.childrens.as_mut() else { + unreachable!() + }; + let Childrens::Only(mark) = filter.childrens.as_mut() else { + unreachable!() + }; + let Childrens::Twins { left, .. } = mark.childrens.as_mut() else { + unreachable!() + }; + let Operator::MarkApply(lower) = &left.operator else { + unreachable!() + }; + assert!(lower.guard.is_none()); + } + Ok(()) + } + #[test] fn and_or_guards_preserve_three_valued_results() -> Result<(), DatabaseError> { for op in [BinaryOperator::And, BinaryOperator::Or] { @@ -446,6 +708,66 @@ mod tests { Ok(()) } + #[test] + fn numeric_guards_accept_only_safe_operands() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let number = arena.alloc_column(ColumnCatalog::new( + "number".into(), + true, + ColumnDesc::new(LogicalType::Bigint, None, false, None)?, + )); + let number = arena.alloc_expression(ScalarExpression::column_expr(number, 0)); + let zero = arena.alloc_expression(DataValue::Int64(0).into()); + let widening = arena.alloc_expression(ScalarExpression::TypeCast { + expr: number, + ty: LogicalType::Double, + evaluator: None, + }); + let narrowing = arena.alloc_expression(ScalarExpression::TypeCast { + expr: number, + ty: LogicalType::Integer, + evaluator: None, + }); + let arithmetic = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Divide, + left_expr: number, + right_expr: zero, + evaluator: None, + ty: LogicalType::Bigint, + }); + for (operand, valid) in [ + (number, true), + (widening, true), + (narrowing, false), + (arithmetic, false), + ] { + for op in [ + BinaryOperator::Eq, + BinaryOperator::NotEq, + BinaryOperator::Gt, + BinaryOperator::GtEq, + BinaryOperator::Lt, + BinaryOperator::LtEq, + ] { + let predicate = arena.alloc_expression(ScalarExpression::Binary { + op, + left_expr: operand, + right_expr: zero, + evaluator: None, + ty: LogicalType::Boolean, + }); + let mut visitor = SimpleGuardVisitor { + valid: true, + context: GuardContext::Predicate, + }; + visitor.visit(predicate, &arena)?; + assert_eq!(visitor.valid, valid); + } + } + Ok(()) + } + #[test] fn null_checks_and_aliases_keep_the_guard_whitelist() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); @@ -488,7 +810,7 @@ mod tests { let assert_case = |expr, expected| -> Result<(), DatabaseError> { let mut visitor = SimpleGuardVisitor { valid: true, - null_operand: false, + context: GuardContext::Predicate, }; visitor.visit(expr, &arena)?; assert_eq!(visitor.valid, expected); diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 296a1ec7..cf155b8f 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -159,45 +159,47 @@ fn extract_parameterized_probe_side( fn parameterize_right_subtree( plan: &mut LogicalPlan, right_column: &ColumnRef, - arena: &dyn MetaArena, + arena: &mut PlanArena<'_>, ) -> bool { - if matches!(plan.operator, Operator::TableScan(_)) { - let index_info = { - let Operator::TableScan(scan_op) = &mut plan.operator else { - unreachable!(); + match (&mut plan.operator, plan.childrens.as_mut()) { + (Operator::Project(project), Childrens::Only(child)) => { + let Some(mut expr) = project.exprs.iter().copied().find(|expr| { + let column = expr.output_column_ref(arena); + arena.same_column(column, *right_column) + }) else { + return false; + }; + // Follow the source, not the projection's output relation/name. + // Computed and cast keys are not index probes. + while let ScalarExpression::Alias { expr: source, .. } = arena.expression(expr) { + expr = *source; + } + let ScalarExpression::ColumnRef { column, .. } = *arena.expression(expr) else { + return false; }; - let Some(target_index) = - pick_parameterized_index_position(scan_op, right_column, arena) + parameterize_right_subtree(child, &column, arena) + } + (Operator::TableScan(scan), _) => { + if scan.limit != (None, None) { + return false; + } + let Some(position) = pick_parameterized_index_position(scan, right_column, arena) else { return false; }; - scan_op.index_infos[target_index].lookup = Some(IndexLookup::Probe); - scan_op.index_infos[target_index].clone() - }; - let sort_option = index_info.sort_option.clone(); - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::IndexScan(Box::new(index_info)), - sort_option, - RowCountOption::Unknown, - )); - return true; - } - - let passthrough = matches!( - plan.operator, - Operator::Filter(_) - | Operator::Project(_) - | Operator::Limit(_) - | Operator::Sort(_) - | Operator::TopK(_) - ); - - if !passthrough { - return false; - } - - match plan.childrens.as_mut() { - Childrens::Only(child) => parameterize_right_subtree(child, right_column, arena), + scan.index_infos[position].lookup = Some(IndexLookup::Probe); + let index_info = scan.index_infos[position].clone(); + let sort_option = index_info.sort_option.clone(); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::IndexScan(Box::new(index_info)), + sort_option, + RowCountOption::Unknown, + )); + true + } + (Operator::Filter(_) | Operator::Sort(_), Childrens::Only(child)) => { + parameterize_right_subtree(child, right_column, arena) + } _ => false, } } @@ -222,6 +224,7 @@ fn pick_parameterized_index_position( .filter(|(_, index_info)| { let index_meta = arena.index(index_info.meta); index_meta.table_name == *table_name + && index_meta.column_ids.len() == 1 && index_meta.column_ids.first().copied() == Some(column_id) }) .min_by_key(|(_, index_info)| index_priority(arena.index(index_info.meta).ty)) @@ -557,7 +560,7 @@ mod tests { let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::None); assert!(!ParameterizeMarkApply.apply(&mut plan, &mut arena)?); - assert!(!parameterize_right_subtree(&mut plan, &column, &arena)); + assert!(!parameterize_right_subtree(&mut plan, &column, &mut arena)); let mut filter = LogicalPlan::new( Operator::Filter(FilterOperator { @@ -567,7 +570,11 @@ mod tests { }), Childrens::None, ); - assert!(!parameterize_right_subtree(&mut filter, &column, &arena)); + assert!(!parameterize_right_subtree( + &mut filter, + &column, + &mut arena + )); assert_eq!( index_priority(IndexType::PrimaryKey { is_multiple: false }), @@ -588,7 +595,9 @@ mod inner_join_tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc}; use crate::expression::range_detacher::Range; + use crate::expression::{AliasType, UnaryOperator}; use crate::planner::operator::join::JoinOperator; + use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::TableArenaCell; @@ -703,6 +712,130 @@ mod inner_join_tests { ) } + #[test] + fn scalar_probe_rejects_composite_unique_keys() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut right = scan( + &mut arena, + "right", + &[LogicalType::Integer, LogicalType::Integer], + &[&[0, 1], &[0]], + ); + let (column, single) = { + let Operator::TableScan(scan) = &mut right.operator else { + unreachable!() + }; + let mut composite = arena.index(scan.index_infos[0].meta).clone(); + composite.ty = IndexType::Unique; + scan.index_infos[0].meta = arena.alloc_index(composite); + (scan.columns[0], scan.index_infos.pop().unwrap()) + }; + assert!(!parameterize_right_subtree(&mut right, &column, &mut arena)); + assert!(right.physical_option.is_none()); + let Operator::TableScan(scan) = &mut right.operator else { + unreachable!() + }; + assert!(scan.index_infos[0].lookup.is_none()); + scan.index_infos.push(single); + // Prefer a usable scalar index even though UNIQUE normally ranks higher. + assert_eq!(pick_parameterized_index_position(scan, &column, &arena), Some(1)); + assert!(parameterize_right_subtree(&mut right, &column, &mut arena)); + let PlanImpl::IndexScan(index) = &right.physical_option.as_ref().unwrap().plan else { + unreachable!() + }; + assert_eq!(arena.index(index.meta).column_ids, vec![0]); + assert!(matches!(index.lookup, Some(IndexLookup::Probe))); + Ok(()) + } + + #[test] + fn projected_probe_resolves_output_alias_but_rejects_computed_keys_and_limits( + ) -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let base = scan(&mut arena, "right", &[LogicalType::Integer], &[&[0]]); + let source = column(&base, 0, &mut arena); + let output = arena.alloc_column(ColumnCatalog::new( + "renamed".to_string(), + true, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, + )); + let alias_column = arena.alloc_expression(ScalarExpression::column_expr(output, 0)); + let alias = arena.alloc_expression(ScalarExpression::Alias { + expr: source, + alias: AliasType::Expr(alias_column), + }); + let project = |input| { + LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: vec![alias] }), + Childrens::Only(Box::new(input)), + ) + }; + let mut projected = project(base.clone()); + assert!(parameterize_right_subtree( + &mut projected, + &output, + &mut arena + )); + let input = projected.childrens.only(); + let Operator::TableScan(input) = &input.operator else { + unreachable!() + }; + assert_eq!(input.index_infos[0].lookup, Some(IndexLookup::Probe)); + assert!(matches!( + projected + .childrens + .only() + .physical_option + .as_ref() + .unwrap() + .plan, + PlanImpl::IndexScan(_) + )); + + let mut limited = project(LimitOperator::build(Some(1), Some(1), base.clone())); + assert!(!parameterize_right_subtree( + &mut limited, + &output, + &mut arena + )); + let mut pushed = base.clone(); + let Operator::TableScan(op) = &mut pushed.operator else { + unreachable!() + }; + op.limit = (None, Some(1)); + let mut pushed = project(pushed); + assert!(!parameterize_right_subtree( + &mut pushed, + &output, + &mut arena + )); + + let computed = arena.alloc_expression(ScalarExpression::Unary { + op: UnaryOperator::Minus, + expr: source, + evaluator: None, + ty: LogicalType::Integer, + }); + let computed = arena.alloc_expression(ScalarExpression::Alias { + expr: computed, + alias: AliasType::Expr(alias_column), + }); + let mut projected = LogicalPlan::new( + Operator::Project(ProjectOperator { + exprs: vec![computed], + }), + Childrens::Only(Box::new(base)), + ); + assert!(!parameterize_right_subtree( + &mut projected, + &output, + &mut arena + )); + Ok(()) + } + #[test] fn composite_probe_uses_index_order_and_preserves_static_filter() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); diff --git a/tests/slt/subquery.slt b/tests/slt/subquery.slt index 3f008708..6bb0902c 100644 --- a/tests/slt/subquery.slt +++ b/tests/slt/subquery.slt @@ -1126,6 +1126,84 @@ true statement error select '20' + cast(1 as bigint); +# Projection output aliases must map back to the indexed source column. +statement ok +create index corr_b on corr_rows(b); + +query I +select id from corr_rows where b in (select x.b as probe_key from corr_rows x where x.id<3) order by id; +---- +1 +2 +5 + +# A computed key is not a probe of its source column. +query I +select id from corr_rows where -b in (select -x.b as probe_key from corr_rows x where x.id<3) order by id; +---- +1 +2 +5 + +# Probing below LIMIT/OFFSET would change the original RHS set. +query I +select id from corr_rows where b in (select x.b as probe_key from corr_rows x order by x.id limit 1) order by id; +---- +1 + +query I +select id from corr_rows where b in (select x.b as probe_key from corr_rows x order by x.id limit 1 offset 1) order by id; +---- +2 +5 + +# Numeric guards keep NULL rows that still depend on the IN result. +query I +select id from corr_rows where b>15 or id in (select x.id from corr_rows x where x.id=4) order by id; +---- +2 +3 +4 +5 + +# A marker visible in SELECT must remain real even when the OR branch is true. +query IT +select id,b in (select x.b from corr_rows x where x.id=1) as matched from corr_rows where b>15 or matched order by id; +---- +1 true +2 false +3 false +5 false + +statement ok +create table guard_bad_cast(id int primary key, txt varchar); + +statement ok +insert into guard_bad_cast values(1,'not an integer'); + +# A shared numeric guard protects every marker in a consecutive Apply segment. +query I +select id from corr_rows where id<0 and (b in (select cast(txt as int) from guard_bad_cast) or b in (select cast(txt as int) from guard_bad_cast)); +---- + +query I +select id from corr_rows where id>0 or (b in (select cast(txt as int) from guard_bad_cast) and b in (select cast(txt as int) from guard_bad_cast)) order by id; +---- +1 +2 +3 +4 +5 + +statement error invalid digit found in string +select id from corr_rows where id>0 and b in (select cast(txt as int) from guard_bad_cast); + +statement error invalid digit found in string +select id from corr_rows where b>0 or id in (select cast(txt as int) from guard_bad_cast); + +statement ok +drop table guard_bad_cast; + statement ok drop table corr_lookup; From 8a8e89785a982c7321aae7136bccf8495e4c44dd Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:38 +0800 Subject: [PATCH 09/62] perf: attach mark producers before consumers and orient nested loops --- src/binder/mod.rs | 11 +- src/binder/select.rs | 188 ++++++++++----- src/db.rs | 5 + src/optimizer/heuristic/optimizer.rs | 38 +-- .../rule/normalization/join_reorder.rs | 219 ++++++++++++++++-- src/optimizer/rule/normalization/mod.rs | 7 +- .../rule/normalization/parameterized_index.rs | 5 +- src/planner/mod.rs | 8 + src/planner/operator/mod.rs | 12 + tests/slt/join.slt | 18 ++ tests/slt/subquery.slt | 30 +++ 11 files changed, 433 insertions(+), 108 deletions(-) diff --git a/src/binder/mod.rs b/src/binder/mod.rs index 18e13ef8..b63af559 100644 --- a/src/binder/mod.rs +++ b/src/binder/mod.rs @@ -261,7 +261,7 @@ pub struct BinderContext<'a, T: Transaction> { using: HashMap, has_join_outer_ref: bool, // subquery - sub_queries: HashMap>, + sub_queries: Vec, scalar_queries: Vec, capture_scalar_outer: bool, scalar_outer_bindings: Vec<(ScalarQueryRef, ExprRef)>, @@ -430,14 +430,7 @@ impl<'a, T: Transaction> BinderContext<'a, T> { } pub fn sub_query(&mut self, sub_query: SubQueryType) { - self.sub_queries - .entry(self.bind_step) - .or_default() - .push(sub_query) - } - - pub fn sub_queries_at_now(&mut self) -> Option> { - self.sub_queries.remove(&self.bind_step) + self.sub_queries.push(sub_query); } pub fn mark_join_outer_ref(&mut self) { diff --git a/src/binder/select.rs b/src/binder/select.rs index 8aa225d8..43b024d0 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -48,24 +48,24 @@ use crate::storage::Transaction; use crate::types::tuple::Schema; use crate::types::{ColumnId, LogicalType}; -struct RightSidePositionGlobalizer<'a> { - right_schema: &'a Schema, - left_len: usize, +struct InputPositionBinder<'a> { + schema: &'a Schema, + offset: usize, } -impl ExprVisitorMut for RightSidePositionGlobalizer<'_> { +impl ExprVisitorMut for InputPositionBinder<'_> { fn visit_column_ref( &mut self, column: &mut ColumnRef, position: &mut usize, arena: &mut (dyn MetaArena + '_), ) -> Result<(), DatabaseError> { - if self - .right_schema + if let Some(index) = self + .schema .iter() - .any(|right| arena.same_column(*right, *column)) + .position(|input| arena.same_column(*input, *column)) { - *position += self.left_len; + *position = self.offset + index; } Ok(()) } @@ -77,25 +77,6 @@ struct AppendedRightOutput { output_position: usize, } -struct MarkerPositionGlobalizer<'a> { - output_column: &'a ColumnRef, - left_len: usize, -} - -impl ExprVisitorMut for MarkerPositionGlobalizer<'_> { - fn visit_column_ref( - &mut self, - column: &mut ColumnRef, - position: &mut usize, - arena: &mut (dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { - if arena.same_column(*column, *self.output_column) { - *position = self.left_len; - } - Ok(()) - } -} - struct ProjectionOutputBinder<'a> { project_exprs: &'a [ExprRef], } @@ -1481,8 +1462,9 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b )); } - if self.context.sub_queries.contains_key(&QueryBindStep::Where) { - children = self.bind_mark_queries(children, std::iter::once(&mut predicate), arena)?; + let pending = self.context.sub_queries.len(); + children = self.bind_mark_queries(children, std::iter::once(&mut predicate), arena)?; + if self.context.sub_queries.len() < pending { let passthrough_exprs = children .output_schema(arena) .iter() @@ -1506,9 +1488,9 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b exprs: impl Iterator, arena: &mut PlanArena, ) -> Result { - let mut output_positions = Vec::new(); - if let Some(sub_queries) = self.context.sub_queries_at_now() { - for sub_query in sub_queries { + for expr in exprs { + while let Some(index) = self.next_mark_query(*expr, arena)? { + let sub_query = self.context.sub_queries.remove(index); match sub_query { SubQueryType::ExistsSubQuery { plan, @@ -1516,7 +1498,6 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b output_column, } => { let left_schema = children.output_schema(arena).clone(); - output_positions.push((output_column, left_schema.len())); let (plan, predicates) = Self::prepare_mark_apply( left_schema.as_ref(), plan, @@ -1546,14 +1527,25 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b arena, ); } + if self.context.is_step(&QueryBindStep::Where) { + if let ScalarExpression::Binary { left_expr, .. } = + &quantified_predicate + { + if left_expr.has_agg_call(arena)? { + return Err(DatabaseError::AggMiss( + "aggregate functions are not allowed in WHERE".into(), + )); + } + } + } + let predicate = arena.alloc_expression(quantified_predicate); let left_schema = children.output_schema(arena).clone(); - output_positions.push((output_column, left_schema.len())); let (plan, predicates) = Self::prepare_mark_apply( left_schema.as_ref(), plan, correlated, true, - vec![arena.alloc_expression(quantified_predicate)], + vec![predicate], arena, )?; children = MarkApplyOperator::build_quantified( @@ -1566,17 +1558,43 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b } } } + InputPositionBinder { + schema: children.output_schema(arena), + offset: 0, + } + .visit(expr, arena)?; } - for expr in exprs { - for (output_column, left_len) in &output_positions { - MarkerPositionGlobalizer { - output_column, - left_len: *left_len, + Ok(children) + } + + fn next_mark_query( + &self, + expr: ExprRef, + arena: &PlanArena, + ) -> Result, DatabaseError> { + let mut next = None; + expr.any_referenced_column(arena, |arena, column| { + next = self.context.sub_queries.iter().position(|query| { + let output = match query { + SubQueryType::ExistsSubQuery { output_column, .. } + | SubQueryType::QuantifiedSubQuery { output_column, .. } => *output_column, + }; + arena.same_column(output, *column) + }); + next.is_some() + })?; + if let Some(index) = next { + if let SubQueryType::QuantifiedSubQuery { + predicate: ScalarExpression::Binary { left_expr, .. }, + .. + } = &self.context.sub_queries[index] + { + if let Some(dependency) = self.next_mark_query(*left_expr, arena)? { + return Ok(Some(dependency)); } - .visit(expr, arena)?; } } - Ok(children) + Ok(next) } fn ensure_mark_apply_right_outputs( @@ -1662,9 +1680,14 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b } let right_schema = plan.output_schema(arena); for expr in &mut apply_predicates { - RightSidePositionGlobalizer { - right_schema, - left_len, + InputPositionBinder { + schema: left_schema, + offset: 0, + } + .visit(expr, arena)?; + InputPositionBinder { + schema: right_schema, + offset: left_len, } .visit(expr, arena)?; } @@ -2154,7 +2177,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b // GRCOV_EXCL_START #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { - use super::{ProjectionOutputBinder, RightSidePositionGlobalizer}; + use super::{InputPositionBinder, ProjectionOutputBinder}; use crate::binder::test::build_t1_table; use crate::catalog::{ColumnCatalog, ColumnDesc}; use crate::errors::DatabaseError; @@ -2208,8 +2231,7 @@ mod tests { } #[test] - fn test_right_side_position_globalizer_only_shifts_right_columns() -> Result<(), DatabaseError> - { + fn test_input_position_binder_only_binds_available_columns() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); let left_column = arena.alloc_column(ColumnCatalog::new( @@ -2223,7 +2245,7 @@ mod tests { ColumnDesc::new(LogicalType::Integer, None, false, None).unwrap(), )); let right_schema = vec![right_column]; - let left_expr = arena.alloc_expression(ScalarExpression::column_expr(left_column, 0)); + let left_expr = arena.alloc_expression(ScalarExpression::column_expr(left_column, 7)); let right_expr = arena.alloc_expression(ScalarExpression::column_expr(right_column, 0)); let mut expr = arena.alloc_expression(ScalarExpression::Binary { op: crate::expression::BinaryOperator::Eq, @@ -2233,9 +2255,23 @@ mod tests { ty: LogicalType::Boolean, }); - RightSidePositionGlobalizer { - right_schema: &right_schema, - left_len: 2, + InputPositionBinder { + schema: &right_schema, + offset: 2, + } + .visit(&mut expr, &mut arena)?; + InputPositionBinder { + schema: &right_schema, + offset: 2, + } + .visit(&mut expr, &mut arena)?; + assert!(matches!( + arena.expression(left_expr), + ScalarExpression::ColumnRef { position: 7, .. } + )); + InputPositionBinder { + schema: &vec![left_column], + offset: 0, } .visit(&mut expr, &mut arena)?; @@ -2364,6 +2400,54 @@ mod tests { Ok(()) } + #[test] + fn test_select_mark_alias_is_produced_before_where() -> Result<(), DatabaseError> { + let tables = build_t1_table()?; + let mut arena = PlanArena::new(&tables.table_arena); + let mut plan = tables.plan_with_arena( + "select c1,c2 in (select c3 from t2) as matched from t1 where matched", + &mut arena, + )?; + let marker = *find_mark_apply(&plan).unwrap().output_column(); + let Operator::Project(project) = &plan.operator else { + unreachable!() + }; + let selected = project.exprs[1].unpack_alias(&arena); + assert!( + matches!(arena.expression(selected), ScalarExpression::ColumnRef { column, position: 2 } + if arena.same_column(*column, marker)) + ); + let mut input = &mut plan; + let mut marks = 0; + let mut filters = 0; + loop { + match (&input.operator, input.childrens.as_mut()) { + (Operator::Filter(filter), Childrens::Only(child)) => { + let schema = child.output_schema(&mut arena); + assert!(arena.same_column(schema[2], marker)); + let referenced = filter.predicate.unpack_alias(&arena); + assert!( + matches!(arena.expression(referenced), ScalarExpression::ColumnRef { column, position: 2 } + if arena.same_column(*column, marker)) + ); + filters += 1; + input = child; + } + (Operator::MarkApply(op), Childrens::Twins { left, .. }) => { + assert!(arena.same_column(*op.output_column(), marker)); + marks += 1; + input = left; + } + (_, Childrens::Only(child)) => input = child, + (_, Childrens::None) => break, + _ => unreachable!(), + } + } + assert_eq!(marks, 1); + assert_eq!(filters, 1); + Ok(()) + } + #[test] fn test_any_subquery_in_where_binds_as_mark_apply() -> Result<(), DatabaseError> { let table_states = build_t1_table()?; diff --git a/src/db.rs b/src/db.rs index f611207d..428d829d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -466,6 +466,11 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { HepBatchStrategy::fix_point_bottomup(1), vec![NormalizationRuleImpl::MaterializeMarkApply], ) + .after_batch( + "Orient Nested Loop Join".to_string(), + HepBatchStrategy::fix_point_bottomup(1), + vec![NormalizationRuleImpl::OrientNestedLoopJoin], + ) .after_batch( "Expression Remapper".to_string(), HepBatchStrategy::once_topdown(), diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index 3c7a0cd9..46036c2d 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -263,25 +263,6 @@ impl<'a> HepOptimizer<'a> { inherited_stream_aggregate_hints(scan_op, arena)?; } - { - let LogicalPlan { - operator, - physical_option, - .. - } = plan; - if let Some(option) = implementation_index.direct_physical_option(operator) { - *physical_option = Some(option); - } else { - let mut best_physical_option: BestPhysicalOption = None; - for rule in implementation_index.for_matching_operator(operator) { - rule.update_best_option(operator, arena, loader, &mut best_physical_option)?; - } - if let Some((option, _)) = best_physical_option { - *physical_option = Some(option); - } - } - } - { let LogicalPlan { operator, @@ -325,6 +306,25 @@ impl<'a> HepOptimizer<'a> { })?; } + { + let LogicalPlan { + operator, + physical_option, + .. + } = plan; + if let Some(option) = implementation_index.direct_physical_option(operator) { + *physical_option = Some(option); + } else { + let mut best_physical_option: BestPhysicalOption = None; + for rule in implementation_index.for_matching_operator(operator) { + rule.update_best_option(operator, arena, loader, &mut best_physical_option)?; + } + if let Some((option, _)) = best_physical_option { + *physical_option = Some(option); + } + } + } + apply_annotated_post_rules(plan, arena)?; Ok(()) diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 39ee7f80..24af285c 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -22,8 +22,86 @@ use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; use crate::types::tuple::Schema; +use std::mem::swap; pub(crate) struct GreedyJoinReorder; +pub(crate) struct OrientNestedLoopJoin; + +impl NormalizationRule for OrientNestedLoopJoin { + fn apply(&self, plan: &mut LogicalPlan, arena: &mut PlanArena) -> Result { + let ( + Operator::Join(JoinOperator { + join_type: JoinType::Cross, + on: JoinCondition::None, + limit_pushed: false, + .. + }), + Childrens::Twins { left, right }, + Some(PhysicalOption { + plan: PlanImpl::NestLoopJoin, + .. + }), + ) = ( + &plan.operator, + plan.childrens.as_ref(), + plan.physical_option.as_ref(), + ) + else { + return Ok(false); + }; + if left.has_materialization() || !right.has_materialization() { + return Ok(false); + } + let original_schema = plan.output_schema(arena).clone(); + for (index, column) in original_schema.iter().enumerate() { + if original_schema[index + 1..] + .iter() + .any(|other| arena.same_column(*column, *other)) + { + return Ok(false); + } + } + let mut join = plan.take(); + let Childrens::Twins { left, right } = join.childrens.as_mut() else { + unreachable!() + }; + swap(left, right); + join.reset_output_schema_cache(); + *plan = restore_join_output(join, original_schema, arena); + Ok(true) + } +} + +fn restore_join_output( + mut join: LogicalPlan, + original_schema: Schema, + arena: &mut PlanArena, +) -> LogicalPlan { + let schema = join.output_schema(arena); + if &original_schema == schema { + return join; + } + let exprs = original_schema + .iter() + .map(|column| { + let position = schema + .iter() + .position(|candidate| arena.same_column(*candidate, *column)) + .unwrap(); + arena.alloc_expression(ScalarExpression::column_expr(*column, position)) + }) + .collect(); + let mut plan = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs }), + Childrens::Only(Box::new(join)), + ); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )); + plan +} // Prefer equijoins, then fewer rows, then the original input order. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -268,31 +346,8 @@ impl NormalizationRule for GreedyJoinReorder { inputs.insert(i, (join, product)); } // SAFETY: At this point, there should be exactly one plan left in the inputs vector, which is the final join result. - let (mut join, _) = inputs.pop().unwrap(); - let schema = join.output_schema(arena); - if &original_schema == schema { - *plan = join; - return Ok(true); - } - let exprs = original_schema - .iter() - .map(|column| { - let position = schema - .iter() - .position(|candidate| arena.same_column(*candidate, *column)) - .unwrap(); - arena.alloc_expression(ScalarExpression::column_expr(*column, position)) - }) - .collect(); - *plan = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs }), - Childrens::Only(Box::new(join)), - ); - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::Project, - SortOption::Follow, - RowCountOption::Follow, - )); + let (join, _) = inputs.pop().unwrap(); + *plan = restore_join_output(join, original_schema, arena); Ok(true) } } @@ -333,6 +388,120 @@ mod tests { (plan, key) } + fn materialized_right(arena: &mut PlanArena) -> LogicalPlan { + let (b, b_key) = input("b", 10, arena); + let (c, c_key) = input("c", 20, arena); + let mut join = JoinOperator::build( + b, + c, + JoinCondition::On { + on: vec![(b_key, c_key)], + filter: None, + }, + JoinType::Inner, + false, + ); + join.physical_option = Some(PhysicalOption::new( + PlanImpl::HashJoin, + SortOption::None, + RowCountOption::Unknown, + )); + let schema = join.output_schema(arena); + let exprs = schema + .iter() + .enumerate() + .map(|(position, column)| { + arena.alloc_expression(ScalarExpression::column_expr(*column, position)) + }) + .collect(); + let mut project = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs }), + Childrens::Only(Box::new(join)), + ); + project.physical_option = Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )); + project + } + + #[test] + fn nested_loop_moves_materialization_left_and_restores_positions() -> Result<(), DatabaseError> + { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let (left, _) = input("a", 100, &mut arena); + let right = materialized_right(&mut arena); + assert!(!left.has_materialization()); + assert!(right.has_materialization()); + let mut plan = + JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::NestLoopJoin, + SortOption::None, + RowCountOption::Estimated(20000), + )); + let schema = plan.output_schema(&mut arena).clone(); + assert!(OrientNestedLoopJoin.apply(&mut plan, &mut arena)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + assert_eq!(plan.estimated_rows(), Some(20000)); + let Operator::Project(project) = &plan.operator else { + unreachable!() + }; + for (expr, position) in project.exprs.iter().zip([2, 0, 1]) { + assert!(matches!( + arena.expression(*expr), + ScalarExpression::ColumnRef { position: actual, .. } if *actual == position + )); + } + let join = plan.childrens.only(); + let (left, right) = join.childrens.twins(); + assert_eq!(left.referenced_table(), vec!["b".into(), "c".into()]); + assert_eq!(right.referenced_table(), vec!["a".into()]); + let mut join = join.clone(); + assert!(!OrientNestedLoopJoin.apply(&mut join, &mut arena)?); + Ok(()) + } + + #[test] + fn nested_loop_orientation_keeps_equal_work_and_semantic_boundaries( + ) -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + for (left_materialized, right_materialized, join_type, limit_pushed) in [ + (false, false, JoinType::Cross, false), + (true, false, JoinType::Cross, false), + (true, true, JoinType::Cross, false), + (false, true, JoinType::LeftOuter, false), + (false, true, JoinType::Cross, true), + ] { + let left = if left_materialized { + materialized_right(&mut arena) + } else { + input("a", 100, &mut arena).0 + }; + let right = if right_materialized { + materialized_right(&mut arena) + } else { + input("d", 100, &mut arena).0 + }; + let mut plan = JoinOperator::build(left, right, JoinCondition::None, join_type, false); + if let Operator::Join(join) = &mut plan.operator { + join.limit_pushed = limit_pushed; + } + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::NestLoopJoin, + SortOption::None, + RowCountOption::Unknown, + )); + let original = plan.clone(); + assert!(!OrientNestedLoopJoin.apply(&mut plan, &mut arena)?); + assert_eq!(plan, original); + } + Ok(()) + } + fn three_inputs(arena: &mut PlanArena) -> LogicalPlan { let (a, a_key) = input("a", 1000, arena); let (b, b_key) = input("b", 10, arena); diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index 8f7928a5..3b4159ba 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -45,7 +45,7 @@ mod join_reorder; mod materialize_mark_apply; mod min_max_top_k; use guard_mark_apply::GuardMarkApply; -use join_reorder::GreedyJoinReorder; +use join_reorder::{GreedyJoinReorder, OrientNestedLoopJoin}; use materialize_mark_apply::MaterializeMarkApply; mod parameterized_index; mod pushdown_limit; @@ -87,6 +87,7 @@ pub enum NormalizationRuleImpl { ParameterizeInnerJoin, EliminateIndexFilter, GreedyJoinReorder, + OrientNestedLoopJoin, MaterializeMarkApply, GuardMarkApply, } @@ -196,7 +197,8 @@ impl NormalizationRuleImpl { NormalizationRuleImpl::MinMaxToTopK => NormalizationRuleRootTag::Aggregate, NormalizationRuleImpl::EliminateIndexFilter => NormalizationRuleRootTag::Filter, NormalizationRuleImpl::ParameterizeInnerJoin - | NormalizationRuleImpl::GreedyJoinReorder => NormalizationRuleRootTag::Join, + | NormalizationRuleImpl::GreedyJoinReorder + | NormalizationRuleImpl::OrientNestedLoopJoin => NormalizationRuleRootTag::Join, NormalizationRuleImpl::ParameterizeMarkApply | NormalizationRuleImpl::MaterializeMarkApply => NormalizationRuleRootTag::MarkApply, } @@ -212,6 +214,7 @@ impl NormalizationRule for NormalizationRuleImpl { match self { NormalizationRuleImpl::ColumnPruning => ColumnPruning.apply(plan, arena), NormalizationRuleImpl::GreedyJoinReorder => GreedyJoinReorder.apply(plan, arena), + NormalizationRuleImpl::OrientNestedLoopJoin => OrientNestedLoopJoin.apply(plan, arena), NormalizationRuleImpl::GuardMarkApply => GuardMarkApply.apply(plan, arena), NormalizationRuleImpl::CollapseProject => CollapseProject.apply(plan, arena), NormalizationRuleImpl::CollapseGroupByAgg => CollapseGroupByAgg.apply(plan, arena), diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index cf155b8f..f790642a 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -739,7 +739,10 @@ mod inner_join_tests { assert!(scan.index_infos[0].lookup.is_none()); scan.index_infos.push(single); // Prefer a usable scalar index even though UNIQUE normally ranks higher. - assert_eq!(pick_parameterized_index_position(scan, &column, &arena), Some(1)); + assert_eq!( + pick_parameterized_index_position(scan, &column, &arena), + Some(1) + ); assert!(parameterize_right_subtree(&mut right, &column, &mut arena)); let PlanImpl::IndexScan(index) = &right.physical_option.as_ref().unwrap().plan else { unreachable!() diff --git a/src/planner/mod.rs b/src/planner/mod.rs index 14c7db99..04147229 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -197,6 +197,14 @@ impl LogicalPlan { } } + pub(crate) fn has_materialization(&self) -> bool { + self.physical_option + .as_ref() + .is_some_and(PhysicalOption::materializes) + || matches!(&self.operator, Operator::MarkApply(op) if op.materialize) + || self.childrens.iter().any(Self::has_materialization) + } + pub fn referenced_table(&self) -> Vec { fn collect_table(plan: &LogicalPlan, results: &mut Vec) { if let Operator::TableScan(op) = &plan.operator { diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index f205a748..af22e300 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -182,6 +182,18 @@ impl PhysicalOption { pub fn sort_option(&self) -> &SortOption { &self.sort_option } + + pub(crate) fn materializes(&self) -> bool { + matches!( + self.plan, + PlanImpl::SimpleAggregate + | PlanImpl::HashAggregate + | PlanImpl::HashJoin + | PlanImpl::Sort + | PlanImpl::TopK + | PlanImpl::Window + ) + } } #[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] diff --git a/tests/slt/join.slt b/tests/slt/join.slt index 78c8283f..8eb3707b 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -624,6 +624,24 @@ select a.v,b.v,c.v from greedy_a a join greedy_b b on a.k=b.k join greedy_c c on 10 100 1100 20 200 2000 +# A materializing derived input can drive a CrossJoin without changing output slots. +query III rowsort +select a.v,h.bv,h.cv from greedy_a a cross join (select b.v as bv,c.v as cv from greedy_b b join greedy_c c on b.k=c.k) h where a.id<=2 +---- +10 100 1000 +10 100 1100 +10 200 2000 +20 100 1000 +20 100 1100 +20 200 2000 + +query III +select a.v,h.bv,h.cv from greedy_a a cross join (select b.v as bv,c.v as cv from greedy_b b join greedy_c c on b.k=c.k) h where a.id<=2 order by a.v desc,h.cv desc limit 3 +---- +20 200 2000 +20 100 1100 +20 100 1000 + # The smallest inputs b and c have no direct key; apply both keys when a joins. query III rowsort select a.v,b.v,c.v from greedy_a a join greedy_b b on a.k=b.k join greedy_c c on a.k=c.k diff --git a/tests/slt/subquery.slt b/tests/slt/subquery.slt index 6bb0902c..189a13ba 100644 --- a/tests/slt/subquery.slt +++ b/tests/slt/subquery.slt @@ -1175,6 +1175,36 @@ select id,b in (select x.b from corr_rows x where x.id=1) as matched from corr_r 3 false 5 false +# Mark definitions are attached at their first consumer, not their SELECT declaration. +query ITT +select id,b in (select x.b from corr_rows x where x.id=1) as first_match,b in (select x.b from corr_rows x where x.id=2) as second_match from corr_rows where second_match or first_match order by id; +---- +1 true false +2 false true +5 false true + +query IT +select id,b not in (select x.b from corr_rows x where x.id=1) as unmatched from corr_rows where unmatched order by id; +---- +2 true +3 true +5 true + +query IT +select id,b=all(select x.b from corr_rows x where x.id=1) as matched from corr_rows where matched is null or matched order by id; +---- +1 true +4 NULL + +# The outer marker consumes an earlier SELECT marker; neither is produced twice. +query ITT +select id,b in (select x.b from corr_rows x where x.id=1) as first_match,first_match in (select true) as second_match from corr_rows where second_match order by id; +---- +1 true true + +statement error aggregate functions are not allowed in WHERE +select count(*) in (select x.id from corr_rows x) as matched from corr_rows where matched; + statement ok create table guard_bad_cast(id int primary key, txt varchar); From 9dea1511f854f5db3edf802d29171ccf91f78e60 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:38 +0800 Subject: [PATCH 10/62] fix: normalize parameterized mark probes to index key types --- .../rule/normalization/parameterized_index.rs | 135 ++++++++++++------ tests/slt/subquery.slt | 35 +++++ 2 files changed, 130 insertions(+), 40 deletions(-) diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index f790642a..295add70 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -28,6 +28,7 @@ use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; use crate::types::index::{IndexLookup, IndexType}; use crate::types::tuple::Schema; use crate::types::LogicalType; +use std::borrow::Cow; pub(crate) struct ParameterizeMarkApply; @@ -48,9 +49,11 @@ impl NormalizationRule for ParameterizeMarkApply { right.output_schema(arena), arena, )?; - let new_probe = probe.and_then(|(right_column, left_expr)| { - parameterize_right_subtree(right, &right_column, arena).then_some(left_expr) - }); + let new_probe = if let Some((right_column, left_expr)) = probe { + parameterize_right_subtree(right, &right_column, left_expr, arena)? + } else { + None + }; (op, new_probe) } _ => return Ok(false), @@ -159,15 +162,16 @@ fn extract_parameterized_probe_side( fn parameterize_right_subtree( plan: &mut LogicalPlan, right_column: &ColumnRef, + mut left_expr: ExprRef, arena: &mut PlanArena<'_>, -) -> bool { +) -> Result, DatabaseError> { match (&mut plan.operator, plan.childrens.as_mut()) { (Operator::Project(project), Childrens::Only(child)) => { let Some(mut expr) = project.exprs.iter().copied().find(|expr| { let column = expr.output_column_ref(arena); arena.same_column(column, *right_column) }) else { - return false; + return Ok(None); }; // Follow the source, not the projection's output relation/name. // Computed and cast keys are not index probes. @@ -175,18 +179,27 @@ fn parameterize_right_subtree( expr = *source; } let ScalarExpression::ColumnRef { column, .. } = *arena.expression(expr) else { - return false; + return Ok(None); }; - parameterize_right_subtree(child, &column, arena) + parameterize_right_subtree(child, &column, left_expr, arena) } (Operator::TableScan(scan), _) => { if scan.limit != (None, None) { - return false; + return Ok(None); } let Some(position) = pick_parameterized_index_position(scan, right_column, arena) else { - return false; + return Ok(None); }; + let key_type = &arena.index(scan.index_infos[position].meta).value_ty; + let left_type = left_expr.return_type(arena); + if left_type.as_ref() != key_type { + let common = LogicalType::max_logical_type(&left_type, key_type)?; + if common.as_ref() != key_type { + return Ok(None); + } + left_expr = left_expr.type_cast(Cow::Owned(common.into_owned()), arena)?; + } scan.index_infos[position].lookup = Some(IndexLookup::Probe); let index_info = scan.index_infos[position].clone(); let sort_option = index_info.sort_option.clone(); @@ -195,12 +208,12 @@ fn parameterize_right_subtree( sort_option, RowCountOption::Unknown, )); - true + Ok(Some(left_expr)) } (Operator::Filter(_) | Operator::Sort(_), Childrens::Only(child)) => { - parameterize_right_subtree(child, right_column, arena) + parameterize_right_subtree(child, right_column, left_expr, arena) } - _ => false, + _ => Ok(None), } } @@ -557,10 +570,11 @@ mod tests { let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); let column = column(&mut arena, "value"); + let probe = arena.alloc_expression(ScalarExpression::from(2)); let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::None); assert!(!ParameterizeMarkApply.apply(&mut plan, &mut arena)?); - assert!(!parameterize_right_subtree(&mut plan, &column, &mut arena)); + assert!(parameterize_right_subtree(&mut plan, &column, probe, &mut arena)?.is_none()); let mut filter = LogicalPlan::new( Operator::Filter(FilterOperator { @@ -570,11 +584,7 @@ mod tests { }), Childrens::None, ); - assert!(!parameterize_right_subtree( - &mut filter, - &column, - &mut arena - )); + assert!(parameterize_right_subtree(&mut filter, &column, probe, &mut arena)?.is_none()); assert_eq!( index_priority(IndexType::PrimaryKey { is_multiple: false }), @@ -602,6 +612,7 @@ mod inner_join_tests { use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::TableArenaCell; use crate::types::index::{IndexInfo, IndexMeta, IndexType}; + use crate::types::tuple::Tuple; use crate::types::value::DataValue; fn scan( @@ -731,7 +742,8 @@ mod inner_join_tests { scan.index_infos[0].meta = arena.alloc_index(composite); (scan.columns[0], scan.index_infos.pop().unwrap()) }; - assert!(!parameterize_right_subtree(&mut right, &column, &mut arena)); + let probe = arena.alloc_expression(ScalarExpression::from(2)); + assert!(parameterize_right_subtree(&mut right, &column, probe, &mut arena)?.is_none()); assert!(right.physical_option.is_none()); let Operator::TableScan(scan) = &mut right.operator else { unreachable!() @@ -743,7 +755,10 @@ mod inner_join_tests { pick_parameterized_index_position(scan, &column, &arena), Some(1) ); - assert!(parameterize_right_subtree(&mut right, &column, &mut arena)); + assert_eq!( + parameterize_right_subtree(&mut right, &column, probe, &mut arena)?, + Some(probe) + ); let PlanImpl::IndexScan(index) = &right.physical_option.as_ref().unwrap().plan else { unreachable!() }; @@ -752,6 +767,58 @@ mod inner_join_tests { Ok(()) } + #[test] + fn scalar_probe_normalizes_only_safe_key_types() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut right = scan(&mut arena, "right", &[LogicalType::Bigint], &[&[0]]); + let Operator::TableScan(op) = &right.operator else { + unreachable!() + }; + let column = op.columns[0]; + let original = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(2))); + let probe = parameterize_right_subtree(&mut right, &column, original, &mut arena)?.unwrap(); + assert_eq!(probe.return_type(&arena).as_ref(), &LogicalType::Bigint); + assert_eq!( + *arena.expression(probe).eval(&arena, None::<&Tuple>)?, + DataValue::Int64(2) + ); + assert_eq!( + *arena.expression(original).eval(&arena, None::<&Tuple>)?, + DataValue::Int32(2) + ); + + for value in [ + DataValue::Float64(2.5.into()), + DataValue::from("hello".to_string()), + ] { + let mut right = scan(&mut arena, "right", &[LogicalType::Bigint], &[&[0]]); + let Operator::TableScan(op) = &right.operator else { + unreachable!() + }; + let column = op.columns[0]; + let original = arena.alloc_expression(ScalarExpression::Constant(value)); + assert!( + parameterize_right_subtree(&mut right, &column, original, &mut arena)?.is_none() + ); + assert!(right.physical_option.is_none()); + let Operator::TableScan(op) = &right.operator else { + unreachable!() + }; + assert!(op.index_infos[0].lookup.is_none()); + } + + let mut right = scan(&mut arena, "narrow", &[LogicalType::Integer], &[&[0]]); + let Operator::TableScan(op) = &right.operator else { + unreachable!() + }; + let column = op.columns[0]; + let large = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int64(1 << 40))); + assert!(parameterize_right_subtree(&mut right, &column, large, &mut arena)?.is_none()); + assert!(right.physical_option.is_none()); + Ok(()) + } + #[test] fn projected_probe_resolves_output_alias_but_rejects_computed_keys_and_limits( ) -> Result<(), DatabaseError> { @@ -775,12 +842,12 @@ mod inner_join_tests { Childrens::Only(Box::new(input)), ) }; + let probe = arena.alloc_expression(ScalarExpression::from(2)); let mut projected = project(base.clone()); - assert!(parameterize_right_subtree( - &mut projected, - &output, - &mut arena - )); + assert_eq!( + parameterize_right_subtree(&mut projected, &output, probe, &mut arena)?, + Some(probe) + ); let input = projected.childrens.only(); let Operator::TableScan(input) = &input.operator else { unreachable!() @@ -798,22 +865,14 @@ mod inner_join_tests { )); let mut limited = project(LimitOperator::build(Some(1), Some(1), base.clone())); - assert!(!parameterize_right_subtree( - &mut limited, - &output, - &mut arena - )); + assert!(parameterize_right_subtree(&mut limited, &output, probe, &mut arena)?.is_none()); let mut pushed = base.clone(); let Operator::TableScan(op) = &mut pushed.operator else { unreachable!() }; op.limit = (None, Some(1)); let mut pushed = project(pushed); - assert!(!parameterize_right_subtree( - &mut pushed, - &output, - &mut arena - )); + assert!(parameterize_right_subtree(&mut pushed, &output, probe, &mut arena)?.is_none()); let computed = arena.alloc_expression(ScalarExpression::Unary { op: UnaryOperator::Minus, @@ -831,11 +890,7 @@ mod inner_join_tests { }), Childrens::Only(Box::new(base)), ); - assert!(!parameterize_right_subtree( - &mut projected, - &output, - &mut arena - )); + assert!(parameterize_right_subtree(&mut projected, &output, probe, &mut arena)?.is_none()); Ok(()) } diff --git a/tests/slt/subquery.slt b/tests/slt/subquery.slt index 189a13ba..3201835b 100644 --- a/tests/slt/subquery.slt +++ b/tests/slt/subquery.slt @@ -1231,6 +1231,41 @@ select id from corr_rows where id>0 and b in (select cast(txt as int) from guard statement error invalid digit found in string select id from corr_rows where b>0 or id in (select cast(txt as int) from guard_bad_cast); +# A scalar index probe must use the comparison's common type, not the raw +# integer literal or narrower outer column. NULL remains three-valued. +statement ok +create table probe_key(id int primary key, k bigint unique); + +statement ok +insert into probe_key values (1, 2), (2, 3), (3, 4), (4, NULL); + +query III +select 2 in (select k from probe_key), 2 not in (select k from probe_key), 9 in (select k from probe_key); +---- +true false NULL + +query I rowsort +select id from corr_rows where id in (select k from probe_key); +---- +2 +3 +4 + +# Casting the indexed side would invalidate a direct lookup. These use the +# normal comparison path rather than truncate, parse, or overflow a probe. +query III +select 2.0 in (select k from probe_key), 2.5 in (select k from probe_key), '2' in (select k from probe_key); +---- +true NULL true + +query III +select 1099511627776 in (select id from corr_rows), 1.5 in (select id from corr_rows), 'hello' in (select id from corr_rows); +---- +false false false + +statement ok +drop table probe_key; + statement ok drop table guard_bad_cast; From ce85cbbbcbceef022c400df205bd165027f55dac Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:38 +0800 Subject: [PATCH 11/62] perf: collect column statistics and refine join cost estimates --- src/db.rs | 65 +- src/execution/ddl/drop_index.rs | 2 +- src/execution/ddl_apply.rs | 15 +- src/execution/dml/analyze.rs | 196 ++- src/optimizer/core/histogram.rs | 131 +- src/optimizer/core/rule.rs | 7 +- src/optimizer/core/statistics_meta.rs | 70 +- src/optimizer/heuristic/optimizer.rs | 52 +- .../rule/implementation/dql/aggregate.rs | 2 +- .../rule/implementation/dql/table_scan.rs | 47 +- .../rule/normalization/column_pruning.rs | 8 +- .../rule/normalization/combine_operators.rs | 16 +- .../normalization/compilation_in_advance.rs | 8 +- .../rule/normalization/elimination.rs | 55 +- .../rule/normalization/guard_mark_apply.rs | 34 +- .../rule/normalization/join_reorder.rs | 1402 ++++++++++++++++- .../normalization/materialize_mark_apply.rs | 28 +- .../rule/normalization/min_max_top_k.rs | 10 +- src/optimizer/rule/normalization/mod.rs | 66 +- .../rule/normalization/parameterized_index.rs | 13 +- .../rule/normalization/pushdown_limit.rs | 12 +- .../rule/normalization/pushdown_predicates.rs | 10 +- .../rule/normalization/simplification.rs | 8 +- src/optimizer/rule/normalization/top_k.rs | 6 +- src/planner/arena.rs | 2 +- src/planner/mod.rs | 168 +- src/planner/operator/mod.rs | 147 +- src/planner/operator/visitor.rs | 192 ++- src/planner/operator/visitor_mut.rs | 6 +- src/serdes/mod.rs | 1 + src/serdes/physical_option.rs | 234 +++ src/storage/mod.rs | 126 +- src/storage/table_codec.rs | 273 ++-- 33 files changed, 2958 insertions(+), 454 deletions(-) create mode 100644 src/serdes/physical_option.rs diff --git a/src/db.rs b/src/db.rs index 428d829d..df2e6b92 100644 --- a/src/db.rs +++ b/src/db.rs @@ -341,7 +341,7 @@ impl DataBaseBuilder { let transaction_isolation = transaction_isolation.unwrap_or_else(|| storage.default_transaction_isolation()); storage.validate_transaction_isolation(transaction_isolation)?; - let meta_cache = HashMap::default(); + let meta_cache = StatisticsMetaCache::default(); let table_cache = HashMap::default(); let view_cache = HashMap::default(); let table_arena = TableArenaCell::default(); @@ -808,17 +808,13 @@ impl DDLApply { let table = table.transplant_to_table_arena(plan_arena)?; state.table_cache.insert(name.clone(), table); if clear_statistics { - state - .meta_cache - .retain(|(cached_table_name, _), _| cached_table_name != &name); + state.meta_cache.remove_table(&name); } catalog_changed = true; } DDLApply::DropTable { name } => { state.table_cache.remove(&name); - state - .meta_cache - .retain(|(cached_table_name, _), _| cached_table_name != &name); + state.meta_cache.remove_table(&name); catalog_changed = true; } DDLApply::UpsertView { view } => { @@ -831,18 +827,31 @@ impl DDLApply { state.view_cache.remove(&name); catalog_changed = true; } - DDLApply::UpsertStatisticsMeta { + DDLApply::UpsertIndexStatistics { table_name, index_id, meta, } => { - state.meta_cache.insert((table_name, index_id), meta); + state + .meta_cache + .indexes + .insert((table_name, index_id), meta); } - DDLApply::RemoveStatisticsMeta { + DDLApply::RemoveIndexStatistics { table_name, index_id, } => { - state.meta_cache.remove(&(table_name, index_id)); + state.meta_cache.indexes.remove(&(table_name, index_id)); + } + DDLApply::UpsertColumnStatistics { + table_name, + column_id, + meta, + } => { + state + .meta_cache + .columns + .insert((table_name, column_id), meta); } } Ok(catalog_changed) @@ -953,17 +962,47 @@ impl Database { name.clone(), )? .ok_or(DatabaseError::TableNotFound)?; + let mut indexes = Vec::new(); + let mut columns = Vec::new(); for index in table.indexes() { let index_id = self.state.table_arena.borrow().index(*index).id; - if let Some(meta) = transaction.statistics_meta( + if let Some(meta) = transaction.index_statistics( &mut table_codec, name.as_ref(), index_id, self.state.table_arena.borrow_mut(), )? { - self.state.meta_cache.insert((name.clone(), index_id), meta); + indexes.push((index_id, meta)); + } + } + for column in table.columns() { + let column_id = self + .state + .table_arena + .borrow() + .column(*column) + .id() + .unwrap(); + if let Some(meta) = transaction.column_statistics( + &mut table_codec, + name.as_ref(), + column_id, + self.state.table_arena.borrow_mut(), + )? { + columns.push((column_id, meta)); } } + self.state.meta_cache.remove_table(&name); + self.state.meta_cache.indexes.extend( + indexes + .into_iter() + .map(|(id, meta)| ((name.clone(), id), meta)), + ); + self.state.meta_cache.columns.extend( + columns + .into_iter() + .map(|(id, meta)| ((name.clone(), id), meta)), + ); self.state.table_cache.insert(name, table); self.state.recycle_table_arena()?; Ok(()) diff --git a/src/execution/ddl/drop_index.rs b/src/execution/ddl/drop_index.rs index a4c53446..1984a0b1 100644 --- a/src/execution/ddl/drop_index.rs +++ b/src/execution/ddl/drop_index.rs @@ -62,7 +62,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for DropIndex<'a> { }; if let Some((table, index_id)) = dropped { arena.push_ddl_apply(DDLApply::upsert_table(table, false)); - arena.push_ddl_apply(DDLApply::RemoveStatisticsMeta { + arena.push_ddl_apply(DDLApply::RemoveIndexStatistics { table_name: table_name.clone(), index_id, }); diff --git a/src/execution/ddl_apply.rs b/src/execution/ddl_apply.rs index 7809a3e9..90240945 100644 --- a/src/execution/ddl_apply.rs +++ b/src/execution/ddl_apply.rs @@ -14,8 +14,10 @@ use crate::catalog::view::View; use crate::catalog::{TableCatalog, TableName}; -use crate::optimizer::core::statistics_meta::StatisticsMeta; +use crate::optimizer::core::histogram::HistogramMeta; +use crate::optimizer::core::statistics_meta::IndexStatistics; use crate::types::index::IndexId; +use crate::types::ColumnId; pub(crate) enum DDLApply { UpsertTable { @@ -31,15 +33,20 @@ pub(crate) enum DDLApply { DropView { name: TableName, }, - UpsertStatisticsMeta { + UpsertIndexStatistics { table_name: TableName, index_id: IndexId, - meta: StatisticsMeta, + meta: IndexStatistics, }, - RemoveStatisticsMeta { + RemoveIndexStatistics { table_name: TableName, index_id: IndexId, }, + UpsertColumnStatistics { + table_name: TableName, + column_id: ColumnId, + meta: HistogramMeta, + }, } impl DDLApply { diff --git a/src/execution/dml/analyze.rs b/src/execution/dml/analyze.rs index d3144016..a635ed47 100644 --- a/src/execution/dml/analyze.rs +++ b/src/execution/dml/analyze.rs @@ -20,15 +20,17 @@ use crate::execution::{ }; use crate::expression::ScalarExpression; use crate::iter_ext::Itertools; -use crate::optimizer::core::histogram::{HistogramBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR}; -use crate::optimizer::core::statistics_meta::StatisticsMeta; +use crate::optimizer::core::histogram::{ + HistogramBuilder, HistogramMetaBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR, +}; +use crate::optimizer::core::statistics_meta::IndexStatistics; use crate::planner::operator::analyze::AnalyzeOperator; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::{table_codec::TableCodec, Transaction}; use crate::types::index::IndexId; use crate::types::value::{DataValue, Utf8Type}; -use crate::types::CharLengthUnits; +use crate::types::{CharLengthUnits, ColumnId}; use std::fmt::{self, Formatter}; const DEFAULT_NUM_OF_BUCKETS: usize = 100; @@ -82,12 +84,27 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Analyze<'a> { return Ok(()); }; - let mut builders = { + let (mut columns, mut builders) = { let table = arena .transaction() .table(arena.table_cache(), self.table_name.clone())? .ok_or(DatabaseError::TableNotFound)?; - table + let columns = table + .columns() + .enumerate() + .map(|(position, column)| { + let column = plan_arena.column(*column); + let Some(column_id) = column.id() else { + return Err(DatabaseError::invalid_column(column.name())); + }; + Ok(( + column_id, + position, + HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?, + )) + }) + .collect::, DatabaseError>>()?; + let indexes = table .indexes() .map(|index| { let index = plan_arena.index(*index); @@ -101,11 +118,15 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Analyze<'a> { histogram_buckets: self.histogram_buckets, }) }) - .collect::, DatabaseError>>()? + .collect::, DatabaseError>>()?; + (columns, indexes) }; while arena.next_tuple(input, plan_arena)? { let tuple = arena.materialize_tuple(); + for (_, position, builder) in &mut columns { + builder.append(&tuple.values[*position]); + } for State { exprs, builder, .. } in builders.iter_mut() { let value = match exprs.as_slice() { [expr] => expr.eval(plan_arena, Some(&tuple))?.into_owned(), @@ -119,8 +140,9 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Analyze<'a> { } let mut state = arena.local_state(plan_arena); let (transaction, table_codec, ddl_apply) = state.write_transaction_codec_ddl_apply_mut(); - let values = Self::persist_statistics_meta( + let values = Self::persist_statistics( self.table_name, + columns, builders, ddl_apply, transaction, @@ -141,8 +163,9 @@ struct State { } impl<'a> Analyze<'a> { - fn persist_statistics_meta( + fn persist_statistics( table_name: &TableName, + columns: Vec<(ColumnId, usize, HistogramMetaBuilder)>, builders: Vec, applies: &mut Vec, transaction: &mut U, @@ -150,6 +173,21 @@ impl<'a> Analyze<'a> { plan_arena: &(dyn MetaArena + '_), ) -> Result, DatabaseError> { let mut values = Vec::with_capacity(builders.len()); + for (column_id, _, builder) in columns { + let meta = builder.build(); + transaction.save_column_statistics( + table_codec, + table_name, + column_id, + &meta, + plan_arena, + )?; + applies.push(DDLApply::UpsertColumnStatistics { + table_name: table_name.clone(), + column_id, + meta, + }); + } for State { index_id, @@ -160,10 +198,10 @@ impl<'a> Analyze<'a> { { let (histogram, sketch, top_n) = builder.build(histogram_buckets.unwrap_or(DEFAULT_NUM_OF_BUCKETS))?; - let meta = StatisticsMeta::new(histogram, sketch, top_n); + let meta = IndexStatistics::new(histogram, sketch, top_n); - transaction.save_statistics_meta(table_codec, table_name, meta.clone(), plan_arena)?; - applies.push(DDLApply::UpsertStatisticsMeta { + transaction.save_index_statistics(table_codec, table_name, meta.clone(), plan_arena)?; + applies.push(DDLApply::UpsertIndexStatistics { table_name: table_name.clone(), index_id, meta, @@ -205,7 +243,7 @@ mod test { #[test] fn test_analyze() -> Result<(), DatabaseError> { - test_statistics_meta_roundtrip()?; + test_column_and_index_statistics_roundtrip()?; test_meta_loader_uses_cache()?; test_meta_loader_negative_cache()?; test_clean_expired_index()?; @@ -227,20 +265,23 @@ mod test { database.ddl(sql) } - fn test_statistics_meta_roundtrip() -> Result<(), DatabaseError> { + fn test_column_and_index_statistics_roundtrip() -> Result<(), DatabaseError> { let temp_dir = TempDir::new().expect("unable to create temporary working directory"); let buckets = 10; let mut kite_sql = DataBaseBuilder::path(temp_dir.path()) .histogram_buckets(buckets) .build_rocksdb()?; - create_table(&mut kite_sql, "create table t1 (a int primary key, b int)")?; + create_table( + &mut kite_sql, + "create table t1 (a int primary key, b int, c varchar)", + )?; create_index(&mut kite_sql, "create index b_index on t1 (b)")?; create_index(&mut kite_sql, "create index p_index on t1 (a, b)")?; for i in 0..DEFAULT_NUM_OF_BUCKETS + 1 { kite_sql - .run(format!("insert into t1 values({i}, {})", i % 20))? + .run(format!("insert into t1 values({i}, {}, NULL)", i % 20))? .done()?; } kite_sql.analyze("t1")?; @@ -248,6 +289,10 @@ mod test { let table_name = "t1".to_string().into(); let loader = StatisticMetaLoader::new(kite_sql.state.meta_cache()); + let table = kite_sql.state.table_cache().get(&table_name).unwrap(); + let a_id = *table.get_column_id_by_name("a").unwrap(); + let b_id = *table.get_column_id_by_name("b").unwrap(); + let c_id = *table.get_column_id_by_name("c").unwrap(); let statistics_meta_pk_index = loader.load(&table_name, 0)?.unwrap(); assert_eq!(statistics_meta_pk_index.index_id(), 0); assert_eq!(statistics_meta_pk_index.histogram().values_len(), 101); @@ -262,6 +307,16 @@ mod test { assert_eq!(statistics_meta_p_index.index_id(), 2); assert_eq!(statistics_meta_p_index.histogram().values_len(), 101); assert_eq!(statistics_meta_p_index.histogram().buckets_len(), buckets); + for (column, distinct) in [(a_id, 101), (b_id, 20)] { + let meta = loader.load_column(&table_name, column).unwrap(); + assert_eq!(meta.values_len(), 101); + assert_eq!(meta.distinct_values_len(), distinct); + assert_eq!(meta.buckets_len(), 0); + } + let unindexed = loader.load_column(&table_name, c_id).unwrap(); + assert_eq!(unindexed.values_len(), 0); + assert_eq!(unindexed.null_count(), 101); + assert_eq!(unindexed.distinct_values_len(), 0); let expected_frequency = loader.collect_count(&table_name, 1, &Range::Eq(DataValue::Int32(7)))?; @@ -280,6 +335,74 @@ mod test { loader.collect_count(&table_name, 1, &Range::Eq(DataValue::Int32(7)))?, expected_frequency ); + assert_eq!( + loader + .load_column(&table_name, a_id) + .unwrap() + .distinct_values_len(), + 101 + ); + assert_eq!( + loader + .load_column(&table_name, b_id) + .unwrap() + .distinct_values_len(), + 20 + ); + assert_eq!( + loader.load_column(&table_name, c_id).unwrap().null_count(), + 101 + ); + reopened.ddl("drop index t1.b_index")?; + let loader = StatisticMetaLoader::new(reopened.state.meta_cache()); + assert!(loader.load(&table_name, 1)?.is_none()); + assert_eq!( + loader + .load_column(&table_name, b_id) + .unwrap() + .distinct_values_len(), + 20 + ); + let tx = reopened.storage.transaction()?; + assert!(tx + .index_statistics( + &mut TableCodec::default(), + "t1", + 1, + reopened.state.table_arena().borrow_mut() + )? + .is_none()); + assert_eq!( + tx.column_statistics( + &mut TableCodec::default(), + "t1", + b_id, + reopened.state.table_arena().borrow_mut() + )? + .unwrap() + .distinct_values_len(), + 20 + ); + drop(tx); + reopened.ddl("alter table t1 drop column c")?; + let tx = reopened.storage.transaction()?; + assert!(tx + .column_statistics( + &mut TableCodec::default(), + "t1", + c_id, + reopened.state.table_arena().borrow_mut() + )? + .is_none()); + assert!(tx + .column_statistics( + &mut TableCodec::default(), + "t1", + a_id, + reopened.state.table_arena().borrow_mut() + )? + .is_some()); + drop(tx); let mut iter = reopened.run("select a from t1 where b=7 order by a")?; let mut rows = Vec::new(); while iter @@ -292,9 +415,9 @@ mod test { // A missing root must not publish partially reconstructed statistics. let mut codec = TableCodec::default(); let mut tx = reopened.storage.transaction()?; - codec.with_statistics_meta( + codec.with_index_statistics( "t1", - 1, + 2, None, reopened.state.table_arena().borrow(), |key, _| tx.remove(key), @@ -304,6 +427,8 @@ mod test { let mut reopened = DataBaseBuilder::path(temp_dir.path()).build_rocksdb()?; assert!(matches!(reopened.load(CatalogKind::Table(table_name)), Err(DatabaseError::InvalidValue(message)) if message == "statistics meta is incomplete")); + assert!(reopened.state.meta_cache().indexes.is_empty()); + assert!(reopened.state.meta_cache().columns.is_empty()); Ok(()) } @@ -325,12 +450,14 @@ mod test { let loader = StatisticMetaLoader::new(kite_sql.state.meta_cache()); assert!(loader.load(&table_name, 1)?.is_some()); assert_eq!( - loader.collect_count(&table_name, 1, &Range::Eq(DataValue::Int32(7)))?, + loader + .collect_count(&table_name, 1, &Range::Eq(DataValue::Int32(7)))? + .map(|(rows, _)| rows), Some(1) ); let mut transaction = kite_sql.storage.transaction()?; let mut table_codec = TableCodec::default(); - let keys: Vec> = table_codec.with_statistics_index_bound("t1", 1, |min, max| { + let keys: Vec> = table_codec.with_index_statistics_bound("t1", 1, |min, max| { let mut iter = transaction.range(Bound::Included(min), Bound::Included(max))?; let mut keys = Vec::new(); while let Some((key, _)) = iter.try_next()? { @@ -345,7 +472,9 @@ mod test { let loader = StatisticMetaLoader::new(kite_sql.state.meta_cache()); assert!(loader.load(&table_name, 1)?.is_some()); assert_eq!( - loader.collect_count(&table_name, 1, &Range::Eq(DataValue::Int32(7)))?, + loader + .collect_count(&table_name, 1, &Range::Eq(DataValue::Int32(7)))? + .map(|(rows, _)| rows), Some(1) ); @@ -363,7 +492,11 @@ mod test { let loader = StatisticMetaLoader::new(kite_sql.state.meta_cache()); assert!(loader.load(&table_name, 1)?.is_none()); - assert!(!kite_sql.state.meta_cache().contains_key(&(table_name, 1))); + assert!(!kite_sql + .state + .meta_cache() + .indexes + .contains_key(&(table_name, 1))); Ok(()) } @@ -412,16 +545,27 @@ mod test { })?; let table_name = "t1".to_string().into(); let loader = StatisticMetaLoader::new(kite_sql.state.meta_cache()); - let statistics_meta = loader.load(&table_name, 0)?.unwrap(); - let expected_keys = 1 + let index_statistics = loader.load(&table_name, 0)?.unwrap(); + let expected_keys = 2 + 1 - + statistics_meta + + index_statistics .sketch() .storage_page_count(COUNT_MIN_SKETCH_STORAGE_PAGE_LEN) + 1 - + statistics_meta.histogram().buckets_len(); + + index_statistics.histogram().buckets_len(); assert_eq!(keys, expected_keys); - + drop(transaction); + kite_sql.ddl("drop table t1")?; + assert!(kite_sql.state.meta_cache().columns.is_empty()); + assert!(kite_sql.state.meta_cache().indexes.is_empty()); + let transaction = kite_sql.storage.transaction()?; + table_codec.with_statistics_bound("t1", |min, max| { + assert!(transaction + .range(Bound::Included(min), Bound::Included(max))? + .try_next()? + .is_none()); + Ok(()) + })?; Ok(()) } } diff --git a/src/optimizer/core/histogram.rs b/src/optimizer/core/histogram.rs index a2f28dcb..708e58af 100644 --- a/src/optimizer/core/histogram.rs +++ b/src/optimizer/core/histogram.rs @@ -34,13 +34,17 @@ use std::sync::OnceLock; const ANALYZE_STATISTICS_CONFIDENCE: f64 = 0.95; pub(crate) const ANALYZE_STATISTICS_RELATIVE_ERROR: f64 = 0.001; -pub struct HistogramBuilder { - index_id: IndexId, +pub struct HistogramMetaBuilder { null_count: usize, values_len: usize, + hll: HyperLogLog, +} + +pub struct HistogramBuilder { + index_id: IndexId, + meta: HistogramMetaBuilder, quantile: KllSketchBuilder, sketch: CountMinSketch, - hll: HyperLogLog, top_n: ColumnTopN, } @@ -54,7 +58,6 @@ struct BoundComparator { #[derive(Debug, Clone, PartialEq, ReferenceSerialization)] pub struct HistogramMeta { - index_id: IndexId, number_of_distinct_value: usize, null_count: usize, values_len: usize, @@ -68,6 +71,7 @@ pub struct HistogramMeta { // Equal depth histogram #[derive(Debug)] pub struct Histogram { + index_id: IndexId, meta: HistogramMeta, buckets: Vec, comparator: OnceLock, @@ -76,6 +80,7 @@ pub struct Histogram { impl Clone for Histogram { fn clone(&self) -> Self { Self { + index_id: self.index_id, meta: self.meta.clone(), buckets: self.buckets.clone(), comparator: OnceLock::new(), @@ -85,7 +90,7 @@ impl Clone for Histogram { impl PartialEq for Histogram { fn eq(&self, other: &Self) -> bool { - self.meta == other.meta && self.buckets == other.buckets + self.index_id == other.index_id && self.meta == other.meta && self.buckets == other.buckets } } @@ -97,32 +102,61 @@ pub struct Bucket { // repeat: u64, } +impl HistogramMetaBuilder { + pub fn new(relative_error: f64) -> Result { + Ok(Self { + null_count: 0, + values_len: 0, + hll: HyperLogLog::with_relative_error(relative_error)?, + }) + } + + pub fn append(&mut self, value: &DataValue) -> bool { + if value.is_null() { + self.null_count += 1; + false + } else { + self.values_len += 1; + self.hll.add(value); + true + } + } + + pub fn build(self) -> HistogramMeta { + HistogramMeta { + number_of_distinct_value: if self.values_len == 0 { + 0 + } else { + self.hll.estimate().clamp(1, self.values_len) + }, + null_count: self.null_count, + values_len: self.values_len, + buckets_len: 0, + correlation: 0.0, + } + } +} + impl HistogramBuilder { pub fn new(index_meta: &IndexMeta, relative_error: f64) -> Result { Ok(Self { index_id: index_meta.id, - null_count: 0, - values_len: 0, + meta: HistogramMetaBuilder::new(relative_error)?, quantile: KllSketchBuilder::with_relative_error(relative_error)?, sketch: CountMinSketch::with_relative_error( ANALYZE_STATISTICS_CONFIDENCE, relative_error, )?, - hll: HyperLogLog::with_relative_error(relative_error)?, top_n: ColumnTopN::default(), }) } pub fn append(&mut self, value: DataValue) -> Result<(), DatabaseError> { - if value.is_null() { - self.null_count += 1; - } else { + if self.meta.append(&value) { self.sketch.increment(&value); - self.hll.add(&value); self.top_n .add_with_size(ANALYZE_STATISTICS_TOP_N_SIZE, value.clone(), 1); self.quantile.insert(value)?; - self.values_len += 1; } Ok(()) @@ -138,19 +172,17 @@ impl HistogramBuilder { )); } - let values_len = self.values_len; + let values_len = self.meta.values_len; if number_of_buckets > values_len { return Err(DatabaseError::TooManyBuckets(number_of_buckets, values_len)); } let HistogramBuilder { index_id, - null_count, + meta, quantile, mut sketch, - hll, top_n, - .. } = self; let mut buckets = Vec::with_capacity(number_of_buckets); let quantile = quantile.build()?; @@ -178,20 +210,16 @@ impl HistogramBuilder { count: (upper_rank - lower_rank + 1) as u64, }); } - sketch.add(&DataValue::Null, self.null_count); - let number_of_distinct_value = hll.estimate().clamp(1, values_len); + let mut meta = meta.build(); + sketch.add(&DataValue::Null, meta.null_count); + meta.buckets_len = buckets.len(); + meta.correlation = correlation; let top_n = top_n.finish_with_size(ANALYZE_STATISTICS_TOP_N_SIZE); Ok(( Histogram { - meta: HistogramMeta { - index_id, - number_of_distinct_value, - null_count, - values_len, - buckets_len: buckets.len(), - correlation, - }, + index_id, + meta, buckets, comparator: OnceLock::new(), }, @@ -282,7 +310,11 @@ fn is_above( } impl Histogram { - pub fn from_parts(meta: HistogramMeta, buckets: Vec) -> Result { + pub fn from_parts( + index_id: IndexId, + meta: HistogramMeta, + buckets: Vec, + ) -> Result { if meta.buckets_len != buckets.len() { return Err(DatabaseError::InvalidValue(format!( "histogram bucket count mismatch: meta={}, actual={}", @@ -292,14 +324,15 @@ impl Histogram { } Ok(Self { + index_id, meta, buckets, comparator: OnceLock::new(), }) } - pub fn into_parts(self) -> (HistogramMeta, Vec) { - (self.meta, self.buckets) + pub fn into_parts(self) -> (IndexId, HistogramMeta, Vec) { + (self.index_id, self.meta, self.buckets) } fn comparator(&self) -> Result<&BoundComparator, DatabaseError> { @@ -331,7 +364,7 @@ impl Histogram { } pub fn index_id(&self) -> IndexId { - self.meta.index_id + self.index_id } pub fn values_len(&self) -> usize { @@ -670,8 +703,8 @@ fn scope_lower_includes_null(min: &Bound) -> bool { } impl HistogramMeta { - pub fn index_id(&self) -> IndexId { - self.index_id + pub fn null_count(&self) -> usize { + self.null_count } pub fn values_len(&self) -> usize { @@ -694,7 +727,7 @@ mod tests { use crate::expression::range_detacher::Range; use crate::optimizer::core::cm_sketch::CountMinSketch; use crate::optimizer::core::histogram::{ - Bucket, HistogramBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR, + Bucket, HistogramBuilder, HistogramMetaBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR, }; use crate::optimizer::core::top_n::ColumnTopN; use crate::types::index::{IndexMeta, IndexType}; @@ -702,6 +735,36 @@ mod tests { use crate::types::LogicalType; use std::ops::Bound; + #[test] + fn column_meta_counts_values_without_histogram_buckets() -> Result<(), DatabaseError> { + let empty = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?.build(); + assert_eq!(empty.values_len(), 0); + assert_eq!(empty.null_count(), 0); + assert_eq!(empty.distinct_values_len(), 0); + assert_eq!(empty.buckets_len(), 0); + let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + for value in [ + DataValue::Null, + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(2), + ] { + builder.append(&value); + } + let meta = builder.build(); + assert_eq!(meta.values_len(), 3); + assert_eq!(meta.null_count(), 1); + assert_eq!(meta.distinct_values_len(), 2); + assert_eq!(meta.buckets_len(), 0); + let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + builder.append(&DataValue::Null); + let meta = builder.build(); + assert_eq!(meta.values_len(), 0); + assert_eq!(meta.null_count(), 1); + assert_eq!(meta.distinct_values_len(), 0); + Ok(()) + } + fn index_meta() -> IndexMeta { IndexMeta { id: 0, diff --git a/src/optimizer/core/rule.rs b/src/optimizer/core/rule.rs index ffc62673..1821f1ca 100644 --- a/src/optimizer/core/rule.rs +++ b/src/optimizer/core/rule.rs @@ -28,7 +28,12 @@ pub trait MatchPattern { pub trait NormalizationRule { /// Returns true when the plan tree is modified. - fn apply(&self, plan: &mut LogicalPlan, arena: &mut PlanArena) -> Result; + fn apply( + &self, + plan: &mut LogicalPlan, + arena: &mut PlanArena, + loader: Option<&StatisticMetaLoader<'_>>, + ) -> Result; } fn compare_costs(candidate_cost: Option, best_cost: Option) -> Ordering { diff --git a/src/optimizer/core/statistics_meta.rs b/src/optimizer/core/statistics_meta.rs index d0a3260f..7dc91716 100644 --- a/src/optimizer/core/statistics_meta.rs +++ b/src/optimizer/core/statistics_meta.rs @@ -21,6 +21,7 @@ use crate::optimizer::core::top_n::ColumnTopN; use crate::storage::StatisticsMetaCache; use crate::types::index::IndexId; use crate::types::value::DataValue; +use crate::types::ColumnId; use kite_sql_serde_macros::ReferenceSerialization; use std::slice; @@ -37,9 +38,17 @@ impl<'a> StatisticMetaLoader<'a> { &self, table_name: &TableName, index_id: IndexId, - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { let key = (table_name.clone(), index_id); - Ok(self.cache.get(&key)) + Ok(self.cache.indexes.get(&key)) + } + + pub fn load_column( + &self, + table_name: &TableName, + column_id: ColumnId, + ) -> Option<&HistogramMeta> { + self.cache.columns.get(&(table_name.clone(), column_id)) } pub fn collect_count( @@ -47,7 +56,7 @@ impl<'a> StatisticMetaLoader<'a> { table_name: &TableName, index_id: IndexId, range: &Range, - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { let Some(entry) = self.load(table_name, index_id)? else { return Ok(None); }; @@ -60,20 +69,20 @@ impl<'a> StatisticMetaLoader<'a> { entry .histogram() .collect_count(ranges, entry.sketch(), entry.top_n()) - .map(Some) + .map(|rows| Some((rows, entry.histogram().distinct_values_len()))) } } #[derive(Debug, Clone, ReferenceSerialization)] -pub struct StatisticsMetaRoot { +pub struct IndexStatisticsRoot { index_id: IndexId, histogram_meta: HistogramMeta, } -impl StatisticsMetaRoot { - pub fn new(histogram_meta: HistogramMeta) -> Self { +impl IndexStatisticsRoot { + pub fn new(index_id: IndexId, histogram_meta: HistogramMeta) -> Self { Self { - index_id: histogram_meta.index_id(), + index_id, histogram_meta, } } @@ -92,16 +101,16 @@ impl StatisticsMetaRoot { } #[derive(Debug, Clone)] -pub struct StatisticsMeta { +pub struct IndexStatistics { index_id: IndexId, histogram: Histogram, sketch: CountMinSketch, top_n: ColumnTopN, } -impl StatisticsMeta { +impl IndexStatistics { pub fn new(histogram: Histogram, sketch: CountMinSketch, top_n: ColumnTopN) -> Self { - StatisticsMeta { + IndexStatistics { index_id: histogram.index_id(), histogram, sketch, @@ -110,12 +119,13 @@ impl StatisticsMeta { } pub fn from_parts( - root: StatisticsMetaRoot, + root: IndexStatisticsRoot, buckets: Vec, sketch: CountMinSketch, top_n: Option, ) -> Result { - let histogram = Histogram::from_parts(root.into_histogram_meta(), buckets)?; + let histogram = + Histogram::from_parts(root.index_id(), root.into_histogram_meta(), buckets)?; Ok(Self::new(histogram, sketch, top_n.unwrap_or_default())) } @@ -123,14 +133,14 @@ impl StatisticsMeta { pub fn into_parts( self, ) -> ( - StatisticsMetaRoot, + IndexStatisticsRoot, Vec, CountMinSketch, ColumnTopN, ) { - let (histogram_meta, buckets) = self.histogram.into_parts(); + let (index_id, histogram_meta, buckets) = self.histogram.into_parts(); ( - StatisticsMetaRoot::new(histogram_meta), + IndexStatisticsRoot::new(index_id, histogram_meta), buckets, self.sketch, self.top_n, @@ -158,8 +168,10 @@ impl StatisticsMeta { #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::errors::DatabaseError; + use crate::expression::range_detacher::Range; use crate::optimizer::core::histogram::{HistogramBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR}; - use crate::optimizer::core::statistics_meta::StatisticsMeta; + use crate::optimizer::core::statistics_meta::{IndexStatistics, StatisticMetaLoader}; + use crate::storage::StatisticsMetaCache; use crate::types::index::{IndexMeta, IndexType}; use crate::types::value::DataValue; use crate::types::LogicalType; @@ -199,18 +211,32 @@ mod tests { let (histogram, sketch, top_n) = builder.build(4)?; let expected_estimate = sketch.estimate(&DataValue::Int32(7)); let expected_top_n_count = top_n.get(&DataValue::Int32(7)); - let meta = StatisticsMeta::new(histogram.clone(), sketch, top_n); + let meta = IndexStatistics::new(histogram.clone(), sketch, top_n); let (root, buckets, sketch, top_n) = meta.into_parts(); - let statistics_meta = StatisticsMeta::from_parts(root, buckets, sketch, Some(top_n))?; + let index_statistics = IndexStatistics::from_parts(root, buckets, sketch, Some(top_n))?; - assert_eq!(histogram, statistics_meta.histogram); + let mut cache = StatisticsMetaCache::default(); + cache.indexes.insert( + (index.table_name.clone(), index.id), + index_statistics.clone(), + ); + let loader = StatisticMetaLoader::new(&cache); + assert_eq!( + loader.collect_count(&index.table_name, index.id, &Range::Eq(DataValue::Int32(7)))?, + Some((1, histogram.distinct_values_len())) + ); + assert_eq!( + loader.collect_count(&index.table_name, 1, &Range::Dummy)?, + None + ); + assert_eq!(histogram, index_statistics.histogram); assert_eq!( expected_estimate, - statistics_meta.sketch().estimate(&DataValue::Int32(7)) + index_statistics.sketch().estimate(&DataValue::Int32(7)) ); assert_eq!( expected_top_n_count, - statistics_meta.top_n().get(&DataValue::Int32(7)) + index_statistics.top_n().get(&DataValue::Int32(7)) ); Ok(()) diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index 46036c2d..e3b6181b 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -68,6 +68,7 @@ impl<'a> HepOptimizer<'a> { self.before_batches, &mut applied_rules, arena, + loader, )?; if let Some(loader) = loader { @@ -91,6 +92,7 @@ impl<'a> HepOptimizer<'a> { self.after_batches, &mut applied_rules, arena, + loader, )?; Ok(self.plan) @@ -102,24 +104,25 @@ impl<'a> HepOptimizer<'a> { batches: &[HepBatch], applied_rules: &mut Vec, arena: &mut PlanArena, + loader: Option<&StatisticMetaLoader<'_>>, ) -> Result<(), DatabaseError> { for batch in batches { match batch.strategy { HepBatchStrategy::Once(order) => { - Self::apply_batch(plan, batch, order, applied_rules, arena)?; + Self::apply_batch(plan, batch, order, applied_rules, arena, loader)?; } HepBatchStrategy::MaxTimes { max_iteration, order, } => { for _ in 0..max_iteration { - if !Self::apply_batch(plan, batch, order, applied_rules, arena)? { + if !Self::apply_batch(plan, batch, order, applied_rules, arena, loader)? { break; } } } HepBatchStrategy::LoopIfApplied(order) => { - while Self::apply_batch(plan, batch, order, applied_rules, arena)? {} + while Self::apply_batch(plan, batch, order, applied_rules, arena, loader)? {} } } } @@ -133,12 +136,13 @@ impl<'a> HepOptimizer<'a> { order: HepMatchOrder, applied_rules: &mut Vec, arena: &mut PlanArena, + loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let mut applied = false; for step in &batch.steps { match step { HepBatchStep::WholeTree(pass) => { - if Self::apply_whole_tree_pass(plan, pass, arena)? { + if Self::apply_whole_tree_pass(plan, pass, arena, loader)? { applied = true; } } @@ -147,7 +151,7 @@ impl<'a> HepOptimizer<'a> { if matches!(batch.strategy, HepBatchStrategy::Once(_)) { applied_rules.resize(rules.len(), false); } - if Self::apply_local_rules(plan, rules, order, applied_rules, arena)? { + if Self::apply_local_rules(plan, rules, order, applied_rules, arena, loader)? { applied = true; } } @@ -160,12 +164,13 @@ impl<'a> HepOptimizer<'a> { plan: &mut LogicalPlan, pass: &HepWholeTreePass, arena: &mut PlanArena, + loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { match pass.kind { WholeTreePassKind::ColumnPruning => { let mut applied = false; for rule in &pass.rules { - applied |= rule.apply(plan, arena)?; + applied |= rule.apply(plan, arena, loader)?; } if applied { plan.reset_output_schema_cache_recursive(); @@ -407,24 +412,25 @@ impl<'a> HepOptimizer<'a> { order: HepMatchOrder, applied_rules: &mut [bool], arena: &mut PlanArena, + loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { if !applied_rules.is_empty() && applied_rules.iter().all(|done| *done) { return Ok(false); } let mut applied = false; if matches!(order, HepMatchOrder::TopDown) { - applied |= Self::apply_local_rules_current(plan, rules, applied_rules, arena)?; + applied |= Self::apply_local_rules_current(plan, rules, applied_rules, arena, loader)?; } let child_applied = match plan.childrens.as_mut() { Childrens::Only(child) => { - Self::apply_local_rules(child, rules, order, applied_rules, arena)? + Self::apply_local_rules(child, rules, order, applied_rules, arena, loader)? } Childrens::Twins { left, right } => { let left_applied = - Self::apply_local_rules(left, rules, order, applied_rules, arena)?; + Self::apply_local_rules(left, rules, order, applied_rules, arena, loader)?; let right_applied = - Self::apply_local_rules(right, rules, order, applied_rules, arena)?; + Self::apply_local_rules(right, rules, order, applied_rules, arena, loader)?; left_applied || right_applied } Childrens::None => false, @@ -435,7 +441,7 @@ impl<'a> HepOptimizer<'a> { applied |= child_applied; if matches!(order, HepMatchOrder::BottomUp) { - applied |= Self::apply_local_rules_current(plan, rules, applied_rules, arena)?; + applied |= Self::apply_local_rules_current(plan, rules, applied_rules, arena, loader)?; } Ok(applied) } @@ -445,6 +451,7 @@ impl<'a> HepOptimizer<'a> { rules: &HepLocalRewriteBatch, applied_rules: &mut [bool], arena: &mut PlanArena, + loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let mut applied = false; let mut next_rule_idx = 0; @@ -453,7 +460,7 @@ impl<'a> HepOptimizer<'a> { if applied_rules.get(idx).copied().unwrap_or(false) { continue; } - if rules.rules[idx].apply(plan, arena)? { + if rules.rules[idx].apply(plan, arena, loader)? { if let Some(done) = applied_rules.get_mut(idx) { *done = true; } @@ -506,7 +513,7 @@ impl ImplementationRuleIndex { Some(PhysicalOption::new( PlanImpl::SimpleAggregate, SortOption::None, - RowCountOption::Estimated(1), + RowCountOption::Estimated { rows: 1, ndv: None }, )) } Operator::Dummy if self.contains(ImplementationRuleImpl::Dummy) => Some( @@ -593,7 +600,10 @@ impl ImplementationRuleIndex { Some(PhysicalOption::new( PlanImpl::Values, SortOption::None, - RowCountOption::Estimated(op.rows.len() / op.schema_ref.len()), + RowCountOption::Estimated { + rows: op.rows.len() / op.schema_ref.len(), + ndv: None, + }, )) } Operator::Window(op) if self.contains(ImplementationRuleImpl::Window) => Some( @@ -737,7 +747,10 @@ mod tests { child.physical_option = Some(PhysicalOption::new( PlanImpl::Dummy, SortOption::None, - RowCountOption::Estimated(12), + RowCountOption::Estimated { + rows: 12, + ndv: None, + }, )); let implementations = ImplementationRuleIndex::new(vec![ImplementationRuleImpl::Filter]); plan.physical_option = implementations.direct_physical_option(&plan.operator); @@ -786,7 +799,8 @@ mod tests { &batch, HepMatchOrder::TopDown, &mut applied_rules, - &mut arena + &mut arena, + None, )?); assert!(matches!( topdown.childrens.only().operator, @@ -798,7 +812,8 @@ mod tests { &batch, HepMatchOrder::BottomUp, &mut applied_rules, - &mut arena + &mut arena, + None, )?); assert!(matches!( bottomup.childrens.only().operator, @@ -809,7 +824,8 @@ mod tests { &batch, HepMatchOrder::BottomUp, &mut applied_rules, - &mut arena + &mut arena, + None, )?); for strategy in [ diff --git a/src/optimizer/rule/implementation/dql/aggregate.rs b/src/optimizer/rule/implementation/dql/aggregate.rs index e92dada3..598a8abd 100644 --- a/src/optimizer/rule/implementation/dql/aggregate.rs +++ b/src/optimizer/rule/implementation/dql/aggregate.rs @@ -60,6 +60,6 @@ single_mapping!( PhysicalOption::new( PlanImpl::SimpleAggregate, SortOption::None, - RowCountOption::Estimated(1) + RowCountOption::Estimated { rows: 1, ndv: None } ) ); diff --git a/src/optimizer/rule/implementation/dql/table_scan.rs b/src/optimizer/rule/implementation/dql/table_scan.rs index e7d46521..18739a8f 100644 --- a/src/optimizer/rule/implementation/dql/table_scan.rs +++ b/src/optimizer/rule/implementation/dql/table_scan.rs @@ -15,9 +15,12 @@ use crate::errors::DatabaseError; use crate::expression::range_detacher::Range; use crate::optimizer::core::pattern::Pattern; -use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; +use crate::optimizer::core::rule::{ + keep_best_physical_option, BestPhysicalOption, ImplementationRule, MatchPattern, +}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; +use crate::planner::PlanArena; use crate::types::index::{IndexLookup, IndexType}; use std::sync::LazyLock; @@ -38,7 +41,7 @@ impl ImplementationRule for SeqScanImplementation { fn update_best_option( &self, op: &Operator, - arena: &crate::planner::PlanArena, + arena: &PlanArena, loader: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { @@ -55,18 +58,15 @@ impl ImplementationRule for SeqScanImplementation { .map(|index_info| loader.load(&scan_op.table_name, arena.index(index_info.meta).id)) .transpose()? .flatten() - .map(|statistics_meta| statistics_meta.histogram().values_len()); + .map(|index_statistics| index_statistics.histogram().values_len()); - crate::optimizer::core::rule::keep_best_physical_option( - best_physical_option, - PhysicalOption::new( - PlanImpl::SeqScan, - SortOption::None, - cost.map(RowCountOption::Estimated) - .unwrap_or(RowCountOption::Unknown), - ), - cost, + let option = PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + cost.map(|rows| RowCountOption::Estimated { rows, ndv: None }) + .unwrap_or(RowCountOption::Unknown), ); + keep_best_physical_option(best_physical_option, option, cost); Ok(()) } else { unreachable!("invalid operator!") @@ -86,7 +86,7 @@ impl ImplementationRule for IndexScanImplementation { fn update_best_option( &self, op: &Operator, - arena: &crate::planner::PlanArena, + arena: &PlanArena, loader: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { @@ -97,13 +97,15 @@ impl ImplementationRule for IndexScanImplementation { }; let mut cost = None; let mut estimated_rows = None; + let mut ndv = None; let index_meta = arena.index(index_info.meta); - if let Some(mut row_count) = + if let Some((mut row_count, distinct_count)) = loader.collect_count(&scan_op.table_name, index_meta.id, range)? { row_count = adjust_index_row_count(index_meta.ty, range, row_count); estimated_rows = Some(row_count); + ndv = Some(distinct_count.min(row_count)); if index_info.covered_deserializers.is_none() && !matches!(index_meta.ty, IndexType::PrimaryKey { .. }) { @@ -149,17 +151,14 @@ impl ImplementationRule for IndexScanImplementation { } } - crate::optimizer::core::rule::keep_best_physical_option( - best_physical_option, - PhysicalOption::new( - PlanImpl::IndexScan(Box::new(index_info.clone())), - index_info.sort_option.clone(), - estimated_rows - .map(RowCountOption::Estimated) - .unwrap_or(RowCountOption::Unknown), - ), - cost, + let option = PhysicalOption::new( + PlanImpl::IndexScan(Box::new(index_info.clone())), + index_info.sort_option.clone(), + estimated_rows + .map(|rows| RowCountOption::Estimated { rows, ndv }) + .unwrap_or(RowCountOption::Unknown), ); + keep_best_physical_option(best_physical_option, option, cost); } Ok(()) diff --git a/src/optimizer/rule/normalization/column_pruning.rs b/src/optimizer/rule/normalization/column_pruning.rs index 9d4ef677..0b2eb1a5 100644 --- a/src/optimizer/rule/normalization/column_pruning.rs +++ b/src/optimizer/rule/normalization/column_pruning.rs @@ -18,6 +18,7 @@ use crate::expression::agg::AggKind; use crate::expression::visitor::ExprVisitor; use crate::expression::{AliasType, HasCountStar, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::rule::normalization::{ remap_expr_positions, remap_exprs_positions, PositionRemapper, }; @@ -131,7 +132,7 @@ impl ColumnPruning { arena, }; OperatorExprVisitor::new(&mut collector, arena as &dyn MetaArena) - .visit_operator(operator)?; + .visit_operator(operator, None)?; struct ReferencedOperatorColumnCollector<'a, 'p> { referenced_columns: &'a mut ReferencedColumns, @@ -213,7 +214,7 @@ impl ColumnPruning { referenced_columns, arena, } - .visit_operator(operator) + .visit_operator(operator, None) } fn extend_expr_referenced_columns<'a>( @@ -835,7 +836,8 @@ impl NormalizationRule for ColumnPruning { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let outcome = Self::_apply( ReferencedColumns::with_arena_capacity(arena), diff --git a/src/optimizer/rule/normalization/combine_operators.rs b/src/optimizer/rule/normalization/combine_operators.rs index b45b8903..68560f20 100644 --- a/src/optimizer/rule/normalization/combine_operators.rs +++ b/src/optimizer/rule/normalization/combine_operators.rs @@ -16,6 +16,7 @@ use crate::errors::DatabaseError; use crate::expression::visitor_mut::{walk_mut_expr, ExprVisitorMut}; use crate::expression::{BinaryOperator, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::{only_child_mut, replace_with_only_child}; use crate::optimizer::rule::normalization::strip_alias; use crate::planner::operator::filter::FilterOperator; @@ -118,7 +119,8 @@ impl NormalizationRule for CollapseProject { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let Operator::Project(parent_op) = &mut plan.operator else { return Ok(false); @@ -223,7 +225,8 @@ impl NormalizationRule for CombineFilter { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let parent_filter = match mem::replace(&mut plan.operator, Operator::Dummy) { Operator::Filter(op) => op, @@ -295,7 +298,8 @@ impl NormalizationRule for CollapseGroupByAgg { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let can_collapse = { let LogicalPlan { @@ -442,7 +446,7 @@ mod tests { Childrens::Only(Box::new(child)), ); - assert!(CollapseProject.apply(&mut plan, &mut arena)?); + assert!(CollapseProject.apply(&mut plan, &mut arena, None)?); let Operator::Project(op) = &plan.operator else { unreachable!("expected project"); @@ -469,7 +473,7 @@ mod tests { project.exprs.reverse(); } } - assert!(!super::CombineFilter.apply(&mut filter, &mut arena)?); + assert!(!super::CombineFilter.apply(&mut filter, &mut arena, None)?); assert!(matches!( filter.childrens.pop_only().operator, Operator::Project(_) @@ -552,7 +556,7 @@ mod tests { let expr = column_expr(&mut arena, "c2", 0); let mut plan = AggregateOperator::build(child, vec![], vec![expr], true, false); - assert!(CollapseGroupByAgg.apply(&mut plan, &mut arena)?); + assert!(CollapseGroupByAgg.apply(&mut plan, &mut arena, None)?); let Operator::Aggregate(op) = &plan.operator else { unreachable!("expected aggregate"); }; diff --git a/src/optimizer/rule/normalization/compilation_in_advance.rs b/src/optimizer/rule/normalization/compilation_in_advance.rs index e26a2111..5eac05c1 100644 --- a/src/optimizer/rule/normalization/compilation_in_advance.rs +++ b/src/optimizer/rule/normalization/compilation_in_advance.rs @@ -15,6 +15,7 @@ use crate::errors::DatabaseError; use crate::expression::BindEvaluator; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan, PlanArena}; @@ -60,7 +61,8 @@ impl NormalizationRule for EvaluatorBind { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { Self::_apply(plan, arena)?; Ok(true) @@ -222,7 +224,7 @@ mod tests { right: Box::new(right_filter), }, ); - assert!(EvaluatorBind.apply(&mut join, &mut arena)?); + assert!(EvaluatorBind.apply(&mut join, &mut arena, None)?); let Operator::Join(op) = &join.operator else { unreachable!() }; @@ -243,7 +245,7 @@ mod tests { let union_left = filter(&mut arena, column); let union_right = filter(&mut arena, column); let mut union = UnionOperator::build(vec![column], vec![column], union_left, union_right); - EvaluatorBind.apply(&mut union, &mut arena)?; + EvaluatorBind.apply(&mut union, &mut arena, None)?; assert!(union.childrens.iter().all(|child| { matches!(&child.operator, Operator::Filter(op) if is_bound(op.predicate, &arena)) })); diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index 992f88fc..a2ecabcc 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -15,12 +15,13 @@ use crate::errors::DatabaseError; use crate::expression::ScalarExpression; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::{only_child_mut, replace_with_only_child, wrap_child_with}; use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; -use crate::planner::{Childrens, ExprRef, LogicalPlan}; +use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; use crate::types::index::{IndexLookup, IndexOrderHint}; pub struct EliminateRedundantSort; @@ -29,7 +30,8 @@ impl NormalizationRule for EliminateRedundantSort { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let (sort_fields, topk_limit) = match &plan.operator { Operator::Sort(sort_op) => (sort_op.sort_fields.clone(), None), @@ -74,7 +76,8 @@ impl NormalizationRule for EliminateIndexFilter { fn apply( &self, plan: &mut LogicalPlan, - _: &mut crate::planner::PlanArena, + _: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { if !matches!(plan.operator, Operator::Filter(_)) { return Ok(false); @@ -268,7 +271,8 @@ impl NormalizationRule for UseStreamAggregate { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let Operator::Aggregate(op) = &plan.operator else { return Ok(false); @@ -315,7 +319,8 @@ impl NormalizationRule for ForceSpillAggregate { fn apply( &self, plan: &mut LogicalPlan, - _: &mut crate::planner::PlanArena, + _: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let (implementation, sort_fields) = match (&plan.operator, &plan.physical_option) { ( @@ -369,14 +374,14 @@ pub(crate) fn apply_annotated_post_rules( ) -> Result { let mut changed = false; - if EliminateRedundantSort.apply(plan, arena)? { + if EliminateRedundantSort.apply(plan, arena, None)? { changed = true; } - if UseStreamAggregate.apply(plan, arena)? { + if UseStreamAggregate.apply(plan, arena, None)? { changed = true; } // Run last so an existing ordered child is reused before a forced spill adds a Sort. - if ForceSpillAggregate.apply(plan, arena)? { + if ForceSpillAggregate.apply(plan, arena, None)? { changed = true; } @@ -741,7 +746,7 @@ mod tests { let mut plan = build_filter_with_selected_index(&mut arena, predicate, None); let rule = EliminateIndexFilter; - assert!(rule.apply(&mut plan, &mut arena)?); + assert!(rule.apply(&mut plan, &mut arena, None)?); assert!(matches!(plan.operator, Operator::TableScan(_))); Ok(()) } @@ -757,7 +762,7 @@ mod tests { let mut plan = build_filter_with_selected_index(&mut arena, predicate, Some(residual)); let rule = EliminateIndexFilter; - assert!(rule.apply(&mut plan, &mut arena)?); + assert!(rule.apply(&mut plan, &mut arena, None)?); let Operator::Filter(filter_op) = &plan.operator else { unreachable!("residual predicate should keep filter"); }; @@ -798,7 +803,7 @@ mod tests { index_info.lookup = Some(IndexLookup::Probe); let rule = EliminateIndexFilter; - assert!(!rule.apply(&mut plan, &mut arena)?); + assert!(!rule.apply(&mut plan, &mut arena, None)?); let Operator::Filter(filter) = &plan.operator else { panic!("probe cannot discharge the static filter") }; @@ -814,7 +819,7 @@ mod tests { let mut plan = build_plan(&mut arena, vec![sort_field.clone()], vec![sort_field], 0); let rule = EliminateRedundantSort; - assert!(rule.apply(&mut plan, &mut arena)?); + assert!(rule.apply(&mut plan, &mut arena, None)?); assert!(matches!(plan.operator, Operator::Filter(_))); Ok(()) } @@ -837,7 +842,7 @@ mod tests { }); let rule = EliminateRedundantSort; - assert!(rule.apply(&mut plan, &mut arena)?); + assert!(rule.apply(&mut plan, &mut arena, None)?); match plan.operator { Operator::Limit(limit_op) => { assert_eq!(limit_op.limit, Some(10)); @@ -893,7 +898,7 @@ mod tests { let mut arena = crate::planner::PlanArena::new(&table_arena); let (mut plan, _) = desc_topk_plan(&mut arena, false); - assert!(EliminateRedundantSort.apply(&mut plan, &mut arena)?); + assert!(EliminateRedundantSort.apply(&mut plan, &mut arena, None)?); assert!(matches!(plan.operator, Operator::Limit(_))); assert!(scan_is_reverse(&plan)); Ok(()) @@ -905,7 +910,7 @@ mod tests { let mut arena = crate::planner::PlanArena::new(&table_arena); let (mut plan, _) = desc_topk_plan(&mut arena, true); - assert!(!EliminateRedundantSort.apply(&mut plan, &mut arena)?); + assert!(!EliminateRedundantSort.apply(&mut plan, &mut arena, None)?); assert!(matches!(plan.operator, Operator::TopK(_))); assert!(!scan_is_reverse(&plan)); Ok(()) @@ -929,7 +934,7 @@ mod tests { RowCountOption::Unknown, )); - assert!(!EliminateRedundantSort.apply(&mut plan, &mut arena)?); + assert!(!EliminateRedundantSort.apply(&mut plan, &mut arena, None)?); assert!(!scan_is_reverse(&plan)); Ok(()) } @@ -967,7 +972,7 @@ mod tests { info.lookup = lookup; } super::mark_sort_preserving_indexes(&mut plan, std::slice::from_ref(&c2), &arena)?; - assert!(EliminateRedundantSort.apply(&mut plan, &mut arena)?); + assert!(EliminateRedundantSort.apply(&mut plan, &mut arena, None)?); } Ok(()) } @@ -995,7 +1000,7 @@ mod tests { }); let rule = EliminateRedundantSort; - assert!(rule.apply(&mut plan, &mut arena)?); + assert!(rule.apply(&mut plan, &mut arena, None)?); assert!(matches!(plan.operator, Operator::Limit(_))); Ok(()) } @@ -1107,7 +1112,7 @@ mod tests { )); let rule = UseStreamAggregate; - assert!(rule.apply(&mut plan, &mut arena)?); + assert!(rule.apply(&mut plan, &mut arena, None)?); assert!(matches!( plan.physical_option, Some(PhysicalOption { @@ -1146,7 +1151,7 @@ mod tests { )); let rule = UseStreamAggregate; - assert!(rule.apply(&mut plan, &mut arena)?); + assert!(rule.apply(&mut plan, &mut arena, None)?); assert!(matches!( plan.physical_option, Some(PhysicalOption { @@ -1155,7 +1160,7 @@ mod tests { }) )); let force_spill = ForceSpillAggregate; - assert!(!force_spill.apply(&mut plan, &mut arena)?); + assert!(!force_spill.apply(&mut plan, &mut arena, None)?); assert!(!matches!( plan.childrens.as_ref(), Childrens::Only(child) if matches!(child.operator, Operator::Sort(_)) @@ -1171,7 +1176,7 @@ mod tests { SortOption::None, RowCountOption::Unknown, )); - assert!(!rule.apply(&mut unordered, &mut arena)?); + assert!(!rule.apply(&mut unordered, &mut arena, None)?); assert!(matches!( unordered.physical_option, Some(PhysicalOption { @@ -1200,7 +1205,7 @@ mod tests { )); let rule = ForceSpillAggregate; - assert!(rule.apply(&mut plan, &mut arena)?); + assert!(rule.apply(&mut plan, &mut arena, None)?); assert!(matches!( plan.physical_option, Some(PhysicalOption { @@ -1239,7 +1244,7 @@ mod tests { super::mark_sort_preserving_indexes(&mut plan, &[c2], &arena)?; let rule = EliminateRedundantSort; - assert!(!rule.apply(&mut plan, &mut arena)?); + assert!(!rule.apply(&mut plan, &mut arena, None)?); assert!(matches!(plan.operator, Operator::Sort(_))); Ok(()) } @@ -1309,7 +1314,7 @@ mod tests { }; super::mark_sort_preserving_indexes(&mut plan, &sort_fields, &arena)?; let rule = EliminateRedundantSort; - assert!(rule.apply(&mut plan, &mut arena)?); + assert!(rule.apply(&mut plan, &mut arena, None)?); assert!(matches!(plan.operator, Operator::Filter(_))); let table_plan = plan.childrens.pop_only(); diff --git a/src/optimizer/rule/normalization/guard_mark_apply.rs b/src/optimizer/rule/normalization/guard_mark_apply.rs index d00484a7..12ba2188 100644 --- a/src/optimizer/rule/normalization/guard_mark_apply.rs +++ b/src/optimizer/rule/normalization/guard_mark_apply.rs @@ -18,6 +18,7 @@ use crate::errors::DatabaseError; use crate::expression::visitor::ExprVisitor; use crate::expression::{AliasType, BinaryOperator, ScalarExpression, TypeCast, UnaryOperator}; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::Operator; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; @@ -28,7 +29,12 @@ use crate::types::LogicalType; pub struct GuardMarkApply; impl NormalizationRule for GuardMarkApply { - fn apply(&self, plan: &mut LogicalPlan, arena: &mut PlanArena) -> Result { + fn apply( + &self, + plan: &mut LogicalPlan, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, + ) -> Result { let (Operator::Project(project), Childrens::Only(filter)) = (&plan.operator, plan.childrens.as_mut()) else { @@ -448,7 +454,7 @@ mod tests { let mut arena = PlanArena::new(&tables); let mut fixture = Fixture::new(&mut arena, op, false)?; fixture.add_mark(&mut arena, op)?; - assert!(GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); let upper = fixture.mark().guard.unwrap(); let Childrens::Only(filter) = fixture.plan.childrens.as_mut() else { unreachable!() @@ -465,7 +471,7 @@ mod tests { let lower = lower.guard.unwrap(); assert_ne!(upper, lower); let predicate = fixture.predicate(); - EvaluatorBind.apply(&mut fixture.plan, &mut arena)?; + EvaluatorBind.apply(&mut fixture.plan, &mut arena, None)?; for flag in [ DataValue::Boolean(false), DataValue::Boolean(true), @@ -512,7 +518,7 @@ mod tests { } } } - assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); } Ok(()) } @@ -555,7 +561,7 @@ mod tests { .exprs .push(if escaped == 1 { fixture.marker } else { upper }); } - assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); assert!(fixture.mark().guard.is_none()); let Childrens::Only(filter) = fixture.plan.childrens.as_mut() else { unreachable!() @@ -581,11 +587,11 @@ mod tests { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let mut fixture = Fixture::new(&mut arena, op, marker_first)?; - assert!(GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); let guard = fixture.mark().guard.unwrap(); assert_ne!(guard, fixture.flag); - assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); - EvaluatorBind.apply(&mut fixture.plan, &mut arena)?; + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); + EvaluatorBind.apply(&mut fixture.plan, &mut arena, None)?; let predicate = fixture.predicate(); for flag in [ DataValue::Boolean(false), @@ -641,7 +647,7 @@ mod tests { unreachable!() }; project.exprs.push(fixture.marker); - assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); assert!(fixture.mark().guard.is_none()); let Operator::Project(project) = &mut fixture.plan.operator else { unreachable!() @@ -662,7 +668,7 @@ mod tests { unreachable!() }; op.predicate = predicate; - assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); assert!(fixture.mark().guard.is_none()); Ok(()) } @@ -686,9 +692,9 @@ mod tests { unreachable!() }; *right_expr = failing; - assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); assert!(fixture.mark().guard.is_none()); - EvaluatorBind.apply(&mut fixture.plan, &mut arena)?; + EvaluatorBind.apply(&mut fixture.plan, &mut arena, None)?; let matched = Tuple::new( None, vec![DataValue::Boolean(false), DataValue::Boolean(true)], @@ -830,7 +836,7 @@ mod tests { unreachable!() }; *left_expr = alias; - assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); assert!(fixture.mark().guard.is_none()); let alias = arena.alloc_expression(ScalarExpression::Alias { expr: fixture.flag, @@ -841,7 +847,7 @@ mod tests { unreachable!() }; *left_expr = alias; - assert!(GuardMarkApply.apply(&mut fixture.plan, &mut arena)?); + assert!(GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); Ok(()) } } diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 24af285c..56109878 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -13,22 +13,34 @@ // limitations under the License. use crate::catalog::ColumnRef; +use crate::catalog::TableName; use crate::errors::DatabaseError; +use crate::expression::visitor::ExprVisitor; use crate::expression::visitor_mut::ExprVisitorMut; -use crate::expression::ScalarExpression; +use crate::expression::{AliasType, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::operator::project::ProjectOperator; +use crate::planner::operator::table_scan::TableScanOperator; +use crate::planner::operator::visitor::OperatorVisitor; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; +use crate::types::index::{IndexMeta, IndexMetaRef, IndexType}; use crate::types::tuple::Schema; +use crate::types::ColumnId; use std::mem::swap; pub(crate) struct GreedyJoinReorder; pub(crate) struct OrientNestedLoopJoin; impl NormalizationRule for OrientNestedLoopJoin { - fn apply(&self, plan: &mut LogicalPlan, arena: &mut PlanArena) -> Result { + fn apply( + &self, + plan: &mut LogicalPlan, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, + ) -> Result { let ( Operator::Join(JoinOperator { join_type: JoinType::Cross, @@ -43,13 +55,42 @@ impl NormalizationRule for OrientNestedLoopJoin { }), ) = ( &plan.operator, - plan.childrens.as_ref(), + plan.childrens.as_mut(), plan.physical_option.as_ref(), ) else { return Ok(false); }; - if left.has_materialization() || !right.has_materialization() { + let (Some(left_startup), Some(right_startup)) = ( + left.estimated_startup_work()?, + right.estimated_startup_work()?, + ) else { + return Ok(false); + }; + let should_swap = match (left.estimated_rows(), right.estimated_rows()) { + (Some(left_rows), Some(right_rows)) => { + if left_rows == 0 { + false + } else if right_rows == 0 { + true + } else { + // T(input) = startup + rows * row-work. Count the inner + // startup again for every outer row; shared output work is + // omitted from this local direction comparison. + let (Some(left_work), Some(right_work)) = + (left.estimated_work()?, right.estimated_work()?) + else { + return Ok(false); + }; + let keep = left_work.saturating_add(left_rows.saturating_mul(right_work)); + let swap = right_work.saturating_add(right_rows.saturating_mul(left_work)); + swap < keep + } + } + (Some(0 | 1), None) => false, + _ => right_startup > left_startup, + }; + if !should_swap { return Ok(false); } let original_schema = plan.output_schema(arena).clone(); @@ -67,6 +108,7 @@ impl NormalizationRule for OrientNestedLoopJoin { }; swap(left, right); join.reset_output_schema_cache(); + join.clear_startup_work(); *plan = restore_join_output(join, original_schema, arena); Ok(true) } @@ -112,11 +154,324 @@ struct JoinCandidate { right: usize, } -fn estimate_join_rows(left: usize, right: usize) -> usize { - // TODO: For INNER equijoins, use left_rows * right_rows / max(left_ndv, right_ndv) - // and cap the estimate with join-key uniqueness. Uniqueness bounds matches; - // it does not guarantee every input row finds a match. - left.saturating_mul(right) +struct ScanColumnVisitor<'plan, 'arena, A> { + column: Option, + source: Option<(&'plan TableName, ColumnId)>, + arena: &'arena A, +} + +impl<'plan, A: MetaArena> OperatorVisitor<'plan> for ScanColumnVisitor<'plan, '_, A> { + fn visit_plan(&mut self, plan: &'plan LogicalPlan) -> Result<(), DatabaseError> { + if self.column.is_none() { + return Ok(()); + } + self.visit_operator(&plan.operator, plan.physical_option.as_ref())?; + let follow = match &plan.operator { + Operator::Project(_) => true, + Operator::Join(_) | Operator::Aggregate(_) | Operator::Limit(_) | Operator::TopK(_) => { + false + } + _ => plan + .physical_option + .as_ref() + .is_some_and(|option| matches!(option.row_count, RowCountOption::Follow)), + }; + if follow && self.column.is_some() { + self.visit_children(&plan.childrens)?; + } + Ok(()) + } + + fn visit_children(&mut self, childrens: &'plan Childrens) -> Result<(), DatabaseError> { + match childrens { + Childrens::Only(input) | Childrens::Twins { left: input, .. } => self.visit_plan(input), + Childrens::None => Ok(()), + } + } + + fn visit_table_scan(&mut self, scan: &'plan TableScanOperator) -> Result<(), DatabaseError> { + if scan.limit == (None, None) { + if let Some(source) = self.column.and_then(|column| { + scan.columns + .iter() + .find(|source| self.arena.same_column(**source, column)) + }) { + self.source = self + .arena + .column(*source) + .id() + .map(|id| (&scan.table_name, id)); + } + } + self.column = None; + Ok(()) + } + + fn visit_project(&mut self, project: &'plan ProjectOperator) -> Result<(), DatabaseError> { + let Some(column) = self.column.take() else { + return Ok(()); + }; + let arena = self.arena; + for &candidate in &project.exprs { + let source = match arena.expression(candidate) { + ScalarExpression::Alias { + expr, + alias: AliasType::Name(_), + } if arena.column(column).id().is_none() + && arena.column(column).table_name().is_none() + && candidate.output_name(arena) == arena.column(column).name() => + { + *expr + } + ScalarExpression::Alias { + expr, + alias: AliasType::Expr(alias), + } if matches!(arena.expression(*alias), ScalarExpression::ColumnRef { column: target, .. } + if arena.same_column(*target, column)) => + { + *expr + } + ScalarExpression::ColumnRef { column: target, .. } + if arena.same_column(*target, column) => + { + candidate + } + _ => continue, + }; + return ExprVisitor::visit(self, source, arena); + } + Ok(()) + } +} + +impl ExprVisitor for ScanColumnVisitor<'_, '_, A> { + fn visit_expression( + &mut self, + expr: &ScalarExpression, + _arena: &A, + ) -> Result { + Ok(matches!( + expr, + ScalarExpression::Alias { .. } | ScalarExpression::ColumnRef { .. } + )) + } + + fn visit_alias( + &mut self, + expr: ExprRef, + _alias: &AliasType, + arena: &A, + ) -> Result<(), DatabaseError> { + // The alias target describes the output, not the source column. + self.visit(expr, arena) + } + + fn visit_column_ref(&mut self, column: &ColumnRef) -> Result<(), DatabaseError> { + self.column = Some(*column); + Ok(()) + } +} + +fn scan_column<'plan>( + plan: &'plan LogicalPlan, + column: ColumnRef, + arena: &PlanArena, +) -> Result, DatabaseError> { + let mut visitor = ScanColumnVisitor { + column: Some(column), + source: None, + arena, + }; + visitor.visit_plan(plan)?; + Ok(visitor.source) +} + +fn column_statistics( + left: &LogicalPlan, + right: &LogicalPlan, + keys: &[(ExprRef, ExprRef)], + loader: Option<&StatisticMetaLoader<'_>>, + arena: &PlanArena, +) -> Result, DatabaseError> { + let Some(loader) = loader else { + return Ok(None); + }; + let (Some(left_rows), Some(right_rows)) = (left.estimated_rows(), right.estimated_rows()) + else { + return Ok(None); + }; + let mut result = None; + let mut divisor = 0; + for &(left_key, right_key) in keys { + let ( + ScalarExpression::ColumnRef { column: l, .. }, + ScalarExpression::ColumnRef { column: r, .. }, + ) = (arena.expression(left_key), arena.expression(right_key)) + else { + continue; + }; + for (l, r) in [(*l, *r), (*r, *l)] { + let Some((left_table, left_column)) = scan_column(left, l, arena)? else { + continue; + }; + let Some((right_table, right_column)) = scan_column(right, r, arena)? else { + continue; + }; + let Some(left_meta) = loader.load_column(left_table, left_column) else { + continue; + }; + let Some(right_meta) = loader.load_column(right_table, right_column) else { + continue; + }; + let left_ndv = left_meta.distinct_values_len().min(left_rows); + let right_ndv = right_meta.distinct_values_len().min(right_rows); + if left_ndv == 0 || right_ndv == 0 { + return Ok(Some((left_ndv, right_ndv))); + } + // Do not multiply selection rates for potentially correlated keys. + let candidate = left_ndv.max(right_ndv); + if candidate > divisor { + divisor = candidate; + result = Some((left_ndv, right_ndv)); + } + } + } + Ok(result) +} + +fn index_statistics(plan: &LogicalPlan) -> Option<(IndexMetaRef, Option)> { + let option = plan.row_count_option()?; + let (PlanImpl::IndexScan(index), RowCountOption::Estimated { ndv, .. }) = + (&option.plan, &option.row_count) + else { + return None; + }; + Some((index.meta, *ndv)) +} + +struct JoinInputStatistics<'a> { + rows: usize, + schema: &'a [ColumnRef], + index: Option<(&'a IndexMeta, Option)>, +} + +fn index_column(column: ColumnRef, index: &IndexMeta, arena: &PlanArena) -> bool { + let column = arena.column(column); + column.table_name() == Some(&index.table_name) + && column.id().is_some_and(|id| index.column_ids.contains(&id)) +} + +fn covers_index( + index: &IndexMeta, + other: &JoinInputStatistics, + keys: &[(ExprRef, ExprRef)], + require_other_index: bool, + arena: &PlanArena, +) -> bool { + index.column_ids.iter().all(|id| { + keys.iter().any(|&(left, right)| { + let ( + ScalarExpression::ColumnRef { column: left, .. }, + ScalarExpression::ColumnRef { column: right, .. }, + ) = (arena.expression(left), arena.expression(right)) + else { + return false; + }; + [(*left, *right), (*right, *left)] + .iter() + .any(|&(own, opposite)| { + index_column(own, index, arena) + && arena.column(own).id() == Some(*id) + && other + .schema + .iter() + .any(|column| arena.same_column(*column, opposite)) + && (!require_other_index + || other + .index + .is_some_and(|(index, _)| index_column(opposite, index, arena))) + }) + }) + }) +} + +#[derive(Clone, Copy)] +struct JoinInputEstimate { + rows: usize, + ndv: Option, + unique: bool, +} + +fn estimate_join_rows(left: JoinInputEstimate, right: JoinInputEstimate) -> usize { + let mut rows = left.rows.saturating_mul(right.rows); + if left.ndv == Some(0) || right.ndv == Some(0) { + return 0; + } + if let (Some(left_ndv), Some(right_ndv)) = (left.ndv, right.ndv) { + let ndv = left_ndv + .min(left.rows) + .max(right_ndv.min(right.rows)) + .max(1); + rows = ((left.rows as u128 * right.rows as u128).div_ceil(ndv as u128)) + .min(usize::MAX as u128) as usize; + } + if left.unique { + rows = rows.min(right.rows); + } + if right.unique { + rows = rows.min(left.rows); + } + rows +} + +fn join_rows( + left: &JoinInputStatistics, + right: &JoinInputStatistics, + keys: &[(ExprRef, ExprRef)], + columns: Option<(usize, usize)>, + arena: &PlanArena, +) -> usize { + let mut left_estimate = JoinInputEstimate { + rows: left.rows, + ndv: None, + unique: false, + }; + let mut right_estimate = JoinInputEstimate { + rows: right.rows, + ndv: None, + unique: false, + }; + if let (Some((left_index, Some(left_ndv))), Some((right_index, Some(right_ndv)))) = + (left.index, right.index) + { + if covers_index(left_index, right, keys, true, arena) + && covers_index(right_index, left, keys, true, arena) + { + left_estimate.ndv = Some(left_ndv); + right_estimate.ndv = Some(right_ndv); + } + } + if left_estimate.ndv.is_none() || right_estimate.ndv.is_none() { + if let Some((left_ndv, right_ndv)) = columns { + left_estimate.ndv = Some(left_ndv); + right_estimate.ndv = Some(right_ndv); + } + } + for (own, other, estimate) in [ + (left, right, &mut left_estimate), + (right, left, &mut right_estimate), + ] { + if let Some((index, _)) = own.index { + if matches!(index.ty, IndexType::PrimaryKey { .. } | IndexType::Unique) + && covers_index(index, other, keys, false, arena) + { + estimate.unique = true; + } + } + } + // TODO: Estimate expression keys and intermediate Join key distributions; + // do not inherit uniqueness through joins that can duplicate an input row. + estimate_join_rows(left_estimate, right_estimate) } fn can_expand(plan: &LogicalPlan) -> bool { @@ -139,7 +494,8 @@ fn inspect<'plan>( schemas: &mut Vec<&'plan Schema>, keys: &mut Vec<(ExprRef, ExprRef)>, arena: &mut PlanArena, -) -> Option { + loader: Option<&StatisticMetaLoader<'_>>, +) -> Result, DatabaseError> { if can_expand(plan) { let (Operator::Join(join), Childrens::Twins { left, right }) = (&plan.operator, plan.childrens.as_mut()) @@ -149,17 +505,60 @@ fn inspect<'plan>( if let JoinCondition::On { on, .. } = &join.on { keys.extend_from_slice(on); } - let left_rows = inspect(left, schemas, keys, arena)?; - let right_rows = inspect(right, schemas, keys, arena)?; - let rows = estimate_join_rows(left_rows, right_rows); + let left_stats = index_statistics(left); + let right_stats = index_statistics(right); + let on = match &join.on { + JoinCondition::On { on, .. } => on.as_slice(), + JoinCondition::None => &[], + }; + let columns = column_statistics(left, right, on, loader, arena)?; + let start = schemas.len(); + let Some(left_rows) = inspect(left, schemas, keys, arena, loader)? else { + return Ok(None); + }; + let middle = schemas.len(); + let Some(right_rows) = inspect(right, schemas, keys, arena, loader)? else { + return Ok(None); + }; + let rows = join_rows( + &JoinInputStatistics { + rows: left_rows, + schema: if middle == start + 1 { + schemas[start] + } else { + &[] + }, + index: left_stats.map(|(index, ndv)| (arena.index(index), ndv)), + }, + &JoinInputStatistics { + rows: right_rows, + schema: if schemas.len() == middle + 1 { + schemas[middle] + } else { + &[] + }, + index: right_stats.map(|(index, ndv)| (arena.index(index), ndv)), + }, + on, + columns, + arena, + ); if let Some(option) = &mut plan.physical_option { - option.row_count = RowCountOption::Estimated(rows); + if option.row_count != (RowCountOption::Estimated { rows, ndv: None }) { + option.row_count = RowCountOption::Estimated { + rows: rows, + ndv: None, + }; + option.startup_work = None; + } } - Some(rows) + Ok(Some(rows)) } else { - let rows = plan.estimated_rows()?; + let Some(rows) = plan.estimated_rows() else { + return Ok(None); + }; schemas.push(plan.output_schema(arena)); - Some(rows) + Ok(Some(rows)) } } @@ -218,7 +617,12 @@ impl ExprVisitorMut for BindPosition<'_> { } impl NormalizationRule for GreedyJoinReorder { - fn apply(&self, plan: &mut LogicalPlan, arena: &mut PlanArena) -> Result { + fn apply( + &self, + plan: &mut LogicalPlan, + arena: &mut PlanArena, + loader: Option<&StatisticMetaLoader<'_>>, + ) -> Result { if !matches!(&plan.operator, Operator::Join(join) if join.join_type == JoinType::Inner) || !can_expand(plan) { @@ -227,7 +631,7 @@ impl NormalizationRule for GreedyJoinReorder { let original_schema = plan.output_schema(arena).clone(); let mut schemas = Vec::new(); let mut keys = Vec::new(); - if inspect(plan, &mut schemas, &mut keys, arena).is_none() || schemas.len() < 3 { + if inspect(plan, &mut schemas, &mut keys, arena, loader)?.is_none() || schemas.len() < 3 { return Ok(false); } // Ambiguous column identities cannot be rebound to an individual input. @@ -269,17 +673,34 @@ impl NormalizationRule for GreedyJoinReorder { for i in 0..inputs.len() { let (prefix, suffix) = inputs.split_at_mut(i + 1); let (left, left_rows) = &mut prefix[i]; - let left_schema = left.output_schema(arena); + let left_stats = index_statistics(left); for (offset, (right, right_rows)) in suffix.iter_mut().enumerate() { let j = i + 1 + offset; - let product = estimate_join_rows(*left_rows, *right_rows); + let right_stats = index_statistics(right); + let columns = column_statistics(left, right, &keys, loader, arena)?; + let left_schema = left.output_schema(arena); + let right_schema = right.output_schema(arena); + let product = join_rows( + &JoinInputStatistics { + rows: *left_rows, + schema: left_schema, + index: left_stats.map(|(index, ndv)| (arena.index(index), ndv)), + }, + &JoinInputStatistics { + rows: *right_rows, + schema: right_schema, + index: right_stats.map(|(index, ndv)| (arena.index(index), ndv)), + }, + &keys, + columns, + arena, + ); if let Some(previous) = best { // Equal row estimates retain the earlier pair in enumeration order. if !previous.cross_join && product >= previous.rows { continue; } } - let right_schema = right.output_schema(arena); let mut connected = false; for &key in &keys { if key_between(key, left_schema, right_schema, arena)?.is_some() { @@ -341,7 +762,10 @@ impl NormalizationRule for GreedyJoinReorder { join.physical_option = Some(PhysicalOption::new( implementation, SortOption::None, - RowCountOption::Estimated(product), + RowCountOption::Estimated { + rows: product, + ndv: None, + }, )); inputs.insert(i, (join, product)); } @@ -357,18 +781,33 @@ impl NormalizationRule for GreedyJoinReorder { mod tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc}; + use crate::expression::BinaryOperator; + use crate::optimizer::core::histogram::{ + HistogramMetaBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR, + }; + use crate::optimizer::heuristic::batch::HepBatchStrategy; + use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; + use crate::optimizer::rule::normalization::NormalizationRuleImpl; + use crate::planner::operator::aggregate::AggregateOperator; use crate::planner::operator::filter::FilterOperator; + use crate::planner::operator::limit::LimitOperator; + use crate::planner::operator::mark_apply::MarkApplyOperator; + use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::TableArenaCell; + use crate::storage::StatisticsMetaCache; + use crate::types::index::IndexInfo; use crate::types::value::DataValue; use crate::types::LogicalType; fn input(name: &str, rows: usize, arena: &mut PlanArena) -> (LogicalPlan, ExprRef) { - let column = arena.alloc_column(ColumnCatalog::new( + let mut column = ColumnCatalog::new( name.into(), false, ColumnDesc::new(LogicalType::Integer, None, false, None).unwrap(), - )); + ); + column.set_ref_table(name.into(), 0, false); + let column = arena.alloc_column(column); let key = arena.alloc_expression(ScalarExpression::column_expr(column, 0)); let mut plan = LogicalPlan::new( Operator::TableScan(TableScanOperator { @@ -383,11 +822,698 @@ mod tests { plan.physical_option = Some(PhysicalOption::new( PlanImpl::SeqScan, SortOption::None, - RowCountOption::Estimated(rows), + RowCountOption::Estimated { + rows: rows, + ndv: None, + }, )); (plan, key) } + fn indexed_input( + name: &str, + rows: usize, + ndv: Option, + arena: &mut PlanArena, + ) -> (LogicalPlan, ExprRef, IndexMetaRef) { + let (mut plan, key) = input(name, rows, arena); + let meta = arena.alloc_index(IndexMeta { + id: 1, + table_name: name.into(), + name: format!("idx_{name}"), + column_ids: vec![0], + ty: IndexType::Normal, + pk_ty: LogicalType::Integer, + value_ty: LogicalType::Integer, + }); + let info = IndexInfo { + meta, + sort_option: SortOption::None, + lookup: None, + residual_predicate: None, + covered_deserializers: None, + cover_mapping: None, + sort_elimination_hint: None, + stream_aggregate_hint: None, + }; + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::IndexScan(Box::new(info)), + SortOption::None, + RowCountOption::Estimated { rows, ndv }, + )); + (plan, key, meta) + } + + #[test] + fn column_statistics_are_read_from_scan_without_an_index() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (mut left, l) = input("l", 1000, &mut arena); + let (mut right, r) = input("r", 200, &mut arena); + let mut cache = StatisticsMetaCache::default(); + for (table, rows) in [("l", 1000), ("r", 200)] { + let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + for value in 0..rows { + builder.append(&DataValue::Int32(value % 100)); + } + cache.columns.insert((table.into(), 0), builder.build()); + } + let loader = StatisticMetaLoader::new(&cache); + assert_eq!( + column_statistics(&left, &right, &[(l, r)], Some(&loader), &arena)?, + Some((100, 100)) + ); + assert_eq!( + column_statistics(&left, &right, &[(l, r)], None, &arena)?, + None + ); + assert_eq!( + column_statistics(&left, &right, &[], Some(&loader), &arena)?, + None + ); + left.physical_option.as_mut().unwrap().row_count = + RowCountOption::Estimated { rows: 8, ndv: None }; + assert_eq!( + column_statistics(&left, &right, &[(l, r)], Some(&loader), &arena)?, + Some((8, 100)) + ); + left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { + rows: 1000, + ndv: None, + }; + let mut nulls = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + nulls.append(&DataValue::Null); + cache.columns.insert(("r".into(), 0), nulls.build()); + assert_eq!( + column_statistics( + &left, + &right, + &[(l, r)], + Some(&StatisticMetaLoader::new(&cache)), + &arena + )?, + Some((100, 0)) + ); + cache.columns.remove(&("r".into(), 0)); + assert_eq!( + column_statistics( + &left, + &right, + &[(l, r)], + Some(&StatisticMetaLoader::new(&cache)), + &arena + )?, + None + ); + let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + for value in 0..200 { + builder.append(&DataValue::Int32(value % 100)); + } + cache.columns.insert(("r".into(), 0), builder.build()); + let loader = StatisticMetaLoader::new(&cache); + let left_schema = left.output_schema(&mut arena); + let right_schema = right.output_schema(&mut arena); + assert_eq!( + join_rows( + &JoinInputStatistics { + rows: 1000, + schema: left_schema, + index: None + }, + &JoinInputStatistics { + rows: 200, + schema: right_schema, + index: None + }, + &[(l, r)], + Some((100, 100)), + &arena + ), + 2000 + ); + let mut join = JoinOperator::build( + left, + right, + JoinCondition::On { + on: vec![(l, r)], + filter: None, + }, + JoinType::Inner, + false, + ); + join.physical_option = Some(PhysicalOption::new( + PlanImpl::HashJoin, + SortOption::None, + RowCountOption::Unknown, + )); + let pipeline = HepOptimizerPipeline::builder() + .after_batch( + "column statistics".into(), + HepBatchStrategy::fix_point_topdown(1), + vec![NormalizationRuleImpl::GreedyJoinReorder], + ) + .build(); + let plan = pipeline + .instantiate(join) + .find_best(Some(&loader), &mut arena)?; + assert_eq!(plan.estimated_rows(), Some(2000)); + Ok(()) + } + + #[test] + fn column_origin_follows_direct_projections_but_stops_at_computation( + ) -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (left, key) = input("l", 10, &mut arena); + let alias = arena.alloc_expression(ScalarExpression::Alias { + expr: key, + alias: AliasType::Name("alias_l".into()), + }); + let output = alias.output_column_ref(&mut arena); + let mut projected = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: vec![alias] }), + Childrens::Only(Box::new(left)), + ); + projected.physical_option = Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )); + assert_eq!( + scan_column(&projected, output, &arena)?, + Some((&"l".into(), 0)) + ); + let alias_target = arena.alloc_expression(ScalarExpression::column_expr(output, 0)); + let alias = arena.alloc_expression(ScalarExpression::Alias { + expr: key, + alias: AliasType::Expr(alias_target), + }); + let Operator::Project(project) = &mut projected.operator else { + unreachable!() + }; + project.exprs[0] = alias; + assert_eq!( + scan_column(&projected, output, &arena)?, + Some((&"l".into(), 0)) + ); + let predicate = + arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(true))); + let mut filtered = LogicalPlan::new( + Operator::Filter(FilterOperator { + predicate, + having: false, + is_optimized: false, + }), + Childrens::Only(Box::new(projected)), + ); + filtered.physical_option = Some(PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Follow, + )); + assert_eq!( + scan_column(&filtered, output, &arena)?, + Some((&"l".into(), 0)) + ); + let (other, other_key) = input("other", 10, &mut arena); + assert_eq!( + scan_column( + &filtered, + other_key.unpack_bound_col(&arena, false).unwrap().0, + &arena + )?, + None + ); + let mut join = JoinOperator::build( + filtered, + other, + JoinCondition::On { + on: vec![(key, other_key)], + filter: None, + }, + JoinType::Inner, + false, + ); + join.physical_option = Some(PhysicalOption::new( + PlanImpl::HashJoin, + SortOption::None, + RowCountOption::Estimated { + rows: 100, + ndv: None, + }, + )); + assert_eq!(scan_column(&join, output, &arena)?, None); + let one = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(1))); + let computed = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Plus, + left_expr: key, + right_expr: one, + ty: LogicalType::Integer, + evaluator: None, + }); + let alias = arena.alloc_expression(ScalarExpression::Alias { + expr: computed, + alias: AliasType::Name("computed".into()), + }); + let output = alias.output_column_ref(&mut arena); + let (source, _) = input("l", 10, &mut arena); + let mut project = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: vec![alias] }), + Childrens::Only(Box::new(source)), + ); + project.physical_option = Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )); + assert_eq!(scan_column(&project, output, &arena)?, None); + let (mut scan, key) = input("limited", 10, &mut arena); + let column = key.unpack_bound_col(&arena, false).unwrap().0; + let Operator::TableScan(op) = &mut scan.operator else { + unreachable!() + }; + op.limit = (Some(1), Some(3)); + assert_eq!(scan_column(&scan, column, &arena)?, None); + let limited = LogicalPlan::new( + Operator::Limit(LimitOperator { + offset: Some(1), + limit: Some(3), + }), + Childrens::Only(Box::new(scan)), + ); + assert_eq!(scan_column(&limited, column, &arena)?, None); + let aggregate = LogicalPlan::new( + Operator::Aggregate(AggregateOperator { + agg_calls: vec![], + groupby_exprs: vec![key], + is_distinct: false, + force_spill: false, + }), + *limited.childrens, + ); + assert_eq!(scan_column(&aggregate, column, &arena)?, None); + Ok(()) + } + + #[test] + fn column_origin_ignores_nested_alias_targets_and_rejects_casts() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (scan, key) = input("l", 10, &mut arena); + let (_, unrelated) = input("other", 10, &mut arena); + let inner_alias = arena.alloc_expression(ScalarExpression::Alias { + expr: key, + alias: AliasType::Expr(unrelated), + }); + let alias = arena.alloc_expression(ScalarExpression::Alias { + expr: inner_alias, + alias: AliasType::Name("nested".into()), + }); + let output = alias.output_column_ref(&mut arena); + let mut project = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: vec![alias] }), + Childrens::Only(Box::new(scan)), + ); + assert_eq!( + scan_column(&project, output, &arena)?, + Some((&"l".into(), 0)) + ); + let cast = arena.alloc_expression(ScalarExpression::TypeCast { + expr: key, + ty: LogicalType::Bigint, + evaluator: None, + }); + let alias = arena.alloc_expression(ScalarExpression::Alias { + expr: cast, + alias: AliasType::Name("casted".into()), + }); + let output = alias.output_column_ref(&mut arena); + let Operator::Project(op) = &mut project.operator else { + unreachable!() + }; + op.exprs[0] = alias; + assert_eq!(scan_column(&project, output, &arena)?, None); + Ok(()) + } + + #[test] + fn column_origin_only_follows_the_preserved_left_input() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (left, key) = input("l", 10, &mut arena); + let (right, other) = input("r", 10, &mut arena); + let marker = arena.alloc_column(ColumnCatalog::new( + "marker".into(), + true, + ColumnDesc::new(LogicalType::Boolean, None, false, None)?, + )); + let mut plan = MarkApplyOperator::build_exists(left, right, marker, vec![]); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::MarkApply, + SortOption::Follow, + RowCountOption::Follow, + )); + let left_column = key.unpack_bound_col(&arena, false).unwrap().0; + let right_column = other.unpack_bound_col(&arena, false).unwrap().0; + assert_eq!( + scan_column(&plan, left_column, &arena)?, + Some((&"l".into(), 0)) + ); + assert_eq!(scan_column(&plan, right_column, &arena)?, None); + assert_eq!(scan_column(&plan, marker, &arena)?, None); + plan.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; + assert_eq!(scan_column(&plan, left_column, &arena)?, None); + Ok(()) + } + + #[test] + fn column_ndv_is_independent_of_an_unrelated_filtering_index() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (mut left, l, index) = indexed_input("l", 12, Some(2), &mut arena); + let (mut right, r) = input("r", 110, &mut arena); + let mut meta = arena.index(index).clone(); + meta.column_ids = vec![1]; + let index = arena.alloc_index(meta); + let PlanImpl::IndexScan(info) = &mut left.physical_option.as_mut().unwrap().plan else { + unreachable!() + }; + info.meta = index; + let mut cache = StatisticsMetaCache::default(); + for (table, ndv) in [("l", 30), ("r", 70)] { + let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + for value in 0..ndv { + builder.append(&DataValue::Int32(value)); + } + cache.columns.insert((table.into(), 0), builder.build()); + } + let loader = StatisticMetaLoader::new(&cache); + let columns = column_statistics(&left, &right, &[(l, r)], Some(&loader), &arena)?; + assert_eq!(columns, Some((12, 70))); + let left_schema = left.output_schema(&mut arena); + let right_schema = right.output_schema(&mut arena); + assert_eq!( + join_rows( + &JoinInputStatistics { + rows: 12, + schema: left_schema, + index: Some((arena.index(index), Some(2))) + }, + &JoinInputStatistics { + rows: 110, + schema: right_schema, + index: None + }, + &[(l, r)], + columns, + &arena + ), + 19 + ); + Ok(()) + } + + #[test] + fn ordinary_join_keys_use_the_strongest_single_divisor() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (mut left, l) = input("l", 1000, &mut arena); + let (mut right, r) = input("r", 200, &mut arena); + let mut keys = vec![(l, r)]; + let mut extra_keys = Vec::new(); + for (table, plan) in [("l", &mut left), ("r", &mut right)] { + let mut column = ColumnCatalog::new( + "extra".into(), + false, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, + ); + column.set_ref_table(table.into(), 1, false); + let column = arena.alloc_column(column); + let Operator::TableScan(scan) = &mut plan.operator else { + unreachable!() + }; + scan.columns.push(column); + extra_keys.push(arena.alloc_expression(ScalarExpression::column_expr(column, 1))); + } + keys.push((extra_keys[0], extra_keys[1])); + let mut cache = StatisticsMetaCache::default(); + for (table, id, ndv) in [("l", 0, 100), ("r", 0, 100), ("l", 1, 80), ("r", 1, 70)] { + let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + for value in 0..ndv { + builder.append(&DataValue::Int32(value)); + } + cache.columns.insert((table.into(), id), builder.build()); + } + let columns = column_statistics( + &left, + &right, + &keys, + Some(&StatisticMetaLoader::new(&cache)), + &arena, + )?; + assert_eq!(columns, Some((100, 100))); + assert_eq!( + estimate_join_rows( + JoinInputEstimate { + rows: 1000, + ndv: Some(100), + unique: false + }, + JoinInputEstimate { + rows: 200, + ndv: Some(100), + unique: false + } + ), + 2000 + ); + keys.reverse(); + assert_eq!( + column_statistics( + &left, + &right, + &keys, + Some(&StatisticMetaLoader::new(&cache)), + &arena + )?, + columns + ); + Ok(()) + } + + #[test] + fn complete_index_keys_reduce_rows_and_bound_uniqueness() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (mut left, l, li) = indexed_input("l", 1000, Some(100), &mut arena); + let (mut right, r, ri) = indexed_input("r", 200, Some(100), &mut arena); + let one = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(1))); + let expr_key = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Plus, + left_expr: l, + right_expr: one, + evaluator: None, + ty: LogicalType::Integer, + }); + let left_schema = left.output_schema(&mut arena); + let right_schema = right.output_schema(&mut arena); + let l_input = JoinInputStatistics { + rows: 1000, + schema: &left_schema, + index: Some((arena.index(li), Some(100))), + }; + let r_input = JoinInputStatistics { + rows: 200, + schema: &right_schema, + index: Some((arena.index(ri), Some(100))), + }; + assert_eq!(join_rows(&l_input, &r_input, &[(l, r)], None, &arena), 2000); + assert_eq!(join_rows(&l_input, &r_input, &[], None, &arena), 200000); + let mut unique_right = arena.index(ri).clone(); + unique_right.ty = IndexType::Unique; + let r_input = JoinInputStatistics { + rows: 200, + schema: &right_schema, + index: Some((&unique_right, None)), + }; + assert_eq!(join_rows(&l_input, &r_input, &[(l, r)], None, &arena), 1000); + let mut unique_left = arena.index(li).clone(); + unique_left.ty = IndexType::PrimaryKey { is_multiple: false }; + let l_input = JoinInputStatistics { + rows: 1000, + schema: &left_schema, + index: Some((&unique_left, None)), + }; + assert_eq!(join_rows(&l_input, &r_input, &[(l, r)], None, &arena), 200); + unique_right.column_ids.push(1); + let r_input = JoinInputStatistics { + rows: 200, + schema: &right_schema, + index: Some((&unique_right, Some(100))), + }; + let l_input = JoinInputStatistics { + rows: 1000, + schema: &left_schema, + index: Some((arena.index(li), Some(100))), + }; + assert_eq!( + join_rows(&l_input, &r_input, &[(l, r)], None, &arena), + 200000 + ); + let r_input = JoinInputStatistics { + rows: 200, + schema: &right_schema, + index: Some((arena.index(ri), Some(100))), + }; + assert_eq!( + join_rows(&l_input, &r_input, &[(expr_key, r)], None, &arena), + 200000 + ); + Ok(()) + } + + #[test] + fn follow_keeps_index_statistics_but_join_results_do_not() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (left, l, li) = indexed_input("l", 1000, Some(100), &mut arena); + let (right, r, _) = indexed_input("r", 200, Some(100), &mut arena); + let mut left = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: vec![l] }), + Childrens::Only(Box::new(left)), + ); + left.physical_option = Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )); + assert_eq!(index_statistics(&left), Some((li, Some(100)))); + let mut join = JoinOperator::build( + left, + right, + JoinCondition::On { + on: vec![(l, r)], + filter: None, + }, + JoinType::Inner, + false, + ); + join.physical_option = Some(PhysicalOption::new( + PlanImpl::HashJoin, + SortOption::None, + RowCountOption::Unknown, + )); + assert!(!GreedyJoinReorder.apply(&mut join, &mut arena, None)?); + assert_eq!(join.estimated_rows(), Some(2000)); + assert_eq!(index_statistics(&join), None); + Ok(()) + } + + #[test] + fn greedy_uses_ndv_before_pruning_connected_candidates() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (left, l, _) = indexed_input("l", 1000, Some(100), &mut arena); + let (right, r, _) = indexed_input("r", 200, Some(100), &mut arena); + let (third, c) = input("c", 20, &mut arena); + let mut lr = JoinOperator::build( + left, + right, + JoinCondition::On { + on: vec![(l, r)], + filter: None, + }, + JoinType::Inner, + false, + ); + lr.physical_option = Some(PhysicalOption::new( + PlanImpl::HashJoin, + SortOption::None, + RowCountOption::Unknown, + )); + let mut plan = JoinOperator::build( + lr, + third, + JoinCondition::On { + on: vec![(r, c)], + filter: None, + }, + JoinType::Inner, + false, + ); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::HashJoin, + SortOption::None, + RowCountOption::Unknown, + )); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); + let (lr, _) = plan.childrens.twins(); + assert_eq!(lr.referenced_table(), vec!["l".into(), "r".into()]); + assert_eq!(lr.estimated_rows(), Some(2000)); + assert_eq!(index_statistics(lr), None); + assert_eq!(plan.estimated_rows(), Some(40000)); + Ok(()) + } + + #[test] + fn composite_ndv_requires_the_whole_key() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (_left, l, li) = indexed_input("l", 1000, Some(100), &mut arena); + let (_right, r, ri) = indexed_input("r", 200, Some(100), &mut arena); + let mut lc = ColumnCatalog::new( + "l_second".into(), + false, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, + ); + lc.set_ref_table("l".into(), 1, false); + let lc = arena.alloc_column(lc); + let mut rc = ColumnCatalog::new( + "r_second".into(), + false, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, + ); + rc.set_ref_table("r".into(), 1, false); + let rc = arena.alloc_column(rc); + let l2 = arena.alloc_expression(ScalarExpression::column_expr(lc, 1)); + let r2 = arena.alloc_expression(ScalarExpression::column_expr(rc, 1)); + let mut l_index = arena.index(li).clone(); + let mut r_index = arena.index(ri).clone(); + for index in [&mut l_index, &mut r_index] { + index.column_ids.push(1); + index.ty = IndexType::Composite; + index.value_ty = LogicalType::Tuple(vec![LogicalType::Integer, LogicalType::Integer]); + } + let ScalarExpression::ColumnRef { column: l_col, .. } = arena.expression(l) else { + unreachable!() + }; + let ScalarExpression::ColumnRef { column: r_col, .. } = arena.expression(r) else { + unreachable!() + }; + let l_schema = [*l_col, lc]; + let r_schema = [*r_col, rc]; + let l_input = JoinInputStatistics { + rows: 1000, + schema: &l_schema, + index: Some((&l_index, Some(100))), + }; + let r_input = JoinInputStatistics { + rows: 200, + schema: &r_schema, + index: Some((&r_index, Some(100))), + }; + assert_eq!( + join_rows(&l_input, &r_input, &[(l, r)], None, &arena), + 200000 + ); + assert_eq!( + join_rows(&l_input, &r_input, &[(l, r), (l2, r2)], None, &arena), + 2000 + ); + Ok(()) + } + fn materialized_right(arena: &mut PlanArena) -> LogicalPlan { let (b, b_key) = input("b", 10, arena); let (c, c_key) = input("c", 20, arena); @@ -404,7 +1530,10 @@ mod tests { join.physical_option = Some(PhysicalOption::new( PlanImpl::HashJoin, SortOption::None, - RowCountOption::Unknown, + RowCountOption::Estimated { + rows: 200, + ndv: None, + }, )); let schema = join.output_schema(arena); let exprs = schema @@ -426,24 +1555,183 @@ mod tests { project } + #[test] + fn startup_estimates_follow_inputs_and_final_implementation() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (mut scan, key) = input("source", 16, &mut arena); + assert_eq!(scan.estimated_startup_work()?, Some(0)); + assert_eq!(scan.estimated_work()?, Some(64)); + let mut sort = LogicalPlan::new( + Operator::Sort(SortOperator { + sort_fields: vec![SortField { + expr: key, + asc: true, + nulls_first: false, + }], + }), + Childrens::Only(Box::new(scan.clone())), + ); + sort.physical_option = Some(PhysicalOption::new( + PlanImpl::Sort, + SortOption::None, + RowCountOption::Follow, + )); + assert_eq!(sort.estimated_startup_work()?, Some(192)); + assert_eq!(sort.estimated_work()?, Some(256)); + let mut project = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: vec![key] }), + Childrens::Only(Box::new(sort)), + ); + project.physical_option = Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )); + assert_eq!(project.estimated_startup_work()?, Some(192)); + assert_eq!(project.estimated_work()?, Some(256)); + + let mut aggregate = LogicalPlan::new( + Operator::Aggregate(AggregateOperator { + agg_calls: vec![key], + groupby_exprs: vec![key], + is_distinct: false, + force_spill: false, + }), + Childrens::Only(Box::new(scan)), + ); + aggregate.physical_option = Some(PhysicalOption::new( + PlanImpl::HashAggregate, + SortOption::None, + RowCountOption::Unknown, + )); + assert_eq!(aggregate.estimated_startup_work()?, Some(160)); + assert_eq!(aggregate.estimated_work()?, None); + aggregate.physical_option = Some(PhysicalOption::new( + PlanImpl::StreamAggregate, + SortOption::None, + RowCountOption::Unknown, + )); + assert_eq!(aggregate.estimated_startup_work()?, Some(0)); + Ok(()) + } + + #[test] + fn startup_unknown_and_overflow_do_not_become_zero() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (mut scan, key) = input("source", 16, &mut arena); + scan.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; + let mut sort = LogicalPlan::new( + Operator::Sort(SortOperator { + sort_fields: vec![SortField { + expr: key, + asc: true, + nulls_first: false, + }], + }), + Childrens::Only(Box::new(scan)), + ); + sort.physical_option = Some(PhysicalOption::new( + PlanImpl::Sort, + SortOption::None, + RowCountOption::Follow, + )); + assert_eq!(sort.estimated_startup_work()?, None); + assert_eq!(sort.estimated_work()?, None); + + let (a, _) = input("a", usize::MAX, &mut arena); + let (b, _) = input("b", 2, &mut arena); + let mut cross = JoinOperator::build(a, b, JoinCondition::None, JoinType::Cross, false); + cross.physical_option = Some(PhysicalOption::new( + PlanImpl::NestLoopJoin, + SortOption::None, + RowCountOption::Unknown, + )); + assert_eq!(cross.estimated_work()?, Some(usize::MAX)); + assert_eq!(cross.estimated_rows(), Some(usize::MAX)); + assert!(!OrientNestedLoopJoin.apply(&mut cross, &mut arena, None)?); + Ok(()) + } + + #[test] + fn nested_loop_cost_combines_startup_and_driver_rows() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + for (left_rows, right_rows, swap) in [ + (100, 1, true), + (1, 100, false), + (10, 10, false), + (10, 0, true), + (0, 10, false), + ] { + let (left, _) = input("left", left_rows, &mut arena); + let (right, _) = input("right", right_rows, &mut arena); + let mut plan = + JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::NestLoopJoin, + SortOption::None, + RowCountOption::Unknown, + )); + plan.estimated_startup_work()?; + assert_eq!( + plan.estimated_rows(), + Some(left_rows.saturating_mul(right_rows)) + ); + let schema = plan.output_schema(&mut arena).clone(); + assert_eq!( + OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?, + swap + ); + assert_eq!(plan.output_schema(&mut arena), &schema); + } + + // A large startup does not outweigh a driver that already executes the + // right side once. Missing HashJoin output rows remain unknown as well. + for unknown in [false, true] { + let (left, _) = input("single", 1, &mut arena); + let mut right = materialized_right(&mut arena); + if unknown { + let Childrens::Only(join) = right.childrens.as_mut() else { + unreachable!() + }; + join.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; + join.reset_output_schema_cache(); + } + let mut plan = + JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::NestLoopJoin, + SortOption::None, + RowCountOption::Unknown, + )); + assert!(!OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); + } + Ok(()) + } + #[test] fn nested_loop_moves_materialization_left_and_restores_positions() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); - let (left, _) = input("a", 100, &mut arena); - let right = materialized_right(&mut arena); - assert!(!left.has_materialization()); - assert!(right.has_materialization()); + let (mut left, _) = input("a", 100, &mut arena); + let mut right = materialized_right(&mut arena); + assert_eq!(left.estimated_startup_work()?, Some(0)); + assert_eq!(right.estimated_startup_work()?, Some(90)); let mut plan = JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); plan.physical_option = Some(PhysicalOption::new( PlanImpl::NestLoopJoin, SortOption::None, - RowCountOption::Estimated(20000), + RowCountOption::Estimated { + rows: 20000, + ndv: None, + }, )); let schema = plan.output_schema(&mut arena).clone(); - assert!(OrientNestedLoopJoin.apply(&mut plan, &mut arena)?); + assert!(OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); assert_eq!(plan.estimated_rows(), Some(20000)); let Operator::Project(project) = &plan.operator else { @@ -460,7 +1748,7 @@ mod tests { assert_eq!(left.referenced_table(), vec!["b".into(), "c".into()]); assert_eq!(right.referenced_table(), vec!["a".into()]); let mut join = join.clone(); - assert!(!OrientNestedLoopJoin.apply(&mut join, &mut arena)?); + assert!(!OrientNestedLoopJoin.apply(&mut join, &mut arena, None)?); Ok(()) } @@ -496,7 +1784,7 @@ mod tests { RowCountOption::Unknown, )); let original = plan.clone(); - assert!(!OrientNestedLoopJoin.apply(&mut plan, &mut arena)?); + assert!(!OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); assert_eq!(plan, original); } Ok(()) @@ -537,7 +1825,7 @@ mod tests { let mut arena = PlanArena::new(&table_arena); let mut plan = three_inputs(&mut arena); let schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); assert_eq!(plan.estimated_rows(), Some(200000)); let (left, right) = plan.childrens.twins(); @@ -589,7 +1877,7 @@ mod tests { false, ); let schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); let Operator::Project(project) = &plan.operator else { unreachable!() @@ -627,12 +1915,18 @@ mod tests { let mut plan = three_inputs(&mut arena); if let Childrens::Twins { left, .. } = plan.childrens.as_mut() { if let Childrens::Twins { left, right } = left.childrens.as_mut() { - left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated(10); - right.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated(1000); + left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { + rows: 10, + ndv: None, + }; + right.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { + rows: 1000, + ndv: None, + }; } } let schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); let (left, right) = plan.childrens.twins(); assert_eq!(left.referenced_table(), vec!["a".into(), "b".into()]); @@ -660,7 +1954,7 @@ mod tests { let (c, c_key) = input("c", 20, &mut arena); let mut cross = JoinOperator::build(a, b, JoinCondition::None, JoinType::Cross, false); let original = cross.clone(); - assert!(!GreedyJoinReorder.apply(&mut cross, &mut arena)?); + assert!(!GreedyJoinReorder.apply(&mut cross, &mut arena, None)?); assert_eq!(cross, original); let b_column = b_key.output_column_ref(&mut arena); let b_key = arena.alloc_expression(ScalarExpression::column_expr(b_column, 1)); @@ -675,7 +1969,7 @@ mod tests { false, ); let schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); let (left, right) = plan.childrens.twins(); assert_eq!(left.referenced_table(), vec!["a".into()]); @@ -717,7 +2011,7 @@ mod tests { false, ); let schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); let join = plan.childrens.only(); assert!( @@ -768,7 +2062,7 @@ mod tests { let Childrens::Only(join) = plan.childrens.as_mut() else { unreachable!() }; - assert!(GreedyJoinReorder.apply(join, &mut arena)?); + assert!(GreedyJoinReorder.apply(join, &mut arena, None)?); assert_eq!(plan.operator, original_parent); assert_eq!(plan.output_schema(&mut arena), &schema); let (_, right) = plan.childrens.only().childrens.twins(); @@ -788,29 +2082,35 @@ mod tests { right.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; } let original = plan.clone(); - assert!(!GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert!(!GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan, original); if let Operator::Join(op) = &mut plan.operator { op.join_type = JoinType::LeftOuter; } let original = plan.clone(); - assert!(!GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert!(!GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan, original); let mut plan = three_inputs(&mut arena); if let Childrens::Twins { left, right } = plan.childrens.as_mut() { - right.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated(10); + right.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { + rows: 10, + ndv: None, + }; if let Childrens::Twins { left, .. } = left.childrens.as_mut() { - left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated(10); + left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { + rows: 10, + ndv: None, + }; } } let schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena)?); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); let (left, right) = plan.childrens.twins(); assert_eq!(left.referenced_table(), vec!["a".into(), "b".into()]); assert_eq!(right.referenced_table(), vec!["c".into()]); - assert_eq!(estimate_join_rows(usize::MAX, 2), usize::MAX); - assert_eq!(estimate_join_rows(0, 20), 0); + assert_eq!(usize::MAX.saturating_mul(2), usize::MAX); + assert_eq!(0_usize.saturating_mul(20), 0); Ok(()) } } diff --git a/src/optimizer/rule/normalization/materialize_mark_apply.rs b/src/optimizer/rule/normalization/materialize_mark_apply.rs index 2bc131c6..c1d310ae 100644 --- a/src/optimizer/rule/normalization/materialize_mark_apply.rs +++ b/src/optimizer/rule/normalization/materialize_mark_apply.rs @@ -14,6 +14,7 @@ use super::NormalizationRule; use crate::errors::DatabaseError; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyOperator}; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan, PlanArena}; @@ -30,7 +31,12 @@ impl MaterializeMarkApply { } impl NormalizationRule for MaterializeMarkApply { - fn apply(&self, plan: &mut LogicalPlan, _: &mut PlanArena) -> Result { + fn apply( + &self, + plan: &mut LogicalPlan, + _: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, + ) -> Result { let (Operator::MarkApply(op), Childrens::Twins { left, right }) = (&mut plan.operator, plan.childrens.as_ref()) else { @@ -118,32 +124,38 @@ mod tests { left.physical_option = Some(PhysicalOption::new( PlanImpl::Values, SortOption::None, - RowCountOption::Estimated(left_rows), + RowCountOption::Estimated { + rows: left_rows, + ndv: None, + }, )); right.physical_option = Some(PhysicalOption::new( PlanImpl::Values, SortOption::None, - RowCountOption::Estimated(right_rows), + RowCountOption::Estimated { + rows: right_rows, + ndv: None, + }, )); assert_eq!( - MaterializeMarkApply.apply(&mut plan, &mut arena)?, + MaterializeMarkApply.apply(&mut plan, &mut arena, None)?, materialize ); } - assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena)?); + assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena, None)?); let Operator::MarkApply(op) = &mut plan.operator else { unreachable!() }; assert!(op.materialize); op.materialize = false; op.set_parameterized_probe(Some(predicate)); - assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena)?); + assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena, None)?); let Operator::MarkApply(op) = &mut plan.operator else { unreachable!() }; op.set_parameterized_probe(None); op.predicates.push(predicate); - assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena)?); + assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena, None)?); let Operator::MarkApply(op) = &mut plan.operator else { unreachable!() }; @@ -152,7 +164,7 @@ mod tests { unreachable!() }; right.physical_option = None; - assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena)?); + assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena, None)?); Ok(()) } } diff --git a/src/optimizer/rule/normalization/min_max_top_k.rs b/src/optimizer/rule/normalization/min_max_top_k.rs index 28bfae1c..bbf062e1 100644 --- a/src/optimizer/rule/normalization/min_max_top_k.rs +++ b/src/optimizer/rule/normalization/min_max_top_k.rs @@ -16,11 +16,12 @@ use crate::errors::DatabaseError; use crate::expression::agg::AggKind; use crate::expression::ScalarExpression; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::{only_child, wrap_child_with}; use crate::planner::operator::sort::SortField; use crate::planner::operator::top_k::TopKOperator; use crate::planner::operator::Operator; -use crate::planner::LogicalPlan; +use crate::planner::{LogicalPlan, PlanArena}; pub struct MinMaxToTopK; @@ -28,7 +29,8 @@ impl NormalizationRule for MinMaxToTopK { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let Operator::Aggregate(op) = &plan.operator else { return Ok(false); @@ -119,7 +121,7 @@ mod tests { let mut plan = table_state.plan_with_arena("select min(c1) from t1", &mut arena)?; let agg_plan = find_aggregate_mut(&mut plan); - assert!(MinMaxToTopK.apply(agg_plan, &mut arena)?); + assert!(MinMaxToTopK.apply(agg_plan, &mut arena, None)?); let agg_plan = find_aggregate(&plan); let Operator::Aggregate(op) = &agg_plan.operator else { @@ -157,7 +159,7 @@ mod tests { let mut plan = table_state.plan_with_arena("select max(c2) from t1", &mut arena)?; let agg_plan = find_aggregate_mut(&mut plan); - assert!(MinMaxToTopK.apply(agg_plan, &mut arena)?); + assert!(MinMaxToTopK.apply(agg_plan, &mut arena, None)?); let agg_plan = find_aggregate(&plan); let child = match agg_plan.childrens.as_ref() { diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index 3b4159ba..45097a4a 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -16,6 +16,7 @@ use crate::errors::DatabaseError; use crate::expression::visitor_mut::ExprVisitorMut; use crate::expression::{AliasType, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::rule::normalization::column_pruning::ColumnPruning; use crate::optimizer::rule::normalization::combine_operators::{ CollapseGroupByAgg, CollapseProject, CombineFilter, @@ -34,7 +35,7 @@ use crate::optimizer::rule::normalization::pushdown_predicates::{ use crate::optimizer::rule::normalization::simplification::ConstantCalculation; use crate::optimizer::rule::normalization::simplification::SimplifyFilter; use crate::optimizer::rule::normalization::top_k::TopK; -use crate::planner::{ExprRef, LogicalPlan}; +use crate::planner::{ExprRef, LogicalPlan, PlanArena}; use std::collections::HashSet; mod column_pruning; mod combine_operators; @@ -209,43 +210,60 @@ impl NormalizationRule for NormalizationRuleImpl { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { match self { - NormalizationRuleImpl::ColumnPruning => ColumnPruning.apply(plan, arena), - NormalizationRuleImpl::GreedyJoinReorder => GreedyJoinReorder.apply(plan, arena), - NormalizationRuleImpl::OrientNestedLoopJoin => OrientNestedLoopJoin.apply(plan, arena), - NormalizationRuleImpl::GuardMarkApply => GuardMarkApply.apply(plan, arena), - NormalizationRuleImpl::CollapseProject => CollapseProject.apply(plan, arena), - NormalizationRuleImpl::CollapseGroupByAgg => CollapseGroupByAgg.apply(plan, arena), - NormalizationRuleImpl::CombineFilter => CombineFilter.apply(plan, arena), + NormalizationRuleImpl::ColumnPruning => ColumnPruning.apply(plan, arena, loader), + NormalizationRuleImpl::GreedyJoinReorder => { + GreedyJoinReorder.apply(plan, arena, loader) + } + NormalizationRuleImpl::OrientNestedLoopJoin => { + OrientNestedLoopJoin.apply(plan, arena, loader) + } + NormalizationRuleImpl::GuardMarkApply => GuardMarkApply.apply(plan, arena, loader), + NormalizationRuleImpl::CollapseProject => CollapseProject.apply(plan, arena, loader), + NormalizationRuleImpl::CollapseGroupByAgg => { + CollapseGroupByAgg.apply(plan, arena, loader) + } + NormalizationRuleImpl::CombineFilter => CombineFilter.apply(plan, arena, loader), NormalizationRuleImpl::LimitProjectTranspose => { - LimitProjectTranspose.apply(plan, arena) + LimitProjectTranspose.apply(plan, arena, loader) + } + NormalizationRuleImpl::PushLimitThroughJoin => { + PushLimitThroughJoin.apply(plan, arena, loader) + } + NormalizationRuleImpl::PushLimitIntoTableScan => { + PushLimitIntoScan.apply(plan, arena, loader) } - NormalizationRuleImpl::PushLimitThroughJoin => PushLimitThroughJoin.apply(plan, arena), - NormalizationRuleImpl::PushLimitIntoTableScan => PushLimitIntoScan.apply(plan, arena), NormalizationRuleImpl::PushPredicateThroughJoin => { - PushPredicateThroughJoin.apply(plan, arena) + PushPredicateThroughJoin.apply(plan, arena, loader) } NormalizationRuleImpl::PushJoinPredicateIntoScan => { - PushJoinPredicateIntoScan.apply(plan, arena) + PushJoinPredicateIntoScan.apply(plan, arena, loader) } - NormalizationRuleImpl::SimplifyFilter => SimplifyFilter.apply(plan, arena), + NormalizationRuleImpl::SimplifyFilter => SimplifyFilter.apply(plan, arena, loader), NormalizationRuleImpl::PushPredicateIntoScan => { - PushPredicateIntoScan.apply(plan, arena) + PushPredicateIntoScan.apply(plan, arena, loader) + } + NormalizationRuleImpl::ConstantCalculation => { + ConstantCalculation.apply(plan, arena, loader) + } + NormalizationRuleImpl::EvaluatorBind => EvaluatorBind.apply(plan, arena, loader), + NormalizationRuleImpl::MinMaxToTopK => MinMaxToTopK.apply(plan, arena, loader), + NormalizationRuleImpl::TopK => TopK.apply(plan, arena, loader), + NormalizationRuleImpl::EliminateIndexFilter => { + EliminateIndexFilter.apply(plan, arena, loader) } - NormalizationRuleImpl::ConstantCalculation => ConstantCalculation.apply(plan, arena), - NormalizationRuleImpl::EvaluatorBind => EvaluatorBind.apply(plan, arena), - NormalizationRuleImpl::MinMaxToTopK => MinMaxToTopK.apply(plan, arena), - NormalizationRuleImpl::TopK => TopK.apply(plan, arena), - NormalizationRuleImpl::EliminateIndexFilter => EliminateIndexFilter.apply(plan, arena), NormalizationRuleImpl::ParameterizeInnerJoin => { - ParameterizeInnerJoin.apply(plan, arena) + ParameterizeInnerJoin.apply(plan, arena, loader) } NormalizationRuleImpl::ParameterizeMarkApply => { - ParameterizeMarkApply.apply(plan, arena) + ParameterizeMarkApply.apply(plan, arena, loader) + } + NormalizationRuleImpl::MaterializeMarkApply => { + MaterializeMarkApply.apply(plan, arena, loader) } - NormalizationRuleImpl::MaterializeMarkApply => MaterializeMarkApply.apply(plan, arena), } } } diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 295add70..9374ed68 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -17,6 +17,7 @@ use crate::errors::DatabaseError; use crate::expression::visitor_mut::{ExprVisitorMut, PositionShift}; use crate::expression::{BinaryOperator, ScalarExpression, TypeCast}; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::join::{JoinCondition, JoinType}; use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyOperator, MarkApplyQuantifier}; @@ -36,7 +37,8 @@ impl NormalizationRule for ParameterizeMarkApply { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let (op, new_probe) = match (&mut plan.operator, plan.childrens.as_mut()) { (Operator::MarkApply(op), Childrens::Twins { left, right }) @@ -348,6 +350,7 @@ impl NormalizationRule for ParameterizeInnerJoin { &self, plan: &mut LogicalPlan, arena: &mut PlanArena<'_>, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let (Operator::Join(join), Childrens::Twins { left, right }) = (&mut plan.operator, plan.childrens.as_mut()) @@ -573,7 +576,7 @@ mod tests { let probe = arena.alloc_expression(ScalarExpression::from(2)); let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::None); - assert!(!ParameterizeMarkApply.apply(&mut plan, &mut arena)?); + assert!(!ParameterizeMarkApply.apply(&mut plan, &mut arena, None)?); assert!(parameterize_right_subtree(&mut plan, &column, probe, &mut arena)?.is_none()); let mut filter = LogicalPlan::new( @@ -936,7 +939,7 @@ mod inner_join_tests { let right = FilterOperator::build(predicate, right, false); let mut plan = join(left, right, vec![(l0, r1), (l1, r2)], None); - assert!(ParameterizeInnerJoin.apply(&mut plan, &mut arena)?); + assert!(ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); let Operator::MarkApply(apply) = &plan.operator else { panic!("expected apply without projection") }; @@ -988,7 +991,7 @@ mod inner_join_tests { let predicate = binary(&mut arena, BinaryOperator::Gt, filter_left, filter_right); let mut plan = join(left, right, vec![(l, r)], Some(predicate)); - assert!(ParameterizeInnerJoin.apply(&mut plan, &mut arena)?); + assert!(ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); let Operator::Filter(filter) = &plan.operator else { panic!("join filter must remain above projection") @@ -1077,7 +1080,7 @@ mod inner_join_tests { } let before = plan.clone(); assert!( - !ParameterizeInnerJoin.apply(&mut plan, &mut arena)?, + !ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?, "{case}" ); assert_eq!(plan, before, "{case}"); diff --git a/src/optimizer/rule/normalization/pushdown_limit.rs b/src/optimizer/rule/normalization/pushdown_limit.rs index e33dc91c..011a7251 100644 --- a/src/optimizer/rule/normalization/pushdown_limit.rs +++ b/src/optimizer/rule/normalization/pushdown_limit.rs @@ -14,11 +14,12 @@ use crate::errors::DatabaseError; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::{only_child_mut, replace_with_only_child, wrap_child_with}; use crate::planner::operator::join::JoinType; use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::Operator; -use crate::planner::LogicalPlan; +use crate::planner::{LogicalPlan, PlanArena}; pub struct LimitProjectTranspose; @@ -26,7 +27,8 @@ impl NormalizationRule for LimitProjectTranspose { fn apply( &self, plan: &mut LogicalPlan, - _: &mut crate::planner::PlanArena, + _: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let operator = std::mem::replace(&mut plan.operator, Operator::Dummy); @@ -71,7 +73,8 @@ impl NormalizationRule for PushLimitThroughJoin { fn apply( &self, plan: &mut LogicalPlan, - _: &mut crate::planner::PlanArena, + _: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let limit_op = match &plan.operator { Operator::Limit(LimitOperator { @@ -110,7 +113,8 @@ impl NormalizationRule for PushLimitIntoScan { fn apply( &self, plan: &mut LogicalPlan, - _: &mut crate::planner::PlanArena, + _: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let (offset, limit) = match &plan.operator { Operator::Limit(limit_op) => (limit_op.offset, limit_op.limit), diff --git a/src/optimizer/rule/normalization/pushdown_predicates.rs b/src/optimizer/rule/normalization/pushdown_predicates.rs index eda006b6..8e88f562 100644 --- a/src/optimizer/rule/normalization/pushdown_predicates.rs +++ b/src/optimizer/rule/normalization/pushdown_predicates.rs @@ -17,6 +17,7 @@ use crate::expression::range_detacher::{DetachedPredicate, Range, RangeDetacher} use crate::expression::visitor_mut::{ExprVisitorMut, PositionShift}; use crate::expression::{BinaryOperator, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::{replace_with_only_child, wrap_child_with}; use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::join::extract_join_keys; @@ -133,7 +134,8 @@ impl NormalizationRule for PushPredicateThroughJoin { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let mut applied = false; @@ -298,7 +300,8 @@ impl NormalizationRule for PushPredicateIntoScan { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let LogicalPlan { operator, @@ -472,7 +475,8 @@ impl NormalizationRule for PushJoinPredicateIntoScan { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let (join_type, filter_expr) = { let Operator::Join(join_op) = &mut plan.operator else { diff --git a/src/optimizer/rule/normalization/simplification.rs b/src/optimizer/rule/normalization/simplification.rs index 9de97e4e..b6e55058 100644 --- a/src/optimizer/rule/normalization/simplification.rs +++ b/src/optimizer/rule/normalization/simplification.rs @@ -16,8 +16,10 @@ use crate::errors::DatabaseError; use crate::expression::simplify::{ConstantCalculator, Simplify}; use crate::expression::visitor_mut::ExprVisitorMut; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; use crate::planner::operator::Operator; +use crate::planner::PlanArena; use crate::planner::{Childrens, LogicalPlan}; #[derive(Copy, Clone)] @@ -54,7 +56,8 @@ impl NormalizationRule for ConstantCalculation { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { Self::_apply(plan, arena)?; Ok(true) @@ -82,7 +85,8 @@ impl NormalizationRule for SimplifyFilter { fn apply( &self, plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { if let Operator::Filter(filter_op) = &mut plan.operator { if filter_op.is_optimized { diff --git a/src/optimizer/rule/normalization/top_k.rs b/src/optimizer/rule/normalization/top_k.rs index 73cd74a1..7b7fbc64 100644 --- a/src/optimizer/rule/normalization/top_k.rs +++ b/src/optimizer/rule/normalization/top_k.rs @@ -14,10 +14,11 @@ use crate::errors::DatabaseError; use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::{only_child_mut, replace_with_only_child}; use crate::planner::operator::top_k::TopKOperator; use crate::planner::operator::Operator; -use crate::planner::LogicalPlan; +use crate::planner::{LogicalPlan, PlanArena}; pub struct TopK; @@ -25,7 +26,8 @@ impl NormalizationRule for TopK { fn apply( &self, plan: &mut LogicalPlan, - _: &mut crate::planner::PlanArena, + _: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let (offset, limit) = match &plan.operator { Operator::Limit(op) => match op.limit { diff --git a/src/planner/arena.rs b/src/planner/arena.rs index 8df83e70..fad10836 100644 --- a/src/planner/arena.rs +++ b/src/planner/arena.rs @@ -1211,6 +1211,7 @@ impl MetaArena for ParamArena<'_> { mod tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc}; + use crate::planner::Childrens; use crate::types::index::{IndexMeta, IndexType}; use crate::types::LogicalType; use std::panic::{catch_unwind, AssertUnwindSafe}; @@ -1238,7 +1239,6 @@ mod tests { #[test] fn materializing_scalar_references_preserves_shared_catalog_ids() -> Result<(), DatabaseError> { use crate::planner::operator::project::ProjectOperator; - use crate::planner::Childrens; let catalog = TableArenaCell::default(); let persistent = catalog.borrow_mut().alloc_scalar_query_ref(false); let mut arena = PlanArena::new(&catalog); diff --git a/src/planner/mod.rs b/src/planner/mod.rs index 04147229..d187412a 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -20,12 +20,13 @@ mod scalar_query_ref; use crate::catalog::TableName; use crate::errors::DatabaseError; use crate::expression::visitor_mut::ExprCloner; +use crate::planner::operator::join::JoinType; use crate::planner::operator::recursive_cte::{RecursiveCteOperator, RecursiveScanOperator}; use crate::planner::operator::set_membership::SetMembershipOperator; use crate::planner::operator::union::UnionOperator; use crate::planner::operator::values::ValuesOperator; use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; -use crate::planner::operator::{Operator, PhysicalOption, RowCountOption}; +use crate::planner::operator::{Operator, PhysicalOption, RowCountOption, CPU_ROW_WORK}; use kite_sql_serde_macros::ReferenceSerialization; use std::fmt; use std::hash::{Hash, Hasher}; @@ -152,6 +153,28 @@ impl<'a> Iterator for ChildrensIter<'a> { } } +struct StartupWorkVisitor; + +impl<'a> OperatorVisitorMut<'a> for StartupWorkVisitor { + fn visit_plan(&mut self, plan: &'a mut LogicalPlan) -> Result<(), DatabaseError> { + let LogicalPlan { + operator, + childrens, + physical_option, + .. + } = plan; + let Some(option) = physical_option.as_mut() else { + return Ok(()); + }; + if option.startup_work.is_some() { + return Ok(()); + } + OperatorVisitorMut::<'_>::visit_children(self, childrens)?; + option.calculate_startup_work(operator, childrens); + Ok(()) + } +} + #[derive(Debug)] pub struct LogicalPlan { pub(crate) operator: Operator, @@ -185,10 +208,33 @@ impl LogicalPlan { std::mem::replace(self, Self::new(Operator::Dummy, Childrens::None)) } + pub(crate) fn row_count_option(&self) -> Option<&PhysicalOption> { + let option = self.physical_option.as_ref()?; + match option.row_count { + RowCountOption::Estimated { .. } => Some(option), + RowCountOption::Unknown => None, + RowCountOption::Follow => match self.childrens.as_ref() { + Childrens::Only(child) => child.row_count_option(), + Childrens::Twins { left, .. } => left.row_count_option(), + Childrens::None => None, + }, + } + } + pub(crate) fn estimated_rows(&self) -> Option { match &self.physical_option.as_ref()?.row_count { - RowCountOption::Estimated(rows) => Some(*rows), - RowCountOption::Unknown => None, + RowCountOption::Estimated { rows, .. } => Some(*rows), + RowCountOption::Unknown => match (&self.operator, self.childrens.as_ref()) { + (Operator::Join(op), Childrens::Twins { left, right }) + if op.join_type == JoinType::Cross && !op.limit_pushed => + { + Some( + left.estimated_rows()? + .saturating_mul(right.estimated_rows()?), + ) + } + _ => None, + }, RowCountOption::Follow => match self.childrens.as_ref() { Childrens::Only(child) => child.estimated_rows(), Childrens::Twins { left, .. } => left.estimated_rows(), @@ -197,12 +243,30 @@ impl LogicalPlan { } } - pub(crate) fn has_materialization(&self) -> bool { - self.physical_option + // Pass-local memoization for NLJ orientation. New physical options start + // empty; orientation preserves child rows/startup, so their cache is reusable. + pub(crate) fn estimated_startup_work(&mut self) -> Result, DatabaseError> { + StartupWorkVisitor.visit_plan(self)?; + Ok(self + .physical_option .as_ref() - .is_some_and(PhysicalOption::materializes) - || matches!(&self.operator, Operator::MarkApply(op) if op.materialize) - || self.childrens.iter().any(Self::has_materialization) + .and_then(|option| option.startup_work.flatten())) + } + + pub(crate) fn estimated_work(&mut self) -> Result, DatabaseError> { + let (Some(startup), Some(rows)) = (self.estimated_startup_work()?, self.estimated_rows()) + else { + return Ok(None); + }; + Ok(Some( + startup.saturating_add(rows.saturating_mul(CPU_ROW_WORK)), + )) + } + + pub(crate) fn clear_startup_work(&mut self) { + if let Some(option) = self.physical_option.as_mut() { + option.startup_work = None; + } } pub fn referenced_table(&self) -> Vec { @@ -544,8 +608,10 @@ impl crate::serdes::ReferenceSerialization for LogicalPlan { mod tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc, ColumnRef}; + use crate::expression::ScalarExpression; use crate::planner::operator::describe::DescribeOperator; use crate::planner::operator::limit::LimitOperator; + use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::{PlanImpl, SortOption}; use crate::types::LogicalType; @@ -586,7 +652,10 @@ mod tests { input.physical_option = Some(PhysicalOption::new( PlanImpl::SeqScan, SortOption::None, - RowCountOption::Estimated(12), + RowCountOption::Estimated { + rows: 12, + ndv: None, + }, )); let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::Only(Box::new(input))); plan.physical_option = Some(PhysicalOption::new( @@ -597,12 +666,91 @@ mod tests { assert_eq!(plan.estimated_rows(), Some(12)); plan.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; assert_eq!(plan.estimated_rows(), None); - plan.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated(0); + plan.physical_option.as_mut().unwrap().row_count = + RowCountOption::Estimated { rows: 0, ndv: None }; assert_eq!(plan.estimated_rows(), Some(0)); plan.physical_option = None; assert_eq!(plan.estimated_rows(), None); } + #[test] + fn startup_cache_reuses_known_and_unknown_estimates_for_the_round() -> Result<(), DatabaseError> + { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let id = column("id", LogicalType::Integer, &mut arena); + let key = arena.alloc_expression(ScalarExpression::column_expr(id, 0)); + let mut scan = table_scan("users", vec![id]); + scan.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Unknown, + )); + let mut sort = LogicalPlan::new( + Operator::Sort(SortOperator { + sort_fields: vec![SortField { + expr: key, + asc: true, + nulls_first: false, + }], + }), + Childrens::Only(Box::new(scan)), + ); + sort.physical_option = Some(PhysicalOption::new( + PlanImpl::Sort, + SortOption::None, + RowCountOption::Follow, + )); + assert_eq!(sort.estimated_startup_work()?, None); + assert_eq!( + sort.physical_option.as_ref().unwrap().startup_work, + Some(None) + ); + assert_eq!(sort.estimated_startup_work()?, None); + assert_eq!( + sort.physical_option.as_ref().unwrap().startup_work, + Some(None) + ); + + // A new physical selection starts the next round with empty caches. + let Childrens::Only(scan) = sort.childrens.as_mut() else { + unreachable!() + }; + scan.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated { + rows: 16, + ndv: None, + }, + )); + sort.physical_option = Some(PhysicalOption::new( + PlanImpl::Sort, + SortOption::None, + RowCountOption::Follow, + )); + assert_eq!(sort.estimated_startup_work()?, Some(192)); + assert_eq!( + sort.physical_option.as_ref().unwrap().startup_work, + Some(Some(192)) + ); + let Childrens::Only(scan) = sort.childrens.as_mut() else { + unreachable!() + }; + assert_eq!( + scan.physical_option.as_ref().unwrap().startup_work, + Some(Some(0)) + ); + // A cached parent does not walk its children again in this round. + scan.clear_startup_work(); + assert_eq!(sort.estimated_startup_work()?, Some(192)); + let Childrens::Only(scan) = sort.childrens.as_mut() else { + unreachable!() + }; + assert_eq!(scan.physical_option.as_ref().unwrap().startup_work, None); + Ok(()) + } + #[test] fn childrens_iterates_and_pops_expected_plans() { let left = LogicalPlan::new(Operator::ShowTable, Childrens::None); diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index af22e300..37db75a7 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -90,9 +90,10 @@ use crate::planner::operator::union::UnionOperator; use crate::planner::operator::update::UpdateOperator; use crate::planner::operator::values::ValuesOperator; use crate::planner::operator::visitor::OperatorVisitor; -use crate::planner::{fmt_explain_list, Explain, ExprRef, MetaArena}; +use crate::planner::{fmt_explain_list, Childrens, Explain, ExprRef, LogicalPlan, MetaArena}; use crate::types::index::{IndexInfo, IndexMetaRef}; use kite_sql_serde_macros::ReferenceSerialization; +use std::hash::{Hash, Hasher}; #[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] pub enum Operator { @@ -156,18 +157,24 @@ pub enum SortOption { None, } -#[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] +#[derive(Debug, PartialEq, Eq, Clone, Hash)] pub enum RowCountOption { Follow, Unknown, - Estimated(usize), + Estimated { rows: usize, ndv: Option }, } -#[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] +// Relative CPU units: a row costs four units, an operator/hash operation one. +// This is the same scale as row = 1 and operator = 0.25; I/O is not modeled. +pub(crate) const CPU_ROW_WORK: usize = 4; +pub(crate) const CPU_OPERATOR_WORK: usize = 1; + +#[derive(Debug, Clone)] pub struct PhysicalOption { pub plan: PlanImpl, pub(crate) sort_option: SortOption, pub(crate) row_count: RowCountOption, + pub(crate) startup_work: Option>, } impl PhysicalOption { @@ -176,6 +183,7 @@ impl PhysicalOption { plan, sort_option, row_count, + startup_work: None, } } @@ -183,16 +191,125 @@ impl PhysicalOption { &self.sort_option } - pub(crate) fn materializes(&self) -> bool { - matches!( - self.plan, - PlanImpl::SimpleAggregate - | PlanImpl::HashAggregate - | PlanImpl::HashJoin - | PlanImpl::Sort - | PlanImpl::TopK - | PlanImpl::Window - ) + pub(crate) fn calculate_startup_work(&mut self, operator: &Operator, childrens: &Childrens) { + let startup_work = + |input: &LogicalPlan| input.physical_option.as_ref()?.startup_work.flatten(); + let work = |input: &LogicalPlan| { + Some( + startup_work(input)? + .saturating_add(input.estimated_rows()?.saturating_mul(CPU_ROW_WORK)), + ) + }; + self.startup_work = Some(match (&self.plan, operator, childrens) { + ( + PlanImpl::SeqScan | PlanImpl::IndexScan(_) | PlanImpl::Values | PlanImpl::Dummy, + _, + Childrens::None, + ) => Some(0), + ( + PlanImpl::Project + | PlanImpl::Filter + | PlanImpl::Limit + | PlanImpl::StreamAggregate + | PlanImpl::StreamDistinct, + _, + Childrens::Only(input), + ) => startup_work(input), + (PlanImpl::HashJoin, Operator::Join(join), Childrens::Twins { left, right }) => { + let keys = match &join.on { + JoinCondition::On { on, .. } => on.len(), + JoinCondition::None => 0, + }; + work(left).and_then(|build| { + Some( + build + .saturating_add(left.estimated_rows()?.saturating_mul( + CPU_ROW_WORK.saturating_add(keys.saturating_mul(CPU_OPERATOR_WORK)), + )) + .saturating_add(startup_work(right)?), + ) + }) + } + (PlanImpl::NestLoopJoin, _, Childrens::Twins { left, right }) => startup_work(left) + .and_then(|left| startup_work(right).map(|right| left.saturating_add(right))), + (PlanImpl::Sort | PlanImpl::TopK | PlanImpl::Window, _, Childrens::Only(input)) => { + let keys = match operator { + Operator::Sort(op) => op.sort_fields.len(), + Operator::TopK(op) => op.sort_fields.len(), + Operator::Window(op) => op.sort_fields.len(), + _ => unreachable!(), + }; + work(input).and_then(|read| { + let rows = input.estimated_rows()?; + let comparisons = if rows <= 1 { + 0 + } else { + ((rows as f64) * (rows as f64).log2()).ceil() as usize + }; + Some( + read.saturating_add( + comparisons.saturating_mul( + 2usize + .saturating_mul(keys) + .saturating_mul(CPU_OPERATOR_WORK), + ), + ), + ) + }) + } + ( + PlanImpl::SimpleAggregate | PlanImpl::HashAggregate, + Operator::Aggregate(op), + Childrens::Only(input), + ) => { + let per_row = op + .agg_calls + .len() + .saturating_mul(CPU_OPERATOR_WORK) + .saturating_add(if matches!(self.plan, PlanImpl::HashAggregate) { + CPU_ROW_WORK.saturating_add( + op.groupby_exprs.len().saturating_mul(CPU_OPERATOR_WORK), + ) + } else { + 0 + }); + work(input).and_then(|read| { + Some(read.saturating_add(input.estimated_rows()?.saturating_mul(per_row))) + }) + } + (PlanImpl::MarkApply, Operator::MarkApply(op), Childrens::Twins { left, right }) => { + startup_work(left).and_then(|left| { + let right = if op.materialize { + work(right) + } else { + startup_work(right) + }?; + Some(left.saturating_add(right)) + }) + } + (PlanImpl::ScalarQueryInit, _, Childrens::Twins { left, right }) => startup_work(left) + .and_then(|left| work(right).map(|right| left.saturating_add(right))), + _ => None, + }); + } +} + +// Startup estimates are pass-local caches, not plan semantics or wire data. +impl PartialEq for PhysicalOption { + fn eq(&self, other: &Self) -> bool { + self.plan == other.plan + && self.sort_option == other.sort_option + && self.row_count == other.row_count + } +} + +impl Eq for PhysicalOption {} + +impl Hash for PhysicalOption { + fn hash(&self, state: &mut H) { + self.plan.hash(state); + self.sort_option.hash(state); + self.row_count.hash(state); } } @@ -590,7 +707,7 @@ impl Operator { f, keep_going: true, }; - visitor.visit_operator(self)?; + visitor.visit_operator(self, None)?; Ok(visitor.keep_going) } diff --git a/src/planner/operator/visitor.rs b/src/planner/operator/visitor.rs index f949c6e3..1525b261 100644 --- a/src/planner/operator/visitor.rs +++ b/src/planner/operator/visitor.rs @@ -16,11 +16,48 @@ use super::alter_table::change_column::DefaultChange; use super::*; use crate::errors::DatabaseError; use crate::expression::visitor::ExprVisitor; -use crate::planner::MetaArena; +use crate::planner::{Childrens, LogicalPlan, MetaArena}; pub trait OperatorVisitor<'a>: Sized { - fn visit_operator(&mut self, operator: &'a Operator) -> Result<(), DatabaseError> { - walk_operator(self, operator) + fn visit_plan(&mut self, plan: &'a LogicalPlan) -> Result<(), DatabaseError> { + let LogicalPlan { + operator, + physical_option, + childrens, + .. + } = plan; + self.visit_operator(operator, physical_option.as_ref())?; + self.visit_children(childrens) + } + + fn visit_children(&mut self, childrens: &'a Childrens) -> Result<(), DatabaseError> { + match childrens { + Childrens::Only(child) => self.visit_plan(child), + Childrens::Twins { left, right } => { + self.visit_plan(left)?; + self.visit_plan(right) + } + Childrens::None => Ok(()), + } + } + + fn visit_operator( + &mut self, + operator: &'a Operator, + physical_option: Option<&'a PhysicalOption>, + ) -> Result<(), DatabaseError> { + walk_operator(self, operator)?; + if let Some(physical_option) = physical_option { + self.visit_physical_option(physical_option)?; + } + Ok(()) + } + + fn visit_physical_option( + &mut self, + _physical_option: &'a PhysicalOption, + ) -> Result<(), DatabaseError> { + Ok(()) } fn visit_dummy(&mut self) -> Result<(), DatabaseError> { @@ -415,6 +452,7 @@ pub(crate) mod tests { use crate::planner::operator::join::{JoinOperator, JoinType}; use crate::planner::operator::mark_apply::MarkApplyOperator; use crate::planner::operator::set_membership::SetMembershipKind; + use crate::planner::operator::visitor_mut::OperatorVisitorMut; use crate::planner::test::PlanArenaTestExt; use crate::planner::ExprRef; use crate::planner::{Childrens, LogicalPlan}; @@ -659,6 +697,150 @@ pub(crate) mod tests { } } + #[test] + fn read_only_and_mutable_visitors_follow_the_same_order() -> Result<(), DatabaseError> { + #[derive(Default)] + struct VisitOrder(Vec<&'static str>); + + impl<'a> OperatorVisitor<'a> for VisitOrder { + fn visit_join(&mut self, _op: &'a JoinOperator) -> Result<(), DatabaseError> { + self.0.push("operator"); + Ok(()) + } + + fn visit_dummy(&mut self) -> Result<(), DatabaseError> { + self.0.push("operator"); + Ok(()) + } + + fn visit_physical_option( + &mut self, + _option: &'a PhysicalOption, + ) -> Result<(), DatabaseError> { + self.0.push("physical"); + Ok(()) + } + } + + impl<'a> OperatorVisitorMut<'a> for VisitOrder { + fn visit_join(&mut self, _op: &'a mut JoinOperator) -> Result<(), DatabaseError> { + self.0.push("operator"); + Ok(()) + } + + fn visit_dummy(&mut self) -> Result<(), DatabaseError> { + self.0.push("operator"); + Ok(()) + } + + fn visit_physical_option( + &mut self, + _option: &'a mut PhysicalOption, + ) -> Result<(), DatabaseError> { + self.0.push("physical"); + Ok(()) + } + } + + let left = LogicalPlan::new(Operator::Dummy, Childrens::None); + let mut right = LogicalPlan::new(Operator::Dummy, Childrens::None); + right.physical_option = Some(PhysicalOption::new( + PlanImpl::Dummy, + SortOption::None, + RowCountOption::Unknown, + )); + let mut plan = + JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::NestLoopJoin, + SortOption::None, + RowCountOption::Unknown, + )); + let mut read_only = VisitOrder::default(); + OperatorVisitor::visit_plan(&mut read_only, &plan)?; + let mut mutable = VisitOrder::default(); + OperatorVisitorMut::visit_plan(&mut mutable, &mut plan)?; + assert_eq!( + read_only.0, + ["operator", "physical", "operator", "operator", "physical"] + ); + assert_eq!(read_only.0, mutable.0); + Ok(()) + } + + #[test] + fn physical_option_error_stops_read_only_plan_traversal() { + struct FailOnPhysical(usize); + + impl<'a> OperatorVisitor<'a> for FailOnPhysical { + fn visit_dummy(&mut self) -> Result<(), DatabaseError> { + self.0 += 1; + Ok(()) + } + + fn visit_physical_option( + &mut self, + _option: &'a PhysicalOption, + ) -> Result<(), DatabaseError> { + Err(DatabaseError::invalid_column("physical")) + } + } + + let child = LogicalPlan::new(Operator::Dummy, Childrens::None); + let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::Only(Box::new(child))); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::Dummy, + SortOption::None, + RowCountOption::Unknown, + )); + let mut visitor = FailOnPhysical(0); + assert!(visitor.visit_plan(&plan).is_err()); + assert_eq!(visitor.0, 1); + } + + #[test] + fn traverses_read_only_plans_in_input_order() -> Result<(), DatabaseError> { + #[derive(Default)] + struct VisitOrder(Vec<&'static str>); + + impl OperatorVisitor<'_> for VisitOrder { + fn visit_project(&mut self, _op: &ProjectOperator) -> Result<(), DatabaseError> { + self.0.push("project"); + Ok(()) + } + + fn visit_join(&mut self, _op: &JoinOperator) -> Result<(), DatabaseError> { + self.0.push("join"); + Ok(()) + } + + fn visit_dummy(&mut self) -> Result<(), DatabaseError> { + self.0.push("dummy"); + Ok(()) + } + } + + let right = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: vec![] }), + Childrens::Only(Box::new(LogicalPlan::new(Operator::Dummy, Childrens::None))), + ); + let join = JoinOperator::build( + LogicalPlan::new(Operator::Dummy, Childrens::None), + right, + JoinCondition::None, + JoinType::Cross, + false, + ); + let plan = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: vec![] }), + Childrens::Only(Box::new(join)), + ); + let mut visitor = VisitOrder::default(); + visitor.visit_plan(&plan)?; + assert_eq!(visitor.0, ["project", "join", "dummy", "project", "dummy"]); + Ok(()) + } + #[test] fn dispatches_all_variants_and_visits_expressions() -> Result<(), DatabaseError> { struct NoopVisitor; @@ -668,13 +850,13 @@ pub(crate) mod tests { let mut arena = crate::planner::PlanArena::new(&table_arena); let operators = all_operators(&mut arena)?; for operator in &operators { - NoopVisitor.visit_operator(operator)?; + NoopVisitor.visit_operator(operator, None)?; } let mut counter = ExpressionCounter::default(); let mut visitor = OperatorExprVisitor::new(&mut counter, &arena); for operator in &operators { - visitor.visit_operator(operator)?; + visitor.visit_operator(operator, None)?; } assert_eq!(counter.0, 22); // Includes the Values row expression. diff --git a/src/planner/operator/visitor_mut.rs b/src/planner/operator/visitor_mut.rs index 12235428..07d51c32 100644 --- a/src/planner/operator/visitor_mut.rs +++ b/src/planner/operator/visitor_mut.rs @@ -32,7 +32,11 @@ pub trait OperatorVisitorMut<'a>: Sized { .. } = plan; self.visit_operator(operator, physical_option.as_mut())?; - match childrens.as_mut() { + self.visit_children(childrens) + } + + fn visit_children(&mut self, childrens: &'a mut Childrens) -> Result<(), DatabaseError> { + match childrens { Childrens::Only(child) => self.visit_plan(child), Childrens::Twins { left, right } => { self.visit_plan(left)?; diff --git a/src/serdes/mod.rs b/src/serdes/mod.rs index 3f4a4626..242cce54 100644 --- a/src/serdes/mod.rs +++ b/src/serdes/mod.rs @@ -29,6 +29,7 @@ mod option; mod pair; mod path_buf; mod phantom; +mod physical_option; mod ptr; mod slice; pub(crate) mod stable_hash; diff --git a/src/serdes/physical_option.rs b/src/serdes/physical_option.rs new file mode 100644 index 00000000..d8e954c3 --- /dev/null +++ b/src/serdes/physical_option.rs @@ -0,0 +1,234 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::errors::DatabaseError; +use crate::planner::operator::{PhysicalOption, PlanImpl, RowCountOption, SortOption}; +use crate::planner::MetaArena; +use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTables}; +use crate::storage::Transaction; +use std::io::{Read, Write}; + +impl ReferenceSerialization for RowCountOption { + fn encode( + &self, + writer: &mut W, + is_direct: bool, + reference_tables: &mut ReferenceTables, + arena: &A, + ) -> Result<(), DatabaseError> { + match self { + Self::Follow => writer.write_all(&[0])?, + Self::Unknown => writer.write_all(&[1])?, + Self::Estimated { rows, .. } => { + writer.write_all(&[2])?; + rows.encode(writer, is_direct, reference_tables, arena)?; + } + } + Ok(()) + } + + fn decode( + reader: &mut R, + context: Option<&ReferenceDecodeContext<'_, T>>, + reference_tables: &ReferenceTables, + arena: &mut A, + ) -> Result { + let mut tag = [0]; + reader.read_exact(&mut tag)?; + Ok(match tag[0] { + 0 => Self::Follow, + 1 => Self::Unknown, + 2 => Self::Estimated { + rows: usize::decode(reader, context, reference_tables, arena)?, + ndv: None, + }, + _ => return Err(DatabaseError::InvalidValue("invalid row count tag".into())), + }) + } +} + +impl ReferenceSerialization for PhysicalOption { + fn encode( + &self, + writer: &mut W, + is_direct: bool, + reference_tables: &mut ReferenceTables, + arena: &A, + ) -> Result<(), DatabaseError> { + self.plan + .encode(writer, is_direct, reference_tables, arena)?; + self.sort_option + .encode(writer, is_direct, reference_tables, arena)?; + self.row_count + .encode(writer, is_direct, reference_tables, arena) + } + + fn decode( + reader: &mut R, + context: Option<&ReferenceDecodeContext<'_, T>>, + reference_tables: &ReferenceTables, + arena: &mut A, + ) -> Result { + Ok(Self { + plan: PlanImpl::decode(reader, context, reference_tables, arena)?, + sort_option: SortOption::decode(reader, context, reference_tables, arena)?, + row_count: RowCountOption::decode(reader, context, reference_tables, arena)?, + startup_work: None, + }) + } +} + +// GRCOV_EXCL_START +#[cfg(all( + test, + not(target_arch = "wasm32"), + any(feature = "rocksdb", feature = "lmdb") +))] +mod tests { + use super::*; + use crate::expression::ScalarExpression; + use crate::planner::operator::sort::SortField; + use crate::planner::{PlanArena, TableArenaCell}; + #[cfg(all(not(feature = "rocksdb"), feature = "lmdb"))] + use crate::storage::lmdb::LmdbTransaction as TestTransaction; + #[cfg(feature = "rocksdb")] + use crate::storage::rocksdb::OptimisticRocksTransaction as TestTransaction; + use crate::types::value::DataValue; + + #[test] + fn physical_option_preserves_fields_in_roundtrip() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + for option in [ + PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated { + rows: 12, + ndv: None, + }, + ), + PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + ), + PhysicalOption::new(PlanImpl::Sort, SortOption::None, RowCountOption::Unknown), + PhysicalOption::new( + PlanImpl::Values, + SortOption::None, + RowCountOption::Estimated { rows: 0, ndv: None }, + ), + ] { + for is_direct in [false, true] { + let mut encoded = Vec::new(); + let mut references = ReferenceTables::new(); + option.encode(&mut encoded, is_direct, &mut references, &arena)?; + let mut reader = encoded.as_slice(); + let decoded = PhysicalOption::decode::, _, _>( + &mut reader, + None, + &references, + &mut arena, + )?; + assert_eq!(decoded, option); + assert_eq!(decoded.startup_work, None); + assert!(reader.is_empty()); + } + } + Ok(()) + } + + #[test] + fn physical_option_keeps_wire_order_and_discards_startup_cache() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let key = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(1))); + let mut option = PhysicalOption::new( + PlanImpl::Sort, + SortOption::OrderBy { + fields: vec![SortField { + expr: key, + asc: false, + nulls_first: true, + }], + ignore_prefix_len: 1, + }, + RowCountOption::Estimated { + rows: 42, + ndv: None, + }, + ); + option.row_count = RowCountOption::Estimated { + rows: 42, + ndv: Some(37), + }; + for is_direct in [false, true] { + // This is the pre-cache wire layout: exactly three ordered fields. + let mut expected = Vec::new(); + let mut reference_tables = ReferenceTables::new(); + option + .plan + .encode(&mut expected, is_direct, &mut reference_tables, &arena)?; + option + .sort_option + .encode(&mut expected, is_direct, &mut reference_tables, &arena)?; + option + .row_count + .encode(&mut expected, is_direct, &mut reference_tables, &arena)?; + for cache in [None, Some(None), Some(Some(192))] { + option.startup_work = cache; + let mut encoded = Vec::new(); + let mut references = ReferenceTables::new(); + option.encode(&mut encoded, is_direct, &mut references, &arena)?; + assert_eq!(encoded, expected); + assert_eq!(references, reference_tables); + let mut reader = encoded.as_slice(); + let decoded = PhysicalOption::decode::, _, _>( + &mut reader, + None, + &references, + &mut arena, + )?; + assert_eq!(decoded.plan, PlanImpl::Sort); + assert_eq!( + decoded.row_count, + RowCountOption::Estimated { + rows: 42, + ndv: None + } + ); + assert_eq!(decoded.startup_work, None); + let SortOption::OrderBy { + fields, + ignore_prefix_len, + } = decoded.sort_option + else { + unreachable!() + }; + assert_eq!(ignore_prefix_len, 1); + assert_eq!(fields.len(), 1); + assert!(!fields[0].asc); + assert!(fields[0].nulls_first); + assert_eq!( + arena.expression(fields[0].expr), + &ScalarExpression::Constant(DataValue::Int32(1)) + ); + assert!(reader.is_empty()); + } + } + Ok(()) + } +} +// GRCOV_EXCL_STOP diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 8e65c650..a7c7a0e5 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -29,11 +29,12 @@ use crate::iter_ext::Itertools; use crate::optimizer::core::cm_sketch::{ CountMinSketch, CountMinSketchPage, COUNT_MIN_SKETCH_STORAGE_PAGE_LEN, }; -use crate::optimizer::core::statistics_meta::StatisticsMeta; +use crate::optimizer::core::histogram::HistogramMeta; +use crate::optimizer::core::statistics_meta::IndexStatistics; use crate::planner::operator::alter_table::change_column::{DefaultChange, NotNullChange}; use crate::planner::{MetaArena, TableArenaCell}; use crate::serdes::ReferenceTables; -use crate::storage::table_codec::{Bytes, StatisticsCodecType, TableCodec, BOUND_MAX_TAG}; +use crate::storage::table_codec::{Bytes, IndexStatisticsCodecType, TableCodec, BOUND_MAX_TAG}; use crate::types::index::{Index, IndexId, IndexMeta, IndexMetaRef, IndexType}; use crate::types::serialize::TupleValueSerializableImpl; use crate::types::tuple::{Tuple, TupleId}; @@ -49,7 +50,18 @@ use std::path::Path; pub type KeyValueRef<'a> = (&'a [u8], &'a [u8]); -pub(crate) type StatisticsMetaCache = HashMap<(TableName, IndexId), StatisticsMeta>; +#[derive(Default)] +pub(crate) struct StatisticsMetaCache { + pub(crate) indexes: HashMap<(TableName, IndexId), IndexStatistics>, + pub(crate) columns: HashMap<(TableName, ColumnId), HistogramMeta>, +} + +impl StatisticsMetaCache { + pub(crate) fn remove_table(&mut self, table_name: &TableName) { + self.indexes.retain(|(name, _), _| name != table_name); + self.columns.retain(|(name, _), _| name != table_name); + } +} pub(crate) type TableCache = HashMap; pub(crate) type ViewCache = HashMap; @@ -632,6 +644,7 @@ pub trait Transaction: Sized { column_id }; + self.remove_column_statistics(table_codec, table_name, column_id, plan_arena)?; for index_meta in table_catalog.indexes.iter() { let index_meta = plan_arena.index(*index_meta); if !index_meta.column_ids.contains(&column_id) { @@ -649,7 +662,7 @@ pub trait Transaction: Sized { self.remove_range(Bound::Included(min), Bound::Included(max)) })?; - self.remove_statistics_meta(table_codec, table_name, index_meta.id)?; + self.remove_index_statistics(table_codec, table_name, index_meta.id)?; } self.load_table(table_codec, plan_arena, table_name.clone())? .ok_or(DatabaseError::TableNotFound) @@ -800,7 +813,7 @@ pub trait Transaction: Sized { self.remove_range(Bound::Included(min), Bound::Included(max)) })?; - self.remove_statistics_meta(table_codec, &table_name, index_id)?; + self.remove_index_statistics(table_codec, &table_name, index_id)?; self.load_table(table_codec, plan_arena, table_name.clone())? .map(|table| (table, index_id)) @@ -950,16 +963,61 @@ pub trait Transaction: Sized { }) } - fn save_statistics_meta( + fn save_column_statistics( + &mut self, + table_codec: &mut TableCodec, + table_name: &TableName, + column_id: ColumnId, + meta: &HistogramMeta, + arena: &(impl MetaArena + ?Sized), + ) -> Result<(), DatabaseError> { + table_codec.with_column_statistics( + table_name, + column_id, + Some(meta), + arena, + |key, value| self.set(key, value), + ) + } + + fn column_statistics( + &self, + table_codec: &mut TableCodec, + table_name: &str, + column_id: ColumnId, + arena: &mut (impl MetaArena + ?Sized), + ) -> Result, DatabaseError> { + let value = + table_codec.with_column_statistics(table_name, column_id, None, arena, |key, _| { + self.get_borrowed(key) + })?; + value + .map(|bytes| TableCodec::decode_column_statistics::(bytes.as_ref(), arena)) + .transpose() + } + + fn remove_column_statistics( + &mut self, + table_codec: &mut TableCodec, + table_name: &TableName, + column_id: ColumnId, + arena: &(impl MetaArena + ?Sized), + ) -> Result<(), DatabaseError> { + table_codec.with_column_statistics(table_name, column_id, None, arena, |key, _| { + self.remove(key) + }) + } + + fn save_index_statistics( &mut self, table_codec: &mut TableCodec, table_name: &TableName, - statistics_meta: StatisticsMeta, + index_statistics: IndexStatistics, arena: &(impl MetaArena + ?Sized), ) -> Result<(), DatabaseError> { - let index_id = statistics_meta.index_id(); - let (root, buckets, cm_sketch, top_n) = statistics_meta.into_parts(); - table_codec.with_statistics_meta( + let index_id = index_statistics.index_id(); + let (root, buckets, cm_sketch, top_n) = index_statistics.into_parts(); + table_codec.with_index_statistics( table_name.as_ref(), index_id, Some(&root), @@ -968,7 +1026,7 @@ pub trait Transaction: Sized { )?; let (sketch_meta, sketch_pages) = cm_sketch.into_storage_parts(COUNT_MIN_SKETCH_STORAGE_PAGE_LEN); - table_codec.with_statistics_sketch_meta( + table_codec.with_index_statistics_sketch_meta( table_name.as_ref(), index_id, Some(&sketch_meta), @@ -976,7 +1034,7 @@ pub trait Transaction: Sized { |key, value| self.set(key, value), )?; for sketch_page in sketch_pages { - table_codec.with_statistics_sketch_page( + table_codec.with_index_statistics_sketch_page( table_name.as_ref(), index_id, &sketch_page, @@ -987,7 +1045,7 @@ pub trait Transaction: Sized { } for (ordinal, bucket) in buckets.iter().enumerate() { - table_codec.with_statistics_bucket( + table_codec.with_index_statistics_bucket( table_name.as_ref(), index_id, ordinal as u32, @@ -996,7 +1054,7 @@ pub trait Transaction: Sized { |key, value| self.set(key, value), )?; } - table_codec.with_statistics_top_n( + table_codec.with_index_statistics_top_n( table_name.as_ref(), index_id, Some(&top_n), @@ -1007,14 +1065,14 @@ pub trait Transaction: Sized { Ok(()) } - fn statistics_meta( + fn index_statistics( &self, table_codec: &mut TableCodec, table_name: &str, index_id: IndexId, arena: &mut (impl MetaArena + ?Sized), - ) -> Result, DatabaseError> { - table_codec.with_statistics_index_bound(table_name, index_id, |min, max| { + ) -> Result, DatabaseError> { + table_codec.with_index_statistics_bound(table_name, index_id, |min, max| { let mut iter = self.range(Bound::Included(min), Bound::Included(max))?; let mut root = None; let mut buckets = Vec::new(); @@ -1023,25 +1081,29 @@ pub trait Transaction: Sized { let mut top_n = None; while let Some((key, value)) = iter.try_next()? { - match TableCodec::decode_statistics_codec_type(key)? { - StatisticsCodecType::Root => { - root = Some(TableCodec::decode_statistics_meta::(value, arena)?); + match TableCodec::decode_index_statistics_codec_type(key)? { + IndexStatisticsCodecType::Root => { + root = Some(TableCodec::decode_index_statistics::(value, arena)?); } - StatisticsCodecType::SketchMeta => { - sketch_meta = Some(TableCodec::decode_statistics_sketch_meta::( + IndexStatisticsCodecType::SketchMeta => { + sketch_meta = Some( + TableCodec::decode_index_statistics_sketch_meta::(value, arena)?, + ); + } + IndexStatisticsCodecType::SketchPage => { + sketch_pages.push(TableCodec::decode_index_statistics_sketch_page::( value, arena, )?); } - StatisticsCodecType::SketchPage => { - sketch_pages.push(TableCodec::decode_statistics_sketch_page::( + IndexStatisticsCodecType::Bucket => { + buckets.push(TableCodec::decode_index_statistics_bucket::( value, arena, )?); } - StatisticsCodecType::Bucket => { - buckets.push(TableCodec::decode_statistics_bucket::(value, arena)?); - } - StatisticsCodecType::TopN => { - top_n = Some(TableCodec::decode_statistics_top_n::(value, arena)?); + IndexStatisticsCodecType::TopN => { + top_n = Some(TableCodec::decode_index_statistics_top_n::( + value, arena, + )?); } } } @@ -1049,7 +1111,7 @@ pub trait Transaction: Sized { match (root, sketch_meta) { (Some(root), Some(sketch_meta)) => { let sketch = CountMinSketch::from_storage_parts(sketch_meta, sketch_pages)?; - StatisticsMeta::from_parts(root, buckets, sketch, top_n).map(Some) + IndexStatistics::from_parts(root, buckets, sketch, top_n).map(Some) } (None, None) if buckets.is_empty() && sketch_pages.is_empty() && top_n.is_none() => @@ -1063,13 +1125,13 @@ pub trait Transaction: Sized { }) } - fn remove_statistics_meta( + fn remove_index_statistics( &mut self, table_codec: &mut TableCodec, table_name: &TableName, index_id: IndexId, ) -> Result<(), DatabaseError> { - table_codec.with_statistics_index_bound(table_name.as_ref(), index_id, |min, max| { + table_codec.with_index_statistics_bound(table_name.as_ref(), index_id, |min, max| { self.remove_range(Bound::Included(min), Bound::Included(max)) })?; diff --git a/src/storage/table_codec.rs b/src/storage/table_codec.rs index 4d9672e4..13f6938b 100644 --- a/src/storage/table_codec.rs +++ b/src/storage/table_codec.rs @@ -18,7 +18,8 @@ use crate::db::{ScalaFunctions, TableFunctions}; use crate::errors::DatabaseError; use crate::optimizer::core::cm_sketch::{CountMinSketchMeta, CountMinSketchPage}; use crate::optimizer::core::histogram::Bucket; -use crate::optimizer::core::statistics_meta::StatisticsMetaRoot; +use crate::optimizer::core::histogram::HistogramMeta; +use crate::optimizer::core::statistics_meta::IndexStatisticsRoot; use crate::optimizer::core::top_n::ColumnTopN; use crate::planner::MetaArena; use crate::serdes::stable_hash::{StableHasher, TABLE_NAME_HASH_KEYS}; @@ -28,7 +29,7 @@ use crate::types::index::{Index, IndexId, IndexMeta, IndexType, INDEX_ID_LEN}; use crate::types::serialize::TupleValueSerializableImpl; use crate::types::tuple::{Tuple, TupleId}; use crate::types::value::{DataValue, IndexKeyMapping}; -use crate::types::LogicalType; +use crate::types::{ColumnId, LogicalType}; use std::borrow::Borrow; use std::hash::{Hash, Hasher}; use std::io::{Cursor, Read, Seek, SeekFrom}; @@ -90,8 +91,11 @@ enum CodecType { Hash, } +const INDEX_STATISTICS_TAG: u8 = b'i'; +const COLUMN_STATISTICS_TAG: u8 = b'c'; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum StatisticsCodecType { +pub(crate) enum IndexStatisticsCodecType { Root, SketchMeta, SketchPage, @@ -99,24 +103,24 @@ pub(crate) enum StatisticsCodecType { TopN, } -impl StatisticsCodecType { +impl IndexStatisticsCodecType { fn tag(self) -> u8 { match self { - StatisticsCodecType::Root => b'0', - StatisticsCodecType::SketchMeta => b'1', - StatisticsCodecType::SketchPage => b'2', - StatisticsCodecType::Bucket => b'3', - StatisticsCodecType::TopN => b'4', + IndexStatisticsCodecType::Root => b'0', + IndexStatisticsCodecType::SketchMeta => b'1', + IndexStatisticsCodecType::SketchPage => b'2', + IndexStatisticsCodecType::Bucket => b'3', + IndexStatisticsCodecType::TopN => b'4', } } fn from_tag(tag: u8) -> Result { match tag { - b'0' => Ok(StatisticsCodecType::Root), - b'1' => Ok(StatisticsCodecType::SketchMeta), - b'2' => Ok(StatisticsCodecType::SketchPage), - b'3' => Ok(StatisticsCodecType::Bucket), - b'4' => Ok(StatisticsCodecType::TopN), + b'0' => Ok(IndexStatisticsCodecType::Root), + b'1' => Ok(IndexStatisticsCodecType::SketchMeta), + b'2' => Ok(IndexStatisticsCodecType::SketchPage), + b'3' => Ok(IndexStatisticsCodecType::Bucket), + b'4' => Ok(IndexStatisticsCodecType::TopN), _ => Err(DatabaseError::InvalidValue(format!( "invalid statistics codec tag: {tag}" ))), @@ -548,7 +552,7 @@ impl TableCodec { } /// Range bounds covering all statistics keys for one index. - pub fn with_statistics_index_bound( + pub fn with_index_statistics_bound( &mut self, table_name: &str, index_id: IndexId, @@ -559,6 +563,7 @@ impl TableCodec { Self::write_key_prefix(lower, CodecType::Statistics, table_hash); lower.push(BOUND_MIN_TAG); lower.extend_from_slice(&index_id.to_le_bytes()); + lower.push(INDEX_STATISTICS_TAG); upper.clear(); upper.extend_from_slice(lower); @@ -568,32 +573,79 @@ impl TableCodec { }) } - /// Key: `{TableName}{STATISTICS_TAG}{BOUND_MIN_TAG}{INDEX_ID}{ROOT_TAG}`. - pub fn with_statistics_meta( + fn with_statistics( &mut self, table_name: &str, - index_id: IndexId, - statistics_meta: Option<&StatisticsMetaRoot>, + id: &[u8], + suffix: &[u8], + statistics: Option<&V>, arena: &(impl MetaArena + ?Sized), f: impl FnOnce(&[u8], &[u8]) -> Result, ) -> Result { self.clear_buffers(); - self.with_table_hash_buffers(table_name, |lower, table_hash, value, refs| { - Self::write_key_prefix(lower, CodecType::Statistics, table_hash); - lower.push(BOUND_MIN_TAG); - lower.extend_from_slice(&index_id.to_le_bytes()); - lower.push(StatisticsCodecType::Root.tag()); - - if let Some(statistics_meta) = statistics_meta { - Self::encode_statistics_meta_value_into(statistics_meta, refs, value, arena)?; + self.with_table_hash_buffers(table_name, |key, table_hash, value, refs| { + Self::write_key_prefix(key, CodecType::Statistics, table_hash); + key.push(BOUND_MIN_TAG); + key.extend_from_slice(id); + key.extend_from_slice(suffix); + if let Some(statistics) = statistics { + statistics.encode(value, true, refs, arena)?; } - - f(lower.as_slice(), value.as_slice()) + f(key.as_slice(), value.as_slice()) }) } - /// Key: `{TableName}{STATISTICS_TAG}{BOUND_MIN_TAG}{INDEX_ID}{SKETCH_META_TAG}`. - pub fn with_statistics_sketch_meta( + pub fn with_column_statistics( + &mut self, + table_name: &str, + column_id: ColumnId, + meta: Option<&HistogramMeta>, + arena: &(impl MetaArena + ?Sized), + f: impl FnOnce(&[u8], &[u8]) -> Result, + ) -> Result { + self.with_statistics( + table_name, + &column_id.to_le_bytes(), + &[COLUMN_STATISTICS_TAG], + meta, + arena, + f, + ) + } + + pub fn decode_column_statistics( + bytes: &[u8], + arena: &mut (impl MetaArena + ?Sized), + ) -> Result { + HistogramMeta::decode::( + &mut Cursor::new(bytes), + None, + &ReferenceTables::new(), + arena, + ) + } + + /// Key: `{TableName}{STATISTICS_TAG}{BOUND_MIN_TAG}{INDEX_ID}{INDEX_STATISTICS_TAG}{ROOT_TAG}`. + pub fn with_index_statistics( + &mut self, + table_name: &str, + index_id: IndexId, + index_statistics: Option<&IndexStatisticsRoot>, + arena: &(impl MetaArena + ?Sized), + f: impl FnOnce(&[u8], &[u8]) -> Result, + ) -> Result { + self.with_statistics( + table_name, + &index_id.to_le_bytes(), + &[INDEX_STATISTICS_TAG, IndexStatisticsCodecType::Root.tag()], + index_statistics, + arena, + f, + ) + } + + /// Key: `{TableName}{STATISTICS_TAG}{BOUND_MIN_TAG}{INDEX_ID}{INDEX_STATISTICS_TAG}{SKETCH_META_TAG}`. + pub fn with_index_statistics_sketch_meta( &mut self, table_name: &str, index_id: IndexId, @@ -606,7 +658,8 @@ impl TableCodec { Self::write_key_prefix(lower, CodecType::Statistics, table_hash); lower.push(BOUND_MIN_TAG); lower.extend_from_slice(&index_id.to_le_bytes()); - lower.push(StatisticsCodecType::SketchMeta.tag()); + lower.push(INDEX_STATISTICS_TAG); + lower.push(IndexStatisticsCodecType::SketchMeta.tag()); if let Some(sketch_meta) = sketch_meta { Self::encode_statistics_sketch_meta_value_into(sketch_meta, refs, value, arena)?; @@ -616,8 +669,8 @@ impl TableCodec { }) } - /// Key: `{TableName}{STATISTICS_TAG}{BOUND_MIN_TAG}{INDEX_ID}{SKETCH_PAGE_TAG}{BOUND_MIN_TAG}{ROW_ID}{BOUND_MIN_TAG}{PAGE_ID}`. - pub fn with_statistics_sketch_page( + /// Key: `{TableName}{STATISTICS_TAG}{BOUND_MIN_TAG}{INDEX_ID}{INDEX_STATISTICS_TAG}{SKETCH_PAGE_TAG}{BOUND_MIN_TAG}{ROW_ID}{BOUND_MIN_TAG}{PAGE_ID}`. + pub fn with_index_statistics_sketch_page( &mut self, table_name: &str, index_id: IndexId, @@ -631,7 +684,8 @@ impl TableCodec { Self::write_key_prefix(lower, CodecType::Statistics, table_hash); lower.push(BOUND_MIN_TAG); lower.extend_from_slice(&index_id.to_le_bytes()); - lower.push(StatisticsCodecType::SketchPage.tag()); + lower.push(INDEX_STATISTICS_TAG); + lower.push(IndexStatisticsCodecType::SketchPage.tag()); lower.push(BOUND_MIN_TAG); lower.extend_from_slice(&(sketch_page.row_idx() as u32).to_be_bytes()); lower.push(BOUND_MIN_TAG); @@ -645,8 +699,8 @@ impl TableCodec { }) } - /// Key: `{TableName}{STATISTICS_TAG}{BOUND_MIN_TAG}{INDEX_ID}{BUCKET_TAG}{BOUND_MIN_TAG}{ORDINAL}`. - pub fn with_statistics_bucket( + /// Key: `{TableName}{STATISTICS_TAG}{BOUND_MIN_TAG}{INDEX_ID}{INDEX_STATISTICS_TAG}{BUCKET_TAG}{BOUND_MIN_TAG}{ORDINAL}`. + pub fn with_index_statistics_bucket( &mut self, table_name: &str, index_id: IndexId, @@ -660,7 +714,8 @@ impl TableCodec { Self::write_key_prefix(lower, CodecType::Statistics, table_hash); lower.push(BOUND_MIN_TAG); lower.extend_from_slice(&index_id.to_le_bytes()); - lower.push(StatisticsCodecType::Bucket.tag()); + lower.push(INDEX_STATISTICS_TAG); + lower.push(IndexStatisticsCodecType::Bucket.tag()); lower.push(BOUND_MIN_TAG); lower.extend_from_slice(&ordinal.to_be_bytes()); @@ -672,8 +727,8 @@ impl TableCodec { }) } - /// Key: `{TableName}{STATISTICS_TAG}{BOUND_MIN_TAG}{INDEX_ID}{TOP_N_TAG}`. - pub fn with_statistics_top_n( + /// Key: `{TableName}{STATISTICS_TAG}{BOUND_MIN_TAG}{INDEX_ID}{INDEX_STATISTICS_TAG}{TOP_N_TAG}`. + pub fn with_index_statistics_top_n( &mut self, table_name: &str, index_id: IndexId, @@ -686,7 +741,8 @@ impl TableCodec { Self::write_key_prefix(lower, CodecType::Statistics, table_hash); lower.push(BOUND_MIN_TAG); lower.extend_from_slice(&index_id.to_le_bytes()); - lower.push(StatisticsCodecType::TopN.tag()); + lower.push(INDEX_STATISTICS_TAG); + lower.push(IndexStatisticsCodecType::TopN.tag()); if let Some(top_n) = top_n { Self::encode_statistics_top_n_value_into(top_n, refs, value, arena)?; @@ -869,20 +925,11 @@ impl TableCodec { ColumnCatalog::decode::(reader, None, reference_tables, arena) } - fn encode_statistics_meta_value_into( - statistics_meta: &StatisticsMetaRoot, - reference_tables: &mut ReferenceTables, - value: &mut Bytes, - arena: &(impl MetaArena + ?Sized), - ) -> Result<(), DatabaseError> { - statistics_meta.encode(value, true, reference_tables, arena) - } - - pub fn decode_statistics_meta( + pub fn decode_index_statistics( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), - ) -> Result { - StatisticsMetaRoot::decode::( + ) -> Result { + IndexStatisticsRoot::decode::( &mut Cursor::new(bytes), None, &EMPTY_REFERENCE_TABLES, @@ -899,7 +946,7 @@ impl TableCodec { sketch_meta.encode(value, true, reference_tables, arena) } - pub fn decode_statistics_sketch_meta( + pub fn decode_index_statistics_sketch_meta( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { @@ -920,7 +967,7 @@ impl TableCodec { sketch_page.encode(value, true, reference_tables, arena) } - pub fn decode_statistics_sketch_page( + pub fn decode_index_statistics_sketch_page( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { @@ -941,20 +988,20 @@ impl TableCodec { bucket.encode(value, true, reference_tables, arena) } - pub(crate) fn decode_statistics_codec_type( + pub(crate) fn decode_index_statistics_codec_type( key: &[u8], - ) -> Result { - let prefix_len = TUPLE_KEY_PREFIX_LEN + INDEX_ID_LEN; + ) -> Result { + let prefix_len = TUPLE_KEY_PREFIX_LEN + INDEX_ID_LEN + 1; let Some(tag) = key.get(prefix_len).copied() else { return Err(DatabaseError::InvalidValue( "statistics key is too short".to_string(), )); }; - StatisticsCodecType::from_tag(tag) + IndexStatisticsCodecType::from_tag(tag) } - pub fn decode_statistics_bucket( + pub fn decode_index_statistics_bucket( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { @@ -975,7 +1022,7 @@ impl TableCodec { top_n.encode(value, true, reference_tables, arena) } - pub fn decode_statistics_top_n( + pub fn decode_index_statistics_top_n( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { @@ -987,7 +1034,7 @@ impl TableCodec { ) } - pub fn decode_statistics_bucket_ordinal(bytes: &[u8]) -> Result { + pub fn decode_index_statistics_bucket_ordinal(bytes: &[u8]) -> Result { let len = bytes.len(); let Some(ordinal_bytes) = bytes.get(len - STATISTICS_BUCKET_ORD_LEN..) else { return Err(DatabaseError::InvalidValue( @@ -1067,8 +1114,10 @@ mod tests { use crate::catalog::{ColumnCatalog, ColumnDesc, ColumnRelation, TableCatalog, TableMeta}; use crate::errors::DatabaseError; use crate::iter_ext::Itertools; - use crate::optimizer::core::histogram::{HistogramBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR}; - use crate::optimizer::core::statistics_meta::StatisticsMeta; + use crate::optimizer::core::histogram::{ + HistogramBuilder, HistogramMetaBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR, + }; + use crate::optimizer::core::statistics_meta::IndexStatistics; use crate::planner::{PlanArena, TableArenaCell}; use crate::serdes::ReferenceTables; use crate::storage::rocksdb::RocksTransaction; @@ -1166,7 +1215,7 @@ mod tests { } #[test] - fn test_table_codec_statistics_meta() -> Result<(), DatabaseError> { + fn test_table_codec_column_and_index_statistics() -> Result<(), DatabaseError> { let mut table_codec = TableCodec::default(); let table_arena = TableArenaCell::default(); let mut plan_arena = PlanArena::new(&table_arena); @@ -1186,15 +1235,52 @@ mod tests { } let (histogram, sketch, top_n) = builder.build(2)?; let (root, buckets, _, top_n) = - StatisticsMeta::new(histogram, sketch.clone(), top_n).into_parts(); + IndexStatistics::new(histogram, sketch.clone(), top_n).into_parts(); let root_bytes = - table_codec.with_statistics_meta("t1", 0, Some(&root), &plan_arena, |_, value| { + table_codec.with_index_statistics("t1", 0, Some(&root), &plan_arena, |_, value| { Ok::<_, DatabaseError>(value.to_vec()) })?; let decoded_root = - TableCodec::decode_statistics_meta::(&root_bytes, &mut plan_arena)?; + TableCodec::decode_index_statistics::(&root_bytes, &mut plan_arena)?; assert_eq!(decoded_root.index_id(), root.index_id()); + let index_key = + table_codec + .with_index_statistics("t1", 0, None, &plan_arena, |key, _| Ok(key.to_vec()))?; + let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + for value in [ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Null, + ] { + builder.append(&value); + } + let column_meta = builder.build(); + let (column_key, bytes) = table_codec.with_column_statistics( + "t1", + 0, + Some(&column_meta), + &plan_arena, + |key, value| Ok((key.to_vec(), value.to_vec())), + )?; + assert_ne!(column_key, index_key); + table_codec.with_index_statistics_bound("t1", 0, |min, max| { + assert!(index_key.as_slice() >= min && index_key.as_slice() <= max); + assert!(column_key.as_slice() < min || column_key.as_slice() > max); + Ok(()) + })?; + table_codec.with_statistics_bound("t1", |min, max| { + assert!(index_key.as_slice() >= min && index_key.as_slice() <= max); + assert!(column_key.as_slice() >= min && column_key.as_slice() <= max); + Ok(()) + })?; + let decoded = + TableCodec::decode_column_statistics::(&bytes, &mut plan_arena)?; + assert_eq!(decoded, column_meta); + assert_eq!(decoded.distinct_values_len(), 2); + assert_eq!(decoded.null_count(), 1); + assert_eq!(decoded.buckets_len(), 0); assert_eq!( decoded_root.histogram_meta().values_len(), root.histogram_meta().values_len() @@ -1205,22 +1291,21 @@ mod tests { ); let (sketch_meta, mut sketch_pages) = sketch.into_storage_parts(1); - let sketch_meta_bytes = table_codec.with_statistics_sketch_meta( + let sketch_meta_bytes = table_codec.with_index_statistics_sketch_meta( "t1", 0, Some(&sketch_meta), &plan_arena, |_, value| Ok::<_, DatabaseError>(value.to_vec()), )?; - let decoded_sketch_meta = TableCodec::decode_statistics_sketch_meta::( - &sketch_meta_bytes, - &mut plan_arena, - )?; + let decoded_sketch_meta = TableCodec::decode_index_statistics_sketch_meta::< + RocksTransaction, + >(&sketch_meta_bytes, &mut plan_arena)?; assert_eq!(decoded_sketch_meta.width(), sketch_meta.width()); assert_eq!(decoded_sketch_meta.k_num(), sketch_meta.k_num()); let first_sketch_page = sketch_pages.next().unwrap(); - let sketch_page_bytes = table_codec.with_statistics_sketch_page( + let sketch_page_bytes = table_codec.with_index_statistics_sketch_page( "t1", 0, &first_sketch_page, @@ -1228,30 +1313,29 @@ mod tests { &plan_arena, |_, value| Ok::<_, DatabaseError>(value.to_vec()), )?; - let decoded_sketch_page = TableCodec::decode_statistics_sketch_page::( - &sketch_page_bytes, - &mut plan_arena, - )?; + let decoded_sketch_page = TableCodec::decode_index_statistics_sketch_page::< + RocksTransaction, + >(&sketch_page_bytes, &mut plan_arena)?; assert_eq!(decoded_sketch_page.counters(), first_sketch_page.counters()); let bucket0_key = - table_codec.with_statistics_bucket("t1", 0, 0, None, &plan_arena, |key, _| { + table_codec.with_index_statistics_bucket("t1", 0, 0, None, &plan_arena, |key, _| { Ok::<_, DatabaseError>(key.to_vec()) })?; let bucket1_key = - table_codec.with_statistics_bucket("t1", 0, 1, None, &plan_arena, |key, _| { + table_codec.with_index_statistics_bucket("t1", 0, 1, None, &plan_arena, |key, _| { Ok::<_, DatabaseError>(key.to_vec()) })?; assert!(bucket0_key < bucket1_key); let (bucket0_min, bucket0_max) = - table_codec.with_statistics_index_bound("t1", 0, |min, max| { + table_codec.with_index_statistics_bound("t1", 0, |min, max| { Ok::<_, DatabaseError>((min.to_vec(), max.to_vec())) })?; assert!(bucket0_key.as_slice() >= bucket0_min.as_slice()); assert!(bucket1_key.as_slice() <= bucket0_max.as_slice()); - let bucket_bytes = table_codec.with_statistics_bucket( + let bucket_bytes = table_codec.with_index_statistics_bucket( "t1", 0, 0, @@ -1259,33 +1343,38 @@ mod tests { &plan_arena, |_, value| Ok::<_, DatabaseError>(value.to_vec()), )?; - let decoded_bucket = TableCodec::decode_statistics_bucket::( + let decoded_bucket = TableCodec::decode_index_statistics_bucket::( &bucket_bytes, &mut plan_arena, )?; assert_eq!(decoded_bucket, buckets[0]); assert_eq!( - TableCodec::decode_statistics_bucket_ordinal(&bucket0_key)?, + TableCodec::decode_index_statistics_bucket_ordinal(&bucket0_key)?, 0 ); assert_eq!( - TableCodec::decode_statistics_bucket_ordinal(&bucket1_key)?, + TableCodec::decode_index_statistics_bucket_ordinal(&bucket1_key)?, 1 ); let top_n_key = - table_codec.with_statistics_top_n("t1", 0, None, &plan_arena, |key, _| { + table_codec.with_index_statistics_top_n("t1", 0, None, &plan_arena, |key, _| { Ok::<_, DatabaseError>(key.to_vec()) })?; assert!(top_n_key.as_slice() >= bucket0_min.as_slice()); assert!(top_n_key.as_slice() <= bucket0_max.as_slice()); - let top_n_bytes = - table_codec.with_statistics_top_n("t1", 0, Some(&top_n), &plan_arena, |_, value| { - Ok::<_, DatabaseError>(value.to_vec()) - })?; - let decoded_top_n = - TableCodec::decode_statistics_top_n::(&top_n_bytes, &mut plan_arena)?; + let top_n_bytes = table_codec.with_index_statistics_top_n( + "t1", + 0, + Some(&top_n), + &plan_arena, + |_, value| Ok::<_, DatabaseError>(value.to_vec()), + )?; + let decoded_top_n = TableCodec::decode_index_statistics_top_n::( + &top_n_bytes, + &mut plan_arena, + )?; assert_eq!(decoded_top_n, top_n); Ok(()) From 194c681d80f661ed7695139f04881e56a6764b3d Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 12/62] perf: preserve join properties and account for filtered input work --- src/optimizer/core/rule.rs | 4 +- src/optimizer/heuristic/optimizer.rs | 47 +- src/optimizer/plan_utils.rs | 144 ++++- .../rule/implementation/dql/filter.rs | 569 +++++++++++++++++- src/optimizer/rule/implementation/dql/join.rs | 5 +- .../rule/implementation/dql/mark_apply.rs | 6 +- src/optimizer/rule/implementation/dql/sort.rs | 5 +- .../rule/implementation/dql/table_scan.rs | 10 +- .../rule/implementation/dql/top_k.rs | 5 +- .../rule/implementation/dql/window.rs | 5 +- src/optimizer/rule/implementation/macros.rs | 2 +- src/optimizer/rule/implementation/mod.rs | 10 +- .../rule/normalization/join_reorder.rs | 199 ++---- .../rule/normalization/parameterized_index.rs | 171 +++++- src/orm/mod.rs | 2 +- src/planner/mod.rs | 117 +++- src/planner/operator/mod.rs | 32 +- 17 files changed, 1121 insertions(+), 212 deletions(-) diff --git a/src/optimizer/core/rule.rs b/src/optimizer/core/rule.rs index 1821f1ca..b53e4237 100644 --- a/src/optimizer/core/rule.rs +++ b/src/optimizer/core/rule.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::{Operator, PhysicalOption}; +use crate::planner::operator::PhysicalOption; use crate::planner::{LogicalPlan, PlanArena}; use std::cmp::Ordering; @@ -63,7 +63,7 @@ pub fn keep_best_physical_option( pub trait ImplementationRule: MatchPattern { fn update_best_option( &self, - op: &Operator, + plan: &LogicalPlan, arena: &PlanArena, loader: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index e3b6181b..92402220 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -311,22 +311,15 @@ impl<'a> HepOptimizer<'a> { })?; } - { - let LogicalPlan { - operator, - physical_option, - .. - } = plan; - if let Some(option) = implementation_index.direct_physical_option(operator) { - *physical_option = Some(option); - } else { - let mut best_physical_option: BestPhysicalOption = None; - for rule in implementation_index.for_matching_operator(operator) { - rule.update_best_option(operator, arena, loader, &mut best_physical_option)?; - } - if let Some((option, _)) = best_physical_option { - *physical_option = Some(option); - } + if let Some(option) = implementation_index.direct_physical_option(&plan.operator) { + plan.physical_option = Some(option); + } else { + let mut best_physical_option: BestPhysicalOption = None; + for rule in implementation_index.for_matching_operator(&plan.operator) { + rule.update_best_option(plan, arena, loader, &mut best_physical_option)?; + } + if let Some((option, _)) = best_physical_option { + plan.physical_option = Some(option); } } @@ -519,9 +512,6 @@ impl ImplementationRuleIndex { Operator::Dummy if self.contains(ImplementationRuleImpl::Dummy) => Some( PhysicalOption::new(PlanImpl::Dummy, SortOption::None, RowCountOption::Unknown), ), - Operator::Filter(_) if self.contains(ImplementationRuleImpl::Filter) => Some( - PhysicalOption::new(PlanImpl::Filter, SortOption::Follow, RowCountOption::Follow), - ), Operator::Join(join_op) if self.contains(ImplementationRuleImpl::HashJoin) => { Some(PhysicalOption::new( join_op.plan_impl(), @@ -703,6 +693,7 @@ mod tests { use crate::errors::DatabaseError; use crate::expression::range_detacher::Range; use crate::expression::ScalarExpression; + use crate::optimizer::core::rule::ImplementationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::heuristic::batch::{HepBatch, HepBatchStrategy, HepMatchOrder}; use crate::optimizer::heuristic::optimizer::{ @@ -719,7 +710,7 @@ mod tests { Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption, }; use crate::planner::{Childrens, LogicalPlan, PlanArena, TableArenaCell}; - use crate::storage::{Storage, Transaction}; + use crate::storage::{StatisticsMetaCache, Storage, Transaction}; use crate::types::index::IndexLookup; use crate::types::value::DataValue; use crate::types::LogicalType; @@ -737,7 +728,8 @@ mod tests { } #[test] - fn row_preserving_fast_paths_follow_input_row_count() -> Result<(), DatabaseError> { + fn filter_fallback_and_row_preserving_fast_paths_follow_input_row_count( + ) -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); let mut plan = filter_chain(1, &mut arena); @@ -753,7 +745,18 @@ mod tests { }, )); let implementations = ImplementationRuleIndex::new(vec![ImplementationRuleImpl::Filter]); - plan.physical_option = implementations.direct_physical_option(&plan.operator); + assert!(implementations + .direct_physical_option(&plan.operator) + .is_none()); + let cache = StatisticsMetaCache::default(); + let mut best = None; + ImplementationRuleImpl::Filter.update_best_option( + &plan, + &arena, + &StatisticMetaLoader::new(&cache), + &mut best, + )?; + plan.physical_option = best.map(|(option, _)| option); assert_eq!( plan.physical_option.as_ref().unwrap().row_count, RowCountOption::Follow diff --git a/src/optimizer/plan_utils.rs b/src/optimizer/plan_utils.rs index 94c9f6c4..e1ca6319 100644 --- a/src/optimizer/plan_utils.rs +++ b/src/optimizer/plan_utils.rs @@ -12,8 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::planner::operator::Operator; -use crate::planner::{Childrens, LogicalPlan}; +use crate::catalog::{ColumnRef, TableName}; +use crate::errors::DatabaseError; +use crate::expression::visitor::ExprVisitor; +use crate::expression::{AliasType, ScalarExpression}; +use crate::planner::operator::project::ProjectOperator; +use crate::planner::operator::table_scan::TableScanOperator; +use crate::planner::operator::visitor::OperatorVisitor; +use crate::planner::operator::{Operator, RowCountOption}; +use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; +use crate::types::ColumnId; use std::mem; #[allow(dead_code)] @@ -116,6 +124,138 @@ fn take_childrens(plan: &mut LogicalPlan) -> Childrens { mem::replace(&mut *plan.childrens, Childrens::None) } +struct ScanColumnVisitor<'plan, 'arena, A> { + column: Option, + source: Option<(&'plan TableName, ColumnId)>, + arena: &'arena A, +} + +impl<'plan, A: MetaArena> OperatorVisitor<'plan> for ScanColumnVisitor<'plan, '_, A> { + fn visit_plan(&mut self, plan: &'plan LogicalPlan) -> Result<(), DatabaseError> { + if self.column.is_none() { + return Ok(()); + } + self.visit_operator(&plan.operator, plan.physical_option.as_ref())?; + let follow = match &plan.operator { + Operator::Project(_) | Operator::Filter(_) => true, + Operator::Join(_) | Operator::Aggregate(_) | Operator::Limit(_) | Operator::TopK(_) => { + false + } + _ => plan + .physical_option + .as_ref() + .is_some_and(|option| matches!(option.row_count, RowCountOption::Follow)), + }; + if follow && self.column.is_some() { + self.visit_children(&plan.childrens)?; + } + Ok(()) + } + + fn visit_children(&mut self, childrens: &'plan Childrens) -> Result<(), DatabaseError> { + match childrens { + Childrens::Only(input) | Childrens::Twins { left: input, .. } => self.visit_plan(input), + Childrens::None => Ok(()), + } + } + + fn visit_table_scan(&mut self, scan: &'plan TableScanOperator) -> Result<(), DatabaseError> { + if scan.limit == (None, None) { + if let Some(source) = self.column.and_then(|column| { + scan.columns + .iter() + .find(|source| self.arena.same_column(**source, column)) + }) { + self.source = self + .arena + .column(*source) + .id() + .map(|id| (&scan.table_name, id)); + } + } + self.column = None; + Ok(()) + } + + fn visit_project(&mut self, project: &'plan ProjectOperator) -> Result<(), DatabaseError> { + let Some(column) = self.column.take() else { + return Ok(()); + }; + let arena = self.arena; + for &candidate in &project.exprs { + let source = match arena.expression(candidate) { + ScalarExpression::Alias { + expr, + alias: AliasType::Name(_), + } if arena.column(column).id().is_none() + && arena.column(column).table_name().is_none() + && candidate.output_name(arena) == arena.column(column).name() => + { + *expr + } + ScalarExpression::Alias { + expr, + alias: AliasType::Expr(alias), + } if matches!(arena.expression(*alias), ScalarExpression::ColumnRef { column: target, .. } + if arena.same_column(*target, column)) => + { + *expr + } + ScalarExpression::ColumnRef { column: target, .. } + if arena.same_column(*target, column) => + { + candidate + } + _ => continue, + }; + return ExprVisitor::visit(self, source, arena); + } + Ok(()) + } +} + +impl ExprVisitor for ScanColumnVisitor<'_, '_, A> { + fn visit_expression( + &mut self, + expr: &ScalarExpression, + _arena: &A, + ) -> Result { + Ok(matches!( + expr, + ScalarExpression::Alias { .. } | ScalarExpression::ColumnRef { .. } + )) + } + + fn visit_alias( + &mut self, + expr: ExprRef, + _alias: &AliasType, + arena: &A, + ) -> Result<(), DatabaseError> { + // The alias target describes the output, not the source column. + self.visit(expr, arena) + } + + fn visit_column_ref(&mut self, column: &ColumnRef) -> Result<(), DatabaseError> { + self.column = Some(*column); + Ok(()) + } +} + +pub(crate) fn scan_column<'plan>( + plan: &'plan LogicalPlan, + column: ColumnRef, + arena: &PlanArena, +) -> Result, DatabaseError> { + let mut visitor = ScanColumnVisitor { + column: Some(column), + source: None, + arena, + }; + visitor.visit_plan(plan)?; + Ok(visitor.source) +} + // GRCOV_EXCL_START #[cfg(test)] mod tests { diff --git a/src/optimizer/rule/implementation/dql/filter.rs b/src/optimizer/rule/implementation/dql/filter.rs index 92a3021c..f09fc6a1 100644 --- a/src/optimizer/rule/implementation/dql/filter.rs +++ b/src/optimizer/rule/implementation/dql/filter.rs @@ -12,13 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::catalog::ColumnRef; use crate::errors::DatabaseError; +use crate::expression::visitor::ExprVisitor; +use crate::expression::{BinaryOperator, ScalarExpression}; use crate::optimizer::core::pattern::Pattern; -use crate::optimizer::core::rule::BestPhysicalOption; -use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; +use crate::optimizer::core::rule::{ + keep_best_physical_option, BestPhysicalOption, ImplementationRule, MatchPattern, +}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; +use crate::optimizer::plan_utils::scan_column; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; -use crate::single_mapping; +use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; +use crate::types::value::DataValue; +use std::collections::HashSet; use std::sync::LazyLock; static FILTER_PATTERN: LazyLock = LazyLock::new(|| Pattern { @@ -28,9 +35,553 @@ static FILTER_PATTERN: LazyLock = LazyLock::new(|| Pattern { #[derive(Clone)] pub struct FilterImplementation; -// Without a selectivity estimate, retain the input estimate rather than assuming a filtering benefit. -single_mapping!( - FilterImplementation, - FILTER_PATTERN, - PhysicalOption::new(PlanImpl::Filter, SortOption::Follow, RowCountOption::Follow) -); +impl MatchPattern for FilterImplementation { + fn pattern(&self) -> &Pattern { + &FILTER_PATTERN + } +} + +impl ImplementationRule for FilterImplementation { + fn update_best_option( + &self, + plan: &LogicalPlan, + arena: &PlanArena, + loader: &StatisticMetaLoader<'_>, + best_physical_option: &mut BestPhysicalOption, + ) -> Result<(), DatabaseError> { + let rows = estimate_filter_rows(plan, arena, loader)?; + let row_count = rows.map_or(RowCountOption::Follow, |rows| RowCountOption::Estimated { + rows, + ndv: None, + }); + keep_best_physical_option( + best_physical_option, + PhysicalOption::new(PlanImpl::Filter, SortOption::Follow, row_count), + None, + ); + Ok(()) + } +} + +struct PointFilterVisitor<'a, A> { + arena: &'a A, + column: Option, + values: HashSet<&'a DataValue>, + supported: bool, +} + +impl<'a, A: MetaArena> PointFilterVisitor<'a, A> { + fn bind_column(&mut self, column: ColumnRef) { + if self + .column + .is_some_and(|previous| !self.arena.same_column(previous, column)) + { + self.supported = false; + } else { + self.column = Some(column); + } + } + + fn add_value(&mut self, column: ColumnRef, expr: ExprRef) { + let arena = self.arena; + let ScalarExpression::Constant(value) = arena.expression(expr) else { + self.supported = false; + return; + }; + if value.is_null() { + return; + } + if value.logical_type() != *arena.column(column).datatype() { + self.supported = false; + return; + } + self.values.insert(value); + } +} + +impl ExprVisitor for PointFilterVisitor<'_, A> { + fn visit_expression_ref(&mut self, expr: ExprRef, _arena: &A) -> Result { + if !self.supported { + return Ok(false); + } + let arena = self.arena; + match arena.expression(expr) { + ScalarExpression::Binary { + op: BinaryOperator::Or, + .. + } => return Ok(true), + ScalarExpression::Binary { + op: BinaryOperator::Eq, + left_expr, + right_expr, + .. + } => { + let (column, value) = + match (arena.expression(*left_expr), arena.expression(*right_expr)) { + ( + ScalarExpression::ColumnRef { column, .. }, + ScalarExpression::Constant(_), + ) => (*column, *right_expr), + ( + ScalarExpression::Constant(_), + ScalarExpression::ColumnRef { column, .. }, + ) => (*column, *left_expr), + _ => { + self.supported = false; + return Ok(false); + } + }; + self.bind_column(column); + self.add_value(column, value); + } + ScalarExpression::In { + negated: false, + expr, + args, + .. + } => { + let ScalarExpression::ColumnRef { column, .. } = arena.expression(*expr) else { + self.supported = false; + return Ok(false); + }; + self.bind_column(*column); + for &arg in args { + self.add_value(*column, arg); + } + } + _ => self.supported = false, + } + Ok(false) + } +} + +fn estimate_filter_rows( + plan: &LogicalPlan, + arena: &PlanArena, + loader: &StatisticMetaLoader<'_>, +) -> Result, DatabaseError> { + let (Operator::Filter(filter), Childrens::Only(input)) = + (&plan.operator, plan.childrens.as_ref()) + else { + return Ok(None); + }; + let Some(rows) = input.estimated_rows() else { + return Ok(None); + }; + // Do not apply the same point selectivity again to an already restricted + // index or an earlier estimated filter. Start with unrestricted scan inputs. + if !matches!( + input.row_count_option().map(|option| &option.plan), + Some(PlanImpl::SeqScan) + ) { + return Ok(None); + } + let mut visitor = PointFilterVisitor { + arena, + column: None, + values: HashSet::new(), + supported: true, + }; + visitor.visit(filter.predicate, arena)?; + let Some(column) = visitor.column.filter(|_| visitor.supported) else { + return Ok(None); + }; + let Some((table, column)) = scan_column(input, column, arena)? else { + return Ok(None); + }; + let Some(meta) = loader.load_column(table, column) else { + return Ok(None); + }; + let ndv = meta.distinct_values_len(); + if ndv == 0 || visitor.values.is_empty() || meta.values_len() == 0 { + return Ok(Some(0)); + } + // Uniform single-column estimate only: no products of correlated selectivities. + let non_null_fraction = + meta.values_len() as f64 / meta.values_len().saturating_add(meta.null_count()) as f64; + let selectivity = visitor.values.len().min(ndv) as f64 / ndv as f64; + Ok(Some( + ((rows as f64 * non_null_fraction * selectivity).ceil() as usize).min(rows), + )) +} + +// GRCOV_EXCL_START +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::*; + use crate::catalog::{ColumnCatalog, ColumnDesc}; + use crate::expression::AliasType; + use crate::optimizer::core::histogram::{ + HistogramMetaBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR, + }; + use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; + use crate::optimizer::rule::implementation::ImplementationRuleImpl; + use crate::planner::operator::filter::FilterOperator; + use crate::planner::operator::limit::LimitOperator; + use crate::planner::operator::project::ProjectOperator; + use crate::planner::operator::table_scan::TableScanOperator; + use crate::planner::TableArenaCell; + use crate::storage::StatisticsMetaCache; + use crate::types::index::{IndexInfo, IndexMeta, IndexType}; + use crate::types::LogicalType; + + fn input(arena: &mut PlanArena, rows: usize) -> (LogicalPlan, [ExprRef; 2]) { + let mut columns = Vec::new(); + let mut keys = Vec::new(); + for id in 0..2 { + let mut column = ColumnCatalog::new( + format!("c{id}"), + true, + ColumnDesc::new(LogicalType::Integer, None, false, None).unwrap(), + ); + column.set_ref_table("t".into(), id, false); + let column = arena.alloc_column(column); + columns.push(column); + keys.push(arena.alloc_expression(ScalarExpression::column_expr(column, id as usize))); + } + let mut plan = LogicalPlan::new( + Operator::TableScan(TableScanOperator { + table_name: "t".into(), + columns, + limit: (None, None), + index_infos: vec![], + with_pk: false, + }), + Childrens::None, + ); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated { rows, ndv: None }, + )); + (plan, [keys[0], keys[1]]) + } + + fn statistics(non_null: usize, nulls: usize, ndv: usize) -> StatisticsMetaCache { + let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR).unwrap(); + for i in 0..non_null { + builder.append(&DataValue::Int32((i % ndv) as i32)); + } + for _ in 0..nulls { + builder.append(&DataValue::Null); + } + let mut cache = StatisticsMetaCache::default(); + cache.columns.insert(("t".into(), 0), builder.build()); + cache + } + + fn binary(arena: &mut PlanArena, op: BinaryOperator, left: ExprRef, right: ExprRef) -> ExprRef { + arena.alloc_expression(ScalarExpression::Binary { + op, + left_expr: left, + right_expr: right, + evaluator: None, + ty: LogicalType::Boolean, + }) + } + + fn equal(arena: &mut PlanArena, column: ExprRef, value: DataValue) -> ExprRef { + let value = arena.alloc_expression(ScalarExpression::Constant(value)); + binary(arena, BinaryOperator::Eq, column, value) + } + + fn in_list( + arena: &mut PlanArena, + column: ExprRef, + values: &[DataValue], + negated: bool, + ) -> ExprRef { + let args = values + .iter() + .map(|value| arena.alloc_expression(ScalarExpression::Constant(value.clone()))) + .collect(); + arena.alloc_expression(ScalarExpression::In { + negated, + expr: column, + args, + evaluator: None, + }) + } + + #[test] + fn point_estimates_deduplicate_in_and_or_values() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (scan, keys) = input(&mut arena, 100); + let cache = statistics(100, 0, 10); + let loader = StatisticMetaLoader::new(&cache); + let eq = equal(&mut arena, keys[0], DataValue::Int32(1)); + let list = in_list( + &mut arena, + keys[0], + &[ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Null, + ], + false, + ); + let union = binary(&mut arena, BinaryOperator::Or, eq, list); + for (predicate, expected) in [(eq, 10), (list, 20), (union, 20)] { + let plan = FilterOperator::build(predicate, scan.clone(), false); + assert_eq!( + estimate_filter_rows(&plan, &arena, &loader)?, + Some(expected) + ); + } + let value = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(1))); + let reversed = binary(&mut arena, BinaryOperator::Eq, value, keys[0]); + assert_eq!( + estimate_filter_rows( + &FilterOperator::build(reversed, scan.clone(), false), + &arena, + &loader + )?, + Some(10) + ); + let many = in_list( + &mut arena, + keys[0], + &(0..20).map(DataValue::Int32).collect::>(), + false, + ); + assert_eq!( + estimate_filter_rows(&FilterOperator::build(many, scan, false), &arena, &loader)?, + Some(100) + ); + Ok(()) + } + + #[test] + fn point_estimates_account_for_nulls_empty_inputs_and_missing_stats( + ) -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (scan, keys) = input(&mut arena, 100); + let eq = equal(&mut arena, keys[0], DataValue::Int32(1)); + let null = equal(&mut arena, keys[0], DataValue::Null); + let mixed = in_list( + &mut arena, + keys[0], + &[DataValue::Null, DataValue::Int32(1)], + false, + ); + let all_null = in_list(&mut arena, keys[0], &[DataValue::Null], false); + for (non_null, nulls, ndv, expected) in [(75, 25, 5, 15), (0, 100, 0, 0), (0, 0, 0, 0)] { + let cache = statistics(non_null, nulls, ndv); + let loader = StatisticMetaLoader::new(&cache); + assert_eq!( + estimate_filter_rows( + &FilterOperator::build(eq, scan.clone(), false), + &arena, + &loader + )?, + Some(expected) + ); + assert_eq!( + estimate_filter_rows( + &FilterOperator::build(mixed, scan.clone(), false), + &arena, + &loader + )?, + Some(expected) + ); + for predicate in [null, all_null] { + assert_eq!( + estimate_filter_rows( + &FilterOperator::build(predicate, scan.clone(), false), + &arena, + &loader + )?, + Some(0) + ); + } + } + let empty_cache = StatisticsMetaCache::default(); + let loader = StatisticMetaLoader::new(&empty_cache); + let plan = FilterOperator::build(eq, scan, false); + let mut best = None; + FilterImplementation.update_best_option(&plan, &arena, &loader, &mut best)?; + assert_eq!(best.unwrap().0.row_count, RowCountOption::Follow); + let (empty, _) = input(&mut arena, 0); + let cache = statistics(100, 0, 10); + assert_eq!( + estimate_filter_rows( + &FilterOperator::build(eq, empty, false), + &arena, + &StatisticMetaLoader::new(&cache) + )?, + Some(0) + ); + Ok(()) + } + + #[test] + fn unsupported_predicates_and_restricted_inputs_keep_follow() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (mut scan, keys) = input(&mut arena, 100); + let cache = statistics(100, 0, 10); + let loader = StatisticMetaLoader::new(&cache); + let eq = equal(&mut arena, keys[0], DataValue::Int32(1)); + let other = equal(&mut arena, keys[1], DataValue::Int32(2)); + let multi_column = binary(&mut arena, BinaryOperator::Or, eq, other); + let and = binary(&mut arena, BinaryOperator::And, eq, eq); + let column_eq = binary(&mut arena, BinaryOperator::Eq, keys[0], keys[1]); + let not_in = in_list(&mut arena, keys[0], &[DataValue::Int32(1)], true); + let wrong_type = equal(&mut arena, keys[0], DataValue::Int64(1)); + let cast = arena.alloc_expression(ScalarExpression::TypeCast { + expr: keys[0], + ty: LogicalType::Integer, + evaluator: None, + }); + let cast_eq = equal(&mut arena, cast, DataValue::Int32(1)); + for predicate in [multi_column, and, column_eq, not_in, wrong_type, cast_eq] { + assert_eq!( + estimate_filter_rows( + &FilterOperator::build(predicate, scan.clone(), false), + &arena, + &loader + )?, + None + ); + } + scan.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; + assert_eq!( + estimate_filter_rows( + &FilterOperator::build(eq, scan.clone(), false), + &arena, + &loader + )?, + None + ); + scan.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { + rows: 100, + ndv: None, + }; + let Operator::TableScan(op) = &mut scan.operator else { + unreachable!() + }; + op.limit = (Some(1), Some(5)); + assert_eq!( + estimate_filter_rows( + &FilterOperator::build(eq, scan.clone(), false), + &arena, + &loader + )?, + None + ); + let limited = LogicalPlan::new( + Operator::Limit(LimitOperator { + offset: None, + limit: Some(5), + }), + Childrens::Only(Box::new(scan)), + ); + assert_eq!( + estimate_filter_rows(&FilterOperator::build(eq, limited, false), &arena, &loader)?, + None + ); + let (scan, _) = input(&mut arena, 100); + let mut estimated = FilterOperator::build(eq, scan, false); + estimated.physical_option = Some(PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Estimated { + rows: 10, + ndv: None, + }, + )); + assert_eq!( + estimate_filter_rows( + &FilterOperator::build(eq, estimated, false), + &arena, + &loader + )?, + None + ); + Ok(()) + } + + #[test] + fn point_selectivity_is_not_applied_again_to_index_output() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (mut scan, keys) = input(&mut arena, 10); + let meta = arena.alloc_index(IndexMeta { + id: 1, + table_name: "t".into(), + name: "idx".into(), + column_ids: vec![0], + ty: IndexType::Normal, + pk_ty: LogicalType::Integer, + value_ty: LogicalType::Integer, + }); + scan.physical_option.as_mut().unwrap().plan = PlanImpl::IndexScan(Box::new(IndexInfo { + meta, + sort_option: SortOption::None, + lookup: None, + residual_predicate: None, + covered_deserializers: None, + cover_mapping: None, + sort_elimination_hint: None, + stream_aggregate_hint: None, + })); + let predicate = equal(&mut arena, keys[0], DataValue::Int32(1)); + let cache = statistics(100, 0, 10); + let mut plan = FilterOperator::build(predicate, scan, false); + let mut best = None; + FilterImplementation.update_best_option( + &plan, + &arena, + &StatisticMetaLoader::new(&cache), + &mut best, + )?; + plan.physical_option = best.map(|(option, _)| option); + assert_eq!( + plan.physical_option.as_ref().unwrap().row_count, + RowCountOption::Follow + ); + assert_eq!(plan.estimated_rows(), Some(10)); + Ok(()) + } + + #[test] + fn direct_aliases_and_dynamic_implementation_path_use_column_stats() -> Result<(), DatabaseError> + { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (scan, keys) = input(&mut arena, 100); + let alias = arena.alloc_expression(ScalarExpression::Alias { + expr: keys[0], + alias: AliasType::Name("alias".into()), + }); + let column = alias.output_column_ref(&mut arena); + let key = arena.alloc_expression(ScalarExpression::column_expr(column, 0)); + let mut project = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: vec![alias] }), + Childrens::Only(Box::new(scan)), + ); + project.physical_option = Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )); + let eq = equal(&mut arena, key, DataValue::Int32(1)); + let plan = FilterOperator::build(eq, project, false); + let cache = statistics(100, 0, 10); + let loader = StatisticMetaLoader::new(&cache); + assert_eq!(estimate_filter_rows(&plan, &arena, &loader)?, Some(10)); + let pipeline = HepOptimizerPipeline::builder() + .implementations(vec![ImplementationRuleImpl::Filter]) + .build(); + let mut plan = pipeline + .instantiate(plan) + .find_best(Some(&loader), &mut arena)?; + assert_eq!(plan.estimated_rows(), Some(10)); + assert_eq!(plan.estimated_startup_work()?, Some(0)); + Ok(()) + } +} +// GRCOV_EXCL_STOP diff --git a/src/optimizer/rule/implementation/dql/join.rs b/src/optimizer/rule/implementation/dql/join.rs index 510e0fec..b9671d25 100644 --- a/src/optimizer/rule/implementation/dql/join.rs +++ b/src/optimizer/rule/implementation/dql/join.rs @@ -17,6 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::{Operator, PhysicalOption, RowCountOption, SortOption}; +use crate::planner::LogicalPlan; use std::sync::LazyLock; static JOIN_PATTERN: LazyLock = LazyLock::new(|| Pattern { @@ -35,12 +36,12 @@ impl MatchPattern for JoinImplementation { impl ImplementationRule for JoinImplementation { fn update_best_option( &self, - op: &Operator, + plan: &LogicalPlan, _: &crate::planner::PlanArena, _: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { - let Operator::Join(operator) = op else { + let Operator::Join(operator) = &plan.operator else { unreachable!() }; let physical_option = PhysicalOption::new( diff --git a/src/optimizer/rule/implementation/dql/mark_apply.rs b/src/optimizer/rule/implementation/dql/mark_apply.rs index 7c6502ad..31c7b682 100644 --- a/src/optimizer/rule/implementation/dql/mark_apply.rs +++ b/src/optimizer/rule/implementation/dql/mark_apply.rs @@ -19,7 +19,7 @@ use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; -use crate::planner::PlanArena; +use crate::planner::{LogicalPlan, PlanArena}; use std::sync::LazyLock; static MARK_APPLY_PATTERN: LazyLock = LazyLock::new(|| Pattern { @@ -38,12 +38,12 @@ impl MatchPattern for MarkApplyImplementation { impl ImplementationRule for MarkApplyImplementation { fn update_best_option( &self, - op: &Operator, + plan: &LogicalPlan, _: &PlanArena, _: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { - let Operator::MarkApply(op) = op else { + let Operator::MarkApply(op) = &plan.operator else { return Ok(()); }; // A mark adds one value to each left row; INNER JOIN may duplicate rows. diff --git a/src/optimizer/rule/implementation/dql/sort.rs b/src/optimizer/rule/implementation/dql/sort.rs index 13493a32..9202ed16 100644 --- a/src/optimizer/rule/implementation/dql/sort.rs +++ b/src/optimizer/rule/implementation/dql/sort.rs @@ -17,6 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; +use crate::planner::LogicalPlan; use std::sync::LazyLock; static SORT_PATTERN: LazyLock = LazyLock::new(|| Pattern { @@ -35,12 +36,12 @@ impl MatchPattern for SortImplementation { impl ImplementationRule for SortImplementation { fn update_best_option( &self, - op: &Operator, + plan: &LogicalPlan, _: &crate::planner::PlanArena, _: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { - if let Operator::Sort(op) = op { + if let Operator::Sort(op) = &plan.operator { crate::optimizer::core::rule::keep_best_physical_option( best_physical_option, PhysicalOption::new( diff --git a/src/optimizer/rule/implementation/dql/table_scan.rs b/src/optimizer/rule/implementation/dql/table_scan.rs index 18739a8f..6c89ac04 100644 --- a/src/optimizer/rule/implementation/dql/table_scan.rs +++ b/src/optimizer/rule/implementation/dql/table_scan.rs @@ -20,7 +20,7 @@ use crate::optimizer::core::rule::{ }; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; -use crate::planner::PlanArena; +use crate::planner::{LogicalPlan, PlanArena}; use crate::types::index::{IndexLookup, IndexType}; use std::sync::LazyLock; @@ -40,12 +40,12 @@ impl MatchPattern for SeqScanImplementation { impl ImplementationRule for SeqScanImplementation { fn update_best_option( &self, - op: &Operator, + plan: &LogicalPlan, arena: &PlanArena, loader: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { - if let Operator::TableScan(scan_op) = op { + if let Operator::TableScan(scan_op) = &plan.operator { let cost = scan_op .index_infos .iter() @@ -85,12 +85,12 @@ impl MatchPattern for IndexScanImplementation { impl ImplementationRule for IndexScanImplementation { fn update_best_option( &self, - op: &Operator, + plan: &LogicalPlan, arena: &PlanArena, loader: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { - if let Operator::TableScan(scan_op) = op { + if let Operator::TableScan(scan_op) = &plan.operator { for index_info in scan_op.index_infos.iter() { let Some(IndexLookup::Static(range)) = &index_info.lookup else { continue; diff --git a/src/optimizer/rule/implementation/dql/top_k.rs b/src/optimizer/rule/implementation/dql/top_k.rs index 4dc99cf9..76ebf242 100644 --- a/src/optimizer/rule/implementation/dql/top_k.rs +++ b/src/optimizer/rule/implementation/dql/top_k.rs @@ -17,6 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; +use crate::planner::LogicalPlan; use std::sync::LazyLock; static TOPK_PATTERN: LazyLock = LazyLock::new(|| Pattern { @@ -35,12 +36,12 @@ impl MatchPattern for TopKImplementation { impl ImplementationRule for TopKImplementation { fn update_best_option( &self, - op: &Operator, + plan: &LogicalPlan, _: &crate::planner::PlanArena, _: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { - if let Operator::TopK(op) = op { + if let Operator::TopK(op) = &plan.operator { crate::optimizer::core::rule::keep_best_physical_option( best_physical_option, PhysicalOption::new( diff --git a/src/optimizer/rule/implementation/dql/window.rs b/src/optimizer/rule/implementation/dql/window.rs index 532b63c5..ef28895f 100644 --- a/src/optimizer/rule/implementation/dql/window.rs +++ b/src/optimizer/rule/implementation/dql/window.rs @@ -17,6 +17,7 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption}; +use crate::planner::LogicalPlan; use std::sync::LazyLock; static WINDOW_PATTERN: LazyLock = LazyLock::new(|| Pattern { @@ -35,12 +36,12 @@ impl MatchPattern for WindowImplementation { impl ImplementationRule for WindowImplementation { fn update_best_option( &self, - op: &Operator, + plan: &LogicalPlan, _: &crate::planner::PlanArena, _: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { - if let Operator::Window(op) = op { + if let Operator::Window(op) = &plan.operator { crate::optimizer::core::rule::keep_best_physical_option( best_physical_option, PhysicalOption::new(PlanImpl::Window, op.sort_option(), RowCountOption::Follow), diff --git a/src/optimizer/rule/implementation/macros.rs b/src/optimizer/rule/implementation/macros.rs index 15014862..14d4289f 100644 --- a/src/optimizer/rule/implementation/macros.rs +++ b/src/optimizer/rule/implementation/macros.rs @@ -24,7 +24,7 @@ macro_rules! single_mapping { impl ImplementationRule for $ty { fn update_best_option( &self, - _: &Operator, + _: &$crate::planner::LogicalPlan, _: &$crate::planner::PlanArena, _: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, diff --git a/src/optimizer/rule/implementation/mod.rs b/src/optimizer/rule/implementation/mod.rs index 1671bc1c..40f08eee 100644 --- a/src/optimizer/rule/implementation/mod.rs +++ b/src/optimizer/rule/implementation/mod.rs @@ -55,6 +55,7 @@ use crate::optimizer::rule::implementation::dql::top_k::TopKImplementation; use crate::optimizer::rule::implementation::dql::values::ValuesImplementation; use crate::optimizer::rule::implementation::dql::window::WindowImplementation; use crate::planner::operator::Operator; +use crate::planner::{LogicalPlan, PlanArena}; #[repr(usize)] #[derive(Debug, Copy, Clone, Eq, PartialEq)] @@ -258,14 +259,14 @@ impl ImplementationRuleImpl { impl ImplementationRule for ImplementationRuleImpl { fn update_best_option( &self, - operator: &Operator, - arena: &crate::planner::PlanArena, + plan: &LogicalPlan, + arena: &PlanArena, loader: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { macro_rules! update { ($implementation:expr) => { - $implementation.update_best_option(operator, arena, loader, best_physical_option)? + $implementation.update_best_option(plan, arena, loader, best_physical_option)? }; } @@ -380,7 +381,8 @@ mod tests { let statistics_cache = StatisticsMetaCache::default(); let loader = StatisticMetaLoader::new(&statistics_cache); let mut best = None; - rule.update_best_option(operator, arena, &loader, &mut best)?; + let plan = LogicalPlan::new(operator.clone(), Childrens::None); + rule.update_best_option(&plan, arena, &loader, &mut best)?; Ok(best .expect("implementation rule should produce an option") diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 56109878..d33d6a18 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -13,22 +13,18 @@ // limitations under the License. use crate::catalog::ColumnRef; -use crate::catalog::TableName; use crate::errors::DatabaseError; -use crate::expression::visitor::ExprVisitor; use crate::expression::visitor_mut::ExprVisitorMut; -use crate::expression::{AliasType, ScalarExpression}; +use crate::expression::ScalarExpression; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; +use crate::optimizer::plan_utils::scan_column; use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::operator::project::ProjectOperator; -use crate::planner::operator::table_scan::TableScanOperator; -use crate::planner::operator::visitor::OperatorVisitor; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; use crate::types::index::{IndexMeta, IndexMetaRef, IndexType}; use crate::types::tuple::Schema; -use crate::types::ColumnId; use std::mem::swap; pub(crate) struct GreedyJoinReorder; @@ -74,9 +70,9 @@ impl NormalizationRule for OrientNestedLoopJoin { } else if right_rows == 0 { true } else { - // T(input) = startup + rows * row-work. Count the inner - // startup again for every outer row; shared output work is - // omitted from this local direction comparison. + // Repeat the complete inner work for every outer row, + // including rows discarded by filters. Shared output work + // is omitted from this local direction comparison. let (Some(left_work), Some(right_work)) = (left.estimated_work()?, right.estimated_work()?) else { @@ -154,138 +150,6 @@ struct JoinCandidate { right: usize, } -struct ScanColumnVisitor<'plan, 'arena, A> { - column: Option, - source: Option<(&'plan TableName, ColumnId)>, - arena: &'arena A, -} - -impl<'plan, A: MetaArena> OperatorVisitor<'plan> for ScanColumnVisitor<'plan, '_, A> { - fn visit_plan(&mut self, plan: &'plan LogicalPlan) -> Result<(), DatabaseError> { - if self.column.is_none() { - return Ok(()); - } - self.visit_operator(&plan.operator, plan.physical_option.as_ref())?; - let follow = match &plan.operator { - Operator::Project(_) => true, - Operator::Join(_) | Operator::Aggregate(_) | Operator::Limit(_) | Operator::TopK(_) => { - false - } - _ => plan - .physical_option - .as_ref() - .is_some_and(|option| matches!(option.row_count, RowCountOption::Follow)), - }; - if follow && self.column.is_some() { - self.visit_children(&plan.childrens)?; - } - Ok(()) - } - - fn visit_children(&mut self, childrens: &'plan Childrens) -> Result<(), DatabaseError> { - match childrens { - Childrens::Only(input) | Childrens::Twins { left: input, .. } => self.visit_plan(input), - Childrens::None => Ok(()), - } - } - - fn visit_table_scan(&mut self, scan: &'plan TableScanOperator) -> Result<(), DatabaseError> { - if scan.limit == (None, None) { - if let Some(source) = self.column.and_then(|column| { - scan.columns - .iter() - .find(|source| self.arena.same_column(**source, column)) - }) { - self.source = self - .arena - .column(*source) - .id() - .map(|id| (&scan.table_name, id)); - } - } - self.column = None; - Ok(()) - } - - fn visit_project(&mut self, project: &'plan ProjectOperator) -> Result<(), DatabaseError> { - let Some(column) = self.column.take() else { - return Ok(()); - }; - let arena = self.arena; - for &candidate in &project.exprs { - let source = match arena.expression(candidate) { - ScalarExpression::Alias { - expr, - alias: AliasType::Name(_), - } if arena.column(column).id().is_none() - && arena.column(column).table_name().is_none() - && candidate.output_name(arena) == arena.column(column).name() => - { - *expr - } - ScalarExpression::Alias { - expr, - alias: AliasType::Expr(alias), - } if matches!(arena.expression(*alias), ScalarExpression::ColumnRef { column: target, .. } - if arena.same_column(*target, column)) => - { - *expr - } - ScalarExpression::ColumnRef { column: target, .. } - if arena.same_column(*target, column) => - { - candidate - } - _ => continue, - }; - return ExprVisitor::visit(self, source, arena); - } - Ok(()) - } -} - -impl ExprVisitor for ScanColumnVisitor<'_, '_, A> { - fn visit_expression( - &mut self, - expr: &ScalarExpression, - _arena: &A, - ) -> Result { - Ok(matches!( - expr, - ScalarExpression::Alias { .. } | ScalarExpression::ColumnRef { .. } - )) - } - - fn visit_alias( - &mut self, - expr: ExprRef, - _alias: &AliasType, - arena: &A, - ) -> Result<(), DatabaseError> { - // The alias target describes the output, not the source column. - self.visit(expr, arena) - } - - fn visit_column_ref(&mut self, column: &ColumnRef) -> Result<(), DatabaseError> { - self.column = Some(*column); - Ok(()) - } -} - -fn scan_column<'plan>( - plan: &'plan LogicalPlan, - column: ColumnRef, - arena: &PlanArena, -) -> Result, DatabaseError> { - let mut visitor = ScanColumnVisitor { - column: Some(column), - source: None, - arena, - }; - visitor.visit_plan(plan)?; - Ok(visitor.source) -} - fn column_statistics( left: &LogicalPlan, right: &LogicalPlan, @@ -403,6 +267,9 @@ struct JoinInputEstimate { } fn estimate_join_rows(left: JoinInputEstimate, right: JoinInputEstimate) -> usize { + // J = min(ceil(L * R / D), left_unique ? R : inf, right_unique ? L : inf). + // L/R are rows, N_L/N_R are NDVs; D = max(min(N_L, L), min(N_R, R), 1). + // Missing NDV means D = 1; either known NDV of zero means J = 0. let mut rows = left.rows.saturating_mul(right.rows); if left.ndv == Some(0) || right.ndv == Some(0) { return 0; @@ -781,7 +648,7 @@ impl NormalizationRule for GreedyJoinReorder { mod tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc}; - use crate::expression::BinaryOperator; + use crate::expression::{AliasType, BinaryOperator}; use crate::optimizer::core::histogram::{ HistogramMetaBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR, }; @@ -864,6 +731,54 @@ mod tests { (plan, key, meta) } + #[test] + fn filtered_input_work_changes_nested_loop_direction() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (left, _) = input("small", 7, &mut arena); + let (right, key) = input("filtered", 98, &mut arena); + let value = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(1))); + let predicate = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Eq, + left_expr: key, + right_expr: value, + ty: LogicalType::Boolean, + evaluator: None, + }); + let mut right = FilterOperator::build(predicate, right, false); + right.physical_option = Some(PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Estimated { rows: 8, ndv: None }, + )); + assert_eq!(right.estimated_startup_work()?, Some(0)); + assert_eq!(right.estimated_work()?, Some(392)); + let column = key.unpack_bound_col(&arena, false).unwrap().0; + assert_eq!( + scan_column(&right, column, &arena)?, + Some((&"filtered".into(), 0)) + ); + let mut plan = + JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::NestLoopJoin, + SortOption::None, + RowCountOption::Unknown, + )); + let schema = plan.output_schema(&mut arena).clone(); + assert!(OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + assert_eq!(plan.estimated_rows(), Some(56)); + let Childrens::Only(join) = plan.childrens.as_ref() else { + unreachable!() + }; + let Childrens::Twins { left, .. } = join.childrens.as_ref() else { + unreachable!() + }; + assert!(matches!(left.operator, Operator::Filter(_))); + Ok(()) + } + #[test] fn column_statistics_are_read_from_scan_without_an_index() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 9374ed68..40f41dd5 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -352,6 +352,7 @@ impl NormalizationRule for ParameterizeInnerJoin { arena: &mut PlanArena<'_>, _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { + let rows = plan.estimated_rows(); let (Operator::Join(join), Childrens::Twins { left, right }) = (&mut plan.operator, plan.childrens.as_mut()) else { @@ -430,13 +431,22 @@ impl NormalizationRule for ParameterizeInnerJoin { ty: LogicalType::Boolean, })); } - let apply = LogicalPlan::new( + let mut apply = LogicalPlan::new( Operator::MarkApply(MarkApplyOperator::new_inner_join(predicates, probe)), Childrens::Twins { left: Box::new(left), right: Box::new(right), }, ); + // This is the same join output, not the per-key cardinality of the probe. + apply.physical_option = Some(PhysicalOption::new( + PlanImpl::MarkApply, + SortOption::Follow, + rows.map_or(RowCountOption::Unknown, |rows| RowCountOption::Estimated { + rows, + ndv: None, + }), + )); *plan = if let Some(mut project) = project { project.childrens = Box::new(Childrens::Only(Box::new(apply))); project @@ -449,7 +459,7 @@ impl NormalizationRule for ParameterizeInnerJoin { plan.physical_option = Some(PhysicalOption::new( PlanImpl::Filter, SortOption::Follow, - RowCountOption::Unknown, + RowCountOption::Follow, )); } Ok(true) @@ -609,6 +619,7 @@ mod inner_join_tests { use crate::catalog::{ColumnCatalog, ColumnDesc}; use crate::expression::range_detacher::Range; use crate::expression::{AliasType, UnaryOperator}; + use crate::optimizer::rule::normalization::join_reorder::OrientNestedLoopJoin; use crate::planner::operator::join::JoinOperator; use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::mark_apply::MarkApplyKind; @@ -897,6 +908,123 @@ mod inner_join_tests { Ok(()) } + #[test] + fn parameterized_join_preserves_rows_without_guessing_probe_cardinality( + ) -> Result<(), DatabaseError> { + for rows in [None, Some(0), Some(120)] { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut left = scan(&mut arena, "outer", &[LogicalType::Integer], &[]); + let mut right = scan(&mut arena, "inner", &[LogicalType::Integer], &[&[0]]); + let l = column(&left, 0, &mut arena); + let r = column(&right, 0, &mut arena); + left.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated { + rows: 129, + ndv: None, + }, + )); + right.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated { + rows: 120, + ndv: None, + }, + )); + let mut plan = join(left, right, vec![(l, r)], None); + if let Some(rows) = rows { + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::HashJoin, + SortOption::None, + RowCountOption::Estimated { rows, ndv: None }, + )); + plan.physical_option.as_mut().unwrap().startup_work = Some(Some(9999)); + } + assert!(ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); + let option = plan.physical_option.as_ref().unwrap(); + assert_eq!(option.plan, PlanImpl::MarkApply); + assert_eq!(option.startup_work, None); + assert_eq!(plan.estimated_rows(), rows); + let Childrens::Twins { right, .. } = plan.childrens.as_ref() else { + unreachable!() + }; + assert_eq!(right.estimated_rows(), None); + assert_eq!( + right.physical_option.as_ref().unwrap().row_count, + RowCountOption::Unknown + ); + assert_eq!(plan.estimated_startup_work()?, Some(0)); + } + Ok(()) + } + + #[test] + fn parameterized_join_allows_parent_nested_loop_orientation() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut left = scan(&mut arena, "outer", &[LogicalType::Integer], &[]); + let mut right = scan(&mut arena, "inner", &[LogicalType::Integer], &[&[0]]); + let l = column(&left, 0, &mut arena); + let r = column(&right, 0, &mut arena); + left.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated { + rows: 129, + ndv: None, + }, + )); + right.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated { + rows: 120, + ndv: None, + }, + )); + let mut apply = join(left, right, vec![(l, r)], None); + apply.physical_option = Some(PhysicalOption::new( + PlanImpl::HashJoin, + SortOption::None, + RowCountOption::Estimated { + rows: 10, + ndv: None, + }, + )); + assert!(ParameterizeInnerJoin.apply(&mut apply, &mut arena, None)?); + let mut other = scan(&mut arena, "other", &[LogicalType::Integer], &[]); + other.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated { + rows: 200, + ndv: None, + }, + )); + let mut plan = + JoinOperator::build(other, apply, JoinCondition::None, JoinType::Cross, false); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::NestLoopJoin, + SortOption::None, + RowCountOption::Unknown, + )); + let schema = plan.output_schema(&mut arena).clone(); + assert!(OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + assert_eq!(plan.estimated_rows(), Some(2000)); + let Childrens::Only(join) = plan.childrens.as_ref() else { + panic!("expected output-restoring projection") + }; + let Childrens::Twins { left, .. } = join.childrens.as_ref() else { + unreachable!() + }; + assert!(matches!(left.operator, Operator::MarkApply(_))); + Ok(()) + } + #[test] fn composite_probe_uses_index_order_and_preserves_static_filter() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); @@ -989,9 +1117,39 @@ mod inner_join_tests { let filter_left = arena.alloc_expression(ScalarExpression::column_expr(schema[2], 2)); let filter_right = arena.alloc_expression(ScalarExpression::column_expr(schema[3], 3)); let predicate = binary(&mut arena, BinaryOperator::Gt, filter_left, filter_right); + left.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated { + rows: 100, + ndv: None, + }, + )); + right.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated { + rows: 10, + ndv: None, + }, + )); let mut plan = join(left, right, vec![(l, r)], Some(predicate)); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::HashJoin, + SortOption::None, + RowCountOption::Estimated { + rows: 30, + ndv: None, + }, + )); assert!(ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.estimated_rows(), Some(30)); + assert_eq!( + plan.physical_option.as_ref().unwrap().row_count, + RowCountOption::Follow + ); + assert_eq!(plan.estimated_startup_work()?, Some(0)); assert_eq!(plan.output_schema(&mut arena), &schema); let Operator::Filter(filter) = &plan.operator else { panic!("join filter must remain above projection") @@ -1010,6 +1168,10 @@ mod inner_join_tests { let Operator::Project(project_op) = &project.operator else { panic!("expected reorder projection") }; + assert_eq!( + project.physical_option.as_ref().unwrap().row_count, + RowCountOption::Follow + ); assert_eq!( project_op .exprs @@ -1024,6 +1186,11 @@ mod inner_join_tests { let Operator::MarkApply(apply) = &apply_plan.operator else { panic!("expected apply") }; + assert_eq!(apply_plan.estimated_rows(), Some(30)); + assert_eq!( + apply_plan.physical_option.as_ref().unwrap().plan, + PlanImpl::MarkApply + ); assert_eq!(apply.parameterized_probe(), Some(&r)); assert_eq!((position(r, &arena), position(l, &arena)), (0, 2)); let ScalarExpression::Binary { diff --git a/src/orm/mod.rs b/src/orm/mod.rs index 52c6e582..3693b19c 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -4438,7 +4438,7 @@ mod tests { inner_using_plan, concat!( "Projection [orm_unit_users.id] [Project => (Sort Option: Follow)] ", - "InnerJoinApply ", + "InnerJoinApply [MarkApply => (Sort Option: Follow)] ", "TableScan orm_unit_users -> [orm_unit_users.id] [SeqScan => (Sort Option: None)] ", "TableScan orm_unit_orders -> [orm_unit_orders.id] [IndexScan By pk_index => Probe ? => (Sort Option: OrderBy: (orm_unit_orders.id Asc Nulls Last) ignore_prefix_len: 0)]" ), diff --git a/src/planner/mod.rs b/src/planner/mod.rs index d187412a..a6e9d81b 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -26,7 +26,7 @@ use crate::planner::operator::set_membership::SetMembershipOperator; use crate::planner::operator::union::UnionOperator; use crate::planner::operator::values::ValuesOperator; use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; -use crate::planner::operator::{Operator, PhysicalOption, RowCountOption, CPU_ROW_WORK}; +use crate::planner::operator::{Operator, PhysicalOption, RowCountOption}; use kite_sql_serde_macros::ReferenceSerialization; use std::fmt; use std::hash::{Hash, Hasher}; @@ -254,13 +254,10 @@ impl LogicalPlan { } pub(crate) fn estimated_work(&mut self) -> Result, DatabaseError> { - let (Some(startup), Some(rows)) = (self.estimated_startup_work()?, self.estimated_rows()) - else { - return Ok(None); - }; - Ok(Some( - startup.saturating_add(rows.saturating_mul(CPU_ROW_WORK)), - )) + self.estimated_startup_work()?; + Ok(self.physical_option.as_ref().and_then(|option| { + option.calculate_work(&self.operator, &self.childrens, self.estimated_rows()) + })) } pub(crate) fn clear_startup_work(&mut self) { @@ -610,10 +607,13 @@ mod tests { use crate::catalog::{ColumnCatalog, ColumnDesc, ColumnRef}; use crate::expression::ScalarExpression; use crate::planner::operator::describe::DescribeOperator; + use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::limit::LimitOperator; + use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::{PlanImpl, SortOption}; + use crate::types::value::DataValue; use crate::types::LogicalType; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; @@ -751,6 +751,107 @@ mod tests { Ok(()) } + #[test] + fn filter_and_projection_work_preserve_input_without_blocking() -> Result<(), DatabaseError> { + for output_rows in [0, 8, 98] { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let predicate = + arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(true))); + let mut scan = table_scan("input", vec![]); + scan.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated { + rows: 98, + ndv: None, + }, + )); + let mut filter = FilterOperator::build(predicate, scan, false); + filter.physical_option = Some(PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Estimated { + rows: output_rows, + ndv: None, + }, + )); + assert_eq!(filter.estimated_rows(), Some(output_rows)); + assert_eq!(filter.estimated_startup_work()?, Some(0)); + assert_eq!(filter.estimated_work()?, Some(392)); + let mut project = LogicalPlan::new( + Operator::Project(ProjectOperator { exprs: vec![] }), + Childrens::Only(Box::new(filter)), + ); + project.physical_option = Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )); + assert_eq!(project.estimated_startup_work()?, Some(0)); + assert_eq!(project.estimated_work()?, Some(392)); + } + Ok(()) + } + + #[test] + fn blocking_operator_consumes_complete_filtered_input_work() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let id = column("id", LogicalType::Integer, &mut arena); + let key = arena.alloc_expression(ScalarExpression::column_expr(id, 0)); + let predicate = + arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(true))); + let mut scan = table_scan("input", vec![id]); + scan.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Estimated { + rows: 98, + ndv: None, + }, + )); + let mut filter = FilterOperator::build(predicate, scan, false); + filter.physical_option = Some(PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Estimated { rows: 8, ndv: None }, + )); + let mut sort = LogicalPlan::new( + Operator::Sort(SortOperator { + sort_fields: vec![SortField { + expr: key, + asc: true, + nulls_first: false, + }], + }), + Childrens::Only(Box::new(filter)), + ); + sort.physical_option = Some(PhysicalOption::new( + PlanImpl::Sort, + SortOption::None, + RowCountOption::Follow, + )); + // 392 input work + 24 comparisons * 2 operator units, then 8 output rows. + assert_eq!(sort.estimated_startup_work()?, Some(440)); + assert_eq!(sort.estimated_work()?, Some(472)); + let mut unknown = table_scan("unknown", vec![]); + unknown.physical_option = Some(PhysicalOption::new( + PlanImpl::SeqScan, + SortOption::None, + RowCountOption::Unknown, + )); + let mut filter = FilterOperator::build(predicate, unknown, false); + filter.physical_option = Some(PhysicalOption::new( + PlanImpl::Filter, + SortOption::Follow, + RowCountOption::Estimated { rows: 8, ndv: None }, + )); + assert_eq!(filter.estimated_startup_work()?, Some(0)); + assert_eq!(filter.estimated_work()?, None); + Ok(()) + } + #[test] fn childrens_iterates_and_pops_expected_plans() { let left = LogicalPlan::new(Operator::ShowTable, Childrens::None); diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 37db75a7..14df253f 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -191,13 +191,39 @@ impl PhysicalOption { &self.sort_option } + pub(crate) fn calculate_work( + &self, + operator: &Operator, + childrens: &Childrens, + rows: Option, + ) -> Option { + match (&self.plan, operator, childrens) { + (PlanImpl::Filter, Operator::Filter(_), Childrens::Only(input)) + | (PlanImpl::Project, Operator::Project(_), Childrens::Only(input)) => { + // Output selectivity must not erase the work of reading the input. + // Predicate and projection CPU are not charged by this lower bound. + input.physical_option.as_ref()?.calculate_work( + &input.operator, + &input.childrens, + input.estimated_rows(), + ) + } + _ => Some( + self.startup_work + .flatten()? + .saturating_add(rows?.saturating_mul(CPU_ROW_WORK)), + ), + } + } + pub(crate) fn calculate_startup_work(&mut self, operator: &Operator, childrens: &Childrens) { let startup_work = |input: &LogicalPlan| input.physical_option.as_ref()?.startup_work.flatten(); let work = |input: &LogicalPlan| { - Some( - startup_work(input)? - .saturating_add(input.estimated_rows()?.saturating_mul(CPU_ROW_WORK)), + input.physical_option.as_ref()?.calculate_work( + &input.operator, + &input.childrens, + input.estimated_rows(), ) }; self.startup_work = Some(match (&self.plan, operator, childrens) { From 4ecab5926f8759c0dd78a9750f1b5d7f0a910bd9 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 13/62] perf: account for parameterized inner join input work --- .../rule/normalization/join_reorder.rs | 83 ++++++++++++++++++- src/planner/operator/mod.rs | 44 ++++++++-- 2 files changed, 121 insertions(+), 6 deletions(-) diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index d33d6a18..4153ab36 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -663,7 +663,7 @@ mod tests { use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::TableArenaCell; use crate::storage::StatisticsMetaCache; - use crate::types::index::IndexInfo; + use crate::types::index::{IndexInfo, IndexLookup}; use crate::types::value::DataValue; use crate::types::LogicalType; @@ -731,6 +731,87 @@ mod tests { (plan, key, meta) } + fn parameterized_input( + input_rows: usize, + output_rows: usize, + arena: &mut PlanArena, + ) -> LogicalPlan { + let (left, probe) = input("probe_input", input_rows, arena); + let (mut right, _, _) = indexed_input("lookup", 0, None, arena); + let option = right.physical_option.as_mut().unwrap(); + option.row_count = RowCountOption::Unknown; + let PlanImpl::IndexScan(info) = &mut option.plan else { + unreachable!() + }; + info.lookup = Some(IndexLookup::Probe); + let mut plan = LogicalPlan::new( + Operator::MarkApply(MarkApplyOperator::new_inner_join(Vec::new(), probe)), + Childrens::Twins { + left: Box::new(left), + right: Box::new(right), + }, + ); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::MarkApply, + SortOption::Follow, + RowCountOption::Estimated { + rows: output_rows, + ndv: None, + }, + )); + plan + } + + #[test] + fn parameterized_join_work_keeps_input_and_probe_cost() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut plan = parameterized_input(12, 13, &mut arena); + assert_eq!(plan.estimated_startup_work()?, Some(0)); + assert_eq!(plan.estimated_work()?, Some(48 + 12 + 52)); + assert_eq!(plan.estimated_rows(), Some(13)); + let Childrens::Twins { right, .. } = plan.childrens.as_ref() else { + unreachable!() + }; + assert_eq!(right.estimated_rows(), None); + + let Childrens::Twins { left, .. } = plan.childrens.as_mut() else { + unreachable!() + }; + left.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; + assert_eq!(plan.estimated_work()?, None); + Ok(()) + } + + #[test] + fn parameterized_join_work_changes_nested_loop_direction() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (left, _) = input("cheap", 12, &mut arena); + let right = parameterized_input(12, 13, &mut arena); + let mut plan = + JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::NestLoopJoin, + SortOption::None, + RowCountOption::Unknown, + )); + let schema = plan.output_schema(&mut arena).clone(); + assert!(OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + assert_eq!(plan.estimated_rows(), Some(156)); + let Childrens::Only(join) = plan.childrens.as_ref() else { + unreachable!() + }; + let Childrens::Twins { left, right } = join.childrens.as_ref() else { + unreachable!() + }; + assert!(matches!(left.operator, Operator::MarkApply(_))); + assert_eq!(left.estimated_rows(), Some(13)); + assert_eq!(right.estimated_rows(), Some(12)); + Ok(()) + } + #[test] fn filtered_input_work_changes_nested_loop_direction() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 14df253f..468d1a63 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -55,11 +55,18 @@ pub mod window; use self::recursive_cte::{RecursiveCteOperator, RecursiveScanOperator}; use self::{ - aggregate::AggregateOperator, alter_table::add_column::AddColumnOperator, - alter_table::change_column::ChangeColumnOperator, filter::FilterOperator, join::JoinOperator, - limit::LimitOperator, mark_apply::MarkApplyOperator, project::ProjectOperator, - scalar_query_init::ScalarQueryInitOperator, scalar_subquery::ScalarSubqueryOperator, - sort::SortOperator, table_scan::TableScanOperator, + aggregate::AggregateOperator, + alter_table::add_column::AddColumnOperator, + alter_table::change_column::ChangeColumnOperator, + filter::FilterOperator, + join::JoinOperator, + limit::LimitOperator, + mark_apply::{MarkApplyKind, MarkApplyOperator}, + project::ProjectOperator, + scalar_query_init::ScalarQueryInitOperator, + scalar_subquery::ScalarSubqueryOperator, + sort::SortOperator, + table_scan::TableScanOperator, }; use crate::catalog::ColumnRef; use crate::errors::DatabaseError; @@ -208,6 +215,33 @@ impl PhysicalOption { input.estimated_rows(), ) } + (PlanImpl::MarkApply, Operator::MarkApply(op), Childrens::Twins { left, right }) + if matches!(op.kind, MarkApplyKind::InnerJoin) + && op.parameterized_probe().is_some() + && !op.materialize => + { + // The output estimate is not the cost of repeatedly reading the + // input and probing. Keep Probe rows unknown, but charge at least + // one lookup operation and its startup for each input row. + // + // work = input_work + input_rows × lookup_work + output_rows × CPU_ROW_WORK. + let input_work = left.physical_option.as_ref()?.calculate_work( + &left.operator, + &left.childrens, + left.estimated_rows(), + )?; + let lookup_work = right + .physical_option + .as_ref()? + .startup_work + .flatten()? + .saturating_add(CPU_OPERATOR_WORK); + Some( + input_work + .saturating_add(left.estimated_rows()?.saturating_mul(lookup_work)) + .saturating_add(rows?.saturating_mul(CPU_ROW_WORK)), + ) + } _ => Some( self.startup_work .flatten()? From 3f40287c54749bea438d9bcb0db5b3d6c185b654 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 14/62] fix: preserve child work in cross join cost estimates --- .../rule/normalization/join_reorder.rs | 30 +++++++++++++++++++ src/planner/operator/mod.rs | 19 ++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 4153ab36..ad6a4af9 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -1707,6 +1707,36 @@ mod tests { Ok(()) } + #[test] + fn nested_cross_preserves_parameterized_input_work() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut expensive = parameterized_input(100, 2, &mut arena); + assert_eq!(expensive.estimated_work()?, Some(508)); + let (peer, _) = input("peer", 3, &mut arena); + let mut cross = + JoinOperator::build(expensive, peer, JoinCondition::None, JoinType::Cross, false); + cross.physical_option = Some(PhysicalOption::new( + PlanImpl::NestLoopJoin, + SortOption::None, + RowCountOption::Unknown, + )); + // Read the expensive child once, peer twice, then emit six rows. + assert_eq!(cross.estimated_work()?, Some(508 + 2 * 12 + 6 * 4)); + let (outer, _) = input("outer", 2, &mut arena); + let mut plan = + JoinOperator::build(cross, outer, JoinCondition::None, JoinType::Cross, false); + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::NestLoopJoin, + SortOption::None, + RowCountOption::Unknown, + )); + let original = plan.clone(); + assert!(!OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan, original); + Ok(()) + } + #[test] fn nested_loop_moves_materialization_left_and_restores_positions() -> Result<(), DatabaseError> { diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 468d1a63..9cfab936 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -242,6 +242,25 @@ impl PhysicalOption { .saturating_add(rows?.saturating_mul(CPU_ROW_WORK)), ) } + (PlanImpl::NestLoopJoin, Operator::Join(join), Childrens::Twins { left, right }) + if join.join_type == join::JoinType::Cross => + { + let left_work = left.physical_option.as_ref()?.calculate_work( + &left.operator, + &left.childrens, + left.estimated_rows(), + )?; + let right_work = right.physical_option.as_ref()?.calculate_work( + &right.operator, + &right.childrens, + right.estimated_rows(), + )?; + Some( + left_work + .saturating_add(left.estimated_rows()?.saturating_mul(right_work)) + .saturating_add(rows?.saturating_mul(CPU_ROW_WORK)), + ) + } _ => Some( self.startup_work .flatten()? From 99271bac73e2a3b4fb0485c8311a346c0448da7f Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 15/62] perf: reorder unestimated cross joins and make analyze sketches configurable - GreedyJoinReorder defers Cartesian products by connectivity when a region containing a cross join lacks row estimates; malformed join keys are now reported as InvalidPlan instead of silently skipping the region. - Add DataBaseBuilder::hll_precision (default 2^14 registers) and statistics_relative_error (KLL / Count-Min, default 0.001) and validate them when the database is built. - Reject duplicate visible table names in one FROM namespace. - Bind non-LATERAL derived tables without the sibling FROM sources and reject LATERAL explicitly. --- src/binder/analyze.rs | 2 + src/binder/mod.rs | 23 +- src/binder/parser.rs | 9 +- src/binder/select.rs | 12 + src/db.rs | 103 ++ src/errors.rs | 14 + src/execution/dml/analyze.rs | 93 +- src/optimizer/core/histogram.rs | 116 ++- src/optimizer/core/hll.rs | 60 +- src/optimizer/core/statistics_meta.rs | 10 +- .../rule/implementation/dql/filter.rs | 6 +- .../rule/normalization/join_reorder.rs | 957 +++++++++++++++--- src/planner/arena.rs | 2 +- src/planner/operator/analyze.rs | 3 + src/planner/operator/mod.rs | 4 + src/planner/operator/visitor.rs | 2 + src/serdes/num.rs | 22 + src/storage/table_codec.rs | 11 +- tests/slt/join.slt | 35 + tests/slt/subquery.slt | 23 + 20 files changed, 1265 insertions(+), 242 deletions(-) diff --git a/src/binder/analyze.rs b/src/binder/analyze.rs index 2021329d..9b51233a 100644 --- a/src/binder/analyze.rs +++ b/src/binder/analyze.rs @@ -47,6 +47,8 @@ impl> Binder<'_, '_, T, A> { table_name, index_metas, histogram_buckets: None, + hll_precision: None, + statistics_relative_error: None, }), Childrens::Only(Box::new(scan_op)), )) diff --git a/src/binder/mod.rs b/src/binder/mod.rs index b63af559..68096830 100644 --- a/src/binder/mod.rs +++ b/src/binder/mod.rs @@ -249,6 +249,9 @@ pub struct BinderContext<'a, T: Transaction> { // Tips: retain binding order so wildcard expansion and position derivation // follow FROM/JOIN order directly. pub(crate) bind_table: Vec>, + // Sources before this index were inherited from an enclosing query by `fork` + // and do not share the current FROM namespace. + source_scope_start: usize, ctes: Vec, pub(crate) cte_depth: usize, // alias @@ -318,6 +321,7 @@ impl<'a, T: Transaction> BinderContext<'a, T> { view_cache, transaction, bind_table: Default::default(), + source_scope_start: 0, ctes: Default::default(), cte_depth: 0, expr_aliases: Default::default(), @@ -347,6 +351,7 @@ impl<'a, T: Transaction> BinderContext<'a, T> { view_cache: self.view_cache, transaction: self.transaction, bind_table: self.bind_table.clone(), + source_scope_start: self.bind_table.len(), ctes: self.ctes.clone(), cte_depth: self.cte_depth, expr_aliases: self.expr_aliases.clone(), @@ -549,6 +554,19 @@ impl<'a, T: Transaction> BinderContext<'a, T> { Ok(source) } + /// Rejects a FROM source whose visible name is already bound in the current + /// FROM namespace. Two such sources would also expose identical column + /// identities, which later stages cannot tell apart. + pub(crate) fn ensure_unique_source_name(&self, name: &TableName) -> Result<(), DatabaseError> { + if self.bind_table[self.source_scope_start..] + .iter() + .any(|bound_source| bound_source.visible_name() == name) + { + return Err(DatabaseError::DuplicateTableName(name.to_string())); + } + Ok(()) + } + pub fn add_bound_source( &mut self, table_name: TableName, @@ -658,8 +676,10 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' false } - pub(crate) fn extend(&mut self, context: BinderContext<'a, T>) { + pub(crate) fn extend(&mut self, context: BinderContext<'a, T>) -> Result<(), DatabaseError> { for bound_source in context.bind_table { + self.context + .ensure_unique_source_name(bound_source.visible_name())?; self.context.add_bound_source( bound_source.table_name, bound_source.alias, @@ -673,6 +693,7 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' for (key, table_name) in context.table_aliases { self.context.table_aliases.insert(key, table_name); } + Ok(()) } } diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 71357ab9..ca72acae 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -2091,10 +2091,15 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' sql_optional_table_alias(alias.clone()), arena, ), + TableFactor::Derived { lateral: true, .. } => Err(DatabaseError::UnsupportedStmt( + "LATERAL derived tables are not supported".to_string(), + )), TableFactor::Derived { subquery, alias, .. } => { - let mut binder = Binder::new(self.context.fork(), self.args, Some(&self.context)); + // A non-LATERAL derived table cannot see the sources bound before + // it in the same FROM clause. + let mut binder = Binder::new(self.context.fork_empty(), self.args, None); let plan = binder.bind_query(subquery, arena)?; self.bind_derived_source( plan, @@ -2157,7 +2162,7 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' let right = binder.bind_single_table_ref_sql(relation, Some(join_type), arena)?; (right, binder.context) }; - self.extend(context); + self.extend(context)?; let constraint = match joint_condition { Some(constraint) => self.bind_join_constraint_sql(constraint, arena)?, diff --git a/src/binder/select.rs b/src/binder/select.rs index 43b024d0..7bba39d8 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -1204,6 +1204,8 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b arena: &mut PlanArena, ) -> Result { let table_alias = alias.as_ref().map(|alias| alias.name.clone()); + self.context + .ensure_unique_source_name(table_alias.as_ref().unwrap_or(&table_name))?; if let Some(plan_ref) = self.context.cte(&table_name).map(|cte| cte.plan_ref) { let mut plan = arena.plan(plan_ref).clone(); @@ -1275,6 +1277,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b arena: &mut PlanArena, ) -> Result { if let Some(alias) = alias { + self.context.ensure_unique_source_name(&alias.name)?; let source_name = arena.temp_table(); plan = self.bind_alias( @@ -1304,6 +1307,9 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b None } }; + if let Some(source_name) = &passthrough_source { + self.context.ensure_unique_source_name(source_name)?; + } let needs_virtual_source = passthrough_source.is_none(); let source_name = passthrough_source.unwrap_or_else(|| arena.temp_table()); @@ -1337,6 +1343,12 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b let mut table_alias = None; let table_name: TableName = function.summary().name.clone(); + self.context.ensure_unique_source_name( + alias + .as_ref() + .map(|alias| &alias.name) + .unwrap_or(&table_name), + )?; let mut plan = FunctionScanOperator::build(function); if let Some(alias) = alias { diff --git a/src/db.rs b/src/db.rs index df2e6b92..7af04f49 100644 --- a/src/db.rs +++ b/src/db.rs @@ -35,6 +35,7 @@ use crate::function::lower::Lower; use crate::function::numbers::Numbers; use crate::function::octet_length::OctetLength; use crate::function::upper::Upper; +use crate::optimizer::core::hll; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::heuristic::batch::HepBatchStrategy; use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; @@ -59,6 +60,7 @@ use crate::storage::{ use crate::types::tuple::{Schema, SchemaView, Tuple}; use crate::types::value::DataValue; use crate::types::LogicalType; +use ordered_float::OrderedFloat; #[cfg(any(feature = "parser", feature = "orm"))] pub use prepared::PreparedPlan; use std::collections::{HashMap, HashSet}; @@ -112,6 +114,8 @@ pub struct DataBaseBuilder { ))] path: PathBuf, histogram_buckets: Option, + hll_precision: Option, + statistics_relative_error: Option, parser_recursion_limit: Option, transaction_isolation: Option, #[cfg(all(not(target_arch = "wasm32"), feature = "rocksdb"))] @@ -144,6 +148,8 @@ impl DataBaseBuilder { ))] path, histogram_buckets: None, + hll_precision: None, + statistics_relative_error: None, parser_recursion_limit: None, transaction_isolation: None, #[cfg(all(not(target_arch = "wasm32"), feature = "rocksdb"))] @@ -159,6 +165,26 @@ impl DataBaseBuilder { self } + /// Sets the HyperLogLog precision `p` used by `ANALYZE` to estimate column NDV. + /// + /// Each sketch has `2^p` registers (one byte each) with a standard error of + /// about `1.04 / sqrt(2^p)`. Valid range is 4..=26; unset uses 14 + /// (16 KiB, ~0.81% error). + pub fn hll_precision(mut self, precision: u8) -> Self { + self.hll_precision = Some(precision); + self + } + + /// Sets the relative error `ε` of the quantile (KLL) and frequency + /// (Count-Min) sketches built by `ANALYZE`. + /// + /// Sketch size grows roughly as `1 / ε` (Count-Min width is + /// `next_power_of_two(2 / ε)`). Must be in `(0, 1)`; unset uses 0.001. + pub fn statistics_relative_error(mut self, relative_error: f64) -> Self { + self.statistics_relative_error = Some(relative_error); + self + } + /// Sets the SQL parser recursion limit. Unset uses the parser's default limit. /// Higher limits may require a larger thread stack for deeply nested SQL. #[cfg(feature = "parser")] @@ -250,6 +276,8 @@ impl DataBaseBuilder { Self::_build::( storage, self.histogram_buckets, + self.hll_precision, + self.statistics_relative_error, self.parser_recursion_limit, self.transaction_isolation, ) @@ -263,6 +291,8 @@ impl DataBaseBuilder { Self::_build::( storage, self.histogram_buckets, + self.hll_precision, + self.statistics_relative_error, self.parser_recursion_limit, self.transaction_isolation, ) @@ -276,6 +306,8 @@ impl DataBaseBuilder { Self::_build::( storage, self.histogram_buckets, + self.hll_precision, + self.statistics_relative_error, self.parser_recursion_limit, self.transaction_isolation, ) @@ -290,6 +322,8 @@ impl DataBaseBuilder { Self::_build::( storage, self.histogram_buckets, + self.hll_precision, + self.statistics_relative_error, self.parser_recursion_limit, self.transaction_isolation, ) @@ -303,6 +337,8 @@ impl DataBaseBuilder { Self::_build::( storage, self.histogram_buckets, + self.hll_precision, + self.statistics_relative_error, self.parser_recursion_limit, self.transaction_isolation, ) @@ -317,6 +353,8 @@ impl DataBaseBuilder { Self::_build::( storage, self.histogram_buckets, + self.hll_precision, + self.statistics_relative_error, self.parser_recursion_limit, self.transaction_isolation, ) @@ -325,6 +363,8 @@ impl DataBaseBuilder { fn _build( storage: T, histogram_buckets: Option, + hll_precision: Option, + statistics_relative_error: Option, parser_recursion_limit: Option, transaction_isolation: Option, ) -> Result, DatabaseError> { @@ -333,6 +373,23 @@ impl DataBaseBuilder { "histogram buckets must be >= 1".to_string(), )); } + // Reject out-of-range values when the database opens, not at the first ANALYZE. + if let Some(precision) = hll_precision { + if !(hll::MIN_PRECISION..=hll::MAX_PRECISION).contains(&precision) { + return Err(DatabaseError::InvalidValue(format!( + "hll precision must be between {} and {}, got {precision}", + hll::MIN_PRECISION, + hll::MAX_PRECISION + ))); + } + } + if let Some(relative_error) = statistics_relative_error { + if !(relative_error > 0.0 && relative_error < 1.0) { + return Err(DatabaseError::InvalidValue(format!( + "statistics relative error must be in (0, 1), got {relative_error}" + ))); + } + } if matches!(parser_recursion_limit, Some(0)) { return Err(DatabaseError::InvalidValue( "parser recursion limit must be >= 1".to_string(), @@ -355,6 +412,8 @@ impl DataBaseBuilder { table_arena, optimizer_pipeline: optimizer_pipeline(), histogram_buckets, + hll_precision, + statistics_relative_error: statistics_relative_error.map(OrderedFloat), parser_recursion_limit, _p: Default::default(), }; @@ -549,6 +608,8 @@ pub(crate) struct State { table_arena: TableArenaCell, optimizer_pipeline: HepOptimizerPipeline, histogram_buckets: Option, + hll_precision: Option, + statistics_relative_error: Option>, #[cfg_attr(not(feature = "parser"), allow(dead_code))] parser_recursion_limit: Option, _p: PhantomData, @@ -666,6 +727,12 @@ impl State { if op.histogram_buckets.is_none() { op.histogram_buckets = self.histogram_buckets; } + if op.hll_precision.is_none() { + op.hll_precision = self.hll_precision; + } + if op.statistics_relative_error.is_none() { + op.statistics_relative_error = self.statistics_relative_error; + } } plan.populate_output_schema_recursive(&mut arena); @@ -738,6 +805,8 @@ impl State { table_arena, optimizer_pipeline, histogram_buckets, + hll_precision, + statistics_relative_error, .. } = self; let mut plan_arena = PlanArena::new(table_arena); @@ -762,6 +831,12 @@ impl State { if op.histogram_buckets.is_none() { op.histogram_buckets = *histogram_buckets; } + if op.hll_precision.is_none() { + op.hll_precision = *hll_precision; + } + if op.statistics_relative_error.is_none() { + op.statistics_relative_error = *statistics_relative_error; + } } plan.populate_output_schema_recursive(&mut plan_arena); @@ -2398,6 +2473,34 @@ pub(crate) mod test { )); } + #[test] + fn test_invalid_statistics_relative_error() { + for relative_error in [0.0, 1.0, -0.5, f64::NAN] { + let result = DataBaseBuilder::path(".") + .statistics_relative_error(relative_error) + .build_in_memory(); + assert!(matches!( + result, + Err(DatabaseError::InvalidValue(message)) + if message == format!("statistics relative error must be in (0, 1), got {relative_error}") + )); + } + } + + #[test] + fn test_invalid_hll_precision() { + for precision in [3, 27] { + let result = DataBaseBuilder::path(".") + .hll_precision(precision) + .build_in_memory(); + assert!(matches!( + result, + Err(DatabaseError::InvalidValue(message)) + if message == format!("hll precision must be between 4 and 26, got {precision}") + )); + } + } + #[test] fn parser_recursion_limit_applies_to_sql_frontends() -> Result<(), DatabaseError> { let expression = format!("{}7{}", "(".repeat(8), ")".repeat(8)); diff --git a/src/errors.rs b/src/errors.rs index e1f4d04b..e74e7409 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -80,6 +80,7 @@ pub enum DatabaseError { DuplicateColumn(String), DuplicateSourceHash(String), DuplicateIndex(String), + DuplicateTableName(String), DuplicatePrimaryKey, DuplicateUniqueValue, FunctionNotFound { @@ -96,6 +97,7 @@ pub enum DatabaseError { span: Option, }, InvalidIndex, + InvalidPlan(String), InvalidTable { name: String, span: Option, @@ -178,6 +180,9 @@ impl fmt::Display for DatabaseError { write!(f, "table or view: `{value}` hash already exists") } Self::DuplicateIndex(value) => write!(f, "index: `{value}` already exists"), + Self::DuplicateTableName(value) => { + write!(f, "table name: `{value}` specified more than once") + } Self::DuplicatePrimaryKey => f.write_str("duplicate primary key"), Self::DuplicateUniqueValue => { f.write_str("the column has been declared unique and the value already exists") @@ -200,6 +205,7 @@ impl fmt::Display for DatabaseError { write!(f, "invalid column: `{name}`{}", format_sql_error_loc(span)) } Self::InvalidIndex => f.write_str("invalid index"), + Self::InvalidPlan(value) => write!(f, "invalid plan: {value}"), Self::InvalidTable { name, span } => { write!(f, "invalid table: `{name}`{}", format_sql_error_loc(span)) } @@ -671,6 +677,14 @@ mod tests { DatabaseError::DuplicateIndex("idx".into()), "index: `idx` already exists", ), + ( + DatabaseError::DuplicateTableName("t".into()), + "table name: `t` specified more than once", + ), + ( + DatabaseError::InvalidPlan("join key".into()), + "invalid plan: join key", + ), ( DatabaseError::Incomparable(LogicalType::Integer, LogicalType::Boolean), "can not compare two types: Integer and Boolean", diff --git a/src/execution/dml/analyze.rs b/src/execution/dml/analyze.rs index a635ed47..3e49ae68 100644 --- a/src/execution/dml/analyze.rs +++ b/src/execution/dml/analyze.rs @@ -21,7 +21,8 @@ use crate::execution::{ use crate::expression::ScalarExpression; use crate::iter_ext::Itertools; use crate::optimizer::core::histogram::{ - HistogramBuilder, HistogramMetaBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR, + HistogramBuilder, HistogramMetaBuilder, DEFAULT_HLL_PRECISION, + DEFAULT_STATISTICS_RELATIVE_ERROR, }; use crate::optimizer::core::statistics_meta::IndexStatistics; use crate::planner::operator::analyze::AnalyzeOperator; @@ -31,6 +32,7 @@ use crate::storage::{table_codec::TableCodec, Transaction}; use crate::types::index::IndexId; use crate::types::value::{DataValue, Utf8Type}; use crate::types::{CharLengthUnits, ColumnId}; +use ordered_float::OrderedFloat; use std::fmt::{self, Formatter}; const DEFAULT_NUM_OF_BUCKETS: usize = 100; @@ -39,6 +41,8 @@ pub struct Analyze<'a> { table_name: &'a TableName, input: Option, histogram_buckets: Option, + hll_precision: Option, + statistics_relative_error: Option>, } impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for Analyze<'a> { @@ -49,6 +53,8 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for Analyze<'a> { AnalyzeOperator { table_name, histogram_buckets, + hll_precision, + statistics_relative_error, .. }, input_plan, @@ -69,6 +75,8 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for Analyze<'a> { table_name, input, histogram_buckets: *histogram_buckets, + hll_precision: *hll_precision, + statistics_relative_error: *statistics_relative_error, })) } } @@ -84,6 +92,10 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Analyze<'a> { return Ok(()); }; + let hll_precision = self.hll_precision.unwrap_or(DEFAULT_HLL_PRECISION); + let relative_error = self + .statistics_relative_error + .map_or(DEFAULT_STATISTICS_RELATIVE_ERROR, |error| error.0); let (mut columns, mut builders) = { let table = arena .transaction() @@ -100,7 +112,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Analyze<'a> { Ok(( column_id, position, - HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?, + HistogramMetaBuilder::new(hll_precision)?, )) }) .collect::, DatabaseError>>()?; @@ -109,7 +121,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Analyze<'a> { .map(|index| { let index = plan_arena.index(*index); let index_id = index.id; - let builder = HistogramBuilder::new(index, ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let builder = HistogramBuilder::new(index, relative_error, hll_precision)?; let exprs = index.column_exprs(table).collect::, _>>()?; Ok(State { index_id, @@ -234,7 +246,9 @@ mod test { use crate::errors::DatabaseError; use crate::execution::dml::analyze::DEFAULT_NUM_OF_BUCKETS; use crate::expression::range_detacher::Range; + use crate::iter_ext::Itertools; use crate::optimizer::core::cm_sketch::COUNT_MIN_SKETCH_STORAGE_PAGE_LEN; + use crate::optimizer::core::histogram::{HistogramMetaBuilder, DEFAULT_HLL_PRECISION}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::storage::{table_codec::TableCodec, InnerIter, Storage, Transaction}; use crate::types::value::DataValue; @@ -247,10 +261,83 @@ mod test { test_meta_loader_uses_cache()?; test_meta_loader_negative_cache()?; test_clean_expired_index()?; + test_hll_precision_is_configurable()?; + test_statistics_relative_error_is_configurable()?; Ok(()) } + fn test_hll_precision_is_configurable() -> Result<(), DatabaseError> { + let rows = 1000; + let ndv_with = |precision: u8| -> Result { + let mut builder = HistogramMetaBuilder::new(precision)?; + for i in 0..rows { + builder.append(&DataValue::Int32(i)); + } + Ok(builder.build().distinct_values_len()) + }; + let (coarse, default) = (ndv_with(4)?, ndv_with(DEFAULT_HLL_PRECISION)?); + // Precision 4 has 16 registers, so its estimate must differ from the default sketch. + assert_ne!(coarse, default); + + let values = (0..rows).map(|i| format!("({i})")).join(", "); + for (builder, expected) in [ + (DataBaseBuilder::path("."), default), + (DataBaseBuilder::path(".").hll_precision(4), coarse), + ] { + let mut kite_sql = builder.build_in_memory()?; + kite_sql.ddl("create table t1 (a int primary key)")?; + kite_sql + .run(format!("insert into t1 values {values}"))? + .done()?; + kite_sql.analyze("t1")?; + + let table_name = "t1".to_string().into(); + let a_id = *kite_sql + .state + .table_cache() + .get(&table_name) + .unwrap() + .get_column_id_by_name("a") + .unwrap(); + let loader = StatisticMetaLoader::new(kite_sql.state.meta_cache()); + let column = loader.load_column(&table_name, a_id).unwrap(); + assert_eq!(column.distinct_values_len(), expected); + let index = loader.load(&table_name, 0)?.unwrap(); + assert_eq!(index.histogram().distinct_values_len(), expected); + } + Ok(()) + } + + fn test_statistics_relative_error_is_configurable() -> Result<(), DatabaseError> { + // Count-Min width is next_power_of_two(2 / error): 0.001 -> 2048, 0.01 -> 256, + // and error_bound(n) = ceil(2n / width). + for (builder, width) in [ + (DataBaseBuilder::path("."), 2048), + ( + DataBaseBuilder::path(".").statistics_relative_error(0.01), + 256, + ), + ] { + let mut kite_sql = builder.build_in_memory()?; + kite_sql.ddl("create table t1 (a int primary key)")?; + // ANALYZE needs at least DEFAULT_NUM_OF_BUCKETS rows. + let values = (0..DEFAULT_NUM_OF_BUCKETS) + .map(|i| format!("({i})")) + .join(", "); + kite_sql + .run(format!("insert into t1 values {values}"))? + .done()?; + kite_sql.analyze("t1")?; + + let table_name = "t1".to_string().into(); + let loader = StatisticMetaLoader::new(kite_sql.state.meta_cache()); + let index = loader.load(&table_name, 0)?.unwrap(); + assert_eq!(index.sketch().error_bound(width), 2); + } + Ok(()) + } + fn create_table( database: &mut Database, sql: &str, diff --git a/src/optimizer/core/histogram.rs b/src/optimizer/core/histogram.rs index 708e58af..d8512daa 100644 --- a/src/optimizer/core/histogram.rs +++ b/src/optimizer/core/histogram.rs @@ -32,7 +32,12 @@ use std::mem; use std::sync::OnceLock; const ANALYZE_STATISTICS_CONFIDENCE: f64 = 0.95; -pub(crate) const ANALYZE_STATISTICS_RELATIVE_ERROR: f64 = 0.001; +// Relative error of the KLL quantile and Count-Min sketches when +// `DataBaseBuilder::statistics_relative_error` is unset. +pub(crate) const DEFAULT_STATISTICS_RELATIVE_ERROR: f64 = 0.001; +// HyperLogLog precision used for NDV when `DataBaseBuilder::hll_precision` is +// unset: 2^14 registers (16 KiB per sketch, ~0.81% standard error). +pub(crate) const DEFAULT_HLL_PRECISION: u8 = 14; pub struct HistogramMetaBuilder { null_count: usize, @@ -103,11 +108,11 @@ pub struct Bucket { } impl HistogramMetaBuilder { - pub fn new(relative_error: f64) -> Result { + pub fn new(hll_precision: u8) -> Result { Ok(Self { null_count: 0, values_len: 0, - hll: HyperLogLog::with_relative_error(relative_error)?, + hll: HyperLogLog::with_precision(hll_precision)?, }) } @@ -138,10 +143,17 @@ impl HistogramMetaBuilder { } impl HistogramBuilder { - pub fn new(index_meta: &IndexMeta, relative_error: f64) -> Result { + pub fn new( + index_meta: &IndexMeta, + relative_error: f64, + hll_precision: u8, + ) -> Result { Ok(Self { index_id: index_meta.id, - meta: HistogramMetaBuilder::new(relative_error)?, + meta: HistogramMetaBuilder::new(hll_precision)?, + // TODO: derive the KLL rank error from the histogram bucket count + // (e.g. 1 / (2 * buckets)) instead of sharing `relative_error` with the + // Count-Min sketch; 100 buckets only need ~0.5% rank error, not 0.1%. quantile: KllSketchBuilder::with_relative_error(relative_error)?, sketch: CountMinSketch::with_relative_error( ANALYZE_STATISTICS_CONFIDENCE, @@ -162,6 +174,11 @@ impl HistogramBuilder { Ok(()) } + // TODO: shrink the Count-Min sketch to the observed row count before returning it. + // Its width is fixed by `relative_error` (2048 x 5 counters, ~80 KiB at 0.001) + // and persisted per index even for tiny tables; since the width is a power of + // two, rows can be folded in half (summing both halves) down to about + // `next_power_of_two(values_len)` without losing accuracy on large tables. pub fn build( self, number_of_buckets: usize, @@ -727,7 +744,8 @@ mod tests { use crate::expression::range_detacher::Range; use crate::optimizer::core::cm_sketch::CountMinSketch; use crate::optimizer::core::histogram::{ - Bucket, HistogramBuilder, HistogramMetaBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR, + Bucket, HistogramBuilder, HistogramMetaBuilder, DEFAULT_HLL_PRECISION, + DEFAULT_STATISTICS_RELATIVE_ERROR, }; use crate::optimizer::core::top_n::ColumnTopN; use crate::types::index::{IndexMeta, IndexType}; @@ -737,12 +755,12 @@ mod tests { #[test] fn column_meta_counts_values_without_histogram_buckets() -> Result<(), DatabaseError> { - let empty = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?.build(); + let empty = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?.build(); assert_eq!(empty.values_len(), 0); assert_eq!(empty.null_count(), 0); assert_eq!(empty.distinct_values_len(), 0); assert_eq!(empty.buckets_len(), 0); - let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; for value in [ DataValue::Null, DataValue::Int32(1), @@ -756,7 +774,7 @@ mod tests { assert_eq!(meta.null_count(), 1); assert_eq!(meta.distinct_values_len(), 2); assert_eq!(meta.buckets_len(), 0); - let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; builder.append(&DataValue::Null); let meta = builder.build(); assert_eq!(meta.values_len(), 0); @@ -779,7 +797,11 @@ mod tests { #[test] fn test_sort_tuples_on_histogram() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; builder.append(DataValue::Int32(0))?; builder.append(DataValue::Int32(1))?; @@ -845,7 +867,11 @@ mod tests { #[test] fn test_rev_sort_tuples_on_histogram() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; builder.append(DataValue::Int32(14))?; builder.append(DataValue::Int32(13))?; @@ -909,7 +935,11 @@ mod tests { #[test] fn test_non_average_on_histogram() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; builder.append(DataValue::Int32(14))?; builder.append(DataValue::Int32(13))?; @@ -968,7 +998,11 @@ mod tests { #[test] fn parameterized_tuple_ranges_accumulate_prefix_selectivity() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; for value in 0..100 { builder.append(DataValue::Tuple(vec![DataValue::Int32(value); 4]))?; } @@ -1233,7 +1267,11 @@ mod tests { #[test] fn parameterized_range_count_uses_shape_and_ndv() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; for value in 0..1_000 { builder.append(DataValue::Int32(value % 100))?; } @@ -1276,7 +1314,11 @@ mod tests { #[test] fn test_collect_count() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; builder.append(DataValue::Int32(14))?; builder.append(DataValue::Int32(13))?; @@ -1430,7 +1472,11 @@ mod tests { #[test] fn test_builder_uses_hll_for_distinct_values() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; for _ in 0..50 { builder.append(DataValue::Int32(1))?; @@ -1449,7 +1495,11 @@ mod tests { #[test] fn test_eq_count_uses_cm_sketch_when_error_is_small() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; for _ in 0..70 { builder.append(DataValue::Int32(1))?; @@ -1471,7 +1521,11 @@ mod tests { #[test] fn test_eq_count_uses_top_n_when_value_is_cached() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; for _ in 0..70 { builder.append(DataValue::Int32(1))?; @@ -1493,7 +1547,11 @@ mod tests { #[test] fn test_eq_count_falls_back_to_average_when_cm_error_is_large() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; for value in 0..10_000 { builder.append(DataValue::Int32(value))?; @@ -1517,7 +1575,11 @@ mod tests { #[test] fn test_eq_count_uses_cm_sketch_for_clear_heavy_hitter() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; for _ in 0..1_000 { builder.append(DataValue::Int32(7))?; @@ -1544,7 +1606,11 @@ mod tests { #[test] fn test_eq_count_falls_back_when_top_n_error_is_large() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; for value in 0..10_000 { builder.append(DataValue::Int32(value))?; @@ -1574,7 +1640,11 @@ mod tests { #[test] fn test_collect_count_ignores_tuple_prefix_endpoint_count() -> Result<(), DatabaseError> { - let mut builder = HistogramBuilder::new(&index_meta(), ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta(), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; for value in 0..15 { builder.append(DataValue::Tuple(vec![ @@ -1610,7 +1680,7 @@ mod tests { }; let mut sketch = CountMinSketch::with_relative_error( super::ANALYZE_STATISTICS_CONFIDENCE, - ANALYZE_STATISTICS_RELATIVE_ERROR, + DEFAULT_STATISTICS_RELATIVE_ERROR, )?; let real_key = DataValue::Tuple(vec![DataValue::Int32(8), DataValue::Int32(8)]); diff --git a/src/optimizer/core/hll.rs b/src/optimizer/core/hll.rs index 8dae20ae..f9807604 100644 --- a/src/optimizer/core/hll.rs +++ b/src/optimizer/core/hll.rs @@ -18,8 +18,8 @@ use std::cmp; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; -const MIN_PRECISION: u8 = 4; -const MAX_PRECISION: u8 = 26; +pub(crate) const MIN_PRECISION: u8 = 4; +pub(crate) const MAX_PRECISION: u8 = 26; #[derive(Debug, Clone)] pub struct HyperLogLog { @@ -29,17 +29,16 @@ pub struct HyperLogLog { } impl HyperLogLog { - pub fn with_relative_error(relative_error: f64) -> Result { - if relative_error <= 0.0 || relative_error.is_nan() { + /// Creates a sketch with `2^precision` registers, whose standard error is + /// about `1.04 / sqrt(2^precision)`. + pub fn with_precision(precision: u8) -> Result { + if !(MIN_PRECISION..=MAX_PRECISION).contains(&precision) { return Err(DatabaseError::InvalidValue(format!( - "hyperloglog relative error must be greater than zero, got {relative_error}" + "hyperloglog precision must be between {MIN_PRECISION} and {MAX_PRECISION}, got {precision}" ))); } - - let register_count = Self::optimal_register_count(relative_error)?; - let precision = register_count.trailing_zeros() as u8; Ok(Self { - registers: vec![0; register_count], + registers: vec![0; 1 << precision], precision, phantom_k: PhantomData, }) @@ -80,32 +79,6 @@ impl HyperLogLog { estimate.round() as usize } - fn optimal_register_count(relative_error: f64) -> Result { - let count = (1.04 / relative_error).powi(2).ceil(); - if !count.is_finite() || count > usize::MAX as f64 { - return Err(DatabaseError::InvalidValue(format!( - "hyperloglog relative error is too small: {relative_error}" - ))); - } - - let min_count = 1usize << MIN_PRECISION; - let count = cmp::max(min_count, count as usize) - .checked_next_power_of_two() - .ok_or_else(|| { - DatabaseError::InvalidValue(format!( - "hyperloglog register count overflow for relative error: {relative_error}" - )) - })?; - let precision = count.trailing_zeros() as u8; - if precision > MAX_PRECISION { - return Err(DatabaseError::InvalidValue(format!( - "hyperloglog relative error is too small: {relative_error}" - ))); - } - - Ok(count) - } - fn hash(value: &Q) -> u64 { let (key0, key1) = HLL_HASH_KEYS; let mut hasher = StableHasher::new_with_keys(key0, key1); @@ -130,7 +103,7 @@ mod tests { #[test] fn estimate_ignores_duplicates() { - let mut hll = HyperLogLog::::with_relative_error(0.01).unwrap(); + let mut hll = HyperLogLog::::with_precision(14).unwrap(); for _ in 0..1000 { hll.add(&7); } @@ -138,9 +111,22 @@ mod tests { assert_eq!(hll.estimate(), 1); } + #[test] + fn precision_sets_register_count() { + assert_eq!( + HyperLogLog::::with_precision(14) + .unwrap() + .registers + .len(), + 1 << 14 + ); + assert!(HyperLogLog::::with_precision(3).is_err()); + assert!(HyperLogLog::::with_precision(27).is_err()); + } + #[test] fn estimate_distinct_values_within_error_budget() { - let mut hll = HyperLogLog::::with_relative_error(0.02).unwrap(); + let mut hll = HyperLogLog::::with_precision(12).unwrap(); for value in 0..10_000 { hll.add(&value); } diff --git a/src/optimizer/core/statistics_meta.rs b/src/optimizer/core/statistics_meta.rs index 7dc91716..672fe6ed 100644 --- a/src/optimizer/core/statistics_meta.rs +++ b/src/optimizer/core/statistics_meta.rs @@ -169,7 +169,9 @@ impl IndexStatistics { mod tests { use crate::errors::DatabaseError; use crate::expression::range_detacher::Range; - use crate::optimizer::core::histogram::{HistogramBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR}; + use crate::optimizer::core::histogram::{ + HistogramBuilder, DEFAULT_HLL_PRECISION, DEFAULT_STATISTICS_RELATIVE_ERROR, + }; use crate::optimizer::core::statistics_meta::{IndexStatistics, StatisticMetaLoader}; use crate::storage::StatisticsMetaCache; use crate::types::index::{IndexMeta, IndexType}; @@ -188,7 +190,11 @@ mod tests { ty: IndexType::PrimaryKey { is_multiple: false }, }; - let mut builder = HistogramBuilder::new(&index, ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index, + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; builder.append(DataValue::Int32(14))?; builder.append(DataValue::Int32(13))?; diff --git a/src/optimizer/rule/implementation/dql/filter.rs b/src/optimizer/rule/implementation/dql/filter.rs index f09fc6a1..4d7e2afa 100644 --- a/src/optimizer/rule/implementation/dql/filter.rs +++ b/src/optimizer/rule/implementation/dql/filter.rs @@ -211,9 +211,7 @@ mod tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc}; use crate::expression::AliasType; - use crate::optimizer::core::histogram::{ - HistogramMetaBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR, - }; + use crate::optimizer::core::histogram::{HistogramMetaBuilder, DEFAULT_HLL_PRECISION}; use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; use crate::optimizer::rule::implementation::ImplementationRuleImpl; use crate::planner::operator::filter::FilterOperator; @@ -258,7 +256,7 @@ mod tests { } fn statistics(non_null: usize, nulls: usize, ndv: usize) -> StatisticsMetaCache { - let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR).unwrap(); + let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION).unwrap(); for i in 0..non_null { builder.append(&DataValue::Int32((i % ndv) as i32)); } diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index ad6a4af9..1afb6450 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -25,6 +25,8 @@ use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOptio use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; use crate::types::index::{IndexMeta, IndexMetaRef, IndexType}; use crate::types::tuple::Schema; +use std::borrow::Borrow; +use std::collections::{btree_map::Entry, BTreeMap, HashMap}; use std::mem::swap; pub(crate) struct GreedyJoinReorder; @@ -150,13 +152,17 @@ struct JoinCandidate { right: usize, } -fn column_statistics( +fn column_ndv>( left: &LogicalPlan, right: &LogicalPlan, - keys: &[(ExprRef, ExprRef)], + keys: impl IntoIterator, loader: Option<&StatisticMetaLoader<'_>>, arena: &PlanArena, ) -> Result, DatabaseError> { + // scan_column cannot trace columns through a join. + if matches!(&left.operator, Operator::Join(_)) || matches!(&right.operator, Operator::Join(_)) { + return Ok(None); + } let Some(loader) = loader else { return Ok(None); }; @@ -166,7 +172,8 @@ fn column_statistics( }; let mut result = None; let mut divisor = 0; - for &(left_key, right_key) in keys { + for key in keys { + let &(left_key, right_key) = key.borrow(); let ( ScalarExpression::ColumnRef { column: l, .. }, ScalarExpression::ColumnRef { column: r, .. }, @@ -225,15 +232,15 @@ fn index_column(column: ColumnRef, index: &IndexMeta, arena: &PlanArena) -> bool && column.id().is_some_and(|id| index.column_ids.contains(&id)) } -fn covers_index( +fn covers_index>( index: &IndexMeta, other: &JoinInputStatistics, - keys: &[(ExprRef, ExprRef)], + keys: impl Fn() -> I, require_other_index: bool, arena: &PlanArena, ) -> bool { index.column_ids.iter().all(|id| { - keys.iter().any(|&(left, right)| { + keys().any(|(left, right)| { let ( ScalarExpression::ColumnRef { column: left, .. }, ScalarExpression::ColumnRef { column: right, .. }, @@ -291,11 +298,11 @@ fn estimate_join_rows(left: JoinInputEstimate, right: JoinInputEstimate) -> usiz rows } -fn join_rows( +fn join_rows>( left: &JoinInputStatistics, right: &JoinInputStatistics, - keys: &[(ExprRef, ExprRef)], - columns: Option<(usize, usize)>, + keys: impl Fn() -> I, + ndvs: Option<(usize, usize)>, arena: &PlanArena, ) -> usize { let mut left_estimate = JoinInputEstimate { @@ -311,15 +318,15 @@ fn join_rows( if let (Some((left_index, Some(left_ndv))), Some((right_index, Some(right_ndv)))) = (left.index, right.index) { - if covers_index(left_index, right, keys, true, arena) - && covers_index(right_index, left, keys, true, arena) + if covers_index(left_index, right, &keys, true, arena) + && covers_index(right_index, left, &keys, true, arena) { left_estimate.ndv = Some(left_ndv); right_estimate.ndv = Some(right_ndv); } } if left_estimate.ndv.is_none() || right_estimate.ndv.is_none() { - if let Some((left_ndv, right_ndv)) = columns { + if let Some((left_ndv, right_ndv)) = ndvs { left_estimate.ndv = Some(left_ndv); right_estimate.ndv = Some(right_ndv); } @@ -330,7 +337,7 @@ fn join_rows( ] { if let Some((index, _)) = own.index { if matches!(index.ty, IndexType::PrimaryKey { .. } | IndexType::Unique) - && covers_index(index, other, keys, false, arena) + && covers_index(index, other, &keys, false, arena) { estimate.unique = true; } @@ -356,10 +363,33 @@ fn can_expand(plan: &LogicalPlan) -> bool { (JoinType::Inner, JoinCondition::On { on, filter: None }) if !on.is_empty()) } +// Counts a region's inputs and reports whether it already contains a Cartesian product. +fn region_shape(plan: &LogicalPlan) -> (usize, bool) { + if !can_expand(plan) { + return (1, false); + } + let (left, right) = plan.childrens.twins(); + let (left_inputs, left_cross) = region_shape(left); + let (right_inputs, right_cross) = region_shape(right); + let cross = matches!( + &plan.operator, + Operator::Join(JoinOperator { + join_type: JoinType::Cross, + .. + }) + ); + ( + left_inputs + right_inputs, + cross || left_cross || right_cross, + ) +} + +// Collects input schemas and join keys +// returns `None` when any input lacks a row estimate. fn inspect<'plan>( plan: &'plan mut LogicalPlan, schemas: &mut Vec<&'plan Schema>, - keys: &mut Vec<(ExprRef, ExprRef)>, + keys: &mut JoinKeys, arena: &mut PlanArena, loader: Option<&StatisticMetaLoader<'_>>, ) -> Result, DatabaseError> { @@ -369,22 +399,46 @@ fn inspect<'plan>( else { unreachable!() }; - if let JoinCondition::On { on, .. } = &join.on { - keys.extend_from_slice(on); - } let left_stats = index_statistics(left); let right_stats = index_statistics(right); let on = match &join.on { JoinCondition::On { on, .. } => on.as_slice(), JoinCondition::None => &[], }; - let columns = column_statistics(left, right, on, loader, arena)?; + let ndvs = column_ndv(left, right, on.iter().copied(), loader, arena)?; let start = schemas.len(); - let Some(left_rows) = inspect(left, schemas, keys, arena, loader)? else { - return Ok(None); - }; + let left_rows = inspect(left, schemas, keys, arena, loader)?; let middle = schemas.len(); - let Some(right_rows) = inspect(right, schemas, keys, arena, loader)? else { + let right_rows = inspect(right, schemas, keys, arena, loader)?; + + for &(left, right) in on { + let mut endpoints = [None, None]; + for (endpoint, expr) in endpoints.iter_mut().zip([left, right]) { + for (index, schema) in schemas.iter().enumerate() { + if key_in_schema(expr, schema, arena)? { + *endpoint = Some(index); + break; + } + } + if endpoint.is_none() { + return Err(DatabaseError::InvalidPlan(format!( + "join key `{}` is not produced by any join input", + expr.output_name(arena) + ))); + } + } + if endpoints[0] == endpoints[1] { + return Err(DatabaseError::InvalidPlan(format!( + "join key `{} = {}` compares columns of the same join input", + left.output_name(arena), + right.output_name(arena) + ))); + } + // SAFETY: The loop above returns early if either endpoint is None. + let [l, r] = endpoints.map(Option::unwrap); + keys.insert(1u64 << l, 1u64 << r, (left, right)); + } + let (Some(left_rows), Some(right_rows)) = (left_rows, right_rows) else { return Ok(None); }; let rows = join_rows( @@ -406,8 +460,8 @@ fn inspect<'plan>( }, index: right_stats.map(|(index, ndv)| (arena.index(index), ndv)), }, - on, - columns, + || on.iter().copied(), + ndvs, arena, ); if let Some(option) = &mut plan.physical_option { @@ -421,23 +475,22 @@ fn inspect<'plan>( } Ok(Some(rows)) } else { - let Some(rows) = plan.estimated_rows() else { - return Ok(None); - }; + let rows = plan.estimated_rows(); schemas.push(plan.output_schema(arena)); - Ok(Some(rows)) + Ok(rows) } } -fn flatten(plan: LogicalPlan, inputs: &mut Vec<(LogicalPlan, usize)>) { +fn flatten(plan: LogicalPlan, inputs: &mut Vec<(LogicalPlan, usize, u64)>) { if can_expand(&plan) { let (left, right) = plan.childrens.pop_twins(); flatten(left, inputs); flatten(right, inputs); } else { - // SAFETY: inspect has already verified every input's row estimate before detaching the region. - let rows = plan.estimated_rows().unwrap(); - inputs.push((plan, rows)); + // Unused when the region is not fully estimated. + let rows = plan.estimated_rows().unwrap_or(0); + let mask = 1u64 << inputs.len(); + inputs.push((plan, rows, mask)); } } @@ -449,19 +502,84 @@ fn key_in_schema(expr: ExprRef, schema: &Schema, arena: &PlanArena) -> Result Result, DatabaseError> { - if key_in_schema(left, left_schema, arena)? && key_in_schema(right, right_schema, arena)? { - Ok(Some((left, right))) - } else if key_in_schema(right, left_schema, arena)? && key_in_schema(left, right_schema, arena)? - { - Ok(Some((right, left))) - } else { - Ok(None) +#[derive(Default)] +struct JoinKeys { + links: BTreeMap<(u64, u64), usize>, + // Link IDs are indices here; consumed lists leave empty slots. + on_keys: Vec>, +} + +impl JoinKeys { + fn insert(&mut self, left: u64, right: u64, mut exprs: (ExprRef, ExprRef)) { + if left > right { + swap(&mut exprs.0, &mut exprs.1); + } + match self.links.entry((left, right)) { + Entry::Occupied(entry) => self.on_keys[*entry.get()].push(exprs), + Entry::Vacant(entry) => { + let id = self.on_keys.len(); + self.on_keys.push(vec![exprs]); + entry.insert(id); + self.links.insert((right, left), id); + } + } + } + + // Both directions share one canonically oriented list; take aligns the selected ON. + fn get(&self, left: u64, right: u64) -> impl Iterator + '_ { + self.links + .get(&(left, right)) + .into_iter() + .flat_map(|&id| self.on_keys[id].iter().copied()) + } + + fn pairs(&self) -> impl Iterator + '_ { + self.links + .keys() + .copied() + .filter(|(left, right)| left < right) + } + + fn take(&mut self, left: u64, right: u64) -> Vec<(ExprRef, ExprRef)> { + let Some(id) = self.links.remove(&(left, right)) else { + return Vec::new(); + }; + self.links.remove(&(right, left)); + let mut on = std::mem::take(&mut self.on_keys[id]); + if left > right { + for (l, r) in &mut on { + swap(l, r); + } + } + on + } + + fn rewrite(&mut self, left: u64, right: u64) { + // take has already consumed the internal edge; only external links remain. + debug_assert!(!self.links.contains_key(&(left, right))); + let merged = left | right; + for old in [left, right] { + loop { + let link = self + .links + .extract_if((old, 0)..=(old, u64::MAX), |_, _| true) + .next(); + let Some(((_, other), id)) = link else { break }; + self.links.remove(&(other, old)); + if (old > other) != (merged > other) { + for (l, r) in &mut self.on_keys[id] { + swap(l, r); + } + } + if let Some(&existing) = self.links.get(&(merged, other)) { + let mut extra = std::mem::take(&mut self.on_keys[id]); + self.on_keys[existing].append(&mut extra); + } else { + self.links.insert((merged, other), id); + self.links.insert((other, merged), id); + } + } + } } } @@ -495,59 +613,47 @@ impl NormalizationRule for GreedyJoinReorder { { return Ok(false); } + let (input_count, cross_join) = region_shape(plan); + if input_count > u64::BITS as usize { + return Ok(false); + } let original_schema = plan.output_schema(arena).clone(); let mut schemas = Vec::new(); - let mut keys = Vec::new(); - if inspect(plan, &mut schemas, &mut keys, arena, loader)?.is_none() || schemas.len() < 3 { + let mut keys = JoinKeys::default(); + let estimated = inspect(plan, &mut schemas, &mut keys, arena, loader)?.is_some(); + // Without row estimates the greedy order degrades to the heuristic + // "equijoins first, then input order", which only helps by deferring + // Cartesian products already present in the region. + if input_count < 3 || (!estimated && !cross_join) { return Ok(false); } - // Ambiguous column identities cannot be rebound to an individual input. - for (i, left) in schemas.iter().enumerate() { - for right in &schemas[i + 1..] { - if left - .iter() - .any(|l| right.iter().any(|r| arena.same_column(*l, *r))) - { - return Ok(false); - } - } - } - // Constants and keys spanning several inputs need separate predicate handling. - for &(left, right) in &keys { - let mut endpoints = [None, None]; - for (endpoint, expr) in endpoints.iter_mut().zip([left, right]) { - if !expr.any_referenced_column(arena, |_, _| true)? { - return Ok(false); - } - for (index, schema) in schemas.iter().enumerate() { - if key_in_schema(expr, schema, arena)? { - *endpoint = Some(index); - break; - } - } - if endpoint.is_none() { - return Ok(false); - } - } - if endpoints[0] == endpoints[1] { - return Ok(false); - } - } let mut inputs = Vec::with_capacity(schemas.len()); flatten(plan.take(), &mut inputs); + let mut row_cache = HashMap::new(); while inputs.len() > 1 { let mut best: Option = None; - for i in 0..inputs.len() { - let (prefix, suffix) = inputs.split_at_mut(i + 1); - let (left, left_rows) = &mut prefix[i]; - let left_stats = index_statistics(left); - for (offset, (right, right_rows)) in suffix.iter_mut().enumerate() { - let j = i + 1 + offset; + let connected = !keys.links.is_empty(); + let mut evaluate = |inputs: &mut [(LogicalPlan, usize, u64)], + i: usize, + j: usize| + -> Result<(), DatabaseError> { + let (prefix, suffix) = inputs.split_at_mut(j); + let (left, left_rows, left_mask) = &mut prefix[i]; + let (right, right_rows, right_mask) = &mut suffix[0]; + // This pair's inputs and keys stay fixed until either input is merged. + let cache_key = (*left_mask, *right_mask); + let candidate_keys = || keys.get(*left_mask, *right_mask); + let product = if !estimated { + 0 + } else if let Some(rows) = row_cache.get(&cache_key) { + *rows + } else { + let ndvs = column_ndv(left, right, candidate_keys(), loader, arena)?; + let left_stats = index_statistics(left); let right_stats = index_statistics(right); - let columns = column_statistics(left, right, &keys, loader, arena)?; let left_schema = left.output_schema(arena); let right_schema = right.output_schema(arena); - let product = join_rows( + let rows = join_rows( &JoinInputStatistics { rows: *left_rows, schema: left_schema, @@ -558,32 +664,49 @@ impl NormalizationRule for GreedyJoinReorder { schema: right_schema, index: right_stats.map(|(index, ndv)| (arena.index(index), ndv)), }, - &keys, - columns, + &candidate_keys, + ndvs, arena, ); - if let Some(previous) = best { - // Equal row estimates retain the earlier pair in enumeration order. - if !previous.cross_join && product >= previous.rows { - continue; - } - } - let mut connected = false; - for &key in &keys { - if key_between(key, left_schema, right_schema, arena)?.is_some() { - connected = true; - break; - } - } - // Apply available join predicates before forming Cartesian intermediates. - let candidate = JoinCandidate { - cross_join: !connected, - rows: product, - left: i, - right: j, - }; - if best.is_none_or(|previous| candidate < previous) { - best = Some(candidate); + row_cache.insert(cache_key, rows); + rows + }; + // Edge order differs from input order: equal rows must reach the tie-breaker. + if best.is_some_and(|previous| product > previous.rows) { + return Ok(()); + } + let candidate = JoinCandidate { + cross_join: !connected, + rows: product, + left: i, + right: j, + }; + if best.is_none_or(|previous| candidate < previous) { + best = Some(candidate); + } + Ok(()) + }; + if connected { + for (left, right) in keys.pairs() { + // SAFETY: keys only contains active input masks; merge rewrites removed + // endpoints. Disjoint masks have unique lowest bits, and inserting each + // merged input at the earlier position preserves their sorted order. + let i = inputs + .binary_search_by_key(&(left.trailing_zeros(), left), |(_, _, mask)| { + (mask.trailing_zeros(), *mask) + }) + .unwrap(); + let j = inputs + .binary_search_by_key(&(right.trailing_zeros(), right), |(_, _, mask)| { + (mask.trailing_zeros(), *mask) + }) + .unwrap(); + evaluate(&mut inputs, i.min(j), i.max(j))?; + } + } else { + for i in 0..inputs.len() { + for j in i + 1..inputs.len() { + evaluate(&mut inputs, i, j)?; } } } @@ -596,20 +719,11 @@ impl NormalizationRule for GreedyJoinReorder { else { return Ok(false); }; - let (mut right, _) = inputs.remove(j); - let (mut left, _) = inputs.remove(i); + let (mut right, _, right_mask) = inputs.remove(j); + let (mut left, _, left_mask) = inputs.remove(i); let left_schema = left.output_schema(arena); let right_schema = right.output_schema(arena); - let mut on = Vec::new(); - let mut index = 0; - while index < keys.len() { - if let Some(key) = key_between(keys[index], left_schema, right_schema, arena)? { - keys.remove(index); - on.push(key); - } else { - index += 1; - } - } + let mut on = keys.take(left_mask, right_mask); for (l, r) in &mut on { *l = l.clone_expression(arena)?; *r = r.clone_expression(arena)?; @@ -629,15 +743,20 @@ impl NormalizationRule for GreedyJoinReorder { join.physical_option = Some(PhysicalOption::new( implementation, SortOption::None, - RowCountOption::Estimated { - rows: product, - ndv: None, + if estimated { + RowCountOption::Estimated { + rows: product, + ndv: None, + } + } else { + RowCountOption::Unknown }, )); - inputs.insert(i, (join, product)); + keys.rewrite(left_mask, right_mask); + inputs.insert(i, (join, product, left_mask | right_mask)); } // SAFETY: At this point, there should be exactly one plan left in the inputs vector, which is the final join result. - let (join, _) = inputs.pop().unwrap(); + let (join, _, _) = inputs.pop().unwrap(); *plan = restore_join_output(join, original_schema, arena); Ok(true) } @@ -649,9 +768,7 @@ mod tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc}; use crate::expression::{AliasType, BinaryOperator}; - use crate::optimizer::core::histogram::{ - HistogramMetaBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR, - }; + use crate::optimizer::core::histogram::{HistogramMetaBuilder, DEFAULT_HLL_PRECISION}; use crate::optimizer::heuristic::batch::HepBatchStrategy; use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; use crate::optimizer::rule::normalization::NormalizationRuleImpl; @@ -861,14 +978,367 @@ mod tests { } #[test] - fn column_statistics_are_read_from_scan_without_an_index() -> Result<(), DatabaseError> { + fn join_keys_share_on_ids_and_move_vectors_without_copy() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (_, a) = input("a", 10, &mut arena); + let (_, b) = input("b", 10, &mut arena); + let (_, c) = input("c", 10, &mut arena); + let (_, d) = input("d", 10, &mut arena); + let alias = a.clone_expression(&mut arena)?; + let mut keys = JoinKeys::default(); + keys.insert(1, 4, (a, c)); + keys.insert(1, 4, (alias, c)); + keys.insert(4, 1, (c, a)); + keys.insert(2, 4, (b, c)); + keys.insert(1, 2, (a, b)); + keys.insert(2, 8, (b, d)); + keys.insert(4, 8, (c, d)); + assert_eq!(keys.links.len(), 10); + assert_eq!(keys.on_keys.len(), 5); + let ac = keys.links[&(1, 4)]; + assert_eq!(keys.links[&(4, 1)], ac); + assert_eq!(keys.on_keys[ac], vec![(a, c), (alias, c), (a, c)]); + let bd = keys.links[&(2, 8)]; + let bd_data = keys.on_keys[bd].as_ptr(); + assert_eq!( + keys.pairs().collect::>(), + vec![(1, 2), (1, 4), (2, 4), (2, 8), (4, 8)] + ); + assert_eq!( + keys.get(4, 1).collect::>(), + vec![(a, c), (alias, c), (a, c)] + ); + assert!(keys.get(1, 8).next().is_none()); + let ab_data = keys.on_keys[keys.links[&(1, 2)]].as_ptr(); + let on = keys.take(1, 2); + assert_eq!(on, vec![(a, b)]); + assert_eq!(on.as_ptr(), ab_data); + assert!(keys.links.contains_key(&(1, 4))); + keys.rewrite(1, 2); + assert_eq!( + keys.get(3, 4).collect::>(), + vec![(a, c), (alias, c), (a, c), (b, c)] + ); + assert_eq!(keys.get(3, 8).collect::>(), vec![(b, d)]); + assert_eq!(keys.links[&(3, 8)], bd); + assert_eq!(keys.on_keys[bd].as_ptr(), bd_data); + assert!(keys.get(1, 4).next().is_none()); + assert!(keys.get(1, 2).next().is_none()); + assert_eq!(keys.take(4, 3), vec![(c, a), (c, alias), (c, a), (c, b)]); + keys.rewrite(4, 3); + assert_eq!(keys.take(8, 7), vec![(d, c), (d, b)]); + keys.rewrite(8, 7); + assert!(keys.links.is_empty()); + assert!(keys.on_keys.iter().all(Vec::is_empty)); + Ok(()) + } + + #[test] + fn inspect_inserts_keys_directly_and_keeps_duplicate_conditions() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let (a, ak) = input("a", 1, &mut arena); + let (b, bk) = input("b", 1, &mut arena); + let (c, ck) = input("c", 1, &mut arena); + let ab = JoinOperator::build( + a, + b, + JoinCondition::On { + on: vec![(ak, bk), (ak, bk)], + filter: None, + }, + JoinType::Inner, + false, + ); + let mut plan = JoinOperator::build( + ab, + c, + JoinCondition::On { + on: vec![(ak, bk), (bk, ck)], + filter: None, + }, + JoinType::Inner, + false, + ); + let mut schemas = Vec::new(); + let mut keys = JoinKeys::default(); + assert!(inspect(&mut plan, &mut schemas, &mut keys, &mut arena, None)?.is_some()); + assert_eq!(keys.links.len(), 4); + assert_eq!(keys.links[&(1, 2)], keys.links[&(2, 1)]); + assert_eq!(keys.on_keys.len(), 2); + assert_eq!(keys.take(1, 2), vec![(ak, bk); 3]); + Ok(()) + } + + #[test] + fn link_prefixes_update_incoming_edges_without_copying_on() { + let (a, b, c) = (ExprRef::new(0), ExprRef::new(1), ExprRef::new(2)); + let mut keys = JoinKeys::default(); + keys.insert(1, 8, (a, b)); + keys.insert(8, 16, (b, c)); + let id = keys.links[&(1, 8)]; + let data = keys.on_keys[id].as_ptr(); + assert_eq!(keys.take(8, 16), vec![(b, c)]); + assert!(keys.links.contains_key(&(1, 8))); + assert!(!keys.links.contains_key(&(1, 24))); + keys.rewrite(8, 16); + assert_eq!(keys.links[&(1, 24)], id); + assert_eq!(keys.links[&(24, 1)], id); + assert_eq!(keys.on_keys[id].as_ptr(), data); + assert_eq!(keys.get(24, 1).collect::>(), vec![(a, b)]); + assert_eq!(keys.pairs().collect::>(), vec![(1, 24)]); + assert!(keys + .links + .keys() + .all(|(l, r)| ![8, 16].contains(l) && ![8, 16].contains(r))); + assert_eq!(keys.take(24, 1), vec![(b, a)]); + assert!(keys.links.is_empty()); + } + + #[test] + fn shared_on_id_preserves_order_when_merged_direction_flips() { + let (a, b, c, d, e) = ( + ExprRef::new(0), + ExprRef::new(1), + ExprRef::new(2), + ExprRef::new(3), + ExprRef::new(4), + ); + let mut keys = JoinKeys::default(); + keys.insert(2, 4, (b, c)); + keys.insert(2, 4, (d, a)); + keys.insert(8, 4, (e, c)); + let id = keys.links[&(2, 4)]; + assert!(keys.take(2, 8).is_empty()); + keys.rewrite(2, 8); + assert_eq!(keys.links[&(10, 4)], id); + assert_eq!(keys.links[&(4, 10)], id); + assert_eq!( + keys.get(10, 4).collect::>(), + vec![(c, b), (a, d), (c, e)] + ); + assert_eq!(keys.take(10, 4), vec![(b, c), (d, a), (e, c)]); + assert!(keys.links.is_empty()); + assert!(keys.on_keys.iter().all(Vec::is_empty)); + } + + #[test] + fn malformed_regions_are_planning_errors() { + // (outer join key, expected message) + let cases: [( + fn(ExprRef, ExprRef, &mut PlanArena) -> (ExprRef, ExprRef), + &str, + ); 2] = [ + (|a, _, _| (a, a), "compares columns of the same join input"), + ( + |_, c, arena| (input("z", 1, arena).1, c), + "is not produced by any join input", + ), + ]; + for (outer_key, message) in cases { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (a, ak) = input("a", 1, &mut arena); + let (b, bk) = input("b", 1, &mut arena); + let (c, ck) = input("c", 1, &mut arena); + let ab = JoinOperator::build( + a, + b, + JoinCondition::On { + on: vec![(ak, bk)], + filter: None, + }, + JoinType::Inner, + false, + ); + let mut plan = JoinOperator::build( + ab, + c, + JoinCondition::On { + on: vec![outer_key(ak, ck, &mut arena)], + filter: None, + }, + JoinType::Inner, + false, + ); + let Err(DatabaseError::InvalidPlan(error)) = + GreedyJoinReorder.apply(&mut plan, &mut arena, None) + else { + panic!("expected invalid plan: {message}"); + }; + assert!(error.contains(message), "{error}"); + } + } + + #[test] + fn input_masks_support_64_inputs_and_skip_larger_regions() -> Result<(), DatabaseError> { + for count in [64, 65] { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (mut plan, mut previous) = input("t0", 1, &mut arena); + for index in 1..count { + let (next, key) = input(&format!("t{index}"), 1, &mut arena); + plan = JoinOperator::build( + plan, + next, + JoinCondition::On { + on: vec![(previous, key)], + filter: None, + }, + JoinType::Inner, + false, + ); + previous = key; + } + let schema = plan.output_schema(&mut arena).clone(); + let original = plan.clone(); + assert_eq!( + GreedyJoinReorder.apply(&mut plan, &mut arena, None)?, + count == 64 + ); + assert_eq!(plan.output_schema(&mut arena), &schema); + if count == 65 { + assert_eq!(plan, original); + } + } + Ok(()) + } + + #[test] + fn column_ndv_skips_unusable_inputs_and_unrelated_keys() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (a, ka) = input("a", 100, &mut arena); + let (b, kb) = input("b", 100, &mut arena); + let (c, kc) = input("c", 100, &mut arena); + let keys = [(ka, kb), (ka, kc)]; + let mut join_keys = JoinKeys::default(); + join_keys.insert(1, 2, (ka, kb)); + join_keys.insert(1, 4, (ka, kc)); + let mut cache = StatisticsMetaCache::default(); + for (name, ndv) in [("a", 10), ("b", 20), ("c", 30)] { + let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; + for value in 0..ndv { + builder.append(&DataValue::Int32(value)); + } + cache.columns.insert((name.into(), 0), builder.build()); + } + let loader = StatisticMetaLoader::new(&cache); + for (left, right, lm, rm, expected) in [(&a, &b, 1, 2, (10, 20)), (&c, &a, 4, 1, (30, 10))] + { + let mut visited = 0; + let ndvs = column_ndv( + left, + right, + join_keys.get(lm, rm).inspect(|_| visited += 1), + Some(&loader), + &arena, + )?; + assert_eq!(ndvs, Some(expected)); + assert_eq!(visited, 1); + } + let mut join = JoinOperator::build( + a, + b, + JoinCondition::On { + on: vec![(ka, kb)], + filter: None, + }, + JoinType::Inner, + false, + ); + join.physical_option = Some(PhysicalOption::new( + PlanImpl::HashJoin, + SortOption::None, + RowCountOption::Estimated { + rows: 100, + ndv: None, + }, + )); + let mut visited = 0; + assert_eq!( + column_ndv( + &join, + &c, + keys.iter().inspect(|_| visited += 1), + Some(&loader), + &arena + )?, + None + ); + assert_eq!(visited, 0); + Ok(()) + } + + #[test] + fn merged_inputs_do_not_reuse_leaf_pair_row_estimates() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (a, ak) = input("a", 100, &mut arena); + let (b, bk) = input("b", 100, &mut arena); + let (c, ck) = input("c", 100, &mut arena); + let (d, dk) = input("d", 100, &mut arena); + let ab = JoinOperator::build( + a, + b, + JoinCondition::On { + on: vec![(ak, bk)], + filter: None, + }, + JoinType::Inner, + false, + ); + let abc = JoinOperator::build( + ab, + c, + JoinCondition::On { + on: vec![(bk, ck)], + filter: None, + }, + JoinType::Inner, + false, + ); + let mut plan = JoinOperator::build( + abc, + d, + JoinCondition::On { + on: vec![(ck, dk)], + filter: None, + }, + JoinType::Inner, + false, + ); + let schema = plan.output_schema(&mut arena).clone(); + let mut cache = StatisticsMetaCache::default(); + for (table, ndv) in [("a", 100), ("b", 100), ("c", 10), ("d", 10)] { + let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; + for value in 0..ndv { + builder.append(&DataValue::Int32(value)); + } + cache.columns.insert((table.into(), 0), builder.build()); + } + assert!(GreedyJoinReorder.apply( + &mut plan, + &mut arena, + Some(&StatisticMetaLoader::new(&cache)) + )?); + // A-B estimates 100, C-D estimates 1,000. Their merged inputs must not + // reuse any leaf-pair result when Vec positions are removed and reused. + assert_eq!(plan.estimated_rows(), Some(100 * 1_000)); + assert_eq!(plan.output_schema(&mut arena), &schema); + Ok(()) + } + + #[test] + fn column_ndv_is_read_from_scan_without_an_index() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let (mut left, l) = input("l", 1000, &mut arena); let (mut right, r) = input("r", 200, &mut arena); let mut cache = StatisticsMetaCache::default(); for (table, rows) in [("l", 1000), ("r", 200)] { - let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; for value in 0..rows { builder.append(&DataValue::Int32(value % 100)); } @@ -876,32 +1346,26 @@ mod tests { } let loader = StatisticMetaLoader::new(&cache); assert_eq!( - column_statistics(&left, &right, &[(l, r)], Some(&loader), &arena)?, + column_ndv(&left, &right, &[(l, r)], Some(&loader), &arena)?, Some((100, 100)) ); - assert_eq!( - column_statistics(&left, &right, &[(l, r)], None, &arena)?, - None - ); - assert_eq!( - column_statistics(&left, &right, &[], Some(&loader), &arena)?, - None - ); + assert_eq!(column_ndv(&left, &right, &[(l, r)], None, &arena)?, None); + assert_eq!(column_ndv(&left, &right, &[], Some(&loader), &arena)?, None); left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { rows: 8, ndv: None }; assert_eq!( - column_statistics(&left, &right, &[(l, r)], Some(&loader), &arena)?, + column_ndv(&left, &right, &[(l, r)], Some(&loader), &arena)?, Some((8, 100)) ); left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { rows: 1000, ndv: None, }; - let mut nulls = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut nulls = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; nulls.append(&DataValue::Null); cache.columns.insert(("r".into(), 0), nulls.build()); assert_eq!( - column_statistics( + column_ndv( &left, &right, &[(l, r)], @@ -912,7 +1376,7 @@ mod tests { ); cache.columns.remove(&("r".into(), 0)); assert_eq!( - column_statistics( + column_ndv( &left, &right, &[(l, r)], @@ -921,7 +1385,7 @@ mod tests { )?, None ); - let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; for value in 0..200 { builder.append(&DataValue::Int32(value % 100)); } @@ -941,7 +1405,7 @@ mod tests { schema: right_schema, index: None }, - &[(l, r)], + || std::iter::once((l, r)), Some((100, 100)), &arena ), @@ -1198,15 +1662,15 @@ mod tests { info.meta = index; let mut cache = StatisticsMetaCache::default(); for (table, ndv) in [("l", 30), ("r", 70)] { - let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; for value in 0..ndv { builder.append(&DataValue::Int32(value)); } cache.columns.insert((table.into(), 0), builder.build()); } let loader = StatisticMetaLoader::new(&cache); - let columns = column_statistics(&left, &right, &[(l, r)], Some(&loader), &arena)?; - assert_eq!(columns, Some((12, 70))); + let ndvs = column_ndv(&left, &right, &[(l, r)], Some(&loader), &arena)?; + assert_eq!(ndvs, Some((12, 70))); let left_schema = left.output_schema(&mut arena); let right_schema = right.output_schema(&mut arena); assert_eq!( @@ -1221,8 +1685,8 @@ mod tests { schema: right_schema, index: None }, - &[(l, r)], - columns, + || std::iter::once((l, r)), + ndvs, &arena ), 19 @@ -1255,20 +1719,20 @@ mod tests { keys.push((extra_keys[0], extra_keys[1])); let mut cache = StatisticsMetaCache::default(); for (table, id, ndv) in [("l", 0, 100), ("r", 0, 100), ("l", 1, 80), ("r", 1, 70)] { - let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; for value in 0..ndv { builder.append(&DataValue::Int32(value)); } cache.columns.insert((table.into(), id), builder.build()); } - let columns = column_statistics( + let ndvs = column_ndv( &left, &right, &keys, Some(&StatisticMetaLoader::new(&cache)), &arena, )?; - assert_eq!(columns, Some((100, 100))); + assert_eq!(ndvs, Some((100, 100))); assert_eq!( estimate_join_rows( JoinInputEstimate { @@ -1286,14 +1750,14 @@ mod tests { ); keys.reverse(); assert_eq!( - column_statistics( + column_ndv( &left, &right, &keys, Some(&StatisticMetaLoader::new(&cache)), &arena )?, - columns + ndvs ); Ok(()) } @@ -1324,8 +1788,14 @@ mod tests { schema: &right_schema, index: Some((arena.index(ri), Some(100))), }; - assert_eq!(join_rows(&l_input, &r_input, &[(l, r)], None, &arena), 2000); - assert_eq!(join_rows(&l_input, &r_input, &[], None, &arena), 200000); + assert_eq!( + join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), + 2000 + ); + assert_eq!( + join_rows(&l_input, &r_input, std::iter::empty, None, &arena), + 200000 + ); let mut unique_right = arena.index(ri).clone(); unique_right.ty = IndexType::Unique; let r_input = JoinInputStatistics { @@ -1333,7 +1803,10 @@ mod tests { schema: &right_schema, index: Some((&unique_right, None)), }; - assert_eq!(join_rows(&l_input, &r_input, &[(l, r)], None, &arena), 1000); + assert_eq!( + join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), + 1000 + ); let mut unique_left = arena.index(li).clone(); unique_left.ty = IndexType::PrimaryKey { is_multiple: false }; let l_input = JoinInputStatistics { @@ -1341,7 +1814,10 @@ mod tests { schema: &left_schema, index: Some((&unique_left, None)), }; - assert_eq!(join_rows(&l_input, &r_input, &[(l, r)], None, &arena), 200); + assert_eq!( + join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), + 200 + ); unique_right.column_ids.push(1); let r_input = JoinInputStatistics { rows: 200, @@ -1354,7 +1830,7 @@ mod tests { index: Some((arena.index(li), Some(100))), }; assert_eq!( - join_rows(&l_input, &r_input, &[(l, r)], None, &arena), + join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), 200000 ); let r_input = JoinInputStatistics { @@ -1363,7 +1839,13 @@ mod tests { index: Some((arena.index(ri), Some(100))), }; assert_eq!( - join_rows(&l_input, &r_input, &[(expr_key, r)], None, &arena), + join_rows( + &l_input, + &r_input, + || std::iter::once((expr_key, r)), + None, + &arena + ), 200000 ); Ok(()) @@ -1500,11 +1982,17 @@ mod tests { index: Some((&r_index, Some(100))), }; assert_eq!( - join_rows(&l_input, &r_input, &[(l, r)], None, &arena), + join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), 200000 ); assert_eq!( - join_rows(&l_input, &r_input, &[(l, r), (l2, r2)], None, &arena), + join_rows( + &l_input, + &r_input, + || [(l, r), (l2, r2)].into_iter(), + None, + &arena + ), 2000 ); Ok(()) @@ -1873,6 +2361,76 @@ mod tests { Ok(()) } + #[test] + fn input_mask_search_uses_lowest_bit_and_checks_full_mask() { + let masks = [9u64, 6, 16]; + let find = |mask: u64| { + masks.binary_search_by_key(&(mask.trailing_zeros(), mask), |mask| { + (mask.trailing_zeros(), *mask) + }) + }; + assert_eq!(find(9), Ok(0)); + assert_eq!(find(6), Ok(1)); + assert_eq!(find(16), Ok(2)); + assert!(find(1).is_err()); + assert!(find(2).is_err()); + assert!(find(64).is_err()); + } + + #[test] + fn edge_order_ties_keep_original_input_order() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let (a, ak) = input("a", 1, &mut arena); + let (b, bk) = input("b", 1, &mut arena); + let (c, ck) = input("c", 1, &mut arena); + let (d, dk) = input("d", 1, &mut arena); + let (e, ek) = input("e", 10, &mut arena); + let bc = JoinOperator::build( + b, + c, + JoinCondition::On { + on: vec![(bk, ck)], + filter: None, + }, + JoinType::Inner, + false, + ); + let abc = JoinOperator::build(a, bc, JoinCondition::None, JoinType::Cross, false); + let abcd = JoinOperator::build( + abc, + d, + JoinCondition::On { + on: vec![(ak, dk)], + filter: None, + }, + JoinType::Inner, + false, + ); + let mut plan = JoinOperator::build( + abcd, + e, + JoinCondition::On { + on: vec![(ak, ek), (bk, ek)], + filter: None, + }, + JoinType::Inner, + false, + ); + let schema = plan.output_schema(&mut arena).clone(); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + // AD (mask 9) precedes BC (mask 6) in inputs, despite their reverse tree order. + let join = plan.childrens.only(); + let (left, right) = join.childrens.twins(); + assert_eq!( + left.referenced_table(), + vec!["a".into(), "d".into(), "e".into()] + ); + assert_eq!(right.referenced_table(), vec!["b".into(), "c".into()]); + Ok(()) + } + #[test] fn nonadjacent_pair_restores_output_order_and_keeps_all_keys() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); @@ -2055,6 +2613,73 @@ mod tests { Ok(()) } + #[test] + fn unestimated_region_defers_cartesian_products_by_input_order() -> Result<(), DatabaseError> { + // FROM a, c, d, b WHERE a = b AND c = b: FROM order crosses a, c and d + // before any join key applies. Without estimates the region is rebuilt + // by connectivity and input position, and only d stays a cross join. + for known_b in [false, true] { + let table_arena = TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let unknown = |name: &str, arena: &mut PlanArena| { + let (mut plan, key) = input(name, 10, arena); + if !(known_b && name == "b") { + plan.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; + } + (plan, key) + }; + let (a, a_key) = unknown("a", &mut arena); + let (c, c_key) = unknown("c", &mut arena); + let (d, _) = unknown("d", &mut arena); + let (b, b_key) = unknown("b", &mut arena); + let ac = JoinOperator::build(a, c, JoinCondition::None, JoinType::Cross, false); + let acd = JoinOperator::build(ac, d, JoinCondition::None, JoinType::Cross, false); + let c_column = c_key.output_column_ref(&mut arena); + let c_key = arena.alloc_expression(ScalarExpression::column_expr(c_column, 1)); + let mut plan = JoinOperator::build( + acd, + b, + JoinCondition::On { + on: vec![(a_key, b_key), (c_key, b_key)], + filter: None, + }, + JoinType::Inner, + false, + ); + let schema = plan.output_schema(&mut arena).clone(); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + + let cross = plan.childrens.only(); + assert!(matches!( + &cross.operator, + Operator::Join(join) if join.join_type == JoinType::Cross + )); + let (abc, d) = cross.childrens.twins(); + assert_eq!(d.referenced_table(), vec!["d".into()]); + let (ab, c) = abc.childrens.twins(); + assert_eq!(c.referenced_table(), vec!["c".into()]); + let (a, b) = ab.childrens.twins(); + assert_eq!(a.referenced_table(), vec!["a".into()]); + assert_eq!(b.referenced_table(), vec!["b".into()]); + for join in [cross, abc, ab] { + assert_eq!(join.row_count_option(), None); + } + for join in [abc, ab] { + let Operator::Join(JoinOperator { + join_type: JoinType::Inner, + on: JoinCondition::On { on, .. }, + .. + }) = &join.operator + else { + unreachable!() + }; + assert_eq!(on.len(), 1); + } + } + Ok(()) + } + #[test] fn rebuilding_join_region_preserves_parent_and_input_subtrees() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); diff --git a/src/planner/arena.rs b/src/planner/arena.rs index fad10836..c2ee70be 100644 --- a/src/planner/arena.rs +++ b/src/planner/arena.rs @@ -152,7 +152,7 @@ pub struct PlanArena<'a> { plans: Vec, } -#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct ExprRef { pos: usize, } diff --git a/src/planner/operator/analyze.rs b/src/planner/operator/analyze.rs index 101c08d7..e82566d7 100644 --- a/src/planner/operator/analyze.rs +++ b/src/planner/operator/analyze.rs @@ -17,12 +17,15 @@ use crate::planner::MetaArena; use crate::planner::{fmt_explain_list, Explain}; use crate::types::index::IndexMetaRef; use kite_sql_serde_macros::ReferenceSerialization; +use ordered_float::OrderedFloat; #[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] pub struct AnalyzeOperator { pub table_name: TableName, pub index_metas: Vec, pub histogram_buckets: Option, + pub hll_precision: Option, + pub statistics_relative_error: Option>, } impl Explain for AnalyzeOperator { diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 9cfab936..6399a3d7 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -1248,6 +1248,8 @@ mod tests { table_name: "users".into(), index_metas: vec![IndexMetaRef::new(1)], histogram_buckets: Some(8), + hll_precision: None, + statistics_relative_error: None, }), ]; for operator in no_reference_operators { @@ -1491,6 +1493,8 @@ mod tests { table_name: "users".into(), index_metas: vec![index], histogram_buckets: Some(128), + hll_precision: None, + statistics_relative_error: None, }), "Analyze users -> [idx_users_id]", ), diff --git a/src/planner/operator/visitor.rs b/src/planner/operator/visitor.rs index 1525b261..ca94b0c2 100644 --- a/src/planner/operator/visitor.rs +++ b/src/planner/operator/visitor.rs @@ -594,6 +594,8 @@ pub(crate) mod tests { table_name: "t1".into(), index_metas: vec![IndexMetaRef::new(0)], histogram_buckets: Some(1), + hll_precision: None, + statistics_relative_error: None, }), Operator::AddColumn(AddColumnOperator { table_name: "t1".into(), diff --git a/src/serdes/num.rs b/src/serdes/num.rs index c2792807..293bc196 100644 --- a/src/serdes/num.rs +++ b/src/serdes/num.rs @@ -16,6 +16,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; use crate::storage::Transaction; +use ordered_float::OrderedFloat; use std::io::Read; use std::io::Write; use std::mem::size_of; @@ -63,6 +64,27 @@ implement_num_serialization!(u64); implement_num_serialization!(f32); implement_num_serialization!(f64); +impl ReferenceSerialization for OrderedFloat { + fn encode( + &self, + writer: &mut W, + is_direct: bool, + reference_tables: &mut ReferenceTables, + arena: &A, + ) -> Result<(), DatabaseError> { + self.0.encode(writer, is_direct, reference_tables, arena) + } + + fn decode( + reader: &mut R, + drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, + reference_tables: &ReferenceTables, + arena: &mut A, + ) -> Result { + f64::decode(reader, drive, reference_tables, arena).map(OrderedFloat) + } +} + impl ReferenceSerialization for usize { fn encode( &self, diff --git a/src/storage/table_codec.rs b/src/storage/table_codec.rs index 13f6938b..95d7daf5 100644 --- a/src/storage/table_codec.rs +++ b/src/storage/table_codec.rs @@ -1115,7 +1115,8 @@ mod tests { use crate::errors::DatabaseError; use crate::iter_ext::Itertools; use crate::optimizer::core::histogram::{ - HistogramBuilder, HistogramMetaBuilder, ANALYZE_STATISTICS_RELATIVE_ERROR, + HistogramBuilder, HistogramMetaBuilder, DEFAULT_HLL_PRECISION, + DEFAULT_STATISTICS_RELATIVE_ERROR, }; use crate::optimizer::core::statistics_meta::IndexStatistics; use crate::planner::{PlanArena, TableArenaCell}; @@ -1228,7 +1229,11 @@ mod tests { name: "pk_c1".to_string(), ty: IndexType::PrimaryKey { is_multiple: false }, }; - let mut builder = HistogramBuilder::new(&index_meta, ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramBuilder::new( + &index_meta, + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; for value in 0..4 { builder.append(DataValue::Int32(value))?; @@ -1247,7 +1252,7 @@ mod tests { let index_key = table_codec .with_index_statistics("t1", 0, None, &plan_arena, |key, _| Ok(key.to_vec()))?; - let mut builder = HistogramMetaBuilder::new(ANALYZE_STATISTICS_RELATIVE_ERROR)?; + let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; for value in [ DataValue::Int32(1), DataValue::Int32(1), diff --git a/tests/slt/join.slt b/tests/slt/join.slt index 8eb3707b..a36a325b 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -206,6 +206,41 @@ select * from a as aliased(only_one) statement error select * from a join b using (missing) +# One FROM namespace cannot bind the same visible name twice. +statement error table name: `a` specified more than once +select * from a, a + +statement error table name: `a` specified more than once +select * from a join a on true + +statement error table name: `x` specified more than once +select * from a x join b x on x.v1 = x.v3 + +statement error table name: `b` specified more than once +select * from a, a b, b + +statement error table name: `x` specified more than once +select * from a x, (select * from b) x + +statement error table name: `c` specified more than once +with c as (select * from a) select * from c, c + +# Distinct aliases and nested scopes may reuse a table. +query I +select count(*) from a x, a y +---- +9 + +query I +with c as (select * from a) select count(*) from c x, c y +---- +9 + +query I +select count(*) from a where exists (select 1 from a where a.v1 = 1) +---- +3 + # Parameterized inner join statement ok diff --git a/tests/slt/subquery.slt b/tests/slt/subquery.slt index 3201835b..81c9c931 100644 --- a/tests/slt/subquery.slt +++ b/tests/slt/subquery.slt @@ -37,6 +37,29 @@ select x.a from (select -a as a from t1) as x; -1 -3 +# A derived table does not see sources bound earlier in the same FROM. +query IIIII rowsort +select * from t1 x, (select a, b from t1) y; +---- +0 1 2 1 2 +0 1 2 3 4 +1 3 4 1 2 +1 3 4 3 4 + +query I rowsort +select y.* from t1 x, (select id from t1) y; +---- +0 +0 +1 +1 + +statement error +select * from t1 x, (select a from t1 where t1.a = x.a) y; + +statement error LATERAL derived tables are not supported +select * from t1 x, lateral (select a from t1) y; + query III select * from t1 where a <= (select 4) and a > (select 1) ---- From 76f85d8af1373b55841d834c72273c3fe625c8bb Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 16/62] perf: reset read executors without rebuilding join inputs --- src/db/prepared.rs | 21 ++- src/execution/dql/aggregate/hash_agg.rs | 9 + src/execution/dql/aggregate/simple_agg.rs | 9 + src/execution/dql/aggregate/stream_agg.rs | 10 + .../dql/aggregate/stream_distinct.rs | 9 + src/execution/dql/describe.rs | 10 + src/execution/dql/dummy.rs | 9 + src/execution/dql/explain.rs | 9 + src/execution/dql/external_sort.rs | 9 + src/execution/dql/filter.rs | 8 + src/execution/dql/function_scan.rs | 9 + src/execution/dql/index_scan.rs | 134 ++++++++++++- src/execution/dql/join/hash_join.rs | 11 ++ src/execution/dql/join/nested_loop_join.rs | 44 +++-- src/execution/dql/limit.rs | 10 + src/execution/dql/mark_apply.rs | 148 ++++++++------- src/execution/dql/projection.rs | 8 + src/execution/dql/recursive_cte.rs | 65 ++++++- src/execution/dql/scalar_query_init.rs | 31 +-- src/execution/dql/scalar_subquery.rs | 9 + src/execution/dql/seq_scan.rs | 97 ++++++++++ src/execution/dql/set_membership.rs | 11 ++ src/execution/dql/show_table.rs | 9 + src/execution/dql/show_view.rs | 9 + src/execution/dql/sort.rs | 47 ++++- src/execution/dql/top_k.rs | 10 + src/execution/dql/union.rs | 10 + src/execution/dql/values.rs | 31 +-- src/execution/dql/window.rs | 65 +++++++ src/execution/mod.rs | 177 +++++++++++++++++- src/storage/mod.rs | 25 +++ src/types/tuple.rs | 22 +++ 32 files changed, 938 insertions(+), 147 deletions(-) diff --git a/src/db/prepared.rs b/src/db/prepared.rs index e731bd66..a93bb7b8 100644 --- a/src/db/prepared.rs +++ b/src/db/prepared.rs @@ -141,6 +141,7 @@ impl State { mod tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc}; + use crate::execution::dql::index_scan::IndexScan; use crate::expression::range_detacher::Range; use crate::expression::{BinaryOperator, ScalarExpression}; use crate::planner::operator::filter::FilterOperator; @@ -314,7 +315,7 @@ mod tests { let Childrens::Only(scan) = plan.childrens.as_ref() else { panic!("expected scan") }; - let Operator::TableScan(scan_op) = &scan.operator else { + let Operator::TableScan(_) = &scan.operator else { panic!("expected table scan") }; let Some(PhysicalOption { @@ -330,12 +331,12 @@ mod tests { let mut execution_arena = crate::execution::ExecArena::< ::TransactionType<'_>, >::with_capacity(0); - let executor = crate::execution::dql::index_scan::IndexScan::new( - scan_op, + let ranges = IndexScan::ranges( info, info.lookup.as_ref().expect("lookup"), - ); - let ranges = executor.ranges(&mut execution_arena, &mut arena)?; + &mut execution_arena, + &mut arena, + )?; let expected_range = match &expected_index.lookup { Some(IndexLookup::Static(range)) => range, _ => unreachable!(), @@ -595,7 +596,7 @@ mod tests { .or_else(|| plan.childrens.iter().find_map(find_scan)) } let scan = find_scan(&plan.plan).expect("table scan"); - let Operator::TableScan(scan_op) = &scan.operator else { + let Operator::TableScan(_) = &scan.operator else { unreachable!() }; let Some(PhysicalOption { @@ -608,12 +609,12 @@ mod tests { let mut execution_arena = crate::execution::ExecArena::< ::TransactionType<'_>, >::with_capacity(0); - let ranges = crate::execution::dql::index_scan::IndexScan::new( - scan_op, + let ranges = IndexScan::ranges( info, info.lookup.as_ref().expect("lookup"), - ) - .ranges(&mut execution_arena, &mut bound_arena)?; + &mut execution_arena, + &mut bound_arena, + )?; let mut ranges = ranges; assert_eq!( ranges.next(), diff --git a/src/execution/dql/aggregate/hash_agg.rs b/src/execution/dql/aggregate/hash_agg.rs index 927913dd..3b112736 100644 --- a/src/execution/dql/aggregate/hash_agg.rs +++ b/src/execution/dql/aggregate/hash_agg.rs @@ -65,6 +65,15 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for HashAggExecutor<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for HashAggExecutor<'a> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.output = None; + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/aggregate/simple_agg.rs b/src/execution/dql/aggregate/simple_agg.rs index 64e1fe2b..180770a1 100644 --- a/src/execution/dql/aggregate/simple_agg.rs +++ b/src/execution/dql/aggregate/simple_agg.rs @@ -49,6 +49,15 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for SimpleAggExecutor<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for SimpleAggExecutor<'a> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.returned = false; + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/aggregate/stream_agg.rs b/src/execution/dql/aggregate/stream_agg.rs index 63b58304..9f57b614 100644 --- a/src/execution/dql/aggregate/stream_agg.rs +++ b/src/execution/dql/aggregate/stream_agg.rs @@ -65,6 +65,16 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for StreamAggExecutor<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for StreamAggExecutor<'a> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.group_keys = None; + self.accs.clear(); + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/aggregate/stream_distinct.rs b/src/execution/dql/aggregate/stream_distinct.rs index da1250ee..7fc62bfa 100644 --- a/src/execution/dql/aggregate/stream_distinct.rs +++ b/src/execution/dql/aggregate/stream_distinct.rs @@ -50,6 +50,15 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for StreamDistinctExecutor<'a> } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for StreamDistinctExecutor<'a> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.last_keys = None; + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/describe.rs b/src/execution/dql/describe.rs index cbd18eff..fa581663 100644 --- a/src/execution/dql/describe.rs +++ b/src/execution/dql/describe.rs @@ -72,6 +72,16 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Describe { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Describe { + fn reset( + &mut self, + _arena: &mut ExecArena<'a, T>, + _plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.columns = None; + self.cursor = 0; + Ok(()) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/dummy.rs b/src/execution/dql/dummy.rs index 019053dc..5070bf70 100644 --- a/src/execution/dql/dummy.rs +++ b/src/execution/dql/dummy.rs @@ -46,6 +46,15 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Dummy { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Dummy { + fn reset( + &mut self, + _arena: &mut ExecArena<'a, T>, + _plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.row = Some(Tuple::default()); + Ok(()) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/explain.rs b/src/execution/dql/explain.rs index dd999aba..0922ffb8 100644 --- a/src/execution/dql/explain.rs +++ b/src/execution/dql/explain.rs @@ -43,6 +43,15 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Explain<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Explain<'a> { + fn reset( + &mut self, + _arena: &mut ExecArena<'a, T>, + _plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.emitted = false; + Ok(()) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/external_sort.rs b/src/execution/dql/external_sort.rs index c8a914c2..fec0e9f3 100644 --- a/src/execution/dql/external_sort.rs +++ b/src/execution/dql/external_sort.rs @@ -69,6 +69,15 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ExternalSort<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ExternalSort<'a> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.rows = None; + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/filter.rs b/src/execution/dql/filter.rs index 757ed6ec..fc2a2932 100644 --- a/src/execution/dql/filter.rs +++ b/src/execution/dql/filter.rs @@ -45,6 +45,14 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Filter { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Filter { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/function_scan.rs b/src/execution/dql/function_scan.rs index e7b206c1..f2c4cb4d 100644 --- a/src/execution/dql/function_scan.rs +++ b/src/execution/dql/function_scan.rs @@ -50,6 +50,15 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for FunctionScan<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for FunctionScan<'a> { + fn reset( + &mut self, + _arena: &mut ExecArena<'a, T>, + _plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.iter = None; + Ok(()) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/index_scan.rs b/src/execution/dql/index_scan.rs index 164fafa4..95319bb2 100644 --- a/src/execution/dql/index_scan.rs +++ b/src/execution/dql/index_scan.rs @@ -44,12 +44,12 @@ impl<'a, T: Transaction + 'a> IndexScan<'a, T> { } pub(crate) fn ranges( - &self, + info: &'a IndexInfo, + lookup: &'a IndexLookup, arena: &mut ExecArena<'a, T>, plan_arena: &mut (dyn MetaArena + 'a), ) -> Result, DatabaseError> { - let info = self.info; - let mut range = match self.lookup { + let mut range = match lookup { IndexLookup::Static(range) => Cow::Borrowed(range), IndexLookup::Probe => Cow::Owned(match arena.pop_runtime_probe() { RuntimeIndexProbe::Eq(value) => Range::Eq(value), @@ -99,6 +99,18 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for IndexScan<'a, T> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for IndexScan<'a, T> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + if let Some(iter) = self.iter.as_mut() { + let ranges = Self::ranges(self.info, self.lookup, arena, plan_arena)?; + iter.reset(ranges, self.op.limit); + } + Ok(()) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, @@ -107,7 +119,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for IndexScan<'a, T> { let iter = match &mut self.iter { Some(iter) => iter, None => { - let ranges = self.ranges(arena, plan_arena)?; + let ranges = Self::ranges(self.info, self.lookup, arena, plan_arena)?; let TableScanOperator { table_name, columns, @@ -140,3 +152,117 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for IndexScan<'a, T> { Ok(()) } } + +// GRCOV_EXCL_START +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::*; + use crate::db::test::build_table; + use crate::execution::{empty_context, ExecStatus}; + use crate::planner::{PlanArena, TableArenaCell}; + use crate::storage::memory::MemoryStorage; + use crate::storage::table_codec::TableCodec; + use crate::storage::{StatisticsMetaCache, Storage, TableCache, ViewCache}; + use crate::types::tuple::Tuple; + use crate::types::value::DataValue; + use crate::types::LogicalType; + use std::ops::Bound; + + #[test] + fn reset_rebinds_probe_after_partial_read_eof_and_missing_key() -> Result<(), DatabaseError> { + let storage = MemoryStorage::new(); + let mut tx = storage.transaction()?; + let mut tables = TableCache::default(); + let cell = TableArenaCell::default(); + let mut meta = PlanArena::new(&cell); + build_table(&mut tables, &mut tx, &mut meta)?; + let meta = PlanArena::new(&cell); + let serializers = [ + LogicalType::Integer.serializable(), + LogicalType::Boolean.serializable(), + LogicalType::Integer.serializable(), + ]; + let mut codec = TableCodec::default(); + for id in 1..=4 { + tx.append_tuple( + &mut codec, + "t1", + &Tuple::new( + Some(DataValue::Int32(id)), + vec![ + DataValue::Int32(id), + DataValue::Boolean(true), + DataValue::Int32(id * 10), + ], + ), + &serializers, + false, + )?; + } + let table = tables.get("t1").unwrap(); + let op = TableScanOperator { + table_name: "t1".into(), + columns: table.columns().copied().collect(), + limit: (None, None), + index_infos: vec![], + with_pk: true, + }; + let info = IndexInfo { + meta: *table.indexes().next().unwrap(), + sort_option: SortOption::None, + lookup: None, + residual_predicate: None, + covered_deserializers: None, + cover_mapping: None, + sort_elimination_hint: None, + stream_aggregate_hint: None, + }; + let views = ViewCache::default(); + let statistics = StatisticsMetaCache::default(); + let mut arena = ExecArena::with_capacity(0); + arena.init_context(empty_context(&tables, &views, &statistics), &tx); + let mut meta = ExecMetaArena::new(meta); + let mut scan = IndexScan::new(&op, &info, &IndexLookup::Probe); + arena.push_runtime_probe(RuntimeIndexProbe::Scope { + min: Bound::Included(DataValue::Int32(1)), + max: Bound::Included(DataValue::Int32(4)), + }); + scan.next_tuple(&mut arena, &mut meta)?; + assert_eq!(arena.result_tuple().values[0], DataValue::Int32(1)); + let iterator = scan.iter.as_ref().unwrap() as *const _; + for (probe, expected) in [ + (RuntimeIndexProbe::Eq(DataValue::Int32(3)), vec![3]), + (RuntimeIndexProbe::Eq(DataValue::Int32(99)), vec![]), + ( + RuntimeIndexProbe::Scope { + min: Bound::Included(DataValue::Int32(1)), + max: Bound::Included(DataValue::Int32(4)), + }, + vec![1, 2, 3, 4], + ), + ( + RuntimeIndexProbe::Scope { + min: Bound::Excluded(DataValue::Int32(1)), + max: Bound::Excluded(DataValue::Int32(4)), + }, + vec![2, 3], + ), + (RuntimeIndexProbe::Eq(DataValue::Int32(1)), vec![1]), + ] { + arena.push_runtime_probe(probe); + scan.reset(&mut arena, &mut meta)?; + // An initialized scan is ready now, not at its next pull. + assert!(arena.runtime_probe_stack.is_empty()); + for id in expected { + scan.next_tuple(&mut arena, &mut meta)?; + assert_eq!(arena.result_tuple().values[0], DataValue::Int32(id)); + assert_eq!(scan.iter.as_ref().unwrap() as *const _, iterator); + } + scan.next_tuple(&mut arena, &mut meta)?; + assert_eq!(arena.result.status, Some(ExecStatus::End)); + assert!(arena.runtime_probe_stack.is_empty()); + } + Ok(()) + } +} +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/join/hash_join.rs b/src/execution/dql/join/hash_join.rs index b34405e6..d92f977e 100644 --- a/src/execution/dql/join/hash_join.rs +++ b/src/execution/dql/join/hash_join.rs @@ -251,6 +251,17 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for HashJoin { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for HashJoin { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.state = HashJoinState::Build; + self.bump.reset(); + arena.reset(self.left_input, plan_arena)?; + arena.reset(self.right_input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/join/nested_loop_join.rs b/src/execution/dql/join/nested_loop_join.rs index e31a2d7e..99a2bcd3 100644 --- a/src/execution/dql/join/nested_loop_join.rs +++ b/src/execution/dql/join/nested_loop_join.rs @@ -74,13 +74,12 @@ impl EqualCondition { /// | Right/RightSemi/RightAnti/Full | left | right | /// |--------------------------------|----------------|----------------| /// | Full | left | right | -pub struct NestedLoopJoin<'a> { - right_input_plan: &'a LogicalPlan, +pub struct NestedLoopJoin { ty: JoinType, filter: Option, eq_cond: EqualCondition, left_input: ExecId, - right_pos: ExecId, + right_input: ExecId, state: NestedLoopJoinState, } @@ -108,7 +107,7 @@ struct ActiveLeftState { first_matches: Vec, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for NestedLoopJoin<'a> { +impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for NestedLoopJoin { type Input = (&'a JoinOperator, &'a LogicalPlan, &'a LogicalPlan); fn into_executor( @@ -139,21 +138,29 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for NestedLoopJoin<'a> { }; let left_input = build_read(arena, plan_arena, left_plan, cache, transaction); - let right_pos = arena.nodes.position(); - build_read(arena, plan_arena, right_plan, cache, transaction); + let right_input = build_read(arena, plan_arena, right_plan, cache, transaction); arena.push(ExecNode::NestedLoopJoin(NestedLoopJoin { - right_input_plan: right_plan, ty: join_type, filter, eq_cond, left_input, - right_pos, + right_input, state: NestedLoopJoinState::PullLeft { right_bitmap: None }, })) } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin<'a> { +impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.state = NestedLoopJoinState::PullLeft { right_bitmap: None }; + arena.reset(self.left_input, plan_arena)?; + arena.reset(self.right_input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, @@ -167,7 +174,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin<'a> { if !arena.next_tuple(self.left_input, plan_arena)? { if matches!(self.ty, JoinType::Full) { state = NestedLoopJoinState::EmitRightUnmatched { - right_input: self.build_right_input(arena, plan_arena), + right_input: self.reset_right_input(arena, plan_arena)?, right_bitmap, right_emit_index: 0, }; @@ -182,7 +189,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin<'a> { state = NestedLoopJoinState::ScanRight { active_left: ActiveLeftState { left_tuple, - right_input: self.build_right_input(arena, plan_arena), + right_input: self.reset_right_input(arena, plan_arena)?, right_index: 0, has_matched: false, first_matches: Vec::new(), @@ -321,17 +328,14 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin<'a> { } } -impl<'a> NestedLoopJoin<'a> { - fn build_right_input( +impl NestedLoopJoin { + fn reset_right_input<'a, T: Transaction + 'a, A: MetaArena + 'a>( &mut self, arena: &mut ExecArena<'a, T>, - plan_arena: &mut (dyn MetaArena + 'a), - ) -> ExecId { - let cache = arena.context(); - let transaction = arena.transaction(); - // The same right-hand plan rebuilds the same slots, including nested joins. - arena.nodes.seek(self.right_pos); - build_read(arena, plan_arena, self.right_input_plan, cache, transaction) + plan_arena: &mut ExecMetaArena, + ) -> Result { + arena.reset(self.right_input, plan_arena)?; + Ok(self.right_input) } /// Emit a tuple according to the join type. diff --git a/src/execution/dql/limit.rs b/src/execution/dql/limit.rs index a49a0665..8ac1cce9 100644 --- a/src/execution/dql/limit.rs +++ b/src/execution/dql/limit.rs @@ -50,6 +50,16 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Limit { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Limit { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.skipped = 0; + self.emitted = 0; + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index 47ec37c2..31c7da10 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -38,14 +38,12 @@ type MaterializedRows = SpillVec<'static, Tuple>; #[cfg(not(feature = "spill"))] type MaterializedRows = Vec; -enum RightInput<'a> { - Rebuild { - plan: &'a LogicalPlan, - pos: ExecId, +enum RightInput { + Streaming { + root: ExecId, }, Materialize { - plan: &'a LogicalPlan, - pos: ExecId, + root: ExecId, }, Materializing { root: ExecId, @@ -54,32 +52,26 @@ enum RightInput<'a> { Materialized(MaterializedRows), } -impl<'a> RightInput<'a> { - fn build( +impl RightInput { + fn reset<'a, T: Transaction + 'a, A: MetaArena + 'a>( &self, arena: &mut ExecArena<'a, T>, - plan_arena: &mut (dyn MetaArena + 'a), + plan_arena: &mut ExecMetaArena, probe: Option, - ) -> ExecId { - let (Self::Rebuild { plan, pos } | Self::Materialize { plan, pos }) = self else { - unreachable!("materialized input no longer builds its RHS") + ) -> Result { + let (Self::Streaming { root } | Self::Materialize { root }) = self else { + unreachable!("materialized input replays its rows") }; - arena.nodes.seek(*pos); if let Some(probe) = probe { arena.push_runtime_probe(probe); } - build_read( - arena, - plan_arena, - plan, - arena.context(), - arena.transaction(), - ) + arena.reset(*root, plan_arena)?; + Ok(*root) } // Return the first terminal predicate result, or None after exhausting the RHS. - // The caller supplies Mark semantics; this enum owns reconstruction and replay. - fn scan( + // The caller supplies Mark semantics; this enum owns resetting and replay. + fn scan<'a, T: Transaction + 'a, A: MetaArena + 'a>( self, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, @@ -87,8 +79,8 @@ impl<'a> RightInput<'a> { mut evaluate: impl FnMut(&Tuple, &ExecMetaArena) -> Result, DatabaseError>, ) -> Result<(Self, Option), DatabaseError> { match self { - input @ Self::Rebuild { .. } => { - let root = input.build(arena, plan_arena, probe); + input @ Self::Streaming { .. } => { + let root = input.reset(arena, plan_arena, probe)?; while arena.next_tuple(root, plan_arena)? { if let Some(value) = evaluate(arena.result_tuple(), plan_arena)? { return Ok((input, Some(value))); @@ -97,7 +89,7 @@ impl<'a> RightInput<'a> { Ok((input, None)) } input @ Self::Materialize { .. } => { - let root = input.build(arena, plan_arena, None); + let root = input.reset(arena, plan_arena, None)?; Self::Materializing { root, rows: MaterializedRows::new(), @@ -152,8 +144,9 @@ impl<'a> RightInput<'a> { pub struct MarkApply<'a> { op: &'a MarkApplyOperator, left_input: ExecId, + right_root: ExecId, // Temporarily None while scan owns and transitions the right input. - right_input: Option>, + right_input: Option, // Retain a streaming inner input across next_tuple calls, not its result rows. join_input: Option<(ExecId, Tuple)>, } @@ -169,22 +162,16 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for MarkApply<'a> { transaction: &T, ) -> ExecId { let left_input = build_read(arena, plan_arena, left_input, cache, transaction); - let pos = arena.nodes.position(); - build_read(arena, plan_arena, right_input, cache, transaction); + let right_root = build_read(arena, plan_arena, right_input, cache, transaction); let right_input = if op.materialize { - RightInput::Materialize { - plan: right_input, - pos, - } + RightInput::Materialize { root: right_root } } else { - RightInput::Rebuild { - plan: right_input, - pos, - } + RightInput::Streaming { root: right_root } }; arena.push(ExecNode::MarkApply(Self { op, left_input, + right_root, right_input: Some(right_input), join_input: None, })) @@ -192,6 +179,24 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for MarkApply<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.join_input = None; + self.right_input = Some(if self.op.materialize { + RightInput::Materialize { + root: self.right_root, + } + } else { + RightInput::Streaming { + root: self.right_root, + } + }); + arena.reset(self.left_input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, @@ -230,7 +235,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { impl<'a> MarkApply<'a> { fn next_join_tuple( &mut self, - input: &RightInput<'a>, + input: &RightInput, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { @@ -256,7 +261,7 @@ impl<'a> MarkApply<'a> { } let left = arena.materialize_tuple(); let value = self.parameterized_probe_value(&left, plan_arena)?; - let root = input.build(arena, plan_arena, self.runtime_probe_for(value)); + let root = input.reset(arena, plan_arena, self.runtime_probe_for(value))?; self.join_input = Some((root, left)); } } @@ -297,11 +302,11 @@ impl<'a> MarkApply<'a> { fn mark_value( &self, - input: RightInput<'a>, + input: RightInput, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, left_tuple: &Tuple, - ) -> Result<(RightInput<'a>, DataValue), DatabaseError> { + ) -> Result<(RightInput, DataValue), DatabaseError> { let probe = self.parameterized_probe_value(left_tuple, plan_arena)?; match self.op.kind { MarkApplyKind::InnerJoin => unreachable!("inner join streams tuples"), @@ -360,13 +365,13 @@ impl<'a> MarkApply<'a> { fn scan_quantified_right_input( &self, - input: RightInput<'a>, + input: RightInput, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, probe: Option, quantifier: MarkApplyQuantifier, left_tuple: &Tuple, - ) -> Result<(RightInput<'a>, DataValue), DatabaseError> { + ) -> Result<(RightInput, DataValue), DatabaseError> { let mut saw_null = false; let predicates = self.op.predicates(); let runtime_probe = self.runtime_probe_for(probe); @@ -832,12 +837,12 @@ mod tests { &transaction, ); + let cache = arena.context(); + let right_root = build_read(&mut arena, &mut plan_arena, &right, cache, &transaction); let mut exec: MarkApply = MarkApply { + right_root, op: &op, - right_input: Some(RightInput::Rebuild { - plan: &right, - pos: arena.nodes.position(), - }), + right_input: Some(RightInput::Streaming { root: right_root }), left_input: 0, join_input: None, }; @@ -890,12 +895,12 @@ mod tests { &transaction, ); + let cache = arena.context(); + let right_root = build_read(&mut arena, &mut plan_arena, &right, cache, &transaction); let mut exec: MarkApply = MarkApply { + right_root, op: &op, - right_input: Some(RightInput::Rebuild { - plan: &right, - pos: arena.nodes.position(), - }), + right_input: Some(RightInput::Streaming { root: right_root }), left_input: 0, join_input: None, }; @@ -948,12 +953,12 @@ mod tests { &transaction, ); + let cache = arena.context(); + let right_root = build_read(&mut arena, &mut plan_arena, &right, cache, &transaction); let mut exec: MarkApply = MarkApply { + right_root, op: &op, - right_input: Some(RightInput::Rebuild { - plan: &right, - pos: arena.nodes.position(), - }), + right_input: Some(RightInput::Streaming { root: right_root }), left_input: 0, join_input: None, }; @@ -1084,14 +1089,14 @@ mod tests { let mut arena = ExecArena::with_capacity(0); arena.init_context(cache, &transaction); let left_input = build_read(&mut arena, &mut meta, &left, cache, &transaction); - let pos = arena.nodes.position(); - build_read(&mut arena, &mut meta, &right, cache, &transaction); + let right_root = build_read(&mut arena, &mut meta, &right, cache, &transaction); let right_input = if op.materialize { - RightInput::Materialize { plan: &right, pos } + RightInput::Materialize { root: right_root } } else { - RightInput::Rebuild { plan: &right, pos } + RightInput::Streaming { root: right_root } }; let mut exec = MarkApply { + right_root, op: &op, left_input, right_input: Some(right_input), @@ -1107,7 +1112,7 @@ mod tests { ); assert!(matches!( exec.right_input, - Some(RightInput::Rebuild { .. } | RightInput::Materialize { .. }) + Some(RightInput::Streaming { .. } | RightInput::Materialize { .. }) )); exec.next_tuple(&mut arena, &mut meta)?; assert!(exec.right_input.is_none()); @@ -1168,14 +1173,14 @@ mod tests { let mut arena = ExecArena::with_capacity(0); arena.init_context(cache, &transaction); let left_input = build_read(&mut arena, &mut meta, &left, cache, &transaction); - let pos = arena.nodes.position(); - build_read(&mut arena, &mut meta, &right, cache, &transaction); + let right_root = build_read(&mut arena, &mut meta, &right, cache, &transaction); let right_input = if materialize { - RightInput::Materialize { plan: &right, pos } + RightInput::Materialize { root: right_root } } else { - RightInput::Rebuild { plan: &right, pos } + RightInput::Streaming { root: right_root } }; let mut exec = MarkApply { + right_root, op: &op, left_input, right_input: Some(right_input), @@ -1216,7 +1221,10 @@ mod tests { )); } } else { - assert!(matches!(exec.right_input, Some(RightInput::Rebuild { .. }))); + assert!(matches!( + exec.right_input, + Some(RightInput::Streaming { .. }) + )); } } exec.next_tuple(&mut arena, &mut meta)?; @@ -1257,11 +1265,11 @@ mod tests { let mut arena = ExecArena::with_capacity(0); arena.init_context(cache, &transaction); let left_input = build_read(&mut arena, &mut meta, &left, cache, &transaction); - let pos = arena.nodes.position(); - build_read(&mut arena, &mut meta, &right, cache, &transaction); + let right_root = build_read(&mut arena, &mut meta, &right, cache, &transaction); let mut exec = MarkApply { + right_root, op: &op, - right_input: Some(RightInput::Materialize { plan: &right, pos }), + right_input: Some(RightInput::Materialize { root: right_root }), left_input, join_input: None, }; @@ -1308,7 +1316,11 @@ mod tests { } } // An empty RHS completes immediately, including for a NULL left value. - exec.right_input = Some(RightInput::Materialize { plan: &empty, pos }); + let cache = arena.context(); + exec.right_root = build_read(&mut arena, &mut meta, &empty, cache, &transaction); + exec.right_input = Some(RightInput::Materialize { + root: exec.right_root, + }); let input = exec.right_input.take().unwrap(); let (input, value) = exec.mark_value( input, diff --git a/src/execution/dql/projection.rs b/src/execution/dql/projection.rs index 5c9f03a3..eadf834b 100644 --- a/src/execution/dql/projection.rs +++ b/src/execution/dql/projection.rs @@ -44,6 +44,14 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Projection<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Projection<'a> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/recursive_cte.rs b/src/execution/dql/recursive_cte.rs index eb8c06c2..48ccb759 100644 --- a/src/execution/dql/recursive_cte.rs +++ b/src/execution/dql/recursive_cte.rs @@ -30,11 +30,17 @@ use crate::types::tuple::Tuple; use std::mem; pub(crate) enum RecursiveInput { - One(Option), + One { + tuple: Tuple, + emitted: bool, + }, #[cfg(feature = "spill")] Many(SpillReader), #[cfg(not(feature = "spill"))] - Many(std::vec::IntoIter), + Many { + rows: Vec, + position: usize, + }, } impl Iterator for RecursiveInput { @@ -42,12 +48,36 @@ impl Iterator for RecursiveInput { fn next(&mut self) -> Option { match self { - Self::One(tuple) => tuple.take().map(Ok), + Self::One { tuple, emitted } => { + if *emitted { + None + } else { + *emitted = true; + Some(Ok(tuple.clone())) + } + } #[cfg(feature = "spill")] Self::Many(rows) => rows.next(), #[cfg(not(feature = "spill"))] - Self::Many(rows) => rows.next().map(Ok), + Self::Many { rows, position } => { + let row = rows.get(*position).cloned(); + *position += usize::from(row.is_some()); + row.map(Ok) + } + } + } +} + +impl RecursiveInput { + fn reset(&mut self) -> Result<(), DatabaseError> { + match self { + Self::One { emitted, .. } => *emitted = false, + #[cfg(feature = "spill")] + Self::Many(rows) => rows.reset()?, + #[cfg(not(feature = "spill"))] + Self::Many { position, .. } => *position = 0, } + Ok(()) } } @@ -157,14 +187,17 @@ impl RecursiveRows { fn into_input(self) -> Result, DatabaseError> { match self { Self::Empty => Ok(None), - Self::One { tuple, .. } => Ok(Some(RecursiveInput::One(Some(tuple)))), + Self::One { tuple, .. } => Ok(Some(RecursiveInput::One { + tuple, + emitted: false, + })), #[cfg(feature = "spill")] Self::Reading(mut reader) => { reader.reset()?; Ok(Some(RecursiveInput::Many(reader))) } #[cfg(not(feature = "spill"))] - Self::Reading { rows, .. } => Ok(Some(RecursiveInput::Many(rows.into_iter()))), + Self::Reading { rows, .. } => Ok(Some(RecursiveInput::Many { rows, position: 0 })), Self::Writing(_) => unreachable!("recursive generation must be finished first"), } } @@ -248,6 +281,18 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for RecursiveCte<'a, T> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for RecursiveCte<'a, T> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.working = RecursiveRows::default(); + self.next = RecursiveRows::default(); + self.phase = RecursivePhase::Anchor; + self.recursive_arena.reset_for_rebuild(); + arena.reset(self.anchor_input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, @@ -309,6 +354,14 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for RecursiveScan { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for RecursiveScan { + fn reset( + &mut self, + _arena: &mut ExecArena<'a, T>, + _plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.input.reset() + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/scalar_query_init.rs b/src/execution/dql/scalar_query_init.rs index 449b870d..433cbca3 100644 --- a/src/execution/dql/scalar_query_init.rs +++ b/src/execution/dql/scalar_query_init.rs @@ -39,8 +39,6 @@ pub struct ScalarQueryInit<'a> { param_bindings: &'a [(ScalarQueryRef, ExprRef)], scratch_tuple: Tuple, - init_plan: &'a LogicalPlan, - init_pos: ExecId, init: ExecId, } @@ -72,7 +70,6 @@ impl<'a> ScalarQueryInit<'a> { cache: ExecutionContext<'_>, transaction: &T, ) -> ExecId { - let init_pos = arena.nodes.position(); let init = build_read(arena, plan_arena, init_plan, cache, transaction); arena.push(ExecNode::ScalarQueryInit(Self { input, @@ -88,8 +85,6 @@ impl<'a> ScalarQueryInit<'a> { }, param_bindings: &op.param_bindings, scratch_tuple: Tuple::default(), - init_plan, - init_pos, })) } } @@ -114,6 +109,20 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ScalarQueryInit<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ScalarQueryInit<'a> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.state = if self.reference.is_outer_value { + ScalarQueryInitState::ReadOuter + } else { + ScalarQueryInitState::Initialize + }; + self.scratch_tuple.clear(); + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, @@ -123,6 +132,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ScalarQueryInit<'a> { match self.state { ScalarQueryInitState::Initialize => { if plan_arena.init_value(self.reference).is_none() { + arena.reset(self.init, plan_arena)?; self.evaluate_init(arena, plan_arena)?; } self.state = ScalarQueryInitState::ReadInput; @@ -150,16 +160,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ScalarQueryInit<'a> { self.state = ScalarQueryInitState::EvaluateOuter; } ScalarQueryInitState::EvaluateOuter => { - let previous = arena.nodes.position(); - arena.nodes.seek(self.init_pos); - self.init = build_read( - arena, - plan_arena, - self.init_plan, - arena.context(), - arena.transaction(), - ); - arena.nodes.seek(previous); + arena.reset(self.init, plan_arena)?; self.evaluate_init(arena, plan_arena)?; std::mem::swap(arena.result_tuple_mut(), &mut self.scratch_tuple); self.state = ScalarQueryInitState::ReadOuter; diff --git a/src/execution/dql/scalar_subquery.rs b/src/execution/dql/scalar_subquery.rs index 98d060a4..a4ef96d4 100644 --- a/src/execution/dql/scalar_subquery.rs +++ b/src/execution/dql/scalar_subquery.rs @@ -49,6 +49,15 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ScalarSubquery { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ScalarSubquery { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.returned = false; + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/seq_scan.rs b/src/execution/dql/seq_scan.rs index 2e237084..d351d09f 100644 --- a/src/execution/dql/seq_scan.rs +++ b/src/execution/dql/seq_scan.rs @@ -38,6 +38,23 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for SeqScan<'a, T> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for SeqScan<'a, T> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + if let Some(iter) = &mut self.iter { + let state = arena.local_state(plan_arena); + iter.reset( + state.transaction(), + state.table_codec, + &self.op.table_name, + self.op.limit, + )?; + } + Ok(()) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, @@ -74,3 +91,83 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for SeqScan<'a, T> { Ok(()) } } + +// GRCOV_EXCL_START +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::*; + use crate::db::test::build_table; + use crate::execution::empty_context; + use crate::planner::{PlanArena, TableArenaCell}; + use crate::storage::memory::MemoryStorage; + use crate::storage::table_codec::TableCodec; + use crate::storage::{StatisticsMetaCache, Storage, TableCache, ViewCache}; + use crate::types::tuple::Tuple; + use crate::types::value::DataValue; + use crate::types::LogicalType; + + #[test] + fn reset_keeps_seq_iterator_and_restores_offset_limit() -> Result<(), DatabaseError> { + let storage = MemoryStorage::new(); + let mut tx = storage.transaction()?; + let mut tables = TableCache::default(); + let cell = TableArenaCell::default(); + let mut meta = PlanArena::new(&cell); + build_table(&mut tables, &mut tx, &mut meta)?; + let meta = PlanArena::new(&cell); + let serializers = [ + LogicalType::Integer.serializable(), + LogicalType::Boolean.serializable(), + LogicalType::Integer.serializable(), + ]; + let mut codec = TableCodec::default(); + for id in 1..=4 { + tx.append_tuple( + &mut codec, + "t1", + &Tuple::new( + Some(DataValue::Int32(id)), + vec![ + DataValue::Int32(id), + DataValue::Boolean(true), + DataValue::Int32(id * 10), + ], + ), + &serializers, + false, + )?; + } + let op = TableScanOperator { + table_name: "t1".into(), + columns: tables.get("t1").unwrap().columns().copied().collect(), + limit: (Some(1), Some(2)), + index_infos: vec![], + with_pk: true, + }; + let views = ViewCache::default(); + let statistics = StatisticsMetaCache::default(); + let mut arena = ExecArena::with_capacity(0); + arena.init_context(empty_context(&tables, &views, &statistics), &tx); + let mut meta = ExecMetaArena::new(meta); + let mut scan = SeqScan { + op: &op, + iter: None, + }; + scan.next_tuple(&mut arena, &mut meta)?; + assert_eq!(arena.result_tuple().values[0], DataValue::Int32(2)); + let iterator = scan.iter.as_ref().unwrap() as *const _; + for _ in 0..3 { + scan.reset(&mut arena, &mut meta)?; + scan.reset(&mut arena, &mut meta)?; + for id in [2, 3] { + scan.next_tuple(&mut arena, &mut meta)?; + assert_eq!(arena.result_tuple().values[0], DataValue::Int32(id)); + assert_eq!(scan.iter.as_ref().unwrap() as *const _, iterator); + } + scan.next_tuple(&mut arena, &mut meta)?; + assert_eq!(arena.result.status, Some(crate::execution::ExecStatus::End)); + } + Ok(()) + } +} +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/set_membership.rs b/src/execution/dql/set_membership.rs index a83a8679..4482d77b 100644 --- a/src/execution/dql/set_membership.rs +++ b/src/execution/dql/set_membership.rs @@ -54,6 +54,17 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for SetMembership { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for SetMembership { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.built = false; + self.right_counts.clear(); + arena.reset(self.left_input, plan_arena)?; + arena.reset(self.right_input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/show_table.rs b/src/execution/dql/show_table.rs index ebd617ad..5643b56e 100644 --- a/src/execution/dql/show_table.rs +++ b/src/execution/dql/show_table.rs @@ -38,6 +38,15 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ShowTables<'a, T> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ShowTables<'a, T> { + fn reset( + &mut self, + _arena: &mut ExecArena<'a, T>, + _plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.metas = None; + Ok(()) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/show_view.rs b/src/execution/dql/show_view.rs index dfeef23d..e90eb79d 100644 --- a/src/execution/dql/show_view.rs +++ b/src/execution/dql/show_view.rs @@ -38,6 +38,15 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ShowViews<'a, T> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ShowViews<'a, T> { + fn reset( + &mut self, + _arena: &mut ExecArena<'a, T>, + _plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.metas = None; + Ok(()) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/sort.rs b/src/execution/dql/sort.rs index cec626f6..867ac6b3 100644 --- a/src/execution/dql/sort.rs +++ b/src/execution/dql/sort.rs @@ -49,6 +49,10 @@ impl<'a, T> NullableVec<'a, T> { self.0.len() } + pub(crate) fn clear(&mut self) { + while self.pop().is_some() {} + } + #[inline] pub(crate) fn pop(&mut self) -> Option { self.0.pop().map(|item| unsafe { item.assume_init() }) @@ -57,7 +61,7 @@ impl<'a, T> NullableVec<'a, T> { impl Drop for NullableVec<'_, T> { fn drop(&mut self) { - while self.pop().is_some() {} + self.clear(); } } @@ -184,6 +188,15 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Sort<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Sort<'a> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.rows.clear(); + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, @@ -303,6 +316,11 @@ mod test { &transaction, ); let mut metadata = ExecMetaArena::new(metadata); + let nodes = arena.nodes.items.len(); + assert!(arena.next_tuple(root, &mut metadata)?); + assert_eq!(arena.result_tuple().values, vec![DataValue::Int32(1)]); + arena.reset(root, &mut metadata)?; + assert_eq!(arena.nodes.items.len(), nodes); for expected in [1, 2, 3] { assert!(arena.next_tuple(root, &mut metadata)?); assert_eq!( @@ -322,11 +340,25 @@ mod test { ); } assert!(!arena.next_tuple(root, &mut metadata)?); + arena.reset(root, &mut metadata)?; + for expected in [1, 2, 3] { + assert!(arena.next_tuple(root, &mut metadata)?); + assert_eq!( + arena.result_tuple().values, + vec![DataValue::Int32(expected)] + ); + assert_eq!( + metadata.init_value(result), + Some(&DataValue::Int32(expected)) + ); + } + assert!(!arena.next_tuple(root, &mut metadata)?); + assert_eq!(arena.nodes.items.len(), nodes); Ok(()) } #[test] - fn nullable_vec_drops_values() { + fn nullable_vec_clear_and_drop_release_values_without_reallocating() { struct DropValue<'a>(&'a Cell); impl Drop for DropValue<'_> { @@ -341,8 +373,17 @@ mod test { let mut values = NullableVec::new(&arena); values.put(DropValue(&dropped)); values.put(DropValue(&dropped)); + let allocation = (values.0.as_ptr(), values.0.capacity()); + values.clear(); + assert_eq!(dropped.get(), 2); + assert_eq!(values.len(), 0); + assert_eq!((values.0.as_ptr(), values.0.capacity()), allocation); + values.clear(); + assert_eq!(dropped.get(), 2); + values.put(DropValue(&dropped)); + assert_eq!((values.0.as_ptr(), values.0.capacity()), allocation); } - assert_eq!(dropped.get(), 2); + assert_eq!(dropped.get(), 3); } fn sorted_rows<'a>( diff --git a/src/execution/dql/top_k.rs b/src/execution/dql/top_k.rs index ac925004..0c319ee7 100644 --- a/src/execution/dql/top_k.rs +++ b/src/execution/dql/top_k.rs @@ -147,6 +147,16 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for TopK<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for TopK<'a> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.output = None; + self.arena.reset(); + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/union.rs b/src/execution/dql/union.rs index 1d409344..1891dfd6 100644 --- a/src/execution/dql/union.rs +++ b/src/execution/dql/union.rs @@ -46,6 +46,16 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Union { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Union { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.reading_left = true; + arena.reset(self.left_input, plan_arena)?; + arena.reset(self.right_input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/values.rs b/src/execution/dql/values.rs index 9d75aa43..50b0558d 100644 --- a/src/execution/dql/values.rs +++ b/src/execution/dql/values.rs @@ -18,27 +18,20 @@ use crate::planner::operator::values::ValuesOperator; use crate::planner::ExprRef; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::Transaction; -use crate::types::tuple::Schema; use crate::types::value::DataValue; pub struct Values<'a> { + op: &'a ValuesOperator, rows: std::slice::Iter<'a, ExprRef>, remaining_rows: usize, - schema_ref: &'a Schema, } impl<'a> From<&'a ValuesOperator> for Values<'a> { - fn from( - ValuesOperator { - rows, - row_count, - schema_ref, - }: &'a ValuesOperator, - ) -> Self { + fn from(op: &'a ValuesOperator) -> Self { Values { - rows: rows.iter(), - remaining_rows: *row_count, - schema_ref, + op, + rows: op.rows.iter(), + remaining_rows: op.row_count, } } } @@ -59,6 +52,16 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Values<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Values<'a> { + fn reset( + &mut self, + _arena: &mut ExecArena<'a, T>, + _plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.rows = self.op.rows.iter(); + self.remaining_rows = self.op.row_count; + Ok(()) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, @@ -69,13 +72,13 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Values<'a> { return Ok(()); } self.remaining_rows -= 1; - let width = self.schema_ref.len(); + let width = self.op.schema_ref.len(); let output = arena.result_tuple_mut(); output.pk = None; output.values.clear(); for (i, expr) in self.rows.by_ref().take(width).enumerate() { - let ty = plan_arena.column(self.schema_ref[i]).datatype(); + let ty = plan_arena.column(self.op.schema_ref[i]).datatype(); output.values.push( plan_arena .expression(*expr) diff --git a/src/execution/dql/window.rs b/src/execution/dql/window.rs index 32cc9aa1..f8b51908 100644 --- a/src/execution/dql/window.rs +++ b/src/execution/dql/window.rs @@ -54,6 +54,14 @@ struct WindowState { } impl WindowState { + fn clear(&mut self) { + self.buffered.clear(); + self.pending = None; + self.sort_values.clear(); + self.started = false; + self.begin_partition(); + } + fn begin_partition(&mut self) { self.partition_rows = 0; self.peer_start = 0; @@ -219,6 +227,17 @@ impl<'a> Window<'a> { } impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Window<'a> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + self.state.clear(); + self.input_exhausted = false; + self.reset_functions()?; + arena.reset(self.input, plan_arena) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, @@ -291,6 +310,52 @@ mod tests { Tuple::new(None, values.iter().copied().map(DataValue::from).collect()) } + #[test] + fn clear_restores_window_state_and_keeps_buffer_capacity() { + let mut state = WindowState { + buffered: Vec::with_capacity(8), + pending: Some((tuple(&[2, 20]), Boundary::Peer)), + sort_values: Vec::with_capacity(4), + started: true, + partition_rows: 5, + peer_start: 3, + peer_index: 2, + }; + state.buffered.push((0, tuple(&[1, 10]))); + state.sort_values.push(DataValue::Int32(10)); + let buffered = (state.buffered.as_ptr(), state.buffered.capacity()); + let keys = (state.sort_values.as_ptr(), state.sort_values.capacity()); + for _ in 0..2 { + state.clear(); + assert!(state.buffered.is_empty()); + assert!(state.pending.is_none()); + assert!(state.sort_values.is_empty()); + assert!(!state.started); + assert_eq!( + (state.partition_rows, state.peer_start, state.peer_index), + (0, 0, 0) + ); + assert_eq!( + (state.buffered.as_ptr(), state.buffered.capacity()), + buffered + ); + assert_eq!( + (state.sort_values.as_ptr(), state.sort_values.capacity()), + keys + ); + } + state.buffered.push((0, tuple(&[3, 30]))); + state.sort_values.push(DataValue::Int32(30)); + assert_eq!( + (state.buffered.as_ptr(), state.buffered.capacity()), + buffered + ); + assert_eq!( + (state.sort_values.as_ptr(), state.sort_values.capacity()), + keys + ); + } + #[test] fn row_materialization_streams_rows() -> Result<(), DatabaseError> { let table_arena = crate::planner::TableArenaCell::default(); diff --git a/src/execution/mod.rs b/src/execution/mod.rs index 64c29a74..cc784c71 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -226,7 +226,7 @@ pub(crate) enum ExecNode<'a, T: Transaction + 'a> { Insert(Insert<'a>), Limit(Limit), MarkApply(MarkApply<'a>), - NestedLoopJoin(NestedLoopJoin<'a>), + NestedLoopJoin(NestedLoopJoin), Projection(Projection<'a>), RecursiveCte(RecursiveCte<'a, T>), RecursiveScan(RecursiveScan), @@ -249,6 +249,17 @@ pub(crate) enum ExecNode<'a, T: Transaction + 'a> { } pub(crate) trait ExecutorNode<'a, T: Transaction + 'a>: Sized { + // Reads restore their local state and reset inputs. Writes must not be replayed. + fn reset( + &mut self, + _arena: &mut ExecArena<'a, T>, + _plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + Err(DatabaseError::UnsupportedStmt( + "resetting a write executor".into(), + )) + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, @@ -257,6 +268,147 @@ pub(crate) trait ExecutorNode<'a, T: Transaction + 'a>: Sized { } impl<'a, T: Transaction + 'a> ExecNode<'a, T> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + match self { + ExecNode::AddColumn(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::Analyze(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::ChangeColumn(exec) => { + >::reset(exec, arena, plan_arena) + } + #[cfg(feature = "copy")] + ExecNode::CopyFromFile(exec) => { + >::reset(exec, arena, plan_arena) + } + #[cfg(feature = "copy")] + ExecNode::CopyToFile(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::CreateIndex(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::CreateTable(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::CreateView(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::Delete(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::Describe(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::DropColumn(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::DropIndex(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::DropTable(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::DropView(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::Dummy(exec) => >::reset(exec, arena, plan_arena), + ExecNode::Explain(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + #[cfg(feature = "spill")] + ExecNode::ExternalSort(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::Filter(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::FunctionScan(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::HashAgg(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::HashJoin(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::IndexScan(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::Insert(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::Limit(exec) => >::reset(exec, arena, plan_arena), + ExecNode::MarkApply(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::NestedLoopJoin(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::Projection(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::RecursiveCte(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::RecursiveScan(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::ScalarQueryInit(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::ScalarSubquery(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::SetMembership(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::SeqScan(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::ShowTables(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::ShowViews(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::SimpleAgg(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::Sort(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::StreamAgg(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::StreamDistinct(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::TopK(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::Truncate(exec) => { + >::reset(exec, arena, plan_arena) + } + ExecNode::Union(exec) => >::reset(exec, arena, plan_arena), + ExecNode::Update(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::Values(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + ExecNode::Window(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } + } + } + fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, @@ -342,7 +494,7 @@ impl<'a, T: Transaction + 'a> ExecNode<'a, T> { as ExecutorNode<'a, T>>::next_tuple(exec, arena, plan_arena) } ExecNode::NestedLoopJoin(exec) => { - as ExecutorNode<'a, T>>::next_tuple(exec, arena, plan_arena) + >::next_tuple(exec, arena, plan_arena) } ExecNode::Projection(exec) => { as ExecutorNode<'a, T>>::next_tuple(exec, arena, plan_arena) @@ -414,10 +566,7 @@ struct ExecNodes<'a, T: Transaction + 'a> { } impl<'a, T: Transaction + 'a> ExecNodes<'a, T> { - fn position(&self) -> ExecId { - self.pos - } - + #[cfg(test)] fn seek(&mut self, pos: ExecId) { assert!(pos <= self.items.len(), "node position out of bounds"); self.pos = pos; @@ -690,6 +839,20 @@ impl<'a, T: Transaction + 'a> ExecArena<'a, T> { self.resume(); } + pub(crate) fn reset( + &mut self, + id: ExecId, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + let slot = &self.nodes.items[id] as *const std::cell::RefCell>; + self.nodes.executing += 1; + // SAFETY: the same slot/RefCell invariant as next_tuple. Reset may recurse + // into inputs, but cannot relocate slots or overwrite an active node. + let result = unsafe { (&*slot).borrow_mut().reset(self, plan_arena) }; + self.nodes.executing -= 1; + result + } + pub(crate) fn next_tuple( &mut self, id: ExecId, @@ -817,7 +980,7 @@ where if use_hash_join { read!(HashJoin, (op, left, right)) } else { - read!(NestedLoopJoin<'a>, (op, left, right)) + read!(NestedLoopJoin, (op, left, right)) } } Operator::Project(op) => read!(Projection<'a>, (op, plan.childrens.only())), diff --git a/src/storage/mod.rs b/src/storage/mod.rs index a7c7a0e5..f300b384 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1979,6 +1979,22 @@ pub struct TupleIter<'a, T: Transaction + 'a> { iter: T::IterType<'a>, } +impl<'a, T: Transaction + 'a> TupleIter<'a, T> { + pub(crate) fn reset( + &mut self, + tx: &'a T, + table_codec: &mut TableCodec, + table_name: &TableName, + bounds: Bounds, + ) -> Result<(), DatabaseError> { + self.iter = table_codec.with_tuple_bound(table_name, |min, max| { + tx.range(Bound::Included(min), Bound::Included(max)) + })?; + self.bounds = IterBounds::new(bounds.0.unwrap_or(0), bounds.1); + Ok(()) + } +} + impl<'a, T: Transaction + 'a> Iter for TupleIter<'a, T> { fn next_tuple_into( &mut self, @@ -2132,6 +2148,15 @@ pub enum IndexIterState<'a, T: Transaction + 'a> { Over, } +impl<'a, T: Transaction> IndexIter<'a, T> { + pub(crate) fn reset(&mut self, ranges: IndexRanges<'a>, bounds: Bounds) { + self.state = IndexIterState::Init; + self.params.reverse = ranges.reverse; + self.ranges = ranges; + self.bounds = IterBounds::new(bounds.0.unwrap_or(0), bounds.1); + } +} + impl Iter for IndexIter<'_, T> { fn next_tuple_into( &mut self, diff --git a/src/types/tuple.rs b/src/types/tuple.rs index da734600..64e97e04 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -193,6 +193,11 @@ impl<'a> From<&'a Tuple> for &'a [DataValue] { } impl Tuple { + pub fn clear(&mut self) { + self.pk = None; + self.values.clear(); + } + pub fn new(pk: Option, values: Vec) -> Self { Tuple { pk, values } } @@ -313,6 +318,23 @@ mod tests { } } + #[test] + fn clear_removes_pk_and_values_without_reallocating() { + let mut values = Vec::with_capacity(8); + values.push(DataValue::Int32(1)); + values.push(DataValue::Null); + let mut tuple = Tuple::new(Some(DataValue::Int32(1)), values); + let allocation = (tuple.values.as_ptr(), tuple.values.capacity()); + for _ in 0..2 { + tuple.clear(); + assert!(tuple.pk.is_none()); + assert!(tuple.values.is_empty()); + assert_eq!((tuple.values.as_ptr(), tuple.values.capacity()), allocation); + } + tuple.values.push(DataValue::Int32(2)); + assert_eq!((tuple.values.as_ptr(), tuple.values.capacity()), allocation); + } + #[test] fn test_tuple_serialize_to_and_deserialize_from() { let columns = Arc::new(vec![ From 9544379955d4d98693dd3ec3884eec4d31248442 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 17/62] perf: collapse projects after join orientation --- src/db.rs | 218 ++++++++++++++++-- src/execution/dql/aggregate/hash_agg.rs | 2 +- .../dql/aggregate/stream_distinct.rs | 2 +- src/execution/dql/join/hash_join.rs | 2 +- src/execution/dql/join/nested_loop_join.rs | 2 +- src/expression/range_detacher.rs | 2 +- src/optimizer/heuristic/batch.rs | 4 +- src/optimizer/heuristic/optimizer.rs | 20 +- .../rule/normalization/column_pruning.rs | 4 +- .../rule/normalization/combine_operators.rs | 8 +- .../rule/normalization/join_reorder.rs | 2 +- .../rule/normalization/pushdown_limit.rs | 6 +- .../rule/normalization/pushdown_predicates.rs | 22 +- .../rule/normalization/simplification.rs | 6 +- 14 files changed, 237 insertions(+), 63 deletions(-) diff --git a/src/db.rs b/src/db.rs index 7af04f49..708ffcc9 100644 --- a/src/db.rs +++ b/src/db.rs @@ -443,22 +443,22 @@ impl DataBaseBuilder { fn optimizer_pipeline() -> HepOptimizerPipeline { HepOptimizerPipeline::builder() .before_batch( - "Column Pruning".to_string(), + "Column Pruning", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::ColumnPruning], ) .before_batch( - "Simplify Filter".to_string(), + "Simplify Filter", HepBatchStrategy::fix_point_topdown(10), vec![NormalizationRuleImpl::SimplifyFilter], ) .before_batch( - "Constant Calculation".to_string(), + "Constant Calculation", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::ConstantCalculation], ) .before_batch( - "Predicate Pushdown".to_string(), + "Predicate Pushdown", HepBatchStrategy::fix_point_topdown(30), vec![ NormalizationRuleImpl::PushPredicateThroughJoin, @@ -466,7 +466,7 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { ], ) .before_batch( - "Limit Pushdown".to_string(), + "Limit Pushdown", HepBatchStrategy::fix_point_topdown(10), vec![ NormalizationRuleImpl::LimitProjectTranspose, @@ -474,7 +474,7 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { ], ) .before_batch( - "TopK".to_string(), + "TopK", HepBatchStrategy::fix_point_topdown(1), vec![ NormalizationRuleImpl::MinMaxToTopK, @@ -482,31 +482,30 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { ], ) .before_batch( - "Combine Operators".to_string(), + "Combine Operators", HepBatchStrategy::fix_point_topdown(10), vec![ - NormalizationRuleImpl::CollapseProject, NormalizationRuleImpl::CollapseGroupByAgg, NormalizationRuleImpl::CombineFilter, ], ) .before_batch( - "Predicate Into Scan".to_string(), + "Predicate Into Scan", HepBatchStrategy::fix_point_topdown(10), vec![NormalizationRuleImpl::PushPredicateIntoScan], ) .before_batch( - "Guard Mark Apply".to_string(), + "Guard Mark Apply", HepBatchStrategy::fix_point_topdown(1), vec![NormalizationRuleImpl::GuardMarkApply], ) .after_batch( - "Greedy Join Reorder".to_string(), + "Greedy Join Reorder", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::GreedyJoinReorder], ) .after_batch( - "Parameterize Mark Apply".to_string(), + "Parameterize Mark Apply", HepBatchStrategy::fix_point_topdown(1), vec![ NormalizationRuleImpl::ParameterizeMarkApply, @@ -516,27 +515,32 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { // Only discard predicates after parameterization has chosen the final // lookup. A Probe must not inherit residuals from a replaced static range. .after_batch( - "Eliminate Index Filter".to_string(), + "Eliminate Index Filter", HepBatchStrategy::fix_point_topdown(1), vec![NormalizationRuleImpl::EliminateIndexFilter], ) .after_batch( - "Materialize Mark Apply".to_string(), + "Materialize Mark Apply", HepBatchStrategy::fix_point_bottomup(1), vec![NormalizationRuleImpl::MaterializeMarkApply], ) .after_batch( - "Orient Nested Loop Join".to_string(), + "Orient Nested Loop Join", HepBatchStrategy::fix_point_bottomup(1), vec![NormalizationRuleImpl::OrientNestedLoopJoin], ) .after_batch( - "Expression Remapper".to_string(), + "Collapse Project", + HepBatchStrategy::fix_point_topdown(10), + vec![NormalizationRuleImpl::CollapseProject], + ) + .after_batch( + "Expression Remapper", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::EvaluatorBind], ) .after_batch( - "Limit Into Scan".to_string(), + "Limit Into Scan", HepBatchStrategy::fix_point_topdown(10), vec![NormalizationRuleImpl::PushLimitIntoTableScan], ) @@ -1535,14 +1539,20 @@ impl ResultIter for TransactionIter<'_, T, A> { pub(crate) mod test { use crate::binder::{Binder, BinderContext}; use crate::catalog::{ColumnCatalog, ColumnDesc}; - use crate::db::{CatalogKind, DataBaseBuilder, DatabaseError, ResultIter}; - use crate::expression::ScalarExpression; - use crate::planner::operator::join::JoinCondition; + use crate::db::{optimizer_pipeline, CatalogKind, DataBaseBuilder, DatabaseError, ResultIter}; + use crate::execution::dql::projection::Projection; + use crate::execution::{empty_context, execute_input}; + use crate::expression::{AliasType, BinaryOperator, ScalarExpression}; + use crate::optimizer::core::statistics_meta::StatisticMetaLoader; + use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; + use crate::planner::operator::project::ProjectOperator; + use crate::planner::operator::values::ValuesOperator; use crate::planner::operator::Operator; - use crate::planner::PlanArena; + use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena, TableArenaCell}; use crate::storage::memory::MemoryStorage; use crate::storage::{ - table_codec::TableCodec, Storage, TableCache, Transaction, TransactionIsolationLevel, + table_codec::TableCodec, StatisticsMetaCache, Storage, TableCache, Transaction, + TransactionIsolationLevel, ViewCache, }; use crate::types::tuple::Tuple; use crate::types::value::DataValue; @@ -1561,6 +1571,170 @@ pub(crate) mod test { use std::time::Duration; use tempfile::TempDir; + fn literal_input( + name: &str, + values: &[i32], + position: usize, + arena: &mut PlanArena, + ) -> Result<(LogicalPlan, ExprRef), DatabaseError> { + let mut column = ColumnCatalog::new( + name.into(), + false, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, + ); + column.set_ref_table(name.into(), 0, false); + let column = arena.alloc_column(column); + let key = arena.alloc_expression(ScalarExpression::column_expr(column, position)); + let rows = values + .iter() + .map(|value| { + arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(*value))) + }) + .collect(); + Ok(( + LogicalPlan::new( + Operator::Values(ValuesOperator { + rows, + row_count: values.len(), + schema_ref: vec![column].into(), + }), + Childrens::None, + ), + key, + )) + } + + #[test] + fn late_project_collapse_preserves_oriented_columns_and_aliases() -> Result<(), DatabaseError> { + for alias in [false, true] { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (left, large) = literal_input("large", &[1, 2, 3, 4], 0, &mut arena)?; + let (right, small) = literal_input("small", &[99], 1, &mut arena)?; + let join = + JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); + let output = if alias { + arena.alloc_expression(ScalarExpression::Alias { + expr: large, + alias: AliasType::Name("wanted".into()), + }) + } else { + large + }; + // Repeating the selected column must not lose an output slot. + let mut plan = LogicalPlan::new( + Operator::Project(ProjectOperator { + exprs: vec![output, output, small], + }), + Childrens::Only(Box::new(join)), + ); + let schema = plan.output_schema(&mut arena).clone(); + let cache = StatisticsMetaCache::default(); + let loader = StatisticMetaLoader::new(&cache); + let mut plan = optimizer_pipeline() + .instantiate(plan) + .find_best(Some(&loader), &mut arena)?; + plan.populate_output_schema_recursive(&mut arena); + assert_eq!(plan.output_schema(&mut arena), &schema); + let Childrens::Only(join) = plan.childrens.as_ref() else { + unreachable!() + }; + assert!( + matches!(join.operator, Operator::Join(_)), + "restore Project should be collapsed" + ); + let Childrens::Twins { left, right } = join.childrens.as_ref() else { + unreachable!() + }; + assert_eq!(left.estimated_rows(), Some(1)); + assert_eq!(right.estimated_rows(), Some(4)); + let Operator::Project(project) = &plan.operator else { + unreachable!() + }; + let Childrens::Only(input) = plan.childrens.as_ref() else { + unreachable!() + }; + let storage = MemoryStorage::new(); + let transaction = storage.transaction()?; + let table_cache = TableCache::default(); + let view_cache = ViewCache::default(); + let context = empty_context(&table_cache, &view_cache, &cache); + let mut executor = + execute_input::<_, Projection>((project, input), context, arena, &transaction); + for value in [1, 2, 3, 4] { + assert_eq!( + executor.next_tuple()?.map(|row| row.values.clone()), + Some(vec![ + DataValue::Int32(value), + DataValue::Int32(value), + DataValue::Int32(99) + ]) + ); + } + assert!(executor.next_tuple()?.is_none()); + } + Ok(()) + } + + #[test] + fn late_project_collapse_keeps_computed_output_boundary() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (left, large) = literal_input("large", &[1, 2, 3, 4], 0, &mut arena)?; + let (right, small) = literal_input("small", &[99], 1, &mut arena)?; + let join = JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); + let ten = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(10))); + let computed = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Plus, + left_expr: large, + right_expr: ten, + evaluator: None, + ty: LogicalType::Integer, + }); + let mut plan = LogicalPlan::new( + Operator::Project(ProjectOperator { + exprs: vec![computed, small], + }), + Childrens::Only(Box::new(join)), + ); + let schema = plan.output_schema(&mut arena).clone(); + let cache = StatisticsMetaCache::default(); + let loader = StatisticMetaLoader::new(&cache); + let mut plan = optimizer_pipeline() + .instantiate(plan) + .find_best(Some(&loader), &mut arena)?; + plan.populate_output_schema_recursive(&mut arena); + assert_eq!(plan.output_schema(&mut arena), &schema); + let Childrens::Only(child) = plan.childrens.as_ref() else { + unreachable!() + }; + assert!( + matches!(child.operator, Operator::Project(_)), + "computed expressions are not substituted" + ); + let Operator::Project(project) = &plan.operator else { + unreachable!() + }; + let Childrens::Only(input) = plan.childrens.as_ref() else { + unreachable!() + }; + let storage = MemoryStorage::new(); + let transaction = storage.transaction()?; + let table_cache = TableCache::default(); + let view_cache = ViewCache::default(); + let context = empty_context(&table_cache, &view_cache, &cache); + let mut executor = + execute_input::<_, Projection>((project, input), context, arena, &transaction); + for value in [11, 12, 13, 14] { + assert_eq!( + executor.next_tuple()?.map(|row| row.values.clone()), + Some(vec![DataValue::Int32(value), DataValue::Int32(99)]) + ); + } + assert!(executor.next_tuple()?.is_none()); + Ok(()) + } + fn assert_send_sync() {} #[test] diff --git a/src/execution/dql/aggregate/hash_agg.rs b/src/execution/dql/aggregate/hash_agg.rs index 3b112736..442f894e 100644 --- a/src/execution/dql/aggregate/hash_agg.rs +++ b/src/execution/dql/aggregate/hash_agg.rs @@ -213,7 +213,7 @@ mod test { let pipeline = HepOptimizerPipeline::builder() .before_batch( - "Expression Remapper".to_string(), + "Expression Remapper", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::EvaluatorBind], ) diff --git a/src/execution/dql/aggregate/stream_distinct.rs b/src/execution/dql/aggregate/stream_distinct.rs index 7fc62bfa..a6f7b3be 100644 --- a/src/execution/dql/aggregate/stream_distinct.rs +++ b/src/execution/dql/aggregate/stream_distinct.rs @@ -139,7 +139,7 @@ mod tests { ) -> Result { HepOptimizerPipeline::builder() .before_batch( - "Expression Remapper".to_string(), + "Expression Remapper", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::EvaluatorBind], ) diff --git a/src/execution/dql/join/hash_join.rs b/src/execution/dql/join/hash_join.rs index d92f977e..716f651d 100644 --- a/src/execution/dql/join/hash_join.rs +++ b/src/execution/dql/join/hash_join.rs @@ -404,7 +404,7 @@ mod test { ) -> Result { HepOptimizerPipeline::builder() .before_batch( - "Expression Remapper".to_string(), + "Expression Remapper", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::EvaluatorBind], ) diff --git a/src/execution/dql/join/nested_loop_join.rs b/src/execution/dql/join/nested_loop_join.rs index 99a2bcd3..b841c16d 100644 --- a/src/execution/dql/join/nested_loop_join.rs +++ b/src/execution/dql/join/nested_loop_join.rs @@ -390,7 +390,7 @@ mod test { ) -> Result { HepOptimizerPipeline::builder() .before_batch( - "Expression Remapper".to_string(), + "Expression Remapper", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::EvaluatorBind], ) diff --git a/src/expression/range_detacher.rs b/src/expression/range_detacher.rs index 211e1481..8a4a308f 100644 --- a/src/expression/range_detacher.rs +++ b/src/expression/range_detacher.rs @@ -1313,7 +1313,7 @@ mod test { ) -> Result, DatabaseError> { let pipeline = HepOptimizerPipeline::builder() .before_batch( - "test_simplify_filter".to_string(), + "test_simplify_filter", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::SimplifyFilter], ) diff --git a/src/optimizer/heuristic/batch.rs b/src/optimizer/heuristic/batch.rs index 28b98971..6c28f440 100644 --- a/src/optimizer/heuristic/batch.rs +++ b/src/optimizer/heuristic/batch.rs @@ -22,7 +22,7 @@ use std::array; #[derive(Clone)] pub struct HepBatch { #[allow(dead_code)] - pub name: String, + pub name: &'static str, pub strategy: HepBatchStrategy, pub steps: Vec, } @@ -90,7 +90,7 @@ impl HepLocalRewriteBatch { impl HepBatch { pub fn new( - name: String, + name: &'static str, strategy: HepBatchStrategy, rules: Vec, ) -> Self { diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index 92402220..b69e7ac6 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -791,7 +791,7 @@ mod tests { let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); let batch = HepBatch::new( - "Combine Filters".into(), + "Combine Filters", HepBatchStrategy::fix_point_bottomup(1), vec![NormalizationRuleImpl::CombineFilter], ); @@ -844,7 +844,7 @@ mod tests { ); let pipeline = HepOptimizerPipeline::builder() .before_batch( - "Combine Filters".into(), + "Combine Filters", strategy, vec![NormalizationRuleImpl::CombineFilter], ) @@ -876,7 +876,7 @@ mod tests { ); let pipeline = HepOptimizerPipeline::builder() .before_batch( - "Combine Filters".into(), + "Combine Filters", strategy, vec![NormalizationRuleImpl::CombineFilter], ) @@ -910,7 +910,7 @@ mod tests { plan = LimitOperator::build(None, Some(5), plan); let pipeline = HepOptimizerPipeline::builder() .before_batch( - "Limit Project Transpose".into(), + "Limit Project Transpose", strategy, vec![NormalizationRuleImpl::LimitProjectTranspose], ) @@ -941,7 +941,7 @@ mod tests { plan = LimitOperator::build(None, Some(5), plan); let pipeline = HepOptimizerPipeline::builder() .before_batch( - "Limit Project Transpose".into(), + "Limit Project Transpose", HepBatchStrategy::fix_point_bottomup(iterations), vec![NormalizationRuleImpl::LimitProjectTranspose], ) @@ -1003,12 +1003,12 @@ mod tests { let plan = binder.bind(&stmt, &mut plan_arena)?; let pipeline = HepOptimizerPipeline::builder() .before_batch( - "Simplify Filter".to_string(), + "Simplify Filter", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::SimplifyFilter], ) .before_batch( - "Predicate Pushdown".to_string(), + "Predicate Pushdown", HepBatchStrategy::fix_point_topdown(10), vec![ NormalizationRuleImpl::PushPredicateThroughJoin, @@ -1017,7 +1017,7 @@ mod tests { ], ) .after_batch( - "Eliminate Index Filter".to_string(), + "Eliminate Index Filter", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::EliminateIndexFilter], ) @@ -1124,7 +1124,7 @@ pub struct HepOptimizerPipelineBuilder { impl HepOptimizerPipelineBuilder { pub fn before_batch( mut self, - name: String, + name: &'static str, strategy: HepBatchStrategy, rules: Vec, ) -> Self { @@ -1135,7 +1135,7 @@ impl HepOptimizerPipelineBuilder { pub fn after_batch( mut self, - name: String, + name: &'static str, strategy: HepBatchStrategy, rules: Vec, ) -> Self { diff --git a/src/optimizer/rule/normalization/column_pruning.rs b/src/optimizer/rule/normalization/column_pruning.rs index 0b2eb1a5..7cb221b2 100644 --- a/src/optimizer/rule/normalization/column_pruning.rs +++ b/src/optimizer/rule/normalization/column_pruning.rs @@ -870,7 +870,7 @@ mod tests { HepOptimizerPipeline::builder() .before_batch( - format!("column_pruning::{sql}"), + "column_pruning", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::ColumnPruning], ) @@ -1022,7 +1022,7 @@ mod tests { let pipeline = HepOptimizerPipeline::builder() .before_batch( - "test_column_pruning".to_string(), + "test_column_pruning", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::ColumnPruning], ) diff --git a/src/optimizer/rule/normalization/combine_operators.rs b/src/optimizer/rule/normalization/combine_operators.rs index 68560f20..a5a89b1e 100644 --- a/src/optimizer/rule/normalization/combine_operators.rs +++ b/src/optimizer/rule/normalization/combine_operators.rs @@ -364,7 +364,7 @@ mod tests { let pipeline = HepOptimizerPipeline::builder() .before_batch( - "test_collapse_project".to_string(), + "test_collapse_project", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::CollapseProject], ) @@ -403,7 +403,7 @@ mod tests { let pipeline = HepOptimizerPipeline::builder() .before_batch( - "test_collapse_project_with_alias".to_string(), + "test_collapse_project_with_alias", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::CollapseProject], ) @@ -492,7 +492,7 @@ mod tests { let pipeline = HepOptimizerPipeline::builder() .before_batch( - "test_combine_filter".to_string(), + "test_combine_filter", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::CombineFilter], ) @@ -522,7 +522,7 @@ mod tests { let pipeline = HepOptimizerPipeline::builder() .before_batch( - "test_collapse_group_by_agg".to_string(), + "test_collapse_group_by_agg", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::CollapseGroupByAgg], ) diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 1afb6450..033d685e 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -1428,7 +1428,7 @@ mod tests { )); let pipeline = HepOptimizerPipeline::builder() .after_batch( - "column statistics".into(), + "column statistics", HepBatchStrategy::fix_point_topdown(1), vec![NormalizationRuleImpl::GreedyJoinReorder], ) diff --git a/src/optimizer/rule/normalization/pushdown_limit.rs b/src/optimizer/rule/normalization/pushdown_limit.rs index 011a7251..76d78c57 100644 --- a/src/optimizer/rule/normalization/pushdown_limit.rs +++ b/src/optimizer/rule/normalization/pushdown_limit.rs @@ -152,7 +152,7 @@ mod tests { let pipeline = HepOptimizerPipeline::builder() .before_batch( - "test_limit_project_transpose".to_string(), + "test_limit_project_transpose", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::LimitProjectTranspose], ) @@ -184,7 +184,7 @@ mod tests { let pipeline = HepOptimizerPipeline::builder() .before_batch( - "test_push_limit_through_join".to_string(), + "test_push_limit_through_join", HepBatchStrategy::once_topdown(), vec![ NormalizationRuleImpl::LimitProjectTranspose, @@ -218,7 +218,7 @@ mod tests { let pipeline = HepOptimizerPipeline::builder() .before_batch( - "test_push_limit_into_table_scan".to_string(), + "test_push_limit_into_table_scan", HepBatchStrategy::once_topdown(), vec![ NormalizationRuleImpl::LimitProjectTranspose, diff --git a/src/optimizer/rule/normalization/pushdown_predicates.rs b/src/optimizer/rule/normalization/pushdown_predicates.rs index 8e88f562..4046766c 100644 --- a/src/optimizer/rule/normalization/pushdown_predicates.rs +++ b/src/optimizer/rule/normalization/pushdown_predicates.rs @@ -659,12 +659,12 @@ mod tests { plan, HepOptimizerPipeline::builder() .before_batch( - "simplify_filter".to_string(), + "simplify_filter", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::SimplifyFilter], ) .before_batch( - "test_push_predicate_into_scan".to_string(), + "test_push_predicate_into_scan", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::PushPredicateIntoScan], ), @@ -760,7 +760,7 @@ mod tests { let plan = apply_pipeline( FilterOperator::build(predicate, scan, false), HepOptimizerPipeline::builder().before_batch( - "unique_composite_range".to_string(), + "unique_composite_range", HepBatchStrategy::fix_point_topdown(1), vec![NormalizationRuleImpl::PushPredicateIntoScan], ), @@ -1019,7 +1019,7 @@ mod tests { let best_plan = apply_pipeline( filter_plan, HepOptimizerPipeline::builder().before_batch( - "push_cover_mapping".to_string(), + "push_cover_mapping", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::PushPredicateIntoScan], ), @@ -1082,7 +1082,7 @@ mod tests { let best_plan = apply_pipeline( plan, HepOptimizerPipeline::builder().before_batch( - "test_push_predicate_through_join".to_string(), + "test_push_predicate_through_join", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::PushPredicateThroughJoin], ), @@ -1132,7 +1132,7 @@ mod tests { let best_plan = apply_pipeline( plan, HepOptimizerPipeline::builder().before_batch( - "test_push_predicate_through_join".to_string(), + "test_push_predicate_through_join", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::PushPredicateThroughJoin], ), @@ -1182,7 +1182,7 @@ mod tests { let plan = apply_pipeline( plan, HepOptimizerPipeline::builder().before_batch( - "cross join pushdown".into(), + "cross join pushdown", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::PushPredicateThroughJoin], ), @@ -1215,7 +1215,7 @@ mod tests { let best_plan = apply_pipeline( plan, HepOptimizerPipeline::builder().before_batch( - "test_push_predicate_through_join".to_string(), + "test_push_predicate_through_join", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::PushPredicateThroughJoin], ), @@ -1270,7 +1270,7 @@ mod tests { let mut best_plan = apply_pipeline( plan, HepOptimizerPipeline::builder().before_batch( - "push_join_predicate_into_scan".to_string(), + "push_join_predicate_into_scan", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::PushJoinPredicateIntoScan], ), @@ -1346,7 +1346,7 @@ mod tests { let mut best_plan = apply_pipeline( plan, HepOptimizerPipeline::builder().before_batch( - "push_join_predicate_into_scan".to_string(), + "push_join_predicate_into_scan", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::PushJoinPredicateIntoScan], ), @@ -1394,7 +1394,7 @@ mod tests { let mut best_plan = apply_pipeline( plan, HepOptimizerPipeline::builder().before_batch( - "push_join_predicate_into_scan".to_string(), + "push_join_predicate_into_scan", HepBatchStrategy::once_topdown(), vec![NormalizationRuleImpl::PushJoinPredicateIntoScan], ), diff --git a/src/optimizer/rule/normalization/simplification.rs b/src/optimizer/rule/normalization/simplification.rs index b6e55058..cccbd480 100644 --- a/src/optimizer/rule/normalization/simplification.rs +++ b/src/optimizer/rule/normalization/simplification.rs @@ -126,13 +126,13 @@ mod test { fn run_with_single_batch( plan: LogicalPlan, - name: &str, + name: &'static str, strategy: HepBatchStrategy, rules: Vec, arena: &mut PlanArena, ) -> Result { HepOptimizerPipeline::builder() - .before_batch(name.to_string(), strategy, rules) + .before_batch(name, strategy, rules) .build() .instantiate(plan) .find_best(None, arena) @@ -150,7 +150,7 @@ mod test { let best_plan = HepOptimizerPipeline::builder() .before_batch( - "test_simplification".to_string(), + "test_simplification", HepBatchStrategy::once_topdown(), vec![ NormalizationRuleImpl::SimplifyFilter, From 496f225992402ef35171d151e11547758c59efaa Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 18/62] perf: retain large constant IN lists and detach point ranges --- src/expression/range_detacher.rs | 203 +++++++++- src/expression/simplify.rs | 40 ++ .../rule/normalization/simplification.rs | 66 +++- tests/slt/where.slt | 359 ++++++++++++++++++ 4 files changed, 661 insertions(+), 7 deletions(-) diff --git a/src/expression/range_detacher.rs b/src/expression/range_detacher.rs index 8a4a308f..cdbfcf25 100644 --- a/src/expression/range_detacher.rs +++ b/src/expression/range_detacher.rs @@ -505,6 +505,47 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { _ => None, } } + ScalarExpression::In { + negated, + expr, + args, + .. + } => { + let Some(column) = expr.constant_in_column(&args, self.arena) else { + return Ok(None); + }; + if !self.matches_column(column) { + return Ok(None); + } + let mut values = Vec::with_capacity(args.len()); + for arg in args { + let ScalarExpression::Constant(value) = self.arena.expression(arg) else { + unreachable!(); + }; + if value.is_null() { + // NOT IN with NULL cannot be true in a Filter; positive + // IN ignores NULL points but must not match NULL rows. + if negated { + return Ok(Some(DetachedPredicate::consumed(Range::Dummy))); + } + } else { + values.push(value.clone()); + } + } + // Keep NOT IN residual, as with the old NotEq expansion. + if negated { + return Ok(None); + } + // constant_in_column guarantees comparable values in one domain. + values.sort_unstable_by(|left, right| left.partial_cmp(right).unwrap()); + values.dedup(); + let range = match values.len() { + 0 => Range::Dummy, + 1 => Range::Eq(values.pop().unwrap()), + _ => Range::SortedRanges(values.into_iter().map(Range::Eq).collect()), + }; + Some(DetachedPredicate::consumed(range)) + } ScalarExpression::Alias { expr, .. } | ScalarExpression::TypeCast { expr, .. } => { self.detach(expr)? } @@ -557,7 +598,6 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { | ScalarExpression::ColumnRef { .. } => None, // FIXME: support [RangeDetacher::_detach] ScalarExpression::Unary { .. } - | ScalarExpression::In { .. } | ScalarExpression::Between { .. } | ScalarExpression::SubString { .. } | ScalarExpression::Position { .. } @@ -1302,7 +1342,7 @@ mod test { use crate::optimizer::rule::normalization::NormalizationRuleImpl; use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::Operator; - use crate::planner::{ExprRef, LogicalPlan, MetaArena, PlanArena}; + use crate::planner::{ExprRef, LogicalPlan, MetaArena, PlanArena, TableArenaCell}; use crate::types::value::DataValue; use crate::types::LogicalType; use std::ops::Bound; @@ -1370,6 +1410,165 @@ mod test { }) } + fn point_list( + arena: &mut PlanArena, + column: ColumnRef, + values: &[DataValue], + negated: bool, + ) -> ExprRef { + let expr = arena.alloc_expression(ScalarExpression::column_expr(column, 0)); + let args = values + .iter() + .map(|value| arena.alloc_expression(ScalarExpression::Constant(value.clone()))) + .collect(); + arena.alloc_expression(ScalarExpression::In { + negated, + expr, + args, + evaluator: None, + }) + } + + #[test] + fn detach_in_deduplicates_points_and_never_selects_null() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let name: TableName = "points".into(); + let column = test_column(&mut arena, &name, 0, "c1", true)?; + for (values, expected) in [ + ( + vec![ + DataValue::Int32(3), + DataValue::Null, + DataValue::Int32(1), + DataValue::Int32(3), + ], + Range::SortedRanges(vec![ + Range::Eq(DataValue::Int32(1)), + Range::Eq(DataValue::Int32(3)), + ]), + ), + ( + vec![DataValue::Null, DataValue::Int32(1)], + Range::Eq(DataValue::Int32(1)), + ), + (vec![DataValue::Null], Range::Dummy), + ] { + let predicate = point_list(&mut arena, column, &values, false); + let detached = RangeDetacher::new(name.as_ref(), &0, &mut arena) + .detach(predicate)? + .unwrap(); + assert_eq!(detached.range, expected); + assert_eq!(detached.residual, None); + } + Ok(()) + } + + #[test] + fn detach_not_in_keeps_residual_unless_null_proves_no_match() -> Result<(), DatabaseError> { + for nullable in [false, true] { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let name: TableName = "points".into(); + let column = test_column(&mut arena, &name, 0, "c1", nullable)?; + let predicate = point_list( + &mut arena, + column, + &[ + DataValue::Int32(3), + DataValue::Int32(1), + DataValue::Int32(3), + ], + true, + ); + assert_eq!( + RangeDetacher::new(name.as_ref(), &0, &mut arena).detach(predicate)?, + None + ); + let predicate = point_list( + &mut arena, + column, + &[DataValue::Int32(1), DataValue::Null], + true, + ); + assert_eq!( + RangeDetacher::new(name.as_ref(), &0, &mut arena) + .detach(predicate)? + .unwrap() + .range, + Range::Dummy + ); + } + Ok(()) + } + + #[test] + fn in_detachment_keeps_other_column_residual_and_intersects_bounds() -> Result<(), DatabaseError> + { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let name: TableName = "points".into(); + let column = test_column(&mut arena, &name, 0, "c1", true)?; + let other = test_column(&mut arena, &name, 1, "c2", true)?; + let points = point_list( + &mut arena, + column, + &[ + DataValue::Int32(1), + DataValue::Int32(3), + DataValue::Int32(5), + ], + false, + ); + let lower = cmp_predicate(&mut arena, column, BinaryOperator::Gt, 2); + let limited = and_predicate(&mut arena, points, lower); + let residual = cmp_predicate(&mut arena, other, BinaryOperator::Eq, 7); + let combined = and_predicate(&mut arena, limited, residual); + let detached = RangeDetacher::new(name.as_ref(), &0, &mut arena) + .detach(combined)? + .unwrap(); + assert_eq!( + detached.range, + Range::SortedRanges(vec![ + Range::Eq(DataValue::Int32(3)), + Range::Eq(DataValue::Int32(5)) + ]) + ); + assert_eq!(detached.residual, Some(residual)); + assert_eq!( + RangeDetacher::new(name.as_ref(), &1, &mut arena).detach(points)?, + None + ); + Ok(()) + } + + #[test] + fn unsafe_point_lists_are_not_detached() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let name: TableName = "points".into(); + let column = test_column(&mut arena, &name, 0, "c1", true)?; + for values in [ + vec![DataValue::Int64(2147483648)], + vec![DataValue::Parameter { + id: 0, + ty: LogicalType::Integer, + }], + ] { + let predicate = point_list(&mut arena, column, &values, false); + assert_eq!( + RangeDetacher::new(name.as_ref(), &0, &mut arena).detach(predicate)?, + None + ); + } + let predicate = point_list(&mut arena, column, &[], false); + assert_eq!( + RangeDetacher::new(name.as_ref(), &0, &mut arena).detach(predicate)?, + None + ); + Ok(()) + } + #[test] fn test_detach_consumes_and_predicates_with_residual() -> Result<(), DatabaseError> { let table_state = build_t1_table()?; diff --git a/src/expression/simplify.rs b/src/expression/simplify.rs index 59aa9d9f..fd311a1e 100644 --- a/src/expression/simplify.rs +++ b/src/expression/simplify.rs @@ -107,6 +107,8 @@ impl ExprVisitorMut for ConstantCalculator { } } +const IN_EXPANSION_LIMIT: usize = 5; + #[derive(Debug, Default)] pub struct Simplify; @@ -198,6 +200,13 @@ impl ExprVisitorMut for Simplify { if args.is_empty() { return Ok(false); } + // Keep point lists compact without changing pairwise coercion or + // repeated evaluation of non-column operands in the old rewrite. + if args.len() > IN_EXPANSION_LIMIT + && arg_expr.constant_in_column(args, arena).is_some() + { + return Ok(true); + } let (op_1, op_2) = if *negated { (BinaryOperator::NotEq, BinaryOperator::And) @@ -502,6 +511,37 @@ impl Simplify { } impl ExprRef { + pub(crate) fn constant_in_column( + mut self, + args: &[ExprRef], + arena: &A, + ) -> Option { + while let ScalarExpression::Alias { expr, .. } = arena.expression(self) { + self = *expr; + } + let ScalarExpression::ColumnRef { column, .. } = arena.expression(self) else { + return None; + }; + let ty = arena.column(*column).datatype(); + // Floating key order need not agree with SQL equality for NaNs/zeros. + if args.is_empty() + || matches!( + ty, + LogicalType::Tuple(_) | LogicalType::Float | LogicalType::Double + ) + { + return None; + } + args.iter() + .all(|arg| match arena.expression(*arg) { + ScalarExpression::Constant(value) if !value.has_parameter() => { + value.is_null() || value.logical_type() == *ty + } + _ => false, + }) + .then_some(*column) + } + pub(crate) fn unpack_val(self, arena: &A) -> Option { match arena.expression(self) { ScalarExpression::Constant(val) => Some(val.clone()), diff --git a/src/optimizer/rule/normalization/simplification.rs b/src/optimizer/rule/normalization/simplification.rs index cccbd480..2f8a2629 100644 --- a/src/optimizer/rule/normalization/simplification.rs +++ b/src/optimizer/rule/normalization/simplification.rs @@ -114,12 +114,14 @@ mod test { use crate::errors::DatabaseError; use crate::expression::range_detacher::{Range, RangeDetacher}; + use crate::expression::simplify::Simplify; + use crate::expression::visitor_mut::ExprVisitorMut; use crate::expression::{BinaryOperator, ScalarExpression, UnaryOperator}; use crate::optimizer::heuristic::batch::HepBatchStrategy; use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; use crate::optimizer::rule::normalization::NormalizationRuleImpl; use crate::planner::operator::Operator; - use crate::planner::{LogicalPlan, PlanArena}; + use crate::planner::{LogicalPlan, PlanArena, TableArenaCell}; use crate::types::value::DataValue; use crate::types::{ColumnId, LogicalType}; use std::collections::Bound; @@ -238,10 +240,6 @@ mod test { #[test] fn test_constant_is_null_elimination() -> Result<(), DatabaseError> { - use crate::expression::simplify::Simplify; - use crate::expression::visitor_mut::ExprVisitorMut; - use crate::planner::TableArenaCell; - let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); for (value, negated, expected) in [ @@ -658,6 +656,64 @@ mod test { Ok(()) } + #[test] + fn constant_in_expansion_uses_original_list_length() -> Result<(), DatabaseError> { + let table_state = build_t1_table()?; + for (list, kept) in [ + ("1,2,3,4,5", false), + ("1,2,3,4,5,6", true), + ("1,1,1,1,1,NULL", true), + ("NULL,NULL,NULL,NULL,NULL,NULL", true), + ] { + for negated in ["", "not "] { + let mut arena = PlanArena::new(&table_state.table_arena); + let plan = table_state.plan_with_arena( + &format!("select * from t1 where c1 {negated}in ({list})"), + &mut arena, + )?; + let Operator::Filter(filter) = plan.childrens.pop_only().operator else { + unreachable!(); + }; + let mut predicate = filter.predicate; + Simplify.visit(&mut predicate, &mut arena)?; + assert_eq!( + matches!(arena.expression(predicate), ScalarExpression::In { .. }), + kept, + "{negated}IN ({list})" + ); + } + } + Ok(()) + } + + #[test] + fn unsafe_large_in_keeps_pairwise_expansion() -> Result<(), DatabaseError> { + let table_state = build_t1_table()?; + for predicate in [ + "c1 in (1,2,3,4,5,2147483648)", + "c1 in (1,2,3,4,5,c1)", + "(c1 + 1) in (1,2,3,4,5,6)", + "cast(c1 as double) in (1.0,2.0,3.0,4.0,5.0,6.0)", + ] { + let mut arena = PlanArena::new(&table_state.table_arena); + let plan = table_state + .plan_with_arena(&format!("select * from t1 where {predicate}"), &mut arena)?; + let Operator::Filter(filter) = plan.childrens.pop_only().operator else { + unreachable!(); + }; + let mut expression = filter.predicate; + Simplify.visit(&mut expression, &mut arena)?; + assert!( + matches!( + arena.expression(expression), + ScalarExpression::Binary { .. } + ), + "{predicate}" + ); + } + Ok(()) + } + #[test] fn test_simplify_filter_column_in() -> Result<(), DatabaseError> { let table_state = build_t1_table()?; diff --git a/tests/slt/where.slt b/tests/slt/where.slt index 82c6ee46..0700e0c7 100644 --- a/tests/slt/where.slt +++ b/tests/slt/where.slt @@ -421,3 +421,362 @@ select cast(-2147483649.0 as int); statement ok drop table alias_input; + +# Native point-list predicates cross the 5/6 expansion boundary. Repeat them +# before and after index creation so range elimination agrees with full scans. +statement ok +create table native_in_points(id int primary key, v int null, payload int); + +statement ok +insert into native_in_points values +(1,NULL,10),(2,-1,20),(3,1,30),(4,1,40),(5,2,50),(6,3,60), +(7,4,70),(8,5,80),(9,6,90),(10,7,100),(11,8,110),(12,9,120), +(13,NULL,130),(14,NULL,140),(15,NULL,150),(16,NULL,160), +(17,NULL,170),(18,NULL,180),(19,NULL,190),(20,NULL,200), +(21,NULL,210),(22,NULL,220),(23,NULL,230),(24,NULL,240), +(25,NULL,250),(26,NULL,260),(27,NULL,270),(28,NULL,280), +(29,NULL,290),(30,NULL,300),(31,NULL,310),(32,NULL,320); + +statement ok +insert into native_in_points values +(33,10,330),(34,11,340),(35,12,350),(36,13,360),(37,14,370), +(38,15,380),(39,16,390),(40,17,400),(41,18,410),(42,19,420), +(43,20,430),(44,21,440),(45,22,450),(46,23,460),(47,24,470), +(48,25,480),(49,26,490),(50,27,500),(51,28,510),(52,29,520), +(53,30,530),(54,31,540),(55,32,550),(56,33,560),(57,34,570), +(58,35,580),(59,36,590),(60,37,600),(61,38,610),(62,39,620), +(63,40,630),(64,41,640),(65,42,650),(66,43,660),(67,44,670), +(68,45,680),(69,46,690),(70,47,700),(71,48,710),(72,49,720), +(73,50,730),(74,51,740),(75,52,750),(76,53,760),(77,54,770), +(78,55,780),(79,56,790),(80,57,800),(81,58,810),(82,59,820), +(83,60,830),(84,61,840),(85,62,850),(86,63,860),(87,64,870), +(88,65,880),(89,66,890),(90,67,900),(91,68,910),(92,69,920), +(93,70,930),(94,71,940),(95,72,950),(96,73,960),(97,74,970), +(98,75,980),(99,76,990),(100,77,1000),(101,78,1010),(102,79,1020), +(103,80,1030),(104,81,1040),(105,82,1050),(106,83,1060),(107,84,1070), +(108,85,1080),(109,86,1090),(110,87,1100),(111,88,1110),(112,89,1120), +(113,90,1130),(114,91,1140),(115,92,1150),(116,93,1160),(117,94,1170), +(118,95,1180),(119,96,1190),(120,97,1200),(121,98,1210),(122,99,1220), +(123,100,1230),(124,101,1240),(125,102,1250),(126,103,1260),(127,104,1270), +(128,105,1280),(129,106,1290),(130,107,1300),(131,108,1310),(132,109,1320); + +statement ok +analyze table native_in_points; + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5); +---- +3 +4 +5 +6 +7 +8 + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5,6); +---- +3 +4 +5 +6 +7 +8 +9 + +query I rowsort +select id from native_in_points where v not in (1,2,3,4,5,6) and v < 10; +---- +10 +11 +12 +2 + +query I rowsort +select id from native_in_points where v in (9,NULL,1,1,1,2); +---- +12 +3 +4 +5 + +query I rowsort +select id from native_in_points where v not in (1,2,3,4,5,NULL); +---- + +query I rowsort +select id from native_in_points where v in (NULL,NULL,NULL,NULL,NULL,NULL); +---- + +query I rowsort +select id from native_in_points where v not in (NULL,NULL,NULL,NULL,NULL,NULL); +---- + +query I rowsort +select id from native_in_points where v in (-999,-998,-997,6,1,6); +---- +3 +4 +9 + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5,6) and v > 3; +---- +7 +8 +9 + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5,6) and payload > 50; +---- +6 +7 +8 +9 + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5,6) or payload = 10; +---- +1 +3 +4 +5 +6 +7 +8 +9 + +query I +select count(*) from native_in_points where v not in (1,2,3,4,5,6); +---- +104 + +query I rowsort +select id from native_in_points where v not in (1,2,3,4,5,6) limit 2 offset 1; +---- +10 +11 + +statement ok +create index native_in_v on native_in_points(v); + +statement ok +analyze table native_in_points; + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5); +---- +3 +4 +5 +6 +7 +8 + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5,6); +---- +3 +4 +5 +6 +7 +8 +9 + +query I rowsort +select id from native_in_points where v not in (1,2,3,4,5,6) and v < 10; +---- +10 +11 +12 +2 + +query I rowsort +select id from native_in_points where v in (9,NULL,1,1,1,2); +---- +12 +3 +4 +5 + +query I rowsort +select id from native_in_points where v not in (1,2,3,4,5,NULL); +---- + +query I rowsort +select id from native_in_points where v in (NULL,NULL,NULL,NULL,NULL,NULL); +---- + +query I rowsort +select id from native_in_points where v not in (NULL,NULL,NULL,NULL,NULL,NULL); +---- + +query I rowsort +select id from native_in_points where v in (-999,-998,-997,6,1,6); +---- +3 +4 +9 + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5,6) and v > 3; +---- +7 +8 +9 + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5,6) and payload > 50; +---- +6 +7 +8 +9 + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5,6) or payload = 10; +---- +1 +3 +4 +5 +6 +7 +8 +9 + +query I +select count(*) from native_in_points where v not in (1,2,3,4,5,6); +---- +104 + +query I rowsort +select id from native_in_points where v not in (1,2,3,4,5,6) limit 2 offset 1; +---- +10 +11 + +statement ok +drop index native_in_v on native_in_points; + +statement ok +create unique index native_in_prefix on native_in_points(v,payload); + +statement ok +analyze table native_in_points; + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5); +---- +3 +4 +5 +6 +7 +8 + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5,6); +---- +3 +4 +5 +6 +7 +8 +9 + +query I rowsort +select id from native_in_points where v not in (1,2,3,4,5,6) and v < 10; +---- +10 +11 +12 +2 + +query I rowsort +select id from native_in_points where v in (9,NULL,1,1,1,2); +---- +12 +3 +4 +5 + +query I rowsort +select id from native_in_points where v not in (1,2,3,4,5,NULL); +---- + +query I rowsort +select id from native_in_points where v in (NULL,NULL,NULL,NULL,NULL,NULL); +---- + +query I rowsort +select id from native_in_points where v not in (NULL,NULL,NULL,NULL,NULL,NULL); +---- + +query I rowsort +select id from native_in_points where v in (-999,-998,-997,6,1,6); +---- +3 +4 +9 + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5,6) and v > 3; +---- +7 +8 +9 + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5,6) and payload > 50; +---- +6 +7 +8 +9 + +query I rowsort +select id from native_in_points where v in (1,2,3,4,5,6) or payload = 10; +---- +1 +3 +4 +5 +6 +7 +8 +9 + +query I +select count(*) from native_in_points where v not in (1,2,3,4,5,6); +---- +104 + +query I rowsort +select id from native_in_points where v not in (1,2,3,4,5,6) limit 2 offset 1; +---- +10 +11 + +query IIIIII rowsort +select id, v in (1,2,3,4,5), v in (1,2,3,4,5,6), + v not in (1,2,3,4,5,6), v in (9,NULL,1,1,1,2), v not in (9,NULL,1,1,1,2) +from native_in_points where id <= 12; +---- +1 NULL NULL NULL NULL NULL +10 false false true NULL NULL +11 false false true NULL NULL +12 false false true true false +2 false false true NULL NULL +3 true true false true false +4 true true false true false +5 true true false true false +6 true true false NULL NULL +7 true true false NULL NULL +8 true true false NULL NULL +9 false true false NULL NULL + +statement ok +drop table native_in_points; From d99218c8b82936a2500af62328bfe2ca7b303ad5 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 19/62] perf: skip unused covered index key fields --- src/storage/mod.rs | 30 +++-- src/storage/rocksdb.rs | 31 +++++ src/storage/table_codec.rs | 82 +++++++++++-- src/types/value.rs | 246 ++++++++++++++++++++++++++++++++++++- 4 files changed, 367 insertions(+), 22 deletions(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index f300b384..05b7d55e 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -263,7 +263,14 @@ pub trait Transaction: Sized { LogicalType::Tuple(tys) => tys.len(), _ => 1, }; - let cover_mapping = mapping.map(|slots| TupleMapping::new(slots, tuple_len)); + let cover_mapping = match mapping { + Some(slots) => Some(TupleMapping::new(slots.iter().copied(), tuple_len)), + None if columns.len() < tuple_len => { + // Omitted mapping denotes an ordered prefix, not every key field. + Some(TupleMapping::new(0..columns.len(), tuple_len)) + } + None => None, + }; ( IndexImplEnum::Covered(CoveredIndexImpl), @@ -1439,15 +1446,17 @@ struct TupleMapping { } impl TupleMapping { - fn new(scan_to_index: &[usize], tuple_len: usize) -> Self { + fn new(scan_to_index: impl Iterator, tuple_len: usize) -> Self { let mut index_to_scan = vec![usize::MAX; tuple_len]; + let mut target_len = 0; - for (scan_idx, index_idx) in scan_to_index.iter().enumerate() { - index_to_scan[*index_idx] = scan_idx; + for index_idx in scan_to_index { + index_to_scan[index_idx] = target_len; + target_len += 1; } TupleMapping { index_to_scan, - target_len: scan_to_index.len(), + target_len, } } @@ -1891,12 +1900,14 @@ impl IndexImpl for CoveredIndexImpl { value: &[u8], params: &IndexImplParams, ) -> Result<(), DatabaseError> { - let key = match ¶ms.cover_mapping { + match ¶ms.cover_mapping { Some(mapping) => { - TableCodec::decode_index_key(key, params.value_ty(), &mapping.as_ref())? + TableCodec::decode_index_key_into(tuple, key, params.value_ty(), &mapping.as_ref())? } - None => TableCodec::decode_index_key(key, params.value_ty(), &OrderedMapping)?, - }; + None => { + TableCodec::decode_index_key_into(tuple, key, params.value_ty(), &OrderedMapping)? + } + } let tuple_id = if params.with_pk { Some(TableCodec::decode_index(value)?) @@ -1904,7 +1915,6 @@ impl IndexImpl for CoveredIndexImpl { None }; tuple.pk = tuple_id; - tuple.values = key; Ok(()) } diff --git a/src/storage/rocksdb.rs b/src/storage/rocksdb.rs index 11e23f45..f5f5c3ab 100644 --- a/src/storage/rocksdb.rs +++ b/src/storage/rocksdb.rs @@ -1392,6 +1392,37 @@ mod test { ); drop(iter); + // An identity prefix projection still skips the remaining key fields; + // omitted mapping must not widen the output back to the full index. + let prefix_deserializers = [ + plan_arena.column(b_cover_column).datatype().serializable(), + plan_arena + .column(a_cover_column) + .datatype() + .skip_serializable(), + ]; + let mut iter = transaction.read_by_index( + kite_sql.state.table_cache(), + &plan_arena, + "t1".to_string().into(), + (None, None), + &[b_cover_column], + composite_index, + &[Range::Scope { + min: Bound::Unbounded, + max: Bound::Unbounded, + }][..], + false, + Some(&prefix_deserializers), + None, + )?; + for value in [0, 1, 2, 4] { + let tuple = crate::storage::next_tuple_for_test(&mut iter)?.unwrap(); + assert_eq!(tuple.values, vec![DataValue::Int32(value)]); + } + assert!(crate::storage::next_tuple_for_test(&mut iter)?.is_none()); + drop(iter); + let target_pk = DataValue::Int32(3); let covered_value = DataValue::Int32(4); let covered_ranges = [Range::Eq(covered_value.clone())]; diff --git a/src/storage/table_codec.rs b/src/storage/table_codec.rs index 95d7daf5..81878d98 100644 --- a/src/storage/table_codec.rs +++ b/src/storage/table_codec.rs @@ -881,26 +881,37 @@ impl TableCodec { ) } - pub fn decode_index_key( + pub fn decode_index_key_into( + tuple: &mut Tuple, bytes: &[u8], ty: &LogicalType, mapping: &M, - ) -> Result, DatabaseError> { + ) -> Result<(), DatabaseError> { // Hash + TypeTag + Bound Min + Index Id Len + Bound Min let start = TUPLE_KEY_PREFIX_LEN + INDEX_ID_LEN + KEY_BOUND_LEN; - let mut reader = Cursor::new(&bytes[start..]); + let bytes = bytes.get(start..).ok_or(DatabaseError::InvalidIndex)?; + let mut reader = Cursor::new(bytes); let types = match ty { LogicalType::Tuple(types) => types.as_slice(), ty => std::slice::from_ref(ty), }; - let mut values = vec![DataValue::Null; mapping.target_len(types.len())]; + tuple.values.clear(); + tuple + .values + .resize(mapping.target_len(types.len()), DataValue::Null); for (index_pos, ty) in types.iter().enumerate() { - let value = DataValue::memcomparable_decode(&mut reader, ty)?; if let Some(scan_pos) = mapping.scan_index(index_pos) { - values[scan_pos] = value; + let value = DataValue::memcomparable_decode(&mut reader, ty)?; + let target = tuple + .values + .get_mut(scan_pos) + .ok_or(DatabaseError::InvalidIndex)?; + *target = value; + } else { + DataValue::memcomparable_skip(&mut reader, ty)?; } } - Ok(values) + Ok(()) } pub fn decode_index(bytes: &[u8]) -> Result { @@ -1126,9 +1137,9 @@ mod tests { use crate::storage::Storage; use crate::types::index::{Index, IndexMeta, IndexType}; use crate::types::tuple::Tuple; - use crate::types::value::DataValue; - use crate::types::ColumnId; + use crate::types::value::{DataValue, OrderedMapping, TupleMappingRef, Utf8Type}; use crate::types::LogicalType; + use crate::types::{CharLengthUnits, ColumnId}; use rust_decimal::Decimal; use std::collections::BTreeSet; use std::io::Cursor; @@ -1151,6 +1162,59 @@ mod tests { TableCatalog::new("t1".to_string().into(), columns, table_arena.borrow_mut()).unwrap() } + #[test] + fn covered_key_decode_uses_skip_configuration_and_output_mapping() -> Result<(), DatabaseError> + { + let varchar = LogicalType::Varchar(None, CharLengthUnits::Characters); + let char_type = LogicalType::Char(16, CharLengthUnits::Octets); + let values = vec![ + DataValue::Int32(7), + DataValue::Null, + DataValue::Utf8 { + value: "skipped汉字abcdefgh".into(), + ty: Utf8Type::Variable(None), + unit: CharLengthUnits::Characters, + }, + DataValue::Utf8 { + value: "0123456789abcdef".into(), + ty: Utf8Type::Fixed(16), + unit: CharLengthUnits::Octets, + }, + DataValue::Int32(99), + ]; + let index = Index::new(7, &values, IndexType::Normal); + let mut codec = TableCodec::default(); + let key = codec.with_index("cover", &index, Some(&DataValue::Int32(1)), |key, _| { + Ok(key.to_vec()) + })?; + let index_ty = LogicalType::Tuple(vec![ + LogicalType::Integer, + varchar.clone(), + varchar, + char_type, + LogicalType::Integer, + ]); + let mapping = TupleMappingRef::new(&[1, usize::MAX, usize::MAX, usize::MAX, 0], 2); + let mut tuple = Tuple::new(None, Vec::with_capacity(8)); + let allocation = (tuple.values.as_ptr(), tuple.values.capacity()); + for _ in 0..2 { + TableCodec::decode_index_key_into(&mut tuple, &key, &index_ty, &mapping)?; + assert_eq!( + tuple.values, + vec![DataValue::Int32(99), DataValue::Int32(7)] + ); + assert_eq!((tuple.values.as_ptr(), tuple.values.capacity()), allocation); + } + + let mapping = TupleMappingRef::new(&[usize::MAX, 0, usize::MAX, usize::MAX, usize::MAX], 1); + TableCodec::decode_index_key_into(&mut tuple, &key, &index_ty, &mapping)?; + assert_eq!(tuple.values, vec![DataValue::Null]); + assert!( + TableCodec::decode_index_key_into(&mut tuple, &[], &index_ty, &OrderedMapping).is_err() + ); + Ok(()) + } + #[test] fn test_table_codec_tuple() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); diff --git a/src/types/value.rs b/src/types/value.rs index a1e8fe0f..ba3925df 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -1073,7 +1073,14 @@ impl DataValue { #[inline] fn decode_string(reader: &mut R) -> std::io::Result> { let mut result = Vec::new(); + Self::read_memcomparable_string(reader, |group| result.extend_from_slice(group))?; + Ok(result) + } + fn read_memcomparable_string( + reader: &mut R, + mut append: impl FnMut(&[u8]), + ) -> std::io::Result<()> { loop { let mut group = [0u8; ENCODE_GROUP_SIZE]; reader.read_exact(&mut group)?; @@ -1092,14 +1099,40 @@ impl DataValue { } let data_len = ENCODE_GROUP_SIZE - pad_count; - result.extend_from_slice(&group[..data_len]); + append(&group[..data_len]); if pad_count != 0 { - break; + return Ok(()); } } + } - Ok(result) + fn skip_memcomparable_string(reader: &mut R) -> std::io::Result<()> { + Self::read_memcomparable_string(reader, |_| {}) + } + + fn skip_memcomparable_decimal(reader: &mut R) -> Result<(), DatabaseError> { + let flag = reader.read_u8()?; + match flag { + 0x08 | 0x14 | 0x16 | 0x22 => { + reader.read_u8()?; + } + 0x09..=0x13 | 0x17..=0x21 => {} + 0x15 => return Ok(()), + _ => { + return Err(DatabaseError::InvalidValue(format!( + "invalid decimal exponent: {flag}" + ))) + } + } + let negative = (0x07..0x15).contains(&flag); + loop { + let byte = reader.read_u8()?; + let byte = if negative { !byte } else { byte }; + if byte & 1 == 0 { + return Ok(()); + } + } } #[inline] @@ -1189,6 +1222,44 @@ impl DataValue { self.memcomparable_encode_with_null_order(b, false) } + pub(crate) fn memcomparable_skip( + reader: &mut R, + ty: &LogicalType, + ) -> Result<(), DatabaseError> { + if reader.read_u8()? == NULL_TAG { + return Ok(()); + } + let size = match ty { + LogicalType::SqlNull => return Ok(()), + LogicalType::Boolean | LogicalType::Tinyint | LogicalType::UTinyint => 1, + LogicalType::Smallint | LogicalType::USmallint => 2, + LogicalType::Integer + | LogicalType::UInteger + | LogicalType::Float + | LogicalType::Date + | LogicalType::Time(_) => 4, + LogicalType::Bigint + | LogicalType::UBigint + | LogicalType::Double + | LogicalType::DateTime + | LogicalType::TimeStamp(..) => 8, + LogicalType::Char(..) | LogicalType::Varchar(..) | LogicalType::Varbinary(_) => { + Self::skip_memcomparable_string(reader)?; + return Ok(()); + } + LogicalType::Decimal(..) => return Self::skip_memcomparable_decimal(reader), + LogicalType::Tuple(types) => { + for ty in types { + Self::memcomparable_skip(reader, ty)?; + } + return Ok(()); + } + }; + let mut discard = [0u8; 8]; + reader.read_exact(&mut discard[..size])?; + Ok(()) + } + pub fn memcomparable_decode( reader: &mut R, ty: &LogicalType, @@ -2351,6 +2422,175 @@ mod test { ); } + #[test] + fn memcomparable_skip_keeps_following_values_aligned() -> Result<(), DatabaseError> { + let cases = [ + (LogicalType::Integer, DataValue::Int32(-42)), + ( + LogicalType::Varchar(None, CharLengthUnits::Characters), + DataValue::Utf8 { + value: "汉字abcdefgh".into(), + ty: Utf8Type::Variable(None), + unit: CharLengthUnits::Characters, + }, + ), + ( + LogicalType::Char(16, CharLengthUnits::Octets), + DataValue::Utf8 { + value: "0123456789abcdef".into(), + ty: Utf8Type::Fixed(16), + unit: CharLengthUnits::Octets, + }, + ), + ( + LogicalType::Varbinary(None), + DataValue::Binary { + value: vec![0xff; 19], + len: None, + }, + ), + ( + LogicalType::Tuple(vec![ + LogicalType::Integer, + LogicalType::Varchar(None, CharLengthUnits::Characters), + ]), + DataValue::Tuple(vec![DataValue::Int32(1), DataValue::Null]), + ), + ]; + for (ty, value) in cases { + for value in [value, DataValue::Null] { + let mut bytes = Vec::new(); + value.memcomparable_encode(&mut bytes)?; + let boundary = bytes.len(); + DataValue::Int32(99).memcomparable_encode(&mut bytes)?; + let mut reader = Cursor::new(bytes.as_slice()); + DataValue::memcomparable_skip(&mut reader, &ty)?; + assert_eq!(reader.position() as usize, boundary); + assert_eq!( + DataValue::memcomparable_decode(&mut reader, &LogicalType::Integer)?, + DataValue::Int32(99) + ); + assert_eq!(reader.position() as usize, bytes.len()); + } + } + Ok(()) + } + + #[test] + fn memcomparable_skip_matches_all_scalar_payload_widths() -> Result<(), DatabaseError> { + for ty in [ + LogicalType::Boolean, + LogicalType::Tinyint, + LogicalType::Smallint, + LogicalType::Integer, + LogicalType::Bigint, + LogicalType::UTinyint, + LogicalType::USmallint, + LogicalType::UInteger, + LogicalType::UBigint, + LogicalType::Float, + LogicalType::Double, + LogicalType::Date, + LogicalType::DateTime, + LogicalType::Time(Some(3)), + LogicalType::TimeStamp(Some(6), true), + ] { + let mut bytes = Vec::new(); + DataValue::init(&ty).memcomparable_encode(&mut bytes)?; + let boundary = bytes.len(); + DataValue::Int32(99).memcomparable_encode(&mut bytes)?; + let mut reader = Cursor::new(bytes.as_slice()); + DataValue::memcomparable_skip(&mut reader, &ty)?; + assert_eq!(reader.position() as usize, boundary, "{ty:?}"); + assert_eq!( + DataValue::memcomparable_decode(&mut reader, &LogicalType::Integer)?, + DataValue::Int32(99) + ); + } + Ok(()) + } + + #[test] + fn memcomparable_skip_matches_variable_group_boundaries() -> Result<(), DatabaseError> { + for length in [0, 1, 7, 8, 9, 16, 65] { + let value = DataValue::Binary { + value: vec![0xff; length], + len: None, + }; + let mut bytes = Vec::new(); + value.memcomparable_encode(&mut bytes)?; + let mut reader = Cursor::new(bytes.as_slice()); + assert_eq!( + DataValue::memcomparable_decode(&mut reader, &LogicalType::Varbinary(None))?, + value + ); + assert_eq!(reader.position() as usize, bytes.len()); + reader.set_position(0); + DataValue::memcomparable_skip(&mut reader, &LogicalType::Varbinary(None))?; + assert_eq!(reader.position() as usize, bytes.len()); + } + Ok(()) + } + + #[test] + fn memcomparable_skip_rejects_truncation_and_invalid_markers() { + assert!(DataValue::memcomparable_skip( + &mut Cursor::new(&[0, 1, 2][..]), + &LogicalType::Integer + ) + .is_err()); + assert!( + DataValue::memcomparable_skip(&mut Cursor::new(&[][..]), &LogicalType::Integer) + .is_err() + ); + for bytes in [ + vec![0, 1, 2], + vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 246], + vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 255], + ] { + assert!(DataValue::memcomparable_skip( + &mut Cursor::new(bytes.as_slice()), + &LogicalType::Varbinary(None) + ) + .is_err()); + } + for bytes in [vec![0, 7], vec![0, 0x22], vec![0, 0x18, 1]] { + assert!(DataValue::memcomparable_skip( + &mut Cursor::new(bytes.as_slice()), + &LogicalType::Decimal(None, None) + ) + .is_err()); + } + } + + #[cfg(feature = "decimal")] + #[test] + fn memcomparable_skip_decimal_uses_variable_payload() -> Result<(), DatabaseError> { + for text in [ + "0", + "1", + "-1", + "123.456", + "-0.001", + "79228162514264337593543950335", + "0.0000000000000000000000000001", + ] { + let value = DataValue::Decimal(text.parse::().unwrap()); + let mut bytes = Vec::new(); + value.memcomparable_encode(&mut bytes)?; + let boundary = bytes.len(); + DataValue::Int32(99).memcomparable_encode(&mut bytes)?; + let mut reader = Cursor::new(bytes.as_slice()); + DataValue::memcomparable_skip(&mut reader, &LogicalType::Decimal(None, None))?; + assert_eq!(reader.position() as usize, boundary); + assert_eq!( + DataValue::memcomparable_decode(&mut reader, &LogicalType::Integer)?, + DataValue::Int32(99) + ); + } + Ok(()) + } + #[test] fn test_memcomparable_decode_rejects_invalid_strings() { let mut invalid_marker = vec![NOTNULL_TAG]; From a691506a462278d45c6d6ada013429b7e2df7ff1 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 20/62] perf: move single-use projection input values --- src/binder/parser.rs | 2 +- src/binder/select.rs | 6 +- src/db.rs | 13 +- src/execution/ddl/create_index.rs | 2 +- src/execution/dml/analyze.rs | 2 +- src/execution/dml/delete.rs | 2 +- src/execution/dml/insert.rs | 2 +- src/execution/dml/update.rs | 4 +- .../dql/aggregate/stream_distinct.rs | 2 +- src/execution/dql/join/hash/full_join.rs | 4 +- src/execution/dql/join/hash/left_join.rs | 4 +- src/execution/dql/join/hash_join.rs | 6 +- src/execution/dql/join/mod.rs | 92 ------- src/execution/dql/join/nested_loop_join.rs | 10 +- src/execution/dql/projection.rs | 12 +- src/execution/dql/recursive_cte.rs | 4 +- src/execution/mod.rs | 14 +- src/expression/evaluator.rs | 8 +- src/expression/function/scala.rs | 4 +- src/function/abs.rs | 4 +- src/function/char_length.rs | 4 +- src/function/current_date.rs | 4 +- src/function/current_timestamp.rs | 4 +- src/function/lower.rs | 4 +- src/function/octet_length.rs | 4 +- src/function/upper.rs | 4 +- src/macros/mod.rs | 2 +- src/optimizer/heuristic/optimizer.rs | 4 +- .../rule/implementation/dql/filter.rs | 2 +- .../rule/normalization/combine_operators.rs | 14 +- .../rule/normalization/guard_mark_apply.rs | 2 +- .../rule/normalization/join_reorder.rs | 16 +- src/optimizer/rule/normalization/mod.rs | 10 +- .../rule/normalization/parameterized_index.rs | 8 +- .../rule/normalization/project_move.rs | 254 ++++++++++++++++++ src/planner/arena.rs | 4 +- src/planner/mod.rs | 2 +- src/planner/operator/mod.rs | 7 +- src/planner/operator/project.rs | 11 + src/planner/operator/visitor.rs | 8 +- src/types/bitset.rs | 141 ++++++++++ src/types/mod.rs | 1 + src/types/tuple.rs | 72 +++++ 43 files changed, 585 insertions(+), 194 deletions(-) create mode 100644 src/optimizer/rule/normalization/project_move.rs create mode 100644 src/types/bitset.rs diff --git a/src/binder/parser.rs b/src/binder/parser.rs index ca72acae..c1acb0b1 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -1318,7 +1318,7 @@ where }) .collect(); plan = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs }), + Operator::Project(ProjectOperator::new(exprs)), Childrens::Only(Box::new(plan)), ); } diff --git a/src/binder/select.rs b/src/binder/select.rs index 7bba39d8..07fff8c2 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -987,14 +987,14 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b if !left_cast.is_empty() { left_plan = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: left_cast }), + Operator::Project(ProjectOperator::new(left_cast)), Childrens::Only(Box::new(left_plan)), ); } if !right_cast.is_empty() { right_plan = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: right_cast }), + Operator::Project(ProjectOperator::new(right_cast)), Childrens::Only(Box::new(right_plan)), ); } @@ -1950,7 +1950,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b select_list: Vec, ) -> LogicalPlan { LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: select_list }), + Operator::Project(ProjectOperator::new(select_list)), Childrens::Only(Box::new(children)), ) } diff --git a/src/db.rs b/src/db.rs index 708ffcc9..3d8f13f9 100644 --- a/src/db.rs +++ b/src/db.rs @@ -544,6 +544,11 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { HepBatchStrategy::fix_point_topdown(10), vec![NormalizationRuleImpl::PushLimitIntoTableScan], ) + .after_batch( + "Move Project Inputs", + HepBatchStrategy::fix_point_bottomup(1), + vec![NormalizationRuleImpl::ProjectMove], + ) .implementations(vec![ // DQL ImplementationRuleImpl::SimpleAggregate, @@ -1623,9 +1628,7 @@ pub(crate) mod test { }; // Repeating the selected column must not lose an output slot. let mut plan = LogicalPlan::new( - Operator::Project(ProjectOperator { - exprs: vec![output, output, small], - }), + Operator::Project(ProjectOperator::new(vec![output, output, small])), Childrens::Only(Box::new(join)), ); let schema = plan.output_schema(&mut arena).clone(); @@ -1692,9 +1695,7 @@ pub(crate) mod test { ty: LogicalType::Integer, }); let mut plan = LogicalPlan::new( - Operator::Project(ProjectOperator { - exprs: vec![computed, small], - }), + Operator::Project(ProjectOperator::new(vec![computed, small])), Childrens::Only(Box::new(join)), ); let schema = plan.output_schema(&mut arena).clone(); diff --git a/src/execution/ddl/create_index.rs b/src/execution/ddl/create_index.rs index 2dd43683..3a3fe894 100644 --- a/src/execution/ddl/create_index.rs +++ b/src/execution/ddl/create_index.rs @@ -120,7 +120,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CreateIndex<'a> { if arena.result_tuple().pk.is_none() { continue; } - arena.rewrite(&column_exprs, plan_arena, None::<&[DataValue]>)?; + arena.rewrite(&column_exprs, plan_arena, None::<&[DataValue]>, None)?; { let mut state = arena.local_state(plan_arena); let (tuple, transaction, table_codec) = state.tuple_transaction_codec_mut(); diff --git a/src/execution/dml/analyze.rs b/src/execution/dml/analyze.rs index 3e49ae68..d3988efe 100644 --- a/src/execution/dml/analyze.rs +++ b/src/execution/dml/analyze.rs @@ -143,7 +143,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Analyze<'a> { let value = match exprs.as_slice() { [expr] => expr.eval(plan_arena, Some(&tuple))?.into_owned(), _ => { - arena.rewrite(exprs, plan_arena, Some(&tuple))?; + arena.rewrite(exprs, plan_arena, Some(&tuple), None)?; DataValue::Tuple(arena.materialize_tuple().values) } }; diff --git a/src/execution/dml/delete.rs b/src/execution/dml/delete.rs index 301e6c98..0af17ff8 100644 --- a/src/execution/dml/delete.rs +++ b/src/execution/dml/delete.rs @@ -94,7 +94,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Delete<'a> { continue; }; for (index_id, index_ty, exprs) in index_templates.iter() { - arena.rewrite(exprs, plan_arena, Some(&tuple))?; + arena.rewrite(exprs, plan_arena, Some(&tuple), None)?; let mut state = arena.local_state(plan_arena); let (values, transaction, table_codec) = state.index_values_transaction_codec_mut(); let index = Index::new(*index_id, values, *index_ty); diff --git a/src/execution/dml/insert.rs b/src/execution/dml/insert.rs index a2838170..aa4987d5 100644 --- a/src/execution/dml/insert.rs +++ b/src/execution/dml/insert.rs @@ -153,7 +153,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Insert<'a> { for (index_meta, exprs) in table_snapshot.index_metas.iter() { let index_meta = plan_arena.index(*index_meta); let tuple_id = tuple.pk.as_ref().ok_or(DatabaseError::PrimaryKeyNotFound)?; - arena.rewrite(exprs, plan_arena, Some(&tuple))?; + arena.rewrite(exprs, plan_arena, Some(&tuple), None)?; let mut state = arena.local_state(plan_arena); let (values, transaction, table_codec) = state.index_values_transaction_codec_mut(); diff --git a/src/execution/dml/update.rs b/src/execution/dml/update.rs index 2a722afa..b20106db 100644 --- a/src/execution/dml/update.rs +++ b/src/execution/dml/update.rs @@ -151,7 +151,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Update<'a> { continue; } - arena.rewrite(exprs, plan_arena, Some(&tuple))?; + arena.rewrite(exprs, plan_arena, Some(&tuple), None)?; let mut state = arena.local_state(plan_arena); let (values, transaction, table_codec) = state.index_values_transaction_codec_mut(); @@ -189,7 +189,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Update<'a> { ) { continue; } - arena.rewrite(exprs, plan_arena, Some(&tuple))?; + arena.rewrite(exprs, plan_arena, Some(&tuple), None)?; let mut state = arena.local_state(plan_arena); let (values, transaction, table_codec) = state.index_values_transaction_codec_mut(); diff --git a/src/execution/dql/aggregate/stream_distinct.rs b/src/execution/dql/aggregate/stream_distinct.rs index a6f7b3be..09574872 100644 --- a/src/execution/dql/aggregate/stream_distinct.rs +++ b/src/execution/dql/aggregate/stream_distinct.rs @@ -74,7 +74,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for StreamDistinctExecutor<'a> } return Ok(()); } - arena.rewrite(self.groupby_exprs, plan_arena, None::<&[DataValue]>)?; + arena.rewrite(self.groupby_exprs, plan_arena, None::<&[DataValue]>, None)?; if let Some(last_keys) = &mut self.last_keys { if last_keys.values == arena.result_tuple().values { diff --git a/src/execution/dql/join/hash/full_join.rs b/src/execution/dql/join/hash/full_join.rs index ff9de82a..50860871 100644 --- a/src/execution/dql/join/hash/full_join.rs +++ b/src/execution/dql/join/hash/full_join.rs @@ -17,16 +17,16 @@ use crate::execution::dql::join::hash::{ filter, JoinProbeState, LeftDropState, LeftDropTuples, ProbeState, }; use crate::execution::dql::join::hash_join::BuildState; -use crate::execution::dql::join::RowBitmap; use crate::planner::ExprRef; use crate::planner::MetaArena; +use crate::types::bitset::BitSet; use crate::types::tuple::{SplitTupleRef, Tuple}; use crate::types::value::DataValue; pub(crate) struct FullJoinState { pub(crate) left_schema_len: usize, pub(crate) right_schema_len: usize, - pub(crate) bits: RowBitmap, + pub(crate) bits: BitSet, } impl JoinProbeState for FullJoinState { diff --git a/src/execution/dql/join/hash/left_join.rs b/src/execution/dql/join/hash/left_join.rs index 5d4ebc82..0d30df2a 100644 --- a/src/execution/dql/join/hash/left_join.rs +++ b/src/execution/dql/join/hash/left_join.rs @@ -17,16 +17,16 @@ use crate::execution::dql::join::hash::{ filter, JoinProbeState, LeftDropState, LeftDropTuples, ProbeState, }; use crate::execution::dql::join::hash_join::BuildState; -use crate::execution::dql::join::RowBitmap; use crate::planner::ExprRef; use crate::planner::MetaArena; +use crate::types::bitset::BitSet; use crate::types::tuple::{SplitTupleRef, Tuple}; use crate::types::value::DataValue; pub(crate) struct LeftJoinState { pub(crate) left_schema_len: usize, pub(crate) right_schema_len: usize, - pub(crate) bits: RowBitmap, + pub(crate) bits: BitSet, } impl JoinProbeState for LeftJoinState { diff --git a/src/execution/dql/join/hash_join.rs b/src/execution/dql/join/hash_join.rs index 716f651d..3e3e6bb4 100644 --- a/src/execution/dql/join/hash_join.rs +++ b/src/execution/dql/join/hash_join.rs @@ -20,7 +20,6 @@ use crate::execution::dql::join::hash::right_join::RightJoinState; use crate::execution::dql::join::hash::{ JoinProbeState, JoinProbeStateImpl, LeftDropState, ProbeState, }; -use crate::execution::dql::join::RowBitmap; use crate::execution::dql::sort::BumpVec; use crate::execution::{ build_read, ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor, @@ -30,6 +29,7 @@ use crate::planner::ExprRef; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::Transaction; +use crate::types::bitset::BitSet; use crate::types::tuple::Tuple; use crate::types::value::DataValue; use bumpalo::Bump; @@ -207,13 +207,13 @@ impl HashJoin { JoinType::LeftOuter => JoinProbeStateImpl::Left(LeftJoinState { left_schema_len, right_schema_len, - bits: RowBitmap::with_capacity(build_count), + bits: BitSet::with_capacity(build_count), }), JoinType::RightOuter => JoinProbeStateImpl::Right(RightJoinState { left_schema_len }), JoinType::Full => JoinProbeStateImpl::Full(FullJoinState { left_schema_len, right_schema_len, - bits: RowBitmap::with_capacity(build_count), + bits: BitSet::with_capacity(build_count), }), JoinType::Cross => unreachable!(), } diff --git a/src/execution/dql/join/mod.rs b/src/execution/dql/join/mod.rs index c72cae3f..d15176f2 100644 --- a/src/execution/dql/join/mod.rs +++ b/src/execution/dql/join/mod.rs @@ -18,40 +18,6 @@ mod hash; pub(crate) mod hash_join; pub(crate) mod nested_loop_join; -pub(crate) struct RowBitmap { - blocks: Vec, - len: usize, -} - -impl RowBitmap { - pub(crate) fn with_capacity(len: usize) -> Self { - let bits_per_block = usize::BITS as usize; - let blocks = len.div_ceil(bits_per_block); - RowBitmap { - blocks: vec![0; blocks], - len, - } - } - - pub(crate) fn insert(&mut self, index: usize) { - assert!( - index < self.len, - "insert at index {index} exceeds bitmap size {}", - self.len - ); - let bits_per_block = usize::BITS as usize; - self.blocks[index / bits_per_block] |= 1usize << (index % bits_per_block); - } - - pub(crate) fn contains(&self, index: usize) -> bool { - if index >= self.len { - return false; - } - let bits_per_block = usize::BITS as usize; - (self.blocks[index / bits_per_block] & (1usize << (index % bits_per_block))) != 0 - } -} - pub fn joins_nullable(join_type: &JoinType) -> (bool, bool) { match join_type { JoinType::Inner => (false, false), @@ -61,61 +27,3 @@ pub fn joins_nullable(join_type: &JoinType) -> (bool, bool) { JoinType::Cross => (true, true), } } - -// GRCOV_EXCL_START -#[cfg(test)] -mod tests { - use super::RowBitmap; - - #[test] - fn row_bitmap_contains_false_for_empty_bitmap() { - let bitmap = RowBitmap::with_capacity(0); - - assert!(!bitmap.contains(0)); - assert!(!bitmap.contains(usize::MAX)); - } - - #[test] - fn row_bitmap_contains_false_for_unset_bits_within_capacity() { - let bitmap = RowBitmap::with_capacity(usize::BITS as usize + 1); - - assert!(!bitmap.contains(0)); - assert!(!bitmap.contains(usize::BITS as usize)); - } - - #[test] - fn row_bitmap_insert_marks_bits_across_blocks() { - let boundary = usize::BITS as usize; - let mut bitmap = RowBitmap::with_capacity(boundary + 2); - - bitmap.insert(0); - bitmap.insert(boundary - 1); - bitmap.insert(boundary); - bitmap.insert(boundary + 1); - - assert!(bitmap.contains(0)); - assert!(bitmap.contains(boundary - 1)); - assert!(bitmap.contains(boundary)); - assert!(bitmap.contains(boundary + 1)); - assert!(!bitmap.contains(1)); - } - - #[test] - fn row_bitmap_contains_returns_false_past_capacity() { - let mut bitmap = RowBitmap::with_capacity(2); - - bitmap.insert(1); - - assert!(!bitmap.contains(2)); - assert!(!bitmap.contains(usize::MAX)); - } - - #[test] - #[should_panic(expected = "exceeds bitmap size")] - fn row_bitmap_insert_panics_past_capacity() { - let mut bitmap = RowBitmap::with_capacity(1); - - bitmap.insert(1); - } -} -// GRCOV_EXCL_STOP diff --git a/src/execution/dql/join/nested_loop_join.rs b/src/execution/dql/join/nested_loop_join.rs index b841c16d..75863995 100644 --- a/src/execution/dql/join/nested_loop_join.rs +++ b/src/execution/dql/join/nested_loop_join.rs @@ -19,13 +19,13 @@ use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::errors::DatabaseError; -use crate::execution::dql::join::RowBitmap; use crate::execution::{ build_read, ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor, }; use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::ExprRef; use crate::storage::Transaction; +use crate::types::bitset::BitSet; use crate::types::tuple::{SplitTupleRef, Tuple}; use crate::types::value::DataValue; @@ -85,15 +85,15 @@ pub struct NestedLoopJoin { enum NestedLoopJoinState { PullLeft { - right_bitmap: Option, + right_bitmap: Option, }, ScanRight { active_left: ActiveLeftState, - right_bitmap: Option, + right_bitmap: Option, }, EmitRightUnmatched { right_input: ExecId, - right_bitmap: Option, + right_bitmap: Option, right_emit_index: usize, }, End, @@ -257,7 +257,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin { bits.insert(idx); } } else { - let mut bits = RowBitmap::with_capacity(active_left.right_index); + let mut bits = BitSet::with_capacity(active_left.right_index); for idx in active_left.first_matches { bits.insert(idx); } diff --git a/src/execution/dql/projection.rs b/src/execution/dql/projection.rs index eadf834b..11cf4b99 100644 --- a/src/execution/dql/projection.rs +++ b/src/execution/dql/projection.rs @@ -21,10 +21,12 @@ use crate::planner::ExprRef; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::Transaction; +use crate::types::bitset::BitSet; use crate::types::value::DataValue; pub struct Projection<'a> { exprs: &'a [ExprRef], + moveable: Option<&'a BitSet>, input: ExecId, } @@ -32,14 +34,18 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Projection<'a> { type Input = (&'a ProjectOperator, &'a LogicalPlan); fn into_executor( - (ProjectOperator { exprs }, input): Self::Input, + (ProjectOperator { exprs, moveable }, input): Self::Input, arena: &mut ExecArena<'a, T>, plan_arena: &mut (dyn MetaArena + 'a), cache: ExecutionContext<'_>, transaction: &T, ) -> ExecId { let input = build_read(arena, plan_arena, input, cache, transaction); - arena.push(ExecNode::Projection(Projection { exprs, input })) + arena.push(ExecNode::Projection(Projection { + exprs, + moveable: moveable.as_ref(), + input, + })) } } @@ -62,7 +68,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Projection<'a> { return Ok(()); } - arena.rewrite(self.exprs, plan_arena, None::<&[DataValue]>)?; + arena.rewrite(self.exprs, plan_arena, None::<&[DataValue]>, self.moveable)?; arena.resume(); Ok(()) } diff --git a/src/execution/dql/recursive_cte.rs b/src/execution/dql/recursive_cte.rs index 48ccb759..43633a1c 100644 --- a/src/execution/dql/recursive_cte.rs +++ b/src/execution/dql/recursive_cte.rs @@ -502,9 +502,7 @@ mod tests { ty: LogicalType::Integer, }); let recursive = LogicalPlan::new( - Operator::Project(ProjectOperator { - exprs: vec![project_expr], - }), + Operator::Project(ProjectOperator::new(vec![project_expr])), Childrens::Only(Box::new(filter)), ); diff --git a/src/execution/mod.rs b/src/execution/mod.rs index cc784c71..35ab5bf2 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -81,8 +81,9 @@ use crate::planner::MetaArena; use crate::planner::{ExecMetaArena, ExprRef, LogicalPlan, PlanKeeper}; use crate::storage::table_codec::TableCodec; use crate::storage::{StatisticsMetaCache, TableCache, Transaction, ViewCache}; +use crate::types::bitset::BitSet; use crate::types::index::RuntimeIndexProbe; -use crate::types::tuple::{Tuple, TupleLike}; +use crate::types::tuple::{Input, MoveableTuple, Tuple}; use crate::types::value::DataValue; #[derive(Clone, Copy)] @@ -789,23 +790,26 @@ impl<'a, T: Transaction + 'a> ExecArena<'a, T> { std::mem::take(&mut self.result.tuple) } - pub(crate) fn rewrite( + pub(crate) fn rewrite( &mut self, exprs: &[E], arena: &dyn MetaArena, input: Option<&TInput>, + moveable: Option<&BitSet>, ) -> Result<(), DatabaseError> { let values = &mut self.result.tuple.values; let base = values.len(); values.reserve(exprs.len()); - + // SAFETY: only the fixed input prefix is exposed. All output capacity + // is reserved before creating the view, and each Cow is owned before + // push. No source slice is subsequently materialized or resized. + let local = unsafe { MoveableTuple::from_raw_parts(values.as_mut_ptr(), base, moveable) }; for expr in exprs { let value = { - let input_values = &values[..base]; let expression = expr.expression(arena); match input { Some(input) => expression.eval(arena, Some(input)), - None => expression.eval(arena, Some(&input_values)), + None => expression.eval(arena, Some(&local)), } .map(|value| value.into_owned()) }; diff --git a/src/expression/evaluator.rs b/src/expression/evaluator.rs index 51212489..7bc35a99 100644 --- a/src/expression/evaluator.rs +++ b/src/expression/evaluator.rs @@ -21,7 +21,7 @@ use crate::expression::function::scala::ScalarFunction; use crate::expression::{AliasType, BinaryOperator, ScalarExpression, TrimWhereField}; use crate::planner::MetaArena; use crate::types::evaluator::binary_create; -use crate::types::tuple::TupleLike; +use crate::types::tuple::{Input, InputRef}; use crate::types::value::{DataValue, Utf8Type}; use crate::types::{CharLengthUnits, LogicalType}; use std::borrow::Cow; @@ -43,7 +43,7 @@ macro_rules! eval_to_num { } impl ScalarExpression { - pub fn eval<'a, T: TupleLike + ?Sized>( + pub fn eval<'a, T: Input + ?Sized>( &'a self, arena: &'a (dyn MetaArena + '_), tuple: Option<&'a T>, @@ -67,7 +67,7 @@ impl ScalarExpression { let Some(tuple) = tuple else { return Ok(Cow::Owned(DataValue::Null)); }; - Ok(Cow::Borrowed(tuple.value_at(*position))) + Ok(tuple.column(*position)) } ScalarExpression::Alias { expr, alias } => { let Some(tuple) = tuple else { @@ -280,7 +280,7 @@ impl ScalarExpression { } ScalarExpression::ScalaFunction(ScalarFunction { inner, args, .. }) => { let value = match tuple { - Some(tuple) => inner.eval(args, arena, Some(&tuple))?, + Some(tuple) => inner.eval(args, arena, Some(&InputRef(tuple)))?, None => inner.eval(args, arena, None)?, }; value.cast(inner.return_type()).map(Cow::Owned) diff --git a/src/expression/function/scala.rs b/src/expression/function/scala.rs index c2e755da..9160fb15 100644 --- a/src/expression/function/scala.rs +++ b/src/expression/function/scala.rs @@ -16,7 +16,7 @@ use crate::errors::DatabaseError; use crate::expression::function::FunctionSummary; use crate::planner::ExprRef; use crate::planner::MetaArena; -use crate::types::tuple::TupleLike; +use crate::types::tuple::Input; use crate::types::value::DataValue; use crate::types::LogicalType; use kite_sql_serde_macros::ReferenceSerialization; @@ -66,7 +66,7 @@ pub trait ScalarFunctionImpl: Debug + Send + Sync { &self, args: &[ExprRef], arena: &(dyn MetaArena + '_), - tuple: Option<&dyn TupleLike>, + tuple: Option<&dyn Input>, ) -> Result; // TODO: Exploiting monotonicity when optimizing `ScalarFunctionImpl::monotonicity()` diff --git a/src/function/abs.rs b/src/function/abs.rs index 9f5cd34c..e5dbbd84 100644 --- a/src/function/abs.rs +++ b/src/function/abs.rs @@ -18,7 +18,7 @@ use crate::expression::function::scala::ScalarFunctionImpl; use crate::expression::function::FunctionSummary; use crate::planner::ExprRef; use crate::planner::MetaArena; -use crate::types::tuple::TupleLike; +use crate::types::tuple::Input; use crate::types::value::DataValue; use crate::types::LogicalType; use ordered_float::OrderedFloat; @@ -62,7 +62,7 @@ impl ScalarFunctionImpl for Abs { &self, exprs: &[ExprRef], arena: &(dyn MetaArena + '_), - tuples: Option<&dyn TupleLike>, + tuples: Option<&dyn Input>, ) -> Result { let value = arena.expression(exprs[0]).eval(arena, tuples)?; let overflow = || DatabaseError::OverFlow; diff --git a/src/function/char_length.rs b/src/function/char_length.rs index e2a49bbc..a999586d 100644 --- a/src/function/char_length.rs +++ b/src/function/char_length.rs @@ -18,7 +18,7 @@ use crate::expression::function::scala::ScalarFunctionImpl; use crate::expression::function::FunctionSummary; use crate::planner::ExprRef; use crate::planner::MetaArena; -use crate::types::tuple::TupleLike; +use crate::types::tuple::Input; use crate::types::value::DataValue; use crate::types::CharLengthUnits; use crate::types::LogicalType; @@ -46,7 +46,7 @@ impl ScalarFunctionImpl for CharLength { &self, exprs: &[ExprRef], arena: &(dyn MetaArena + '_), - tuples: Option<&dyn TupleLike>, + tuples: Option<&dyn Input>, ) -> Result { let mut value = arena.expression(exprs[0]).eval(arena, tuples)?; // SQL: the length of NULL is NULL, not 0. diff --git a/src/function/current_date.rs b/src/function/current_date.rs index 1deb47bf..7d4353bf 100644 --- a/src/function/current_date.rs +++ b/src/function/current_date.rs @@ -18,7 +18,7 @@ use crate::expression::function::scala::ScalarFunctionImpl; use crate::expression::function::FunctionSummary; use crate::planner::ExprRef; use crate::planner::MetaArena; -use crate::types::tuple::TupleLike; +use crate::types::tuple::Input; use crate::types::value::DataValue; use crate::types::LogicalType; use chrono::{Datelike, Local}; @@ -48,7 +48,7 @@ impl ScalarFunctionImpl for CurrentDate { &self, _: &[ExprRef], _: &(dyn MetaArena + '_), - _: Option<&dyn TupleLike>, + _: Option<&dyn Input>, ) -> Result { Ok(DataValue::Date32(Local::now().num_days_from_ce())) } diff --git a/src/function/current_timestamp.rs b/src/function/current_timestamp.rs index b8a00f4d..be3f72a4 100644 --- a/src/function/current_timestamp.rs +++ b/src/function/current_timestamp.rs @@ -18,7 +18,7 @@ use crate::expression::function::scala::ScalarFunctionImpl; use crate::expression::function::FunctionSummary; use crate::planner::ExprRef; use crate::planner::MetaArena; -use crate::types::tuple::TupleLike; +use crate::types::tuple::Input; use crate::types::value::DataValue; use crate::types::LogicalType; use chrono::Utc; @@ -48,7 +48,7 @@ impl ScalarFunctionImpl for CurrentTimeStamp { &self, _: &[ExprRef], _: &(dyn MetaArena + '_), - _: Option<&dyn TupleLike>, + _: Option<&dyn Input>, ) -> Result { Ok(DataValue::Time64(Utc::now().timestamp(), 0, false)) } diff --git a/src/function/lower.rs b/src/function/lower.rs index 8e4e8b53..a4fc59b8 100644 --- a/src/function/lower.rs +++ b/src/function/lower.rs @@ -18,7 +18,7 @@ use crate::expression::function::scala::ScalarFunctionImpl; use crate::expression::function::FunctionSummary; use crate::planner::ExprRef; use crate::planner::MetaArena; -use crate::types::tuple::TupleLike; +use crate::types::tuple::Input; use crate::types::value::DataValue; use crate::types::CharLengthUnits; use crate::types::LogicalType; @@ -48,7 +48,7 @@ impl ScalarFunctionImpl for Lower { &self, exprs: &[ExprRef], arena: &(dyn MetaArena + '_), - tuples: Option<&dyn TupleLike>, + tuples: Option<&dyn Input>, ) -> Result { let mut value = arena.expression(exprs[0]).eval(arena, tuples)?; if !matches!(value.logical_type(), LogicalType::Varchar(_, _)) { diff --git a/src/function/octet_length.rs b/src/function/octet_length.rs index 0a0ad13b..80474722 100644 --- a/src/function/octet_length.rs +++ b/src/function/octet_length.rs @@ -18,7 +18,7 @@ use crate::expression::function::scala::ScalarFunctionImpl; use crate::expression::function::FunctionSummary; use crate::planner::ExprRef; use crate::planner::MetaArena; -use crate::types::tuple::TupleLike; +use crate::types::tuple::Input; use crate::types::value::DataValue; use crate::types::CharLengthUnits; use crate::types::LogicalType; @@ -47,7 +47,7 @@ impl ScalarFunctionImpl for OctetLength { &self, exprs: &[ExprRef], arena: &(dyn MetaArena + '_), - tuples: Option<&dyn TupleLike>, + tuples: Option<&dyn Input>, ) -> Result { let mut value = arena.expression(exprs[0]).eval(arena, tuples)?; // SQL: the length of NULL is NULL, not 0. diff --git a/src/function/upper.rs b/src/function/upper.rs index c6da1e46..b121d83b 100644 --- a/src/function/upper.rs +++ b/src/function/upper.rs @@ -18,7 +18,7 @@ use crate::expression::function::scala::ScalarFunctionImpl; use crate::expression::function::FunctionSummary; use crate::planner::ExprRef; use crate::planner::MetaArena; -use crate::types::tuple::TupleLike; +use crate::types::tuple::Input; use crate::types::value::DataValue; use crate::types::CharLengthUnits; use crate::types::LogicalType; @@ -48,7 +48,7 @@ impl ScalarFunctionImpl for Upper { &self, exprs: &[ExprRef], arena: &(dyn MetaArena + '_), - tuples: Option<&dyn TupleLike>, + tuples: Option<&dyn Input>, ) -> Result { let mut value = arena.expression(exprs[0]).eval(arena, tuples)?; if !matches!(value.logical_type(), LogicalType::Varchar(_, _)) { diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 18a87a1d..46812b0b 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -102,7 +102,7 @@ macro_rules! scala_function { } } impl ::kite_sql::expression::function::scala::ScalarFunctionImpl for $struct_name { #[allow(unused_variables, clippy::redundant_closure_call)] - fn eval(&self, args: &[::kite_sql::planner::ExprRef], arena: &dyn ::kite_sql::planner::MetaArena, tuple: Option<&dyn ::kite_sql::types::tuple::TupleLike>) -> Result<::kite_sql::types::value::DataValue, ::kite_sql::errors::DatabaseError> { + fn eval(&self, args: &[::kite_sql::planner::ExprRef], arena: &dyn ::kite_sql::planner::MetaArena, tuple: Option<&dyn ::kite_sql::types::tuple::Input>) -> Result<::kite_sql::types::value::DataValue, ::kite_sql::errors::DatabaseError> { let mut _index = 0; $closure($({ diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index b69e7ac6..5975806d 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -903,7 +903,7 @@ mod tests { let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::None); for _ in 0..3 { plan = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: Vec::new() }), + Operator::Project(ProjectOperator::new(Vec::new())), Childrens::Only(Box::new(plan)), ); } @@ -934,7 +934,7 @@ mod tests { let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::None); for _ in 0..3 { plan = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: Vec::new() }), + Operator::Project(ProjectOperator::new(Vec::new())), Childrens::Only(Box::new(plan)), ); } diff --git a/src/optimizer/rule/implementation/dql/filter.rs b/src/optimizer/rule/implementation/dql/filter.rs index 4d7e2afa..6a594b92 100644 --- a/src/optimizer/rule/implementation/dql/filter.rs +++ b/src/optimizer/rule/implementation/dql/filter.rs @@ -558,7 +558,7 @@ mod tests { let column = alias.output_column_ref(&mut arena); let key = arena.alloc_expression(ScalarExpression::column_expr(column, 0)); let mut project = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: vec![alias] }), + Operator::Project(ProjectOperator::new(vec![alias])), Childrens::Only(Box::new(scan)), ); project.physical_option = Some(PhysicalOption::new( diff --git a/src/optimizer/rule/normalization/combine_operators.rs b/src/optimizer/rule/normalization/combine_operators.rs index a5a89b1e..225d9a9b 100644 --- a/src/optimizer/rule/normalization/combine_operators.rs +++ b/src/optimizer/rule/normalization/combine_operators.rs @@ -431,18 +431,14 @@ mod tests { let table_arena = crate::planner::TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); let child = LogicalPlan::new( - Operator::Project(ProjectOperator { - exprs: vec![ - column_expr(&mut arena, "c2", 1), - column_expr(&mut arena, "c1", 0), - ], - }), + Operator::Project(ProjectOperator::new(vec![ + column_expr(&mut arena, "c2", 1), + column_expr(&mut arena, "c1", 0), + ])), Childrens::Only(Box::new(LogicalPlan::new(Operator::Dummy, Childrens::None))), ); let mut plan = LogicalPlan::new( - Operator::Project(ProjectOperator { - exprs: vec![column_expr(&mut arena, "c2", 0)], - }), + Operator::Project(ProjectOperator::new(vec![column_expr(&mut arena, "c2", 0)])), Childrens::Only(Box::new(child)), ); diff --git a/src/optimizer/rule/normalization/guard_mark_apply.rs b/src/optimizer/rule/normalization/guard_mark_apply.rs index 12ba2188..34a20470 100644 --- a/src/optimizer/rule/normalization/guard_mark_apply.rs +++ b/src/optimizer/rule/normalization/guard_mark_apply.rs @@ -370,7 +370,7 @@ mod tests { }); let filter = FilterOperator::build(predicate, mark, false); let plan = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: vec![flag] }), + Operator::Project(ProjectOperator::new(vec![flag])), Childrens::Only(Box::new(filter)), ); Ok(Self { plan, flag, marker }) diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 033d685e..c57d1df2 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -132,7 +132,7 @@ fn restore_join_output( }) .collect(); let mut plan = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs }), + Operator::Project(ProjectOperator::new(exprs)), Childrens::Only(Box::new(join)), ); plan.physical_option = Some(PhysicalOption::new( @@ -1452,7 +1452,7 @@ mod tests { }); let output = alias.output_column_ref(&mut arena); let mut projected = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: vec![alias] }), + Operator::Project(ProjectOperator::new(vec![alias])), Childrens::Only(Box::new(left)), ); projected.physical_option = Some(PhysicalOption::new( @@ -1539,7 +1539,7 @@ mod tests { let output = alias.output_column_ref(&mut arena); let (source, _) = input("l", 10, &mut arena); let mut project = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: vec![alias] }), + Operator::Project(ProjectOperator::new(vec![alias])), Childrens::Only(Box::new(source)), ); project.physical_option = Some(PhysicalOption::new( @@ -1592,7 +1592,7 @@ mod tests { }); let output = alias.output_column_ref(&mut arena); let mut project = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: vec![alias] }), + Operator::Project(ProjectOperator::new(vec![alias])), Childrens::Only(Box::new(scan)), ); assert_eq!( @@ -1858,7 +1858,7 @@ mod tests { let (left, l, li) = indexed_input("l", 1000, Some(100), &mut arena); let (right, r, _) = indexed_input("r", 200, Some(100), &mut arena); let mut left = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: vec![l] }), + Operator::Project(ProjectOperator::new(vec![l])), Childrens::Only(Box::new(left)), ); left.physical_option = Some(PhysicalOption::new( @@ -2028,7 +2028,7 @@ mod tests { }) .collect(); let mut project = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs }), + Operator::Project(ProjectOperator::new(exprs)), Childrens::Only(Box::new(join)), ); project.physical_option = Some(PhysicalOption::new( @@ -2064,7 +2064,7 @@ mod tests { assert_eq!(sort.estimated_startup_work()?, Some(192)); assert_eq!(sort.estimated_work()?, Some(256)); let mut project = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: vec![key] }), + Operator::Project(ProjectOperator::new(vec![key])), Childrens::Only(Box::new(sort)), ); project.physical_option = Some(PhysicalOption::new( @@ -2706,7 +2706,7 @@ mod tests { }) .collect(); let mut plan = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs }), + Operator::Project(ProjectOperator::new(exprs)), Childrens::Only(Box::new(join)), ); let original_parent = plan.operator.clone(); diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index 45097a4a..cc51ce61 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -48,6 +48,8 @@ mod min_max_top_k; use guard_mark_apply::GuardMarkApply; use join_reorder::{GreedyJoinReorder, OrientNestedLoopJoin}; use materialize_mark_apply::MaterializeMarkApply; +mod project_move; +use project_move::ProjectMove; mod parameterized_index; mod pushdown_limit; mod pushdown_predicates; @@ -91,6 +93,7 @@ pub enum NormalizationRuleImpl { OrientNestedLoopJoin, MaterializeMarkApply, GuardMarkApply, + ProjectMove, } #[derive(Debug, Copy, Clone, Eq, PartialEq)] @@ -180,9 +183,9 @@ impl NormalizationRuleImpl { pub fn root_tag(&self) -> NormalizationRuleRootTag { match self { NormalizationRuleImpl::ColumnPruning => NormalizationRuleRootTag::Any, - NormalizationRuleImpl::CollapseProject | NormalizationRuleImpl::GuardMarkApply => { - NormalizationRuleRootTag::Project - } + NormalizationRuleImpl::CollapseProject + | NormalizationRuleImpl::GuardMarkApply + | NormalizationRuleImpl::ProjectMove => NormalizationRuleRootTag::Project, NormalizationRuleImpl::CollapseGroupByAgg => NormalizationRuleRootTag::Aggregate, NormalizationRuleImpl::CombineFilter => NormalizationRuleRootTag::Filter, NormalizationRuleImpl::LimitProjectTranspose @@ -223,6 +226,7 @@ impl NormalizationRule for NormalizationRuleImpl { } NormalizationRuleImpl::GuardMarkApply => GuardMarkApply.apply(plan, arena, loader), NormalizationRuleImpl::CollapseProject => CollapseProject.apply(plan, arena, loader), + NormalizationRuleImpl::ProjectMove => ProjectMove.apply(plan, arena, loader), NormalizationRuleImpl::CollapseGroupByAgg => { CollapseGroupByAgg.apply(plan, arena, loader) } diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 40f41dd5..1112e0c1 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -398,7 +398,7 @@ impl NormalizationRule for ParameterizeInnerJoin { .collect(); std::mem::swap(left, right); let mut project = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs }), + Operator::Project(ProjectOperator::new(exprs)), Childrens::None, ); project.physical_option = Some(PhysicalOption::new( @@ -852,7 +852,7 @@ mod inner_join_tests { }); let project = |input| { LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: vec![alias] }), + Operator::Project(ProjectOperator::new(vec![alias])), Childrens::Only(Box::new(input)), ) }; @@ -899,9 +899,7 @@ mod inner_join_tests { alias: AliasType::Expr(alias_column), }); let mut projected = LogicalPlan::new( - Operator::Project(ProjectOperator { - exprs: vec![computed], - }), + Operator::Project(ProjectOperator::new(vec![computed])), Childrens::Only(Box::new(base)), ); assert!(parameterize_right_subtree(&mut projected, &output, probe, &mut arena)?.is_none()); diff --git a/src/optimizer/rule/normalization/project_move.rs b/src/optimizer/rule/normalization/project_move.rs new file mode 100644 index 00000000..19986755 --- /dev/null +++ b/src/optimizer/rule/normalization/project_move.rs @@ -0,0 +1,254 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::errors::DatabaseError; +use crate::expression::function::scala::ScalarFunction; +use crate::expression::visitor::ExprVisitor; +use crate::expression::{AliasType, ScalarExpression}; +use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; +use crate::planner::operator::Operator; +use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; +use crate::types::bitset::BitSet; + +pub struct ProjectMove; + +struct InputUses { + counts: Vec, + invalid: bool, +} + +impl ExprVisitor for InputUses { + fn visit_expression_ref(&mut self, expr: ExprRef, arena: &A) -> Result { + if let ScalarExpression::ColumnRef { position, .. } = arena.expression(expr) { + if let Some(count) = self.counts.get_mut(*position) { + *count = count.saturating_add(1).min(2); + } else { + self.invalid = true; + } + return Ok(false); + } + Ok(true) + } + + fn visit_alias( + &mut self, + expr: ExprRef, + alias: &AliasType, + arena: &A, + ) -> Result<(), DatabaseError> { + let expr = match alias { + AliasType::Expr(expr) => *expr, + AliasType::Name(_) => expr, + }; + self.visit(expr, arena) + } + + fn visit_scala_function( + &mut self, + _function: &ScalarFunction, + _arena: &A, + ) -> Result<(), DatabaseError> { + // Functions can read Input directly, beyond their declared arguments. + self.invalid = true; + Ok(()) + } +} + +impl NormalizationRule for ProjectMove { + fn apply( + &self, + plan: &mut LogicalPlan, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, + ) -> Result { + let (Operator::Project(op), Childrens::Only(input)) = + (&mut plan.operator, plan.childrens.as_mut()) + else { + return Ok(false); + }; + let width = input.output_schema(arena).len(); + let mut uses = InputUses { + counts: vec![0; width], + invalid: false, + }; + for expr in &op.exprs { + uses.visit(*expr, arena)?; + } + if uses.invalid || !uses.counts.contains(&1) { + return Ok(op.moveable.take().is_some()); + } + let changed = op.moveable.as_ref().is_none_or(|mask| { + uses.counts + .iter() + .enumerate() + .any(|(position, count)| mask.contains(position) != (*count == 1)) + }); + let moveable = op + .moveable + .get_or_insert_with(|| BitSet::with_capacity(width)); + moveable.clear(); + for (position, count) in uses.counts.iter().enumerate() { + if *count == 1 { + moveable.insert(position); + } + } + Ok(changed) + } +} + +// GRCOV_EXCL_START +#[cfg(test)] +mod tests { + use super::*; + use crate::catalog::{ColumnCatalog, ColumnDesc}; + use crate::expression::function::scala::ArcScalarFunctionImpl; + use crate::expression::BinaryOperator; + use crate::function::upper::Upper; + use crate::planner::operator::project::ProjectOperator; + use crate::planner::operator::values::ValuesOperator; + use crate::planner::TableArenaCell; + use crate::types::value::DataValue; + use crate::types::LogicalType; + + fn input(arena: &mut PlanArena) -> Result<(LogicalPlan, [ExprRef; 3]), DatabaseError> { + let mut columns = Vec::new(); + let mut exprs = Vec::new(); + for position in 0..3 { + let column = arena.alloc_column(ColumnCatalog::new( + format!("c{position}"), + true, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, + )); + columns.push(column); + exprs.push(arena.alloc_expression(ScalarExpression::column_expr(column, position))); + } + Ok(( + LogicalPlan::new( + Operator::Values(ValuesOperator::new(Vec::new(), 0, columns)), + Childrens::None, + ), + exprs.try_into().unwrap(), + )) + } + + fn project(plan: &LogicalPlan) -> &ProjectOperator { + let Operator::Project(op) = &plan.operator else { + unreachable!() + }; + op + } + + #[test] + fn counts_nested_and_repeated_positions_and_reuses_bitmap() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (source, [a, b, c]) = input(&mut arena)?; + let one = arena.alloc_expression(DataValue::Int32(1).into()); + let sum = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Plus, + left_expr: b, + right_expr: one, + evaluator: None, + ty: LogicalType::Integer, + }); + let mut plan = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![c, a, a, sum])), + Childrens::Only(Box::new(source)), + ); + assert!(project(&plan).moveable.is_none()); + assert!(ProjectMove.apply(&mut plan, &mut arena, None)?); + assert!(!project(&plan).moveable.as_ref().unwrap().contains(0)); + assert!(project(&plan).moveable.as_ref().unwrap().contains(1)); + assert!(project(&plan).moveable.as_ref().unwrap().contains(2)); + assert!(!ProjectMove.apply(&mut plan, &mut arena, None)?); + + let Operator::Project(op) = &mut plan.operator else { + unreachable!() + }; + op.exprs = vec![b, b]; + assert!(ProjectMove.apply(&mut plan, &mut arena, None)?); + assert!(project(&plan).moveable.is_none()); + assert!(!ProjectMove.apply(&mut plan, &mut arena, None)?); + Ok(()) + } + + #[test] + fn counts_only_the_evaluated_alias_branch() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (source, [a, _, c]) = input(&mut arena)?; + let named = arena.alloc_expression(ScalarExpression::Alias { + expr: a, + alias: AliasType::Name("named".into()), + }); + let mapped = arena.alloc_expression(ScalarExpression::Alias { + expr: a, + alias: AliasType::Expr(c), + }); + let mut plan = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![named, mapped])), + Childrens::Only(Box::new(source)), + ); + assert!(ProjectMove.apply(&mut plan, &mut arena, None)?); + assert!(project(&plan).moveable.as_ref().unwrap().contains(0)); + assert!(!project(&plan).moveable.as_ref().unwrap().contains(1)); + assert!(project(&plan).moveable.as_ref().unwrap().contains(2)); + let Operator::Project(op) = &mut plan.operator else { + unreachable!() + }; + op.exprs.push(c); + assert!(ProjectMove.apply(&mut plan, &mut arena, None)?); + assert!(project(&plan).moveable.as_ref().unwrap().contains(0)); + assert!(!project(&plan).moveable.as_ref().unwrap().contains(2)); + Ok(()) + } + + #[test] + fn unknown_function_reads_and_invalid_positions_clear_previous_marks( + ) -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let (source, [a, b, c]) = input(&mut arena)?; + let mut plan = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![a, b, c])), + Childrens::Only(Box::new(source)), + ); + assert!(ProjectMove.apply(&mut plan, &mut arena, None)?); + let function = arena.alloc_expression(ScalarExpression::ScalaFunction(ScalarFunction { + args: vec![a], + inner: ArcScalarFunctionImpl(Upper::new()), + })); + let Operator::Project(op) = &mut plan.operator else { + unreachable!() + }; + op.exprs = vec![function, b]; + assert!(ProjectMove.apply(&mut plan, &mut arena, None)?); + assert!(project(&plan).moveable.is_none()); + assert!(!ProjectMove.apply(&mut plan, &mut arena, None)?); + + let ScalarExpression::ColumnRef { column, .. } = arena.expression(a) else { + unreachable!() + }; + let invalid = arena.alloc_expression(ScalarExpression::column_expr(*column, 3)); + let Operator::Project(op) = &mut plan.operator else { + unreachable!() + }; + op.exprs = vec![invalid, b]; + assert!(!ProjectMove.apply(&mut plan, &mut arena, None)?); + assert!(project(&plan).moveable.is_none()); + Ok(()) + } +} +// GRCOV_EXCL_STOP diff --git a/src/planner/arena.rs b/src/planner/arena.rs index c2ee70be..fe780779 100644 --- a/src/planner/arena.rs +++ b/src/planner/arena.rs @@ -1267,9 +1267,7 @@ mod tests { .map(|expr| arena.alloc_expression(expr)) .collect::>(); let input = LogicalPlan::new( - Operator::Project(ProjectOperator { - exprs: exprs.clone(), - }), + Operator::Project(ProjectOperator::new(exprs.clone())), Childrens::Only(Box::new(LogicalPlan::new(Operator::Dummy, Childrens::None))), ); let source = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(5))); diff --git a/src/planner/mod.rs b/src/planner/mod.rs index a6e9d81b..f298c710 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -780,7 +780,7 @@ mod tests { assert_eq!(filter.estimated_startup_work()?, Some(0)); assert_eq!(filter.estimated_work()?, Some(392)); let mut project = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: vec![] }), + Operator::Project(ProjectOperator::new(vec![])), Childrens::Only(Box::new(filter)), ); project.physical_option = Some(PhysicalOption::new( diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 6399a3d7..58a14e1b 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -1133,9 +1133,10 @@ mod tests { assert_eq!(referenced_columns(&join, &mut arena)?, vec![a, b, c]); assert!(!join.all_referenced_columns(&arena, |column| *column == a)?); - let project = Operator::Project(ProjectOperator { - exprs: vec![column_expr(b, 1, &mut arena), column_expr(c, 2, &mut arena)], - }); + let project = Operator::Project(ProjectOperator::new(vec![ + column_expr(b, 1, &mut arena), + column_expr(c, 2, &mut arena), + ])); assert_eq!(referenced_columns(&project, &mut arena)?, vec![b, c]); let update = Operator::Update(UpdateOperator { diff --git a/src/planner/operator/project.rs b/src/planner/operator/project.rs index 01d74216..6314b753 100644 --- a/src/planner/operator/project.rs +++ b/src/planner/operator/project.rs @@ -14,11 +14,22 @@ use crate::planner::MetaArena; use crate::planner::{fmt_explain_list, Explain, ExprRef}; +use crate::types::bitset::BitSet; use kite_sql_serde_macros::ReferenceSerialization; #[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] pub struct ProjectOperator { pub exprs: Vec, + pub moveable: Option, +} + +impl ProjectOperator { + pub fn new(exprs: Vec) -> Self { + Self { + exprs, + moveable: None, + } + } } impl Explain for ProjectOperator { diff --git a/src/planner/operator/visitor.rs b/src/planner/operator/visitor.rs index ca94b0c2..244c4fa4 100644 --- a/src/planner/operator/visitor.rs +++ b/src/planner/operator/visitor.rs @@ -523,9 +523,7 @@ pub(crate) mod tests { force_nested_loop: false, limit_pushed: false, }), - Operator::Project(ProjectOperator { - exprs: vec![expr(9)], - }), + Operator::Project(ProjectOperator::new(vec![expr(9)])), Operator::ScalarSubquery(ScalarSubqueryOperator), Operator::TableScan(TableScanOperator { table_name: "t1".into(), @@ -823,7 +821,7 @@ pub(crate) mod tests { } let right = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: vec![] }), + Operator::Project(ProjectOperator::new(vec![])), Childrens::Only(Box::new(LogicalPlan::new(Operator::Dummy, Childrens::None))), ); let join = JoinOperator::build( @@ -834,7 +832,7 @@ pub(crate) mod tests { false, ); let plan = LogicalPlan::new( - Operator::Project(ProjectOperator { exprs: vec![] }), + Operator::Project(ProjectOperator::new(vec![])), Childrens::Only(Box::new(join)), ); let mut visitor = VisitOrder::default(); diff --git a/src/types/bitset.rs b/src/types/bitset.rs new file mode 100644 index 00000000..295b7052 --- /dev/null +++ b/src/types/bitset.rs @@ -0,0 +1,141 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use kite_sql_serde_macros::ReferenceSerialization; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, ReferenceSerialization)] +pub struct BitSet { + blocks: Vec, + len: usize, +} + +impl BitSet { + pub fn with_capacity(len: usize) -> Self { + let bits_per_block = usize::BITS as usize; + let blocks = len.div_ceil(bits_per_block); + BitSet { + blocks: vec![0; blocks], + len, + } + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + pub fn clear(&mut self) { + self.blocks.fill(0); + } + + pub fn insert(&mut self, index: usize) { + assert!( + index < self.len, + "insert at index {index} exceeds bitmap size {}", + self.len + ); + let bits_per_block = usize::BITS as usize; + self.blocks[index / bits_per_block] |= 1usize << (index % bits_per_block); + } + + pub fn contains(&self, index: usize) -> bool { + if index >= self.len { + return false; + } + let bits_per_block = usize::BITS as usize; + (self.blocks[index / bits_per_block] & (1usize << (index % bits_per_block))) != 0 + } +} + +// GRCOV_EXCL_START +#[cfg(test)] +mod tests { + use super::BitSet; + + #[test] + fn bit_set_contains_false_for_empty_bitmap() { + let bitmap = BitSet::with_capacity(0); + + assert!(!bitmap.contains(0)); + assert!(!bitmap.contains(usize::MAX)); + } + + #[test] + fn bit_set_contains_false_for_unset_bits_within_capacity() { + let bitmap = BitSet::with_capacity(usize::BITS as usize + 1); + + assert!(!bitmap.contains(0)); + assert!(!bitmap.contains(usize::BITS as usize)); + } + + #[test] + fn bit_set_insert_marks_bits_across_blocks() { + let boundary = usize::BITS as usize; + let mut bitmap = BitSet::with_capacity(boundary + 2); + + bitmap.insert(0); + bitmap.insert(boundary - 1); + bitmap.insert(boundary); + bitmap.insert(boundary + 1); + + assert!(bitmap.contains(0)); + assert!(bitmap.contains(boundary - 1)); + assert!(bitmap.contains(boundary)); + assert!(bitmap.contains(boundary + 1)); + assert!(!bitmap.contains(1)); + } + + #[test] + fn bit_set_contains_returns_false_past_capacity() { + let mut bitmap = BitSet::with_capacity(2); + + bitmap.insert(1); + + assert!(!bitmap.contains(2)); + assert!(!bitmap.contains(usize::MAX)); + } + + #[test] + fn bit_set_clear_preserves_width_and_allocation() { + let width = usize::BITS as usize + 2; + let mut bitmap = BitSet::with_capacity(width); + let blocks = bitmap.blocks.as_ptr(); + bitmap.insert(0); + bitmap.insert(width - 1); + + bitmap.clear(); + assert_eq!(bitmap.len(), width); + assert_eq!(bitmap.blocks.as_ptr(), blocks); + assert!((0..width).all(|position| !bitmap.contains(position))); + bitmap.insert(1); + bitmap.clear(); + assert!(!bitmap.contains(1)); + + let mut empty = BitSet::with_capacity(0); + empty.clear(); + assert!(empty.is_empty()); + } + + #[test] + #[should_panic(expected = "exceeds bitmap size")] + fn bit_set_insert_panics_past_capacity() { + let mut bitmap = BitSet::with_capacity(1); + + bitmap.insert(1); + } +} +// GRCOV_EXCL_STOP diff --git a/src/types/mod.rs b/src/types/mod.rs index 9f6439d0..9f399041 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod bitset; pub mod evaluator; pub mod index; pub mod serialize; diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 64e97e04..d8bf0b8d 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -16,10 +16,14 @@ use crate::catalog::{ColumnCatalog, ColumnRef}; use crate::errors::DatabaseError; use crate::iter_ext::Itertools; use crate::planner::MetaArena; +use crate::types::bitset::BitSet; use crate::types::serialize::{TupleValueSerializable, TupleValueSerializableImpl}; use crate::types::value::DataValue; use std::borrow::Borrow; +use std::borrow::Cow; +use std::cell::Cell; use std::io::Cursor; +use std::slice::from_raw_parts; const BITS_MAX_INDEX: usize = 8; @@ -75,6 +79,74 @@ impl<'a, 'p> SchemaView<'a, 'p> { } } +pub trait Input { + fn width(&self) -> usize; + + fn column(&self, position: usize) -> Cow<'_, DataValue>; +} + +impl Input for T { + fn width(&self) -> usize { + TupleLike::len(self) + } + + fn column(&self, position: usize) -> Cow<'_, DataValue> { + Cow::Borrowed(self.value_at(position)) + } +} + +// Sized adapter for passing any Input through the object-safe function interface. +pub(crate) struct InputRef<'a, T: ?Sized>(pub &'a T); + +impl Input for InputRef<'_, T> { + fn width(&self) -> usize { + self.0.width() + } + + fn column(&self, position: usize) -> Cow<'_, DataValue> { + self.0.column(position) + } +} + +pub(crate) struct MoveableTuple<'a> { + values: &'a [Cell], + moveable: Option<&'a BitSet>, +} + +impl<'a> MoveableTuple<'a> { + /// # Safety + /// The input prefix must remain allocated and must not be accessed mutably + /// outside this view until evaluation finishes. Appended output must not + /// reallocate the buffer. Borrowed columns must never be modified. + pub(crate) unsafe fn from_raw_parts( + values: *mut DataValue, + len: usize, + moveable: Option<&'a BitSet>, + ) -> Self { + Self { + values: from_raw_parts(values.cast::>(), len), + moveable, + } + } +} + +impl Input for MoveableTuple<'_> { + fn width(&self) -> usize { + self.values.len() + } + + fn column(&self, position: usize) -> Cow<'_, DataValue> { + let value = &self.values[position]; + if self.moveable.is_some_and(|mask| mask.contains(position)) { + Cow::Owned(value.replace(DataValue::Null)) + } else { + // The mask is immutable. Borrowed slots are never written, and + // movable slots never return a reference to their contents. + Cow::Borrowed(unsafe { &*value.as_ptr() }) + } + } +} + pub trait TupleLike { fn len(&self) -> usize; From a54bcab6ae171448fc5140e0ce28f48dbe26ce7f Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 21/62] perf: eliminate intermediate project expressions --- src/binder/aggregate.rs | 12 +- src/binder/distinct.rs | 14 +- src/binder/parser.rs | 4 +- src/binder/select.rs | 22 +- src/binder/window.rs | 12 +- src/db.rs | 15 +- src/expression/eq_col.rs | 68 +- src/expression/mod.rs | 54 +- src/expression/simplify.rs | 32 +- src/expression/visitor_mut.rs | 84 +- src/optimizer/heuristic/optimizer.rs | 2 +- .../rule/normalization/combine_operators.rs | 382 +------- .../rule/normalization/elimination.rs | 817 +++++++++++++++++- .../rule/normalization/join_reorder.rs | 4 +- src/optimizer/rule/normalization/mod.rs | 41 +- .../rule/normalization/simplification.rs | 6 +- src/planner/arena.rs | 14 +- src/planner/mod.rs | 20 +- src/planner/operator/aggregate.rs | 4 +- .../operator/alter_table/change_column.rs | 4 +- src/planner/operator/analyze.rs | 4 +- src/planner/operator/copy_from_file.rs | 4 +- src/planner/operator/create_index.rs | 4 +- src/planner/operator/filter.rs | 4 +- src/planner/operator/join.rs | 4 +- src/planner/operator/mod.rs | 26 +- src/planner/operator/project.rs | 4 +- src/planner/operator/recursive_cte.rs | 8 +- src/planner/operator/scalar_query_init.rs | 2 +- src/planner/operator/set_membership.rs | 4 +- src/planner/operator/sort.rs | 8 +- src/planner/operator/table_scan.rs | 4 +- src/planner/operator/top_k.rs | 4 +- src/planner/operator/union.rs | 4 +- src/planner/operator/update.rs | 4 +- src/planner/operator/values.rs | 2 +- src/planner/operator/visitor_mut.rs | 16 +- src/planner/operator/window.rs | 4 +- src/types/index.rs | 2 +- 39 files changed, 1103 insertions(+), 620 deletions(-) diff --git a/src/binder/aggregate.rs b/src/binder/aggregate.rs index 41e0aa12..303fed98 100644 --- a/src/binder/aggregate.rs +++ b/src/binder/aggregate.rs @@ -360,10 +360,10 @@ impl<'a> AggregateOutputBinder<'a> { } } - fn output_ref( + fn output_ref( &mut self, expr: ExprRef, - arena: &mut dyn MetaArena, + arena: &mut A, ) -> Result, DatabaseError> { let output_count = self.agg_calls.len() + self.group_by_exprs.len(); self.agg_calls @@ -395,12 +395,8 @@ impl<'a> AggregateOutputBinder<'a> { } } -impl ExprVisitorMut for AggregateOutputBinder<'_> { - fn visit( - &mut self, - expr: &mut ExprRef, - arena: &mut (dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { +impl ExprVisitorMut for AggregateOutputBinder<'_> { + fn visit(&mut self, expr: &mut ExprRef, arena: &mut A) -> Result<(), DatabaseError> { if let ScalarExpression::Alias { alias: crate::expression::AliasType::Name(_), .. diff --git a/src/binder/distinct.rs b/src/binder/distinct.rs index 305eeb8a..8541b662 100644 --- a/src/binder/distinct.rs +++ b/src/binder/distinct.rs @@ -84,7 +84,11 @@ impl<'a> DistinctOutputBinder<'a> { Self { select_list } } - fn output_ref(&mut self, expr: ExprRef, arena: &mut dyn MetaArena) -> Option { + fn output_ref( + &mut self, + expr: ExprRef, + arena: &mut A, + ) -> Option { self.select_list .iter() .position(|candidate| { @@ -100,12 +104,8 @@ impl<'a> DistinctOutputBinder<'a> { } } -impl ExprVisitorMut for DistinctOutputBinder<'_> { - fn visit( - &mut self, - expr: &mut ExprRef, - arena: &mut (dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { +impl ExprVisitorMut for DistinctOutputBinder<'_> { + fn visit(&mut self, expr: &mut ExprRef, arena: &mut A) -> Result<(), DatabaseError> { let is_named_alias = matches!( arena.expression(*expr), ScalarExpression::Alias { diff --git a/src/binder/parser.rs b/src/binder/parser.rs index c1acb0b1..53df3db5 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -365,12 +365,12 @@ struct UpdateExprTargetRemapper<'a> { target_schema: &'a [ColumnRef], } -impl ExprVisitorMut for UpdateExprTargetRemapper<'_> { +impl ExprVisitorMut for UpdateExprTargetRemapper<'_> { fn visit_column_ref( &mut self, column: &mut ColumnRef, position: &mut usize, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { let Some(target_position) = self .target_schema diff --git a/src/binder/select.rs b/src/binder/select.rs index 07fff8c2..3191e2b8 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -53,12 +53,12 @@ struct InputPositionBinder<'a> { offset: usize, } -impl ExprVisitorMut for InputPositionBinder<'_> { +impl ExprVisitorMut for InputPositionBinder<'_> { fn visit_column_ref( &mut self, column: &mut ColumnRef, position: &mut usize, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { if let Some(index) = self .schema @@ -86,7 +86,11 @@ impl<'a> ProjectionOutputBinder<'a> { Self { project_exprs } } - fn output_ref(&mut self, expr: ExprRef, arena: &mut dyn MetaArena) -> Option { + fn output_ref( + &mut self, + expr: ExprRef, + arena: &mut A, + ) -> Option { self.project_exprs .iter() .position(|candidate| { @@ -102,12 +106,8 @@ impl<'a> ProjectionOutputBinder<'a> { } } -impl ExprVisitorMut for ProjectionOutputBinder<'_> { - fn visit( - &mut self, - expr: &mut ExprRef, - arena: &mut (dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { +impl ExprVisitorMut for ProjectionOutputBinder<'_> { + fn visit(&mut self, expr: &mut ExprRef, arena: &mut A) -> Result<(), DatabaseError> { if let Some(output_ref) = self.output_ref(*expr, arena) { *expr = arena.alloc_expression(output_ref); return Ok(()); @@ -931,12 +931,12 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b appended_outputs: &'a [AppendedRightOutput], } - impl ExprVisitorMut for AppendedRightOutputBinder<'_> { + impl ExprVisitorMut for AppendedRightOutputBinder<'_> { fn visit_column_ref( &mut self, column: &mut ColumnRef, position: &mut usize, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { if let Some(output) = self.appended_outputs.iter().find(|output| { *position == output.child_position && arena.same_column(*column, output.column) diff --git a/src/binder/window.rs b/src/binder/window.rs index 0cb195b8..63a5edf5 100644 --- a/src/binder/window.rs +++ b/src/binder/window.rs @@ -31,12 +31,8 @@ struct WindowCollector { windows: Vec<(WindowCall, ColumnRef)>, } -impl ExprVisitorMut for WindowCollector { - fn visit( - &mut self, - expr: &mut ExprRef, - arena: &mut (dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { +impl ExprVisitorMut for WindowCollector { + fn visit(&mut self, expr: &mut ExprRef, arena: &mut A) -> Result<(), DatabaseError> { let ScalarExpression::WindowCall(window) = arena.expression(*expr) else { return walk_mut_expr(self, expr, arena); }; @@ -71,12 +67,12 @@ struct WindowOutputBinder<'a> { base_position: usize, } -impl ExprVisitorMut for WindowOutputBinder<'_> { +impl ExprVisitorMut for WindowOutputBinder<'_> { fn visit_column_ref( &mut self, column: &mut ColumnRef, position: &mut usize, - _arena: &mut (dyn MetaArena + '_), + _arena: &mut A, ) -> Result<(), DatabaseError> { if let Some(output_position) = self .groups diff --git a/src/db.rs b/src/db.rs index 3d8f13f9..1d4f642e 100644 --- a/src/db.rs +++ b/src/db.rs @@ -532,7 +532,7 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { .after_batch( "Collapse Project", HepBatchStrategy::fix_point_topdown(10), - vec![NormalizationRuleImpl::CollapseProject], + vec![NormalizationRuleImpl::EliminateProject], ) .after_batch( "Expression Remapper", @@ -1680,7 +1680,7 @@ pub(crate) mod test { } #[test] - fn late_project_collapse_keeps_computed_output_boundary() -> Result<(), DatabaseError> { + fn late_project_collapse_inlines_computed_output() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let (left, large) = literal_input("large", &[1, 2, 3, 4], 0, &mut arena)?; @@ -1710,8 +1710,8 @@ pub(crate) mod test { unreachable!() }; assert!( - matches!(child.operator, Operator::Project(_)), - "computed expressions are not substituted" + matches!(child.operator, Operator::Join(_)), + "computed expressions should be substituted into the output Project" ); let Operator::Project(project) = &plan.operator else { unreachable!() @@ -2177,7 +2177,10 @@ pub(crate) mod test { }; assert_eq!(*left_position, 0); assert_eq!(*right_position, 0); - let (_, right_child) = join_plan.childrens.pop_twins(); + let (_, mut right_child) = join_plan.childrens.pop_twins(); + if matches!(right_child.operator, Operator::Project(_)) { + right_child = right_child.childrens.pop_only(); + } let Operator::Filter(filter_op) = right_child.operator else { unreachable!("expected pushed-down filter on right child"); }; @@ -2277,7 +2280,7 @@ pub(crate) mod test { let plan = row.values[0].utf8().unwrap(); assert_eq!( plan, - "Projection [l.a, l.b, t1.a, t1.b, 9] [Project => (Sort Option: Follow)] LeftOuter Join Where (l.a > 0) [NestLoopJoin => (Sort Option: None)] Projection [(t1.a) as (l.a), (t1.b) as (l.b)] [Project => (Sort Option: Follow)] Filter (t1.b > 0), Is Having: false [Filter => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [SeqScan => (Sort Option: None)] Projection [t1.a, t1.b] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [IndexScan By pk_index => ($3, +inf) => (Sort Option: OrderBy: (t1.a Asc Nulls Last) ignore_prefix_len: 0)]" + "Projection [l.a, l.b, t1.a, t1.b, 9] [Project => (Sort Option: Follow)] LeftOuter Join Where (l.a > 0) [NestLoopJoin => (Sort Option: None)] Projection [(t1.a) as (t1.a), (t1.b) as (t1.b)] [Project => (Sort Option: Follow)] Filter (t1.b > 0), Is Having: false [Filter => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [SeqScan => (Sort Option: None)] Projection [t1.a, t1.b] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [IndexScan By pk_index => ($3, +inf) => (Sort Option: OrderBy: (t1.a Asc Nulls Last) ignore_prefix_len: 0)]" ); } diff --git a/src/expression/eq_col.rs b/src/expression/eq_col.rs index 6df7123b..a22e44f3 100644 --- a/src/expression/eq_col.rs +++ b/src/expression/eq_col.rs @@ -30,22 +30,22 @@ use crate::types::evaluator::{BinaryEvaluatorRef, CastEvaluatorRef, UnaryEvaluat use crate::types::value::DataValue; use crate::types::LogicalType; -pub(super) fn eq_ignore_colref_pos( +pub(super) fn eq_ignore_colref_pos( lhs: ExprRef, rhs: ExprRef, - arena: &(dyn MetaArena + '_), + arena: &A, ) -> bool { EqIgnoreColRefPosVisitor::equals(lhs, rhs, arena) } -struct EqIgnoreColRefPosVisitor<'a, 'arena> { +struct EqIgnoreColRefPosVisitor<'a, A: ?Sized> { rhs: ExprRef, - arena: &'a (dyn MetaArena + 'arena), + arena: &'a A, equal: bool, } -impl<'a, 'arena> EqIgnoreColRefPosVisitor<'a, 'arena> { - fn equals(lhs: ExprRef, rhs: ExprRef, arena: &'a (dyn MetaArena + 'arena)) -> bool { +impl<'a, A: MetaArena + ?Sized> EqIgnoreColRefPosVisitor<'a, A> { + fn equals(lhs: ExprRef, rhs: ExprRef, arena: &'a A) -> bool { let mut visitor = Self { rhs, arena, @@ -75,8 +75,8 @@ impl<'a, 'arena> EqIgnoreColRefPosVisitor<'a, 'arena> { } } -impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { - fn visit(&mut self, lhs: ExprRef, arena: &(dyn MetaArena + '_)) -> Result<(), DatabaseError> { +impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, A> { + fn visit(&mut self, lhs: ExprRef, arena: &A) -> Result<(), DatabaseError> { let lhs = lhs.unpack_alias(arena); self.rhs = self.rhs.unpack_alias(arena); if lhs == self.rhs { @@ -89,7 +89,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { &mut self, id: ScalarQueryRef, ty: &LogicalType, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = matches!(self.rhs(), ScalarExpression::OuterParam { id: rhs, ty: rhs_ty } if id == *rhs && ty == rhs_ty); Ok(()) @@ -99,7 +99,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { &mut self, id: ScalarQueryRef, ty: &LogicalType, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = matches!(self.rhs(), ScalarExpression::OuterValue { id: rhs, ty: rhs_ty } if id == *rhs && ty == rhs_ty); Ok(()) @@ -109,7 +109,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { &mut self, id: ScalarQueryRef, ty: &LogicalType, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::InitValue { @@ -140,7 +140,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { lhs_expr: ExprRef, lhs_ty: &LogicalType, lhs_evaluator: Option<&CastEvaluatorRef>, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::TypeCast { @@ -161,7 +161,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { &mut self, lhs_negated: bool, lhs_expr: ExprRef, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::IsNull { @@ -179,7 +179,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { lhs_expr: ExprRef, lhs_evaluator: Option<&UnaryEvaluatorRef>, lhs_ty: &LogicalType, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::Unary { @@ -205,7 +205,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { lhs_right: ExprRef, lhs_evaluator: Option<&BinaryEvaluatorRef>, lhs_ty: &LogicalType, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::Binary { @@ -232,7 +232,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { lhs_kind: &AggKind, lhs_args: &[ExprRef], lhs_ty: &LogicalType, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::AggCall { @@ -251,11 +251,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { Ok(()) } - fn visit_window( - &mut self, - lhs: &WindowCall, - _arena: &(dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { + fn visit_window(&mut self, lhs: &WindowCall, _arena: &A) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::WindowCall(rhs) => { lhs.function.kind == rhs.function.kind @@ -281,7 +277,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { lhs_negated: bool, lhs_expr: ExprRef, lhs_args: &[ExprRef], - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::In { @@ -305,7 +301,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { lhs_expr: ExprRef, lhs_left: ExprRef, lhs_right: ExprRef, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::Between { @@ -330,7 +326,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { lhs_expr: ExprRef, lhs_for: Option, lhs_from: Option, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::SubString { @@ -351,7 +347,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { &mut self, lhs_expr: ExprRef, lhs_in: ExprRef, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::Position { @@ -371,7 +367,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { lhs_expr: ExprRef, lhs_what: Option, lhs_where: Option<&TrimWhereField>, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::Trim { @@ -393,11 +389,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { Ok(()) } - fn visit_tuple( - &mut self, - lhs: &[ExprRef], - _arena: &(dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { + fn visit_tuple(&mut self, lhs: &[ExprRef], _arena: &A) -> Result<(), DatabaseError> { self.equal = matches!(self.rhs(), ScalarExpression::Tuple(rhs) if self.refs_equal(lhs, rhs)); Ok(()) @@ -406,7 +398,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { fn visit_scala_function( &mut self, lhs: &ScalarFunction, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = matches!( self.rhs(), @@ -419,7 +411,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { fn visit_table_function( &mut self, lhs: &TableFunction, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = matches!( self.rhs(), @@ -435,7 +427,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { lhs_left: ExprRef, lhs_right: ExprRef, lhs_ty: &LogicalType, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::If { @@ -459,7 +451,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { lhs_left: ExprRef, lhs_right: ExprRef, lhs_ty: &LogicalType, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::IfNull { @@ -481,7 +473,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { lhs_left: ExprRef, lhs_right: ExprRef, lhs_ty: &LogicalType, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::NullIf { @@ -502,7 +494,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { &mut self, lhs_exprs: &[ExprRef], lhs_ty: &LogicalType, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::Coalesce { @@ -520,7 +512,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { lhs_pairs: &[(ExprRef, ExprRef)], lhs_else: Option, lhs_ty: &LogicalType, - _arena: &(dyn MetaArena + '_), + _arena: &A, ) -> Result<(), DatabaseError> { self.equal = match self.rhs() { ScalarExpression::CaseWhen { diff --git a/src/expression/mod.rs b/src/expression/mod.rs index af3629c9..18e7b681 100644 --- a/src/expression/mod.rs +++ b/src/expression/mod.rs @@ -292,13 +292,13 @@ mod chrono_scalar_expression { pub struct BindEvaluator; -impl ExprVisitorMut for BindEvaluator { +impl ExprVisitorMut for BindEvaluator { fn visit_type_cast( &mut self, expr: &mut ExprRef, ty: &mut LogicalType, evaluator: &mut Option, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena)?; let from = expr.return_type(arena); @@ -317,7 +317,7 @@ impl ExprVisitorMut for BindEvaluator { expr: &mut ExprRef, evaluator: &mut Option, _ty: &mut LogicalType, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena)?; @@ -344,7 +344,7 @@ impl ExprVisitorMut for BindEvaluator { right_expr: &mut ExprRef, evaluator: &mut Option, _ty: &mut LogicalType, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(left_expr, arena)?; self.visit(right_expr, arena)?; @@ -366,7 +366,7 @@ impl ExprVisitorMut for BindEvaluator { expr: &mut ExprRef, args: &mut [ExprRef], evaluator: &mut Option, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena)?; let mut ty = expr.return_type(arena).into_owned(); @@ -391,7 +391,7 @@ impl ExprVisitorMut for BindEvaluator { right_expr: &mut ExprRef, left_evaluator: &mut Option, right_evaluator: &mut Option, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena)?; self.visit(left_expr, arena)?; @@ -442,19 +442,19 @@ impl ExprVisitor for HasCountStar { } pub trait TypeCast: Sized { - fn return_type<'a>(&'a self, arena: &'a (dyn MetaArena + '_)) -> Cow<'a, LogicalType>; + fn return_type<'a, A: MetaArena + ?Sized>(&'a self, arena: &'a A) -> Cow<'a, LogicalType>; - fn into_expr( + fn into_expr( self, ty: LogicalType, evaluator: CastEvaluatorRef, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Self; - fn type_cast( + fn type_cast( self, ty: Cow<'_, LogicalType>, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result { let from = self.return_type(arena); if from.as_ref() == ty.as_ref() { @@ -466,7 +466,7 @@ pub trait TypeCast: Sized { } impl TypeCast for ScalarExpression { - fn return_type<'a>(&'a self, arena: &'a (dyn MetaArena + '_)) -> Cow<'a, LogicalType> { + fn return_type<'a, A: MetaArena + ?Sized>(&'a self, arena: &'a A) -> Cow<'a, LogicalType> { match self { ScalarExpression::Constant(value) => Cow::Owned(value.logical_type()), ScalarExpression::ColumnRef { column, .. } => { @@ -509,11 +509,11 @@ impl TypeCast for ScalarExpression { } } - fn into_expr( + fn into_expr( self, ty: LogicalType, evaluator: CastEvaluatorRef, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Self { ScalarExpression::TypeCast { expr: arena.alloc_expression(self), @@ -524,15 +524,15 @@ impl TypeCast for ScalarExpression { } impl TypeCast for ExprRef { - fn return_type<'a>(&'a self, arena: &'a (dyn MetaArena + '_)) -> Cow<'a, LogicalType> { + fn return_type<'a, A: MetaArena + ?Sized>(&'a self, arena: &'a A) -> Cow<'a, LogicalType> { arena.expression(*self).return_type(arena) } - fn into_expr( + fn into_expr( self, ty: LogicalType, evaluator: CastEvaluatorRef, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Self { arena.alloc_expression(ScalarExpression::TypeCast { expr: self, @@ -549,10 +549,10 @@ impl ScalarExpression { } impl Explain for ExprRef { - fn fmt(&self, arena: &dyn MetaArena, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fn write_exprs( + fn fmt(&self, arena: &A, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn write_exprs( exprs: &[ExprRef], - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { for (index, expr) in exprs.iter().enumerate() { @@ -804,13 +804,17 @@ impl ExprRef { SortField::from(self).nulls_last() } - pub(crate) fn eq_ignore_colref_pos(self, other: ExprRef, arena: &(dyn MetaArena + '_)) -> bool { + pub(crate) fn eq_ignore_colref_pos( + self, + other: ExprRef, + arena: &A, + ) -> bool { eq_col::eq_ignore_colref_pos(self, other, arena) } - pub(crate) fn clone_expression( + pub(crate) fn clone_expression( self, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result { let mut cloned = self; crate::expression::visitor_mut::ExprCloner.visit(&mut cloned, arena)?; @@ -980,11 +984,11 @@ impl ExprRef { Ok(checker.0) } - pub fn output_name(self, arena: &(dyn MetaArena + '_)) -> String { + pub fn output_name(self, arena: &A) -> String { self.explain(arena).to_string() } - pub fn output_column_ref(self, arena: &mut (dyn MetaArena + '_)) -> ColumnRef { + pub fn output_column_ref(self, arena: &mut A) -> ColumnRef { match arena.expression(self) { ScalarExpression::ColumnRef { column, .. } => *column, ScalarExpression::Alias { diff --git a/src/expression/simplify.rs b/src/expression/simplify.rs index fd311a1e..b7627513 100644 --- a/src/expression/simplify.rs +++ b/src/expression/simplify.rs @@ -26,16 +26,16 @@ use std::borrow::Cow; pub struct ConstantCalculator; impl ConstantCalculator { - pub fn new(_arena: &(dyn MetaArena + '_)) -> Self { + pub fn new(_arena: &A) -> Self { Self } } -impl ExprVisitorMut for ConstantCalculator { +impl ExprVisitorMut for ConstantCalculator { fn visit_expression( &mut self, expr: &mut ScalarExpression, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result { match expr { ScalarExpression::Unary { @@ -112,11 +112,11 @@ const IN_EXPANSION_LIMIT: usize = 5; #[derive(Debug, Default)] pub struct Simplify; -impl ExprVisitorMut for Simplify { +impl ExprVisitorMut for Simplify { fn visit_expression( &mut self, expr: &mut ScalarExpression, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result { match expr { ScalarExpression::Unary { @@ -311,9 +311,9 @@ impl Simplify { } } - fn take_range_comparison( + fn take_range_comparison( expr: ExprRef, - arena: &(dyn MetaArena + '_), + arena: &A, ) -> Option { match arena.expression(expr) { expression @ ScalarExpression::Binary { op, .. } @@ -325,9 +325,9 @@ impl Simplify { } } - fn take_negated_range_comparison( + fn take_negated_range_comparison( expr: ExprRef, - arena: &(dyn MetaArena + '_), + arena: &A, ) -> Option { let mut expression = arena.expression(expr).clone(); match &mut expression { @@ -339,17 +339,17 @@ impl Simplify { } } - fn boolean_constant(expr: ExprRef, arena: &(dyn MetaArena + '_)) -> Option { + fn boolean_constant(expr: ExprRef, arena: &A) -> Option { match arena.expression(expr) { ScalarExpression::Constant(DataValue::Boolean(value)) => Some(*value), _ => None, } } - fn take_range_comparison_with_polarity( + fn take_range_comparison_with_polarity( expr: ExprRef, positive: bool, - arena: &(dyn MetaArena + '_), + arena: &A, ) -> Option { if positive { Self::take_range_comparison(expr, arena) @@ -358,11 +358,11 @@ impl Simplify { } } - fn take_bool_normalized_range_comparison( + fn take_bool_normalized_range_comparison( op: BinaryOperator, left_expr: ExprRef, right_expr: ExprRef, - arena: &(dyn MetaArena + '_), + arena: &A, ) -> Option { let is_eq = matches!(op, BinaryOperator::Eq); if !matches!(op, BinaryOperator::Eq | BinaryOperator::NotEq) { @@ -421,11 +421,11 @@ impl Simplify { /// (float and decimal arithmetic round), never `*`/`/` (sign-dependent and /// not invertible for integers), and any overflow while folding aborts. /// Returns `None` when nothing could be peeled. - fn isolate_column( + fn isolate_column( mut expr: ExprRef, mut op: BinaryOperator, value_expr: ExprRef, - arena: &(dyn MetaArena + '_), + arena: &A, ) -> Option<(ExprRef, BinaryOperator, DataValue)> { let mut value = value_expr.unpack_val(arena)?; if value.has_parameter() { diff --git a/src/expression/visitor_mut.rs b/src/expression/visitor_mut.rs index 41c0cb66..f27ff795 100644 --- a/src/expression/visitor_mut.rs +++ b/src/expression/visitor_mut.rs @@ -29,12 +29,8 @@ use crate::types::LogicalType; pub(crate) struct ExprCloner; -impl ExprVisitorMut for ExprCloner { - fn visit( - &mut self, - expr: &mut ExprRef, - arena: &mut (dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { +impl ExprVisitorMut for ExprCloner { + fn visit(&mut self, expr: &mut ExprRef, arena: &mut A) -> Result<(), DatabaseError> { *expr = arena.alloc_expression(arena.expression(*expr).clone()); walk_mut_expr(self, expr, arena) } @@ -44,12 +40,12 @@ pub(crate) struct PositionShift { pub(crate) delta: isize, } -impl ExprVisitorMut for PositionShift { +impl ExprVisitorMut for PositionShift { fn visit_column_ref( &mut self, _column: &mut ColumnRef, position: &mut usize, - _arena: &mut (dyn MetaArena + '_), + _arena: &mut A, ) -> Result<(), DatabaseError> { if self.delta.is_negative() { *position = position.saturating_sub(self.delta.unsigned_abs()); @@ -60,12 +56,12 @@ impl ExprVisitorMut for PositionShift { } } -pub trait ExprVisitorMut: Sized { +pub trait ExprVisitorMut: Sized { fn visit_outer_param( &mut self, _id: &mut ScalarQueryRef, _ty: &mut LogicalType, - _arena: &mut dyn MetaArena, + _arena: &mut A, ) -> Result<(), DatabaseError> { Ok(()) } @@ -74,7 +70,7 @@ pub trait ExprVisitorMut: Sized { &mut self, _id: &mut ScalarQueryRef, _ty: &mut LogicalType, - _arena: &mut dyn MetaArena, + _arena: &mut A, ) -> Result<(), DatabaseError> { Ok(()) } @@ -83,16 +79,12 @@ pub trait ExprVisitorMut: Sized { &mut self, _id: &mut ScalarQueryRef, _ty: &mut LogicalType, - _arena: &mut dyn MetaArena, + _arena: &mut A, ) -> Result<(), DatabaseError> { Ok(()) } - fn visit( - &mut self, - expr: &mut ExprRef, - arena: &mut (dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { + fn visit(&mut self, expr: &mut ExprRef, arena: &mut A) -> Result<(), DatabaseError> { if !self.visit_expression_ref(expr, arena)? { return Ok(()); } @@ -110,7 +102,7 @@ pub trait ExprVisitorMut: Sized { fn visit_expression_ref( &mut self, _expr: &mut ExprRef, - _arena: &mut (dyn MetaArena + '_), + _arena: &mut A, ) -> Result { Ok(true) } @@ -118,7 +110,7 @@ pub trait ExprVisitorMut: Sized { fn visit_expression( &mut self, _expr: &mut ScalarExpression, - _arena: &mut (dyn MetaArena + '_), + _arena: &mut A, ) -> Result { Ok(true) } @@ -126,7 +118,7 @@ pub trait ExprVisitorMut: Sized { fn visit_constant( &mut self, _value: &mut DataValue, - _arena: &mut (dyn MetaArena + '_), + _arena: &mut A, ) -> Result<(), DatabaseError> { Ok(()) } @@ -135,7 +127,7 @@ pub trait ExprVisitorMut: Sized { &mut self, _column: &mut ColumnRef, _position: &mut usize, - _arena: &mut (dyn MetaArena + '_), + _arena: &mut A, ) -> Result<(), DatabaseError> { Ok(()) } @@ -144,7 +136,7 @@ pub trait ExprVisitorMut: Sized { &mut self, expr: &mut ExprRef, alias: &mut AliasType, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { if let AliasType::Expr(alias_expr) = alias { self.visit(alias_expr, arena)?; @@ -157,7 +149,7 @@ pub trait ExprVisitorMut: Sized { expr: &mut ExprRef, _ty: &mut LogicalType, _evaluator: &mut Option, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena) } @@ -166,7 +158,7 @@ pub trait ExprVisitorMut: Sized { &mut self, _negated: bool, expr: &mut ExprRef, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena) } @@ -177,7 +169,7 @@ pub trait ExprVisitorMut: Sized { expr: &mut ExprRef, _evaluator: &mut Option, _ty: &mut LogicalType, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena) } @@ -189,7 +181,7 @@ pub trait ExprVisitorMut: Sized { right_expr: &mut ExprRef, _evaluator: &mut Option, _ty: &mut LogicalType, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(left_expr, arena)?; self.visit(right_expr, arena) @@ -201,7 +193,7 @@ pub trait ExprVisitorMut: Sized { _kind: &mut AggKind, args: &mut [ExprRef], _ty: &mut LogicalType, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { for arg in args { self.visit(arg, arena)?; @@ -212,7 +204,7 @@ pub trait ExprVisitorMut: Sized { fn visit_window( &mut self, window: &mut WindowCall, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { for expr in window .function @@ -232,7 +224,7 @@ pub trait ExprVisitorMut: Sized { expr: &mut ExprRef, args: &mut [ExprRef], _evaluator: &mut Option, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena)?; for arg in args { @@ -250,7 +242,7 @@ pub trait ExprVisitorMut: Sized { right_expr: &mut ExprRef, _left_evaluator: &mut Option, _right_evaluator: &mut Option, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena)?; self.visit(left_expr, arena)?; @@ -262,7 +254,7 @@ pub trait ExprVisitorMut: Sized { expr: &mut ExprRef, for_expr: &mut Option, from_expr: &mut Option, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena)?; if let Some(for_expr) = for_expr { @@ -278,7 +270,7 @@ pub trait ExprVisitorMut: Sized { &mut self, expr: &mut ExprRef, in_expr: &mut ExprRef, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena)?; self.visit(in_expr, arena) @@ -289,7 +281,7 @@ pub trait ExprVisitorMut: Sized { expr: &mut ExprRef, trim_what_expr: &mut Option, _trim_where: &mut Option, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena)?; if let Some(trim_what_expr) = trim_what_expr { @@ -306,16 +298,12 @@ pub trait ExprVisitorMut: Sized { &mut self, expr: &mut ExprRef, _pos: usize, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(expr, arena) } - fn visit_tuple( - &mut self, - exprs: &mut [ExprRef], - arena: &mut (dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { + fn visit_tuple(&mut self, exprs: &mut [ExprRef], arena: &mut A) -> Result<(), DatabaseError> { for expr in exprs { self.visit(expr, arena)?; } @@ -325,7 +313,7 @@ pub trait ExprVisitorMut: Sized { fn visit_scala_function( &mut self, function: &mut ScalarFunction, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { for arg in &mut function.args { self.visit(arg, arena)?; @@ -336,7 +324,7 @@ pub trait ExprVisitorMut: Sized { fn visit_table_function( &mut self, function: &mut TableFunction, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { for arg in &mut function.args { self.visit(arg, arena)?; @@ -350,7 +338,7 @@ pub trait ExprVisitorMut: Sized { left_expr: &mut ExprRef, right_expr: &mut ExprRef, _ty: &mut LogicalType, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(condition, arena)?; self.visit(left_expr, arena)?; @@ -362,7 +350,7 @@ pub trait ExprVisitorMut: Sized { left_expr: &mut ExprRef, right_expr: &mut ExprRef, _ty: &mut LogicalType, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(left_expr, arena)?; self.visit(right_expr, arena) @@ -373,7 +361,7 @@ pub trait ExprVisitorMut: Sized { left_expr: &mut ExprRef, right_expr: &mut ExprRef, _ty: &mut LogicalType, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { self.visit(left_expr, arena)?; self.visit(right_expr, arena) @@ -383,7 +371,7 @@ pub trait ExprVisitorMut: Sized { &mut self, exprs: &mut [ExprRef], _ty: &mut LogicalType, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { for expr in exprs { self.visit(expr, arena)?; @@ -397,7 +385,7 @@ pub trait ExprVisitorMut: Sized { expr_pairs: &mut [(ExprRef, ExprRef)], else_expr: &mut Option, _ty: &mut LogicalType, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { if let Some(expr) = operand_expr { self.visit(expr, arena)?; @@ -413,10 +401,10 @@ pub trait ExprVisitorMut: Sized { } } -pub fn walk_mut_expr( +pub fn walk_mut_expr, A: MetaArena + ?Sized>( visitor: &mut V, expr: &mut ExprRef, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { let mut expression = std::mem::replace(&mut *arena.expression_mut(*expr), ScalarExpression::Empty); diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index 5975806d..cfbfcdb2 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -167,7 +167,7 @@ impl<'a> HepOptimizer<'a> { loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { match pass.kind { - WholeTreePassKind::ColumnPruning => { + WholeTreePassKind::ColumnPruning | WholeTreePassKind::EliminateProject => { let mut applied = false; for rule in &pass.rules { applied |= rule.apply(plan, arena, loader)?; diff --git a/src/optimizer/rule/normalization/combine_operators.rs b/src/optimizer/rule/normalization/combine_operators.rs index 225d9a9b..9e760347 100644 --- a/src/optimizer/rule/normalization/combine_operators.rs +++ b/src/optimizer/rule/normalization/combine_operators.rs @@ -13,211 +13,16 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::expression::visitor_mut::{walk_mut_expr, ExprVisitorMut}; use crate::expression::{BinaryOperator, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::{only_child_mut, replace_with_only_child}; -use crate::optimizer::rule::normalization::strip_alias; use crate::planner::operator::filter::FilterOperator; -use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::Operator; -use crate::planner::MetaArena; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; use crate::types::LogicalType; use std::mem; -fn is_passthrough_project(op: &ProjectOperator, arena: &PlanArena<'_>) -> bool { - op.exprs.iter().all(|expr| { - matches!( - arena.expression(strip_alias(*expr, arena)), - ScalarExpression::ColumnRef { .. } - ) - }) -} - -fn passthrough_source_position(expr: ExprRef, arena: &PlanArena<'_>) -> Option { - match arena.expression(strip_alias(expr, arena)) { - ScalarExpression::ColumnRef { position, .. } => Some(*position), - _ => None, - } -} - -fn collapse_match_expr(expr: ExprRef, arena: &PlanArena<'_>) -> ExprRef { - match arena.expression(expr) { - ScalarExpression::Alias { expr, .. } => *expr, - _ => expr, - } -} - -fn rewrite_column_position( - mut expr: ExprRef, - new_position: usize, - arena: &mut PlanArena<'_>, -) -> Result<(), DatabaseError> { - struct Rewriter(usize); - impl ExprVisitorMut for Rewriter { - fn visit_column_ref( - &mut self, - _column: &mut crate::catalog::ColumnRef, - position: &mut usize, - _arena: &mut (dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { - *position = self.0; - Ok(()) - } - } - Rewriter(new_position).visit(&mut expr, arena) -} - -fn remap_passthrough_project_exprs( - parent_exprs: &mut [ExprRef], - child_exprs: &[ExprRef], - remapped_positions: &mut Vec, - arena: &mut PlanArena<'_>, -) -> Result { - remapped_positions.clear(); - - for parent_expr in parent_exprs.iter() { - let parent_match_expr = collapse_match_expr(*parent_expr, arena); - let Some(position) = child_exprs - .iter() - .find(|child_expr| parent_match_expr.eq_ignore_colref_pos(**child_expr, arena)) - .and_then(|expr| passthrough_source_position(*expr, arena)) - else { - return Ok(false); - }; - remapped_positions.push(position); - } - - for (parent_expr, position) in parent_exprs - .iter_mut() - .zip(remapped_positions.iter().copied()) - { - rewrite_column_position(*parent_expr, position, arena)?; - } - - Ok(true) -} - -fn groupby_exprs_match( - parent_exprs: &[ExprRef], - child_exprs: &[ExprRef], - arena: &crate::planner::PlanArena, -) -> bool { - parent_exprs.len() == child_exprs.len() - && parent_exprs - .iter() - .zip(child_exprs.iter()) - .all(|(parent_expr, child_expr)| parent_expr.eq_ignore_colref_pos(*child_expr, arena)) -} - -/// Combine two adjacent project operators into one. -pub struct CollapseProject; - -impl NormalizationRule for CollapseProject { - fn apply( - &self, - plan: &mut LogicalPlan, - arena: &mut PlanArena, - _loader: Option<&StatisticMetaLoader<'_>>, - ) -> Result { - let Operator::Project(parent_op) = &mut plan.operator else { - return Ok(false); - }; - - let mut removed = false; - let mut remapped_positions = Vec::with_capacity(parent_op.exprs.len()); - while let Childrens::Only(child) = plan.childrens.as_mut() { - match &child.operator { - Operator::Project(child_op) - if is_passthrough_project(child_op, arena) - && remap_passthrough_project_exprs( - &mut parent_op.exprs, - &child_op.exprs, - &mut remapped_positions, - arena, - )? => - { - removed |= replace_with_only_child(child.as_mut()); - } - _ => break, - } - } - - Ok(removed) - } -} - -// Removing a project must preserve every input slot; renaming is allowed. -fn can_remove_project( - op: &ProjectOperator, - childrens: &mut Childrens, - arena: &mut PlanArena<'_>, -) -> bool { - let Childrens::Only(input) = childrens else { - return false; - }; - let schema = input.output_schema(arena); - if op.exprs.len() != schema.len() { - return false; - } - for (i, expr) in op.exprs.iter().enumerate() { - let mut source_expr = *expr; - while let ScalarExpression::Alias { expr, .. } = arena.expression(source_expr) { - source_expr = *expr; - } - let ScalarExpression::ColumnRef { - column: source_column, - .. - } = arena.expression(source_expr) - else { - return false; - }; - let ScalarExpression::ColumnRef { - position: execution_position, - .. - } = arena.expression(strip_alias(*expr, arena)) - else { - return false; - }; - if *source_column != schema[i] || *execution_position != i { - return false; - } - } - true -} - -struct ProjectionSubstitution<'a>(&'a [ExprRef]); - -impl ExprVisitorMut for ProjectionSubstitution<'_> { - fn visit( - &mut self, - expr: &mut ExprRef, - arena: &mut (dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { - if let ScalarExpression::ColumnRef { position, .. } = arena.expression(*expr) { - let Some(source) = self.0.get(*position) else { - return Err(DatabaseError::InvalidValue( - "invalid projection slot".into(), - )); - }; - *expr = source.clone_expression(arena)?; - return Ok(()); - } - walk_mut_expr(self, expr, arena) - } - - fn visit_alias( - &mut self, - expr: &mut ExprRef, - _: &mut crate::expression::AliasType, - arena: &mut (dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { - self.visit(expr, arena) - } -} - /// Combine two adjacent filter operators into one. pub struct CombineFilter; @@ -235,7 +40,7 @@ impl NormalizationRule for CombineFilter { return Ok(false); } }; - let mut parent_filter = parent_filter; + let parent_filter = parent_filter; let cursor = match only_child_mut(plan) { Some(child) => child, @@ -245,53 +50,46 @@ impl NormalizationRule for CombineFilter { } }; - let mut changed = false; - loop { - match &mut cursor.operator { - Operator::Filter(child_op) => { - let FilterOperator { - predicate, - having, - is_optimized: _, - } = parent_filter; - let child_predicate = child_op.predicate; - child_op.predicate = arena.alloc_expression(ScalarExpression::Binary { - op: BinaryOperator::And, - left_expr: predicate, - right_expr: child_predicate, - evaluator: None, - ty: LogicalType::Boolean, - }); - child_op.having = having || child_op.having; - child_op.is_optimized = false; - - return Ok(replace_with_only_child(plan)); - } - Operator::Project(project_op) => { - if !can_remove_project(project_op, &mut cursor.childrens, arena) { - plan.operator = Operator::Filter(parent_filter); - return Ok(changed); - } - let mut predicate = parent_filter.predicate.clone_expression(arena)?; - ProjectionSubstitution(&project_op.exprs).visit(&mut predicate, arena)?; - parent_filter.predicate = predicate; - parent_filter.is_optimized = false; - if replace_with_only_child(cursor) { - changed = true; - continue; - } - plan.operator = Operator::Filter(parent_filter); - return Ok(false); - } - _ => { - plan.operator = Operator::Filter(parent_filter); - return Ok(changed); - } + match &mut cursor.operator { + Operator::Filter(child_op) => { + let FilterOperator { + predicate, + having, + is_optimized: _, + } = parent_filter; + let child_predicate = child_op.predicate; + child_op.predicate = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::And, + left_expr: predicate, + right_expr: child_predicate, + evaluator: None, + ty: LogicalType::Boolean, + }); + child_op.having = having || child_op.having; + child_op.is_optimized = false; + + return Ok(replace_with_only_child(plan)); + } + _ => { + plan.operator = Operator::Filter(parent_filter); + return Ok(false); } } } } +fn groupby_exprs_match( + parent_exprs: &[ExprRef], + child_exprs: &[ExprRef], + arena: &PlanArena<'_>, +) -> bool { + parent_exprs.len() == child_exprs.len() + && parent_exprs + .iter() + .zip(child_exprs) + .all(|(parent, child)| parent.eq_ignore_colref_pos(*child, arena)) +} + pub struct CollapseGroupByAgg; impl NormalizationRule for CollapseGroupByAgg { @@ -341,12 +139,9 @@ mod tests { use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::heuristic::batch::HepBatchStrategy; use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; - use crate::optimizer::rule::normalization::combine_operators::{ - CollapseGroupByAgg, CollapseProject, - }; + use crate::optimizer::rule::normalization::combine_operators::CollapseGroupByAgg; use crate::optimizer::rule::normalization::NormalizationRuleImpl; use crate::planner::operator::aggregate::AggregateOperator; - use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; @@ -355,109 +150,6 @@ mod tests { arena.alloc_expression(ScalarExpression::column_expr(column, position)) } - #[test] - fn test_collapse_project() -> Result<(), DatabaseError> { - let table_state = build_t1_table()?; - let mut arena = PlanArena::new(&table_state.table_arena); - let plan = - table_state.plan_with_arena("select c1 from (select c1, c2 from t1) t", &mut arena)?; - - let pipeline = HepOptimizerPipeline::builder() - .before_batch( - "test_collapse_project", - HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::CollapseProject], - ) - .build(); - let best_plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; - - if let Operator::Project(op) = &best_plan.operator { - assert_eq!(op.exprs.len(), 1); - } else { - unreachable!("Should be a project operator") - } - - let alias_project = best_plan.childrens.pop_only(); - assert!( - matches!(alias_project.operator, Operator::Project(_)), - "Derived-table alias projection should be preserved" - ); - let scan_op = alias_project.childrens.pop_only(); - assert!(matches!(scan_op.operator, Operator::TableScan(_))); - assert!(matches!(scan_op.childrens.as_ref(), Childrens::None)); - - Ok(()) - } - - #[test] - fn test_collapse_project_with_alias() -> Result<(), DatabaseError> { - let table_state = build_t1_table()?; - let mut arena = PlanArena::new(&table_state.table_arena); - let plan = table_state - .plan_with_arena("select t.x from (select c1 as x from t1) t", &mut arena)?; - let original = plan.clone(); - let original_child = original.childrens.pop_only(); - assert!(matches!(original_child.operator, Operator::Project(_))); - let original_grandchild = original_child.childrens.pop_only(); - assert!(matches!(original_grandchild.operator, Operator::Project(_))); - - let pipeline = HepOptimizerPipeline::builder() - .before_batch( - "test_collapse_project_with_alias", - HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::CollapseProject], - ) - .build(); - let best_plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; - if let Operator::Project(op) = &best_plan.operator { - assert_eq!(op.exprs.len(), 1); - } else { - unreachable!("Should be a project operator"); - } - - let scan_op = best_plan.childrens.pop_only(); - assert!(matches!(scan_op.operator, Operator::Project(_))); - let scan_child = scan_op.childrens.pop_only(); - assert!( - !matches!(scan_child.operator, Operator::Project(_)), - "Child project should be collapsed" - ); - - Ok(()) - } - - #[test] - fn test_collapse_project_remaps_reordered_passthrough_positions() -> Result<(), DatabaseError> { - let table_arena = crate::planner::TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - let child = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![ - column_expr(&mut arena, "c2", 1), - column_expr(&mut arena, "c1", 0), - ])), - Childrens::Only(Box::new(LogicalPlan::new(Operator::Dummy, Childrens::None))), - ); - let mut plan = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![column_expr(&mut arena, "c2", 0)])), - Childrens::Only(Box::new(child)), - ); - - assert!(CollapseProject.apply(&mut plan, &mut arena, None)?); - - let Operator::Project(op) = &plan.operator else { - unreachable!("expected project"); - }; - let ScalarExpression::ColumnRef { position, .. } = arena.expression(op.exprs[0]) else { - unreachable!("expected column ref"); - }; - assert_eq!(*position, 1); - assert!(matches!( - plan.childrens.pop_only().operator, - Operator::Dummy - )); - Ok(()) - } - #[test] fn test_combine_filter_keeps_reordered_projection() -> Result<(), DatabaseError> { let table_state = build_t1_table()?; diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index a2ecabcc..f8a85fa4 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -13,16 +13,286 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::expression::ScalarExpression; +use crate::expression::visitor_mut::ExprVisitorMut; +use crate::expression::{AliasType, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::{only_child_mut, replace_with_only_child, wrap_child_with}; +use crate::planner::operator::join::{JoinCondition, JoinType}; use crate::planner::operator::limit::LimitOperator; +use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::table_scan::TableScanOperator; +use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; -use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; +use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; use crate::types::index::{IndexLookup, IndexOrderHint}; +use std::collections::HashSet; + +pub struct EliminateProject; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum OutputMode { + Preserve, + Inline, + ColumnsOnly, +} + +#[derive(Default)] +struct Substitute { + exprs: Option>, + visited: HashSet, + offset: usize, + opaque: bool, +} + +impl ExprVisitorMut for Substitute { + fn visit_expression_ref( + &mut self, + expr: &mut ExprRef, + arena: &mut A, + ) -> Result { + if matches!( + arena.expression(*expr), + ScalarExpression::ScalaFunction(_) | ScalarExpression::TableFunction(_) + ) { + self.opaque = true; + return Ok(false); + } + if let ScalarExpression::ColumnRef { column, position } = arena.expression(*expr) { + if let Some(exprs) = &self.exprs { + *expr = exprs[*position]; + } else if self.offset != 0 { + *expr = arena.alloc_expression(ScalarExpression::column_expr( + *column, + self.offset + *position, + )); + } + return Ok(false); // Do not revisit the replacement's input layout. + } + Ok((self.exprs.is_none() && self.offset == 0) || self.visited.insert(*expr)) + } + + fn visit_alias( + &mut self, + expr: &mut ExprRef, + alias: &mut AliasType, + arena: &mut A, + ) -> Result<(), DatabaseError> { + ExprVisitorMut::visit( + self, + match alias { + AliasType::Expr(expr) => expr, + _ => expr, + }, + arena, + ) + } +} + +struct EliminateVisitor<'a, 'b> { + arena: &'a mut PlanArena<'b>, + mapping: Substitute, + output: OutputMode, + changed: bool, +} + +impl EliminateVisitor<'_, '_> { + fn visit_input( + &mut self, + input: &mut LogicalPlan, + output: OutputMode, + apply: impl FnOnce(&mut Self, &mut LogicalPlan) -> Result<(), DatabaseError>, + ) -> Result<(), DatabaseError> { + self.output = output; + self.visit_plan(input)?; + apply(self, input) + } +} + +impl<'p> OperatorVisitorMut<'p> for EliminateVisitor<'_, '_> { + fn visit_plan(&mut self, plan: &'p mut LogicalPlan) -> Result<(), DatabaseError> { + let output = self.output; + let schema = plan.output_schema(self.arena).clone(); + + self.mapping.exprs = None; + self.mapping.visited.clear(); + self.mapping.opaque = false; + OperatorExprVisitorMut::new(&mut self.mapping, self.arena) + .visit_operator(&mut plan.operator, None)?; + let opaque = self.mapping.opaque; + match plan.childrens.as_mut() { + Childrens::Only(child) => { + let input_schema = child.output_schema(self.arena); + let forwards = schema.starts_with(input_schema); + let produces = + matches!(plan.operator, Operator::Project(_) | Operator::Aggregate(_)); + // A scalar subquery must materialize its value before generating + // the NULL row for an empty input. Other new layouts are also + // boundaries unless expressions explicitly produce their slots. + let input_mode = if opaque + || matches!(plan.operator, Operator::ScalarSubquery(_)) + || !(forwards || produces) + { + OutputMode::Preserve + } else { + match (output, produces) { + (OutputMode::ColumnsOnly, false) => OutputMode::ColumnsOnly, + _ => OutputMode::Inline, + } + }; + self.visit_input(child, input_mode, |visitor, child| { + let Some(_) = &visitor.mapping.exprs else { + return Ok(()); + }; + OperatorExprVisitorMut::new(&mut visitor.mapping, visitor.arena) + .visit_operator(&mut plan.operator, None)?; + if !produces { + let width = child.output_schema(visitor.arena).len(); + if let Some(exprs) = &mut visitor.mapping.exprs { + for (index, column) in schema[exprs.len()..].iter().enumerate() { + exprs.push(visitor.arena.alloc_expression( + ScalarExpression::column_expr(*column, width + index), + )); + } + } + } + Ok(()) + })?; + } + // TODO: Handle other join types and conditions appropriately. + Childrens::Twins { left, right } + if matches!(&plan.operator, Operator::Join(op) + if op.join_type == JoinType::Cross && op.on == JoinCondition::None) => + { + let old_left_width = left.output_schema(self.arena).len(); + let left_mode = match output { + OutputMode::ColumnsOnly => OutputMode::ColumnsOnly, + _ => OutputMode::Inline, + }; + let mut left_mapping = None; + self.visit_input(left, left_mode, |visitor, _| { + left_mapping = visitor.mapping.exprs.take(); + Ok(()) + })?; + let offset = left.output_schema(self.arena).len(); + self.visit_input(right, OutputMode::ColumnsOnly, |visitor, _| { + let mut right_mapping = visitor.mapping.exprs.take(); + if let Some(exprs) = right_mapping.as_mut().filter(|_| offset != 0) { + visitor.mapping.offset = offset; + visitor.mapping.visited.clear(); + for expr in exprs { + // Keep aliases shared with retained branches untouched. + *expr = expr.unpack_alias(visitor.arena); + ExprVisitorMut::visit(&mut visitor.mapping, expr, visitor.arena)?; + } + visitor.mapping.offset = 0; + } + let mut append_identity = |exprs: &mut Vec<_>, columns: &[_], start| { + exprs.extend(columns.iter().enumerate().map(|(index, column)| { + visitor + .arena + .alloc_expression(ScalarExpression::column_expr( + *column, + start + index, + )) + })); + }; + match (left_mapping.as_mut(), right_mapping.as_mut()) { + (Some(left), Some(right)) => left.append(right), + (Some(left), None) => { + append_identity(left, &schema[old_left_width..], offset) + } + (None, Some(right)) => { + append_identity(right, &schema[..old_left_width], 0); + right.rotate_right(old_left_width); + } + (None, None) => (), + } + visitor.mapping.exprs = left_mapping.or(right_mapping); + visitor.mapping.visited.clear(); + Ok(()) + })?; + } + Childrens::Twins { left, right } => { + // Other binary operators retain their input layouts. + self.visit_input(left, OutputMode::Preserve, |_, _| Ok(()))?; + self.visit_input(right, OutputMode::Preserve, |_, _| Ok(()))?; + } + Childrens::None => (), + } + match &mut plan.operator { + Operator::Project(op) => { + self.mapping.exprs = None; // Project defines a new output slot space. + if !opaque + && match output { + OutputMode::Preserve => false, + OutputMode::Inline => true, + OutputMode::ColumnsOnly => op.exprs.iter().all(|expr| { + matches!( + expr.unpack_alias_ref(self.arena), + ScalarExpression::ColumnRef { .. } + ) + }), + } + { + let exprs = std::mem::take(&mut op.exprs); + replace_with_only_child(plan); + self.mapping.exprs = Some(exprs); + self.mapping.visited.clear(); + self.changed = true; + return Ok(()); + } + for (expr, column) in op.exprs.iter_mut().zip(&schema) { + let output = expr.output_column_ref(self.arena); + if self.arena.column(output).name() != self.arena.column(*column).name() { + let alias = AliasType::Name(self.arena.column(*column).name().to_string()); + *expr = self + .arena + .alloc_expression(ScalarExpression::Alias { expr: *expr, alias }); + } + } + op.moveable = None; + } + Operator::Aggregate(_) => self.mapping.exprs = None, + _ => (), + } + plan.reset_output_schema_cache(); + if self.changed { + plan.clear_startup_work(); + } + self.mapping.exprs = match (output, self.mapping.exprs.take()) { + (OutputMode::Preserve, Some(exprs)) => { + let input = plan.take(); + *plan = LogicalPlan::new( + Operator::Project(ProjectOperator::new(exprs)), + Childrens::Only(Box::new(input)), + ); + None + } + (_, exprs) => exprs, + }; + Ok(()) + } +} + +impl NormalizationRule for EliminateProject { + fn apply( + &self, + plan: &mut LogicalPlan, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, + ) -> Result { + let mut visitor = EliminateVisitor { + arena, + mapping: Substitute::default(), + output: OutputMode::Preserve, + changed: false, + }; + visitor.visit_plan(plan)?; + Ok(visitor.changed) + } +} pub struct EliminateRedundantSort; @@ -516,28 +786,565 @@ pub(crate) fn covers( #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::{ - EliminateIndexFilter, EliminateRedundantSort, ForceSpillAggregate, UseStreamAggregate, + EliminateIndexFilter, EliminateProject, EliminateRedundantSort, EliminateVisitor, + ForceSpillAggregate, OutputMode, Substitute, UseStreamAggregate, }; + use crate::binder::test::build_t1_table; use crate::catalog::{ColumnCatalog, TableName}; use crate::errors::DatabaseError; + use crate::expression::agg::AggKind; use crate::expression::range_detacher::Range; - use crate::expression::ScalarExpression; + use crate::expression::{BinaryOperator, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; + use crate::optimizer::heuristic::batch::HepBatchStrategy; + use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; + use crate::optimizer::rule::normalization::NormalizationRuleImpl; use crate::planner::operator::aggregate::AggregateOperator; use crate::planner::operator::filter::FilterOperator; + use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::operator::limit::LimitOperator; + use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::top_k::TopKOperator; + use crate::planner::operator::values::ValuesOperator; use crate::planner::operator::{ Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption, }; - use crate::planner::{Childrens, ExprRef, LogicalPlan}; + use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; use crate::types::index::{IndexInfo, IndexLookup, IndexMeta, IndexType}; use crate::types::value::DataValue; use crate::types::ColumnId; use crate::types::LogicalType; use std::ops::Bound; + fn column(arena: &mut PlanArena, source: ExprRef, position: usize) -> ExprRef { + let column = source.output_column_ref(arena); + arena.alloc_expression(ScalarExpression::column_expr(column, position)) + } + + fn column_expr(arena: &mut PlanArena, name: &str, position: usize) -> ExprRef { + let column = arena.alloc_column(ColumnCatalog::new_dummy(name.to_string())); + arena.alloc_expression(ScalarExpression::column_expr(column, position)) + } + + fn binary(arena: &mut PlanArena, left: ExprRef, op: BinaryOperator, value: i32) -> ExprRef { + let right = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(value))); + arena.alloc_expression(ScalarExpression::Binary { + op, + left_expr: left, + right_expr: right, + evaluator: None, + ty: if matches!(op, BinaryOperator::Gt) { + LogicalType::Boolean + } else { + LogicalType::Integer + }, + }) + } + + fn source(arena: &mut PlanArena, mut plan: LogicalPlan) -> (LogicalPlan, ExprRef) { + let Operator::Project(op) = plan.operator else { + unreachable!() + }; + let expr = binary(arena, op.exprs[1], BinaryOperator::Plus, 1); + let input = plan.childrens.pop_only(); + plan = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![expr, op.exprs[0]])), + Childrens::Only(Box::new(input)), + ); + (plan, expr) + } + + #[test] + fn substitutes_through_filter_and_sort_without_cloning_sources() -> Result<(), DatabaseError> { + let table = build_t1_table()?; + let mut arena = PlanArena::new(&table.table_arena); + let scan = table.plan_with_arena("select c1, c2 from t1", &mut arena)?; + let (input, replacement) = source(&mut arena, scan); + let predicate_column = column(&mut arena, replacement, 0); + let predicate = binary(&mut arena, predicate_column, BinaryOperator::Gt, 10); + let filter = LogicalPlan::new( + Operator::Filter(FilterOperator { + predicate, + having: false, + is_optimized: false, + }), + Childrens::Only(Box::new(input)), + ); + let sort_column = column(&mut arena, replacement, 0); + let sort = LogicalPlan::new( + Operator::Sort(SortOperator { + sort_fields: vec![SortField { + expr: sort_column, + asc: true, + nulls_first: false, + }], + }), + Childrens::Only(Box::new(filter)), + ); + let result_column = column(&mut arena, replacement, 0); + let result = binary(&mut arena, result_column, BinaryOperator::Multiply, 2); + let mut plan = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![result])), + Childrens::Only(Box::new(sort)), + ); + + assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + let Operator::Project(op) = &plan.operator else { + unreachable!() + }; + let ScalarExpression::Binary { left_expr, .. } = op.exprs[0].unpack_alias_ref(&arena) + else { + unreachable!() + }; + assert_eq!(*left_expr, replacement); + let Childrens::Only(sort) = plan.childrens.as_ref() else { + unreachable!() + }; + let Operator::Sort(op) = &sort.operator else { + unreachable!() + }; + assert_eq!(op.sort_fields[0].expr, replacement); + let Childrens::Only(filter) = sort.childrens.as_ref() else { + unreachable!() + }; + let Operator::Filter(op) = &filter.operator else { + unreachable!() + }; + let ScalarExpression::Binary { left_expr, .. } = arena.expression(op.predicate) else { + unreachable!() + }; + assert_eq!(*left_expr, replacement); + let Childrens::Only(scan) = filter.childrens.as_ref() else { + unreachable!() + }; + assert!(matches!(scan.operator, Operator::TableScan(_))); + assert!(!EliminateProject.apply(&mut plan, &mut arena, None)?); + Ok(()) + } + + #[test] + fn aggregate_consumes_mapping_but_its_output_starts_a_new_slot_space( + ) -> Result<(), DatabaseError> { + let table = build_t1_table()?; + let mut arena = PlanArena::new(&table.table_arena); + let scan = table.plan_with_arena("select c1, c2 from t1", &mut arena)?; + let (input, replacement) = source(&mut arena, scan); + let arg = column(&mut arena, replacement, 0); + let sum = arena.alloc_expression(ScalarExpression::AggCall { + distinct: false, + kind: AggKind::Sum, + args: vec![arg], + ty: LogicalType::Integer, + }); + let aggregate = LogicalPlan::new( + Operator::Aggregate(AggregateOperator { + agg_calls: vec![sum], + groupby_exprs: vec![], + is_distinct: false, + force_spill: false, + }), + Childrens::Only(Box::new(input)), + ); + let output = column(&mut arena, sum, 0); + let mut plan = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![output])), + Childrens::Only(Box::new(aggregate)), + ); + + assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + let Operator::Project(op) = &plan.operator else { + unreachable!() + }; + assert_eq!(op.exprs[0], output); + let Childrens::Only(aggregate) = plan.childrens.as_ref() else { + unreachable!() + }; + let Operator::Aggregate(op) = &aggregate.operator else { + unreachable!() + }; + let ScalarExpression::AggCall { args, .. } = arena.expression(op.agg_calls[0]) else { + unreachable!() + }; + assert_eq!(args, &[replacement]); + let Childrens::Only(scan) = aggregate.childrens.as_ref() else { + unreachable!() + }; + assert!(matches!(scan.operator, Operator::TableScan(_))); + Ok(()) + } + + #[test] + fn repeated_refs_are_rewritten_once_and_replacements_are_not_revisited( + ) -> Result<(), DatabaseError> { + let table = build_t1_table()?; + let mut arena = PlanArena::new(&table.table_arena); + let scan = table.plan_with_arena("select c1, c2 from t1", &mut arena)?; + let (input, replacement) = source(&mut arena, scan); + let reference = column(&mut arena, replacement, 0); + let expr = binary(&mut arena, reference, BinaryOperator::Plus, 2); + let mut plan = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![expr, expr])), + Childrens::Only(Box::new(input)), + ); + + assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + let ScalarExpression::Binary { left_expr, .. } = arena.expression(expr) else { + unreachable!() + }; + assert_eq!(*left_expr, replacement); + let ScalarExpression::Binary { left_expr, .. } = arena.expression(replacement) else { + unreachable!() + }; + let ScalarExpression::ColumnRef { position, .. } = left_expr.unpack_alias_ref(&arena) + else { + unreachable!() + }; + assert_eq!(*position, 1); + Ok(()) + } + + #[test] + fn mapping_transfer_keeps_shared_expressions_visited_across_plan_nodes( + ) -> Result<(), DatabaseError> { + let table = build_t1_table()?; + let mut arena = PlanArena::new(&table.table_arena); + let scan = table.plan_with_arena("select c1, c2 from t1", &mut arena)?; + let (input, replacement) = source(&mut arena, scan); + let reference = column(&mut arena, replacement, 0); + let shared = binary(&mut arena, reference, BinaryOperator::Gt, 10); + let filter = LogicalPlan::new( + Operator::Filter(FilterOperator { + predicate: shared, + having: false, + is_optimized: false, + }), + Childrens::Only(Box::new(input)), + ); + let mut plan = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![shared])), + Childrens::Only(Box::new(filter)), + ); + + assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + let ScalarExpression::Binary { left_expr, .. } = arena.expression(shared) else { + unreachable!() + }; + assert_eq!(*left_expr, replacement); + let ScalarExpression::Binary { left_expr, .. } = arena.expression(replacement) else { + unreachable!() + }; + let ScalarExpression::ColumnRef { position, .. } = left_expr.unpack_alias_ref(&arena) + else { + unreachable!() + }; + assert_eq!(*position, 1); + assert!(!EliminateProject.apply(&mut plan, &mut arena, None)?); + Ok(()) + } + + fn values(arena: &mut PlanArena, names: &[&str]) -> LogicalPlan { + let schema = names + .iter() + .map(|name| arena.alloc_column(ColumnCatalog::new_dummy(name.to_string()))) + .collect(); + LogicalPlan::new( + Operator::Values(ValuesOperator::new(vec![], 0, schema)), + Childrens::None, + ) + } + + fn permutation(arena: &mut PlanArena, mut input: LogicalPlan, slots: &[usize]) -> LogicalPlan { + let schema = input.output_schema(arena).clone(); + let exprs = slots + .iter() + .map(|position| { + arena.alloc_expression(ScalarExpression::column_expr(schema[*position], *position)) + }) + .collect(); + LogicalPlan::new( + Operator::Project(ProjectOperator::new(exprs)), + Childrens::Only(Box::new(input)), + ) + } + + #[test] + fn cross_join_composes_p4_and_p9_into_final_computations() -> Result<(), DatabaseError> { + let table_arena = crate::planner::TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let outer_left = values(&mut arena, &["d2", "e2", "c5"]); + let t3 = values(&mut arena, &["a3", "e3", "x3"]); + let t4 = values(&mut arena, &["b4", "d4"]); + let inner = JoinOperator::build(t3, t4, JoinCondition::None, JoinType::Inner, false); + let t9 = values(&mut arena, &["b9", "d9"]); + let t1 = values(&mut arena, &["a1", "e1"]); + let cross4 = JoinOperator::build(t9, t1, JoinCondition::None, JoinType::Cross, false); + let p4 = permutation(&mut arena, cross4, &[2, 3, 0, 1]); + let cross9 = JoinOperator::build(inner, p4, JoinCondition::None, JoinType::Cross, false); + let p9 = permutation(&mut arena, cross9, &[5, 6, 7, 8, 0, 1, 2, 3, 4]); + let outer = + JoinOperator::build(outer_left, p9, JoinCondition::None, JoinType::Cross, false); + let mut plan = permutation(&mut arena, outer, &(0..12).collect::>()); + let Operator::Project(op) = &mut plan.operator else { + unreachable!() + }; + for expr in &mut op.exprs { + *expr = binary(&mut arena, *expr, BinaryOperator::Multiply, 2); + } + let schema = plan.output_schema(&mut arena).clone(); + + assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + assert_eq!(plan.explain(&mut arena, 0).matches("Projection").count(), 1); + let Operator::Project(op) = &plan.operator else { + unreachable!() + }; + for (expr, expected) in op.exprs.iter().zip([0, 1, 2, 10, 11, 8, 9, 3, 4, 5, 6, 7]) { + let ScalarExpression::Binary { left_expr, .. } = expr.unpack_alias_ref(&arena) else { + unreachable!() + }; + let ScalarExpression::ColumnRef { position, .. } = left_expr.unpack_alias_ref(&arena) + else { + unreachable!() + }; + assert_eq!(*position, expected); + } + let Childrens::Only(input) = plan.childrens.as_mut() else { + unreachable!() + }; + let names: Vec<_> = input + .output_schema(&mut arena) + .iter() + .map(|column| arena.column(*column).name().to_string()) + .collect(); + assert_eq!( + names, + ["d2", "e2", "c5", "a3", "e3", "x3", "b4", "d4", "b9", "d9", "a1", "e1"] + ); + assert!(!EliminateProject.apply(&mut plan, &mut arena, None)?); + Ok(()) + } + + #[test] + fn cross_join_uses_new_left_width_for_mapped_and_unchanged_right_inputs( + ) -> Result<(), DatabaseError> { + for (right_slots, expected) in [(Some(&[1, 0][..]), [2, 4, 3]), (None, [2, 3, 4])] { + let table_arena = crate::planner::TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let left = values(&mut arena, &["l0", "l1", "l2"]); + let left = permutation(&mut arena, left, &[2]); + let right = values(&mut arena, &["r0", "r1"]); + let right = match right_slots { + Some(slots) => permutation(&mut arena, right, slots), + None => right, + }; + let cross = + JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); + let mut plan = permutation(&mut arena, cross, &[0, 1, 2]); + + assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.explain(&mut arena, 0).matches("Projection").count(), 1); + let Operator::Project(op) = &plan.operator else { + unreachable!() + }; + for (expr, expected) in op.exprs.iter().zip(expected) { + let ScalarExpression::ColumnRef { position, .. } = expr.unpack_alias_ref(&arena) + else { + unreachable!() + }; + assert_eq!(*position, expected); + } + assert!(!EliminateProject.apply(&mut plan, &mut arena, None)?); + } + Ok(()) + } + + #[test] + fn cross_join_callback_reuses_an_existing_mapping_buffer() -> Result<(), DatabaseError> { + for (map_left, map_right, expected) in [ + (true, true, [1, 0, 3, 2]), + (true, false, [1, 0, 2, 3]), + (false, true, [0, 1, 3, 2]), + ] { + let table_arena = crate::planner::TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let mut left = values(&mut arena, &["l0", "l1"]); + let mut right = values(&mut arena, &["r0", "r1"]); + if map_left { + left = permutation(&mut arena, left, &[1, 0]); + } + if map_right { + right = permutation(&mut arena, right, &[1, 0]); + } + let source = if map_left { &mut left } else { &mut right }; + let Operator::Project(op) = &mut source.operator else { + unreachable!() + }; + op.exprs.reserve(8); + let buffer = op.exprs.as_ptr(); + let mut plan = + JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); + let mut visitor = EliminateVisitor { + arena: &mut arena, + mapping: Substitute::default(), + output: OutputMode::Inline, + changed: false, + }; + visitor.visit_input(&mut plan, OutputMode::Inline, |visitor, _| { + let Some(exprs) = &visitor.mapping.exprs else { + panic!("expected a mapping") + }; + assert_eq!(exprs.as_ptr(), buffer); + assert_eq!(exprs.len(), expected.len()); + for (expr, expected) in exprs.iter().zip(expected) { + let ScalarExpression::ColumnRef { position, .. } = + expr.unpack_alias_ref(visitor.arena) + else { + unreachable!() + }; + assert_eq!(*position, expected); + } + Ok(()) + })?; + } + Ok(()) + } + + #[test] + fn cross_join_distinguishes_empty_mapping_from_identity() -> Result<(), DatabaseError> { + let table_arena = crate::planner::TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let left = values(&mut arena, &["l0", "l1", "l2"]); + let left = permutation(&mut arena, left, &[]); + let right = values(&mut arena, &["r0", "r1"]); + let cross = JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); + let mut plan = permutation(&mut arena, cross, &[0, 1]); + + assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.explain(&mut arena, 0).matches("Projection").count(), 1); + let Operator::Project(op) = &plan.operator else { + unreachable!() + }; + for (expr, expected) in op.exprs.iter().zip([3, 4]) { + let ScalarExpression::ColumnRef { position, .. } = expr.unpack_alias_ref(&arena) else { + unreachable!() + }; + assert_eq!(*position, expected); + } + assert!(!EliminateProject.apply(&mut plan, &mut arena, None)?); + Ok(()) + } + + #[test] + fn cross_join_preserves_computed_right_output_without_copying_it() -> Result<(), DatabaseError> + { + let table = build_t1_table()?; + let mut arena = PlanArena::new(&table.table_arena); + let scan = table.plan_with_arena("select c1, c2 from t1", &mut arena)?; + let (right, _) = source(&mut arena, scan); + let left = values(&mut arena, &["l0"]); + let cross = JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); + let mut plan = permutation(&mut arena, cross, &[0, 1, 2]); + + assert!(!EliminateProject.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.explain(&mut arena, 0).matches("Projection").count(), 2); + Ok(()) + } + + #[test] + fn test_collapse_project() -> Result<(), DatabaseError> { + let table_state = build_t1_table()?; + let mut arena = PlanArena::new(&table_state.table_arena); + let plan = + table_state.plan_with_arena("select c1 from (select c1, c2 from t1) t", &mut arena)?; + + let pipeline = HepOptimizerPipeline::builder() + .before_batch( + "test_collapse_project", + HepBatchStrategy::once_topdown(), + vec![NormalizationRuleImpl::EliminateProject], + ) + .build(); + let best_plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; + + if let Operator::Project(op) = &best_plan.operator { + assert_eq!(op.exprs.len(), 1); + } else { + unreachable!("Should be a project operator") + } + + let scan_op = best_plan.childrens.pop_only(); + assert!(matches!(scan_op.operator, Operator::TableScan(_))); + assert!(matches!(scan_op.childrens.as_ref(), Childrens::None)); + + Ok(()) + } + + #[test] + fn test_collapse_project_with_alias() -> Result<(), DatabaseError> { + let table_state = build_t1_table()?; + let mut arena = PlanArena::new(&table_state.table_arena); + let plan = table_state + .plan_with_arena("select t.x from (select c1 as x from t1) t", &mut arena)?; + let original = plan.clone(); + let original_child = original.childrens.pop_only(); + assert!(matches!(original_child.operator, Operator::Project(_))); + let original_grandchild = original_child.childrens.pop_only(); + assert!(matches!(original_grandchild.operator, Operator::Project(_))); + + let pipeline = HepOptimizerPipeline::builder() + .before_batch( + "test_collapse_project_with_alias", + HepBatchStrategy::once_topdown(), + vec![NormalizationRuleImpl::EliminateProject], + ) + .build(); + let best_plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; + if let Operator::Project(op) = &best_plan.operator { + assert_eq!(op.exprs.len(), 1); + } else { + unreachable!("Should be a project operator"); + } + + let scan_op = best_plan.childrens.pop_only(); + assert!(matches!(scan_op.operator, Operator::TableScan(_))); + + Ok(()) + } + + #[test] + fn test_collapse_project_remaps_reordered_passthrough_positions() -> Result<(), DatabaseError> { + let table_arena = crate::planner::TableArenaCell::default(); + let mut arena = PlanArena::new(&table_arena); + let child = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![ + column_expr(&mut arena, "c2", 1), + column_expr(&mut arena, "c1", 0), + ])), + Childrens::Only(Box::new(LogicalPlan::new(Operator::Dummy, Childrens::None))), + ); + let mut plan = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![column_expr(&mut arena, "c2", 0)])), + Childrens::Only(Box::new(child)), + ); + + assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + + let Operator::Project(op) = &plan.operator else { + unreachable!("expected project"); + }; + let ScalarExpression::ColumnRef { position, .. } = op.exprs[0].unpack_alias_ref(&arena) + else { + unreachable!("expected column ref"); + }; + assert_eq!(*position, 1); + assert!(matches!( + plan.childrens.pop_only().operator, + Operator::Dummy + )); + Ok(()) + } + fn make_sort_field(arena: &mut crate::planner::PlanArena, name: &str) -> SortField { make_sort_field_with_position(arena, name, 0) } diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index c57d1df2..7d21208d 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -585,12 +585,12 @@ impl JoinKeys { struct BindPosition<'a>(&'a Schema); -impl ExprVisitorMut for BindPosition<'_> { +impl ExprVisitorMut for BindPosition<'_> { fn visit_column_ref( &mut self, column: &mut ColumnRef, position: &mut usize, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { *position = self .0 diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index cc51ce61..ea191a4e 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -18,9 +18,7 @@ use crate::expression::{AliasType, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::rule::normalization::column_pruning::ColumnPruning; -use crate::optimizer::rule::normalization::combine_operators::{ - CollapseGroupByAgg, CollapseProject, CombineFilter, -}; +use crate::optimizer::rule::normalization::combine_operators::{CollapseGroupByAgg, CombineFilter}; use crate::optimizer::rule::normalization::compilation_in_advance::EvaluatorBind; use crate::planner::operator::Operator; use crate::planner::MetaArena; @@ -57,8 +55,8 @@ mod simplification; mod top_k; pub(crate) use compilation_in_advance::evaluator_bind_current; pub(crate) use elimination::{ - apply_annotated_post_rules, apply_scan_order_hint, EliminateIndexFilter, OrderHintKind, - ScanOrderHint, + apply_annotated_post_rules, apply_scan_order_hint, EliminateIndexFilter, EliminateProject, + OrderHintKind, ScanOrderHint, }; pub(crate) use parameterized_index::{ParameterizeInnerJoin, ParameterizeMarkApply}; pub(crate) use simplification::constant_calculation_current; @@ -67,7 +65,7 @@ pub(crate) use simplification::constant_calculation_current; pub enum NormalizationRuleImpl { ColumnPruning, // Combine operators - CollapseProject, + EliminateProject, CollapseGroupByAgg, CombineFilter, // PushDown limit @@ -99,6 +97,7 @@ pub enum NormalizationRuleImpl { #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum WholeTreePassKind { ColumnPruning, + EliminateProject, ExpressionRewrite, } @@ -173,6 +172,9 @@ impl NormalizationRuleImpl { NormalizationRuleImpl::ColumnPruning => { NormalizationPassKind::WholeTreePass(WholeTreePassKind::ColumnPruning) } + NormalizationRuleImpl::EliminateProject => { + NormalizationPassKind::WholeTreePass(WholeTreePassKind::EliminateProject) + } NormalizationRuleImpl::ConstantCalculation | NormalizationRuleImpl::EvaluatorBind => { NormalizationPassKind::WholeTreePass(WholeTreePassKind::ExpressionRewrite) } @@ -183,10 +185,11 @@ impl NormalizationRuleImpl { pub fn root_tag(&self) -> NormalizationRuleRootTag { match self { NormalizationRuleImpl::ColumnPruning => NormalizationRuleRootTag::Any, - NormalizationRuleImpl::CollapseProject - | NormalizationRuleImpl::GuardMarkApply - | NormalizationRuleImpl::ProjectMove => NormalizationRuleRootTag::Project, + NormalizationRuleImpl::EliminateProject => NormalizationRuleRootTag::Any, NormalizationRuleImpl::CollapseGroupByAgg => NormalizationRuleRootTag::Aggregate, + NormalizationRuleImpl::GuardMarkApply | NormalizationRuleImpl::ProjectMove => { + NormalizationRuleRootTag::Project + } NormalizationRuleImpl::CombineFilter => NormalizationRuleRootTag::Filter, NormalizationRuleImpl::LimitProjectTranspose | NormalizationRuleImpl::PushLimitThroughJoin @@ -225,7 +228,7 @@ impl NormalizationRule for NormalizationRuleImpl { OrientNestedLoopJoin.apply(plan, arena, loader) } NormalizationRuleImpl::GuardMarkApply => GuardMarkApply.apply(plan, arena, loader), - NormalizationRuleImpl::CollapseProject => CollapseProject.apply(plan, arena, loader), + NormalizationRuleImpl::EliminateProject => EliminateProject.apply(plan, arena, loader), NormalizationRuleImpl::ProjectMove => ProjectMove.apply(plan, arena, loader), NormalizationRuleImpl::CollapseGroupByAgg => { CollapseGroupByAgg.apply(plan, arena, loader) @@ -272,7 +275,7 @@ impl NormalizationRule for NormalizationRuleImpl { } } -pub(crate) fn strip_alias(expr: ExprRef, arena: &crate::planner::PlanArena<'_>) -> ExprRef { +pub(crate) fn strip_alias(expr: ExprRef, arena: &A) -> ExprRef { match arena.expression(expr) { ScalarExpression::Alias { expr, @@ -318,11 +321,11 @@ impl<'positions, 'visited> PositionRemapper<'positions, 'visited> { } } -impl ExprVisitorMut for PositionRemapper<'_, '_> { +impl ExprVisitorMut for PositionRemapper<'_, '_> { fn visit_expression_ref( &mut self, expr: &mut ExprRef, - _arena: &mut (dyn MetaArena + '_), + _arena: &mut A, ) -> Result { Ok(self.visited.insert(*expr)) } @@ -331,7 +334,7 @@ impl ExprVisitorMut for PositionRemapper<'_, '_> { &mut self, _column: &mut crate::catalog::ColumnRef, position: &mut usize, - _arena: &mut (dyn MetaArena + '_), + _arena: &mut A, ) -> Result<(), DatabaseError> { remap_position(position, self.removed_positions); Ok(()) @@ -341,7 +344,7 @@ impl ExprVisitorMut for PositionRemapper<'_, '_> { &mut self, expr: &mut ExprRef, alias: &mut AliasType, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result<(), DatabaseError> { match alias { AliasType::Expr(alias_expr) => self.visit(alias_expr, arena), @@ -350,20 +353,20 @@ impl ExprVisitorMut for PositionRemapper<'_, '_> { } } -pub(crate) fn remap_expr_positions( +pub(crate) fn remap_expr_positions( mut expr: ExprRef, removed_positions: &[usize], visited: &mut HashSet, - arena: &mut crate::planner::PlanArena<'_>, + arena: &mut A, ) -> Result<(), DatabaseError> { PositionRemapper::new(removed_positions, visited).visit(&mut expr, arena) } -pub(crate) fn remap_exprs_positions<'a>( +pub(crate) fn remap_exprs_positions<'a, A: MetaArena + ?Sized>( exprs: impl IntoIterator, removed_positions: &[usize], visited: &mut HashSet, - arena: &mut crate::planner::PlanArena<'_>, + arena: &mut A, ) -> Result<(), DatabaseError> { let mut remapper = PositionRemapper::new(removed_positions, visited); for expr in exprs { diff --git a/src/optimizer/rule/normalization/simplification.rs b/src/optimizer/rule/normalization/simplification.rs index 2f8a2629..b86eaf6b 100644 --- a/src/optimizer/rule/normalization/simplification.rs +++ b/src/optimizer/rule/normalization/simplification.rs @@ -20,14 +20,14 @@ use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; use crate::planner::operator::Operator; use crate::planner::PlanArena; -use crate::planner::{Childrens, LogicalPlan}; +use crate::planner::{Childrens, LogicalPlan, MetaArena}; #[derive(Copy, Clone)] pub struct ConstantCalculation; -pub(crate) fn constant_calculation_current( +pub(crate) fn constant_calculation_current( plan: &mut LogicalPlan, - arena: &mut crate::planner::PlanArena, + arena: &mut A, ) -> Result<(), DatabaseError> { let mut calculator = ConstantCalculator::new(arena); OperatorExprVisitorMut::new(&mut calculator, arena).visit_operator(&mut plan.operator, None) diff --git a/src/planner/arena.rs b/src/planner/arena.rs index fe780779..7473d107 100644 --- a/src/planner/arena.rs +++ b/src/planner/arena.rs @@ -45,12 +45,12 @@ impl ScalarQueryRelocator { } } -impl ExprVisitorMut for ScalarQueryRelocator { +impl ExprVisitorMut for ScalarQueryRelocator { fn visit_outer_param( &mut self, reference: &mut ScalarQueryRef, _ty: &mut LogicalType, - _arena: &mut dyn MetaArena, + _arena: &mut A, ) -> Result<(), DatabaseError> { self.relocate(reference); Ok(()) @@ -60,7 +60,7 @@ impl ExprVisitorMut for ScalarQueryRelocator { &mut self, reference: &mut ScalarQueryRef, _ty: &mut LogicalType, - _arena: &mut dyn MetaArena, + _arena: &mut A, ) -> Result<(), DatabaseError> { self.relocate(reference); Ok(()) @@ -70,19 +70,19 @@ impl ExprVisitorMut for ScalarQueryRelocator { &mut self, reference: &mut ScalarQueryRef, _ty: &mut LogicalType, - _arena: &mut dyn MetaArena, + _arena: &mut A, ) -> Result<(), DatabaseError> { self.relocate(reference); Ok(()) } } -struct ScalarQueryPlanRelocator<'a> { +struct ScalarQueryPlanRelocator<'a, A: ?Sized> { references: ScalarQueryRelocator, - arena: &'a mut dyn MetaArena, + arena: &'a mut A, } -impl<'a> OperatorVisitorMut<'a> for ScalarQueryPlanRelocator<'_> { +impl<'a, A: MetaArena + ?Sized> OperatorVisitorMut<'a> for ScalarQueryPlanRelocator<'_, A> { fn visit_operator( &mut self, operator: &'a mut Operator, diff --git a/src/planner/mod.rs b/src/planner/mod.rs index f298c710..becadd54 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -37,9 +37,9 @@ pub(crate) use plan_keeper::{PlanInput, PlanKeeper}; pub use scalar_query_ref::ScalarQueryRef; pub(crate) trait Explain { - fn fmt(&self, arena: &(dyn MetaArena + '_), f: &mut fmt::Formatter<'_>) -> fmt::Result; + fn fmt(&self, arena: &A, f: &mut fmt::Formatter<'_>) -> fmt::Result; - fn explain<'a, 'p>(&'a self, arena: &'a (dyn MetaArena + 'p)) -> ExplainDisplay<'a, 'p, Self> + fn explain<'a, A: MetaArena + ?Sized>(&'a self, arena: &'a A) -> ExplainDisplay<'a, Self, A> where Self: Sized, { @@ -47,21 +47,21 @@ pub(crate) trait Explain { } } -pub(crate) struct ExplainDisplay<'a, 'p, T: ?Sized> { +pub(crate) struct ExplainDisplay<'a, T: ?Sized, A: ?Sized> { value: &'a T, - arena: &'a (dyn MetaArena + 'p), + arena: &'a A, } -impl fmt::Display for ExplainDisplay<'_, '_, T> { +impl fmt::Display for ExplainDisplay<'_, T, A> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.value.fmt(self.arena, f) } } -pub(crate) fn fmt_explain_list( +pub(crate) fn fmt_explain_list( values: &[T], separator: &str, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { for (index, value) in values.iter().enumerate() { @@ -195,9 +195,9 @@ impl LogicalPlan { } } - pub(crate) fn clone_plan( + pub(crate) fn clone_plan( &self, - arena: &mut (dyn MetaArena + '_), + arena: &mut A, ) -> Result { let mut plan = self.clone(); OperatorExprVisitorMut::new(&mut ExprCloner, arena).visit_plan(&mut plan)?; @@ -488,7 +488,7 @@ impl LogicalPlan { } impl Explain for LogicalPlan { - fn fmt(&self, arena: &(dyn MetaArena + '_), f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, arena: &A, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.operator.fmt(arena, f)?; if let Some(physical_option) = &self.physical_option { diff --git a/src/planner/operator/aggregate.rs b/src/planner/operator/aggregate.rs index 6457aeb9..28d97131 100644 --- a/src/planner/operator/aggregate.rs +++ b/src/planner/operator/aggregate.rs @@ -46,9 +46,9 @@ impl AggregateOperator { } impl Explain for AggregateOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { f.write_str("Aggregate [")?; diff --git a/src/planner/operator/alter_table/change_column.rs b/src/planner/operator/alter_table/change_column.rs index 6c8b2a33..f0b71ec1 100644 --- a/src/planner/operator/alter_table/change_column.rs +++ b/src/planner/operator/alter_table/change_column.rs @@ -43,9 +43,9 @@ pub struct ChangeColumnOperator { } impl Explain for ChangeColumnOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { write!( diff --git a/src/planner/operator/analyze.rs b/src/planner/operator/analyze.rs index e82566d7..b2b4f858 100644 --- a/src/planner/operator/analyze.rs +++ b/src/planner/operator/analyze.rs @@ -29,9 +29,9 @@ pub struct AnalyzeOperator { } impl Explain for AnalyzeOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { write!(f, "Analyze {} -> [", self.table_name)?; diff --git a/src/planner/operator/copy_from_file.rs b/src/planner/operator/copy_from_file.rs index a6c3418d..49b2bff4 100644 --- a/src/planner/operator/copy_from_file.rs +++ b/src/planner/operator/copy_from_file.rs @@ -27,9 +27,9 @@ pub struct CopyFromFileOperator { } impl Explain for CopyFromFileOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { write!(f, "Copy {} -> {} [", self.source.path.display(), self.table)?; diff --git a/src/planner/operator/create_index.rs b/src/planner/operator/create_index.rs index c367e6eb..9a31ee76 100644 --- a/src/planner/operator/create_index.rs +++ b/src/planner/operator/create_index.rs @@ -29,9 +29,9 @@ pub struct CreateIndexOperator { } impl Explain for CreateIndexOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { write!(f, "Create Index On {} -> [", self.table_name)?; diff --git a/src/planner/operator/filter.rs b/src/planner/operator/filter.rs index 2d76b8bd..84a0ed34 100644 --- a/src/planner/operator/filter.rs +++ b/src/planner/operator/filter.rs @@ -39,9 +39,9 @@ impl FilterOperator { } impl Explain for FilterOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { write!( diff --git a/src/planner/operator/join.rs b/src/planner/operator/join.rs index 7ae642cc..54954947 100644 --- a/src/planner/operator/join.rs +++ b/src/planner/operator/join.rs @@ -91,13 +91,13 @@ impl JoinOperator { } impl Explain for JoinOperator { - fn fmt(&self, arena: &(dyn MetaArena + '_), f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, arena: &A, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} Join{}", self.join_type, self.on.explain(arena)) } } impl Explain for JoinCondition { - fn fmt(&self, arena: &(dyn MetaArena + '_), f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, arena: &A, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { JoinCondition::On { on, filter } => { if !on.is_empty() { diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 58a14e1b..72b41d9c 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -432,9 +432,9 @@ pub enum PlanImpl { } impl Explain for ColumnRef { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { let column = arena.column(*self); @@ -447,9 +447,9 @@ impl Explain for ColumnRef { } impl Explain for IndexMetaRef { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { f.write_str(&arena.index(*self).name) @@ -461,7 +461,7 @@ macro_rules! impl_display_explain { $( $(#[$meta])* impl Explain for $ty { - fn fmt(&self, _arena: &(dyn MetaArena + '_), f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, _arena: &A, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self, f) } } @@ -490,9 +490,9 @@ impl_display_explain!( ); impl Explain for Operator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { match self { @@ -818,9 +818,9 @@ impl Operator { } impl Explain for PlanImpl { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { match self { @@ -864,9 +864,9 @@ impl Explain for PlanImpl { } impl Explain for SortOption { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { match self { @@ -885,9 +885,9 @@ impl Explain for SortOption { } impl Explain for PhysicalOption { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { write!( diff --git a/src/planner/operator/project.rs b/src/planner/operator/project.rs index 6314b753..fcd7ac83 100644 --- a/src/planner/operator/project.rs +++ b/src/planner/operator/project.rs @@ -33,9 +33,9 @@ impl ProjectOperator { } impl Explain for ProjectOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { f.write_str("Projection [")?; diff --git a/src/planner/operator/recursive_cte.rs b/src/planner/operator/recursive_cte.rs index 1a420655..6840dde8 100644 --- a/src/planner/operator/recursive_cte.rs +++ b/src/planner/operator/recursive_cte.rs @@ -36,9 +36,9 @@ impl RecursiveCteOperator { } impl Explain for RecursiveCteOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { f.write_str("Recursive CTE: [")?; @@ -53,9 +53,9 @@ pub struct RecursiveScanOperator { } impl Explain for RecursiveScanOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { f.write_str("Recursive Scan: [")?; diff --git a/src/planner/operator/scalar_query_init.rs b/src/planner/operator/scalar_query_init.rs index 1e3b22b8..9cbd2b3b 100644 --- a/src/planner/operator/scalar_query_init.rs +++ b/src/planner/operator/scalar_query_init.rs @@ -52,7 +52,7 @@ impl ScalarQueryInitOperator { } impl Explain for ScalarQueryInitOperator { - fn fmt(&self, arena: &dyn MetaArena, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, arena: &A, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "ScalarQueryInit {}", self.value.explain(arena)) } } diff --git a/src/planner/operator/set_membership.rs b/src/planner/operator/set_membership.rs index 47b538c0..f32d3b8b 100644 --- a/src/planner/operator/set_membership.rs +++ b/src/planner/operator/set_membership.rs @@ -64,9 +64,9 @@ impl SetMembershipOperator { } impl Explain for SetMembershipOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { write!(f, "{}: [", self.kind.name())?; diff --git a/src/planner/operator/sort.rs b/src/planner/operator/sort.rs index d930db0e..d298cf74 100644 --- a/src/planner/operator/sort.rs +++ b/src/planner/operator/sort.rs @@ -65,9 +65,9 @@ pub struct SortOperator { } impl Explain for SortOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { f.write_str("Sort By ")?; @@ -76,9 +76,9 @@ impl Explain for SortOperator { } impl Explain for SortField { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { let direction = if self.asc { "Asc" } else { "Desc" }; diff --git a/src/planner/operator/table_scan.rs b/src/planner/operator/table_scan.rs index 599ca376..8bf22a4b 100644 --- a/src/planner/operator/table_scan.rs +++ b/src/planner/operator/table_scan.rs @@ -92,9 +92,9 @@ impl TableScanOperator { } impl Explain for TableScanOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { write!(f, "TableScan {} -> [", self.table_name)?; diff --git a/src/planner/operator/top_k.rs b/src/planner/operator/top_k.rs index 7eb00cd9..c8624be0 100644 --- a/src/planner/operator/top_k.rs +++ b/src/planner/operator/top_k.rs @@ -44,9 +44,9 @@ impl TopKOperator { } impl Explain for TopKOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { write!(f, "Top {}, ", self.limit)?; diff --git a/src/planner/operator/union.rs b/src/planner/operator/union.rs index 95af467d..9c0179ce 100644 --- a/src/planner/operator/union.rs +++ b/src/planner/operator/union.rs @@ -46,9 +46,9 @@ impl UnionOperator { } impl Explain for UnionOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { f.write_str("Union: [")?; diff --git a/src/planner/operator/update.rs b/src/planner/operator/update.rs index d35b1ff3..472f5516 100644 --- a/src/planner/operator/update.rs +++ b/src/planner/operator/update.rs @@ -24,9 +24,9 @@ pub struct UpdateOperator { } impl Explain for UpdateOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { write!(f, "Update {} set ", self.table_name)?; diff --git a/src/planner/operator/values.rs b/src/planner/operator/values.rs index 0ebc5f2e..e0781ee2 100644 --- a/src/planner/operator/values.rs +++ b/src/planner/operator/values.rs @@ -41,7 +41,7 @@ impl ValuesOperator { } impl Explain for ValuesOperator { - fn fmt(&self, arena: &(dyn MetaArena + '_), f: &mut Formatter) -> fmt::Result { + fn fmt(&self, arena: &A, f: &mut Formatter) -> fmt::Result { f.write_str("Values ")?; let width = self.schema_ref.len(); for i in 0..self.row_count { diff --git a/src/planner/operator/visitor_mut.rs b/src/planner/operator/visitor_mut.rs index 07d51c32..ac0ca48c 100644 --- a/src/planner/operator/visitor_mut.rs +++ b/src/planner/operator/visitor_mut.rs @@ -254,18 +254,20 @@ pub trait OperatorVisitorMut<'a>: Sized { } } -pub struct OperatorExprVisitorMut<'a, 'arena, V> { +pub struct OperatorExprVisitorMut<'a, V, A: ?Sized> { visitor: &'a mut V, - arena: &'a mut (dyn MetaArena + 'arena), + arena: &'a mut A, } -impl<'a, 'arena, V> OperatorExprVisitorMut<'a, 'arena, V> { - pub fn new(visitor: &'a mut V, arena: &'a mut (dyn MetaArena + 'arena)) -> Self { +impl<'a, V, A: ?Sized> OperatorExprVisitorMut<'a, V, A> { + pub fn new(visitor: &'a mut V, arena: &'a mut A) -> Self { Self { visitor, arena } } } -impl<'a, V: ExprVisitorMut> OperatorVisitorMut<'a> for OperatorExprVisitorMut<'_, '_, V> { +impl<'a, V: ExprVisitorMut, A: MetaArena + ?Sized> OperatorVisitorMut<'a> + for OperatorExprVisitorMut<'_, V, A> +{ fn visit_values(&mut self, op: &'a mut ValuesOperator) -> Result<(), DatabaseError> { for expr in op.rows.iter_mut() { self.visitor.visit(expr, self.arena)?; @@ -475,11 +477,11 @@ mod tests { struct IncrementConstants(usize); - impl ExprVisitorMut for IncrementConstants { + impl ExprVisitorMut for IncrementConstants { fn visit_constant( &mut self, value: &mut DataValue, - _arena: &mut (dyn MetaArena + '_), + _arena: &mut A, ) -> Result<(), DatabaseError> { if let DataValue::Int32(value) = value { *value += 1; diff --git a/src/planner/operator/window.rs b/src/planner/operator/window.rs index 0c200128..ac918253 100644 --- a/src/planner/operator/window.rs +++ b/src/planner/operator/window.rs @@ -42,9 +42,9 @@ impl WindowOperator { } impl Explain for WindowOperator { - fn fmt( + fn fmt( &self, - arena: &(dyn MetaArena + '_), + arena: &A, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { let (partition_by, order_by) = self.sort_fields.split_at(self.partition_by_len); diff --git a/src/types/index.rs b/src/types/index.rs index 7a0ff9c4..b09cc940 100644 --- a/src/types/index.rs +++ b/src/types/index.rs @@ -162,7 +162,7 @@ impl IndexInfo { } impl Explain for IndexInfo { - fn fmt(&self, arena: &dyn MetaArena, f: &mut Formatter<'_>) -> fmt::Result { + fn fmt(&self, arena: &A, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{} => ", self.meta.explain(arena))?; match &self.lookup { Some(IndexLookup::Static(range)) => write!(f, "{range}")?, From af7d6d51d8428c8bf34cc4bb917725e10b28fa25 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 22/62] perf: materialize quantified subqueries with searchable runs --- src/execution/dql/external_sort.rs | 10 +- src/execution/dql/mark_apply.rs | 452 +++++++++------- src/execution/mod.rs | 1 + src/execution/runs.rs | 213 ++++++++ src/execution/spill/codec.rs | 83 +++ src/execution/spill/mod.rs | 157 ++++-- src/execution/spill/runs.rs | 501 ++++++++++++++++++ .../rule/normalization/guard_mark_apply.rs | 365 ++++++++++--- .../normalization/materialize_mark_apply.rs | 48 +- .../rule/normalization/parameterized_index.rs | 143 ++++- src/types/mod.rs | 64 +++ tests/slt/any_all_quantifier.slt | 156 ++++++ 12 files changed, 1878 insertions(+), 315 deletions(-) create mode 100644 src/execution/runs.rs create mode 100644 src/execution/spill/runs.rs diff --git a/src/execution/dql/external_sort.rs b/src/execution/dql/external_sort.rs index fec0e9f3..8c8b44bb 100644 --- a/src/execution/dql/external_sort.rs +++ b/src/execution/dql/external_sort.rs @@ -100,7 +100,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ExternalSort<'a> { // pass. By default R <= 1,024 and B is about 1 MiB; one oversized row makes this a // soft bound. let sort_fields = self.sort_fields; - let mut rows = SpillVec::new().on_flush(move |rows| sort_segment(sort_fields, rows)); + let mut rows = SpillVec::new().on_flush(move |_, rows| sort_segment(sort_fields, rows)); let mut runs = Vec::new(); while arena.next_tuple(self.input, plan_arena)? { let tuple = arena.materialize_tuple(); @@ -163,7 +163,7 @@ struct RunCursor<'source> { impl<'source> RunCursor<'source> { fn new(source: &'source SpillReader, run: &Run) -> Result { - let mut reader = source.open_segment_reader()?; + let mut reader = source.open_segment_reader(None)?; reader.reset(run.first_segment)?; let mut cursor = Self { remaining_segments: run.segment_count - 1, @@ -324,7 +324,7 @@ mod test { let mut plan_arena = ExecMetaArena::new(plan_arena); let mut rows = SpillVec::new() .limit(4, usize::MAX) - .on_flush(|rows| sort_segment(&sort_fields, rows)); + .on_flush(|_, rows| sort_segment(&sort_fields, rows)); for value in [DataValue::Int32(2), DataValue::Null, DataValue::Int32(1)] { let _ = rows.push(SortRow::new( &sort_fields, @@ -375,7 +375,7 @@ mod test { let mut plan_arena = ExecMetaArena::new(plan_arena); let mut rows = SpillVec::new() .limit(2, usize::MAX) - .on_flush(|rows| sort_segment(&sort_fields, rows)); + .on_flush(|_, rows| sort_segment(&sort_fields, rows)); let mut runs = Vec::new(); for (sequence, value) in values.into_iter().enumerate() { let tuple = Tuple::new( @@ -470,7 +470,7 @@ mod test { ]; let mut plan_arena = ExecMetaArena::new(plan_arena); - let mut rows = SpillVec::new().on_flush(|rows| sort_segment(&sort_fields, rows)); + let mut rows = SpillVec::new().on_flush(|_, rows| sort_segment(&sort_fields, rows)); let mut runs = Vec::new(); for position in 0..ROW_COUNT { let sequence = (position * 7919) % ROW_COUNT; diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index 31c7da10..688ca637 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -13,17 +13,22 @@ // limitations under the License. use crate::errors::DatabaseError; +use crate::execution::runs::{Runs, RunsBuilder, RunsCursor}; #[cfg(feature = "spill")] -use crate::execution::spill::SpillVec; +use crate::execution::spill::runs::SpillRuns; use crate::execution::{ build_read, ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor, }; +use crate::expression::{BinaryOperator, ScalarExpression, TypeCast}; use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyOperator, MarkApplyQuantifier}; use crate::planner::{ExecMetaArena, ExprRef, LogicalPlan, MetaArena}; use crate::storage::Transaction; +use crate::types::evaluator::BinaryEvaluatorRef; use crate::types::index::RuntimeIndexProbe; use crate::types::tuple::{SplitTupleRef, Tuple}; use crate::types::value::DataValue; +use std::borrow::Cow; +use std::cmp::Ordering; #[derive(PartialEq, Eq)] enum QuantifiedPredicateOutcome { @@ -34,22 +39,30 @@ enum QuantifiedPredicateOutcome { } #[cfg(feature = "spill")] -type MaterializedRows = SpillVec<'static, Tuple>; +type KeysBuilder = SpillRuns; #[cfg(not(feature = "spill"))] -type MaterializedRows = Vec; +type KeysBuilder = crate::execution::runs::SortedVec; +// The binder coerces both comparison sides to one type, so non-NULL keys of +// one apply share a variant and are totally ordered. +fn compare_keys(left: &DataValue, right: &DataValue) -> Ordering { + left.partial_cmp(right).unwrap_or(Ordering::Equal) +} + +/// Sorted non-NULL RHS comparison keys; NULL keys only decide UNKNOWN results. +struct MaterializedKeys { + runs: ::Runs, + is_empty: bool, + has_null: bool, + // Spilled reads size their buffer to one key when the key type is fixed-width. + fixed_width: Option, +} + +// TODO: range ANY/ALL only need the RHS extrema rather than every key. enum RightInput { - Streaming { - root: ExecId, - }, - Materialize { - root: ExecId, - }, - Materializing { - root: ExecId, - rows: MaterializedRows, - }, - Materialized(MaterializedRows), + Streaming { root: ExecId }, + Materialize { root: ExecId }, + Materialized(MaterializedKeys), } impl RightInput { @@ -60,7 +73,7 @@ impl RightInput { probe: Option, ) -> Result { let (Self::Streaming { root } | Self::Materialize { root }) = self else { - unreachable!("materialized input replays its rows") + unreachable!("materialized input looks up its keys") }; if let Some(probe) = probe { arena.push_runtime_probe(probe); @@ -70,74 +83,20 @@ impl RightInput { } // Return the first terminal predicate result, or None after exhausting the RHS. - // The caller supplies Mark semantics; this enum owns resetting and replay. fn scan<'a, T: Transaction + 'a, A: MetaArena + 'a>( - self, + &self, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, probe: Option, mut evaluate: impl FnMut(&Tuple, &ExecMetaArena) -> Result, DatabaseError>, - ) -> Result<(Self, Option), DatabaseError> { - match self { - input @ Self::Streaming { .. } => { - let root = input.reset(arena, plan_arena, probe)?; - while arena.next_tuple(root, plan_arena)? { - if let Some(value) = evaluate(arena.result_tuple(), plan_arena)? { - return Ok((input, Some(value))); - } - } - Ok((input, None)) - } - input @ Self::Materialize { .. } => { - let root = input.reset(arena, plan_arena, None)?; - Self::Materializing { - root, - rows: MaterializedRows::new(), - } - .scan(arena, plan_arena, None, evaluate) - } - Self::Materializing { root, mut rows } => { - if let Some(value) = Self::replay(&rows, plan_arena, &mut evaluate)? { - return Ok((Self::Materializing { root, rows }, Some(value))); - } - while arena.next_tuple(root, plan_arena)? { - let right = arena.materialize_tuple(); - let result = evaluate(&right, plan_arena)?; - #[cfg(feature = "spill")] - let _ = rows.push(right)?; - #[cfg(not(feature = "spill"))] - rows.push(right); - // A terminal result pauses the executor; cached rows remain a prefix. - if result.is_some() { - return Ok((Self::Materializing { root, rows }, result)); - } - } - // Only EOF makes the prefix complete; move its cache into the final state. - Ok((Self::Materialized(rows), None)) - } - Self::Materialized(rows) => { - let result = Self::replay(&rows, plan_arena, &mut evaluate)?; - Ok((Self::Materialized(rows), result)) - } - } - } - - fn replay( - rows: &MaterializedRows, - plan_arena: &ExecMetaArena, - mut evaluate: impl FnMut(&Tuple, &ExecMetaArena) -> Result, DatabaseError>, ) -> Result, DatabaseError> { - #[cfg(feature = "spill")] - return rows.replay(|right| evaluate(right, plan_arena)); - #[cfg(not(feature = "spill"))] - { - for right in rows { - if let Some(value) = evaluate(right, plan_arena)? { - return Ok(Some(value)); - } + let root = self.reset(arena, plan_arena, probe)?; + while arena.next_tuple(root, plan_arena)? { + if let Some(value) = evaluate(arena.result_tuple(), plan_arena)? { + return Ok(Some(value)); } - Ok(None) } + Ok(None) } } @@ -185,15 +144,19 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { self.join_input = None; - self.right_input = Some(if self.op.materialize { - RightInput::Materialize { - root: self.right_root, - } - } else { - RightInput::Streaming { - root: self.right_root, - } - }); + // Materialization already makes the RHS reusable across left rows. + // Keep both complete caches and paused prefixes; only reset the left input. + if !self.op.materialize || self.right_input.is_none() { + self.right_input = Some(if self.op.materialize { + RightInput::Materialize { + root: self.right_root, + } + } else { + RightInput::Streaming { + root: self.right_root, + } + }); + } arena.reset(self.left_input, plan_arena) } @@ -212,6 +175,10 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { return Ok(()); } if !arena.next_tuple(self.left_input, plan_arena)? { + // Return the temporarily owned cache so reset can reuse it after EOF. + if self.op.materialize { + self.right_input = Some(input); + } arena.finish(); return Ok(()); } @@ -302,80 +269,200 @@ impl<'a> MarkApply<'a> { fn mark_value( &self, - input: RightInput, + mut input: RightInput, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, left_tuple: &Tuple, ) -> Result<(RightInput, DataValue), DatabaseError> { let probe = self.parameterized_probe_value(left_tuple, plan_arena)?; - match self.op.kind { + let value = match self.op.kind { MarkApplyKind::InnerJoin => unreachable!("inner join streams tuples"), MarkApplyKind::Exists => { let predicates = self.op.predicates(); let runtime_probe = self.runtime_probe_for(probe); - let (input, value) = - input.scan(arena, plan_arena, runtime_probe, |right, meta| { + input + .scan(arena, plan_arena, runtime_probe, |right, meta| { Ok( Self::predicates_matched(predicates, left_tuple, right, meta)? .then_some(DataValue::Boolean(true)), ) - })?; - Ok((input, value.unwrap_or(DataValue::Boolean(false)))) + })? + .unwrap_or(DataValue::Boolean(false)) } MarkApplyKind::Quantified(quantifier) => { - if matches!(quantifier, MarkApplyQuantifier::Any) { - if let Some(probe_value) = probe { - if !probe_value.is_null() { - let (input, value) = self.scan_quantified_right_input( - input, - arena, - plan_arena, - Some(probe_value), - quantifier, - left_tuple, - )?; - if value == DataValue::Boolean(true) { - return Ok((input, value)); - } - let (input, value) = self.scan_quantified_right_input( - input, + if let RightInput::Materialize { root } = input { + input = RightInput::Materialized( + self.materialize_keys(root, arena, plan_arena, left_tuple)?, + ); + } + match (&input, probe) { + (RightInput::Materialized(keys), _) => { + self.lookup_keys(keys, quantifier, plan_arena, left_tuple)? + } + (_, Some(probe_value)) + if matches!(quantifier, MarkApplyQuantifier::Any) + && !probe_value.is_null() => + { + let value = self.scan_quantified_right_input( + &input, + arena, + plan_arena, + Some(probe_value), + quantifier, + left_tuple, + )?; + if value == DataValue::Boolean(true) { + value + } else if self + .scan_quantified_right_input( + &input, arena, plan_arena, Some(DataValue::Null), quantifier, left_tuple, - )?; - return Ok(( - input, - if value.is_null() { - DataValue::Null - } else { - DataValue::Boolean(false) - }, - )); + )? + .is_null() + { + DataValue::Null + } else { + DataValue::Boolean(false) } } + _ => self.scan_quantified_right_input( + &input, arena, plan_arena, None, quantifier, left_tuple, + )?, + } + } + }; + Ok((input, value)) + } + + /// Splits the single materializable predicate into `left_expr op right_expr`. + fn quantified_comparison<'p>( + predicates: &[ExprRef], + plan_arena: &'p (dyn MetaArena + '_), + ) -> Result<(BinaryOperator, ExprRef, ExprRef, &'p BinaryEvaluatorRef), DatabaseError> { + let [predicate] = predicates else { + return Err(DatabaseError::InvalidType); + }; + let ScalarExpression::Binary { + op, + left_expr, + right_expr, + evaluator, + .. + } = plan_arena.expression(*predicate) + else { + return Err(DatabaseError::InvalidType); + }; + let evaluator = evaluator.as_ref().ok_or(DatabaseError::EvaluatorNotFound)?; + Ok((*op, *left_expr, *right_expr, evaluator)) + } + + /// Reads the whole RHS once, keeping only its comparison keys. + fn materialize_keys( + &self, + root: ExecId, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + left_tuple: &Tuple, + ) -> Result { + let (_, _, right_expr, _) = Self::quantified_comparison(self.op.predicates(), plan_arena)?; + arena.reset(root, plan_arena)?; + let mut builder = KeysBuilder::new(compare_keys); + let (mut is_empty, mut has_null) = (true, false); + while arena.next_tuple(root, plan_arena)? { + // RHS positions follow the outer row, so evaluate against the joined layout. + let values = SplitTupleRef::new(left_tuple, arena.result_tuple()); + let key = plan_arena + .expression(right_expr) + .eval(plan_arena, Some(&values))? + .into_owned(); + is_empty = false; + if key.is_null() { + has_null = true; + } else { + builder.push(key)?; + } + } + Ok(MaterializedKeys { + runs: builder.finish()?, + is_empty, + has_null, + fixed_width: plan_arena + .expression(right_expr) + .return_type(plan_arena) + .fixed_encoded_len(), + }) + } + + fn lookup_keys( + &self, + keys: &MaterializedKeys, + quantifier: MarkApplyQuantifier, + plan_arena: &(dyn MetaArena + '_), + left_tuple: &Tuple, + ) -> Result { + let any = matches!(quantifier, MarkApplyQuantifier::Any); + if keys.is_empty { + return Ok(DataValue::Boolean(!any)); + } + let (op, left_expr, _, evaluator) = + Self::quantified_comparison(self.op.predicates(), plan_arena)?; + let probe = plan_arena + .expression(left_expr) + .eval(plan_arena, Some(left_tuple))?; + let mut cursor = keys.runs.cursor(keys.fixed_width)?; + let mut saw_null = false; + // `= ANY` / `<> ALL` reduce to membership. Tuple probes compare NULL + // fields three-valued, which membership cannot express. + if matches!( + (op, quantifier), + (BinaryOperator::Eq, MarkApplyQuantifier::Any) + | (BinaryOperator::NotEq, MarkApplyQuantifier::All) + ) && !matches!(probe.as_ref(), DataValue::Tuple(_)) + { + if probe.is_null() { + return Ok(DataValue::Null); + } + if cursor.contains_by(|key| Ok(compare_keys(key, &probe)))? { + return Ok(DataValue::Boolean(any)); + } + } else if let Some(value) = cursor.visit_all(|key| { + match evaluator.binary_eval(Cow::Borrowed(probe.as_ref()), key)? { + DataValue::Boolean(value) if value == any => Ok(Some(value)), + DataValue::Boolean(_) => Ok(None), + DataValue::Null => { + saw_null = true; + Ok(None) } - self.scan_quantified_right_input( - input, arena, plan_arena, None, quantifier, left_tuple, - ) + _ => Err(DatabaseError::InvalidType), } + })? { + return Ok(DataValue::Boolean(value)); } + // No decisive key: a NULL key or comparison makes the result UNKNOWN. + Ok(if keys.has_null || saw_null { + DataValue::Null + } else { + DataValue::Boolean(!any) + }) } fn scan_quantified_right_input( &self, - input: RightInput, + input: &RightInput, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, probe: Option, quantifier: MarkApplyQuantifier, left_tuple: &Tuple, - ) -> Result<(RightInput, DataValue), DatabaseError> { + ) -> Result { let mut saw_null = false; let predicates = self.op.predicates(); let runtime_probe = self.runtime_probe_for(probe); - let (input, value) = input.scan(arena, plan_arena, runtime_probe, |right, meta| { + let value = input.scan(arena, plan_arena, runtime_probe, |right, meta| { match Self::quantified_predicate_outcome(predicates, left_tuple, right, meta)? { QuantifiedPredicateOutcome::True if matches!(quantifier, MarkApplyQuantifier::Any) => @@ -392,16 +479,11 @@ impl<'a> MarkApply<'a> { } Ok(None) })?; - Ok(( - input, - value.unwrap_or_else(|| { - if saw_null { - DataValue::Null - } else { - DataValue::Boolean(matches!(quantifier, MarkApplyQuantifier::All)) - } - }), - )) + Ok(value.unwrap_or(if saw_null { + DataValue::Null + } else { + DataValue::Boolean(matches!(quantifier, MarkApplyQuantifier::All)) + })) } fn predicates_matched( @@ -1115,7 +1197,7 @@ mod tests { Some(RightInput::Streaming { .. } | RightInput::Materialize { .. }) )); exec.next_tuple(&mut arena, &mut meta)?; - assert!(exec.right_input.is_none()); + assert_eq!(exec.right_input.is_none(), !op.materialize); } else { assert!(matches!(result, Err(DatabaseError::ParseInt(_)))); } @@ -1126,7 +1208,7 @@ mod tests { } #[test] - fn guard_skips_preserve_paused_and_completed_rhs() -> Result<(), DatabaseError> { + fn guard_skips_preserve_materialized_rhs() -> Result<(), DatabaseError> { let (table_cache, view_cache, meta_cache, _temp_dir, storage) = build_test_storage()?; let transaction = storage.transaction()?; let cache = crate::execution::empty_context(&table_cache, &view_cache, &meta_cache); @@ -1199,26 +1281,12 @@ mod tests { exec.right_input, Some(RightInput::Materialize { .. }) )); - } else if index < 4 { - let Some(RightInput::Materializing { rows, .. }) = &exec.right_input else { - panic!("guard must retain the paused RHS") - }; - #[cfg(feature = "spill")] - { - let mut cached = 0; - rows.replay::<()>(|_| { - cached += 1; - Ok(None) - })?; - assert_eq!(cached, if index < 3 { 1 } else { 2 }); - } - #[cfg(not(feature = "spill"))] - assert_eq!(rows.len(), if index < 3 { 1 } else { 2 }); } else { - assert!(matches!( - exec.right_input, - Some(RightInput::Materialized(_)) - )); + // The first unguarded row materializes every RHS key at once. + let Some(RightInput::Materialized(keys)) = &exec.right_input else { + panic!("guard must retain the materialized RHS") + }; + assert!(!keys.is_empty && keys.has_null); } } else { assert!(matches!( @@ -1228,13 +1296,13 @@ mod tests { } } exec.next_tuple(&mut arena, &mut meta)?; - assert!(exec.right_input.is_none()); + assert_eq!(exec.right_input.is_none(), !materialize); } Ok(()) } #[test] - fn quantified_apply_materializes_once_and_replays_after_short_circuit( + fn quantified_apply_materializes_once_and_reuses_keys_after_short_circuit( ) -> Result<(), DatabaseError> { let (table_cache, view_cache, meta_cache, _temp_dir, storage) = build_test_storage()?; let transaction = storage.transaction()?; @@ -1274,15 +1342,12 @@ mod tests { join_input: None, }; let mut meta = ExecMetaArena::new(meta); - for (index, value) in [ + for value in [ DataValue::Int32(2), DataValue::Int32(1), DataValue::Null, DataValue::Int32(2), - ] - .into_iter() - .enumerate() - { + ] { let expected = match (quantifier, &value) { (MarkApplyQuantifier::Any, DataValue::Int32(2)) => DataValue::Boolean(true), (MarkApplyQuantifier::All, DataValue::Int32(1)) => DataValue::Boolean(false), @@ -1293,28 +1358,42 @@ mod tests { let (input, value) = exec.mark_value(input, &mut arena, &mut meta, &tuple)?; exec.right_input = Some(input); assert_eq!(value, expected); - if index == 0 && matches!(quantifier, MarkApplyQuantifier::Any) { - let Some(RightInput::Materializing { rows, .. }) = &exec.right_input else { - panic!("first match must pause the RHS") - }; - #[cfg(feature = "spill")] - { - let mut cached = 0; - rows.replay::<()>(|_| { - cached += 1; - Ok(None) - })?; - assert_eq!(cached, 1); - } - #[cfg(not(feature = "spill"))] - assert_eq!(rows.len(), 1); - } else { - assert!(matches!( - exec.right_input, - Some(RightInput::Materialized(_)) - )); - } + // A short-circuited match still caches every RHS key. + let Some(RightInput::Materialized(keys)) = &exec.right_input else { + panic!("the first lookup must materialize the RHS") + }; + assert!(!keys.is_empty && keys.has_null); + exec.reset(&mut arena, &mut meta)?; + assert!(matches!( + exec.right_input, + Some(RightInput::Materialized(_)) + )); } + // Exhausting the left must not discard the RHS cache either. + while arena.next_tuple(exec.left_input, &mut meta)? {} + exec.next_tuple(&mut arena, &mut meta)?; + assert!(matches!( + exec.right_input, + Some(RightInput::Materialized(_)) + )); + exec.reset(&mut arena, &mut meta)?; + exec.next_tuple(&mut arena, &mut meta)?; + assert_eq!( + arena.result_tuple().values, + vec![ + DataValue::Int32(2), + if matches!(quantifier, MarkApplyQuantifier::Any) { + DataValue::Boolean(true) + } else { + DataValue::Null + }, + ] + ); + assert!(matches!( + exec.right_input, + Some(RightInput::Materialized(_)) + )); + // An empty RHS completes immediately, including for a NULL left value. let cache = arena.context(); exec.right_root = build_read(&mut arena, &mut meta, &empty, cache, &transaction); @@ -1337,6 +1416,19 @@ mod tests { exec.right_input, Some(RightInput::Materialized(_)) )); + exec.reset(&mut arena, &mut meta)?; + assert!(matches!( + exec.right_input, + Some(RightInput::Materialized(_)) + )); + exec.next_tuple(&mut arena, &mut meta)?; + assert_eq!( + arena.result_tuple().values.last(), + Some(&DataValue::Boolean(matches!( + quantifier, + MarkApplyQuantifier::All + ))) + ); } Ok(()) } diff --git a/src/execution/mod.rs b/src/execution/mod.rs index 35ab5bf2..3be40d6d 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -20,6 +20,7 @@ pub(crate) mod ddl; mod ddl_apply; pub(crate) mod dml; pub(crate) mod dql; +pub(crate) mod runs; #[cfg(feature = "spill")] pub(crate) mod spill; diff --git a/src/execution/runs.rs b/src/execution/runs.rs new file mode 100644 index 00000000..4c1a8680 --- /dev/null +++ b/src/execution/runs.rs @@ -0,0 +1,213 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Sorted runs of DataValues, shared by the in-memory `Vec` and spilled implementations. + +use crate::errors::DatabaseError; +use crate::types::value::DataValue; +use std::cmp::Ordering; + +pub(crate) type RunsCompare = fn(&DataValue, &DataValue) -> Ordering; + +/// Accepts unsorted values; `finish` yields runs each sorted by `compare`. +pub(crate) trait RunsBuilder: Sized { + type Runs: Runs; + + fn new(compare: RunsCompare) -> Self; + + fn push(&mut self, value: DataValue) -> Result<(), DatabaseError>; + + fn finish(self) -> Result; +} + +pub(crate) trait Runs { + type Cursor<'a>: RunsCursor + where + Self: 'a; + + /// `fixed_width` is the encoded width shared by every value, sizing spilled reads. + fn cursor(&self, fixed_width: Option) -> Result, DatabaseError>; +} + +pub(crate) trait RunsCursor { + fn run_count(&self) -> usize; + + /// First index in `run` whose value does not compare `Less` against the target. + fn lower_bound_by( + &mut self, + run: usize, + compare: impl FnMut(&DataValue) -> Result, + ) -> Result; + + /// Visits `run` from `start`, stopping at the first `Some` result. + fn visit_run( + &mut self, + run: usize, + start: usize, + visit: impl FnMut(&DataValue) -> Result, DatabaseError>, + ) -> Result, DatabaseError>; + + fn contains_by( + &mut self, + mut compare: impl FnMut(&DataValue) -> Result, + ) -> Result { + for run in 0..self.run_count() { + let index = self.lower_bound_by(run, &mut compare)?; + if self + .visit_run(run, index, |value| { + Ok(Some(compare(value)? == Ordering::Equal)) + })? + .unwrap_or(false) + { + return Ok(true); + } + } + Ok(false) + } + + /// Visits every value of every run, stopping at the first `Some` result. + fn visit_all( + &mut self, + mut visit: impl FnMut(&DataValue) -> Result, DatabaseError>, + ) -> Result, DatabaseError> { + for run in 0..self.run_count() { + if let Some(result) = self.visit_run(run, 0, &mut visit)? { + return Ok(Some(result)); + } + } + Ok(None) + } +} + +pub(crate) fn lower_bound( + rows: usize, + mut compare_at: impl FnMut(usize) -> Result, +) -> Result { + let (mut low, mut high) = (0, rows); + while low < high { + let mid = low + (high - low) / 2; + if compare_at(mid)? == Ordering::Less { + low = mid + 1; + } else { + high = mid; + } + } + Ok(low) +} + +/// Builds a single in-memory run, sorted once by `finish`. +/// Spill builds use `SpillRuns` instead. +#[cfg(any(test, not(feature = "spill")))] +pub(crate) struct SortedVec { + values: Vec, + compare: RunsCompare, +} + +#[cfg(any(test, not(feature = "spill")))] +impl RunsBuilder for SortedVec { + type Runs = Vec; + + fn new(compare: RunsCompare) -> Self { + Self { + values: Vec::new(), + compare, + } + } + + fn push(&mut self, value: DataValue) -> Result<(), DatabaseError> { + self.values.push(value); + Ok(()) + } + + fn finish(mut self) -> Result, DatabaseError> { + self.values.sort_by(self.compare); + Ok(self.values) + } +} + +impl Runs for Vec { + type Cursor<'a> = &'a [DataValue]; + + fn cursor(&self, _: Option) -> Result, DatabaseError> { + Ok(self.as_slice()) + } +} + +/// A sorted slice is one run; an empty slice has none. +impl RunsCursor for &[DataValue] { + fn run_count(&self) -> usize { + usize::from(!self.is_empty()) + } + + fn lower_bound_by( + &mut self, + _: usize, + mut compare: impl FnMut(&DataValue) -> Result, + ) -> Result { + lower_bound(self.len(), |index| compare(&self[index])) + } + + fn visit_run( + &mut self, + _: usize, + start: usize, + mut visit: impl FnMut(&DataValue) -> Result, DatabaseError>, + ) -> Result, DatabaseError> { + for value in self.iter().skip(start) { + if let Some(result) = visit(value)? { + return Ok(Some(result)); + } + } + Ok(None) + } +} + +// GRCOV_EXCL_START +#[cfg(test)] +mod tests { + use super::*; + + fn compare(left: &DataValue, right: &DataValue) -> Ordering { + left.partial_cmp(right).unwrap() + } + + #[test] + fn vec_runs_sort_once_and_search() -> Result<(), DatabaseError> { + let mut builder = SortedVec::new(compare); + for value in [5, 1, 3, 1] { + builder.push(DataValue::Int32(value))?; + } + let runs = builder.finish()?; + let mut cursor = runs.cursor(None)?; + assert_eq!(cursor.run_count(), 1); + for target in 0..7 { + assert_eq!( + cursor.contains_by(|value| Ok(compare(value, &DataValue::Int32(target))))?, + [1, 3, 5].contains(&target) + ); + } + let mut visited = Vec::new(); + cursor.visit_all::<()>(|value| { + visited.push(value.clone()); + Ok(None) + })?; + assert_eq!(visited, [1, 1, 3, 5].map(DataValue::Int32).to_vec()); + let empty = SortedVec::new(compare).finish()?; + let mut cursor = empty.cursor(None)?; + assert_eq!(cursor.run_count(), 0); + assert!(!cursor.contains_by(|_| Ok(Ordering::Equal))?); + Ok(()) + } +} +// GRCOV_EXCL_STOP diff --git a/src/execution/spill/codec.rs b/src/execution/spill/codec.rs index 37ec0e17..e3229ed9 100644 --- a/src/execution/spill/codec.rs +++ b/src/execution/spill/codec.rs @@ -65,6 +65,12 @@ impl SpillCodec for SortRow { }) } + fn encoded_len(&self) -> Result { + Ok(self.sort_values.encoded_len()? + + self.tuple.encoded_len()? + + self.outer_values.encoded_len()?) + } + fn estimated_size(&self) -> usize { size_of::() .saturating_add( @@ -95,6 +101,10 @@ impl SpillCodec for (A, B) { Ok((A::decode(reader)?, B::decode(reader)?)) } + fn encoded_len(&self) -> Result { + Ok(self.0.encoded_len()? + self.1.encoded_len()?) + } + fn estimated_size(&self) -> usize { size_of::() .saturating_add(self.0.estimated_size().saturating_sub(size_of::())) @@ -133,6 +143,10 @@ impl SpillCodec for ScalarQueryRef { }) } + fn encoded_len(&self) -> Result { + Ok(17) + } + fn estimated_size(&self) -> usize { size_of::() } @@ -147,6 +161,48 @@ impl SpillCodec for DataValue { Self::decode_reference_value(reader) } + fn encoded_len(&self) -> Result { + use crate::types::value::Utf8Type; + let payload = match self { + DataValue::Parameter { .. } => { + return Err(DatabaseError::InvalidValue( + "unbound parameter cannot be serialized".into(), + )) + } + DataValue::Null => 0, + DataValue::Boolean(_) | DataValue::Int8(_) | DataValue::UInt8(_) => 1, + DataValue::Int16(_) | DataValue::UInt16(_) => 2, + DataValue::Float32(_) + | DataValue::Int32(_) + | DataValue::UInt32(_) + | DataValue::Date32(_) => 4, + DataValue::Float64(_) + | DataValue::Int64(_) + | DataValue::UInt64(_) + | DataValue::Date64(_) => 8, + DataValue::Time32(..) => 12, + DataValue::Time64(..) => 17, + #[cfg(feature = "decimal")] + DataValue::Decimal(_) => 16, + DataValue::Binary { value, len } => { + let _: u32 = value.len().try_into()?; + 4 + value.len() + 1 + if len.is_some() { 4 } else { 0 } + } + DataValue::Utf8 { value, ty, .. } => { + let _: u32 = value.len().try_into()?; + 4 + value.len() + + 1 + + match ty { + Utf8Type::Variable(None) => 2, + Utf8Type::Variable(Some(_)) => 6, + Utf8Type::Fixed(_) => 5, + } + } + DataValue::Tuple(values) => values.encoded_len()?, + }; + Ok(1 + payload) // Every DataValue starts with a one-byte variant tag. + } + fn estimated_size(&self) -> usize { size_of::().saturating_add(estimated_dynamic_value_size(self)) } @@ -173,6 +229,14 @@ impl SpillCodec for Vec { Ok(values) } + fn encoded_len(&self) -> Result { + let _: u32 = self.len().try_into()?; + self.iter().try_fold( + size_of::(), + |len, value| Ok(len + value.encoded_len()?), + ) + } + fn estimated_size(&self) -> usize { size_of::() .saturating_add(self.capacity().saturating_mul(size_of::())) @@ -210,6 +274,13 @@ impl SpillCodec for Option { } } + fn encoded_len(&self) -> Result { + Ok(1 + match self { + Some(value) => value.encoded_len()?, + None => 0, + }) + } + fn estimated_size(&self) -> usize { size_of::().saturating_add( self.as_ref() @@ -232,6 +303,10 @@ impl SpillCodec for Tuple { )) } + fn encoded_len(&self) -> Result { + Ok(self.pk.encoded_len()? + self.values.encoded_len()?) + } + fn estimated_size(&self) -> usize { size_of::() .saturating_add( @@ -282,6 +357,7 @@ mod tests { fn round_trip(value: &T) -> Result<(), DatabaseError> { let mut bytes = Vec::new(); value.encode(&mut bytes)?; + assert_eq!(value.encoded_len()?, bytes.len()); let mut reader = bytes.as_slice(); assert_eq!(&T::decode(&mut reader)?, value); assert!(reader.is_empty()); @@ -354,6 +430,12 @@ mod tests { .encode(&mut Vec::new()), Err(DatabaseError::InvalidValue(_)) )); + assert!(DataValue::Tuple(vec![DataValue::Parameter { + id: 1, + ty: LogicalType::Integer + }]) + .encoded_len() + .is_err()); assert!(matches!( DataValue::decode(&mut &[255][..]), Err(DatabaseError::InvalidValue(_)) @@ -397,6 +479,7 @@ mod tests { }; let mut bytes = Vec::new(); row.encode(&mut bytes)?; + assert_eq!(row.encoded_len()?, bytes.len()); let mut reader = bytes.as_slice(); let decoded = SortRow::decode(&mut reader)?; assert!(reader.is_empty()); diff --git a/src/execution/spill/mod.rs b/src/execution/spill/mod.rs index 2b7b1076..b602244c 100644 --- a/src/execution/spill/mod.rs +++ b/src/execution/spill/mod.rs @@ -21,11 +21,13 @@ use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; mod codec; +pub(crate) mod runs; pub(crate) use codec::SortRow; const DEFAULT_MAX_ROWS: usize = 1024; const DEFAULT_MAX_BYTES: usize = 1024 * 1024; +const DEFAULT_READ_BUFFER_SIZE: usize = 8 * 1024; static NEXT_SPILL_FILE_ID: AtomicU64 = AtomicU64::new(0); pub(crate) type SegmentOffset = u64; @@ -35,6 +37,9 @@ pub(crate) trait SpillCodec: Sized { fn decode(reader: &mut R) -> Result; + /// Exact byte length of encode(), without allocating or encoding the value. + fn encoded_len(&self) -> Result; + fn estimated_size(&self) -> usize; } @@ -55,7 +60,17 @@ struct WriteState<'on_flush, T: SpillCodec> { max_bytes: usize, } -type OnFlush<'on_flush, T> = Box) -> Result<(), DatabaseError> + 'on_flush>; +/// Tells the flush callback whether it prepares a memory tail or a disk segment. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum FlushMode { + /// Preparing an in-memory reader, including the tail of a spilled reader. + Memory, + /// Writing a segment to disk, whether explicitly or due to a size limit. + Disk, +} + +type OnFlush<'on_flush, T> = + Box) -> Result<(), DatabaseError> + 'on_flush>; enum ReadState { Memory(std::vec::IntoIter), @@ -96,7 +111,7 @@ impl<'on_flush, T: SpillCodec> SpillVec<'on_flush, T> { pub(crate) fn on_flush(mut self, on_flush: F) -> Self where - F: FnMut(&mut Vec) -> Result<(), DatabaseError> + 'on_flush, + F: FnMut(FlushMode, &mut Vec) -> Result<(), DatabaseError> + 'on_flush, { if let Ok(state) = &mut self.writer { state.on_flush = Some(Box::new(on_flush)); @@ -115,32 +130,6 @@ impl<'on_flush, T: SpillCodec> SpillVec<'on_flush, T> { matches!(&self.writer, Ok(state) if state.file.is_some()) } - /// Replays the current prefix without consuming the writer or flushing its memory tail. - pub(crate) fn replay( - &self, - mut evaluate: impl FnMut(&T) -> Result, DatabaseError>, - ) -> Result, DatabaseError> { - let state = self.writer.as_ref().map_err(|_| { - DatabaseError::InvalidValue("cannot replay a failed SpillVec".to_string()) - })?; - if let Some(file) = &state.file { - let mut reader = SegmentReader::new(BufReader::new(File::open(&file.file_guard.path)?)); - while reader.start_next_segment()? { - for value in &mut reader { - if let Some(result) = evaluate(&value?)? { - return Ok(Some(result)); - } - } - } - } - for value in &state.buffer { - if let Some(result) = evaluate(value)? { - return Ok(Some(result)); - } - } - Ok(None) - } - pub(crate) fn flush(&mut self) -> Result, DatabaseError> { let state = self.writer.as_mut().map_err(|_| { DatabaseError::InvalidValue("cannot flush a failed SpillVec".to_string()) @@ -212,6 +201,14 @@ impl Iterator for SpillReader { } impl SpillReader { + /// The unconsumed memory suffix. Borrowing it prevents advancing the reader. + pub(crate) fn memory_tail(&self) -> &[T] { + match &self.state { + ReadState::Memory(rows) | ReadState::Spilled { tail: rows, .. } => rows.as_slice(), + ReadState::Exhausted(_) => &[], + } + } + /// Rewinds a fully flushed spill file. The reader must not contain an in-memory tail. pub(crate) fn reset(&mut self) -> Result<(), DatabaseError> { let ReadState::Spilled { reader, tail, .. } = &mut self.state else { @@ -227,17 +224,21 @@ impl SpillReader { reader.reset(0) } - pub(crate) fn open_segment_reader<'source>( - &'source self, - ) -> Result, T>, DatabaseError> { + pub(crate) fn open_segment_reader( + &self, + capacity: Option, + ) -> Result, T>, DatabaseError> { let ReadState::Spilled { _file_guard, .. } = &self.state else { return Err(DatabaseError::InvalidValue( "cannot open a segment reader for an in-memory SpillVec".to_string(), )); }; - Ok(SegmentReader::new(BufReader::new(File::open( - &_file_guard.path, - )?))) + Ok(SegmentReader::new(BufReader::with_capacity( + capacity + .unwrap_or(DEFAULT_READ_BUFFER_SIZE) + .clamp(1, DEFAULT_READ_BUFFER_SIZE), + File::open(&_file_guard.path)?, + ))) } } @@ -282,6 +283,20 @@ impl SegmentReader<'_, R, T> { } impl SegmentReader<'_, R, T> { + /// Reads a nested value without parsing a segment header. Call reset before + /// resuming segment iteration. Relative seeking retains BufReader's buffer. + pub(crate) fn read_at(&mut self, offset: u64) -> Result { + self.remaining_rows = 0; + let current = self.reader.stream_position()?; + match i64::try_from(i128::from(offset) - i128::from(current)) { + Ok(delta) if delta != i64::MIN => self.reader.seek_relative(delta)?, + _ => { + self.reader.seek(SeekFrom::Start(offset))?; + } + } + V::decode(&mut self.reader) + } + pub(crate) fn reset(&mut self, offset: SegmentOffset) -> Result<(), DatabaseError> { self.reader.seek(SeekFrom::Start(offset))?; self.remaining_rows = 0; @@ -339,7 +354,7 @@ impl WriteState<'_, T> { return Ok(None); } if let Some(on_flush) = &mut self.on_flush { - on_flush(&mut self.buffer)?; + on_flush(FlushMode::Disk, &mut self.buffer)?; } let Some(file) = self.file.as_mut() else { return Err(DatabaseError::InvalidValue( @@ -354,7 +369,7 @@ impl WriteState<'_, T> { fn into_read(mut self) -> Result, DatabaseError> { if let Some(on_flush) = &mut self.on_flush { - on_flush(&mut self.buffer)?; + on_flush(FlushMode::Memory, &mut self.buffer)?; } let Some(file) = self.file.take() else { return Ok(ReadState::Memory(self.buffer.into_iter())); @@ -522,9 +537,9 @@ mod tests { })?; let source = values.into_iter(); - let mut first_reader = source.open_segment_reader()?; + let mut first_reader = source.open_segment_reader(None)?; first_reader.reset(first)?; - let mut second_reader = source.open_segment_reader()?; + let mut second_reader = source.open_segment_reader(None)?; second_reader.reset(second)?; assert_eq!(first_reader.next().transpose()?, Some(row(1))); @@ -565,7 +580,7 @@ mod tests { let values = SpillVec::from(vec![row(1)]).into_iter(); assert!(matches!( - values.open_segment_reader(), + values.open_segment_reader(None), Err(DatabaseError::InvalidValue(message)) if message == "cannot open a segment reader for an in-memory SpillVec" )); @@ -634,6 +649,10 @@ mod tests { Err(DatabaseError::InvalidValue("encode failed".to_string())) } + fn encoded_len(&self) -> Result { + Err(DatabaseError::InvalidValue("encode failed".to_string())) + } + fn decode(_: &mut R) -> Result { Ok(Self) } @@ -668,6 +687,10 @@ mod tests { Err(DatabaseError::InvalidValue("decode failed".to_string())) } + fn encoded_len(&self) -> Result { + Ok(0) + } + fn estimated_size(&self) -> usize { 0 } @@ -723,20 +746,20 @@ mod tests { } std::fs::remove_file(path.expect("expected spilled reader"))?; - assert!(source.open_segment_reader().is_err()); + assert!(source.open_segment_reader(None).is_err()); Ok(()) } #[test] fn on_flush_runs_before_memory_read_and_segment_flush() -> Result<(), DatabaseError> { - fn reverse(rows: &mut [Vec]) -> Result<(), DatabaseError> { + fn reverse(_: FlushMode, rows: &mut [Vec]) -> Result<(), DatabaseError> { rows.reverse(); Ok(()) } let mut memory = SpillVec::new() .limit(3, usize::MAX) - .on_flush(|rows| reverse(rows)); + .on_flush(|mode, rows| reverse(mode, rows)); let _ = memory.push(row(1))?; let _ = memory.push(row(2))?; assert_eq!( @@ -746,7 +769,7 @@ mod tests { let mut spilled = SpillVec::new() .limit(2, usize::MAX) - .on_flush(|rows| reverse(rows)); + .on_flush(|mode, rows| reverse(mode, rows)); for value in 1..=4 { let _ = spilled.push(row(value))?; } @@ -757,6 +780,54 @@ mod tests { Ok(()) } + #[test] + fn on_flush_receives_memory_or_disk_mode() -> Result<(), DatabaseError> { + for (count, limit, explicit_flush, expected) in [ + (3, 4, false, vec![FlushMode::Memory]), + ( + 8, + 3, + false, + vec![FlushMode::Disk, FlushMode::Disk, FlushMode::Memory], + ), + (4, 5, true, vec![FlushMode::Disk, FlushMode::Memory]), + ] { + let mut modes = Vec::new(); + let mut values = SpillVec::new() + .limit(limit, usize::MAX) + .on_flush(|mode, _| { + modes.push(mode); + Ok(()) + }); + for value in 1..=count { + values.push(row(value))?; + if explicit_flush && value == 2 { + values.flush()?; + } + } + assert_eq!( + values.into_iter().collect::, _>>()?, + (1..=count).map(row).collect::>() + ); + assert_eq!(modes, expected); + } + Ok(()) + } + + #[test] + fn on_flush_errors_propagate_from_memory_and_disk() -> Result<(), DatabaseError> { + let callback = |_: FlushMode, _: &mut Vec>| { + Err(DatabaseError::InvalidValue("callback invoked".into())) + }; + let mut memory = SpillVec::new().limit(2, usize::MAX).on_flush(callback); + memory.push(row(1))?; + assert!(memory.into_iter().next().unwrap().is_err()); + + let mut disk = SpillVec::new().limit(1, usize::MAX).on_flush(callback); + assert!(disk.push(row(1)).is_err()); + Ok(()) + } + #[test] fn tuple_codec_preserves_primary_key_and_values() -> Result<(), DatabaseError> { let tuple = Tuple::new( diff --git a/src/execution/spill/runs.rs b/src/execution/spill/runs.rs new file mode 100644 index 00000000..2e6b8617 --- /dev/null +++ b/src/execution/spill/runs.rs @@ -0,0 +1,501 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{FlushMode, SegmentOffset, SegmentReader, SpillCodec, SpillReader, SpillVec}; +use crate::errors::DatabaseError; +use crate::execution::runs::{lower_bound, Runs, RunsBuilder, RunsCompare, RunsCursor}; +use crate::types::value::DataValue; +use std::cmp::Ordering; +use std::fs::File; +use std::io::BufReader; +use std::mem::size_of; + +pub(crate) struct SpillRuns { + runs: Vec, + spill: SpillVec<'static, DataValue>, +} + +/// Disk layout, entirely encoded as ordinary DataValues: +/// UInt32(N), N UInt32 offsets relative to the records area, N original values. +/// Extend and rotate the existing allocation; no scratch encoding or heap Vec. +fn prepare_disk_run(values: &mut Vec) -> Result<(), DatabaseError> { + let rows = values.len(); + let count = u32::try_from(rows)?; + let slots = rows + .checked_add(1) + .ok_or_else(|| DatabaseError::InvalidValue("run length overflow".into()))?; + let len = rows + .checked_add(slots) + .ok_or_else(|| DatabaseError::InvalidValue("run length overflow".into()))?; + values.resize(len, DataValue::Null); + values.rotate_right(slots); + values[0] = DataValue::UInt32(count); + let mut offset = 0u32; + for index in 0..rows { + let bytes = u32::try_from(values[slots + index].encoded_len()?)?; + values[index + 1] = DataValue::UInt32(offset); + offset = offset.checked_add(bytes).ok_or_else(|| { + DatabaseError::InvalidValue("run payload exceeds UInt32 offsets".into()) + })?; + } + Ok(()) +} + +impl SpillRuns { + /// Ends the current run early; later values start a new one. + #[cfg(test)] + pub(crate) fn flush(&mut self) -> Result<(), DatabaseError> { + if let Some(offset) = self.spill.flush()? { + self.runs.push(offset); + } + Ok(()) + } +} + +/// Every SpillVec flush sorts its bounded segment into one run. +impl RunsBuilder for SpillRuns { + type Runs = SpillRunsReader; + + fn new(compare: RunsCompare) -> Self { + Self { + runs: Vec::new(), + spill: SpillVec::new().on_flush(move |mode, values| { + values.sort_by(compare); + match mode { + FlushMode::Disk => prepare_disk_run(values), + FlushMode::Memory => Ok(()), + } + }), + } + } + + fn push(&mut self, value: DataValue) -> Result<(), DatabaseError> { + if let Some(offset) = self.spill.push(value)? { + self.runs.push(offset); + } + Ok(()) + } + + fn finish(self) -> Result { + Ok(SpillRunsReader { + runs: self.runs, + spill: self.spill.into_iter(), + }) + } +} + +pub(crate) struct SpillRunsReader { + runs: Vec, + spill: SpillReader, +} + +impl Runs for SpillRunsReader { + type Cursor<'a> = SpillRunsCursor<'a>; + + fn cursor(&self, fixed_width: Option) -> Result, DatabaseError> { + let memory = self.spill.memory_tail(); + if self.runs.is_empty() { + return Ok(SpillRunsCursor::Memory(memory)); + } + let directory_width = DataValue::UInt32(0).encoded_len()?; + let reader = self + .spill + .open_segment_reader(fixed_width.map(|width| width.max(directory_width)))?; + Ok(SpillRunsCursor::Spilled { + runs: &self.runs, + memory, + reader, + }) + } +} + +pub(crate) enum SpillRunsCursor<'a> { + Memory(&'a [DataValue]), + Spilled { + runs: &'a [SegmentOffset], + memory: &'a [DataValue], + reader: SegmentReader<'a, BufReader, DataValue>, + }, +} + +fn disk_value( + reader: &mut SegmentReader<'_, BufReader, DataValue>, + index: usize, + rows: usize, + directory: u64, +) -> Result { + let width = DataValue::UInt32(0).encoded_len()? as u64; + let DataValue::UInt32(offset) = reader.read_at(directory + index as u64 * width)? else { + return Err(DatabaseError::InvalidValue("invalid run offset".into())); + }; + // TODO: accept single-BufReader directory/record jumps for now. Profile + // before caching the directory or introducing a second reader. + reader.read_at(directory + rows as u64 * width + u64::from(offset)) +} + +fn disk_layout( + reader: &mut SegmentReader<'_, BufReader, DataValue>, + offset: SegmentOffset, +) -> Result<(usize, u64), DatabaseError> { + let offset = offset + size_of::() as u64; + let DataValue::UInt32(rows) = reader.read_at(offset)? else { + return Err(DatabaseError::InvalidValue("invalid run row count".into())); + }; + Ok(( + rows as usize, + offset + DataValue::UInt32(0).encoded_len()? as u64, + )) +} + +impl RunsCursor for SpillRunsCursor<'_> { + fn run_count(&self) -> usize { + match self { + Self::Memory(memory) => memory.run_count(), + Self::Spilled { runs, memory, .. } => runs.len() + memory.run_count(), + } + } + + fn lower_bound_by( + &mut self, + run: usize, + mut compare: impl FnMut(&DataValue) -> Result, + ) -> Result { + match self { + Self::Memory(memory) => memory.lower_bound_by(run, compare), + Self::Spilled { runs, memory, .. } if run == runs.len() => { + memory.lower_bound_by(0, compare) + } + Self::Spilled { runs, reader, .. } => { + let (rows, directory) = disk_layout(reader, runs[run])?; + lower_bound(rows, |index| { + compare(&disk_value(reader, index, rows, directory)?) + }) + } + } + } + + fn visit_run( + &mut self, + run: usize, + start: usize, + mut visit: impl FnMut(&DataValue) -> Result, DatabaseError>, + ) -> Result, DatabaseError> { + match self { + Self::Memory(memory) => memory.visit_run(run, start, visit), + Self::Spilled { runs, memory, .. } if run == runs.len() => { + memory.visit_run(0, start, visit) + } + Self::Spilled { runs, reader, .. } => { + let (rows, directory) = disk_layout(reader, runs[run])?; + for index in start..rows { + if let Some(result) = visit(&disk_value(reader, index, rows, directory)?)? { + return Ok(Some(result)); + } + } + Ok(None) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn compare(left: &DataValue, right: &DataValue) -> Ordering { + match (left, right) { + (DataValue::Null, DataValue::Null) => Ordering::Equal, + (DataValue::Null, _) => Ordering::Less, + (_, DataValue::Null) => Ordering::Greater, + _ => left.partial_cmp(right).unwrap(), + } + } + + fn descending(left: &DataValue, right: &DataValue) -> Ordering { + compare(right, left) + } + + fn runs_of( + compare: RunsCompare, + batches: impl IntoIterator>, + ) -> Result { + let mut runs = SpillRuns::new(compare); + for batch in batches { + runs.flush()?; + for value in batch { + runs.push(value)?; + } + } + Ok(runs) + } + + #[test] + fn original_memory_and_disk_values_match() -> Result<(), DatabaseError> { + for values in [ + vec![ + DataValue::Int64(3), + DataValue::Null, + DataValue::Int64(-2), + DataValue::Int64(3), + ], + vec![ + DataValue::new_utf8("中文".repeat(100)), + DataValue::Null, + DataValue::new_utf8("".into()), + DataValue::new_utf8("abc".into()), + ], + vec![ + DataValue::Tuple(vec![DataValue::Int64(2), DataValue::Null]), + DataValue::Null, + DataValue::Tuple(vec![DataValue::Int64(1), DataValue::Null]), + ], + vec![DataValue::Null; 9], + ] { + let mut expected = values.clone(); + expected.sort_by(compare); + for flushed in [false, true] { + let mut runs = runs_of(compare, [values.clone()])?; + if flushed { + runs.flush()?; + } + let source = runs.finish()?; + if !flushed { + assert_eq!(source.spill.memory_tail(), expected); + } + for _ in 0..2 { + let mut cursor = source.cursor(None)?; + let mut actual = Vec::new(); + cursor.visit_run::<()>(0, 0, |value| { + actual.push(value.clone()); + Ok(None) + })?; + assert_eq!(actual, expected); + for target in &expected { + assert!(cursor.contains_by(|value| Ok(compare(value, target)))?); + } + assert_eq!( + cursor.lower_bound_by(0, |_| Ok(Ordering::Less))?, + expected.len() + ); + } + } + } + Ok(()) + } + + #[test] + fn disk_runs_and_original_memory_tail() -> Result<(), DatabaseError> { + let source = runs_of( + compare, + [ + vec![DataValue::Int64(4), DataValue::Int64(2)], + vec![DataValue::Int64(7), DataValue::Int64(1)], + vec![DataValue::Int64(9)], + ], + )? + .finish()?; + let mut cursor = source.cursor(Some(9))?; + assert_eq!(cursor.run_count(), 3); + assert!( + matches!(&cursor, SpillRunsCursor::Spilled { runs, memory, .. } if runs.len() == 2 && memory.len() == 1) + ); + assert_eq!(source.spill.memory_tail(), &[DataValue::Int64(9)]); + for target in 0..11 { + assert_eq!( + cursor.contains_by(|value| Ok(compare(value, &DataValue::Int64(target))))?, + [1, 2, 4, 7, 9].contains(&target) + ); + } + let mut other = source.cursor(Some(9))?; + assert_eq!( + other.lower_bound_by(1, |value| Ok(compare(value, &DataValue::Int64(6))))?, + 1 + ); + assert!(cursor.contains_by(|value| Ok(compare(value, &DataValue::Int64(2))))?); + Ok(()) + } + + #[test] + fn automatic_spill_and_928_memory_keys() -> Result<(), DatabaseError> { + let source = runs_of( + compare, + [(0..super::super::DEFAULT_MAX_ROWS + 1) + .rev() + .map(|value| DataValue::Int64(value as i64)) + .collect()], + )? + .finish()?; + let mut cursor = source.cursor(Some(9))?; + assert_eq!(cursor.run_count(), 2); + for target in [0, 1, 1023, 1024] { + assert!(cursor.contains_by(|value| Ok(compare(value, &DataValue::Int64(target))))?); + } + let runs = runs_of(compare, [(0..928).rev().map(DataValue::UInt32).collect()])?; + assert!(!runs.spill.is_spilled()); + let source = runs.finish()?; + let mut cursor = source.cursor(Some(5))?; + assert!(matches!(&cursor, SpillRunsCursor::Memory(_))); + let mut comparisons = 0; + assert!(cursor.contains_by(|value| { + comparisons += 1; + Ok(compare(value, &DataValue::UInt32(900))) + })?); + assert!(comparisons <= 12); + assert_eq!( + SpillRuns::new(compare).finish()?.cursor(None)?.run_count(), + 0 + ); + Ok(()) + } + + #[test] + fn cursor_uses_one_fixed_record_capped_at_default() -> Result<(), DatabaseError> { + let default = super::super::DEFAULT_READ_BUFFER_SIZE; + for (value, expected) in [ + (DataValue::Boolean(true), 5), + (DataValue::Int64(42), 9), + (DataValue::Tuple(vec![DataValue::Int32(1); 5]), 30), + (DataValue::Tuple(vec![DataValue::Int64(1); 5]), 50), + ] { + let width = value.encoded_len()?; + let mut runs = runs_of( + compare, + [vec![value.clone(), DataValue::Null, value.clone()]], + )?; + runs.flush()?; + let source = runs.finish()?; + let mut cursor = source.cursor(Some(width))?; + let SpillRunsCursor::Spilled { reader, .. } = &cursor else { + panic!("expected disk cursor"); + }; + assert_eq!(reader.reader.capacity(), expected); + assert!(cursor.contains_by(|key| Ok(compare(key, &value)))?); + assert!(cursor.contains_by(|key| Ok(compare(key, &DataValue::Null)))?); + for (width, expected) in [(None, default), (Some(0), 5), (Some(default + 1), default)] { + let cursor = source.cursor(width)?; + let SpillRunsCursor::Spilled { reader, .. } = &cursor else { + panic!("expected disk cursor"); + }; + assert_eq!(reader.reader.capacity(), expected); + } + } + Ok(()) + } + + #[test] + fn directory_offsets_match_original_encoding() -> Result<(), DatabaseError> { + let original = vec![ + DataValue::Null, + DataValue::Int8(1), + DataValue::Int64(9), + DataValue::new_utf8("中文".into()), + DataValue::Tuple(vec![DataValue::Null, DataValue::UInt32(4)]), + ]; + let mut values = original.clone(); + prepare_disk_run(&mut values)?; + assert_eq!(values.len(), original.len() * 2 + 1); + assert_eq!(values[0], DataValue::UInt32(original.len() as u32)); + assert_eq!(&values[original.len() + 1..], original); + let mut offset = 0; + for (index, value) in original.iter().enumerate() { + assert_eq!(values[index + 1], DataValue::UInt32(offset)); + let mut bytes = Vec::new(); + value.encode(&mut bytes)?; + offset += bytes.len() as u32; + } + let mut empty = Vec::new(); + prepare_disk_run(&mut empty)?; + assert_eq!(empty, [DataValue::UInt32(0)]); + Ok(()) + } + + #[test] + fn null_last_and_descending_runs() -> Result<(), DatabaseError> { + for flushed in [false, true] { + let mut runs = runs_of( + descending, + [vec![ + DataValue::Int64(1), + DataValue::Null, + DataValue::Int64(5), + ]], + )?; + if flushed { + runs.flush()?; + } + let source = runs.finish()?; + let mut cursor = source.cursor(Some(9))?; + let mut actual = Vec::new(); + cursor.visit_run::<()>(0, 0, |value| { + actual.push(value.clone()); + Ok(None) + })?; + assert_eq!( + actual, + [DataValue::Int64(5), DataValue::Int64(1), DataValue::Null] + ); + for target in [DataValue::Int64(1), DataValue::Null, DataValue::Int64(5)] { + assert!(cursor.contains_by(|value| Ok(compare(&target, value)))?); + } + assert!(!cursor.contains_by(|value| Ok(compare(&DataValue::Int64(3), value)))?); + } + Ok(()) + } + + #[test] + fn directory_entries_stay_outside_search_range() -> Result<(), DatabaseError> { + let mut runs = runs_of( + compare, + [vec![ + DataValue::new_utf8("a".into()), + DataValue::new_utf8("c".into()), + ]], + )?; + runs.flush()?; + let source = runs.finish()?; + let mut cursor = source.cursor(None)?; + assert_eq!(cursor.lower_bound_by(0, |_| Ok(Ordering::Less))?, 2); + assert!(!cursor.contains_by(|value| Ok(compare(value, &DataValue::new_utf8("z".into()))))?); + Ok(()) + } + + #[test] + fn spill_limits_cut_sorted_runs() -> Result<(), DatabaseError> { + let max_rows = super::super::DEFAULT_MAX_ROWS; + let rows = max_rows * 2 + 3; + let mut runs = SpillRuns::new(compare); + for value in (0..rows as i64).rev() { + runs.push(DataValue::Int64(value))?; + } + let source = runs.finish()?; + let mut cursor = source.cursor(Some(9))?; + // Two full disk runs plus the sorted memory tail. + assert_eq!(cursor.run_count(), 3); + for run in 0..cursor.run_count() { + let mut previous: Option = None; + cursor.visit_run::<()>(run, 0, |value| { + if let Some(previous) = &previous { + assert_eq!(compare(previous, value), Ordering::Less); + } + previous = Some(value.clone()); + Ok(None) + })?; + } + for target in [0, 2, max_rows as i64, rows as i64 - 1] { + assert!(cursor.contains_by(|value| Ok(compare(value, &DataValue::Int64(target))))?); + } + assert!(!cursor.contains_by(|value| Ok(compare(value, &DataValue::Int64(-1))))?); + Ok(()) + } +} diff --git a/src/optimizer/rule/normalization/guard_mark_apply.rs b/src/optimizer/rule/normalization/guard_mark_apply.rs index 34a20470..6415bb6a 100644 --- a/src/optimizer/rule/normalization/guard_mark_apply.rs +++ b/src/optimizer/rule/normalization/guard_mark_apply.rs @@ -53,9 +53,6 @@ impl NormalizationRule for GuardMarkApply { if matches!(op.kind, MarkApplyKind::InnerJoin) { break; } - if op.guard.is_some() { - return Ok(false); - } markers.push(*op.output_column()); input = left; } @@ -72,58 +69,91 @@ impl NormalizationRule for GuardMarkApply { return Ok(false); } } - let mut visitor = GuardVisitor { - markers: &markers, - guard: None, - }; - visitor.visit(filter_op.predicate, arena)?; - let Some((guard, negated)) = visitor.guard else { - return Ok(false); - }; - let schema = mark.output_schema(arena); - if !guard.all_referenced_columns(arena, |arena, column| { - !markers - .iter() - .any(|marker| arena.same_column(*marker, *column)) - && schema + // Do not skip values consumed by another apply, even if Filter does not need them. + let mut consumed = Vec::new(); + let mut columns = Vec::new(); + let mut input = mark.as_ref(); + for (index, _) in markers.iter().enumerate() { + columns.clear(); + let (Operator::MarkApply(op), Childrens::Twins { left, right }) = + (&input.operator, input.childrens.as_ref()) + else { + unreachable!() + }; + for expr in op.predicates.iter().chain(op.parameterized_probe.iter()) { + expr.all_referenced_columns(arena, |_, column| { + columns.push(*column); + true + })?; + } + right.visit_column_refs(arena, &mut |column| columns.push(*column))?; + for marker in &markers[index + 1..] { + if columns .iter() - .any(|candidate| arena.same_column(*candidate, *column)) - })? { - return Ok(false); + .any(|column| arena.same_column(*column, *marker)) + { + consumed.push(*marker); + } + } + input = left; } + let mut changed = false; + let mut guards = Vec::new(); let mut input = mark.as_mut(); - for _ in &markers { + for (index, marker) in markers.iter().enumerate() { let (Operator::MarkApply(op), Childrens::Twins { left, .. }) = (&mut input.operator, input.childrens.as_mut()) else { unreachable!() }; - // Share the logical predicate; isolate mutable expression positions. - let guard = guard.clone_expression(arena)?; - op.guard = Some(if negated { - arena.alloc_expression(ScalarExpression::Unary { - op: UnaryOperator::Not, - expr: guard, - evaluator: None, - ty: LogicalType::Boolean, - }) - } else { - guard - }); + if op.guard.is_none() + && !consumed + .iter() + .any(|column| arena.same_column(*column, *marker)) + { + // Top-down plan order is the reverse of execution order. + let unavailable = &markers[..=index]; + guards.clear(); + let mut visitor = GuardVisitor { + marker: *marker, + guards: &mut guards, + }; + visitor.visit(filter_op.predicate, arena)?; + let mut guard = None; + for &(sibling, when_true) in &guards { + let skip = decisive_condition(sibling, when_true, unavailable, arena)?; + let needed = skip.map(|expr| negate(expr, arena)); + guard = combine_conditions(BinaryOperator::And, guard, needed, true, arena); + } + if let Some(guard) = guard { + let schema = left.output_schema(arena); + if guard.all_referenced_columns(arena, |arena, column| { + !unavailable + .iter() + .any(|marker| arena.same_column(*marker, *column)) + && schema + .iter() + .any(|candidate| arena.same_column(*candidate, *column)) + })? { + op.guard = Some(guard.clone_expression(arena)?); + changed = true; + } + } + } input = left; } - Ok(true) + Ok(changed) } } struct GuardVisitor<'a> { - markers: &'a [ColumnRef], - guard: Option<(ExprRef, bool)>, + marker: ColumnRef, + guards: &'a mut Vec<(ExprRef, bool)>, } impl ExprVisitor> for GuardVisitor<'_> { fn visit_expression_ref(&mut self, _: ExprRef, _: &PlanArena) -> Result { - Ok(self.guard.is_none()) + Ok(true) } fn visit_expression( @@ -169,31 +199,119 @@ impl ExprVisitor> for GuardVisitor<'_> { arena: &PlanArena, ) -> Result<(), DatabaseError> { let left_marker = left.any_referenced_column(arena, |arena, column| { - self.markers - .iter() - .any(|marker| arena.same_column(*column, *marker)) + arena.same_column(*column, self.marker) })?; let right_marker = right.any_referenced_column(arena, |arena, column| { - self.markers - .iter() - .any(|marker| arena.same_column(*column, *marker)) + arena.same_column(*column, self.marker) })?; let (branch, sibling) = match (left_marker, right_marker) { (true, false) => (left, right), (false, true) => (right, left), + // Repeated occurrences can only use conditions collected above this node. _ => return Ok(()), }; + self.guards + .push((sibling, matches!(op, BinaryOperator::Or))); + self.visit(branch, arena) + } +} + +// Return only sufficient TRUE/FALSE conditions; unavailable marks and NULL are not evidence. +fn decisive_condition( + expr: ExprRef, + when_true: bool, + unavailable: &[ColumnRef], + arena: &mut PlanArena, +) -> Result, DatabaseError> { + if !expr.any_referenced_column(arena, |arena, column| { + unavailable + .iter() + .any(|marker| arena.same_column(*column, *marker)) + })? { let mut visitor = SimpleGuardVisitor { valid: true, context: GuardContext::Predicate, }; - visitor.visit(sibling, arena)?; + visitor.visit(expr, arena)?; if visitor.valid { - self.guard = Some((sibling, matches!(op, BinaryOperator::Or))); - return Ok(()); + return Ok(Some(if when_true { expr } else { negate(expr, arena) })); } - self.visit(branch, arena) } + match arena.expression(strip_alias(expr, arena)).clone() { + ScalarExpression::Binary { + op: op @ (BinaryOperator::And | BinaryOperator::Or), + left_expr, + right_expr, + .. + } => { + let left = decisive_condition(left_expr, when_true, unavailable, arena)?; + let right = decisive_condition(right_expr, when_true, unavailable, arena)?; + let either = matches!(op, BinaryOperator::Or) == when_true; + Ok(combine_conditions( + if either { + BinaryOperator::Or + } else { + BinaryOperator::And + }, + left, + right, + either, + arena, + )) + } + ScalarExpression::Unary { + op: UnaryOperator::Not, + expr, + .. + } => decisive_condition(expr, !when_true, unavailable, arena), + ScalarExpression::TypeCast { + expr, + ty: LogicalType::Boolean, + .. + } if *expr.return_type(arena) == LogicalType::Boolean => { + decisive_condition(expr, when_true, unavailable, arena) + } + _ => Ok(None), + } +} + +fn combine_conditions( + op: BinaryOperator, + left: Option, + right: Option, + allow_one: bool, + arena: &mut PlanArena, +) -> Option { + match (left, right) { + (Some(left_expr), Some(right_expr)) => { + Some(arena.alloc_expression(ScalarExpression::Binary { + op, + left_expr, + right_expr, + evaluator: None, + ty: LogicalType::Boolean, + })) + } + (Some(expr), None) | (None, Some(expr)) if allow_one => Some(expr), + _ => None, + } +} + +fn negate(expr: ExprRef, arena: &mut PlanArena) -> ExprRef { + if let ScalarExpression::Unary { + op: UnaryOperator::Not, + expr, + .. + } = arena.expression(expr) + { + return *expr; + } + arena.alloc_expression(ScalarExpression::Unary { + op: UnaryOperator::Not, + expr, + evaluator: None, + ty: LogicalType::Boolean, + }) } #[derive(Clone, Copy)] @@ -248,7 +366,9 @@ impl ExprVisitor> for SimpleGuardVisitor { } | ScalarExpression::Binary { op: - BinaryOperator::Eq + BinaryOperator::And + | BinaryOperator::Or + | BinaryOperator::Eq | BinaryOperator::NotEq | BinaryOperator::Gt | BinaryOperator::GtEq @@ -292,7 +412,7 @@ impl ExprVisitor> for SimpleGuardVisitor { fn visit_binary( &mut self, - _: &BinaryOperator, + op: &BinaryOperator, left: ExprRef, right: ExprRef, _: Option<&BinaryEvaluatorRef>, @@ -300,7 +420,11 @@ impl ExprVisitor> for SimpleGuardVisitor { arena: &PlanArena, ) -> Result<(), DatabaseError> { let previous = self.context; - self.context = GuardContext::NumericOperand; + self.context = if matches!(op, BinaryOperator::And | BinaryOperator::Or) { + GuardContext::Predicate + } else { + GuardContext::NumericOperand + }; self.visit(left, arena)?; self.visit(right, arena)?; self.context = previous; @@ -399,7 +523,7 @@ mod tests { unreachable!() }; let lower = mark.take(); - **mark = MarkApplyOperator::build_exists(lower, right, column, vec![self.marker]); + **mark = MarkApplyOperator::build_exists(lower, right, column, vec![]); let markers = arena.alloc_expression(ScalarExpression::Binary { op: if matches!(op, BinaryOperator::And) { BinaryOperator::Or @@ -448,7 +572,7 @@ mod tests { } #[test] - fn shared_segment_guard_preserves_all_marker_combinations() -> Result<(), DatabaseError> { + fn independent_guards_preserve_all_marker_combinations() -> Result<(), DatabaseError> { for op in [BinaryOperator::And, BinaryOperator::Or] { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); @@ -478,17 +602,8 @@ mod tests { DataValue::Null, ] { let input = Tuple::new(None, vec![flag.clone()]); - let guard = arena - .expression(upper) - .eval(&arena, Some(&input))? - .into_owned(); - assert_eq!( - guard, - arena - .expression(lower) - .eval(&arena, Some(&input))? - .into_owned() - ); + let skip_lower = *arena.expression(lower).eval(&arena, Some(&input))? + == DataValue::Boolean(false); for a in [ DataValue::Boolean(false), DataValue::Boolean(true), @@ -500,11 +615,26 @@ mod tests { DataValue::Null, ] { let original = Tuple::new(None, vec![flag.clone(), a.clone(), b.clone()]); - let guarded = if guard == DataValue::Boolean(false) { - Tuple::new(None, vec![flag.clone(), DataValue::Null, DataValue::Null]) + let a_out = if skip_lower { + DataValue::Null } else { - original.clone() + a.clone() }; + let input = Tuple::new(None, vec![flag.clone(), a_out.clone()]); + let skip_upper = *arena.expression(upper).eval(&arena, Some(&input))? + == DataValue::Boolean(false); + let guarded = Tuple::new( + None, + vec![ + flag.clone(), + a_out, + if skip_upper { + DataValue::Null + } else { + b.clone() + }, + ], + ); assert_eq!( arena .expression(predicate) @@ -524,7 +654,7 @@ mod tests { } #[test] - fn segment_requires_common_protection_and_hidden_markers() -> Result<(), DatabaseError> { + fn independent_protection_keeps_projected_markers_unguarded() -> Result<(), DatabaseError> { for escaped in [0, 1, 2] { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); @@ -561,8 +691,11 @@ mod tests { .exprs .push(if escaped == 1 { fixture.marker } else { upper }); } - assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); - assert!(fixture.mark().guard.is_none()); + assert_eq!( + GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?, + escaped == 0 + ); + assert_eq!(fixture.mark().guard.is_some(), escaped == 0); let Childrens::Only(filter) = fixture.plan.childrens.as_mut() else { unreachable!() }; @@ -575,7 +708,7 @@ mod tests { let Operator::MarkApply(lower) = &left.operator else { unreachable!() }; - assert!(lower.guard.is_none()); + assert_eq!(lower.guard.is_some(), escaped == 0); } Ok(()) } @@ -673,6 +806,98 @@ mod tests { Ok(()) } + #[test] + fn three_mark_guards_preserve_all_729_truth_combinations() -> Result<(), DatabaseError> { + use crate::expression::visitor_mut::ExprVisitorMut; + use crate::expression::BindEvaluator; + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let mut columns = Vec::new(); + let mut exprs = Vec::new(); + for (position, name) in ["q2", "p2", "q3", "m1", "m2", "m3"].iter().enumerate() { + let column = arena.alloc_column(ColumnCatalog::new( + (*name).into(), + true, + ColumnDesc::new(LogicalType::Boolean, None, false, None)?, + )); + columns.push(column); + exprs.push(arena.alloc_expression(ScalarExpression::column_expr(column, position))); + } + let binary = |op, left_expr, right_expr, arena: &mut PlanArena| { + arena.alloc_expression(ScalarExpression::Binary { + op, + left_expr, + right_expr, + evaluator: None, + ty: LogicalType::Boolean, + }) + }; + let p2_or_m2 = binary(BinaryOperator::Or, exprs[1], exprs[4], &mut arena); + let b2 = binary(BinaryOperator::And, exprs[0], p2_or_m2, &mut arena); + let b3 = binary(BinaryOperator::And, exprs[2], exprs[5], &mut arena); + let first = binary(BinaryOperator::Or, exprs[3], b2, &mut arena); + let mut predicate = binary(BinaryOperator::Or, first, b3, &mut arena); + let mut guards = Vec::new(); + let mut conditions = Vec::new(); + for index in 3..6 { + conditions.clear(); + let mut visitor = GuardVisitor { + marker: columns[index], + guards: &mut conditions, + }; + visitor.visit(predicate, &arena)?; + let mut guard = None; + for &(sibling, when_true) in &conditions { + let skip = decisive_condition(sibling, when_true, &columns[index..], &mut arena)?; + let needed = skip.map(|expr| negate(expr, &mut arena)); + guard = combine_conditions(BinaryOperator::And, guard, needed, true, &mut arena); + } + let mut guard = guard.unwrap(); + BindEvaluator.visit(&mut guard, &mut arena)?; + guards.push(guard); + } + BindEvaluator.visit(&mut predicate, &mut arena)?; + let values = [ + DataValue::Boolean(false), + DataValue::Boolean(true), + DataValue::Null, + ]; + let mut skipped = [0; 3]; + for combination in 0..729 { + let mut digits = combination; + let original = Tuple::new( + None, + (0..6) + .map(|_| { + let value = values[digits % 3].clone(); + digits /= 3; + value + }) + .collect(), + ); + let mut guarded = Tuple::new(None, original.values[..3].to_vec()); + for index in 0..3 { + let skip = *arena + .expression(guards[index]) + .eval(&arena, Some(&guarded))? + == DataValue::Boolean(false); + skipped[index] += usize::from(skip); + guarded.values.push(if skip { + DataValue::Null + } else { + original.values[index + 3].clone() + }); + } + assert_eq!( + arena.expression(predicate).eval(&arena, Some(&original))?, + arena.expression(predicate).eval(&arena, Some(&guarded))?, + "combination {combination}" + ); + } + assert!(skipped[1] > 0 && skipped[2] > 0); + Ok(()) + } + #[test] fn unsafe_sibling_is_not_evaluated_before_a_matching_marker() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); diff --git a/src/optimizer/rule/normalization/materialize_mark_apply.rs b/src/optimizer/rule/normalization/materialize_mark_apply.rs index c1d310ae..b3ef4f9b 100644 --- a/src/optimizer/rule/normalization/materialize_mark_apply.rs +++ b/src/optimizer/rule/normalization/materialize_mark_apply.rs @@ -14,6 +14,7 @@ use super::NormalizationRule; use crate::errors::DatabaseError; +use crate::expression::ScalarExpression; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyOperator}; use crate::planner::operator::Operator; @@ -22,11 +23,16 @@ use crate::planner::{Childrens, LogicalPlan, PlanArena}; pub(crate) struct MaterializeMarkApply; impl MaterializeMarkApply { - fn can_materialize(op: &MarkApplyOperator) -> bool { + fn can_materialize(op: &MarkApplyOperator, arena: &PlanArena) -> bool { // The first predicate compares values; additional predicates bind outer rows. + // The binder builds that comparison as `outer_operand op subquery_output`, + // so the RHS keys can be cached by evaluating its right side alone. matches!(op.kind, MarkApplyKind::Quantified(_)) && op.parameterized_probe().is_none() - && op.predicates().len() == 1 + && matches!( + op.predicates(), + [predicate] if matches!(arena.expression(*predicate), ScalarExpression::Binary { .. }) + ) } } @@ -34,7 +40,7 @@ impl NormalizationRule for MaterializeMarkApply { fn apply( &self, plan: &mut LogicalPlan, - _: &mut PlanArena, + arena: &mut PlanArena, _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let (Operator::MarkApply(op), Childrens::Twins { left, right }) = @@ -42,7 +48,7 @@ impl NormalizationRule for MaterializeMarkApply { else { return Ok(false); }; - if op.materialize || !Self::can_materialize(op) { + if op.materialize || !Self::can_materialize(op, arena) { return Ok(false); } let (Some(left_rows), Some(right_rows)) = (left.estimated_rows(), right.estimated_rows()) @@ -64,7 +70,7 @@ impl NormalizationRule for MaterializeMarkApply { mod tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc}; - use crate::expression::ScalarExpression; + use crate::expression::BinaryOperator; use crate::planner::operator::mark_apply::MarkApplyQuantifier; use crate::planner::operator::values::ValuesOperator; use crate::planner::operator::{PhysicalOption, PlanImpl, RowCountOption, SortOption}; @@ -87,17 +93,33 @@ mod tests { true, ColumnDesc::new(LogicalType::Boolean, None, false, None)?, )); + let right_key = arena.alloc_column(ColumnCatalog::new( + "right_key".to_string(), + true, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, + )); let value = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(2))); let left = LogicalPlan::new( Operator::Values(ValuesOperator::new(vec![value; 8], 8, vec![key])), Childrens::None, ); let right = LogicalPlan::new( - Operator::Values(ValuesOperator::new(vec![value; 2], 2, vec![key])), + Operator::Values(ValuesOperator::new(vec![value; 2], 2, vec![right_key])), Childrens::None, ); - let predicate = - arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(true))); + let left_ref = arena.alloc_expression(ScalarExpression::column_expr(key, 0)); + let right_ref = arena.alloc_expression(ScalarExpression::column_expr(right_key, 1)); + let compare = |arena: &mut PlanArena, left_expr, right_expr| { + arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Eq, + left_expr, + right_expr, + evaluator: None, + ty: LogicalType::Boolean, + }) + }; + let predicate = compare(&mut arena, left_ref, right_ref); + let constant = arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(true))); let op = MarkApplyOperator::new_quantified(MarkApplyQuantifier::Any, marker, vec![predicate]); let mut plan = LogicalPlan::new( @@ -160,6 +182,16 @@ mod tests { unreachable!() }; op.predicates.pop(); + // Only a binary comparison exposes the RHS side whose keys are cached. + let Operator::MarkApply(op) = &mut plan.operator else { + unreachable!() + }; + op.predicates[0] = constant; + assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena, None)?); + let Operator::MarkApply(op) = &mut plan.operator else { + unreachable!() + }; + op.predicates[0] = predicate; let Childrens::Twins { right, .. } = plan.childrens.as_mut() else { unreachable!() }; diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 1112e0c1..8c8054a0 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -292,18 +292,24 @@ fn constant_keys(expr: ExprRef, keys: &mut Vec<(ExprRef, ExprRef)>, arena: &Plan fn parameterize( plan: &mut LogicalPlan, - mut keys: Vec<(ExprRef, ExprRef)>, + keys: &mut Vec<(ExprRef, ExprRef)>, + join_key_count: usize, arena: &PlanArena<'_>, ) -> Option> { match (&mut plan.operator, plan.childrens.as_mut()) { (Operator::Filter(filter), Childrens::Only(child)) => { - constant_keys(filter.predicate, &mut keys, arena); - parameterize(child, keys, arena) + let key_count = keys.len(); + constant_keys(filter.predicate, keys, arena); + let probe = parameterize(child, keys, join_key_count, arena); + // Keep filter keys local to this traversal, including failed attempts. + keys.truncate(key_count); + probe } (Operator::TableScan(scan), _) if scan.limit == (None, None) => { 'indexes: for info in &mut scan.index_infos { let meta = arena.index(info.meta); let mut probe = Vec::with_capacity(meta.column_ids.len()); + let mut uses_join_key = false; 'columns: for id in &meta.column_ids { let Some(candidate) = scan .columns @@ -312,7 +318,7 @@ fn parameterize( else { continue 'indexes; }; - for &(value, column) in &keys { + for (position, &(value, column)) in keys.iter().enumerate() { let ScalarExpression::ColumnRef { column, .. } = arena.expression(column.unpack_alias(arena)) else { @@ -323,11 +329,17 @@ fn parameterize( == arena.column(*candidate).datatype() { probe.push(value); + uses_join_key |= position < join_key_count; continue 'columns; } } continue 'indexes; } + // Constants appended by filters may complete a composite key, + // but must not trigger a per-outer-row probe on their own. + if !uses_join_key { + continue 'indexes; + } info.lookup = Some(IndexLookup::Probe); info.residual_predicate = None; plan.physical_option = Some(PhysicalOption::new( @@ -368,13 +380,16 @@ impl NormalizationRule for ParameterizeInnerJoin { return Ok(false); } let mut keys = on.clone(); - let (project, probe_keys) = if let Some(probe) = parameterize(right, keys.clone(), arena) { + // Filter traversal only appends constants after these outer-dependent keys. + let join_key_count = keys.len(); + let right_probe = parameterize(right, &mut keys, join_key_count, arena); + let (project, probe_keys) = if let Some(probe) = right_probe { (None, probe) } else { for (l, r) in &mut keys { std::mem::swap(l, r); } - let Some(probe) = parameterize(left, keys.clone(), arena) else { + let Some(probe) = parameterize(left, &mut keys, join_key_count, arena) else { return Ok(false); }; let left_schema = left.output_schema(arena); @@ -1023,6 +1038,115 @@ mod inner_join_tests { Ok(()) } + #[test] + fn constant_only_probe_leaves_static_scan_and_join_unchanged() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let left = scan(&mut arena, "outer", &[LogicalType::Integer], &[]); + let mut right = scan( + &mut arena, + "inner", + &[LogicalType::Integer, LogicalType::Integer], + &[&[1]], + ); + let l = column(&left, 0, &mut arena); + let r = column(&right, 0, &mut arena); + let static_key = column(&right, 1, &mut arena); + let constant = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(782))); + let predicate = binary(&mut arena, BinaryOperator::Eq, static_key, constant); + let Operator::TableScan(scan) = &mut right.operator else { + unreachable!() + }; + let info = &mut scan.index_infos[0]; + info.lookup = Some(IndexLookup::Static(Range::Eq(DataValue::Int32(782)))); + info.residual_predicate = Some(predicate); + right.physical_option = Some(PhysicalOption::new( + PlanImpl::IndexScan(Box::new(info.clone())), + SortOption::None, + RowCountOption::Unknown, + )); + // Keep the join-key boundary intact across multiple filter layers. + let passthrough = arena.alloc_expression(ScalarExpression::from(true)); + let right = FilterOperator::build(passthrough, right, false); + let right = FilterOperator::build(predicate, right, false); + let mut plan = join(left, right, vec![(l, r)], None); + let before = plan.clone(); + + assert!(!ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan, before); + assert_eq!((position(l, &arena), position(r, &arena)), (0, 0)); + Ok(()) + } + + #[test] + fn constant_only_index_does_not_block_outer_key_probe() -> Result<(), DatabaseError> { + for swap in [false, true] { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let left_indexes: &[&[usize]] = if swap { &[&[0]] } else { &[] }; + let right_indexes: &[&[usize]] = if swap { &[&[1]] } else { &[&[1], &[0]] }; + let left = scan(&mut arena, "outer", &[LogicalType::Integer], left_indexes); + let right = scan( + &mut arena, + "inner", + &[LogicalType::Integer, LogicalType::Integer], + right_indexes, + ); + let l = column(&left, 0, &mut arena); + let r = column(&right, 0, &mut arena); + let static_key = column(&right, 1, &mut arena); + let constant = + arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(782))); + let predicate = binary(&mut arena, BinaryOperator::Eq, constant, static_key); + let right = FilterOperator::build(predicate, right, false); + let original_right = right.clone(); + let mut plan = join(left, right, vec![(l, r)], None); + let schema = plan.output_schema(&mut arena).clone(); + + assert!(ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.output_schema(&mut arena), &schema); + let apply_plan = if swap { + let Childrens::Only(child) = plan.childrens.as_ref() else { + panic!("expected output-restoring projection") + }; + child.as_ref() + } else { + &plan + }; + let Operator::MarkApply(apply) = &apply_plan.operator else { + panic!("expected inner join apply") + }; + assert_eq!(apply.kind, MarkApplyKind::InnerJoin); + assert_eq!(apply.predicates().len(), 1); + assert_eq!( + apply.parameterized_probe().copied(), + Some(if swap { r } else { l }) + ); + let Childrens::Twins { left, right } = apply_plan.childrens.as_ref() else { + unreachable!() + }; + let scan = if swap { + assert_eq!(left.as_ref(), &original_right); + right.as_ref() + } else { + let Operator::Filter(filter) = &right.operator else { + panic!("static filter must survive") + }; + assert_eq!(filter.predicate, predicate); + let Childrens::Only(child) = right.childrens.as_ref() else { + unreachable!() + }; + child.as_ref() + }; + let PlanImpl::IndexScan(info) = &scan.physical_option.as_ref().unwrap().plan else { + panic!("expected index scan") + }; + assert_eq!(arena.index(info.meta).column_ids, vec![0]); + assert_eq!(info.lookup, Some(IndexLookup::Probe)); + } + Ok(()) + } + #[test] fn composite_probe_uses_index_order_and_preserves_static_filter() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); @@ -1033,12 +1157,12 @@ mod inner_join_tests { &[const { LogicalType::Integer }; 2], &[], ); - // First index cannot be probed. Second requires constant + two join keys. + // Skip the constant-only and unmatched indexes; the last uses both key sources. let mut right = scan( &mut arena, "inner", &[const { LogicalType::Integer }; 4], - &[&[3], &[0, 2, 1]], + &[&[0], &[3], &[0, 2, 1]], ); let l0 = column(&left, 0, &mut arena); let l1 = column(&left, 1, &mut arena); @@ -1053,7 +1177,7 @@ mod inner_join_tests { let Operator::TableScan(scan) = &mut right.operator else { unreachable!() }; - let info = &mut scan.index_infos[1]; + let info = &mut scan.index_infos[2]; info.lookup = Some(IndexLookup::Static(Range::Eq(DataValue::Int32(7)))); info.residual_predicate = Some(residual); let index = info.meta; @@ -1070,6 +1194,7 @@ mod inner_join_tests { panic!("expected apply without projection") }; assert_eq!(apply.kind, MarkApplyKind::InnerJoin); + assert_eq!(apply.predicates().len(), 2); assert_eq!( arena.expression(*apply.parameterized_probe().unwrap()), &ScalarExpression::Tuple(vec![constant, l1, l0]) diff --git a/src/types/mod.rs b/src/types/mod.rs index 9f399041..f73c0f5f 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -155,6 +155,27 @@ impl LogicalType { } } + pub fn fixed_encoded_len(&self) -> Option { + let payload = match self { + LogicalType::Boolean | LogicalType::Tinyint | LogicalType::UTinyint => 1, + LogicalType::Smallint | LogicalType::USmallint => 2, + LogicalType::Integer + | LogicalType::UInteger + | LogicalType::Float + | LogicalType::Date => 4, + LogicalType::Bigint + | LogicalType::UBigint + | LogicalType::Double + | LogicalType::DateTime => 8, + LogicalType::Time(_) => 12, + LogicalType::TimeStamp(..) => 17, + #[cfg(feature = "decimal")] + LogicalType::Decimal(..) => 16, + _ => return None, + }; + Some(1 + payload) // One-byte variant tag. + } + pub fn numeric() -> Vec { vec![ LogicalType::Tinyint, @@ -496,6 +517,49 @@ pub(crate) mod test { use std::borrow::Cow; use std::io::{Cursor, Seek, SeekFrom}; + #[test] + #[cfg(feature = "spill")] + fn fixed_encoded_len_matches_values() -> Result<(), DatabaseError> { + use crate::execution::spill::SpillCodec; + use crate::types::value::DataValue; + + for ty in [ + LogicalType::Boolean, + LogicalType::Tinyint, + LogicalType::UTinyint, + LogicalType::Smallint, + LogicalType::USmallint, + LogicalType::Integer, + LogicalType::UInteger, + LogicalType::Bigint, + LogicalType::UBigint, + LogicalType::Float, + LogicalType::Double, + LogicalType::Date, + LogicalType::DateTime, + LogicalType::Time(Some(3)), + LogicalType::TimeStamp(Some(6), true), + #[cfg(feature = "decimal")] + LogicalType::Decimal(None, None), + ] { + assert_eq!( + ty.fixed_encoded_len(), + Some(DataValue::init(&ty).encoded_len()?), + "{ty:?}" + ); + } + for ty in [ + LogicalType::SqlNull, + LogicalType::Varchar(None, CharLengthUnits::Characters), + LogicalType::Char(2, CharLengthUnits::Octets), + LogicalType::Varbinary(None), + LogicalType::Tuple(vec![LogicalType::Integer]), + ] { + assert_eq!(ty.fixed_encoded_len(), None); + } + Ok(()) + } + #[test] fn test_char_length_units_display() { assert_eq!(CharLengthUnits::Characters.to_string(), "CHARACTERS"); diff --git a/tests/slt/any_all_quantifier.slt b/tests/slt/any_all_quantifier.slt index caab46ec..2b83405e 100644 --- a/tests/slt/any_all_quantifier.slt +++ b/tests/slt/any_all_quantifier.slt @@ -48,3 +48,159 @@ drop table any_all_outer; statement ok drop table any_all_inner; + +# Independent guards skip RHSs that cannot affect their own AND/OR branches. +statement ok +CREATE TABLE guard_left (x INTEGER PRIMARY KEY) + +statement ok +INSERT INTO guard_left VALUES (1), (2), (3), (4), (5), (6) + +statement ok +CREATE TABLE guard_bad (id INTEGER PRIMARY KEY, s VARCHAR) + +statement ok +INSERT INTO guard_bad VALUES (1, 'not an integer') + +# IN2 is masked by its ordinary OR sibling or by x > 3. +# IN3's AND condition is false for every input row. +# Neither CAST is allowed to execute, including after IN1 returns FALSE. +query I rowsort +SELECT x FROM guard_left WHERE x IN (SELECT x FROM guard_left WHERE x = 5) OR (x <= 3 AND (x < 4 OR x IN (SELECT CAST(s AS INTEGER) FROM guard_bad))) OR (x < 0 AND x IN (SELECT CAST(s AS INTEGER) FROM guard_bad)) +---- +1 +2 +3 +5 + +# NOT preserves the same local demand conditions and three-valued semantics. +query I rowsort +SELECT x FROM guard_left WHERE NOT (x IN (SELECT x FROM guard_left WHERE x = 5) OR (x <= 3 AND (x < 4 OR x IN (SELECT CAST(s AS INTEGER) FROM guard_bad))) OR (x < 0 AND x IN (SELECT CAST(s AS INTEGER) FROM guard_bad))) +---- +4 +6 + +statement ok +CREATE TABLE guard_flags (id INTEGER PRIMARY KEY, b BOOLEAN) + +statement ok +INSERT INTO guard_flags VALUES (1, FALSE) + +# IN1's marker is consumed by IN2, even though it is absent from the final Filter. +# It must not be replaced with NULL before the second comparison. +query I rowsort +SELECT x FROM guard_left WHERE (x IN (SELECT x FROM guard_left WHERE x = 5)) IN (SELECT b FROM guard_flags) +---- +1 +2 +3 +4 +6 + +statement ok +drop table guard_flags; + +statement ok +drop table guard_bad; + +statement ok +drop table guard_left; + +# Repeated outer rows materialize the RHS keys; more than 1024 keys spill into +# several sorted runs. Membership uses binary search, other ops scan the keys. +statement ok +create table mat_outer(id int primary key, a int) + +statement ok +create table mat_inner(id int primary key, b int) + +statement ok +create table mat_inner_null(id int primary key, b int) + +statement ok +insert into mat_outer select cast(number as int), cast(number as int) from table(numbers(2000)) + +statement ok +insert into mat_outer values (2000, null) + +statement ok +insert into mat_inner select cast(number as int), cast(number * 2 as int) from table(numbers(1500)) + +statement ok +insert into mat_inner_null select id, b from mat_inner + +statement ok +insert into mat_inner_null values (1500, null) + +statement ok +analyze table mat_outer + +statement ok +analyze table mat_inner + +statement ok +analyze table mat_inner_null + +query I +select count(*) from mat_outer where a in (select b from mat_inner) +---- +1000 + +query I +select count(*) from mat_outer where a not in (select b from mat_inner) +---- +1000 + +query I +select count(*) from mat_outer where a <> all(select b from mat_inner) +---- +1000 + +query I +select count(*) from mat_outer where a in (select b from mat_inner_null) +---- +1000 + +query I +select count(*) from mat_outer where a not in (select b from mat_inner_null) +---- +0 + +query I +select count(*) from mat_outer where (a in (select b from mat_inner_null)) is null +---- +1001 + +query I +select count(*) from mat_outer where a > any(select b from mat_inner) +---- +1999 + +query I +select count(*) from mat_outer where a <= all(select b from mat_inner) +---- +1 + +query I +select count(*) from mat_outer where a > all(select b from mat_inner_null) +---- +0 + +query I +select count(*) from mat_outer where a >= any(select b from mat_inner_null) +---- +2000 + +query I +select count(*) from mat_outer where (a <= all(select b from mat_inner_null)) is null +---- +2 + +statement ok +drop table mat_outer + +statement ok +drop table mat_inner + +statement ok +drop table mat_inner_null From bafb8d199552b4957e55e121cce513be1539700d Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 23/62] fix: restore index scans under aliased tables - Treat `AliasType::Expr` targets as output identity only: evaluate, remap and substitute through `expr`; the default visitors skip the target, while deep clone and referenced-column collection still walk it. - Run Collapse Project before Combine Operators so filters reach the scan through rename projections, keeping the final collapse pass. - Let Guard Mark Apply skip Sort/Limit/TopK between the projection and filter, checking those operators do not read the markers. --- src/binder/aggregate.rs | 6 +- src/binder/distinct.rs | 6 +- src/binder/expr.rs | 19 ++-- src/binder/select.rs | 38 +------- src/db.rs | 7 +- src/expression/evaluator.rs | 12 +-- src/expression/mod.rs | 8 +- src/expression/visitor.rs | 6 +- src/expression/visitor_mut.rs | 19 +++- src/optimizer/plan_utils.rs | 12 +-- .../rule/normalization/column_pruning.rs | 11 +-- .../rule/normalization/combine_operators.rs | 6 ++ .../rule/normalization/elimination.rs | 16 ---- .../rule/normalization/guard_mark_apply.rs | 92 ++++++++++--------- src/optimizer/rule/normalization/mod.rs | 27 ------ .../rule/normalization/project_move.rs | 20 +--- src/planner/operator/mod.rs | 14 +++ tests/slt/crdb/join.slt | 2 +- tests/slt/cte.slt | 2 +- tests/slt/join.slt | 2 +- tests/slt/subquery.slt | 20 ++++ tests/slt/where_by_index_explain.slt | 2 +- 22 files changed, 147 insertions(+), 200 deletions(-) diff --git a/src/binder/aggregate.rs b/src/binder/aggregate.rs index 303fed98..c3997afb 100644 --- a/src/binder/aggregate.rs +++ b/src/binder/aggregate.rs @@ -506,10 +506,10 @@ mod tests { alias: AliasType::Name("g".to_string()), }); - let constant = arena.alloc_expression(ScalarExpression::Constant(1_i32.into())); + let identity = arena.alloc_expression(ScalarExpression::Constant(1_i32.into())); let mut target = arena.alloc_expression(ScalarExpression::Alias { - expr: constant, - alias: AliasType::Expr(group_expr), + expr: group_expr, + alias: AliasType::Expr(identity), }); { diff --git a/src/binder/distinct.rs b/src/binder/distinct.rs index 8541b662..9608f800 100644 --- a/src/binder/distinct.rs +++ b/src/binder/distinct.rs @@ -197,10 +197,10 @@ mod tests { alias: AliasType::Name("v".to_string()), }); - let constant = arena.alloc_expression(ScalarExpression::Constant(1_i32.into())); + let identity = arena.alloc_expression(ScalarExpression::Constant(1_i32.into())); let mut target = arena.alloc_expression(ScalarExpression::Alias { - expr: constant, - alias: AliasType::Expr(expr), + expr, + alias: AliasType::Expr(identity), }); { diff --git a/src/binder/expr.rs b/src/binder/expr.rs index 29df7e87..5f68820a 100644 --- a/src/binder/expr.rs +++ b/src/binder/expr.rs @@ -83,6 +83,7 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> &mut self, expr: ScalarExpression, sub_query: LogicalPlan, + correlated: bool, arena: &mut PlanArena, ) -> Result<(ScalarExpression, LogicalPlan), DatabaseError> { let (exprs, is_tuple) = match expr { @@ -94,16 +95,18 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> for (position, expr) in exprs.into_iter().enumerate() { let (alias_expr, alias_ref) = self.bind_temp_table_alias(expr, position, arena); - let predicate_alias = arena.alloc_expression(alias_expr); - // The projection evaluates against the subquery-local tuple, while the - // predicate evaluates against the combined left/right tuple. Clone the - // complete expression graph so position rewrites in either context do - // not mutate the other one through shared ExprRefs. - let projection_alias = predicate_alias.clone_expression(arena)?; + // The projection evaluates against the subquery-local tuple, while a + // correlated predicate evaluates `expr` against the combined tuple. + // Clone the complete expression graph so position rewrites in either + // context do not mutate the other one through shared ExprRefs. + let projection_alias = arena.alloc_expression(alias_expr).clone_expression(arena)?; if !is_tuple { let alias_plan = Self::build_project_plan(sub_query, vec![projection_alias]); - return Ok((arena.expression(predicate_alias).clone(), alias_plan)); + // An uncorrelated predicate reads the column produced by the projection. + let predicate = if correlated { expr } else { alias_ref }; + return Ok((arena.expression(predicate).clone(), alias_plan)); } + alias_exprs.push(projection_alias); alias_refs.push(alias_ref); } @@ -316,7 +319,7 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> )); } - let (alias_expr, sub_query) = self.bind_temp_table(column, sub_query, arena)?; + let (alias_expr, sub_query) = self.bind_temp_table(column, sub_query, correlated, arena)?; let alias_expr = arena.alloc_expression(alias_expr); let predicate = ScalarExpression::Binary { op: compare_op, diff --git a/src/binder/select.rs b/src/binder/select.rs index 3191e2b8..d536c1e0 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -1530,15 +1530,9 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b plan, correlated, output_column, - predicate: mut quantified_predicate, + predicate: quantified_predicate, .. } => { - if correlated { - quantified_predicate = Self::rewrite_correlated_quantified_predicate( - quantified_predicate, - arena, - ); - } if self.context.is_step(&QueryBindStep::Where) { if let ScalarExpression::Binary { left_expr, .. } = &quantified_predicate @@ -1707,36 +1701,6 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b Ok((plan, apply_predicates)) } - fn rewrite_correlated_quantified_predicate( - predicate: ScalarExpression, - arena: &PlanArena<'_>, - ) -> ScalarExpression { - let strip_projection_alias = |expr| match arena.expression(expr) { - ScalarExpression::Alias { - expr, - alias: AliasType::Expr(_), - } => *expr, - _ => expr, - }; - - match predicate { - ScalarExpression::Binary { - op, - left_expr, - right_expr, - ty, - .. - } => ScalarExpression::Binary { - op, - left_expr: strip_projection_alias(left_expr), - right_expr: strip_projection_alias(right_expr), - evaluator: None, - ty, - }, - predicate => predicate, - } - } - fn plan_has_correlated_refs( plan: &LogicalPlan, left_schema: &Schema, diff --git a/src/db.rs b/src/db.rs index 1d4f642e..decac063 100644 --- a/src/db.rs +++ b/src/db.rs @@ -481,6 +481,11 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { NormalizationRuleImpl::TopK, ], ) + .before_batch( + "Collapse Project", + HepBatchStrategy::fix_point_topdown(10), + vec![NormalizationRuleImpl::EliminateProject], + ) .before_batch( "Combine Operators", HepBatchStrategy::fix_point_topdown(10), @@ -2280,7 +2285,7 @@ pub(crate) mod test { let plan = row.values[0].utf8().unwrap(); assert_eq!( plan, - "Projection [l.a, l.b, t1.a, t1.b, 9] [Project => (Sort Option: Follow)] LeftOuter Join Where (l.a > 0) [NestLoopJoin => (Sort Option: None)] Projection [(t1.a) as (t1.a), (t1.b) as (t1.b)] [Project => (Sort Option: Follow)] Filter (t1.b > 0), Is Having: false [Filter => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [SeqScan => (Sort Option: None)] Projection [t1.a, t1.b] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [IndexScan By pk_index => ($3, +inf) => (Sort Option: OrderBy: (t1.a Asc Nulls Last) ignore_prefix_len: 0)]" + "Projection [l.a, l.b, t1.a, t1.b, 9] [Project => (Sort Option: Follow)] LeftOuter Join Where (l.a > 0) [NestLoopJoin => (Sort Option: None)] Projection [(t1.a) as (l.a), (t1.b) as (l.b)] [Project => (Sort Option: Follow)] Filter (t1.b > 0), Is Having: false [Filter => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [SeqScan => (Sort Option: None)] Projection [t1.a, t1.b] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [IndexScan By pk_index => ($3, +inf) => (Sort Option: OrderBy: (t1.a Asc Nulls Last) ignore_prefix_len: 0)]" ); } diff --git a/src/expression/evaluator.rs b/src/expression/evaluator.rs index 7bc35a99..7911e33d 100644 --- a/src/expression/evaluator.rs +++ b/src/expression/evaluator.rs @@ -18,7 +18,7 @@ use crate::planner::PlanArena; use crate::errors::DatabaseError; use crate::expression::function::scala::ScalarFunction; -use crate::expression::{AliasType, BinaryOperator, ScalarExpression, TrimWhereField}; +use crate::expression::{BinaryOperator, ScalarExpression, TrimWhereField}; use crate::planner::MetaArena; use crate::types::evaluator::binary_create; use crate::types::tuple::{Input, InputRef}; @@ -69,15 +69,12 @@ impl ScalarExpression { }; Ok(tuple.column(*position)) } - ScalarExpression::Alias { expr, alias } => { + // `AliasType::Expr` only carries the output identity. + ScalarExpression::Alias { expr, .. } => { let Some(tuple) = tuple else { return Ok(Cow::Owned(DataValue::Null)); }; - if let AliasType::Expr(inner_expr) = alias { - arena.expression(*inner_expr).eval(arena, Some(tuple)) - } else { - arena.expression(*expr).eval(arena, Some(tuple)) - } + arena.expression(*expr).eval(arena, Some(tuple)) } ScalarExpression::TypeCast { expr, evaluator, .. @@ -419,6 +416,7 @@ fn trim_string(value: &str, trim_what: &str, trim_where: Option) #[cfg(test)] mod tests { use super::*; + use crate::expression::AliasType; use crate::planner::test::PlanArenaTestExt; use crate::planner::{ExecMetaArena, ExprRef, TableArenaCell}; diff --git a/src/expression/mod.rs b/src/expression/mod.rs index 18e7b681..ba5dd129 100644 --- a/src/expression/mod.rs +++ b/src/expression/mod.rs @@ -822,13 +822,7 @@ impl ExprRef { } pub fn unpack_alias(self, arena: &(impl MetaArena + ?Sized)) -> ExprRef { - if let ScalarExpression::Alias { - alias: AliasType::Expr(expr), - .. - } = arena.expression(self) - { - expr.unpack_alias(arena) - } else if let ScalarExpression::Alias { expr, .. } = arena.expression(self) { + if let ScalarExpression::Alias { expr, .. } = arena.expression(self) { expr.unpack_alias(arena) } else { self diff --git a/src/expression/visitor.rs b/src/expression/visitor.rs index e65c02ac..01aad32a 100644 --- a/src/expression/visitor.rs +++ b/src/expression/visitor.rs @@ -85,12 +85,10 @@ pub trait ExprVisitor: Sized { fn visit_alias( &mut self, expr: ExprRef, - alias: &AliasType, + _alias: &AliasType, arena: &A, ) -> Result<(), DatabaseError> { - if let AliasType::Expr(alias_expr) = alias { - self.visit(*alias_expr, arena)?; - } + // The alias target only carries the output identity. self.visit(expr, arena) } fn visit_type_cast( diff --git a/src/expression/visitor_mut.rs b/src/expression/visitor_mut.rs index f27ff795..7f492292 100644 --- a/src/expression/visitor_mut.rs +++ b/src/expression/visitor_mut.rs @@ -34,6 +34,19 @@ impl ExprVisitorMut for ExprCloner { *expr = arena.alloc_expression(arena.expression(*expr).clone()); walk_mut_expr(self, expr, arena) } + + // A deep clone must not share the alias target with the original. + fn visit_alias( + &mut self, + expr: &mut ExprRef, + alias: &mut AliasType, + arena: &mut A, + ) -> Result<(), DatabaseError> { + if let AliasType::Expr(alias_expr) = alias { + self.visit(alias_expr, arena)?; + } + self.visit(expr, arena) + } } pub(crate) struct PositionShift { @@ -135,12 +148,10 @@ pub trait ExprVisitorMut: Sized { fn visit_alias( &mut self, expr: &mut ExprRef, - alias: &mut AliasType, + _alias: &mut AliasType, arena: &mut A, ) -> Result<(), DatabaseError> { - if let AliasType::Expr(alias_expr) = alias { - self.visit(alias_expr, arena)?; - } + // The alias target only carries the output identity. self.visit(expr, arena) } diff --git a/src/optimizer/plan_utils.rs b/src/optimizer/plan_utils.rs index e1ca6319..fea246d1 100644 --- a/src/optimizer/plan_utils.rs +++ b/src/optimizer/plan_utils.rs @@ -20,7 +20,7 @@ use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::visitor::OperatorVisitor; use crate::planner::operator::{Operator, RowCountOption}; -use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; +use crate::planner::{Childrens, LogicalPlan, MetaArena, PlanArena}; use crate::types::ColumnId; use std::mem; @@ -226,16 +226,6 @@ impl ExprVisitor for ScanColumnVisitor<'_, '_, A> { )) } - fn visit_alias( - &mut self, - expr: ExprRef, - _alias: &AliasType, - arena: &A, - ) -> Result<(), DatabaseError> { - // The alias target describes the output, not the source column. - self.visit(expr, arena) - } - fn visit_column_ref(&mut self, column: &ColumnRef) -> Result<(), DatabaseError> { self.column = Some(*column); Ok(()) diff --git a/src/optimizer/rule/normalization/column_pruning.rs b/src/optimizer/rule/normalization/column_pruning.rs index 7cb221b2..acd37f33 100644 --- a/src/optimizer/rule/normalization/column_pruning.rs +++ b/src/optimizer/rule/normalization/column_pruning.rs @@ -16,7 +16,7 @@ use crate::catalog::ColumnRef; use crate::errors::DatabaseError; use crate::expression::agg::AggKind; use crate::expression::visitor::ExprVisitor; -use crate::expression::{AliasType, HasCountStar, ScalarExpression}; +use crate::expression::{HasCountStar, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::rule::normalization::{ @@ -100,15 +100,6 @@ impl ExprVisitor for ReferencedColumnCollector<'_, '_> { self.referenced_columns.insert(*column, self.arena); Ok(()) } - - fn visit_alias( - &mut self, - expr: ExprRef, - _ty: &AliasType, - arena: &(dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { - self.visit(expr, arena) - } } impl ApplyOutcome { diff --git a/src/optimizer/rule/normalization/combine_operators.rs b/src/optimizer/rule/normalization/combine_operators.rs index 9e760347..4b5bda13 100644 --- a/src/optimizer/rule/normalization/combine_operators.rs +++ b/src/optimizer/rule/normalization/combine_operators.rs @@ -178,7 +178,13 @@ mod tests { &mut arena, )?; + // Projects are eliminated before filters are combined. let pipeline = HepOptimizerPipeline::builder() + .before_batch( + "Collapse Project", + HepBatchStrategy::fix_point_topdown(10), + vec![NormalizationRuleImpl::EliminateProject], + ) .before_batch( "test_combine_filter", HepBatchStrategy::once_topdown(), diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index f8a85fa4..d120706d 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -72,22 +72,6 @@ impl ExprVisitorMut for Substitute { } Ok((self.exprs.is_none() && self.offset == 0) || self.visited.insert(*expr)) } - - fn visit_alias( - &mut self, - expr: &mut ExprRef, - alias: &mut AliasType, - arena: &mut A, - ) -> Result<(), DatabaseError> { - ExprVisitorMut::visit( - self, - match alias { - AliasType::Expr(expr) => expr, - _ => expr, - }, - arena, - ) - } } struct EliminateVisitor<'a, 'b> { diff --git a/src/optimizer/rule/normalization/guard_mark_apply.rs b/src/optimizer/rule/normalization/guard_mark_apply.rs index 6415bb6a..60f6af23 100644 --- a/src/optimizer/rule/normalization/guard_mark_apply.rs +++ b/src/optimizer/rule/normalization/guard_mark_apply.rs @@ -12,19 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::strip_alias; use crate::catalog::ColumnRef; use crate::errors::DatabaseError; use crate::expression::visitor::ExprVisitor; -use crate::expression::{AliasType, BinaryOperator, ScalarExpression, TypeCast, UnaryOperator}; +use crate::expression::{BinaryOperator, ScalarExpression, TypeCast, UnaryOperator}; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; +use crate::optimizer::plan_utils::{only_child, only_child_mut}; use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::Operator; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; use crate::types::evaluator::BinaryEvaluatorRef; use crate::types::value::DataValue; use crate::types::LogicalType; +use std::iter::successors; pub struct GuardMarkApply; @@ -35,16 +36,18 @@ impl NormalizationRule for GuardMarkApply { arena: &mut PlanArena, _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { - let (Operator::Project(project), Childrens::Only(filter)) = - (&plan.operator, plan.childrens.as_mut()) - else { + if !matches!(plan.operator, Operator::Project(_)) { + return Ok(false); + } + let Some(filter) = skip_row_order(plan) else { return Ok(false); }; let (Operator::Filter(filter_op), Childrens::Only(mark)) = - (&filter.operator, filter.childrens.as_mut()) + (&filter.operator, filter.childrens.as_ref()) else { return Ok(false); }; + let predicate = filter_op.predicate; let mut markers = Vec::new(); let mut input = mark.as_ref(); while let (Operator::MarkApply(op), Childrens::Twins { left, .. }) = @@ -59,13 +62,18 @@ impl NormalizationRule for GuardMarkApply { if markers.is_empty() { return Ok(false); } - // No marker in this segment may escape through the projection. - for expr in &project.exprs { - if expr.any_referenced_column(arena, |arena, column| { - markers - .iter() - .any(|marker| arena.same_column(*column, *marker)) - })? { + // No marker in this segment may be read above the filter. + let segment = successors(Some(&*plan), |node| only_child(node)) + .take_while(|node| !matches!(node.operator, Operator::Filter(_))); + for node in segment { + if !node + .operator + .visit_referenced_columns(arena, &mut |arena, column| { + !markers + .iter() + .any(|marker| arena.same_column(*column, *marker)) + })? + { return Ok(false); } } @@ -99,7 +107,9 @@ impl NormalizationRule for GuardMarkApply { } let mut changed = false; let mut guards = Vec::new(); - let mut input = mark.as_mut(); + let Some(mut input) = mark_below_filter(plan) else { + return Ok(false); + }; for (index, marker) in markers.iter().enumerate() { let (Operator::MarkApply(op), Childrens::Twins { left, .. }) = (&mut input.operator, input.childrens.as_mut()) @@ -118,7 +128,7 @@ impl NormalizationRule for GuardMarkApply { marker: *marker, guards: &mut guards, }; - visitor.visit(filter_op.predicate, arena)?; + visitor.visit(predicate, arena)?; let mut guard = None; for &(sibling, when_true) in &guards { let skip = decisive_condition(sibling, when_true, unavailable, arena)?; @@ -146,6 +156,26 @@ impl NormalizationRule for GuardMarkApply { } } +// Sort/Limit/TopK between the projection and the filter only reorder or cut rows. +fn skip_row_order(plan: &LogicalPlan) -> Option<&LogicalPlan> { + let mut child = only_child(plan)?; + while matches!( + child.operator, + Operator::Sort(_) | Operator::Limit(_) | Operator::TopK(_) + ) { + child = only_child(child)?; + } + Some(child) +} + +fn mark_below_filter(plan: &mut LogicalPlan) -> Option<&mut LogicalPlan> { + let mut node = plan; + while !matches!(node.operator, Operator::Filter(_)) { + node = only_child_mut(node)?; + } + only_child_mut(node) +} + struct GuardVisitor<'a> { marker: ColumnRef, guards: &'a mut Vec<(ExprRef, bool)>, @@ -174,21 +204,6 @@ impl ExprVisitor> for GuardVisitor<'_> { )) } - fn visit_alias( - &mut self, - expr: ExprRef, - alias: &AliasType, - arena: &PlanArena, - ) -> Result<(), DatabaseError> { - self.visit( - match alias { - AliasType::Expr(expr) => *expr, - AliasType::Name(_) => expr, - }, - arena, - ) - } - fn visit_binary( &mut self, op: &BinaryOperator, @@ -237,7 +252,7 @@ fn decisive_condition( return Ok(Some(if when_true { expr } else { negate(expr, arena) })); } } - match arena.expression(strip_alias(expr, arena)).clone() { + match arena.expression(expr.unpack_alias(arena)).clone() { ScalarExpression::Binary { op: op @ (BinaryOperator::And | BinaryOperator::Or), left_expr, @@ -335,7 +350,7 @@ impl ExprVisitor> for SimpleGuardVisitor { if !self.valid { return Ok(false); } - let unaliased = strip_alias(expr, arena); + let unaliased = expr.unpack_alias(arena); if unaliased != expr { self.visit(unaliased, arena)?; return Ok(false); @@ -437,6 +452,7 @@ impl ExprVisitor> for SimpleGuardVisitor { mod tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc}; + use crate::expression::AliasType; use crate::optimizer::rule::normalization::compilation_in_advance::EvaluatorBind; use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::mark_apply::MarkApplyOperator; @@ -1025,8 +1041,8 @@ mod tests { ty: LogicalType::Boolean, }); let alias = arena.alloc_expression(ScalarExpression::Alias { - expr: number, - alias: AliasType::Expr(fixture.flag), + expr: fixture.flag, + alias: AliasType::Expr(number), }); let not_number = arena.alloc_expression(ScalarExpression::Unary { op: UnaryOperator::Not, @@ -1054,15 +1070,7 @@ mod tests { assert_case(not_flag, true)?; assert_case(alias, true)?; assert_case(nested, false)?; - // Passing the whitelist does not waive the rule's left-input dependency check. let predicate = fixture.predicate(); - let ScalarExpression::Binary { left_expr, .. } = &mut *arena.expression_mut(predicate) - else { - unreachable!() - }; - *left_expr = alias; - assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); - assert!(fixture.mark().guard.is_none()); let alias = arena.alloc_expression(ScalarExpression::Alias { expr: fixture.flag, alias: AliasType::Name("flag_alias".to_string()), diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index ea191a4e..298ab5c0 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -14,7 +14,6 @@ use crate::errors::DatabaseError; use crate::expression::visitor_mut::ExprVisitorMut; -use crate::expression::{AliasType, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::rule::normalization::column_pruning::ColumnPruning; @@ -275,20 +274,6 @@ impl NormalizationRule for NormalizationRuleImpl { } } -pub(crate) fn strip_alias(expr: ExprRef, arena: &A) -> ExprRef { - match arena.expression(expr) { - ScalarExpression::Alias { - expr, - alias: AliasType::Name(_), - } => strip_alias(*expr, arena), - ScalarExpression::Alias { - alias: AliasType::Expr(alias_expr), - .. - } => strip_alias(*alias_expr, arena), - _ => expr, - } -} - pub(crate) fn remap_position(position: &mut usize, removed_positions: &[usize]) { match removed_positions.binary_search(position) { Ok(_) => { @@ -339,18 +324,6 @@ impl ExprVisitorMut for PositionRemapper<'_, '_> { remap_position(position, self.removed_positions); Ok(()) } - - fn visit_alias( - &mut self, - expr: &mut ExprRef, - alias: &mut AliasType, - arena: &mut A, - ) -> Result<(), DatabaseError> { - match alias { - AliasType::Expr(alias_expr) => self.visit(alias_expr, arena), - AliasType::Name(_) => self.visit(expr, arena), - } - } } pub(crate) fn remap_expr_positions( diff --git a/src/optimizer/rule/normalization/project_move.rs b/src/optimizer/rule/normalization/project_move.rs index 19986755..1960aab2 100644 --- a/src/optimizer/rule/normalization/project_move.rs +++ b/src/optimizer/rule/normalization/project_move.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::expression::function::scala::ScalarFunction; use crate::expression::visitor::ExprVisitor; -use crate::expression::{AliasType, ScalarExpression}; +use crate::expression::ScalarExpression; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::Operator; @@ -42,19 +42,6 @@ impl ExprVisitor for InputUses { Ok(true) } - fn visit_alias( - &mut self, - expr: ExprRef, - alias: &AliasType, - arena: &A, - ) -> Result<(), DatabaseError> { - let expr = match alias { - AliasType::Expr(expr) => *expr, - AliasType::Name(_) => expr, - }; - self.visit(expr, arena) - } - fn visit_scala_function( &mut self, _function: &ScalarFunction, @@ -114,6 +101,7 @@ mod tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc}; use crate::expression::function::scala::ArcScalarFunctionImpl; + use crate::expression::AliasType; use crate::expression::BinaryOperator; use crate::function::upper::Upper; use crate::planner::operator::project::ProjectOperator; @@ -194,8 +182,8 @@ mod tests { alias: AliasType::Name("named".into()), }); let mapped = arena.alloc_expression(ScalarExpression::Alias { - expr: a, - alias: AliasType::Expr(c), + expr: c, + alias: AliasType::Expr(a), }); let mut plan = LogicalPlan::new( Operator::Project(ProjectOperator::new(vec![named, mapped])), diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 72b41d9c..22f3b74c 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -71,6 +71,7 @@ use self::{ use crate::catalog::ColumnRef; use crate::errors::DatabaseError; use crate::expression::visitor::{walk_expr, ExprVisitor}; +use crate::expression::AliasType; use crate::planner::operator::alter_table::change_column::DefaultChange as ColumnDefaultChange; use crate::planner::operator::alter_table::drop_column::DropColumnOperator; use crate::planner::operator::analyze::AnalyzeOperator; @@ -571,6 +572,19 @@ impl Operator { } Ok(()) } + + // The alias target column is still owned by the plan. + fn visit_alias( + &mut self, + expr: ExprRef, + alias: &AliasType, + arena: &A, + ) -> Result<(), DatabaseError> { + if let AliasType::Expr(alias_expr) = alias { + self.visit(*alias_expr, arena)?; + } + self.visit(expr, arena) + } } impl<'operator, A, F> OperatorVisitor<'operator> for ReferencedColumnVisitor<'_, A, F> diff --git a/tests/slt/crdb/join.slt b/tests/slt/crdb/join.slt index ee22890c..41b5820f 100644 --- a/tests/slt/crdb/join.slt +++ b/tests/slt/crdb/join.slt @@ -1211,7 +1211,7 @@ insert into ij_c values (1,1),(2,2); query T explain select a.id,b.id from ij_a a,ij_b b where b.k=a.k and a.v>15 and b.v<250 and a.v (Sort Option: Follow)] Filter (a.v < b.v), Is Having: false [Filter => (Sort Option: Follow)] Inner Join On cast (a.k as Bigint) = b.k [HashJoin => (Sort Option: None)] Filter ((ij_a.v) as (a.v) > 15), Is Having: false [Filter => (Sort Option: Follow)] TableScan ij_a -> [ij_a.id, ij_a.k, ij_a.v] [SeqScan => (Sort Option: None)] Filter ((ij_b.v) as (b.v) < 250), Is Having: false [Filter => (Sort Option: Follow)] TableScan ij_b -> [ij_b.id, ij_b.k, ij_b.v] [SeqScan => (Sort Option: None)] +Projection [a.id, b.id] [Project => (Sort Option: Follow)] Filter (a.v < b.v), Is Having: false [Filter => (Sort Option: Follow)] Inner Join On cast (a.k as Bigint) = b.k [HashJoin => (Sort Option: None)] Projection [(ij_a.id) as (a.id), (ij_a.k) as (a.k), (ij_a.v) as (a.v)] [Project => (Sort Option: Follow)] Filter ((ij_a.v) as (a.v) > 15), Is Having: false [Filter => (Sort Option: Follow)] TableScan ij_a -> [ij_a.id, ij_a.k, ij_a.v] [SeqScan => (Sort Option: None)] Projection [(ij_b.id) as (b.id), (ij_b.k) as (b.k), (ij_b.v) as (b.v)] [Project => (Sort Option: Follow)] Filter ((ij_b.v) as (b.v) < 250), Is Having: false [Filter => (Sort Option: Follow)] TableScan ij_b -> [ij_b.id, ij_b.k, ij_b.v] [SeqScan => (Sort Option: None)] query II select a.id,b.id from ij_a a,ij_b b where b.k=a.k and a.v>15 and b.v<250 and a.v (Sort Option: Follow)] Recursive CTE: [recursive_cte.value] Projection [(1) as (recursive_cte.value)] [Project => (Sort Option: Follow)] Projection [1] [Project => (Sort Option: Follow)] Dummy [Dummy => (Sort Option: None)] Projection [(recursive_cte.value + 1)] [Project => (Sort Option: Follow)] Filter (recursive_cte.value < 3), Is Having: false [Filter => (Sort Option: Follow)] Recursive Scan: [recursive_cte.value] +Projection [recursive_cte.value] [Project => (Sort Option: Follow)] Recursive CTE: [recursive_cte.value] Projection [(1) as (recursive_cte.value)] [Project => (Sort Option: Follow)] Dummy [Dummy => (Sort Option: None)] Projection [(recursive_cte.value + 1)] [Project => (Sort Option: Follow)] Filter (recursive_cte.value < 3), Is Having: false [Filter => (Sort Option: Follow)] Recursive Scan: [recursive_cte.value] query I WITH RECURSIVE recursive_cte(value) AS ( diff --git a/tests/slt/join.slt b/tests/slt/join.slt index a36a325b..de0a8aa7 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -262,7 +262,7 @@ insert into lookup_inner values (1,10,7),(2,10,9),(3,20,3),(4,null,8); query T explain select lookup_outer.id,lookup_inner.id from lookup_outer join lookup_inner on lookup_outer.k=lookup_inner.k; ---- -Projection [lookup_outer.id, lookup_inner.id] [Project => (Sort Option: Follow)] InnerJoinApply TableScan lookup_outer -> [lookup_outer.id, lookup_outer.k] [SeqScan => (Sort Option: None)] TableScan lookup_inner -> [lookup_inner.id, lookup_inner.k] [IndexScan By lookup_inner_k => Probe ? => (Sort Option: OrderBy: (lookup_inner.k Asc Nulls Last) ignore_prefix_len: 0)] +Projection [lookup_outer.id, lookup_inner.id] [Project => (Sort Option: Follow)] InnerJoinApply [MarkApply => (Sort Option: Follow)] TableScan lookup_outer -> [lookup_outer.id, lookup_outer.k] [SeqScan => (Sort Option: None)] TableScan lookup_inner -> [lookup_inner.id, lookup_inner.k] [IndexScan By lookup_inner_k => Probe ? => (Sort Option: OrderBy: (lookup_inner.k Asc Nulls Last) ignore_prefix_len: 0)] # Resume the same non-unique probe across calls, then rebuild after NULL and # missing keys. In particular, NULL = NULL must not produce the inner NULL row. diff --git a/tests/slt/subquery.slt b/tests/slt/subquery.slt index 81c9c931..677e4fe0 100644 --- a/tests/slt/subquery.slt +++ b/tests/slt/subquery.slt @@ -1225,6 +1225,16 @@ select id,b in (select x.b from corr_rows x where x.id=1) as first_match,first_m ---- 1 true true +# A marker exposed by a derived table is read above the filter, so it cannot be guarded. +query IT +select id,m from (select id,b in (select x.b from corr_rows x where x.id=1) as m from corr_rows) where id>0 or m order by id; +---- +1 true +2 false +3 false +4 NULL +5 false + statement error aggregate functions are not allowed in WHERE select count(*) in (select x.id from corr_rows x) as matched from corr_rows where matched; @@ -1239,6 +1249,16 @@ query I select id from corr_rows where id<0 and (b in (select cast(txt as int) from guard_bad_cast) or b in (select cast(txt as int) from guard_bad_cast)); ---- +# An outer filter merged through a derived table also guards the inner Apply. +query I +select id from (select id,b from corr_rows where b in (select cast(txt as int) from guard_bad_cast)) x where x.id<0; +---- + +# Sort between the projection and the filter does not hide the guard. +query I +select id from corr_rows where id<0 and b in (select cast(txt as int) from guard_bad_cast) order by id; +---- + query I select id from corr_rows where id>0 or (b in (select cast(txt as int) from guard_bad_cast) and b in (select cast(txt as int) from guard_bad_cast)) order by id; ---- diff --git a/tests/slt/where_by_index_explain.slt b/tests/slt/where_by_index_explain.slt index 60d39041..fc1f0452 100644 --- a/tests/slt/where_by_index_explain.slt +++ b/tests/slt/where_by_index_explain.slt @@ -23,7 +23,7 @@ analyze table t1; query T explain select a.c2, b.c2 from t1 a join t1 b on a.id + 3 = b.id where a.id = 3 and b.id = 6; ---- -Projection [a.c2, b.c2] [Project => (Sort Option: Follow)] Inner Join Where ((a.id + 3) = b.id) [NestLoopJoin => (Sort Option: None)] TableScan t1 -> [t1.id, t1.c2] [IndexScan By pk_index => 3 => (Sort Option: OrderBy: (t1.id Asc Nulls Last) ignore_prefix_len: 0)] TableScan t1 -> [t1.id, t1.c2] [IndexScan By pk_index => 6 => (Sort Option: OrderBy: (t1.id Asc Nulls Last) ignore_prefix_len: 0)] +Projection [a.c2, b.c2] [Project => (Sort Option: Follow)] Inner Join Where ((a.id + 3) = b.id) [NestLoopJoin => (Sort Option: None)] Projection [(t1.id) as (a.id), (t1.c2) as (a.c2)] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.id, t1.c2] [IndexScan By pk_index => 3 => (Sort Option: OrderBy: (t1.id Asc Nulls Last) ignore_prefix_len: 0)] Projection [(t1.id) as (b.id), (t1.c2) as (b.c2)] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.id, t1.c2] [IndexScan By pk_index => 6 => (Sort Option: OrderBy: (t1.id Asc Nulls Last) ignore_prefix_len: 0)] query II select a.c2, b.c2 from t1 a join t1 b on a.id + 3 = b.id where a.id = 3 and b.id = 6; From 4ff994de6bb4e52c6f106663f9a3e76d14415ad2 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 24/62] perf: reuse schema caches and track join output positions - Invalidate schema caches along rewrite paths without clearing untouched subtrees, and preallocate concatenated join schemas. - Restore join output using tracked column positions instead of schema lookups, sharing projection construction with nested-loop orientation. - Remove the unused child replacement helper and streamline reorder tests. --- src/optimizer/core/rule.rs | 2 + src/optimizer/heuristic/optimizer.rs | 3 +- src/optimizer/plan_utils.rs | 21 +- .../rule/normalization/join_reorder.rs | 293 +++++------------- .../rule/normalization/pushdown_limit.rs | 1 + src/planner/mod.rs | 175 ++++++++++- 6 files changed, 261 insertions(+), 234 deletions(-) diff --git a/src/optimizer/core/rule.rs b/src/optimizer/core/rule.rs index b53e4237..e6550b47 100644 --- a/src/optimizer/core/rule.rs +++ b/src/optimizer/core/rule.rs @@ -28,6 +28,8 @@ pub trait MatchPattern { pub trait NormalizationRule { /// Returns true when the plan tree is modified. + /// Local rewrites must invalidate changed descendants before reading their schemas. + /// The optimizer invalidates the rewrite root and its ancestors, not untouched subtrees. fn apply( &self, plan: &mut LogicalPlan, diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index cfbfcdb2..ef997448 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -458,7 +458,8 @@ impl<'a> HepOptimizer<'a> { *done = true; } applied = true; - plan.reset_output_schema_cache_recursive(); + // Rules invalidate changed descendants; untouched subtrees retain their caches. + plan.reset_output_schema_cache(); } } Ok(applied) diff --git a/src/optimizer/plan_utils.rs b/src/optimizer/plan_utils.rs index fea246d1..ad94842c 100644 --- a/src/optimizer/plan_utils.rs +++ b/src/optimizer/plan_utils.rs @@ -102,18 +102,11 @@ pub fn replace_with_only_child(plan: &mut LogicalPlan) -> bool { } } -#[allow(dead_code)] -pub fn replace_child_with_only_child(plan: &mut LogicalPlan, child_idx: usize) -> bool { - if let Some(child_plan) = child_mut(plan, child_idx) { - return replace_with_only_child(child_plan); - } - false -} - pub fn wrap_child_with(plan: &mut LogicalPlan, child_idx: usize, operator: Operator) -> bool { if let Some(slot) = child_mut(plan, child_idx) { let previous = mem::replace(slot, LogicalPlan::new(operator, Childrens::None)); *slot.childrens = Childrens::Only(Box::new(previous)); + plan.reset_output_schema_cache(); true } else { false @@ -322,20 +315,10 @@ mod tests { let mut parent = LogicalPlan::new( Operator::Dummy, Childrens::Twins { - left: Box::new(LogicalPlan::new( - Operator::ShowView, - Childrens::Only(Box::new(leaf(Operator::ShowTable))), - )), + left: Box::new(leaf(Operator::ShowTable)), right: Box::new(leaf(Operator::Dummy)), }, ); - assert!(replace_child_with_only_child(&mut parent, 0)); - assert!(matches!( - child(&parent, 0).unwrap().operator, - Operator::ShowTable - )); - assert!(!replace_child_with_only_child(&mut parent, 1)); - assert!(!replace_child_with_only_child(&mut parent, 2)); assert!(wrap_child_with(&mut parent, 1, Operator::ShowView)); let wrapped = child(&parent, 1).unwrap(); diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 7d21208d..ea38e086 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -91,6 +91,7 @@ impl NormalizationRule for OrientNestedLoopJoin { if !should_swap { return Ok(false); } + let left_len = left.output_schema(arena).len(); let original_schema = plan.output_schema(arena).clone(); for (index, column) in original_schema.iter().enumerate() { if original_schema[index + 1..] @@ -107,28 +108,27 @@ impl NormalizationRule for OrientNestedLoopJoin { swap(left, right); join.reset_output_schema_cache(); join.clear_startup_work(); - *plan = restore_join_output(join, original_schema, arena); + let right_len = original_schema.len() - left_len; + let positions = (right_len..original_schema.len()).chain(0..right_len); + *plan = project_join_output(join, original_schema, positions, arena); Ok(true) } } -fn restore_join_output( - mut join: LogicalPlan, +fn project_join_output( + join: LogicalPlan, original_schema: Schema, + positions: impl Iterator + Clone, arena: &mut PlanArena, ) -> LogicalPlan { - let schema = join.output_schema(arena); - if &original_schema == schema { + if positions.clone().eq(0..original_schema.len()) { return join; } let exprs = original_schema - .iter() - .map(|column| { - let position = schema - .iter() - .position(|candidate| arena.same_column(*candidate, *column)) - .unwrap(); - arena.alloc_expression(ScalarExpression::column_expr(*column, position)) + .into_iter() + .zip(positions) + .map(|(column, position)| { + arena.alloc_expression(ScalarExpression::column_expr(column, position)) }) .collect(); let mut plan = LogicalPlan::new( @@ -627,6 +627,11 @@ impl NormalizationRule for GreedyJoinReorder { if input_count < 3 || (!estimated && !cross_join) { return Ok(false); } + // Each original column keeps its input bit and position within its current join. + let mut positions = Vec::with_capacity(original_schema.len()); + for (input, schema) in schemas.iter().enumerate() { + positions.extend((0..schema.len()).map(|position| (1u64 << input, position))); + } let mut inputs = Vec::with_capacity(schemas.len()); flatten(plan.take(), &mut inputs); let mut row_cache = HashMap::new(); @@ -723,6 +728,11 @@ impl NormalizationRule for GreedyJoinReorder { let (mut left, _, left_mask) = inputs.remove(i); let left_schema = left.output_schema(arena); let right_schema = right.output_schema(arena); + for (input, position) in &mut positions { + if *input & right_mask != 0 { + *position += left_schema.len(); + } + } let mut on = keys.take(left_mask, right_mask); for (l, r) in &mut on { *l = l.clone_expression(arena)?; @@ -757,7 +767,12 @@ impl NormalizationRule for GreedyJoinReorder { } // SAFETY: At this point, there should be exactly one plan left in the inputs vector, which is the final join result. let (join, _, _) = inputs.pop().unwrap(); - *plan = restore_join_output(join, original_schema, arena); + *plan = project_join_output( + join, + original_schema, + positions.iter().map(|(_, position)| *position), + arena, + ); Ok(true) } } @@ -880,23 +895,49 @@ mod tests { } #[test] - fn parameterized_join_work_keeps_input_and_probe_cost() -> Result<(), DatabaseError> { + fn greedy_tracks_positions_across_unequal_width_inputs() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); - let mut plan = parameterized_input(12, 13, &mut arena); - assert_eq!(plan.estimated_startup_work()?, Some(0)); - assert_eq!(plan.estimated_work()?, Some(48 + 12 + 52)); - assert_eq!(plan.estimated_rows(), Some(13)); - let Childrens::Twins { right, .. } = plan.childrens.as_ref() else { - unreachable!() - }; - assert_eq!(right.estimated_rows(), None); - - let Childrens::Twins { left, .. } = plan.childrens.as_mut() else { - unreachable!() + let (mut a, a_key) = input("a", 100, &mut arena); + let (b, _) = input("b", 1, &mut arena); + let (mut c, c_key) = input("c", 10, &mut arena); + for (plan, key, width, table) in [(&mut a, a_key, 2, "a"), (&mut c, c_key, 3, "c")] { + let column = key.output_column_ref(&mut arena); + let column = arena.column(column).clone(); + let Operator::TableScan(scan) = &mut plan.operator else { + panic!("expected a scan"); + }; + for index in 1..width { + let mut extra = column.clone(); + extra.summary_mut().name = format!("{table}{index}"); + extra.set_ref_table(table.into(), index, false); + scan.columns.push(arena.alloc_column(extra)); + } + } + let left = JoinOperator::build(a, b, JoinCondition::None, JoinType::Cross, false); + let mut plan = JoinOperator::build( + left, + c, + JoinCondition::On { + on: vec![(a_key, c_key)], + filter: None, + }, + JoinType::Inner, + false, + ); + let original_schema = plan.output_schema(&mut arena).clone(); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.output_schema(&mut arena), &original_schema); + let Operator::Project(project) = &plan.operator else { + panic!("expected a projection restoring A, B, C output order"); }; - left.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; - assert_eq!(plan.estimated_work()?, None); + assert_eq!(project.exprs.len(), 6); + for (expr, position) in project.exprs.iter().zip([0, 1, 5, 2, 3, 4]) { + assert!(matches!( + arena.expression(*expr), + ScalarExpression::ColumnRef { position: actual, .. } if *actual == position + )); + } Ok(()) } @@ -917,6 +958,16 @@ mod tests { assert!(OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); assert_eq!(plan.estimated_rows(), Some(156)); + let Operator::Project(project) = &plan.operator else { + panic!("expected a projection restoring the two swapped inputs"); + }; + assert_eq!(project.exprs.len(), 3); + for (expr, position) in project.exprs.iter().zip([2, 0, 1]) { + assert!(matches!( + arena.expression(*expr), + ScalarExpression::ColumnRef { position: actual, .. } if *actual == position + )); + } let Childrens::Only(join) = plan.childrens.as_ref() else { unreachable!() }; @@ -978,7 +1029,7 @@ mod tests { } #[test] - fn join_keys_share_on_ids_and_move_vectors_without_copy() -> Result<(), DatabaseError> { + fn join_keys_preserve_conditions_across_merges() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let (_, a) = input("a", 10, &mut arena); @@ -994,13 +1045,6 @@ mod tests { keys.insert(1, 2, (a, b)); keys.insert(2, 8, (b, d)); keys.insert(4, 8, (c, d)); - assert_eq!(keys.links.len(), 10); - assert_eq!(keys.on_keys.len(), 5); - let ac = keys.links[&(1, 4)]; - assert_eq!(keys.links[&(4, 1)], ac); - assert_eq!(keys.on_keys[ac], vec![(a, c), (alias, c), (a, c)]); - let bd = keys.links[&(2, 8)]; - let bd_data = keys.on_keys[bd].as_ptr(); assert_eq!( keys.pairs().collect::>(), vec![(1, 2), (1, 4), (2, 4), (2, 8), (4, 8)] @@ -1010,10 +1054,7 @@ mod tests { vec![(a, c), (alias, c), (a, c)] ); assert!(keys.get(1, 8).next().is_none()); - let ab_data = keys.on_keys[keys.links[&(1, 2)]].as_ptr(); - let on = keys.take(1, 2); - assert_eq!(on, vec![(a, b)]); - assert_eq!(on.as_ptr(), ab_data); + assert_eq!(keys.take(1, 2), vec![(a, b)]); assert!(keys.links.contains_key(&(1, 4))); keys.rewrite(1, 2); assert_eq!( @@ -1021,8 +1062,6 @@ mod tests { vec![(a, c), (alias, c), (a, c), (b, c)] ); assert_eq!(keys.get(3, 8).collect::>(), vec![(b, d)]); - assert_eq!(keys.links[&(3, 8)], bd); - assert_eq!(keys.on_keys[bd].as_ptr(), bd_data); assert!(keys.get(1, 4).next().is_none()); assert!(keys.get(1, 2).next().is_none()); assert_eq!(keys.take(4, 3), vec![(c, a), (c, alias), (c, a), (c, b)]); @@ -1030,74 +1069,11 @@ mod tests { assert_eq!(keys.take(8, 7), vec![(d, c), (d, b)]); keys.rewrite(8, 7); assert!(keys.links.is_empty()); - assert!(keys.on_keys.iter().all(Vec::is_empty)); Ok(()) } #[test] - fn inspect_inserts_keys_directly_and_keeps_duplicate_conditions() -> Result<(), DatabaseError> { - let table_arena = TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - let (a, ak) = input("a", 1, &mut arena); - let (b, bk) = input("b", 1, &mut arena); - let (c, ck) = input("c", 1, &mut arena); - let ab = JoinOperator::build( - a, - b, - JoinCondition::On { - on: vec![(ak, bk), (ak, bk)], - filter: None, - }, - JoinType::Inner, - false, - ); - let mut plan = JoinOperator::build( - ab, - c, - JoinCondition::On { - on: vec![(ak, bk), (bk, ck)], - filter: None, - }, - JoinType::Inner, - false, - ); - let mut schemas = Vec::new(); - let mut keys = JoinKeys::default(); - assert!(inspect(&mut plan, &mut schemas, &mut keys, &mut arena, None)?.is_some()); - assert_eq!(keys.links.len(), 4); - assert_eq!(keys.links[&(1, 2)], keys.links[&(2, 1)]); - assert_eq!(keys.on_keys.len(), 2); - assert_eq!(keys.take(1, 2), vec![(ak, bk); 3]); - Ok(()) - } - - #[test] - fn link_prefixes_update_incoming_edges_without_copying_on() { - let (a, b, c) = (ExprRef::new(0), ExprRef::new(1), ExprRef::new(2)); - let mut keys = JoinKeys::default(); - keys.insert(1, 8, (a, b)); - keys.insert(8, 16, (b, c)); - let id = keys.links[&(1, 8)]; - let data = keys.on_keys[id].as_ptr(); - assert_eq!(keys.take(8, 16), vec![(b, c)]); - assert!(keys.links.contains_key(&(1, 8))); - assert!(!keys.links.contains_key(&(1, 24))); - keys.rewrite(8, 16); - assert_eq!(keys.links[&(1, 24)], id); - assert_eq!(keys.links[&(24, 1)], id); - assert_eq!(keys.on_keys[id].as_ptr(), data); - assert_eq!(keys.get(24, 1).collect::>(), vec![(a, b)]); - assert_eq!(keys.pairs().collect::>(), vec![(1, 24)]); - assert!(keys - .links - .keys() - .all(|(l, r)| ![8, 16].contains(l) && ![8, 16].contains(r))); - assert_eq!(keys.take(24, 1), vec![(b, a)]); - assert!(keys.links.is_empty()); - } - - #[test] - fn shared_on_id_preserves_order_when_merged_direction_flips() { + fn join_keys_preserve_condition_direction_after_merge() { let (a, b, c, d, e) = ( ExprRef::new(0), ExprRef::new(1), @@ -1109,18 +1085,14 @@ mod tests { keys.insert(2, 4, (b, c)); keys.insert(2, 4, (d, a)); keys.insert(8, 4, (e, c)); - let id = keys.links[&(2, 4)]; assert!(keys.take(2, 8).is_empty()); keys.rewrite(2, 8); - assert_eq!(keys.links[&(10, 4)], id); - assert_eq!(keys.links[&(4, 10)], id); assert_eq!( keys.get(10, 4).collect::>(), vec![(c, b), (a, d), (c, e)] ); assert_eq!(keys.take(10, 4), vec![(b, c), (d, a), (e, c)]); assert!(keys.links.is_empty()); - assert!(keys.on_keys.iter().all(Vec::is_empty)); } #[test] @@ -1205,72 +1177,6 @@ mod tests { Ok(()) } - #[test] - fn column_ndv_skips_unusable_inputs_and_unrelated_keys() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (a, ka) = input("a", 100, &mut arena); - let (b, kb) = input("b", 100, &mut arena); - let (c, kc) = input("c", 100, &mut arena); - let keys = [(ka, kb), (ka, kc)]; - let mut join_keys = JoinKeys::default(); - join_keys.insert(1, 2, (ka, kb)); - join_keys.insert(1, 4, (ka, kc)); - let mut cache = StatisticsMetaCache::default(); - for (name, ndv) in [("a", 10), ("b", 20), ("c", 30)] { - let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; - for value in 0..ndv { - builder.append(&DataValue::Int32(value)); - } - cache.columns.insert((name.into(), 0), builder.build()); - } - let loader = StatisticMetaLoader::new(&cache); - for (left, right, lm, rm, expected) in [(&a, &b, 1, 2, (10, 20)), (&c, &a, 4, 1, (30, 10))] - { - let mut visited = 0; - let ndvs = column_ndv( - left, - right, - join_keys.get(lm, rm).inspect(|_| visited += 1), - Some(&loader), - &arena, - )?; - assert_eq!(ndvs, Some(expected)); - assert_eq!(visited, 1); - } - let mut join = JoinOperator::build( - a, - b, - JoinCondition::On { - on: vec![(ka, kb)], - filter: None, - }, - JoinType::Inner, - false, - ); - join.physical_option = Some(PhysicalOption::new( - PlanImpl::HashJoin, - SortOption::None, - RowCountOption::Estimated { - rows: 100, - ndv: None, - }, - )); - let mut visited = 0; - assert_eq!( - column_ndv( - &join, - &c, - keys.iter().inspect(|_| visited += 1), - Some(&loader), - &arena - )?, - None - ); - assert_eq!(visited, 0); - Ok(()) - } - #[test] fn merged_inputs_do_not_reuse_leaf_pair_row_estimates() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); @@ -2361,22 +2267,6 @@ mod tests { Ok(()) } - #[test] - fn input_mask_search_uses_lowest_bit_and_checks_full_mask() { - let masks = [9u64, 6, 16]; - let find = |mask: u64| { - masks.binary_search_by_key(&(mask.trailing_zeros(), mask), |mask| { - (mask.trailing_zeros(), *mask) - }) - }; - assert_eq!(find(9), Ok(0)); - assert_eq!(find(6), Ok(1)); - assert_eq!(find(16), Ok(2)); - assert!(find(1).is_err()); - assert!(find(2).is_err()); - assert!(find(64).is_err()); - } - #[test] fn edge_order_ties_keep_original_input_order() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); @@ -2741,27 +2631,6 @@ mod tests { let original = plan.clone(); assert!(!GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan, original); - let mut plan = three_inputs(&mut arena); - if let Childrens::Twins { left, right } = plan.childrens.as_mut() { - right.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { - rows: 10, - ndv: None, - }; - if let Childrens::Twins { left, .. } = left.childrens.as_mut() { - left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { - rows: 10, - ndv: None, - }; - } - } - let schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan.output_schema(&mut arena), &schema); - let (left, right) = plan.childrens.twins(); - assert_eq!(left.referenced_table(), vec!["a".into(), "b".into()]); - assert_eq!(right.referenced_table(), vec!["c".into()]); - assert_eq!(usize::MAX.saturating_mul(2), usize::MAX); - assert_eq!(0_usize.saturating_mul(20), 0); Ok(()) } } diff --git a/src/optimizer/rule/normalization/pushdown_limit.rs b/src/optimizer/rule/normalization/pushdown_limit.rs index 76d78c57..9f232dbd 100644 --- a/src/optimizer/rule/normalization/pushdown_limit.rs +++ b/src/optimizer/rule/normalization/pushdown_limit.rs @@ -48,6 +48,7 @@ impl NormalizationRule for LimitProjectTranspose { &mut child.operator, Operator::Limit(limit_op.clone()), )); + child.reset_output_schema_cache(); } } diff --git a/src/planner/mod.rs b/src/planner/mod.rs index becadd54..040ebf60 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -342,8 +342,11 @@ impl LogicalPlan { }, Operator::Join(_) => match childrens { Childrens::Twins { left, right } => { - let mut schema = left.output_schema(arena).clone(); - schema.extend_from_slice(right.output_schema(arena)); + let left_schema = left.output_schema(arena); + let right_schema = right.output_schema(arena); + let mut schema = Vec::with_capacity(left_schema.len() + right_schema.len()); + schema.extend_from_slice(left_schema); + schema.extend_from_slice(right_schema); schema } _ => unreachable!(), @@ -606,8 +609,13 @@ mod tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc, ColumnRef}; use crate::expression::ScalarExpression; + use crate::optimizer::core::rule::NormalizationRule; + use crate::optimizer::heuristic::batch::HepBatchStrategy; + use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; + use crate::optimizer::rule::normalization::NormalizationRuleImpl; use crate::planner::operator::describe::DescribeOperator; use crate::planner::operator::filter::FilterOperator; + use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::sort::{SortField, SortOperator}; @@ -948,6 +956,169 @@ mod tests { assert_eq!(schema_names(&recomputed, &arena), vec!["name"]); } + #[test] + fn transpose_invalidates_changed_child_before_schema_is_read() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let a = column("a", LogicalType::Integer, &mut arena); + let b = column("b", LogicalType::Integer, &mut arena); + let expr = arena.alloc_expression(ScalarExpression::column_expr(a, 0)); + let project = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![expr])), + Childrens::Only(Box::new(table_scan("t", vec![a, b]))), + ); + let mut plan = LimitOperator::build(None, Some(1), project); + plan.populate_output_schema_recursive(&mut arena); + assert_eq!(plan.childrens.only().output_schema.as_ref().unwrap(), &[a]); + + assert!(NormalizationRuleImpl::LimitProjectTranspose.apply(&mut plan, &mut arena, None)?); + let Childrens::Only(limit) = plan.childrens.as_mut() else { + panic!("expected a limit below the projection"); + }; + assert!(limit.output_schema.is_none()); + assert_eq!( + limit.childrens.only().output_schema.as_ref().unwrap(), + &[a, b] + ); + assert_eq!(limit.output_schema(&mut arena), &[a, b]); + assert_eq!(plan.output_schema(&mut arena), &[a]); + Ok(()) + } + + #[test] + fn local_rewrite_invalidates_ancestors_but_preserves_untouched_caches( + ) -> Result<(), DatabaseError> { + for strategy in [ + HepBatchStrategy::once_topdown(), + HepBatchStrategy::once_bottomup(), + ] { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let a = column("a", LogicalType::Integer, &mut arena); + let b = column("b", LogicalType::Integer, &mut arena); + let c = column("c", LogicalType::Integer, &mut arena); + let expr = arena.alloc_expression(ScalarExpression::column_expr(a, 0)); + let project = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![expr])), + Childrens::Only(Box::new(table_scan("left", vec![a, b]))), + ); + let mut plan = JoinOperator::build( + LimitOperator::build(None, Some(1), project), + table_scan("right", vec![c]), + JoinCondition::None, + JoinType::Cross, + false, + ); + plan.populate_output_schema_recursive(&mut arena); + assert_eq!(plan.output_schema.as_ref().unwrap(), &[a, c]); + let pipeline = HepOptimizerPipeline::builder() + .before_batch( + "Transpose", + strategy, + vec![NormalizationRuleImpl::LimitProjectTranspose], + ) + .build(); + let mut plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; + assert!(plan.output_schema.is_none()); + let Childrens::Twins { left, right } = plan.childrens.as_mut() else { + panic!("expected a join"); + }; + assert!(left.output_schema.is_none()); + assert_eq!(right.output_schema.as_ref().unwrap(), &[c]); + let Childrens::Only(limit) = left.childrens.as_mut() else { + panic!("expected a limit below the projection"); + }; + assert!(limit.output_schema.is_none()); + assert_eq!( + limit.childrens.only().output_schema.as_ref().unwrap(), + &[a, b] + ); + assert_eq!(limit.output_schema(&mut arena), &[a, b]); + assert_eq!(plan.output_schema(&mut arena), &[a, c]); + } + Ok(()) + } + + #[test] + fn wrap_child_invalidates_parent_without_clearing_input() { + use crate::optimizer::plan_utils::wrap_child_with; + + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let a = column("a", LogicalType::Integer, &mut arena); + let b = column("b", LogicalType::Integer, &mut arena); + let expr = arena.alloc_expression(ScalarExpression::column_expr(a, 0)); + let mut plan = LimitOperator::build(None, Some(1), table_scan("t", vec![a, b])); + plan.populate_output_schema_recursive(&mut arena); + + assert!(wrap_child_with( + &mut plan, + 0, + Operator::Project(ProjectOperator::new(vec![expr])), + )); + assert!(plan.output_schema.is_none()); + assert!(plan + .childrens + .only() + .childrens + .only() + .output_schema + .is_some()); + assert_eq!(plan.output_schema(&mut arena), &[a]); + } + + #[test] + fn predicate_pushdown_preserves_untouched_subtree_caches() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let a = column("a", LogicalType::Integer, &mut arena); + let b = column("b", LogicalType::Integer, &mut arena); + let c = column("c", LogicalType::Integer, &mut arena); + let left = JoinOperator::build( + table_scan("a", vec![a]), + table_scan("b", vec![b]), + JoinCondition::None, + JoinType::Cross, + false, + ); + let join = JoinOperator::build( + left, + table_scan("c", vec![c]), + JoinCondition::None, + JoinType::Cross, + false, + ); + let c_expr = arena.alloc_expression(ScalarExpression::column_expr(c, 2)); + let zero = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(0))); + let predicate = arena.alloc_expression(ScalarExpression::Binary { + op: crate::expression::BinaryOperator::Gt, + left_expr: c_expr, + right_expr: zero, + evaluator: None, + ty: LogicalType::Boolean, + }); + let mut plan = FilterOperator::build(predicate, join, false); + plan.populate_output_schema_recursive(&mut arena); + let pipeline = HepOptimizerPipeline::builder() + .before_batch( + "Push Predicate", + HepBatchStrategy::once_topdown(), + vec![NormalizationRuleImpl::PushPredicateThroughJoin], + ) + .build(); + let mut plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; + assert!(matches!(plan.operator, Operator::Join(_))); + assert!(plan.output_schema.is_none()); + let (left, right) = plan.childrens.twins(); + assert_eq!(left.output_schema.as_ref().unwrap(), &[a, b]); + assert!(left.childrens.twins().0.output_schema.is_some()); + assert!(left.childrens.twins().1.output_schema.is_some()); + assert!(matches!(right.operator, Operator::Filter(_))); + assert_eq!(right.childrens.only().output_schema.as_ref().unwrap(), &[c]); + assert_eq!(plan.output_schema(&mut arena), &[a, b, c]); + Ok(()) + } + #[test] fn dummy_operators_produce_expected_output_schema() { let table_arena = TableArenaCell::default(); From 010615c0e787616f4336974196a60c8f44578b87 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 25/62] perf: move left rows for unique index joins - Limit InnerJoinApply conversion to complete primary/unique-key point probes. - Move matched rows into the output instead of cloning and rotating the left values, and remove the cross-call join input state. - Keep non-unique index joins on the ordinary join path and cover NULL, residual rejection, repeated probes, and complete composite keys. --- src/execution/dql/mark_apply.rs | 104 +++++++----------- .../rule/normalization/parameterized_index.rs | 65 +++++++++-- src/planner/operator/mark_apply.rs | 1 + tests/slt/join.slt | 24 +++- 4 files changed, 116 insertions(+), 78 deletions(-) diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index 688ca637..eae307b2 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -106,8 +106,6 @@ pub struct MarkApply<'a> { right_root: ExecId, // Temporarily None while scan owns and transitions the right input. right_input: Option, - // Retain a streaming inner input across next_tuple calls, not its result rows. - join_input: Option<(ExecId, Tuple)>, } impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for MarkApply<'a> { @@ -132,7 +130,6 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for MarkApply<'a> { left_input, right_root, right_input: Some(right_input), - join_input: None, })) } } @@ -143,7 +140,6 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { - self.join_input = None; // Materialization already makes the RHS reusable across left rows. // Keep both complete caches and paused prefixes; only reset the left input. if !self.op.materialize || self.right_input.is_none() { @@ -206,31 +202,28 @@ impl<'a> MarkApply<'a> { arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { - loop { - if let Some((root, left)) = &mut self.join_input { - while arena.next_tuple(*root, plan_arena)? { - let right = arena.result_tuple(); - if Self::predicates_matched(self.op.predicates(), left, right, plan_arena)? { - let output = arena.result_tuple_mut(); - let right_len = output.values.len(); - output.values.extend(left.values.iter().cloned()); - output.values.rotate_left(right_len); - output.pk = left.pk.clone().or(output.pk.take()); - arena.resume(); - return Ok(()); - } - } - } - if !arena.next_tuple(self.left_input, plan_arena)? { - self.join_input = None; - arena.finish(); - return Ok(()); - } - let left = arena.materialize_tuple(); + while arena.next_tuple(self.left_input, plan_arena)? { + let mut left = arena.materialize_tuple(); let value = self.parameterized_probe_value(&left, plan_arena)?; let root = input.reset(arena, plan_arena, self.runtime_probe_for(value))?; - self.join_input = Some((root, left)); + // A full unique-key probe cannot have another match, so consume the left row. + if arena.next_tuple(root, plan_arena)? + && Self::predicates_matched( + self.op.predicates(), + &left, + arena.result_tuple(), + plan_arena, + )? + { + let right = arena.result_tuple_mut(); + left.values.append(&mut right.values); + left.pk = left.pk.or(right.pk.take()); + arena.produce_tuple(left); + return Ok(()); + } } + arena.finish(); + Ok(()) } fn runtime_probe_for(&self, param_value: Option) -> Option { @@ -566,7 +559,7 @@ mod tests { use crate::types::evaluator::{binary_create, cast_create}; use crate::types::index::RuntimeIndexProbe; use crate::types::tuple::Tuple; - use crate::types::LogicalType; + use crate::types::{CharLengthUnits, LogicalType}; use std::borrow::Cow; use tempfile::TempDir; @@ -656,17 +649,23 @@ mod tests { } #[test] - fn inner_join_apply_emits_all_matches_and_reuses_right_slots() -> Result<(), DatabaseError> { + fn inner_join_apply_moves_single_match_and_reuses_right_slots() -> Result<(), DatabaseError> { let table_arena = crate::planner::TableArenaCell::default(); let mut plan_arena = crate::planner::PlanArena::new(&table_arena); - let mut left = build_values( + let mut left = build_values_with_schema( &mut plan_arena, - "left_key", vec![ - vec![DataValue::Int32(2)], - vec![DataValue::Null], - vec![DataValue::Int32(99)], - vec![DataValue::Int32(2)], + ("left_key", LogicalType::Integer), + ( + "payload", + LogicalType::Varchar(None, CharLengthUnits::Characters), + ), + ], + vec![ + vec![DataValue::Int32(2), DataValue::from("first".to_string())], + vec![DataValue::Null, DataValue::from("null".to_string())], + vec![DataValue::Int32(99), DataValue::from("missing".to_string())], + vec![DataValue::Int32(2), DataValue::from("again".to_string())], ], ); let mut right = build_values_with_schema( @@ -675,23 +674,17 @@ mod tests { ("right_key", LogicalType::Integer), ("flag", LogicalType::Boolean), ], - vec![ - vec![DataValue::Int32(2), DataValue::Boolean(true)], - vec![DataValue::Int32(2), DataValue::Boolean(false)], - vec![DataValue::Int32(2), DataValue::Null], - vec![DataValue::Int32(2), DataValue::Boolean(true)], - vec![DataValue::Null, DataValue::Boolean(true)], - ], + vec![vec![DataValue::Int32(2), DataValue::Boolean(true)]], ); let left_column = left.output_schema(&mut plan_arena)[0]; let right_schema = right.output_schema(&mut plan_arena).clone(); let equality = - build_equality_predicate(&mut plan_arena, left_column, 0, right_schema[0], 1)?; + build_equality_predicate(&mut plan_arena, left_column, 0, right_schema[0], 2)?; let residual = - plan_arena.alloc_expression(ScalarExpression::column_expr(right_schema[1], 2)); + plan_arena.alloc_expression(ScalarExpression::column_expr(right_schema[1], 3)); let probe = plan_arena.alloc_expression(ScalarExpression::column_expr(left_column, 0)); let mut op = MarkApplyOperator::new_inner_join(vec![equality, residual], probe); - // Values has no parameterized IndexScan to consume a runtime probe. + // A single Values row stands in for the unique lookup, without a runtime probe. op.set_parameterized_probe(None); let (table_cache, view_cache, meta_cache, _temp_dir, storage) = build_test_storage()?; let transaction = storage.transaction()?; @@ -709,12 +702,13 @@ mod tests { let count = arena.nodes.items.len(); assert_eq!(count, 3); let mut plan_arena = ExecMetaArena::new(plan_arena); - for _ in 0..4 { + for payload in ["first", "again"] { assert!(arena.next_tuple(root, &mut plan_arena)?); assert_eq!( arena.materialize_tuple().values, vec![ DataValue::Int32(2), + DataValue::from(payload.to_string()), DataValue::Int32(2), DataValue::Boolean(true) ] @@ -727,18 +721,14 @@ mod tests { } #[test] - fn inner_join_apply_does_not_read_past_the_returned_match() -> Result<(), DatabaseError> { + fn inner_join_apply_skips_a_rejected_unique_match() -> Result<(), DatabaseError> { let table_arena = crate::planner::TableArenaCell::default(); let mut plan_arena = crate::planner::PlanArena::new(&table_arena); let mut left = build_values(&mut plan_arena, "left_key", vec![vec![DataValue::Int32(1)]]); let mut right = build_values_with_schema( &mut plan_arena, vec![("flag", LogicalType::Boolean)], - // A later row that cannot be cast must not fail the first fetch. - vec![ - vec![DataValue::Boolean(true)], - vec![DataValue::from("not-a-boolean".to_string())], - ], + vec![vec![DataValue::Boolean(false)]], ); let left_column = left.output_schema(&mut plan_arena)[0]; let right_column = right.output_schema(&mut plan_arena)[0]; @@ -754,11 +744,7 @@ mod tests { plan_arena, &transaction, ); - assert_eq!( - executor.next_tuple()?.unwrap().values, - vec![DataValue::Int32(1), DataValue::Boolean(true)] - ); - assert!(executor.next_tuple().is_err()); + assert!(executor.next_tuple()?.is_none()); Ok(()) } @@ -926,7 +912,6 @@ mod tests { op: &op, right_input: Some(RightInput::Streaming { root: right_root }), left_input: 0, - join_input: None, }; let left_tuple = Tuple::new(None, vec![DataValue::Int32(2), DataValue::Int32(1)]); @@ -984,7 +969,6 @@ mod tests { op: &op, right_input: Some(RightInput::Streaming { root: right_root }), left_input: 0, - join_input: None, }; let left_tuple = Tuple::new(None, vec![DataValue::Int32(2)]); @@ -1042,7 +1026,6 @@ mod tests { op: &op, right_input: Some(RightInput::Streaming { root: right_root }), left_input: 0, - join_input: None, }; let left_tuple = Tuple::new(None, vec![DataValue::Null]); @@ -1182,7 +1165,6 @@ mod tests { op: &op, left_input, right_input: Some(right_input), - join_input: None, }; let mut meta = ExecMetaArena::new(meta); let result = exec.next_tuple(&mut arena, &mut meta); @@ -1266,7 +1248,6 @@ mod tests { op: &op, left_input, right_input: Some(right_input), - join_input: None, }; let mut meta = ExecMetaArena::new(meta); for (index, (key, flag, marker)) in cases.iter().enumerate() { @@ -1339,7 +1320,6 @@ mod tests { op: &op, right_input: Some(RightInput::Materialize { root: right_root }), left_input, - join_input: None, }; let mut meta = ExecMetaArena::new(meta); for value in [ diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 8c8054a0..65aca773 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -308,6 +308,9 @@ fn parameterize( (Operator::TableScan(scan), _) if scan.limit == (None, None) => { 'indexes: for info in &mut scan.index_infos { let meta = arena.index(info.meta); + if !matches!(meta.ty, IndexType::PrimaryKey { .. } | IndexType::Unique) { + continue; + } let mut probe = Vec::with_capacity(meta.column_ids.len()); let mut uses_join_key = false; 'columns: for id in &meta.column_ids { @@ -705,6 +708,24 @@ mod inner_join_tests { ) } + fn unique_scan( + arena: &mut PlanArena<'_>, + name: &str, + types: &[LogicalType], + indexes: &[&[usize]], + ) -> LogicalPlan { + let mut plan = scan(arena, name, types, indexes); + let Operator::TableScan(scan) = &mut plan.operator else { + unreachable!() + }; + for info in &mut scan.index_infos { + let mut meta = arena.index(info.meta).clone(); + meta.ty = IndexType::Unique; + info.meta = arena.alloc_index(meta); + } + plan + } + fn column(plan: &LogicalPlan, position: usize, arena: &mut PlanArena<'_>) -> ExprRef { let Operator::TableScan(scan) = &plan.operator else { panic!("expected scan") @@ -928,7 +949,7 @@ mod inner_join_tests { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let mut left = scan(&mut arena, "outer", &[LogicalType::Integer], &[]); - let mut right = scan(&mut arena, "inner", &[LogicalType::Integer], &[&[0]]); + let mut right = unique_scan(&mut arena, "inner", &[LogicalType::Integer], &[&[0]]); let l = column(&left, 0, &mut arena); let r = column(&right, 0, &mut arena); left.physical_option = Some(PhysicalOption::new( @@ -979,7 +1000,7 @@ mod inner_join_tests { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let mut left = scan(&mut arena, "outer", &[LogicalType::Integer], &[]); - let mut right = scan(&mut arena, "inner", &[LogicalType::Integer], &[&[0]]); + let mut right = unique_scan(&mut arena, "inner", &[LogicalType::Integer], &[&[0]]); let l = column(&left, 0, &mut arena); let r = column(&right, 0, &mut arena); left.physical_option = Some(PhysicalOption::new( @@ -1043,7 +1064,7 @@ mod inner_join_tests { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let left = scan(&mut arena, "outer", &[LogicalType::Integer], &[]); - let mut right = scan( + let mut right = unique_scan( &mut arena, "inner", &[LogicalType::Integer, LogicalType::Integer], @@ -1085,8 +1106,8 @@ mod inner_join_tests { let mut arena = PlanArena::new(&tables); let left_indexes: &[&[usize]] = if swap { &[&[0]] } else { &[] }; let right_indexes: &[&[usize]] = if swap { &[&[1]] } else { &[&[1], &[0]] }; - let left = scan(&mut arena, "outer", &[LogicalType::Integer], left_indexes); - let right = scan( + let left = unique_scan(&mut arena, "outer", &[LogicalType::Integer], left_indexes); + let right = unique_scan( &mut arena, "inner", &[LogicalType::Integer, LogicalType::Integer], @@ -1158,7 +1179,7 @@ mod inner_join_tests { &[], ); // Skip the constant-only and unmatched indexes; the last uses both key sources. - let mut right = scan( + let mut right = unique_scan( &mut arena, "inner", &[const { LogicalType::Integer }; 4], @@ -1178,6 +1199,9 @@ mod inner_join_tests { unreachable!() }; let info = &mut scan.index_infos[2]; + let mut meta = arena.index(info.meta).clone(); + meta.ty = IndexType::PrimaryKey { is_multiple: true }; + info.meta = arena.alloc_index(meta); info.lookup = Some(IndexLookup::Static(Range::Eq(DataValue::Int32(7)))); info.residual_predicate = Some(residual); let index = info.meta; @@ -1226,7 +1250,7 @@ mod inner_join_tests { fn swapped_join_restores_unequal_widths_and_filter_positions() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); - let mut left = scan( + let mut left = unique_scan( &mut arena, "left", &[const { LogicalType::Integer }; 3], @@ -1342,7 +1366,14 @@ mod inner_join_tests { #[test] fn rejected_probe_leaves_join_and_expression_positions_unchanged() -> Result<(), DatabaseError> { - for case in ["missing_key", "type_mismatch", "inner_limit", "outer_join"] { + for case in [ + "missing_key", + "type_mismatch", + "inner_limit", + "outer_join", + "normal_index", + "composite_index", + ] { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let left = scan(&mut arena, "left", &[LogicalType::Integer], &[]); @@ -1351,8 +1382,16 @@ mod inner_join_tests { } else { LogicalType::Integer }; - let index: &[usize] = if case == "missing_key" { &[0, 1] } else { &[0] }; - let mut right = scan(&mut arena, "right", &[ty, LogicalType::Integer], &[index]); + let index: &[usize] = if matches!(case, "missing_key" | "composite_index") { + &[0, 1] + } else { + &[0] + }; + let mut right = if matches!(case, "normal_index" | "composite_index") { + scan(&mut arena, "right", &[ty, LogicalType::Integer], &[index]) + } else { + unique_scan(&mut arena, "right", &[ty, LogicalType::Integer], &[index]) + }; if case == "inner_limit" { let Operator::TableScan(scan) = &mut right.operator else { unreachable!() @@ -1361,7 +1400,11 @@ mod inner_join_tests { } let l = column(&left, 0, &mut arena); let r = column(&right, 0, &mut arena); - let mut plan = join(left, right, vec![(l, r)], None); + let mut keys = vec![(l, r)]; + if case == "composite_index" { + keys.push((l, column(&right, 1, &mut arena))); + } + let mut plan = join(left, right, keys, None); if case == "outer_join" { let Operator::Join(join) = &mut plan.operator else { unreachable!() diff --git a/src/planner/operator/mark_apply.rs b/src/planner/operator/mark_apply.rs index 35217fb3..ec2defbd 100644 --- a/src/planner/operator/mark_apply.rs +++ b/src/planner/operator/mark_apply.rs @@ -137,6 +137,7 @@ impl MarkApplyOperator { .expect("marker apply output column") } + // The RHS must be a full primary/unique-key point probe, yielding at most one row. pub(crate) fn new_inner_join(predicates: Vec, probe: ExprRef) -> Self { Self { kind: MarkApplyKind::InnerJoin, diff --git a/tests/slt/join.slt b/tests/slt/join.slt index de0a8aa7..9a328170 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -258,14 +258,28 @@ insert into lookup_outer values (1,10),(2,20),(3,null),(4,99),(5,10); statement ok insert into lookup_inner values (1,10,7),(2,10,9),(3,20,3),(4,null,8); -# A plain SeqScan outer input is eligible; no size/limit heuristic is required. +# A non-unique index stays on the ordinary join path, preserving all matches. query T explain select lookup_outer.id,lookup_inner.id from lookup_outer join lookup_inner on lookup_outer.k=lookup_inner.k; ---- -Projection [lookup_outer.id, lookup_inner.id] [Project => (Sort Option: Follow)] InnerJoinApply [MarkApply => (Sort Option: Follow)] TableScan lookup_outer -> [lookup_outer.id, lookup_outer.k] [SeqScan => (Sort Option: None)] TableScan lookup_inner -> [lookup_inner.id, lookup_inner.k] [IndexScan By lookup_inner_k => Probe ? => (Sort Option: OrderBy: (lookup_inner.k Asc Nulls Last) ignore_prefix_len: 0)] +Projection [lookup_outer.id, lookup_inner.id] [Project => (Sort Option: Follow)] Inner Join On lookup_outer.k = lookup_inner.k [HashJoin => (Sort Option: None)] TableScan lookup_outer -> [lookup_outer.id, lookup_outer.k] [SeqScan => (Sort Option: None)] TableScan lookup_inner -> [lookup_inner.id, lookup_inner.k] [SeqScan => (Sort Option: None)] -# Resume the same non-unique probe across calls, then rebuild after NULL and -# missing keys. In particular, NULL = NULL must not produce the inner NULL row. +# A complete primary-key equality uses the single-match path. +query III +select o.id,i.id,i.v from lookup_outer o join lookup_inner i on o.id=i.id order by o.id; +---- +1 1 7 +2 2 9 +3 3 3 +4 4 8 + +# Extra equalities can reject the unique hit, including UNKNOWN from NULL. +query II +select o.id,i.id from lookup_outer o join lookup_inner i on o.id=i.id and o.k=i.k order by o.id; +---- +1 1 + +# Non-unique keys still emit every match. NULL = NULL must not match. query II select lookup_outer.id,lookup_inner.id from lookup_outer join lookup_inner on lookup_outer.k=lookup_inner.k @@ -710,7 +724,7 @@ select greedy_a.v,greedy_b.v,greedy_c.v from greedy_a,greedy_b,greedy_c where gr 10 100 1100 20 200 2000 -# Parameterization must still select an index probe after the join tree is reordered. +# A non-unique index added after reorder must preserve the ordinary join results. statement ok create index greedy_b_k on greedy_b(k) From 1d502ab52c9f7398a4faf9e9f47cc0e8811926b6 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 26/62] perf: index binder names and reduce join copying Keep ordered name bindings with a single-source lookup fast path.\nAccount for intermediate/build width when reordering joins, move the final hash probe match, and classify predicates in one traversal. --- src/binder/analyze.rs | 2 +- src/binder/create_index.rs | 2 +- src/binder/expr.rs | 75 +---- src/binder/mod.rs | 298 +++++++++++++++--- src/binder/parser.rs | 20 +- src/binder/select.rs | 90 ++++-- src/execution/dql/join/hash/inner_join.rs | 30 +- .../rule/normalization/join_reorder.rs | 43 ++- .../rule/normalization/pushdown_predicates.rs | 68 +++- src/orm/mod.rs | 4 +- tests/slt/where.slt | 51 +++ 11 files changed, 506 insertions(+), 177 deletions(-) diff --git a/src/binder/analyze.rs b/src/binder/analyze.rs index 9b51233a..edcd560a 100644 --- a/src/binder/analyze.rs +++ b/src/binder/analyze.rs @@ -30,7 +30,7 @@ impl> Binder<'_, '_, T, A> { ) -> Result { let table = self .context - .source_and_bind(table_name.clone(), None, None, true)? + .source_and_bind(table_name.clone(), None, None, true, arena)? .and_then(|source| { if let Source::Table(table) = source { Some(table) diff --git a/src/binder/create_index.rs b/src/binder/create_index.rs index 24702eda..5fdabdef 100644 --- a/src/binder/create_index.rs +++ b/src/binder/create_index.rs @@ -31,7 +31,7 @@ impl> Binder<'_, '_, T, A> { ) -> Result { let source = self .context - .source_and_bind(table_name.clone(), None, None, false)? + .source_and_bind(table_name.clone(), None, None, false, arena)? .ok_or(DatabaseError::SourceNotFound)?; match source { Source::Table(table) => { diff --git a/src/binder/expr.rs b/src/binder/expr.rs index 5f68820a..0ac1b9b5 100644 --- a/src/binder/expr.rs +++ b/src/binder/expr.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::catalog::ColumnRef; use crate::errors::DatabaseError; use crate::expression; use crate::expression::agg::AggKind; @@ -39,46 +38,6 @@ macro_rules! try_default { } impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> { - fn find_column_in_schema<'schema>( - schema_ref: impl IntoIterator, - arena: &PlanArena, - column_name: &str, - ) -> Option<(usize, ColumnRef)> { - schema_ref - .into_iter() - .enumerate() - .find(|(_, column)| arena.column(**column).name() == column_name) - .map(|(position, column)| (position, *column)) - } - - fn find_column_in_scope( - context: &BinderContext<'a, T>, - arena: &PlanArena, - column_name: &str, - ) -> Result, DatabaseError> { - let mut position_offset = 0; - let mut found = None; - - for bound_source in &context.bind_table { - let source = &bound_source.source; - - if let Some((position, column)) = - Self::find_column_in_schema(source.schema().iter(), arena, column_name) - { - if found.is_some() { - return Err(DatabaseError::AmbiguousColumn(column_name.to_string())); - } - found = Some(ScalarExpression::column_expr( - column, - position_offset + position, - )); - } - - position_offset += source.schema_len(); - } - - Ok(found) - } pub(crate) fn bind_temp_table( &mut self, expr: ScalarExpression, @@ -369,7 +328,7 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> { let input = match self.context.using.get(column_name) { Some(column) => Some(column.visible_expr(arena)?), - None => Self::find_column_in_scope(&self.context, arena, column_name)?, + None => self.context.resolve_column(column_name, None, arena)?, }; if let Some(input) = input { return Ok(input); @@ -395,24 +354,20 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> } if let Some(table) = table_name.or(bind_table_name) { let mut is_outer = false; - let (source, position_offset) = - match Self::resolve_source_columns_in_scope(&self.context, table) { - Ok(source) => source, - Err(err) => { - if let Some(parent) = self.parent { - self.context.mark_join_outer_ref(); - is_outer = true; - Self::resolve_source_columns_in_scope(parent, table).map_err(|_| err)? - } else { - return Err(err); - } + let expr = match self.context.resolve_column(column_name, Some(table), arena) { + Ok(expr) => expr, + Err(err @ DatabaseError::InvalidTable { .. }) => { + if let Some(parent) = self.parent { + self.context.mark_join_outer_ref(); + is_outer = true; + parent.resolve_column(column_name, Some(table), arena)? + } else { + return Err(err); } - }; - let (position, column) = - Self::find_column_in_schema(source.schema().iter(), arena, column_name) - .ok_or_else(|| DatabaseError::column_not_found(column_name.to_string()))?; - - let expr = ScalarExpression::column_expr(column, position_offset + position); + } + Err(err) => return Err(err), + } + .ok_or_else(|| DatabaseError::column_not_found(column_name.to_string()))?; if is_outer && self.context.capture_scalar_outer { return self.capture_scalar_outer(expr, arena); } @@ -423,7 +378,7 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> |context: &BinderContext<'a, T>| -> Result, DatabaseError> { match context.using.get(column_name) { Some(using_column) => using_column.visible_expr(arena).map(Some), - None => Self::find_column_in_scope(context, arena, column_name), + None => context.resolve_column(column_name, None, arena), } }; let mut got_column = find_visible_column(&self.context)?; diff --git a/src/binder/mod.rs b/src/binder/mod.rs index 68096830..43e1ba34 100644 --- a/src/binder/mod.rs +++ b/src/binder/mod.rs @@ -128,6 +128,31 @@ pub struct BoundSource<'a> { pub(crate) alias: Option, pub(crate) join_type: Option, pub(crate) source: Source<'a>, + // The first source with this visible name owns qualified name resolution. + qualified_visible: bool, +} + +// Sorted by (name, visible source name, source index, position). A source entry +// has no column; names are borrowed from existing metadata instead of copied. +#[derive(Debug, Clone, Copy)] +struct NameBinding { + source: usize, + column: Option, + position: usize, +} + +impl NameBinding { + fn key<'n>( + &self, + sources: &'n [BoundSource<'_>], + arena: &'n PlanArena<'_>, + ) -> (&'n str, Option<&'n str>) { + let source_name = sources[self.source].visible_name().as_ref(); + match self.column { + Some(column) => (arena.column(column).name(), Some(source_name)), + None => (source_name, None), + } + } } #[derive(Debug, Clone)] @@ -144,13 +169,6 @@ pub(crate) struct CteCheckpoint { } impl BoundSource<'_> { - pub(crate) fn matches_name(&self, table_name: &str) -> bool { - match &self.alias { - Some(alias) => alias.as_ref() == table_name, - None => self.table_name.as_ref() == table_name, - } - } - pub(crate) fn same_binding( &self, table_name: &TableName, @@ -249,6 +267,7 @@ pub struct BinderContext<'a, T: Transaction> { // Tips: retain binding order so wildcard expansion and position derivation // follow FROM/JOIN order directly. pub(crate) bind_table: Vec>, + name_bindings: Vec, // Sources before this index were inherited from an enclosing query by `fork` // and do not share the current FROM namespace. source_scope_start: usize, @@ -321,6 +340,7 @@ impl<'a, T: Transaction> BinderContext<'a, T> { view_cache, transaction, bind_table: Default::default(), + name_bindings: Vec::new(), source_scope_start: 0, ctes: Default::default(), cte_depth: 0, @@ -351,6 +371,7 @@ impl<'a, T: Transaction> BinderContext<'a, T> { view_cache: self.view_cache, transaction: self.transaction, bind_table: self.bind_table.clone(), + name_bindings: self.name_bindings.clone(), source_scope_start: self.bind_table.len(), ctes: self.ctes.clone(), cte_depth: self.cte_depth, @@ -474,55 +495,40 @@ impl<'a, T: Transaction> BinderContext<'a, T> { } } - #[allow(unused_assignments)] pub fn source_and_bind( &mut self, table_name: TableName, alias: Option<&TableName>, join_type: Option, only_table: bool, + arena: &PlanArena, ) -> Result>, DatabaseError> { - let mut source = None; - - source = if let Some(real_name) = self.table_aliases.get(table_name.as_ref()) { - self.transaction.table(self.table_cache, real_name.clone()) - } else { - self.transaction.table(self.table_cache, table_name.clone()) - }? - .map(Source::Table); - - if source.is_none() && !only_table { - source = if let Some(real_name) = self.table_aliases.get(table_name.as_ref()) { - self.transaction.view( - self.table_cache, - self.view_cache, - self.scala_functions, - self.table_functions, - real_name.clone(), - ) - } else { - self.transaction.view( - self.table_cache, - self.view_cache, - self.scala_functions, - self.table_functions, - table_name.clone(), - ) - }? - .map(Source::View); + if let Some(source) = self.bound_source(&table_name, alias, join_type) { + if !only_table || matches!(source, Source::Table(_)) { + return Ok(Some(source.clone())); + } } + let source = if only_table { + let name = self.table_aliases.get(&table_name).unwrap_or(&table_name); + self.transaction + .table(self.table_cache, name.clone())? + .map(Source::Table) + } else { + self.source(&table_name)? + }; if let Some(source) = &source { self.add_bound_source( table_name.clone(), alias.cloned(), join_type, source.clone(), + arena, ); } Ok(source) } - pub fn source(&self, table_name: &TableName) -> Result>, DatabaseError> { + pub fn source(&self, table_name: &TableName) -> Result>, DatabaseError> { let mut source = if let Some(real_name) = self.table_aliases.get(table_name.as_ref()) { self.transaction.table(self.table_cache, real_name.clone()) } else { @@ -567,27 +573,151 @@ impl<'a, T: Transaction> BinderContext<'a, T> { Ok(()) } + fn bound_source( + &self, + table_name: &TableName, + alias: Option<&TableName>, + join_type: Option, + ) -> Option<&Source<'a>> { + self.bind_table + .iter() + .find(|bound| bound.same_binding(table_name, alias, join_type)) + .map(|bound| &bound.source) + } + pub fn add_bound_source( &mut self, table_name: TableName, alias: Option, join_type: Option, source: Source<'a>, + arena: &PlanArena, ) { - if let Some(bound_source) = self - .bind_table - .iter_mut() - .find(|bound_source| bound_source.same_binding(&table_name, alias.as_ref(), join_type)) + if self + .bound_source(&table_name, alias.as_ref(), join_type) + .is_some() { - bound_source.source = source; return; } + let offset = self + .bind_table + .iter() + .map(|bound| bound.source.schema_len()) + .sum::(); + let index = self.bind_table.len(); + let qualified_visible = self + .name_binding(alias.as_ref().unwrap_or(&table_name), None, arena) + .is_none(); self.bind_table.push(BoundSource { table_name, alias, join_type, source, + qualified_visible, + }); + self.name_bindings + .reserve(self.bind_table[index].source.schema_len() + 1); + self.insert_name_binding( + NameBinding { + source: index, + column: None, + position: offset, + }, + arena, + ); + for position in 0..self.bind_table[index].source.schema_len() { + let column = self.bind_table[index].source.schema()[position]; + self.insert_name_binding( + NameBinding { + source: index, + column: Some(column), + position: offset + position, + }, + arena, + ); + } + } + + fn insert_name_binding(&mut self, binding: NameBinding, arena: &PlanArena) { + let key = binding.key(&self.bind_table, arena); + let index = self.name_bindings.partition_point(|existing| { + existing + .key(&self.bind_table, arena) + .cmp(&key) + .then(existing.source.cmp(&binding.source)) + .then(existing.position.cmp(&binding.position)) + .is_lt() }); + self.name_bindings.insert(index, binding); + } + + fn name_binding( + &self, + name: &str, + source: Option<&str>, + arena: &PlanArena, + ) -> Option { + let key = (name, source); + let index = self + .name_bindings + .partition_point(|binding| binding.key(&self.bind_table, arena) < key); + self.name_bindings + .get(index) + .filter(|binding| { + binding.key(&self.bind_table, arena) == key + && (source.is_none() || self.bind_table[binding.source].qualified_visible) + }) + .copied() + } + + fn resolve_column( + &self, + name: &str, + source: Option<&str>, + arena: &PlanArena, + ) -> Result, DatabaseError> { + // Fast path for single source + if let [bound] = self.bind_table.as_slice() { + if let Some(source) = source { + if bound.visible_name().as_ref() != source { + return Err(DatabaseError::invalid_table(source)); + } + } + return Ok(bound + .source + .schema() + .iter() + .enumerate() + .find(|(_, column)| arena.column(**column).name() == name) + .map(|(position, column)| ScalarExpression::column_expr(*column, position))); + } + let binding = if let Some(source) = source { + let binding = self.name_binding(name, Some(source), arena); + if binding.is_none() && self.name_binding(source, None, arena).is_none() { + return Err(DatabaseError::invalid_table(source)); + } + binding + } else { + let start = self + .name_bindings + .partition_point(|binding| binding.key(&self.bind_table, arena).0 < name); + let mut columns = self.name_bindings[start..] + .iter() + .take_while(|binding| binding.key(&self.bind_table, arena).0 == name) + .filter(|binding| binding.column.is_some()); + let Some(first) = columns.next() else { + return Ok(None); + }; + if columns.any(|binding| binding.source != first.source) { + return Err(DatabaseError::AmbiguousColumn(name.to_string())); + } + Some(*first) + }; + Ok(binding.and_then(|binding| { + binding + .column + .map(|column| ScalarExpression::column_expr(column, binding.position)) + })) } // Tips: The order of this index is based on Aggregate being bound first. @@ -676,7 +806,11 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' false } - pub(crate) fn extend(&mut self, context: BinderContext<'a, T>) -> Result<(), DatabaseError> { + pub(crate) fn extend( + &mut self, + context: BinderContext<'a, T>, + arena: &PlanArena, + ) -> Result<(), DatabaseError> { for bound_source in context.bind_table { self.context .ensure_unique_source_name(bound_source.visible_name())?; @@ -685,6 +819,7 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' bound_source.alias, bound_source.join_type, bound_source.source, + arena, ); } for (key, expr) in context.expr_aliases { @@ -843,6 +978,83 @@ pub mod test { Ok(storage) } + #[test] + fn sorted_bindings_preserve_names_positions_and_scope_isolation() -> Result<(), DatabaseError> { + use crate::binder::Source; + use crate::expression::ScalarExpression; + + let state = build_t1_table()?; + let arena = PlanArena::new(&state.table_arena); + let transaction = state.storage.transaction()?; + let scala_functions = Default::default(); + let table_functions = Default::default(); + let mut context = BinderContext::new( + &state.table_cache, + &state.view_cache, + &transaction, + &scala_functions, + &table_functions, + ); + let c1 = state.table.get_column_by_name("c1").unwrap(); + let c2 = state.table.get_column_by_name("c2").unwrap(); + // A source name may equal a column name; duplicates within that source + // retain their first position and are not ambiguous. + context.add_bound_source( + "a".into(), + Some("c1".into()), + None, + Source::Schema(vec![c1, c1]), + &arena, + ); + assert!(matches!( + context.resolve_column("c1", None, &arena)?, + Some(ScalarExpression::ColumnRef { column, position: 0 }) if column == c1 + )); + context.add_bound_source("b".into(), None, None, Source::Schema(vec![c2]), &arena); + assert!(matches!( + context.resolve_column("c2", Some("b"), &arena)?, + Some(ScalarExpression::ColumnRef { column, position: 2 }) if column == c2 + )); + // Re-registering a binding preserves its original columns and positions. + context.add_bound_source("b".into(), None, None, Source::Schema(vec![c1]), &arena); + assert!(matches!( + context.resolve_column("c2", Some("b"), &arena)?, + Some(ScalarExpression::ColumnRef { column, position: 2 }) if column == c2 + )); + assert!(matches!( + context.resolve_column("c1", Some("c1"), &arena)?, + Some(ScalarExpression::ColumnRef { position: 0, .. }) + )); + context.add_bound_source( + "a".into(), + Some("c1".into()), + None, + Source::Schema(vec![c2]), + &arena, + ); + assert_eq!(context.name_binding("b", None, &arena).unwrap().position, 2); + assert!(matches!( + context.resolve_column("c1", None, &arena)?, + Some(ScalarExpression::ColumnRef { column, position: 0 }) if column == c1 + )); + assert!(matches!( + context.resolve_column("c1", Some("a"), &arena), + Err(DatabaseError::InvalidTable { .. }) + )); + let mut fork = context.fork(); + fork.add_bound_source("c".into(), None, None, Source::Schema(vec![c1]), &arena); + assert!(matches!( + fork.resolve_column("c1", None, &arena), + Err(DatabaseError::AmbiguousColumn(_)) + )); + assert!(context.resolve_column("c1", None, &arena)?.is_some()); + assert!(context + .fork_empty() + .resolve_column("c1", None, &arena)? + .is_none()); + Ok(()) + } + #[test] pub fn test_valid_identifier() { assert!(is_valid_identifier("valid_table")); diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 53df3db5..c1e3d9c1 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -13,8 +13,9 @@ // limitations under the License. use super::select::{ - BindPlanAggregated, BindPlanComplete, BindPlanDistinct, BindPlanFiltered, BindPlanFrom, - BindPlanProjected, BindPlanSelectList, BindPlanStart, JoinConstraintInput, TableAliasInput, + AliasPurpose, BindPlanAggregated, BindPlanComplete, BindPlanDistinct, BindPlanFiltered, + BindPlanFrom, BindPlanProjected, BindPlanSelectList, BindPlanStart, JoinConstraintInput, + TableAliasInput, }; use super::{is_valid_identifier, Binder, CteBinding, QueryBindStep, SetOperatorKind}; #[cfg(feature = "copy")] @@ -1062,7 +1063,7 @@ where let source = self .binder .context - .source_and_bind(table_name.clone(), None, None, false)? + .source_and_bind(table_name.clone(), None, None, false, self.arena)? .ok_or(DatabaseError::TableNotFound)?; let values_len = expr_rows[0].len(); @@ -1216,6 +1217,7 @@ where Binder::<'a, 'b, T, A>::resolve_source_columns_in_scope( &self.binder.context, &table_name, + self.arena, )?; let target_schema = target_source.schema().to_vec(); @@ -2162,7 +2164,7 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' let right = binder.bind_single_table_ref_sql(relation, Some(join_type), arena)?; (right, binder.context) }; - self.extend(context)?; + self.extend(context, arena)?; let constraint = match joint_condition { Some(constraint) => self.bind_join_constraint_sql(constraint, arena)?, @@ -3108,7 +3110,14 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' let mut binder = Binder::new(self.context.fork(), self.args, Some(&self.context)); let plan = binder.bind_query(&cte.query, arena)?; let source_name = arena.temp_table(); - binder.bind_alias(plan, &alias.columns, alias.name.clone(), source_name, arena) + binder.bind_alias( + plan, + &alias.columns, + alias.name.clone(), + source_name, + AliasPurpose::Output, + arena, + ) } fn add_cte_plan( @@ -3170,6 +3179,7 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' &alias.columns, alias.name.clone(), source_name, + AliasPurpose::Output, arena, )?; let schema = anchor.output_schema(arena).clone(); diff --git a/src/binder/select.rs b/src/binder/select.rs index d536c1e0..38b2aace 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -237,6 +237,11 @@ pub(crate) struct TableAliasInput { pub(crate) columns: Vec, } +pub(crate) enum AliasPurpose { + Source, + Output, +} + pub(crate) enum JoinConstraintInput { On(ExprRef), Using(Vec), @@ -275,7 +280,7 @@ where join_type: JoinType, constraint: JoinConstraintInput, ) -> Result { - self.binder.extend(right_context); + self.binder.extend(right_context, self.arena); self.plan = self .binder .bind_join_plans(self.plan, right_plan, join_type, constraint, self.arena)?; @@ -889,18 +894,12 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b pub(crate) fn resolve_source_columns_in_scope<'context>( context: &'context BinderContext<'a, T>, table_name: &str, + arena: &PlanArena, ) -> Result<(&'context Source<'a>, usize), DatabaseError> { - let mut position_offset = 0; - - for bound_source in &context.bind_table { - if bound_source.matches_name(table_name) { - return Ok((&bound_source.source, position_offset)); - } - - position_offset += bound_source.source.schema_len(); - } - - Err(DatabaseError::invalid_table(table_name)) + context + .name_binding(table_name, None, arena) + .map(|binding| (&context.bind_table[binding.source].source, binding.position)) + .ok_or_else(|| DatabaseError::invalid_table(table_name)) } fn localize_join_condition_from_join_scope( @@ -1116,6 +1115,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b alias_column: &[String], table_alias: TableName, table_name: TableName, + purpose: AliasPurpose, arena: &mut PlanArena, ) -> Result { let input_schema = plan.output_schema(arena); @@ -1123,6 +1123,14 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b if !alias_column.is_empty() && alias_column.len() != input_schema_len { return Err(DatabaseError::MisMatch("alias", "columns")); } + let reused_source = match purpose { + AliasPurpose::Source => self + .context + .name_binding(&table_alias, None, arena) + .filter(|binding| binding.source < self.context.source_scope_start) + .map(|binding| binding.source), + AliasPurpose::Output => None, + }; let mut alias_exprs = Vec::with_capacity(input_schema_len); for (position, column) in input_schema.iter().copied().enumerate() { @@ -1131,20 +1139,22 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b } else { alias_column[position].clone() }; - let (mut alias_column, column_id, is_temp) = { + let alias_column = if let Some(source) = reused_source { + self.context.bind_table[source].source.schema()[position] + } else { let source_column = arena.column(column); - ( - source_column.clone(), + let mut alias_column = source_column.clone(); + alias_column.set_name(alias.clone()); + alias_column.set_ref_table( + table_alias.clone(), source_column.id().unwrap_or_default(), matches!( &source_column.summary().relation, ColumnRelation::Table { is_temp: true, .. } ), - ) + ); + arena.alloc_column(alias_column) }; - alias_column.set_name(alias.clone()); - alias_column.set_ref_table(table_alias.clone(), column_id, is_temp); - let alias_column = arena.alloc_column(alias_column); let expr = arena.alloc_expression(ScalarExpression::column_expr(column, position)); let alias_expr = @@ -1206,7 +1216,6 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b let table_alias = alias.as_ref().map(|alias| alias.name.clone()); self.context .ensure_unique_source_name(table_alias.as_ref().unwrap_or(&table_name))?; - if let Some(plan_ref) = self.context.cte(&table_name).map(|cte| cte.plan_ref) { let mut plan = arena.plan(plan_ref).clone(); OperatorExprVisitorMut::new(&mut ExprCloner, arena).visit_plan(&mut plan)?; @@ -1216,6 +1225,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b &alias.columns, alias.name.clone(), table_name.clone(), + AliasPurpose::Source, arena, )?; } @@ -1225,6 +1235,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b table_alias, join_type, Source::Schema(output_schema), + arena, ); return Ok(plan); } @@ -1232,9 +1243,9 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b let with_pk = self.is_scan_with_pk(&table_name); let source = self .context - .source_and_bind(table_name.clone(), table_alias.as_ref(), join_type, false)? + .source(&table_name)? .ok_or(DatabaseError::SourceNotFound)?; - let mut plan = match source { + let mut plan = match &source { Source::Table(table) => { TableScanOperator::build(table_name.clone(), table, with_pk, arena)? } @@ -1250,22 +1261,21 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b } }; - if let Some(alias) = alias { + let source = if let Some(alias) = alias { plan = self.bind_alias( plan, &alias.columns, - alias.name.clone(), + alias.name, table_name.clone(), + AliasPurpose::Source, arena, )?; - let output_schema = plan.output_schema(arena).clone(); - self.context.add_bound_source( - table_name, - Some(alias.name), - join_type, - Source::Schema(output_schema), - ); - } + Source::Schema(plan.output_schema(arena).clone()) + } else { + source + }; + self.context + .add_bound_source(table_name, table_alias, join_type, source, arena); Ok(plan) } @@ -1285,6 +1295,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b &alias.columns, alias.name.clone(), source_name.clone(), + AliasPurpose::Source, arena, )?; let output_schema = plan.output_schema(arena).clone(); @@ -1293,6 +1304,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b Some(alias.name), joint_type, Source::Schema(output_schema), + arena, ); } else { let passthrough_source = { @@ -1322,6 +1334,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b None, joint_type, Source::Schema(output_schema), + arena, ); } @@ -1354,12 +1367,19 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b if let Some(alias) = alias { table_alias = Some(alias.name.clone()); - plan = self.bind_alias(plan, &alias.columns, alias.name, table_name.clone(), arena)?; + plan = self.bind_alias( + plan, + &alias.columns, + alias.name, + table_name.clone(), + AliasPurpose::Source, + arena, + )?; } let source = Source::Schema(plan.output_schema(arena).clone()); self.context - .add_bound_source(table_name, table_alias, joint_type, source); + .add_bound_source(table_name, table_alias, joint_type, source, arena); Ok(plan) } @@ -1378,7 +1398,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b is_qualified_wildcard: bool, ) -> Result<(), DatabaseError> { let (source, position_offset) = - Self::resolve_source_columns_in_scope(context, table_name.as_ref())?; + Self::resolve_source_columns_in_scope(context, table_name.as_ref(), arena)?; let fn_not_on_using = |column: &ColumnRef, arena: &PlanArena<'_>| { let column_catalog = arena.column(*column); diff --git a/src/execution/dql/join/hash/inner_join.rs b/src/execution/dql/join/hash/inner_join.rs index 9df19866..1a097dc6 100644 --- a/src/execution/dql/join/hash/inner_join.rs +++ b/src/execution/dql/join/hash/inner_join.rs @@ -40,26 +40,28 @@ impl JoinProbeState for InnerJoinState { }; while probe_state.index < build_state.tuples.len() { - let (_, Tuple { values, pk }) = &build_state.tuples[probe_state.index]; + let (_, build_tuple) = &build_state.tuples[probe_state.index]; probe_state.index += 1; - if let Some(filter_expr) = filter_expr { - let full_values = - SplitTupleRef::from_slices(values, &probe_state.probe_tuple.values); + let full_values = SplitTupleRef::new(build_tuple, &probe_state.probe_tuple); if !filter(&full_values, filter_expr, plan_arena)? { continue; } } - let full_values = Vec::from_iter( - values - .iter() - .chain(probe_state.probe_tuple.values.iter()) - .cloned(), - ); - return Ok(Some(Tuple::new( - pk.as_ref().or(probe_state.probe_tuple.pk.as_ref()).cloned(), - full_values, - ))); + let probe = &mut probe_state.probe_tuple; + let mut values = Vec::with_capacity(build_tuple.values.len() + probe.values.len()); + values.extend_from_slice(&build_tuple.values); + let mut pk = build_tuple.pk.clone(); + if probe_state.index == build_state.tuples.len() { + // No future match can read this probe row, so move its values and pk. + probe_state.finished = true; + values.append(&mut probe.values); + pk = pk.or(probe.pk.take()); + } else { + values.extend_from_slice(&probe.values); + pk = pk.or_else(|| probe.pk.clone()); + } + return Ok(Some(Tuple::new(pk, values))); } probe_state.finished = true; diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index ea38e086..adffbce0 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -143,13 +143,15 @@ fn project_join_output( plan } -// Prefer equijoins, then fewer rows, then the original input order. +// Prefer equijoins, then less intermediate/build data, then the original input order. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] struct JoinCandidate { cross_join: bool, + work: u128, rows: usize, left: usize, right: usize, + swapped: bool, } fn column_ndv>( @@ -613,7 +615,7 @@ impl NormalizationRule for GreedyJoinReorder { { return Ok(false); } - let (input_count, cross_join) = region_shape(plan); + let (input_count, _) = region_shape(plan); if input_count > u64::BITS as usize { return Ok(false); } @@ -621,12 +623,8 @@ impl NormalizationRule for GreedyJoinReorder { let mut schemas = Vec::new(); let mut keys = JoinKeys::default(); let estimated = inspect(plan, &mut schemas, &mut keys, arena, loader)?.is_some(); - // Without row estimates the greedy order degrades to the heuristic - // "equijoins first, then input order", which only helps by deferring - // Cartesian products already present in the region. - if input_count < 3 || (!estimated && !cross_join) { - return Ok(false); - } + // Unknown row counts remain unknown; only column widths break their ties. + // Two-input regions also need to choose the smaller hash build side. // Each original column keeps its input bit and position within its current join. let mut positions = Vec::with_capacity(original_schema.len()); for (input, schema) in schemas.iter().enumerate() { @@ -676,15 +674,26 @@ impl NormalizationRule for GreedyJoinReorder { row_cache.insert(cache_key, rows); rows }; - // Edge order differs from input order: equal rows must reach the tie-breaker. - if best.is_some_and(|previous| product > previous.rows) { - return Ok(()); - } + let left_width = left.output_schema(arena).len() as u128; + let right_width = right.output_schema(arena).len() as u128; + let left_size = if estimated { *left_rows as u128 } else { 1 } * left_width; + let right_size = if estimated { *right_rows as u128 } else { 1 } * right_width; + // HashJoin builds on the left. Count intermediate values and saved build values, + // not just output rows; use widths alone when cardinalities are unavailable. + let work = (if estimated { product as u128 } else { 1 }) + .saturating_mul(left_width + right_width) + .saturating_add(if connected { + left_size.min(right_size) + } else { + 0 + }); let candidate = JoinCandidate { cross_join: !connected, + work, rows: product, left: i, right: j, + swapped: connected && right_size < left_size, }; if best.is_none_or(|previous| candidate < previous) { best = Some(candidate); @@ -719,13 +728,19 @@ impl NormalizationRule for GreedyJoinReorder { rows: product, left: i, right: j, + swapped, .. }) = best else { return Ok(false); }; - let (mut right, _, right_mask) = inputs.remove(j); - let (mut left, _, left_mask) = inputs.remove(i); + let mut right = inputs.remove(j); + let mut left = inputs.remove(i); + if swapped { + swap(&mut left, &mut right); + } + let (mut left, _, left_mask) = left; + let (mut right, _, right_mask) = right; let left_schema = left.output_schema(arena); let right_schema = right.output_schema(arena); for (input, position) in &mut positions { diff --git a/src/optimizer/rule/normalization/pushdown_predicates.rs b/src/optimizer/rule/normalization/pushdown_predicates.rs index 4046766c..eefc893d 100644 --- a/src/optimizer/rule/normalization/pushdown_predicates.rs +++ b/src/optimizer/rule/normalization/pushdown_predicates.rs @@ -83,9 +83,15 @@ fn classify_join_filters( let mut right_filter = None; let mut common_filter = None; split_conjunctive_predicates(predicate, arena, &mut |expr, arena| { - if expr.all_referenced_columns(arena, |_, column| left_columns.contains(column))? { + let (mut left, mut right) = (true, true); + expr.all_referenced_columns(arena, |_, column| { + left = left && left_columns.contains(column); + right = right && right_columns.contains(column); + left || right + })?; + if left { append_filter(&mut left_filter, expr, arena); - } else if expr.all_referenced_columns(arena, |_, column| right_columns.contains(column))? { + } else if right { append_filter(&mut right_filter, expr, arena); } else { append_filter(&mut common_filter, expr, arena); @@ -647,6 +653,64 @@ mod tests { }) } + #[test] + fn single_pass_filter_classification_preserves_reference_semantics() -> Result<(), DatabaseError> + { + use crate::expression::AliasType; + use crate::planner::operator::values::ValuesOperator; + + let table_state = build_t1_table()?; + let mut arena = PlanArena::new(&table_state.table_arena); + let table: TableName = "classification".to_string().into(); + let left_column = build_test_column(&mut arena, &table, 1, "left")?; + let right_column = build_test_column(&mut arena, &table, 2, "right")?; + let unknown_column = build_test_column(&mut arena, &table, 3, "unknown")?; + let left = cmp_predicate(&mut arena, BinaryOperator::Eq, left_column, 0, 1); + let right = cmp_predicate(&mut arena, BinaryOperator::Eq, right_column, 1, 1); + let unknown = cmp_predicate(&mut arena, BinaryOperator::Eq, unknown_column, 2, 1); + let common = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Or, + left_expr: left, + right_expr: right, + evaluator: None, + ty: LogicalType::Boolean, + }); + let constant = arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(true))); + let alias = arena.alloc_expression(ScalarExpression::Alias { + expr: right, + alias: AliasType::Expr(left), + }); + for right_columns in [vec![right_column], vec![left_column, right_column]] { + let mut children = Childrens::Twins { + left: Box::new(LogicalPlan::new( + Operator::Values(ValuesOperator::new(Vec::new(), 0, vec![left_column])), + Childrens::None, + )), + right: Box::new(LogicalPlan::new( + Operator::Values(ValuesOperator::new(Vec::new(), 0, right_columns.clone())), + Childrens::None, + )), + }; + for expr in [left, right, unknown, common, constant, alias] { + let expected = + if expr.all_referenced_columns(&arena, |_, column| *column == left_column)? { + (Some(expr), None, None, 1) + } else if expr.all_referenced_columns(&arena, |_, column| { + right_columns.contains(column) + })? { + (None, Some(expr), None, 1) + } else { + (None, None, Some(expr), 1) + }; + assert_eq!( + super::classify_join_filters(expr, &mut children, &mut arena)?, + expected + ); + } + } + Ok(()) + } + #[test] fn test_push_predicate_into_scan() -> Result<(), DatabaseError> { let table_state = build_t1_table()?; diff --git a/src/orm/mod.rs b/src/orm/mod.rs index 3693b19c..e0755d38 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -2062,7 +2062,7 @@ where bind_orm_source(&mut right_binder, source, Some(join_type), self.arena)?; (right_plan, right_binder.context) }; - self.binder.extend(right_context); + self.binder.extend(right_context, self.arena); let on = with_query_bind_step!(self.binder, QueryBindStep::From, { let mut scope = self.expr_scope(); let handle = scope.handle(); @@ -2699,7 +2699,7 @@ where let table_name: TableName = M::table_name().into(); let source = binder .context - .source_and_bind(table_name.clone(), None, None, false)? + .source_and_bind(table_name.clone(), None, None, false, arena)? .ok_or(DatabaseError::TableNotFound)?; let params = params.into_iter().collect::>(); let mut schema_ref = Vec::with_capacity(M::fields().len()); diff --git a/tests/slt/where.slt b/tests/slt/where.slt index 0700e0c7..5152726f 100644 --- a/tests/slt/where.slt +++ b/tests/slt/where.slt @@ -422,6 +422,57 @@ select cast(-2147483649.0 as int); statement ok drop table alias_input; +# Name lookup order is independent of FROM layout and wildcard expansion. +statement ok +create table scope_a(id int primary key, v int); + +statement ok +create table scope_b(id int primary key, w int); + +statement ok +create table scope_c(id int primary key, v int, z int); + +statement ok +insert into scope_a values(1,10); + +statement ok +insert into scope_b values(2,10); + +statement ok +insert into scope_c values(1,20,30); + +query IIII +select * from scope_a a join scope_b b on v=b.w; +---- +1 10 2 10 + +query IIII +select b.*,a.* from scope_a a join scope_b b on v=b.w; +---- +2 10 1 10 + +query III +select a.v,w,z from scope_a a join scope_b b on v=b.w join scope_c c on a.id=c.id; +---- +10 10 30 + +statement error is ambiguous +select v from scope_a a join scope_b b on v=b.w join scope_c c on a.id=c.id; + +query III +select w,(select w+z from scope_c where scope_c.id=a.id),(select v from scope_c where scope_c.id=a.id) from scope_a a join scope_b b on v=b.w; +---- +10 40 20 + +statement ok +drop table scope_a; + +statement ok +drop table scope_b; + +statement ok +drop table scope_c; + # Native point-list predicates cross the 5/6 expansion boundary. Repeat them # before and after index creation so range elimination agrees with full scans. statement ok From 4ca0a57d59e02eee37b3127d2f5c90fa673f3fad Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 27/62] refactor: simplify project elimination and child traversal Rewrite Project elimination as local bottom-up transformations and bound join-key source searches. Add guarded child traversal to operator visitors and adapt expression consumers. --- src/db.rs | 4 +- src/optimizer/heuristic/optimizer.rs | 2 +- src/optimizer/plan_utils.rs | 14 +- .../rule/normalization/column_pruning.rs | 21 +- .../normalization/compilation_in_advance.rs | 10 +- .../rule/normalization/elimination.rs | 516 ++++++++---------- .../rule/normalization/join_reorder.rs | 9 +- src/optimizer/rule/normalization/mod.rs | 4 - .../rule/normalization/simplification.rs | 10 +- src/planner/arena.rs | 17 +- src/planner/operator/mod.rs | 53 +- src/planner/operator/visitor.rs | 291 +++++++--- src/planner/operator/visitor_mut.rs | 253 +++++++-- 13 files changed, 767 insertions(+), 437 deletions(-) diff --git a/src/db.rs b/src/db.rs index decac063..1bc7c92b 100644 --- a/src/db.rs +++ b/src/db.rs @@ -483,7 +483,7 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { ) .before_batch( "Collapse Project", - HepBatchStrategy::fix_point_topdown(10), + HepBatchStrategy::fix_point_bottomup(10), vec![NormalizationRuleImpl::EliminateProject], ) .before_batch( @@ -536,7 +536,7 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { ) .after_batch( "Collapse Project", - HepBatchStrategy::fix_point_topdown(10), + HepBatchStrategy::fix_point_bottomup(10), vec![NormalizationRuleImpl::EliminateProject], ) .after_batch( diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index ef997448..252e1ed0 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -167,7 +167,7 @@ impl<'a> HepOptimizer<'a> { loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { match pass.kind { - WholeTreePassKind::ColumnPruning | WholeTreePassKind::EliminateProject => { + WholeTreePassKind::ColumnPruning => { let mut applied = false; for rule in &pass.rules { applied |= rule.apply(plan, arena, loader)?; diff --git a/src/optimizer/plan_utils.rs b/src/optimizer/plan_utils.rs index ad94842c..a8974c37 100644 --- a/src/optimizer/plan_utils.rs +++ b/src/optimizer/plan_utils.rs @@ -18,7 +18,7 @@ use crate::expression::visitor::ExprVisitor; use crate::expression::{AliasType, ScalarExpression}; use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::table_scan::TableScanOperator; -use crate::planner::operator::visitor::OperatorVisitor; +use crate::planner::operator::visitor::{ChildrenVisit, OperatorVisitor}; use crate::planner::operator::{Operator, RowCountOption}; use crate::planner::{Childrens, LogicalPlan, MetaArena, PlanArena}; use crate::types::ColumnId; @@ -128,7 +128,11 @@ impl<'plan, A: MetaArena> OperatorVisitor<'plan> for ScanColumnVisitor<'plan, '_ if self.column.is_none() { return Ok(()); } - self.visit_operator(&plan.operator, plan.physical_option.as_ref())?; + self.visit_operator( + &plan.operator, + plan.physical_option.as_ref(), + &mut ChildrenVisit::empty(), + )?; let follow = match &plan.operator { Operator::Project(_) | Operator::Filter(_) => true, Operator::Join(_) | Operator::Aggregate(_) | Operator::Limit(_) | Operator::TopK(_) => { @@ -170,7 +174,11 @@ impl<'plan, A: MetaArena> OperatorVisitor<'plan> for ScanColumnVisitor<'plan, '_ Ok(()) } - fn visit_project(&mut self, project: &'plan ProjectOperator) -> Result<(), DatabaseError> { + fn visit_project( + &mut self, + project: &'plan ProjectOperator, + _children: &mut ChildrenVisit<'plan>, + ) -> Result<(), DatabaseError> { let Some(column) = self.column.take() else { return Ok(()); }; diff --git a/src/optimizer/rule/normalization/column_pruning.rs b/src/optimizer/rule/normalization/column_pruning.rs index acd37f33..22033018 100644 --- a/src/optimizer/rule/normalization/column_pruning.rs +++ b/src/optimizer/rule/normalization/column_pruning.rs @@ -23,8 +23,10 @@ use crate::optimizer::rule::normalization::{ remap_expr_positions, remap_exprs_positions, PositionRemapper, }; use crate::planner::operator::join::JoinCondition; -use crate::planner::operator::visitor::{OperatorExprVisitor, OperatorVisitor}; -use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; +use crate::planner::operator::visitor::{ChildrenVisit, OperatorExprVisitor, OperatorVisitor}; +use crate::planner::operator::visitor_mut::{ + ChildrenVisitMut, OperatorExprVisitorMut, OperatorVisitorMut, +}; use crate::planner::operator::Operator; use crate::planner::MetaArena; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; @@ -122,8 +124,11 @@ impl ColumnPruning { referenced_columns, arena, }; - OperatorExprVisitor::new(&mut collector, arena as &dyn MetaArena) - .visit_operator(operator, None)?; + OperatorExprVisitor::new(&mut collector, arena as &dyn MetaArena).visit_operator( + operator, + None, + &mut ChildrenVisit::empty(), + )?; struct ReferencedOperatorColumnCollector<'a, 'p> { referenced_columns: &'a mut ReferencedColumns, @@ -134,6 +139,7 @@ impl ColumnPruning { fn visit_mark_apply( &mut self, op: &'a crate::planner::operator::mark_apply::MarkApplyOperator, + _children: &mut ChildrenVisit<'a>, ) -> Result<(), DatabaseError> { if !matches!( op.kind, @@ -166,6 +172,7 @@ impl ColumnPruning { fn visit_union( &mut self, op: &'a crate::planner::operator::union::UnionOperator, + _children: &mut ChildrenVisit<'a>, ) -> Result<(), DatabaseError> { self.referenced_columns.extend( op.left_schema_ref @@ -180,6 +187,7 @@ impl ColumnPruning { fn visit_set_membership( &mut self, op: &'a crate::planner::operator::set_membership::SetMembershipOperator, + _children: &mut ChildrenVisit<'a>, ) -> Result<(), DatabaseError> { self.referenced_columns.extend( op.left_schema_ref @@ -194,6 +202,7 @@ impl ColumnPruning { fn visit_delete( &mut self, op: &'a crate::planner::operator::delete::DeleteOperator, + _children: &mut ChildrenVisit<'a>, ) -> Result<(), DatabaseError> { self.referenced_columns .extend(op.primary_keys.iter().copied(), self.arena); @@ -205,7 +214,7 @@ impl ColumnPruning { referenced_columns, arena, } - .visit_operator(operator, None) + .visit_operator(operator, None, &mut ChildrenVisit::empty()) } fn extend_expr_referenced_columns<'a>( @@ -262,7 +271,7 @@ impl ColumnPruning { &mut PositionRemapper::new(removed_positions, remapped_exprs), arena, ) - .visit_operator(operator, None) + .visit_operator(operator, None, &mut ChildrenVisitMut::empty()) } fn remap_exprs_after_child_change<'a>( diff --git a/src/optimizer/rule/normalization/compilation_in_advance.rs b/src/optimizer/rule/normalization/compilation_in_advance.rs index 5eac05c1..9e183ea8 100644 --- a/src/optimizer/rule/normalization/compilation_in_advance.rs +++ b/src/optimizer/rule/normalization/compilation_in_advance.rs @@ -16,7 +16,9 @@ use crate::errors::DatabaseError; use crate::expression::BindEvaluator; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; +use crate::planner::operator::visitor_mut::{ + ChildrenVisitMut, OperatorExprVisitorMut, OperatorVisitorMut, +}; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan, PlanArena}; @@ -28,7 +30,11 @@ pub(crate) fn evaluator_bind_current( arena: &mut PlanArena, ) -> Result<(), DatabaseError> { let mut evaluator = BindEvaluator; - OperatorExprVisitorMut::new(&mut evaluator, arena).visit_operator(&mut plan.operator, None) + OperatorExprVisitorMut::new(&mut evaluator, arena).visit_operator( + &mut plan.operator, + None, + &mut ChildrenVisitMut::empty(), + ) } impl EvaluatorBind { diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index d120706d..c31d5716 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; +use crate::expression::visitor::ExprVisitor; use crate::expression::visitor_mut::ExprVisitorMut; use crate::expression::{AliasType, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; @@ -20,261 +21,261 @@ use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::{only_child_mut, replace_with_only_child, wrap_child_with}; use crate::planner::operator::join::{JoinCondition, JoinType}; use crate::planner::operator::limit::LimitOperator; -use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::table_scan::TableScanOperator; -use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; +use crate::planner::operator::visitor::{ChildrenVisit, OperatorExprVisitor, OperatorVisitor}; +use crate::planner::operator::visitor_mut::{ + ChildrenVisitMut, OperatorExprVisitorMut, OperatorVisitorMut, +}; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; use crate::types::index::{IndexLookup, IndexOrderHint}; -use std::collections::HashSet; pub struct EliminateProject; -#[derive(Clone, Copy, PartialEq, Eq)] -enum OutputMode { - Preserve, - Inline, - ColumnsOnly, +struct Substitute<'m> { + exprs: Option<&'m [ExprRef]>, + offset: usize, } -#[derive(Default)] -struct Substitute { - exprs: Option>, - visited: HashSet, - offset: usize, - opaque: bool, +struct OpaqueCheck { + found: bool, +} + +impl ExprVisitor for OpaqueCheck { + fn visit_expression_ref(&mut self, expr: ExprRef, arena: &A) -> Result { + if self.found { + return Ok(false); + } + self.found = matches!( + arena.expression(expr), + ScalarExpression::ScalaFunction(_) | ScalarExpression::TableFunction(_) + ); + Ok(!self.found) + } } -impl ExprVisitorMut for Substitute { +fn contains_opaque( + exprs: impl Iterator, + arena: &A, +) -> Result { + let mut check = OpaqueCheck { found: false }; + for expr in exprs { + ExprVisitor::visit(&mut check, expr, arena)?; + if check.found { + break; + } + } + Ok(check.found) +} + +impl ExprVisitorMut for Substitute<'_> { fn visit_expression_ref( &mut self, expr: &mut ExprRef, arena: &mut A, ) -> Result { - if matches!( - arena.expression(*expr), - ScalarExpression::ScalaFunction(_) | ScalarExpression::TableFunction(_) - ) { - self.opaque = true; - return Ok(false); - } - if let ScalarExpression::ColumnRef { column, position } = arena.expression(*expr) { - if let Some(exprs) = &self.exprs { - *expr = exprs[*position]; - } else if self.offset != 0 { - *expr = arena.alloc_expression(ScalarExpression::column_expr( - *column, - self.offset + *position, - )); + match arena.expression(*expr) { + ScalarExpression::ColumnRef { position, column } => { + let (position, column) = (*position, *column); + if let Some(replacement) = self.exprs.and_then(|exprs| exprs.get(position)) { + // Do not revisit or mutate the replacement's input expression. + *expr = *replacement; + } else if self.offset != 0 { + *expr = arena.alloc_expression(ScalarExpression::column_expr( + column, + position + self.offset, + )); + } + Ok(false) + } + ScalarExpression::Constant(_) + | ScalarExpression::OuterParam { .. } + | ScalarExpression::OuterValue { .. } + | ScalarExpression::InitValue { .. } + | ScalarExpression::Empty => Ok(false), + _ => { + // Reached only for a matched Project rewrite or a nonzero Cross offset. + // Copy before changing child refs: a retained operator may share this node + // but still require its original input positions. + let expression = arena.expression(*expr).clone(); + *expr = arena.alloc_expression(expression); + Ok(true) } - return Ok(false); // Do not revisit the replacement's input layout. } - Ok((self.exprs.is_none() && self.offset == 0) || self.visited.insert(*expr)) } } -struct EliminateVisitor<'a, 'b> { - arena: &'a mut PlanArena<'b>, - mapping: Substitute, - output: OutputMode, - changed: bool, +fn movable_project(plan: &LogicalPlan, arena: &PlanArena) -> Result { + let (Operator::Project(op), Childrens::Only(_)) = (&plan.operator, plan.childrens.as_ref()) + else { + return Ok(false); + }; + Ok(!contains_opaque(op.exprs.iter().copied(), arena)?) } -impl EliminateVisitor<'_, '_> { - fn visit_input( - &mut self, - input: &mut LogicalPlan, - output: OutputMode, - apply: impl FnOnce(&mut Self, &mut LogicalPlan) -> Result<(), DatabaseError>, - ) -> Result<(), DatabaseError> { - self.output = output; - self.visit_plan(input)?; - apply(self, input) +fn lift_cross_project( + plan: &mut LogicalPlan, + arena: &mut PlanArena, +) -> Result { + let Childrens::Twins { left, right } = plan.childrens.as_mut() else { + return Ok(false); + }; + let (input, other, from_right) = if movable_project(left, arena)? { + (left, right, false) + } else if movable_project(right, arena)? { + (right, left, true) + } else { + return Ok(false); + }; + let (Operator::Project(op), Childrens::Only(base)) = + (&mut input.operator, input.childrens.as_mut()) + else { + return Ok(false); + }; + let offset = if from_right { + other.output_schema(arena).len() + } else { + base.output_schema(arena).len() + }; + if from_right && offset != 0 { + let mut shift = Substitute { + exprs: None, + offset, + }; + for expr in &mut op.exprs { + ExprVisitorMut::visit(&mut shift, expr, arena)?; + } + } + let identity_start = if from_right { 0 } else { offset }; + let columns = other.output_schema(arena); + op.exprs + .extend(columns.iter().enumerate().map(|(index, column)| { + arena.alloc_expression(ScalarExpression::column_expr( + *column, + identity_start + index, + )) + })); + if from_right { + op.exprs.rotate_right(offset); } + op.moveable = None; + + let mut project = input.take(); + **input = std::mem::replace(&mut *project.childrens, Childrens::None).pop_only(); + let mut join = plan.take(); + + join.reset_output_schema_cache(); + join.clear_startup_work(); + + *project.childrens = Childrens::Only(Box::new(join)); + project.physical_option = None; + project.reset_output_schema_cache(); + project.clear_startup_work(); + + *plan = project; + Ok(true) } -impl<'p> OperatorVisitorMut<'p> for EliminateVisitor<'_, '_> { - fn visit_plan(&mut self, plan: &'p mut LogicalPlan) -> Result<(), DatabaseError> { - let output = self.output; - let schema = plan.output_schema(self.arena).clone(); - - self.mapping.exprs = None; - self.mapping.visited.clear(); - self.mapping.opaque = false; - OperatorExprVisitorMut::new(&mut self.mapping, self.arena) - .visit_operator(&mut plan.operator, None)?; - let opaque = self.mapping.opaque; - match plan.childrens.as_mut() { - Childrens::Only(child) => { - let input_schema = child.output_schema(self.arena); - let forwards = schema.starts_with(input_schema); - let produces = - matches!(plan.operator, Operator::Project(_) | Operator::Aggregate(_)); - // A scalar subquery must materialize its value before generating - // the NULL row for an empty input. Other new layouts are also - // boundaries unless expressions explicitly produce their slots. - let input_mode = if opaque - || matches!(plan.operator, Operator::ScalarSubquery(_)) - || !(forwards || produces) - { - OutputMode::Preserve - } else { - match (output, produces) { - (OutputMode::ColumnsOnly, false) => OutputMode::ColumnsOnly, - _ => OutputMode::Inline, - } - }; - self.visit_input(child, input_mode, |visitor, child| { - let Some(_) = &visitor.mapping.exprs else { - return Ok(()); - }; - OperatorExprVisitorMut::new(&mut visitor.mapping, visitor.arena) - .visit_operator(&mut plan.operator, None)?; - if !produces { - let width = child.output_schema(visitor.arena).len(); - if let Some(exprs) = &mut visitor.mapping.exprs { - for (index, column) in schema[exprs.len()..].iter().enumerate() { - exprs.push(visitor.arena.alloc_expression( - ScalarExpression::column_expr(*column, width + index), - )); - } - } - } - Ok(()) - })?; - } - // TODO: Handle other join types and conditions appropriately. - Childrens::Twins { left, right } - if matches!(&plan.operator, Operator::Join(op) - if op.join_type == JoinType::Cross && op.on == JoinCondition::None) => - { - let old_left_width = left.output_schema(self.arena).len(); - let left_mode = match output { - OutputMode::ColumnsOnly => OutputMode::ColumnsOnly, - _ => OutputMode::Inline, - }; - let mut left_mapping = None; - self.visit_input(left, left_mode, |visitor, _| { - left_mapping = visitor.mapping.exprs.take(); - Ok(()) - })?; - let offset = left.output_schema(self.arena).len(); - self.visit_input(right, OutputMode::ColumnsOnly, |visitor, _| { - let mut right_mapping = visitor.mapping.exprs.take(); - if let Some(exprs) = right_mapping.as_mut().filter(|_| offset != 0) { - visitor.mapping.offset = offset; - visitor.mapping.visited.clear(); - for expr in exprs { - // Keep aliases shared with retained branches untouched. - *expr = expr.unpack_alias(visitor.arena); - ExprVisitorMut::visit(&mut visitor.mapping, expr, visitor.arena)?; - } - visitor.mapping.offset = 0; - } - let mut append_identity = |exprs: &mut Vec<_>, columns: &[_], start| { - exprs.extend(columns.iter().enumerate().map(|(index, column)| { - visitor - .arena - .alloc_expression(ScalarExpression::column_expr( - *column, - start + index, - )) - })); - }; - match (left_mapping.as_mut(), right_mapping.as_mut()) { - (Some(left), Some(right)) => left.append(right), - (Some(left), None) => { - append_identity(left, &schema[old_left_width..], offset) - } - (None, Some(right)) => { - append_identity(right, &schema[..old_left_width], 0); - right.rotate_right(old_left_width); - } - (None, None) => (), - } - visitor.mapping.exprs = left_mapping.or(right_mapping); - visitor.mapping.visited.clear(); - Ok(()) - })?; - } - Childrens::Twins { left, right } => { - // Other binary operators retain their input layouts. - self.visit_input(left, OutputMode::Preserve, |_, _| Ok(()))?; - self.visit_input(right, OutputMode::Preserve, |_, _| Ok(()))?; - } - Childrens::None => (), +impl NormalizationRule for EliminateProject { + fn apply( + &self, + plan: &mut LogicalPlan, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, + ) -> Result { + if matches!(&plan.operator, Operator::Join(op) + if op.join_type == JoinType::Cross && op.on == JoinCondition::None) + { + return lift_cross_project(plan, arena); + } + if !matches!( + plan.operator, + Operator::Project(_) + | Operator::Filter(_) + | Operator::Sort(_) + | Operator::Limit(_) + | Operator::TopK(_) + ) { + return Ok(false); + } + let Childrens::Only(child) = plan.childrens.as_ref() else { + return Ok(false); + }; + if !movable_project(child, arena)? { + return Ok(false); } + let Operator::Project(project) = &child.operator else { + return Ok(false); + }; + let mut check = OpaqueCheck { found: false }; + OperatorExprVisitor::new(&mut check, arena).visit_operator( + &plan.operator, + None, + &mut ChildrenVisit::empty(), + )?; + if check.found { + return Ok(false); + } + let mut substitute = Substitute { + exprs: Some(&project.exprs), + offset: 0, + }; + let merge = matches!(plan.operator, Operator::Project(_)); + let schema = if let Operator::Project(op) = &plan.operator { + Some( + op.exprs + .iter() + .map(|expr| expr.output_column_ref(arena)) + .collect::>(), + ) + } else { + None + }; + OperatorExprVisitorMut::new(&mut substitute, arena).visit_operator( + &mut plan.operator, + None, + &mut ChildrenVisitMut::empty(), + )?; match &mut plan.operator { Operator::Project(op) => { - self.mapping.exprs = None; // Project defines a new output slot space. - if !opaque - && match output { - OutputMode::Preserve => false, - OutputMode::Inline => true, - OutputMode::ColumnsOnly => op.exprs.iter().all(|expr| { - matches!( - expr.unpack_alias_ref(self.arena), - ScalarExpression::ColumnRef { .. } - ) - }), - } - { - let exprs = std::mem::take(&mut op.exprs); - replace_with_only_child(plan); - self.mapping.exprs = Some(exprs); - self.mapping.visited.clear(); - self.changed = true; - return Ok(()); - } - for (expr, column) in op.exprs.iter_mut().zip(&schema) { - let output = expr.output_column_ref(self.arena); - if self.arena.column(output).name() != self.arena.column(*column).name() { - let alias = AliasType::Name(self.arena.column(*column).name().to_string()); - *expr = self - .arena - .alloc_expression(ScalarExpression::Alias { expr: *expr, alias }); + if let Some(schema) = schema { + for (expr, column) in op.exprs.iter_mut().zip(schema) { + let current = expr.output_column_ref(arena); + if arena.column(current).name() != arena.column(column).name() { + let alias = AliasType::Name(arena.column(column).name().to_string()); + *expr = arena + .alloc_expression(ScalarExpression::Alias { expr: *expr, alias }); + } } } op.moveable = None; } - Operator::Aggregate(_) => self.mapping.exprs = None, + Operator::Filter(op) => op.is_optimized = false, _ => (), } - plan.reset_output_schema_cache(); - if self.changed { - plan.clear_startup_work(); - } - self.mapping.exprs = match (output, self.mapping.exprs.take()) { - (OutputMode::Preserve, Some(exprs)) => { - let input = plan.take(); - *plan = LogicalPlan::new( - Operator::Project(ProjectOperator::new(exprs)), - Childrens::Only(Box::new(input)), - ); - None - } - (_, exprs) => exprs, - }; - Ok(()) - } -} - -impl NormalizationRule for EliminateProject { - fn apply( - &self, - plan: &mut LogicalPlan, - arena: &mut PlanArena, - _loader: Option<&StatisticMetaLoader<'_>>, - ) -> Result { - let mut visitor = EliminateVisitor { - arena, - mapping: Substitute::default(), - output: OutputMode::Preserve, - changed: false, + let Childrens::Only(child) = plan.childrens.as_mut() else { + return Ok(false); }; - visitor.visit_plan(plan)?; - Ok(visitor.changed) + if merge { + replace_with_only_child(child); + } else { + // Lift the Project: its expressions keep the parent's output layout. + let mut project = child.take(); + let mut parent = plan.take(); + + std::mem::swap(&mut parent.childrens, &mut project.childrens); + + parent.reset_output_schema_cache(); + parent.clear_startup_work(); + + *project.childrens = Childrens::Only(Box::new(parent)); + project.physical_option = None; + *plan = project; + } + plan.reset_output_schema_cache(); + plan.clear_startup_work(); + Ok(true) } } @@ -770,8 +771,8 @@ pub(crate) fn covers( #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::{ - EliminateIndexFilter, EliminateProject, EliminateRedundantSort, EliminateVisitor, - ForceSpillAggregate, OutputMode, Substitute, UseStreamAggregate, + EliminateIndexFilter, EliminateProject, EliminateRedundantSort, ForceSpillAggregate, + UseStreamAggregate, }; use crate::binder::test::build_t1_table; use crate::catalog::{ColumnCatalog, TableName}; @@ -783,7 +784,6 @@ mod tests { use crate::optimizer::heuristic::batch::HepBatchStrategy; use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; use crate::optimizer::rule::normalization::NormalizationRuleImpl; - use crate::planner::operator::aggregate::AggregateOperator; use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::operator::limit::LimitOperator; @@ -1143,57 +1143,6 @@ mod tests { Ok(()) } - #[test] - fn cross_join_callback_reuses_an_existing_mapping_buffer() -> Result<(), DatabaseError> { - for (map_left, map_right, expected) in [ - (true, true, [1, 0, 3, 2]), - (true, false, [1, 0, 2, 3]), - (false, true, [0, 1, 3, 2]), - ] { - let table_arena = crate::planner::TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - let mut left = values(&mut arena, &["l0", "l1"]); - let mut right = values(&mut arena, &["r0", "r1"]); - if map_left { - left = permutation(&mut arena, left, &[1, 0]); - } - if map_right { - right = permutation(&mut arena, right, &[1, 0]); - } - let source = if map_left { &mut left } else { &mut right }; - let Operator::Project(op) = &mut source.operator else { - unreachable!() - }; - op.exprs.reserve(8); - let buffer = op.exprs.as_ptr(); - let mut plan = - JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); - let mut visitor = EliminateVisitor { - arena: &mut arena, - mapping: Substitute::default(), - output: OutputMode::Inline, - changed: false, - }; - visitor.visit_input(&mut plan, OutputMode::Inline, |visitor, _| { - let Some(exprs) = &visitor.mapping.exprs else { - panic!("expected a mapping") - }; - assert_eq!(exprs.as_ptr(), buffer); - assert_eq!(exprs.len(), expected.len()); - for (expr, expected) in exprs.iter().zip(expected) { - let ScalarExpression::ColumnRef { position, .. } = - expr.unpack_alias_ref(visitor.arena) - else { - unreachable!() - }; - assert_eq!(*position, expected); - } - Ok(()) - })?; - } - Ok(()) - } - #[test] fn cross_join_distinguishes_empty_mapping_from_identity() -> Result<(), DatabaseError> { let table_arena = crate::planner::TableArenaCell::default(); @@ -1220,18 +1169,31 @@ mod tests { } #[test] - fn cross_join_preserves_computed_right_output_without_copying_it() -> Result<(), DatabaseError> - { + fn cross_join_inlines_computed_right_output_without_changing_source( + ) -> Result<(), DatabaseError> { let table = build_t1_table()?; let mut arena = PlanArena::new(&table.table_arena); let scan = table.plan_with_arena("select c1, c2 from t1", &mut arena)?; - let (right, _) = source(&mut arena, scan); + let (right, original) = source(&mut arena, scan); let left = values(&mut arena, &["l0"]); let cross = JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); let mut plan = permutation(&mut arena, cross, &[0, 1, 2]); - assert!(!EliminateProject.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan.explain(&mut arena, 0).matches("Projection").count(), 2); + assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan.explain(&mut arena, 0).matches("Projection").count(), 1); + let Operator::Project(op) = &plan.operator else { + unreachable!() + }; + for (expr, expected) in [(op.exprs[1], 2), (original, 1)] { + let ScalarExpression::Binary { left_expr, .. } = expr.unpack_alias_ref(&arena) else { + unreachable!() + }; + let ScalarExpression::ColumnRef { position, .. } = left_expr.unpack_alias_ref(&arena) + else { + unreachable!() + }; + assert_eq!(*position, expected); + } Ok(()) } diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index adffbce0..1db54f2c 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -415,9 +415,12 @@ fn inspect<'plan>( for &(left, right) in on { let mut endpoints = [None, None]; - for (endpoint, expr) in endpoints.iter_mut().zip([left, right]) { - for (index, schema) in schemas.iter().enumerate() { - if key_in_schema(expr, schema, arena)? { + for (endpoint, (expr, range)) in endpoints + .iter_mut() + .zip([(left, start..middle), (right, middle..schemas.len())]) + { + for index in range { + if key_in_schema(expr, schemas[index], arena)? { *endpoint = Some(index); break; } diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index 298ab5c0..9b2700b1 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -96,7 +96,6 @@ pub enum NormalizationRuleImpl { #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum WholeTreePassKind { ColumnPruning, - EliminateProject, ExpressionRewrite, } @@ -171,9 +170,6 @@ impl NormalizationRuleImpl { NormalizationRuleImpl::ColumnPruning => { NormalizationPassKind::WholeTreePass(WholeTreePassKind::ColumnPruning) } - NormalizationRuleImpl::EliminateProject => { - NormalizationPassKind::WholeTreePass(WholeTreePassKind::EliminateProject) - } NormalizationRuleImpl::ConstantCalculation | NormalizationRuleImpl::EvaluatorBind => { NormalizationPassKind::WholeTreePass(WholeTreePassKind::ExpressionRewrite) } diff --git a/src/optimizer/rule/normalization/simplification.rs b/src/optimizer/rule/normalization/simplification.rs index b86eaf6b..3be9ec53 100644 --- a/src/optimizer/rule/normalization/simplification.rs +++ b/src/optimizer/rule/normalization/simplification.rs @@ -17,7 +17,9 @@ use crate::expression::simplify::{ConstantCalculator, Simplify}; use crate::expression::visitor_mut::ExprVisitorMut; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; +use crate::planner::operator::visitor_mut::{ + ChildrenVisitMut, OperatorExprVisitorMut, OperatorVisitorMut, +}; use crate::planner::operator::Operator; use crate::planner::PlanArena; use crate::planner::{Childrens, LogicalPlan, MetaArena}; @@ -30,7 +32,11 @@ pub(crate) fn constant_calculation_current( arena: &mut A, ) -> Result<(), DatabaseError> { let mut calculator = ConstantCalculator::new(arena); - OperatorExprVisitorMut::new(&mut calculator, arena).visit_operator(&mut plan.operator, None) + OperatorExprVisitorMut::new(&mut calculator, arena).visit_operator( + &mut plan.operator, + None, + &mut ChildrenVisitMut::empty(), + ) } impl ConstantCalculation { diff --git a/src/planner/arena.rs b/src/planner/arena.rs index 7473d107..656d338c 100644 --- a/src/planner/arena.rs +++ b/src/planner/arena.rs @@ -18,7 +18,7 @@ use crate::expression::visitor_mut::ExprVisitorMut; use crate::expression::ScalarExpression; use crate::planner::operator::scalar_query_init::ScalarQueryInitOperator; use crate::planner::operator::visitor_mut::{ - walk_mut_operator, OperatorExprVisitorMut, OperatorVisitorMut, + walk_mut_operator, ChildrenVisitMut, OperatorExprVisitorMut, OperatorVisitorMut, }; use crate::planner::operator::{Operator, PhysicalOption}; use crate::planner::{LogicalPlan, ScalarQueryRef}; @@ -87,15 +87,20 @@ impl<'a, A: MetaArena + ?Sized> OperatorVisitorMut<'a> for ScalarQueryPlanReloca &mut self, operator: &'a mut Operator, physical_option: Option<&'a mut PhysicalOption>, + children: &mut ChildrenVisitMut<'a>, ) -> Result<(), DatabaseError> { - OperatorExprVisitorMut::new(&mut self.references, self.arena) - .visit_operator(operator, physical_option)?; - walk_mut_operator(self, operator) + OperatorExprVisitorMut::new(&mut self.references, self.arena).visit_operator( + operator, + physical_option, + &mut ChildrenVisitMut::empty(), + )?; + walk_mut_operator(self, operator, children) } fn visit_scalar_query_init( &mut self, op: &'a mut ScalarQueryInitOperator, + _children: &mut ChildrenVisitMut<'a>, ) -> Result<(), DatabaseError> { for (reference, _) in &mut op.param_bindings { self.references.relocate(reference); @@ -252,7 +257,7 @@ pub trait MetaArena { } fn same_column(&self, left: ColumnRef, right: ColumnRef) -> bool { - self.column(left).summary() == self.column(right).summary() + left == right || self.column(left).summary() == self.column(right).summary() } fn clone_column(&self, column: ColumnRef) -> ColumnCatalog { @@ -880,7 +885,7 @@ impl<'a> PlanArena<'a> { } pub(crate) fn same_column(&self, left: ColumnRef, right: ColumnRef) -> bool { - self.column(left).summary() == self.column(right).summary() + left == right || self.column(left).summary() == self.column(right).summary() } pub(crate) fn nullable_for_join( diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 22f3b74c..aa6eaace 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -97,7 +97,7 @@ use crate::planner::operator::truncate::TruncateOperator; use crate::planner::operator::union::UnionOperator; use crate::planner::operator::update::UpdateOperator; use crate::planner::operator::values::ValuesOperator; -use crate::planner::operator::visitor::OperatorVisitor; +use crate::planner::operator::visitor::{ChildrenVisit, OperatorVisitor}; use crate::planner::{fmt_explain_list, Childrens, Explain, ExprRef, LogicalPlan, MetaArena}; use crate::types::index::{IndexInfo, IndexMetaRef}; use kite_sql_serde_macros::ReferenceSerialization; @@ -595,6 +595,7 @@ impl Operator { fn visit_aggregate( &mut self, op: &'operator AggregateOperator, + _children: &mut ChildrenVisit<'operator>, ) -> Result<(), DatabaseError> { for expr in op.agg_calls.iter().chain(&op.groupby_exprs) { ExprVisitor::visit(self, *expr, self.arena)?; @@ -605,6 +606,7 @@ impl Operator { fn visit_scalar_query_init( &mut self, op: &'operator ScalarQueryInitOperator, + _children: &mut ChildrenVisit<'operator>, ) -> Result<(), DatabaseError> { for (_, expr) in &op.param_bindings { ExprVisitor::visit(self, *expr, self.arena)?; @@ -615,6 +617,7 @@ impl Operator { fn visit_mark_apply( &mut self, op: &'operator MarkApplyOperator, + _children: &mut ChildrenVisit<'operator>, ) -> Result<(), DatabaseError> { for expr in &op.predicates { ExprVisitor::visit(self, *expr, self.arena)?; @@ -628,11 +631,19 @@ impl Operator { Ok(()) } - fn visit_filter(&mut self, op: &'operator FilterOperator) -> Result<(), DatabaseError> { + fn visit_filter( + &mut self, + op: &'operator FilterOperator, + _children: &mut ChildrenVisit<'operator>, + ) -> Result<(), DatabaseError> { ExprVisitor::visit(self, op.predicate, self.arena) } - fn visit_join(&mut self, op: &'operator JoinOperator) -> Result<(), DatabaseError> { + fn visit_join( + &mut self, + op: &'operator JoinOperator, + _children: &mut ChildrenVisit<'operator>, + ) -> Result<(), DatabaseError> { if let JoinCondition::On { on, filter } = &op.on { for (left_expr, right_expr) in on { ExprVisitor::visit(self, *left_expr, self.arena)?; @@ -648,6 +659,7 @@ impl Operator { fn visit_project( &mut self, op: &'operator ProjectOperator, + _children: &mut ChildrenVisit<'operator>, ) -> Result<(), DatabaseError> { for expr in &op.exprs { ExprVisitor::visit(self, *expr, self.arena)?; @@ -675,7 +687,11 @@ impl Operator { Ok(()) } - fn visit_sort(&mut self, op: &'operator SortOperator) -> Result<(), DatabaseError> { + fn visit_sort( + &mut self, + op: &'operator SortOperator, + _children: &mut ChildrenVisit<'operator>, + ) -> Result<(), DatabaseError> { for field in &op.sort_fields { ExprVisitor::visit(self, field.expr, self.arena)?; } @@ -685,6 +701,7 @@ impl Operator { fn visit_window( &mut self, op: &'operator window::WindowOperator, + _children: &mut ChildrenVisit<'operator>, ) -> Result<(), DatabaseError> { for expr in op .sort_fields @@ -697,7 +714,11 @@ impl Operator { Ok(()) } - fn visit_top_k(&mut self, op: &'operator TopKOperator) -> Result<(), DatabaseError> { + fn visit_top_k( + &mut self, + op: &'operator TopKOperator, + _children: &mut ChildrenVisit<'operator>, + ) -> Result<(), DatabaseError> { for field in &op.sort_fields { ExprVisitor::visit(self, field.expr, self.arena)?; } @@ -711,7 +732,11 @@ impl Operator { Ok(()) } - fn visit_union(&mut self, op: &'operator UnionOperator) -> Result<(), DatabaseError> { + fn visit_union( + &mut self, + op: &'operator UnionOperator, + _children: &mut ChildrenVisit<'operator>, + ) -> Result<(), DatabaseError> { for column in op.left_schema_ref.iter().chain(&op._right_schema_ref) { ExprVisitor::visit_column_ref(self, column)?; } @@ -721,6 +746,7 @@ impl Operator { fn visit_recursive_cte( &mut self, op: &'operator RecursiveCteOperator, + _children: &mut ChildrenVisit<'operator>, ) -> Result<(), DatabaseError> { for column in &op.schema_ref { ExprVisitor::visit_column_ref(self, column)?; @@ -741,6 +767,7 @@ impl Operator { fn visit_set_membership( &mut self, op: &'operator SetMembershipOperator, + _children: &mut ChildrenVisit<'operator>, ) -> Result<(), DatabaseError> { for column in op.left_schema_ref.iter().chain(&op._right_schema_ref) { ExprVisitor::visit_column_ref(self, column)?; @@ -748,14 +775,22 @@ impl Operator { Ok(()) } - fn visit_delete(&mut self, op: &'operator DeleteOperator) -> Result<(), DatabaseError> { + fn visit_delete( + &mut self, + op: &'operator DeleteOperator, + _children: &mut ChildrenVisit<'operator>, + ) -> Result<(), DatabaseError> { for column in &op.primary_keys { ExprVisitor::visit_column_ref(self, column)?; } Ok(()) } - fn visit_update(&mut self, op: &'operator UpdateOperator) -> Result<(), DatabaseError> { + fn visit_update( + &mut self, + op: &'operator UpdateOperator, + _children: &mut ChildrenVisit<'operator>, + ) -> Result<(), DatabaseError> { for (_, expr) in &op.value_exprs { ExprVisitor::visit(self, *expr, self.arena)?; } @@ -800,7 +835,7 @@ impl Operator { f, keep_going: true, }; - visitor.visit_operator(self, None)?; + visitor.visit_operator(self, None, &mut ChildrenVisit::empty())?; Ok(visitor.keep_going) } diff --git a/src/planner/operator/visitor.rs b/src/planner/operator/visitor.rs index 244c4fa4..5cbfa351 100644 --- a/src/planner/operator/visitor.rs +++ b/src/planner/operator/visitor.rs @@ -18,16 +18,36 @@ use crate::errors::DatabaseError; use crate::expression::visitor::ExprVisitor; use crate::planner::{Childrens, LogicalPlan, MetaArena}; +/// One node's children. Taking the reference records that traversal was handled. +pub struct ChildrenVisit<'a> { + children: Option<&'a Childrens>, +} + +impl<'a> ChildrenVisit<'a> { + pub fn new(children: &'a Childrens) -> Self { + Self { + children: Some(children), + } + } + + pub fn empty() -> Self { + Self { children: None } + } + + pub fn visit( + &mut self, + walk: impl FnOnce(&'a Childrens) -> Result<(), DatabaseError>, + ) -> Result<(), DatabaseError> { + if let Some(children) = self.children.take() { + walk(children)?; + } + Ok(()) + } +} + pub trait OperatorVisitor<'a>: Sized { fn visit_plan(&mut self, plan: &'a LogicalPlan) -> Result<(), DatabaseError> { - let LogicalPlan { - operator, - physical_option, - childrens, - .. - } = plan; - self.visit_operator(operator, physical_option.as_ref())?; - self.visit_children(childrens) + walk_plan(self, plan) } fn visit_children(&mut self, childrens: &'a Childrens) -> Result<(), DatabaseError> { @@ -45,8 +65,9 @@ pub trait OperatorVisitor<'a>: Sized { &mut self, operator: &'a Operator, physical_option: Option<&'a PhysicalOption>, + children: &mut ChildrenVisit<'a>, ) -> Result<(), DatabaseError> { - walk_operator(self, operator)?; + walk_operator(self, operator, children)?; if let Some(physical_option) = physical_option { self.visit_physical_option(physical_option)?; } @@ -64,36 +85,58 @@ pub trait OperatorVisitor<'a>: Sized { Ok(()) } - fn visit_aggregate(&mut self, _op: &'a AggregateOperator) -> Result<(), DatabaseError> { + fn visit_aggregate( + &mut self, + _op: &'a AggregateOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } fn visit_scalar_query_init( &mut self, _op: &'a ScalarQueryInitOperator, + _children: &mut ChildrenVisit<'a>, ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_mark_apply(&mut self, _op: &'a MarkApplyOperator) -> Result<(), DatabaseError> { + fn visit_mark_apply( + &mut self, + _op: &'a MarkApplyOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_filter(&mut self, _op: &'a FilterOperator) -> Result<(), DatabaseError> { + fn visit_filter( + &mut self, + _op: &'a FilterOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_join(&mut self, _op: &'a JoinOperator) -> Result<(), DatabaseError> { + fn visit_join( + &mut self, + _op: &'a JoinOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_project(&mut self, _op: &'a ProjectOperator) -> Result<(), DatabaseError> { + fn visit_project( + &mut self, + _op: &'a ProjectOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } fn visit_scalar_subquery( &mut self, _op: &'a ScalarSubqueryOperator, + _children: &mut ChildrenVisit<'a>, ) -> Result<(), DatabaseError> { Ok(()) } @@ -106,19 +149,35 @@ pub trait OperatorVisitor<'a>: Sized { Ok(()) } - fn visit_sort(&mut self, _op: &'a SortOperator) -> Result<(), DatabaseError> { + fn visit_sort( + &mut self, + _op: &'a SortOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_window(&mut self, _op: &'a window::WindowOperator) -> Result<(), DatabaseError> { + fn visit_window( + &mut self, + _op: &'a window::WindowOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_limit(&mut self, _op: &'a LimitOperator) -> Result<(), DatabaseError> { + fn visit_limit( + &mut self, + _op: &'a LimitOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_top_k(&mut self, _op: &'a TopKOperator) -> Result<(), DatabaseError> { + fn visit_top_k( + &mut self, + _op: &'a TopKOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } @@ -134,7 +193,7 @@ pub trait OperatorVisitor<'a>: Sized { Ok(()) } - fn visit_explain(&mut self) -> Result<(), DatabaseError> { + fn visit_explain(&mut self, _children: &mut ChildrenVisit<'a>) -> Result<(), DatabaseError> { Ok(()) } @@ -145,15 +204,24 @@ pub trait OperatorVisitor<'a>: Sized { fn visit_set_membership( &mut self, _op: &'a SetMembershipOperator, + _children: &mut ChildrenVisit<'a>, ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_union(&mut self, _op: &'a UnionOperator) -> Result<(), DatabaseError> { + fn visit_union( + &mut self, + _op: &'a UnionOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_recursive_cte(&mut self, _op: &'a RecursiveCteOperator) -> Result<(), DatabaseError> { + fn visit_recursive_cte( + &mut self, + _op: &'a RecursiveCteOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } @@ -164,19 +232,35 @@ pub trait OperatorVisitor<'a>: Sized { Ok(()) } - fn visit_insert(&mut self, _op: &'a InsertOperator) -> Result<(), DatabaseError> { + fn visit_insert( + &mut self, + _op: &'a InsertOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_update(&mut self, _op: &'a UpdateOperator) -> Result<(), DatabaseError> { + fn visit_update( + &mut self, + _op: &'a UpdateOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_delete(&mut self, _op: &'a DeleteOperator) -> Result<(), DatabaseError> { + fn visit_delete( + &mut self, + _op: &'a DeleteOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_analyze(&mut self, _op: &'a AnalyzeOperator) -> Result<(), DatabaseError> { + fn visit_analyze( + &mut self, + _op: &'a AnalyzeOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } @@ -196,7 +280,11 @@ pub trait OperatorVisitor<'a>: Sized { Ok(()) } - fn visit_create_index(&mut self, _op: &'a CreateIndexOperator) -> Result<(), DatabaseError> { + fn visit_create_index( + &mut self, + _op: &'a CreateIndexOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } @@ -226,7 +314,11 @@ pub trait OperatorVisitor<'a>: Sized { } #[cfg(feature = "copy")] - fn visit_copy_to_file(&mut self, _op: &'a CopyToFileOperator) -> Result<(), DatabaseError> { + fn visit_copy_to_file( + &mut self, + _op: &'a CopyToFileOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } } @@ -252,7 +344,11 @@ impl<'a, V: ExprVisitor, A: MetaArena + ?Sized> OperatorVisitor<'a> Ok(()) } - fn visit_aggregate(&mut self, op: &'a AggregateOperator) -> Result<(), DatabaseError> { + fn visit_aggregate( + &mut self, + op: &'a AggregateOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { for expr in op.agg_calls.iter().chain(&op.groupby_exprs) { ExprVisitor::visit(self.visitor, *expr, self.arena)?; } @@ -262,6 +358,7 @@ impl<'a, V: ExprVisitor, A: MetaArena + ?Sized> OperatorVisitor<'a> fn visit_scalar_query_init( &mut self, op: &'a ScalarQueryInitOperator, + _children: &mut ChildrenVisit<'a>, ) -> Result<(), DatabaseError> { ExprVisitor::visit(self.visitor, op.value, self.arena)?; for (_, expr) in &op.param_bindings { @@ -270,7 +367,11 @@ impl<'a, V: ExprVisitor, A: MetaArena + ?Sized> OperatorVisitor<'a> Ok(()) } - fn visit_mark_apply(&mut self, op: &'a MarkApplyOperator) -> Result<(), DatabaseError> { + fn visit_mark_apply( + &mut self, + op: &'a MarkApplyOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { for expr in &op.predicates { ExprVisitor::visit(self.visitor, *expr, self.arena)?; } @@ -283,11 +384,19 @@ impl<'a, V: ExprVisitor, A: MetaArena + ?Sized> OperatorVisitor<'a> Ok(()) } - fn visit_filter(&mut self, op: &'a FilterOperator) -> Result<(), DatabaseError> { + fn visit_filter( + &mut self, + op: &'a FilterOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { ExprVisitor::visit(self.visitor, op.predicate, self.arena) } - fn visit_join(&mut self, op: &'a JoinOperator) -> Result<(), DatabaseError> { + fn visit_join( + &mut self, + op: &'a JoinOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { if let JoinCondition::On { on, filter } = &op.on { for (left_expr, right_expr) in on { ExprVisitor::visit(self.visitor, *left_expr, self.arena)?; @@ -300,7 +409,11 @@ impl<'a, V: ExprVisitor, A: MetaArena + ?Sized> OperatorVisitor<'a> Ok(()) } - fn visit_project(&mut self, op: &'a ProjectOperator) -> Result<(), DatabaseError> { + fn visit_project( + &mut self, + op: &'a ProjectOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { for expr in &op.exprs { ExprVisitor::visit(self.visitor, *expr, self.arena)?; } @@ -328,14 +441,22 @@ impl<'a, V: ExprVisitor, A: MetaArena + ?Sized> OperatorVisitor<'a> Ok(()) } - fn visit_sort(&mut self, op: &'a SortOperator) -> Result<(), DatabaseError> { + fn visit_sort( + &mut self, + op: &'a SortOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { for field in &op.sort_fields { ExprVisitor::visit(self.visitor, field.expr, self.arena)?; } Ok(()) } - fn visit_window(&mut self, op: &'a window::WindowOperator) -> Result<(), DatabaseError> { + fn visit_window( + &mut self, + op: &'a window::WindowOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { for expr in op .sort_fields .iter() @@ -347,14 +468,22 @@ impl<'a, V: ExprVisitor, A: MetaArena + ?Sized> OperatorVisitor<'a> Ok(()) } - fn visit_top_k(&mut self, op: &'a TopKOperator) -> Result<(), DatabaseError> { + fn visit_top_k( + &mut self, + op: &'a TopKOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { for field in &op.sort_fields { ExprVisitor::visit(self.visitor, field.expr, self.arena)?; } Ok(()) } - fn visit_update(&mut self, op: &'a UpdateOperator) -> Result<(), DatabaseError> { + fn visit_update( + &mut self, + op: &'a UpdateOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { for (_, expr) in &op.value_exprs { ExprVisitor::visit(self.visitor, *expr, self.arena)?; } @@ -385,42 +514,58 @@ impl<'a, V: ExprVisitor, A: MetaArena + ?Sized> OperatorVisitor<'a> } } +pub fn walk_plan<'a, V: OperatorVisitor<'a>>( + visitor: &mut V, + plan: &'a LogicalPlan, +) -> Result<(), DatabaseError> { + let LogicalPlan { + operator, + physical_option, + childrens, + .. + } = plan; + let mut children = ChildrenVisit::new(childrens); + visitor.visit_operator(operator, physical_option.as_ref(), &mut children)?; + children.visit(|children| visitor.visit_children(children)) +} + pub fn walk_operator<'a, V: OperatorVisitor<'a>>( visitor: &mut V, operator: &'a Operator, + children: &mut ChildrenVisit<'a>, ) -> Result<(), DatabaseError> { match operator { Operator::Dummy => visitor.visit_dummy(), - Operator::Aggregate(op) => visitor.visit_aggregate(op), - Operator::ScalarQueryInit(op) => visitor.visit_scalar_query_init(op), - Operator::MarkApply(op) => visitor.visit_mark_apply(op), - Operator::Filter(op) => visitor.visit_filter(op), - Operator::Join(op) => visitor.visit_join(op), - Operator::Project(op) => visitor.visit_project(op), - Operator::ScalarSubquery(op) => visitor.visit_scalar_subquery(op), + Operator::Aggregate(op) => visitor.visit_aggregate(op, children), + Operator::ScalarQueryInit(op) => visitor.visit_scalar_query_init(op, children), + Operator::MarkApply(op) => visitor.visit_mark_apply(op, children), + Operator::Filter(op) => visitor.visit_filter(op, children), + Operator::Join(op) => visitor.visit_join(op, children), + Operator::Project(op) => visitor.visit_project(op, children), + Operator::ScalarSubquery(op) => visitor.visit_scalar_subquery(op, children), Operator::TableScan(op) => visitor.visit_table_scan(op), Operator::FunctionScan(op) => visitor.visit_function_scan(op), - Operator::Sort(op) => visitor.visit_sort(op), - Operator::Limit(op) => visitor.visit_limit(op), - Operator::TopK(op) => visitor.visit_top_k(op), + Operator::Sort(op) => visitor.visit_sort(op, children), + Operator::Limit(op) => visitor.visit_limit(op, children), + Operator::TopK(op) => visitor.visit_top_k(op, children), Operator::Values(op) => visitor.visit_values(op), Operator::ShowTable => visitor.visit_show_table(), Operator::ShowView => visitor.visit_show_view(), - Operator::Explain => visitor.visit_explain(), + Operator::Explain => visitor.visit_explain(children), Operator::Describe(op) => visitor.visit_describe(op), - Operator::SetMembership(op) => visitor.visit_set_membership(op), - Operator::Union(op) => visitor.visit_union(op), - Operator::RecursiveCte(op) => visitor.visit_recursive_cte(op), + Operator::SetMembership(op) => visitor.visit_set_membership(op, children), + Operator::Union(op) => visitor.visit_union(op, children), + Operator::RecursiveCte(op) => visitor.visit_recursive_cte(op, children), Operator::RecursiveScan(op) => visitor.visit_recursive_scan(op), - Operator::Insert(op) => visitor.visit_insert(op), - Operator::Update(op) => visitor.visit_update(op), - Operator::Delete(op) => visitor.visit_delete(op), - Operator::Analyze(op) => visitor.visit_analyze(op), + Operator::Insert(op) => visitor.visit_insert(op, children), + Operator::Update(op) => visitor.visit_update(op, children), + Operator::Delete(op) => visitor.visit_delete(op, children), + Operator::Analyze(op) => visitor.visit_analyze(op, children), Operator::AddColumn(op) => visitor.visit_add_column(op), Operator::ChangeColumn(op) => visitor.visit_change_column(op), Operator::DropColumn(op) => visitor.visit_drop_column(op), Operator::CreateTable(op) => visitor.visit_create_table(op), - Operator::CreateIndex(op) => visitor.visit_create_index(op), + Operator::CreateIndex(op) => visitor.visit_create_index(op, children), Operator::CreateView(op) => visitor.visit_create_view(op), Operator::DropTable(op) => visitor.visit_drop_table(op), Operator::DropView(op) => visitor.visit_drop_view(op), @@ -429,8 +574,8 @@ pub fn walk_operator<'a, V: OperatorVisitor<'a>>( #[cfg(feature = "copy")] Operator::CopyFromFile(op) => visitor.visit_copy_from_file(op), #[cfg(feature = "copy")] - Operator::CopyToFile(op) => visitor.visit_copy_to_file(op), - Operator::Window(op) => visitor.visit_window(op), + Operator::CopyToFile(op) => visitor.visit_copy_to_file(op, children), + Operator::Window(op) => visitor.visit_window(op, children), } } @@ -452,7 +597,7 @@ pub(crate) mod tests { use crate::planner::operator::join::{JoinOperator, JoinType}; use crate::planner::operator::mark_apply::MarkApplyOperator; use crate::planner::operator::set_membership::SetMembershipKind; - use crate::planner::operator::visitor_mut::OperatorVisitorMut; + use crate::planner::operator::visitor_mut::{ChildrenVisitMut, OperatorVisitorMut}; use crate::planner::test::PlanArenaTestExt; use crate::planner::ExprRef; use crate::planner::{Childrens, LogicalPlan}; @@ -703,7 +848,11 @@ pub(crate) mod tests { struct VisitOrder(Vec<&'static str>); impl<'a> OperatorVisitor<'a> for VisitOrder { - fn visit_join(&mut self, _op: &'a JoinOperator) -> Result<(), DatabaseError> { + fn visit_join( + &mut self, + _op: &'a JoinOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { self.0.push("operator"); Ok(()) } @@ -723,7 +872,11 @@ pub(crate) mod tests { } impl<'a> OperatorVisitorMut<'a> for VisitOrder { - fn visit_join(&mut self, _op: &'a mut JoinOperator) -> Result<(), DatabaseError> { + fn visit_join( + &mut self, + _op: &'a mut JoinOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { self.0.push("operator"); Ok(()) } @@ -803,13 +956,21 @@ pub(crate) mod tests { #[derive(Default)] struct VisitOrder(Vec<&'static str>); - impl OperatorVisitor<'_> for VisitOrder { - fn visit_project(&mut self, _op: &ProjectOperator) -> Result<(), DatabaseError> { + impl<'a> OperatorVisitor<'a> for VisitOrder { + fn visit_project( + &mut self, + _op: &'a ProjectOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { self.0.push("project"); Ok(()) } - fn visit_join(&mut self, _op: &JoinOperator) -> Result<(), DatabaseError> { + fn visit_join( + &mut self, + _op: &'a JoinOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { self.0.push("join"); Ok(()) } @@ -850,13 +1011,13 @@ pub(crate) mod tests { let mut arena = crate::planner::PlanArena::new(&table_arena); let operators = all_operators(&mut arena)?; for operator in &operators { - NoopVisitor.visit_operator(operator, None)?; + NoopVisitor.visit_operator(operator, None, &mut ChildrenVisit::empty())?; } let mut counter = ExpressionCounter::default(); let mut visitor = OperatorExprVisitor::new(&mut counter, &arena); for operator in &operators { - visitor.visit_operator(operator, None)?; + visitor.visit_operator(operator, None, &mut ChildrenVisit::empty())?; } assert_eq!(counter.0, 22); // Includes the Values row expression. diff --git a/src/planner/operator/visitor_mut.rs b/src/planner/operator/visitor_mut.rs index ac0ca48c..c8bf54f8 100644 --- a/src/planner/operator/visitor_mut.rs +++ b/src/planner/operator/visitor_mut.rs @@ -23,16 +23,36 @@ use crate::expression::visitor_mut::ExprVisitorMut; use crate::planner::MetaArena; use crate::planner::{Childrens, LogicalPlan}; +/// One node's children. Taking the reference records that traversal was handled. +pub struct ChildrenVisitMut<'a> { + children: Option<&'a mut Childrens>, +} + +impl<'a> ChildrenVisitMut<'a> { + pub fn new(children: &'a mut Childrens) -> Self { + Self { + children: Some(children), + } + } + + pub fn empty() -> Self { + Self { children: None } + } + + pub fn visit( + &mut self, + walk: impl FnOnce(&'a mut Childrens) -> Result<(), DatabaseError>, + ) -> Result<(), DatabaseError> { + if let Some(children) = self.children.take() { + walk(children)?; + } + Ok(()) + } +} + pub trait OperatorVisitorMut<'a>: Sized { fn visit_plan(&mut self, plan: &'a mut LogicalPlan) -> Result<(), DatabaseError> { - let LogicalPlan { - operator, - physical_option, - childrens, - .. - } = plan; - self.visit_operator(operator, physical_option.as_mut())?; - self.visit_children(childrens) + walk_mut_plan(self, plan) } fn visit_children(&mut self, childrens: &'a mut Childrens) -> Result<(), DatabaseError> { @@ -50,8 +70,9 @@ pub trait OperatorVisitorMut<'a>: Sized { &mut self, operator: &'a mut Operator, physical_option: Option<&'a mut PhysicalOption>, + children: &mut ChildrenVisitMut<'a>, ) -> Result<(), DatabaseError> { - walk_mut_operator(self, operator)?; + walk_mut_operator(self, operator, children)?; if let Some(physical_option) = physical_option { self.visit_physical_option(physical_option)?; } @@ -69,36 +90,58 @@ pub trait OperatorVisitorMut<'a>: Sized { Ok(()) } - fn visit_aggregate(&mut self, _op: &'a mut AggregateOperator) -> Result<(), DatabaseError> { + fn visit_aggregate( + &mut self, + _op: &'a mut AggregateOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } fn visit_scalar_query_init( &mut self, _op: &'a mut ScalarQueryInitOperator, + _children: &mut ChildrenVisitMut<'a>, ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_mark_apply(&mut self, _op: &'a mut MarkApplyOperator) -> Result<(), DatabaseError> { + fn visit_mark_apply( + &mut self, + _op: &'a mut MarkApplyOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_filter(&mut self, _op: &'a mut FilterOperator) -> Result<(), DatabaseError> { + fn visit_filter( + &mut self, + _op: &'a mut FilterOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_join(&mut self, _op: &'a mut JoinOperator) -> Result<(), DatabaseError> { + fn visit_join( + &mut self, + _op: &'a mut JoinOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_project(&mut self, _op: &'a mut ProjectOperator) -> Result<(), DatabaseError> { + fn visit_project( + &mut self, + _op: &'a mut ProjectOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } fn visit_scalar_subquery( &mut self, _op: &'a mut ScalarSubqueryOperator, + _children: &mut ChildrenVisitMut<'a>, ) -> Result<(), DatabaseError> { Ok(()) } @@ -114,19 +157,35 @@ pub trait OperatorVisitorMut<'a>: Sized { Ok(()) } - fn visit_sort(&mut self, _op: &'a mut SortOperator) -> Result<(), DatabaseError> { + fn visit_sort( + &mut self, + _op: &'a mut SortOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_window(&mut self, _op: &'a mut window::WindowOperator) -> Result<(), DatabaseError> { + fn visit_window( + &mut self, + _op: &'a mut window::WindowOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_limit(&mut self, _op: &'a mut LimitOperator) -> Result<(), DatabaseError> { + fn visit_limit( + &mut self, + _op: &'a mut LimitOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_top_k(&mut self, _op: &'a mut TopKOperator) -> Result<(), DatabaseError> { + fn visit_top_k( + &mut self, + _op: &'a mut TopKOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } @@ -142,7 +201,7 @@ pub trait OperatorVisitorMut<'a>: Sized { Ok(()) } - fn visit_explain(&mut self) -> Result<(), DatabaseError> { + fn visit_explain(&mut self, _children: &mut ChildrenVisitMut<'a>) -> Result<(), DatabaseError> { Ok(()) } @@ -153,17 +212,23 @@ pub trait OperatorVisitorMut<'a>: Sized { fn visit_set_membership( &mut self, _op: &'a mut SetMembershipOperator, + _children: &mut ChildrenVisitMut<'a>, ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_union(&mut self, _op: &'a mut UnionOperator) -> Result<(), DatabaseError> { + fn visit_union( + &mut self, + _op: &'a mut UnionOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } fn visit_recursive_cte( &mut self, _op: &'a mut RecursiveCteOperator, + _children: &mut ChildrenVisitMut<'a>, ) -> Result<(), DatabaseError> { Ok(()) } @@ -175,19 +240,35 @@ pub trait OperatorVisitorMut<'a>: Sized { Ok(()) } - fn visit_insert(&mut self, _op: &'a mut InsertOperator) -> Result<(), DatabaseError> { + fn visit_insert( + &mut self, + _op: &'a mut InsertOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_update(&mut self, _op: &'a mut UpdateOperator) -> Result<(), DatabaseError> { + fn visit_update( + &mut self, + _op: &'a mut UpdateOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_delete(&mut self, _op: &'a mut DeleteOperator) -> Result<(), DatabaseError> { + fn visit_delete( + &mut self, + _op: &'a mut DeleteOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } - fn visit_analyze(&mut self, _op: &'a mut AnalyzeOperator) -> Result<(), DatabaseError> { + fn visit_analyze( + &mut self, + _op: &'a mut AnalyzeOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } @@ -216,6 +297,7 @@ pub trait OperatorVisitorMut<'a>: Sized { fn visit_create_index( &mut self, _op: &'a mut CreateIndexOperator, + _children: &mut ChildrenVisitMut<'a>, ) -> Result<(), DatabaseError> { Ok(()) } @@ -249,7 +331,11 @@ pub trait OperatorVisitorMut<'a>: Sized { } #[cfg(feature = "copy")] - fn visit_copy_to_file(&mut self, _op: &'a mut CopyToFileOperator) -> Result<(), DatabaseError> { + fn visit_copy_to_file( + &mut self, + _op: &'a mut CopyToFileOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { Ok(()) } } @@ -275,7 +361,11 @@ impl<'a, V: ExprVisitorMut, A: MetaArena + ?Sized> OperatorVisitorMut<'a> Ok(()) } - fn visit_aggregate(&mut self, op: &'a mut AggregateOperator) -> Result<(), DatabaseError> { + fn visit_aggregate( + &mut self, + op: &'a mut AggregateOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { for expr in op.agg_calls.iter_mut().chain(&mut op.groupby_exprs) { ExprVisitorMut::visit(self.visitor, expr, self.arena)?; } @@ -285,6 +375,7 @@ impl<'a, V: ExprVisitorMut, A: MetaArena + ?Sized> OperatorVisitorMut<'a> fn visit_scalar_query_init( &mut self, op: &'a mut ScalarQueryInitOperator, + _children: &mut ChildrenVisitMut<'a>, ) -> Result<(), DatabaseError> { ExprVisitorMut::visit(self.visitor, &mut op.value, self.arena)?; for (_, expr) in &mut op.param_bindings { @@ -293,7 +384,11 @@ impl<'a, V: ExprVisitorMut, A: MetaArena + ?Sized> OperatorVisitorMut<'a> Ok(()) } - fn visit_mark_apply(&mut self, op: &'a mut MarkApplyOperator) -> Result<(), DatabaseError> { + fn visit_mark_apply( + &mut self, + op: &'a mut MarkApplyOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { for expr in &mut op.predicates { ExprVisitorMut::visit(self.visitor, expr, self.arena)?; } @@ -306,11 +401,19 @@ impl<'a, V: ExprVisitorMut, A: MetaArena + ?Sized> OperatorVisitorMut<'a> Ok(()) } - fn visit_filter(&mut self, op: &'a mut FilterOperator) -> Result<(), DatabaseError> { + fn visit_filter( + &mut self, + op: &'a mut FilterOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { ExprVisitorMut::visit(self.visitor, &mut op.predicate, self.arena) } - fn visit_join(&mut self, op: &'a mut JoinOperator) -> Result<(), DatabaseError> { + fn visit_join( + &mut self, + op: &'a mut JoinOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { if let JoinCondition::On { on, filter } = &mut op.on { for (left_expr, right_expr) in on { ExprVisitorMut::visit(self.visitor, left_expr, self.arena)?; @@ -323,7 +426,11 @@ impl<'a, V: ExprVisitorMut, A: MetaArena + ?Sized> OperatorVisitorMut<'a> Ok(()) } - fn visit_project(&mut self, op: &'a mut ProjectOperator) -> Result<(), DatabaseError> { + fn visit_project( + &mut self, + op: &'a mut ProjectOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { for expr in &mut op.exprs { ExprVisitorMut::visit(self.visitor, expr, self.arena)?; } @@ -354,14 +461,22 @@ impl<'a, V: ExprVisitorMut, A: MetaArena + ?Sized> OperatorVisitorMut<'a> Ok(()) } - fn visit_sort(&mut self, op: &'a mut SortOperator) -> Result<(), DatabaseError> { + fn visit_sort( + &mut self, + op: &'a mut SortOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { for field in &mut op.sort_fields { ExprVisitorMut::visit(self.visitor, &mut field.expr, self.arena)?; } Ok(()) } - fn visit_window(&mut self, op: &'a mut window::WindowOperator) -> Result<(), DatabaseError> { + fn visit_window( + &mut self, + op: &'a mut window::WindowOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { for expr in op .sort_fields .iter_mut() @@ -377,14 +492,22 @@ impl<'a, V: ExprVisitorMut, A: MetaArena + ?Sized> OperatorVisitorMut<'a> Ok(()) } - fn visit_top_k(&mut self, op: &'a mut TopKOperator) -> Result<(), DatabaseError> { + fn visit_top_k( + &mut self, + op: &'a mut TopKOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { for field in &mut op.sort_fields { ExprVisitorMut::visit(self.visitor, &mut field.expr, self.arena)?; } Ok(()) } - fn visit_update(&mut self, op: &'a mut UpdateOperator) -> Result<(), DatabaseError> { + fn visit_update( + &mut self, + op: &'a mut UpdateOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { for (_, expr) in &mut op.value_exprs { ExprVisitorMut::visit(self.visitor, expr, self.arena)?; } @@ -418,42 +541,58 @@ impl<'a, V: ExprVisitorMut, A: MetaArena + ?Sized> OperatorVisitorMut<'a> } } +pub fn walk_mut_plan<'a, V: OperatorVisitorMut<'a>>( + visitor: &mut V, + plan: &'a mut LogicalPlan, +) -> Result<(), DatabaseError> { + let LogicalPlan { + operator, + physical_option, + childrens, + .. + } = plan; + let mut children = ChildrenVisitMut::new(childrens); + visitor.visit_operator(operator, physical_option.as_mut(), &mut children)?; + children.visit(|children| visitor.visit_children(children)) +} + pub fn walk_mut_operator<'a, V: OperatorVisitorMut<'a>>( visitor: &mut V, operator: &'a mut Operator, + children: &mut ChildrenVisitMut<'a>, ) -> Result<(), DatabaseError> { match operator { Operator::Dummy => visitor.visit_dummy(), - Operator::Aggregate(op) => visitor.visit_aggregate(op), - Operator::ScalarQueryInit(op) => visitor.visit_scalar_query_init(op), - Operator::MarkApply(op) => visitor.visit_mark_apply(op), - Operator::Filter(op) => visitor.visit_filter(op), - Operator::Join(op) => visitor.visit_join(op), - Operator::Project(op) => visitor.visit_project(op), - Operator::ScalarSubquery(op) => visitor.visit_scalar_subquery(op), + Operator::Aggregate(op) => visitor.visit_aggregate(op, children), + Operator::ScalarQueryInit(op) => visitor.visit_scalar_query_init(op, children), + Operator::MarkApply(op) => visitor.visit_mark_apply(op, children), + Operator::Filter(op) => visitor.visit_filter(op, children), + Operator::Join(op) => visitor.visit_join(op, children), + Operator::Project(op) => visitor.visit_project(op, children), + Operator::ScalarSubquery(op) => visitor.visit_scalar_subquery(op, children), Operator::TableScan(op) => visitor.visit_table_scan(op), Operator::FunctionScan(op) => visitor.visit_function_scan(op), - Operator::Sort(op) => visitor.visit_sort(op), - Operator::Limit(op) => visitor.visit_limit(op), - Operator::TopK(op) => visitor.visit_top_k(op), + Operator::Sort(op) => visitor.visit_sort(op, children), + Operator::Limit(op) => visitor.visit_limit(op, children), + Operator::TopK(op) => visitor.visit_top_k(op, children), Operator::Values(op) => visitor.visit_values(op), Operator::ShowTable => visitor.visit_show_table(), Operator::ShowView => visitor.visit_show_view(), - Operator::Explain => visitor.visit_explain(), + Operator::Explain => visitor.visit_explain(children), Operator::Describe(op) => visitor.visit_describe(op), - Operator::SetMembership(op) => visitor.visit_set_membership(op), - Operator::Union(op) => visitor.visit_union(op), - Operator::RecursiveCte(op) => visitor.visit_recursive_cte(op), + Operator::SetMembership(op) => visitor.visit_set_membership(op, children), + Operator::Union(op) => visitor.visit_union(op, children), + Operator::RecursiveCte(op) => visitor.visit_recursive_cte(op, children), Operator::RecursiveScan(op) => visitor.visit_recursive_scan(op), - Operator::Insert(op) => visitor.visit_insert(op), - Operator::Update(op) => visitor.visit_update(op), - Operator::Delete(op) => visitor.visit_delete(op), - Operator::Analyze(op) => visitor.visit_analyze(op), + Operator::Insert(op) => visitor.visit_insert(op, children), + Operator::Update(op) => visitor.visit_update(op, children), + Operator::Delete(op) => visitor.visit_delete(op, children), + Operator::Analyze(op) => visitor.visit_analyze(op, children), Operator::AddColumn(op) => visitor.visit_add_column(op), Operator::ChangeColumn(op) => visitor.visit_change_column(op), Operator::DropColumn(op) => visitor.visit_drop_column(op), Operator::CreateTable(op) => visitor.visit_create_table(op), - Operator::CreateIndex(op) => visitor.visit_create_index(op), + Operator::CreateIndex(op) => visitor.visit_create_index(op, children), Operator::CreateView(op) => visitor.visit_create_view(op), Operator::DropTable(op) => visitor.visit_drop_table(op), Operator::DropView(op) => visitor.visit_drop_view(op), @@ -462,8 +601,8 @@ pub fn walk_mut_operator<'a, V: OperatorVisitorMut<'a>>( #[cfg(feature = "copy")] Operator::CopyFromFile(op) => visitor.visit_copy_from_file(op), #[cfg(feature = "copy")] - Operator::CopyToFile(op) => visitor.visit_copy_to_file(op), - Operator::Window(op) => visitor.visit_window(op), + Operator::CopyToFile(op) => visitor.visit_copy_to_file(op, children), + Operator::Window(op) => visitor.visit_window(op, children), } } @@ -545,14 +684,14 @@ mod tests { let mut arena = PlanArena::new(&table_arena); let mut operators = all_operators(&mut arena)?; for operator in &mut operators { - NoopVisitor.visit_operator(operator, None)?; + NoopVisitor.visit_operator(operator, None, &mut ChildrenVisitMut::empty())?; } let mut counter = IncrementConstants(0); { let mut visitor = OperatorExprVisitorMut::new(&mut counter, &mut arena); for operator in &mut operators { - visitor.visit_operator(operator, None)?; + visitor.visit_operator(operator, None, &mut ChildrenVisitMut::empty())?; } } assert_eq!(counter.0, 22); // Includes the Values row expression. From bee9d37eb71a1fe7b6c4dce40745ce6cee91c853 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 28/62] docs: add RFC 0001 for greedy join reorder owning join decisions - Add rfc/0001: GreedyJoinReorder decides join order, implementation, direction and probe index; unified row cost model; OrientNestedLoopJoin merged into Greedy. - Lower the default ANALYZE histogram bucket count from 100 to 10. --- ...greedy-join-reorder-owns-join-decisions.md | 369 ++++++++++++++++++ src/execution/dml/analyze.rs | 2 +- 2 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 rfc/0001-greedy-join-reorder-owns-join-decisions.md diff --git a/rfc/0001-greedy-join-reorder-owns-join-decisions.md b/rfc/0001-greedy-join-reorder-owns-join-decisions.md new file mode 100644 index 00000000..afbfd958 --- /dev/null +++ b/rfc/0001-greedy-join-reorder-owns-join-decisions.md @@ -0,0 +1,369 @@ +# RFC 0001: 由 GreedyJoinReorder 主导 Join 决策(顺序、实现与索引选择) + +- 状态:Draft +- 日期:2026-10-20 +- 涉及模块:`optimizer/rule/normalization/join_reorder.rs`(`GreedyJoinReorder`、`OrientNestedLoopJoin`)、 + `optimizer/rule/normalization/parameterized_index.rs`(`ParameterizeInnerJoin`)、 + `planner/operator`(`PhysicalOption`)、`optimizer/rule/implementation`(scan cost) + +## 1. 背景 + +### 1.1 Join 决策分散在四处 + +| 阶段 | 规则 | 决定什么 | 依据 | +|---|---|---|---| +| implementation | `IndexScanImplementation` 等 | 每个 scan 的访问路径 | 静态 range 代价,不知道连接键 | +| after_batches 1 | `GreedyJoinReorder` | 连接顺序、hash 的 build 方向 | 只按 hash 估算 | +| after_batches 2 | `ParameterizeInnerJoin` | 是否改写为索引 probe、用哪个索引 | 能 probe 就改写,取 `index_infos` 中第一个可用的 PK/Unique | +| after_batches 5 | `OrientNestedLoopJoin` | Cross NLJ 的内外侧 | `estimated_work` | + +Greedy 处于被动位置:它拿到的是 scan 已经选好的计划,只调整 join 的位置,而且按"全部是 hash"排序; +排好之后,`ParameterizeInnerJoin` 不经代价比较就把部分 join 改成 probe,`OrientNestedLoopJoin` +再独立调整 NLJ 方向。连接顺序是按一种实现的代价定的,最终执行的却可能是另一种实现。 + +### 1.2 后果 + +- 连接顺序没有考虑 probe:一个"小外侧 ⋈ 大唯一内侧"的 pair,按 hash 估算很贵,会被推迟; + 而按 probe 它本应最先合并。 +- probe 是否更优从未被比较:大外侧 ⋈ 小唯一内侧时,probe 的 seek 次数远多于 hash 的全量读取, + 但仍会被改写成 probe。 +- 索引选择与连接无关:probe 用的是第一个可用索引;Greedy 的 `index_statistics` 只读 implementation + 选中的那个索引,覆盖连接键的其它索引提供的唯一性和 NDV 被忽略。 +- 代价单位不统一,且 probe 单价被低估、HashJoin / NLJ 漏算部分代价(见第 4 节), + 即使让 Greedy 比较,也比较不出正确结果。 + +## 2. 目标与非目标 + +目标: + +- `GreedyJoinReorder` 成为 Greedy 区域内 join 的唯一决策者:在合并每一个 pair 时,同时决定 + 连接顺序、join 实现(Hash / IndexProbe / NestLoop)和方向,以及区域内每个叶子 scan 使用的索引。 +- 下游规则 `ParameterizeInnerJoin` 对 Greedy 已决策的 join 只负责执行和校验,不再自行判断; + 原有逻辑仅作为未经 Greedy 处理的 join 的兜底。`OrientNestedLoopJoin` 并入 Greedy 后删除。 +- 提供支撑上述决策的统一代价模型。 + +非目标: + +- 不改变 optimizer pipeline 的 batch 顺序(Greedy 依赖 implementation 产出的 `row_count`)。 +- 不支持非唯一索引(Normal / Composite)的 inner join probe,见第 8 节。 +- 不建模 I/O。 + +## 3. 设计:Greedy 的决策权 + +### 3.1 职责划分 + +| 规则 | 现在 | 改造后 | +|---|---|---| +| scan impl rule | 选访问路径 | 只提供静态最优的默认值;Greedy 区域内的叶子以 Greedy 的决策为准 | +| `GreedyJoinReorder` | 只排顺序,按 hash 估算 | 每个 pair 决定顺序 + 实现 + 方向 + probe 索引,写入 `join_hint` | +| `ParameterizeInnerJoin` | 能 probe 就改写 | 有 hint:校验后按 hint 执行;无 hint:原逻辑兜底 | +| `OrientNestedLoopJoin` | 调整所有 Cross NLJ 方向 | 删除,职责并入 Greedy(3.10) | + +Greedy 区域内每个叶子的访问路径都由 Greedy 决定,scan impl rule 的结果只是 Greedy 的输入之一: + +- 叶子作为 probe 内侧:访问路径是 Greedy 选中的 probe 索引(hint 中的 `index`), + 由 `ParameterizeInnerJoin` 替换掉 scan 原来的索引与 range。不存在"probe 更优但 scan 用了别的索引"的情况。 +- 叶子作为 hash / NLJ 的输入:访问路径不使用连接键,候选就是 scan impl rule 用静态 range 比较过的那些, + 其最低代价方案也就是此时的最优方案;Greedy 以它的 `work` 作为该形态的输入代价,选择 Hash 即确认采用它。 + - 例:`a JOIN b ON a.id = b.a_id WHERE b.status = 1`,b 上有 `idx(a_id)`、`idx(status)`。hash 时 b 被独立读出, + 扫描时拿不到 a 的值,`idx(a_id)` 无法限定范围,可用的只有 `status = 1` 对应的 range 或全表扫描; + 只有 probe 才能把 `a.id` 的值传给 b 的 scan,在 `idx(a_id)` 上 seek。 +- 不在 Greedy 区域内的 scan,以及未经 Greedy 处理的 join 下的 scan,仍由 scan impl rule 自己决定。 + +Greedy 在 implementation 之后运行(依赖其产出的 `row_count`),所以 scan 节点上先有一个静态最优的默认值, +Greedy 的决策在其上生效;对同一个叶子,Greedy 的决策优先级最高。 + +### 3.2 决策流程 + +1. 收集区域内的叶子和连接边(现有逻辑)。 +2. 对每个叶子生成 probe 候选并缓存(3.3)。 +3. 每轮对每个候选 pair 列出可行的实现形态,用代价模型给每种形态打分,取 pair 内最优形态(3.4)。 +4. 在所有 pair 中按 `JoinCandidate` 的顺序选出本轮合并的 pair。 +5. 按选中的形态构造 join:确定左右(probe 内侧、NLJ 内侧放右边,hash build 放左边),写入 `join_hint`(3.5)。 +6. 合并结果作为新输入参与下一轮,其 `InputCost` 由所选形态的代价函数给出。 + +### 3.3 Source 与 probe 候选 + +Greedy 根据 source 的列信息与其上存在的索引,自己生成候选,不依赖 impl rule 保留候选: + +```rust +struct ProbeCandidate { + index: IndexMetaRef, + key_columns: Vec, // 必须由连接键提供的索引列(已扣除常量等值列),非空 + lookup_work: usize, // 单次 probe 代价,见 4.3 + rows_per_lookup: usize, // 唯一索引 = 1,为非唯一索引预留 +} +``` + +- 适用的叶子:`Filter*` + 没有 limit 的 `TableScan`。已合并的 join 结果不能作为 probe 内侧, + 所以 probe 只出现在"已合并结果 ⋈ 叶子"的形态,与 Greedy 左深扩展的方式吻合。 +- 索引的每一列要么出现在某条连接边上(属于该叶子一侧),要么被叶子 filter 的常量等值绑定; + 两者都满足时按常量处理,减少对连接键的要求。 +- 每个叶子最多 10 个候选,按 PK → Unique → `key_columns` 少的优先 → index id 排序后截断。 +- 同一组覆盖连接键的索引也用于行数估算:任意被覆盖的 PK/Unique 即认为该侧唯一; + NDV 通过 `loader.load(table, index_id)` 读取该索引的 `distinct_values_len`(复合键得到联合 NDV), + 再按 filter 后行数以 `min(N, rows)` 收紧。不再只看 implementation 选中的索引。 + +### 3.4 Pair 内的实现形态 + +对一个 pair (L, R),可行形态如下: + +| 形态 | 条件 | +|---|---| +| Hash(build = L) / Hash(build = R) | pair 有连接键 | +| Probe(inner = R) / Probe(inner = L) | 对应一侧是叶子,且某个候选的 `key_columns` 被该 pair 在这一侧的连接键覆盖 | +| NestLoop(outer = L) / NestLoop(outer = R) | pair 没有连接键(cross) | + +每种形态按第 4 节的代价模型打分,pair 内取最低分。`JoinCandidate` 的比较顺序(`derive(Ord)`): + +``` +cross_join → score → impl_rank → rows → left → right → swapped +``` + +- `cross_join` 按 pair 判断(该 pair 是否有连接键),不再用全局的 `connected`。 +- `impl_rank`:probe < hash < NLJ,只在分数相同或无统计时起作用。 + +### 3.5 决策下发:`join_hint` + +```rust +enum JoinHint { + Hash, + Probe { index: IndexMetaRef, lookup_work: usize }, + NestLoop, +} + +// PhysicalOption 新增,参与 PartialEq / Hash(会影响后续改写结果) +join_hint: Option, +``` + +Hint 写在 Greedy 构造 join 处(现在设置 `PhysicalOption(HashJoin/NestLoopJoin, ...)` 的位置)。 +左右已经由 Greedy 定好,hint 只表达"按这个形态执行,不要再判断"。 + +### 3.6 下游规则的执行与兜底 + +- `ParameterizeInnerJoin` + - `Probe`:用共用判定函数(3.7)再校验一次,通过则按指定索引改写,并把 `lookup_work` 写入 + MarkApply 的 `PhysicalOption`;不尝试交换左右;校验失败按 hash 处理。 + - `Hash` / `NestLoop`:不改写。 + - `None`:Greedy 没处理过的 join(ON 带 filter、`force_nested_loop`、`limit_pushed` 等),走原逻辑兜底。 +- NestLoop 方向由 Greedy 决定,下游不再有调整方向的规则(3.10)。 +- 被替换的静态 range 不会丢失过滤语义:Filter 仍在 scan 上方,`EliminateIndexFilter` 在 Parameterize 之后运行。 +- explain 只展示选中的索引(现有行为),不展示 hint 与 `lookup_work`。 + +### 3.7 共用判定函数 + +从 `parameterize` 中拆出纯判定函数(不修改 plan),Greedy 生成候选与 `ParameterizeInnerJoin` 校验都调用它: + +- 索引列都在 scan 的 `columns` 中; +- 连接值表达式类型与索引列类型完全一致(`value.return_type == column.datatype()`); +- 至少用到一个连接键(`uses_join_key`),只靠常量凑满索引不算 probe。 + +Greedy 拥有决策权的前提是它的判断与执行一致;若两边各写一份,可能出现 Greedy 按 probe 定了顺序、 +执行时却退回 hash,结果比不考虑 probe 更差。 + +### 3.8 索引覆盖:probe 与 scan 用同一口径 + +现状:覆盖信息(`covered_deserializers` / `cover_mapping`)只在 `PushPredicateIntoScan` 中计算, +且只针对能从 filter 拆出静态 range 的索引。只被连接键用到的索引覆盖信息始终为 `None`; +`parameterize` 改写为 probe 时只设置 `lookup = Probe`、`residual_predicate = None`,不重算覆盖。 +执行器(`index_scan.rs`)会按覆盖信息决定是否回表,因此一个实际覆盖了 scan 所需列的二级 Unique 索引, +probe 时仍会回表,代价按 `seeks = 2` 计。结果是 probe 与 scan 选中的覆盖索引比较时被系统性高估。 + +修正: + +1. 把覆盖计算从 `PushPredicateIntoScan` 拆成纯函数 + `index_covering(scan, index) -> Option<(deserializers, mapping)>`,只依赖 scan 的 `columns` 与索引列,与 range 无关。 +2. Greedy 生成候选时用它决定 `seeks`(PK 或覆盖 Unique = 1,否则 = 2);scan cost 中非覆盖 ×2 的判断使用同一函数。 +3. `ParameterizeInnerJoin` 按 hint(以及兜底路径)改写为 probe 时,同时写入 `covered_deserializers` / `cover_mapping`, + 使执行器真正跳过回表。 + +这样 Hash 形态用 scan 已选路径(可能是覆盖索引)的 `work`,Probe 形态枚举 `scan.index_infos` 中全部候选并按真实覆盖计价, +"scan 选了覆盖索引,但 probe 更优"的情况在 pair 内比较时直接体现,由 Greedy 选出后覆盖 scan 的选择。 + +### 3.9 无统计信息时的回退 + +一个 pair 只要有一侧缺少统计信息(行数,或 probe 所需的 N),该 pair 不做代价比较, +按 `impl_rank` 启发式选择 probe > hash > NLJ,与现有 `ParameterizeInnerJoin` 的行为一致。 +NestLoop 方向在缺少统计时沿用 `OrientNestedLoopJoin` 现有的回退:一侧行数为 0 / 1 时它作外侧,否则比较 `startup_work`。 + +### 3.10 融合 `OrientNestedLoopJoin` + +`OrientNestedLoopJoin` 只匹配 `Cross` + `on = None` + `limit_pushed = false` + `NestLoopJoin`, +用 `left_work + left_rows × right_work` 与对侧比较决定方向,与 `JoinImpl::NestLoop` 的公式本质相同。 +它处理三类 join,全部并入 Greedy 后删除该规则及 `Orient Nested Loop Join` batch: + +| 来源 | 现在 | 融合后 | +|---|---|---| +| Greedy 区域内无连接键的 pair | Greedy 构造时 `swapped: connected && ...`,cross 永不交换,方向靠 Orient 修正 | Greedy 比较 `NestLoop(outer = L / R)`,写入 `NestLoop` hint | +| 根为 `Cross` 的区域(如 `FROM a, b`,或位于 outer join / 带 filter 的 inner join 之下) | Greedy 要求根为 `Inner`(`apply` 入口),不处理 | Greedy 触发条件放宽为 `can_expand(plan)`;`can_expand` 本就接受 Cross | +| `force_nested_loop` 的 cross(`FORCE_NEST_LOOP_JOIN`) | `can_expand` 排除,不进区域;Orient 不检查该字段,仍调整方向 | 仍不进区域、不重排;Greedy 对这种单个 join 只做方向选择,行为不变 | + +`force_nested_loop` 的语义是限制实现,不放开重排,所以只接管方向。 + +时序代价:Orient 原本在 `Parameterize Mark Apply`、`Eliminate Index Filter`、`Materialize Mark Apply` 之后运行, +看到的是改写后的 `estimated_work`。并入 Greedy 后,NLJ 内侧子树若含子查询类 MarkApply,方向按其改写前的估算决定; +物化正好降低重跑内侧的代价。该偏差先接受(Greedy 中 cross 本就排在最后,此类情况少见), +与 `rescan_work` 一并作为未来工作。改写对这类 MarkApply `estimated_work` 的实际影响尚未核实。 + +## 4. 支撑:代价模型 + +Greedy 的决策只有在代价可比时才有意义,本节是决策权改造的前提。 + +### 4.1 现状 + +| 位置 | 单位 | 是否考虑行宽 | +|---|---|---| +| scan impl rule 的 `cost` | 行数(非覆盖索引 ×2,排序 hint 有折扣) | 否 | +| `calculate_work` / `calculate_startup_work` | CPU 单位:`CPU_ROW_WORK = 4`,`CPU_OPERATOR_WORK = 1` | 否 | +| `GreedyJoinReorder` | 值个数(行数 × 列数) | 是 | + +- MarkApply 每次 lookup 只算 `startup_work(scan) + OP = 1`,比顺序扫一行(4)还便宜。 +- HashJoin 走默认分支,漏算 probe 侧的全量读取。 +- NLJ 只有 Cross 单独处理且没计逐对比较;非 Cross 的 NLJ 完全没计"每个外侧行重跑内侧"。 + +### 4.2 统一的行代价与 join 代价函数 + +```rust +// width 为列数;每行有固定开销,零列行不会被估成 0 +fn rows_work(rows: usize, width: usize) -> usize { + rows * (CPU_ROW_WORK + width * CPU_OPERATOR_WORK) +} + +enum JoinImpl { Hash { build: Side }, NestLoop { outer: Side }, IndexProbe { inner: Side, lookup_work: usize } } +struct InputCost { rows: usize, width: usize, work: usize, rescan_work: usize, startup_work: usize } +impl JoinImpl { + fn work(&self, left: &InputCost, right: &InputCost, out_rows: usize, out_width: usize) -> usize; +} +``` + +``` +Hash = build.work + probe.work + build.rows × OP + probe.rows × OP + rows_work(out) +IndexProbe = outer.work + outer.rows × lookup_work + rows_work(out) // 内侧不全量读 +NestLoop = outer.work + inner.work + (outer.rows − 1) × inner.rescan_work + + outer.rows × inner.rows × OP + rows_work(out) + (+ inner.rescan_work,仅 Full join) +``` + +- 代价函数只接收统计量,不依赖 `&LogicalPlan`,因为 Greedy 评估的是尚未构造的 join。 + Greedy 与 `calculate_work` 共用它。 +- `calculate_work`、`calculate_startup_work`、scan impl rule 都改用 `rows_work`; + scan 的 ×2 和排序 bonus 规则不变。 +- `rescan_work` 先预留,默认等于 `work`(例外见 6.2)。 + +### 4.3 单次 probe 代价 `lookup_work` + +Hash 的 probe 是内存哈希查找(约 `OP`),前提是两侧全量读;MarkApply 的 probe 是存储层 seek, +每次都要重建内侧 executor,单价高,好处是内侧不全量读。 + +``` +lookup_work = PROBE_RESET_WORK(= CPU_ROW_WORK) + + seeks × ⌈log2(N)⌉ × OP // seeks: PK/覆盖 Unique = 1,非覆盖 Unique = 2 + + rows_per_lookup × rows_work(1, inner_width) +``` + +- N 取该索引直方图的 `values_len`,缺失时取 PK 直方图;都没有视为无统计。内侧 Filter 只按行计。 +- 示例:内侧 100 万行、宽度 2,扫一行 = 6,查一次 = 30;外侧约小于内侧 1/4 时 probe 更优。 +- 存于 `PhysicalOption.lookup_work: Option`,由 Greedy 经 hint 传入,兜底路径由 + `ParameterizeInnerJoin` 用 loader 计算。semi / anti 等子查询类 MarkApply 也改用该公式。 + +### 4.4 Pair 分数:本步处理量 + +``` +score(Hash / NestLoop) = work(join) − left.work − right.work +score(Probe) = outer.rows × lookup_work + rows_work(out) − inner_leaf.work +``` + +- 分数只计这一步的中间处理量。全 hash 时每张叶子无论顺序都读一遍,计入只会让大表被推迟。 + 现有 Greedy 公式(`product × (left_width + right_width) + min(left_size, right_size)`)已是这个思路。 +- Probe 的 `− inner_leaf.work` 是对基准(每张叶子都完整读一遍)的修正,不是处理量; + 内侧大、外侧小时分数为负,Greedy 会优先合并。分数用 `i128`,代码中需注释该项含义。 + +## 5. Pipeline 与 hint 的有效性 + +`db.rs` 中 after_batches 的顺序: + +1. `Greedy Join Reorder`(once_topdown) +2. `Parameterize Mark Apply`:`ParameterizeMarkApply` + `ParameterizeInnerJoin`(fix_point_topdown(1)) +3. `Eliminate Index Filter` +4. `Materialize Mark Apply` +5. `Orient Nested Loop Join`(本 RFC 删除) +6. …… + +Greedy 与 Parameterize 之间没有其它 batch;同一 batch 的 `ParameterizeMarkApply` 只匹配 `MarkApply`, +其 `parameterize_right_subtree` 只处理 `Project` 和 `TableScan`,不修改 Join 节点,hint 不会被破坏。 +改写出的 MarkApply 的 `estimated_work` 仍须使用 Greedy 传入的 `lookup_work`,使 `calculate_work` +(含 explain 中的代价)与 Greedy 的决策口径一致。 + +## 6. 已核实的执行器行为 + +### 6.1 MarkApply inner probe(`execution/dql/mark_apply.rs`) + +- `predicates` 包含全部 `on` 等值条件(常量等值在 `parameterize` 返回前被 `truncate`);`next_join_tuple` + 取出内侧行后用 `predicates_matched` 校验全部条件,连接键多于 `key_columns` 时多出的条件也会被校验。 +- 原 join 的 `filter` 包成 `Filter` 放在 MarkApply 之上;被 probe 一侧原有的 Filter 保留。 +- 每个外侧行先 reset 内侧,最多取 1 行,所以本 RFC 只支持 PK/Unique probe。 + +### 6.2 NestLoopJoin 与 `reset` + +NLJ 不物化内侧:每取一个外侧行就 `reset_right_input()` 并把内侧扫到底;Full join 最后再扫一遍内侧。 +`SeqScan` / `IndexScan` 的 `reset` 回到存储层重新定位。有内部状态的算子 `reset` 时清空状态并递归 reset 输入: + +| 算子 | `reset` 行为 | +|---|---| +| HashJoin | `state = Build`,`bump.reset()`,reset 两侧 | +| Sort / ExternalSort | 清空 `rows`,reset 输入 | +| TopK | `output = None`,`arena.reset()`,reset 输入 | +| HashAgg / SimpleAgg | 清空输出状态,reset 输入 | +| Window | `state.clear()`,`reset_functions()`,reset 输入 | +| NestLoopJoin | 回到 `PullLeft`,reset 两侧 | +| MarkApply | 只 reset 左侧;`materialize` 且已有缓存时保留右侧缓存 | + +因此 `rescan_work = work` 是准确的,唯一例外是开启 `materialize` 的 MarkApply。 + +### 6.3 排序 hint 不影响 Greedy 叶子 + +scan cost 中的排序折扣来自 `sort_elimination_hint` / `stream_aggregate_hint`,需确认 Greedy 把叶子当作 +hash 输入或改写为 probe 时,不会丢失上层依赖的有序性、也不会用到打过折的 `work`: + +- hint 在 implementation 阶段由 `annotate_hints_and_physical_options`(`heuristic/optimizer.rs`)自上而下下发, + 只穿过 `Filter` / `Project` / `Limit` / `TopK` / `Sort`;遇到 Join 等其它算子即换成空 hint。 + 所以 Join 之下的 scan(即 Greedy 的叶子)`hint_sum = 0`,没有排序折扣。 +- 消费有序性的 `EliminateRedundantSort` / `UseStreamAggregate` 在同一阶段的 `apply_annotated_post_rules` 中运行, + 早于 Greedy;其 `ensure_order` 只沿 `Childrens::Only` 下钻,不穿过 Join。Join 的 `PhysicalOption` + 本身是 `SortOption::None`。 +- 叶子与 Join 之间若有 Sort / Aggregate,该子树不是 `Filter* + TableScan`,不会作为 probe 内侧, + 其有序性只在子树内部被消费。 + +结论:Greedy 改变叶子的访问路径不会破坏任何上层有序性依赖,Hash 形态使用的 `work` 也不含排序折扣。 + +## 7. 实施计划 + +代价模型是 Greedy 决策的前提,所以先统一代价,再移交决策权,分两步提交以便定位回归。 + +1. 统一代价(只改行为):引入 `rows_work`、`JoinImpl` / `InputCost`,修正 HashJoin、NLJ、MarkApply 的估算, + Greedy 分数改为本步处理量。现有连接顺序与 explain / slt 期望值会变化,需整体重新校准。 +2. 移交决策权:拆出共用判定函数与 `index_covering`;Greedy 生成 probe 候选、按形态比较并写入 `join_hint`; + `ParameterizeInnerJoin` 改为有 hint 时只执行;Greedy 触发条件放宽到 `can_expand`、接管 NestLoop 方向 + (含 `force_nested_loop` 的单个 cross),删除 `OrientNestedLoopJoin` 及其 batch。 + +测试(单测走 `make test`,slt 走 `make test-slt`): + +- `rows_work` 与各 `JoinImpl::work`:Full join、零列行、饱和运算。 +- Greedy:大外侧 × 小唯一内侧选 hash;小外侧 × 大唯一内侧选 probe 且优先合并;临界点; + 多个可用唯一索引时选代价最低的;复合唯一索引 + 常量等值;无统计回退启发式。 +- 下游规则:hint 为 `Probe` / `Hash` / `NestLoop` / `None` 四种路径;`Probe` 校验失败退回 hash。 +- NestLoop 方向:Greedy 区域内的 cross pair、根为 Cross 的区域、`force_nested_loop` 的 cross 三类, + 方向均与原 `OrientNestedLoopJoin` 的现有用例结论一致(迁移其单测),`force_nested_loop` 下不发生重排。 +- 覆盖:scan 选了覆盖索引、另一个覆盖的 Unique 可 probe 时,probe 按 `seeks = 1` 计价并被选中, + 改写后的 IndexScan 带 `covered_deserializers`,执行时不回表。 +- 正确性:probe 后全部 `on` 条件、join filter、静态 filter 均保留(slt 覆盖)。 + +## 8. 未来工作 + +- 开启 `materialize` 的 MarkApply 的 `rescan_work`;以及 NLJ 内侧含子查询类 MarkApply 时, + Greedy 按其参数化 / 物化前的估算决定方向的偏差(3.10)。 +- 常量传播:目前没有等价类 / 传递闭包,`pa.id = 1 AND pa.id = pb.a_id` 推不出 `pb.a_id = 1`(已用 explain 实测)。 + 它是独立的改写规则,加入后 Greedy 的叶子会多出静态 range,也多出可用于凑齐 probe 索引的常量等值条件,与本 RFC 的设计不冲突。 +- 非唯一索引 probe:执行器需支持每个外侧行输出多行,且每行 reset 内侧开销较大,按具体 case 评估; + 届时 `rows_per_lookup` 由索引 NDV 估算,10 个候选上限才真正起作用。 +- I/O 建模。 diff --git a/src/execution/dml/analyze.rs b/src/execution/dml/analyze.rs index d3988efe..f689765a 100644 --- a/src/execution/dml/analyze.rs +++ b/src/execution/dml/analyze.rs @@ -35,7 +35,7 @@ use crate::types::{CharLengthUnits, ColumnId}; use ordered_float::OrderedFloat; use std::fmt::{self, Formatter}; -const DEFAULT_NUM_OF_BUCKETS: usize = 100; +const DEFAULT_NUM_OF_BUCKETS: usize = 10; pub struct Analyze<'a> { table_name: &'a TableName, From 43f9b0642d1aa14d38fdcdb0da8f2373d74d2838 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 29/62] feat(optimizer): propagate constants through equality predicates - PropagateFilterConstants: derive `b = c` from `a = b AND a = c` inside a filter, closing chains. - PropagateJoinConstants: derive constants across join equi-keys onto the inputs. Inner joins propagate both ways; left joins only to the right side, right joins only to the left side; full joins are skipped. Outer joins also read the preserved side's ON constants kept on the join. - Only plain table columns of identical types participate; NULL constants are ignored. - Run both rules in the "Predicate Pushdown" batch so derived predicates become index ranges. - Add unit tests and slt cases in where.slt. - Fix a missing AggregateOperator import in elimination tests. --- src/db.rs | 2 + .../normalization/constant_propagation.rs | 611 ++++++++++++++++++ .../rule/normalization/elimination.rs | 1 + src/optimizer/rule/normalization/mod.rs | 18 +- tests/slt/where.slt | 131 ++++ 5 files changed, 761 insertions(+), 2 deletions(-) create mode 100644 src/optimizer/rule/normalization/constant_propagation.rs diff --git a/src/db.rs b/src/db.rs index 1bc7c92b..b9a48204 100644 --- a/src/db.rs +++ b/src/db.rs @@ -461,6 +461,8 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { "Predicate Pushdown", HepBatchStrategy::fix_point_topdown(30), vec![ + NormalizationRuleImpl::PropagateFilterConstants, + NormalizationRuleImpl::PropagateJoinConstants, NormalizationRuleImpl::PushPredicateThroughJoin, NormalizationRuleImpl::PushJoinPredicateIntoScan, ], diff --git a/src/optimizer/rule/normalization/constant_propagation.rs b/src/optimizer/rule/normalization/constant_propagation.rs new file mode 100644 index 00000000..394e972f --- /dev/null +++ b/src/optimizer/rule/normalization/constant_propagation.rs @@ -0,0 +1,611 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Constant propagation over equality predicates. +//! +//! `a = b AND a = 1` implies `b = 1`. The derived predicate is added (never replaces anything), +//! so later rules can turn it into a static index range on the other column. +//! +//! - [`PropagateFilterConstants`] derives constants inside one `Filter`'s top-level conjuncts. +//! - [`PropagateJoinConstants`] derives constants across a join's equi-keys and wraps the +//! target input with a new `Filter`. Only the sides whose rows may be filtered without +//! changing the join result are targeted: both sides of an inner join, the right side of a +//! left join and the left side of a right join. Constants come from filters below the join +//! and, for outer joins, also from the preserved side's ON conjuncts that stay on the join. +//! +//! Only plain table columns of identical types participate, and NULL constants are ignored: +//! `col = NULL` is never true, so it carries no value to propagate. + +use crate::catalog::{ColumnRef, ColumnRelation}; +use crate::errors::DatabaseError; +use crate::expression::{BinaryOperator, ScalarExpression}; +use crate::optimizer::core::rule::NormalizationRule; +use crate::optimizer::core::statistics_meta::StatisticMetaLoader; +use crate::optimizer::plan_utils::wrap_child_with; +use crate::planner::operator::filter::FilterOperator; +use crate::planner::operator::join::{JoinCondition, JoinType}; +use crate::planner::operator::Operator; +use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; +use crate::types::LogicalType; + +#[derive(Clone, Copy)] +struct Fact { + column: ColumnRef, + position: usize, + constant: ExprRef, + left: bool, +} + +fn lookup(facts: &[Fact], arena: &PlanArena<'_>, column: ColumnRef, left: bool) -> Option { + facts + .iter() + .find(|fact| fact.left == left && arena.same_column(fact.column, column)) + .map(|fact| fact.constant) +} + +fn for_each_conjunct(expr: ExprRef, arena: &PlanArena<'_>, f: &mut impl FnMut(ExprRef)) { + match arena.expression(expr) { + ScalarExpression::Binary { + op: BinaryOperator::And, + left_expr, + right_expr, + .. + } => { + let (left_expr, right_expr) = (*left_expr, *right_expr); + for_each_conjunct(left_expr, arena, f); + for_each_conjunct(right_expr, arena, f); + } + _ => f(expr), + } +} + +fn table_column(expr: ExprRef, arena: &PlanArena<'_>) -> Option<(ColumnRef, usize)> { + match expr.unpack_alias_ref(arena) { + ScalarExpression::ColumnRef { column, position } + if matches!( + arena.column(*column).summary().relation, + ColumnRelation::Table { .. } + ) => + { + Some((*column, *position)) + } + _ => None, + } +} + +fn eq_operands(expr: ExprRef, arena: &PlanArena<'_>) -> Option<(ExprRef, ExprRef)> { + match expr.unpack_alias_ref(arena) { + ScalarExpression::Binary { + op: BinaryOperator::Eq, + left_expr, + right_expr, + .. + } => Some((*left_expr, *right_expr)), + _ => None, + } +} + +fn as_fact(expr: ExprRef, arena: &PlanArena<'_>, left: bool) -> Option { + let (lhs, rhs) = eq_operands(expr, arena)?; + let ((column, position), constant) = match (table_column(lhs, arena), table_column(rhs, arena)) + { + (Some(column), None) => (column, rhs.unpack_alias(arena)), + (None, Some(column)) => (column, lhs.unpack_alias(arena)), + _ => return None, + }; + matches!(arena.expression(constant), ScalarExpression::Constant(value) if !value.is_null()) + .then_some(Fact { + column, + position, + constant, + left, + }) +} + +type ColumnSlot = (ColumnRef, usize); + +fn as_column_pair( + lhs: ExprRef, + rhs: ExprRef, + arena: &PlanArena<'_>, +) -> Option<(ColumnSlot, ColumnSlot)> { + let lhs = table_column(lhs, arena)?; + let rhs = table_column(rhs, arena)?; + (arena.column(lhs.0).datatype() == arena.column(rhs.0).datatype() + && !arena.same_column(lhs.0, rhs.0)) + .then_some((lhs, rhs)) +} + +fn and_fact( + acc: Option, + fact: Fact, + arena: &mut PlanArena<'_>, +) -> Result { + let left_expr = arena.alloc_expression(ScalarExpression::ColumnRef { + column: fact.column, + position: fact.position, + }); + let right_expr = fact.constant.clone_expression(arena)?; + let eq = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Eq, + left_expr, + right_expr, + evaluator: None, + ty: LogicalType::Boolean, + }); + Ok(match acc { + Some(left_expr) => arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::And, + left_expr, + right_expr: eq, + evaluator: None, + ty: LogicalType::Boolean, + }), + None => eq, + }) +} + +pub struct PropagateFilterConstants; + +impl NormalizationRule for PropagateFilterConstants { + fn apply( + &self, + plan: &mut LogicalPlan, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, + ) -> Result { + let Operator::Filter(filter) = &plan.operator else { + return Ok(false); + }; + let predicate = filter.predicate; + + let mut facts = Vec::new(); + for_each_conjunct(predicate, arena, &mut |conjunct| { + facts.extend(as_fact(conjunct, arena, false)); + }); + if facts.is_empty() { + return Ok(false); + } + + let base = facts.len(); + loop { + let before = facts.len(); + for_each_conjunct(predicate, arena, &mut |conjunct| { + let Some((lhs, rhs)) = eq_operands(conjunct, arena) else { + return; + }; + let Some((lhs, rhs)) = as_column_pair(lhs, rhs, arena) else { + return; + }; + for (from, to) in [(lhs, rhs), (rhs, lhs)] { + if lookup(&facts, arena, to.0, false).is_some() { + continue; + } + if let Some(constant) = lookup(&facts, arena, from.0, false) { + facts.push(Fact { + column: to.0, + position: to.1, + constant, + left: false, + }); + } + } + }); + if facts.len() == before { + break; + } + } + if facts.len() == base { + return Ok(false); + } + + let mut acc = predicate; + for &fact in &facts[base..] { + acc = and_fact(Some(acc), fact, arena)?; + } + if let Operator::Filter(filter) = &mut plan.operator { + filter.predicate = acc; + } + Ok(true) + } +} + +fn collect_facts(plan: &LogicalPlan, arena: &PlanArena<'_>, left: bool, facts: &mut Vec) { + match (&plan.operator, plan.childrens.as_ref()) { + (Operator::Filter(filter), Childrens::Only(child)) => { + for_each_conjunct(filter.predicate, arena, &mut |conjunct| { + facts.extend(as_fact(conjunct, arena, left)); + }); + collect_facts(child, arena, left, facts); + } + (Operator::Join(join), Childrens::Twins { left: l, right: r }) => match join.join_type { + JoinType::Inner | JoinType::Cross => { + collect_facts(l, arena, left, facts); + collect_facts(r, arena, left, facts); + } + JoinType::LeftOuter => collect_facts(l, arena, left, facts), + JoinType::RightOuter => collect_facts(r, arena, left, facts), + JoinType::Full => {} + }, + _ => {} + } +} + +pub struct PropagateJoinConstants; + +impl NormalizationRule for PropagateJoinConstants { + fn apply( + &self, + plan: &mut LogicalPlan, + arena: &mut PlanArena, + _loader: Option<&StatisticMetaLoader<'_>>, + ) -> Result { + let Operator::Join(join) = &plan.operator else { + return Ok(false); + }; + let (to_left, to_right) = match join.join_type { + JoinType::Inner => (true, true), + JoinType::LeftOuter => (false, true), + JoinType::RightOuter => (true, false), + JoinType::Full | JoinType::Cross => return Ok(false), + }; + let JoinCondition::On { on, filter } = &join.on else { + return Ok(false); + }; + if on.is_empty() { + return Ok(false); + } + let join_filter = *filter; + + let mut facts = Vec::new(); + if let (Some(join_filter), true) = (join_filter, to_left != to_right) { + let preserved_left = to_right; + let Childrens::Twins { left, right } = plan.childrens.as_mut() else { + return Ok(false); + }; + let preserved = if preserved_left { left } else { right }; + let preserved_columns = preserved.output_schema(arena); + for_each_conjunct(join_filter, arena, &mut |conjunct| { + facts.extend( + as_fact(conjunct, arena, preserved_left) + .filter(|fact| preserved_columns.contains(&fact.column)), + ); + }); + } + let Childrens::Twins { left, right } = plan.childrens.as_ref() else { + return Ok(false); + }; + collect_facts(left, arena, true, &mut facts); + collect_facts(right, arena, false, &mut facts); + if facts.is_empty() { + return Ok(false); + } + + let base = facts.len(); + for &(left_key, right_key) in on { + let Some((l, r)) = as_column_pair(left_key, right_key, arena) else { + continue; + }; + for (allowed, from, to, to_left_side) in + [(to_right, l, r, false), (to_left, r, l, true)] + { + if !allowed || lookup(&facts, arena, to.0, to_left_side).is_some() { + continue; + } + if let Some(constant) = lookup(&facts, arena, from.0, !to_left_side) { + facts.push(Fact { + column: to.0, + position: to.1, + constant, + left: to_left_side, + }); + } + } + } + if facts.len() == base { + return Ok(false); + } + + let (mut left_acc, mut right_acc) = (None, None); + for &fact in &facts[base..] { + let acc = if fact.left { + &mut left_acc + } else { + &mut right_acc + }; + *acc = Some(and_fact(acc.take(), fact, arena)?); + } + let mut applied = false; + for (child_idx, acc) in [(0, left_acc), (1, right_acc)] { + if let Some(predicate) = acc { + applied |= wrap_child_with( + plan, + child_idx, + Operator::Filter(FilterOperator { + predicate, + is_optimized: false, + having: false, + }), + ); + } + } + Ok(applied) + } +} + +#[cfg(test)] +mod tests { + use super::{eq_operands, for_each_conjunct, table_column}; + use super::{PropagateFilterConstants, PropagateJoinConstants}; + use crate::binder::test::build_t1_table; + use crate::errors::DatabaseError; + use crate::expression::ScalarExpression; + use crate::optimizer::core::rule::NormalizationRule; + use crate::optimizer::heuristic::batch::HepBatchStrategy; + use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; + use crate::optimizer::plan_utils::wrap_child_with; + use crate::optimizer::rule::normalization::NormalizationRuleImpl; + use crate::planner::operator::filter::FilterOperator; + use crate::planner::operator::Operator; + use crate::planner::{Childrens, LogicalPlan, PlanArena}; + + /// `column = constant` conjuncts of the filters stacked directly on `plan`, as + /// sorted `"column = value"` strings. + fn facts(plan: &LogicalPlan, arena: &PlanArena<'_>) -> Vec { + let mut out = Vec::new(); + let mut node = plan; + while let (Operator::Filter(filter), Childrens::Only(child)) = + (&node.operator, node.childrens.as_ref()) + { + for_each_conjunct(filter.predicate, arena, &mut |conjunct| { + let Some((lhs, rhs)) = eq_operands(conjunct, arena) else { + return; + }; + let (column, constant) = match (table_column(lhs, arena), table_column(rhs, arena)) + { + (Some((column, _)), None) => (column, rhs), + (None, Some((column, _))) => (column, lhs), + _ => return, + }; + if let ScalarExpression::Constant(value) = constant.unpack_alias_ref(arena) { + out.push(format!("{} = {}", arena.column(column).name(), value)); + } + }); + node = child; + } + out.sort(); + out + } + + fn find_filter(plan: &LogicalPlan) -> &LogicalPlan { + match (&plan.operator, plan.childrens.as_ref()) { + (Operator::Filter(_), _) => plan, + (_, Childrens::Only(child)) => find_filter(child), + _ => unreachable!("expected a filter"), + } + } + + fn find_join(plan: &LogicalPlan) -> (&LogicalPlan, &LogicalPlan) { + match (&plan.operator, plan.childrens.as_ref()) { + (Operator::Join(_), Childrens::Twins { left, right }) => (left, right), + (_, Childrens::Only(child)) => find_join(child), + _ => unreachable!("expected a join"), + } + } + + fn find_join_mut(plan: &mut LogicalPlan) -> &mut LogicalPlan { + if matches!(plan.operator, Operator::Join(_)) { + return plan; + } + match plan.childrens.as_mut() { + Childrens::Only(child) => find_join_mut(child), + _ => unreachable!("expected a join"), + } + } + + /// Runs the rules of the default "Predicate Pushdown" batch (same order as `db.rs`). + fn propagate_join(sql: &str) -> Result<(Vec, Vec), DatabaseError> { + let table_state = build_t1_table()?; + let mut arena = PlanArena::new(&table_state.table_arena); + let plan = table_state.plan_with_arena(sql, &mut arena)?; + let mut plan = HepOptimizerPipeline::builder() + .before_batch( + "test_propagate_join_constants", + HepBatchStrategy::fix_point_topdown(30), + vec![ + NormalizationRuleImpl::PropagateFilterConstants, + NormalizationRuleImpl::PropagateJoinConstants, + NormalizationRuleImpl::PushPredicateThroughJoin, + NormalizationRuleImpl::PushJoinPredicateIntoScan, + ], + ) + .build() + .instantiate(plan) + .find_best(None, &mut arena)?; + + // Fix point reached: a second application derives nothing new. + assert!(!PropagateJoinConstants.apply(find_join_mut(&mut plan), &mut arena, None)?); + + let (left, right) = find_join(&plan); + Ok((facts(left, &arena), facts(right, &arena))) + } + + #[test] + fn filter_derives_constant_through_column_equality() -> Result<(), DatabaseError> { + let table_state = build_t1_table()?; + let mut arena = PlanArena::new(&table_state.table_arena); + let mut plan = + table_state.plan_with_arena("select * from t1 where c1 = c2 and c2 = 1", &mut arena)?; + let filter = find_filter_mut(&mut plan); + + assert!(PropagateFilterConstants.apply(filter, &mut arena, None)?); + assert_eq!(facts(filter, &arena), ["c1 = 1", "c2 = 1"]); + // Idempotent: the derived fact is now known, so nothing is added again. + assert!(!PropagateFilterConstants.apply(filter, &mut arena, None)?); + Ok(()) + } + + #[test] + fn filter_closes_equality_chains() -> Result<(), DatabaseError> { + let table_state = build_t1_table()?; + let mut arena = PlanArena::new(&table_state.table_arena); + let mut plan = table_state.plan_with_arena( + "select * from t1, t2 where c1 = c3 and c3 = c2 and c2 = 1", + &mut arena, + )?; + let filter = find_filter_mut(&mut plan); + + assert!(PropagateFilterConstants.apply(filter, &mut arena, None)?); + assert_eq!(facts(filter, &arena), ["c1 = 1", "c2 = 1", "c3 = 1"]); + Ok(()) + } + + #[test] + fn filter_ignores_null_and_non_equality() -> Result<(), DatabaseError> { + let table_state = build_t1_table()?; + for sql in [ + "select * from t1 where c1 = c2 and c2 = null", + "select * from t1 where c1 > c2 and c2 = 1", + "select * from t1 where c1 = c2 + 1 and c2 = 1", + ] { + let mut arena = PlanArena::new(&table_state.table_arena); + let mut plan = table_state.plan_with_arena(sql, &mut arena)?; + let filter = find_filter_mut(&mut plan); + assert!( + !PropagateFilterConstants.apply(filter, &mut arena, None)?, + "{sql}" + ); + } + Ok(()) + } + + #[test] + fn inner_join_propagates_both_ways() -> Result<(), DatabaseError> { + assert_eq!( + propagate_join("select * from t1 join t2 on c1 = c3 where c1 = 1")?, + (vec!["c1 = 1".into()], vec!["c3 = 1".into()]) + ); + assert_eq!( + propagate_join("select * from t1 join t2 on c1 = c3 where c3 = 1")?, + (vec!["c1 = 1".into()], vec!["c3 = 1".into()]) + ); + Ok(()) + } + + #[test] + fn left_join_only_constrains_right_side() -> Result<(), DatabaseError> { + assert_eq!( + propagate_join("select * from t1 left join t2 on c1 = c3 where c1 = 1")?, + (vec!["c1 = 1".into()], vec!["c3 = 1".into()]) + ); + // The non-preserved side's ON constant is pushed down, never derived onto the left. + assert_eq!( + propagate_join("select * from t1 left join t2 on c1 = c3 and c3 = 1")?, + (vec![], vec!["c3 = 1".into()]) + ); + // The preserved side's ON constant stays on the join but still derives the right side. + assert_eq!( + propagate_join("select * from t1 left join t2 on c1 = c3 and c1 = 1")?, + (vec![], vec!["c3 = 1".into()]) + ); + Ok(()) + } + + #[test] + fn right_join_only_constrains_left_side() -> Result<(), DatabaseError> { + assert_eq!( + propagate_join("select * from t1 right join t2 on c1 = c3 where c3 = 1")?, + (vec!["c1 = 1".into()], vec!["c3 = 1".into()]) + ); + assert_eq!( + propagate_join("select * from t1 right join t2 on c1 = c3 and c1 = 1")?, + (vec!["c1 = 1".into()], vec![]) + ); + assert_eq!( + propagate_join("select * from t1 right join t2 on c1 = c3 and c3 = 1")?, + (vec!["c1 = 1".into()], vec![]) + ); + Ok(()) + } + + fn propagate_from_side( + join: &str, + fact_on_left: bool, + ) -> Result<(bool, Vec, Vec), DatabaseError> { + let table_state = build_t1_table()?; + let mut arena = PlanArena::new(&table_state.table_arena); + let mut plan = table_state + .plan_with_arena(format!("select * from t1 {join} t2 on c1 = c3"), &mut arena)?; + let (fact_sql, child_idx) = if fact_on_left { + ("select * from t1 where c1 = 1", 0) + } else { + ("select * from t2 where c3 = 1", 1) + }; + // Same table arena, so the fact's column has the input's summary and position. + let fact_plan = table_state.plan_with_arena(fact_sql, &mut arena)?; + let Operator::Filter(filter) = &find_filter(&fact_plan).operator else { + unreachable!() + }; + let predicate = filter.predicate; + + let join_plan = find_join_mut(&mut plan); + assert!(wrap_child_with( + join_plan, + child_idx, + Operator::Filter(FilterOperator { + predicate, + is_optimized: false, + having: false, + }), + )); + let applied = PropagateJoinConstants.apply(join_plan, &mut arena, None)?; + let (left, right) = find_join(&plan); + Ok((applied, facts(left, &arena), facts(right, &arena))) + } + + #[test] + fn join_type_limits_propagation_direction() -> Result<(), DatabaseError> { + let one = |column: &str| vec![format!("{column} = 1")]; + // (join, fact on left, expected (applied, left facts, right facts)) + let cases = [ + ("join", true, (true, one("c1"), one("c3"))), + ("join", false, (true, one("c1"), one("c3"))), + ("left join", true, (true, one("c1"), one("c3"))), + // Filtering the preserved left side would drop unmatched rows. + ("left join", false, (false, vec![], one("c3"))), + ("right join", true, (false, one("c1"), vec![])), + ("right join", false, (true, one("c1"), one("c3"))), + ("full join", true, (false, one("c1"), vec![])), + ("full join", false, (false, vec![], one("c3"))), + ]; + for (join, fact_on_left, expected) in cases { + assert_eq!( + propagate_from_side(join, fact_on_left)?, + expected, + "{join}, fact on left: {fact_on_left}" + ); + } + Ok(()) + } + + fn find_filter_mut(plan: &mut LogicalPlan) -> &mut LogicalPlan { + if matches!(plan.operator, Operator::Filter(_)) { + return plan; + } + match plan.childrens.as_mut() { + Childrens::Only(child) => find_filter_mut(child), + _ => unreachable!("expected a filter"), + } + } +} diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index c31d5716..102548fc 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -784,6 +784,7 @@ mod tests { use crate::optimizer::heuristic::batch::HepBatchStrategy; use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; use crate::optimizer::rule::normalization::NormalizationRuleImpl; + use crate::planner::operator::aggregate::AggregateOperator; use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::operator::limit::LimitOperator; diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index 9b2700b1..96cddd5b 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -19,6 +19,9 @@ use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::rule::normalization::column_pruning::ColumnPruning; use crate::optimizer::rule::normalization::combine_operators::{CollapseGroupByAgg, CombineFilter}; use crate::optimizer::rule::normalization::compilation_in_advance::EvaluatorBind; +use crate::optimizer::rule::normalization::constant_propagation::{ + PropagateFilterConstants, PropagateJoinConstants, +}; use crate::planner::operator::Operator; use crate::planner::MetaArena; @@ -37,6 +40,7 @@ use std::collections::HashSet; mod column_pruning; mod combine_operators; mod compilation_in_advance; +mod constant_propagation; mod elimination; mod guard_mark_apply; mod join_reorder; @@ -72,6 +76,8 @@ pub enum NormalizationRuleImpl { PushLimitThroughJoin, PushLimitIntoTableScan, // PushDown predicates + PropagateFilterConstants, + PropagateJoinConstants, PushPredicateThroughJoin, PushJoinPredicateIntoScan, // Tips: need to be used with `SimplifyFilter` @@ -190,10 +196,12 @@ impl NormalizationRuleImpl { | NormalizationRuleImpl::PushLimitThroughJoin | NormalizationRuleImpl::PushLimitIntoTableScan | NormalizationRuleImpl::TopK => NormalizationRuleRootTag::Limit, - NormalizationRuleImpl::PushPredicateThroughJoin + NormalizationRuleImpl::PropagateFilterConstants + | NormalizationRuleImpl::PushPredicateThroughJoin | NormalizationRuleImpl::PushPredicateIntoScan | NormalizationRuleImpl::SimplifyFilter => NormalizationRuleRootTag::Filter, - NormalizationRuleImpl::PushJoinPredicateIntoScan => NormalizationRuleRootTag::Join, + NormalizationRuleImpl::PropagateJoinConstants + | NormalizationRuleImpl::PushJoinPredicateIntoScan => NormalizationRuleRootTag::Join, NormalizationRuleImpl::ConstantCalculation => NormalizationRuleRootTag::Any, NormalizationRuleImpl::EvaluatorBind => NormalizationRuleRootTag::Any, NormalizationRuleImpl::MinMaxToTopK => NormalizationRuleRootTag::Aggregate, @@ -238,6 +246,12 @@ impl NormalizationRule for NormalizationRuleImpl { NormalizationRuleImpl::PushLimitIntoTableScan => { PushLimitIntoScan.apply(plan, arena, loader) } + NormalizationRuleImpl::PropagateFilterConstants => { + PropagateFilterConstants.apply(plan, arena, loader) + } + NormalizationRuleImpl::PropagateJoinConstants => { + PropagateJoinConstants.apply(plan, arena, loader) + } NormalizationRuleImpl::PushPredicateThroughJoin => { PushPredicateThroughJoin.apply(plan, arena, loader) } diff --git a/tests/slt/where.slt b/tests/slt/where.slt index 5152726f..26c20549 100644 --- a/tests/slt/where.slt +++ b/tests/slt/where.slt @@ -831,3 +831,134 @@ from native_in_points where id <= 12; statement ok drop table native_in_points; + +# Constant propagation: `a = b AND a = c` derives `b = c`, so the derived side can use an index. + +statement ok +create table cp_a(id int primary key, c1 int, c2 int); + +statement ok +create table cp_b(id int primary key, c1 int, c2 int); + +statement ok +create table cp_c(id int primary key, c1 int, c2 int); + +statement ok +copy cp_a from 'tests/data/row_20000.csv' ( DELIMITER '|' ); + +statement ok +copy cp_b from 'tests/data/row_20000.csv' ( DELIMITER '|' ); + +statement ok +copy cp_c from 'tests/data/row_20000.csv' ( DELIMITER '|' ); + +statement ok +create index cp_b_c1 on cp_b (c1); + +statement ok +create index cp_c_c1 on cp_c (c1); + +statement ok +analyze table cp_a; + +statement ok +analyze table cp_b; + +statement ok +analyze table cp_c; + +# Inner join: a constant on the ON key's left column reaches the right scan. +query T +explain select cp_a.id, cp_b.id from cp_a join cp_b on cp_a.c1 = cp_b.c1 where cp_a.c1 = 4; +---- +Projection [cp_a.id, cp_b.id] [Project => (Sort Option: Follow)] Inner Join On cp_a.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] Filter (cp_a.c1 = 4), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_a -> [cp_a.id, cp_a.c1] [SeqScan => (Sort Option: None)] TableScan cp_b -> [cp_b.id, cp_b.c1] [IndexScan By cp_b_c1 => 4 => (Sort Option: OrderBy: (cp_b.c1 Asc Nulls Last) ignore_prefix_len: 0)] + +query II +select cp_a.id, cp_b.id from cp_a join cp_b on cp_a.c1 = cp_b.c1 where cp_a.c1 = 4; +---- +3 3 + +query T +explain select cp_a.id, cp_b.id from cp_a, cp_b where cp_a.c1 = cp_b.c1 and cp_a.c1 = 4; +---- +Projection [cp_a.id, cp_b.id] [Project => (Sort Option: Follow)] Inner Join On cp_a.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] Filter (cp_a.c1 = 4), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_a -> [cp_a.id, cp_a.c1] [SeqScan => (Sort Option: None)] TableScan cp_b -> [cp_b.id, cp_b.c1] [IndexScan By cp_b_c1 => 4 => (Sort Option: OrderBy: (cp_b.c1 Asc Nulls Last) ignore_prefix_len: 0)] + +query II +select cp_a.id, cp_b.id from cp_a, cp_b where cp_a.c1 = cp_b.c1 and cp_a.c1 = 4; +---- +3 3 + +# Chains close transitively inside one filter. +query T +explain select cp_a.id, cp_b.id, cp_c.id from cp_a, cp_b, cp_c where cp_a.c1 = cp_b.c1 and cp_b.c1 = cp_c.c1 and cp_a.c1 = 4; +---- +Projection [cp_a.id, cp_b.id, cp_c.id] [Project => (Sort Option: Follow)] Inner Join On cp_c.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] TableScan cp_c -> [cp_c.id, cp_c.c1] [IndexScan By cp_c_c1 => 4 => (Sort Option: OrderBy: (cp_c.c1 Asc Nulls Last) ignore_prefix_len: 0)] Inner Join On cp_a.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] Filter (cp_a.c1 = 4), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_a -> [cp_a.id, cp_a.c1] [SeqScan => (Sort Option: None)] TableScan cp_b -> [cp_b.id, cp_b.c1] [IndexScan By cp_b_c1 => 4 => (Sort Option: OrderBy: (cp_b.c1 Asc Nulls Last) ignore_prefix_len: 0)] + +query III +select cp_a.id, cp_b.id, cp_c.id from cp_a, cp_b, cp_c where cp_a.c1 = cp_b.c1 and cp_b.c1 = cp_c.c1 and cp_a.c1 = 4; +---- +3 3 3 + +# Single table. +query T +explain select id from cp_b where c1 = c2 and c2 = 4; +---- +Projection [cp_b.id] [Project => (Sort Option: Follow)] Filter ((cp_b.c1 = cp_b.c2) && (cp_b.c2 = 4)), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_b -> [cp_b.id, cp_b.c1, cp_b.c2] [IndexScan By cp_b_c1 => 4 => (Sort Option: OrderBy: (cp_b.c1 Asc Nulls Last) ignore_prefix_len: 0)] + +query I +select id from cp_b where c1 = c2 and c2 = 4; +---- + + +# Left join: the preserved left side may constrain the right side. +query T +explain select cp_a.id, cp_b.id from cp_a left join cp_b on cp_a.c1 = cp_b.c1 where cp_a.c1 = 4; +---- +Projection [cp_a.id, cp_b.id] [Project => (Sort Option: Follow)] LeftOuter Join On cp_a.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] Filter (cp_a.c1 = 4), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_a -> [cp_a.id, cp_a.c1] [SeqScan => (Sort Option: None)] TableScan cp_b -> [cp_b.id, cp_b.c1] [IndexScan By cp_b_c1 => 4 => (Sort Option: OrderBy: (cp_b.c1 Asc Nulls Last) ignore_prefix_len: 0)] + +query II +select cp_a.id, cp_b.id from cp_a left join cp_b on cp_a.c1 = cp_b.c1 where cp_a.c1 = 4; +---- +3 3 + +# Left join: a right-side constant must not filter the preserved left side. +query T +explain select count(*), count(cp_b.id) from cp_a left join cp_b on cp_a.c1 = cp_b.c1 and cp_b.c1 = 4; +---- +Projection [Count(*), Count(cp_b.id)] [Project => (Sort Option: Follow)] Aggregate [Count(*), Count(cp_b.id)] [SimpleAggregate => (Sort Option: None)] LeftOuter Join On cp_a.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] TableScan cp_a -> [cp_a.c1] [SeqScan => (Sort Option: None)] TableScan cp_b -> [cp_b.id, cp_b.c1] [IndexScan By cp_b_c1 => 4 => (Sort Option: OrderBy: (cp_b.c1 Asc Nulls Last) ignore_prefix_len: 0)] + +query II +select count(*), count(cp_b.id) from cp_a left join cp_b on cp_a.c1 = cp_b.c1 and cp_b.c1 = 4; +---- +20000 1 + +# Right join: a left-side constant must not filter the preserved right side. +query T +explain select count(*), count(cp_a.id) from cp_a right join cp_b on cp_a.c1 = cp_b.c1 and cp_a.c1 = 4; +---- +Projection [Count(*), Count(cp_a.id)] [Project => (Sort Option: Follow)] Aggregate [Count(*), Count(cp_a.id)] [SimpleAggregate => (Sort Option: None)] RightOuter Join On cp_a.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] Filter (cp_a.c1 = 4), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_a -> [cp_a.id, cp_a.c1] [SeqScan => (Sort Option: None)] TableScan cp_b -> [cp_b.c1] [SeqScan => (Sort Option: None)] + +query II +select count(*), count(cp_a.id) from cp_a right join cp_b on cp_a.c1 = cp_b.c1 and cp_a.c1 = 4; +---- +20000 1 + +# NULL constants carry nothing to propagate. +query T +explain select count(*) from cp_a join cp_b on cp_a.c1 = cp_b.c1 where cp_a.c1 = null; +---- +Projection [Count(*)] [Project => (Sort Option: Follow)] Aggregate [Count(*)] [SimpleAggregate => (Sort Option: None)] Inner Join On cp_a.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] Filter (cp_a.c1 = cast (NULL as Integer)), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_a -> [cp_a.c1] [SeqScan => (Sort Option: None)] TableScan cp_b -> [cp_b.c1] [SeqScan => (Sort Option: None)] + +query I +select count(*) from cp_a join cp_b on cp_a.c1 = cp_b.c1 where cp_a.c1 = null; +---- +0 + +statement ok +drop table cp_a; + +statement ok +drop table cp_b; + +statement ok +drop table cp_c; From 7c8766ee36be5a99e8b20541f03354be37d3bffa Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 30/62] refactor(optimizer): take rule lists as slices HepBatch::new, before_batch/after_batch and implementations only iterate the given rules to build their own grouped storage, so accept &[NormalizationRuleImpl] / &[ImplementationRuleImpl] instead of owned Vecs. The pipeline builder now builds ImplementationRuleIndex directly instead of keeping an intermediate Vec. Also drop constant propagation from RFC 0001 future work, since it is implemented in 179c4b9. --- ...greedy-join-reorder-owns-join-decisions.md | 2 - src/db.rs | 40 +++++++-------- src/execution/dql/aggregate/hash_agg.rs | 2 +- .../dql/aggregate/stream_distinct.rs | 2 +- src/execution/dql/join/hash_join.rs | 2 +- src/execution/dql/join/nested_loop_join.rs | 2 +- src/expression/range_detacher.rs | 2 +- src/optimizer/heuristic/batch.rs | 4 +- src/optimizer/heuristic/optimizer.rs | 50 +++++++++---------- .../rule/implementation/dql/filter.rs | 2 +- .../rule/normalization/column_pruning.rs | 4 +- .../rule/normalization/combine_operators.rs | 6 +-- .../normalization/constant_propagation.rs | 2 +- .../rule/normalization/elimination.rs | 4 +- .../rule/normalization/join_reorder.rs | 2 +- .../rule/normalization/pushdown_limit.rs | 6 +-- .../rule/normalization/pushdown_predicates.rs | 22 ++++---- .../rule/normalization/simplification.rs | 14 +++--- src/planner/mod.rs | 4 +- 19 files changed, 85 insertions(+), 87 deletions(-) diff --git a/rfc/0001-greedy-join-reorder-owns-join-decisions.md b/rfc/0001-greedy-join-reorder-owns-join-decisions.md index afbfd958..02525064 100644 --- a/rfc/0001-greedy-join-reorder-owns-join-decisions.md +++ b/rfc/0001-greedy-join-reorder-owns-join-decisions.md @@ -362,8 +362,6 @@ hash 输入或改写为 probe 时,不会丢失上层依赖的有序性、也 - 开启 `materialize` 的 MarkApply 的 `rescan_work`;以及 NLJ 内侧含子查询类 MarkApply 时, Greedy 按其参数化 / 物化前的估算决定方向的偏差(3.10)。 -- 常量传播:目前没有等价类 / 传递闭包,`pa.id = 1 AND pa.id = pb.a_id` 推不出 `pb.a_id = 1`(已用 explain 实测)。 - 它是独立的改写规则,加入后 Greedy 的叶子会多出静态 range,也多出可用于凑齐 probe 索引的常量等值条件,与本 RFC 的设计不冲突。 - 非唯一索引 probe:执行器需支持每个外侧行输出多行,且每行 reset 内侧开销较大,按具体 case 评估; 届时 `rows_per_lookup` 由索引 NDV 估算,10 个候选上限才真正起作用。 - I/O 建模。 diff --git a/src/db.rs b/src/db.rs index b9a48204..c66d30e6 100644 --- a/src/db.rs +++ b/src/db.rs @@ -445,22 +445,22 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { .before_batch( "Column Pruning", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::ColumnPruning], + &[NormalizationRuleImpl::ColumnPruning], ) .before_batch( "Simplify Filter", HepBatchStrategy::fix_point_topdown(10), - vec![NormalizationRuleImpl::SimplifyFilter], + &[NormalizationRuleImpl::SimplifyFilter], ) .before_batch( "Constant Calculation", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::ConstantCalculation], + &[NormalizationRuleImpl::ConstantCalculation], ) .before_batch( "Predicate Pushdown", HepBatchStrategy::fix_point_topdown(30), - vec![ + &[ NormalizationRuleImpl::PropagateFilterConstants, NormalizationRuleImpl::PropagateJoinConstants, NormalizationRuleImpl::PushPredicateThroughJoin, @@ -470,7 +470,7 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { .before_batch( "Limit Pushdown", HepBatchStrategy::fix_point_topdown(10), - vec![ + &[ NormalizationRuleImpl::LimitProjectTranspose, NormalizationRuleImpl::PushLimitThroughJoin, ], @@ -478,7 +478,7 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { .before_batch( "TopK", HepBatchStrategy::fix_point_topdown(1), - vec![ + &[ NormalizationRuleImpl::MinMaxToTopK, NormalizationRuleImpl::TopK, ], @@ -486,12 +486,12 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { .before_batch( "Collapse Project", HepBatchStrategy::fix_point_bottomup(10), - vec![NormalizationRuleImpl::EliminateProject], + &[NormalizationRuleImpl::EliminateProject], ) .before_batch( "Combine Operators", HepBatchStrategy::fix_point_topdown(10), - vec![ + &[ NormalizationRuleImpl::CollapseGroupByAgg, NormalizationRuleImpl::CombineFilter, ], @@ -499,22 +499,22 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { .before_batch( "Predicate Into Scan", HepBatchStrategy::fix_point_topdown(10), - vec![NormalizationRuleImpl::PushPredicateIntoScan], + &[NormalizationRuleImpl::PushPredicateIntoScan], ) .before_batch( "Guard Mark Apply", HepBatchStrategy::fix_point_topdown(1), - vec![NormalizationRuleImpl::GuardMarkApply], + &[NormalizationRuleImpl::GuardMarkApply], ) .after_batch( "Greedy Join Reorder", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::GreedyJoinReorder], + &[NormalizationRuleImpl::GreedyJoinReorder], ) .after_batch( "Parameterize Mark Apply", HepBatchStrategy::fix_point_topdown(1), - vec![ + &[ NormalizationRuleImpl::ParameterizeMarkApply, NormalizationRuleImpl::ParameterizeInnerJoin, ], @@ -524,39 +524,39 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { .after_batch( "Eliminate Index Filter", HepBatchStrategy::fix_point_topdown(1), - vec![NormalizationRuleImpl::EliminateIndexFilter], + &[NormalizationRuleImpl::EliminateIndexFilter], ) .after_batch( "Materialize Mark Apply", HepBatchStrategy::fix_point_bottomup(1), - vec![NormalizationRuleImpl::MaterializeMarkApply], + &[NormalizationRuleImpl::MaterializeMarkApply], ) .after_batch( "Orient Nested Loop Join", HepBatchStrategy::fix_point_bottomup(1), - vec![NormalizationRuleImpl::OrientNestedLoopJoin], + &[NormalizationRuleImpl::OrientNestedLoopJoin], ) .after_batch( "Collapse Project", HepBatchStrategy::fix_point_bottomup(10), - vec![NormalizationRuleImpl::EliminateProject], + &[NormalizationRuleImpl::EliminateProject], ) .after_batch( "Expression Remapper", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::EvaluatorBind], + &[NormalizationRuleImpl::EvaluatorBind], ) .after_batch( "Limit Into Scan", HepBatchStrategy::fix_point_topdown(10), - vec![NormalizationRuleImpl::PushLimitIntoTableScan], + &[NormalizationRuleImpl::PushLimitIntoTableScan], ) .after_batch( "Move Project Inputs", HepBatchStrategy::fix_point_bottomup(1), - vec![NormalizationRuleImpl::ProjectMove], + &[NormalizationRuleImpl::ProjectMove], ) - .implementations(vec![ + .implementations(&[ // DQL ImplementationRuleImpl::SimpleAggregate, ImplementationRuleImpl::GroupByAggregate, diff --git a/src/execution/dql/aggregate/hash_agg.rs b/src/execution/dql/aggregate/hash_agg.rs index 442f894e..348f56f2 100644 --- a/src/execution/dql/aggregate/hash_agg.rs +++ b/src/execution/dql/aggregate/hash_agg.rs @@ -215,7 +215,7 @@ mod test { .before_batch( "Expression Remapper", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::EvaluatorBind], + &[NormalizationRuleImpl::EvaluatorBind], ) .build(); let plan = pipeline diff --git a/src/execution/dql/aggregate/stream_distinct.rs b/src/execution/dql/aggregate/stream_distinct.rs index 09574872..79375b9b 100644 --- a/src/execution/dql/aggregate/stream_distinct.rs +++ b/src/execution/dql/aggregate/stream_distinct.rs @@ -141,7 +141,7 @@ mod tests { .before_batch( "Expression Remapper", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::EvaluatorBind], + &[NormalizationRuleImpl::EvaluatorBind], ) .build() .instantiate(plan) diff --git a/src/execution/dql/join/hash_join.rs b/src/execution/dql/join/hash_join.rs index 3e3e6bb4..a2d3644f 100644 --- a/src/execution/dql/join/hash_join.rs +++ b/src/execution/dql/join/hash_join.rs @@ -406,7 +406,7 @@ mod test { .before_batch( "Expression Remapper", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::EvaluatorBind], + &[NormalizationRuleImpl::EvaluatorBind], ) .build() .instantiate(plan) diff --git a/src/execution/dql/join/nested_loop_join.rs b/src/execution/dql/join/nested_loop_join.rs index 75863995..00463816 100644 --- a/src/execution/dql/join/nested_loop_join.rs +++ b/src/execution/dql/join/nested_loop_join.rs @@ -392,7 +392,7 @@ mod test { .before_batch( "Expression Remapper", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::EvaluatorBind], + &[NormalizationRuleImpl::EvaluatorBind], ) .build() .instantiate(plan) diff --git a/src/expression/range_detacher.rs b/src/expression/range_detacher.rs index cdbfcf25..2186a358 100644 --- a/src/expression/range_detacher.rs +++ b/src/expression/range_detacher.rs @@ -1355,7 +1355,7 @@ mod test { .before_batch( "test_simplify_filter", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::SimplifyFilter], + &[NormalizationRuleImpl::SimplifyFilter], ) .build(); let best_plan = pipeline.instantiate(plan).find_best(None, arena)?; diff --git a/src/optimizer/heuristic/batch.rs b/src/optimizer/heuristic/batch.rs index 6c28f440..8269e350 100644 --- a/src/optimizer/heuristic/batch.rs +++ b/src/optimizer/heuristic/batch.rs @@ -92,11 +92,11 @@ impl HepBatch { pub fn new( name: &'static str, strategy: HepBatchStrategy, - rules: Vec, + rules: &[NormalizationRuleImpl], ) -> Self { let mut steps = Vec::new(); - for rule in rules { + for &rule in rules { match rule.pass_kind() { NormalizationPassKind::WholeTreePass(kind) => match steps.last_mut() { Some(HepBatchStep::WholeTree(pass)) if pass.kind == kind => { diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index 252e1ed0..f6fcebed 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -472,9 +472,9 @@ struct ImplementationRuleIndex { } impl ImplementationRuleIndex { - fn new(implementations: Vec) -> Self { + fn new(implementations: &[ImplementationRuleImpl]) -> Self { let mut groups = array::from_fn(|_| Vec::new()); - for implementation in implementations { + for &implementation in implementations { groups[implementation.root_tag() as usize].push(implementation); } Self { groups } @@ -745,7 +745,7 @@ mod tests { ndv: None, }, )); - let implementations = ImplementationRuleIndex::new(vec![ImplementationRuleImpl::Filter]); + let implementations = ImplementationRuleIndex::new(&[ImplementationRuleImpl::Filter]); assert!(implementations .direct_physical_option(&plan.operator) .is_none()); @@ -771,7 +771,7 @@ mod tests { )); let right = LogicalPlan::new(Operator::Dummy, Childrens::None); let mut plan = MarkApplyOperator::build_exists(plan, right, marker, vec![]); - let implementations = ImplementationRuleIndex::new(vec![ImplementationRuleImpl::MarkApply]); + let implementations = ImplementationRuleIndex::new(&[ImplementationRuleImpl::MarkApply]); plan.physical_option = implementations.direct_physical_option(&plan.operator); assert_eq!(plan.estimated_rows(), Some(12)); let Operator::MarkApply(op) = &mut plan.operator else { @@ -794,7 +794,7 @@ mod tests { let batch = HepBatch::new( "Combine Filters", HepBatchStrategy::fix_point_bottomup(1), - vec![NormalizationRuleImpl::CombineFilter], + &[NormalizationRuleImpl::CombineFilter], ); let mut applied_rules = Vec::new(); let mut topdown = filter_chain(3, &mut arena); @@ -847,7 +847,7 @@ mod tests { .before_batch( "Combine Filters", strategy, - vec![NormalizationRuleImpl::CombineFilter], + &[NormalizationRuleImpl::CombineFilter], ) .build(); let result = pipeline.instantiate(plan).find_best(None, &mut arena)?; @@ -879,7 +879,7 @@ mod tests { .before_batch( "Combine Filters", strategy, - vec![NormalizationRuleImpl::CombineFilter], + &[NormalizationRuleImpl::CombineFilter], ) .build(); let result = pipeline.instantiate(plan).find_best(None, &mut arena)?; @@ -913,7 +913,7 @@ mod tests { .before_batch( "Limit Project Transpose", strategy, - vec![NormalizationRuleImpl::LimitProjectTranspose], + &[NormalizationRuleImpl::LimitProjectTranspose], ) .build(); let result = pipeline.instantiate(plan).find_best(None, &mut arena)?; @@ -944,7 +944,7 @@ mod tests { .before_batch( "Limit Project Transpose", HepBatchStrategy::fix_point_bottomup(iterations), - vec![NormalizationRuleImpl::LimitProjectTranspose], + &[NormalizationRuleImpl::LimitProjectTranspose], ) .build(); let result = pipeline.instantiate(plan).find_best(None, &mut arena)?; @@ -1006,12 +1006,12 @@ mod tests { .before_batch( "Simplify Filter", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::SimplifyFilter], + &[NormalizationRuleImpl::SimplifyFilter], ) .before_batch( "Predicate Pushdown", HepBatchStrategy::fix_point_topdown(10), - vec![ + &[ NormalizationRuleImpl::PushPredicateThroughJoin, NormalizationRuleImpl::PushJoinPredicateIntoScan, NormalizationRuleImpl::PushPredicateIntoScan, @@ -1020,9 +1020,9 @@ mod tests { .after_batch( "Eliminate Index Filter", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::EliminateIndexFilter], + &[NormalizationRuleImpl::EliminateIndexFilter], ) - .implementations(vec![ + .implementations(&[ ImplementationRuleImpl::Projection, ImplementationRuleImpl::Filter, ImplementationRuleImpl::HashJoin, @@ -1090,14 +1090,14 @@ impl HepOptimizerPipeline { HepOptimizerPipelineBuilder { before_batches: vec![], after_batches: vec![], - implementations: vec![], + implementation_index: ImplementationRuleIndex::new(&[]), } } pub fn new( before_batches: Vec, after_batches: Vec, - implementations: Vec, + implementations: &[ImplementationRuleImpl], ) -> Self { Self { before_batches, @@ -1119,7 +1119,7 @@ impl HepOptimizerPipeline { pub struct HepOptimizerPipelineBuilder { before_batches: Vec, after_batches: Vec, - implementations: Vec, + implementation_index: ImplementationRuleIndex, } impl HepOptimizerPipelineBuilder { @@ -1127,7 +1127,7 @@ impl HepOptimizerPipelineBuilder { mut self, name: &'static str, strategy: HepBatchStrategy, - rules: Vec, + rules: &[NormalizationRuleImpl], ) -> Self { self.before_batches .push(HepBatch::new(name, strategy, rules)); @@ -1138,23 +1138,23 @@ impl HepOptimizerPipelineBuilder { mut self, name: &'static str, strategy: HepBatchStrategy, - rules: Vec, + rules: &[NormalizationRuleImpl], ) -> Self { self.after_batches .push(HepBatch::new(name, strategy, rules)); self } - pub fn implementations(mut self, implementations: Vec) -> Self { - self.implementations = implementations; + pub fn implementations(mut self, implementations: &[ImplementationRuleImpl]) -> Self { + self.implementation_index = ImplementationRuleIndex::new(implementations); self } pub fn build(self) -> HepOptimizerPipeline { - HepOptimizerPipeline::new( - self.before_batches, - self.after_batches, - self.implementations, - ) + HepOptimizerPipeline { + before_batches: self.before_batches, + after_batches: self.after_batches, + implementation_index: self.implementation_index, + } } } diff --git a/src/optimizer/rule/implementation/dql/filter.rs b/src/optimizer/rule/implementation/dql/filter.rs index 6a594b92..9e030bb2 100644 --- a/src/optimizer/rule/implementation/dql/filter.rs +++ b/src/optimizer/rule/implementation/dql/filter.rs @@ -572,7 +572,7 @@ mod tests { let loader = StatisticMetaLoader::new(&cache); assert_eq!(estimate_filter_rows(&plan, &arena, &loader)?, Some(10)); let pipeline = HepOptimizerPipeline::builder() - .implementations(vec![ImplementationRuleImpl::Filter]) + .implementations(&[ImplementationRuleImpl::Filter]) .build(); let mut plan = pipeline .instantiate(plan) diff --git a/src/optimizer/rule/normalization/column_pruning.rs b/src/optimizer/rule/normalization/column_pruning.rs index 22033018..244716ee 100644 --- a/src/optimizer/rule/normalization/column_pruning.rs +++ b/src/optimizer/rule/normalization/column_pruning.rs @@ -872,7 +872,7 @@ mod tests { .before_batch( "column_pruning", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::ColumnPruning], + &[NormalizationRuleImpl::ColumnPruning], ) .build() .instantiate(plan) @@ -1024,7 +1024,7 @@ mod tests { .before_batch( "test_column_pruning", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::ColumnPruning], + &[NormalizationRuleImpl::ColumnPruning], ) .build(); let best_plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; diff --git a/src/optimizer/rule/normalization/combine_operators.rs b/src/optimizer/rule/normalization/combine_operators.rs index 4b5bda13..f8d6edb8 100644 --- a/src/optimizer/rule/normalization/combine_operators.rs +++ b/src/optimizer/rule/normalization/combine_operators.rs @@ -183,12 +183,12 @@ mod tests { .before_batch( "Collapse Project", HepBatchStrategy::fix_point_topdown(10), - vec![NormalizationRuleImpl::EliminateProject], + &[NormalizationRuleImpl::EliminateProject], ) .before_batch( "test_combine_filter", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::CombineFilter], + &[NormalizationRuleImpl::CombineFilter], ) .build(); let best_plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; @@ -218,7 +218,7 @@ mod tests { .before_batch( "test_collapse_group_by_agg", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::CollapseGroupByAgg], + &[NormalizationRuleImpl::CollapseGroupByAgg], ) .build(); diff --git a/src/optimizer/rule/normalization/constant_propagation.rs b/src/optimizer/rule/normalization/constant_propagation.rs index 394e972f..ea271279 100644 --- a/src/optimizer/rule/normalization/constant_propagation.rs +++ b/src/optimizer/rule/normalization/constant_propagation.rs @@ -423,7 +423,7 @@ mod tests { .before_batch( "test_propagate_join_constants", HepBatchStrategy::fix_point_topdown(30), - vec![ + &[ NormalizationRuleImpl::PropagateFilterConstants, NormalizationRuleImpl::PropagateJoinConstants, NormalizationRuleImpl::PushPredicateThroughJoin, diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index 102548fc..b309f1ba 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -1209,7 +1209,7 @@ mod tests { .before_batch( "test_collapse_project", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::EliminateProject], + &[NormalizationRuleImpl::EliminateProject], ) .build(); let best_plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; @@ -1243,7 +1243,7 @@ mod tests { .before_batch( "test_collapse_project_with_alias", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::EliminateProject], + &[NormalizationRuleImpl::EliminateProject], ) .build(); let best_plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 1db54f2c..5fb98c21 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -1354,7 +1354,7 @@ mod tests { .after_batch( "column statistics", HepBatchStrategy::fix_point_topdown(1), - vec![NormalizationRuleImpl::GreedyJoinReorder], + &[NormalizationRuleImpl::GreedyJoinReorder], ) .build(); let plan = pipeline diff --git a/src/optimizer/rule/normalization/pushdown_limit.rs b/src/optimizer/rule/normalization/pushdown_limit.rs index 9f232dbd..84f1a46c 100644 --- a/src/optimizer/rule/normalization/pushdown_limit.rs +++ b/src/optimizer/rule/normalization/pushdown_limit.rs @@ -155,7 +155,7 @@ mod tests { .before_batch( "test_limit_project_transpose", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::LimitProjectTranspose], + &[NormalizationRuleImpl::LimitProjectTranspose], ) .build(); let best_plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; @@ -187,7 +187,7 @@ mod tests { .before_batch( "test_push_limit_through_join", HepBatchStrategy::once_topdown(), - vec![ + &[ NormalizationRuleImpl::LimitProjectTranspose, NormalizationRuleImpl::PushLimitThroughJoin, ], @@ -221,7 +221,7 @@ mod tests { .before_batch( "test_push_limit_into_table_scan", HepBatchStrategy::once_topdown(), - vec![ + &[ NormalizationRuleImpl::LimitProjectTranspose, NormalizationRuleImpl::PushLimitIntoTableScan, ], diff --git a/src/optimizer/rule/normalization/pushdown_predicates.rs b/src/optimizer/rule/normalization/pushdown_predicates.rs index eefc893d..c7c0a0c4 100644 --- a/src/optimizer/rule/normalization/pushdown_predicates.rs +++ b/src/optimizer/rule/normalization/pushdown_predicates.rs @@ -725,12 +725,12 @@ mod tests { .before_batch( "simplify_filter", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::SimplifyFilter], + &[NormalizationRuleImpl::SimplifyFilter], ) .before_batch( "test_push_predicate_into_scan", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::PushPredicateIntoScan], + &[NormalizationRuleImpl::PushPredicateIntoScan], ), &mut arena, )?; @@ -826,7 +826,7 @@ mod tests { HepOptimizerPipeline::builder().before_batch( "unique_composite_range", HepBatchStrategy::fix_point_topdown(1), - vec![NormalizationRuleImpl::PushPredicateIntoScan], + &[NormalizationRuleImpl::PushPredicateIntoScan], ), &mut arena, )?; @@ -1085,7 +1085,7 @@ mod tests { HepOptimizerPipeline::builder().before_batch( "push_cover_mapping", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::PushPredicateIntoScan], + &[NormalizationRuleImpl::PushPredicateIntoScan], ), &mut arena, )?; @@ -1148,7 +1148,7 @@ mod tests { HepOptimizerPipeline::builder().before_batch( "test_push_predicate_through_join", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::PushPredicateThroughJoin], + &[NormalizationRuleImpl::PushPredicateThroughJoin], ), &mut arena, )?; @@ -1198,7 +1198,7 @@ mod tests { HepOptimizerPipeline::builder().before_batch( "test_push_predicate_through_join", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::PushPredicateThroughJoin], + &[NormalizationRuleImpl::PushPredicateThroughJoin], ), &mut arena, )?; @@ -1248,7 +1248,7 @@ mod tests { HepOptimizerPipeline::builder().before_batch( "cross join pushdown", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::PushPredicateThroughJoin], + &[NormalizationRuleImpl::PushPredicateThroughJoin], ), &mut arena, )?; @@ -1281,7 +1281,7 @@ mod tests { HepOptimizerPipeline::builder().before_batch( "test_push_predicate_through_join", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::PushPredicateThroughJoin], + &[NormalizationRuleImpl::PushPredicateThroughJoin], ), &mut arena, )?; @@ -1336,7 +1336,7 @@ mod tests { HepOptimizerPipeline::builder().before_batch( "push_join_predicate_into_scan", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::PushJoinPredicateIntoScan], + &[NormalizationRuleImpl::PushJoinPredicateIntoScan], ), &mut arena, )?; @@ -1412,7 +1412,7 @@ mod tests { HepOptimizerPipeline::builder().before_batch( "push_join_predicate_into_scan", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::PushJoinPredicateIntoScan], + &[NormalizationRuleImpl::PushJoinPredicateIntoScan], ), &mut arena, )?; @@ -1460,7 +1460,7 @@ mod tests { HepOptimizerPipeline::builder().before_batch( "push_join_predicate_into_scan", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::PushJoinPredicateIntoScan], + &[NormalizationRuleImpl::PushJoinPredicateIntoScan], ), &mut arena, )?; diff --git a/src/optimizer/rule/normalization/simplification.rs b/src/optimizer/rule/normalization/simplification.rs index 3be9ec53..3d824201 100644 --- a/src/optimizer/rule/normalization/simplification.rs +++ b/src/optimizer/rule/normalization/simplification.rs @@ -136,7 +136,7 @@ mod test { plan: LogicalPlan, name: &'static str, strategy: HepBatchStrategy, - rules: Vec, + rules: &[NormalizationRuleImpl], arena: &mut PlanArena, ) -> Result { HepOptimizerPipeline::builder() @@ -160,7 +160,7 @@ mod test { .before_batch( "test_simplification", HepBatchStrategy::once_topdown(), - vec![ + &[ NormalizationRuleImpl::SimplifyFilter, NormalizationRuleImpl::ConstantCalculation, ], @@ -214,7 +214,7 @@ mod test { plan, "test_constant_cast_elimination", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::ConstantCalculation], + &[NormalizationRuleImpl::ConstantCalculation], &mut arena, )?; @@ -313,7 +313,7 @@ mod test { plan, "test_simplify_filter", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::SimplifyFilter], + &[NormalizationRuleImpl::SimplifyFilter], &mut arena, )?; @@ -385,7 +385,7 @@ mod test { plan, "test_simplify_filter", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::SimplifyFilter], + &[NormalizationRuleImpl::SimplifyFilter], &mut arena, )?; @@ -439,7 +439,7 @@ mod test { plan, "test_non_comparison_does_not_apply_pending_rearrangement", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::SimplifyFilter], + &[NormalizationRuleImpl::SimplifyFilter], &mut arena, )?; let filter = best_plan.childrens.pop_only(); @@ -481,7 +481,7 @@ mod test { plan.clone(), "test_simplify_filter", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::SimplifyFilter], + &[NormalizationRuleImpl::SimplifyFilter], arena, )?; diff --git a/src/planner/mod.rs b/src/planner/mod.rs index 040ebf60..6aec5bce 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -1015,7 +1015,7 @@ mod tests { .before_batch( "Transpose", strategy, - vec![NormalizationRuleImpl::LimitProjectTranspose], + &[NormalizationRuleImpl::LimitProjectTranspose], ) .build(); let mut plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; @@ -1103,7 +1103,7 @@ mod tests { .before_batch( "Push Predicate", HepBatchStrategy::once_topdown(), - vec![NormalizationRuleImpl::PushPredicateThroughJoin], + &[NormalizationRuleImpl::PushPredicateThroughJoin], ) .build(); let mut plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; From e90e8acba57d6def47cc2247433a9f2139fcc278 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 31/62] test(join_reorder): update greedy tests to current build-side behavior Twelve greedy tests predated two behaviors and had been failing: - the smaller connected input is swapped to the hash build (left) side, so rebuilt regions now carry a Project restoring the original output order; - two-input and unestimated regions are rebuilt too (build side by size, or by width when rows are unknown). Expectations now assert the same merge order with the build side on the left. The same-input key case reports "not produced by any join input", because each key side is only searched within its own input range. --- .../rule/normalization/join_reorder.rs | 88 +++++++++++-------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 5fb98c21..2186ad43 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -950,7 +950,8 @@ mod tests { panic!("expected a projection restoring A, B, C output order"); }; assert_eq!(project.exprs.len(), 6); - for (expr, position) in project.exprs.iter().zip([0, 1, 5, 2, 3, 4]) { + // C (smaller) builds against A, so the join emits C, A, B. + for (expr, position) in project.exprs.iter().zip([3, 4, 5, 0, 1, 2]) { assert!(matches!( arena.expression(*expr), ScalarExpression::ColumnRef { position: actual, .. } if *actual == position @@ -1115,12 +1116,13 @@ mod tests { #[test] fn malformed_regions_are_planning_errors() { - // (outer join key, expected message) + // (outer join key, expected message). Each side of a key is only searched in its own + // input range, so a left column used on the right is reported as not produced. let cases: [( fn(ExprRef, ExprRef, &mut PlanArena) -> (ExprRef, ExprRef), &str, ); 2] = [ - (|a, _, _| (a, a), "compares columns of the same join input"), + (|a, _, _| (a, a), "is not produced by any join input"), ( |_, c, arena| (input("z", 1, arena).1, c), "is not produced by any join input", @@ -1806,9 +1808,11 @@ mod tests { SortOption::None, RowCountOption::Unknown, )); - assert!(!GreedyJoinReorder.apply(&mut join, &mut arena, None)?); + // The two-input region is rebuilt so the smaller right input becomes the build side. + assert!(GreedyJoinReorder.apply(&mut join, &mut arena, None)?); + let join = join.childrens.only(); assert_eq!(join.estimated_rows(), Some(2000)); - assert_eq!(index_statistics(&join), None); + assert_eq!(index_statistics(join), None); Ok(()) } @@ -1850,8 +1854,8 @@ mod tests { RowCountOption::Unknown, )); assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); - let (lr, _) = plan.childrens.twins(); - assert_eq!(lr.referenced_table(), vec!["l".into(), "r".into()]); + let (_, lr) = plan.childrens.only().childrens.twins(); + assert_eq!(lr.referenced_table(), vec!["r".into(), "l".into()]); assert_eq!(lr.estimated_rows(), Some(2000)); assert_eq!(index_statistics(lr), None); assert_eq!(plan.estimated_rows(), Some(40000)); @@ -2266,11 +2270,12 @@ mod tests { assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); assert_eq!(plan.estimated_rows(), Some(200000)); - let (left, right) = plan.childrens.twins(); - assert_eq!(left.referenced_table(), vec!["a".into()]); - assert_eq!(right.referenced_table(), vec!["b".into(), "c".into()]); - assert_eq!(right.estimated_rows(), Some(200)); - let Operator::Join(op) = &right.operator else { + // BC (200 rows) is merged first and builds against A (1000 rows). + let (bc, a) = plan.childrens.only().childrens.twins(); + assert_eq!(a.referenced_table(), vec!["a".into()]); + assert_eq!(bc.referenced_table(), vec!["b".into(), "c".into()]); + assert_eq!(bc.estimated_rows(), Some(200)); + let Operator::Join(op) = &bc.operator else { unreachable!() }; let JoinCondition::On { on, .. } = &op.on else { @@ -2328,14 +2333,15 @@ mod tests { let schema = plan.output_schema(&mut arena).clone(); assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); - // AD (mask 9) precedes BC (mask 6) in inputs, despite their reverse tree order. + // E ties between AD and BC; AD (mask 9) precedes BC (mask 6) in inputs, despite their + // reverse tree order, so E joins AD. BC then builds against the larger ADE. let join = plan.childrens.only(); - let (left, right) = join.childrens.twins(); + let (bc, ade) = join.childrens.twins(); assert_eq!( - left.referenced_table(), + ade.referenced_table(), vec!["a".into(), "d".into(), "e".into()] ); - assert_eq!(right.referenced_table(), vec!["b".into(), "c".into()]); + assert_eq!(bc.referenced_table(), vec!["b".into(), "c".into()]); Ok(()) } @@ -2420,11 +2426,13 @@ mod tests { let schema = plan.output_schema(&mut arena).clone(); assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); - let (left, right) = plan.childrens.twins(); - assert_eq!(left.referenced_table(), vec!["a".into(), "b".into()]); - assert_eq!(right.referenced_table(), vec!["c".into()]); - assert_eq!(left.estimated_rows(), Some(10000)); - for join in [&plan, left] { + // AB is merged first despite its larger product; C then builds against it. + let join = plan.childrens.only(); + let (c, ab) = join.childrens.twins(); + assert_eq!(ab.referenced_table(), vec!["a".into(), "b".into()]); + assert_eq!(c.referenced_table(), vec!["c".into()]); + assert_eq!(ab.estimated_rows(), Some(10000)); + for join in [join, ab] { let Operator::Join(op) = &join.operator else { unreachable!() }; @@ -2463,11 +2471,12 @@ mod tests { let schema = plan.output_schema(&mut arena).clone(); assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); - let (left, right) = plan.childrens.twins(); - assert_eq!(left.referenced_table(), vec!["a".into()]); - assert_eq!(right.referenced_table(), vec!["b".into(), "c".into()]); - assert_eq!(right.estimated_rows(), Some(200)); - for join in [&plan, right] { + let join = plan.childrens.only(); + let (bc, a) = join.childrens.twins(); + assert_eq!(a.referenced_table(), vec!["a".into()]); + assert_eq!(bc.referenced_table(), vec!["b".into(), "c".into()]); + assert_eq!(bc.estimated_rows(), Some(200)); + for join in [join, bc] { let Operator::Join(op) = &join.operator else { unreachable!() }; @@ -2512,10 +2521,10 @@ mod tests { let (left, right) = join.childrens.twins(); assert_eq!( left.referenced_table(), - vec!["a".into(), "b".into(), "c".into()] + vec!["b".into(), "c".into(), "a".into()] ); assert_eq!(right.referenced_table(), vec!["d".into()]); - let (_, bc) = left.childrens.twins(); + let (bc, _) = left.childrens.twins(); assert_eq!(bc.referenced_table(), vec!["b".into(), "c".into()]); assert_eq!(bc.estimated_rows(), Some(200)); Ok(()) @@ -2565,7 +2574,8 @@ mod tests { )); let (abc, d) = cross.childrens.twins(); assert_eq!(d.referenced_table(), vec!["d".into()]); - let (ab, c) = abc.childrens.twins(); + // Without estimates, the narrower C becomes the build side of AB. + let (c, ab) = abc.childrens.twins(); assert_eq!(c.referenced_table(), vec!["c".into()]); let (a, b) = ab.childrens.twins(); assert_eq!(a.referenced_table(), vec!["a".into()]); @@ -2624,8 +2634,9 @@ mod tests { assert!(GreedyJoinReorder.apply(join, &mut arena, None)?); assert_eq!(plan.operator, original_parent); assert_eq!(plan.output_schema(&mut arena), &schema); - let (_, right) = plan.childrens.only().childrens.twins(); - let (_, filter) = right.childrens.twins(); + // Greedy restores the region's output order with its own Project below the parent. + let (bc, _) = plan.childrens.only().childrens.only().childrens.twins(); + let (_, filter) = bc.childrens.twins(); assert!(matches!(filter.operator, Operator::Filter(_))); assert_eq!(filter.childrens.only().referenced_table(), vec!["c".into()]); assert_eq!(filter.estimated_rows(), Some(20)); @@ -2633,16 +2644,23 @@ mod tests { } #[test] - fn unknown_inputs_and_outer_joins_keep_original_order() -> Result<(), DatabaseError> { + fn unknown_inputs_are_rebuilt_but_outer_joins_keep_original_order() -> Result<(), DatabaseError> + { let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); let mut plan = three_inputs(&mut arena); if let Childrens::Twins { right, .. } = plan.childrens.as_mut() { right.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; } - let original = plan.clone(); - assert!(!GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan, original); + let mut outer = plan.clone(); + // Unknown inputs are still rebuilt by connectivity and width, without row estimates. + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); + let (c, ab) = plan.childrens.only().childrens.twins(); + assert_eq!(c.referenced_table(), vec!["c".into()]); + assert_eq!(ab.referenced_table(), vec!["a".into(), "b".into()]); + assert_eq!(plan.row_count_option(), None); + + let mut plan = outer.take(); if let Operator::Join(op) = &mut plan.operator { op.join_type = JoinType::LeftOuter; } From 5f21ac48abbf3ea769d8245a2830e0bf34f5496a Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 32/62] refactor(optimizer): drop unused HepOptimizerPipeline::new Left without callers after rule lists became slices in 9e2235e. --- src/optimizer/heuristic/optimizer.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index f6fcebed..127aeb89 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -1094,18 +1094,6 @@ impl HepOptimizerPipeline { } } - pub fn new( - before_batches: Vec, - after_batches: Vec, - implementations: &[ImplementationRuleImpl], - ) -> Self { - Self { - before_batches, - after_batches, - implementation_index: ImplementationRuleIndex::new(implementations), - } - } - pub fn instantiate(&self, plan: LogicalPlan) -> HepOptimizer<'_> { HepOptimizer::new( plan, From ab5fc417dfae3df9273a32b9e8c1e3db6be9345c Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 33/62] feat(planner): unify join cost model (RFC 0001 stage 1) - Add rows_work(rows, width), InputCost and JoinImpl { Hash, NestLoop, IndexProbe } so calculate_work, Greedy and OrientNestedLoopJoin share one join cost function. - HashJoin also charges reading its probe input; every NLJ type re-runs the inner input per outer row plus pairwise comparisons; parameterized MarkApply probes (inner and subquery) are priced per lookup. - StartupWorkVisitor caches output width next to startup work. - Greedy scores each pair by this step's work and picks the cheaper direction, including the outer side of cross nested loops. - Trim join_reorder unit tests to the necessary cases and recalibrate expectations. - Align RFC 0001 formulas with the implementation. --- ...greedy-join-reorder-owns-join-decisions.md | 30 +- .../rule/implementation/dql/filter.rs | 2 +- .../rule/normalization/join_reorder.rs | 1213 ++--------------- .../rule/normalization/parameterized_index.rs | 4 +- src/planner/mod.rs | 80 +- src/planner/operator/mod.rs | 255 ++-- src/serdes/physical_option.rs | 1 + 7 files changed, 388 insertions(+), 1197 deletions(-) diff --git a/rfc/0001-greedy-join-reorder-owns-join-decisions.md b/rfc/0001-greedy-join-reorder-owns-join-decisions.md index 02525064..600e947e 100644 --- a/rfc/0001-greedy-join-reorder-owns-join-decisions.md +++ b/rfc/0001-greedy-join-reorder-owns-join-decisions.md @@ -229,26 +229,34 @@ fn rows_work(rows: usize, width: usize) -> usize { rows * (CPU_ROW_WORK + width * CPU_OPERATOR_WORK) } -enum JoinImpl { Hash { build: Side }, NestLoop { outer: Side }, IndexProbe { inner: Side, lookup_work: usize } } -struct InputCost { rows: usize, width: usize, work: usize, rescan_work: usize, startup_work: usize } +enum JoinImpl { Hash, NestLoop { full: bool }, IndexProbe { lookup_work: usize } } +struct InputCost { rows: usize, width: usize, work: usize } impl JoinImpl { - fn work(&self, left: &InputCost, right: &InputCost, out_rows: usize, out_width: usize) -> usize; + fn work(self, left: &InputCost, right: &InputCost, out_rows: usize, out_width: usize) -> usize; } ``` ``` -Hash = build.work + probe.work + build.rows × OP + probe.rows × OP + rows_work(out) -IndexProbe = outer.work + outer.rows × lookup_work + rows_work(out) // 内侧不全量读 -NestLoop = outer.work + inner.work + (outer.rows − 1) × inner.rescan_work - + outer.rows × inner.rows × OP + rows_work(out) - (+ inner.rescan_work,仅 Full join) +Hash = build.work // build 侧完整读一遍 + + probe.work // probe 侧完整读一遍 + + rows_work(build) // build 侧整行存入哈希表 + + probe.rows × OP // probe 侧每行查一次哈希表 + + rows_work(out) // 输出满足条件的行 +IndexProbe = outer.work // 外侧完整读一遍 + + outer.rows × lookup_work // 每个外侧行,去内侧索引 seek 一次;内侧不全量读 + + rows_work(out) // 输出满足条件的行 +NestLoop = outer.work // 外侧完整读一遍 + + outer.rows × (inner.work // 每个外侧行,内侧从头重跑一遍 + + inner.rows × OP) // 每个外侧行,和内侧每一行比较一次 + + rows_work(out) // 输出满足条件的行 + (+ inner.work,仅 Full join:最后再扫一遍内侧输出未匹配行) ``` - 代价函数只接收统计量,不依赖 `&LogicalPlan`,因为 Greedy 评估的是尚未构造的 join。 Greedy 与 `calculate_work` 共用它。 -- `calculate_work`、`calculate_startup_work`、scan impl rule 都改用 `rows_work`; - scan 的 ×2 和排序 bonus 规则不变。 -- `rescan_work` 先预留,默认等于 `work`(例外见 6.2)。 +- `calculate_work`、`calculate_startup_work` 改用 `rows_work`; + scan impl rule 不变(同一 scan 的候选列数相同,换单位不影响排序)。 +- `rescan_work` 暂不引入,视为等于 `work`(例外见 6.2)。 ### 4.3 单次 probe 代价 `lookup_work` diff --git a/src/optimizer/rule/implementation/dql/filter.rs b/src/optimizer/rule/implementation/dql/filter.rs index 9e030bb2..d0c5e046 100644 --- a/src/optimizer/rule/implementation/dql/filter.rs +++ b/src/optimizer/rule/implementation/dql/filter.rs @@ -578,7 +578,7 @@ mod tests { .instantiate(plan) .find_best(Some(&loader), &mut arena)?; assert_eq!(plan.estimated_rows(), Some(10)); - assert_eq!(plan.estimated_startup_work()?, Some(0)); + assert_eq!(plan.estimated_startup_work(&mut arena)?, Some(0)); Ok(()) } } diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 2186ad43..fbf3315a 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -21,7 +21,9 @@ use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::scan_column; use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::operator::project::ProjectOperator; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; +use crate::planner::operator::{ + rows_work, InputCost, JoinImpl, Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption, +}; use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; use crate::types::index::{IndexMeta, IndexMetaRef, IndexType}; use crate::types::tuple::Schema; @@ -60,8 +62,8 @@ impl NormalizationRule for OrientNestedLoopJoin { return Ok(false); }; let (Some(left_startup), Some(right_startup)) = ( - left.estimated_startup_work()?, - right.estimated_startup_work()?, + left.estimated_startup_work(arena)?, + right.estimated_startup_work(arena)?, ) else { return Ok(false); }; @@ -72,17 +74,14 @@ impl NormalizationRule for OrientNestedLoopJoin { } else if right_rows == 0 { true } else { - // Repeat the complete inner work for every outer row, - // including rows discarded by filters. Shared output work - // is omitted from this local direction comparison. - let (Some(left_work), Some(right_work)) = - (left.estimated_work()?, right.estimated_work()?) + let (Some(left), Some(right)) = + (left.estimated_cost(arena)?, right.estimated_cost(arena)?) else { return Ok(false); }; - let keep = left_work.saturating_add(left_rows.saturating_mul(right_work)); - let swap = right_work.saturating_add(right_rows.saturating_mul(left_work)); - swap < keep + // Both directions emit the same output, so it is left out of the comparison. + let nest_loop = JoinImpl::NestLoop { full: false }; + nest_loop.work(&right, &left, 0, 0) < nest_loop.work(&left, &right, 0, 0) } } (Some(0 | 1), None) => false, @@ -143,11 +142,11 @@ fn project_join_output( plan } -// Prefer equijoins, then less intermediate/build data, then the original input order. +// Prefer equijoins, then less work in this step, then the original input order. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] struct JoinCandidate { cross_join: bool, - work: u128, + work: usize, rows: usize, left: usize, right: usize, @@ -677,26 +676,45 @@ impl NormalizationRule for GreedyJoinReorder { row_cache.insert(cache_key, rows); rows }; - let left_width = left.output_schema(arena).len() as u128; - let right_width = right.output_schema(arena).len() as u128; - let left_size = if estimated { *left_rows as u128 } else { 1 } * left_width; - let right_size = if estimated { *right_rows as u128 } else { 1 } * right_width; - // HashJoin builds on the left. Count intermediate values and saved build values, - // not just output rows; use widths alone when cardinalities are unavailable. - let work = (if estimated { product as u128 } else { 1 }) - .saturating_mul(left_width + right_width) - .saturating_add(if connected { - left_size.min(right_size) + // Without cardinalities every input counts as one row, so widths break ties. + let input_cost = |input: &mut LogicalPlan, arena: &mut PlanArena| { + let cost = if estimated { + input.estimated_cost(arena)? } else { - 0 - }); + None + }; + Ok::<_, DatabaseError>(cost.unwrap_or_else(|| { + let width = input.output_schema(arena).len(); + InputCost { + rows: 1, + width, + work: rows_work(1, width), + } + })) + }; + let (left, right) = (input_cost(left, arena)?, input_cost(right, arena)?); + let out_rows = if estimated { product } else { 1 }; + let implementation = if connected { + JoinImpl::Hash + } else { + JoinImpl::NestLoop { full: false } + }; + // Every input is read once in any order, so only this step's work is compared. + let step = |left: &InputCost, right: &InputCost| { + implementation + .work(left, right, out_rows, left.width + right.width) + .saturating_sub(left.work) + .saturating_sub(right.work) + }; + // The left input is the hash build side and the nested loop outer side. + let (keep, swap) = (step(&left, &right), step(&right, &left)); let candidate = JoinCandidate { cross_join: !connected, - work, + work: keep.min(swap), rows: product, left: i, right: j, - swapped: connected && right_size < left_size, + swapped: swap < keep, }; if best.is_none_or(|previous| candidate < previous) { best = Some(candidate); @@ -800,20 +818,17 @@ impl NormalizationRule for GreedyJoinReorder { mod tests { use super::*; use crate::catalog::{ColumnCatalog, ColumnDesc}; - use crate::expression::{AliasType, BinaryOperator}; + use crate::expression::BinaryOperator; use crate::optimizer::core::histogram::{HistogramMetaBuilder, DEFAULT_HLL_PRECISION}; use crate::optimizer::heuristic::batch::HepBatchStrategy; use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; use crate::optimizer::rule::normalization::NormalizationRuleImpl; - use crate::planner::operator::aggregate::AggregateOperator; use crate::planner::operator::filter::FilterOperator; - use crate::planner::operator::limit::LimitOperator; - use crate::planner::operator::mark_apply::MarkApplyOperator; use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::TableArenaCell; use crate::storage::StatisticsMetaCache; - use crate::types::index::{IndexInfo, IndexLookup}; + use crate::types::index::IndexInfo; use crate::types::value::DataValue; use crate::types::LogicalType; @@ -881,239 +896,6 @@ mod tests { (plan, key, meta) } - fn parameterized_input( - input_rows: usize, - output_rows: usize, - arena: &mut PlanArena, - ) -> LogicalPlan { - let (left, probe) = input("probe_input", input_rows, arena); - let (mut right, _, _) = indexed_input("lookup", 0, None, arena); - let option = right.physical_option.as_mut().unwrap(); - option.row_count = RowCountOption::Unknown; - let PlanImpl::IndexScan(info) = &mut option.plan else { - unreachable!() - }; - info.lookup = Some(IndexLookup::Probe); - let mut plan = LogicalPlan::new( - Operator::MarkApply(MarkApplyOperator::new_inner_join(Vec::new(), probe)), - Childrens::Twins { - left: Box::new(left), - right: Box::new(right), - }, - ); - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::MarkApply, - SortOption::Follow, - RowCountOption::Estimated { - rows: output_rows, - ndv: None, - }, - )); - plan - } - - #[test] - fn greedy_tracks_positions_across_unequal_width_inputs() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (mut a, a_key) = input("a", 100, &mut arena); - let (b, _) = input("b", 1, &mut arena); - let (mut c, c_key) = input("c", 10, &mut arena); - for (plan, key, width, table) in [(&mut a, a_key, 2, "a"), (&mut c, c_key, 3, "c")] { - let column = key.output_column_ref(&mut arena); - let column = arena.column(column).clone(); - let Operator::TableScan(scan) = &mut plan.operator else { - panic!("expected a scan"); - }; - for index in 1..width { - let mut extra = column.clone(); - extra.summary_mut().name = format!("{table}{index}"); - extra.set_ref_table(table.into(), index, false); - scan.columns.push(arena.alloc_column(extra)); - } - } - let left = JoinOperator::build(a, b, JoinCondition::None, JoinType::Cross, false); - let mut plan = JoinOperator::build( - left, - c, - JoinCondition::On { - on: vec![(a_key, c_key)], - filter: None, - }, - JoinType::Inner, - false, - ); - let original_schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan.output_schema(&mut arena), &original_schema); - let Operator::Project(project) = &plan.operator else { - panic!("expected a projection restoring A, B, C output order"); - }; - assert_eq!(project.exprs.len(), 6); - // C (smaller) builds against A, so the join emits C, A, B. - for (expr, position) in project.exprs.iter().zip([3, 4, 5, 0, 1, 2]) { - assert!(matches!( - arena.expression(*expr), - ScalarExpression::ColumnRef { position: actual, .. } if *actual == position - )); - } - Ok(()) - } - - #[test] - fn parameterized_join_work_changes_nested_loop_direction() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (left, _) = input("cheap", 12, &mut arena); - let right = parameterized_input(12, 13, &mut arena); - let mut plan = - JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::NestLoopJoin, - SortOption::None, - RowCountOption::Unknown, - )); - let schema = plan.output_schema(&mut arena).clone(); - assert!(OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan.output_schema(&mut arena), &schema); - assert_eq!(plan.estimated_rows(), Some(156)); - let Operator::Project(project) = &plan.operator else { - panic!("expected a projection restoring the two swapped inputs"); - }; - assert_eq!(project.exprs.len(), 3); - for (expr, position) in project.exprs.iter().zip([2, 0, 1]) { - assert!(matches!( - arena.expression(*expr), - ScalarExpression::ColumnRef { position: actual, .. } if *actual == position - )); - } - let Childrens::Only(join) = plan.childrens.as_ref() else { - unreachable!() - }; - let Childrens::Twins { left, right } = join.childrens.as_ref() else { - unreachable!() - }; - assert!(matches!(left.operator, Operator::MarkApply(_))); - assert_eq!(left.estimated_rows(), Some(13)); - assert_eq!(right.estimated_rows(), Some(12)); - Ok(()) - } - - #[test] - fn filtered_input_work_changes_nested_loop_direction() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (left, _) = input("small", 7, &mut arena); - let (right, key) = input("filtered", 98, &mut arena); - let value = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(1))); - let predicate = arena.alloc_expression(ScalarExpression::Binary { - op: BinaryOperator::Eq, - left_expr: key, - right_expr: value, - ty: LogicalType::Boolean, - evaluator: None, - }); - let mut right = FilterOperator::build(predicate, right, false); - right.physical_option = Some(PhysicalOption::new( - PlanImpl::Filter, - SortOption::Follow, - RowCountOption::Estimated { rows: 8, ndv: None }, - )); - assert_eq!(right.estimated_startup_work()?, Some(0)); - assert_eq!(right.estimated_work()?, Some(392)); - let column = key.unpack_bound_col(&arena, false).unwrap().0; - assert_eq!( - scan_column(&right, column, &arena)?, - Some((&"filtered".into(), 0)) - ); - let mut plan = - JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::NestLoopJoin, - SortOption::None, - RowCountOption::Unknown, - )); - let schema = plan.output_schema(&mut arena).clone(); - assert!(OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan.output_schema(&mut arena), &schema); - assert_eq!(plan.estimated_rows(), Some(56)); - let Childrens::Only(join) = plan.childrens.as_ref() else { - unreachable!() - }; - let Childrens::Twins { left, .. } = join.childrens.as_ref() else { - unreachable!() - }; - assert!(matches!(left.operator, Operator::Filter(_))); - Ok(()) - } - - #[test] - fn join_keys_preserve_conditions_across_merges() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (_, a) = input("a", 10, &mut arena); - let (_, b) = input("b", 10, &mut arena); - let (_, c) = input("c", 10, &mut arena); - let (_, d) = input("d", 10, &mut arena); - let alias = a.clone_expression(&mut arena)?; - let mut keys = JoinKeys::default(); - keys.insert(1, 4, (a, c)); - keys.insert(1, 4, (alias, c)); - keys.insert(4, 1, (c, a)); - keys.insert(2, 4, (b, c)); - keys.insert(1, 2, (a, b)); - keys.insert(2, 8, (b, d)); - keys.insert(4, 8, (c, d)); - assert_eq!( - keys.pairs().collect::>(), - vec![(1, 2), (1, 4), (2, 4), (2, 8), (4, 8)] - ); - assert_eq!( - keys.get(4, 1).collect::>(), - vec![(a, c), (alias, c), (a, c)] - ); - assert!(keys.get(1, 8).next().is_none()); - assert_eq!(keys.take(1, 2), vec![(a, b)]); - assert!(keys.links.contains_key(&(1, 4))); - keys.rewrite(1, 2); - assert_eq!( - keys.get(3, 4).collect::>(), - vec![(a, c), (alias, c), (a, c), (b, c)] - ); - assert_eq!(keys.get(3, 8).collect::>(), vec![(b, d)]); - assert!(keys.get(1, 4).next().is_none()); - assert!(keys.get(1, 2).next().is_none()); - assert_eq!(keys.take(4, 3), vec![(c, a), (c, alias), (c, a), (c, b)]); - keys.rewrite(4, 3); - assert_eq!(keys.take(8, 7), vec![(d, c), (d, b)]); - keys.rewrite(8, 7); - assert!(keys.links.is_empty()); - Ok(()) - } - - #[test] - fn join_keys_preserve_condition_direction_after_merge() { - let (a, b, c, d, e) = ( - ExprRef::new(0), - ExprRef::new(1), - ExprRef::new(2), - ExprRef::new(3), - ExprRef::new(4), - ); - let mut keys = JoinKeys::default(); - keys.insert(2, 4, (b, c)); - keys.insert(2, 4, (d, a)); - keys.insert(8, 4, (e, c)); - assert!(keys.take(2, 8).is_empty()); - keys.rewrite(2, 8); - assert_eq!( - keys.get(10, 4).collect::>(), - vec![(c, b), (a, d), (c, e)] - ); - assert_eq!(keys.take(10, 4), vec![(b, c), (d, a), (e, c)]); - assert!(keys.links.is_empty()); - } - #[test] fn malformed_regions_are_planning_errors() { // (outer join key, expected message). Each side of a key is only searched in its own @@ -1367,511 +1149,104 @@ mod tests { } #[test] - fn column_origin_follows_direct_projections_but_stops_at_computation( - ) -> Result<(), DatabaseError> { + fn complete_index_keys_reduce_rows_and_bound_uniqueness() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); - let (left, key) = input("l", 10, &mut arena); - let alias = arena.alloc_expression(ScalarExpression::Alias { - expr: key, - alias: AliasType::Name("alias_l".into()), + let (mut left, l, li) = indexed_input("l", 1000, Some(100), &mut arena); + let (mut right, r, ri) = indexed_input("r", 200, Some(100), &mut arena); + let one = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(1))); + let expr_key = arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Plus, + left_expr: l, + right_expr: one, + evaluator: None, + ty: LogicalType::Integer, }); - let output = alias.output_column_ref(&mut arena); - let mut projected = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![alias])), - Childrens::Only(Box::new(left)), + let left_schema = left.output_schema(&mut arena); + let right_schema = right.output_schema(&mut arena); + let l_input = JoinInputStatistics { + rows: 1000, + schema: &left_schema, + index: Some((arena.index(li), Some(100))), + }; + let r_input = JoinInputStatistics { + rows: 200, + schema: &right_schema, + index: Some((arena.index(ri), Some(100))), + }; + assert_eq!( + join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), + 2000 ); - projected.physical_option = Some(PhysicalOption::new( - PlanImpl::Project, - SortOption::Follow, - RowCountOption::Follow, - )); assert_eq!( - scan_column(&projected, output, &arena)?, - Some((&"l".into(), 0)) + join_rows(&l_input, &r_input, std::iter::empty, None, &arena), + 200000 ); - let alias_target = arena.alloc_expression(ScalarExpression::column_expr(output, 0)); - let alias = arena.alloc_expression(ScalarExpression::Alias { - expr: key, - alias: AliasType::Expr(alias_target), - }); - let Operator::Project(project) = &mut projected.operator else { - unreachable!() + let mut unique_right = arena.index(ri).clone(); + unique_right.ty = IndexType::Unique; + let r_input = JoinInputStatistics { + rows: 200, + schema: &right_schema, + index: Some((&unique_right, None)), }; - project.exprs[0] = alias; assert_eq!( - scan_column(&projected, output, &arena)?, - Some((&"l".into(), 0)) - ); - let predicate = - arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(true))); - let mut filtered = LogicalPlan::new( - Operator::Filter(FilterOperator { - predicate, - having: false, - is_optimized: false, - }), - Childrens::Only(Box::new(projected)), + join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), + 1000 ); - filtered.physical_option = Some(PhysicalOption::new( - PlanImpl::Filter, - SortOption::Follow, - RowCountOption::Follow, - )); + let mut unique_left = arena.index(li).clone(); + unique_left.ty = IndexType::PrimaryKey { is_multiple: false }; + let l_input = JoinInputStatistics { + rows: 1000, + schema: &left_schema, + index: Some((&unique_left, None)), + }; assert_eq!( - scan_column(&filtered, output, &arena)?, - Some((&"l".into(), 0)) + join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), + 200 ); - let (other, other_key) = input("other", 10, &mut arena); + unique_right.column_ids.push(1); + let r_input = JoinInputStatistics { + rows: 200, + schema: &right_schema, + index: Some((&unique_right, Some(100))), + }; + let l_input = JoinInputStatistics { + rows: 1000, + schema: &left_schema, + index: Some((arena.index(li), Some(100))), + }; assert_eq!( - scan_column( - &filtered, - other_key.unpack_bound_col(&arena, false).unwrap().0, - &arena - )?, - None - ); - let mut join = JoinOperator::build( - filtered, - other, - JoinCondition::On { - on: vec![(key, other_key)], - filter: None, - }, - JoinType::Inner, - false, - ); - join.physical_option = Some(PhysicalOption::new( - PlanImpl::HashJoin, - SortOption::None, - RowCountOption::Estimated { - rows: 100, - ndv: None, - }, - )); - assert_eq!(scan_column(&join, output, &arena)?, None); - let one = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(1))); - let computed = arena.alloc_expression(ScalarExpression::Binary { - op: BinaryOperator::Plus, - left_expr: key, - right_expr: one, - ty: LogicalType::Integer, - evaluator: None, - }); - let alias = arena.alloc_expression(ScalarExpression::Alias { - expr: computed, - alias: AliasType::Name("computed".into()), - }); - let output = alias.output_column_ref(&mut arena); - let (source, _) = input("l", 10, &mut arena); - let mut project = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![alias])), - Childrens::Only(Box::new(source)), + join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), + 200000 ); - project.physical_option = Some(PhysicalOption::new( - PlanImpl::Project, - SortOption::Follow, - RowCountOption::Follow, - )); - assert_eq!(scan_column(&project, output, &arena)?, None); - let (mut scan, key) = input("limited", 10, &mut arena); - let column = key.unpack_bound_col(&arena, false).unwrap().0; - let Operator::TableScan(op) = &mut scan.operator else { - unreachable!() + let r_input = JoinInputStatistics { + rows: 200, + schema: &right_schema, + index: Some((arena.index(ri), Some(100))), }; - op.limit = (Some(1), Some(3)); - assert_eq!(scan_column(&scan, column, &arena)?, None); - let limited = LogicalPlan::new( - Operator::Limit(LimitOperator { - offset: Some(1), - limit: Some(3), - }), - Childrens::Only(Box::new(scan)), - ); - assert_eq!(scan_column(&limited, column, &arena)?, None); - let aggregate = LogicalPlan::new( - Operator::Aggregate(AggregateOperator { - agg_calls: vec![], - groupby_exprs: vec![key], - is_distinct: false, - force_spill: false, - }), - *limited.childrens, + assert_eq!( + join_rows( + &l_input, + &r_input, + || std::iter::once((expr_key, r)), + None, + &arena + ), + 200000 ); - assert_eq!(scan_column(&aggregate, column, &arena)?, None); Ok(()) } #[test] - fn column_origin_ignores_nested_alias_targets_and_rejects_casts() -> Result<(), DatabaseError> { + fn composite_ndv_requires_the_whole_key() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); - let (scan, key) = input("l", 10, &mut arena); - let (_, unrelated) = input("other", 10, &mut arena); - let inner_alias = arena.alloc_expression(ScalarExpression::Alias { - expr: key, - alias: AliasType::Expr(unrelated), - }); - let alias = arena.alloc_expression(ScalarExpression::Alias { - expr: inner_alias, - alias: AliasType::Name("nested".into()), - }); - let output = alias.output_column_ref(&mut arena); - let mut project = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![alias])), - Childrens::Only(Box::new(scan)), - ); - assert_eq!( - scan_column(&project, output, &arena)?, - Some((&"l".into(), 0)) - ); - let cast = arena.alloc_expression(ScalarExpression::TypeCast { - expr: key, - ty: LogicalType::Bigint, - evaluator: None, - }); - let alias = arena.alloc_expression(ScalarExpression::Alias { - expr: cast, - alias: AliasType::Name("casted".into()), - }); - let output = alias.output_column_ref(&mut arena); - let Operator::Project(op) = &mut project.operator else { - unreachable!() - }; - op.exprs[0] = alias; - assert_eq!(scan_column(&project, output, &arena)?, None); - Ok(()) - } - - #[test] - fn column_origin_only_follows_the_preserved_left_input() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (left, key) = input("l", 10, &mut arena); - let (right, other) = input("r", 10, &mut arena); - let marker = arena.alloc_column(ColumnCatalog::new( - "marker".into(), - true, - ColumnDesc::new(LogicalType::Boolean, None, false, None)?, - )); - let mut plan = MarkApplyOperator::build_exists(left, right, marker, vec![]); - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::MarkApply, - SortOption::Follow, - RowCountOption::Follow, - )); - let left_column = key.unpack_bound_col(&arena, false).unwrap().0; - let right_column = other.unpack_bound_col(&arena, false).unwrap().0; - assert_eq!( - scan_column(&plan, left_column, &arena)?, - Some((&"l".into(), 0)) - ); - assert_eq!(scan_column(&plan, right_column, &arena)?, None); - assert_eq!(scan_column(&plan, marker, &arena)?, None); - plan.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; - assert_eq!(scan_column(&plan, left_column, &arena)?, None); - Ok(()) - } - - #[test] - fn column_ndv_is_independent_of_an_unrelated_filtering_index() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (mut left, l, index) = indexed_input("l", 12, Some(2), &mut arena); - let (mut right, r) = input("r", 110, &mut arena); - let mut meta = arena.index(index).clone(); - meta.column_ids = vec![1]; - let index = arena.alloc_index(meta); - let PlanImpl::IndexScan(info) = &mut left.physical_option.as_mut().unwrap().plan else { - unreachable!() - }; - info.meta = index; - let mut cache = StatisticsMetaCache::default(); - for (table, ndv) in [("l", 30), ("r", 70)] { - let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; - for value in 0..ndv { - builder.append(&DataValue::Int32(value)); - } - cache.columns.insert((table.into(), 0), builder.build()); - } - let loader = StatisticMetaLoader::new(&cache); - let ndvs = column_ndv(&left, &right, &[(l, r)], Some(&loader), &arena)?; - assert_eq!(ndvs, Some((12, 70))); - let left_schema = left.output_schema(&mut arena); - let right_schema = right.output_schema(&mut arena); - assert_eq!( - join_rows( - &JoinInputStatistics { - rows: 12, - schema: left_schema, - index: Some((arena.index(index), Some(2))) - }, - &JoinInputStatistics { - rows: 110, - schema: right_schema, - index: None - }, - || std::iter::once((l, r)), - ndvs, - &arena - ), - 19 - ); - Ok(()) - } - - #[test] - fn ordinary_join_keys_use_the_strongest_single_divisor() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (mut left, l) = input("l", 1000, &mut arena); - let (mut right, r) = input("r", 200, &mut arena); - let mut keys = vec![(l, r)]; - let mut extra_keys = Vec::new(); - for (table, plan) in [("l", &mut left), ("r", &mut right)] { - let mut column = ColumnCatalog::new( - "extra".into(), - false, - ColumnDesc::new(LogicalType::Integer, None, false, None)?, - ); - column.set_ref_table(table.into(), 1, false); - let column = arena.alloc_column(column); - let Operator::TableScan(scan) = &mut plan.operator else { - unreachable!() - }; - scan.columns.push(column); - extra_keys.push(arena.alloc_expression(ScalarExpression::column_expr(column, 1))); - } - keys.push((extra_keys[0], extra_keys[1])); - let mut cache = StatisticsMetaCache::default(); - for (table, id, ndv) in [("l", 0, 100), ("r", 0, 100), ("l", 1, 80), ("r", 1, 70)] { - let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; - for value in 0..ndv { - builder.append(&DataValue::Int32(value)); - } - cache.columns.insert((table.into(), id), builder.build()); - } - let ndvs = column_ndv( - &left, - &right, - &keys, - Some(&StatisticMetaLoader::new(&cache)), - &arena, - )?; - assert_eq!(ndvs, Some((100, 100))); - assert_eq!( - estimate_join_rows( - JoinInputEstimate { - rows: 1000, - ndv: Some(100), - unique: false - }, - JoinInputEstimate { - rows: 200, - ndv: Some(100), - unique: false - } - ), - 2000 - ); - keys.reverse(); - assert_eq!( - column_ndv( - &left, - &right, - &keys, - Some(&StatisticMetaLoader::new(&cache)), - &arena - )?, - ndvs - ); - Ok(()) - } - - #[test] - fn complete_index_keys_reduce_rows_and_bound_uniqueness() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (mut left, l, li) = indexed_input("l", 1000, Some(100), &mut arena); - let (mut right, r, ri) = indexed_input("r", 200, Some(100), &mut arena); - let one = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(1))); - let expr_key = arena.alloc_expression(ScalarExpression::Binary { - op: BinaryOperator::Plus, - left_expr: l, - right_expr: one, - evaluator: None, - ty: LogicalType::Integer, - }); - let left_schema = left.output_schema(&mut arena); - let right_schema = right.output_schema(&mut arena); - let l_input = JoinInputStatistics { - rows: 1000, - schema: &left_schema, - index: Some((arena.index(li), Some(100))), - }; - let r_input = JoinInputStatistics { - rows: 200, - schema: &right_schema, - index: Some((arena.index(ri), Some(100))), - }; - assert_eq!( - join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), - 2000 - ); - assert_eq!( - join_rows(&l_input, &r_input, std::iter::empty, None, &arena), - 200000 - ); - let mut unique_right = arena.index(ri).clone(); - unique_right.ty = IndexType::Unique; - let r_input = JoinInputStatistics { - rows: 200, - schema: &right_schema, - index: Some((&unique_right, None)), - }; - assert_eq!( - join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), - 1000 - ); - let mut unique_left = arena.index(li).clone(); - unique_left.ty = IndexType::PrimaryKey { is_multiple: false }; - let l_input = JoinInputStatistics { - rows: 1000, - schema: &left_schema, - index: Some((&unique_left, None)), - }; - assert_eq!( - join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), - 200 - ); - unique_right.column_ids.push(1); - let r_input = JoinInputStatistics { - rows: 200, - schema: &right_schema, - index: Some((&unique_right, Some(100))), - }; - let l_input = JoinInputStatistics { - rows: 1000, - schema: &left_schema, - index: Some((arena.index(li), Some(100))), - }; - assert_eq!( - join_rows(&l_input, &r_input, || std::iter::once((l, r)), None, &arena), - 200000 - ); - let r_input = JoinInputStatistics { - rows: 200, - schema: &right_schema, - index: Some((arena.index(ri), Some(100))), - }; - assert_eq!( - join_rows( - &l_input, - &r_input, - || std::iter::once((expr_key, r)), - None, - &arena - ), - 200000 - ); - Ok(()) - } - - #[test] - fn follow_keeps_index_statistics_but_join_results_do_not() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (left, l, li) = indexed_input("l", 1000, Some(100), &mut arena); - let (right, r, _) = indexed_input("r", 200, Some(100), &mut arena); - let mut left = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![l])), - Childrens::Only(Box::new(left)), - ); - left.physical_option = Some(PhysicalOption::new( - PlanImpl::Project, - SortOption::Follow, - RowCountOption::Follow, - )); - assert_eq!(index_statistics(&left), Some((li, Some(100)))); - let mut join = JoinOperator::build( - left, - right, - JoinCondition::On { - on: vec![(l, r)], - filter: None, - }, - JoinType::Inner, - false, - ); - join.physical_option = Some(PhysicalOption::new( - PlanImpl::HashJoin, - SortOption::None, - RowCountOption::Unknown, - )); - // The two-input region is rebuilt so the smaller right input becomes the build side. - assert!(GreedyJoinReorder.apply(&mut join, &mut arena, None)?); - let join = join.childrens.only(); - assert_eq!(join.estimated_rows(), Some(2000)); - assert_eq!(index_statistics(join), None); - Ok(()) - } - - #[test] - fn greedy_uses_ndv_before_pruning_connected_candidates() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (left, l, _) = indexed_input("l", 1000, Some(100), &mut arena); - let (right, r, _) = indexed_input("r", 200, Some(100), &mut arena); - let (third, c) = input("c", 20, &mut arena); - let mut lr = JoinOperator::build( - left, - right, - JoinCondition::On { - on: vec![(l, r)], - filter: None, - }, - JoinType::Inner, - false, - ); - lr.physical_option = Some(PhysicalOption::new( - PlanImpl::HashJoin, - SortOption::None, - RowCountOption::Unknown, - )); - let mut plan = JoinOperator::build( - lr, - third, - JoinCondition::On { - on: vec![(r, c)], - filter: None, - }, - JoinType::Inner, - false, - ); - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::HashJoin, - SortOption::None, - RowCountOption::Unknown, - )); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); - let (_, lr) = plan.childrens.only().childrens.twins(); - assert_eq!(lr.referenced_table(), vec!["r".into(), "l".into()]); - assert_eq!(lr.estimated_rows(), Some(2000)); - assert_eq!(index_statistics(lr), None); - assert_eq!(plan.estimated_rows(), Some(40000)); - Ok(()) - } - - #[test] - fn composite_ndv_requires_the_whole_key() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (_left, l, li) = indexed_input("l", 1000, Some(100), &mut arena); - let (_right, r, ri) = indexed_input("r", 200, Some(100), &mut arena); - let mut lc = ColumnCatalog::new( - "l_second".into(), - false, - ColumnDesc::new(LogicalType::Integer, None, false, None)?, + let (_left, l, li) = indexed_input("l", 1000, Some(100), &mut arena); + let (_right, r, ri) = indexed_input("r", 200, Some(100), &mut arena); + let mut lc = ColumnCatalog::new( + "l_second".into(), + false, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, ); lc.set_ref_table("l".into(), 1, false); let lc = arena.alloc_column(lc); @@ -1967,67 +1342,6 @@ mod tests { project } - #[test] - fn startup_estimates_follow_inputs_and_final_implementation() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (mut scan, key) = input("source", 16, &mut arena); - assert_eq!(scan.estimated_startup_work()?, Some(0)); - assert_eq!(scan.estimated_work()?, Some(64)); - let mut sort = LogicalPlan::new( - Operator::Sort(SortOperator { - sort_fields: vec![SortField { - expr: key, - asc: true, - nulls_first: false, - }], - }), - Childrens::Only(Box::new(scan.clone())), - ); - sort.physical_option = Some(PhysicalOption::new( - PlanImpl::Sort, - SortOption::None, - RowCountOption::Follow, - )); - assert_eq!(sort.estimated_startup_work()?, Some(192)); - assert_eq!(sort.estimated_work()?, Some(256)); - let mut project = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![key])), - Childrens::Only(Box::new(sort)), - ); - project.physical_option = Some(PhysicalOption::new( - PlanImpl::Project, - SortOption::Follow, - RowCountOption::Follow, - )); - assert_eq!(project.estimated_startup_work()?, Some(192)); - assert_eq!(project.estimated_work()?, Some(256)); - - let mut aggregate = LogicalPlan::new( - Operator::Aggregate(AggregateOperator { - agg_calls: vec![key], - groupby_exprs: vec![key], - is_distinct: false, - force_spill: false, - }), - Childrens::Only(Box::new(scan)), - ); - aggregate.physical_option = Some(PhysicalOption::new( - PlanImpl::HashAggregate, - SortOption::None, - RowCountOption::Unknown, - )); - assert_eq!(aggregate.estimated_startup_work()?, Some(160)); - assert_eq!(aggregate.estimated_work()?, None); - aggregate.physical_option = Some(PhysicalOption::new( - PlanImpl::StreamAggregate, - SortOption::None, - RowCountOption::Unknown, - )); - assert_eq!(aggregate.estimated_startup_work()?, Some(0)); - Ok(()) - } - #[test] fn startup_unknown_and_overflow_do_not_become_zero() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); @@ -2049,8 +1363,8 @@ mod tests { SortOption::None, RowCountOption::Follow, )); - assert_eq!(sort.estimated_startup_work()?, None); - assert_eq!(sort.estimated_work()?, None); + assert_eq!(sort.estimated_startup_work(&mut arena)?, None); + assert_eq!(sort.estimated_cost(&mut arena)?.map(|cost| cost.work), None); let (a, _) = input("a", usize::MAX, &mut arena); let (b, _) = input("b", 2, &mut arena); @@ -2060,99 +1374,15 @@ mod tests { SortOption::None, RowCountOption::Unknown, )); - assert_eq!(cross.estimated_work()?, Some(usize::MAX)); + assert_eq!( + cross.estimated_cost(&mut arena)?.map(|cost| cost.work), + Some(usize::MAX) + ); assert_eq!(cross.estimated_rows(), Some(usize::MAX)); assert!(!OrientNestedLoopJoin.apply(&mut cross, &mut arena, None)?); Ok(()) } - #[test] - fn nested_loop_cost_combines_startup_and_driver_rows() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - for (left_rows, right_rows, swap) in [ - (100, 1, true), - (1, 100, false), - (10, 10, false), - (10, 0, true), - (0, 10, false), - ] { - let (left, _) = input("left", left_rows, &mut arena); - let (right, _) = input("right", right_rows, &mut arena); - let mut plan = - JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::NestLoopJoin, - SortOption::None, - RowCountOption::Unknown, - )); - plan.estimated_startup_work()?; - assert_eq!( - plan.estimated_rows(), - Some(left_rows.saturating_mul(right_rows)) - ); - let schema = plan.output_schema(&mut arena).clone(); - assert_eq!( - OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?, - swap - ); - assert_eq!(plan.output_schema(&mut arena), &schema); - } - - // A large startup does not outweigh a driver that already executes the - // right side once. Missing HashJoin output rows remain unknown as well. - for unknown in [false, true] { - let (left, _) = input("single", 1, &mut arena); - let mut right = materialized_right(&mut arena); - if unknown { - let Childrens::Only(join) = right.childrens.as_mut() else { - unreachable!() - }; - join.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; - join.reset_output_schema_cache(); - } - let mut plan = - JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::NestLoopJoin, - SortOption::None, - RowCountOption::Unknown, - )); - assert!(!OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); - } - Ok(()) - } - - #[test] - fn nested_cross_preserves_parameterized_input_work() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let mut expensive = parameterized_input(100, 2, &mut arena); - assert_eq!(expensive.estimated_work()?, Some(508)); - let (peer, _) = input("peer", 3, &mut arena); - let mut cross = - JoinOperator::build(expensive, peer, JoinCondition::None, JoinType::Cross, false); - cross.physical_option = Some(PhysicalOption::new( - PlanImpl::NestLoopJoin, - SortOption::None, - RowCountOption::Unknown, - )); - // Read the expensive child once, peer twice, then emit six rows. - assert_eq!(cross.estimated_work()?, Some(508 + 2 * 12 + 6 * 4)); - let (outer, _) = input("outer", 2, &mut arena); - let mut plan = - JoinOperator::build(cross, outer, JoinCondition::None, JoinType::Cross, false); - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::NestLoopJoin, - SortOption::None, - RowCountOption::Unknown, - )); - let original = plan.clone(); - assert!(!OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan, original); - Ok(()) - } - #[test] fn nested_loop_moves_materialization_left_and_restores_positions() -> Result<(), DatabaseError> { @@ -2160,8 +1390,9 @@ mod tests { let mut arena = PlanArena::new(&table_arena); let (mut left, _) = input("a", 100, &mut arena); let mut right = materialized_right(&mut arena); - assert_eq!(left.estimated_startup_work()?, Some(0)); - assert_eq!(right.estimated_startup_work()?, Some(90)); + assert_eq!(left.estimated_startup_work(&mut arena)?, Some(0)); + // The 10-row build side is read (50) and stored whole (50). + assert_eq!(right.estimated_startup_work(&mut arena)?, Some(100)); let mut plan = JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); plan.physical_option = Some(PhysicalOption::new( @@ -2194,44 +1425,6 @@ mod tests { Ok(()) } - #[test] - fn nested_loop_orientation_keeps_equal_work_and_semantic_boundaries( - ) -> Result<(), DatabaseError> { - let table_arena = TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - for (left_materialized, right_materialized, join_type, limit_pushed) in [ - (false, false, JoinType::Cross, false), - (true, false, JoinType::Cross, false), - (true, true, JoinType::Cross, false), - (false, true, JoinType::LeftOuter, false), - (false, true, JoinType::Cross, true), - ] { - let left = if left_materialized { - materialized_right(&mut arena) - } else { - input("a", 100, &mut arena).0 - }; - let right = if right_materialized { - materialized_right(&mut arena) - } else { - input("d", 100, &mut arena).0 - }; - let mut plan = JoinOperator::build(left, right, JoinCondition::None, join_type, false); - if let Operator::Join(join) = &mut plan.operator { - join.limit_pushed = limit_pushed; - } - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::NestLoopJoin, - SortOption::None, - RowCountOption::Unknown, - )); - let original = plan.clone(); - assert!(!OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan, original); - } - Ok(()) - } - fn three_inputs(arena: &mut PlanArena) -> LogicalPlan { let (a, a_key) = input("a", 1000, arena); let (b, b_key) = input("b", 10, arena); @@ -2290,61 +1483,6 @@ mod tests { Ok(()) } - #[test] - fn edge_order_ties_keep_original_input_order() -> Result<(), DatabaseError> { - let table_arena = TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - let (a, ak) = input("a", 1, &mut arena); - let (b, bk) = input("b", 1, &mut arena); - let (c, ck) = input("c", 1, &mut arena); - let (d, dk) = input("d", 1, &mut arena); - let (e, ek) = input("e", 10, &mut arena); - let bc = JoinOperator::build( - b, - c, - JoinCondition::On { - on: vec![(bk, ck)], - filter: None, - }, - JoinType::Inner, - false, - ); - let abc = JoinOperator::build(a, bc, JoinCondition::None, JoinType::Cross, false); - let abcd = JoinOperator::build( - abc, - d, - JoinCondition::On { - on: vec![(ak, dk)], - filter: None, - }, - JoinType::Inner, - false, - ); - let mut plan = JoinOperator::build( - abcd, - e, - JoinCondition::On { - on: vec![(ak, ek), (bk, ek)], - filter: None, - }, - JoinType::Inner, - false, - ); - let schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan.output_schema(&mut arena), &schema); - // E ties between AD and BC; AD (mask 9) precedes BC (mask 6) in inputs, despite their - // reverse tree order, so E joins AD. BC then builds against the larger ADE. - let join = plan.childrens.only(); - let (bc, ade) = join.childrens.twins(); - assert_eq!( - ade.referenced_table(), - vec!["a".into(), "d".into(), "e".into()] - ); - assert_eq!(bc.referenced_table(), vec!["b".into(), "c".into()]); - Ok(()) - } - #[test] fn nonadjacent_pair_restores_output_order_and_keeps_all_keys() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); @@ -2445,91 +1583,6 @@ mod tests { Ok(()) } - #[test] - fn inner_join_expands_cross_join_but_cross_join_is_not_an_entry() -> Result<(), DatabaseError> { - let table_arena = TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - let (a, a_key) = input("a", 1000, &mut arena); - let (b, b_key) = input("b", 10, &mut arena); - let (c, c_key) = input("c", 20, &mut arena); - let mut cross = JoinOperator::build(a, b, JoinCondition::None, JoinType::Cross, false); - let original = cross.clone(); - assert!(!GreedyJoinReorder.apply(&mut cross, &mut arena, None)?); - assert_eq!(cross, original); - let b_column = b_key.output_column_ref(&mut arena); - let b_key = arena.alloc_expression(ScalarExpression::column_expr(b_column, 1)); - let mut plan = JoinOperator::build( - cross, - c, - JoinCondition::On { - on: vec![(a_key, c_key), (b_key, c_key)], - filter: None, - }, - JoinType::Inner, - false, - ); - let schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan.output_schema(&mut arena), &schema); - let join = plan.childrens.only(); - let (bc, a) = join.childrens.twins(); - assert_eq!(a.referenced_table(), vec!["a".into()]); - assert_eq!(bc.referenced_table(), vec!["b".into(), "c".into()]); - assert_eq!(bc.estimated_rows(), Some(200)); - for join in [join, bc] { - let Operator::Join(op) = &join.operator else { - unreachable!() - }; - let JoinCondition::On { on, .. } = &op.on else { - unreachable!() - }; - assert_eq!(op.join_type, JoinType::Inner); - assert_eq!(on.len(), 1); - } - Ok(()) - } - - #[test] - fn disconnected_input_keeps_a_final_cross_join() -> Result<(), DatabaseError> { - let table_arena = TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - let (a, a_key) = input("a", 1000, &mut arena); - let (b, b_key) = input("b", 10, &mut arena); - let (c, c_key) = input("c", 20, &mut arena); - let (d, _) = input("d", 1, &mut arena); - let ab = JoinOperator::build(a, b, JoinCondition::None, JoinType::Cross, false); - let abd = JoinOperator::build(ab, d, JoinCondition::None, JoinType::Cross, false); - let b_column = b_key.output_column_ref(&mut arena); - let b_key = arena.alloc_expression(ScalarExpression::column_expr(b_column, 1)); - let mut plan = JoinOperator::build( - abd, - c, - JoinCondition::On { - on: vec![(a_key, c_key), (b_key, c_key)], - filter: None, - }, - JoinType::Inner, - false, - ); - let schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan.output_schema(&mut arena), &schema); - let join = plan.childrens.only(); - assert!( - matches!(&join.operator, Operator::Join(join) if join.join_type == JoinType::Cross) - ); - let (left, right) = join.childrens.twins(); - assert_eq!( - left.referenced_table(), - vec!["b".into(), "c".into(), "a".into()] - ); - assert_eq!(right.referenced_table(), vec!["d".into()]); - let (bc, _) = left.childrens.twins(); - assert_eq!(bc.referenced_table(), vec!["b".into(), "c".into()]); - assert_eq!(bc.estimated_rows(), Some(200)); - Ok(()) - } - #[test] fn unestimated_region_defers_cartesian_products_by_input_order() -> Result<(), DatabaseError> { // FROM a, c, d, b WHERE a = b AND c = b: FROM order crosses a, c and d diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 65aca773..f71b7d9b 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -990,7 +990,7 @@ mod inner_join_tests { right.physical_option.as_ref().unwrap().row_count, RowCountOption::Unknown ); - assert_eq!(plan.estimated_startup_work()?, Some(0)); + assert_eq!(plan.estimated_startup_work(&mut arena)?, Some(0)); } Ok(()) } @@ -1296,7 +1296,7 @@ mod inner_join_tests { plan.physical_option.as_ref().unwrap().row_count, RowCountOption::Follow ); - assert_eq!(plan.estimated_startup_work()?, Some(0)); + assert_eq!(plan.estimated_startup_work(&mut arena)?, Some(0)); assert_eq!(plan.output_schema(&mut arena), &schema); let Operator::Filter(filter) = &plan.operator else { panic!("join filter must remain above projection") diff --git a/src/planner/mod.rs b/src/planner/mod.rs index 6aec5bce..73063e42 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -26,7 +26,7 @@ use crate::planner::operator::set_membership::SetMembershipOperator; use crate::planner::operator::union::UnionOperator; use crate::planner::operator::values::ValuesOperator; use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; -use crate::planner::operator::{Operator, PhysicalOption, RowCountOption}; +use crate::planner::operator::{input_cost, InputCost, Operator, PhysicalOption, RowCountOption}; use kite_sql_serde_macros::ReferenceSerialization; use std::fmt; use std::hash::{Hash, Hasher}; @@ -153,10 +153,20 @@ impl<'a> Iterator for ChildrensIter<'a> { } } -struct StartupWorkVisitor; +struct StartupWorkVisitor<'r, A> { + arena: &'r mut A, +} -impl<'a> OperatorVisitorMut<'a> for StartupWorkVisitor { +impl<'a, A: MetaArena> OperatorVisitorMut<'a> for StartupWorkVisitor<'_, A> { fn visit_plan(&mut self, plan: &'a mut LogicalPlan) -> Result<(), DatabaseError> { + if plan + .physical_option + .as_ref() + .is_none_or(|option| option.startup_work.is_some()) + { + return Ok(()); + } + let width = plan.output_schema(self.arena).len(); let LogicalPlan { operator, childrens, @@ -166,10 +176,8 @@ impl<'a> OperatorVisitorMut<'a> for StartupWorkVisitor { let Some(option) = physical_option.as_mut() else { return Ok(()); }; - if option.startup_work.is_some() { - return Ok(()); - } OperatorVisitorMut::<'_>::visit_children(self, childrens)?; + option.width = Some(width); option.calculate_startup_work(operator, childrens); Ok(()) } @@ -245,19 +253,23 @@ impl LogicalPlan { // Pass-local memoization for NLJ orientation. New physical options start // empty; orientation preserves child rows/startup, so their cache is reusable. - pub(crate) fn estimated_startup_work(&mut self) -> Result, DatabaseError> { - StartupWorkVisitor.visit_plan(self)?; + pub(crate) fn estimated_startup_work( + &mut self, + arena: &mut A, + ) -> Result, DatabaseError> { + StartupWorkVisitor { arena }.visit_plan(self)?; Ok(self .physical_option .as_ref() .and_then(|option| option.startup_work.flatten())) } - pub(crate) fn estimated_work(&mut self) -> Result, DatabaseError> { - self.estimated_startup_work()?; - Ok(self.physical_option.as_ref().and_then(|option| { - option.calculate_work(&self.operator, &self.childrens, self.estimated_rows()) - })) + pub(crate) fn estimated_cost( + &mut self, + arena: &mut A, + ) -> Result, DatabaseError> { + self.estimated_startup_work(arena)?; + Ok(input_cost(self)) } pub(crate) fn clear_startup_work(&mut self) { @@ -709,12 +721,12 @@ mod tests { SortOption::None, RowCountOption::Follow, )); - assert_eq!(sort.estimated_startup_work()?, None); + assert_eq!(sort.estimated_startup_work(&mut arena)?, None); assert_eq!( sort.physical_option.as_ref().unwrap().startup_work, Some(None) ); - assert_eq!(sort.estimated_startup_work()?, None); + assert_eq!(sort.estimated_startup_work(&mut arena)?, None); assert_eq!( sort.physical_option.as_ref().unwrap().startup_work, Some(None) @@ -737,10 +749,11 @@ mod tests { SortOption::None, RowCountOption::Follow, )); - assert_eq!(sort.estimated_startup_work()?, Some(192)); + // 16 one-column rows (5 each) + 64 comparisons * 2 operator units. + assert_eq!(sort.estimated_startup_work(&mut arena)?, Some(208)); assert_eq!( sort.physical_option.as_ref().unwrap().startup_work, - Some(Some(192)) + Some(Some(208)) ); let Childrens::Only(scan) = sort.childrens.as_mut() else { unreachable!() @@ -751,7 +764,7 @@ mod tests { ); // A cached parent does not walk its children again in this round. scan.clear_startup_work(); - assert_eq!(sort.estimated_startup_work()?, Some(192)); + assert_eq!(sort.estimated_startup_work(&mut arena)?, Some(208)); let Childrens::Only(scan) = sort.childrens.as_mut() else { unreachable!() }; @@ -785,8 +798,11 @@ mod tests { }, )); assert_eq!(filter.estimated_rows(), Some(output_rows)); - assert_eq!(filter.estimated_startup_work()?, Some(0)); - assert_eq!(filter.estimated_work()?, Some(392)); + assert_eq!(filter.estimated_startup_work(&mut arena)?, Some(0)); + assert_eq!( + filter.estimated_cost(&mut arena)?.map(|cost| cost.work), + Some(392) + ); let mut project = LogicalPlan::new( Operator::Project(ProjectOperator::new(vec![])), Childrens::Only(Box::new(filter)), @@ -796,8 +812,11 @@ mod tests { SortOption::Follow, RowCountOption::Follow, )); - assert_eq!(project.estimated_startup_work()?, Some(0)); - assert_eq!(project.estimated_work()?, Some(392)); + assert_eq!(project.estimated_startup_work(&mut arena)?, Some(0)); + assert_eq!( + project.estimated_cost(&mut arena)?.map(|cost| cost.work), + Some(392) + ); } Ok(()) } @@ -840,9 +859,13 @@ mod tests { SortOption::None, RowCountOption::Follow, )); - // 392 input work + 24 comparisons * 2 operator units, then 8 output rows. - assert_eq!(sort.estimated_startup_work()?, Some(440)); - assert_eq!(sort.estimated_work()?, Some(472)); + // 98 one-column input rows (5 each) + 24 comparisons * 2 operator units, + // then 8 output rows. + assert_eq!(sort.estimated_startup_work(&mut arena)?, Some(538)); + assert_eq!( + sort.estimated_cost(&mut arena)?.map(|cost| cost.work), + Some(578) + ); let mut unknown = table_scan("unknown", vec![]); unknown.physical_option = Some(PhysicalOption::new( PlanImpl::SeqScan, @@ -855,8 +878,11 @@ mod tests { SortOption::Follow, RowCountOption::Estimated { rows: 8, ndv: None }, )); - assert_eq!(filter.estimated_startup_work()?, Some(0)); - assert_eq!(filter.estimated_work()?, None); + assert_eq!(filter.estimated_startup_work(&mut arena)?, Some(0)); + assert_eq!( + filter.estimated_cost(&mut arena)?.map(|cost| cost.work), + None + ); Ok(()) } diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index aa6eaace..9863927e 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -55,18 +55,11 @@ pub mod window; use self::recursive_cte::{RecursiveCteOperator, RecursiveScanOperator}; use self::{ - aggregate::AggregateOperator, - alter_table::add_column::AddColumnOperator, - alter_table::change_column::ChangeColumnOperator, - filter::FilterOperator, - join::JoinOperator, - limit::LimitOperator, - mark_apply::{MarkApplyKind, MarkApplyOperator}, - project::ProjectOperator, - scalar_query_init::ScalarQueryInitOperator, - scalar_subquery::ScalarSubqueryOperator, - sort::SortOperator, - table_scan::TableScanOperator, + aggregate::AggregateOperator, alter_table::add_column::AddColumnOperator, + alter_table::change_column::ChangeColumnOperator, filter::FilterOperator, join::JoinOperator, + limit::LimitOperator, mark_apply::MarkApplyOperator, project::ProjectOperator, + scalar_query_init::ScalarQueryInitOperator, scalar_subquery::ScalarSubqueryOperator, + sort::SortOperator, table_scan::TableScanOperator, }; use crate::catalog::ColumnRef; use crate::errors::DatabaseError; @@ -176,6 +169,80 @@ pub enum RowCountOption { // This is the same scale as row = 1 and operator = 0.25; I/O is not modeled. pub(crate) const CPU_ROW_WORK: usize = 4; pub(crate) const CPU_OPERATOR_WORK: usize = 1; +// Rebuilding the inner executor for each outer row of an index probe. +pub(crate) const PROBE_RESET_WORK: usize = CPU_ROW_WORK; + +/// Work of producing `rows` rows of `width` columns. The fixed per-row part keeps +/// zero-column rows (e.g. the input of `count(*)`) from being free. +pub(crate) fn rows_work(rows: usize, width: usize) -> usize { + rows.saturating_mul(CPU_ROW_WORK.saturating_add(width.saturating_mul(CPU_OPERATOR_WORK))) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct InputCost { + pub(crate) rows: usize, + pub(crate) width: usize, + /// Work of producing all rows once, including the input's own inputs. + pub(crate) work: usize, +} + +/// Join implementations priced from input statistics alone, so the join reorder can price +/// joins it has not built yet. `left` is the hash build side, the nested loop outer side and +/// the index probe outer side. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum JoinImpl { + Hash, + /// `full` re-reads the inner input once more to emit its unmatched rows. + NestLoop { + full: bool, + }, + /// The probed (right) input is never read whole; each outer row pays `lookup_work`. + IndexProbe { + lookup_work: usize, + }, +} + +impl JoinImpl { + /// Total work, including reading both inputs. + pub(crate) fn work( + self, + left: &InputCost, + right: &InputCost, + out_rows: usize, + out_width: usize, + ) -> usize { + let join = match self { + // Build rows are stored whole; each probe row costs one hash lookup. + JoinImpl::Hash => right + .work + .saturating_add(rows_work(left.rows, left.width)) + .saturating_add(right.rows.saturating_mul(CPU_OPERATOR_WORK)), + // The inner input is re-run and compared for every outer row. + JoinImpl::NestLoop { full } => left + .rows + .saturating_mul( + right + .work + .saturating_add(right.rows.saturating_mul(CPU_OPERATOR_WORK)), + ) + .saturating_add(if full { right.work } else { 0 }), + JoinImpl::IndexProbe { lookup_work } => left.rows.saturating_mul(lookup_work), + }; + left.work + .saturating_add(join) + .saturating_add(rows_work(out_rows, out_width)) + } +} + +pub(crate) fn input_cost(input: &LogicalPlan) -> Option { + let option = input.physical_option.as_ref()?; + let rows = input.estimated_rows()?; + Some(InputCost { + rows, + width: option.width?, + work: option.calculate_work(&input.operator, &input.childrens, Some(rows))?, + }) +} #[derive(Debug, Clone)] pub struct PhysicalOption { @@ -183,6 +250,8 @@ pub struct PhysicalOption { pub(crate) sort_option: SortOption, pub(crate) row_count: RowCountOption, pub(crate) startup_work: Option>, + /// Output column count, cached together with `startup_work`. + pub(crate) width: Option, } impl PhysicalOption { @@ -192,6 +261,7 @@ impl PhysicalOption { sort_option, row_count, startup_work: None, + width: None, } } @@ -199,6 +269,8 @@ impl PhysicalOption { &self.sort_option } + /// Join formulas: RFC 0001 §4.2 (`rfc/0001-greedy-join-reorder-owns-join-decisions.md`); + /// probe `lookup_work`: §4.3. pub(crate) fn calculate_work( &self, operator: &Operator, @@ -208,64 +280,53 @@ impl PhysicalOption { match (&self.plan, operator, childrens) { (PlanImpl::Filter, Operator::Filter(_), Childrens::Only(input)) | (PlanImpl::Project, Operator::Project(_), Childrens::Only(input)) => { - // Output selectivity must not erase the work of reading the input. - // Predicate and projection CPU are not charged by this lower bound. - input.physical_option.as_ref()?.calculate_work( - &input.operator, - &input.childrens, - input.estimated_rows(), - ) + Some(input_cost(input)?.work) } (PlanImpl::MarkApply, Operator::MarkApply(op), Childrens::Twins { left, right }) - if matches!(op.kind, MarkApplyKind::InnerJoin) - && op.parameterized_probe().is_some() - && !op.materialize => + if op.parameterized_probe().is_some() && !op.materialize => { - // The output estimate is not the cost of repeatedly reading the - // input and probing. Keep Probe rows unknown, but charge at least - // one lookup operation and its startup for each input row. - // - // work = input_work + input_rows × lookup_work + output_rows × CPU_ROW_WORK. - let input_work = left.physical_option.as_ref()?.calculate_work( - &left.operator, - &left.childrens, - left.estimated_rows(), - )?; - let lookup_work = right - .physical_option - .as_ref()? - .startup_work - .flatten()? - .saturating_add(CPU_OPERATOR_WORK); - Some( - input_work - .saturating_add(left.estimated_rows()?.saturating_mul(lookup_work)) - .saturating_add(rows?.saturating_mul(CPU_ROW_WORK)), - ) + let right_option = right.physical_option.as_ref()?; + let probed = InputCost { + rows: 1, + width: right_option.width?, + work: right_option.startup_work.flatten()?, + }; + let lookup_work = probed + .work + .saturating_add(PROBE_RESET_WORK) + .saturating_add(rows_work(probed.rows, probed.width)); + Some(JoinImpl::IndexProbe { lookup_work }.work( + &input_cost(left)?, + &probed, + rows?, + self.width?, + )) } - (PlanImpl::NestLoopJoin, Operator::Join(join), Childrens::Twins { left, right }) - if join.join_type == join::JoinType::Cross => - { - let left_work = left.physical_option.as_ref()?.calculate_work( - &left.operator, - &left.childrens, - left.estimated_rows(), - )?; - let right_work = right.physical_option.as_ref()?.calculate_work( - &right.operator, - &right.childrens, - right.estimated_rows(), - )?; + (PlanImpl::HashJoin, Operator::Join(_), Childrens::Twins { left, right }) => Some( + JoinImpl::Hash.work(&input_cost(left)?, &input_cost(right)?, rows?, self.width?), + ), + (PlanImpl::NestLoopJoin, Operator::Join(join), Childrens::Twins { left, right }) => { + let (outer, inner) = if join.join_type == join::JoinType::RightOuter { + (right, left) + } else { + (left, right) + }; Some( - left_work - .saturating_add(left.estimated_rows()?.saturating_mul(right_work)) - .saturating_add(rows?.saturating_mul(CPU_ROW_WORK)), + JoinImpl::NestLoop { + full: join.join_type == join::JoinType::Full, + } + .work( + &input_cost(outer)?, + &input_cost(inner)?, + rows?, + self.width?, + ), ) } _ => Some( self.startup_work .flatten()? - .saturating_add(rows?.saturating_mul(CPU_ROW_WORK)), + .saturating_add(rows_work(rows?, self.width?)), ), } } @@ -273,13 +334,7 @@ impl PhysicalOption { pub(crate) fn calculate_startup_work(&mut self, operator: &Operator, childrens: &Childrens) { let startup_work = |input: &LogicalPlan| input.physical_option.as_ref()?.startup_work.flatten(); - let work = |input: &LogicalPlan| { - input.physical_option.as_ref()?.calculate_work( - &input.operator, - &input.childrens, - input.estimated_rows(), - ) - }; + let work = |input: &LogicalPlan| Some(input_cost(input)?.work); self.startup_work = Some(match (&self.plan, operator, childrens) { ( PlanImpl::SeqScan | PlanImpl::IndexScan(_) | PlanImpl::Values | PlanImpl::Dummy, @@ -295,17 +350,12 @@ impl PhysicalOption { _, Childrens::Only(input), ) => startup_work(input), - (PlanImpl::HashJoin, Operator::Join(join), Childrens::Twins { left, right }) => { - let keys = match &join.on { - JoinCondition::On { on, .. } => on.len(), - JoinCondition::None => 0, - }; - work(left).and_then(|build| { + (PlanImpl::HashJoin, _, Childrens::Twins { left, right }) => { + input_cost(left).and_then(|build| { Some( build - .saturating_add(left.estimated_rows()?.saturating_mul( - CPU_ROW_WORK.saturating_add(keys.saturating_mul(CPU_OPERATOR_WORK)), - )) + .work + .saturating_add(rows_work(build.rows, build.width)) .saturating_add(startup_work(right)?), ) }) @@ -972,6 +1022,59 @@ mod tests { use crate::types::value::DataValue; use crate::types::LogicalType; + #[test] + fn rows_work_charges_rows_without_columns() { + assert_eq!(rows_work(3, 0), 3 * CPU_ROW_WORK); + assert_eq!(rows_work(3, 2), 3 * (CPU_ROW_WORK + 2)); + assert_eq!(rows_work(usize::MAX, 1), usize::MAX); + } + + #[test] + fn join_work_includes_inputs_and_prefers_cheap_sides() { + let small = InputCost { + rows: 10, + width: 1, + work: 50, + }; + let large = InputCost { + rows: 1000, + width: 1, + work: 5000, + }; + let output = rows_work(100, 2); + // Both inputs read once, the build side stored, every probe row looked up once. + assert_eq!( + JoinImpl::Hash.work(&small, &large, 100, 2), + 50 + 5000 + rows_work(10, 1) + 1000 + output + ); + assert!( + JoinImpl::Hash.work(&small, &large, 100, 2) + < JoinImpl::Hash.work(&large, &small, 100, 2) + ); + // The inner input is re-run and compared for every outer row. + let nest_loop = JoinImpl::NestLoop { full: false }; + assert_eq!( + nest_loop.work(&small, &large, 100, 2), + 50 + 10 * (5000 + 1000) + output + ); + assert!(nest_loop.work(&small, &large, 100, 2) < nest_loop.work(&large, &small, 100, 2)); + // A full join reads the inner input once more for its unmatched rows. + assert_eq!( + JoinImpl::NestLoop { full: true }.work(&small, &large, 100, 2), + nest_loop.work(&small, &large, 100, 2) + 5000 + ); + // The probed input is never read whole: a few outer rows beat hashing a large input. + let probe = JoinImpl::IndexProbe { lookup_work: 30 }; + assert_eq!(probe.work(&small, &large, 100, 2), 50 + 10 * 30 + output); + assert!(probe.work(&small, &large, 100, 2) < JoinImpl::Hash.work(&small, &large, 100, 2)); + let huge = InputCost { + rows: usize::MAX, + width: 1, + work: usize::MAX, + }; + assert_eq!(nest_loop.work(&huge, &small, 0, 2), usize::MAX); + } + fn column_catalog(name: &str) -> ColumnCatalog { ColumnCatalog::new( name.to_string(), diff --git a/src/serdes/physical_option.rs b/src/serdes/physical_option.rs index d8e954c3..8843f4f6 100644 --- a/src/serdes/physical_option.rs +++ b/src/serdes/physical_option.rs @@ -85,6 +85,7 @@ impl ReferenceSerialization for PhysicalOption { sort_option: SortOption::decode(reader, context, reference_tables, arena)?, row_count: RowCountOption::decode(reader, context, reference_tables, arena)?, startup_work: None, + width: None, }) } } From 83358f8aedd3fab94930b37793fd7a1e1adfe4a8 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 34/62] feat(optimizer): let greedy join reorder own join decisions (RFC 0001 stage 2) - Greedy compares hash and index probe per pair and rewrites chosen probes directly; other joins it builds are marked `join_decided` so ParameterizeInnerJoin leaves them alone. Probe judgement and rewrite are shared with ParameterizeInnerJoin (`probe_index` / `probe_join`). - Probe `lookup_work` includes the log2 N seek; covered unique indexes are priced with one seek and probed without a table lookup (`index_covering`). - Search is left-deep from the cheapest input, so remaining inputs stay probeable leaves; a single-row result may take a cross join first. - Inputs without statistics are priced with DEFAULT_TABLE_ROWS instead of a separate heuristic path; PK/Unique equality ranges get exact row counts. - Hash cost gains HASH_BUILD_WORK, calibrated on LMDB. - Greedy also handles cross-rooted and FORCE_NEST_LOOP_JOIN regions (nested loops only); OrientNestedLoopJoin is removed. - Node work estimates are cached together in `PhysicalOption.cost` (`WorkCost`), making each merge price only the new node. - Fix: primary-key index scans honour `with_pk`, so unmatched rows of a right-join UPDATE no longer carry the source table's key. - TODOs in hash_join for BuildState deallocation and SipHash cost. --- ...greedy-join-reorder-owns-join-decisions.md | 117 ++--- src/db.rs | 5 - src/execution/dql/join/hash_join.rs | 4 + .../rule/implementation/dql/table_scan.rs | 8 +- .../rule/normalization/elimination.rs | 8 +- .../rule/normalization/join_reorder.rs | 480 ++++++++---------- src/optimizer/rule/normalization/mod.rs | 9 +- .../rule/normalization/parameterized_index.rs | 253 ++++++--- .../rule/normalization/pushdown_predicates.rs | 96 ++-- src/orm/mod.rs | 2 +- src/planner/mod.rs | 51 +- src/planner/operator/mod.rs | 94 ++-- src/serdes/physical_option.rs | 16 +- src/storage/mod.rs | 9 +- tests/slt/join.slt | 61 +++ 15 files changed, 700 insertions(+), 513 deletions(-) diff --git a/rfc/0001-greedy-join-reorder-owns-join-decisions.md b/rfc/0001-greedy-join-reorder-owns-join-decisions.md index 600e947e..42c4c089 100644 --- a/rfc/0001-greedy-join-reorder-owns-join-decisions.md +++ b/rfc/0001-greedy-join-reorder-owns-join-decisions.md @@ -28,7 +28,7 @@ Greedy 处于被动位置:它拿到的是 scan 已经选好的计划,只调 - probe 是否更优从未被比较:大外侧 ⋈ 小唯一内侧时,probe 的 seek 次数远多于 hash 的全量读取, 但仍会被改写成 probe。 - 索引选择与连接无关:probe 用的是第一个可用索引;Greedy 的 `index_statistics` 只读 implementation - 选中的那个索引,覆盖连接键的其它索引提供的唯一性和 NDV 被忽略。 + 选中的那个索引,被连接键绑定的其它索引提供的唯一性和 NDV 被忽略。 - 代价单位不统一,且 probe 单价被低估、HashJoin / NLJ 漏算部分代价(见第 4 节), 即使让 Greedy 比较,也比较不出正确结果。 @@ -55,14 +55,14 @@ Greedy 处于被动位置:它拿到的是 scan 已经选好的计划,只调 | 规则 | 现在 | 改造后 | |---|---|---| | scan impl rule | 选访问路径 | 只提供静态最优的默认值;Greedy 区域内的叶子以 Greedy 的决策为准 | -| `GreedyJoinReorder` | 只排顺序,按 hash 估算 | 每个 pair 决定顺序 + 实现 + 方向 + probe 索引,写入 `join_hint` | -| `ParameterizeInnerJoin` | 能 probe 就改写 | 有 hint:校验后按 hint 执行;无 hint:原逻辑兜底 | +| `GreedyJoinReorder` | 只排顺序,按 hash 估算 | 每个 pair 决定顺序 + 实现 + 方向 + probe 索引;probe 直接改写,其余标记 `join_decided` | +| `ParameterizeInnerJoin` | 能 probe 就改写 | `join_decided`:不改写;否则原逻辑兜底 | | `OrientNestedLoopJoin` | 调整所有 Cross NLJ 方向 | 删除,职责并入 Greedy(3.10) | Greedy 区域内每个叶子的访问路径都由 Greedy 决定,scan impl rule 的结果只是 Greedy 的输入之一: -- 叶子作为 probe 内侧:访问路径是 Greedy 选中的 probe 索引(hint 中的 `index`), - 由 `ParameterizeInnerJoin` 替换掉 scan 原来的索引与 range。不存在"probe 更优但 scan 用了别的索引"的情况。 +- 叶子作为 probe 内侧:访问路径是 Greedy 选中的 probe 索引, + Greedy 改写时替换掉 scan 原来的索引与 range。不存在"probe 更优但 scan 用了别的索引"的情况。 - 叶子作为 hash / NLJ 的输入:访问路径不使用连接键,候选就是 scan impl rule 用静态 range 比较过的那些, 其最低代价方案也就是此时的最优方案;Greedy 以它的 `work` 作为该形态的输入代价,选择 Hash 即确认采用它。 - 例:`a JOIN b ON a.id = b.a_id WHERE b.status = 1`,b 上有 `idx(a_id)`、`idx(status)`。hash 时 b 被独立读出, @@ -76,22 +76,22 @@ Greedy 的决策在其上生效;对同一个叶子,Greedy 的决策优先级 ### 3.2 决策流程 1. 收集区域内的叶子和连接边(现有逻辑)。 -2. 对每个叶子生成 probe 候选并缓存(3.3)。 -3. 每轮对每个候选 pair 列出可行的实现形态,用代价模型给每种形态打分,取 pair 内最优形态(3.4)。 -4. 在所有 pair 中按 `JoinCandidate` 的顺序选出本轮合并的 pair。 -5. 按选中的形态构造 join:确定左右(probe 内侧、NLJ 内侧放右边,hash build 放左边),写入 `join_hint`(3.5)。 +2. 对每个 pair,用共用判定函数在叶子一侧找可 probe 的索引(3.3)。 +3. 搜索为左深:从行数最少(相同时读取代价最低)的输入开始,每轮只评估"当前结果 ⋈ 某个剩余输入",列出可行形态并打分,取 pair 内最优形态(3.4)。 + 剩余输入在合并前始终是叶子,保留被 probe 的机会。 +4. 在这些 pair 中按 `JoinCandidate` 的顺序选出本轮合并的输入;当前结果估算不超过 1 行时,cross 不降优先级。 +5. 按选中的形态构造 join:确定左右(probe 内侧、NLJ 内侧放右边,hash build 放左边);probe 直接改写为 MarkApply,其余标记 `join_decided`(3.5)。 6. 合并结果作为新输入参与下一轮,其 `InputCost` 由所选形态的代价函数给出。 ### 3.3 Source 与 probe 候选 -Greedy 根据 source 的列信息与其上存在的索引,自己生成候选,不依赖 impl rule 保留候选: +Greedy 根据 source 的列信息与其上存在的索引,自己判定可用的 probe 索引,不依赖 impl rule 保留候选: ```rust -struct ProbeCandidate { - index: IndexMetaRef, - key_columns: Vec, // 必须由连接键提供的索引列(已扣除常量等值列),非空 - lookup_work: usize, // 单次 probe 代价,见 4.3 - rows_per_lookup: usize, // 唯一索引 = 1,为非唯一索引预留 +struct ProbeIndex { + position: usize, // scan.index_infos 中的位置 + probe: Vec, // 外侧值与常量,按索引列顺序 + lookup_work: usize, // 单次 probe 代价,见 4.3 } ``` @@ -99,10 +99,8 @@ struct ProbeCandidate { 所以 probe 只出现在"已合并结果 ⋈ 叶子"的形态,与 Greedy 左深扩展的方式吻合。 - 索引的每一列要么出现在某条连接边上(属于该叶子一侧),要么被叶子 filter 的常量等值绑定; 两者都满足时按常量处理,减少对连接键的要求。 -- 每个叶子最多 10 个候选,按 PK → Unique → `key_columns` 少的优先 → index id 排序后截断。 -- 同一组覆盖连接键的索引也用于行数估算:任意被覆盖的 PK/Unique 即认为该侧唯一; - NDV 通过 `loader.load(table, index_id)` 读取该索引的 `distinct_values_len`(复合键得到联合 NDV), - 再按 filter 后行数以 `min(N, rows)` 收紧。不再只看 implementation 选中的索引。 +- 每个 pair 直接调用判定函数,取 `lookup_work` 最低的 PK/Unique(相同时按索引顺序);不缓存、不截断,候选数由表上的索引数决定。 +- probe 的输出行数按唯一性收紧为 `min(估算行数, 外侧行数)`:被 probe 的索引被连接键绑定,唯一性已知。 ### 3.4 Pair 内的实现形态 @@ -111,44 +109,36 @@ struct ProbeCandidate { | 形态 | 条件 | |---|---| | Hash(build = L) / Hash(build = R) | pair 有连接键 | -| Probe(inner = R) / Probe(inner = L) | 对应一侧是叶子,且某个候选的 `key_columns` 被该 pair 在这一侧的连接键覆盖 | +| Probe(inner = R) / Probe(inner = L) | 对应一侧是叶子,且判定函数能用该 pair 的连接键找到索引 | | NestLoop(outer = L) / NestLoop(outer = R) | pair 没有连接键(cross) | 每种形态按第 4 节的代价模型打分,pair 内取最低分。`JoinCandidate` 的比较顺序(`derive(Ord)`): ``` -cross_join → score → impl_rank → rows → left → right → swapped +cross_join → score → rows → left → right → swapped → probe ``` -- `cross_join` 按 pair 判断(该 pair 是否有连接键),不再用全局的 `connected`。 -- `impl_rank`:probe < hash < NLJ,只在分数相同或无统计时起作用。 +- pair 内分数相同时 probe 优先;无统计时按默认行数计价,见 3.9。 -### 3.5 决策下发:`join_hint` +### 3.5 决策下发:`join_decided` ```rust -enum JoinHint { - Hash, - Probe { index: IndexMetaRef, lookup_work: usize }, - NestLoop, -} - -// PhysicalOption 新增,参与 PartialEq / Hash(会影响后续改写结果) -join_hint: Option, +// PhysicalOption 新增;与 startup_work 一样是 pass 内的标注,不参与 PartialEq / Hash,也不序列化 +join_decided: bool, ``` -Hint 写在 Greedy 构造 join 处(现在设置 `PhysicalOption(HashJoin/NestLoopJoin, ...)` 的位置)。 -左右已经由 Greedy 定好,hint 只表达"按这个形态执行,不要再判断"。 +Greedy 选中 probe 时直接调用与 `ParameterizeInnerJoin` 共用的改写函数生成 MarkApply,判定与执行是同一份代码, +无需再校验;Hash / NestLoop 的 join 标记 `join_decided`,表达"按这个形态执行,不要再判断"。 ### 3.6 下游规则的执行与兜底 - `ParameterizeInnerJoin` - - `Probe`:用共用判定函数(3.7)再校验一次,通过则按指定索引改写,并把 `lookup_work` 写入 - MarkApply 的 `PhysicalOption`;不尝试交换左右;校验失败按 hash 处理。 - - `Hash` / `NestLoop`:不改写。 - - `None`:Greedy 没处理过的 join(ON 带 filter、`force_nested_loop`、`limit_pushed` 等),走原逻辑兜底。 + - `join_decided`:不改写。 + - 否则:Greedy 没处理过的 join(ON 带 filter、`limit_pushed` 等),走原逻辑兜底, + 用同一判定函数选索引、用 loader 计算 `lookup_work`。 - NestLoop 方向由 Greedy 决定,下游不再有调整方向的规则(3.10)。 - 被替换的静态 range 不会丢失过滤语义:Filter 仍在 scan 上方,`EliminateIndexFilter` 在 Parameterize 之后运行。 -- explain 只展示选中的索引(现有行为),不展示 hint 与 `lookup_work`。 +- explain 只展示选中的索引(现有行为),不展示 `join_decided` 与 `lookup_work`。 ### 3.7 共用判定函数 @@ -174,17 +164,17 @@ probe 时仍会回表,代价按 `seeks = 2` 计。结果是 probe 与 scan 选 1. 把覆盖计算从 `PushPredicateIntoScan` 拆成纯函数 `index_covering(scan, index) -> Option<(deserializers, mapping)>`,只依赖 scan 的 `columns` 与索引列,与 range 无关。 2. Greedy 生成候选时用它决定 `seeks`(PK 或覆盖 Unique = 1,否则 = 2);scan cost 中非覆盖 ×2 的判断使用同一函数。 -3. `ParameterizeInnerJoin` 按 hint(以及兜底路径)改写为 probe 时,同时写入 `covered_deserializers` / `cover_mapping`, +3. probe 改写(Greedy 与兜底路径共用),同时写入 `covered_deserializers` / `cover_mapping`, 使执行器真正跳过回表。 这样 Hash 形态用 scan 已选路径(可能是覆盖索引)的 `work`,Probe 形态枚举 `scan.index_infos` 中全部候选并按真实覆盖计价, "scan 选了覆盖索引,但 probe 更优"的情况在 pair 内比较时直接体现,由 Greedy 选出后覆盖 scan 的选择。 -### 3.9 无统计信息时的回退 +### 3.9 无统计信息时的默认行数 -一个 pair 只要有一侧缺少统计信息(行数,或 probe 所需的 N),该 pair 不做代价比较, -按 `impl_rank` 启发式选择 probe > hash > NLJ,与现有 `ParameterizeInnerJoin` 的行为一致。 -NestLoop 方向在缺少统计时沿用 `OrientNestedLoopJoin` 现有的回退:一侧行数为 0 / 1 时它作外侧,否则比较 `startup_work`。 +缺少统计信息的输入按 `DEFAULT_TABLE_ROWS`(1000)行计价,probe 所需的 N 缺失时也取该值,代价比较照常进行,不再有启发式分支。 +PK/Unique 等值的静态 range 在无统计时也有确切行数,scan impl rule 据此选它并给出行数。 +由默认行数合并出的 join 仍记为未知行数,避免假估算泄漏给后续规则。 ### 3.10 融合 `OrientNestedLoopJoin` @@ -194,11 +184,11 @@ NestLoop 方向在缺少统计时沿用 `OrientNestedLoopJoin` 现有的回退 | 来源 | 现在 | 融合后 | |---|---|---| -| Greedy 区域内无连接键的 pair | Greedy 构造时 `swapped: connected && ...`,cross 永不交换,方向靠 Orient 修正 | Greedy 比较 `NestLoop(outer = L / R)`,写入 `NestLoop` hint | +| Greedy 区域内无连接键的 pair | Greedy 构造时 `swapped: connected && ...`,cross 永不交换,方向靠 Orient 修正 | Greedy 比较 `NestLoop(outer = L / R)` | | 根为 `Cross` 的区域(如 `FROM a, b`,或位于 outer join / 带 filter 的 inner join 之下) | Greedy 要求根为 `Inner`(`apply` 入口),不处理 | Greedy 触发条件放宽为 `can_expand(plan)`;`can_expand` 本就接受 Cross | -| `force_nested_loop` 的 cross(`FORCE_NEST_LOOP_JOIN`) | `can_expand` 排除,不进区域;Orient 不检查该字段,仍调整方向 | 仍不进区域、不重排;Greedy 对这种单个 join 只做方向选择,行为不变 | +| `FORCE_NEST_LOOP_JOIN` 下的 join | `can_expand` 排除,不进区域;Orient 只调整其中 cross 的方向 | 进入区域(只与同样带该标记的 join 组成区域),照常重排,但每个 pair 只比较 NestLoop | -`force_nested_loop` 的语义是限制实现,不放开重排,所以只接管方向。 +该 hint 的语义是"用 NLJ、不用 hash",不承诺按书写顺序执行。 时序代价:Orient 原本在 `Parameterize Mark Apply`、`Eliminate Index Filter`、`Materialize Mark Apply` 之后运行, 看到的是改写后的 `estimated_work`。并入 Greedy 后,NLJ 内侧子树若含子查询类 MarkApply,方向按其改写前的估算决定; @@ -239,7 +229,7 @@ impl JoinImpl { ``` Hash = build.work // build 侧完整读一遍 + probe.work // probe 侧完整读一遍 - + rows_work(build) // build 侧整行存入哈希表 + + rows_work(build) + build.rows × HASH_BUILD_WORK // build 侧整行存入哈希表;哈希、分配与释放按 LMDB 实测校准 + probe.rows × OP // probe 侧每行查一次哈希表 + rows_work(out) // 输出满足条件的行 IndexProbe = outer.work // 外侧完整读一遍 @@ -269,10 +259,10 @@ lookup_work = PROBE_RESET_WORK(= CPU_ROW_WORK) + rows_per_lookup × rows_work(1, inner_width) ``` -- N 取该索引直方图的 `values_len`,缺失时取 PK 直方图;都没有视为无统计。内侧 Filter 只按行计。 +- N 取该索引直方图的 `values_len`,没有时取 `DEFAULT_TABLE_ROWS`。内侧 Filter 只按行计。 - 示例:内侧 100 万行、宽度 2,扫一行 = 6,查一次 = 30;外侧约小于内侧 1/4 时 probe 更优。 -- 存于 `PhysicalOption.lookup_work: Option`,由 Greedy 经 hint 传入,兜底路径由 - `ParameterizeInnerJoin` 用 loader 计算。semi / anti 等子查询类 MarkApply 也改用该公式。 +- 存于 `PhysicalOption.lookup_work: Option`,由判定函数用 loader 计算,probe 改写时写入 + (Greedy 与兜底路径共用)。semi / anti 等子查询类 MarkApply 也改用该公式。 ### 4.4 Pair 分数:本步处理量 @@ -286,7 +276,7 @@ score(Probe) = outer.rows × lookup_work + rows_work(out) − inner_le - Probe 的 `− inner_leaf.work` 是对基准(每张叶子都完整读一遍)的修正,不是处理量; 内侧大、外侧小时分数为负,Greedy 会优先合并。分数用 `i128`,代码中需注释该项含义。 -## 5. Pipeline 与 hint 的有效性 +## 5. Pipeline 与 `join_decided` 的有效性 `db.rs` 中 after_batches 的顺序: @@ -298,8 +288,8 @@ score(Probe) = outer.rows × lookup_work + rows_work(out) − inner_le 6. …… Greedy 与 Parameterize 之间没有其它 batch;同一 batch 的 `ParameterizeMarkApply` 只匹配 `MarkApply`, -其 `parameterize_right_subtree` 只处理 `Project` 和 `TableScan`,不修改 Join 节点,hint 不会被破坏。 -改写出的 MarkApply 的 `estimated_work` 仍须使用 Greedy 传入的 `lookup_work`,使 `calculate_work` +其 `parameterize_right_subtree` 只处理 `Project` 和 `TableScan`,不修改 Join 节点,标记不会被破坏。 +改写出的 MarkApply 的 `estimated_work` 仍须使用 Greedy 写入的 `lookup_work`,使 `calculate_work` (含 explain 中的代价)与 Greedy 的决策口径一致。 ## 6. 已核实的执行器行为 @@ -350,18 +340,18 @@ hash 输入或改写为 probe 时,不会丢失上层依赖的有序性、也 1. 统一代价(只改行为):引入 `rows_work`、`JoinImpl` / `InputCost`,修正 HashJoin、NLJ、MarkApply 的估算, Greedy 分数改为本步处理量。现有连接顺序与 explain / slt 期望值会变化,需整体重新校准。 -2. 移交决策权:拆出共用判定函数与 `index_covering`;Greedy 生成 probe 候选、按形态比较并写入 `join_hint`; - `ParameterizeInnerJoin` 改为有 hint 时只执行;Greedy 触发条件放宽到 `can_expand`、接管 NestLoop 方向 - (含 `force_nested_loop` 的单个 cross),删除 `OrientNestedLoopJoin` 及其 batch。 +2. 移交决策权:拆出共用判定函数与 `index_covering`;Greedy 按形态比较,probe 直接改写、其余标记 `join_decided`; + `ParameterizeInnerJoin` 跳过 `join_decided` 的 join;Greedy 触发条件放宽到 `can_expand`、接管 NestLoop 方向 + (`FORCE_NEST_LOOP_JOIN` 区域只比较 NestLoop),删除 `OrientNestedLoopJoin` 及其 batch。 测试(单测走 `make test`,slt 走 `make test-slt`): - `rows_work` 与各 `JoinImpl::work`:Full join、零列行、饱和运算。 - Greedy:大外侧 × 小唯一内侧选 hash;小外侧 × 大唯一内侧选 probe 且优先合并;临界点; - 多个可用唯一索引时选代价最低的;复合唯一索引 + 常量等值;无统计回退启发式。 -- 下游规则:hint 为 `Probe` / `Hash` / `NestLoop` / `None` 四种路径;`Probe` 校验失败退回 hash。 -- NestLoop 方向:Greedy 区域内的 cross pair、根为 Cross 的区域、`force_nested_loop` 的 cross 三类, - 方向均与原 `OrientNestedLoopJoin` 的现有用例结论一致(迁移其单测),`force_nested_loop` 下不发生重排。 + 多个可用唯一索引时选代价最低的;复合唯一索引 + 常量等值;无统计按默认行数。 +- 下游规则:`join_decided` 的 join 不被改写;未标记的走兜底。 +- NestLoop 方向:Greedy 区域内的 cross pair、根为 Cross 的区域、`FORCE_NEST_LOOP_JOIN` 区域三类, + 方向均与原 `OrientNestedLoopJoin` 的现有用例结论一致(迁移其单测),`FORCE_NEST_LOOP_JOIN` 下只出现 NLJ。 - 覆盖:scan 选了覆盖索引、另一个覆盖的 Unique 可 probe 时,probe 按 `seeks = 1` 计价并被选中, 改写后的 IndexScan 带 `covered_deserializers`,执行时不回表。 - 正确性:probe 后全部 `on` 条件、join filter、静态 filter 均保留(slt 覆盖)。 @@ -371,5 +361,8 @@ hash 输入或改写为 probe 时,不会丢失上层依赖的有序性、也 - 开启 `materialize` 的 MarkApply 的 `rescan_work`;以及 NLJ 内侧含子查询类 MarkApply 时, Greedy 按其参数化 / 物化前的估算决定方向的偏差(3.10)。 - 非唯一索引 probe:执行器需支持每个外侧行输出多行,且每行 reset 内侧开销较大,按具体 case 评估; - 届时 `rows_per_lookup` 由索引 NDV 估算,10 个候选上限才真正起作用。 + 届时 `rows_per_lookup` 由索引 NDV 估算。 +- 复合连接键的联合 NDV 与唯一性上界:hash 形态下,用被连接键绑定的任意 PK/Unique 估算行数, + 不限于 implementation 选中的索引(行数是数据的性质,与 scan 走哪个索引无关)。单列键已能从列直方图得到 NDV, + 收益只在复合键,按具体 case 再做。 - I/O 建模。 diff --git a/src/db.rs b/src/db.rs index c66d30e6..f679fb8e 100644 --- a/src/db.rs +++ b/src/db.rs @@ -531,11 +531,6 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { HepBatchStrategy::fix_point_bottomup(1), &[NormalizationRuleImpl::MaterializeMarkApply], ) - .after_batch( - "Orient Nested Loop Join", - HepBatchStrategy::fix_point_bottomup(1), - &[NormalizationRuleImpl::OrientNestedLoopJoin], - ) .after_batch( "Collapse Project", HepBatchStrategy::fix_point_bottomup(10), diff --git a/src/execution/dql/join/hash_join.rs b/src/execution/dql/join/hash_join.rs index a2d3644f..2544d37e 100644 --- a/src/execution/dql/join/hash_join.rs +++ b/src/execution/dql/join/hash_join.rs @@ -155,6 +155,8 @@ impl HashJoin { // build phase: // 1.construct hashtable, one hash key may contains multiple rows indices. // 2.merged all left tuples. + // TODO: std `RandomState` (SipHash) costs ~10% of an LMDB PK-sized hash join; a faster + // non-DoS-resistant hasher would do for build keys. #[allow(clippy::mutable_key_type)] let mut build_map = HashMap::new(); let mut build_buf = BumpVec::with_capacity_in(self.on_left_keys.len(), &self.bump); @@ -220,6 +222,8 @@ impl HashJoin { } } +// TODO: dropping the build map (one heap `Vec` and owned `Tuple`s per key) costs ~13.5% of an +// LMDB PK-sized hash join plus allocator consolidation; allocate build rows in `bump` instead. #[derive(Default, Debug)] pub(crate) struct BuildState { pub(crate) tuples: Vec<(usize, Tuple)>, diff --git a/src/optimizer/rule/implementation/dql/table_scan.rs b/src/optimizer/rule/implementation/dql/table_scan.rs index 6c89ac04..53a09b8b 100644 --- a/src/optimizer/rule/implementation/dql/table_scan.rs +++ b/src/optimizer/rule/implementation/dql/table_scan.rs @@ -100,9 +100,11 @@ impl ImplementationRule for IndexScanImplementation { let mut ndv = None; let index_meta = arena.index(index_info.meta); - if let Some((mut row_count, distinct_count)) = - loader.collect_count(&scan_op.table_name, index_meta.id, range)? - { + // A PK/Unique equality range has an exact row count even without statistics. + let counted = loader + .collect_count(&scan_op.table_name, index_meta.id, range)? + .or_else(|| unique_eq_row_count(index_meta.ty, range).map(|rows| (rows, rows))); + if let Some((mut row_count, distinct_count)) = counted { row_count = adjust_index_row_count(index_meta.ty, range, row_count); estimated_rows = Some(row_count); ndv = Some(distinct_count.min(row_count)); diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index b309f1ba..891bfcb3 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -166,12 +166,12 @@ fn lift_cross_project( let mut join = plan.take(); join.reset_output_schema_cache(); - join.clear_startup_work(); + join.clear_work_cost(); *project.childrens = Childrens::Only(Box::new(join)); project.physical_option = None; project.reset_output_schema_cache(); - project.clear_startup_work(); + project.clear_work_cost(); *plan = project; Ok(true) @@ -267,14 +267,14 @@ impl NormalizationRule for EliminateProject { std::mem::swap(&mut parent.childrens, &mut project.childrens); parent.reset_output_schema_cache(); - parent.clear_startup_work(); + parent.clear_work_cost(); *project.childrens = Childrens::Only(Box::new(parent)); project.physical_option = None; *plan = project; } plan.reset_output_schema_cache(); - plan.clear_startup_work(); + plan.clear_work_cost(); Ok(true) } } diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index fbf3315a..e159da0c 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -19,98 +19,34 @@ use crate::expression::ScalarExpression; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::scan_column; +use crate::optimizer::rule::normalization::parameterized_index::{ + probe_index, probe_join, ProbeIndex, +}; use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::{ rows_work, InputCost, JoinImpl, Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption, + DEFAULT_TABLE_ROWS, }; use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; use crate::types::index::{IndexMeta, IndexMetaRef, IndexType}; use crate::types::tuple::Schema; use std::borrow::Borrow; -use std::collections::{btree_map::Entry, BTreeMap, HashMap}; +use std::collections::{btree_map::Entry, BTreeMap}; use std::mem::swap; pub(crate) struct GreedyJoinReorder; -pub(crate) struct OrientNestedLoopJoin; -impl NormalizationRule for OrientNestedLoopJoin { - fn apply( - &self, - plan: &mut LogicalPlan, - arena: &mut PlanArena, - _loader: Option<&StatisticMetaLoader<'_>>, - ) -> Result { - let ( - Operator::Join(JoinOperator { - join_type: JoinType::Cross, - on: JoinCondition::None, - limit_pushed: false, - .. - }), - Childrens::Twins { left, right }, - Some(PhysicalOption { - plan: PlanImpl::NestLoopJoin, - .. - }), - ) = ( - &plan.operator, - plan.childrens.as_mut(), - plan.physical_option.as_ref(), - ) - else { - return Ok(false); - }; - let (Some(left_startup), Some(right_startup)) = ( - left.estimated_startup_work(arena)?, - right.estimated_startup_work(arena)?, - ) else { - return Ok(false); - }; - let should_swap = match (left.estimated_rows(), right.estimated_rows()) { - (Some(left_rows), Some(right_rows)) => { - if left_rows == 0 { - false - } else if right_rows == 0 { - true - } else { - let (Some(left), Some(right)) = - (left.estimated_cost(arena)?, right.estimated_cost(arena)?) - else { - return Ok(false); - }; - // Both directions emit the same output, so it is left out of the comparison. - let nest_loop = JoinImpl::NestLoop { full: false }; - nest_loop.work(&right, &left, 0, 0) < nest_loop.work(&left, &right, 0, 0) - } - } - (Some(0 | 1), None) => false, - _ => right_startup > left_startup, - }; - if !should_swap { - return Ok(false); - } - let left_len = left.output_schema(arena).len(); - let original_schema = plan.output_schema(arena).clone(); - for (index, column) in original_schema.iter().enumerate() { - if original_schema[index + 1..] - .iter() - .any(|other| arena.same_column(*column, *other)) - { - return Ok(false); - } - } - let mut join = plan.take(); - let Childrens::Twins { left, right } = join.childrens.as_mut() else { - unreachable!() - }; - swap(left, right); - join.reset_output_schema_cache(); - join.clear_startup_work(); - let right_len = original_schema.len() - left_len; - let positions = (right_len..original_schema.len()).chain(0..right_len); - *plan = project_join_output(join, original_schema, positions, arena); - Ok(true) +/// Whether a cross nested loop should drive from its right input. +fn nest_loop_swap(left: &InputCost, right: &InputCost) -> bool { + if left.rows == 0 { + false + } else if right.rows == 0 { + true + } else { + // Both directions emit the same output, so it is left out of the comparison. + let nest_loop = JoinImpl::NestLoop { full: false }; + nest_loop.work(right, left, 0, 0) < nest_loop.work(left, right, 0, 0) } } @@ -146,11 +82,12 @@ fn project_join_output( #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] struct JoinCandidate { cross_join: bool, - work: usize, + work: i128, rows: usize, left: usize, right: usize, swapped: bool, + probe: bool, } fn column_ndv>( @@ -349,29 +286,31 @@ fn join_rows>( estimate_join_rows(left_estimate, right_estimate) } -fn can_expand(plan: &LogicalPlan) -> bool { +// A region only spans joins sharing the `FORCE_NEST_LOOP_JOIN` setting of its root. +fn can_expand(plan: &LogicalPlan, forced: bool) -> bool { let Operator::Join(JoinOperator { join_type, on, - force_nested_loop: false, + force_nested_loop, limit_pushed: false, }) = &plan.operator else { return false; }; - matches!((join_type, on), (JoinType::Cross, JoinCondition::None)) - || matches!((join_type, on), - (JoinType::Inner, JoinCondition::On { on, filter: None }) if !on.is_empty()) + *force_nested_loop == forced + && (matches!((join_type, on), (JoinType::Cross, JoinCondition::None)) + || matches!((join_type, on), + (JoinType::Inner, JoinCondition::On { on, filter: None }) if !on.is_empty())) } // Counts a region's inputs and reports whether it already contains a Cartesian product. -fn region_shape(plan: &LogicalPlan) -> (usize, bool) { - if !can_expand(plan) { +fn region_shape(plan: &LogicalPlan, forced: bool) -> (usize, bool) { + if !can_expand(plan, forced) { return (1, false); } let (left, right) = plan.childrens.twins(); - let (left_inputs, left_cross) = region_shape(left); - let (right_inputs, right_cross) = region_shape(right); + let (left_inputs, left_cross) = region_shape(left, forced); + let (right_inputs, right_cross) = region_shape(right, forced); let cross = matches!( &plan.operator, Operator::Join(JoinOperator { @@ -393,8 +332,9 @@ fn inspect<'plan>( keys: &mut JoinKeys, arena: &mut PlanArena, loader: Option<&StatisticMetaLoader<'_>>, + forced: bool, ) -> Result, DatabaseError> { - if can_expand(plan) { + if can_expand(plan, forced) { let (Operator::Join(join), Childrens::Twins { left, right }) = (&plan.operator, plan.childrens.as_mut()) else { @@ -408,9 +348,9 @@ fn inspect<'plan>( }; let ndvs = column_ndv(left, right, on.iter().copied(), loader, arena)?; let start = schemas.len(); - let left_rows = inspect(left, schemas, keys, arena, loader)?; + let left_rows = inspect(left, schemas, keys, arena, loader, forced)?; let middle = schemas.len(); - let right_rows = inspect(right, schemas, keys, arena, loader)?; + let right_rows = inspect(right, schemas, keys, arena, loader, forced)?; for &(left, right) in on { let mut endpoints = [None, None]; @@ -474,7 +414,7 @@ fn inspect<'plan>( rows: rows, ndv: None, }; - option.startup_work = None; + option.cost = None; } } Ok(Some(rows)) @@ -485,14 +425,14 @@ fn inspect<'plan>( } } -fn flatten(plan: LogicalPlan, inputs: &mut Vec<(LogicalPlan, usize, u64)>) { - if can_expand(&plan) { +fn flatten(plan: LogicalPlan, inputs: &mut Vec<(LogicalPlan, usize, u64)>, forced: bool) { + if can_expand(&plan, forced) { let (left, right) = plan.childrens.pop_twins(); - flatten(left, inputs); - flatten(right, inputs); + flatten(left, inputs, forced); + flatten(right, inputs, forced); } else { - // Unused when the region is not fully estimated. - let rows = plan.estimated_rows().unwrap_or(0); + // Without statistics an input is assumed to have the default rows (RFC 0001 §3.9). + let rows = plan.estimated_rows().unwrap_or(DEFAULT_TABLE_ROWS); let mask = 1u64 << inputs.len(); inputs.push((plan, rows, mask)); } @@ -537,13 +477,6 @@ impl JoinKeys { .flat_map(|&id| self.on_keys[id].iter().copied()) } - fn pairs(&self) -> impl Iterator + '_ { - self.links - .keys() - .copied() - .filter(|(left, right)| left < right) - } - fn take(&mut self, left: u64, right: u64) -> Vec<(ExprRef, ExprRef)> { let Some(id) = self.links.remove(&(left, right)) else { return Vec::new(); @@ -612,32 +545,61 @@ impl NormalizationRule for GreedyJoinReorder { arena: &mut PlanArena, loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { - if !matches!(&plan.operator, Operator::Join(join) if join.join_type == JoinType::Inner) - || !can_expand(plan) + if plan + .physical_option + .as_ref() + .is_some_and(|option| option.join_decided) { return Ok(false); } - let (input_count, _) = region_shape(plan); + // `FORCE_NEST_LOOP_JOIN` regions are reordered too, but only with nested loops. + let forced = matches!( + &plan.operator, + Operator::Join(JoinOperator { + force_nested_loop: true, + .. + }) + ); + if !can_expand(plan, forced) { + return Ok(false); + } + let (input_count, _) = region_shape(plan, forced); if input_count > u64::BITS as usize { return Ok(false); } let original_schema = plan.output_schema(arena).clone(); let mut schemas = Vec::new(); let mut keys = JoinKeys::default(); - let estimated = inspect(plan, &mut schemas, &mut keys, arena, loader)?.is_some(); - // Unknown row counts remain unknown; only column widths break their ties. - // Two-input regions also need to choose the smaller hash build side. + // Inputs without statistics are priced with default rows, but joins built from them keep + // an unknown row count for later rules. + let estimated = inspect(plan, &mut schemas, &mut keys, arena, loader, forced)?.is_some(); // Each original column keeps its input bit and position within its current join. let mut positions = Vec::with_capacity(original_schema.len()); for (input, schema) in schemas.iter().enumerate() { positions.extend((0..schema.len()).map(|position| (1u64 << input, position))); } let mut inputs = Vec::with_capacity(schemas.len()); - flatten(plan.take(), &mut inputs); - let mut row_cache = HashMap::new(); + flatten(plan.take(), &mut inputs, forced); + let mut probe_keys = Vec::new(); + // Left-deep: start from the smallest input (cheapest to read on ties), then add one + // remaining input per step, so every input stays a leaf until it joins and can still be + // probed (RFC 0001 §3.2). + let mut current = 0; + let mut start = (usize::MAX, usize::MAX); + for (i, (input, rows, _)) in inputs.iter_mut().enumerate() { + let work = input + .estimated_cost(arena)? + .map_or(usize::MAX, |cost| cost.work); + if (*rows, work) < start { + (start, current) = ((*rows, work), i); + } + } while inputs.len() > 1 { let mut best: Option = None; - let connected = !keys.links.is_empty(); + // The index chosen for `best` when it probes, kept to build the probe without a re-search. + let mut best_probe: Option = None; + // A result of at most one row may take a Cartesian product before an equijoin. + let single = inputs[current].1 <= 1; let mut evaluate = |inputs: &mut [(LogicalPlan, usize, u64)], i: usize, j: usize| @@ -645,20 +607,15 @@ impl NormalizationRule for GreedyJoinReorder { let (prefix, suffix) = inputs.split_at_mut(j); let (left, left_rows, left_mask) = &mut prefix[i]; let (right, right_rows, right_mask) = &mut suffix[0]; - // This pair's inputs and keys stay fixed until either input is merged. - let cache_key = (*left_mask, *right_mask); let candidate_keys = || keys.get(*left_mask, *right_mask); - let product = if !estimated { - 0 - } else if let Some(rows) = row_cache.get(&cache_key) { - *rows - } else { + // Left-deep evaluates each pair once, so the estimate is not cached. + let product = { let ndvs = column_ndv(left, right, candidate_keys(), loader, arena)?; let left_stats = index_statistics(left); let right_stats = index_statistics(right); let left_schema = left.output_schema(arena); let right_schema = right.output_schema(arena); - let rows = join_rows( + join_rows( &JoinInputStatistics { rows: *left_rows, schema: left_schema, @@ -672,77 +629,105 @@ impl NormalizationRule for GreedyJoinReorder { &candidate_keys, ndvs, arena, - ); - row_cache.insert(cache_key, rows); - rows + ) }; - // Without cardinalities every input counts as one row, so widths break ties. - let input_cost = |input: &mut LogicalPlan, arena: &mut PlanArena| { - let cost = if estimated { - input.estimated_cost(arena)? - } else { - None - }; - Ok::<_, DatabaseError>(cost.unwrap_or_else(|| { - let width = input.output_schema(arena).len(); - InputCost { - rows: 1, - width, - work: rows_work(1, width), + let connected = candidate_keys().next().is_some(); + let hash = connected && !forced; + // An input without a complete estimate is read once at its (default) rows. + let input_cost = |input: &mut LogicalPlan, rows: usize, arena: &mut PlanArena| { + Ok::<_, DatabaseError>(match input.estimated_cost(arena)? { + Some(cost) => cost, + None => { + let width = input.output_schema(arena).len(); + InputCost { + rows, + width, + work: rows_work(rows, width), + } } - })) - }; - let (left, right) = (input_cost(left, arena)?, input_cost(right, arena)?); - let out_rows = if estimated { product } else { 1 }; - let implementation = if connected { - JoinImpl::Hash - } else { - JoinImpl::NestLoop { full: false } + }) }; + let (left_cost, right_cost) = ( + input_cost(left, *left_rows, arena)?, + input_cost(right, *right_rows, arena)?, + ); + let nest_loop_swapped = !hash && nest_loop_swap(&left_cost, &right_cost); + let out_rows = product; // Every input is read once in any order, so only this step's work is compared. - let step = |left: &InputCost, right: &InputCost| { - implementation - .work(left, right, out_rows, left.width + right.width) - .saturating_sub(left.work) - .saturating_sub(right.work) + // A probe never reads its inner input, so its step can be negative. + let step = + |implementation: JoinImpl, outer: &InputCost, inner: &InputCost, rows| { + implementation.work(outer, inner, rows, outer.width + inner.width) as i128 + - outer.work as i128 + - inner.work as i128 + }; + // (work, rows, swapped, probe index); the left input is the hash build side and the + // nested loop / probe outer side. + let shape = if hash { + let keep = step(JoinImpl::Hash, &left_cost, &right_cost, out_rows); + let swap = step(JoinImpl::Hash, &right_cost, &left_cost, out_rows); + let mut shape = (keep.min(swap), product, swap < keep, None); + for swapped in [false, true] { + let (inner, outer_cost, inner_cost) = if swapped { + (&*left, &right_cost, &left_cost) + } else { + (&*right, &left_cost, &right_cost) + }; + // Keys run from the outer value to the probed column. + let flip = (*left_mask > *right_mask) != swapped; + probe_keys.clear(); + probe_keys.extend( + candidate_keys().map(|(l, r)| if flip { (r, l) } else { (l, r) }), + ); + let key_count = probe_keys.len(); + let Some(index) = + probe_index(inner, &mut probe_keys, key_count, arena, loader)? + else { + continue; + }; + // A unique probe emits at most one row per outer row. + let rows = product.min(outer_cost.rows); + let work = step( + JoinImpl::IndexProbe { + lookup_work: index.lookup_work, + }, + outer_cost, + inner_cost, + rows, + ); + if work <= shape.0 { + shape = (work, rows, swapped, Some(index)); + } + } + shape + } else { + let (outer, inner) = if nest_loop_swapped { + (&right_cost, &left_cost) + } else { + (&left_cost, &right_cost) + }; + let work = step(JoinImpl::NestLoop { full: false }, outer, inner, out_rows); + (work, product, nest_loop_swapped, None) }; - // The left input is the hash build side and the nested loop outer side. - let (keep, swap) = (step(&left, &right), step(&right, &left)); + let (work, rows, swapped, probe) = shape; let candidate = JoinCandidate { - cross_join: !connected, - work: keep.min(swap), - rows: product, + cross_join: !connected && !single, + work, + rows, left: i, right: j, - swapped: swap < keep, + swapped, + probe: probe.is_some(), }; if best.is_none_or(|previous| candidate < previous) { best = Some(candidate); + best_probe = probe; } Ok(()) }; - if connected { - for (left, right) in keys.pairs() { - // SAFETY: keys only contains active input masks; merge rewrites removed - // endpoints. Disjoint masks have unique lowest bits, and inserting each - // merged input at the earlier position preserves their sorted order. - let i = inputs - .binary_search_by_key(&(left.trailing_zeros(), left), |(_, _, mask)| { - (mask.trailing_zeros(), *mask) - }) - .unwrap(); - let j = inputs - .binary_search_by_key(&(right.trailing_zeros(), right), |(_, _, mask)| { - (mask.trailing_zeros(), *mask) - }) - .unwrap(); - evaluate(&mut inputs, i.min(j), i.max(j))?; - } - } else { - for i in 0..inputs.len() { - for j in i + 1..inputs.len() { - evaluate(&mut inputs, i, j)?; - } + for other in 0..inputs.len() { + if other != current { + evaluate(&mut inputs, current.min(other), current.max(other))?; } } let Some(JoinCandidate { @@ -776,30 +761,40 @@ impl NormalizationRule for GreedyJoinReorder { BindPosition(left_schema).visit(l, arena)?; BindPosition(right_schema).visit(r, arena)?; } - let (condition, join_type, implementation) = if on.is_empty() { - (JoinCondition::None, JoinType::Cross, PlanImpl::NestLoopJoin) + let row_count = if estimated { + RowCountOption::Estimated { + rows: product, + ndv: None, + } } else { - ( - JoinCondition::On { on, filter: None }, - JoinType::Inner, - PlanImpl::HashJoin, - ) + RowCountOption::Unknown }; - let mut join = JoinOperator::build(left, right, condition, join_type, false); - join.physical_option = Some(PhysicalOption::new( - implementation, - SortOption::None, - if estimated { - RowCountOption::Estimated { - rows: product, - ndv: None, - } + let join = if let Some(mut index) = best_probe { + // Probe values were chosen before binding; they come from the outer (left) input. + for value in &mut index.probe { + *value = value.clone_expression(arena)?; + BindPosition(left_schema).visit(value, arena)?; + } + probe_join(left, right, on, index, row_count, arena)? + } else { + let (condition, join_type) = if on.is_empty() { + (JoinCondition::None, JoinType::Cross) } else { - RowCountOption::Unknown - }, - )); + (JoinCondition::On { on, filter: None }, JoinType::Inner) + }; + let mut join = JoinOperator::build(left, right, condition, join_type, forced); + let Operator::Join(op) = &join.operator else { + unreachable!() + }; + let implementation = op.plan_impl(); + let mut option = PhysicalOption::new(implementation, SortOption::None, row_count); + option.join_decided = true; + join.physical_option = Some(option); + join + }; keys.rewrite(left_mask, right_mask); inputs.insert(i, (join, product, left_mask | right_mask)); + current = i; } // SAFETY: At this point, there should be exactly one plan left in the inputs vector, which is the final join result. let (join, _, _) = inputs.pop().unwrap(); @@ -979,65 +974,6 @@ mod tests { Ok(()) } - #[test] - fn merged_inputs_do_not_reuse_leaf_pair_row_estimates() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (a, ak) = input("a", 100, &mut arena); - let (b, bk) = input("b", 100, &mut arena); - let (c, ck) = input("c", 100, &mut arena); - let (d, dk) = input("d", 100, &mut arena); - let ab = JoinOperator::build( - a, - b, - JoinCondition::On { - on: vec![(ak, bk)], - filter: None, - }, - JoinType::Inner, - false, - ); - let abc = JoinOperator::build( - ab, - c, - JoinCondition::On { - on: vec![(bk, ck)], - filter: None, - }, - JoinType::Inner, - false, - ); - let mut plan = JoinOperator::build( - abc, - d, - JoinCondition::On { - on: vec![(ck, dk)], - filter: None, - }, - JoinType::Inner, - false, - ); - let schema = plan.output_schema(&mut arena).clone(); - let mut cache = StatisticsMetaCache::default(); - for (table, ndv) in [("a", 100), ("b", 100), ("c", 10), ("d", 10)] { - let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; - for value in 0..ndv { - builder.append(&DataValue::Int32(value)); - } - cache.columns.insert((table.into(), 0), builder.build()); - } - assert!(GreedyJoinReorder.apply( - &mut plan, - &mut arena, - Some(&StatisticMetaLoader::new(&cache)) - )?); - // A-B estimates 100, C-D estimates 1,000. Their merged inputs must not - // reuse any leaf-pair result when Vec positions are removed and reused. - assert_eq!(plan.estimated_rows(), Some(100 * 1_000)); - assert_eq!(plan.output_schema(&mut arena), &schema); - Ok(()) - } - #[test] fn column_ndv_is_read_from_scan_without_an_index() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); @@ -1379,7 +1315,6 @@ mod tests { Some(usize::MAX) ); assert_eq!(cross.estimated_rows(), Some(usize::MAX)); - assert!(!OrientNestedLoopJoin.apply(&mut cross, &mut arena, None)?); Ok(()) } @@ -1391,8 +1326,11 @@ mod tests { let (mut left, _) = input("a", 100, &mut arena); let mut right = materialized_right(&mut arena); assert_eq!(left.estimated_startup_work(&mut arena)?, Some(0)); - // The 10-row build side is read (50) and stored whole (50). - assert_eq!(right.estimated_startup_work(&mut arena)?, Some(100)); + // The 10-row build side is read (50), stored whole (50) and hashed. + assert_eq!( + right.estimated_startup_work(&mut arena)?, + Some(100 + 10 * crate::planner::operator::HASH_BUILD_WORK) + ); let mut plan = JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); plan.physical_option = Some(PhysicalOption::new( @@ -1404,7 +1342,7 @@ mod tests { }, )); let schema = plan.output_schema(&mut arena).clone(); - assert!(OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); assert_eq!(plan.estimated_rows(), Some(20000)); let Operator::Project(project) = &plan.operator else { @@ -1421,7 +1359,7 @@ mod tests { assert_eq!(left.referenced_table(), vec!["b".into(), "c".into()]); assert_eq!(right.referenced_table(), vec!["a".into()]); let mut join = join.clone(); - assert!(!OrientNestedLoopJoin.apply(&mut join, &mut arena, None)?); + assert!(!GreedyJoinReorder.apply(&mut join, &mut arena, None)?); Ok(()) } @@ -1586,8 +1524,8 @@ mod tests { #[test] fn unestimated_region_defers_cartesian_products_by_input_order() -> Result<(), DatabaseError> { // FROM a, c, d, b WHERE a = b AND c = b: FROM order crosses a, c and d - // before any join key applies. Without estimates the region is rebuilt - // by connectivity and input position, and only d stays a cross join. + // before any join key applies. Without estimates the region is priced with + // default rows, and only d stays a cross join. for known_b in [false, true] { let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); @@ -1630,9 +1568,9 @@ mod tests { // Without estimates, the narrower C becomes the build side of AB. let (c, ab) = abc.childrens.twins(); assert_eq!(c.referenced_table(), vec!["c".into()]); - let (a, b) = ab.childrens.twins(); - assert_eq!(a.referenced_table(), vec!["a".into()]); - assert_eq!(b.referenced_table(), vec!["b".into()]); + let mut tables = ab.referenced_table(); + tables.sort(); + assert_eq!(tables, vec!["a".into(), "b".into()]); for join in [cross, abc, ab] { assert_eq!(join.row_count_option(), None); } @@ -1706,11 +1644,13 @@ mod tests { right.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; } let mut outer = plan.clone(); - // Unknown inputs are still rebuilt by connectivity and width, without row estimates. + // Unknown inputs are rebuilt with default rows, but joins keep unknown row counts. assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); let (c, ab) = plan.childrens.only().childrens.twins(); assert_eq!(c.referenced_table(), vec!["c".into()]); - assert_eq!(ab.referenced_table(), vec!["a".into(), "b".into()]); + let mut tables = ab.referenced_table(); + tables.sort(); + assert_eq!(tables, vec!["a".into(), "b".into()]); assert_eq!(plan.row_count_option(), None); let mut plan = outer.take(); diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index 96cddd5b..c9bab9c1 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -47,7 +47,7 @@ mod join_reorder; mod materialize_mark_apply; mod min_max_top_k; use guard_mark_apply::GuardMarkApply; -use join_reorder::{GreedyJoinReorder, OrientNestedLoopJoin}; +use join_reorder::GreedyJoinReorder; use materialize_mark_apply::MaterializeMarkApply; mod project_move; use project_move::ProjectMove; @@ -93,7 +93,6 @@ pub enum NormalizationRuleImpl { ParameterizeInnerJoin, EliminateIndexFilter, GreedyJoinReorder, - OrientNestedLoopJoin, MaterializeMarkApply, GuardMarkApply, ProjectMove, @@ -207,8 +206,7 @@ impl NormalizationRuleImpl { NormalizationRuleImpl::MinMaxToTopK => NormalizationRuleRootTag::Aggregate, NormalizationRuleImpl::EliminateIndexFilter => NormalizationRuleRootTag::Filter, NormalizationRuleImpl::ParameterizeInnerJoin - | NormalizationRuleImpl::GreedyJoinReorder - | NormalizationRuleImpl::OrientNestedLoopJoin => NormalizationRuleRootTag::Join, + | NormalizationRuleImpl::GreedyJoinReorder => NormalizationRuleRootTag::Join, NormalizationRuleImpl::ParameterizeMarkApply | NormalizationRuleImpl::MaterializeMarkApply => NormalizationRuleRootTag::MarkApply, } @@ -227,9 +225,6 @@ impl NormalizationRule for NormalizationRuleImpl { NormalizationRuleImpl::GreedyJoinReorder => { GreedyJoinReorder.apply(plan, arena, loader) } - NormalizationRuleImpl::OrientNestedLoopJoin => { - OrientNestedLoopJoin.apply(plan, arena, loader) - } NormalizationRuleImpl::GuardMarkApply => GuardMarkApply.apply(plan, arena, loader), NormalizationRuleImpl::EliminateProject => EliminateProject.apply(plan, arena, loader), NormalizationRuleImpl::ProjectMove => ProjectMove.apply(plan, arena, loader), diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index f71b7d9b..563c49d1 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -18,18 +18,23 @@ use crate::expression::visitor_mut::{ExprVisitorMut, PositionShift}; use crate::expression::{BinaryOperator, ScalarExpression, TypeCast}; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; +use crate::optimizer::rule::normalization::pushdown_predicates::{index_covering, is_index_covers}; use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::join::{JoinCondition, JoinType}; use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyOperator, MarkApplyQuantifier}; use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::table_scan::TableScanOperator; -use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; +use crate::planner::operator::{ + rows_work, Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption, CPU_OPERATOR_WORK, + DEFAULT_TABLE_ROWS, PROBE_RESET_WORK, +}; use crate::planner::MetaArena; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; -use crate::types::index::{IndexLookup, IndexType}; +use crate::types::index::{IndexLookup, IndexMetaRef, IndexType}; use crate::types::tuple::Schema; use crate::types::LogicalType; use std::borrow::Cow; +use std::mem; pub(crate) struct ParameterizeMarkApply; @@ -290,28 +295,38 @@ fn constant_keys(expr: ExprRef, keys: &mut Vec<(ExprRef, ExprRef)>, arena: &Plan } } -fn parameterize( - plan: &mut LogicalPlan, +/// A PK/Unique index of a `Filter* -> TableScan` input that join keys can probe. +pub(crate) struct ProbeIndex { + position: usize, + pub(crate) probe: Vec, + pub(crate) lookup_work: usize, +} + +pub(crate) fn probe_index( + plan: &LogicalPlan, keys: &mut Vec<(ExprRef, ExprRef)>, join_key_count: usize, arena: &PlanArena<'_>, -) -> Option> { - match (&mut plan.operator, plan.childrens.as_mut()) { + loader: Option<&StatisticMetaLoader<'_>>, +) -> Result, DatabaseError> { + match (&plan.operator, plan.childrens.as_ref()) { (Operator::Filter(filter), Childrens::Only(child)) => { let key_count = keys.len(); constant_keys(filter.predicate, keys, arena); - let probe = parameterize(child, keys, join_key_count, arena); + let probe = probe_index(child, keys, join_key_count, arena, loader); // Keep filter keys local to this traversal, including failed attempts. keys.truncate(key_count); probe } (Operator::TableScan(scan), _) if scan.limit == (None, None) => { - 'indexes: for info in &mut scan.index_infos { + let mut best: Option = None; + let mut probe = Vec::new(); + 'indexes: for (position, info) in scan.index_infos.iter().enumerate() { let meta = arena.index(info.meta); if !matches!(meta.ty, IndexType::PrimaryKey { .. } | IndexType::Unique) { continue; } - let mut probe = Vec::with_capacity(meta.column_ids.len()); + probe.clear(); let mut uses_join_key = false; 'columns: for id in &meta.column_ids { let Some(candidate) = scan @@ -341,32 +356,137 @@ fn parameterize( // Constants appended by filters may complete a composite key, // but must not trigger a per-outer-row probe on their own. if !uses_join_key { - continue 'indexes; + continue; + } + let lookup_work = lookup_work(scan, info.meta, arena, loader)?; + // Equal work keeps the index order. + if best + .as_ref() + .is_none_or(|best| lookup_work < best.lookup_work) + { + best = Some(ProbeIndex { + position, + probe: mem::take(&mut probe), + lookup_work, + }); } - info.lookup = Some(IndexLookup::Probe); - info.residual_predicate = None; - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::IndexScan(Box::new(info.clone())), - info.sort_option.clone(), - RowCountOption::Unknown, - )); - return Some(probe); } - None + Ok(best) + } + _ => Ok(None), + } +} + +/// RFC 0001 §4.3: reset the probed input, seek the index (twice when it must read the +/// table row too), and read one row. Without statistics the index has `DEFAULT_TABLE_ROWS`. +fn lookup_work( + scan: &TableScanOperator, + index: IndexMetaRef, + arena: &PlanArena<'_>, + loader: Option<&StatisticMetaLoader<'_>>, +) -> Result { + let meta = arena.index(index); + let rows = match loader { + Some(loader) => loader + .load(&scan.table_name, meta.id)? + .map(|statistics| statistics.histogram().values_len()), + None => None, + } + .unwrap_or(DEFAULT_TABLE_ROWS); + let seeks = if matches!(meta.ty, IndexType::PrimaryKey { .. }) + || is_index_covers(&scan.columns, index, arena) + { + 1 + } else { + 2 + }; + let depth = (usize::BITS - rows.saturating_sub(1).leading_zeros()) as usize; + Ok(PROBE_RESET_WORK + .saturating_add(seeks * depth * CPU_OPERATOR_WORK) + .saturating_add(rows_work(1, scan.columns.len()))) +} + +fn use_probe_index(plan: &mut LogicalPlan, position: usize, arena: &PlanArena<'_>) { + match (&mut plan.operator, plan.childrens.as_mut()) { + (Operator::Filter(_), Childrens::Only(child)) => use_probe_index(child, position, arena), + (Operator::TableScan(scan), _) => { + let info = &mut scan.index_infos[position]; + info.lookup = Some(IndexLookup::Probe); + info.residual_predicate = None; + (info.covered_deserializers, info.cover_mapping) = + match index_covering(&scan.columns, info.meta, arena) { + Some((deserializers, mapping)) => (Some(deserializers), mapping), + None => (None, None), + }; + plan.physical_option = Some(PhysicalOption::new( + PlanImpl::IndexScan(Box::new(info.clone())), + info.sort_option.clone(), + RowCountOption::Unknown, + )); } - // LIMIT/aggregation/projection are not row-local filters. Moving a probe - // below them could change which rows the original inner input produces. - _ => None, + // SAFETY: `probe_index` only accepts filters over a table scan. + _ => unreachable!(), } } +/// Builds the inner `MarkApply` probing `right` with `index` for every `left` row. `on` holds +/// the join keys bound to each side; all of them are still checked after the probe. +pub(crate) fn probe_join( + mut left: LogicalPlan, + mut right: LogicalPlan, + on: Vec<(ExprRef, ExprRef)>, + index: ProbeIndex, + row_count: RowCountOption, + arena: &mut PlanArena<'_>, +) -> Result { + use_probe_index(&mut right, index.position, arena); + let left_len = left.output_schema(arena).len(); + let probe = if index.probe.len() == 1 { + index.probe[0] + } else { + arena.alloc_expression(ScalarExpression::Tuple(index.probe)) + }; + let mut predicates = Vec::with_capacity(on.len()); + for (left_expr, mut right_expr) in on { + PositionShift { + delta: left_len as isize, + } + .visit(&mut right_expr, arena)?; + predicates.push(arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Eq, + left_expr, + right_expr, + evaluator: None, + ty: LogicalType::Boolean, + })); + } + let mut apply = LogicalPlan::new( + Operator::MarkApply(MarkApplyOperator::new_inner_join(predicates, probe)), + Childrens::Twins { + left: Box::new(left), + right: Box::new(right), + }, + ); + let mut option = PhysicalOption::new(PlanImpl::MarkApply, SortOption::Follow, row_count); + option.lookup_work = Some(index.lookup_work); + apply.physical_option = Some(option); + Ok(apply) +} + impl NormalizationRule for ParameterizeInnerJoin { fn apply( &self, plan: &mut LogicalPlan, arena: &mut PlanArena<'_>, - _loader: Option<&StatisticMetaLoader<'_>>, + loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { + if plan + .physical_option + .as_ref() + .is_some_and(|option| option.join_decided) + { + return Ok(false); + } let rows = plan.estimated_rows(); let (Operator::Join(join), Childrens::Twins { left, right }) = (&mut plan.operator, plan.childrens.as_mut()) @@ -385,14 +505,14 @@ impl NormalizationRule for ParameterizeInnerJoin { let mut keys = on.clone(); // Filter traversal only appends constants after these outer-dependent keys. let join_key_count = keys.len(); - let right_probe = parameterize(right, &mut keys, join_key_count, arena); - let (project, probe_keys) = if let Some(probe) = right_probe { - (None, probe) + let right_probe = probe_index(right, &mut keys, join_key_count, arena, loader)?; + let (project, index) = if let Some(index) = right_probe { + (None, index) } else { for (l, r) in &mut keys { std::mem::swap(l, r); } - let Some(probe) = parameterize(left, &mut keys, join_key_count, arena) else { + let Some(index) = probe_index(left, &mut keys, join_key_count, arena, loader)? else { return Ok(false); }; let left_schema = left.output_schema(arena); @@ -424,47 +544,17 @@ impl NormalizationRule for ParameterizeInnerJoin { SortOption::Follow, RowCountOption::Follow, )); - (Some(project), probe) + (Some(project), index) }; let filter = filter.take(); - let (mut left, right) = plan.take().childrens.pop_twins(); - let left_len = left.output_schema(arena).len(); - let probe = if probe_keys.len() == 1 { - probe_keys[0] - } else { - arena.alloc_expression(ScalarExpression::Tuple(probe_keys)) - }; - let mut predicates = Vec::with_capacity(keys.len()); - for (left_expr, mut right_expr) in keys { - PositionShift { - delta: left_len as isize, - } - .visit(&mut right_expr, arena)?; - predicates.push(arena.alloc_expression(ScalarExpression::Binary { - op: BinaryOperator::Eq, - left_expr, - right_expr, - evaluator: None, - ty: LogicalType::Boolean, - })); - } - let mut apply = LogicalPlan::new( - Operator::MarkApply(MarkApplyOperator::new_inner_join(predicates, probe)), - Childrens::Twins { - left: Box::new(left), - right: Box::new(right), - }, - ); + let (left, right) = plan.take().childrens.pop_twins(); // This is the same join output, not the per-key cardinality of the probe. - apply.physical_option = Some(PhysicalOption::new( - PlanImpl::MarkApply, - SortOption::Follow, - rows.map_or(RowCountOption::Unknown, |rows| RowCountOption::Estimated { - rows, - ndv: None, - }), - )); + let row_count = rows.map_or(RowCountOption::Unknown, |rows| RowCountOption::Estimated { + rows, + ndv: None, + }); + let apply = probe_join(left, right, keys, index, row_count, arena)?; *plan = if let Some(mut project) = project { project.childrens = Box::new(Childrens::Only(Box::new(apply))); project @@ -637,11 +727,12 @@ mod inner_join_tests { use crate::catalog::{ColumnCatalog, ColumnDesc}; use crate::expression::range_detacher::Range; use crate::expression::{AliasType, UnaryOperator}; - use crate::optimizer::rule::normalization::join_reorder::OrientNestedLoopJoin; + use crate::optimizer::rule::normalization::join_reorder::GreedyJoinReorder; use crate::planner::operator::join::JoinOperator; use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::table_scan::TableScanOperator; + use crate::planner::operator::WorkCost; use crate::planner::TableArenaCell; use crate::types::index::{IndexInfo, IndexMeta, IndexType}; use crate::types::tuple::Tuple; @@ -975,12 +1066,16 @@ mod inner_join_tests { SortOption::None, RowCountOption::Estimated { rows, ndv: None }, )); - plan.physical_option.as_mut().unwrap().startup_work = Some(Some(9999)); + plan.physical_option.as_mut().unwrap().cost = Some(WorkCost { + width: 1, + startup: Some(9999), + total: Some(9999), + }); } assert!(ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); let option = plan.physical_option.as_ref().unwrap(); assert_eq!(option.plan, PlanImpl::MarkApply); - assert_eq!(option.startup_work, None); + assert_eq!(option.cost, None); assert_eq!(plan.estimated_rows(), rows); let Childrens::Twins { right, .. } = plan.childrens.as_ref() else { unreachable!() @@ -995,6 +1090,28 @@ mod inner_join_tests { Ok(()) } + #[test] + fn greedy_decided_join_is_not_parameterized() -> Result<(), DatabaseError> { + let tables = TableArenaCell::default(); + let mut arena = PlanArena::new(&tables); + let left = scan(&mut arena, "outer", &[LogicalType::Integer], &[]); + let right = unique_scan(&mut arena, "inner", &[LogicalType::Integer], &[&[0]]); + let l = column(&left, 0, &mut arena); + let r = column(&right, 0, &mut arena); + let mut plan = join(left, right, vec![(l, r)], None); + let mut option = PhysicalOption::new( + PlanImpl::HashJoin, + SortOption::None, + RowCountOption::Unknown, + ); + option.join_decided = true; + plan.physical_option = Some(option); + let original = plan.clone(); + assert!(!ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); + assert_eq!(plan, original); + Ok(()) + } + #[test] fn parameterized_join_allows_parent_nested_loop_orientation() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); @@ -1046,7 +1163,7 @@ mod inner_join_tests { RowCountOption::Unknown, )); let schema = plan.output_schema(&mut arena).clone(); - assert!(OrientNestedLoopJoin.apply(&mut plan, &mut arena, None)?); + assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); assert_eq!(plan.output_schema(&mut arena), &schema); assert_eq!(plan.estimated_rows(), Some(2000)); let Childrens::Only(join) = plan.childrens.as_ref() else { diff --git a/src/optimizer/rule/normalization/pushdown_predicates.rs b/src/optimizer/rule/normalization/pushdown_predicates.rs index c7c0a0c4..ad949050 100644 --- a/src/optimizer/rule/normalization/pushdown_predicates.rs +++ b/src/optimizer/rule/normalization/pushdown_predicates.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::catalog::ColumnRef; use crate::errors::DatabaseError; use crate::expression::range_detacher::{DetachedPredicate, Range, RangeDetacher}; use crate::expression::visitor_mut::{ExprVisitorMut, PositionShift}; @@ -25,6 +26,7 @@ use crate::planner::operator::join::{JoinCondition, JoinType}; use crate::planner::operator::{Operator, SortOption}; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; use crate::types::index::{IndexInfo, IndexLookup, IndexMetaRef}; +use crate::types::serialize::TupleValueSerializableImpl; use crate::types::value::DataValue; use crate::types::LogicalType; use std::ops::Bound; @@ -362,49 +364,69 @@ impl NormalizationRule for PushPredicateIntoScan { *lookup = Some(IndexLookup::Static(detached.range)); *residual_predicate = detached.residual; - *covered_deserializers = None; - *cover_mapping = None; - - // try index covered - let mut mapping_slots = vec![usize::MAX; scan_op.columns.len()]; - let mut needs_mapping = false; - let index_meta = arena.index(*meta); - let index_column_types = match &index_meta.value_ty { - LogicalType::Tuple(tys) => tys, - ty => slice::from_ref(ty), - }; - let mut deserializers = Vec::with_capacity(index_meta.column_ids.len()); - - for (idx, column_id) in index_meta.column_ids.iter().enumerate() { - if let Some((scan_idx, column)) = - scan_op.columns.iter().enumerate().find(|(_, column)| { - arena - .column(**column) - .id() - .map(|id| id == *column_id) - .unwrap_or(false) - }) - { - mapping_slots[scan_idx] = idx; - needs_mapping |= scan_idx != idx; - deserializers.push(arena.column(*column).datatype().serializable()); - } else { - deserializers.push(index_column_types[idx].skip_serializable()); - } - } - - if mapping_slots.iter().all(|slot| *slot != usize::MAX) { - *covered_deserializers = Some(deserializers); - if needs_mapping { - *cover_mapping = Some(mapping_slots); - } - } + (*covered_deserializers, *cover_mapping) = + match index_covering(&scan_op.columns, *meta, arena) { + Some((deserializers, mapping)) => (Some(deserializers), mapping), + None => (None, None), + }; } Ok(changed) } } +/// Index-only read layout when `index` holds every scanned column: deserializers for the +/// index columns, plus a scan-to-index slot mapping when their orders differ. +pub(crate) fn index_covering( + columns: &[ColumnRef], + index: IndexMetaRef, + arena: &PlanArena, +) -> Option<(Vec, Option>)> { + if !is_index_covers(columns, index, arena) { + return None; + } + let mut mapping_slots = vec![usize::MAX; columns.len()]; + let mut needs_mapping = false; + let index_meta = arena.index(index); + let index_column_types = match &index_meta.value_ty { + LogicalType::Tuple(tys) => tys, + ty => slice::from_ref(ty), + }; + let mut deserializers = Vec::with_capacity(index_meta.column_ids.len()); + + for (idx, column_id) in index_meta.column_ids.iter().enumerate() { + if let Some((scan_idx, column)) = columns.iter().enumerate().find(|(_, column)| { + arena + .column(**column) + .id() + .map(|id| id == *column_id) + .unwrap_or(false) + }) { + mapping_slots[scan_idx] = idx; + needs_mapping |= scan_idx != idx; + deserializers.push(arena.column(*column).datatype().serializable()); + } else { + deserializers.push(index_column_types[idx].skip_serializable()); + } + } + + Some((deserializers, needs_mapping.then_some(mapping_slots))) +} + +pub(crate) fn is_index_covers( + columns: &[ColumnRef], + index: IndexMetaRef, + arena: &PlanArena, +) -> bool { + let index_meta = arena.index(index); + columns.iter().all(|column| { + arena + .column(*column) + .id() + .is_some_and(|id| index_meta.column_ids.contains(&id)) + }) +} + impl PushPredicateIntoScan { fn composite_range( op: &FilterOperator, diff --git a/src/orm/mod.rs b/src/orm/mod.rs index e0755d38..3ce64423 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -4440,7 +4440,7 @@ mod tests { "Projection [orm_unit_users.id] [Project => (Sort Option: Follow)] ", "InnerJoinApply [MarkApply => (Sort Option: Follow)] ", "TableScan orm_unit_users -> [orm_unit_users.id] [SeqScan => (Sort Option: None)] ", - "TableScan orm_unit_orders -> [orm_unit_orders.id] [IndexScan By pk_index => Probe ? => (Sort Option: OrderBy: (orm_unit_orders.id Asc Nulls Last) ignore_prefix_len: 0)]" + "TableScan orm_unit_orders -> [orm_unit_orders.id] [IndexScan By pk_index => Probe ? Covered => (Sort Option: OrderBy: (orm_unit_orders.id Asc Nulls Last) ignore_prefix_len: 0)]" ), "{inner_using_plan}" ); diff --git a/src/planner/mod.rs b/src/planner/mod.rs index 73063e42..b54feb95 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -26,7 +26,9 @@ use crate::planner::operator::set_membership::SetMembershipOperator; use crate::planner::operator::union::UnionOperator; use crate::planner::operator::values::ValuesOperator; use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; -use crate::planner::operator::{input_cost, InputCost, Operator, PhysicalOption, RowCountOption}; +use crate::planner::operator::{ + input_cost, InputCost, Operator, PhysicalOption, RowCountOption, WorkCost, +}; use kite_sql_serde_macros::ReferenceSerialization; use std::fmt; use std::hash::{Hash, Hasher}; @@ -162,11 +164,12 @@ impl<'a, A: MetaArena> OperatorVisitorMut<'a> for StartupWorkVisitor<'_, A> { if plan .physical_option .as_ref() - .is_none_or(|option| option.startup_work.is_some()) + .is_none_or(|option| option.cost.is_some()) { return Ok(()); } let width = plan.output_schema(self.arena).len(); + let rows = plan.estimated_rows(); let LogicalPlan { operator, childrens, @@ -177,8 +180,14 @@ impl<'a, A: MetaArena> OperatorVisitorMut<'a> for StartupWorkVisitor<'_, A> { return Ok(()); }; OperatorVisitorMut::<'_>::visit_children(self, childrens)?; - option.width = Some(width); - option.calculate_startup_work(operator, childrens); + // Children are cached first, so this only prices the current node. + let mut cost = WorkCost { + width, + startup: option.calculate_startup_work(operator, childrens), + total: None, + }; + cost.total = option.calculate_work(operator, childrens, rows, &cost); + option.cost = Some(cost); Ok(()) } } @@ -261,7 +270,7 @@ impl LogicalPlan { Ok(self .physical_option .as_ref() - .and_then(|option| option.startup_work.flatten())) + .and_then(|option| option.cost?.startup)) } pub(crate) fn estimated_cost( @@ -272,9 +281,9 @@ impl LogicalPlan { Ok(input_cost(self)) } - pub(crate) fn clear_startup_work(&mut self) { + pub(crate) fn clear_work_cost(&mut self) { if let Some(option) = self.physical_option.as_mut() { - option.startup_work = None; + option.cost = None; } } @@ -723,12 +732,20 @@ mod tests { )); assert_eq!(sort.estimated_startup_work(&mut arena)?, None); assert_eq!( - sort.physical_option.as_ref().unwrap().startup_work, + sort.physical_option + .as_ref() + .unwrap() + .cost + .map(|cost| cost.startup), Some(None) ); assert_eq!(sort.estimated_startup_work(&mut arena)?, None); assert_eq!( - sort.physical_option.as_ref().unwrap().startup_work, + sort.physical_option + .as_ref() + .unwrap() + .cost + .map(|cost| cost.startup), Some(None) ); @@ -752,23 +769,31 @@ mod tests { // 16 one-column rows (5 each) + 64 comparisons * 2 operator units. assert_eq!(sort.estimated_startup_work(&mut arena)?, Some(208)); assert_eq!( - sort.physical_option.as_ref().unwrap().startup_work, + sort.physical_option + .as_ref() + .unwrap() + .cost + .map(|cost| cost.startup), Some(Some(208)) ); let Childrens::Only(scan) = sort.childrens.as_mut() else { unreachable!() }; assert_eq!( - scan.physical_option.as_ref().unwrap().startup_work, + scan.physical_option + .as_ref() + .unwrap() + .cost + .map(|cost| cost.startup), Some(Some(0)) ); // A cached parent does not walk its children again in this round. - scan.clear_startup_work(); + scan.clear_work_cost(); assert_eq!(sort.estimated_startup_work(&mut arena)?, Some(208)); let Childrens::Only(scan) = sort.childrens.as_mut() else { unreachable!() }; - assert_eq!(scan.physical_option.as_ref().unwrap().startup_work, None); + assert_eq!(scan.physical_option.as_ref().unwrap().cost, None); Ok(()) } diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 9863927e..e9bb86ff 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -169,8 +169,13 @@ pub enum RowCountOption { // This is the same scale as row = 1 and operator = 0.25; I/O is not modeled. pub(crate) const CPU_ROW_WORK: usize = 4; pub(crate) const CPU_OPERATOR_WORK: usize = 1; +// Rows assumed for a table without statistics (RFC 0001 §3.9). +pub(crate) const DEFAULT_TABLE_ROWS: usize = 1000; // Rebuilding the inner executor for each outer row of an index probe. pub(crate) const PROBE_RESET_WORK: usize = CPU_ROW_WORK; +// Hashing, inserting and later freeing one build row. Calibrated on LMDB, where a hash join +// costs ~7 scanned rows per build/probe pair (RFC 0001 §4.2). +pub(crate) const HASH_BUILD_WORK: usize = 6 * CPU_ROW_WORK; /// Work of producing `rows` rows of `width` columns. The fixed per-row part keeps /// zero-column rows (e.g. the input of `count(*)`) from being free. @@ -178,6 +183,10 @@ pub(crate) fn rows_work(rows: usize, width: usize) -> usize { rows.saturating_mul(CPU_ROW_WORK.saturating_add(width.saturating_mul(CPU_OPERATOR_WORK))) } +fn hash_build_work(build: &InputCost) -> usize { + rows_work(build.rows, build.width).saturating_add(build.rows.saturating_mul(HASH_BUILD_WORK)) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct InputCost { pub(crate) rows: usize, @@ -215,7 +224,7 @@ impl JoinImpl { // Build rows are stored whole; each probe row costs one hash lookup. JoinImpl::Hash => right .work - .saturating_add(rows_work(left.rows, left.width)) + .saturating_add(hash_build_work(left)) .saturating_add(right.rows.saturating_mul(CPU_OPERATOR_WORK)), // The inner input is re-run and compared for every outer row. JoinImpl::NestLoop { full } => left @@ -235,23 +244,35 @@ impl JoinImpl { } pub(crate) fn input_cost(input: &LogicalPlan) -> Option { - let option = input.physical_option.as_ref()?; - let rows = input.estimated_rows()?; + let cost = input.physical_option.as_ref()?.cost?; Some(InputCost { - rows, - width: option.width?, - work: option.calculate_work(&input.operator, &input.childrens, Some(rows))?, + rows: input.estimated_rows()?, + width: cost.width, + work: cost.total?, }) } +/// A node's cached work estimates, filled by `StartupWorkVisitor` and invalidated as a whole. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct WorkCost { + /// Output column count. + pub(crate) width: usize, + /// Work before the first row; `None` when unknown. + pub(crate) startup: Option, + /// Work of producing every row; `None` when unknown. + pub(crate) total: Option, +} + #[derive(Debug, Clone)] pub struct PhysicalOption { pub plan: PlanImpl, pub(crate) sort_option: SortOption, pub(crate) row_count: RowCountOption, - pub(crate) startup_work: Option>, - /// Output column count, cached together with `startup_work`. - pub(crate) width: Option, + /// `None` until computed. + pub(crate) cost: Option, + /// Per-lookup work of a parameterized probe, priced with index statistics. + pub(crate) lookup_work: Option, + pub(crate) join_decided: bool, } impl PhysicalOption { @@ -260,8 +281,9 @@ impl PhysicalOption { plan, sort_option, row_count, - startup_work: None, - width: None, + cost: None, + lookup_work: None, + join_decided: false, } } @@ -276,6 +298,7 @@ impl PhysicalOption { operator: &Operator, childrens: &Childrens, rows: Option, + cost: &WorkCost, ) -> Option { match (&self.plan, operator, childrens) { (PlanImpl::Filter, Operator::Filter(_), Childrens::Only(input)) @@ -285,25 +308,27 @@ impl PhysicalOption { (PlanImpl::MarkApply, Operator::MarkApply(op), Childrens::Twins { left, right }) if op.parameterized_probe().is_some() && !op.materialize => { - let right_option = right.physical_option.as_ref()?; + let right_cost = right.physical_option.as_ref()?.cost?; let probed = InputCost { rows: 1, - width: right_option.width?, - work: right_option.startup_work.flatten()?, + width: right_cost.width, + work: right_cost.startup?, }; - let lookup_work = probed - .work - .saturating_add(PROBE_RESET_WORK) - .saturating_add(rows_work(probed.rows, probed.width)); + let lookup_work = self.lookup_work.unwrap_or_else(|| { + probed + .work + .saturating_add(PROBE_RESET_WORK) + .saturating_add(rows_work(probed.rows, probed.width)) + }); Some(JoinImpl::IndexProbe { lookup_work }.work( &input_cost(left)?, &probed, rows?, - self.width?, + cost.width, )) } (PlanImpl::HashJoin, Operator::Join(_), Childrens::Twins { left, right }) => Some( - JoinImpl::Hash.work(&input_cost(left)?, &input_cost(right)?, rows?, self.width?), + JoinImpl::Hash.work(&input_cost(left)?, &input_cost(right)?, rows?, cost.width), ), (PlanImpl::NestLoopJoin, Operator::Join(join), Childrens::Twins { left, right }) => { let (outer, inner) = if join.join_type == join::JoinType::RightOuter { @@ -319,23 +344,22 @@ impl PhysicalOption { &input_cost(outer)?, &input_cost(inner)?, rows?, - self.width?, + cost.width, ), ) } - _ => Some( - self.startup_work - .flatten()? - .saturating_add(rows_work(rows?, self.width?)), - ), + _ => Some(cost.startup?.saturating_add(rows_work(rows?, cost.width))), } } - pub(crate) fn calculate_startup_work(&mut self, operator: &Operator, childrens: &Childrens) { - let startup_work = - |input: &LogicalPlan| input.physical_option.as_ref()?.startup_work.flatten(); + pub(crate) fn calculate_startup_work( + &self, + operator: &Operator, + childrens: &Childrens, + ) -> Option { + let startup_work = |input: &LogicalPlan| input.physical_option.as_ref()?.cost?.startup; let work = |input: &LogicalPlan| Some(input_cost(input)?.work); - self.startup_work = Some(match (&self.plan, operator, childrens) { + match (&self.plan, operator, childrens) { ( PlanImpl::SeqScan | PlanImpl::IndexScan(_) | PlanImpl::Values | PlanImpl::Dummy, _, @@ -355,7 +379,7 @@ impl PhysicalOption { Some( build .work - .saturating_add(rows_work(build.rows, build.width)) + .saturating_add(hash_build_work(&build)) .saturating_add(startup_work(right)?), ) }) @@ -420,11 +444,11 @@ impl PhysicalOption { (PlanImpl::ScalarQueryInit, _, Childrens::Twins { left, right }) => startup_work(left) .and_then(|left| work(right).map(|right| left.saturating_add(right))), _ => None, - }); + } } } -// Startup estimates are pass-local caches, not plan semantics or wire data. +// Cost caches and Greedy's join decision are pass-local, not plan semantics or wire data. impl PartialEq for PhysicalOption { fn eq(&self, other: &Self) -> bool { self.plan == other.plan @@ -1042,10 +1066,10 @@ mod tests { work: 5000, }; let output = rows_work(100, 2); - // Both inputs read once, the build side stored, every probe row looked up once. + // Both inputs read once, the build side stored and hashed, every probe row looked up once. assert_eq!( JoinImpl::Hash.work(&small, &large, 100, 2), - 50 + 5000 + rows_work(10, 1) + 1000 + output + 50 + 5000 + rows_work(10, 1) + 10 * HASH_BUILD_WORK + 1000 + output ); assert!( JoinImpl::Hash.work(&small, &large, 100, 2) diff --git a/src/serdes/physical_option.rs b/src/serdes/physical_option.rs index 8843f4f6..9d26fd31 100644 --- a/src/serdes/physical_option.rs +++ b/src/serdes/physical_option.rs @@ -84,8 +84,9 @@ impl ReferenceSerialization for PhysicalOption { plan: PlanImpl::decode(reader, context, reference_tables, arena)?, sort_option: SortOption::decode(reader, context, reference_tables, arena)?, row_count: RowCountOption::decode(reader, context, reference_tables, arena)?, - startup_work: None, - width: None, + cost: None, + lookup_work: None, + join_decided: false, }) } } @@ -100,6 +101,7 @@ mod tests { use super::*; use crate::expression::ScalarExpression; use crate::planner::operator::sort::SortField; + use crate::planner::operator::WorkCost; use crate::planner::{PlanArena, TableArenaCell}; #[cfg(all(not(feature = "rocksdb"), feature = "lmdb"))] use crate::storage::lmdb::LmdbTransaction as TestTransaction; @@ -144,7 +146,7 @@ mod tests { &mut arena, )?; assert_eq!(decoded, option); - assert_eq!(decoded.startup_work, None); + assert_eq!(decoded.cost, None); assert!(reader.is_empty()); } } @@ -189,7 +191,11 @@ mod tests { .row_count .encode(&mut expected, is_direct, &mut reference_tables, &arena)?; for cache in [None, Some(None), Some(Some(192))] { - option.startup_work = cache; + option.cost = cache.map(|startup| WorkCost { + width: 1, + startup, + total: startup, + }); let mut encoded = Vec::new(); let mut references = ReferenceTables::new(); option.encode(&mut encoded, is_direct, &mut references, &arena)?; @@ -210,7 +216,7 @@ mod tests { ndv: None } ); - assert_eq!(decoded.startup_work, None); + assert_eq!(decoded.cost, None); let SortOption::OrderBy { fields, ignore_prefix_len, diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 05b7d55e..d6a03898 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1640,11 +1640,14 @@ impl IndexImpl for PrimaryKeyIndexImpl { value: &[u8], params: &IndexImplParams, ) -> Result<(), DatabaseError> { - let tuple_id = TableCodec::decode_tuple_key(key, ¶ms.index_meta().pk_ty)?; + let tuple_id = params + .with_pk + .then(|| TableCodec::decode_tuple_key(key, ¶ms.index_meta().pk_ty)) + .transpose()?; TableCodec::decode_tuple_into( tuple, ¶ms.deserializers[..], - Some(tuple_id), + tuple_id, value, params.total_len, ) @@ -1675,7 +1678,7 @@ impl IndexImpl for PrimaryKeyIndexImpl { TableCodec::decode_tuple_into( tuple, ¶ms.deserializers[..], - Some(tuple_id.clone()), + params.with_pk.then(|| tuple_id.clone()), bytes.as_ref(), params.total_len, )?; diff --git a/tests/slt/join.slt b/tests/slt/join.slt index 9a328170..9075001c 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -758,3 +758,64 @@ drop table a statement ok drop table b + +# Greedy chooses the join implementation: a few outer rows probe a large unique key, +# a large outer side hashes a small input, and a cross join drives from the smaller side. +statement ok +create table jp_big(id int primary key, c1 int, c2 int); + +statement ok +copy jp_big from 'tests/data/row_20000.csv' ( DELIMITER '|' ); + +statement ok +create table jp_small(id int primary key, c1 int, c2 int); + +statement ok +insert into jp_small values (0,3,1),(1,6,1),(2,9,1),(3,12,1),(4,15,1),(5,18,1),(6,21,1),(7,24,1),(8,27,1),(9,30,1); + +statement ok +analyze table jp_big; + +statement ok +analyze table jp_small; + +query T +explain select jp_small.id, jp_big.c2 from jp_small join jp_big on jp_small.c1 = jp_big.id; +---- +Projection [jp_small.id, jp_big.c2] [Project => (Sort Option: Follow)] InnerJoinApply [MarkApply => (Sort Option: Follow)] TableScan jp_small -> [jp_small.id, jp_small.c1] [SeqScan => (Sort Option: None)] TableScan jp_big -> [jp_big.id, jp_big.c2] [IndexScan By pk_index => Probe ? => (Sort Option: OrderBy: (jp_big.id Asc Nulls Last) ignore_prefix_len: 0)] + +query II rowsort +select jp_small.id, jp_big.c2 from jp_small join jp_big on jp_small.c1 = jp_big.id; +---- +0 5 +1 8 +2 11 +3 14 +4 17 +5 20 +6 23 +7 26 +8 29 +9 32 + +query T +explain select jp_big.id, jp_small.c2 from jp_big join jp_small on jp_big.c1 = jp_small.id; +---- +Projection [jp_big.id, jp_small.c2] [Project => (Sort Option: Follow)] Inner Join On jp_small.id = jp_big.c1 [HashJoin => (Sort Option: None)] TableScan jp_small -> [jp_small.id, jp_small.c2] [SeqScan => (Sort Option: None)] TableScan jp_big -> [jp_big.id, jp_big.c1] [SeqScan => (Sort Option: None)] + +query T +explain select jp_big.id, jp_small.id from jp_big, jp_small; +---- +Projection [jp_big.id, jp_small.id] [Project => (Sort Option: Follow)] Cross Join Nothing [NestLoopJoin => (Sort Option: None)] TableScan jp_small -> [jp_small.id] [SeqScan => (Sort Option: None)] TableScan jp_big -> [jp_big.id] [SeqScan => (Sort Option: None)] + +# FORCE_NEST_LOOP_JOIN regions are still reordered, but only with nested loops. +query T +explain select /*+ FORCE_NEST_LOOP_JOIN */ jp_big.id, jp_small.c2 from jp_big join jp_small on jp_big.c1 = jp_small.id; +---- +Projection [jp_big.id, jp_small.c2] [Project => (Sort Option: Follow)] Inner Join On jp_small.id = jp_big.c1 [NestLoopJoin => (Sort Option: None)] TableScan jp_small -> [jp_small.id, jp_small.c2] [SeqScan => (Sort Option: None)] TableScan jp_big -> [jp_big.id, jp_big.c1] [SeqScan => (Sort Option: None)] + +statement ok +drop table jp_big + +statement ok +drop table jp_small From d782639642ab085e3970d17d0bab9de50a61fc0c Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 35/62] refactor(optimizer): simplify greedy join reorder for left-deep search Left-deep search only ever joins the current result with one remaining leaf, so the bushy-era structures go away: - Inputs split into `current` and `leaves`; no sorted insertion of merged inputs, no index tracking, no `split_at_mut`. - JoinKeys is a flat edge list between original inputs; `get(current, leaf)` orients keys from `current`. Used keys need no removal or rewrite. - The pair evaluation closure is an inline loop; `JoinCandidate` keeps one `leaf` index instead of `left/right`. - `flatten` computes each leaf's cost once and tracks the start input; `current`'s cost is computed once per merge. - `region_shape` becomes `region_inputs`, dropping the unused cross flag. Plans of all 732 select5 queries are unchanged with and without ANALYZE. Only exact ties between equal-cost hash inputs now keep the start input on the left; three where.slt explains are updated accordingly. --- .../rule/normalization/join_reorder.rs | 344 +++++++----------- tests/slt/where.slt | 6 +- 2 files changed, 141 insertions(+), 209 deletions(-) diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index e159da0c..ca0bda3e 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -32,7 +32,6 @@ use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; use crate::types::index::{IndexMeta, IndexMetaRef, IndexType}; use crate::types::tuple::Schema; use std::borrow::Borrow; -use std::collections::{btree_map::Entry, BTreeMap}; use std::mem::swap; pub(crate) struct GreedyJoinReorder; @@ -84,8 +83,7 @@ struct JoinCandidate { cross_join: bool, work: i128, rows: usize, - left: usize, - right: usize, + leaf: usize, swapped: bool, probe: bool, } @@ -303,25 +301,13 @@ fn can_expand(plan: &LogicalPlan, forced: bool) -> bool { (JoinType::Inner, JoinCondition::On { on, filter: None }) if !on.is_empty())) } -// Counts a region's inputs and reports whether it already contains a Cartesian product. -fn region_shape(plan: &LogicalPlan, forced: bool) -> (usize, bool) { +// Counts a region's inputs. +fn region_inputs(plan: &LogicalPlan, forced: bool) -> usize { if !can_expand(plan, forced) { - return (1, false); + return 1; } let (left, right) = plan.childrens.twins(); - let (left_inputs, left_cross) = region_shape(left, forced); - let (right_inputs, right_cross) = region_shape(right, forced); - let cross = matches!( - &plan.operator, - Operator::Join(JoinOperator { - join_type: JoinType::Cross, - .. - }) - ); - ( - left_inputs + right_inputs, - cross || left_cross || right_cross, - ) + region_inputs(left, forced) + region_inputs(right, forced) } // Collects input schemas and join keys @@ -425,17 +411,52 @@ fn inspect<'plan>( } } -fn flatten(plan: LogicalPlan, inputs: &mut Vec<(LogicalPlan, usize, u64)>, forced: bool) { +/// An input's cost, read once at its (default) rows when its estimate is incomplete. +fn input_cost( + input: &mut LogicalPlan, + rows: usize, + arena: &mut PlanArena, +) -> Result { + Ok(match input.estimated_cost(arena)? { + Some(cost) => cost, + None => { + let width = input.output_schema(arena).len(); + InputCost { + rows, + width, + work: rows_work(rows, width), + } + } + }) +} + +/// Collects a region's inputs with their costs; a leaf's cost never changes. `start` tracks +/// the smallest input (cheapest to read on ties, earliest on full ties). +fn flatten( + mut plan: LogicalPlan, + leaves: &mut Vec<(LogicalPlan, InputCost, u64)>, + start: &mut usize, + forced: bool, + arena: &mut PlanArena, +) -> Result<(), DatabaseError> { if can_expand(&plan, forced) { let (left, right) = plan.childrens.pop_twins(); - flatten(left, inputs, forced); - flatten(right, inputs, forced); + flatten(left, leaves, start, forced, arena)?; + flatten(right, leaves, start, forced, arena)?; } else { // Without statistics an input is assumed to have the default rows (RFC 0001 §3.9). let rows = plan.estimated_rows().unwrap_or(DEFAULT_TABLE_ROWS); - let mask = 1u64 << inputs.len(); - inputs.push((plan, rows, mask)); + let cost = input_cost(&mut plan, rows, arena)?; + if leaves + .get(*start) + .is_some_and(|(_, best, _)| (cost.rows, cost.work) < (best.rows, best.work)) + { + *start = leaves.len(); + } + let mask = 1u64 << leaves.len(); + leaves.push((plan, cost, mask)); } + Ok(()) } fn key_in_schema(expr: ExprRef, schema: &Schema, arena: &PlanArena) -> Result { @@ -446,77 +467,26 @@ fn key_in_schema(expr: ExprRef, schema: &Schema, arena: &PlanArena) -> Result, - // Link IDs are indices here; consumed lists leave empty slots. - on_keys: Vec>, -} +struct JoinKeys(Vec<(u64, u64, ExprRef, ExprRef)>); impl JoinKeys { - fn insert(&mut self, left: u64, right: u64, mut exprs: (ExprRef, ExprRef)) { - if left > right { - swap(&mut exprs.0, &mut exprs.1); - } - match self.links.entry((left, right)) { - Entry::Occupied(entry) => self.on_keys[*entry.get()].push(exprs), - Entry::Vacant(entry) => { - let id = self.on_keys.len(); - self.on_keys.push(vec![exprs]); - entry.insert(id); - self.links.insert((right, left), id); - } - } + fn insert(&mut self, left: u64, right: u64, (l, r): (ExprRef, ExprRef)) { + self.0.push((left, right, l, r)); } - // Both directions share one canonically oriented list; take aligns the selected ON. - fn get(&self, left: u64, right: u64) -> impl Iterator + '_ { - self.links - .get(&(left, right)) - .into_iter() - .flat_map(|&id| self.on_keys[id].iter().copied()) - } - - fn take(&mut self, left: u64, right: u64) -> Vec<(ExprRef, ExprRef)> { - let Some(id) = self.links.remove(&(left, right)) else { - return Vec::new(); - }; - self.links.remove(&(right, left)); - let mut on = std::mem::take(&mut self.on_keys[id]); - if left > right { - for (l, r) in &mut on { - swap(l, r); - } - } - on - } - - fn rewrite(&mut self, left: u64, right: u64) { - // take has already consumed the internal edge; only external links remain. - debug_assert!(!self.links.contains_key(&(left, right))); - let merged = left | right; - for old in [left, right] { - loop { - let link = self - .links - .extract_if((old, 0)..=(old, u64::MAX), |_, _| true) - .next(); - let Some(((_, other), id)) = link else { break }; - self.links.remove(&(other, old)); - if (old > other) != (merged > other) { - for (l, r) in &mut self.on_keys[id] { - swap(l, r); - } - } - if let Some(&existing) = self.links.get(&(merged, other)) { - let mut extra = std::mem::take(&mut self.on_keys[id]); - self.on_keys[existing].append(&mut extra); - } else { - self.links.insert((merged, other), id); - self.links.insert((other, merged), id); - } + /// Keys between the joined inputs `current` and the input `leaf`, oriented from `current`. + fn get(&self, current: u64, leaf: u64) -> impl Iterator + '_ { + self.0.iter().filter_map(move |&(left, right, l, r)| { + if left & current != 0 && right == leaf { + Some((l, r)) + } else if right & current != 0 && left == leaf { + Some((r, l)) + } else { + None } - } + }) } } @@ -563,7 +533,7 @@ impl NormalizationRule for GreedyJoinReorder { if !can_expand(plan, forced) { return Ok(false); } - let (input_count, _) = region_shape(plan, forced); + let input_count = region_inputs(plan, forced); if input_count > u64::BITS as usize { return Ok(false); } @@ -578,53 +548,37 @@ impl NormalizationRule for GreedyJoinReorder { for (input, schema) in schemas.iter().enumerate() { positions.extend((0..schema.len()).map(|position| (1u64 << input, position))); } - let mut inputs = Vec::with_capacity(schemas.len()); - flatten(plan.take(), &mut inputs, forced); + let mut leaves = Vec::with_capacity(schemas.len()); + // Left-deep: start from the smallest input, then join one remaining leaf per step, so + // every leaf can still be probed (RFC 0001 §3.2). + let mut start = 0; + flatten(plan.take(), &mut leaves, &mut start, forced, arena)?; let mut probe_keys = Vec::new(); - // Left-deep: start from the smallest input (cheapest to read on ties), then add one - // remaining input per step, so every input stays a leaf until it joins and can still be - // probed (RFC 0001 §3.2). - let mut current = 0; - let mut start = (usize::MAX, usize::MAX); - for (i, (input, rows, _)) in inputs.iter_mut().enumerate() { - let work = input - .estimated_cost(arena)? - .map_or(usize::MAX, |cost| cost.work); - if (*rows, work) < start { - (start, current) = ((*rows, work), i); - } - } - while inputs.len() > 1 { - let mut best: Option = None; - // The index chosen for `best` when it probes, kept to build the probe without a re-search. - let mut best_probe: Option = None; + let (mut current, mut current_cost, mut current_mask) = leaves.remove(start); + while !leaves.is_empty() { + let mut best: Option<(JoinCandidate, Option)> = None; // A result of at most one row may take a Cartesian product before an equijoin. - let single = inputs[current].1 <= 1; - let mut evaluate = |inputs: &mut [(LogicalPlan, usize, u64)], - i: usize, - j: usize| - -> Result<(), DatabaseError> { - let (prefix, suffix) = inputs.split_at_mut(j); - let (left, left_rows, left_mask) = &mut prefix[i]; - let (right, right_rows, right_mask) = &mut suffix[0]; - let candidate_keys = || keys.get(*left_mask, *right_mask); - // Left-deep evaluates each pair once, so the estimate is not cached. + let single = current_cost.rows <= 1; + for (k, (leaf, leaf_cost, leaf_mask)) in leaves.iter_mut().enumerate() { + let leaf_cost = *leaf_cost; + // Keys run from `current` to `leaf`, matching the unswapped join sides. + let candidate_keys = || keys.get(current_mask, *leaf_mask); let product = { - let ndvs = column_ndv(left, right, candidate_keys(), loader, arena)?; - let left_stats = index_statistics(left); - let right_stats = index_statistics(right); - let left_schema = left.output_schema(arena); - let right_schema = right.output_schema(arena); + let ndvs = column_ndv(¤t, leaf, candidate_keys(), loader, arena)?; + let current_stats = index_statistics(¤t); + let leaf_stats = index_statistics(leaf); + let current_schema = current.output_schema(arena); + let leaf_schema = leaf.output_schema(arena); join_rows( &JoinInputStatistics { - rows: *left_rows, - schema: left_schema, - index: left_stats.map(|(index, ndv)| (arena.index(index), ndv)), + rows: current_cost.rows, + schema: current_schema, + index: current_stats.map(|(index, ndv)| (arena.index(index), ndv)), }, &JoinInputStatistics { - rows: *right_rows, - schema: right_schema, - index: right_stats.map(|(index, ndv)| (arena.index(index), ndv)), + rows: leaf_cost.rows, + schema: leaf_schema, + index: leaf_stats.map(|(index, ndv)| (arena.index(index), ndv)), }, &candidate_keys, ndvs, @@ -633,26 +587,6 @@ impl NormalizationRule for GreedyJoinReorder { }; let connected = candidate_keys().next().is_some(); let hash = connected && !forced; - // An input without a complete estimate is read once at its (default) rows. - let input_cost = |input: &mut LogicalPlan, rows: usize, arena: &mut PlanArena| { - Ok::<_, DatabaseError>(match input.estimated_cost(arena)? { - Some(cost) => cost, - None => { - let width = input.output_schema(arena).len(); - InputCost { - rows, - width, - work: rows_work(rows, width), - } - } - }) - }; - let (left_cost, right_cost) = ( - input_cost(left, *left_rows, arena)?, - input_cost(right, *right_rows, arena)?, - ); - let nest_loop_swapped = !hash && nest_loop_swap(&left_cost, &right_cost); - let out_rows = product; // Every input is read once in any order, so only this step's work is compared. // A probe never reads its inner input, so its step can be negative. let step = @@ -661,24 +595,28 @@ impl NormalizationRule for GreedyJoinReorder { - outer.work as i128 - inner.work as i128 }; - // (work, rows, swapped, probe index); the left input is the hash build side and the - // nested loop / probe outer side. - let shape = if hash { - let keep = step(JoinImpl::Hash, &left_cost, &right_cost, out_rows); - let swap = step(JoinImpl::Hash, &right_cost, &left_cost, out_rows); + // (work, rows, swapped, probe index). Unswapped, `current` is the hash build side + // and the nested loop / probe outer side. + let (work, rows, swapped, probe) = if hash { + let keep = step(JoinImpl::Hash, ¤t_cost, &leaf_cost, product); + let swap = step(JoinImpl::Hash, &leaf_cost, ¤t_cost, product); let mut shape = (keep.min(swap), product, swap < keep, None); + // Only a leaf can be probed: `leaf` always, `current` before its first join. for swapped in [false, true] { let (inner, outer_cost, inner_cost) = if swapped { - (&*left, &right_cost, &left_cost) + (¤t, &leaf_cost, ¤t_cost) } else { - (&*right, &left_cost, &right_cost) + (&*leaf, ¤t_cost, &leaf_cost) }; - // Keys run from the outer value to the probed column. - let flip = (*left_mask > *right_mask) != swapped; + // Probe keys run from the outer value to the probed column. probe_keys.clear(); - probe_keys.extend( - candidate_keys().map(|(l, r)| if flip { (r, l) } else { (l, r) }), - ); + probe_keys.extend(candidate_keys().map(|(c, l)| { + if swapped { + (l, c) + } else { + (c, l) + } + })); let key_count = probe_keys.len(); let Some(index) = probe_index(inner, &mut probe_keys, key_count, arena, loader)? @@ -687,10 +625,9 @@ impl NormalizationRule for GreedyJoinReorder { }; // A unique probe emits at most one row per outer row. let rows = product.min(outer_cost.rows); + let lookup_work = index.lookup_work; let work = step( - JoinImpl::IndexProbe { - lookup_work: index.lookup_work, - }, + JoinImpl::IndexProbe { lookup_work }, outer_cost, inner_cost, rows, @@ -701,52 +638,51 @@ impl NormalizationRule for GreedyJoinReorder { } shape } else { - let (outer, inner) = if nest_loop_swapped { - (&right_cost, &left_cost) + let swapped = nest_loop_swap(¤t_cost, &leaf_cost); + let (outer, inner) = if swapped { + (&leaf_cost, ¤t_cost) } else { - (&left_cost, &right_cost) + (¤t_cost, &leaf_cost) }; - let work = step(JoinImpl::NestLoop { full: false }, outer, inner, out_rows); - (work, product, nest_loop_swapped, None) + let work = step(JoinImpl::NestLoop { full: false }, outer, inner, product); + (work, product, swapped, None) }; - let (work, rows, swapped, probe) = shape; let candidate = JoinCandidate { cross_join: !connected && !single, work, rows, - left: i, - right: j, + leaf: k, swapped, probe: probe.is_some(), }; - if best.is_none_or(|previous| candidate < previous) { - best = Some(candidate); - best_probe = probe; - } - Ok(()) - }; - for other in 0..inputs.len() { - if other != current { - evaluate(&mut inputs, current.min(other), current.max(other))?; + if best + .as_ref() + .is_none_or(|(previous, _)| candidate < *previous) + { + best = Some((candidate, probe)); } } - let Some(JoinCandidate { - rows: product, - left: i, - right: j, - swapped, - .. - }) = best - else { - return Ok(false); + // SAFETY: `leaves` is not empty, so the loop above chose a candidate. + let ( + JoinCandidate { + rows: product, + leaf: k, + swapped, + .. + }, + probe, + ) = best.unwrap(); + let (leaf, _, leaf_mask) = leaves.remove(k); + // Used keys need no removal: both ends are now in `current`, so no later leaf matches. + let mut on: Vec<_> = keys.get(current_mask, leaf_mask).collect(); + let (mut left, mut right, right_mask) = if swapped { + for (c, l) in &mut on { + swap(c, l); + } + (leaf, current, current_mask) + } else { + (current, leaf, leaf_mask) }; - let mut right = inputs.remove(j); - let mut left = inputs.remove(i); - if swapped { - swap(&mut left, &mut right); - } - let (mut left, _, left_mask) = left; - let (mut right, _, right_mask) = right; let left_schema = left.output_schema(arena); let right_schema = right.output_schema(arena); for (input, position) in &mut positions { @@ -754,7 +690,6 @@ impl NormalizationRule for GreedyJoinReorder { *position += left_schema.len(); } } - let mut on = keys.take(left_mask, right_mask); for (l, r) in &mut on { *l = l.clone_expression(arena)?; *r = r.clone_expression(arena)?; @@ -769,7 +704,7 @@ impl NormalizationRule for GreedyJoinReorder { } else { RowCountOption::Unknown }; - let join = if let Some(mut index) = best_probe { + current = if let Some(mut index) = probe { // Probe values were chosen before binding; they come from the outer (left) input. for value in &mut index.probe { *value = value.clone_expression(arena)?; @@ -792,14 +727,11 @@ impl NormalizationRule for GreedyJoinReorder { join.physical_option = Some(option); join }; - keys.rewrite(left_mask, right_mask); - inputs.insert(i, (join, product, left_mask | right_mask)); - current = i; + current_cost = input_cost(&mut current, product, arena)?; + current_mask |= leaf_mask; } - // SAFETY: At this point, there should be exactly one plan left in the inputs vector, which is the final join result. - let (join, _, _) = inputs.pop().unwrap(); *plan = project_join_output( - join, + current, original_schema, positions.iter().map(|(_, position)| *position), arena, diff --git a/tests/slt/where.slt b/tests/slt/where.slt index 26c20549..0331c930 100644 --- a/tests/slt/where.slt +++ b/tests/slt/where.slt @@ -871,7 +871,7 @@ analyze table cp_c; query T explain select cp_a.id, cp_b.id from cp_a join cp_b on cp_a.c1 = cp_b.c1 where cp_a.c1 = 4; ---- -Projection [cp_a.id, cp_b.id] [Project => (Sort Option: Follow)] Inner Join On cp_a.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] Filter (cp_a.c1 = 4), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_a -> [cp_a.id, cp_a.c1] [SeqScan => (Sort Option: None)] TableScan cp_b -> [cp_b.id, cp_b.c1] [IndexScan By cp_b_c1 => 4 => (Sort Option: OrderBy: (cp_b.c1 Asc Nulls Last) ignore_prefix_len: 0)] +Projection [cp_a.id, cp_b.id] [Project => (Sort Option: Follow)] Inner Join On cp_b.c1 = cp_a.c1 [HashJoin => (Sort Option: None)] TableScan cp_b -> [cp_b.id, cp_b.c1] [IndexScan By cp_b_c1 => 4 => (Sort Option: OrderBy: (cp_b.c1 Asc Nulls Last) ignore_prefix_len: 0)] Filter (cp_a.c1 = 4), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_a -> [cp_a.id, cp_a.c1] [SeqScan => (Sort Option: None)] query II select cp_a.id, cp_b.id from cp_a join cp_b on cp_a.c1 = cp_b.c1 where cp_a.c1 = 4; @@ -881,7 +881,7 @@ select cp_a.id, cp_b.id from cp_a join cp_b on cp_a.c1 = cp_b.c1 where cp_a.c1 = query T explain select cp_a.id, cp_b.id from cp_a, cp_b where cp_a.c1 = cp_b.c1 and cp_a.c1 = 4; ---- -Projection [cp_a.id, cp_b.id] [Project => (Sort Option: Follow)] Inner Join On cp_a.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] Filter (cp_a.c1 = 4), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_a -> [cp_a.id, cp_a.c1] [SeqScan => (Sort Option: None)] TableScan cp_b -> [cp_b.id, cp_b.c1] [IndexScan By cp_b_c1 => 4 => (Sort Option: OrderBy: (cp_b.c1 Asc Nulls Last) ignore_prefix_len: 0)] +Projection [cp_a.id, cp_b.id] [Project => (Sort Option: Follow)] Inner Join On cp_b.c1 = cp_a.c1 [HashJoin => (Sort Option: None)] TableScan cp_b -> [cp_b.id, cp_b.c1] [IndexScan By cp_b_c1 => 4 => (Sort Option: OrderBy: (cp_b.c1 Asc Nulls Last) ignore_prefix_len: 0)] Filter (cp_a.c1 = 4), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_a -> [cp_a.id, cp_a.c1] [SeqScan => (Sort Option: None)] query II select cp_a.id, cp_b.id from cp_a, cp_b where cp_a.c1 = cp_b.c1 and cp_a.c1 = 4; @@ -892,7 +892,7 @@ select cp_a.id, cp_b.id from cp_a, cp_b where cp_a.c1 = cp_b.c1 and cp_a.c1 = 4; query T explain select cp_a.id, cp_b.id, cp_c.id from cp_a, cp_b, cp_c where cp_a.c1 = cp_b.c1 and cp_b.c1 = cp_c.c1 and cp_a.c1 = 4; ---- -Projection [cp_a.id, cp_b.id, cp_c.id] [Project => (Sort Option: Follow)] Inner Join On cp_c.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] TableScan cp_c -> [cp_c.id, cp_c.c1] [IndexScan By cp_c_c1 => 4 => (Sort Option: OrderBy: (cp_c.c1 Asc Nulls Last) ignore_prefix_len: 0)] Inner Join On cp_a.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] Filter (cp_a.c1 = 4), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_a -> [cp_a.id, cp_a.c1] [SeqScan => (Sort Option: None)] TableScan cp_b -> [cp_b.id, cp_b.c1] [IndexScan By cp_b_c1 => 4 => (Sort Option: OrderBy: (cp_b.c1 Asc Nulls Last) ignore_prefix_len: 0)] +Projection [cp_a.id, cp_b.id, cp_c.id] [Project => (Sort Option: Follow)] Inner Join On cp_c.c1 = cp_b.c1 [HashJoin => (Sort Option: None)] TableScan cp_c -> [cp_c.id, cp_c.c1] [IndexScan By cp_c_c1 => 4 => (Sort Option: OrderBy: (cp_c.c1 Asc Nulls Last) ignore_prefix_len: 0)] Inner Join On cp_b.c1 = cp_a.c1 [HashJoin => (Sort Option: None)] TableScan cp_b -> [cp_b.id, cp_b.c1] [IndexScan By cp_b_c1 => 4 => (Sort Option: OrderBy: (cp_b.c1 Asc Nulls Last) ignore_prefix_len: 0)] Filter (cp_a.c1 = 4), Is Having: false [Filter => (Sort Option: Follow)] TableScan cp_a -> [cp_a.id, cp_a.c1] [SeqScan => (Sort Option: None)] query III select cp_a.id, cp_b.id, cp_c.id from cp_a, cp_b, cp_c where cp_a.c1 = cp_b.c1 and cp_b.c1 = cp_c.c1 and cp_a.c1 = 4; From 28eca596c30326e18ea3f6ca4f705b14aece99f8 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 36/62] perf(optimizer): index join keys by input and compare join columns by ref - JoinKeys stores each edge once per endpoint in one Vec sorted by endpoint, so a leaf's keys are found by one binary search instead of scanning every edge; a merged leaf's entries are drained. Key order is unchanged. - Greedy's BindPosition and extract_join_keys compare ColumnRefs directly: ON keys keep the refs their inputs output. - nullable_for_join only makes a column nullable: an inner join no longer marks a nullable column NOT NULL (and no longer clones it to do so). --- .../rule/normalization/join_reorder.rs | 47 ++++++++++++------- src/planner/arena.rs | 6 +-- src/planner/operator/join.rs | 16 ++----- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index ca0bda3e..c94c44f0 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -33,6 +33,7 @@ use crate::types::index::{IndexMeta, IndexMetaRef, IndexType}; use crate::types::tuple::Schema; use std::borrow::Borrow; use std::mem::swap; +use std::ops::Range; pub(crate) struct GreedyJoinReorder; @@ -467,26 +468,40 @@ fn key_in_schema(expr: ExprRef, schema: &Schema, arena: &PlanArena) -> Result); impl JoinKeys { fn insert(&mut self, left: u64, right: u64, (l, r): (ExprRef, ExprRef)) { - self.0.push((left, right, l, r)); + for entry in [(left, right, l, r), (right, left, r, l)] { + let at = self.0.partition_point(|e| e.0 <= entry.0); + self.0.insert(at, entry); + } + } + + fn range(&self, leaf: u64) -> Range { + let start = self.0.partition_point(|e| e.0 < leaf); + let len = self.0[start..].iter().take_while(|e| e.0 == leaf).count(); + start..start + len } - /// Keys between the joined inputs `current` and the input `leaf`, oriented from `current`. fn get(&self, current: u64, leaf: u64) -> impl Iterator + '_ { - self.0.iter().filter_map(move |&(left, right, l, r)| { - if left & current != 0 && right == leaf { - Some((l, r)) - } else if right & current != 0 && left == leaf { - Some((r, l)) - } else { - None - } - }) + self.0[self.range(leaf)] + .iter() + .filter(move |e| e.1 & current != 0) + .map(|&(_, _, leaf_key, other_key)| (other_key, leaf_key)) + } + + fn take(&mut self, current: u64, leaf: u64) -> Vec<(ExprRef, ExprRef)> { + let range = self.range(leaf); + self.0 + .drain(range) + .filter(|e| e.1 & current != 0) + .map(|(_, _, leaf_key, other_key)| (other_key, leaf_key)) + .collect() } } @@ -497,12 +512,13 @@ impl ExprVisitorMut for BindPosition<'_> { &mut self, column: &mut ColumnRef, position: &mut usize, - arena: &mut A, + _: &mut A, ) -> Result<(), DatabaseError> { + // Join keys keep the refs their inputs output. *position = self .0 .iter() - .position(|candidate| arena.same_column(*candidate, *column)) + .position(|candidate| candidate == column) .ok_or(DatabaseError::SourceNotFound)?; Ok(()) } @@ -673,8 +689,7 @@ impl NormalizationRule for GreedyJoinReorder { probe, ) = best.unwrap(); let (leaf, _, leaf_mask) = leaves.remove(k); - // Used keys need no removal: both ends are now in `current`, so no later leaf matches. - let mut on: Vec<_> = keys.get(current_mask, leaf_mask).collect(); + let mut on = keys.take(current_mask, leaf_mask); let (mut left, mut right, right_mask) = if swapped { for (c, l) in &mut on { swap(c, l); diff --git a/src/planner/arena.rs b/src/planner/arena.rs index 656d338c..87c5bf3f 100644 --- a/src/planner/arena.rs +++ b/src/planner/arena.rs @@ -893,13 +893,13 @@ impl<'a> PlanArena<'a> { column: ColumnRef, nullable: bool, ) -> Option { + // A join can only make a column nullable; it never removes existing NULLs. let source = self.column(column); - if source.nullable() == nullable { + if !nullable || source.nullable() { return None; } - // FIXME let mut joined = source.clone(); - joined.set_nullable(nullable); + joined.set_nullable(true); joined.set_in_join(true); Some(self.alloc_column(joined)) } diff --git a/src/planner/operator/join.rs b/src/planner/operator/join.rs index 54954947..6cb5cb17 100644 --- a/src/planner/operator/join.rs +++ b/src/planner/operator/join.rs @@ -152,14 +152,9 @@ pub(crate) fn extract_join_keys( right_schema: &Schema, arena: &mut PlanArena, ) -> Result<(), DatabaseError> { - let fn_contains = |schema: &Schema, column: ColumnRef| { - let summary = arena.column(column).summary(); - schema - .iter() - .any(|candidate| arena.column(*candidate).summary() == summary) - }; + // ON keys keep the refs their inputs output, so refs identify the side. let fn_or_contains = - |column: ColumnRef| fn_contains(left_schema, column) || fn_contains(right_schema, column); + |column: ColumnRef| left_schema.contains(&column) || right_schema.contains(&column); let expr = expr.unpack_alias(arena); match arena.expression(expr) { @@ -181,12 +176,9 @@ pub(crate) fn extract_join_keys( ScalarExpression::ColumnRef { column: r, .. }, ) => { // reorder left and right joins keys to pattern: (left, right) - let key = if fn_contains(left_schema, *l) - && fn_contains(right_schema, *r) - { + let key = if left_schema.contains(l) && right_schema.contains(r) { Some((*left_expr, *right_expr)) - } else if fn_contains(left_schema, *r) && fn_contains(right_schema, *l) - { + } else if left_schema.contains(r) && right_schema.contains(l) { Some((*right_expr, *left_expr)) } else { if fn_or_contains(*l) || fn_or_contains(*r) { From b8970fcade21e5ef414ef34616edd06340615b00 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:39 +0800 Subject: [PATCH 37/62] perf(optimizer): push predicates through nested joins in one pass - PushPredicateThroughJoin pushes a filter recursively through nested joins (push_through_join / push_into_child) instead of one level per fixpoint iteration. - ExprVisitor::visit_column_ref and all_referenced_columns now carry the column position, so join filter classification checks the slot at that position before falling back to a linear contains. - PropagateFilterConstants skips the propagation loop when the predicate has no column = column conjunct. --- src/db.rs | 2 +- src/expression/eq_col.rs | 2 +- src/expression/mod.rs | 16 +- src/expression/visitor.rs | 10 +- src/optimizer/plan_utils.rs | 2 +- .../rule/normalization/column_pruning.rs | 1 + .../normalization/constant_propagation.rs | 23 +- .../rule/normalization/elimination.rs | 2 +- .../rule/normalization/guard_mark_apply.rs | 4 +- .../rule/normalization/join_reorder.rs | 2 +- .../rule/normalization/parameterized_index.rs | 2 +- .../rule/normalization/pushdown_predicates.rs | 311 +++++++++--------- src/planner/operator/join.rs | 10 +- src/planner/operator/mod.rs | 44 ++- 14 files changed, 235 insertions(+), 196 deletions(-) diff --git a/src/db.rs b/src/db.rs index f679fb8e..97a18e7a 100644 --- a/src/db.rs +++ b/src/db.rs @@ -2086,7 +2086,7 @@ pub(crate) mod test { unreachable!("expected join filter"); }; let mut referenced_columns = Vec::new(); - filter.all_referenced_columns(&source_plan_arena, |_, column| { + filter.all_referenced_columns(&source_plan_arena, |_, column, _| { referenced_columns.push(*column); true })?; diff --git a/src/expression/eq_col.rs b/src/expression/eq_col.rs index a22e44f3..14e3b379 100644 --- a/src/expression/eq_col.rs +++ b/src/expression/eq_col.rs @@ -126,7 +126,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, A> { Ok(()) } - fn visit_column_ref(&mut self, lhs: &ColumnRef) -> Result<(), DatabaseError> { + fn visit_column_ref(&mut self, lhs: &ColumnRef, _: usize) -> Result<(), DatabaseError> { self.equal = matches!( self.rhs(), ScalarExpression::ColumnRef { column: rhs, .. } diff --git a/src/expression/mod.rs b/src/expression/mod.rs index ba5dd129..134cd964 100644 --- a/src/expression/mod.rs +++ b/src/expression/mod.rs @@ -858,7 +858,7 @@ impl ExprRef { Ok(()) } - fn visit_column_ref(&mut self, col: &ColumnRef) -> Result<(), DatabaseError> { + fn visit_column_ref(&mut self, col: &ColumnRef, _: usize) -> Result<(), DatabaseError> { self.any = (self.f)(self.arena, col); Ok(()) } @@ -876,7 +876,7 @@ impl ExprRef { pub fn all_referenced_columns( self, arena: &(dyn MetaArena + '_), - mut predicate: impl FnMut(&(dyn MetaArena + '_), &ColumnRef) -> bool, + mut predicate: impl FnMut(&(dyn MetaArena + '_), &ColumnRef, usize) -> bool, ) -> Result { struct ColumnRefVisitor<'a, 'arena, F> { f: &'a mut F, @@ -884,8 +884,8 @@ impl ExprRef { arena: &'a (dyn MetaArena + 'arena), } - impl bool> ExprVisitor - for ColumnRefVisitor<'_, '_, F> + impl bool> + ExprVisitor for ColumnRefVisitor<'_, '_, F> { fn visit( &mut self, @@ -898,8 +898,12 @@ impl ExprRef { Ok(()) } - fn visit_column_ref(&mut self, col: &ColumnRef) -> Result<(), DatabaseError> { - self.all = (self.f)(self.arena, col); + fn visit_column_ref( + &mut self, + col: &ColumnRef, + position: usize, + ) -> Result<(), DatabaseError> { + self.all = (self.f)(self.arena, col, position); Ok(()) } } diff --git a/src/expression/visitor.rs b/src/expression/visitor.rs index 01aad32a..a1a81f9f 100644 --- a/src/expression/visitor.rs +++ b/src/expression/visitor.rs @@ -79,7 +79,11 @@ pub trait ExprVisitor: Sized { fn visit_constant(&mut self, _value: &DataValue) -> Result<(), DatabaseError> { Ok(()) } - fn visit_column_ref(&mut self, _column: &ColumnRef) -> Result<(), DatabaseError> { + fn visit_column_ref( + &mut self, + _column: &ColumnRef, + _position: usize, + ) -> Result<(), DatabaseError> { Ok(()) } fn visit_alias( @@ -322,7 +326,9 @@ pub fn walk_expr>( ScalarExpression::OuterParam { id, ty } => visitor.visit_outer_param(*id, ty, arena), ScalarExpression::OuterValue { id, ty } => visitor.visit_outer_value(*id, ty, arena), ScalarExpression::InitValue { id, ty } => visitor.visit_init_value(*id, ty, arena), - ScalarExpression::ColumnRef { column, .. } => visitor.visit_column_ref(column), + ScalarExpression::ColumnRef { column, position } => { + visitor.visit_column_ref(column, *position) + } ScalarExpression::Alias { expr, alias } => visitor.visit_alias(*expr, alias, arena), ScalarExpression::TypeCast { expr, diff --git a/src/optimizer/plan_utils.rs b/src/optimizer/plan_utils.rs index a8974c37..16f54bf7 100644 --- a/src/optimizer/plan_utils.rs +++ b/src/optimizer/plan_utils.rs @@ -227,7 +227,7 @@ impl ExprVisitor for ScanColumnVisitor<'_, '_, A> { )) } - fn visit_column_ref(&mut self, column: &ColumnRef) -> Result<(), DatabaseError> { + fn visit_column_ref(&mut self, column: &ColumnRef, _: usize) -> Result<(), DatabaseError> { self.column = Some(*column); Ok(()) } diff --git a/src/optimizer/rule/normalization/column_pruning.rs b/src/optimizer/rule/normalization/column_pruning.rs index 244716ee..ed26dfc3 100644 --- a/src/optimizer/rule/normalization/column_pruning.rs +++ b/src/optimizer/rule/normalization/column_pruning.rs @@ -98,6 +98,7 @@ impl ExprVisitor for ReferencedColumnCollector<'_, '_> { fn visit_column_ref( &mut self, column: &crate::catalog::ColumnRef, + _: usize, ) -> Result<(), DatabaseError> { self.referenced_columns.insert(*column, self.arena); Ok(()) diff --git a/src/optimizer/rule/normalization/constant_propagation.rs b/src/optimizer/rule/normalization/constant_propagation.rs index ea271279..e47c4239 100644 --- a/src/optimizer/rule/normalization/constant_propagation.rs +++ b/src/optimizer/rule/normalization/constant_propagation.rs @@ -96,13 +96,22 @@ fn eq_operands(expr: ExprRef, arena: &PlanArena<'_>) -> Option<(ExprRef, ExprRef } } -fn as_fact(expr: ExprRef, arena: &PlanArena<'_>, left: bool) -> Option { +fn as_fact( + expr: ExprRef, + arena: &PlanArena<'_>, + left: bool, + has_col_connection: &mut bool, +) -> Option { let (lhs, rhs) = eq_operands(expr, arena)?; let ((column, position), constant) = match (table_column(lhs, arena), table_column(rhs, arena)) { (Some(column), None) => (column, rhs.unpack_alias(arena)), (None, Some(column)) => (column, lhs.unpack_alias(arena)), - _ => return None, + (Some(_), Some(_)) => { + *has_col_connection = true; + return None; + } + (None, None) => return None, }; matches!(arena.expression(constant), ScalarExpression::Constant(value) if !value.is_null()) .then_some(Fact { @@ -171,10 +180,12 @@ impl NormalizationRule for PropagateFilterConstants { let predicate = filter.predicate; let mut facts = Vec::new(); + let mut has_col_connection = false; for_each_conjunct(predicate, arena, &mut |conjunct| { - facts.extend(as_fact(conjunct, arena, false)); + facts.extend(as_fact(conjunct, arena, false, &mut has_col_connection)); }); - if facts.is_empty() { + // Without both a constant and a column pair nothing can be derived. + if facts.is_empty() || !has_col_connection { return Ok(false); } @@ -225,7 +236,7 @@ fn collect_facts(plan: &LogicalPlan, arena: &PlanArena<'_>, left: bool, facts: & match (&plan.operator, plan.childrens.as_ref()) { (Operator::Filter(filter), Childrens::Only(child)) => { for_each_conjunct(filter.predicate, arena, &mut |conjunct| { - facts.extend(as_fact(conjunct, arena, left)); + facts.extend(as_fact(conjunct, arena, left, &mut false)); }); collect_facts(child, arena, left, facts); } @@ -278,7 +289,7 @@ impl NormalizationRule for PropagateJoinConstants { let preserved_columns = preserved.output_schema(arena); for_each_conjunct(join_filter, arena, &mut |conjunct| { facts.extend( - as_fact(conjunct, arena, preserved_left) + as_fact(conjunct, arena, preserved_left, &mut false) .filter(|fact| preserved_columns.contains(&fact.column)), ); }); diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index 891bfcb3..05079832 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -437,7 +437,7 @@ pub(crate) fn apply_scan_order_hint( ScanOrderHint::SortFields(fields) => fields[index].expr, ScanOrderHint::GroupBy(groupby_exprs) => groupby_exprs[index], }; - if !expr.all_referenced_columns(arena, |arena, column| { + if !expr.all_referenced_columns(arena, |arena, column, _| { scan_op .columns .iter() diff --git a/src/optimizer/rule/normalization/guard_mark_apply.rs b/src/optimizer/rule/normalization/guard_mark_apply.rs index 60f6af23..7ba5e84a 100644 --- a/src/optimizer/rule/normalization/guard_mark_apply.rs +++ b/src/optimizer/rule/normalization/guard_mark_apply.rs @@ -89,7 +89,7 @@ impl NormalizationRule for GuardMarkApply { unreachable!() }; for expr in op.predicates.iter().chain(op.parameterized_probe.iter()) { - expr.all_referenced_columns(arena, |_, column| { + expr.all_referenced_columns(arena, |_, column, _| { columns.push(*column); true })?; @@ -137,7 +137,7 @@ impl NormalizationRule for GuardMarkApply { } if let Some(guard) = guard { let schema = left.output_schema(arena); - if guard.all_referenced_columns(arena, |arena, column| { + if guard.all_referenced_columns(arena, |arena, column, _| { !unavailable .iter() .any(|marker| arena.same_column(*marker, *column)) diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index c94c44f0..322db6d5 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -461,7 +461,7 @@ fn flatten( } fn key_in_schema(expr: ExprRef, schema: &Schema, arena: &PlanArena) -> Result { - expr.all_referenced_columns(arena, |arena, column| { + expr.all_referenced_columns(arena, |arena, column, _| { schema .iter() .any(|candidate| arena.same_column(*candidate, *column)) diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 563c49d1..67494192 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -152,7 +152,7 @@ fn extract_parameterized_probe_side( if !schema_contains_column(right_schema, &right_column, arena) { return Ok(None); } - if !left_expr.all_referenced_columns(arena, |arena, candidate| { + if !left_expr.all_referenced_columns(arena, |arena, candidate, _| { schema_contains_column(left_schema, candidate, arena) })? { return Ok(None); diff --git a/src/optimizer/rule/normalization/pushdown_predicates.rs b/src/optimizer/rule/normalization/pushdown_predicates.rs index ad949050..076c5fca 100644 --- a/src/optimizer/rule/normalization/pushdown_predicates.rs +++ b/src/optimizer/rule/normalization/pushdown_predicates.rs @@ -86,9 +86,13 @@ fn classify_join_filters( let mut common_filter = None; split_conjunctive_predicates(predicate, arena, &mut |expr, arena| { let (mut left, mut right) = (true, true); - expr.all_referenced_columns(arena, |_, column| { - left = left && left_columns.contains(column); - right = right && right_columns.contains(column); + expr.all_referenced_columns(arena, |_, column, position| { + let side_holds = |columns: &[ColumnRef], position: Option| { + position.and_then(|position| columns.get(position)) == Some(column) + || columns.contains(column) + }; + left = left && side_holds(left_columns, Some(position)); + right = right && side_holds(right_columns, position.checked_sub(left_len)); left || right })?; if left { @@ -145,161 +149,159 @@ impl NormalizationRule for PushPredicateThroughJoin { arena: &mut PlanArena, _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { - let mut applied = false; - - let parent_replacement = { - let LogicalPlan { - operator, - childrens, - .. - } = plan; - let filter_op = match operator { - Operator::Filter(op) => op, - _ => return Ok(false), - }; - - let Childrens::Only(join_plan) = childrens.as_mut() else { - return Ok(false); - }; - let join_plan = join_plan.as_mut(); - - let join_type = match &join_plan.operator { - Operator::Join(op) => op.join_type, - _ => return Ok(false), - }; - - if !matches!( - join_type, - JoinType::Inner | JoinType::Cross | JoinType::LeftOuter | JoinType::RightOuter - ) { - return Ok(false); + let Operator::Filter(filter_op) = &plan.operator else { + return Ok(false); + }; + let (predicate, having) = (filter_op.predicate, filter_op.having); + let Childrens::Only(join_plan) = plan.childrens.as_mut() else { + return Ok(false); + }; + let Some((mut applied, common)) = push_through_join(join_plan, predicate, having, arena)? + else { + return Ok(false); + }; + match common { + Some(common) => { + plan.operator = Operator::Filter(filter(common, having)); + applied = true; } + None if applied => { + applied |= replace_with_only_child(plan); + } + None => {} + } + Ok(applied) + } +} - let (left_filter, mut right_filter, mut common_filter, left_len) = - classify_join_filters(filter_op.predicate, join_plan.childrens.as_mut(), arena)?; +fn filter(predicate: ExprRef, having: bool) -> FilterOperator { + FilterOperator { + predicate, + is_optimized: false, + having, + } +} - if join_type == JoinType::Cross { - if let Some(predicate) = common_filter { - let Childrens::Twins { left, right } = join_plan.childrens.as_mut() else { - return Ok(false); - }; - let left_schema = left.output_schema(arena).clone(); - let right_schema = right.output_schema(arena).clone(); - let mut keys = Vec::new(); - let mut residual = Vec::new(); - extract_join_keys( - predicate, - &mut keys, - &mut residual, - &left_schema, - &right_schema, - arena, - )?; - if !keys.is_empty() { - for (_, right) in &mut keys { - // WHERE uses joined positions; join keys use each input's local positions. - *right = right.clone_expression(arena)?; - PositionShift { - delta: -(left_len as isize), - } - .visit(right, arena)?; - } - if let Operator::Join(op) = &mut join_plan.operator { - op.join_type = JoinType::Inner; - op.on = JoinCondition::On { - on: keys, - filter: None, - }; - } - common_filter = reduce_filters(residual, filter_op.having, arena) - .map(|op| op.predicate); - applied = true; - } - } - } +fn push_through_join( + join_plan: &mut LogicalPlan, + predicate: ExprRef, + having: bool, + arena: &mut PlanArena, +) -> Result)>, DatabaseError> { + let join_type = match &join_plan.operator { + Operator::Join(op) => op.join_type, + _ => return Ok(None), + }; + if !matches!( + join_type, + JoinType::Inner | JoinType::Cross | JoinType::LeftOuter | JoinType::RightOuter + ) { + return Ok(None); + } + let mut applied = false; - let mut new_ops = (None, None, None); - match join_type { - JoinType::Inner | JoinType::Cross => { - if let Some(left_filter_op) = - reduce_filters(left_filter, filter_op.having, arena) - { - new_ops.0 = Some(Operator::Filter(left_filter_op)); - } + let (left_filter, mut right_filter, mut common_filter, left_len) = + classify_join_filters(predicate, join_plan.childrens.as_mut(), arena)?; - if let Some(expr) = &mut right_filter { - PositionShift { - delta: -(left_len as isize), - } - .visit(expr, arena)?; - } - if let Some(right_filter_op) = - reduce_filters(right_filter, filter_op.having, arena) - { - new_ops.1 = Some(Operator::Filter(right_filter_op)); - } - new_ops.2 = reduce_filters(common_filter, filter_op.having, arena) - .map(Operator::Filter); - } - JoinType::LeftOuter => { - if let Some(left_filter_op) = - reduce_filters(left_filter, filter_op.having, arena) - { - new_ops.0 = Some(Operator::Filter(left_filter_op)); + if join_type == JoinType::Cross { + if let Some(predicate) = common_filter { + let Childrens::Twins { left, right } = join_plan.childrens.as_mut() else { + return Ok(None); + }; + let left_schema = left.output_schema(arena).clone(); + let right_schema = right.output_schema(arena).clone(); + let mut keys = Vec::new(); + let mut residual = Vec::new(); + extract_join_keys( + predicate, + &mut keys, + &mut residual, + &left_schema, + &right_schema, + arena, + )?; + if !keys.is_empty() { + for (_, right) in &mut keys { + // WHERE uses joined positions; join keys use each input's local positions. + *right = right.clone_expression(arena)?; + PositionShift { + delta: -(left_len as isize), } - new_ops.2 = reduce_filters( - common_filter.into_iter().chain(right_filter), - filter_op.having, - arena, - ) - .map(Operator::Filter); + .visit(right, arena)?; } - JoinType::RightOuter => { - if let Some(expr) = &mut right_filter { - PositionShift { - delta: -(left_len as isize), - } - .visit(expr, arena)?; - } - if let Some(right_filter_op) = - reduce_filters(right_filter, filter_op.having, arena) - { - new_ops.1 = Some(Operator::Filter(right_filter_op)); - } - new_ops.2 = reduce_filters( - common_filter.into_iter().chain(left_filter), - filter_op.having, - arena, - ) - .map(Operator::Filter); + if let Operator::Join(op) = &mut join_plan.operator { + op.join_type = JoinType::Inner; + op.on = JoinCondition::On { + on: keys, + filter: None, + }; } - _ => {} - } - - if let Some(left_op) = new_ops.0 { - applied |= wrap_child_with(join_plan, 0, left_op); - } - - if let Some(right_op) = new_ops.1 { - applied |= wrap_child_with(join_plan, 1, right_op); - } - - new_ops.2 - }; - - match parent_replacement { - Some(common_op) => { - plan.operator = common_op; + common_filter = reduce_filters(residual, having, arena).map(|op| op.predicate); applied = true; } - None if applied => { - applied |= replace_with_only_child(plan); + } + } + + if let Some(expr) = &mut right_filter { + if matches!( + join_type, + JoinType::Inner | JoinType::Cross | JoinType::RightOuter + ) { + PositionShift { + delta: -(left_len as isize), } - _ => {} + .visit(expr, arena)?; } + } + let (left, right, common) = match join_type { + JoinType::Inner | JoinType::Cross => (left_filter, right_filter, common_filter), + JoinType::LeftOuter => ( + left_filter, + None, + reduce_filters(common_filter.into_iter().chain(right_filter), having, arena) + .map(|op| op.predicate), + ), + JoinType::RightOuter => ( + None, + right_filter, + reduce_filters(common_filter.into_iter().chain(left_filter), having, arena) + .map(|op| op.predicate), + ), + _ => unreachable!(), + }; + if let Some(left) = left { + applied |= push_into_child(join_plan, 0, left, having, arena)?; + } + if let Some(right) = right { + applied |= push_into_child(join_plan, 1, right, having, arena)?; + } + Ok(Some((applied, common))) +} - Ok(applied) +/// Pushes `predicate` into the `child_idx` input of `join_plan`, through it when it is a join. +fn push_into_child( + join_plan: &mut LogicalPlan, + child_idx: usize, + predicate: ExprRef, + having: bool, + arena: &mut PlanArena, +) -> Result { + let Childrens::Twins { left, right } = join_plan.childrens.as_mut() else { + return Ok(false); + }; + let child = if child_idx == 0 { left } else { right }; + let remaining = match push_through_join(child, predicate, having, arena)? { + Some((_, remaining)) => remaining, + None => Some(predicate), + }; + if let Some(remaining) = remaining { + wrap_child_with( + join_plan, + child_idx, + Operator::Filter(filter(remaining, having)), + ); } + Ok(true) } pub struct PushPredicateIntoScan; @@ -714,16 +716,17 @@ mod tests { )), }; for expr in [left, right, unknown, common, constant, alias] { - let expected = - if expr.all_referenced_columns(&arena, |_, column| *column == left_column)? { - (Some(expr), None, None, 1) - } else if expr.all_referenced_columns(&arena, |_, column| { - right_columns.contains(column) - })? { - (None, Some(expr), None, 1) - } else { - (None, None, Some(expr), 1) - }; + let expected = if expr + .all_referenced_columns(&arena, |_, column, _| *column == left_column)? + { + (Some(expr), None, None, 1) + } else if expr + .all_referenced_columns(&arena, |_, column, _| right_columns.contains(column))? + { + (None, Some(expr), None, 1) + } else { + (None, None, Some(expr), 1) + }; assert_eq!( super::classify_join_filters(expr, &mut children, &mut arena)?, expected diff --git a/src/planner/operator/join.rs b/src/planner/operator/join.rs index 6cb5cb17..77412c6f 100644 --- a/src/planner/operator/join.rs +++ b/src/planner/operator/join.rs @@ -209,9 +209,9 @@ pub(crate) fn extract_join_keys( } _other => { // example: baz > 1 - if left_expr.all_referenced_columns(arena, |_, column| { + if left_expr.all_referenced_columns(arena, |_, column, _| { fn_or_contains(*column) - })? && right_expr.all_referenced_columns(arena, |_, column| { + })? && right_expr.all_referenced_columns(arena, |_, column, _| { fn_or_contains(*column) })? { accum_filter.push(expr); @@ -244,9 +244,9 @@ pub(crate) fn extract_join_keys( } _ => { if left_expr - .all_referenced_columns(arena, |_, column| fn_or_contains(*column))? + .all_referenced_columns(arena, |_, column, _| fn_or_contains(*column))? && right_expr - .all_referenced_columns(arena, |_, column| fn_or_contains(*column))? + .all_referenced_columns(arena, |_, column, _| fn_or_contains(*column))? { accum_filter.push(expr); } @@ -254,7 +254,7 @@ pub(crate) fn extract_join_keys( } } _ => { - if expr.all_referenced_columns(arena, |_, column| fn_or_contains(*column))? { + if expr.all_referenced_columns(arena, |_, column, _| fn_or_contains(*column))? { // example: baz > 1 accum_filter.push(expr); } diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index e9bb86ff..2d854497 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -640,7 +640,11 @@ impl Operator { Ok(()) } - fn visit_column_ref(&mut self, column: &ColumnRef) -> Result<(), DatabaseError> { + fn visit_column_ref( + &mut self, + column: &ColumnRef, + _position: usize, + ) -> Result<(), DatabaseError> { if self.keep_going { self.keep_going = (self.f)(self.arena, column); } @@ -745,8 +749,8 @@ impl Operator { &mut self, op: &'operator TableScanOperator, ) -> Result<(), DatabaseError> { - for column in &op.columns { - ExprVisitor::visit_column_ref(self, column)?; + for (position, column) in op.columns.iter().enumerate() { + ExprVisitor::visit_column_ref(self, column, position)?; } Ok(()) } @@ -800,8 +804,8 @@ impl Operator { } fn visit_values(&mut self, op: &'operator ValuesOperator) -> Result<(), DatabaseError> { - for column in &op.schema_ref { - ExprVisitor::visit_column_ref(self, column)?; + for (position, column) in op.schema_ref.iter().enumerate() { + ExprVisitor::visit_column_ref(self, column, position)?; } Ok(()) } @@ -811,8 +815,13 @@ impl Operator { op: &'operator UnionOperator, _children: &mut ChildrenVisit<'operator>, ) -> Result<(), DatabaseError> { - for column in op.left_schema_ref.iter().chain(&op._right_schema_ref) { - ExprVisitor::visit_column_ref(self, column)?; + for (position, column) in op + .left_schema_ref + .iter() + .chain(&op._right_schema_ref) + .enumerate() + { + ExprVisitor::visit_column_ref(self, column, position)?; } Ok(()) } @@ -822,8 +831,8 @@ impl Operator { op: &'operator RecursiveCteOperator, _children: &mut ChildrenVisit<'operator>, ) -> Result<(), DatabaseError> { - for column in &op.schema_ref { - ExprVisitor::visit_column_ref(self, column)?; + for (position, column) in op.schema_ref.iter().enumerate() { + ExprVisitor::visit_column_ref(self, column, position)?; } Ok(()) } @@ -832,8 +841,8 @@ impl Operator { &mut self, op: &'operator RecursiveScanOperator, ) -> Result<(), DatabaseError> { - for column in &op.schema_ref { - ExprVisitor::visit_column_ref(self, column)?; + for (position, column) in op.schema_ref.iter().enumerate() { + ExprVisitor::visit_column_ref(self, column, position)?; } Ok(()) } @@ -843,8 +852,13 @@ impl Operator { op: &'operator SetMembershipOperator, _children: &mut ChildrenVisit<'operator>, ) -> Result<(), DatabaseError> { - for column in op.left_schema_ref.iter().chain(&op._right_schema_ref) { - ExprVisitor::visit_column_ref(self, column)?; + for (position, column) in op + .left_schema_ref + .iter() + .chain(&op._right_schema_ref) + .enumerate() + { + ExprVisitor::visit_column_ref(self, column, position)?; } Ok(()) } @@ -854,8 +868,8 @@ impl Operator { op: &'operator DeleteOperator, _children: &mut ChildrenVisit<'operator>, ) -> Result<(), DatabaseError> { - for column in &op.primary_keys { - ExprVisitor::visit_column_ref(self, column)?; + for (position, column) in op.primary_keys.iter().enumerate() { + ExprVisitor::visit_column_ref(self, column, position)?; } Ok(()) } From 02c4dcb81379e3e96b54f97cf004713c66e6c5f6 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:49 +0800 Subject: [PATCH 38/62] refactor(execution): carry rows as TupleImpl to join without materializing Introduce TupleImpl (a plain Tuple or a binary Concat of two rows) as the executor result type. Joins (MarkApply, nested loop, hash) emit Concat rows instead of copying both inputs into one buffer, and every consumer reads positions through TupleLike, so no operator flattens a joined row. - Sort/top-k/window/spill/set membership/hash join state store TupleImpl; spill encodes Concat with the same wire format as a flat Tuple. - Storage accepts TupleImpl (append_tuple, serialize_to) and generic TupleLike index values (Index, with_index, add/del_index). - Public API: ResultIter::next_tuple, FromQueryRow and FromQueryTuple now take &mut TupleImpl. - Remove MoveableTuple, the ProjectMove rule and ProjectOperator::moveable; rewrite evaluates against TupleImpl and reuses the result buffer. - Add a Miri CI job for unsafe-covering unit tests. --- .github/workflows/ci.yml | 19 + examples/transaction.rs | 15 +- fuzz/fuzz_targets/sql_exec.rs | 4 +- fuzz/fuzz_targets/sql_gen.rs | 16 +- kite_sql_serde_macros/src/orm.rs | 2 +- kite_sql_serde_macros/src/projection.rs | 2 +- src/bin/shell.rs | 1 - src/db.rs | 96 ++- src/db/prepared.rs | 8 +- src/execution/ddl/add_column.rs | 12 +- src/execution/ddl/change_column.rs | 8 +- src/execution/ddl/create_index.rs | 8 +- src/execution/ddl/drop_column.rs | 2 +- src/execution/ddl/mod.rs | 28 +- src/execution/dml/analyze.rs | 13 +- src/execution/dml/copy_from_file.rs | 8 +- src/execution/dml/copy_to_file.rs | 4 +- src/execution/dml/delete.rs | 10 +- src/execution/dml/insert.rs | 31 +- src/execution/dml/update.rs | 23 +- src/execution/dql/aggregate/hash_agg.rs | 7 +- src/execution/dql/aggregate/simple_agg.rs | 2 +- src/execution/dql/aggregate/stream_agg.rs | 8 +- .../dql/aggregate/stream_distinct.rs | 21 +- src/execution/dql/describe.rs | 2 +- src/execution/dql/dummy.rs | 2 +- src/execution/dql/explain.rs | 2 +- src/execution/dql/external_sort.rs | 28 +- src/execution/dql/function_scan.rs | 2 +- src/execution/dql/index_scan.rs | 9 +- src/execution/dql/join/hash/full_join.rs | 45 +- src/execution/dql/join/hash/inner_join.rs | 25 +- src/execution/dql/join/hash/left_join.rs | 28 +- src/execution/dql/join/hash/mod.rs | 37 +- src/execution/dql/join/hash/right_join.rs | 33 +- src/execution/dql/join/hash_join.rs | 48 +- src/execution/dql/join/nested_loop_join.rs | 71 +- src/execution/dql/mark_apply.rs | 85 +-- src/execution/dql/projection.rs | 12 +- src/execution/dql/recursive_cte.rs | 48 +- src/execution/dql/scalar_query_init.rs | 14 +- src/execution/dql/scalar_subquery.rs | 2 +- src/execution/dql/seq_scan.rs | 9 +- src/execution/dql/set_membership.rs | 8 +- src/execution/dql/show_table.rs | 2 +- src/execution/dql/show_view.rs | 2 +- src/execution/dql/sort.rs | 130 ++-- src/execution/dql/top_k.rs | 140 ++-- src/execution/dql/values.rs | 2 +- src/execution/dql/window.rs | 26 +- src/execution/dql/window/function.rs | 16 +- src/execution/mod.rs | 107 +-- src/execution/spill/codec.rs | 80 ++- src/macros/mod.rs | 6 +- .../rule/normalization/elimination.rs | 4 +- src/optimizer/rule/normalization/mod.rs | 8 +- .../rule/normalization/project_move.rs | 242 ------- src/orm/mod.rs | 71 +- src/planner/operator/project.rs | 7 +- src/python.rs | 9 +- src/storage/lmdb.rs | 2 +- src/storage/memory.rs | 12 +- src/storage/mod.rs | 51 +- src/storage/rocksdb.rs | 6 +- src/storage/table_codec.rs | 37 +- src/types/index.rs | 19 +- src/types/tuple.rs | 612 +++++++++++++++--- src/types/tuple_builder.rs | 2 +- src/wasm.rs | 13 +- tests/macros-test/src/main.rs | 40 +- tests/sqllogictest/src/lib.rs | 1 - tpcc/src/backend/dual.rs | 32 +- tpcc/src/backend/kitesql_lmdb.rs | 8 +- tpcc/src/backend/kitesql_rocksdb.rs | 10 +- tpcc/src/backend/mod.rs | 8 +- tpcc/src/backend/sqlite.rs | 14 +- tpcc/src/delivery.rs | 6 +- tpcc/src/main.rs | 61 +- tpcc/src/new_ord.rs | 50 +- tpcc/src/order_stat.rs | 30 +- tpcc/src/payment.rs | 58 +- tpcc/src/slev.rs | 6 +- 82 files changed, 1528 insertions(+), 1260 deletions(-) delete mode 100644 src/optimizer/rule/normalization/project_move.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4930a5a..4a1089d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -165,3 +165,22 @@ jobs: with: name: fuzz-artifacts path: fuzz/artifacts + # 10 + miri: + name: Miri (unsafe code UB check) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2026-10-04 + components: miri, rust-src + - uses: Swatinem/rust-cache@v2 + with: + key: miri + + - name: Set up Miri sysroot + run: cargo +nightly-2026-10-04 miri setup + + - name: Run Miri tests + run: make miri diff --git a/examples/transaction.rs b/examples/transaction.rs index 00b4c868..7732a289 100644 --- a/examples/transaction.rs +++ b/examples/transaction.rs @@ -16,7 +16,6 @@ mod app { use kite_sql::db::DataBaseBuilder; use kite_sql::errors::DatabaseError; - use kite_sql::types::tuple::Tuple; use kite_sql::types::value::DataValue; use std::fs; use std::io::ErrorKind; @@ -57,17 +56,11 @@ mod app { let mut iter = database.run("select * from t1")?; iter.next_tuple(|_, tuple| { - assert_eq!( - tuple, - &Tuple::new(None, vec![DataValue::Int32(0), DataValue::Int32(0)]) - ); + assert_eq!(*tuple, vec![DataValue::Int32(0), DataValue::Int32(0)]); })? .unwrap(); iter.next_tuple(|_, tuple| { - assert_eq!( - tuple, - &Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(1)]) - ); + assert_eq!(*tuple, vec![DataValue::Int32(1), DataValue::Int32(1)]); })? .unwrap(); assert!(iter.next_tuple(|_, _| ())?.is_none()); @@ -77,9 +70,7 @@ mod app { tx2.run("update t1 set c2 = 99 where c1 = 0")?.done()?; let mut c2_iter = database.run("select c2 from t1 where c1 = 0")?; assert_eq!( - c2_iter - .next_tuple(|_, tuple| tuple.values[0].i32())? - .unwrap(), + c2_iter.next_tuple(|_, tuple| tuple[0].i32())?.unwrap(), Some(0) ); c2_iter.done()?; diff --git a/fuzz/fuzz_targets/sql_exec.rs b/fuzz/fuzz_targets/sql_exec.rs index af0342fc..50ec7e31 100644 --- a/fuzz/fuzz_targets/sql_exec.rs +++ b/fuzz/fuzz_targets/sql_exec.rs @@ -72,7 +72,7 @@ fn reset(db: &mut Database) { let mut names = Vec::new(); if let Ok(mut iter) = db.run(show) { while let Ok(Some(())) = iter.next_tuple(|_, tuple| { - if let Some(DataValue::Utf8 { value, .. }) = tuple.values.first() { + if let Some(DataValue::Utf8 { value, .. }) = tuple.get(0) { names.push(value.clone()); } }) {} @@ -165,7 +165,7 @@ fn run_one(db: &mut Database, sql: &str) { // Drain and render every value, like the sqllogictest harness does. loop { match iter.next_tuple(|_, tuple| { - for value in tuple.values.iter() { + for value in tuple.iter() { let _ = value.to_string(); } }) { diff --git a/fuzz/fuzz_targets/sql_gen.rs b/fuzz/fuzz_targets/sql_gen.rs index c6d06d4a..608978f3 100644 --- a/fuzz/fuzz_targets/sql_gen.rs +++ b/fuzz/fuzz_targets/sql_gen.rs @@ -34,6 +34,7 @@ use kite_sql::db::{DataBaseBuilder, Database}; use kite_sql::errors::DatabaseError; use kite_sql::storage::lmdb::LmdbStorage; +use kite_sql::types::tuple::TupleImpl; use kite_sql::types::value::DataValue; use libfuzzer_sys::arbitrary::{self, Unstructured}; use libfuzzer_sys::fuzz_target; @@ -735,7 +736,11 @@ fn open_db() -> Database { fn check_tlp(db: &Database, select: &Select, predicate: &str) { let base_sql = select.render(false, false); let mut base = Vec::new(); - if for_each_row(db, &base_sql, |row| base.push(row.to_vec())).is_err() { + if for_each_row(db, &base_sql, |row| { + base.push(row.iter().cloned().collect()) + }) + .is_err() + { return; } let order_by = select.order_by.map(|(column, _)| column); @@ -778,8 +783,11 @@ fn rows_match( } let mut matched = true; let mut i = offset; + let mut key = Vec::new(); let rows = for_each_row(db, sql, |row| { - let in_base = match remaining.get_mut(row) { + key.clear(); + key.extend(row.iter().cloned()); + let in_base = match remaining.get_mut(key.as_slice()) { Some(count) if *count > 0 => { *count -= 1; true @@ -807,7 +815,7 @@ fn run(db: &Database, sql: &str) -> Result { fn for_each_row( db: &Database, sql: &str, - mut f: impl FnMut(&[DataValue]), + mut f: impl FnMut(&TupleImpl), ) -> Result { // Printed before running so a crashing statement is still visible. if debug() { @@ -815,7 +823,7 @@ fn for_each_row( } let mut iter = db.run(sql)?; let mut rows = 0; - while iter.next_tuple(|_, tuple| f(&tuple.values))?.is_some() { + while iter.next_tuple(|_, tuple| f(tuple))?.is_some() { rows += 1; } iter.done()?; diff --git a/kite_sql_serde_macros/src/orm.rs b/kite_sql_serde_macros/src/orm.rs index 24d53f26..1ebe4f3c 100644 --- a/kite_sql_serde_macros/src/orm.rs +++ b/kite_sql_serde_macros/src/orm.rs @@ -387,7 +387,7 @@ pub(crate) fn handle(ast: DeriveInput) -> Result { { fn from_query_row( schema: &::kite_sql::types::tuple::SchemaView<'_, '_>, - tuple: &mut ::kite_sql::types::tuple::Tuple, + tuple: &mut ::kite_sql::types::tuple::TupleImpl, ) -> ::std::result::Result { let mut __kite_orm_found_fields = 0usize; #(#field_index_declarations)* diff --git a/kite_sql_serde_macros/src/projection.rs b/kite_sql_serde_macros/src/projection.rs index fff67210..29b7079f 100644 --- a/kite_sql_serde_macros/src/projection.rs +++ b/kite_sql_serde_macros/src/projection.rs @@ -121,7 +121,7 @@ pub(crate) fn handle(ast: DeriveInput) -> Result { { fn from_query_row( schema: &::kite_sql::types::tuple::SchemaView<'_, '_>, - tuple: &mut ::kite_sql::types::tuple::Tuple, + tuple: &mut ::kite_sql::types::tuple::TupleImpl, ) -> ::std::result::Result { let mut __kite_orm_found_fields = 0usize; #(#field_index_declarations)* diff --git a/src/bin/shell.rs b/src/bin/shell.rs index 83945998..b31dadfa 100644 --- a/src/bin/shell.rs +++ b/src/bin/shell.rs @@ -211,7 +211,6 @@ Transaction commands: let mut row_count = 0usize; while let Some(row) = iter.next_tuple(|_, tuple| { tuple - .values .iter() .map(|value| Cell::new(format!("{value}"))) .collect::>() diff --git a/src/db.rs b/src/db.rs index 97a18e7a..499e8798 100644 --- a/src/db.rs +++ b/src/db.rs @@ -57,7 +57,7 @@ use crate::storage::{ CheckpointableStorage, StatisticsMetaCache, Storage, TableCache, Transaction, TransactionIsolationLevel, ViewCache, }; -use crate::types::tuple::{Schema, SchemaView, Tuple}; +use crate::types::tuple::{Schema, SchemaView, TupleImpl}; use crate::types::value::DataValue; use crate::types::LogicalType; use ordered_float::OrderedFloat; @@ -546,11 +546,6 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { HepBatchStrategy::fix_point_topdown(10), &[NormalizationRuleImpl::PushLimitIntoTableScan], ) - .after_batch( - "Move Project Inputs", - HepBatchStrategy::fix_point_bottomup(1), - &[NormalizationRuleImpl::ProjectMove], - ) .implementations(&[ // DQL ImplementationRuleImpl::SimpleAggregate, @@ -1223,7 +1218,7 @@ pub trait ResultIter { /// Advances to the next row and runs `f` while the executor row slot is borrowed. fn next_tuple( &mut self, - f: impl FnOnce(&SchemaView<'_, '_>, &mut Tuple) -> R, + f: impl FnOnce(&SchemaView<'_, '_>, &mut TupleImpl) -> R, ) -> Result, DatabaseError>; /// Finishes consuming the iterator and flushes any remaining work. @@ -1317,7 +1312,7 @@ impl DatabaseIter<'_, S> { #[inline] pub fn next_tuple( &mut self, - f: impl FnOnce(&SchemaView<'_, '_>, &mut Tuple) -> R, + f: impl FnOnce(&SchemaView<'_, '_>, &mut TupleImpl) -> R, ) -> Result, DatabaseError> { unsafe { (*self.inner).next_tuple(f) } } @@ -1341,7 +1336,7 @@ impl ResultIter for DatabaseIter<'_, S> { fn next_tuple( &mut self, - f: impl FnOnce(&SchemaView<'_, '_>, &mut Tuple) -> R, + f: impl FnOnce(&SchemaView<'_, '_>, &mut TupleImpl) -> R, ) -> Result, DatabaseError> { DatabaseIter::next_tuple(self, f) } @@ -1480,7 +1475,7 @@ impl<'a, T: Transaction + 'a, A: MetaArena + 'a> TransactionIter<'a, T, A> { #[inline] pub fn next_tuple( &mut self, - f: impl FnOnce(&SchemaView<'_, '_>, &mut Tuple) -> R, + f: impl FnOnce(&SchemaView<'_, '_>, &mut TupleImpl) -> R, ) -> Result, DatabaseError> { let Some(executor) = self.guard.executor.as_mut() else { return Ok(None); @@ -1531,7 +1526,7 @@ impl ResultIter for TransactionIter<'_, T, A> { fn next_tuple( &mut self, - f: impl FnOnce(&SchemaView<'_, '_>, &mut Tuple) -> R, + f: impl FnOnce(&SchemaView<'_, '_>, &mut TupleImpl) -> R, ) -> Result, DatabaseError> { TransactionIter::next_tuple(self, f) } @@ -1561,7 +1556,7 @@ pub(crate) mod test { table_codec::TableCodec, StatisticsMetaCache, Storage, TableCache, Transaction, TransactionIsolationLevel, ViewCache, }; - use crate::types::tuple::Tuple; + use crate::types::tuple::{Tuple, TupleImpl}; use crate::types::value::DataValue; use crate::types::LogicalType; use chrono::{Datelike, Local}; @@ -1668,12 +1663,12 @@ pub(crate) mod test { execute_input::<_, Projection>((project, input), context, arena, &transaction); for value in [1, 2, 3, 4] { assert_eq!( - executor.next_tuple()?.map(|row| row.values.clone()), - Some(vec![ + executor.next_tuple()?.cloned().unwrap(), + vec![ DataValue::Int32(value), DataValue::Int32(value), DataValue::Int32(99) - ]) + ] ); } assert!(executor.next_tuple()?.is_none()); @@ -1730,8 +1725,8 @@ pub(crate) mod test { execute_input::<_, Projection>((project, input), context, arena, &transaction); for value in [11, 12, 13, 14] { assert_eq!( - executor.next_tuple()?.map(|row| row.values.clone()), - Some(vec![DataValue::Int32(value), DataValue::Int32(99)]) + executor.next_tuple()?.cloned().unwrap(), + vec![DataValue::Int32(value), DataValue::Int32(99)] ); } assert!(executor.next_tuple()?.is_none()); @@ -1767,13 +1762,13 @@ pub(crate) mod test { cross join scalar_b b cross join scalar_nested c"; let mut iter = db.run(sql)?; assert_eq!( - iter.next_tuple(|_, row| row.values.clone())?, - Some(vec![ + iter.next_tuple(|_, row| row.clone())?.unwrap(), + vec![ DataValue::Int32(11), DataValue::Int32(22), DataValue::Int32(33), DataValue::Int32(44), - ]) + ] ); assert!(iter.next_tuple(|_, _| ())?.is_none()); iter.done()?; @@ -1782,8 +1777,8 @@ pub(crate) mod test { db.ddl("create view scalar_new as select (select 55) as v")?; let mut iter = db.run("select a.v, b.v from scalar_a a cross join scalar_new b")?; assert_eq!( - iter.next_tuple(|_, row| row.values.clone())?, - Some(vec![DataValue::Int32(11), DataValue::Int32(55)]) + iter.next_tuple(|_, row| row.clone())?.unwrap(), + vec![DataValue::Int32(11), DataValue::Int32(55)] ); iter.done()?; Ok(()) @@ -1826,28 +1821,21 @@ pub(crate) mod test { Ok(()) } - fn tuple_owned(tuple: &Tuple) -> Tuple { - Tuple { - pk: tuple.pk.clone(), - values: tuple.values.clone(), - } - } - - fn next_tuple_owned(iter: &mut I) -> Result, DatabaseError> { - iter.next_tuple(|_, tuple| tuple_owned(tuple)) + fn next_tuple_owned(iter: &mut I) -> Result, DatabaseError> { + iter.next_tuple(|_, tuple| tuple.clone()) } - fn next_values(iter: &mut I) -> Result>, DatabaseError> { - iter.next_tuple(|_, tuple| tuple.values.clone()) + fn next_values(iter: &mut I) -> Result, DatabaseError> { + iter.next_tuple(|_, tuple| tuple.clone()) } fn read_single_i32(mut iter: I) -> Result where I: ResultIter, { - let value = match iter.next_tuple(|_, tuple| match tuple.values.as_slice() { - [DataValue::Int32(value)] => *value, - other => panic!("expected a single Int32 column, got {other:?}"), + let value = match iter.next_tuple(|_, tuple| match (tuple.len(), tuple.get(0)) { + (1, Some(DataValue::Int32(value))) => *value, + _ => panic!("expected a single Int32 column, got {tuple:?}"), })? { Some(value) => value, None => panic!("expected one result row"), @@ -1862,9 +1850,9 @@ pub(crate) mod test { sql: &str, ) -> Result { let mut iter = database.run(sql)?; - let value = match iter.next_tuple(|_, tuple| match tuple.values.as_slice() { - [DataValue::Int32(value)] => *value, - other => panic!("expected a single Int32 column, got {other:?}"), + let value = match iter.next_tuple(|_, tuple| match (tuple.len(), tuple.get(0)) { + (1, Some(DataValue::Int32(value))) => *value, + _ => panic!("expected a single Int32 column, got {tuple:?}"), })? { Some(value) => value, None => panic!("expected one result row for query: {sql}"), @@ -2233,7 +2221,7 @@ pub(crate) mod test { let mut iter = kite_sql.execute(&statement, &[(1, DataValue::Int32(0))])?; let row = next_tuple_owned(&mut iter)?.unwrap(); - let plan = row.values[0].utf8().unwrap(); + let plan = row[0].utf8().unwrap(); assert_eq!( plan, "Projection [t1.a, t1.b] [Project => (Sort Option: Follow)] Filter (t1.b > 0), Is Having: false [Filter => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [SeqScan => (Sort Option: None)]" @@ -2260,7 +2248,7 @@ pub(crate) mod test { ], )?; let row = next_tuple_owned(&mut iter)?.unwrap(); - let plan = row.values[0].utf8().unwrap(); + let plan = row[0].utf8().unwrap(); assert_eq!( plan, "Projection [(t1.a + $1), Max((t1.b + $2))] [Project => (Sort Option: Follow)] Aggregate [Max((t1.b + 0))] -> Group By [(t1.a + 0)] [HashAggregate => (Sort Option: None)] Filter (t1.b > 1), Is Having: false [Filter => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [SeqScan => (Sort Option: None)]" @@ -2279,7 +2267,7 @@ pub(crate) mod test { ], )?; let row = next_tuple_owned(&mut iter)?.unwrap(); - let plan = row.values[0].utf8().unwrap(); + let plan = row[0].utf8().unwrap(); assert_eq!( plan, "Projection [l.a, l.b, t1.a, t1.b, 9] [Project => (Sort Option: Follow)] LeftOuter Join Where (l.a > 0) [NestLoopJoin => (Sort Option: None)] Projection [(t1.a) as (l.a), (t1.b) as (l.b)] [Project => (Sort Option: Follow)] Filter (t1.b > 0), Is Having: false [Filter => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [SeqScan => (Sort Option: None)] Projection [t1.a, t1.b] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [IndexScan By pk_index => ($3, +inf) => (Sort Option: OrderBy: (t1.a Asc Nulls Last) ignore_prefix_len: 0)]" @@ -2698,30 +2686,30 @@ pub(crate) mod test { assert_eq!(read_single_i32(db.run("select * from deep_view")?)?, 7); db.run_mut(&sql, |iter| { assert_eq!( - iter.next_tuple(|_, row| row.values.clone())?, - Some(vec![DataValue::Int32(7)]) + iter.next_tuple(|_, row| row.clone())?.unwrap(), + vec![DataValue::Int32(7)] ); Ok(()) })?; let prepared = db.prepare_sql(&sql, &[])?; let mut iter = db.execute(&prepared, [])?; assert_eq!( - iter.next_tuple(|_, row| row.values.clone())?, - Some(vec![DataValue::Int32(7)]) + iter.next_tuple(|_, row| row.clone())?.unwrap(), + vec![DataValue::Int32(7)] ); iter.done()?; let mut tx = db.new_transaction()?; let mut iter = tx.run(&sql)?; assert_eq!( - iter.next_tuple(|_, row| row.values.clone())?, - Some(vec![DataValue::Int32(7)]) + iter.next_tuple(|_, row| row.clone())?.unwrap(), + vec![DataValue::Int32(7)] ); iter.done()?; let prepared = tx.prepare_sql(&sql, &[])?; let mut iter = tx.execute(&prepared, [])?; assert_eq!( - iter.next_tuple(|_, row| row.values.clone())?, - Some(vec![DataValue::Int32(7)]) + iter.next_tuple(|_, row| row.clone())?.unwrap(), + vec![DataValue::Int32(7)] ); iter.done()?; tx.commit()?; @@ -2824,12 +2812,12 @@ pub(crate) mod test { snapshot.load(CatalogKind::View("checkpoint_view".into()))?; let mut rows = snapshot.run("select id,v from checkpoint_rows order by id")?; assert_eq!( - rows.next_tuple(|_, row| row.values.clone())?, - Some(vec![DataValue::Int32(1), DataValue::Int32(10)]) + rows.next_tuple(|_, row| row.clone())?.unwrap(), + vec![DataValue::Int32(1), DataValue::Int32(10)] ); assert_eq!( - rows.next_tuple(|_, row| row.values.clone())?, - Some(vec![DataValue::Int32(2), DataValue::Int32(20)]) + rows.next_tuple(|_, row| row.clone())?.unwrap(), + vec![DataValue::Int32(2), DataValue::Int32(20)] ); assert!(rows.next_tuple(|_, _| ())?.is_none()); rows.done()?; diff --git a/src/db/prepared.rs b/src/db/prepared.rs index a93bb7b8..a278f778 100644 --- a/src/db/prepared.rs +++ b/src/db/prepared.rs @@ -171,8 +171,8 @@ mod tests { ); }); assert_eq!( - iter.next_tuple(|_, row| row.values.clone())?, - Some(vec![DataValue::Int64(expected)]) + iter.next_tuple(|_, row| row.clone())?.unwrap(), + vec![DataValue::Int64(expected)] ); assert!(iter.next_tuple(|_, _| ())?.is_none()); iter.done()?; @@ -180,8 +180,8 @@ mod tests { let mut tx = db.new_transaction()?; let mut iter = tx.execute(&plan, [(1, DataValue::Int64(7))])?; assert_eq!( - iter.next_tuple(|_, row| row.values.clone())?, - Some(vec![DataValue::Int64(31)]) + iter.next_tuple(|_, row| row.clone())?.unwrap(), + vec![DataValue::Int64(31)] ); iter.done()?; tx.commit()?; diff --git a/src/execution/ddl/add_column.rs b/src/execution/ddl/add_column.rs index c0b78dcb..3c7c9ee7 100644 --- a/src/execution/ddl/add_column.rs +++ b/src/execution/ddl/add_column.rs @@ -118,18 +118,16 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for AddColumn<'a> { }, |tuple| { if let Some(value) = &default_value { - tuple.values.push(value.clone()); + tuple.push(value.clone()); } else { - tuple.values.push(DataValue::Null); + tuple.push(DataValue::Null); } Ok(()) }, |transaction, table_codec, tuple| { - if let (Some(unique_index_id), Some(value), Some(tuple_id)) = ( - unique_index_id.as_ref(), - default_value.as_ref(), - tuple.pk.as_ref(), - ) { + if let (Some(unique_index_id), Some(value), Some(tuple_id)) = + (unique_index_id.as_ref(), default_value.as_ref(), tuple.pk()) + { let index = Index::new( *unique_index_id, std::slice::from_ref(value), diff --git a/src/execution/ddl/change_column.rs b/src/execution/ddl/change_column.rs index ccddade6..1c1ae9a0 100644 --- a/src/execution/ddl/change_column.rs +++ b/src/execution/ddl/change_column.rs @@ -128,9 +128,9 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ChangeColumn<'a> { }) }, |tuple| { - let value = std::mem::replace(&mut tuple.values[column_index], DataValue::Null); - tuple.values[column_index] = value.cast(&target_data_type)?; - if needs_not_null_validation && tuple.values[column_index].is_null() { + let value = std::mem::replace(&mut tuple[column_index], DataValue::Null); + tuple[column_index] = value.cast(&target_data_type)?; + if needs_not_null_validation && tuple[column_index].is_null() { return Err(DatabaseError::not_null_column(target_column_name.clone())); } Ok(()) @@ -154,7 +154,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ChangeColumn<'a> { .map(|column| plan_arena.column(*column).datatype().serializable()) }, |tuple| { - if tuple.values[column_index].is_null() { + if tuple[column_index].is_null() { return Err(DatabaseError::not_null_column(target_column_name.clone())); } Ok(()) diff --git a/src/execution/ddl/create_index.rs b/src/execution/ddl/create_index.rs index 3a3fe894..60b70990 100644 --- a/src/execution/ddl/create_index.rs +++ b/src/execution/ddl/create_index.rs @@ -117,15 +117,15 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CreateIndex<'a> { }; while arena.next_tuple(self.input, plan_arena)? { - if arena.result_tuple().pk.is_none() { + if arena.result_tuple().pk().is_none() { continue; } - arena.rewrite(&column_exprs, plan_arena, None::<&[DataValue]>, None)?; + arena.rewrite(&column_exprs, plan_arena, None::<&[DataValue]>)?; { let mut state = arena.local_state(plan_arena); let (tuple, transaction, table_codec) = state.tuple_transaction_codec_mut(); - let tuple_pk = tuple.pk.as_ref().ok_or(DatabaseError::PrimaryKeyNotFound)?; - let index = Index::new(index_id, &tuple.values, *ty); + let tuple_pk = tuple.pk().ok_or(DatabaseError::PrimaryKeyNotFound)?; + let index = Index::new(index_id, tuple, *ty); transaction.add_index(table_codec, table_name.as_ref(), index, tuple_pk)?; } } diff --git a/src/execution/ddl/drop_column.rs b/src/execution/ddl/drop_column.rs index db7c2c7e..6e1712ae 100644 --- a/src/execution/ddl/drop_column.rs +++ b/src/execution/ddl/drop_column.rs @@ -100,7 +100,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for DropColumn<'a> { .map(|(_, column)| plan_arena.column(*column).datatype().serializable()) }, |tuple| { - let _ = tuple.values.remove(column_index); + let _ = tuple.remove(column_index); Ok(()) }, |_, _, _| Ok(()), diff --git a/src/execution/ddl/mod.rs b/src/execution/ddl/mod.rs index ca1e68d3..743e125d 100644 --- a/src/execution/ddl/mod.rs +++ b/src/execution/ddl/mod.rs @@ -28,7 +28,7 @@ use crate::errors::DatabaseError; use crate::storage::table_codec::TableCodec; use crate::storage::{InnerIter, Transaction}; use crate::types::serialize::TupleValueSerializableImpl; -use crate::types::tuple::{Tuple, TupleId}; +use crate::types::tuple::{Tuple, TupleId, TupleImpl}; use crate::types::LogicalType; use std::collections::Bound; @@ -43,7 +43,7 @@ fn read_tuple_batch( old_values_len: usize, old_deserializers: &mut D, start_after: Option<&TupleId>, - batch: &mut Vec, + batch: &mut Vec, batch_size: usize, ) -> Result where @@ -76,16 +76,16 @@ where let tuple = if len < batch.len() { &mut batch[len] } else { - batch.push(Tuple { - pk: None, - values: Vec::with_capacity(old_values_len), - }); + batch.push(TupleImpl::from(Tuple::new( + None, + Vec::with_capacity(old_values_len), + ))); batch .last_mut() .expect("batch contains the tuple that was just pushed") }; TableCodec::decode_tuple_into( - tuple, + tuple.output_mut(), old_deserializers(), Some(tuple_id), value, @@ -111,7 +111,7 @@ where T: Transaction, D: FnMut() -> I, I: IntoIterator, - F: FnMut(&Tuple) -> Result<(), DatabaseError>, + F: FnMut(&TupleImpl) -> Result<(), DatabaseError>, { let mut last_pk = None; let mut batch = Vec::with_capacity(REWRITE_BATCH_SIZE); @@ -131,7 +131,9 @@ where if batch_len == 0 { break; } - last_pk = batch.get(batch_len - 1).and_then(|tuple| tuple.pk.clone()); + last_pk = batch + .get(batch_len - 1) + .and_then(|tuple| tuple.pk().cloned()); for tuple in batch.iter().take(batch_len) { visit(tuple)?; @@ -163,8 +165,8 @@ where I: IntoIterator, S: FnMut() -> J, J: IntoIterator, - F: FnMut(&mut Tuple) -> Result<(), DatabaseError>, - G: FnMut(&mut T, &mut TableCodec, &Tuple) -> Result<(), DatabaseError>, + F: FnMut(&mut TupleImpl) -> Result<(), DatabaseError>, + G: FnMut(&mut T, &mut TableCodec, &TupleImpl) -> Result<(), DatabaseError>, { let mut last_pk = None; let mut batch = Vec::with_capacity(REWRITE_BATCH_SIZE); @@ -184,7 +186,9 @@ where if batch_len == 0 { break; } - last_pk = batch.get(batch_len - 1).and_then(|tuple| tuple.pk.clone()); + last_pk = batch + .get(batch_len - 1) + .and_then(|tuple| tuple.pk().cloned()); for tuple in batch.iter_mut().take(batch_len) { rewrite(tuple)?; diff --git a/src/execution/dml/analyze.rs b/src/execution/dml/analyze.rs index f689765a..b9f164df 100644 --- a/src/execution/dml/analyze.rs +++ b/src/execution/dml/analyze.rs @@ -30,6 +30,7 @@ use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::{table_codec::TableCodec, Transaction}; use crate::types::index::IndexId; +use crate::types::tuple::TupleLike; use crate::types::value::{DataValue, Utf8Type}; use crate::types::{CharLengthUnits, ColumnId}; use ordered_float::OrderedFloat; @@ -135,16 +136,16 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Analyze<'a> { }; while arena.next_tuple(input, plan_arena)? { - let tuple = arena.materialize_tuple(); + let tuple = arena.take_tuple(); for (_, position, builder) in &mut columns { - builder.append(&tuple.values[*position]); + builder.append(tuple.value_at(*position)); } for State { exprs, builder, .. } in builders.iter_mut() { let value = match exprs.as_slice() { [expr] => expr.eval(plan_arena, Some(&tuple))?.into_owned(), _ => { - arena.rewrite(exprs, plan_arena, Some(&tuple), None)?; - DataValue::Tuple(arena.materialize_tuple().values) + arena.rewrite(exprs, plan_arena, Some(&tuple))?; + DataValue::Tuple(arena.result_mut().take_values()) } }; builder.append(value)?; @@ -162,7 +163,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Analyze<'a> { plan_arena, )?; - arena.produce_tuple(crate::types::tuple::Tuple::new(None, values)); + arena.produce(crate::types::tuple::Tuple::new(None, values).into()); Ok(()) } } @@ -493,7 +494,7 @@ mod test { let mut iter = reopened.run("select a from t1 where b=7 order by a")?; let mut rows = Vec::new(); while iter - .next_tuple(|_, row| rows.push(row.values[0].clone()))? + .next_tuple(|_, row| rows.push(row[0].clone()))? .is_some() {} iter.done()?; diff --git a/src/execution/dml/copy_from_file.rs b/src/execution/dml/copy_from_file.rs index 1d842de8..927d2a36 100644 --- a/src/execution/dml/copy_from_file.rs +++ b/src/execution/dml/copy_from_file.rs @@ -21,7 +21,7 @@ use crate::iter_ext::Itertools; use crate::planner::operator::copy_from_file::CopyFromFileOperator; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::Transaction; -use crate::types::tuple::Tuple; +use crate::types::tuple::{Tuple, TupleImpl}; use crate::types::tuple_builder::TupleBuilder; use std::fs::File; use std::io::BufReader; @@ -87,7 +87,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CopyFromFile<'a> { let tuple_builder = TupleBuilder::new(column_types, Some(table.primary_key_indices())); let mut record = csv::StringRecord::new(); - let mut chunk = Tuple::new(None, Vec::with_capacity(column_count)); + let mut chunk = TupleImpl::from(Tuple::new(None, Vec::with_capacity(column_count))); while reader.read_record(&mut record)? { if !(record.len() == column_count || record.len() == column_count + 1 && record.get(column_count) == Some("")) @@ -95,7 +95,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CopyFromFile<'a> { return Err(DatabaseError::MisMatch("columns", "values")); } - tuple_builder.build_with_row(&mut chunk, record.iter().take(column_count))?; + tuple_builder.build_with_row(chunk.output_mut(), record.iter().take(column_count))?; let mut state = arena.local_state(plan_arena); let (transaction, table_codec) = state.transaction_codec_mut(); transaction.append_tuple(table_codec, &table_name, &chunk, &serializers, false)?; @@ -189,7 +189,7 @@ mod tests { let mut iter = db.run("select count(*) from test_copy")?; let count = iter - .next_tuple(|_, tuple| tuple.values[0].to_string())? + .next_tuple(|_, tuple| tuple[0].to_string())? .expect("count row"); assert_eq!(count, "2"); iter.done()?; diff --git a/src/execution/dml/copy_to_file.rs b/src/execution/dml/copy_to_file.rs index 6fc3bf47..6cc39682 100644 --- a/src/execution/dml/copy_to_file.rs +++ b/src/execution/dml/copy_to_file.rs @@ -96,7 +96,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CopyToFile<'a> { let mut writer = self.create_writer()?; while arena.next_tuple(input, plan_arena)? { - writer.write_record(arena.result_tuple().values.iter().map(|v| v.to_string()))?; + writer.write_record(arena.result_tuple().iter().map(|v| v.to_string()))?; } writer.flush().map_err(DatabaseError::from)?; @@ -105,7 +105,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CopyToFile<'a> { } else { format!("{} [{}]", self.op, self.column_names.iter().join(", ")) }; - arena.produce_tuple(TupleBuilder::build_result(message)); + arena.produce(TupleBuilder::build_result(message).into()); arena.resume(); Ok(()) } diff --git a/src/execution/dml/delete.rs b/src/execution/dml/delete.rs index 0af17ff8..4140e36c 100644 --- a/src/execution/dml/delete.rs +++ b/src/execution/dml/delete.rs @@ -89,14 +89,14 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Delete<'a> { let mut deleted_count = 0; while arena.next_tuple(input, plan_arena)? { - let tuple = arena.materialize_tuple(); - let Some(tuple_id) = tuple.pk.as_ref() else { + let tuple = arena.take_tuple(); + let Some(tuple_id) = tuple.pk() else { continue; }; for (index_id, index_ty, exprs) in index_templates.iter() { - arena.rewrite(exprs, plan_arena, Some(&tuple), None)?; + arena.rewrite(exprs, plan_arena, Some(&tuple))?; let mut state = arena.local_state(plan_arena); - let (values, transaction, table_codec) = state.index_values_transaction_codec_mut(); + let (values, transaction, table_codec) = state.tuple_transaction_codec_mut(); let index = Index::new(*index_id, values, *index_ty); transaction.del_index(table_codec, self.table_name, &index, tuple_id)?; } @@ -107,7 +107,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Delete<'a> { deleted_count += 1; } - arena.produce_tuple(TupleBuilder::build_result(deleted_count.to_string())); + arena.produce(TupleBuilder::build_result(deleted_count.to_string()).into()); arena.resume(); Ok(()) } diff --git a/src/execution/dml/insert.rs b/src/execution/dml/insert.rs index aa4987d5..0d274505 100644 --- a/src/execution/dml/insert.rs +++ b/src/execution/dml/insert.rs @@ -23,7 +23,7 @@ use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::Transaction; use crate::types::index::Index; -use crate::types::tuple::{Schema, Tuple}; +use crate::types::tuple::{Schema, Tuple, TupleImpl}; use crate::types::tuple_builder::TupleBuilder; use crate::types::value::DataValue; use crate::types::ColumnId; @@ -121,16 +121,22 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Insert<'a> { .iter() .map(|column| plan_arena.column(*column).datatype().serializable()) .collect_vec(); - let mut tuple = Tuple::new(None, Vec::with_capacity(table_snapshot.columns_len)); + let mut tuple = TupleImpl::from(Tuple::new( + None, + Vec::with_capacity(table_snapshot.columns_len), + )); while arena.next_tuple(input, plan_arena)? { let mut tuple_map = HashMap::with_capacity(self.input_schema.len()); - for (i, value) in arena.result_tuple_mut().values.drain(..).enumerate() { + let mut i = 0; + arena.result_mut().drain_values(|value| { let column = plan_arena.column(self.input_schema[i]); tuple_map.insert(Self::column_key(column, self.is_mapping_by_name), value); - } + i += 1; + }); - tuple.values.clear(); + let row = tuple.output_mut(); + row.values.clear(); for column in table_snapshot.columns.iter() { let column = plan_arena.column(*column); let value = match tuple_map @@ -144,19 +150,18 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Insert<'a> { if value.is_null() && !column.nullable() { return Err(DatabaseError::not_null_column(column.name().to_string())); } - tuple.values.push(value) + row.values.push(value) } - tuple.pk = Some(Tuple::primary_projection( + row.pk = Some(Tuple::primary_projection( table_snapshot.primary_key_indices, - &tuple.values, + row.values.as_slice(), )); for (index_meta, exprs) in table_snapshot.index_metas.iter() { let index_meta = plan_arena.index(*index_meta); - let tuple_id = tuple.pk.as_ref().ok_or(DatabaseError::PrimaryKeyNotFound)?; - arena.rewrite(exprs, plan_arena, Some(&tuple), None)?; + let tuple_id = tuple.pk().ok_or(DatabaseError::PrimaryKeyNotFound)?; + arena.rewrite(exprs, plan_arena, Some(&tuple))?; let mut state = arena.local_state(plan_arena); - let (values, transaction, table_codec) = - state.index_values_transaction_codec_mut(); + let (values, transaction, table_codec) = state.tuple_transaction_codec_mut(); let index = Index::new(index_meta.id, values, index_meta.ty); transaction.add_index(table_codec, self.table_name, index, tuple_id)?; } @@ -172,7 +177,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Insert<'a> { inserted_count += 1; } } - arena.produce_tuple(TupleBuilder::build_result(inserted_count.to_string())); + arena.produce(TupleBuilder::build_result(inserted_count.to_string()).into()); arena.resume(); Ok(()) } diff --git a/src/execution/dml/update.rs b/src/execution/dml/update.rs index b20106db..8fdd680e 100644 --- a/src/execution/dml/update.rs +++ b/src/execution/dml/update.rs @@ -136,8 +136,8 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Update<'a> { while arena.next_tuple(input, plan_arena)? { let mut is_overwrite = true; - let mut tuple = arena.materialize_tuple(); - let Some(old_pk) = tuple.pk.clone() else { + let mut tuple = arena.take_tuple(); + let Some(old_pk) = tuple.pk().cloned() else { continue; }; @@ -151,10 +151,9 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Update<'a> { continue; } - arena.rewrite(exprs, plan_arena, Some(&tuple), None)?; + arena.rewrite(exprs, plan_arena, Some(&tuple))?; let mut state = arena.local_state(plan_arena); - let (values, transaction, table_codec) = - state.index_values_transaction_codec_mut(); + let (values, transaction, table_codec) = state.tuple_transaction_codec_mut(); let old_index = Index::new(index_meta.id, values, index_meta.ty); transaction.del_index(table_codec, self.table_name, &old_index, &old_pk)?; } @@ -166,12 +165,11 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Update<'a> { let value = plan_arena .expression(*expr) .eval(plan_arena, Some(&tuple))?; - tuple.values[i] = value.into_owned(); + tuple[i] = value.into_owned(); } } - let new_pk = - Tuple::primary_projection(table_snapshot.primary_key_indices, &tuple.values); + let new_pk = Tuple::primary_projection(table_snapshot.primary_key_indices, &tuple); let primary_key_changed = new_pk != old_pk; if primary_key_changed { let mut state = arena.local_state(plan_arena); @@ -189,15 +187,14 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Update<'a> { ) { continue; } - arena.rewrite(exprs, plan_arena, Some(&tuple), None)?; + arena.rewrite(exprs, plan_arena, Some(&tuple))?; let mut state = arena.local_state(plan_arena); - let (values, transaction, table_codec) = - state.index_values_transaction_codec_mut(); + let (values, transaction, table_codec) = state.tuple_transaction_codec_mut(); let new_index = Index::new(index_meta.id, values, index_meta.ty); transaction.add_index(table_codec, self.table_name, new_index, &new_pk)?; } - tuple.pk = Some(new_pk); + tuple.set_pk(Some(new_pk)); let mut state = arena.local_state(plan_arena); let (transaction, table_codec) = state.transaction_codec_mut(); let stamp = if is_overwrite { 0 } else { table_codec.stamp() }; @@ -213,7 +210,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Update<'a> { updated_count += 1; } } - arena.produce_tuple(TupleBuilder::build_result(updated_count.to_string())); + arena.produce(TupleBuilder::build_result(updated_count.to_string()).into()); arena.resume(); Ok(()) } diff --git a/src/execution/dql/aggregate/hash_agg.rs b/src/execution/dql/aggregate/hash_agg.rs index 348f56f2..5864dece 100644 --- a/src/execution/dql/aggregate/hash_agg.rs +++ b/src/execution/dql/aggregate/hash_agg.rs @@ -113,7 +113,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for HashAggExecutor<'a> { return Ok(()); }; - write_aggregate_output(arena.result_tuple_mut(), accs, group_keys)?; + write_aggregate_output(arena.output_tuple_mut(), accs, group_keys)?; arena.resume(); Ok(()) } @@ -234,7 +234,10 @@ mod test { assert_eq!(tuples.len(), 2); - let vec_values = tuples.into_iter().map(|tuple| tuple.values).collect_vec(); + let vec_values = tuples + .into_iter() + .map(|mut tuple| tuple.take_values()) + .collect_vec(); assert!(vec_values.contains(&build_integers(vec![Some(3), Some(0)]))); assert!(vec_values.contains(&build_integers(vec![Some(5), Some(1)]))); diff --git a/src/execution/dql/aggregate/simple_agg.rs b/src/execution/dql/aggregate/simple_agg.rs index 180770a1..a975da0e 100644 --- a/src/execution/dql/aggregate/simple_agg.rs +++ b/src/execution/dql/aggregate/simple_agg.rs @@ -90,7 +90,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for SimpleAggExecutor<'a> { } } - let output = arena.result_tuple_mut(); + let output = arena.output_tuple_mut(); output.pk = None; output.values.clear(); output.values.reserve(accs.len()); diff --git a/src/execution/dql/aggregate/stream_agg.rs b/src/execution/dql/aggregate/stream_agg.rs index 9f57b614..12768256 100644 --- a/src/execution/dql/aggregate/stream_agg.rs +++ b/src/execution/dql/aggregate/stream_agg.rs @@ -87,7 +87,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for StreamAggExecutor<'a> { return Ok(()); }; write_aggregate_output( - arena.result_tuple_mut(), + arena.output_tuple_mut(), mem::take(&mut self.accs), group_keys, )?; @@ -120,7 +120,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for StreamAggExecutor<'a> { update_accumulators(&mut next_accs, self.agg_calls, tuple, plan_arena)?; mem::swap(current_keys, &mut group_keys); let current_accs = mem::replace(&mut self.accs, next_accs); - write_aggregate_output(arena.result_tuple_mut(), current_accs, group_keys)?; + write_aggregate_output(arena.output_tuple_mut(), current_accs, group_keys)?; arena.resume(); return Ok(()); } @@ -206,7 +206,9 @@ mod tests { ))?; assert_eq!( - rows.into_iter().map(|row| row.values).collect::>(), + rows.into_iter() + .map(|mut row| row.take_values()) + .collect::>(), vec![ vec![30.into(), 2.into(), 1.into()], vec![12.into(), 2.into(), 2.into()], diff --git a/src/execution/dql/aggregate/stream_distinct.rs b/src/execution/dql/aggregate/stream_distinct.rs index 79375b9b..5d946d4c 100644 --- a/src/execution/dql/aggregate/stream_distinct.rs +++ b/src/execution/dql/aggregate/stream_distinct.rs @@ -21,13 +21,13 @@ use crate::planner::ExprRef; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::Transaction; -use crate::types::tuple::Tuple; +use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; pub struct StreamDistinctExecutor<'a> { groupby_exprs: &'a [ExprRef], input: ExecId, - last_keys: Option, + last_keys: Option, } impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for StreamDistinctExecutor<'a> { @@ -67,24 +67,24 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for StreamDistinctExecutor<'a> loop { if !arena.next_tuple(self.input, plan_arena)? { if let Some(last_keys) = self.last_keys.take() { - drop(std::mem::replace(arena.result_tuple_mut(), last_keys)); + drop(std::mem::replace(arena.result_mut(), last_keys)); arena.resume(); } else { arena.finish(); } return Ok(()); } - arena.rewrite(self.groupby_exprs, plan_arena, None::<&[DataValue]>, None)?; + arena.rewrite(self.groupby_exprs, plan_arena, None::<&[DataValue]>)?; if let Some(last_keys) = &mut self.last_keys { - if last_keys.values == arena.result_tuple().values { + if last_keys.values_eq(arena.result_tuple()) { continue; } - std::mem::swap(last_keys, arena.result_tuple_mut()); + std::mem::swap(last_keys, arena.result_mut()); arena.resume(); return Ok(()); } - self.last_keys = Some(arena.materialize_tuple()); + self.last_keys = Some(arena.take_tuple()); } } } @@ -195,7 +195,7 @@ mod tests { let actual = tuples .into_iter() - .flat_map(|tuple| tuple.values) + .flat_map(|mut tuple| tuple.take_values()) .flat_map(|value| value.i32()) .collect_vec(); assert_eq!(actual, vec![1, 2, 3]); @@ -251,7 +251,10 @@ mod tests { &transaction, ))?; - let actual = tuples.into_iter().map(|tuple| tuple.values).collect_vec(); + let actual = tuples + .into_iter() + .map(|mut tuple| tuple.take_values()) + .collect_vec(); assert_eq!( actual, vec![ diff --git a/src/execution/dql/describe.rs b/src/execution/dql/describe.rs index fa581663..77ee8470 100644 --- a/src/execution/dql/describe.rs +++ b/src/execution/dql/describe.rs @@ -109,7 +109,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Describe { let column = plan_arena.column(column_ref); let default = describe_default(column, plan_arena); - let output = arena.result_tuple_mut(); + let output = arena.output_tuple_mut(); output.pk = None; output.values.clear(); fill_describe_row(&mut output.values, column, default); diff --git a/src/execution/dql/dummy.rs b/src/execution/dql/dummy.rs index 5070bf70..163d3150 100644 --- a/src/execution/dql/dummy.rs +++ b/src/execution/dql/dummy.rs @@ -64,7 +64,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Dummy { arena.finish(); return Ok(()); }; - arena.produce_tuple(row); + arena.produce(row.into()); Ok(()) } } diff --git a/src/execution/dql/explain.rs b/src/execution/dql/explain.rs index 0922ffb8..d7582ec5 100644 --- a/src/execution/dql/explain.rs +++ b/src/execution/dql/explain.rs @@ -63,7 +63,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Explain<'a> { } let plan = self.plan.explain(plan_arena, 0); - let output = arena.result_tuple_mut(); + let output = arena.output_tuple_mut(); output.pk = None; output.values.clear(); output.values.push(DataValue::Utf8 { diff --git a/src/execution/dql/external_sort.rs b/src/execution/dql/external_sort.rs index 8c8b44bb..c3992f2a 100644 --- a/src/execution/dql/external_sort.rs +++ b/src/execution/dql/external_sort.rs @@ -90,7 +90,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ExternalSort<'a> { return Ok(()); }; plan_arena.restore_outer_values(row.outer_values); - arena.produce_tuple(row.tuple); + arena.produce(row.tuple); return Ok(()); } @@ -103,7 +103,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ExternalSort<'a> { let mut rows = SpillVec::new().on_flush(move |_, rows| sort_segment(sort_fields, rows)); let mut runs = Vec::new(); while arena.next_tuple(self.input, plan_arena)? { - let tuple = arena.materialize_tuple(); + let tuple = arena.take_tuple(); let row = SortRow::new(sort_fields, tuple, plan_arena)?; if let Some(segment) = rows.push(row)? { runs.push(Run::new(segment, 1)); @@ -298,7 +298,7 @@ mod test { use crate::expression::ScalarExpression; use crate::planner::operator::sort::SortField; use crate::planner::{ExecMetaArena, MetaArena}; - use crate::types::tuple::Tuple; + use crate::types::tuple::{Tuple, TupleLike}; use crate::types::value::{DataValue, Utf8Type}; use crate::types::{CharLengthUnits, LogicalType}; use std::cmp::Ordering; @@ -328,13 +328,13 @@ mod test { for value in [DataValue::Int32(2), DataValue::Null, DataValue::Int32(1)] { let _ = rows.push(SortRow::new( &sort_fields, - Tuple::new(None, vec![value]), + Tuple::new(None, vec![value]).into(), &mut plan_arena, )?)?; } let values = finish_sort(rows, Vec::new(), &sort_fields, 2)? - .map(|row| row.map(|row| row.tuple.values[0].clone())) + .map(|row| row.map(|row| row.tuple.value_at(0).clone())) .collect::, _>>()?; assert_eq!( values, @@ -391,7 +391,7 @@ mod test { unit: CharLengthUnits::Characters, }, ); - let row = SortRow::new(&sort_fields, tuple, &mut plan_arena)?; + let row = SortRow::new(&sort_fields, tuple.into(), &mut plan_arena)?; if let Some(segment) = rows.push(row)? { runs.push(Run::new(segment, 1)); } @@ -401,7 +401,7 @@ mod test { .map(|row| { row.map(|row| { assert_eq!(row.outer_values.len(), 1); - let sequence = row.tuple.values[1].i32().unwrap(); + let sequence = row.tuple.value_at(1).i32().unwrap(); assert_eq!(row.outer_values[0].0.pos, sequence as usize); assert_eq!( row.outer_values[0].1.utf8(), @@ -413,7 +413,7 @@ mod test { .collect::, _>>()?; let positions = tuples .iter() - .map(|tuple| tuple.values[1].clone()) + .map(|tuple| tuple.value_at(1).clone()) .collect::>(); assert_eq!( positions, @@ -429,8 +429,8 @@ mod test { DataValue::Int32(6), ] ); - assert_eq!(tuples[0].pk, Some(DataValue::Int32(1))); - assert_eq!(tuples[1].pk, Some(DataValue::Int32(5))); + assert_eq!(tuples[0].pk().cloned(), Some(DataValue::Int32(1))); + assert_eq!(tuples[1].pk().cloned(), Some(DataValue::Int32(5))); Ok(()) } @@ -481,7 +481,9 @@ mod test { }; let sequence = DataValue::Int32(sequence as i32); let tuple = Tuple::new(Some(sequence.clone()), vec![key, sequence]); - if let Some(segment) = rows.push(SortRow::new(&sort_fields, tuple, &mut plan_arena)?)? { + if let Some(segment) = + rows.push(SortRow::new(&sort_fields, tuple.into(), &mut plan_arena)?)? + { runs.push(Run::new(segment, 1)); } } @@ -503,8 +505,8 @@ mod test { let actual = tuples .iter() .map(|tuple| { - assert_eq!(tuple.pk.as_ref(), tuple.values.get(1)); - tuple.values[1].clone() + assert_eq!(tuple.pk(), Some(tuple.value_at(1))); + tuple.value_at(1).clone() }) .collect::>(); assert_eq!( diff --git a/src/execution/dql/function_scan.rs b/src/execution/dql/function_scan.rs index f2c4cb4d..71185a65 100644 --- a/src/execution/dql/function_scan.rs +++ b/src/execution/dql/function_scan.rs @@ -74,7 +74,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for FunctionScan<'a> { arena.finish(); return Ok(()); }; - arena.produce_tuple(tuple); + arena.produce(tuple.into()); Ok(()) } } diff --git a/src/execution/dql/index_scan.rs b/src/execution/dql/index_scan.rs index 95319bb2..6532d347 100644 --- a/src/execution/dql/index_scan.rs +++ b/src/execution/dql/index_scan.rs @@ -144,7 +144,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for IndexScan<'a, T> { }; let state = arena.local_state(plan_arena); - if iter.next_tuple_into(state.table_codec, &mut state.result.tuple)? { + if iter.next_tuple_into(state.table_codec, state.result.tuple.output_mut())? { arena.resume(); } else { arena.finish(); @@ -194,7 +194,8 @@ mod tests { DataValue::Boolean(true), DataValue::Int32(id * 10), ], - ), + ) + .into(), &serializers, false, )?; @@ -228,7 +229,7 @@ mod tests { max: Bound::Included(DataValue::Int32(4)), }); scan.next_tuple(&mut arena, &mut meta)?; - assert_eq!(arena.result_tuple().values[0], DataValue::Int32(1)); + assert_eq!(arena.result_tuple()[0], DataValue::Int32(1)); let iterator = scan.iter.as_ref().unwrap() as *const _; for (probe, expected) in [ (RuntimeIndexProbe::Eq(DataValue::Int32(3)), vec![3]), @@ -255,7 +256,7 @@ mod tests { assert!(arena.runtime_probe_stack.is_empty()); for id in expected { scan.next_tuple(&mut arena, &mut meta)?; - assert_eq!(arena.result_tuple().values[0], DataValue::Int32(id)); + assert_eq!(arena.result_tuple()[0], DataValue::Int32(id)); assert_eq!(scan.iter.as_ref().unwrap() as *const _, iterator); } scan.next_tuple(&mut arena, &mut meta)?; diff --git a/src/execution/dql/join/hash/full_join.rs b/src/execution/dql/join/hash/full_join.rs index 50860871..95c1aa17 100644 --- a/src/execution/dql/join/hash/full_join.rs +++ b/src/execution/dql/join/hash/full_join.rs @@ -14,13 +14,13 @@ use crate::errors::DatabaseError; use crate::execution::dql::join::hash::{ - filter, JoinProbeState, LeftDropState, LeftDropTuples, ProbeState, + filter, join_rows, null_left_row, JoinProbeState, LeftDropState, LeftDropTuples, ProbeState, }; use crate::execution::dql::join::hash_join::BuildState; use crate::planner::ExprRef; use crate::planner::MetaArena; use crate::types::bitset::BitSet; -use crate::types::tuple::{SplitTupleRef, Tuple}; +use crate::types::tuple::{SplitTupleRef, TupleImpl}; use crate::types::value::DataValue; pub(crate) struct FullJoinState { @@ -36,7 +36,7 @@ impl JoinProbeState for FullJoinState { build_state: Option<&mut BuildState>, filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { if probe_state.is_keys_has_null { if probe_state.emitted_unmatched { probe_state.finished = true; @@ -44,7 +44,7 @@ impl JoinProbeState for FullJoinState { } probe_state.emitted_unmatched = true; probe_state.finished = true; - return Ok(Some(Self::take_full_right_row( + return Ok(Some(null_left_row( self.left_schema_len, &mut probe_state.probe_tuple, ))); @@ -57,41 +57,35 @@ impl JoinProbeState for FullJoinState { } probe_state.emitted_unmatched = true; probe_state.finished = true; - return Ok(Some(Self::take_full_right_row( + return Ok(Some(null_left_row( self.left_schema_len, &mut probe_state.probe_tuple, ))); }; while probe_state.index < build_state.tuples.len() { - let (i, Tuple { values, pk }) = &build_state.tuples[probe_state.index]; + let (i, build_tuple) = &build_state.tuples[probe_state.index]; probe_state.index += 1; if let Some(filter_expr) = filter_expr { - let full_values = - SplitTupleRef::from_slices(values, &probe_state.probe_tuple.values); + let full_values = SplitTupleRef::new(build_tuple, &probe_state.probe_tuple); if !filter(&full_values, filter_expr, plan_arena)? { continue; } } - let full_values = Vec::from_iter( - values - .iter() - .chain(probe_state.probe_tuple.values.iter()) - .cloned(), - ); self.bits.insert(*i); probe_state.produced = true; - return Ok(Some(Tuple::new( - pk.as_ref().or(probe_state.probe_tuple.pk.as_ref()).cloned(), - full_values, + return Ok(Some(join_rows( + build_tuple, + &mut probe_state.probe_tuple, + false, ))); } probe_state.finished = true; if !probe_state.produced && !probe_state.emitted_unmatched { probe_state.emitted_unmatched = true; - return Ok(Some(Self::take_full_right_row( + return Ok(Some(null_left_row( self.left_schema_len, &mut probe_state.probe_tuple, ))); @@ -104,7 +98,7 @@ impl JoinProbeState for FullJoinState { left_drop_state: &mut LeftDropState, _filter_expr: Option<&ExprRef>, _plan_arena: &(dyn MetaArena + '_), - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { let full_schema_len = self.right_schema_len + self.left_schema_len; loop { @@ -113,7 +107,7 @@ impl JoinProbeState for FullJoinState { if self.bits.contains(i) { continue; } - left_tuple.values.resize(full_schema_len, DataValue::Null); + left_tuple.resize(full_schema_len, DataValue::Null); return Ok(Some(left_tuple)); } left_drop_state.current = None; @@ -129,14 +123,3 @@ impl JoinProbeState for FullJoinState { } } } - -impl FullJoinState { - pub(crate) fn take_full_right_row(left_schema_len: usize, probe_tuple: &mut Tuple) -> Tuple { - let mut tuple = std::mem::take(probe_tuple); - tuple - .values - .resize(tuple.values.len() + left_schema_len, DataValue::Null); - tuple.values.rotate_right(left_schema_len); - tuple - } -} diff --git a/src/execution/dql/join/hash/inner_join.rs b/src/execution/dql/join/hash/inner_join.rs index 1a097dc6..10d168d1 100644 --- a/src/execution/dql/join/hash/inner_join.rs +++ b/src/execution/dql/join/hash/inner_join.rs @@ -13,11 +13,11 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::execution::dql::join::hash::{filter, JoinProbeState, ProbeState}; +use crate::execution::dql::join::hash::{filter, join_rows, JoinProbeState, ProbeState}; use crate::execution::dql::join::hash_join::BuildState; use crate::planner::ExprRef; use crate::planner::MetaArena; -use crate::types::tuple::{SplitTupleRef, Tuple}; +use crate::types::tuple::{SplitTupleRef, TupleImpl}; pub(crate) struct InnerJoinState; @@ -28,7 +28,7 @@ impl JoinProbeState for InnerJoinState { build_state: Option<&mut BuildState>, filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { if probe_state.is_keys_has_null { probe_state.finished = true; return Ok(None); @@ -48,20 +48,15 @@ impl JoinProbeState for InnerJoinState { continue; } } - let probe = &mut probe_state.probe_tuple; - let mut values = Vec::with_capacity(build_tuple.values.len() + probe.values.len()); - values.extend_from_slice(&build_tuple.values); - let mut pk = build_tuple.pk.clone(); - if probe_state.index == build_state.tuples.len() { - // No future match can read this probe row, so move its values and pk. + let is_last = probe_state.index == build_state.tuples.len(); + if is_last { probe_state.finished = true; - values.append(&mut probe.values); - pk = pk.or(probe.pk.take()); - } else { - values.extend_from_slice(&probe.values); - pk = pk.or_else(|| probe.pk.clone()); } - return Ok(Some(Tuple::new(pk, values))); + return Ok(Some(join_rows( + build_tuple, + &mut probe_state.probe_tuple, + is_last, + ))); } probe_state.finished = true; diff --git a/src/execution/dql/join/hash/left_join.rs b/src/execution/dql/join/hash/left_join.rs index 0d30df2a..5933732e 100644 --- a/src/execution/dql/join/hash/left_join.rs +++ b/src/execution/dql/join/hash/left_join.rs @@ -14,13 +14,13 @@ use crate::errors::DatabaseError; use crate::execution::dql::join::hash::{ - filter, JoinProbeState, LeftDropState, LeftDropTuples, ProbeState, + filter, join_rows, JoinProbeState, LeftDropState, LeftDropTuples, ProbeState, }; use crate::execution::dql::join::hash_join::BuildState; use crate::planner::ExprRef; use crate::planner::MetaArena; use crate::types::bitset::BitSet; -use crate::types::tuple::{SplitTupleRef, Tuple}; +use crate::types::tuple::{SplitTupleRef, TupleImpl}; use crate::types::value::DataValue; pub(crate) struct LeftJoinState { @@ -36,7 +36,7 @@ impl JoinProbeState for LeftJoinState { build_state: Option<&mut BuildState>, filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { if probe_state.is_keys_has_null { probe_state.finished = true; return Ok(None); @@ -48,26 +48,20 @@ impl JoinProbeState for LeftJoinState { }; while probe_state.index < build_state.tuples.len() { - let (i, Tuple { values, pk }) = &build_state.tuples[probe_state.index]; + let (i, build_tuple) = &build_state.tuples[probe_state.index]; probe_state.index += 1; if let Some(filter_expr) = filter_expr { - let full_values = - SplitTupleRef::from_slices(values, &probe_state.probe_tuple.values); + let full_values = SplitTupleRef::new(build_tuple, &probe_state.probe_tuple); if !filter(&full_values, filter_expr, plan_arena)? { continue; } } - let full_values = Vec::from_iter( - values - .iter() - .chain(probe_state.probe_tuple.values.iter()) - .cloned(), - ); self.bits.insert(*i); - return Ok(Some(Tuple::new( - pk.as_ref().or(probe_state.probe_tuple.pk.as_ref()).cloned(), - full_values, + return Ok(Some(join_rows( + build_tuple, + &mut probe_state.probe_tuple, + false, ))); } @@ -80,7 +74,7 @@ impl JoinProbeState for LeftJoinState { left_drop_state: &mut LeftDropState, _filter_expr: Option<&ExprRef>, _plan_arena: &(dyn MetaArena + '_), - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { let full_schema_len = self.right_schema_len + self.left_schema_len; loop { @@ -89,7 +83,7 @@ impl JoinProbeState for LeftJoinState { if self.bits.contains(i) { continue; } - left_tuple.values.resize(full_schema_len, DataValue::Null); + left_tuple.resize(full_schema_len, DataValue::Null); return Ok(Some(left_tuple)); } left_drop_state.current = None; diff --git a/src/execution/dql/join/hash/mod.rs b/src/execution/dql/join/hash/mod.rs index 6f22e876..0dffd829 100644 --- a/src/execution/dql/join/hash/mod.rs +++ b/src/execution/dql/join/hash/mod.rs @@ -26,13 +26,13 @@ use crate::execution::dql::join::hash_join::BuildState; use crate::execution::dql::sort::BumpVec; use crate::planner::ExprRef; use crate::planner::MetaArena; -use crate::types::tuple::{Tuple, TupleLike}; +use crate::types::tuple::{Tuple, TupleImpl, TupleLike}; use crate::types::value::DataValue; use std::collections::hash_map::IntoIter as HashMapIntoIter; pub(crate) struct ProbeState { pub(crate) is_keys_has_null: bool, - pub(crate) probe_tuple: Tuple, + pub(crate) probe_tuple: TupleImpl, pub(crate) index: usize, pub(crate) produced: bool, pub(crate) finished: bool, @@ -45,7 +45,7 @@ pub(crate) struct LeftDropState { } pub(crate) struct LeftDropTuples { - pub(crate) tuples: std::vec::IntoIter<(usize, Tuple)>, + pub(crate) tuples: std::vec::IntoIter<(usize, TupleImpl)>, } pub(crate) trait JoinProbeState { @@ -55,14 +55,14 @@ pub(crate) trait JoinProbeState { build_state: Option<&mut BuildState>, filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), - ) -> Result, DatabaseError>; + ) -> Result, DatabaseError>; fn left_drop_next( &mut self, _left_drop_state: &mut LeftDropState, _filter_expr: Option<&ExprRef>, _plan_arena: &(dyn MetaArena + '_), - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { Ok(None) } } @@ -81,7 +81,7 @@ impl JoinProbeState for JoinProbeStateImpl { build_state: Option<&mut BuildState>, filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { match self { JoinProbeStateImpl::Inner(state) => { state.probe_next(probe_state, build_state, filter_expr, plan_arena) @@ -103,7 +103,7 @@ impl JoinProbeState for JoinProbeStateImpl { left_drop_state: &mut LeftDropState, filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { match self { JoinProbeStateImpl::Inner(state) => { state.left_drop_next(left_drop_state, filter_expr, plan_arena) @@ -135,3 +135,26 @@ pub(crate) fn filter( _ => Err(DatabaseError::InvalidType), } } + +pub(crate) fn join_rows( + build: &TupleImpl, + probe: &mut TupleImpl, + consume_probe: bool, +) -> TupleImpl { + let mut probe = if consume_probe { + std::mem::take(probe) + } else { + probe.clone() + }; + let pk = build.pk().cloned().or_else(|| probe.take_pk()); + TupleImpl::concat(build.clone(), probe, pk) +} + +pub(crate) fn null_left_row(left_len: usize, probe: &mut TupleImpl) -> TupleImpl { + let pk = probe.take_pk(); + TupleImpl::concat( + Tuple::new(None, vec![DataValue::Null; left_len]).into(), + std::mem::take(probe), + pk, + ) +} diff --git a/src/execution/dql/join/hash/right_join.rs b/src/execution/dql/join/hash/right_join.rs index cee9034d..1631b06d 100644 --- a/src/execution/dql/join/hash/right_join.rs +++ b/src/execution/dql/join/hash/right_join.rs @@ -13,12 +13,13 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::execution::dql::join::hash::full_join::FullJoinState; -use crate::execution::dql::join::hash::{filter, JoinProbeState, ProbeState}; +use crate::execution::dql::join::hash::{ + filter, join_rows, null_left_row, JoinProbeState, ProbeState, +}; use crate::execution::dql::join::hash_join::BuildState; use crate::planner::ExprRef; use crate::planner::MetaArena; -use crate::types::tuple::{SplitTupleRef, Tuple}; +use crate::types::tuple::{SplitTupleRef, TupleImpl}; pub(crate) struct RightJoinState { pub(crate) left_schema_len: usize, @@ -31,7 +32,7 @@ impl JoinProbeState for RightJoinState { build_state: Option<&mut BuildState>, filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { if probe_state.is_keys_has_null { if probe_state.emitted_unmatched { probe_state.finished = true; @@ -39,7 +40,7 @@ impl JoinProbeState for RightJoinState { } probe_state.emitted_unmatched = true; probe_state.finished = true; - return Ok(Some(FullJoinState::take_full_right_row( + return Ok(Some(null_left_row( self.left_schema_len, &mut probe_state.probe_tuple, ))); @@ -52,40 +53,34 @@ impl JoinProbeState for RightJoinState { } probe_state.emitted_unmatched = true; probe_state.finished = true; - return Ok(Some(FullJoinState::take_full_right_row( + return Ok(Some(null_left_row( self.left_schema_len, &mut probe_state.probe_tuple, ))); }; while probe_state.index < build_state.tuples.len() { - let (_, Tuple { values, pk }) = &build_state.tuples[probe_state.index]; + let (_, build_tuple) = &build_state.tuples[probe_state.index]; probe_state.index += 1; if let Some(filter_expr) = filter_expr { - let full_values = - SplitTupleRef::from_slices(values, &probe_state.probe_tuple.values); + let full_values = SplitTupleRef::new(build_tuple, &probe_state.probe_tuple); if !filter(&full_values, filter_expr, plan_arena)? { continue; } } - let full_values = Vec::from_iter( - values - .iter() - .chain(probe_state.probe_tuple.values.iter()) - .cloned(), - ); probe_state.produced = true; - return Ok(Some(Tuple::new( - pk.as_ref().or(probe_state.probe_tuple.pk.as_ref()).cloned(), - full_values, + return Ok(Some(join_rows( + build_tuple, + &mut probe_state.probe_tuple, + false, ))); } if !probe_state.produced && !probe_state.emitted_unmatched { probe_state.emitted_unmatched = true; probe_state.finished = true; - return Ok(Some(FullJoinState::take_full_right_row( + return Ok(Some(null_left_row( self.left_schema_len, &mut probe_state.probe_tuple, ))); diff --git a/src/execution/dql/join/hash_join.rs b/src/execution/dql/join/hash_join.rs index 2544d37e..3e8793ae 100644 --- a/src/execution/dql/join/hash_join.rs +++ b/src/execution/dql/join/hash_join.rs @@ -30,7 +30,7 @@ use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::Transaction; use crate::types::bitset::BitSet; -use crate::types::tuple::Tuple; +use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; use bumpalo::Bump; use std::collections::HashMap; @@ -127,7 +127,7 @@ impl HashJoin { fn eval_keys( on_keys: &[ExprRef], - tuple: &Tuple, + tuple: &TupleImpl, build_buf: &mut BumpVec<'_, DataValue>, plan_arena: &(dyn MetaArena + '_), ) -> Result<(), DatabaseError> { @@ -163,7 +163,7 @@ impl HashJoin { let mut build_count = 0usize; while arena.next_tuple(self.left_input, plan_arena)? { - let tuple = arena.materialize_tuple(); + let tuple = arena.take_tuple(); Self::eval_keys(&self.on_left_keys, &tuple, &mut build_buf, plan_arena)?; match build_map.get_mut(&build_buf) { @@ -226,7 +226,7 @@ impl HashJoin { // LMDB PK-sized hash join plus allocator consolidation; allocate build rows in `bump` instead. #[derive(Default, Debug)] pub(crate) struct BuildState { - pub(crate) tuples: Vec<(usize, Tuple)>, + pub(crate) tuples: Vec<(usize, TupleImpl)>, } impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for HashJoin { @@ -292,7 +292,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for HashJoin { if !arena.next_tuple(self.right_input, plan_arena)? { break true; } - let tuple = arena.materialize_tuple(); + let tuple = arena.take_tuple(); Self::eval_keys( &self.on_right_keys, &tuple, @@ -333,7 +333,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for HashJoin { probe_buf, probe_state, }; - arena.produce_tuple(tuple); + arena.produce(tuple); return Ok(()); } @@ -364,7 +364,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for HashJoin { join_impl, left_drop, }; - arena.produce_tuple(tuple); + arena.produce(tuple); return Ok(()); } state = HashJoinState::End; @@ -542,15 +542,15 @@ mod test { assert_eq!(tuples.len(), 3); assert_eq!( - tuples[0].values, + tuples[0], build_integers(vec![Some(0), Some(2), Some(4), Some(0), Some(2), Some(4)]) ); assert_eq!( - tuples[1].values, + tuples[1], build_integers(vec![Some(1), Some(3), Some(5), Some(1), Some(3), Some(5)]) ); assert_eq!( - tuples[2].values, + tuples[2], build_integers(vec![Some(1), Some(3), Some(5), Some(1), Some(1), Some(1)]) ); @@ -603,19 +603,19 @@ mod test { assert_eq!(tuples.len(), 4); assert_eq!( - tuples[0].values, + tuples[0], build_integers(vec![Some(0), Some(2), Some(4), Some(0), Some(2), Some(4)]) ); assert_eq!( - tuples[1].values, + tuples[1], build_integers(vec![Some(1), Some(3), Some(5), Some(1), Some(3), Some(5)]) ); assert_eq!( - tuples[2].values, + tuples[2], build_integers(vec![Some(1), Some(3), Some(5), Some(1), Some(1), Some(1)]) ); assert_eq!( - tuples[3].values, + tuples[3], build_integers(vec![Some(3), Some(5), Some(7), None, None, None]) ); } @@ -669,19 +669,19 @@ mod test { assert_eq!(tuples.len(), 4); assert_eq!( - tuples[0].values, + tuples[0], build_integers(vec![Some(0), Some(2), Some(4), Some(0), Some(2), Some(4)]) ); assert_eq!( - tuples[1].values, + tuples[1], build_integers(vec![Some(1), Some(3), Some(5), Some(1), Some(3), Some(5)]) ); assert_eq!( - tuples[2].values, + tuples[2], build_integers(vec![None, None, None, Some(4), Some(6), Some(8)]) ); assert_eq!( - tuples[3].values, + tuples[3], build_integers(vec![Some(1), Some(3), Some(5), Some(1), Some(1), Some(1)]) ); @@ -778,7 +778,7 @@ mod test { assert_eq!(tuples.len(), 1); assert_eq!( - tuples[0].values, + tuples[0], vec![ DataValue::Int32(2), DataValue::Int32(5), @@ -835,23 +835,23 @@ mod test { assert_eq!(tuples.len(), 5); assert_eq!( - tuples[0].values, + tuples[0], build_integers(vec![Some(0), Some(2), Some(4), Some(0), Some(2), Some(4)]) ); assert_eq!( - tuples[1].values, + tuples[1], build_integers(vec![Some(1), Some(3), Some(5), Some(1), Some(3), Some(5)]) ); assert_eq!( - tuples[2].values, + tuples[2], build_integers(vec![None, None, None, Some(4), Some(6), Some(8)]) ); assert_eq!( - tuples[3].values, + tuples[3], build_integers(vec![Some(1), Some(3), Some(5), Some(1), Some(1), Some(1)]) ); assert_eq!( - tuples[4].values, + tuples[4], build_integers(vec![Some(3), Some(5), Some(7), None, None, None]) ); diff --git a/src/execution/dql/join/nested_loop_join.rs b/src/execution/dql/join/nested_loop_join.rs index 00463816..f6fb6c16 100644 --- a/src/execution/dql/join/nested_loop_join.rs +++ b/src/execution/dql/join/nested_loop_join.rs @@ -26,7 +26,7 @@ use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::ExprRef; use crate::storage::Transaction; use crate::types::bitset::BitSet; -use crate::types::tuple::{SplitTupleRef, Tuple}; +use crate::types::tuple::{SplitTupleRef, Tuple, TupleImpl, TupleLike}; use crate::types::value::DataValue; /// Equivalent condition @@ -41,10 +41,10 @@ impl EqualCondition { /// Compare left tuple and right tuple on equivalent condition /// `left_tuple` must be from the [`NestedLoopJoin::left_input`] /// `right_tuple` must be from the [`NestedLoopJoin::right_input`] - fn equals( + fn equals( &self, - left_tuple: &Tuple, - right_tuple: &Tuple, + left_tuple: &L, + right_tuple: &R, arena: &(dyn MetaArena + '_), ) -> Result { if self.on_left_keys.is_empty() { @@ -100,7 +100,7 @@ enum NestedLoopJoinState { } struct ActiveLeftState { - left_tuple: Tuple, + left_tuple: TupleImpl, right_input: ExecId, right_index: usize, has_matched: bool, @@ -184,7 +184,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin { arena.finish(); return Ok(()); } - let left_tuple = arena.materialize_tuple(); + let left_tuple = arena.take_tuple(); state = NestedLoopJoinState::ScanRight { active_left: ActiveLeftState { @@ -230,11 +230,12 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin { active_left.has_matched = true; // A matched pair is always one output row, even when // neither side projects a column (e.g. `count(*)`). - Self::emit_tuple( - &active_left.left_tuple, - arena.result_tuple_mut(), + let joined = Self::emit_tuple( + active_left.left_tuple.clone(), + arena.take_tuple(), self.ty, ); + *arena.result_mut() = joined; if matches!(self.ty, JoinType::Full) { if let Some(bits) = right_bitmap.as_mut() { bits.insert(idx); @@ -269,11 +270,12 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin { JoinType::LeftOuter | JoinType::RightOuter | JoinType::Full if !active_left.has_matched => { - let right_tuple = arena.result_tuple_mut(); - right_tuple.pk = None; - right_tuple.values.clear(); - right_tuple.values.resize(right_schema_len, DataValue::Null); - Self::emit_tuple(&active_left.left_tuple, right_tuple, self.ty); + let nulls = Tuple::new(None, vec![DataValue::Null; right_schema_len]); + *arena.result_mut() = Self::emit_tuple( + std::mem::take(&mut active_left.left_tuple), + nulls.into(), + self.ty, + ); true } _ => false, @@ -301,9 +303,11 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin { }; if is_unmatched { - let values = &mut arena.result_tuple_mut().values; - values.resize(values.len() + self.eq_cond.left_len, DataValue::Null); - values.rotate_right(self.eq_cond.left_len); + let mut right = arena.take_tuple(); + let pk = right.take_pk(); + let nulls = + Tuple::new(None, vec![DataValue::Null; self.eq_cond.left_len]); + *arena.result_mut() = TupleImpl::concat(nulls.into(), right, pk); self.state = NestedLoopJoinState::EmitRightUnmatched { right_input, right_bitmap, @@ -340,21 +344,16 @@ impl NestedLoopJoin { /// Emit a tuple according to the join type. /// - /// `left_tuple`: retained outer tuple (logical right for RightOuter). - /// `right_tuple`: current inner tuple (logical left for RightOuter), rewritten in place. + /// `left`: retained outer tuple (logical right for RightOuter). + /// `right`: current inner tuple (logical left for RightOuter). /// `ty`: the type of join - fn emit_tuple(left_tuple: &Tuple, right_tuple: &mut Tuple, ty: JoinType) { - let right_len = right_tuple.values.len(); - right_tuple.values.extend(left_tuple.values.iter().cloned()); + fn emit_tuple(mut left: TupleImpl, mut right: TupleImpl, ty: JoinType) -> TupleImpl { if matches!(ty, JoinType::RightOuter) { - if right_tuple.pk.is_none() { - right_tuple.pk = left_tuple.pk.clone(); - } + let pk = right.take_pk().or_else(|| left.pk().cloned()); + TupleImpl::concat(right, left, pk) } else { - right_tuple.values.rotate_left(right_len); - if left_tuple.pk.is_some() { - right_tuple.pk = left_tuple.pk.clone(); - } + let pk = left.take_pk().or_else(|| right.take_pk()); + TupleImpl::concat(left, right, pk) } } } @@ -399,9 +398,8 @@ mod test { .find_best(None, arena) } - fn tuple_to_strings(tuple: &Tuple) -> Vec> { + fn tuple_to_strings(tuple: &TupleImpl) -> Vec> { tuple - .values .iter() .map(|value| match value { DataValue::Null => None, @@ -529,12 +527,11 @@ mod test { (on_keys, values_t1, values_t2, filter) } - fn valid_result(expected: &mut HashSet>, actual: &[Tuple]) { + fn valid_result(expected: &mut HashSet>, actual: &[TupleImpl]) { assert_eq!(actual.len(), expected.len()); for tuple in actual { let values = tuple - .values .iter() .map(|v| { if matches!(v, DataValue::Null) { @@ -587,7 +584,7 @@ mod test { let mut plan_arena = ExecMetaArena::new(plan_arena); while arena.next_tuple(root, &mut plan_arena)? { rows += 1; - assert_eq!(arena.result_tuple().values.len(), 9); + assert_eq!(arena.result_tuple().len(), 9); assert_eq!(arena.nodes.items.len(), count); assert_eq!(arena.nodes.items.as_ptr(), address); } @@ -695,7 +692,7 @@ mod test { let tuples = try_collect(executor)?; assert_eq!( - tuples[0].values, + tuples[0], build_integers(vec![Some(0), Some(2), Some(4), None, None, None]) ); @@ -1033,7 +1030,7 @@ mod test { let tuples = try_collect(executor)?; assert_eq!( - tuples[0].values, + tuples[0], build_integers(vec![Some(0), Some(2), Some(4), None, None, None]) ); @@ -1194,7 +1191,7 @@ mod test { assert_eq!(tuples.len(), 1); assert_eq!( - tuples[0].values, + tuples[0], vec![ DataValue::Int32(2), DataValue::Int32(5), diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index eae307b2..d83188b1 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -25,7 +25,7 @@ use crate::planner::{ExecMetaArena, ExprRef, LogicalPlan, MetaArena}; use crate::storage::Transaction; use crate::types::evaluator::BinaryEvaluatorRef; use crate::types::index::RuntimeIndexProbe; -use crate::types::tuple::{SplitTupleRef, Tuple}; +use crate::types::tuple::{SplitTupleRef, TupleImpl}; use crate::types::value::DataValue; use std::borrow::Cow; use std::cmp::Ordering; @@ -88,7 +88,10 @@ impl RightInput { arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, probe: Option, - mut evaluate: impl FnMut(&Tuple, &ExecMetaArena) -> Result, DatabaseError>, + mut evaluate: impl FnMut( + &TupleImpl, + &ExecMetaArena, + ) -> Result, DatabaseError>, ) -> Result, DatabaseError> { let root = self.reset(arena, plan_arena, probe)?; while arena.next_tuple(root, plan_arena)? { @@ -178,7 +181,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { arena.finish(); return Ok(()); } - let mut left = arena.materialize_tuple(); + let mut left = arena.take_tuple(); let (input, marker) = match self.op.guard { Some(guard) if *plan_arena.expression(guard).eval(plan_arena, Some(&left))? @@ -189,8 +192,8 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { _ => self.mark_value(input, arena, plan_arena, &left)?, }; self.right_input = Some(input); - left.values.push(marker); - arena.produce_tuple(left); + left.push(marker); + arena.produce(left); Ok(()) } } @@ -203,7 +206,7 @@ impl<'a> MarkApply<'a> { plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { while arena.next_tuple(self.left_input, plan_arena)? { - let mut left = arena.materialize_tuple(); + let mut left = arena.take_tuple(); let value = self.parameterized_probe_value(&left, plan_arena)?; let root = input.reset(arena, plan_arena, self.runtime_probe_for(value))?; // A full unique-key probe cannot have another match, so consume the left row. @@ -215,10 +218,9 @@ impl<'a> MarkApply<'a> { plan_arena, )? { - let right = arena.result_tuple_mut(); - left.values.append(&mut right.values); - left.pk = left.pk.or(right.pk.take()); - arena.produce_tuple(left); + let mut right = arena.take_tuple(); + let pk = left.take_pk().or_else(|| right.take_pk()); + arena.produce(TupleImpl::concat(left, right, pk)); return Ok(()); } } @@ -246,7 +248,7 @@ impl<'a> MarkApply<'a> { fn parameterized_probe_value( &self, - left_tuple: &Tuple, + left_tuple: &TupleImpl, plan_arena: &(dyn MetaArena + '_), ) -> Result, DatabaseError> { self.op @@ -265,7 +267,7 @@ impl<'a> MarkApply<'a> { mut input: RightInput, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, - left_tuple: &Tuple, + left_tuple: &TupleImpl, ) -> Result<(RightInput, DataValue), DatabaseError> { let probe = self.parameterized_probe_value(left_tuple, plan_arena)?; let value = match self.op.kind { @@ -359,7 +361,7 @@ impl<'a> MarkApply<'a> { root: ExecId, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, - left_tuple: &Tuple, + left_tuple: &TupleImpl, ) -> Result { let (_, _, right_expr, _) = Self::quantified_comparison(self.op.predicates(), plan_arena)?; arena.reset(root, plan_arena)?; @@ -395,7 +397,7 @@ impl<'a> MarkApply<'a> { keys: &MaterializedKeys, quantifier: MarkApplyQuantifier, plan_arena: &(dyn MetaArena + '_), - left_tuple: &Tuple, + left_tuple: &TupleImpl, ) -> Result { let any = matches!(quantifier, MarkApplyQuantifier::Any); if keys.is_empty { @@ -450,7 +452,7 @@ impl<'a> MarkApply<'a> { plan_arena: &mut ExecMetaArena, probe: Option, quantifier: MarkApplyQuantifier, - left_tuple: &Tuple, + left_tuple: &TupleImpl, ) -> Result { let mut saw_null = false; let predicates = self.op.predicates(); @@ -481,8 +483,8 @@ impl<'a> MarkApply<'a> { fn predicates_matched( predicates: &[ExprRef], - left_tuple: &Tuple, - right_tuple: &Tuple, + left_tuple: &TupleImpl, + right_tuple: &TupleImpl, plan_arena: &(dyn MetaArena + '_), ) -> Result { let values = SplitTupleRef::new(left_tuple, right_tuple); @@ -501,8 +503,8 @@ impl<'a> MarkApply<'a> { fn quantified_predicate_outcome( predicates: &[ExprRef], - left_tuple: &Tuple, - right_tuple: &Tuple, + left_tuple: &TupleImpl, + right_tuple: &TupleImpl, plan_arena: &(dyn MetaArena + '_), ) -> Result { match Self::eval_predicates(predicates, left_tuple, right_tuple, plan_arena)? { @@ -516,8 +518,8 @@ impl<'a> MarkApply<'a> { fn eval_predicates( predicates: &[ExprRef], - left_tuple: &Tuple, - right_tuple: &Tuple, + left_tuple: &TupleImpl, + right_tuple: &TupleImpl, plan_arena: &(dyn MetaArena + '_), ) -> Result, DatabaseError> { let values = SplitTupleRef::new(left_tuple, right_tuple); @@ -705,7 +707,7 @@ mod tests { for payload in ["first", "again"] { assert!(arena.next_tuple(root, &mut plan_arena)?); assert_eq!( - arena.materialize_tuple().values, + arena.take_tuple(), vec![ DataValue::Int32(2), DataValue::from(payload.to_string()), @@ -787,15 +789,16 @@ mod tests { let mut tuples = Vec::new(); let mut plan_arena = ExecMetaArena::new(plan_arena); while arena.next_tuple(root, &mut plan_arena)? { - tuples.push(arena.materialize_tuple()); + tuples.push(arena.take_tuple()); assert_eq!(arena.nodes.items.len(), count); assert_eq!(arena.nodes.items.as_ptr(), address); } assert_eq!( tuples - .into_iter() - .flat_map(|tuple| tuple.values) + .iter() + .flat_map(TupleImpl::iter) + .cloned() .collect::>(), vec![ DataValue::Int32(1), @@ -841,7 +844,7 @@ mod tests { assert_eq!( tuples .into_iter() - .flat_map(|tuple| tuple.values) + .flat_map(|mut tuple| tuple.take_values()) .collect::>(), vec![ DataValue::Int32(1), @@ -913,7 +916,8 @@ mod tests { right_input: Some(RightInput::Streaming { root: right_root }), left_input: 0, }; - let left_tuple = Tuple::new(None, vec![DataValue::Int32(2), DataValue::Int32(1)]); + let left_tuple: TupleImpl = + Tuple::new(None, vec![DataValue::Int32(2), DataValue::Int32(1)]).into(); let mut plan_arena = ExecMetaArena::new(plan_arena); let input = exec.right_input.take().unwrap(); @@ -970,7 +974,7 @@ mod tests { right_input: Some(RightInput::Streaming { root: right_root }), left_input: 0, }; - let left_tuple = Tuple::new(None, vec![DataValue::Int32(2)]); + let left_tuple: TupleImpl = Tuple::new(None, vec![DataValue::Int32(2)]).into(); let mut plan_arena = ExecMetaArena::new(plan_arena); let input = exec.right_input.take().unwrap(); @@ -1027,7 +1031,7 @@ mod tests { right_input: Some(RightInput::Streaming { root: right_root }), left_input: 0, }; - let left_tuple = Tuple::new(None, vec![DataValue::Null]); + let left_tuple: TupleImpl = Tuple::new(None, vec![DataValue::Null]).into(); let mut plan_arena = ExecMetaArena::new(plan_arena); let input = exec.right_input.take().unwrap(); @@ -1082,7 +1086,7 @@ mod tests { assert_eq!( tuples .into_iter() - .flat_map(|tuple| tuple.values) + .flat_map(|mut tuple| tuple.take_values()) .collect::>(), vec![ DataValue::Int32(1), @@ -1171,7 +1175,7 @@ mod tests { if use_guard && flag == DataValue::Boolean(false) { result?; assert_eq!( - arena.result_tuple().values, + *arena.result_tuple(), vec![DataValue::Int32(1), flag, DataValue::Null] ); assert!(matches!( @@ -1253,7 +1257,7 @@ mod tests { for (index, (key, flag, marker)) in cases.iter().enumerate() { exec.next_tuple(&mut arena, &mut meta)?; assert_eq!( - arena.result_tuple().values, + *arena.result_tuple(), vec![DataValue::Int32(*key), flag.clone(), marker.clone()] ); if materialize { @@ -1333,7 +1337,7 @@ mod tests { (MarkApplyQuantifier::All, DataValue::Int32(1)) => DataValue::Boolean(false), _ => DataValue::Null, }; - let tuple = Tuple::new(None, vec![value]); + let tuple: TupleImpl = Tuple::new(None, vec![value]).into(); let input = exec.right_input.take().unwrap(); let (input, value) = exec.mark_value(input, &mut arena, &mut meta, &tuple)?; exec.right_input = Some(input); @@ -1359,7 +1363,7 @@ mod tests { exec.reset(&mut arena, &mut meta)?; exec.next_tuple(&mut arena, &mut meta)?; assert_eq!( - arena.result_tuple().values, + *arena.result_tuple(), vec![ DataValue::Int32(2), if matches!(quantifier, MarkApplyQuantifier::Any) { @@ -1385,7 +1389,7 @@ mod tests { input, &mut arena, &mut meta, - &Tuple::new(None, vec![DataValue::Null]), + &TupleImpl::from(Tuple::new(None, vec![DataValue::Null])), )?; exec.right_input = Some(input); assert_eq!( @@ -1403,7 +1407,7 @@ mod tests { )); exec.next_tuple(&mut arena, &mut meta)?; assert_eq!( - arena.result_tuple().values.last(), + arena.result_tuple().iter().last(), Some(&DataValue::Boolean(matches!( quantifier, MarkApplyQuantifier::All @@ -1461,7 +1465,10 @@ mod tests { &transaction, ))?; assert_eq!( - tuples.into_iter().map(|row| row.values).collect::>(), + tuples + .into_iter() + .map(|mut row| row.take_values()) + .collect::>(), vec![ vec![ DataValue::Int32(1), @@ -1523,7 +1530,7 @@ mod tests { assert_eq!( tuples .into_iter() - .flat_map(|tuple| tuple.values) + .flat_map(|mut tuple| tuple.take_values()) .collect::>(), vec![ DataValue::Int32(1), @@ -1594,7 +1601,7 @@ mod tests { assert_eq!( tuples .into_iter() - .flat_map(|tuple| tuple.values) + .flat_map(|mut tuple| tuple.take_values()) .collect::>(), vec![ DataValue::Int32(1), diff --git a/src/execution/dql/projection.rs b/src/execution/dql/projection.rs index 11cf4b99..eadf834b 100644 --- a/src/execution/dql/projection.rs +++ b/src/execution/dql/projection.rs @@ -21,12 +21,10 @@ use crate::planner::ExprRef; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::Transaction; -use crate::types::bitset::BitSet; use crate::types::value::DataValue; pub struct Projection<'a> { exprs: &'a [ExprRef], - moveable: Option<&'a BitSet>, input: ExecId, } @@ -34,18 +32,14 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Projection<'a> { type Input = (&'a ProjectOperator, &'a LogicalPlan); fn into_executor( - (ProjectOperator { exprs, moveable }, input): Self::Input, + (ProjectOperator { exprs }, input): Self::Input, arena: &mut ExecArena<'a, T>, plan_arena: &mut (dyn MetaArena + 'a), cache: ExecutionContext<'_>, transaction: &T, ) -> ExecId { let input = build_read(arena, plan_arena, input, cache, transaction); - arena.push(ExecNode::Projection(Projection { - exprs, - moveable: moveable.as_ref(), - input, - })) + arena.push(ExecNode::Projection(Projection { exprs, input })) } } @@ -68,7 +62,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Projection<'a> { return Ok(()); } - arena.rewrite(self.exprs, plan_arena, None::<&[DataValue]>, self.moveable)?; + arena.rewrite(self.exprs, plan_arena, None::<&[DataValue]>)?; arena.resume(); Ok(()) } diff --git a/src/execution/dql/recursive_cte.rs b/src/execution/dql/recursive_cte.rs index 43633a1c..69deed0e 100644 --- a/src/execution/dql/recursive_cte.rs +++ b/src/execution/dql/recursive_cte.rs @@ -26,25 +26,25 @@ use crate::planner::operator::recursive_cte::RecursiveScanOperator; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::Transaction; -use crate::types::tuple::Tuple; +use crate::types::tuple::TupleImpl; use std::mem; pub(crate) enum RecursiveInput { One { - tuple: Tuple, + tuple: TupleImpl, emitted: bool, }, #[cfg(feature = "spill")] - Many(SpillReader), + Many(SpillReader), #[cfg(not(feature = "spill"))] Many { - rows: Vec, + rows: Vec, position: usize, }, } impl Iterator for RecursiveInput { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { match self { @@ -86,24 +86,24 @@ enum RecursiveRows { #[default] Empty, One { - tuple: Tuple, + tuple: TupleImpl, output_done: bool, }, #[cfg(feature = "spill")] - Writing(SpillVec<'static, Tuple>), + Writing(SpillVec<'static, TupleImpl>), #[cfg(feature = "spill")] - Reading(SpillReader), + Reading(SpillReader), #[cfg(not(feature = "spill"))] - Writing(Vec), + Writing(Vec), #[cfg(not(feature = "spill"))] Reading { - rows: Vec, + rows: Vec, output_index: usize, }, } impl RecursiveRows { - fn push(&mut self, tuple: Tuple) -> Result<(), DatabaseError> { + fn push(&mut self, tuple: TupleImpl) -> Result<(), DatabaseError> { match self { Self::Empty => { *self = Self::One { @@ -162,7 +162,7 @@ impl RecursiveRows { } } - fn next_output(&mut self) -> Result, DatabaseError> { + fn next_output(&mut self) -> Result, DatabaseError> { match self { Self::Empty => Ok(None), Self::One { tuple, output_done } => { @@ -302,14 +302,14 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for RecursiveCte<'a, T> { match self.phase { RecursivePhase::Anchor => { while arena.next_tuple(self.anchor_input, plan_arena)? { - self.next.push(arena.materialize_tuple())?; + self.next.push(arena.take_tuple())?; } self.working = mem::take(&mut self.next).finish()?; self.phase = RecursivePhase::Output; } RecursivePhase::Output => { if let Some(tuple) = self.working.next_output()? { - arena.produce_tuple(tuple); + arena.produce(tuple); return Ok(()); } if !self.start_recursive(plan_arena)? { @@ -323,7 +323,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for RecursiveCte<'a, T> { .recursive_arena .next_tuple(self.recursive_root, plan_arena)? { - self.next.push(self.recursive_arena.materialize_tuple())?; + self.next.push(self.recursive_arena.take_tuple())?; } self.recursive_arena.reset_for_rebuild(); self.working = mem::take(&mut self.next).finish()?; @@ -368,7 +368,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for RecursiveScan { _plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { match self.input.next().transpose()? { - Some(tuple) => arena.produce_tuple(tuple), + Some(tuple) => arena.produce(tuple), None => arena.finish(), } Ok(()) @@ -402,7 +402,12 @@ mod tests { #[test] fn spilled_generation_is_written_once_and_replayed_for_scan() -> Result<(), DatabaseError> { let expected = (0..1100) - .map(|value| Tuple::new(None, vec![DataValue::Int32(value)])) + .map(|value| { + TupleImpl::from(crate::types::tuple::Tuple::new( + None, + vec![DataValue::Int32(value)], + )) + }) .collect::>(); let mut rows = RecursiveRows::default(); for tuple in expected.iter().cloned() { @@ -426,7 +431,12 @@ mod tests { #[test] fn memory_generation_is_replayed_for_scan() -> Result<(), DatabaseError> { let expected = (0..3) - .map(|value| Tuple::new(None, vec![DataValue::Int32(value)])) + .map(|value| { + TupleImpl::from(crate::types::tuple::Tuple::new( + None, + vec![DataValue::Int32(value)], + )) + }) .collect::>(); let mut rows = RecursiveRows::default(); for tuple in expected.iter().cloned() { @@ -522,7 +532,7 @@ mod tests { assert_eq!( tuples .into_iter() - .flat_map(|tuple| tuple.values) + .flat_map(|mut tuple| tuple.take_values()) .collect::>(), vec![ DataValue::Int32(1), diff --git a/src/execution/dql/scalar_query_init.rs b/src/execution/dql/scalar_query_init.rs index 433cbca3..da640862 100644 --- a/src/execution/dql/scalar_query_init.rs +++ b/src/execution/dql/scalar_query_init.rs @@ -21,7 +21,7 @@ use crate::planner::operator::scalar_query_init::ScalarQueryInitOperator; use crate::planner::ExecMetaArena; use crate::planner::{ExprRef, LogicalPlan, MetaArena, ScalarQueryRef}; use crate::storage::Transaction; -use crate::types::tuple::Tuple; +use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; enum ScalarQueryInitState { @@ -37,7 +37,7 @@ pub struct ScalarQueryInit<'a> { reference: ScalarQueryRef, state: ScalarQueryInitState, param_bindings: &'a [(ScalarQueryRef, ExprRef)], - scratch_tuple: Tuple, + scratch_tuple: TupleImpl, init: ExecId, } @@ -50,7 +50,7 @@ impl<'a> ScalarQueryInit<'a> { ) -> Result<(), DatabaseError> { let mut value = DataValue::Null; if arena.next_tuple(self.init, plan_arena)? { - std::mem::swap(&mut value, &mut arena.result_tuple_mut().values[0]); + std::mem::swap(&mut value, &mut arena.result_mut()[0]); } if arena.next_tuple(self.init, plan_arena)? { return Err(DatabaseError::InvalidValue( @@ -84,7 +84,7 @@ impl<'a> ScalarQueryInit<'a> { ScalarQueryInitState::Initialize }, param_bindings: &op.param_bindings, - scratch_tuple: Tuple::default(), + scratch_tuple: TupleImpl::default(), })) } } @@ -119,7 +119,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ScalarQueryInit<'a> { } else { ScalarQueryInitState::Initialize }; - self.scratch_tuple.clear(); + self.scratch_tuple.output_mut().clear(); arena.reset(self.input, plan_arena) } @@ -149,7 +149,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ScalarQueryInit<'a> { return Ok(()); } // Preserve the outer row and lend the previous subquery buffer to the arena. - std::mem::swap(arena.result_tuple_mut(), &mut self.scratch_tuple); + std::mem::swap(arena.result_mut(), &mut self.scratch_tuple); for (reference, expr) in self.param_bindings { let value = plan_arena .expression(*expr) @@ -162,7 +162,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ScalarQueryInit<'a> { ScalarQueryInitState::EvaluateOuter => { arena.reset(self.init, plan_arena)?; self.evaluate_init(arena, plan_arena)?; - std::mem::swap(arena.result_tuple_mut(), &mut self.scratch_tuple); + std::mem::swap(arena.result_mut(), &mut self.scratch_tuple); self.state = ScalarQueryInitState::ReadOuter; arena.resume(); return Ok(()); diff --git a/src/execution/dql/scalar_subquery.rs b/src/execution/dql/scalar_subquery.rs index a4ef96d4..24d1e387 100644 --- a/src/execution/dql/scalar_subquery.rs +++ b/src/execution/dql/scalar_subquery.rs @@ -76,7 +76,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ScalarSubquery { self.returned = true; if !has_next { - let output = arena.result_tuple_mut(); + let output = arena.output_tuple_mut(); output.pk = None; output.values.clear(); output diff --git a/src/execution/dql/seq_scan.rs b/src/execution/dql/seq_scan.rs index d351d09f..4ff2b81e 100644 --- a/src/execution/dql/seq_scan.rs +++ b/src/execution/dql/seq_scan.rs @@ -83,7 +83,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for SeqScan<'a, T> { } }; - if iter.next_tuple_into(state.table_codec, &mut state.result.tuple)? { + if iter.next_tuple_into(state.table_codec, state.result.tuple.output_mut())? { arena.resume(); } else { arena.finish(); @@ -132,7 +132,8 @@ mod tests { DataValue::Boolean(true), DataValue::Int32(id * 10), ], - ), + ) + .into(), &serializers, false, )?; @@ -154,14 +155,14 @@ mod tests { iter: None, }; scan.next_tuple(&mut arena, &mut meta)?; - assert_eq!(arena.result_tuple().values[0], DataValue::Int32(2)); + assert_eq!(arena.result_tuple()[0], DataValue::Int32(2)); let iterator = scan.iter.as_ref().unwrap() as *const _; for _ in 0..3 { scan.reset(&mut arena, &mut meta)?; scan.reset(&mut arena, &mut meta)?; for id in [2, 3] { scan.next_tuple(&mut arena, &mut meta)?; - assert_eq!(arena.result_tuple().values[0], DataValue::Int32(id)); + assert_eq!(arena.result_tuple()[0], DataValue::Int32(id)); assert_eq!(scan.iter.as_ref().unwrap() as *const _, iterator); } scan.next_tuple(&mut arena, &mut meta)?; diff --git a/src/execution/dql/set_membership.rs b/src/execution/dql/set_membership.rs index 4482d77b..6eca9d99 100644 --- a/src/execution/dql/set_membership.rs +++ b/src/execution/dql/set_membership.rs @@ -20,14 +20,14 @@ use crate::planner::operator::set_membership::SetMembershipKind; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::Transaction; -use crate::types::tuple::Tuple; +use crate::types::tuple::TupleImpl; use std::collections::HashMap; pub struct SetMembership { kind: SetMembershipKind, left_input: ExecId, right_input: ExecId, - right_counts: HashMap, + right_counts: HashMap, built: bool, } @@ -75,7 +75,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for SetMembership { if let Some(count) = self.right_counts.get_mut(arena.result_tuple()) { *count += 1; } else { - self.right_counts.insert(arena.materialize_tuple(), 1); + self.right_counts.insert(arena.take_tuple(), 1); } } self.built = true; @@ -102,7 +102,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for SetMembership { } impl SetMembership { - fn consume_right_match(&mut self, tuple: &Tuple) -> bool { + fn consume_right_match(&mut self, tuple: &TupleImpl) -> bool { if let Some(count) = self.right_counts.get_mut(tuple) { if *count > 0 { *count -= 1; diff --git a/src/execution/dql/show_table.rs b/src/execution/dql/show_table.rs index 5643b56e..4666e6c7 100644 --- a/src/execution/dql/show_table.rs +++ b/src/execution/dql/show_table.rs @@ -68,7 +68,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ShowTables<'a, T> { return Ok(()); }; - let output = arena.result_tuple_mut(); + let output = arena.output_tuple_mut(); output.pk = None; output.values.clear(); output.values.push(DataValue::Utf8 { diff --git a/src/execution/dql/show_view.rs b/src/execution/dql/show_view.rs index e90eb79d..fff31a49 100644 --- a/src/execution/dql/show_view.rs +++ b/src/execution/dql/show_view.rs @@ -75,7 +75,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ShowViews<'a, T> { return Ok(()); }; - let output = arena.result_tuple_mut(); + let output = arena.output_tuple_mut(); output.pk = None; output.values.clear(); output.values.push(DataValue::Utf8 { diff --git a/src/execution/dql/sort.rs b/src/execution/dql/sort.rs index 867ac6b3..f7fe3a7d 100644 --- a/src/execution/dql/sort.rs +++ b/src/execution/dql/sort.rs @@ -21,7 +21,7 @@ use crate::planner::LogicalPlan; use crate::planner::ScalarQueryRef; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::Transaction; -use crate::types::tuple::Tuple; +use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; use bumpalo::Bump; use std::cmp::Ordering; @@ -150,7 +150,7 @@ pub(crate) fn compare_sort_keys<'a>( } pub(crate) struct SortTuple { - tuple: Tuple, + tuple: TupleImpl, outer_values: Vec<(ScalarQueryRef, DataValue)>, } @@ -205,7 +205,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Sort<'a> { loop { if let Some((_, row)) = self.rows.pop() { plan_arena.restore_outer_values(row.outer_values); - arena.produce_tuple(row.tuple); + arena.produce(row.tuple); return Ok(()); } while arena.next_tuple(self.input, plan_arena)? { @@ -213,7 +213,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Sort<'a> { self.rows.put(( offset, SortTuple { - tuple: arena.materialize_tuple(), + tuple: arena.take_tuple(), outer_values: plan_arena.take_outer_values(), }, )); @@ -237,7 +237,7 @@ mod test { use crate::expression::ScalarExpression; use crate::planner::operator::sort::SortField; use crate::planner::{ExecMetaArena, PlanArena}; - use crate::types::tuple::Tuple; + use crate::types::tuple::{Tuple, TupleImpl}; use crate::types::value::DataValue; use crate::types::LogicalType; use bumpalo::Bump; @@ -318,15 +318,12 @@ mod test { let mut metadata = ExecMetaArena::new(metadata); let nodes = arena.nodes.items.len(); assert!(arena.next_tuple(root, &mut metadata)?); - assert_eq!(arena.result_tuple().values, vec![DataValue::Int32(1)]); + assert_eq!(*arena.result_tuple(), vec![DataValue::Int32(1)]); arena.reset(root, &mut metadata)?; assert_eq!(arena.nodes.items.len(), nodes); for expected in [1, 2, 3] { assert!(arena.next_tuple(root, &mut metadata)?); - assert_eq!( - arena.result_tuple().values, - vec![DataValue::Int32(expected)] - ); + assert_eq!(*arena.result_tuple(), vec![DataValue::Int32(expected)]); assert_eq!( metadata.init_value(result), Some(&DataValue::Int32(expected)) @@ -343,10 +340,7 @@ mod test { arena.reset(root, &mut metadata)?; for expected in [1, 2, 3] { assert!(arena.next_tuple(root, &mut metadata)?); - assert_eq!( - arena.result_tuple().values, - vec![DataValue::Int32(expected)] - ); + assert_eq!(*arena.result_tuple(), vec![DataValue::Int32(expected)]); assert_eq!( metadata.init_value(result), Some(&DataValue::Int32(expected)) @@ -390,7 +384,7 @@ mod test { sort_fields: &[SortField], mut tuples: NullableVec<'a, (usize, SortTuple)>, plan_arena: &mut ExecMetaArena>, - ) -> Result + 'a, DatabaseError> { + ) -> Result + 'a, DatabaseError> { sort_tuples(sort_fields, &mut tuples, plan_arena)?; let mut rows = Vec::with_capacity(tuples.len()); while let Some((_, row)) = tuples.pop() { @@ -433,91 +427,91 @@ mod test { vec.put(( 0_usize, SortTuple { - tuple: Tuple::new(None, vec![DataValue::Null]), + tuple: Tuple::new(None, vec![DataValue::Null]).into(), outer_values: Vec::new(), }, )); vec.put(( 1_usize, SortTuple { - tuple: Tuple::new(None, vec![DataValue::Int32(0)]), + tuple: Tuple::new(None, vec![DataValue::Int32(0)]).into(), outer_values: Vec::new(), }, )); vec.put(( 2_usize, SortTuple { - tuple: Tuple::new(None, vec![DataValue::Int32(1)]), + tuple: Tuple::new(None, vec![DataValue::Int32(1)]).into(), outer_values: Vec::new(), }, )); vec }; - let fn_asc_and_nulls_last_eq = |mut iter: Box>| { + let fn_asc_and_nulls_last_eq = |mut iter: Box>| { if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Int32(0)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(1)]) + assert_eq!(tuple, vec![DataValue::Int32(1)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Null]) } else { unreachable!() } }; - let fn_desc_and_nulls_last_eq = |mut iter: Box>| { + let fn_desc_and_nulls_last_eq = |mut iter: Box>| { if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(1)]) + assert_eq!(tuple, vec![DataValue::Int32(1)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Int32(0)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Null]) } else { unreachable!() } }; - let fn_asc_and_nulls_first_eq = |mut iter: Box>| { + let fn_asc_and_nulls_first_eq = |mut iter: Box>| { if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Int32(0)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(1)]) + assert_eq!(tuple, vec![DataValue::Int32(1)]) } else { unreachable!() } }; - let fn_desc_and_nulls_first_eq = |mut iter: Box>| { + let fn_desc_and_nulls_first_eq = |mut iter: Box>| { if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(1)]) + assert_eq!(tuple, vec![DataValue::Int32(1)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Int32(0)]) } else { unreachable!() } @@ -604,175 +598,175 @@ mod test { vec.put(( 0_usize, SortTuple { - tuple: Tuple::new(None, vec![DataValue::Null, DataValue::Null]), + tuple: Tuple::new(None, vec![DataValue::Null, DataValue::Null]).into(), outer_values: Vec::new(), }, )); vec.put(( 1_usize, SortTuple { - tuple: Tuple::new(None, vec![DataValue::Int32(0), DataValue::Null]), + tuple: Tuple::new(None, vec![DataValue::Int32(0), DataValue::Null]).into(), outer_values: Vec::new(), }, )); vec.put(( 2_usize, SortTuple { - tuple: Tuple::new(None, vec![DataValue::Int32(1), DataValue::Null]), + tuple: Tuple::new(None, vec![DataValue::Int32(1), DataValue::Null]).into(), outer_values: Vec::new(), }, )); vec.put(( 3_usize, SortTuple { - tuple: Tuple::new(None, vec![DataValue::Null, DataValue::Int32(0)]), + tuple: Tuple::new(None, vec![DataValue::Null, DataValue::Int32(0)]).into(), outer_values: Vec::new(), }, )); vec.put(( 4_usize, SortTuple { - tuple: Tuple::new(None, vec![DataValue::Int32(0), DataValue::Int32(0)]), + tuple: Tuple::new(None, vec![DataValue::Int32(0), DataValue::Int32(0)]).into(), outer_values: Vec::new(), }, )); vec.put(( 5_usize, SortTuple { - tuple: Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(0)]), + tuple: Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(0)]).into(), outer_values: Vec::new(), }, )); vec }; let fn_asc_1_and_nulls_first_1_and_asc_2_and_nulls_first_2_eq = - |mut iter: Box>| { + |mut iter: Box>| { if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Null, DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Null, DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Null, DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Null, DataValue::Int32(0)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(0), DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Int32(0), DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(0), DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Int32(0), DataValue::Int32(0)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(1), DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Int32(1), DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(1), DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Int32(1), DataValue::Int32(0)]) } else { unreachable!() } }; let fn_asc_1_and_nulls_last_1_and_asc_2_and_nulls_first_2_eq = - |mut iter: Box>| { + |mut iter: Box>| { if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(0), DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Int32(0), DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(0), DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Int32(0), DataValue::Int32(0)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(1), DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Int32(1), DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(1), DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Int32(1), DataValue::Int32(0)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Null, DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Null, DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Null, DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Null, DataValue::Int32(0)]) } else { unreachable!() } }; let fn_desc_1_and_nulls_first_1_and_asc_2_and_nulls_first_2_eq = - |mut iter: Box>| { + |mut iter: Box>| { if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Null, DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Null, DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Null, DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Null, DataValue::Int32(0)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(1), DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Int32(1), DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(1), DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Int32(1), DataValue::Int32(0)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(0), DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Int32(0), DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(0), DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Int32(0), DataValue::Int32(0)]) } else { unreachable!() } }; let fn_desc_1_and_nulls_last_1_and_asc_2_and_nulls_first_2_eq = - |mut iter: Box>| { + |mut iter: Box>| { if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(1), DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Int32(1), DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(1), DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Int32(1), DataValue::Int32(0)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(0), DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Int32(0), DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Int32(0), DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Int32(0), DataValue::Int32(0)]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Null, DataValue::Null]) + assert_eq!(tuple, vec![DataValue::Null, DataValue::Null]) } else { unreachable!() } if let Some(tuple) = iter.next() { - assert_eq!(tuple.values, vec![DataValue::Null, DataValue::Int32(0)]) + assert_eq!(tuple, vec![DataValue::Null, DataValue::Int32(0)]) } else { unreachable!() } diff --git a/src/execution/dql/top_k.rs b/src/execution/dql/top_k.rs index 0c319ee7..57693917 100644 --- a/src/execution/dql/top_k.rs +++ b/src/execution/dql/top_k.rs @@ -23,7 +23,7 @@ use crate::planner::{ExecMetaArena, MetaArena}; use crate::planner::{LogicalPlan, ScalarQueryRef}; use crate::storage::table_codec::BumpBytes; use crate::storage::Transaction; -use crate::types::tuple::Tuple; +use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; use bumpalo::Bump; use std::cmp::Ordering; @@ -34,7 +34,7 @@ use std::mem::transmute; struct CmpItem<'a> { key: BumpVec<'a, u8>, sequence: usize, - tuple: Tuple, + tuple: TupleImpl, outer_values: Vec<(ScalarQueryRef, DataValue)>, } @@ -65,7 +65,7 @@ fn top_sort<'a>( full_key: &mut BumpBytes<'a>, sort_fields: &[SortField], heap: &mut BTreeSet>, - tuple: &mut Tuple, + tuple: &mut TupleImpl, keep_count: usize, sequence: usize, plan_arena: &mut ExecMetaArena, @@ -174,7 +174,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for TopK<'a> { &mut key_scratch, self.sort_fields, &mut set, - arena.result_tuple_mut(), + arena.result_mut(), keep_count, sequence, plan_arena, @@ -196,7 +196,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for TopK<'a> { if let Some(item) = self.output.as_mut().and_then(std::iter::Iterator::next) { plan_arena.restore_outer_values(item.outer_values); - arena.produce_tuple(item.tuple); + arena.produce(item.tuple); } else { arena.finish(); } @@ -214,7 +214,7 @@ mod test { use crate::expression::ScalarExpression; use crate::planner::operator::sort::SortField; use crate::planner::ExecMetaArena; - use crate::types::tuple::Tuple; + use crate::types::tuple::{Tuple, TupleLike}; use crate::types::value::DataValue; use crate::types::LogicalType; use bumpalo::Bump; @@ -229,7 +229,7 @@ mod test { CmpItem { key, sequence, - tuple: Tuple::new(None, vec![DataValue::Int32(value)]), + tuple: Tuple::new(None, vec![DataValue::Int32(value)]).into(), outer_values: Vec::new(), } }; @@ -276,48 +276,48 @@ mod test { let fn_asc_and_nulls_last_eq = |mut heap: BTreeSet>| { if let Some(reverse) = heap.pop_first() { - assert_eq!(reverse.tuple.values, vec![DataValue::Int32(0)]) + assert_eq!(reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(0)]) } else { unreachable!() } if let Some(reverse) = heap.pop_first() { - assert_eq!(reverse.tuple.values, vec![DataValue::Int32(1)]) + assert_eq!(reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(1)]) } else { unreachable!() } }; let fn_desc_and_nulls_last_eq = |mut heap: BTreeSet>| { if let Some(reverse) = heap.pop_first() { - assert_eq!(reverse.tuple.values, vec![DataValue::Int32(1)]) + assert_eq!(reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(1)]) } else { unreachable!() } if let Some(reverse) = heap.pop_first() { - assert_eq!(reverse.tuple.values, vec![DataValue::Int32(0)]) + assert_eq!(reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(0)]) } else { unreachable!() } }; let fn_asc_and_nulls_first_eq = |mut heap: BTreeSet>| { if let Some(reverse) = heap.pop_first() { - assert_eq!(reverse.tuple.values, vec![DataValue::Null]) + assert_eq!(reverse.tuple.as_slice().unwrap(), vec![DataValue::Null]) } else { unreachable!() } if let Some(reverse) = heap.pop_first() { - assert_eq!(reverse.tuple.values, vec![DataValue::Int32(0)]) + assert_eq!(reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(0)]) } else { unreachable!() } }; let fn_desc_and_nulls_first_eq = |mut heap: BTreeSet>| { if let Some(reverse) = heap.pop_first() { - assert_eq!(reverse.tuple.values, vec![DataValue::Null]) + assert_eq!(reverse.tuple.as_slice().unwrap(), vec![DataValue::Null]) } else { unreachable!() } if let Some(reverse) = heap.pop_first() { - assert_eq!(reverse.tuple.values, vec![DataValue::Int32(1)]) + assert_eq!(reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(1)]) } else { unreachable!() } @@ -329,7 +329,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Null]).into(), 2, 0, &mut plan_arena, @@ -338,7 +338,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Int32(0)]).into(), 2, 1, &mut plan_arena, @@ -347,7 +347,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(1)]), + &mut Tuple::new(None, vec![DataValue::Int32(1)]).into(), 2, 2, &mut plan_arena, @@ -360,7 +360,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, false), &mut indices, - &mut Tuple::new(None, vec![DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Null]).into(), 2, 3, &mut plan_arena, @@ -369,7 +369,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, false), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Int32(0)]).into(), 2, 4, &mut plan_arena, @@ -378,7 +378,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, false), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(1)]), + &mut Tuple::new(None, vec![DataValue::Int32(1)]).into(), 2, 5, &mut plan_arena, @@ -391,7 +391,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Null]).into(), 2, 6, &mut plan_arena, @@ -400,7 +400,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Int32(0)]).into(), 2, 7, &mut plan_arena, @@ -409,7 +409,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(1)]), + &mut Tuple::new(None, vec![DataValue::Int32(1)]).into(), 2, 8, &mut plan_arena, @@ -422,7 +422,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, false), &mut indices, - &mut Tuple::new(None, vec![DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Null]).into(), 2, 9, &mut plan_arena, @@ -431,7 +431,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, false), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Int32(0)]).into(), 2, 10, &mut plan_arena, @@ -440,7 +440,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, false), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(1)]), + &mut Tuple::new(None, vec![DataValue::Int32(1)]).into(), 2, 11, &mut plan_arena, @@ -494,13 +494,16 @@ mod test { let fn_asc_1_and_nulls_first_1_and_asc_2_and_nulls_first_2_eq = |mut heap: BTreeSet>| { if let Some(reverse) = heap.pop_first() { - assert_eq!(reverse.tuple.values, vec![DataValue::Null, DataValue::Null]) + assert_eq!( + reverse.tuple.as_slice().unwrap(), + vec![DataValue::Null, DataValue::Null] + ) } else { unreachable!() } if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Null, DataValue::Int32(0)] ) } else { @@ -508,7 +511,7 @@ mod test { } if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(0), DataValue::Null] ) } else { @@ -516,7 +519,7 @@ mod test { } if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(0), DataValue::Int32(0)] ) } else { @@ -527,7 +530,7 @@ mod test { |mut heap: BTreeSet>| { if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(0), DataValue::Null] ) } else { @@ -535,7 +538,7 @@ mod test { } if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(0), DataValue::Int32(0)] ) } else { @@ -543,7 +546,7 @@ mod test { } if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(1), DataValue::Null] ) } else { @@ -551,7 +554,7 @@ mod test { } if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(1), DataValue::Int32(0)] ) } else { @@ -561,13 +564,16 @@ mod test { let fn_desc_1_and_nulls_first_1_and_asc_2_and_nulls_first_2_eq = |mut heap: BTreeSet>| { if let Some(reverse) = heap.pop_first() { - assert_eq!(reverse.tuple.values, vec![DataValue::Null, DataValue::Null]) + assert_eq!( + reverse.tuple.as_slice().unwrap(), + vec![DataValue::Null, DataValue::Null] + ) } else { unreachable!() } if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Null, DataValue::Int32(0)] ) } else { @@ -575,7 +581,7 @@ mod test { } if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(1), DataValue::Null] ) } else { @@ -583,7 +589,7 @@ mod test { } if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(1), DataValue::Int32(0)] ) } else { @@ -594,7 +600,7 @@ mod test { |mut heap: BTreeSet>| { if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(1), DataValue::Null] ) } else { @@ -602,7 +608,7 @@ mod test { } if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(1), DataValue::Int32(0)] ) } else { @@ -610,7 +616,7 @@ mod test { } if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(0), DataValue::Null] ) } else { @@ -618,7 +624,7 @@ mod test { } if let Some(reverse) = heap.pop_first() { assert_eq!( - reverse.tuple.values, + reverse.tuple.as_slice().unwrap(), vec![DataValue::Int32(0), DataValue::Int32(0)] ) } else { @@ -632,7 +638,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, true, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Null, DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Null, DataValue::Null]).into(), 4, 12, &mut plan_arena, @@ -641,7 +647,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, true, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Null]).into(), 4, 13, &mut plan_arena, @@ -650,7 +656,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, true, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Null]).into(), 4, 14, &mut plan_arena, @@ -659,7 +665,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, true, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Null, DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Null, DataValue::Int32(0)]).into(), 4, 15, &mut plan_arena, @@ -668,7 +674,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, true, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Int32(0)]).into(), 4, 16, &mut plan_arena, @@ -677,7 +683,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, true, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(0)]).into(), 4, 17, &mut plan_arena, @@ -690,7 +696,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, false, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Null, DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Null, DataValue::Null]).into(), 4, 18, &mut plan_arena, @@ -699,7 +705,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, false, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Null]).into(), 4, 19, &mut plan_arena, @@ -708,7 +714,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, false, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Null]).into(), 4, 20, &mut plan_arena, @@ -717,7 +723,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, false, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Null, DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Null, DataValue::Int32(0)]).into(), 4, 21, &mut plan_arena, @@ -726,7 +732,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, false, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Int32(0)]).into(), 4, 22, &mut plan_arena, @@ -735,7 +741,7 @@ mod test { &mut key_scratch, &fn_sort_fields(true, false, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(0)]).into(), 4, 23, &mut plan_arena, @@ -748,7 +754,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, true, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Null, DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Null, DataValue::Null]).into(), 4, 24, &mut plan_arena, @@ -757,7 +763,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, true, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Null]).into(), 4, 25, &mut plan_arena, @@ -766,7 +772,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, true, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Null]).into(), 4, 26, &mut plan_arena, @@ -775,7 +781,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, true, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Null, DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Null, DataValue::Int32(0)]).into(), 4, 27, &mut plan_arena, @@ -784,7 +790,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, true, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Int32(0)]).into(), 4, 28, &mut plan_arena, @@ -793,7 +799,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, true, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(0)]).into(), 4, 29, &mut plan_arena, @@ -806,7 +812,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, false, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Null, DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Null, DataValue::Null]).into(), 4, 30, &mut plan_arena, @@ -815,7 +821,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, false, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Null]).into(), 4, 31, &mut plan_arena, @@ -824,7 +830,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, false, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Null]), + &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Null]).into(), 4, 32, &mut plan_arena, @@ -833,7 +839,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, false, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Null, DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Null, DataValue::Int32(0)]).into(), 4, 33, &mut plan_arena, @@ -842,7 +848,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, false, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Int32(0), DataValue::Int32(0)]).into(), 4, 34, &mut plan_arena, @@ -851,7 +857,7 @@ mod test { &mut key_scratch, &fn_sort_fields(false, false, true, true), &mut indices, - &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(0)]), + &mut Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(0)]).into(), 4, 35, &mut plan_arena, diff --git a/src/execution/dql/values.rs b/src/execution/dql/values.rs index 50b0558d..265587e2 100644 --- a/src/execution/dql/values.rs +++ b/src/execution/dql/values.rs @@ -74,7 +74,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Values<'a> { self.remaining_rows -= 1; let width = self.op.schema_ref.len(); - let output = arena.result_tuple_mut(); + let output = arena.output_tuple_mut(); output.pk = None; output.values.clear(); for (i, expr) in self.rows.by_ref().take(width).enumerate() { diff --git a/src/execution/dql/window.rs b/src/execution/dql/window.rs index f8b51908..caf7656c 100644 --- a/src/execution/dql/window.rs +++ b/src/execution/dql/window.rs @@ -22,7 +22,7 @@ use crate::planner::operator::window::WindowOperator; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::Transaction; -use crate::types::tuple::Tuple; +use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; mod function; @@ -44,8 +44,8 @@ enum Boundary { #[derive(Default)] struct WindowState { - buffered: Vec<(usize, Tuple)>, - pending: Option<(Tuple, Boundary)>, + buffered: Vec<(usize, TupleImpl)>, + pending: Option<(TupleImpl, Boundary)>, sort_values: Vec, started: bool, partition_rows: usize, @@ -132,7 +132,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Window<'a> { impl<'a> Window<'a> { fn update_keys( &mut self, - tuple: &Tuple, + tuple: &TupleImpl, plan_arena: &(dyn MetaArena + '_), ) -> Result, DatabaseError> { let mut boundary = (!self.state.started).then_some(Boundary::Partition); @@ -166,10 +166,9 @@ impl<'a> Window<'a> { if self.state.buffered.is_empty() { return Ok(()); } - let output_offset = self.state.buffered[0].1.values.len(); + let output_offset = self.state.buffered[0].1.len(); for (_, row) in &mut self.state.buffered { - row.values - .resize(output_offset + self.functions.len(), DataValue::Null); + row.resize(output_offset + self.functions.len(), DataValue::Null); } let len = self.state.buffered.len(); for (slot, function) in self.functions.iter_mut().enumerate() { @@ -188,7 +187,7 @@ impl<'a> Window<'a> { fn eval( &mut self, - tuple: Tuple, + tuple: TupleImpl, boundary: Option, plan_arena: &(dyn MetaArena + '_), ) -> Result { @@ -247,7 +246,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Window<'a> { loop { if output_ready { if let Some((_, tuple)) = self.state.buffered.pop() { - arena.produce_tuple(tuple); + arena.produce(tuple); return Ok(()); } } @@ -259,7 +258,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Window<'a> { let (tuple, boundary) = if let Some((tuple, boundary)) = self.state.pending.take() { (tuple, Some(boundary)) } else if arena.next_tuple(self.input, plan_arena)? { - (arena.materialize_tuple(), None) + (arena.take_tuple(), None) } else { self.eval_functions(plan_arena)?; self.input_exhausted = true; @@ -306,8 +305,9 @@ mod tests { } } - fn tuple(values: &[i32]) -> Tuple { - Tuple::new(None, values.iter().copied().map(DataValue::from).collect()) + fn tuple(values: &[i32]) -> TupleImpl { + crate::types::tuple::Tuple::new(None, values.iter().copied().map(DataValue::from).collect()) + .into() } #[test] @@ -382,7 +382,7 @@ mod tests { for (value, expected) in [(10, [1_i64, 1]), (10, [2, 1]), (20, [3, 3])] { window.eval(tuple(&[1, value]), None, &plan_arena)?; let row = window.state.buffered.pop().unwrap().1; - assert_eq!(row.values[2..], expected.map(DataValue::from)); + assert!(row.iter().skip(2).eq(&expected.map(DataValue::from))); } Ok(()) } diff --git a/src/execution/dql/window/function.rs b/src/execution/dql/window/function.rs index 53808f3d..1558bf28 100644 --- a/src/execution/dql/window/function.rs +++ b/src/execution/dql/window/function.rs @@ -18,7 +18,7 @@ use crate::expression::agg::AggKind; use crate::expression::window::WindowFunctionKind; use crate::planner::ExprRef; use crate::planner::MetaArena; -use crate::types::tuple::Tuple; +use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; use crate::types::LogicalType; use std::ops::Range; @@ -30,7 +30,7 @@ pub(super) trait WindowFunction { fn evaluate( &mut self, - rows: &mut [(usize, Tuple)], + rows: &mut [(usize, TupleImpl)], peer: Range, peer_start: usize, peer_index: usize, @@ -44,7 +44,7 @@ struct RowNumber; impl WindowFunction for RowNumber { fn evaluate( &mut self, - rows: &mut [(usize, Tuple)], + rows: &mut [(usize, TupleImpl)], peer: Range, _peer_start: usize, _peer_index: usize, @@ -52,7 +52,7 @@ impl WindowFunction for RowNumber { _arena: &(dyn MetaArena + '_), ) -> Result<(), DatabaseError> { for (row_index, row) in &mut rows[peer] { - row.values[output_position] = DataValue::Int64((*row_index + 1) as i64); + row[output_position] = DataValue::Int64((*row_index + 1) as i64); } Ok(()) } @@ -65,7 +65,7 @@ struct Rank { impl WindowFunction for Rank { fn evaluate( &mut self, - rows: &mut [(usize, Tuple)], + rows: &mut [(usize, TupleImpl)], peer: Range, peer_start: usize, peer_index: usize, @@ -78,7 +78,7 @@ impl WindowFunction for Rank { peer_start + 1 }; for (_, row) in &mut rows[peer] { - row.values[output_position] = DataValue::Int64(rank as i64); + row[output_position] = DataValue::Int64(rank as i64); } Ok(()) } @@ -99,7 +99,7 @@ impl WindowFunction for Aggregate { fn evaluate( &mut self, - rows: &mut [(usize, Tuple)], + rows: &mut [(usize, TupleImpl)], peer: Range, _peer_start: usize, _peer_index: usize, @@ -116,7 +116,7 @@ impl WindowFunction for Aggregate { accumulator.evaluate()?; let result = accumulator.result(); for (_, row) in &mut rows[peer] { - row.values[output_position] = result.clone(); + row[output_position] = result.clone(); } Ok(()) } diff --git a/src/execution/mod.rs b/src/execution/mod.rs index 3be40d6d..df7a352a 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -82,10 +82,8 @@ use crate::planner::MetaArena; use crate::planner::{ExecMetaArena, ExprRef, LogicalPlan, PlanKeeper}; use crate::storage::table_codec::TableCodec; use crate::storage::{StatisticsMetaCache, TableCache, Transaction, ViewCache}; -use crate::types::bitset::BitSet; use crate::types::index::RuntimeIndexProbe; -use crate::types::tuple::{Input, MoveableTuple, Tuple}; -use crate::types::value::DataValue; +use crate::types::tuple::{Input, Tuple, TupleImpl}; #[derive(Clone, Copy)] pub(crate) struct ExecutionContext<'a> { @@ -144,7 +142,7 @@ pub(crate) enum ExecStatus { #[derive(Debug, Default)] pub(crate) struct ExecResult { - pub(crate) tuple: Tuple, + pub(crate) tuple: TupleImpl, pub(crate) status: Option, } @@ -186,11 +184,11 @@ impl<'a, T: Transaction + 'a> Executor<'a, T> { pub(crate) fn next_tuple( &mut self, plan_arena: &mut ExecMetaArena, - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { if !self.arena.next_tuple(self.root, plan_arena)? { return Ok(None); } - Ok(Some(self.arena.result_tuple_mut())) + Ok(Some(self.arena.result_mut())) } pub(crate) fn take_ddl_apply(&mut self) -> Vec { @@ -622,7 +620,7 @@ impl<'b, 'a, T: Transaction + 'a> ExecArenaLocalState<'b, 'a, T> { unsafe { (&mut *self.transaction, &mut *self.table_codec) } } - pub(crate) fn tuple_transaction_codec_mut(&mut self) -> (&Tuple, &mut T, &mut TableCodec) { + pub(crate) fn tuple_transaction_codec_mut(&mut self) -> (&TupleImpl, &mut T, &mut TableCodec) { unsafe { ( &self.result.tuple, @@ -632,18 +630,6 @@ impl<'b, 'a, T: Transaction + 'a> ExecArenaLocalState<'b, 'a, T> { } } - pub(crate) fn index_values_transaction_codec_mut( - &mut self, - ) -> (&[DataValue], &mut T, &mut TableCodec) { - unsafe { - ( - &self.result.tuple.values, - &mut *self.transaction, - &mut *self.table_codec, - ) - } - } - pub(crate) fn transaction_codec(&mut self) -> (&'a T, &mut TableCodec) { unsafe { (&*self.transaction, &mut *self.table_codec) } } @@ -772,22 +758,27 @@ impl<'a, T: Transaction + 'a> ExecArena<'a, T> { debug_assert!(self.runtime_probe_stack.is_empty()); debug_assert!(self.ddl_apply.is_empty()); self.nodes.clear(); - self.result.tuple = Tuple::default(); + self.result.tuple = TupleImpl::default(); self.result.status = None; self.recursive_input = None; } #[inline] - pub(crate) fn result_tuple(&self) -> &Tuple { + pub(crate) fn result_tuple(&self) -> &TupleImpl { &self.result.tuple } #[inline] - pub(crate) fn result_tuple_mut(&mut self) -> &mut Tuple { + pub(crate) fn result_mut(&mut self) -> &mut TupleImpl { &mut self.result.tuple } - pub(crate) fn materialize_tuple(&mut self) -> Tuple { + #[inline] + pub(crate) fn output_tuple_mut(&mut self) -> &mut Tuple { + self.result.tuple.output_mut() + } + + pub(crate) fn take_tuple(&mut self) -> TupleImpl { std::mem::take(&mut self.result.tuple) } @@ -796,35 +787,35 @@ impl<'a, T: Transaction + 'a> ExecArena<'a, T> { exprs: &[E], arena: &dyn MetaArena, input: Option<&TInput>, - moveable: Option<&BitSet>, ) -> Result<(), DatabaseError> { - let values = &mut self.result.tuple.values; - let base = values.len(); - values.reserve(exprs.len()); - // SAFETY: only the fixed input prefix is exposed. All output capacity - // is reserved before creating the view, and each Cow is owned before - // push. No source slice is subsequently materialized or resized. - let local = unsafe { MoveableTuple::from_raw_parts(values.as_mut_ptr(), base, moveable) }; + let base = { + let values = &mut self.result.tuple.rightmost_mut().values; + values.reserve(exprs.len()); + values.len() + }; for expr in exprs { let value = { let expression = expr.expression(arena); match input { Some(input) => expression.eval(arena, Some(input)), - None => expression.eval(arena, Some(&local)), + None => expression.eval(arena, Some(&self.result.tuple)), } .map(|value| value.into_owned()) }; match value { - Ok(value) => values.push(value), + Ok(value) => self.result.tuple.push(value), Err(error) => { - values.truncate(base); + self.result.tuple.rightmost_mut().values.truncate(base); return Err(error); } } } - values.rotate_left(base); - values.truncate(exprs.len()); + let pk = self.result.tuple.take_pk(); + let tuple = self.result.tuple.retain_rightmost(); + tuple.values.rotate_left(base); + tuple.values.truncate(exprs.len()); + tuple.pk = pk; Ok(()) } @@ -839,7 +830,7 @@ impl<'a, T: Transaction + 'a> ExecArena<'a, T> { } #[inline] - pub(crate) fn produce_tuple(&mut self, tuple: Tuple) { + pub(crate) fn produce(&mut self, tuple: TupleImpl) { self.result.tuple = tuple; self.resume(); } @@ -1124,7 +1115,7 @@ mod test_utils { } impl TestExecutor<'_, T> { - pub(crate) fn next_tuple(&mut self) -> Result, DatabaseError> { + pub(crate) fn next_tuple(&mut self) -> Result, DatabaseError> { self.executor.next_tuple(&mut self.plan_arena) } } @@ -1182,12 +1173,12 @@ mod test_utils { pub fn try_collect( executor: TestExecutor<'_, T>, - ) -> Result, DatabaseError> { + ) -> Result, DatabaseError> { let mut executor = executor; let mut tuples = Vec::new(); while let Some(tuple) = executor.next_tuple()? { - tuples.push(tuple.clone()); + tuples.push(std::mem::take(tuple)); } Ok(tuples) } @@ -1233,5 +1224,41 @@ mod test { assert_eq!(arena.nodes.items.len(), 1); assert_eq!(arena.nodes.items.as_ptr(), slot); } + + #[test] + fn rewrite_projects_joined_row() -> Result<(), DatabaseError> { + use crate::catalog::{ColumnCatalog, ColumnDesc}; + use crate::types::value::DataValue; + use crate::types::LogicalType; + + let table_arena = TableArenaCell::default(); + let mut plan_arena = PlanArena::new(&table_arena); + let column = plan_arena.alloc_column(ColumnCatalog::new( + "c".to_string(), + true, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, + )); + let exprs = [3, 0, 1].map(|position| { + plan_arena.alloc_expression(ScalarExpression::column_expr(column, position)) + }); + let ints = |values: [i32; 2]| Tuple::new(None, values.map(DataValue::Int32).to_vec()); + + let mut arena = ExecArena::<'_, MemoryTransaction>::with_capacity(0); + for base in [0, 10] { + arena.result.tuple = TupleImpl::concat( + ints([base, base + 1]).into(), + ints([base + 2, base + 3]).into(), + Some(DataValue::Int32(base)), + ); + arena.rewrite(&exprs, &plan_arena, None::<&[DataValue]>)?; + assert!(matches!(arena.result_tuple(), TupleImpl::Tuple(_))); + assert_eq!(arena.result_tuple().pk(), Some(&DataValue::Int32(base))); + assert_eq!( + *arena.result_tuple(), + [base + 3, base, base + 1].map(DataValue::Int32) + ); + } + Ok(()) + } } // GRCOV_EXCL_STOP diff --git a/src/execution/spill/codec.rs b/src/execution/spill/codec.rs index e3229ed9..1692b62e 100644 --- a/src/execution/spill/codec.rs +++ b/src/execution/spill/codec.rs @@ -16,21 +16,21 @@ use super::SpillCodec; use crate::errors::DatabaseError; use crate::planner::operator::sort::SortField; use crate::planner::{ExecMetaArena, MetaArena, ScalarQueryRef}; -use crate::types::tuple::Tuple; +use crate::types::tuple::{Tuple, TupleImpl}; use crate::types::value::DataValue; use std::io::{Read, Write}; use std::mem::size_of; pub(crate) struct SortRow { pub(crate) sort_values: Vec, - pub(crate) tuple: Tuple, + pub(crate) tuple: TupleImpl, pub(crate) outer_values: Vec<(ScalarQueryRef, DataValue)>, } impl SortRow { pub(crate) fn new( sort_fields: &[SortField], - tuple: Tuple, + tuple: TupleImpl, arena: &mut ExecMetaArena, ) -> Result { let sort_values = sort_fields @@ -60,7 +60,7 @@ impl SpillCodec for SortRow { fn decode(reader: &mut R) -> Result { Ok(Self { sort_values: Vec::::decode(reader)?, - tuple: Tuple::decode(reader)?, + tuple: TupleImpl::decode(reader)?, outer_values: Vec::decode(reader)?, }) } @@ -86,7 +86,7 @@ impl SpillCodec for SortRow { .saturating_add( self.tuple .estimated_size() - .saturating_sub(size_of::()), + .saturating_sub(size_of::()), ) } } @@ -329,6 +329,60 @@ impl SpillCodec for Tuple { } } +impl SpillCodec for TupleImpl { + fn encode(&self, writer: &mut W) -> Result<(), DatabaseError> { + if let TupleImpl::Tuple(tuple) = self { + return tuple.encode(writer); + } + match self.pk() { + Some(pk) => { + writer.write_all(&[1])?; + pk.encode(writer)?; + } + None => writer.write_all(&[0])?, + } + let len: u32 = self.len().try_into()?; + writer.write_all(&len.to_le_bytes())?; + for value in self.iter() { + value.encode(writer)?; + } + Ok(()) + } + + fn decode(reader: &mut R) -> Result { + Tuple::decode(reader).map(TupleImpl::Tuple) + } + + fn encoded_len(&self) -> Result { + if let TupleImpl::Tuple(tuple) = self { + return tuple.encoded_len(); + } + let _: u32 = self.len().try_into()?; + let pk_len = match self.pk() { + Some(pk) => 1 + pk.encoded_len()?, + None => 1, + }; + self.iter() + .try_fold(pk_len + size_of::(), |len, value| { + Ok(len + value.encoded_len()?) + }) + } + + fn estimated_size(&self) -> usize { + match self { + TupleImpl::Tuple(tuple) => tuple.estimated_size(), + TupleImpl::Concat(_) => size_of::() + .saturating_add(self.len().saturating_mul(size_of::())) + .saturating_add( + self.iter() + .chain(self.pk()) + .map(estimated_dynamic_value_size) + .fold(0usize, usize::saturating_add), + ), + } + } +} + fn estimated_dynamic_value_size(value: &DataValue) -> usize { match value { DataValue::Utf8 { value, .. } => value.capacity(), @@ -463,11 +517,23 @@ mod tests { round_trip(&(reference, DataValue::Null))?; } for pk in [None, Some(DataValue::new_utf8("primary".into()))] { - let tuple = Tuple::new(pk, values.clone()); + let tuple = Tuple::new(pk.clone(), values.clone()); round_trip(&tuple)?; + let (left, right) = values.split_at(values.len() / 2); + let joined = TupleImpl::concat( + Tuple::new(None, left.to_vec()).into(), + Tuple::new(None, right.to_vec()).into(), + pk, + ); + round_trip(&joined)?; + let mut flat_bytes = Vec::new(); + tuple.encode(&mut flat_bytes)?; + let mut joined_bytes = Vec::new(); + joined.encode(&mut joined_bytes)?; + assert_eq!(joined_bytes, flat_bytes); let row = SortRow { sort_values: vec![DataValue::Int32(2), DataValue::Null], - tuple, + tuple: joined, outer_values: vec![( ScalarQueryRef { arena_id: 0, diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 46812b0b..71b7ddbc 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -41,13 +41,13 @@ macro_rules! from_tuple { impl ::kite_sql::orm::FromQueryRow for $struct_name { fn from_query_row( schema: &::kite_sql::types::tuple::SchemaView<'_, '_>, - tuple: &mut ::kite_sql::types::tuple::Tuple, + tuple: &mut ::kite_sql::types::tuple::TupleImpl, ) -> ::std::result::Result { - fn try_get(tuple: &mut ::kite_sql::types::tuple::Tuple, schema: &::kite_sql::types::tuple::SchemaView<'_, '_>, field_name: &str) -> Option<::kite_sql::types::value::DataValue> { + fn try_get(tuple: &mut ::kite_sql::types::tuple::TupleImpl, schema: &::kite_sql::types::tuple::SchemaView<'_, '_>, field_name: &str) -> Option<::kite_sql::types::value::DataValue> { let ty = ::kite_sql::types::LogicalType::type_trans::()?; let idx = schema.position(field_name)?; - std::mem::replace(&mut tuple.values[idx], ::kite_sql::types::value::DataValue::Null).cast(&ty).ok() + std::mem::replace(&mut tuple[idx], ::kite_sql::types::value::DataValue::Null).cast(&ty).ok() } let mut struct_instance = $struct_name::default(); diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index 05079832..802e0cb7 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -159,7 +159,6 @@ fn lift_cross_project( if from_right { op.exprs.rotate_right(offset); } - op.moveable = None; let mut project = input.take(); **input = std::mem::replace(&mut *project.childrens, Childrens::None).pop_only(); @@ -249,7 +248,6 @@ impl NormalizationRule for EliminateProject { } } } - op.moveable = None; } Operator::Filter(op) => op.is_optimized = false, _ => (), @@ -263,7 +261,7 @@ impl NormalizationRule for EliminateProject { // Lift the Project: its expressions keep the parent's output layout. let mut project = child.take(); let mut parent = plan.take(); - + std::mem::swap(&mut parent.childrens, &mut project.childrens); parent.reset_output_schema_cache(); diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index c9bab9c1..33d18fa7 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -49,8 +49,6 @@ mod min_max_top_k; use guard_mark_apply::GuardMarkApply; use join_reorder::GreedyJoinReorder; use materialize_mark_apply::MaterializeMarkApply; -mod project_move; -use project_move::ProjectMove; mod parameterized_index; mod pushdown_limit; mod pushdown_predicates; @@ -95,7 +93,6 @@ pub enum NormalizationRuleImpl { GreedyJoinReorder, MaterializeMarkApply, GuardMarkApply, - ProjectMove, } #[derive(Debug, Copy, Clone, Eq, PartialEq)] @@ -187,9 +184,7 @@ impl NormalizationRuleImpl { NormalizationRuleImpl::ColumnPruning => NormalizationRuleRootTag::Any, NormalizationRuleImpl::EliminateProject => NormalizationRuleRootTag::Any, NormalizationRuleImpl::CollapseGroupByAgg => NormalizationRuleRootTag::Aggregate, - NormalizationRuleImpl::GuardMarkApply | NormalizationRuleImpl::ProjectMove => { - NormalizationRuleRootTag::Project - } + NormalizationRuleImpl::GuardMarkApply => NormalizationRuleRootTag::Project, NormalizationRuleImpl::CombineFilter => NormalizationRuleRootTag::Filter, NormalizationRuleImpl::LimitProjectTranspose | NormalizationRuleImpl::PushLimitThroughJoin @@ -227,7 +222,6 @@ impl NormalizationRule for NormalizationRuleImpl { } NormalizationRuleImpl::GuardMarkApply => GuardMarkApply.apply(plan, arena, loader), NormalizationRuleImpl::EliminateProject => EliminateProject.apply(plan, arena, loader), - NormalizationRuleImpl::ProjectMove => ProjectMove.apply(plan, arena, loader), NormalizationRuleImpl::CollapseGroupByAgg => { CollapseGroupByAgg.apply(plan, arena, loader) } diff --git a/src/optimizer/rule/normalization/project_move.rs b/src/optimizer/rule/normalization/project_move.rs deleted file mode 100644 index 1960aab2..00000000 --- a/src/optimizer/rule/normalization/project_move.rs +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright 2024 KipData/KiteSQL -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::errors::DatabaseError; -use crate::expression::function::scala::ScalarFunction; -use crate::expression::visitor::ExprVisitor; -use crate::expression::ScalarExpression; -use crate::optimizer::core::rule::NormalizationRule; -use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::Operator; -use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; -use crate::types::bitset::BitSet; - -pub struct ProjectMove; - -struct InputUses { - counts: Vec, - invalid: bool, -} - -impl ExprVisitor for InputUses { - fn visit_expression_ref(&mut self, expr: ExprRef, arena: &A) -> Result { - if let ScalarExpression::ColumnRef { position, .. } = arena.expression(expr) { - if let Some(count) = self.counts.get_mut(*position) { - *count = count.saturating_add(1).min(2); - } else { - self.invalid = true; - } - return Ok(false); - } - Ok(true) - } - - fn visit_scala_function( - &mut self, - _function: &ScalarFunction, - _arena: &A, - ) -> Result<(), DatabaseError> { - // Functions can read Input directly, beyond their declared arguments. - self.invalid = true; - Ok(()) - } -} - -impl NormalizationRule for ProjectMove { - fn apply( - &self, - plan: &mut LogicalPlan, - arena: &mut PlanArena, - _loader: Option<&StatisticMetaLoader<'_>>, - ) -> Result { - let (Operator::Project(op), Childrens::Only(input)) = - (&mut plan.operator, plan.childrens.as_mut()) - else { - return Ok(false); - }; - let width = input.output_schema(arena).len(); - let mut uses = InputUses { - counts: vec![0; width], - invalid: false, - }; - for expr in &op.exprs { - uses.visit(*expr, arena)?; - } - if uses.invalid || !uses.counts.contains(&1) { - return Ok(op.moveable.take().is_some()); - } - let changed = op.moveable.as_ref().is_none_or(|mask| { - uses.counts - .iter() - .enumerate() - .any(|(position, count)| mask.contains(position) != (*count == 1)) - }); - let moveable = op - .moveable - .get_or_insert_with(|| BitSet::with_capacity(width)); - moveable.clear(); - for (position, count) in uses.counts.iter().enumerate() { - if *count == 1 { - moveable.insert(position); - } - } - Ok(changed) - } -} - -// GRCOV_EXCL_START -#[cfg(test)] -mod tests { - use super::*; - use crate::catalog::{ColumnCatalog, ColumnDesc}; - use crate::expression::function::scala::ArcScalarFunctionImpl; - use crate::expression::AliasType; - use crate::expression::BinaryOperator; - use crate::function::upper::Upper; - use crate::planner::operator::project::ProjectOperator; - use crate::planner::operator::values::ValuesOperator; - use crate::planner::TableArenaCell; - use crate::types::value::DataValue; - use crate::types::LogicalType; - - fn input(arena: &mut PlanArena) -> Result<(LogicalPlan, [ExprRef; 3]), DatabaseError> { - let mut columns = Vec::new(); - let mut exprs = Vec::new(); - for position in 0..3 { - let column = arena.alloc_column(ColumnCatalog::new( - format!("c{position}"), - true, - ColumnDesc::new(LogicalType::Integer, None, false, None)?, - )); - columns.push(column); - exprs.push(arena.alloc_expression(ScalarExpression::column_expr(column, position))); - } - Ok(( - LogicalPlan::new( - Operator::Values(ValuesOperator::new(Vec::new(), 0, columns)), - Childrens::None, - ), - exprs.try_into().unwrap(), - )) - } - - fn project(plan: &LogicalPlan) -> &ProjectOperator { - let Operator::Project(op) = &plan.operator else { - unreachable!() - }; - op - } - - #[test] - fn counts_nested_and_repeated_positions_and_reuses_bitmap() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (source, [a, b, c]) = input(&mut arena)?; - let one = arena.alloc_expression(DataValue::Int32(1).into()); - let sum = arena.alloc_expression(ScalarExpression::Binary { - op: BinaryOperator::Plus, - left_expr: b, - right_expr: one, - evaluator: None, - ty: LogicalType::Integer, - }); - let mut plan = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![c, a, a, sum])), - Childrens::Only(Box::new(source)), - ); - assert!(project(&plan).moveable.is_none()); - assert!(ProjectMove.apply(&mut plan, &mut arena, None)?); - assert!(!project(&plan).moveable.as_ref().unwrap().contains(0)); - assert!(project(&plan).moveable.as_ref().unwrap().contains(1)); - assert!(project(&plan).moveable.as_ref().unwrap().contains(2)); - assert!(!ProjectMove.apply(&mut plan, &mut arena, None)?); - - let Operator::Project(op) = &mut plan.operator else { - unreachable!() - }; - op.exprs = vec![b, b]; - assert!(ProjectMove.apply(&mut plan, &mut arena, None)?); - assert!(project(&plan).moveable.is_none()); - assert!(!ProjectMove.apply(&mut plan, &mut arena, None)?); - Ok(()) - } - - #[test] - fn counts_only_the_evaluated_alias_branch() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (source, [a, _, c]) = input(&mut arena)?; - let named = arena.alloc_expression(ScalarExpression::Alias { - expr: a, - alias: AliasType::Name("named".into()), - }); - let mapped = arena.alloc_expression(ScalarExpression::Alias { - expr: c, - alias: AliasType::Expr(a), - }); - let mut plan = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![named, mapped])), - Childrens::Only(Box::new(source)), - ); - assert!(ProjectMove.apply(&mut plan, &mut arena, None)?); - assert!(project(&plan).moveable.as_ref().unwrap().contains(0)); - assert!(!project(&plan).moveable.as_ref().unwrap().contains(1)); - assert!(project(&plan).moveable.as_ref().unwrap().contains(2)); - let Operator::Project(op) = &mut plan.operator else { - unreachable!() - }; - op.exprs.push(c); - assert!(ProjectMove.apply(&mut plan, &mut arena, None)?); - assert!(project(&plan).moveable.as_ref().unwrap().contains(0)); - assert!(!project(&plan).moveable.as_ref().unwrap().contains(2)); - Ok(()) - } - - #[test] - fn unknown_function_reads_and_invalid_positions_clear_previous_marks( - ) -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (source, [a, b, c]) = input(&mut arena)?; - let mut plan = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![a, b, c])), - Childrens::Only(Box::new(source)), - ); - assert!(ProjectMove.apply(&mut plan, &mut arena, None)?); - let function = arena.alloc_expression(ScalarExpression::ScalaFunction(ScalarFunction { - args: vec![a], - inner: ArcScalarFunctionImpl(Upper::new()), - })); - let Operator::Project(op) = &mut plan.operator else { - unreachable!() - }; - op.exprs = vec![function, b]; - assert!(ProjectMove.apply(&mut plan, &mut arena, None)?); - assert!(project(&plan).moveable.is_none()); - assert!(!ProjectMove.apply(&mut plan, &mut arena, None)?); - - let ScalarExpression::ColumnRef { column, .. } = arena.expression(a) else { - unreachable!() - }; - let invalid = arena.alloc_expression(ScalarExpression::column_expr(*column, 3)); - let Operator::Project(op) = &mut plan.operator else { - unreachable!() - }; - op.exprs = vec![invalid, b]; - assert!(!ProjectMove.apply(&mut plan, &mut arena, None)?); - assert!(project(&plan).moveable.is_none()); - Ok(()) - } -} -// GRCOV_EXCL_STOP diff --git a/src/orm/mod.rs b/src/orm/mod.rs index 3ce64423..fa08ade1 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -18,7 +18,7 @@ use crate::planner::operator::mark_apply::MarkApplyQuantifier; use crate::planner::operator::sort::SortField; use crate::planner::{ExprRef, LogicalPlan, PlanArena}; use crate::storage::{Storage, Transaction}; -use crate::types::tuple::{SchemaView, Tuple}; +use crate::types::tuple::{SchemaView, TupleImpl}; use crate::types::value::DataValue; use crate::types::CharLengthUnits; use crate::types::LogicalType; @@ -83,7 +83,10 @@ pub struct DescribeColumn { } impl FromQueryRow for DescribeColumn { - fn from_query_row(_: &SchemaView<'_, '_>, tuple: &mut Tuple) -> Result { + fn from_query_row( + _: &SchemaView<'_, '_>, + tuple: &mut TupleImpl, + ) -> Result { let field = describe_text_value(take_projected_value(tuple, 0)); let data_type = describe_text_value(take_projected_value(tuple, 1)); let len = describe_text_value(take_projected_value(tuple, 2)); @@ -2810,7 +2813,7 @@ pub trait FromDataValue: Sized { /// ``` pub trait FromQueryTuple: Sized { /// Decodes one projected tuple into `Self`. - fn from_query_tuple(tuple: &mut Tuple) -> Result; + fn from_query_tuple(tuple: &mut TupleImpl) -> Result; } /// Conversion trait from a query result row into a Rust value. @@ -2820,7 +2823,7 @@ pub trait FromQueryRow: Sized { /// Decodes one result row into `Self`. fn from_query_row( schema: &SchemaView<'_, '_>, - tuple: &mut Tuple, + tuple: &mut TupleImpl, ) -> Result; } @@ -2994,7 +2997,7 @@ pub trait DecimalType {} #[doc(hidden)] pub fn take_value_at( - tuple: &mut Tuple, + tuple: &mut TupleImpl, index: Option, field_name: &str, ) -> Result { @@ -3002,7 +3005,7 @@ pub fn take_value_at( name: field_name.to_string(), span: None, })?; - let value = tuple.values.get_mut(idx).ok_or(DatabaseError::MisMatch( + let value = tuple.get_mut(idx).ok_or(DatabaseError::MisMatch( "the query result schema", "the query result tuple", ))?; @@ -3246,9 +3249,9 @@ macro_rules! impl_from_query_tuple { $($name: FromDataValue,)+ { #[allow(non_snake_case)] - fn from_query_tuple(tuple: &mut Tuple) -> Result { + fn from_query_tuple(tuple: &mut TupleImpl) -> Result { let expected_len = [$(stringify!($name)),+].len(); - if tuple.values.len() != expected_len { + if tuple.len() != expected_len { return Err(DatabaseError::MisMatch( "the expected tuple projection width", "the query result", @@ -3360,9 +3363,8 @@ fn invalid_from_data_value(value: &DataValue) -> DatabaseError { )) } -fn take_projected_value(tuple: &mut Tuple, index: usize) -> Option { +fn take_projected_value(tuple: &mut TupleImpl, index: usize) -> Option { tuple - .values .get_mut(index) .map(|value| std::mem::replace(value, DataValue::Null)) } @@ -3378,8 +3380,8 @@ fn extract_projected_data_value( convert_projected_value::(value) } -fn extract_value_from_tuple(tuple: &mut Tuple) -> Result { - let value = if tuple.values.len() == 1 { +fn extract_value_from_tuple(tuple: &mut TupleImpl) -> Result { + let value = if tuple.len() == 1 { take_projected_value(tuple, 0).expect("checked one projected expression") } else { return Err(DatabaseError::MisMatch( @@ -3391,7 +3393,7 @@ fn extract_value_from_tuple(tuple: &mut Tuple) -> Result(value) } -fn extract_projected_tuple(tuple: &mut Tuple) -> Result { +fn extract_projected_tuple(tuple: &mut TupleImpl) -> Result { T::from_query_tuple(tuple) } @@ -3455,7 +3457,7 @@ fn orm_list<'a, E: BindSource<'a>, M: Model>( mod tests { use super::*; use crate::expression::BinaryOperator; - use crate::types::tuple::{Schema, SchemaView}; + use crate::types::tuple::{Schema, SchemaView, Tuple}; use std::sync::Arc; #[derive(Debug, PartialEq, Eq)] @@ -3515,7 +3517,7 @@ mod tests { impl FromQueryRow for OrmUnitUser { fn from_query_row( schema: &SchemaView<'_, '_>, - tuple: &mut Tuple, + tuple: &mut TupleImpl, ) -> Result { Ok(Self { id: take_value_at(tuple, schema.position("id"), "id")?, @@ -3603,7 +3605,7 @@ mod tests { impl FromQueryRow for OrmUnitOrder { fn from_query_row( schema: &SchemaView<'_, '_>, - tuple: &mut Tuple, + tuple: &mut TupleImpl, ) -> Result { Ok(Self { id: take_value_at(tuple, schema.position("id"), "id")?, @@ -3718,7 +3720,7 @@ mod tests { let arena = PlanArena::new(&table_arena); let schema: Schema = Vec::new(); let schema_view = SchemaView::new(&schema, &arena); - let mut tuple = Tuple::new( + let mut tuple: TupleImpl = Tuple::new( None, vec![ DataValue::from("id".to_string()), @@ -3728,7 +3730,8 @@ mod tests { DataValue::from("PRI".to_string()), DataValue::Null, ], - ); + ) + .into(); let column = DescribeColumn::from_query_row(&schema_view, &mut tuple)?; assert_eq!( @@ -3742,10 +3745,7 @@ mod tests { default: "NULL".to_string(), } ); - assert!(tuple - .values - .iter() - .all(|value| matches!(value, DataValue::Null))); + assert!(tuple.iter().all(|value| matches!(value, DataValue::Null))); assert_eq!(describe_text_value(None), ""); Ok(()) @@ -3810,29 +3810,29 @@ mod tests { #[test] fn tuple_projection_helpers_cast_extract_and_report_width_mismatch() -> Result<(), DatabaseError> { - let mut tuple = Tuple::new( + let mut tuple: TupleImpl = Tuple::new( None, vec![DataValue::Int8(7), DataValue::from("seven".to_string())], - ); + ) + .into(); assert_eq!(take_value_at::(&mut tuple, Some(0), "id")?, 7); - assert!(matches!(tuple.values[0], DataValue::Null)); + assert!(matches!(tuple[0], DataValue::Null)); assert!(matches!( take_value_at::(&mut tuple, None, "missing"), Err(DatabaseError::ColumnNotFound { .. }) )); - let mut tuple = Tuple::new( + let mut tuple: TupleImpl = Tuple::new( None, vec![DataValue::Int32(1), DataValue::from("one".to_string())], - ); + ) + .into(); let projected = <(i32, String) as FromQueryTuple>::from_query_tuple(&mut tuple)?; assert_eq!(projected, (1, "one".to_string())); - assert!(tuple - .values - .iter() - .all(|value| matches!(value, DataValue::Null))); + assert!(tuple.iter().all(|value| matches!(value, DataValue::Null))); - let mut too_wide = Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(2)]); + let mut too_wide: TupleImpl = + Tuple::new(None, vec![DataValue::Int32(1), DataValue::Int32(2)]).into(); assert!(matches!( extract_value_from_tuple::(&mut too_wide), Err(DatabaseError::MisMatch( @@ -3841,7 +3841,7 @@ mod tests { )) )); - let mut too_narrow = Tuple::new(None, vec![DataValue::Int32(1)]); + let mut too_narrow: TupleImpl = Tuple::new(None, vec![DataValue::Int32(1)]).into(); assert!(matches!( <(i32, i32) as FromQueryTuple>::from_query_tuple(&mut too_narrow), Err(DatabaseError::MisMatch( @@ -4204,10 +4204,7 @@ mod tests { let mut iter = database.run("select id,name,age from orm_unit_users where id>3 order by id")?; let mut rows = Vec::new(); - while iter - .next_tuple(|_, row| rows.push(row.values.clone()))? - .is_some() - {} + while iter.next_tuple(|_, row| rows.push(row.clone()))?.is_some() {} iter.done()?; assert_eq!( rows, diff --git a/src/planner/operator/project.rs b/src/planner/operator/project.rs index fcd7ac83..00140a7a 100644 --- a/src/planner/operator/project.rs +++ b/src/planner/operator/project.rs @@ -14,21 +14,16 @@ use crate::planner::MetaArena; use crate::planner::{fmt_explain_list, Explain, ExprRef}; -use crate::types::bitset::BitSet; use kite_sql_serde_macros::ReferenceSerialization; #[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] pub struct ProjectOperator { pub exprs: Vec, - pub moveable: Option, } impl ProjectOperator { pub fn new(exprs: Vec) -> Self { - Self { - exprs, - moveable: None, - } + Self { exprs } } } diff --git a/src/python.rs b/src/python.rs index 91da11a5..7d179fe6 100644 --- a/src/python.rs +++ b/src/python.rs @@ -21,7 +21,7 @@ use crate::storage::lmdb::LmdbStorage; use crate::storage::memory::MemoryStorage; #[cfg(feature = "rocksdb")] use crate::storage::rocksdb::RocksStorage; -use crate::types::tuple::{SchemaView, Tuple}; +use crate::types::tuple::{SchemaView, TupleImpl}; use crate::types::value::DataValue; use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; @@ -70,16 +70,15 @@ fn data_value_to_py(py: Python<'_>, value: &DataValue) -> PyResult { Ok(object) } -fn tuple_to_python_row(py: Python<'_>, tuple: &Tuple) -> PyResult { +fn tuple_to_python_row(py: Python<'_>, tuple: &TupleImpl) -> PyResult { let row = PyDict::new(py); - match tuple.pk.as_ref() { + match tuple.pk() { Some(pk) => row.set_item("pk", data_value_to_py(py, pk)?)?, None => row.set_item("pk", py.None())?, } let values = tuple - .values .iter() .map(|value| data_value_to_py(py, value)) .collect::>>()?; @@ -170,7 +169,7 @@ enum PythonResultIterInner { impl PythonResultIterInner { fn next_tuple( &mut self, - f: impl FnOnce(&mut Tuple) -> R, + f: impl FnOnce(&mut TupleImpl) -> R, ) -> Result, DatabaseError> { match self { #[cfg(feature = "lmdb")] diff --git a/src/storage/lmdb.rs b/src/storage/lmdb.rs index 94621ca8..86162b54 100644 --- a/src/storage/lmdb.rs +++ b/src/storage/lmdb.rs @@ -414,7 +414,7 @@ mod tests { let mut iter = kite_sql.run("select b from t1 where a = 2").unwrap(); let tuple = iter.next_tuple(|_, tuple| tuple.clone()).unwrap().unwrap(); - assert_eq!(tuple.values[0].to_string(), "20"); + assert_eq!(tuple[0].to_string(), "20"); iter.done().unwrap(); } diff --git a/src/storage/memory.rs b/src/storage/memory.rs index bdf5e794..5612cc63 100644 --- a/src/storage/memory.rs +++ b/src/storage/memory.rs @@ -275,7 +275,8 @@ mod wasm_tests { &Tuple::new( Some(DataValue::Int32(1)), vec![DataValue::Int32(1), DataValue::Boolean(true)], - ), + ) + .into(), &[ LogicalType::Integer.serializable(), LogicalType::Boolean.serializable(), @@ -288,7 +289,8 @@ mod wasm_tests { &Tuple::new( Some(DataValue::Int32(2)), vec![DataValue::Int32(2), DataValue::Boolean(true)], - ), + ) + .into(), &[ LogicalType::Integer.serializable(), LogicalType::Boolean.serializable(), @@ -440,7 +442,8 @@ mod native_tests { &Tuple::new( Some(DataValue::Int32(1)), vec![DataValue::Int32(1), DataValue::Boolean(true)], - ), + ) + .into(), &[ LogicalType::Integer.serializable(), LogicalType::Boolean.serializable(), @@ -453,7 +456,8 @@ mod native_tests { &Tuple::new( Some(DataValue::Int32(2)), vec![DataValue::Int32(2), DataValue::Boolean(true)], - ), + ) + .into(), &[ LogicalType::Integer.serializable(), LogicalType::Boolean.serializable(), diff --git a/src/storage/mod.rs b/src/storage/mod.rs index d6a03898..a7d43ac4 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -37,7 +37,7 @@ use crate::serdes::ReferenceTables; use crate::storage::table_codec::{Bytes, IndexStatisticsCodecType, TableCodec, BOUND_MAX_TAG}; use crate::types::index::{Index, IndexId, IndexMeta, IndexMetaRef, IndexType}; use crate::types::serialize::TupleValueSerializableImpl; -use crate::types::tuple::{Tuple, TupleId}; +use crate::types::tuple::{Tuple, TupleId, TupleImpl, TupleLike}; use crate::types::value::{DataValue, OrderedMapping, TupleMappingRef}; use crate::types::{ColumnId, LogicalType}; use std::borrow::{Borrow, Cow}; @@ -380,11 +380,11 @@ pub trait Transaction: Sized { Ok((table, index_id)) } - fn add_index( + fn add_index( &mut self, table_codec: &mut TableCodec, table_name: &str, - index: Index, + index: Index<'_, V>, tuple_id: &TupleId, ) -> Result<(), DatabaseError> { if matches!(index.ty, IndexType::PrimaryKey { .. }) { @@ -406,11 +406,11 @@ pub trait Transaction: Sized { }) } - fn del_index( + fn del_index( &mut self, table_codec: &mut TableCodec, table_name: &str, - index: &Index, + index: &Index<'_, V>, tuple_id: &TupleId, ) -> Result<(), DatabaseError> { if matches!(index.ty, IndexType::PrimaryKey { .. }) { @@ -425,7 +425,7 @@ pub trait Transaction: Sized { &mut self, table_codec: &mut TableCodec, table_name: &str, - tuple: &Tuple, + tuple: &TupleImpl, serializers: I, is_overwrite: bool, ) -> Result<(), DatabaseError> @@ -433,14 +433,13 @@ pub trait Transaction: Sized { I: IntoIterator, S: Borrow, { - let tuple_id = tuple.pk.as_ref().ok_or(DatabaseError::PrimaryKeyNotFound)?; + let tuple_id = tuple.pk().ok_or(DatabaseError::PrimaryKeyNotFound)?; let mut serializers = serializers.into_iter(); - let mut write_value = - |tuple: &Tuple, value: &mut Bytes| tuple.serialize_to(&mut serializers, value); + let mut write_value = |value: &mut Bytes| tuple.serialize_to(&mut serializers, value); table_codec.with_tuple( table_name, tuple_id, - Some((tuple, &mut write_value)), + Some(&mut write_value), |key, value| { if !is_overwrite && self.exists(key)? { return Err(DatabaseError::DuplicatePrimaryKey); @@ -2429,7 +2428,7 @@ pub(crate) fn check_explicit_transaction_does_not_rescan_own_writes( .done()?; let mut iter = tx.run("select id from t order by id")?; let mut ids = Vec::new(); - while let Some(id) = iter.next_tuple(|_, tuple| tuple.values[0].clone())? { + while let Some(id) = iter.next_tuple(|_, tuple| tuple[0].clone())? { ids.push(id); } iter.done()?; @@ -2666,11 +2665,11 @@ mod test { let plan_arena = PlanArena::new(&table_arena); let tuples = build_tuples(); - for tuple in tuples.iter() { + for tuple in build_tuples() { transaction.append_tuple( &mut table_codec, "t1", - tuple, + &tuple.into(), &[ LogicalType::Integer.serializable(), LogicalType::Boolean.serializable(), @@ -2981,11 +2980,11 @@ mod test { for (tuple_id, index) in indexes.iter().cloned() { transaction.add_index(&mut table_codec, "t1", index, &tuple_id)?; } - for tuple in tuples.iter() { + for tuple in build_tuples() { transaction.append_tuple( &mut table_codec, "t1", - tuple, + &tuple.into(), &[ LogicalType::Integer.serializable(), LogicalType::Boolean.serializable(), @@ -3060,22 +3059,24 @@ mod test { LogicalType::Integer.serializable(), ]; - let initial_tuple = Tuple::new( + let initial_tuple: crate::types::tuple::TupleImpl = Tuple::new( Some(DataValue::Int32(0)), vec![ DataValue::Int32(0), DataValue::Boolean(true), DataValue::Int32(0), ], - ); - let updated_tuple = Tuple::new( + ) + .into(); + let updated_tuple: crate::types::tuple::TupleImpl = Tuple::new( Some(DataValue::Int32(0)), vec![ DataValue::Int32(0), DataValue::Boolean(true), DataValue::Int32(1), ], - ); + ) + .into(); let table_arena = TableArenaCell::default(); let mut plan_arena = PlanArena::new(&table_arena); @@ -3103,10 +3104,10 @@ mod test { "t1", Index::new( index_id, - std::slice::from_ref(&initial_tuple.values[2]), + std::slice::from_ref(&initial_tuple[2]), IndexType::Normal, ), - initial_tuple.pk.as_ref().unwrap(), + initial_tuple.pk().unwrap(), )?; setup_tx.append_tuple(&mut table_codec, "t1", &initial_tuple, &serializers, false)?; setup_tx.commit()?; @@ -3145,20 +3146,20 @@ mod test { "t1", &Index::new( index_id, - std::slice::from_ref(&initial_tuple.values[2]), + std::slice::from_ref(&initial_tuple[2]), IndexType::Normal, ), - initial_tuple.pk.as_ref().unwrap(), + initial_tuple.pk().unwrap(), )?; writer_tx.add_index( &mut table_codec, "t1", Index::new( index_id, - std::slice::from_ref(&updated_tuple.values[2]), + std::slice::from_ref(&updated_tuple[2]), IndexType::Normal, ), - updated_tuple.pk.as_ref().unwrap(), + updated_tuple.pk().unwrap(), )?; writer_tx.append_tuple(&mut table_codec, "t1", &updated_tuple, &serializers, true)?; writer_tx.commit()?; diff --git a/src/storage/rocksdb.rs b/src/storage/rocksdb.rs index f5f5c3ab..f6491a1d 100644 --- a/src/storage/rocksdb.rs +++ b/src/storage/rocksdb.rs @@ -1044,7 +1044,8 @@ mod test { &Tuple::new( Some(DataValue::Int32(1)), vec![DataValue::Int32(1), DataValue::Boolean(true)], - ), + ) + .into(), &[ LogicalType::Integer.serializable(), LogicalType::Boolean.serializable(), @@ -1057,7 +1058,8 @@ mod test { &Tuple::new( Some(DataValue::Int32(2)), vec![DataValue::Int32(2), DataValue::Boolean(true)], - ), + ) + .into(), &[ LogicalType::Integer.serializable(), LogicalType::Boolean.serializable(), diff --git a/src/storage/table_codec.rs b/src/storage/table_codec.rs index 81878d98..fd8e137e 100644 --- a/src/storage/table_codec.rs +++ b/src/storage/table_codec.rs @@ -27,7 +27,7 @@ use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTab use crate::storage::{TableCache, Transaction}; use crate::types::index::{Index, IndexId, IndexMeta, IndexType, INDEX_ID_LEN}; use crate::types::serialize::TupleValueSerializableImpl; -use crate::types::tuple::{Tuple, TupleId}; +use crate::types::tuple::{Tuple, TupleId, TupleLike}; use crate::types::value::{DataValue, IndexKeyMapping}; use crate::types::{ColumnId, LogicalType}; use std::borrow::Borrow; @@ -54,7 +54,7 @@ static EMPTY_REFERENCE_TABLES: LazyLock = LazyLock::new(Referen pub type Bytes = Vec; pub type BumpBytes<'bump> = bumpalo::collections::Vec<'bump, u8>; -type TupleValueWriter<'a> = &'a mut dyn FnMut(&Tuple, &mut Bytes) -> Result<(), DatabaseError>; +type TupleValueWriter<'a> = &'a mut dyn FnMut(&mut Bytes) -> Result<(), DatabaseError>; #[derive(Default)] pub struct TableCodec { @@ -294,7 +294,7 @@ impl TableCodec { &mut self, table_name: &str, tuple_id: &TupleId, - tuple_value: Option<(&Tuple, TupleValueWriter<'_>)>, + tuple_value: Option>, f: impl FnOnce(&[u8], &[u8]) -> Result, ) -> Result { Self::check_primary_key(tuple_id, 0)?; @@ -306,7 +306,7 @@ impl TableCodec { &mut self, table_name: &str, tuple_id: &TupleId, - tuple_value: Option<(&Tuple, TupleValueWriter<'_>)>, + tuple_value: Option>, f: impl FnOnce(&[u8], &[u8]) -> Result, ) -> Result { self.clear_buffers(); @@ -316,8 +316,8 @@ impl TableCodec { lower.push(BOUND_MIN_TAG); tuple_id.memcomparable_encode(lower)?; - if let Some((tuple, serializers)) = tuple_value { - serializers(tuple, value)?; + if let Some(write_value) = tuple_value { + write_value(value)?; value.extend_from_slice(&stamp.to_le_bytes()); } @@ -430,10 +430,10 @@ impl TableCodec { /// /// Unique index key: /// `{TableName}{INDEX_TAG}{BOUND_MIN_TAG}{IndexID}{BOUND_MIN_TAG}{DataValue}` - pub fn with_index( + pub fn with_index( &mut self, table_name: &str, - index: &Index, + index: &Index<'_, V>, tuple_id: Option<&TupleId>, f: impl FnOnce(&[u8], &[u8]) -> Result, ) -> Result { @@ -444,8 +444,8 @@ impl TableCodec { lower.push(BOUND_MIN_TAG); lower.extend_from_slice(&index.id.to_le_bytes()); lower.push(BOUND_MIN_TAG); - for value in index.values { - value.memcomparable_encode(lower)?; + for i in 0..index.values.len() { + index.values.value_at(i).memcomparable_encode(lower)?; } if let Some(tuple_id) = tuple_id { @@ -1182,7 +1182,7 @@ mod tests { }, DataValue::Int32(99), ]; - let index = Index::new(7, &values, IndexType::Normal); + let index = Index::new(7, values.as_slice(), IndexType::Normal); let mut codec = TableCodec::default(); let key = codec.with_index("cover", &index, Some(&DataValue::Int32(1)), |key, _| { Ok(key.to_vec()) @@ -1221,20 +1221,19 @@ mod tests { let table_catalog = build_table_codec(&table_arena); let plan_arena = PlanArena::new(&table_arena); - let expected = Tuple::new( + let expected = crate::types::tuple::TupleImpl::from(Tuple::new( Some(DataValue::Int32(0)), vec![DataValue::Int32(0), DataValue::Decimal(Decimal::new(1, 0))], - ); + )); let serializers = [ LogicalType::Integer.serializable(), LogicalType::Decimal(None, None).serializable(), ]; - let mut write_value = - |tuple: &Tuple, value: &mut Bytes| tuple.serialize_to(&serializers, value); + let mut write_value = |value: &mut Bytes| expected.serialize_to(&serializers, value); let bytes = TableCodec::default().with_tuple( "t1", &DataValue::Int32(0), - Some((&expected, &mut write_value)), + Some(&mut write_value), |_, value| Ok(value.to_vec()), )?; let deserializers = table_catalog @@ -1479,7 +1478,7 @@ mod tests { #[test] fn test_table_codec_index() -> Result<(), DatabaseError> { let tuple_id = DataValue::Int32(0); - let index = Index::new(0, &[], IndexType::Unique); + let index = Index::new(0, &[][..], IndexType::Unique); let bytes = TableCodec::default() .with_index("t1", &index, Some(&tuple_id), |_, value| Ok(value.to_vec()))?; @@ -1805,7 +1804,7 @@ mod tests { let value = Arc::new(value); let index = Index::new( index_id as u32, - std::slice::from_ref(&value), + std::slice::from_ref(value.as_ref()), IndexType::PrimaryKey { is_multiple: false }, ); @@ -1858,7 +1857,7 @@ mod tests { let value = Arc::new(value); let index = Index::new( index_id as u32, - std::slice::from_ref(&value), + std::slice::from_ref(value.as_ref()), IndexType::PrimaryKey { is_multiple: false }, ); diff --git a/src/types/index.rs b/src/types/index.rs index b09cc940..020355d1 100644 --- a/src/types/index.rs +++ b/src/types/index.rs @@ -21,6 +21,7 @@ use crate::planner::Explain; use crate::planner::ExprRef; use crate::planner::MetaArena; use crate::types::serialize::TupleValueSerializableImpl; +use crate::types::tuple::TupleLike; use crate::types::value::DataValue; use crate::types::{ColumnId, LogicalType}; use kite_sql_serde_macros::ReferenceSerialization; @@ -130,15 +131,23 @@ impl IndexMeta { } } -#[derive(Debug, Clone, Copy)] -pub struct Index<'a> { +#[derive(Debug)] +pub struct Index<'a, V: TupleLike + ?Sized = [DataValue]> { pub id: IndexId, - pub values: &'a [DataValue], + pub values: &'a V, pub ty: IndexType, } -impl<'a> Index<'a> { - pub fn new(id: IndexId, values: &'a [DataValue], ty: IndexType) -> Self { +impl Clone for Index<'_, V> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for Index<'_, V> {} + +impl<'a, V: TupleLike + ?Sized> Index<'a, V> { + pub fn new(id: IndexId, values: &'a V, ty: IndexType) -> Self { Index { id, values, ty } } } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index d8bf0b8d..b54fff79 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -16,14 +16,11 @@ use crate::catalog::{ColumnCatalog, ColumnRef}; use crate::errors::DatabaseError; use crate::iter_ext::Itertools; use crate::planner::MetaArena; -use crate::types::bitset::BitSet; use crate::types::serialize::{TupleValueSerializable, TupleValueSerializableImpl}; use crate::types::value::DataValue; use std::borrow::Borrow; use std::borrow::Cow; -use std::cell::Cell; use std::io::Cursor; -use std::slice::from_raw_parts; const BITS_MAX_INDEX: usize = 8; @@ -108,45 +105,6 @@ impl Input for InputRef<'_, T> { } } -pub(crate) struct MoveableTuple<'a> { - values: &'a [Cell], - moveable: Option<&'a BitSet>, -} - -impl<'a> MoveableTuple<'a> { - /// # Safety - /// The input prefix must remain allocated and must not be accessed mutably - /// outside this view until evaluation finishes. Appended output must not - /// reallocate the buffer. Borrowed columns must never be modified. - pub(crate) unsafe fn from_raw_parts( - values: *mut DataValue, - len: usize, - moveable: Option<&'a BitSet>, - ) -> Self { - Self { - values: from_raw_parts(values.cast::>(), len), - moveable, - } - } -} - -impl Input for MoveableTuple<'_> { - fn width(&self) -> usize { - self.values.len() - } - - fn column(&self, position: usize) -> Cow<'_, DataValue> { - let value = &self.values[position]; - if self.moveable.is_some_and(|mask| mask.contains(position)) { - Cow::Owned(value.replace(DataValue::Null)) - } else { - // The mask is immutable. Borrowed slots are never written, and - // movable slots never return a reference to their contents. - Cow::Borrowed(unsafe { &*value.as_ptr() }) - } - } -} - pub trait TupleLike { fn len(&self) -> usize; @@ -258,6 +216,392 @@ impl TupleLike for SplitTupleRef<' } } +#[derive(Clone, Debug)] +pub enum TupleImpl { + Tuple(Tuple), + Concat(Box), +} + +#[derive(Clone, Debug)] +pub struct ConcatTuple { + pk: Option, + left: TupleImpl, + right: TupleImpl, + left_len: usize, +} + +impl ConcatTuple { + #[inline] + fn route(&self, position: &mut usize) -> bool { + if *position < self.left_len { + true + } else { + *position -= self.left_len; + false + } + } +} + +impl Default for TupleImpl { + fn default() -> Self { + TupleImpl::Tuple(Tuple::default()) + } +} + +impl From for TupleImpl { + fn from(tuple: Tuple) -> Self { + TupleImpl::Tuple(tuple) + } +} + +impl TupleImpl { + pub fn concat(left: TupleImpl, right: TupleImpl, pk: Option) -> Self { + let left_len = left.len(); + TupleImpl::Concat(Box::new(ConcatTuple { + pk, + left, + right, + left_len, + })) + } + + #[inline] + pub fn pk(&self) -> Option<&TupleId> { + match self { + TupleImpl::Tuple(tuple) => tuple.pk.as_ref(), + TupleImpl::Concat(concat) => concat.pk.as_ref(), + } + } + + #[inline] + pub fn take_pk(&mut self) -> Option { + match self { + TupleImpl::Tuple(tuple) => tuple.pk.take(), + TupleImpl::Concat(concat) => concat.pk.take(), + } + } + + #[inline] + pub fn set_pk(&mut self, pk: Option) { + match self { + TupleImpl::Tuple(tuple) => tuple.pk = pk, + TupleImpl::Concat(concat) => concat.pk = pk, + } + } + + #[inline] + pub fn len(&self) -> usize { + TupleLike::len(self) + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + #[inline] + pub fn get(&self, index: usize) -> Option<&DataValue> { + (index < self.len()).then(|| self.value_at(index)) + } + + #[inline] + pub fn get_mut(&mut self, index: usize) -> Option<&mut DataValue> { + if index < self.len() { + Some(&mut self[index]) + } else { + None + } + } + + pub fn push(&mut self, value: DataValue) { + self.rightmost_mut().values.push(value); + } + + pub fn remove(&mut self, mut index: usize) -> DataValue { + let mut node = self; + loop { + match node { + TupleImpl::Tuple(tuple) => return tuple.values.remove(index), + TupleImpl::Concat(concat) => { + node = if concat.route(&mut index) { + concat.left_len -= 1; + &mut concat.left + } else { + &mut concat.right + }; + } + } + } + } + + pub fn resize(&mut self, len: usize, value: DataValue) { + let prefix: usize = self.len() - self.rightmost_mut().values.len(); + debug_assert!(len >= prefix, "resize cannot shrink into the left prefix"); + self.rightmost_mut() + .values + .resize(len.saturating_sub(prefix), value); + } + + pub(crate) fn rightmost_mut(&mut self) -> &mut Tuple { + let mut node = self; + loop { + match node { + TupleImpl::Tuple(tuple) => return tuple, + TupleImpl::Concat(concat) => node = &mut concat.right, + } + } + } + + fn value_at_mut(&mut self, mut index: usize) -> &mut DataValue { + let mut node = self; + loop { + match node { + TupleImpl::Tuple(tuple) => return &mut tuple.values[index], + TupleImpl::Concat(concat) => { + node = if concat.route(&mut index) { + &mut concat.left + } else { + &mut concat.right + }; + } + } + } + } + + /// e.g.: bits(u8)..|data_0(len for utf8_1)|utf8_0|data_1| + /// Tips: all len is u32 + pub fn serialize_to( + &self, + serializers: I, + bytes: &mut Vec, + ) -> Result<(), DatabaseError> + where + I: IntoIterator, + S: Borrow, + { + fn flip_bit(bits: u8, i: usize) -> u8 { + bits | (1 << (7 - i)) + } + + let values_len = self.len(); + let bits_len = (values_len + BITS_MAX_INDEX) / BITS_MAX_INDEX; + let values_bytes_len = self + .iter() + .map(DataValue::serialized_len_hint) + .sum::(); + bytes.clear(); + bytes.reserve(bits_len + values_bytes_len); + bytes.resize(bits_len, 0u8); + + for (i, (value, serializer)) in self.iter().zip(serializers).enumerate() { + if value.is_null() { + bytes[i / BITS_MAX_INDEX] = flip_bit(bytes[i / BITS_MAX_INDEX], i % BITS_MAX_INDEX); + } else { + serializer.borrow().to_raw(value, bytes)?; + } + } + + Ok(()) + } + + pub fn iter(&self) -> TupleImplIter<'_> { + match self.as_slice() { + Some(values) => TupleImplIter::Flat(values.iter()), + None => TupleImplIter::Joined { + tuple: self, + position: 0, + len: self.len(), + }, + } + } + + pub fn take_values(&mut self) -> Vec { + match self { + TupleImpl::Tuple(tuple) => std::mem::take(&mut tuple.values), + TupleImpl::Concat(_) => { + let mut values = Vec::with_capacity(self.len()); + self.drain_values(|value| values.push(value)); + values + } + } + } + + pub fn drain_values(&mut self, mut f: impl FnMut(DataValue)) { + self.drain_leaves(&mut f); + self.output_mut().clear(); + } + + fn drain_leaves(&mut self, f: &mut impl FnMut(DataValue)) { + match self { + TupleImpl::Tuple(tuple) => tuple.values.drain(..).for_each(f), + TupleImpl::Concat(concat) => { + concat.left.drain_leaves(f); + concat.right.drain_leaves(f); + } + } + } + + pub fn values_eq(&self, other: &TupleImpl) -> bool { + match (self.as_slice(), other.as_slice()) { + (Some(left), Some(right)) => left == right, + _ => self.len() == other.len() && self.iter().eq(other.iter()), + } + } + + pub fn output_mut(&mut self) -> &mut Tuple { + loop { + match self { + TupleImpl::Tuple(tuple) => return tuple, + TupleImpl::Concat(concat) => *self = std::mem::take(&mut concat.left), + } + } + } + + pub(crate) fn retain_rightmost(&mut self) -> &mut Tuple { + loop { + match self { + TupleImpl::Tuple(tuple) => return tuple, + TupleImpl::Concat(concat) => *self = std::mem::take(&mut concat.right), + } + } + } +} + +impl std::ops::Index for TupleImpl { + type Output = DataValue; + + #[inline] + fn index(&self, index: usize) -> &DataValue { + self.value_at(index) + } +} + +impl std::ops::IndexMut for TupleImpl { + #[inline] + fn index_mut(&mut self, index: usize) -> &mut DataValue { + self.value_at_mut(index) + } +} + +pub enum TupleImplIter<'a> { + Flat(std::slice::Iter<'a, DataValue>), + Joined { + tuple: &'a TupleImpl, + position: usize, + len: usize, + }, +} + +impl<'a> Iterator for TupleImplIter<'a> { + type Item = &'a DataValue; + + #[inline] + fn next(&mut self) -> Option { + match self { + TupleImplIter::Flat(iter) => iter.next(), + TupleImplIter::Joined { + tuple, + position, + len, + } => { + if *position >= *len { + return None; + } + let value = tuple.value_at(*position); + *position += 1; + Some(value) + } + } + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = match self { + TupleImplIter::Flat(iter) => iter.len(), + TupleImplIter::Joined { position, len, .. } => len - position, + }; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for TupleImplIter<'_> {} + +impl PartialEq for TupleImpl { + fn eq(&self, other: &Self) -> bool { + self.pk() == other.pk() && self.values_eq(other) + } +} + +impl Eq for TupleImpl {} + +impl PartialEq<[DataValue]> for TupleImpl { + fn eq(&self, other: &[DataValue]) -> bool { + self.iter().eq(other) + } +} + +impl PartialEq> for TupleImpl { + fn eq(&self, other: &Vec) -> bool { + *self == **other + } +} + +impl PartialEq<[DataValue; N]> for TupleImpl { + fn eq(&self, other: &[DataValue; N]) -> bool { + *self == other[..] + } +} + +impl PartialEq for TupleImpl { + fn eq(&self, other: &Tuple) -> bool { + self.pk() == other.pk.as_ref() && *self == other.values + } +} + +impl std::hash::Hash for TupleImpl { + fn hash(&self, state: &mut H) { + self.pk().hash(state); + state.write_usize(self.len()); + for value in self.iter() { + value.hash(state); + } + } +} + +impl TupleLike for TupleImpl { + #[inline] + fn len(&self) -> usize { + match self { + TupleImpl::Tuple(tuple) => tuple.values.len(), + TupleImpl::Concat(concat) => concat.left_len + concat.right.len(), + } + } + + #[inline] + fn value_at(&self, mut index: usize) -> &DataValue { + let mut node = self; + loop { + match node { + TupleImpl::Tuple(tuple) => return &tuple.values[index], + TupleImpl::Concat(concat) => { + node = if concat.route(&mut index) { + &concat.left + } else { + &concat.right + }; + } + } + } + } + + #[inline] + fn as_slice(&self) -> Option<&[DataValue]> { + match self { + TupleImpl::Tuple(tuple) => Some(tuple.values.as_slice()), + TupleImpl::Concat(_) => None, + } + } +} + impl<'a> From<&'a Tuple> for &'a [DataValue] { fn from(val: &'a Tuple) -> Self { val.values.as_slice() @@ -315,48 +659,16 @@ impl Tuple { Ok(()) } - /// e.g.: bits(u8)..|data_0(len for utf8_1)|utf8_0|data_1| - /// Tips: all len is u32 - pub fn serialize_to( - &self, - serializers: I, - bytes: &mut Vec, - ) -> Result<(), DatabaseError> - where - I: IntoIterator, - S: Borrow, - { - fn flip_bit(bits: u8, i: usize) -> u8 { - bits | (1 << (7 - i)) - } - - let values_len = self.values.len(); - let bits_len = (values_len + BITS_MAX_INDEX) / BITS_MAX_INDEX; - let values_bytes_len = self - .values - .iter() - .map(DataValue::serialized_len_hint) - .sum::(); - bytes.clear(); - bytes.reserve(bits_len + values_bytes_len); - bytes.resize(bits_len, 0u8); - - for (i, (value, serializer)) in self.values.iter().zip(serializers).enumerate() { - if value.is_null() { - bytes[i / BITS_MAX_INDEX] = flip_bit(bytes[i / BITS_MAX_INDEX], i % BITS_MAX_INDEX); - } else { - serializer.borrow().to_raw(value, bytes)?; - } - } - - Ok(()) - } - - pub fn primary_projection(pk_indices: &[usize], values: &[DataValue]) -> TupleId { + pub fn primary_projection(pk_indices: &[usize], values: &V) -> TupleId { if pk_indices.len() > 1 { - DataValue::Tuple(pk_indices.iter().map(|i| values[*i].clone()).collect_vec()) + DataValue::Tuple( + pk_indices + .iter() + .map(|i| values.value_at(*i).clone()) + .collect_vec(), + ) } else { - values[pk_indices[0]].clone() + values.value_at(pk_indices[0]).clone() } } } @@ -369,7 +681,7 @@ mod tests { use crate::iter_ext::Itertools; use crate::planner::{PlanArena, TableArenaCell}; use crate::types::serialize::TupleValueSerializable; - use crate::types::tuple::Tuple; + use crate::types::tuple::{Tuple, TupleImpl}; use crate::types::value::{DataValue, Utf8Type}; use crate::types::CharLengthUnits; use crate::types::LogicalType; @@ -592,7 +904,8 @@ mod tests { DataValue::Null, ], ), - ]; + ] + .map(TupleImpl::from); let serializers = columns .iter() .map(|column| column.datatype().serializable()) @@ -600,7 +913,7 @@ mod tests { let mut bytes = Vec::new(); { let mut tuple_0 = Tuple { - pk: tuples[0].pk.clone(), + pk: tuples[0].pk().cloned(), values: Vec::with_capacity(serializers.len()), }; tuples[0].serialize_to(&serializers, &mut bytes).unwrap(); @@ -608,11 +921,11 @@ mod tests { .deserialize_from_into(&serializers, &bytes, columns.len()) .unwrap(); - assert_eq!(tuples[0], tuple_0); + assert_eq!(tuples[0], TupleImpl::from(tuple_0)); } { let mut tuple_1 = Tuple { - pk: tuples[1].pk.clone(), + pk: tuples[1].pk().cloned(), values: Vec::with_capacity(serializers.len()), }; tuples[1].serialize_to(&serializers, &mut bytes).unwrap(); @@ -620,7 +933,7 @@ mod tests { .deserialize_from_into(&serializers, &bytes, columns.len()) .unwrap(); - assert_eq!(tuples[1], tuple_1); + assert_eq!(tuples[1], TupleImpl::from(tuple_1)); } // projection { @@ -631,7 +944,7 @@ mod tests { columns[3].datatype().serializable(), ]; let mut tuple_2 = Tuple { - pk: tuples[0].pk.clone(), + pk: tuples[0].pk().cloned(), values: Vec::with_capacity(2), }; tuples[0].serialize_to(&serializers, &mut bytes).unwrap(); @@ -649,7 +962,7 @@ mod tests { // A NULL in a skipped column must not shift later projected columns. let mut null_skipped = tuples[0].clone(); - null_skipped.values[1] = DataValue::Null; + null_skipped[1] = DataValue::Null; null_skipped.serialize_to(&serializers, &mut bytes).unwrap(); tuple_2 .deserialize_from_into(&projection_serializers, &bytes, columns.len()) @@ -668,13 +981,11 @@ mod tests { .map(|column| column.datatype().serializable()) .collect_vec(); let mut multi_pk_tuple = tuples[0].clone(); - multi_pk_tuple.pk = Some(DataValue::Tuple(vec![ - multi_pk_tuple.values[4].clone(), - multi_pk_tuple.values[2].clone(), - ])); + let pk = DataValue::Tuple(vec![multi_pk_tuple[4].clone(), multi_pk_tuple[2].clone()]); + multi_pk_tuple.set_pk(Some(pk)); let mut tuple_3 = Tuple { - pk: multi_pk_tuple.pk.clone(), + pk: multi_pk_tuple.pk().cloned(), values: Vec::with_capacity(serializers.len()), }; multi_pk_tuple @@ -687,7 +998,7 @@ mod tests { assert_eq!( tuple_3, Tuple { - pk: multi_pk_tuple.pk.clone(), + pk: multi_pk_tuple.pk().cloned(), values: vec![ DataValue::Int32(0), DataValue::UInt32(1), @@ -829,13 +1140,118 @@ mod tests { assert_eq!(tuple.values, vec![DataValue::Null, DataValue::Int32(7)]); assert_eq!( - Tuple::primary_projection(&[1], &tuple.values), + Tuple::primary_projection(&[1], tuple.values.as_slice()), DataValue::Int32(7) ); assert_eq!( - Tuple::primary_projection(&[0, 1], &tuple.values), + Tuple::primary_projection(&[0, 1], tuple.values.as_slice()), DataValue::Tuple(vec![DataValue::Null, DataValue::Int32(7)]) ); } + + fn ints(pk: Option, values: &[i32]) -> super::TupleImpl { + Tuple::new( + pk.map(DataValue::Int32), + values.iter().copied().map(DataValue::Int32).collect(), + ) + .into() + } + + fn values_of(tuple: &super::TupleImpl) -> Vec { + (0..tuple.len()) + .map(|i| tuple.value_at(i).clone()) + .collect() + } + + #[test] + fn tuple_impl_concat_reads_as_contiguous() { + use super::TupleImpl; + + let left = TupleImpl::concat(ints(Some(9), &[0, 1]), ints(None, &[2]), None); + let right = TupleImpl::concat(ints(None, &[3]), ints(None, &[4, 5]), None); + let mut tuple = TupleImpl::concat(left, right, Some(DataValue::Int32(7))); + + assert_eq!(tuple.len(), 6); + assert!(tuple.as_slice().is_none()); + assert_eq!( + values_of(&tuple), + (0..6).map(DataValue::Int32).collect_vec() + ); + assert_eq!(tuple.pk(), Some(&DataValue::Int32(7))); + + tuple.push(DataValue::Int32(6)); + assert_eq!(tuple.len(), 7); + assert_eq!(tuple.value_at(6), &DataValue::Int32(6)); + + assert_eq!(tuple, (0..7).map(DataValue::Int32).collect_vec()); + assert_eq!(tuple[6], DataValue::Int32(6)); + assert_eq!(tuple.get(7), None); + + tuple[1] = DataValue::Int32(10); + assert_eq!(tuple.get(1), Some(&DataValue::Int32(10))); + tuple.set_pk(None); + assert_eq!(tuple.pk(), None); + + let tuple = TupleImpl::concat(ints(None, &[]), ints(None, &[1, 2]), None); + assert_eq!(tuple, [1, 2].map(DataValue::Int32)); + } + + #[test] + fn tuple_impl_positional_access_ignores_shape() { + use super::TupleImpl; + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let hash = |tuple: &TupleImpl| { + let mut hasher = DefaultHasher::new(); + tuple.hash(&mut hasher); + hasher.finish() + }; + let mut joined = TupleImpl::concat( + TupleImpl::concat(ints(None, &[0]), ints(None, &[1, 2]), None), + ints(None, &[3]), + Some(DataValue::Int32(9)), + ); + let flat = ints(Some(9), &[0, 1, 2, 3]); + + assert_eq!(joined.iter().len(), 4); + assert_eq!(joined, (0..4).map(DataValue::Int32).collect_vec()); + assert!(joined.values_eq(&flat)); + assert_eq!(joined, flat); + assert_eq!(hash(&joined), hash(&flat)); + assert_ne!(joined, ints(None, &[0, 1, 2, 3])); + assert!(!joined.values_eq(&ints(None, &[0, 1, 2]))); + + *joined.value_at_mut(2) = DataValue::Int32(20); + assert_eq!(joined.value_at(2), &DataValue::Int32(20)); + joined.resize(6, DataValue::Null); + assert_eq!(joined.len(), 6); + assert_eq!(joined.value_at(5), &DataValue::Null); + + let mut drained = Vec::new(); + joined.drain_values(|value| drained.push(value)); + assert_eq!( + drained, + [0, 1, 20, 3] + .map(DataValue::Int32) + .into_iter() + .chain([DataValue::Null, DataValue::Null]) + .collect_vec() + ); + assert!(matches!(joined, TupleImpl::Tuple(_))); + assert_eq!(joined.len(), 0); + assert_eq!(joined.pk(), None); + } + + #[test] + fn tuple_impl_output_keeps_leftmost_buffer() { + use super::TupleImpl; + + let mut tuple = TupleImpl::concat(ints(None, &[0, 1, 2, 3]), ints(None, &[4]), None); + let output = tuple.output_mut(); + assert!(output.values.capacity() >= 4); + assert_eq!(output.values.len(), 4); + assert!(matches!(tuple, TupleImpl::Tuple(_))); + } } // GRCOV_EXCL_STOP diff --git a/src/types/tuple_builder.rs b/src/types/tuple_builder.rs index b5d9cd9e..08cd6bc1 100644 --- a/src/types/tuple_builder.rs +++ b/src/types/tuple_builder.rs @@ -75,7 +75,7 @@ impl<'a> TupleBuilder<'a> { tuple.pk = self .pk_indices - .map(|indices| Tuple::primary_projection(indices, &tuple.values)); + .map(|indices| Tuple::primary_projection(indices, tuple.values.as_slice())); Ok(()) } diff --git a/src/wasm.rs b/src/wasm.rs index c8986d12..d3da4870 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -16,7 +16,7 @@ use crate::db::{DataBaseBuilder, Database, DatabaseIter}; use crate::storage::memory::MemoryStorage; -use crate::types::tuple::Tuple; +use crate::types::tuple::TupleImpl; use crate::types::value::{DataValue, Utf8Type}; use crate::types::CharLengthUnits; use js_sys::{Array, Object, Reflect, Uint8Array}; @@ -108,7 +108,9 @@ fn char_length_units_to_js(unit: CharLengthUnits) -> JsValue { }) } -fn data_values_to_js(values: &[DataValue]) -> Result { +fn data_values_to_js<'v>( + values: impl IntoIterator, +) -> Result { let array = Array::new(); for value in values { array.push(&data_value_to_js(value)?); @@ -116,19 +118,18 @@ fn data_values_to_js(values: &[DataValue]) -> Result { Ok(array.into()) } -fn tuple_to_wasm_row(tuple: &Tuple) -> Result { +fn tuple_to_wasm_row(tuple: &TupleImpl) -> Result { let object = Object::new(); set_prop( &object, "pk", tuple - .pk - .as_ref() + .pk() .map(data_value_to_js) .transpose()? .unwrap_or(JsValue::NULL), )?; - set_prop(&object, "values", data_values_to_js(&tuple.values)?)?; + set_prop(&object, "values", data_values_to_js(tuple.iter())?)?; Ok(object.into()) } diff --git a/tests/macros-test/src/main.rs b/tests/macros-test/src/main.rs index ec399e45..b8d54161 100644 --- a/tests/macros-test/src/main.rs +++ b/tests/macros-test/src/main.rs @@ -29,7 +29,7 @@ mod test { use kite_sql::planner::{MetaArena, PlanArena, TableArena, TableArenaCell}; use kite_sql::storage::rocksdb::RocksStorage; use kite_sql::types::evaluator::binary_create; - use kite_sql::types::tuple::{Schema, SchemaView, Tuple}; + use kite_sql::types::tuple::{Schema, SchemaView, Tuple, TupleImpl}; use kite_sql::types::value::{DataValue, Utf8Type}; use kite_sql::types::{CharLengthUnits, LogicalType}; use kite_sql::{from_tuple, scala_function, table_function, Model, Projection}; @@ -68,16 +68,9 @@ mod test { (Tuple::new(None, values), schema) } - fn tuple_owned(tuple: &Tuple) -> Tuple { - Tuple { - pk: tuple.pk.clone(), - values: tuple.values.clone(), - } - } - - fn collect_result_tuples(mut iter: I) -> Result, DatabaseError> { + fn collect_result_tuples(mut iter: I) -> Result, DatabaseError> { let mut rows = Vec::new(); - while let Some(row) = iter.next_tuple(|_, tuple| tuple_owned(tuple))? { + while let Some(row) = iter.next_tuple(|_, tuple| tuple.clone())? { rows.push(row); } iter.done()?; @@ -357,10 +350,11 @@ mod test { fn test_from_tuple() { let table_arena = TableArenaCell::default(); let mut plan_arena = PlanArena::new(&table_arena); - let (mut tuple, schema) = build_tuple(&mut plan_arena); + let (tuple, schema) = build_tuple(&mut plan_arena); let schema = SchemaView::new(&schema, &plan_arena); let my_struct = - ::from_query_row(&schema, &mut tuple).unwrap(); + ::from_query_row(&schema, &mut tuple.into()) + .unwrap(); println!("{:?}", my_struct); @@ -386,7 +380,7 @@ mod test { true, ColumnDesc::new(LogicalType::Integer, None, true, None).unwrap(), ))); - let mut tuple = Tuple::new( + let tuple = Tuple::new( None, vec![ DataValue::Int32(9), @@ -400,9 +394,11 @@ mod test { ); let schema = SchemaView::new(&schema, &plan_arena); - let derived = - ::from_query_row(&schema, &mut tuple) - .unwrap(); + let derived = ::from_query_row( + &schema, + &mut tuple.into(), + ) + .unwrap(); assert_eq!(derived.c1, 9); assert_eq!(derived.name, "LOL"); @@ -483,13 +479,13 @@ mod test { let balance = rows .iter() - .find(|row| match &row.values[0] { + .find(|row| match &row[0] { DataValue::Utf8 { value, .. } => value == "balance", _ => false, }) .expect("balance column should exist"); - match &balance.values[1] { + match &balance[1] { DataValue::Utf8 { value, .. } => assert_eq!(value, "Decimal(Some(10), Some(2))"), other => panic!("unexpected describe datatype: {other:?}"), } @@ -509,13 +505,13 @@ mod test { let code = rows .iter() - .find(|row| match &row.values[0] { + .find(|row| match &row[0] { DataValue::Utf8 { value, .. } => value == "code", _ => false, }) .expect("code column should exist"); - match &code.values[1] { + match &code[1] { DataValue::Utf8 { value, .. } => assert_eq!(value, "Char(2, CHARACTERS)"), other => panic!("unexpected describe datatype: {other:?}"), } @@ -547,7 +543,7 @@ mod test { let describe_rows = collect_result_tuples(database.run("describe migrating_users")?)?; let column_names = describe_rows .iter() - .filter_map(|row| match row.values.first() { + .filter_map(|row| match row.get(0) { Some(DataValue::Utf8 { value, .. }) => Some(value.as_str()), _ => None, }) @@ -2530,7 +2526,7 @@ mod test { collect_result_tuples(database.run("explain select age from users where age = 1050")?)?; let explain_plan = explain_rows .iter() - .filter_map(|row| match row.values.first() { + .filter_map(|row| match row.get(0) { Some(DataValue::Utf8 { value, .. }) => Some(value.as_str()), _ => None, }) diff --git a/tests/sqllogictest/src/lib.rs b/tests/sqllogictest/src/lib.rs index dc125001..852b3130 100644 --- a/tests/sqllogictest/src/lib.rs +++ b/tests/sqllogictest/src/lib.rs @@ -49,7 +49,6 @@ pub fn run_sql( let mut rows = Vec::new(); while let Some(row) = iter.next_tuple(|_, tuple| { tuple - .values .iter() .enumerate() .map(|(i, value)| format(i, value)) diff --git a/tpcc/src/backend/dual.rs b/tpcc/src/backend/dual.rs index 959357c3..ed738bea 100644 --- a/tpcc/src/backend/dual.rs +++ b/tpcc/src/backend/dual.rs @@ -19,7 +19,7 @@ use super::{ SimpleExecutor, StatementSpec, }; use crate::TpccError; -use kite_sql::types::tuple::Tuple; +use kite_sql::types::tuple::TupleImpl; use std::borrow::Cow; pub struct DualBackend { @@ -135,7 +135,7 @@ impl<'a> BackendTransaction for DualTransaction<'a> { &mut self, statement: &mut Self::PreparedStatement, params: &[DbParam], - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { self.with_query_nth(statement, params, 0, visitor) } @@ -144,7 +144,7 @@ impl<'a> BackendTransaction for DualTransaction<'a> { &mut self, statement: &mut Self::PreparedStatement, params: &[DbParam], - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { let mut rows = Vec::new(); self.kitesql @@ -155,12 +155,12 @@ impl<'a> BackendTransaction for DualTransaction<'a> { let mut sqlite_rows = Vec::new(); self.sqlite .with_query_all(&mut statement.sqlite, params, &mut |tuple| { - sqlite_rows.push(tuple.values.clone()); + sqlite_rows.push(tuple.clone()); Ok(()) })?; // SQL without ORDER BY may return the same rows in different orders. for row in &rows { - let Some(index) = sqlite_rows.iter().position(|values| *values == row.values) else { + let Some(index) = sqlite_rows.iter().position(|values| row.values_eq(values)) else { return Err(TpccError::BackendMismatch(format!( "Result mismatch for SQL: {}", statement.spec.sql @@ -185,7 +185,7 @@ impl<'a> BackendTransaction for DualTransaction<'a> { statement: &mut Self::PreparedStatement, params: &[DbParam], n: usize, - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { let spec = statement.spec.clone(); @@ -233,7 +233,7 @@ fn drain_kitesql_iter( fn with_kitesql_nth( mut iter: KiteSqlTxnResult<'_, T>, n: usize, - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { for _ in 0..n { if !iter.skip_next_tuple()? { @@ -249,7 +249,7 @@ fn with_ordered_nth( mut sqlite_iter: SqliteResult<'_, '_>, sql: &'static str, n: usize, - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { let mut visited = false; let mut index = 0usize; @@ -265,9 +265,12 @@ fn with_ordered_nth( ))) } }; - if kitesql_tuple.values != sqlite_tuple.values { + if !kitesql_tuple.iter().eq(&sqlite_tuple.values) { println!("[Dual] mismatch SQL: {}", sql); - println!(" KiteSQL row: {:?}", kitesql_tuple.values); + println!( + " KiteSQL row: {:?}", + kitesql_tuple.iter().collect::>() + ); println!(" SQLite row: {:?}", sqlite_tuple.values); return Err(TpccError::BackendMismatch(format!( "Result mismatch for SQL: {}", @@ -275,7 +278,7 @@ fn with_ordered_nth( ))); } if index == n { - visitor(&sqlite_tuple)?; + visitor(&sqlite_tuple.into())?; visited = true; } index += 1; @@ -316,9 +319,12 @@ fn drain_and_compare_ordered( ))) } }; - if kitesql_tuple.values != sqlite_tuple.values { + if !kitesql_tuple.iter().eq(&sqlite_tuple.values) { println!("[Dual] mismatch SQL: {}", sql); - println!(" KiteSQL row: {:?}", kitesql_tuple.values); + println!( + " KiteSQL row: {:?}", + kitesql_tuple.iter().collect::>() + ); println!(" SQLite row: {:?}", sqlite_tuple.values); return Err(TpccError::BackendMismatch(format!( "Result mismatch for SQL: {}", diff --git a/tpcc/src/backend/kitesql_lmdb.rs b/tpcc/src/backend/kitesql_lmdb.rs index 712ff077..49fea50f 100644 --- a/tpcc/src/backend/kitesql_lmdb.rs +++ b/tpcc/src/backend/kitesql_lmdb.rs @@ -20,7 +20,7 @@ use super::{ use crate::TpccError; use kite_sql::db::{DBTransaction, DataBaseBuilder, Database}; use kite_sql::storage::lmdb::{LmdbStorage, LmdbTransaction as KiteSqlLmdbTransaction}; -use kite_sql::types::tuple::Tuple; +use kite_sql::types::tuple::TupleImpl; pub struct KiteSqlLmdbBackend { database: Database, @@ -120,7 +120,7 @@ impl<'a> BackendTransaction for KiteSqlLmdbTransactionWrapper<'a> { &mut self, statement: &mut Self::PreparedStatement, params: &[DbParam], - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { let mut iter = self.execute_raw(statement, params)?; iter.with_next_tuple(|tuple| visitor(tuple))? @@ -131,7 +131,7 @@ impl<'a> BackendTransaction for KiteSqlLmdbTransactionWrapper<'a> { &mut self, statement: &mut Self::PreparedStatement, params: &[DbParam], - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { let mut iter = self.execute_raw(statement, params)?; while iter.with_next_tuple(|tuple| visitor(tuple))?.is_some() {} @@ -143,7 +143,7 @@ impl<'a> BackendTransaction for KiteSqlLmdbTransactionWrapper<'a> { statement: &mut Self::PreparedStatement, params: &[DbParam], n: usize, - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { let mut iter = self.execute_raw(statement, params)?; for _ in 0..n { diff --git a/tpcc/src/backend/kitesql_rocksdb.rs b/tpcc/src/backend/kitesql_rocksdb.rs index f052d5d6..1ba92c9c 100644 --- a/tpcc/src/backend/kitesql_rocksdb.rs +++ b/tpcc/src/backend/kitesql_rocksdb.rs @@ -21,7 +21,7 @@ use kite_sql::binder::{command_type, CommandType}; use kite_sql::db::{DBTransaction, DataBaseBuilder, Database, Statement, TransactionIter}; use kite_sql::storage::rocksdb::{OptimisticRocksStorage, RocksStorage}; use kite_sql::storage::{Storage, Transaction}; -use kite_sql::types::tuple::Tuple; +use kite_sql::types::tuple::TupleImpl; pub type KiteSqlRocksDbBackend = KiteSqlRocksBackend; pub type KiteSqlOptimisticRocksDbBackend = KiteSqlRocksBackend; @@ -193,7 +193,7 @@ impl<'a, S: Storage> BackendTransaction for KiteSqlRocksTransaction<'a, S> { &mut self, statement: &mut Self::PreparedStatement, params: &[DbParam], - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { let mut iter = self.execute_raw(statement, params)?; iter.with_next_tuple(|tuple| visitor(tuple))? @@ -204,7 +204,7 @@ impl<'a, S: Storage> BackendTransaction for KiteSqlRocksTransaction<'a, S> { &mut self, statement: &mut Self::PreparedStatement, params: &[DbParam], - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { let mut iter = self.execute_raw(statement, params)?; while iter.with_next_tuple(|tuple| visitor(tuple))?.is_some() {} @@ -216,7 +216,7 @@ impl<'a, S: Storage> BackendTransaction for KiteSqlRocksTransaction<'a, S> { statement: &mut Self::PreparedStatement, params: &[DbParam], n: usize, - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { let mut iter = self.execute_raw(statement, params)?; for _ in 0..n { @@ -251,7 +251,7 @@ impl<'a, T: Transaction + 'a> KiteSqlTxnResult<'a, T> { pub(crate) fn with_next_tuple( &mut self, - f: impl FnOnce(&Tuple) -> Result, + f: impl FnOnce(&TupleImpl) -> Result, ) -> Result, TpccError> { match self .0 diff --git a/tpcc/src/backend/mod.rs b/tpcc/src/backend/mod.rs index 09688b0a..fbb13314 100644 --- a/tpcc/src/backend/mod.rs +++ b/tpcc/src/backend/mod.rs @@ -19,7 +19,7 @@ pub mod sqlite; use crate::TpccError; use kite_sql::db::PreparedPlan; -use kite_sql::types::tuple::Tuple; +use kite_sql::types::tuple::TupleImpl; use kite_sql::types::value::DataValue; use kite_sql::types::LogicalType; @@ -62,14 +62,14 @@ pub trait BackendTransaction { &mut self, statement: &mut Self::PreparedStatement, params: &[DbParam], - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError>; fn with_query_all( &mut self, statement: &mut Self::PreparedStatement, params: &[DbParam], - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError>; fn with_query_nth( @@ -77,7 +77,7 @@ pub trait BackendTransaction { statement: &mut Self::PreparedStatement, params: &[DbParam], n: usize, - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError>; fn commit(self) -> Result<(), TpccError>; diff --git a/tpcc/src/backend/sqlite.rs b/tpcc/src/backend/sqlite.rs index c2b56eaa..32e8afe6 100644 --- a/tpcc/src/backend/sqlite.rs +++ b/tpcc/src/backend/sqlite.rs @@ -17,7 +17,7 @@ use super::{ }; use crate::TpccError; use chrono::{NaiveDateTime, TimeZone, Utc}; -use kite_sql::types::tuple::Tuple; +use kite_sql::types::tuple::{Tuple, TupleImpl}; use kite_sql::types::value::{DataValue, Utf8Type}; use kite_sql::types::{CharLengthUnits, LogicalType}; use rust_decimal::Decimal; @@ -156,22 +156,22 @@ impl<'a> BackendTransaction for SqliteTransaction<'a> { &mut self, statement: &mut Self::PreparedStatement, params: &[DbParam], - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { let mut iter = self.execute_raw(statement, params)?; let tuple = iter.next().transpose()?.ok_or(TpccError::EmptyTuples)?; - visitor(&tuple) + visitor(&tuple.into()) } fn with_query_all( &mut self, statement: &mut Self::PreparedStatement, params: &[DbParam], - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { let iter = self.execute_raw(statement, params)?; for row in iter { - visitor(&row?)?; + visitor(&row?.into())?; } Ok(()) } @@ -181,7 +181,7 @@ impl<'a> BackendTransaction for SqliteTransaction<'a> { statement: &mut Self::PreparedStatement, params: &[DbParam], n: usize, - visitor: &mut dyn FnMut(&Tuple) -> Result<(), TpccError>, + visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, ) -> Result<(), TpccError> { let mut iter = self.execute_raw(statement, params)?; for _ in 0..n { @@ -190,7 +190,7 @@ impl<'a> BackendTransaction for SqliteTransaction<'a> { } } let tuple = iter.next().transpose()?.ok_or(TpccError::EmptyTuples)?; - visitor(&tuple) + visitor(&tuple.into()) } fn commit(mut self) -> Result<(), TpccError> { diff --git a/tpcc/src/delivery.rs b/tpcc/src/delivery.rs index 23fd022c..86e3a4a3 100644 --- a/tpcc/src/delivery.rs +++ b/tpcc/src/delivery.rs @@ -55,7 +55,7 @@ impl TpccTransaction for Delivery { (2, DataValue::Int16(args.w_id as i16)), ], &mut |tuple| { - no_o_id = tuple.values[0].i32().unwrap(); + no_o_id = tuple[0].i32().unwrap(); Ok(()) }, )?; @@ -82,7 +82,7 @@ impl TpccTransaction for Delivery { (3, DataValue::Int16(args.w_id as i16)), ], &mut |tuple| { - c_id = tuple.values[0].i32().unwrap(); + c_id = tuple[0].i32().unwrap(); Ok(()) }, )?; @@ -116,7 +116,7 @@ impl TpccTransaction for Delivery { (3, DataValue::Int16(args.w_id as i16)), ], &mut |tuple| { - ol_total = tuple.values[0].decimal().unwrap(); + ol_total = tuple[0].decimal().unwrap(); Ok(()) }, )?; diff --git a/tpcc/src/main.rs b/tpcc/src/main.rs index 9398f9be..72c31821 100644 --- a/tpcc/src/main.rs +++ b/tpcc/src/main.rs @@ -864,7 +864,7 @@ impl_from_tpcc_error!(std::io::Error, Io); fn explain_tpcc() -> Result<(), DatabaseError> { use kite_sql::db::CatalogKind; use kite_sql::db::{DataBaseBuilder, ResultIter}; - use kite_sql::types::tuple::Tuple; + use kite_sql::types::tuple::TupleImpl; fn create_table(mut iter: I) -> Result { let mut output = iter.schema(|schema| { @@ -880,7 +880,6 @@ fn explain_tpcc() -> Result<(), DatabaseError> { while let Some(()) = iter.next_tuple(|_, tuple| { output.push_str( &tuple - .values .iter() .map(|value| value.to_string()) .collect::>() @@ -894,7 +893,7 @@ fn explain_tpcc() -> Result<(), DatabaseError> { fn with_next_tuple( mut iter: I, - f: impl FnOnce(&Tuple) -> R, + f: impl FnOnce(&TupleImpl) -> R, ) -> Result { let value = iter .next_tuple(|_, tuple| f(tuple))? @@ -923,47 +922,35 @@ fn explain_tpcc() -> Result<(), DatabaseError> { tx.run("SELECT c_w_id, c_d_id, c_id, c_last, c_balance, c_data FROM customer limit 1")?, |tuple| { ( - tuple.values[0].clone(), - tuple.values[1].clone(), - tuple.values[2].clone(), - tuple.values[3].clone(), - tuple.values[4].clone(), - tuple.values[5].clone(), + tuple[0].clone(), + tuple[1].clone(), + tuple[2].clone(), + tuple[3].clone(), + tuple[4].clone(), + tuple[5].clone(), ) }, )?; let (d_id, d_w_id, d_next_o_id) = with_next_tuple( tx.run("SELECT d_id, d_w_id, d_next_o_id FROM district limit 1")?, - |tuple| { - ( - tuple.values[0].clone(), - tuple.values[1].clone(), - tuple.values[2].clone(), - ) - }, + |tuple| (tuple[0].clone(), tuple[1].clone(), tuple[2].clone()), )?; let i_id = with_next_tuple(tx.run("SELECT i_id FROM item limit 1")?, |tuple| { - tuple.values[0].clone() + tuple[0].clone() })?; let (s_i_id, s_w_id, s_quantity) = with_next_tuple( tx.run("SELECT s_i_id, s_w_id, s_quantity FROM stock limit 1")?, - |tuple| { - ( - tuple.values[0].clone(), - tuple.values[1].clone(), - tuple.values[2].clone(), - ) - }, + |tuple| (tuple[0].clone(), tuple[1].clone(), tuple[2].clone()), )?; let (o_w_id, o_d_id, o_c_id, o_id, o_carrier_id) = with_next_tuple( tx.run("SELECT o_w_id, o_d_id, o_c_id, o_id, o_carrier_id FROM orders limit 1")?, |tuple| { ( - tuple.values[0].clone(), - tuple.values[1].clone(), - tuple.values[2].clone(), - tuple.values[3].clone(), - tuple.values[4].clone(), + tuple[0].clone(), + tuple[1].clone(), + tuple[2].clone(), + tuple[3].clone(), + tuple[4].clone(), ) }, )?; @@ -971,22 +958,16 @@ fn explain_tpcc() -> Result<(), DatabaseError> { tx.run("SELECT ol_w_id, ol_d_id, ol_o_id, ol_delivery_d FROM order_line limit 1")?, |tuple| { ( - tuple.values[0].clone(), - tuple.values[1].clone(), - tuple.values[2].clone(), - tuple.values[3].clone(), + tuple[0].clone(), + tuple[1].clone(), + tuple[2].clone(), + tuple[3].clone(), ) }, )?; let (no_d_id, no_w_id, no_o_id) = with_next_tuple( tx.run("SELECT no_d_id, no_w_id, no_o_id FROM new_orders limit 1")?, - |tuple| { - ( - tuple.values[0].clone(), - tuple.values[1].clone(), - tuple.values[2].clone(), - ) - }, + |tuple| (tuple[0].clone(), tuple[1].clone(), tuple[2].clone()), )?; // ORDER { diff --git a/tpcc/src/new_ord.rs b/tpcc/src/new_ord.rs index ce2d44fd..7836c219 100644 --- a/tpcc/src/new_ord.rs +++ b/tpcc/src/new_ord.rs @@ -94,10 +94,10 @@ impl TpccTransaction for NewOrd { (4, DataValue::Int64(args.c_id as i64)), ], &mut |tuple| { - c_discount = tuple.values[0].decimal().unwrap(); - c_last = tuple.values[1].utf8().unwrap().to_string(); - c_credit = tuple.values[2].utf8().unwrap().to_string(); - w_tax = tuple.values[3].decimal().unwrap(); + c_discount = tuple[0].decimal().unwrap(); + c_last = tuple[1].utf8().unwrap().to_string(); + c_credit = tuple[2].utf8().unwrap().to_string(); + w_tax = tuple[3].decimal().unwrap(); Ok(()) }, )?; @@ -116,9 +116,9 @@ impl TpccTransaction for NewOrd { (3, DataValue::Int32(args.c_id as i32)), ], &mut |tuple| { - c_discount = tuple.values[0].decimal().unwrap(); - c_last = tuple.values[1].utf8().unwrap().to_string(); - c_credit = tuple.values[2].utf8().unwrap().to_string(); + c_discount = tuple[0].decimal().unwrap(); + c_last = tuple[1].utf8().unwrap().to_string(); + c_credit = tuple[2].utf8().unwrap().to_string(); Ok(()) }, )?; @@ -128,7 +128,7 @@ impl TpccTransaction for NewOrd { &mut statements[2], &[(1, DataValue::Int16(args.w_id as i16))], &mut |tuple| { - w_tax = tuple.values[0].decimal().unwrap(); + w_tax = tuple[0].decimal().unwrap(); Ok(()) }, )?; @@ -145,8 +145,8 @@ impl TpccTransaction for NewOrd { (2, DataValue::Int16(args.w_id as i16)), ], &mut |tuple| { - d_next_o_id = tuple.values[0].i32().unwrap(); - d_tax = tuple.values[1].decimal().unwrap(); + d_next_o_id = tuple[0].i32().unwrap(); + d_tax = tuple[1].decimal().unwrap(); Ok(()) }, )?; @@ -215,9 +215,9 @@ impl TpccTransaction for NewOrd { let mut i_name = String::new(); let mut i_data = String::new(); tx.with_query_one(&mut statements[7], ¶ms, &mut |tuple| { - i_price = tuple.values[0].decimal().unwrap(); - i_name = tuple.values[1].utf8().unwrap().to_string(); - i_data = tuple.values[2].utf8().unwrap().to_string(); + i_price = tuple[0].decimal().unwrap(); + i_name = tuple[1].utf8().unwrap().to_string(); + i_data = tuple[2].utf8().unwrap().to_string(); Ok(()) })?; @@ -242,18 +242,18 @@ impl TpccTransaction for NewOrd { let mut s_dist_09 = String::new(); let mut s_dist_10 = String::new(); tx.with_query_one(&mut statements[8], ¶ms, &mut |tuple| { - s_quantity = tuple.values[0].i16().unwrap(); - s_data = tuple.values[1].utf8().unwrap().to_string(); - s_dist_01 = tuple.values[2].utf8().unwrap().to_string(); - s_dist_02 = tuple.values[3].utf8().unwrap().to_string(); - s_dist_03 = tuple.values[4].utf8().unwrap().to_string(); - s_dist_04 = tuple.values[5].utf8().unwrap().to_string(); - s_dist_05 = tuple.values[6].utf8().unwrap().to_string(); - s_dist_06 = tuple.values[7].utf8().unwrap().to_string(); - s_dist_07 = tuple.values[8].utf8().unwrap().to_string(); - s_dist_08 = tuple.values[9].utf8().unwrap().to_string(); - s_dist_09 = tuple.values[10].utf8().unwrap().to_string(); - s_dist_10 = tuple.values[11].utf8().unwrap().to_string(); + s_quantity = tuple[0].i16().unwrap(); + s_data = tuple[1].utf8().unwrap().to_string(); + s_dist_01 = tuple[2].utf8().unwrap().to_string(); + s_dist_02 = tuple[3].utf8().unwrap().to_string(); + s_dist_03 = tuple[4].utf8().unwrap().to_string(); + s_dist_04 = tuple[5].utf8().unwrap().to_string(); + s_dist_05 = tuple[6].utf8().unwrap().to_string(); + s_dist_06 = tuple[7].utf8().unwrap().to_string(); + s_dist_07 = tuple[8].utf8().unwrap().to_string(); + s_dist_08 = tuple[9].utf8().unwrap().to_string(); + s_dist_09 = tuple[10].utf8().unwrap().to_string(); + s_dist_10 = tuple[11].utf8().unwrap().to_string(); Ok(()) })?; diff --git a/tpcc/src/order_stat.rs b/tpcc/src/order_stat.rs index 5243c599..6bbd0dc4 100644 --- a/tpcc/src/order_stat.rs +++ b/tpcc/src/order_stat.rs @@ -68,7 +68,7 @@ impl TpccTransaction for OrderStat { (3, DataValue::from(args.c_last.clone())), ], &mut |tuple| { - name_cnt = tuple.values[0].i32().unwrap() as usize; + name_cnt = tuple[0].i32().unwrap() as usize; Ok(()) }, )?; @@ -87,10 +87,10 @@ impl TpccTransaction for OrderStat { let mut c_middle = String::new(); let mut c_last = String::new(); tx.with_query_nth(&mut statements[1], ¶ms, target, &mut |tuple| { - c_balance = tuple.values[0].decimal().unwrap(); - c_first = tuple.values[1].utf8().unwrap().to_string(); - c_middle = tuple.values[2].utf8().unwrap().to_string(); - c_last = tuple.values[3].utf8().unwrap().to_string(); + c_balance = tuple[0].decimal().unwrap(); + c_first = tuple[1].utf8().unwrap().to_string(); + c_middle = tuple[2].utf8().unwrap().to_string(); + c_last = tuple[3].utf8().unwrap().to_string(); Ok(()) })?; (c_balance, c_first, c_middle, c_last) @@ -108,10 +108,10 @@ impl TpccTransaction for OrderStat { (3, DataValue::Int32(args.c_id as i32)), ], &mut |tuple| { - c_balance = tuple.values[0].decimal().unwrap(); - c_first = tuple.values[1].utf8().unwrap().to_string(); - c_middle = tuple.values[2].utf8().unwrap().to_string(); - c_last = tuple.values[3].utf8().unwrap().to_string(); + c_balance = tuple[0].decimal().unwrap(); + c_first = tuple[1].utf8().unwrap().to_string(); + c_middle = tuple[2].utf8().unwrap().to_string(); + c_last = tuple[3].utf8().unwrap().to_string(); Ok(()) }, )?; @@ -128,7 +128,7 @@ impl TpccTransaction for OrderStat { ]; let mut o_id = 0; tx.with_query_one(&mut statements[3], ¶ms, &mut |tuple| { - o_id = tuple.values[0].i32().unwrap(); + o_id = tuple[0].i32().unwrap(); Ok(()) })?; // "SELECT ol_i_id, ol_supply_w_id, ol_quantity, ol_amount, ol_delivery_d FROM order_line WHERE ol_w_id = ? AND ol_d_id = ? AND ol_o_id = ?" @@ -138,11 +138,11 @@ impl TpccTransaction for OrderStat { (3, DataValue::Int32(o_id)), ]; tx.with_query_one(&mut statements[4], ¶ms, &mut |_| Ok(()))?; - // let ol_i_id = tuple.values[0].i32(); - // let ol_supply_w_id = tuple.values[1].i16(); - // let ol_quantity = tuple.values[2].i8(); - // let ol_amount = tuple.values[3].decimal(); - // let ol_delivery_d = tuple.values[4].datetime(); + // let ol_i_id = tuple[0].i32(); + // let ol_supply_w_id = tuple[1].i16(); + // let ol_quantity = tuple[2].i8(); + // let ol_amount = tuple[3].decimal(); + // let ol_delivery_d = tuple[4].datetime(); Ok(()) } diff --git a/tpcc/src/payment.rs b/tpcc/src/payment.rs index 80ebe1fd..27eed290 100644 --- a/tpcc/src/payment.rs +++ b/tpcc/src/payment.rs @@ -90,12 +90,12 @@ impl TpccTransaction for Payment { &mut statements[1], &[(1, DataValue::Int16(args.w_id as i16))], &mut |tuple| { - w_street_1 = tuple.values[0].utf8().unwrap().to_string(); - w_street_2 = tuple.values[1].utf8().unwrap().to_string(); - w_city = tuple.values[2].utf8().unwrap().to_string(); - w_state = tuple.values[3].utf8().unwrap().to_string(); - w_zip = tuple.values[4].utf8().unwrap().to_string(); - w_name = tuple.values[5].utf8().unwrap().to_string(); + w_street_1 = tuple[0].utf8().unwrap().to_string(); + w_street_2 = tuple[1].utf8().unwrap().to_string(); + w_city = tuple[2].utf8().unwrap().to_string(); + w_state = tuple[3].utf8().unwrap().to_string(); + w_zip = tuple[4].utf8().unwrap().to_string(); + w_name = tuple[5].utf8().unwrap().to_string(); Ok(()) }, )?; @@ -124,12 +124,12 @@ impl TpccTransaction for Payment { (2, DataValue::Int8(args.d_id as i8)), ], &mut |tuple| { - d_street_1 = tuple.values[0].utf8().unwrap().to_string(); - d_street_2 = tuple.values[1].utf8().unwrap().to_string(); - d_city = tuple.values[2].utf8().unwrap().to_string(); - d_state = tuple.values[3].utf8().unwrap().to_string(); - d_zip = tuple.values[4].utf8().unwrap().to_string(); - d_name = tuple.values[5].utf8().unwrap().to_string(); + d_street_1 = tuple[0].utf8().unwrap().to_string(); + d_street_2 = tuple[1].utf8().unwrap().to_string(); + d_city = tuple[2].utf8().unwrap().to_string(); + d_state = tuple[3].utf8().unwrap().to_string(); + d_zip = tuple[4].utf8().unwrap().to_string(); + d_name = tuple[5].utf8().unwrap().to_string(); Ok(()) }, )?; @@ -146,7 +146,7 @@ impl TpccTransaction for Payment { (3, DataValue::from(args.c_last.clone())), ], &mut |tuple| { - name_cnt = tuple.values[0].i32().unwrap(); + name_cnt = tuple[0].i32().unwrap(); Ok(()) }, )?; @@ -161,7 +161,7 @@ impl TpccTransaction for Payment { } let target = name_cnt as usize / 2 - 1; tx.with_query_nth(&mut statements[5], ¶ms, target, &mut |tuple| { - c_id = tuple.values[0].i32().unwrap(); + c_id = tuple[0].i32().unwrap(); Ok(()) })?; } @@ -188,20 +188,20 @@ impl TpccTransaction for Payment { (3, DataValue::Int32(c_id)), ], &mut |tuple| { - c_first = tuple.values[0].utf8().unwrap().to_string(); - c_middle = tuple.values[1].utf8().unwrap().to_string(); - c_last = tuple.values[2].utf8().unwrap().to_string(); - c_street_1 = tuple.values[3].utf8().unwrap().to_string(); - c_street_2 = tuple.values[4].utf8().unwrap().to_string(); - c_city = tuple.values[5].utf8().unwrap().to_string(); - c_state = tuple.values[6].utf8().unwrap().to_string(); - c_zip = tuple.values[7].utf8().unwrap().to_string(); - c_phone = tuple.values[8].utf8().unwrap().to_string(); - c_credit = tuple.values[9].utf8().map(ToString::to_string); - c_credit_lim = tuple.values[10].i64().unwrap(); - c_discount = tuple.values[11].decimal().unwrap(); - c_balance = tuple.values[12].decimal().unwrap(); - c_since = tuple.values[13].datetime().unwrap(); + c_first = tuple[0].utf8().unwrap().to_string(); + c_middle = tuple[1].utf8().unwrap().to_string(); + c_last = tuple[2].utf8().unwrap().to_string(); + c_street_1 = tuple[3].utf8().unwrap().to_string(); + c_street_2 = tuple[4].utf8().unwrap().to_string(); + c_city = tuple[5].utf8().unwrap().to_string(); + c_state = tuple[6].utf8().unwrap().to_string(); + c_zip = tuple[7].utf8().unwrap().to_string(); + c_phone = tuple[8].utf8().unwrap().to_string(); + c_credit = tuple[9].utf8().map(ToString::to_string); + c_credit_lim = tuple[10].i64().unwrap(); + c_discount = tuple[11].decimal().unwrap(); + c_balance = tuple[12].decimal().unwrap(); + c_since = tuple[13].datetime().unwrap(); Ok(()) }, )?; @@ -219,7 +219,7 @@ impl TpccTransaction for Payment { (3, DataValue::Int32(c_id)), ], &mut |tuple| { - c_data = tuple.values[0].utf8().unwrap().to_string(); + c_data = tuple[0].utf8().unwrap().to_string(); Ok(()) }, )?; diff --git a/tpcc/src/slev.rs b/tpcc/src/slev.rs index 5fc5a773..6c54f7de 100644 --- a/tpcc/src/slev.rs +++ b/tpcc/src/slev.rs @@ -52,7 +52,7 @@ impl TpccTransaction for Slev { (2, DataValue::Int16(args.w_id as i16)), ], &mut |tuple| { - d_next_o_id = tuple.values[0].i32().unwrap(); + d_next_o_id = tuple[0].i32().unwrap(); Ok(()) }, )?; @@ -67,7 +67,7 @@ impl TpccTransaction for Slev { (4, DataValue::Int32(d_next_o_id)), ], &mut |tuple| { - item_ids.push(tuple.values[0].i32().unwrap()); + item_ids.push(tuple[0].i32().unwrap()); Ok(()) }, )?; @@ -82,7 +82,7 @@ impl TpccTransaction for Slev { (3, DataValue::Int16(args.level as i16)), ], &mut |tuple| { - _low_stock += tuple.values[0].i32().unwrap(); + _low_stock += tuple[0].i32().unwrap(); Ok(()) }, )?; From d4361e1bbdeff54c94a97986161bb3f0630435fd Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:58 +0800 Subject: [PATCH 39/62] perf(execution): reuse Concat boxes and copy multi-use join rows flat - Add ConcatPool on ExecResult: Concat boxes are recycled as MaybeUninit when the arena splits a joined row (rewrite, output_mut) or MarkApply drops an unmatched left row, and reused by arena.concat. - Hash join and nested loop join copy the side that is matched many times into one flat buffer instead of cloning it into a new Concat. - Add TupleImpl::prepend/Extend for NULL padding and outer-row emission. - Add the `make miri` target used by the CI miri job. --- Makefile | 8 +- src/execution/dql/index_scan.rs | 2 +- src/execution/dql/join/hash/full_join.rs | 26 ++-- src/execution/dql/join/hash/mod.rs | 23 ++-- src/execution/dql/join/hash/right_join.rs | 29 +++-- src/execution/dql/join/nested_loop_join.rs | 48 ++++--- src/execution/dql/mark_apply.rs | 4 +- src/execution/dql/seq_scan.rs | 2 +- src/execution/mod.rs | 32 ++++- src/types/tuple.rs | 138 +++++++++++++++++++-- 10 files changed, 231 insertions(+), 81 deletions(-) diff --git a/Makefile b/Makefile index 6b763ded..193cdce4 100644 --- a/Makefile +++ b/Makefile @@ -33,9 +33,12 @@ FUZZ_PASS_EXIT_sql_exec = 70 # sql_gen: bounded generated queries, so crashes, timeouts and OOMs all fail. FUZZ_ARGS_sql_gen = -max_len=4096 FUZZ_PASS_EXIT_sql_gen = 0 +MIRI_TOOLCHAIN ?= nightly-2026-10-04 +MIRI_FEATURES ?= spill,decimal,lmdb +MIRI_TESTS ?= types::tuple:: execution::test:: COVERAGE_REPORT_ARGS ?= --llvm --ignore-not-existing --keep-only 'src/**' --ignore 'src/**/tests/**' --ignore 'tests/**' --ignore 'tpcc/**' --excl-start 'GRCOV_EXCL_START' --excl-stop 'GRCOV_EXCL_STOP' -.PHONY: test test-python test-wasm test-slt test-all codecov codecov-html wasm-build check tpcc tpcc-kitesql-rocksdb tpcc-kitesql-lmdb tpcc-lmdb-flamegraph tpcc-lmdb-heaptrack tpcc-sqlite tpcc-dual cargo-check build wasm-examples native-examples fmt clippy fuzz +.PHONY: test test-python test-wasm test-slt test-all codecov codecov-html wasm-build check tpcc tpcc-kitesql-rocksdb tpcc-kitesql-lmdb tpcc-lmdb-flamegraph tpcc-lmdb-heaptrack tpcc-sqlite tpcc-dual cargo-check build wasm-examples native-examples fmt clippy fuzz miri ## Run default Rust tests in the current environment (non-WASM). test: @@ -103,6 +106,9 @@ fuzz: echo "fuzz: $(FUZZ_TARGET) failed (exit $$status), see fuzz/artifacts/$(FUZZ_TARGET)/"; exit $$status; \ fi +miri: + $(CARGO) +$(MIRI_TOOLCHAIN) miri test --lib --features '$(MIRI_FEATURES)' -- $(MIRI_TESTS) + ## Run formatting (check mode) across the workspace. fmt: $(CARGO) fmt --all -- --check diff --git a/src/execution/dql/index_scan.rs b/src/execution/dql/index_scan.rs index 6532d347..d2499cab 100644 --- a/src/execution/dql/index_scan.rs +++ b/src/execution/dql/index_scan.rs @@ -144,7 +144,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for IndexScan<'a, T> { }; let state = arena.local_state(plan_arena); - if iter.next_tuple_into(state.table_codec, state.result.tuple.output_mut())? { + if iter.next_tuple_into(state.table_codec, state.result.output_mut())? { arena.resume(); } else { arena.finish(); diff --git a/src/execution/dql/join/hash/full_join.rs b/src/execution/dql/join/hash/full_join.rs index 95c1aa17..bf170efd 100644 --- a/src/execution/dql/join/hash/full_join.rs +++ b/src/execution/dql/join/hash/full_join.rs @@ -14,7 +14,7 @@ use crate::errors::DatabaseError; use crate::execution::dql::join::hash::{ - filter, join_rows, null_left_row, JoinProbeState, LeftDropState, LeftDropTuples, ProbeState, + filter, join_rows, JoinProbeState, LeftDropState, LeftDropTuples, ProbeState, }; use crate::execution::dql::join::hash_join::BuildState; use crate::planner::ExprRef; @@ -44,10 +44,10 @@ impl JoinProbeState for FullJoinState { } probe_state.emitted_unmatched = true; probe_state.finished = true; - return Ok(Some(null_left_row( - self.left_schema_len, - &mut probe_state.probe_tuple, - ))); + probe_state + .probe_tuple + .prepend(std::iter::repeat_n(DataValue::Null, self.left_schema_len)); + return Ok(Some(std::mem::take(&mut probe_state.probe_tuple))); } let Some(build_state) = build_state else { @@ -57,10 +57,10 @@ impl JoinProbeState for FullJoinState { } probe_state.emitted_unmatched = true; probe_state.finished = true; - return Ok(Some(null_left_row( - self.left_schema_len, - &mut probe_state.probe_tuple, - ))); + probe_state + .probe_tuple + .prepend(std::iter::repeat_n(DataValue::Null, self.left_schema_len)); + return Ok(Some(std::mem::take(&mut probe_state.probe_tuple))); }; while probe_state.index < build_state.tuples.len() { @@ -85,10 +85,10 @@ impl JoinProbeState for FullJoinState { probe_state.finished = true; if !probe_state.produced && !probe_state.emitted_unmatched { probe_state.emitted_unmatched = true; - return Ok(Some(null_left_row( - self.left_schema_len, - &mut probe_state.probe_tuple, - ))); + probe_state + .probe_tuple + .prepend(std::iter::repeat_n(DataValue::Null, self.left_schema_len)); + return Ok(Some(std::mem::take(&mut probe_state.probe_tuple))); } Ok(None) } diff --git a/src/execution/dql/join/hash/mod.rs b/src/execution/dql/join/hash/mod.rs index 0dffd829..ae872a5f 100644 --- a/src/execution/dql/join/hash/mod.rs +++ b/src/execution/dql/join/hash/mod.rs @@ -141,20 +141,15 @@ pub(crate) fn join_rows( probe: &mut TupleImpl, consume_probe: bool, ) -> TupleImpl { - let mut probe = if consume_probe { - std::mem::take(probe) + let mut values = Vec::with_capacity(build.len() + probe.len()); + values.extend(build.iter().cloned()); + let pk = if consume_probe { + let pk = build.pk().cloned().or_else(|| probe.take_pk()); + probe.drain_values(|value| values.push(value)); + pk } else { - probe.clone() + values.extend(probe.iter().cloned()); + build.pk().or(probe.pk()).cloned() }; - let pk = build.pk().cloned().or_else(|| probe.take_pk()); - TupleImpl::concat(build.clone(), probe, pk) -} - -pub(crate) fn null_left_row(left_len: usize, probe: &mut TupleImpl) -> TupleImpl { - let pk = probe.take_pk(); - TupleImpl::concat( - Tuple::new(None, vec![DataValue::Null; left_len]).into(), - std::mem::take(probe), - pk, - ) + Tuple::new(pk, values).into() } diff --git a/src/execution/dql/join/hash/right_join.rs b/src/execution/dql/join/hash/right_join.rs index 1631b06d..8f52a02e 100644 --- a/src/execution/dql/join/hash/right_join.rs +++ b/src/execution/dql/join/hash/right_join.rs @@ -13,13 +13,12 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::execution::dql::join::hash::{ - filter, join_rows, null_left_row, JoinProbeState, ProbeState, -}; +use crate::execution::dql::join::hash::{filter, join_rows, JoinProbeState, ProbeState}; use crate::execution::dql::join::hash_join::BuildState; use crate::planner::ExprRef; use crate::planner::MetaArena; use crate::types::tuple::{SplitTupleRef, TupleImpl}; +use crate::types::value::DataValue; pub(crate) struct RightJoinState { pub(crate) left_schema_len: usize, @@ -40,10 +39,10 @@ impl JoinProbeState for RightJoinState { } probe_state.emitted_unmatched = true; probe_state.finished = true; - return Ok(Some(null_left_row( - self.left_schema_len, - &mut probe_state.probe_tuple, - ))); + probe_state + .probe_tuple + .prepend(std::iter::repeat_n(DataValue::Null, self.left_schema_len)); + return Ok(Some(std::mem::take(&mut probe_state.probe_tuple))); } let Some(build_state) = build_state else { @@ -53,10 +52,10 @@ impl JoinProbeState for RightJoinState { } probe_state.emitted_unmatched = true; probe_state.finished = true; - return Ok(Some(null_left_row( - self.left_schema_len, - &mut probe_state.probe_tuple, - ))); + probe_state + .probe_tuple + .prepend(std::iter::repeat_n(DataValue::Null, self.left_schema_len)); + return Ok(Some(std::mem::take(&mut probe_state.probe_tuple))); }; while probe_state.index < build_state.tuples.len() { @@ -80,10 +79,10 @@ impl JoinProbeState for RightJoinState { if !probe_state.produced && !probe_state.emitted_unmatched { probe_state.emitted_unmatched = true; probe_state.finished = true; - return Ok(Some(null_left_row( - self.left_schema_len, - &mut probe_state.probe_tuple, - ))); + probe_state + .probe_tuple + .prepend(std::iter::repeat_n(DataValue::Null, self.left_schema_len)); + return Ok(Some(std::mem::take(&mut probe_state.probe_tuple))); } probe_state.finished = true; diff --git a/src/execution/dql/join/nested_loop_join.rs b/src/execution/dql/join/nested_loop_join.rs index f6fb6c16..00ef1eff 100644 --- a/src/execution/dql/join/nested_loop_join.rs +++ b/src/execution/dql/join/nested_loop_join.rs @@ -26,7 +26,7 @@ use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::ExprRef; use crate::storage::Transaction; use crate::types::bitset::BitSet; -use crate::types::tuple::{SplitTupleRef, Tuple, TupleImpl, TupleLike}; +use crate::types::tuple::{SplitTupleRef, TupleImpl, TupleLike}; use crate::types::value::DataValue; /// Equivalent condition @@ -230,12 +230,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin { active_left.has_matched = true; // A matched pair is always one output row, even when // neither side projects a column (e.g. `count(*)`). - let joined = Self::emit_tuple( - active_left.left_tuple.clone(), - arena.take_tuple(), - self.ty, - ); - *arena.result_mut() = joined; + Self::emit_tuple(&active_left.left_tuple, arena.result_mut(), self.ty); if matches!(self.ty, JoinType::Full) { if let Some(bits) = right_bitmap.as_mut() { bits.insert(idx); @@ -270,12 +265,14 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin { JoinType::LeftOuter | JoinType::RightOuter | JoinType::Full if !active_left.has_matched => { - let nulls = Tuple::new(None, vec![DataValue::Null; right_schema_len]); - *arena.result_mut() = Self::emit_tuple( - std::mem::take(&mut active_left.left_tuple), - nulls.into(), - self.ty, - ); + let mut row = std::mem::take(&mut active_left.left_tuple); + let nulls = std::iter::repeat_n(DataValue::Null, right_schema_len); + if matches!(self.ty, JoinType::RightOuter) { + row.prepend(nulls); + } else { + row.extend(nulls); + } + *arena.result_mut() = row; true } _ => false, @@ -303,11 +300,10 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin { }; if is_unmatched { - let mut right = arena.take_tuple(); - let pk = right.take_pk(); - let nulls = - Tuple::new(None, vec![DataValue::Null; self.eq_cond.left_len]); - *arena.result_mut() = TupleImpl::concat(nulls.into(), right, pk); + arena.result_mut().prepend(std::iter::repeat_n( + DataValue::Null, + self.eq_cond.left_len, + )); self.state = NestedLoopJoinState::EmitRightUnmatched { right_input, right_bitmap, @@ -347,14 +343,16 @@ impl NestedLoopJoin { /// `left`: retained outer tuple (logical right for RightOuter). /// `right`: current inner tuple (logical left for RightOuter). /// `ty`: the type of join - fn emit_tuple(mut left: TupleImpl, mut right: TupleImpl, ty: JoinType) -> TupleImpl { - if matches!(ty, JoinType::RightOuter) { - let pk = right.take_pk().or_else(|| left.pk().cloned()); - TupleImpl::concat(right, left, pk) + fn emit_tuple(left: &TupleImpl, right: &mut TupleImpl, ty: JoinType) { + let values = left.iter().cloned(); + let pk = if matches!(ty, JoinType::RightOuter) { + right.extend(values); + right.take_pk().or_else(|| left.pk().cloned()) } else { - let pk = left.take_pk().or_else(|| right.take_pk()); - TupleImpl::concat(left, right, pk) - } + right.prepend(values); + left.pk().cloned().or_else(|| right.take_pk()) + }; + right.set_pk(pk); } } diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index d83188b1..9746b9d4 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -220,9 +220,11 @@ impl<'a> MarkApply<'a> { { let mut right = arena.take_tuple(); let pk = left.take_pk().or_else(|| right.take_pk()); - arena.produce(TupleImpl::concat(left, right, pk)); + let joined = arena.concat(left, right, pk); + arena.produce(joined); return Ok(()); } + arena.release(left); } arena.finish(); Ok(()) diff --git a/src/execution/dql/seq_scan.rs b/src/execution/dql/seq_scan.rs index 4ff2b81e..4046893d 100644 --- a/src/execution/dql/seq_scan.rs +++ b/src/execution/dql/seq_scan.rs @@ -83,7 +83,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for SeqScan<'a, T> { } }; - if iter.next_tuple_into(state.table_codec, state.result.tuple.output_mut())? { + if iter.next_tuple_into(state.table_codec, state.result.output_mut())? { arena.resume(); } else { arena.finish(); diff --git a/src/execution/mod.rs b/src/execution/mod.rs index df7a352a..082cf08b 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -83,7 +83,7 @@ use crate::planner::{ExecMetaArena, ExprRef, LogicalPlan, PlanKeeper}; use crate::storage::table_codec::TableCodec; use crate::storage::{StatisticsMetaCache, TableCache, Transaction, ViewCache}; use crate::types::index::RuntimeIndexProbe; -use crate::types::tuple::{Input, Tuple, TupleImpl}; +use crate::types::tuple::{ConcatPool, Input, Tuple, TupleId, TupleImpl}; #[derive(Clone, Copy)] pub(crate) struct ExecutionContext<'a> { @@ -144,6 +144,14 @@ pub(crate) enum ExecStatus { pub(crate) struct ExecResult { pub(crate) tuple: TupleImpl, pub(crate) status: Option, + concat_pool: ConcatPool, +} + +impl ExecResult { + #[inline] + pub(crate) fn output_mut(&mut self) -> &mut Tuple { + self.concat_pool.retain_leftmost(&mut self.tuple) + } } /// Resolves either an arena-backed expression or a direct scalar expression. @@ -775,7 +783,22 @@ impl<'a, T: Transaction + 'a> ExecArena<'a, T> { #[inline] pub(crate) fn output_tuple_mut(&mut self) -> &mut Tuple { - self.result.tuple.output_mut() + self.result.output_mut() + } + + #[inline] + pub(crate) fn concat( + &mut self, + left: TupleImpl, + right: TupleImpl, + pk: Option, + ) -> TupleImpl { + self.result.concat_pool.concat(left, right, pk) + } + + #[inline] + pub(crate) fn release(&mut self, tuple: TupleImpl) { + self.result.concat_pool.release(tuple); } pub(crate) fn take_tuple(&mut self) -> TupleImpl { @@ -812,7 +835,10 @@ impl<'a, T: Transaction + 'a> ExecArena<'a, T> { } let pk = self.result.tuple.take_pk(); - let tuple = self.result.tuple.retain_rightmost(); + let tuple = self + .result + .concat_pool + .retain_rightmost(&mut self.result.tuple); tuple.values.rotate_left(base); tuple.values.truncate(exprs.len()); tuple.pk = pk; diff --git a/src/types/tuple.rs b/src/types/tuple.rs index b54fff79..90702ea1 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -21,6 +21,8 @@ use crate::types::value::DataValue; use std::borrow::Borrow; use std::borrow::Cow; use std::io::Cursor; +use std::mem::MaybeUninit; +use std::ptr; const BITS_MAX_INDEX: usize = 8; @@ -242,6 +244,82 @@ impl ConcatTuple { } } +#[allow(clippy::vec_box)] +#[derive(Debug, Default)] +pub(crate) struct ConcatPool(Vec>>); + +impl ConcatPool { + const CAPACITY: usize = 64; + + pub(crate) fn concat( + &mut self, + left: TupleImpl, + right: TupleImpl, + pk: Option, + ) -> TupleImpl { + let concat = ConcatTuple { + pk, + left_len: left.len(), + left, + right, + }; + TupleImpl::Concat(match self.0.pop() { + Some(slot) => Box::write(slot, concat), + None => Box::new(concat), + }) + } + + pub(crate) fn retain_leftmost<'a>(&mut self, tuple: &'a mut TupleImpl) -> &'a mut Tuple { + self.retain(tuple, true) + } + + pub(crate) fn retain_rightmost<'a>(&mut self, tuple: &'a mut TupleImpl) -> &'a mut Tuple { + self.retain(tuple, false) + } + + fn retain<'a>(&mut self, tuple: &'a mut TupleImpl, keep_left: bool) -> &'a mut Tuple { + loop { + match tuple { + TupleImpl::Tuple(tuple) => return tuple, + TupleImpl::Concat(_) => *tuple = self.split(std::mem::take(tuple), keep_left), + } + } + } + + fn split(&mut self, tuple: TupleImpl, keep_left: bool) -> TupleImpl { + match tuple { + TupleImpl::Tuple(tuple) => TupleImpl::Tuple(tuple), + TupleImpl::Concat(boxed) => { + let ConcatTuple { left, right, .. } = self.recycle(boxed); + let (keep, discard) = if keep_left { + (left, right) + } else { + (right, left) + }; + self.release(discard); + keep + } + } + } + + pub(crate) fn release(&mut self, tuple: TupleImpl) { + if let TupleImpl::Concat(boxed) = tuple { + let ConcatTuple { left, right, .. } = self.recycle(boxed); + self.release(left); + self.release(right); + } + } + + fn recycle(&mut self, boxed: Box) -> ConcatTuple { + let raw = Box::into_raw(boxed); + let (concat, slot) = unsafe { (ptr::read(raw), Box::from_raw(raw.cast())) }; + if self.0.len() < Self::CAPACITY { + self.0.push(slot); + } + concat + } +} + impl Default for TupleImpl { fn default() -> Self { TupleImpl::Tuple(Tuple::default()) @@ -334,6 +412,23 @@ impl TupleImpl { } } + pub fn prepend(&mut self, values: impl IntoIterator) { + match self { + TupleImpl::Tuple(tuple) => { + let len = tuple.values.len(); + tuple.values.extend(values); + let count = tuple.values.len() - len; + tuple.values.rotate_right(count); + } + TupleImpl::Concat(_) => { + let pk = self.take_pk(); + let rest = std::mem::take(self); + let head = Tuple::new(None, values.into_iter().collect()); + *self = TupleImpl::concat(head.into(), rest, pk); + } + } + } + pub fn resize(&mut self, len: usize, value: DataValue) { let prefix: usize = self.len() - self.rightmost_mut().values.len(); debug_assert!(len >= prefix, "resize cannot shrink into the left prefix"); @@ -456,14 +551,11 @@ impl TupleImpl { } } } +} - pub(crate) fn retain_rightmost(&mut self) -> &mut Tuple { - loop { - match self { - TupleImpl::Tuple(tuple) => return tuple, - TupleImpl::Concat(concat) => *self = std::mem::take(&mut concat.right), - } - } +impl Extend for TupleImpl { + fn extend>(&mut self, values: I) { + self.rightmost_mut().values.extend(values); } } @@ -1253,5 +1345,37 @@ mod tests { assert_eq!(output.values.len(), 4); assert!(matches!(tuple, TupleImpl::Tuple(_))); } + + #[test] + fn concat_pool_recycles_and_reuses_boxes() { + use super::{ConcatPool, TupleImpl}; + + let mut pool = ConcatPool::default(); + let inner = pool.concat(ints(None, &[0]), ints(None, &[1]), None); + let mut tuple = pool.concat(inner, ints(None, &[2, 3]), Some(DataValue::Int32(9))); + assert_eq!(tuple, [0, 1, 2, 3].map(DataValue::Int32)); + + assert_eq!( + pool.retain_rightmost(&mut tuple).values, + [2, 3].map(DataValue::Int32) + ); + assert_eq!(pool.0.len(), 2); + + let inner = pool.concat(ints(None, &[4]), ints(None, &[5]), None); + let mut tuple = pool.concat(ints(None, &[6]), inner, None); + assert!(pool.0.is_empty()); + assert_eq!(tuple, [6, 4, 5].map(DataValue::Int32)); + + assert_eq!( + pool.retain_leftmost(&mut tuple).values, + [6].map(DataValue::Int32) + ); + assert!(matches!(tuple, TupleImpl::Tuple(_))); + assert_eq!(pool.0.len(), 2); + + let dropped = pool.concat(ints(None, &[7]), ints(None, &[8]), None); + drop(dropped); + assert_eq!(pool.0.len(), 1); + } } // GRCOV_EXCL_STOP From 593a17118b46220100f3ca7a3d21178ad231b519 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:58 +0800 Subject: [PATCH 40/62] fix(optimizer): propagate index predicates through transparent operators --- src/optimizer/plan_utils.rs | 27 ++++++- .../rule/normalization/elimination.rs | 74 ++++++++++++------- .../rule/normalization/pushdown_predicates.rs | 68 +++++++++-------- tests/slt/where_by_index.slt | 11 +++ 4 files changed, 121 insertions(+), 59 deletions(-) diff --git a/src/optimizer/plan_utils.rs b/src/optimizer/plan_utils.rs index 16f54bf7..19977509 100644 --- a/src/optimizer/plan_utils.rs +++ b/src/optimizer/plan_utils.rs @@ -19,7 +19,7 @@ use crate::expression::{AliasType, ScalarExpression}; use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::visitor::{ChildrenVisit, OperatorVisitor}; -use crate::planner::operator::{Operator, RowCountOption}; +use crate::planner::operator::{Operator, PhysicalOption, RowCountOption}; use crate::planner::{Childrens, LogicalPlan, MetaArena, PlanArena}; use crate::types::ColumnId; use std::mem; @@ -93,6 +93,31 @@ pub fn children(plan: &LogicalPlan) -> Vec<&LogicalPlan> { } } +pub(crate) fn predicate_scan_mut<'plan>( + plan: &'plan mut LogicalPlan, + arena: &PlanArena, +) -> Option<( + &'plan mut TableScanOperator, + &'plan mut Option, +)> { + match (&mut plan.operator, plan.childrens.as_mut()) { + (Operator::TableScan(scan), _) if scan.limit == (None, None) => { + Some((scan, &mut plan.physical_option)) + } + (Operator::Sort(_), Childrens::Only(input)) => predicate_scan_mut(input, arena), + (Operator::ScalarQueryInit(init), Childrens::Twins { left, .. }) + if init.param_bindings.is_empty() + && matches!( + arena.expression(init.value), + ScalarExpression::InitValue { .. } + ) => + { + predicate_scan_mut(left, arena) + } + _ => None, + } +} + pub fn replace_with_only_child(plan: &mut LogicalPlan) -> bool { if let Childrens::Only(child) = take_childrens(plan) { *plan = *child; diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index 802e0cb7..61d6b567 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -18,7 +18,9 @@ use crate::expression::visitor_mut::ExprVisitorMut; use crate::expression::{AliasType, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::optimizer::plan_utils::{only_child_mut, replace_with_only_child, wrap_child_with}; +use crate::optimizer::plan_utils::{ + only_child_mut, predicate_scan_mut, replace_with_only_child, wrap_child_with, +}; use crate::planner::operator::join::{JoinCondition, JoinType}; use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::sort::{SortField, SortOperator}; @@ -329,7 +331,7 @@ impl NormalizationRule for EliminateIndexFilter { fn apply( &self, plan: &mut LogicalPlan, - _: &mut PlanArena, + arena: &mut PlanArena, _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { if !matches!(plan.operator, Operator::Filter(_)) { @@ -337,13 +339,15 @@ impl NormalizationRule for EliminateIndexFilter { } let residual = { - let Some(child) = only_child_mut(plan) else { + let Some((_, physical_option)) = + only_child_mut(plan).and_then(|child| predicate_scan_mut(child, arena)) + else { return Ok(false); }; let Some(PhysicalOption { plan: PlanImpl::IndexScan(index_info), .. - }) = child.physical_option.as_ref() + }) = physical_option.as_ref() else { return Ok(false); }; @@ -357,7 +361,11 @@ impl NormalizationRule for EliminateIndexFilter { let Operator::Filter(filter_op) = &mut plan.operator else { unreachable!("filter operator checked before residual rewrite"); }; + if filter_op.predicate == residual { + return Ok(false); + } filter_op.predicate = residual; + filter_op.is_optimized = false; return Ok(true); } @@ -787,6 +795,7 @@ mod tests { use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::project::ProjectOperator; + use crate::planner::operator::scalar_query_init::ScalarQueryInitOperator; use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::top_k::TopKOperator; @@ -794,7 +803,7 @@ mod tests { use crate::planner::operator::{ Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption, }; - use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; + use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; use crate::types::index::{IndexInfo, IndexLookup, IndexMeta, IndexType}; use crate::types::value::DataValue; use crate::types::ColumnId; @@ -1490,35 +1499,46 @@ mod tests { } #[test] - fn exact_index_filter_is_removed_after_physical_selection() -> Result<(), DatabaseError> { - let table_arena = crate::planner::TableArenaCell::default(); - let mut arena = crate::planner::PlanArena::new(&table_arena); - let predicate = - arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(true))); - let mut plan = build_filter_with_selected_index(&mut arena, predicate, None); - - let rule = EliminateIndexFilter; - assert!(rule.apply(&mut plan, &mut arena, None)?); - assert!(matches!(plan.operator, Operator::TableScan(_))); - Ok(()) - } - - #[test] - fn partial_index_filter_keeps_residual_after_physical_selection() -> Result<(), DatabaseError> { + fn index_filter_elimination_preserves_residual_and_initializer() -> Result<(), DatabaseError> { let table_arena = crate::planner::TableArenaCell::default(); let mut arena = crate::planner::PlanArena::new(&table_arena); let predicate = arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(true))); let residual = arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(false))); - let mut plan = build_filter_with_selected_index(&mut arena, predicate, Some(residual)); - + let id = arena.alloc_scalar_query_ref(false); + let value = arena.alloc_expression(ScalarExpression::InitValue { + id, + ty: LogicalType::Integer, + }); let rule = EliminateIndexFilter; - assert!(rule.apply(&mut plan, &mut arena, None)?); - let Operator::Filter(filter_op) = &plan.operator else { - unreachable!("residual predicate should keep filter"); - }; - assert_eq!(filter_op.predicate, residual); + for residual in [None, Some(residual)] { + for through_init in [false, true] { + let mut plan = build_filter_with_selected_index(&mut arena, predicate, residual); + if through_init { + let scan = plan.childrens.pop_only(); + plan.childrens = + Box::new(Childrens::Only(Box::new(ScalarQueryInitOperator::build( + scan, + LogicalPlan::new(Operator::Dummy, Childrens::None), + value, + vec![], + )))); + } + let input = plan.childrens.only().clone(); + assert!(rule.apply(&mut plan, &mut arena, None)?); + if let Some(residual) = residual { + let Operator::Filter(filter) = &plan.operator else { + panic!("residual predicate should keep filter") + }; + assert_eq!(filter.predicate, residual); + assert_eq!(plan.childrens.only(), &input); + } else { + assert_eq!(plan, input); + } + assert!(!rule.apply(&mut plan, &mut arena, None)?); + } + } Ok(()) } diff --git a/src/optimizer/rule/normalization/pushdown_predicates.rs b/src/optimizer/rule/normalization/pushdown_predicates.rs index 076c5fca..7ca092d8 100644 --- a/src/optimizer/rule/normalization/pushdown_predicates.rs +++ b/src/optimizer/rule/normalization/pushdown_predicates.rs @@ -19,7 +19,7 @@ use crate::expression::visitor_mut::{ExprVisitorMut, PositionShift}; use crate::expression::{BinaryOperator, ScalarExpression}; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::optimizer::plan_utils::{replace_with_only_child, wrap_child_with}; +use crate::optimizer::plan_utils::{predicate_scan_mut, replace_with_only_child, wrap_child_with}; use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::join::extract_join_keys; use crate::planner::operator::join::{JoinCondition, JoinType}; @@ -325,8 +325,7 @@ impl NormalizationRule for PushPredicateIntoScan { let Childrens::Only(child) = childrens.as_mut() else { return Ok(false); }; - let child = child.as_mut(); - let Operator::TableScan(scan_op) = &mut child.operator else { + let Some((scan_op, _)) = predicate_scan_mut(child, arena) else { return Ok(false); }; @@ -740,39 +739,46 @@ mod tests { fn test_push_predicate_into_scan() -> Result<(), DatabaseError> { let table_state = build_t1_table()?; let mut arena = PlanArena::new(&table_state.table_arena); - // 1 - c2 < 0 => c2 > 1 - let plan = - table_state.plan_with_arena("select * from t1 where -(1 - c2) > 0", &mut arena)?; - - let best_plan = apply_pipeline( - plan, - HepOptimizerPipeline::builder() - .before_batch( - "simplify_filter", - HepBatchStrategy::once_topdown(), - &[NormalizationRuleImpl::SimplifyFilter], - ) - .before_batch( - "test_push_predicate_into_scan", - HepBatchStrategy::once_topdown(), - &[NormalizationRuleImpl::PushPredicateIntoScan], - ), - &mut arena, - )?; + // 1 - c2 < 0 => c2 > 1, directly and through scalar initialization. + for (sql, through_init) in [ + ("select * from t1 where -(1 - c2) > 0", false), + ( + "select * from t1 where -(1 - c2) > 0 and c1 = (select 1)", + true, + ), + ] { + let plan = table_state.plan_with_arena(sql, &mut arena)?; + let best_plan = apply_pipeline( + plan, + HepOptimizerPipeline::builder() + .before_batch( + "simplify_filter", + HepBatchStrategy::once_topdown(), + &[NormalizationRuleImpl::SimplifyFilter], + ) + .before_batch( + "test_push_predicate_into_scan", + HepBatchStrategy::once_topdown(), + &[NormalizationRuleImpl::PushPredicateIntoScan], + ), + &mut arena, + )?; - let scan_op = best_plan.childrens.pop_only().childrens.pop_only(); - if let Operator::TableScan(op) = &scan_op.operator { - let mock_range = Range::Scope { - min: Bound::Excluded(DataValue::Int32(1)), - max: Bound::Unbounded, + let mut scan = best_plan.childrens.pop_only().childrens.pop_only(); + if through_init { + assert!(matches!(scan.operator, Operator::ScalarQueryInit(_))); + scan = scan.childrens.pop_twins().0; + } + let Operator::TableScan(op) = &scan.operator else { + panic!("expected table scan") }; - assert_eq!( op.index_infos[0].lookup, - Some(IndexLookup::Static(mock_range)) + Some(IndexLookup::Static(Range::Scope { + min: Bound::Excluded(DataValue::Int32(1)), + max: Bound::Unbounded, + })) ); - } else { - unreachable!("Should be a filter operator") } Ok(()) diff --git a/tests/slt/where_by_index.slt b/tests/slt/where_by_index.slt index 02d78e9b..b43231cf 100644 --- a/tests/slt/where_by_index.slt +++ b/tests/slt/where_by_index.slt @@ -362,6 +362,17 @@ select k from tuple_bounds where w = 1 and k >= 1 and k < 3 order by k; 1 2 +# Scalar initialization must not block the static index prefix or erase its residual. +query T +explain select k from tuple_bounds where w = 1 and k = (select max(k) from tuple_bounds where w = 1); +---- +Projection [tuple_bounds.k] [Project => (Sort Option: Follow)] Filter (tuple_bounds.k = InitValue(1:0)), Is Having: false [Filter => (Sort Option: Follow)] ScalarQueryInit InitValue(1:0) TableScan tuple_bounds -> [tuple_bounds.w, tuple_bounds.k] [IndexScan By pk_index => [(1), (1)] Covered => (Sort Option: OrderBy: (tuple_bounds.w Asc Nulls Last, tuple_bounds.k Asc Nulls Last) ignore_prefix_len: 1)] ScalarSubquery [ScalarSubquery => (Sort Option: Follow)] Projection [Max(tuple_bounds.k)] [Project => (Sort Option: Follow)] Aggregate [Max(tuple_bounds.k)] [SimpleAggregate => (Sort Option: None)] Top 1, Sort By tuple_bounds.k Desc Nulls Last [TopK => (Sort Option: OrderBy: (tuple_bounds.k Desc Nulls Last) ignore_prefix_len: 0)] TableScan tuple_bounds -> [tuple_bounds.w, tuple_bounds.k] [IndexScan By pk_index => [(1), (1)] Covered => (Sort Option: OrderBy: (tuple_bounds.w Asc Nulls Last, tuple_bounds.k Asc Nulls Last) ignore_prefix_len: 1)] + +query I +select k from tuple_bounds where w = 1 and k = (select max(k) from tuple_bounds where w = 1); +---- +3 + statement ok drop table tuple_bounds; From dc167adeb1283694f4c7a5cfd010dff36613ae6c Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:58 +0800 Subject: [PATCH 41/62] perf(execution): skip probed join keys and reuse probe ranges on reset - Drop the equality predicates already enforced by a parameterized index probe; MarkApply now treats NULL probe values as no match up front. - IndexScan reset rewinds static ranges and overwrites the Eq key of runtime probes in place instead of rebuilding the ranges. - Inner hash join builds the last match in the probe row's buffer, and TupleImpl::prepend extends the leftmost leaf of a joined row. --- src/execution/dql/index_scan.rs | 22 +++++++++------ src/execution/dql/join/hash/mod.rs | 18 ++++++------- src/execution/dql/mark_apply.rs | 4 +++ .../rule/normalization/parameterized_index.rs | 27 ++++++++++++++++--- src/storage/mod.rs | 17 ++++++++++-- src/types/index.rs | 9 +++++++ src/types/tuple.rs | 8 +++--- src/types/value.rs | 8 ++++++ 8 files changed, 86 insertions(+), 27 deletions(-) diff --git a/src/execution/dql/index_scan.rs b/src/execution/dql/index_scan.rs index d2499cab..f183c3b9 100644 --- a/src/execution/dql/index_scan.rs +++ b/src/execution/dql/index_scan.rs @@ -14,12 +14,12 @@ use crate::errors::DatabaseError; use crate::execution::{ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor}; -use crate::expression::range_detacher::{IndexRangeColumn, Range, RangeDetacher}; +use crate::expression::range_detacher::{IndexRangeColumn, RangeDetacher}; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::SortOption; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::{IndexIter, IndexRanges, Iter, Transaction}; -use crate::types::index::{IndexInfo, IndexLookup, RuntimeIndexProbe}; +use crate::types::index::{IndexInfo, IndexLookup}; use std::borrow::Cow; pub(crate) struct IndexScan<'a, T: Transaction + 'a> { @@ -51,10 +51,7 @@ impl<'a, T: Transaction + 'a> IndexScan<'a, T> { ) -> Result, DatabaseError> { let mut range = match lookup { IndexLookup::Static(range) => Cow::Borrowed(range), - IndexLookup::Probe => Cow::Owned(match arena.pop_runtime_probe() { - RuntimeIndexProbe::Eq(value) => Range::Eq(value), - RuntimeIndexProbe::Scope { min, max } => Range::Scope { min, max }, - }), + IndexLookup::Probe => Cow::Owned(arena.pop_runtime_probe().into()), }; if plan_arena.has_bound_params() && range.has_parameter() { @@ -105,8 +102,16 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for IndexScan<'a, T> { plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { if let Some(iter) = self.iter.as_mut() { - let ranges = Self::ranges(self.info, self.lookup, arena, plan_arena)?; - iter.reset(ranges, self.op.limit); + match self.lookup { + IndexLookup::Static(_) => iter.rewind(self.op.limit), + IndexLookup::Probe if self.info.residual_predicate.is_none() => { + iter.reset_probe(arena.pop_runtime_probe(), self.op.limit) + } + IndexLookup::Probe => { + let ranges = Self::ranges(self.info, self.lookup, arena, plan_arena)?; + iter.reset(ranges, self.op.limit); + } + } } Ok(()) } @@ -163,6 +168,7 @@ mod tests { use crate::storage::memory::MemoryStorage; use crate::storage::table_codec::TableCodec; use crate::storage::{StatisticsMetaCache, Storage, TableCache, ViewCache}; + use crate::types::index::RuntimeIndexProbe; use crate::types::tuple::Tuple; use crate::types::value::DataValue; use crate::types::LogicalType; diff --git a/src/execution/dql/join/hash/mod.rs b/src/execution/dql/join/hash/mod.rs index ae872a5f..a1f28364 100644 --- a/src/execution/dql/join/hash/mod.rs +++ b/src/execution/dql/join/hash/mod.rs @@ -141,15 +141,15 @@ pub(crate) fn join_rows( probe: &mut TupleImpl, consume_probe: bool, ) -> TupleImpl { + if consume_probe { + let pk = build.pk().cloned().or_else(|| probe.take_pk()); + let mut row = std::mem::take(probe); + row.prepend(build.iter().cloned()); + row.set_pk(pk); + return row; + } let mut values = Vec::with_capacity(build.len() + probe.len()); values.extend(build.iter().cloned()); - let pk = if consume_probe { - let pk = build.pk().cloned().or_else(|| probe.take_pk()); - probe.drain_values(|value| values.push(value)); - pk - } else { - values.extend(probe.iter().cloned()); - build.pk().or(probe.pk()).cloned() - }; - Tuple::new(pk, values).into() + values.extend(probe.iter().cloned()); + Tuple::new(build.pk().or(probe.pk()).cloned(), values).into() } diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index 9746b9d4..7cd73e26 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -208,6 +208,10 @@ impl<'a> MarkApply<'a> { while arena.next_tuple(self.left_input, plan_arena)? { let mut left = arena.take_tuple(); let value = self.parameterized_probe_value(&left, plan_arena)?; + if value.as_ref().is_some_and(DataValue::has_null) { + arena.release(left); + continue; + } let root = input.reset(arena, plan_arena, self.runtime_probe_for(value))?; // A full unique-key probe cannot have another match, so consume the left row. if arena.next_tuple(root, plan_arena)? diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 67494192..81a875a7 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -299,9 +299,22 @@ fn constant_keys(expr: ExprRef, keys: &mut Vec<(ExprRef, ExprRef)>, arena: &Plan pub(crate) struct ProbeIndex { position: usize, pub(crate) probe: Vec, + probed_keys: u64, pub(crate) lookup_work: usize, } +#[inline] +fn mark_probed(probed_keys: &mut u64, position: usize) { + if position < u64::BITS as usize { + *probed_keys |= 1 << position; + } +} + +#[inline] +fn is_probed(probed_keys: u64, position: usize) -> bool { + position < u64::BITS as usize && probed_keys & (1 << position) != 0 +} + pub(crate) fn probe_index( plan: &LogicalPlan, keys: &mut Vec<(ExprRef, ExprRef)>, @@ -328,6 +341,7 @@ pub(crate) fn probe_index( } probe.clear(); let mut uses_join_key = false; + let mut probed_keys = 0u64; 'columns: for id in &meta.column_ids { let Some(candidate) = scan .columns @@ -347,7 +361,10 @@ pub(crate) fn probe_index( == arena.column(*candidate).datatype() { probe.push(value); - uses_join_key |= position < join_key_count; + if position < join_key_count { + uses_join_key = true; + mark_probed(&mut probed_keys, position); + } continue 'columns; } } @@ -367,6 +384,7 @@ pub(crate) fn probe_index( best = Some(ProbeIndex { position, probe: mem::take(&mut probe), + probed_keys, lookup_work, }); } @@ -430,7 +448,7 @@ fn use_probe_index(plan: &mut LogicalPlan, position: usize, arena: &PlanArena<'_ } /// Builds the inner `MarkApply` probing `right` with `index` for every `left` row. `on` holds -/// the join keys bound to each side; all of them are still checked after the probe. +/// the join keys bound to each side; keys not used by the probe are checked after it. pub(crate) fn probe_join( mut left: LogicalPlan, mut right: LogicalPlan, @@ -447,7 +465,10 @@ pub(crate) fn probe_join( arena.alloc_expression(ScalarExpression::Tuple(index.probe)) }; let mut predicates = Vec::with_capacity(on.len()); - for (left_expr, mut right_expr) in on { + for (i, (left_expr, mut right_expr)) in on.into_iter().enumerate() { + if is_probed(index.probed_keys, i) { + continue; + } PositionShift { delta: left_len as isize, } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index a7d43ac4..4ac1deb1 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -35,7 +35,7 @@ use crate::planner::operator::alter_table::change_column::{DefaultChange, NotNul use crate::planner::{MetaArena, TableArenaCell}; use crate::serdes::ReferenceTables; use crate::storage::table_codec::{Bytes, IndexStatisticsCodecType, TableCodec, BOUND_MAX_TAG}; -use crate::types::index::{Index, IndexId, IndexMeta, IndexMetaRef, IndexType}; +use crate::types::index::{Index, IndexId, IndexMeta, IndexMetaRef, IndexType, RuntimeIndexProbe}; use crate::types::serialize::TupleValueSerializableImpl; use crate::types::tuple::{Tuple, TupleId, TupleImpl, TupleLike}; use crate::types::value::{DataValue, OrderedMapping, TupleMappingRef}; @@ -2162,9 +2162,22 @@ pub enum IndexIterState<'a, T: Transaction + 'a> { impl<'a, T: Transaction> IndexIter<'a, T> { pub(crate) fn reset(&mut self, ranges: IndexRanges<'a>, bounds: Bounds) { - self.state = IndexIterState::Init; self.params.reverse = ranges.reverse; self.ranges = ranges; + self.rewind(bounds); + } + + pub(crate) fn reset_probe(&mut self, probe: RuntimeIndexProbe, bounds: Bounds) { + match (&mut self.ranges.source, probe) { + (RangeSource::Owned(Range::Eq(value)), RuntimeIndexProbe::Eq(probe)) => *value = probe, + (source, probe) => *source = RangeSource::Owned(probe.into()), + } + self.rewind(bounds); + } + + pub(crate) fn rewind(&mut self, bounds: Bounds) { + self.state = IndexIterState::Init; + self.ranges.next = 0; self.bounds = IterBounds::new(bounds.0.unwrap_or(0), bounds.1); } } diff --git a/src/types/index.rs b/src/types/index.rs index 020355d1..7b9753d5 100644 --- a/src/types/index.rs +++ b/src/types/index.rs @@ -65,6 +65,15 @@ pub enum RuntimeIndexProbe { }, } +impl From for Range { + fn from(probe: RuntimeIndexProbe) -> Self { + match probe { + RuntimeIndexProbe::Eq(value) => Range::Eq(value), + RuntimeIndexProbe::Scope { min, max } => Range::Scope { min, max }, + } + } +} + #[derive(Debug, Clone, Eq, PartialEq, Hash, ReferenceSerialization)] pub enum IndexLookup { Static(Range), diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 90702ea1..e89054e6 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -420,11 +420,9 @@ impl TupleImpl { let count = tuple.values.len() - len; tuple.values.rotate_right(count); } - TupleImpl::Concat(_) => { - let pk = self.take_pk(); - let rest = std::mem::take(self); - let head = Tuple::new(None, values.into_iter().collect()); - *self = TupleImpl::concat(head.into(), rest, pk); + TupleImpl::Concat(concat) => { + concat.left.prepend(values); + concat.left_len = concat.left.len(); } } } diff --git a/src/types/value.rs b/src/types/value.rs index ba3925df..78b81ab0 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -884,6 +884,14 @@ impl DataValue { matches!(self, DataValue::Null) } + #[inline] + pub fn has_null(&self) -> bool { + match self { + DataValue::Tuple(values) => values.iter().any(DataValue::has_null), + value => value.is_null(), + } + } + #[inline] pub fn init(logic_type: &LogicalType) -> DataValue { match logic_type { From cabdfe41e8f438072409377b021f1519a67febb1 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:58 +0800 Subject: [PATCH 42/62] test(optimizer): expect probed join keys to leave no apply predicates --- .../rule/normalization/parameterized_index.rs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 81a875a7..e5e77807 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -1276,7 +1276,7 @@ mod inner_join_tests { panic!("expected inner join apply") }; assert_eq!(apply.kind, MarkApplyKind::InnerJoin); - assert_eq!(apply.predicates().len(), 1); + assert!(apply.predicates().is_empty()); assert_eq!( apply.parameterized_probe().copied(), Some(if swap { r } else { l }) @@ -1356,13 +1356,12 @@ mod inner_join_tests { panic!("expected apply without projection") }; assert_eq!(apply.kind, MarkApplyKind::InnerJoin); - assert_eq!(apply.predicates().len(), 2); + assert!(apply.predicates().is_empty()); assert_eq!( arena.expression(*apply.parameterized_probe().unwrap()), &ScalarExpression::Tuple(vec![constant, l1, l0]) ); assert_eq!((position(l0, &arena), position(l1, &arena)), (0, 1)); - assert_eq!((position(r1, &arena), position(r2, &arena)), (3, 4)); let Childrens::Twins { right, .. } = plan.childrens.as_ref() else { unreachable!() }; @@ -1477,16 +1476,8 @@ mod inner_join_tests { PlanImpl::MarkApply ); assert_eq!(apply.parameterized_probe(), Some(&r)); - assert_eq!((position(r, &arena), position(l, &arena)), (0, 2)); - let ScalarExpression::Binary { - left_expr, - right_expr, - .. - } = arena.expression(apply.predicates()[0]) - else { - unreachable!() - }; - assert_eq!((*left_expr, *right_expr), (r, l)); + assert_eq!(position(r, &arena), 0); + assert!(apply.predicates().is_empty()); let Childrens::Twins { left, right } = apply_plan.childrens.as_ref() else { unreachable!() }; From 6a461408ca6a367a7252347a7eabe8371a82efc3 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:58 +0800 Subject: [PATCH 43/62] test: align optimizer and ORM regressions with current behavior --- src/execution/dml/analyze.rs | 3 +- .../rule/normalization/elimination.rs | 95 ++++++++++++++----- src/orm/mod.rs | 13 ++- tests/macros-test/src/main.rs | 2 +- 4 files changed, 83 insertions(+), 30 deletions(-) diff --git a/src/execution/dml/analyze.rs b/src/execution/dml/analyze.rs index b9f164df..862d9e98 100644 --- a/src/execution/dml/analyze.rs +++ b/src/execution/dml/analyze.rs @@ -367,7 +367,8 @@ mod test { create_index(&mut kite_sql, "create index b_index on t1 (b)")?; create_index(&mut kite_sql, "create index p_index on t1 (a, b)")?; - for i in 0..DEFAULT_NUM_OF_BUCKETS + 1 { + // Keep this fixture independent of the default histogram bucket count. + for i in 0..101 { kite_sql .run(format!("insert into t1 values({i}, {}, NULL)", i % 20))? .done()?; diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index 61d6b567..481bca9d 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -847,6 +847,22 @@ mod tests { (plan, expr) } + fn eliminate_projects( + plan: &mut LogicalPlan, + arena: &mut PlanArena, + ) -> Result { + let original = plan.clone(); + let pipeline = HepOptimizerPipeline::builder() + .before_batch( + "eliminate_projects", + HepBatchStrategy::fix_point_bottomup(10), + &[NormalizationRuleImpl::EliminateProject], + ) + .build(); + *plan = pipeline.instantiate(plan.take()).find_best(None, arena)?; + Ok(*plan != original) + } + #[test] fn substitutes_through_filter_and_sort_without_cloning_sources() -> Result<(), DatabaseError> { let table = build_t1_table()?; @@ -881,7 +897,7 @@ mod tests { Childrens::Only(Box::new(sort)), ); - assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + assert!(eliminate_projects(&mut plan, &mut arena)?); let Operator::Project(op) = &plan.operator else { unreachable!() }; @@ -911,13 +927,12 @@ mod tests { unreachable!() }; assert!(matches!(scan.operator, Operator::TableScan(_))); - assert!(!EliminateProject.apply(&mut plan, &mut arena, None)?); + assert!(!eliminate_projects(&mut plan, &mut arena)?); Ok(()) } #[test] - fn aggregate_consumes_mapping_but_its_output_starts_a_new_slot_space( - ) -> Result<(), DatabaseError> { + fn aggregate_preserves_input_project_and_separate_output_slots() -> Result<(), DatabaseError> { let table = build_t1_table()?; let mut arena = PlanArena::new(&table.table_arena); let scan = table.plan_with_arena("select c1, c2 from t1", &mut arena)?; @@ -944,7 +959,9 @@ mod tests { Childrens::Only(Box::new(aggregate)), ); - assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + let original = plan.clone(); + assert!(!eliminate_projects(&mut plan, &mut arena)?); + assert_eq!(plan, original); let Operator::Project(op) = &plan.operator else { unreachable!() }; @@ -958,16 +975,23 @@ mod tests { let ScalarExpression::AggCall { args, .. } = arena.expression(op.agg_calls[0]) else { unreachable!() }; - assert_eq!(args, &[replacement]); - let Childrens::Only(scan) = aggregate.childrens.as_ref() else { + assert_eq!(args, &[arg]); + let Childrens::Only(input) = aggregate.childrens.as_ref() else { unreachable!() }; - assert!(matches!(scan.operator, Operator::TableScan(_))); + let Operator::Project(op) = &input.operator else { + unreachable!() + }; + assert_eq!(op.exprs[0], replacement); + assert!(matches!( + input.childrens.only().operator, + Operator::TableScan(_) + )); Ok(()) } #[test] - fn repeated_refs_are_rewritten_once_and_replacements_are_not_revisited( + fn repeated_refs_are_copied_without_mutating_sources_or_revisiting_replacements( ) -> Result<(), DatabaseError> { let table = build_t1_table()?; let mut arena = PlanArena::new(&table.table_arena); @@ -981,10 +1005,21 @@ mod tests { ); assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + let Operator::Project(op) = &plan.operator else { + unreachable!() + }; + for rewritten in &op.exprs { + let ScalarExpression::Binary { left_expr, .. } = rewritten.unpack_alias_ref(&arena) + else { + unreachable!() + }; + assert_eq!(*left_expr, replacement); + assert_ne!(*rewritten, expr); + } let ScalarExpression::Binary { left_expr, .. } = arena.expression(expr) else { unreachable!() }; - assert_eq!(*left_expr, replacement); + assert_eq!(*left_expr, reference); let ScalarExpression::Binary { left_expr, .. } = arena.expression(replacement) else { unreachable!() }; @@ -997,7 +1032,7 @@ mod tests { } #[test] - fn mapping_transfer_keeps_shared_expressions_visited_across_plan_nodes( + fn mapping_transfer_preserves_shared_source_expressions_across_plan_nodes( ) -> Result<(), DatabaseError> { let table = build_t1_table()?; let mut arena = PlanArena::new(&table.table_arena); @@ -1018,11 +1053,25 @@ mod tests { Childrens::Only(Box::new(filter)), ); - assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + assert!(eliminate_projects(&mut plan, &mut arena)?); + let Operator::Project(project) = &plan.operator else { + unreachable!() + }; + let Operator::Filter(filter) = &plan.childrens.only().operator else { + unreachable!() + }; + for rewritten in [project.exprs[0], filter.predicate] { + let ScalarExpression::Binary { left_expr, .. } = rewritten.unpack_alias_ref(&arena) + else { + unreachable!() + }; + assert_eq!(*left_expr, replacement); + assert_ne!(rewritten, shared); + } let ScalarExpression::Binary { left_expr, .. } = arena.expression(shared) else { unreachable!() }; - assert_eq!(*left_expr, replacement); + assert_eq!(*left_expr, reference); let ScalarExpression::Binary { left_expr, .. } = arena.expression(replacement) else { unreachable!() }; @@ -1031,7 +1080,7 @@ mod tests { unreachable!() }; assert_eq!(*position, 1); - assert!(!EliminateProject.apply(&mut plan, &mut arena, None)?); + assert!(!eliminate_projects(&mut plan, &mut arena)?); Ok(()) } @@ -1085,7 +1134,7 @@ mod tests { } let schema = plan.output_schema(&mut arena).clone(); - assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + assert!(eliminate_projects(&mut plan, &mut arena)?); assert_eq!(plan.output_schema(&mut arena), &schema); assert_eq!(plan.explain(&mut arena, 0).matches("Projection").count(), 1); let Operator::Project(op) = &plan.operator else { @@ -1113,7 +1162,7 @@ mod tests { names, ["d2", "e2", "c5", "a3", "e3", "x3", "b4", "d4", "b9", "d9", "a1", "e1"] ); - assert!(!EliminateProject.apply(&mut plan, &mut arena, None)?); + assert!(!eliminate_projects(&mut plan, &mut arena)?); Ok(()) } @@ -1134,7 +1183,7 @@ mod tests { JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); let mut plan = permutation(&mut arena, cross, &[0, 1, 2]); - assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + assert!(eliminate_projects(&mut plan, &mut arena)?); assert_eq!(plan.explain(&mut arena, 0).matches("Projection").count(), 1); let Operator::Project(op) = &plan.operator else { unreachable!() @@ -1146,7 +1195,7 @@ mod tests { }; assert_eq!(*position, expected); } - assert!(!EliminateProject.apply(&mut plan, &mut arena, None)?); + assert!(!eliminate_projects(&mut plan, &mut arena)?); } Ok(()) } @@ -1161,7 +1210,7 @@ mod tests { let cross = JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); let mut plan = permutation(&mut arena, cross, &[0, 1]); - assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + assert!(eliminate_projects(&mut plan, &mut arena)?); assert_eq!(plan.explain(&mut arena, 0).matches("Projection").count(), 1); let Operator::Project(op) = &plan.operator else { unreachable!() @@ -1172,7 +1221,7 @@ mod tests { }; assert_eq!(*position, expected); } - assert!(!EliminateProject.apply(&mut plan, &mut arena, None)?); + assert!(!eliminate_projects(&mut plan, &mut arena)?); Ok(()) } @@ -1187,7 +1236,7 @@ mod tests { let cross = JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); let mut plan = permutation(&mut arena, cross, &[0, 1, 2]); - assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); + assert!(eliminate_projects(&mut plan, &mut arena)?); assert_eq!(plan.explain(&mut arena, 0).matches("Projection").count(), 1); let Operator::Project(op) = &plan.operator else { unreachable!() @@ -1215,7 +1264,7 @@ mod tests { let pipeline = HepOptimizerPipeline::builder() .before_batch( "test_collapse_project", - HepBatchStrategy::once_topdown(), + HepBatchStrategy::fix_point_bottomup(10), &[NormalizationRuleImpl::EliminateProject], ) .build(); @@ -1249,7 +1298,7 @@ mod tests { let pipeline = HepOptimizerPipeline::builder() .before_batch( "test_collapse_project_with_alias", - HepBatchStrategy::once_topdown(), + HepBatchStrategy::fix_point_bottomup(10), &[NormalizationRuleImpl::EliminateProject], ) .build(); diff --git a/src/orm/mod.rs b/src/orm/mod.rs index fa08ade1..8f8dfa0b 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -3903,7 +3903,9 @@ mod tests { #[test] fn database_and_transaction_orm_helpers_bind_expected_plans() -> Result<(), DatabaseError> { - let mut database = crate::db::DataBaseBuilder::path("./orm-unit-test").build_in_memory()?; + let mut database = crate::db::DataBaseBuilder::path("./orm-unit-test") + .histogram_buckets(100) + .build_in_memory()?; database .ddl("create table orm_unit_users (id int primary key, name varchar, age int null)")?; database.create_view("orm_unit_user_names", |ctx| { @@ -4077,8 +4079,9 @@ mod tests { concat!( "Projection [orm_unit_users.id] [Project => (Sort Option: Follow)] ", "Sort By 1 Asc Nulls Last [Sort => (Sort Option: OrderBy: (1 Asc Nulls Last) ignore_prefix_len: 0)] ", - "Filter (((orm_unit_users.id = 3) || ((orm_unit_users.id = 2) || (orm_unit_users.id = 1))) && (orm_unit_users.id != 3)), Is Having: false ", - "[Filter => (Sort Option: Follow)] TableScan orm_unit_users -> [orm_unit_users.id] [SeqScan => (Sort Option: None)]" + "Filter (orm_unit_users.id != 3), Is Having: false ", + "[Filter => (Sort Option: Follow)] TableScan orm_unit_users -> [orm_unit_users.id] ", + "[IndexScan By pk_index => 1, 2, 3 Covered => (Sort Option: OrderBy: (orm_unit_users.id Asc Nulls Last) ignore_prefix_len: 0)]" ), "{list_plan}" ); @@ -4436,8 +4439,8 @@ mod tests { concat!( "Projection [orm_unit_users.id] [Project => (Sort Option: Follow)] ", "InnerJoinApply [MarkApply => (Sort Option: Follow)] ", - "TableScan orm_unit_users -> [orm_unit_users.id] [SeqScan => (Sort Option: None)] ", - "TableScan orm_unit_orders -> [orm_unit_orders.id] [IndexScan By pk_index => Probe ? Covered => (Sort Option: OrderBy: (orm_unit_orders.id Asc Nulls Last) ignore_prefix_len: 0)]" + "TableScan orm_unit_orders -> [orm_unit_orders.id] [SeqScan => (Sort Option: None)] ", + "TableScan orm_unit_users -> [orm_unit_users.id] [IndexScan By pk_index => Probe ? Covered => (Sort Option: OrderBy: (orm_unit_users.id Asc Nulls Last) ignore_prefix_len: 0)]" ), "{inner_using_plan}" ); diff --git a/tests/macros-test/src/main.rs b/tests/macros-test/src/main.rs index b8d54161..174581e1 100644 --- a/tests/macros-test/src/main.rs +++ b/tests/macros-test/src/main.rs @@ -2946,7 +2946,7 @@ mod test { })?; assert_eq!( plan, - "Projection [users.user_name] [Project => (Sort Option: Follow)] Filter (users.id = 1), Is Having: false [Filter => (Sort Option: Follow)] TableScan users -> [users.id, users.user_name] [SeqScan => (Sort Option: None)]" + "Projection [users.user_name] [Project => (Sort Option: Follow)] TableScan users -> [users.id, users.user_name] [IndexScan By pk_index => 1 => (Sort Option: OrderBy: (users.id Asc Nulls Last) ignore_prefix_len: 0)]" ); let set_plan = database.explain(|ctx| { From e095bceef371a05ecd241d249eb085b4deedce48 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:58 +0800 Subject: [PATCH 44/62] fix(storage): decode only view names in SHOW VIEWS without touching TableArena --- src/execution/dql/show_view.rs | 15 ++++----------- src/execution/mod.rs | 8 -------- src/storage/mod.rs | 27 +++++---------------------- src/storage/table_codec.rs | 11 ++++++++++- tests/slt/subquery.slt | 2 +- tests/slt/where_by_index.slt | 2 +- 6 files changed, 21 insertions(+), 44 deletions(-) diff --git a/src/execution/dql/show_view.rs b/src/execution/dql/show_view.rs index fff31a49..2fa98fbd 100644 --- a/src/execution/dql/show_view.rs +++ b/src/execution/dql/show_view.rs @@ -53,23 +53,16 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ShowViews<'a, T> { plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { if self.metas.is_none() { - let context = arena.context(); let mut state = arena.local_state(plan_arena); let (transaction, table_codec) = state.transaction_codec(); - self.metas = Some(transaction.views( - table_codec, - context.table_cache(), - plan_arena.table_arena_cell(), - context.scala_functions(), - context.table_functions(), - )?); + self.metas = Some(transaction.views(table_codec)?); } - let Some(view) = self + let Some(name) = self .metas .as_mut() .expect("show views iterator initialized") - .try_next()? + .try_next(plan_arena)? else { arena.finish(); return Ok(()); @@ -79,7 +72,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ShowViews<'a, T> { output.pk = None; output.values.clear(); output.values.push(DataValue::Utf8 { - value: view.name.to_string(), + value: name.to_string(), ty: Utf8Type::Variable(None), unit: CharLengthUnits::Characters, }); diff --git a/src/execution/mod.rs b/src/execution/mod.rs index 082cf08b..7fb60ea5 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -115,14 +115,6 @@ impl<'a> ExecutionContext<'a> { self.table_cache } - pub(crate) fn scala_functions(self) -> &'a ScalaFunctions { - self.scala_functions - } - - pub(crate) fn table_functions(self) -> &'a TableFunctions { - self.table_functions - } - fn is_same_context(&self, other: ExecutionContext<'_>) -> bool { std::ptr::eq(self.table_cache, other.table_cache) && std::ptr::eq(self.view_cache, other.view_cache) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 4ac1deb1..5f7212f5 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -920,19 +920,10 @@ pub trait Transaction: Sized { fn views<'a>( &'a self, table_codec: &mut TableCodec, - table_cache: &'a TableCache, - table_arena: &'a TableArenaCell, - scala_functions: &'a ScalaFunctions, - table_functions: &'a TableFunctions, ) -> Result, DatabaseError> { table_codec.with_view_bound(|min, max| { Ok(ViewIter { iter: self.range(Bound::Included(min), Bound::Included(max))?, - transaction: self, - table_cache, - table_arena, - scala_functions, - table_functions, }) }) } @@ -2328,26 +2319,18 @@ impl TableIter<'_, T> { pub struct ViewIter<'a, T: Transaction + 'a> { iter: T::IterType<'a>, - transaction: &'a T, - table_cache: &'a TableCache, - table_arena: &'a TableArenaCell, - scala_functions: &'a ScalaFunctions, - table_functions: &'a TableFunctions, } impl ViewIter<'_, T> { - pub fn try_next(&mut self) -> Result, DatabaseError> { + pub fn try_next( + &mut self, + arena: &mut (impl MetaArena + ?Sized), + ) -> Result, DatabaseError> { let Some((_, value)) = self.iter.try_next()? else { return Ok(None); }; - Ok(Some(TableCodec::decode_view( - value, - (self.transaction, self.table_cache), - self.scala_functions, - self.table_functions, - self.table_arena.borrow_mut(), - )?)) + Ok(Some(TableCodec::decode_view_name::(value, arena)?)) } } diff --git a/src/storage/table_codec.rs b/src/storage/table_codec.rs index fd8e137e..91166aec 100644 --- a/src/storage/table_codec.rs +++ b/src/storage/table_codec.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::catalog::view::View; -use crate::catalog::{ColumnCatalog, ColumnRelation, TableMeta}; +use crate::catalog::{ColumnCatalog, ColumnRelation, TableMeta, TableName}; use crate::db::{ScalaFunctions, TableFunctions}; use crate::errors::DatabaseError; use crate::optimizer::core::cm_sketch::{CountMinSketchMeta, CountMinSketchPage}; @@ -1098,6 +1098,15 @@ impl TableCodec { View::decode(&mut cursor, Some(&context), &reference_tables, arena) } + pub fn decode_view_name( + bytes: &[u8], + arena: &mut (impl MetaArena + ?Sized), + ) -> Result { + let mut cursor = Cursor::new(bytes); + cursor.seek(SeekFrom::Start(4))?; + TableName::decode::(&mut cursor, None, &EMPTY_REFERENCE_TABLES, arena) + } + fn encode_root_table_value_into( meta: &TableMeta, reference_tables: &mut ReferenceTables, diff --git a/tests/slt/subquery.slt b/tests/slt/subquery.slt index 677e4fe0..95fa41d1 100644 --- a/tests/slt/subquery.slt +++ b/tests/slt/subquery.slt @@ -850,7 +850,7 @@ insert into corr_rows values (1,10),(2,20),(3,30),(4,null),(5,20); query T explain select id,(select 7) from corr_rows where b<(select 25); ---- -Projection [corr_rows.id, InitValue(1:0)] [Project => (Sort Option: Follow)] ScalarQueryInit InitValue(1:0) Filter (corr_rows.b < InitValue(1:1)), Is Having: false [Filter => (Sort Option: Follow)] ScalarQueryInit InitValue(1:1) TableScan corr_rows -> [corr_rows.id, corr_rows.b] [SeqScan => (Sort Option: None)] ScalarSubquery [ScalarSubquery => (Sort Option: Follow)] Projection [25] [Project => (Sort Option: Follow)] Dummy [Dummy => (Sort Option: None)] ScalarSubquery [ScalarSubquery => (Sort Option: Follow)] Projection [7] [Project => (Sort Option: Follow)] Dummy [Dummy => (Sort Option: None)] +Projection [corr_rows.id, InitValue(1:)] [Project => (Sort Option: Follow)] ScalarQueryInit InitValue(1:) Filter (corr_rows.b < InitValue(1:)), Is Having: false [Filter => (Sort Option: Follow)] ScalarQueryInit InitValue(1:) TableScan corr_rows -> [corr_rows.id, corr_rows.b] [SeqScan => (Sort Option: None)] ScalarSubquery [ScalarSubquery => (Sort Option: Follow)] Projection [25] [Project => (Sort Option: Follow)] Dummy [Dummy => (Sort Option: None)] ScalarSubquery [ScalarSubquery => (Sort Option: Follow)] Projection [7] [Project => (Sort Option: Follow)] Dummy [Dummy => (Sort Option: None)] query II rowsort select id,(select 7) from corr_rows where b<(select 25); diff --git a/tests/slt/where_by_index.slt b/tests/slt/where_by_index.slt index b43231cf..d82745e5 100644 --- a/tests/slt/where_by_index.slt +++ b/tests/slt/where_by_index.slt @@ -366,7 +366,7 @@ select k from tuple_bounds where w = 1 and k >= 1 and k < 3 order by k; query T explain select k from tuple_bounds where w = 1 and k = (select max(k) from tuple_bounds where w = 1); ---- -Projection [tuple_bounds.k] [Project => (Sort Option: Follow)] Filter (tuple_bounds.k = InitValue(1:0)), Is Having: false [Filter => (Sort Option: Follow)] ScalarQueryInit InitValue(1:0) TableScan tuple_bounds -> [tuple_bounds.w, tuple_bounds.k] [IndexScan By pk_index => [(1), (1)] Covered => (Sort Option: OrderBy: (tuple_bounds.w Asc Nulls Last, tuple_bounds.k Asc Nulls Last) ignore_prefix_len: 1)] ScalarSubquery [ScalarSubquery => (Sort Option: Follow)] Projection [Max(tuple_bounds.k)] [Project => (Sort Option: Follow)] Aggregate [Max(tuple_bounds.k)] [SimpleAggregate => (Sort Option: None)] Top 1, Sort By tuple_bounds.k Desc Nulls Last [TopK => (Sort Option: OrderBy: (tuple_bounds.k Desc Nulls Last) ignore_prefix_len: 0)] TableScan tuple_bounds -> [tuple_bounds.w, tuple_bounds.k] [IndexScan By pk_index => [(1), (1)] Covered => (Sort Option: OrderBy: (tuple_bounds.w Asc Nulls Last, tuple_bounds.k Asc Nulls Last) ignore_prefix_len: 1)] +Projection [tuple_bounds.k] [Project => (Sort Option: Follow)] Filter (tuple_bounds.k = InitValue(1:)), Is Having: false [Filter => (Sort Option: Follow)] ScalarQueryInit InitValue(1:) TableScan tuple_bounds -> [tuple_bounds.w, tuple_bounds.k] [IndexScan By pk_index => [(1), (1)] Covered => (Sort Option: OrderBy: (tuple_bounds.w Asc Nulls Last, tuple_bounds.k Asc Nulls Last) ignore_prefix_len: 1)] ScalarSubquery [ScalarSubquery => (Sort Option: Follow)] Projection [Max(tuple_bounds.k)] [Project => (Sort Option: Follow)] Aggregate [Max(tuple_bounds.k)] [SimpleAggregate => (Sort Option: None)] Top 1, Sort By tuple_bounds.k Desc Nulls Last [TopK => (Sort Option: OrderBy: (tuple_bounds.k Desc Nulls Last) ignore_prefix_len: 0)] TableScan tuple_bounds -> [tuple_bounds.w, tuple_bounds.k] [IndexScan By pk_index => [(1), (1)] Covered => (Sort Option: OrderBy: (tuple_bounds.w Asc Nulls Last, tuple_bounds.k Asc Nulls Last) ignore_prefix_len: 1)] query I select k from tuple_bounds where w = 1 and k = (select max(k) from tuple_bounds where w = 1); From bfa128de7a9b97f7a92e8519ce3c93a642b3d456 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:58 +0800 Subject: [PATCH 45/62] perf(planner): index scalar query values by global position instead of hashing --- src/planner/arena.rs | 71 +++++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/src/planner/arena.rs b/src/planner/arena.rs index 87c5bf3f..9cec330a 100644 --- a/src/planner/arena.rs +++ b/src/planner/arena.rs @@ -27,20 +27,18 @@ use crate::types::tuple::Schema; use crate::types::value::DataValue; use crate::types::LogicalType; use std::cell::UnsafeCell; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::fmt; struct ScalarQueryRelocator { source_arena: usize, target_arena: usize, - base: usize, } impl ScalarQueryRelocator { fn relocate(&self, reference: &mut ScalarQueryRef) { if reference.arena_id == self.source_arena { reference.arena_id = self.target_arena; - reference.pos += self.base; } } } @@ -248,6 +246,8 @@ pub trait MetaArena { fn reserve_scalar_query_ref(&mut self, reference: ScalarQueryRef); + fn scalar_query_count(&self) -> usize; + fn init_value(&self, _reference: ScalarQueryRef) -> Option<&DataValue> { None } @@ -314,6 +314,9 @@ impl MetaArena for Box { fn reserve_scalar_query_ref(&mut self, reference: ScalarQueryRef) { (**self).reserve_scalar_query_ref(reference) } + fn scalar_query_count(&self) -> usize { + (**self).scalar_query_count() + } fn init_value(&self, reference: ScalarQueryRef) -> Option<&DataValue> { (**self).init_value(reference) } @@ -349,14 +352,22 @@ impl MetaArena for Box { /// Owns the metadata and scalar-query cache for one statement execution. pub struct ExecMetaArena { parent: A, - init_values: HashMap>, + init_values: Vec>, } impl ExecMetaArena { pub(crate) fn new(parent: A) -> Self { + let mut init_values = Vec::new(); + init_values.resize_with(parent.scalar_query_count(), || None); Self { parent, - init_values: HashMap::new(), + init_values, + } + } + + fn reserve_init_value(&mut self, reference: ScalarQueryRef) { + if self.init_values.len() <= reference.pos { + self.init_values.resize_with(reference.pos + 1, || None); } } @@ -375,13 +386,23 @@ impl ExecMetaArena { } pub(crate) fn set_init_value(&mut self, reference: ScalarQueryRef, value: DataValue) { - self.init_values.insert(reference, Some(value)); + let slot = &mut self.init_values[reference.pos]; + debug_assert!( + slot.as_ref().is_none_or(|(stored, _)| *stored == reference), + "scalar query {reference} shares a slot with {:?}", + slot.as_ref().map(|(stored, _)| *stored) + ); + *slot = Some((reference, value)); } pub(crate) fn take_outer_values(&mut self) -> Vec<(ScalarQueryRef, DataValue)> { self.init_values - .extract_if(|reference, _| reference.is_outer_value) - .filter_map(|(reference, value)| value.map(|value| (reference, value))) + .iter_mut() + .filter(|slot| { + slot.as_ref() + .is_some_and(|(reference, _)| reference.is_outer_value) + }) + .filter_map(Option::take) .collect() } @@ -397,13 +418,21 @@ impl MetaArena for ExecMetaArena { self.parent.arena_id() } fn alloc_scalar_query_ref(&mut self, is_outer_value: bool) -> ScalarQueryRef { - self.parent.alloc_scalar_query_ref(is_outer_value) + let reference = self.parent.alloc_scalar_query_ref(is_outer_value); + self.reserve_init_value(reference); + reference } fn reserve_scalar_query_ref(&mut self, reference: ScalarQueryRef) { - self.parent.reserve_scalar_query_ref(reference) + self.parent.reserve_scalar_query_ref(reference); + self.reserve_init_value(reference); + } + fn scalar_query_count(&self) -> usize { + self.parent.scalar_query_count() } fn init_value(&self, reference: ScalarQueryRef) -> Option<&DataValue> { - self.init_values.get(&reference).and_then(Option::as_ref) + let (stored, value) = self.init_values.get(reference.pos)?.as_ref()?; + debug_assert_eq!(*stored, reference, "scalar query slot collision"); + Some(value) } fn table_arena_cell<'a>(&self) -> &'a TableArenaCell where @@ -624,6 +653,9 @@ impl MetaArena for TableArena { assert_eq!(reference.arena_id, 0); self.scalar_query_count = self.scalar_query_count.max(reference.pos + 1); } + fn scalar_query_count(&self) -> usize { + self.scalar_query_count + } fn alloc_column(&mut self, column: ColumnCatalog) -> ColumnRef { if let Some(column_ref) = self.find_column(&column) { return column_ref; @@ -755,7 +787,7 @@ impl<'a> PlanArena<'a> { allocated_columns_len: 0, temp_table_id: 0, arena_id: table_arena.borrow().arena_id() + 1, - scalar_query_count: 0, + scalar_query_count: table_arena.borrow().scalar_query_count, columns: Vec::new(), indexes: Vec::new(), expressions: Vec::new(), @@ -829,9 +861,8 @@ impl<'a> PlanArena<'a> { let references = ScalarQueryRelocator { source_arena: self.arena_id, target_arena: target.arena_id(), - base: target.scalar_query_count, }; - target.scalar_query_count += self.scalar_query_count; + target.scalar_query_count = self.scalar_query_count; ScalarQueryPlanRelocator { references, arena: self, @@ -985,6 +1016,9 @@ impl MetaArena for PlanArena<'_> { assert!(reference.pos < self.table_arena.borrow().scalar_query_count); } } + fn scalar_query_count(&self) -> usize { + self.scalar_query_count + } fn table_arena_cell<'a>(&self) -> &'a TableArenaCell where Self: 'a, @@ -1121,7 +1155,7 @@ impl<'a> ParamArena<'a> { expressions, parameter_count: parameter_expressions.len(), parent_end: parent.expression_end(), - scalar_query_count: 0, + scalar_query_count: parent.scalar_query_count, }) } } @@ -1144,6 +1178,9 @@ impl MetaArena for ParamArena<'_> { self.scalar_query_count = self.scalar_query_count.max(reference.pos + 1); } } + fn scalar_query_count(&self) -> usize { + self.scalar_query_count + } fn table_arena_cell<'a>(&self) -> &'a TableArenaCell where Self: 'a, @@ -1340,7 +1377,7 @@ mod tests { ScalarQueryRef { is_outer_value: false, arena_id: 1, - pos: 0 + pos: 1 } ); assert_ne!(catalog, local); @@ -1354,7 +1391,7 @@ mod tests { ScalarQueryRef { is_outer_value: false, arena_id: 2, - pos: 0 + pos: 2 } ); let mut view = ExecMetaArena::new(params); From 9d3f0a80c42ef1367dcefc516bc88f235679aa1a Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:58 +0800 Subject: [PATCH 46/62] perf(planner): bind scalar subquery results into index ranges at execution --- src/execution/dql/index_scan.rs | 6 +- src/execution/spill/codec.rs | 2 +- src/expression/range_detacher.rs | 107 +++++++++++++++++++++++++++---- src/planner/arena.rs | 30 +++++---- src/python.rs | 4 +- src/serdes/data_value.rs | 6 +- src/types/value.rs | 62 +++++++++++++++--- src/wasm.rs | 4 +- tests/slt/where_by_index.slt | 2 +- tpcc/src/backend/sqlite.rs | 4 +- 10 files changed, 181 insertions(+), 46 deletions(-) diff --git a/src/execution/dql/index_scan.rs b/src/execution/dql/index_scan.rs index f183c3b9..155243cc 100644 --- a/src/execution/dql/index_scan.rs +++ b/src/execution/dql/index_scan.rs @@ -54,11 +54,13 @@ impl<'a, T: Transaction + 'a> IndexScan<'a, T> { IndexLookup::Probe => Cow::Owned(arena.pop_runtime_probe().into()), }; - if plan_arena.has_bound_params() && range.has_parameter() { + if range.has_parameter() { let plan_arena = &*plan_arena; range .to_mut() - .bind_parameters(&|id| plan_arena.bound_param(id))?; + .bind_parameters(&|id| plan_arena.bound_param(id), &|id| { + plan_arena.init_value(id) + })?; } if let ( Some(predicate), diff --git a/src/execution/spill/codec.rs b/src/execution/spill/codec.rs index 1692b62e..4f8c98dd 100644 --- a/src/execution/spill/codec.rs +++ b/src/execution/spill/codec.rs @@ -164,7 +164,7 @@ impl SpillCodec for DataValue { fn encoded_len(&self) -> Result { use crate::types::value::Utf8Type; let payload = match self { - DataValue::Parameter { .. } => { + DataValue::Parameter { .. } | DataValue::InitValue { .. } => { return Err(DatabaseError::InvalidValue( "unbound parameter cannot be serialized".into(), )) diff --git a/src/expression/range_detacher.rs b/src/expression/range_detacher.rs index 2186a358..ac7a916e 100644 --- a/src/expression/range_detacher.rs +++ b/src/expression/range_detacher.rs @@ -18,6 +18,7 @@ use crate::expression::{BinaryOperator, ScalarExpression}; use crate::iter_ext::Itertools; use crate::planner::ExprRef; use crate::planner::MetaArena; +use crate::planner::ScalarQueryRef; use crate::types::index::IndexMetaRef; use crate::types::value::{DataValue, DataValueRef}; use crate::types::{ColumnId, LogicalType}; @@ -198,25 +199,36 @@ fn build_tree<'a, T: Borrow + 'a, I: EqRangeInput + ?Sized>( } impl Range { - pub(crate) fn bind_parameters<'p, F>(&mut self, get: &F) -> Result<(), DatabaseError> + pub(crate) fn bind_parameters<'p, F, G>( + &mut self, + params: &F, + init_values: &G, + ) -> Result<(), DatabaseError> where F: Fn(usize) -> Option<&'p DataValue>, + G: Fn(ScalarQueryRef) -> Option<&'p DataValue>, { - match self { + let has_null = match self { Self::Scope { min, max } => { + let mut has_null = false; for bound in [min, max] { if let Bound::Included(value) | Bound::Excluded(value) = bound { - value.bind_parameters(get)?; + has_null |= value.bind_parameters(params, init_values)?; } } + has_null } - Self::Eq(value) => value.bind_parameters(get)?, + Self::Eq(value) => value.bind_parameters(params, init_values)?, Self::SortedRanges(ranges) => { for range in ranges { - range.bind_parameters(get)?; + range.bind_parameters(params, init_values)?; } + false } - Self::Dummy => {} + Self::Dummy => false, + }; + if has_null { + *self = Self::Dummy; } Ok(()) } @@ -427,13 +439,13 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { left_expr .unpack_bound_col(self.arena, false) .map(|(column, _)| column), - right_expr.unpack_val(self.arena), + self.range_val(right_expr, op), ) { return self .new_range(op, col, val, false) .map(|range| range.map(DetachedPredicate::consumed)); } else if let (Some(val), Some(col)) = ( - left_expr.unpack_val(self.arena), + self.range_val(left_expr, op), right_expr .unpack_bound_col(self.arena, false) .map(|(column, _)| column), @@ -615,6 +627,19 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { }) } + fn range_val(&self, expr: ExprRef, op: BinaryOperator) -> Option { + expr.unpack_val(self.arena) + .or_else(|| match self.arena.expression(expr) { + ScalarExpression::InitValue { id, ty } if op != BinaryOperator::Spaceship => { + Some(DataValue::InitValue { + id: *id, + ty: ty.clone(), + }) + } + _ => None, + }) + } + #[allow(clippy::result_large_err)] fn merge_binary( op: BinaryOperator, @@ -2948,6 +2973,59 @@ mod test { Ok(()) } + #[test] + fn init_value_ranges_bind_and_drop_null_points() -> Result<(), DatabaseError> { + use crate::planner::{MetaArena, ScalarQueryRef, TableArena}; + + let mut arena = TableArena::default(); + let first = arena.alloc_scalar_query_ref(false); + let second = arena.alloc_scalar_query_ref(false); + let init = |id| DataValue::InitValue { + id, + ty: LogicalType::Bigint, + }; + let seven = DataValue::Int32(7); + let null = DataValue::Null; + let get = |id: ScalarQueryRef| match id.pos { + 0 => Some(&seven), + 1 => Some(&null), + _ => None, + }; + + let mut point = Range::Eq(DataValue::Tuple(vec![DataValue::Int32(1), init(first)])); + assert!(point.has_parameter()); + point.bind_parameters(&|_| None, &get)?; + assert!(!point.has_parameter()); + assert_eq!( + point, + Range::Eq(DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int64(7) + ])) + ); + + let mut null_point = Range::Eq(DataValue::Tuple(vec![DataValue::Null, init(second)])); + null_point.bind_parameters(&|_| None, &get)?; + assert_eq!(null_point, Range::Dummy); + + let mut ranges = Range::SortedRanges(vec![ + Range::Eq(init(first)), + Range::Scope { + min: Bound::Excluded(init(second)), + max: Bound::Excluded(DataValue::Null), + }, + ]); + ranges.bind_parameters(&|_| None, &get)?; + assert_eq!( + ranges, + Range::SortedRanges(vec![Range::Eq(DataValue::Int64(7)), Range::Dummy]) + ); + + let mut unset = Range::Eq(init(first)); + assert!(unset.bind_parameters(&|_| None, &|_| None).is_err()); + Ok(()) + } + #[test] fn parameterized_range_union_keeps_residual_until_values_are_bound() -> Result<(), DatabaseError> { @@ -2974,11 +3052,14 @@ mod test { let mut ranges = Range::SortedRanges(vec![left, right]); let low = DataValue::Int32(8); let high = DataValue::Int32(3); - ranges.bind_parameters(&|id| match id { - 1 => Some(&low), - 2 => Some(&high), - _ => None, - })?; + ranges.bind_parameters( + &|id| match id { + 1 => Some(&low), + 2 => Some(&high), + _ => None, + }, + &|_| None, + )?; assert!(!ranges.has_parameter()); assert_eq!( ranges, diff --git a/src/planner/arena.rs b/src/planner/arena.rs index 9cec330a..e02a6621 100644 --- a/src/planner/arena.rs +++ b/src/planner/arena.rs @@ -1143,11 +1143,14 @@ impl<'a> ParamArena<'a> { }; let id = *id; let mut value = value.clone(); - value.bind_parameters(&|id| { - params - .iter() - .find_map(|(candidate, value)| (*candidate == id).then_some(value)) - })?; + value.bind_parameters( + &|id| { + params + .iter() + .find_map(|(candidate, value)| (*candidate == id).then_some(value)) + }, + &|_| None, + )?; expressions.push((Some(id), ArenaExpr::new(ScalarExpression::Constant(value)))); } Ok(Self { @@ -1191,12 +1194,17 @@ impl MetaArena for ParamArena<'_> { true } fn bound_param(&self, id: usize) -> Option<&DataValue> { - self.expressions - .iter() - .find_map(|(slot, expr)| match (slot, &expr.expression) { - (Some(slot), ScalarExpression::Constant(value)) if *slot == id => Some(value), - _ => None, - }) + for (slot, expr) in &self.expressions { + let Some(slot) = slot else { + return None; + }; + if *slot == id { + if let ScalarExpression::Constant(value) = &expr.expression { + return Some(value); + } + } + } + None } fn column(&self, column: ColumnRef) -> &ColumnCatalog { self.parent.column(column) diff --git a/src/python.rs b/src/python.rs index 7d179fe6..3c54a28c 100644 --- a/src/python.rs +++ b/src/python.rs @@ -34,9 +34,9 @@ fn to_py_err(err: impl ToString) -> PyErr { #[allow(deprecated)] fn data_value_to_py(py: Python<'_>, value: &DataValue) -> PyResult { let object = match value { - DataValue::Parameter { id, .. } => { + DataValue::Parameter { .. } | DataValue::InitValue { .. } => { return Err(PyRuntimeError::new_err(format!( - "unbound parameter ${id} reached Python output" + "unbound parameter {value} reached Python output" ))); } DataValue::Null => py.None(), diff --git a/src/serdes/data_value.rs b/src/serdes/data_value.rs index 718897f5..efbf94ba 100644 --- a/src/serdes/data_value.rs +++ b/src/serdes/data_value.rs @@ -97,9 +97,9 @@ impl DataValue { writer: &mut W, ) -> Result<(), DatabaseError> { match self { - DataValue::Parameter { .. } => Err(DatabaseError::InvalidValue( - "unbound parameter cannot be serialized".to_string(), - )), + DataValue::Parameter { .. } | DataValue::InitValue { .. } => Err( + DatabaseError::InvalidValue("unbound parameter cannot be serialized".to_string()), + ), DataValue::Null => write_u8(writer, TAG_NULL), DataValue::Boolean(value) => { write_u8(writer, TAG_BOOLEAN)?; diff --git a/src/types/value.rs b/src/types/value.rs index 78b81ab0..bee185e8 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -15,6 +15,7 @@ use super::LogicalType; use crate::errors::DatabaseError; use crate::iter_ext::Itertools; +use crate::planner::ScalarQueryRef; use crate::storage::table_codec::{BumpBytes, NOTNULL_TAG, NULL_TAG}; use crate::types::evaluator::cast::cast_create; use crate::types::CharLengthUnits; @@ -137,6 +138,10 @@ pub enum DataValue { id: usize, ty: LogicalType, }, + InitValue { + id: ScalarQueryRef, + ty: LogicalType, + }, Null, Boolean(bool), Float32(OrderedFloat), @@ -363,6 +368,17 @@ impl PartialEq for DataValue { }, ) => left == right && left_ty == right_ty, (Parameter { .. }, _) => false, + ( + InitValue { + id: left, + ty: left_ty, + }, + InitValue { + id: right, + ty: right_ty, + }, + ) => left == right && left_ty == right_ty, + (InitValue { .. }, _) => false, (Boolean(v1), Boolean(v2)) => v1.eq(v2), (Boolean(_), _) => false, (Float32(v1), Float32(v2)) => v1.eq(v2), @@ -456,7 +472,7 @@ impl PartialOrd for DataValue { fn partial_cmp(&self, other: &Self) -> Option { use DataValue::*; match (self, other) { - (Parameter { .. }, _) => None, + (Parameter { .. }, _) | (InitValue { .. }, _) => None, (Boolean(v1), Boolean(v2)) => v1.partial_cmp(v2), (Boolean(_), _) => None, (Float32(v1), Float32(v2)) => v1.partial_cmp(v2), @@ -528,6 +544,11 @@ impl Hash for DataValue { id.hash(state); ty.hash(state); } + InitValue { id, ty } => { + 20u8.hash(state); + id.hash(state); + ty.hash(state); + } Null => 0u8.hash(state), Boolean(v) => { 1u8.hash(state); @@ -676,30 +697,46 @@ impl DataValue { digits.parse().ok() } - pub(crate) fn bind_parameters<'p, F>(&mut self, get: &F) -> Result<(), DatabaseError> + pub(crate) fn bind_parameters<'p, F, G>( + &mut self, + params: &F, + init_values: &G, + ) -> Result where F: Fn(usize) -> Option<&'p DataValue>, + G: Fn(ScalarQueryRef) -> Option<&'p DataValue>, { match self { DataValue::Parameter { id, ty } => { - let Some(input) = get(*id) else { + let Some(input) = params(*id) else { return Err(DatabaseError::parameter_not_found(format!("${id}"))); }; *self = input.clone().cast(ty)?; + Ok(false) + } + DataValue::InitValue { id, ty } => { + let Some(input) = init_values(*id) else { + return Err(DatabaseError::InvalidValue(format!( + "scalar query {id} is not initialized" + ))); + }; + *self = input.clone().cast(ty)?; + Ok(self.is_null()) } DataValue::Tuple(values) => { + let mut has_null = false; for value in values { - value.bind_parameters(get)?; + has_null |= value.bind_parameters(params, init_values)?; } + Ok(has_null) } - _ => {} + _ => Ok(false), } - Ok(()) } pub(crate) fn has_parameter(&self) -> bool { match self { - DataValue::Parameter { .. } => true, + DataValue::Parameter { .. } | DataValue::InitValue { .. } => true, DataValue::Tuple(values) => values.iter().any(Self::has_parameter), _ => false, } @@ -707,7 +744,7 @@ impl DataValue { pub(crate) fn serialized_len_hint(&self) -> usize { match self { - DataValue::Parameter { .. } => 0, + DataValue::Parameter { .. } | DataValue::InitValue { .. } => 0, DataValue::Null => 0, DataValue::Boolean(_) | DataValue::Int8(_) | DataValue::UInt8(_) => 1, DataValue::Int16(_) | DataValue::UInt16(_) => 2, @@ -992,7 +1029,7 @@ impl DataValue { #[inline] pub fn logical_type(&self) -> LogicalType { match self { - DataValue::Parameter { ty, .. } => ty.clone(), + DataValue::Parameter { ty, .. } | DataValue::InitValue { ty, .. } => ty.clone(), DataValue::Null => LogicalType::SqlNull, DataValue::Boolean(_) => LogicalType::Boolean, DataValue::Float32(_) => LogicalType::Float, @@ -1166,7 +1203,7 @@ impl DataValue { b.push_byte(not_null_tag); match self { - DataValue::Parameter { .. } => { + DataValue::Parameter { .. } | DataValue::InitValue { .. } => { return Err(DatabaseError::InvalidValue( "unbound parameter reached storage encoding".to_string(), )); @@ -1580,6 +1617,9 @@ impl DataValue { (DataValue::Parameter { id, .. }, ty) => { Ok(DataValue::Parameter { id, ty: ty.clone() }) } + (DataValue::InitValue { id, .. }, ty) => { + Ok(DataValue::InitValue { id, ty: ty.clone() }) + } (DataValue::Null, _) => Ok(DataValue::Null), (value, _) => { let evaluator = cast_create(&from, to)?; @@ -1902,6 +1942,7 @@ impl fmt::Display for DataValue { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { DataValue::Parameter { id, .. } => write!(f, "${id}")?, + DataValue::InitValue { id, .. } => write!(f, "InitValue({id})")?, DataValue::Boolean(e) => write!(f, "{e}")?, DataValue::Float32(e) => format_float_option!(f, e)?, DataValue::Float64(e) => format_float_option!(f, e)?, @@ -1979,6 +2020,7 @@ impl fmt::Debug for DataValue { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { DataValue::Parameter { ty, .. } => write!(f, "Parameter({self}: {ty})"), + DataValue::InitValue { ty, .. } => write!(f, "{self}: {ty}"), DataValue::Boolean(_) => write!(f, "Boolean({self})"), DataValue::Float32(_) => write!(f, "Float32({self})"), DataValue::Float64(_) => write!(f, "Float64({self})"), diff --git a/src/wasm.rs b/src/wasm.rs index d3da4870..c4123ea5 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -33,8 +33,8 @@ fn set_prop(object: &Object, key: &str, value: JsValue) -> Result<(), JsValue> { fn data_value_to_js(value: &DataValue) -> Result { match value { - DataValue::Parameter { id, .. } => Err(to_js_err(format!( - "unbound parameter ${id} reached WASM output" + DataValue::Parameter { .. } | DataValue::InitValue { .. } => Err(to_js_err(format!( + "unbound parameter {value} reached WASM output" ))), DataValue::Null => Ok(JsValue::NULL), DataValue::Boolean(value) => Ok(JsValue::from_bool(*value)), diff --git a/tests/slt/where_by_index.slt b/tests/slt/where_by_index.slt index d82745e5..dac9b7df 100644 --- a/tests/slt/where_by_index.slt +++ b/tests/slt/where_by_index.slt @@ -366,7 +366,7 @@ select k from tuple_bounds where w = 1 and k >= 1 and k < 3 order by k; query T explain select k from tuple_bounds where w = 1 and k = (select max(k) from tuple_bounds where w = 1); ---- -Projection [tuple_bounds.k] [Project => (Sort Option: Follow)] Filter (tuple_bounds.k = InitValue(1:)), Is Having: false [Filter => (Sort Option: Follow)] ScalarQueryInit InitValue(1:) TableScan tuple_bounds -> [tuple_bounds.w, tuple_bounds.k] [IndexScan By pk_index => [(1), (1)] Covered => (Sort Option: OrderBy: (tuple_bounds.w Asc Nulls Last, tuple_bounds.k Asc Nulls Last) ignore_prefix_len: 1)] ScalarSubquery [ScalarSubquery => (Sort Option: Follow)] Projection [Max(tuple_bounds.k)] [Project => (Sort Option: Follow)] Aggregate [Max(tuple_bounds.k)] [SimpleAggregate => (Sort Option: None)] Top 1, Sort By tuple_bounds.k Desc Nulls Last [TopK => (Sort Option: OrderBy: (tuple_bounds.k Desc Nulls Last) ignore_prefix_len: 0)] TableScan tuple_bounds -> [tuple_bounds.w, tuple_bounds.k] [IndexScan By pk_index => [(1), (1)] Covered => (Sort Option: OrderBy: (tuple_bounds.w Asc Nulls Last, tuple_bounds.k Asc Nulls Last) ignore_prefix_len: 1)] +Projection [tuple_bounds.k] [Project => (Sort Option: Follow)] ScalarQueryInit InitValue(1:) TableScan tuple_bounds -> [tuple_bounds.w, tuple_bounds.k] [IndexScan By pk_index => (1, InitValue(1:)) Covered => (Sort Option: OrderBy: (tuple_bounds.w Asc Nulls Last, tuple_bounds.k Asc Nulls Last) ignore_prefix_len: 2)] ScalarSubquery [ScalarSubquery => (Sort Option: Follow)] Projection [Max(tuple_bounds.k)] [Project => (Sort Option: Follow)] Aggregate [Max(tuple_bounds.k)] [SimpleAggregate => (Sort Option: None)] Top 1, Sort By tuple_bounds.k Desc Nulls Last [TopK => (Sort Option: OrderBy: (tuple_bounds.k Desc Nulls Last) ignore_prefix_len: 0)] TableScan tuple_bounds -> [tuple_bounds.w, tuple_bounds.k] [IndexScan By pk_index => [(1), (1)] Covered => (Sort Option: OrderBy: (tuple_bounds.w Asc Nulls Last, tuple_bounds.k Asc Nulls Last) ignore_prefix_len: 1)] query I select k from tuple_bounds where w = 1 and k = (select max(k) from tuple_bounds where w = 1); diff --git a/tpcc/src/backend/sqlite.rs b/tpcc/src/backend/sqlite.rs index 32e8afe6..a25f261a 100644 --- a/tpcc/src/backend/sqlite.rs +++ b/tpcc/src/backend/sqlite.rs @@ -209,7 +209,9 @@ fn bind_params(statement: &mut SqliteStatement<'_>, params: &[DbParam]) -> Resul fn convert_value(value: &DataValue) -> Result { Ok(match value { - DataValue::Parameter { .. } => return Err(TpccError::InvalidParameter), + DataValue::Parameter { .. } | DataValue::InitValue { .. } => { + return Err(TpccError::InvalidParameter) + } DataValue::Null => Value::Null, DataValue::Boolean(v) => Value::Integer(*v as i64), DataValue::Float32(v) => Value::Float(v.0 as f64), From 8f9fa3fa78c2615954f23e3be0b950dceb885b4a Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:34:58 +0800 Subject: [PATCH 47/62] test: run the SQLite sqllogictest corpus from the kitesql-testing submodule --- .gitmodules | 3 + Makefile | 8 + tests/kitesql-testing | 1 + tests/sqllogictest/src/bin/sqlite.rs | 317 +++++++++++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 .gitmodules create mode 160000 tests/kitesql-testing create mode 100644 tests/sqllogictest/src/bin/sqlite.rs diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..13412ef0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/kitesql-testing"] + path = tests/kitesql-testing + url = https://github.com/KipData/kitesql-testing.git diff --git a/Makefile b/Makefile index 193cdce4..beb5a053 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ CARGO ?= cargo GRCOV ?= grcov WASM_PACK ?= wasm-pack SQLLOGIC_PATH ?= tests/slt/**/*.slt +SQLITE_LOGIC_PATH ?= tests/kitesql-testing/data/sqlite/**/*.slt +SQLITE_LOGIC_ARGS ?= PYO3_PYTHON ?= /usr/bin/python3.12 TPCC_MEASURE_TIME ?= 15 TPCC_NUM_WARE ?= 1 @@ -68,6 +70,12 @@ test-wasm: test-slt: $(CARGO) run -p sqllogictest-test -- --path '$(SQLLOGIC_PATH)' +.PHONY: test-slt-sqlite + +## Run the external SQLite corpus (select1–5 excluded unless --include-select is passed). +test-slt-sqlite: + $(CARGO) run -p sqllogictest-test --bin sqlite -- --path '$(SQLITE_LOGIC_PATH)' $(SQLITE_LOGIC_ARGS) + ## Convenience target to run every suite in sequence. test-all: test test-wasm test-slt test-python diff --git a/tests/kitesql-testing b/tests/kitesql-testing new file mode 160000 index 00000000..3b5449a5 --- /dev/null +++ b/tests/kitesql-testing @@ -0,0 +1 @@ +Subproject commit 3b5449a531ca1e29caf605b4431c29a24015abe3 diff --git a/tests/sqllogictest/src/bin/sqlite.rs b/tests/sqllogictest/src/bin/sqlite.rs new file mode 100644 index 00000000..5f9419fd --- /dev/null +++ b/tests/sqllogictest/src/bin/sqlite.rs @@ -0,0 +1,317 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Runs the SQLite sqllogictest corpus converted by `kitesql-testing/scripts/convert.py`. +//! +//! Unlike the main harness, every file gets its own database, values are rendered the way the +//! SQLite reference runner does (by the column type declared in the query), queries that exceed +//! the parser recursion limit are skipped, and failures are counted instead of aborting the run. + +use clap::Parser; +use kite_sql::db::{DataBaseBuilder, Database}; +use kite_sql::errors::DatabaseError; +use kite_sql::storage::lmdb::LmdbStorage; +use kite_sql::types::value::DataValue; +use sqllogictest::{DBOutput, DefaultColumnType, QueryExpect, Record, Runner, DB}; +use sqllogictest_test::run_sql; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::Instant; +use tempfile::TempDir; + +const STACK_SIZE: usize = 64 * 1024 * 1024; +const MAX_ERROR_LINES: usize = 30; + +#[derive(Parser, Debug)] +struct Args { + #[clap(long, default_value = "tests/kitesql-testing/data/sqlite/**/*.slt")] + path: String, + /// Failures printed per file; the rest are only counted. + #[clap(long, default_value_t = 5)] + max_errors: usize, + /// Print the location of every record before running it, to find a hanging query. + #[clap(long)] + trace: bool, + /// Include select1–5, which are excluded while correlated scalar queries and join costs are investigated. + #[clap(long)] + include_select: bool, + /// Override the ANALYZE bucket count for small corpus fixtures. + #[clap(long)] + histogram_buckets: Option, + /// Override SQL parser recursion depth for deeply nested corpus queries. + #[clap(long)] + parser_recursion_limit: Option, + /// Override the HyperLogLog precision (4..=26) used by ANALYZE for NDV. + #[clap(long)] + hll_precision: Option, + /// Override the relative error (0, 1) of ANALYZE's KLL and Count-Min sketches. + #[clap(long)] + statistics_relative_error: Option, +} + +/// Database settings shared by every file in a run. +#[derive(Clone, Copy)] +struct DbOptions { + histogram_buckets: Option, + parser_recursion_limit: Option, + hll_precision: Option, + statistics_relative_error: Option, +} + +impl DbOptions { + fn builder(self, path: PathBuf) -> DataBaseBuilder { + // Always NO_SYNC, matching the reference SQLite runner's `PRAGMA synchronous=OFF`; + // otherwise every autocommit statement pays an fdatasync that dominates the run. + let mut builder = DataBaseBuilder::path(path).lmdb_no_sync(true); + if let Some(buckets) = self.histogram_buckets { + builder = builder.histogram_buckets(buckets); + } + if let Some(limit) = self.parser_recursion_limit { + builder = builder.parser_recursion_limit(limit); + } + if let Some(precision) = self.hll_precision { + builder = builder.hll_precision(precision); + } + if let Some(relative_error) = self.statistics_relative_error { + builder = builder.statistics_relative_error(relative_error); + } + builder + } +} + +struct SqliteDB { + db: Database, + /// Column types declared by the query being run. + types: Arc>>, +} + +impl DB for SqliteDB { + type Error = DatabaseError; + type ColumnType = DefaultColumnType; + + fn run(&mut self, sql: &str) -> Result, Self::Error> { + let types = self.types.lock().unwrap().clone(); + run_sql(&mut self.db, sql, |i, value| { + format_value(types.get(i), value) + }) + } +} + +/// Renders like the SQLite reference runner: `I` as `%d`, `R` as `%.3f`, NULL as `NULL`, an +/// empty string as `(empty)` and unprintable characters as `@`. +fn format_value(ty: Option<&DefaultColumnType>, value: &DataValue) -> String { + if value.is_null() { + return "NULL".to_string(); + } + match (ty, value) { + (Some(DefaultColumnType::Integer), DataValue::Boolean(b)) => (*b as i64).to_string(), + ( + Some(DefaultColumnType::Integer), + DataValue::Int8(_) + | DataValue::Int16(_) + | DataValue::Int32(_) + | DataValue::Int64(_) + | DataValue::UInt8(_) + | DataValue::UInt16(_) + | DataValue::UInt32(_) + | DataValue::UInt64(_), + ) => value.to_string(), + (Some(DefaultColumnType::Integer), _) => (as_f64(value) as i64).to_string(), + (Some(DefaultColumnType::FloatingPoint), _) => format!("{:.3}", as_f64(value)), + _ => { + let text = value.to_string(); + if text.is_empty() { + return "(empty)".to_string(); + } + text.chars() + .map(|c| if (' '..='~').contains(&c) { c } else { '@' }) + .collect() + } + } +} + +fn as_f64(value: &DataValue) -> f64 { + match value { + DataValue::Boolean(b) => *b as i64 as f64, + DataValue::Float32(f) => f.0 as f64, + DataValue::Float64(f) => f.0, + DataValue::Utf8 { value, .. } => value.trim().parse().unwrap_or(0.0), + other => other.to_string().parse().unwrap_or(0.0), + } +} + +#[derive(Default, Clone, Copy)] +struct Summary { + passed: usize, + failed: usize, + skipped: usize, + /// A statement failed, so the rest of the file was not run. + halted: bool, +} + +fn run_file( + path: &Path, + max_errors: usize, + trace: bool, + options: DbOptions, +) -> Result { + let script = std::fs::read_to_string(path).map_err(|e| e.to_string())?; + let records = + sqllogictest::parse_with_name::(&script, path.display().to_string()) + .map_err(|e| e.to_string())?; + let dir = TempDir::new().map_err(|e| e.to_string())?; + let db_path = dir.path().to_path_buf(); + let types = Arc::new(Mutex::new(Vec::new())); + let db_types = types.clone(); + let mut runner = Runner::new(move || { + let types = db_types.clone(); + let db = options.builder(db_path.clone()).build_lmdb(); + std::future::ready(db.map(|db| SqliteDB { db, types })) + }); + // The corpus was generated with this threshold, even in files that never declare it. + runner.with_hash_threshold(8); + + let mut summary = Summary::default(); + for record in records { + let is_statement = match &record { + Record::Statement { loc, .. } => { + if trace { + println!(" run {loc}"); + } + true + } + Record::Query { loc, expected, .. } => { + if trace { + println!(" run {loc}"); + } + if let QueryExpect::Results { + types: expected, .. + } = expected + { + *types.lock().unwrap() = expected.clone(); + } + false + } + Record::Halt { .. } => break, + _ => { + runner.run(record).map_err(|e| e.to_string())?; + continue; + } + }; + let Err(err) = runner.run(record) else { + summary.passed += 1; + continue; + }; + let message = err.to_string(); + if message.contains("recursion limit exceeded") { + summary.skipped += 1; + continue; + } + summary.failed += 1; + if summary.failed <= max_errors { + let lines: Vec<_> = message.lines().take(MAX_ERROR_LINES).collect(); + println!(" {}", lines.join("\n ")); + } + if is_statement { + summary.halted = true; + break; + } + } + Ok(summary) +} + +fn main() { + let args = Args::parse(); + std::env::set_current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")).unwrap(); + let files: Vec = glob::glob(&args.path) + .expect("invalid --path pattern") + .filter_map(Result::ok) + .collect(); + if files.is_empty() { + eprintln!( + "no file matches {} (run `git submodule update --init`?)", + args.path + ); + std::process::exit(2); + } + + let start = Instant::now(); + let (mut total, mut broken, mut excluded) = (Summary::default(), 0, 0); + for path in files { + if !args.include_select + && matches!( + path.file_stem().and_then(|name| name.to_str()), + Some("select1" | "select2" | "select3" | "select4" | "select5") + ) + { + println!( + "-> {}: excluded (select1–5; use --include-select to run)", + path.display() + ); + excluded += 1; + continue; + } + println!("-> {}", path.display()); + let file_start = Instant::now(); + let (file, max_errors, trace) = (path.clone(), args.max_errors, args.trace); + let options = DbOptions { + histogram_buckets: args.histogram_buckets, + parser_recursion_limit: args.parser_recursion_limit, + hll_precision: args.hll_precision, + statistics_relative_error: args.statistics_relative_error, + }; + let result = std::thread::Builder::new() + .stack_size(STACK_SIZE) + .spawn(move || run_file(&file, max_errors, trace, options)) + .expect("failed to spawn test thread") + .join(); + match result { + Ok(Ok(s)) => { + let halted = if s.halted { + ", halted on a failed statement" + } else { + "" + }; + println!( + " passed {} failed {} skipped {}{halted} ({:?})", + s.passed, + s.failed, + s.skipped, + file_start.elapsed() + ); + total.passed += s.passed; + total.failed += s.failed; + total.skipped += s.skipped; + } + Ok(Err(err)) => { + broken += 1; + println!(" error: {err}"); + } + Err(_) => { + broken += 1; + println!(" panicked"); + } + } + } + println!( + "total: passed {} failed {} skipped {}, {broken} file(s) not run, {excluded} file(s) excluded ({:?})", + total.passed, + total.failed, + total.skipped, + start.elapsed() + ); + if total.failed > 0 || broken > 0 { + std::process::exit(1); + } +} From a710e5cfc501b76f9e5e3c56432636e76fc507c1 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:46:14 +0800 Subject: [PATCH 48/62] docs: translate RFC 0001 to English --- ...greedy-join-reorder-owns-join-decisions.md | 603 ++++++++++-------- 1 file changed, 331 insertions(+), 272 deletions(-) diff --git a/rfc/0001-greedy-join-reorder-owns-join-decisions.md b/rfc/0001-greedy-join-reorder-owns-join-decisions.md index 42c4c089..6a737735 100644 --- a/rfc/0001-greedy-join-reorder-owns-join-decisions.md +++ b/rfc/0001-greedy-join-reorder-owns-join-decisions.md @@ -1,220 +1,259 @@ -# RFC 0001: 由 GreedyJoinReorder 主导 Join 决策(顺序、实现与索引选择) +# RFC 0001: GreedyJoinReorder Owns Join Decisions (Order, Implementation, and Index Choice) -- 状态:Draft -- 日期:2026-10-20 -- 涉及模块:`optimizer/rule/normalization/join_reorder.rs`(`GreedyJoinReorder`、`OrientNestedLoopJoin`)、 - `optimizer/rule/normalization/parameterized_index.rs`(`ParameterizeInnerJoin`)、 - `planner/operator`(`PhysicalOption`)、`optimizer/rule/implementation`(scan cost) +- Status: Draft +- Date: 2026-10-20 +- Modules: `optimizer/rule/normalization/join_reorder.rs` (`GreedyJoinReorder`, `OrientNestedLoopJoin`), + `optimizer/rule/normalization/parameterized_index.rs` (`ParameterizeInnerJoin`), + `planner/operator` (`PhysicalOption`), `optimizer/rule/implementation` (scan cost) -## 1. 背景 +## 1. Background -### 1.1 Join 决策分散在四处 +### 1.1 Join decisions are spread across four places -| 阶段 | 规则 | 决定什么 | 依据 | +| Stage | Rule | Decides | Based on | |---|---|---|---| -| implementation | `IndexScanImplementation` 等 | 每个 scan 的访问路径 | 静态 range 代价,不知道连接键 | -| after_batches 1 | `GreedyJoinReorder` | 连接顺序、hash 的 build 方向 | 只按 hash 估算 | -| after_batches 2 | `ParameterizeInnerJoin` | 是否改写为索引 probe、用哪个索引 | 能 probe 就改写,取 `index_infos` 中第一个可用的 PK/Unique | -| after_batches 5 | `OrientNestedLoopJoin` | Cross NLJ 的内外侧 | `estimated_work` | +| implementation | `IndexScanImplementation`, etc. | Access path of each scan | Static range cost; unaware of join keys | +| after_batches 1 | `GreedyJoinReorder` | Join order and hash build side | Hash estimates only | +| after_batches 2 | `ParameterizeInnerJoin` | Whether to rewrite to an index probe, and which index | Rewrites whenever a probe is possible, using the first usable PK/Unique in `index_infos` | +| after_batches 5 | `OrientNestedLoopJoin` | Outer/inner side of a Cross NLJ | `estimated_work` | -Greedy 处于被动位置:它拿到的是 scan 已经选好的计划,只调整 join 的位置,而且按"全部是 hash"排序; -排好之后,`ParameterizeInnerJoin` 不经代价比较就把部分 join 改成 probe,`OrientNestedLoopJoin` -再独立调整 NLJ 方向。连接顺序是按一种实现的代价定的,最终执行的却可能是另一种实现。 +Greedy is passive: it receives a plan whose scans are already chosen, only moves joins around, and orders them +as if every join were a hash join. Afterwards, `ParameterizeInnerJoin` turns some joins into probes without any +cost comparison, and `OrientNestedLoopJoin` adjusts NLJ direction independently. The join order is chosen for +one implementation, while the one that finally executes may be another. -### 1.2 后果 +### 1.2 Consequences -- 连接顺序没有考虑 probe:一个"小外侧 ⋈ 大唯一内侧"的 pair,按 hash 估算很贵,会被推迟; - 而按 probe 它本应最先合并。 -- probe 是否更优从未被比较:大外侧 ⋈ 小唯一内侧时,probe 的 seek 次数远多于 hash 的全量读取, - 但仍会被改写成 probe。 -- 索引选择与连接无关:probe 用的是第一个可用索引;Greedy 的 `index_statistics` 只读 implementation - 选中的那个索引,被连接键绑定的其它索引提供的唯一性和 NDV 被忽略。 -- 代价单位不统一,且 probe 单价被低估、HashJoin / NLJ 漏算部分代价(见第 4 节), - 即使让 Greedy 比较,也比较不出正确结果。 +- Join order ignores probes: a "small outer ⋈ large unique inner" pair looks expensive as a hash join and is + postponed, although as a probe it should be merged first. +- Whether a probe is better is never compared: for a large outer ⋈ small unique inner, the probe performs far + more seeks than a full hash read, yet it is still rewritten into a probe. +- Index choice is independent of the join: the probe uses the first usable index, and Greedy's + `index_statistics` only reads the index chosen during implementation, ignoring the uniqueness and NDV that + other indexes bound by the join keys would provide. +- Cost units are inconsistent, the per-probe cost is underestimated, and HashJoin / NLJ miss part of their cost + (see Section 4), so even if Greedy compared the options it could not get the right answer. -## 2. 目标与非目标 +## 2. Goals and Non-goals -目标: +Goals: -- `GreedyJoinReorder` 成为 Greedy 区域内 join 的唯一决策者:在合并每一个 pair 时,同时决定 - 连接顺序、join 实现(Hash / IndexProbe / NestLoop)和方向,以及区域内每个叶子 scan 使用的索引。 -- 下游规则 `ParameterizeInnerJoin` 对 Greedy 已决策的 join 只负责执行和校验,不再自行判断; - 原有逻辑仅作为未经 Greedy 处理的 join 的兜底。`OrientNestedLoopJoin` 并入 Greedy 后删除。 -- 提供支撑上述决策的统一代价模型。 +- `GreedyJoinReorder` becomes the only decision maker for joins inside a Greedy region: when merging each pair, + it decides at once the join order, the join implementation (Hash / IndexProbe / NestLoop) and direction, and + the index used by each leaf scan in the region. +- The downstream rule `ParameterizeInnerJoin` only executes and validates joins already decided by Greedy and no + longer makes its own choice; its original logic remains only as a fallback for joins Greedy did not handle. + `OrientNestedLoopJoin` is merged into Greedy and removed. +- Provide a unified cost model that supports these decisions. -非目标: +Non-goals: -- 不改变 optimizer pipeline 的 batch 顺序(Greedy 依赖 implementation 产出的 `row_count`)。 -- 不支持非唯一索引(Normal / Composite)的 inner join probe,见第 8 节。 -- 不建模 I/O。 +- Do not change the batch order of the optimizer pipeline (Greedy relies on the `row_count` produced by + implementation). +- Do not support inner join probes on non-unique indexes (Normal / Composite); see Section 8. +- Do not model I/O. -## 3. 设计:Greedy 的决策权 +## 3. Design: Greedy Owns the Decisions -### 3.1 职责划分 +### 3.1 Responsibilities -| 规则 | 现在 | 改造后 | +| Rule | Today | After | |---|---|---| -| scan impl rule | 选访问路径 | 只提供静态最优的默认值;Greedy 区域内的叶子以 Greedy 的决策为准 | -| `GreedyJoinReorder` | 只排顺序,按 hash 估算 | 每个 pair 决定顺序 + 实现 + 方向 + probe 索引;probe 直接改写,其余标记 `join_decided` | -| `ParameterizeInnerJoin` | 能 probe 就改写 | `join_decided`:不改写;否则原逻辑兜底 | -| `OrientNestedLoopJoin` | 调整所有 Cross NLJ 方向 | 删除,职责并入 Greedy(3.10) | - -Greedy 区域内每个叶子的访问路径都由 Greedy 决定,scan impl rule 的结果只是 Greedy 的输入之一: - -- 叶子作为 probe 内侧:访问路径是 Greedy 选中的 probe 索引, - Greedy 改写时替换掉 scan 原来的索引与 range。不存在"probe 更优但 scan 用了别的索引"的情况。 -- 叶子作为 hash / NLJ 的输入:访问路径不使用连接键,候选就是 scan impl rule 用静态 range 比较过的那些, - 其最低代价方案也就是此时的最优方案;Greedy 以它的 `work` 作为该形态的输入代价,选择 Hash 即确认采用它。 - - 例:`a JOIN b ON a.id = b.a_id WHERE b.status = 1`,b 上有 `idx(a_id)`、`idx(status)`。hash 时 b 被独立读出, - 扫描时拿不到 a 的值,`idx(a_id)` 无法限定范围,可用的只有 `status = 1` 对应的 range 或全表扫描; - 只有 probe 才能把 `a.id` 的值传给 b 的 scan,在 `idx(a_id)` 上 seek。 -- 不在 Greedy 区域内的 scan,以及未经 Greedy 处理的 join 下的 scan,仍由 scan impl rule 自己决定。 - -Greedy 在 implementation 之后运行(依赖其产出的 `row_count`),所以 scan 节点上先有一个静态最优的默认值, -Greedy 的决策在其上生效;对同一个叶子,Greedy 的决策优先级最高。 - -### 3.2 决策流程 - -1. 收集区域内的叶子和连接边(现有逻辑)。 -2. 对每个 pair,用共用判定函数在叶子一侧找可 probe 的索引(3.3)。 -3. 搜索为左深:从行数最少(相同时读取代价最低)的输入开始,每轮只评估"当前结果 ⋈ 某个剩余输入",列出可行形态并打分,取 pair 内最优形态(3.4)。 - 剩余输入在合并前始终是叶子,保留被 probe 的机会。 -4. 在这些 pair 中按 `JoinCandidate` 的顺序选出本轮合并的输入;当前结果估算不超过 1 行时,cross 不降优先级。 -5. 按选中的形态构造 join:确定左右(probe 内侧、NLJ 内侧放右边,hash build 放左边);probe 直接改写为 MarkApply,其余标记 `join_decided`(3.5)。 -6. 合并结果作为新输入参与下一轮,其 `InputCost` 由所选形态的代价函数给出。 - -### 3.3 Source 与 probe 候选 - -Greedy 根据 source 的列信息与其上存在的索引,自己判定可用的 probe 索引,不依赖 impl rule 保留候选: +| scan impl rule | Chooses the access path | Only provides a statically optimal default; leaves inside a Greedy region follow Greedy's decision | +| `GreedyJoinReorder` | Orders only, using hash estimates | Decides order + implementation + direction + probe index for each pair; rewrites probes directly and marks the rest `join_decided` | +| `ParameterizeInnerJoin` | Rewrites whenever it can probe | `join_decided`: no rewrite; otherwise the original logic is the fallback | +| `OrientNestedLoopJoin` | Orients every Cross NLJ | Removed; its responsibility moves into Greedy (3.10) | + +The access path of every leaf inside a Greedy region is decided by Greedy; the scan impl rule's result is only +one of Greedy's inputs: + +- Leaf as a probe inner side: the access path is the probe index chosen by Greedy, which replaces the scan's + original index and range during the rewrite. There is no case where "the probe is better but the scan uses + another index". +- Leaf as a hash / NLJ input: the access path does not use the join keys, so the candidates are exactly those + the scan impl rule already compared with static ranges, and its cheapest plan is also the best one here. + Greedy uses its `work` as the input cost of this shape; choosing Hash confirms it. + - Example: `a JOIN b ON a.id = b.a_id WHERE b.status = 1`, where b has `idx(a_id)` and `idx(status)`. Under + hash, b is read independently and its scan cannot see a's values, so `idx(a_id)` cannot restrict the range; + only the range for `status = 1` or a full scan is usable. Only a probe can pass `a.id` to b's scan and seek + on `idx(a_id)`. +- Scans outside a Greedy region, and scans under joins Greedy did not handle, are still decided by the scan impl + rule. + +Greedy runs after implementation (it relies on the `row_count` produced there), so each scan node already has a +statically optimal default and Greedy's decision is applied on top of it; for the same leaf, Greedy's decision has +the highest priority. + +### 3.2 Decision flow + +1. Collect the leaves and join edges in the region (existing logic). +2. For each pair, use the shared predicate function to find probe-able indexes on the leaf side (3.3). +3. The search is left-deep: start from the input with the fewest rows (ties broken by the lowest read cost). + Each round only evaluates "current result ⋈ one remaining input", lists the feasible shapes, scores them, + and keeps the best shape within the pair (3.4). Remaining inputs are always leaves before they are merged, + so they keep the chance to be probed. +4. Among these pairs, pick the input to merge this round by `JoinCandidate` order; when the current result is + estimated at no more than 1 row, cross joins are not deprioritized. +5. Build the join with the chosen shape: decide left and right (the probe inner side and NLJ inner side go to the + right, the hash build side to the left); rewrite probes directly into MarkApply and mark the rest + `join_decided` (3.5). +6. The merged result becomes a new input for the next round; its `InputCost` comes from the cost function of the + chosen shape. + +### 3.3 Sources and probe candidates + +Greedy decides the usable probe indexes itself from the source's column information and the indexes on it, +without relying on the impl rule to keep candidates: ```rust struct ProbeIndex { - position: usize, // scan.index_infos 中的位置 - probe: Vec, // 外侧值与常量,按索引列顺序 - lookup_work: usize, // 单次 probe 代价,见 4.3 + position: usize, // position in scan.index_infos + probe: Vec, // outer values and constants, in index column order + lookup_work: usize, // cost of one probe, see 4.3 } ``` -- 适用的叶子:`Filter*` + 没有 limit 的 `TableScan`。已合并的 join 结果不能作为 probe 内侧, - 所以 probe 只出现在"已合并结果 ⋈ 叶子"的形态,与 Greedy 左深扩展的方式吻合。 -- 索引的每一列要么出现在某条连接边上(属于该叶子一侧),要么被叶子 filter 的常量等值绑定; - 两者都满足时按常量处理,减少对连接键的要求。 -- 每个 pair 直接调用判定函数,取 `lookup_work` 最低的 PK/Unique(相同时按索引顺序);不缓存、不截断,候选数由表上的索引数决定。 -- probe 的输出行数按唯一性收紧为 `min(估算行数, 外侧行数)`:被 probe 的索引被连接键绑定,唯一性已知。 +- Eligible leaves: `Filter*` + a `TableScan` without a limit. A merged join result cannot be a probe inner side, + so probes only appear in the "merged result ⋈ leaf" shape, which matches how Greedy extends left-deep. +- Every index column either appears on some join edge (on this leaf's side) or is bound to a constant by an + equality in the leaf's filter; when both hold, it is treated as a constant to reduce the join-key requirement. +- Each pair calls the predicate function directly and takes the PK/Unique with the lowest `lookup_work` (ties + broken by index order); nothing is cached or truncated, and the number of candidates is bounded by the indexes + on the table. +- The output rows of a probe are tightened by uniqueness to `min(estimated rows, outer rows)`: the probed index is + bound by the join keys, so its uniqueness is known. -### 3.4 Pair 内的实现形态 +### 3.4 Implementation shapes within a pair -对一个 pair (L, R),可行形态如下: +For a pair (L, R), the feasible shapes are: -| 形态 | 条件 | +| Shape | Condition | |---|---| -| Hash(build = L) / Hash(build = R) | pair 有连接键 | -| Probe(inner = R) / Probe(inner = L) | 对应一侧是叶子,且判定函数能用该 pair 的连接键找到索引 | -| NestLoop(outer = L) / NestLoop(outer = R) | pair 没有连接键(cross) | +| Hash(build = L) / Hash(build = R) | The pair has join keys | +| Probe(inner = R) / Probe(inner = L) | That side is a leaf, and the predicate function finds an index using the pair's join keys | +| NestLoop(outer = L) / NestLoop(outer = R) | The pair has no join keys (cross) | -每种形态按第 4 节的代价模型打分,pair 内取最低分。`JoinCandidate` 的比较顺序(`derive(Ord)`): +Each shape is scored with the cost model in Section 4, and the lowest score within the pair wins. `JoinCandidate` +compares in this order (`derive(Ord)`): ``` cross_join → score → rows → left → right → swapped → probe ``` -- pair 内分数相同时 probe 优先;无统计时按默认行数计价,见 3.9。 +- When scores tie within a pair, the probe wins; without statistics, the default row count is used (see 3.9). -### 3.5 决策下发:`join_decided` +### 3.5 Passing the decision down: `join_decided` ```rust -// PhysicalOption 新增;与 startup_work 一样是 pass 内的标注,不参与 PartialEq / Hash,也不序列化 +// New in PhysicalOption; like startup_work, it is an in-pass annotation that does not take part in +// PartialEq / Hash and is not serialized join_decided: bool, ``` -Greedy 选中 probe 时直接调用与 `ParameterizeInnerJoin` 共用的改写函数生成 MarkApply,判定与执行是同一份代码, -无需再校验;Hash / NestLoop 的 join 标记 `join_decided`,表达"按这个形态执行,不要再判断"。 +When Greedy chooses a probe, it directly calls the rewrite function shared with `ParameterizeInnerJoin` to build +the MarkApply, so the decision and the execution use the same code and need no further validation. Hash / +NestLoop joins are marked `join_decided`, meaning "execute in this shape and do not decide again". -### 3.6 下游规则的执行与兜底 +### 3.6 Execution and fallback in downstream rules - `ParameterizeInnerJoin` - - `join_decided`:不改写。 - - 否则:Greedy 没处理过的 join(ON 带 filter、`limit_pushed` 等),走原逻辑兜底, - 用同一判定函数选索引、用 loader 计算 `lookup_work`。 -- NestLoop 方向由 Greedy 决定,下游不再有调整方向的规则(3.10)。 -- 被替换的静态 range 不会丢失过滤语义:Filter 仍在 scan 上方,`EliminateIndexFilter` 在 Parameterize 之后运行。 -- explain 只展示选中的索引(现有行为),不展示 `join_decided` 与 `lookup_work`。 - -### 3.7 共用判定函数 - -从 `parameterize` 中拆出纯判定函数(不修改 plan),Greedy 生成候选与 `ParameterizeInnerJoin` 校验都调用它: - -- 索引列都在 scan 的 `columns` 中; -- 连接值表达式类型与索引列类型完全一致(`value.return_type == column.datatype()`); -- 至少用到一个连接键(`uses_join_key`),只靠常量凑满索引不算 probe。 - -Greedy 拥有决策权的前提是它的判断与执行一致;若两边各写一份,可能出现 Greedy 按 probe 定了顺序、 -执行时却退回 hash,结果比不考虑 probe 更差。 - -### 3.8 索引覆盖:probe 与 scan 用同一口径 - -现状:覆盖信息(`covered_deserializers` / `cover_mapping`)只在 `PushPredicateIntoScan` 中计算, -且只针对能从 filter 拆出静态 range 的索引。只被连接键用到的索引覆盖信息始终为 `None`; -`parameterize` 改写为 probe 时只设置 `lookup = Probe`、`residual_predicate = None`,不重算覆盖。 -执行器(`index_scan.rs`)会按覆盖信息决定是否回表,因此一个实际覆盖了 scan 所需列的二级 Unique 索引, -probe 时仍会回表,代价按 `seeks = 2` 计。结果是 probe 与 scan 选中的覆盖索引比较时被系统性高估。 - -修正: - -1. 把覆盖计算从 `PushPredicateIntoScan` 拆成纯函数 - `index_covering(scan, index) -> Option<(deserializers, mapping)>`,只依赖 scan 的 `columns` 与索引列,与 range 无关。 -2. Greedy 生成候选时用它决定 `seeks`(PK 或覆盖 Unique = 1,否则 = 2);scan cost 中非覆盖 ×2 的判断使用同一函数。 -3. probe 改写(Greedy 与兜底路径共用),同时写入 `covered_deserializers` / `cover_mapping`, - 使执行器真正跳过回表。 - -这样 Hash 形态用 scan 已选路径(可能是覆盖索引)的 `work`,Probe 形态枚举 `scan.index_infos` 中全部候选并按真实覆盖计价, -"scan 选了覆盖索引,但 probe 更优"的情况在 pair 内比较时直接体现,由 Greedy 选出后覆盖 scan 的选择。 - -### 3.9 无统计信息时的默认行数 - -缺少统计信息的输入按 `DEFAULT_TABLE_ROWS`(1000)行计价,probe 所需的 N 缺失时也取该值,代价比较照常进行,不再有启发式分支。 -PK/Unique 等值的静态 range 在无统计时也有确切行数,scan impl rule 据此选它并给出行数。 -由默认行数合并出的 join 仍记为未知行数,避免假估算泄漏给后续规则。 - -### 3.10 融合 `OrientNestedLoopJoin` - -`OrientNestedLoopJoin` 只匹配 `Cross` + `on = None` + `limit_pushed = false` + `NestLoopJoin`, -用 `left_work + left_rows × right_work` 与对侧比较决定方向,与 `JoinImpl::NestLoop` 的公式本质相同。 -它处理三类 join,全部并入 Greedy 后删除该规则及 `Orient Nested Loop Join` batch: - -| 来源 | 现在 | 融合后 | + - `join_decided`: no rewrite. + - Otherwise: for joins Greedy did not handle (ON with a filter, `limit_pushed`, etc.), the original logic is the + fallback; it chooses the index with the same predicate function and computes `lookup_work` with the loader. +- NestLoop direction is decided by Greedy; no downstream rule adjusts direction any more (3.10). +- The replaced static range does not lose its filtering semantics: the Filter stays above the scan, and + `EliminateIndexFilter` runs after Parameterize. +- explain only shows the chosen index (existing behavior), not `join_decided` or `lookup_work`. + +### 3.7 Shared predicate function + +A pure predicate function (which does not modify the plan) is extracted from `parameterize`; both Greedy's +candidate generation and `ParameterizeInnerJoin`'s validation call it: + +- Every index column is in the scan's `columns`; +- The join value expression type exactly matches the index column type (`value.return_type == column.datatype()`); +- At least one join key is used (`uses_join_key`); filling the index with constants alone is not a probe. + +Greedy can only own the decision if its judgment matches execution. If each side kept its own copy, Greedy could +order joins for a probe while execution falls back to hash, which is worse than ignoring probes altogether. + +### 3.8 Index coverage: probes and scans use the same rule + +Today: coverage information (`covered_deserializers` / `cover_mapping`) is only computed in +`PushPredicateIntoScan`, and only for indexes that a static range can be extracted from the filter. Coverage for an +index used only by join keys is always `None`; when `parameterize` rewrites to a probe it only sets +`lookup = Probe` and `residual_predicate = None`, without recomputing coverage. The executor (`index_scan.rs`) +decides whether to look up the table by coverage, so a secondary Unique index that actually covers the columns the +scan needs still looks up the table during a probe, and its cost is counted with `seeks = 2`. As a result, probes are +systematically overestimated when compared with a covering index chosen by the scan. + +Fix: + +1. Extract the coverage computation from `PushPredicateIntoScan` into a pure function + `index_covering(scan, index) -> Option<(deserializers, mapping)>`, which only depends on the scan's `columns` + and the index columns, not on the range. +2. Greedy uses it to decide `seeks` when generating candidates (PK or covering Unique = 1, otherwise = 2); the + non-covering ×2 rule in scan cost uses the same function. +3. The probe rewrite (shared by Greedy and the fallback path) also writes `covered_deserializers` / + `cover_mapping`, so the executor really skips the table lookup. + +This way, the Hash shape uses the `work` of the path the scan already chose (possibly a covering index), and the +Probe shape enumerates every candidate in `scan.index_infos` and prices it by its real coverage. The case "the scan +chose a covering index, but a probe is better" shows up directly in the in-pair comparison, and Greedy's choice +overrides the scan's. + +### 3.9 Default row count without statistics + +An input without statistics is priced at `DEFAULT_TABLE_ROWS` (1000) rows, and the N needed by a probe also falls +back to this value when missing; cost comparison proceeds as usual, with no heuristic branch. A static range on a +PK/Unique equality has an exact row count even without statistics, and the scan impl rule chooses it and reports +that row count. A join merged from default row counts is still recorded as having unknown rows, so the made-up +estimate does not leak to later rules. + +### 3.10 Merging `OrientNestedLoopJoin` + +`OrientNestedLoopJoin` only matches `Cross` + `on = None` + `limit_pushed = false` + `NestLoopJoin`, and decides the +direction by comparing `left_work + left_rows × right_work` with the opposite side, which is essentially the same +formula as `JoinImpl::NestLoop`. It handles three kinds of joins; all of them move into Greedy, and the rule and its +`Orient Nested Loop Join` batch are removed: + +| Source | Today | After | |---|---|---| -| Greedy 区域内无连接键的 pair | Greedy 构造时 `swapped: connected && ...`,cross 永不交换,方向靠 Orient 修正 | Greedy 比较 `NestLoop(outer = L / R)` | -| 根为 `Cross` 的区域(如 `FROM a, b`,或位于 outer join / 带 filter 的 inner join 之下) | Greedy 要求根为 `Inner`(`apply` 入口),不处理 | Greedy 触发条件放宽为 `can_expand(plan)`;`can_expand` 本就接受 Cross | -| `FORCE_NEST_LOOP_JOIN` 下的 join | `can_expand` 排除,不进区域;Orient 只调整其中 cross 的方向 | 进入区域(只与同样带该标记的 join 组成区域),照常重排,但每个 pair 只比较 NestLoop | +| A pair without join keys inside a Greedy region | Greedy builds it with `swapped: connected && ...`, so cross joins are never swapped and Orient fixes the direction | Greedy compares `NestLoop(outer = L / R)` | +| A region rooted at `Cross` (e.g. `FROM a, b`, or under an outer join / an inner join with a filter) | Greedy requires an `Inner` root (`apply` entry) and skips it | Greedy's trigger is relaxed to `can_expand(plan)`; `can_expand` already accepts Cross | +| Joins under `FORCE_NEST_LOOP_JOIN` | Excluded by `can_expand`, so not in a region; Orient only orients the cross joins among them | They enter a region (only with joins carrying the same hint) and are reordered as usual, but each pair only compares NestLoop | -该 hint 的语义是"用 NLJ、不用 hash",不承诺按书写顺序执行。 +The hint means "use NLJ, not hash"; it does not promise execution in the written order. -时序代价:Orient 原本在 `Parameterize Mark Apply`、`Eliminate Index Filter`、`Materialize Mark Apply` 之后运行, -看到的是改写后的 `estimated_work`。并入 Greedy 后,NLJ 内侧子树若含子查询类 MarkApply,方向按其改写前的估算决定; -物化正好降低重跑内侧的代价。该偏差先接受(Greedy 中 cross 本就排在最后,此类情况少见), -与 `rescan_work` 一并作为未来工作。改写对这类 MarkApply `estimated_work` 的实际影响尚未核实。 +Ordering cost: Orient used to run after `Parameterize Mark Apply`, `Eliminate Index Filter`, and +`Materialize Mark Apply`, so it saw the rewritten `estimated_work`. After moving into Greedy, if an NLJ inner subtree +contains a subquery-style MarkApply, the direction is decided by its pre-rewrite estimate, while materialization +actually lowers the cost of re-running the inner side. This deviation is accepted for now (cross joins are already +ordered last in Greedy, so such cases are rare) and is left as future work together with `rescan_work`. The actual +effect of the rewrite on the `estimated_work` of such MarkApply nodes has not been verified yet. -## 4. 支撑:代价模型 +## 4. Foundation: Cost Model -Greedy 的决策只有在代价可比时才有意义,本节是决策权改造的前提。 +Greedy's decisions only make sense when costs are comparable; this section is the prerequisite for moving the +decisions to Greedy. -### 4.1 现状 +### 4.1 Today -| 位置 | 单位 | 是否考虑行宽 | +| Location | Unit | Accounts for row width | |---|---|---| -| scan impl rule 的 `cost` | 行数(非覆盖索引 ×2,排序 hint 有折扣) | 否 | -| `calculate_work` / `calculate_startup_work` | CPU 单位:`CPU_ROW_WORK = 4`,`CPU_OPERATOR_WORK = 1` | 否 | -| `GreedyJoinReorder` | 值个数(行数 × 列数) | 是 | +| scan impl rule `cost` | Rows (non-covering index ×2, discount for sort hints) | No | +| `calculate_work` / `calculate_startup_work` | CPU units: `CPU_ROW_WORK = 4`, `CPU_OPERATOR_WORK = 1` | No | +| `GreedyJoinReorder` | Number of values (rows × columns) | Yes | -- MarkApply 每次 lookup 只算 `startup_work(scan) + OP = 1`,比顺序扫一行(4)还便宜。 -- HashJoin 走默认分支,漏算 probe 侧的全量读取。 -- NLJ 只有 Cross 单独处理且没计逐对比较;非 Cross 的 NLJ 完全没计"每个外侧行重跑内侧"。 +- Each MarkApply lookup only counts `startup_work(scan) + OP = 1`, cheaper than scanning one row sequentially (4). +- HashJoin takes the default branch and misses the full read of the probe side. +- Among NLJs only Cross is handled separately, and it does not count per-pair comparisons; non-Cross NLJs do not + count "re-run the inner side for each outer row" at all. -### 4.2 统一的行代价与 join 代价函数 +### 4.2 Unified row cost and join cost functions ```rust -// width 为列数;每行有固定开销,零列行不会被估成 0 +// width is the number of columns; each row has a fixed overhead, so zero-column rows are not estimated as 0 fn rows_work(rows: usize, width: usize) -> usize { rows * (CPU_ROW_WORK + width * CPU_OPERATOR_WORK) } @@ -227,142 +266,162 @@ impl JoinImpl { ``` ``` -Hash = build.work // build 侧完整读一遍 - + probe.work // probe 侧完整读一遍 - + rows_work(build) + build.rows × HASH_BUILD_WORK // build 侧整行存入哈希表;哈希、分配与释放按 LMDB 实测校准 - + probe.rows × OP // probe 侧每行查一次哈希表 - + rows_work(out) // 输出满足条件的行 -IndexProbe = outer.work // 外侧完整读一遍 - + outer.rows × lookup_work // 每个外侧行,去内侧索引 seek 一次;内侧不全量读 - + rows_work(out) // 输出满足条件的行 -NestLoop = outer.work // 外侧完整读一遍 - + outer.rows × (inner.work // 每个外侧行,内侧从头重跑一遍 - + inner.rows × OP) // 每个外侧行,和内侧每一行比较一次 - + rows_work(out) // 输出满足条件的行 - (+ inner.work,仅 Full join:最后再扫一遍内侧输出未匹配行) +Hash = build.work // read the build side once + + probe.work // read the probe side once + + rows_work(build) + build.rows × HASH_BUILD_WORK // store whole build rows in the hash table; hashing, allocation, and freeing calibrated on LMDB + + probe.rows × OP // one hash table lookup per probe row + + rows_work(out) // emit matching rows +IndexProbe = outer.work // read the outer side once + + outer.rows × lookup_work // one inner index seek per outer row; the inner side is not fully read + + rows_work(out) // emit matching rows +NestLoop = outer.work // read the outer side once + + outer.rows × (inner.work // re-run the inner side for each outer row + + inner.rows × OP) // compare each outer row with every inner row + + rows_work(out) // emit matching rows + (+ inner.work, Full join only: scan the inner side once more for unmatched rows) ``` -- 代价函数只接收统计量,不依赖 `&LogicalPlan`,因为 Greedy 评估的是尚未构造的 join。 - Greedy 与 `calculate_work` 共用它。 -- `calculate_work`、`calculate_startup_work` 改用 `rows_work`; - scan impl rule 不变(同一 scan 的候选列数相同,换单位不影响排序)。 -- `rescan_work` 暂不引入,视为等于 `work`(例外见 6.2)。 +- The cost functions only take statistics and do not depend on `&LogicalPlan`, because Greedy evaluates joins that + have not been built yet. Greedy and `calculate_work` share them. +- `calculate_work` and `calculate_startup_work` switch to `rows_work`; the scan impl rule is unchanged (candidates of + the same scan have the same number of columns, so changing the unit does not change their order). +- `rescan_work` is not introduced yet and is treated as equal to `work` (exception in 6.2). -### 4.3 单次 probe 代价 `lookup_work` +### 4.3 Cost of one probe: `lookup_work` -Hash 的 probe 是内存哈希查找(约 `OP`),前提是两侧全量读;MarkApply 的 probe 是存储层 seek, -每次都要重建内侧 executor,单价高,好处是内侧不全量读。 +A Hash probe is an in-memory hash lookup (about `OP`), but requires reading both sides fully; a MarkApply probe is a +storage-level seek that rebuilds the inner executor every time, so it costs more per probe, but the inner side is not +read fully. ``` lookup_work = PROBE_RESET_WORK(= CPU_ROW_WORK) - + seeks × ⌈log2(N)⌉ × OP // seeks: PK/覆盖 Unique = 1,非覆盖 Unique = 2 + + seeks × ⌈log2(N)⌉ × OP // seeks: PK/covering Unique = 1, non-covering Unique = 2 + rows_per_lookup × rows_work(1, inner_width) ``` -- N 取该索引直方图的 `values_len`,没有时取 `DEFAULT_TABLE_ROWS`。内侧 Filter 只按行计。 -- 示例:内侧 100 万行、宽度 2,扫一行 = 6,查一次 = 30;外侧约小于内侧 1/4 时 probe 更优。 -- 存于 `PhysicalOption.lookup_work: Option`,由判定函数用 loader 计算,probe 改写时写入 - (Greedy 与兜底路径共用)。semi / anti 等子查询类 MarkApply 也改用该公式。 +- N is the `values_len` of the index histogram, or `DEFAULT_TABLE_ROWS` when absent. An inner Filter is only counted + per row. +- Example: with 1M inner rows of width 2, scanning one row = 6 and one lookup = 30; the probe wins when the outer + side is smaller than roughly 1/4 of the inner side. +- Stored in `PhysicalOption.lookup_work: Option`, computed by the predicate function with the loader and + written during the probe rewrite (shared by Greedy and the fallback path). Subquery-style MarkApply such as semi / + anti also switch to this formula. -### 4.4 Pair 分数:本步处理量 +### 4.4 Pair score: work done in this step ``` score(Hash / NestLoop) = work(join) − left.work − right.work score(Probe) = outer.rows × lookup_work + rows_work(out) − inner_leaf.work ``` -- 分数只计这一步的中间处理量。全 hash 时每张叶子无论顺序都读一遍,计入只会让大表被推迟。 - 现有 Greedy 公式(`product × (left_width + right_width) + min(left_size, right_size)`)已是这个思路。 -- Probe 的 `− inner_leaf.work` 是对基准(每张叶子都完整读一遍)的修正,不是处理量; - 内侧大、外侧小时分数为负,Greedy 会优先合并。分数用 `i128`,代码中需注释该项含义。 +- The score only counts the intermediate work of this step. With all-hash joins each leaf is read once regardless of + order, so counting the reads would only postpone large tables. The existing Greedy formula + (`product × (left_width + right_width) + min(left_size, right_size)`) already follows this idea. +- The `− inner_leaf.work` term of Probe is a correction against the baseline (every leaf is read once), not work + done; when the inner side is large and the outer side small, the score is negative and Greedy merges it first. + Scores use `i128`, and the code must comment on the meaning of this term. -## 5. Pipeline 与 `join_decided` 的有效性 +## 5. Pipeline and the Validity of `join_decided` -`db.rs` 中 after_batches 的顺序: +Order of after_batches in `db.rs`: -1. `Greedy Join Reorder`(once_topdown) -2. `Parameterize Mark Apply`:`ParameterizeMarkApply` + `ParameterizeInnerJoin`(fix_point_topdown(1)) +1. `Greedy Join Reorder` (once_topdown) +2. `Parameterize Mark Apply`: `ParameterizeMarkApply` + `ParameterizeInnerJoin` (fix_point_topdown(1)) 3. `Eliminate Index Filter` 4. `Materialize Mark Apply` -5. `Orient Nested Loop Join`(本 RFC 删除) -6. …… +5. `Orient Nested Loop Join` (removed by this RFC) +6. ... -Greedy 与 Parameterize 之间没有其它 batch;同一 batch 的 `ParameterizeMarkApply` 只匹配 `MarkApply`, -其 `parameterize_right_subtree` 只处理 `Project` 和 `TableScan`,不修改 Join 节点,标记不会被破坏。 -改写出的 MarkApply 的 `estimated_work` 仍须使用 Greedy 写入的 `lookup_work`,使 `calculate_work` -(含 explain 中的代价)与 Greedy 的决策口径一致。 +There is no other batch between Greedy and Parameterize. In the same batch, `ParameterizeMarkApply` only matches +`MarkApply`, and its `parameterize_right_subtree` only handles `Project` and `TableScan` without touching Join nodes, +so the mark is not destroyed. The `estimated_work` of the rewritten MarkApply must still use the `lookup_work` +written by Greedy, so that `calculate_work` (including costs shown in explain) is consistent with Greedy's decision. -## 6. 已核实的执行器行为 +## 6. Verified Executor Behavior -### 6.1 MarkApply inner probe(`execution/dql/mark_apply.rs`) +### 6.1 MarkApply inner probe (`execution/dql/mark_apply.rs`) -- `predicates` 包含全部 `on` 等值条件(常量等值在 `parameterize` 返回前被 `truncate`);`next_join_tuple` - 取出内侧行后用 `predicates_matched` 校验全部条件,连接键多于 `key_columns` 时多出的条件也会被校验。 -- 原 join 的 `filter` 包成 `Filter` 放在 MarkApply 之上;被 probe 一侧原有的 Filter 保留。 -- 每个外侧行先 reset 内侧,最多取 1 行,所以本 RFC 只支持 PK/Unique probe。 +- `predicates` contains every `on` equality (constant equalities are `truncate`d before `parameterize` returns); + after `next_join_tuple` fetches an inner row it checks all conditions with `predicates_matched`, so extra + conditions are also checked when there are more join keys than `key_columns`. +- The original join's `filter` is wrapped into a `Filter` above the MarkApply; the existing Filter on the probed side + is kept. +- The inner side is reset for each outer row and at most 1 row is taken, so this RFC only supports PK/Unique probes. -### 6.2 NestLoopJoin 与 `reset` +### 6.2 NestLoopJoin and `reset` -NLJ 不物化内侧:每取一个外侧行就 `reset_right_input()` 并把内侧扫到底;Full join 最后再扫一遍内侧。 -`SeqScan` / `IndexScan` 的 `reset` 回到存储层重新定位。有内部状态的算子 `reset` 时清空状态并递归 reset 输入: +NLJ does not materialize its inner side: for each outer row it calls `reset_right_input()` and scans the inner side +to the end; a Full join scans the inner side once more at the end. `reset` on `SeqScan` / `IndexScan` repositions in +the storage layer. Stateful operators clear their state on `reset` and reset their inputs recursively: -| 算子 | `reset` 行为 | +| Operator | `reset` behavior | |---|---| -| HashJoin | `state = Build`,`bump.reset()`,reset 两侧 | -| Sort / ExternalSort | 清空 `rows`,reset 输入 | -| TopK | `output = None`,`arena.reset()`,reset 输入 | -| HashAgg / SimpleAgg | 清空输出状态,reset 输入 | -| Window | `state.clear()`,`reset_functions()`,reset 输入 | -| NestLoopJoin | 回到 `PullLeft`,reset 两侧 | -| MarkApply | 只 reset 左侧;`materialize` 且已有缓存时保留右侧缓存 | - -因此 `rescan_work = work` 是准确的,唯一例外是开启 `materialize` 的 MarkApply。 - -### 6.3 排序 hint 不影响 Greedy 叶子 - -scan cost 中的排序折扣来自 `sort_elimination_hint` / `stream_aggregate_hint`,需确认 Greedy 把叶子当作 -hash 输入或改写为 probe 时,不会丢失上层依赖的有序性、也不会用到打过折的 `work`: - -- hint 在 implementation 阶段由 `annotate_hints_and_physical_options`(`heuristic/optimizer.rs`)自上而下下发, - 只穿过 `Filter` / `Project` / `Limit` / `TopK` / `Sort`;遇到 Join 等其它算子即换成空 hint。 - 所以 Join 之下的 scan(即 Greedy 的叶子)`hint_sum = 0`,没有排序折扣。 -- 消费有序性的 `EliminateRedundantSort` / `UseStreamAggregate` 在同一阶段的 `apply_annotated_post_rules` 中运行, - 早于 Greedy;其 `ensure_order` 只沿 `Childrens::Only` 下钻,不穿过 Join。Join 的 `PhysicalOption` - 本身是 `SortOption::None`。 -- 叶子与 Join 之间若有 Sort / Aggregate,该子树不是 `Filter* + TableScan`,不会作为 probe 内侧, - 其有序性只在子树内部被消费。 - -结论:Greedy 改变叶子的访问路径不会破坏任何上层有序性依赖,Hash 形态使用的 `work` 也不含排序折扣。 - -## 7. 实施计划 - -代价模型是 Greedy 决策的前提,所以先统一代价,再移交决策权,分两步提交以便定位回归。 - -1. 统一代价(只改行为):引入 `rows_work`、`JoinImpl` / `InputCost`,修正 HashJoin、NLJ、MarkApply 的估算, - Greedy 分数改为本步处理量。现有连接顺序与 explain / slt 期望值会变化,需整体重新校准。 -2. 移交决策权:拆出共用判定函数与 `index_covering`;Greedy 按形态比较,probe 直接改写、其余标记 `join_decided`; - `ParameterizeInnerJoin` 跳过 `join_decided` 的 join;Greedy 触发条件放宽到 `can_expand`、接管 NestLoop 方向 - (`FORCE_NEST_LOOP_JOIN` 区域只比较 NestLoop),删除 `OrientNestedLoopJoin` 及其 batch。 - -测试(单测走 `make test`,slt 走 `make test-slt`): - -- `rows_work` 与各 `JoinImpl::work`:Full join、零列行、饱和运算。 -- Greedy:大外侧 × 小唯一内侧选 hash;小外侧 × 大唯一内侧选 probe 且优先合并;临界点; - 多个可用唯一索引时选代价最低的;复合唯一索引 + 常量等值;无统计按默认行数。 -- 下游规则:`join_decided` 的 join 不被改写;未标记的走兜底。 -- NestLoop 方向:Greedy 区域内的 cross pair、根为 Cross 的区域、`FORCE_NEST_LOOP_JOIN` 区域三类, - 方向均与原 `OrientNestedLoopJoin` 的现有用例结论一致(迁移其单测),`FORCE_NEST_LOOP_JOIN` 下只出现 NLJ。 -- 覆盖:scan 选了覆盖索引、另一个覆盖的 Unique 可 probe 时,probe 按 `seeks = 1` 计价并被选中, - 改写后的 IndexScan 带 `covered_deserializers`,执行时不回表。 -- 正确性:probe 后全部 `on` 条件、join filter、静态 filter 均保留(slt 覆盖)。 - -## 8. 未来工作 - -- 开启 `materialize` 的 MarkApply 的 `rescan_work`;以及 NLJ 内侧含子查询类 MarkApply 时, - Greedy 按其参数化 / 物化前的估算决定方向的偏差(3.10)。 -- 非唯一索引 probe:执行器需支持每个外侧行输出多行,且每行 reset 内侧开销较大,按具体 case 评估; - 届时 `rows_per_lookup` 由索引 NDV 估算。 -- 复合连接键的联合 NDV 与唯一性上界:hash 形态下,用被连接键绑定的任意 PK/Unique 估算行数, - 不限于 implementation 选中的索引(行数是数据的性质,与 scan 走哪个索引无关)。单列键已能从列直方图得到 NDV, - 收益只在复合键,按具体 case 再做。 -- I/O 建模。 +| HashJoin | `state = Build`, `bump.reset()`, reset both sides | +| Sort / ExternalSort | Clear `rows`, reset input | +| TopK | `output = None`, `arena.reset()`, reset input | +| HashAgg / SimpleAgg | Clear output state, reset input | +| Window | `state.clear()`, `reset_functions()`, reset input | +| NestLoopJoin | Back to `PullLeft`, reset both sides | +| MarkApply | Only resets the left side; keeps the right-side cache when `materialize` is on and a cache exists | + +So `rescan_work = work` is accurate; the only exception is MarkApply with `materialize` enabled. + +### 6.3 Sort hints do not affect Greedy leaves + +The sort discount in scan cost comes from `sort_elimination_hint` / `stream_aggregate_hint`. We need to confirm that +when Greedy treats a leaf as a hash input or rewrites it into a probe, it neither loses an ordering that an upper +operator relies on nor uses a discounted `work`: + +- Hints are pushed top-down during implementation by `annotate_hints_and_physical_options` + (`heuristic/optimizer.rs`) and only pass through `Filter` / `Project` / `Limit` / `TopK` / `Sort`; any other + operator such as Join replaces them with an empty hint. So scans under a Join (i.e. Greedy leaves) have + `hint_sum = 0` and no sort discount. +- `EliminateRedundantSort` / `UseStreamAggregate`, which consume ordering, run in `apply_annotated_post_rules` of the + same stage, before Greedy; their `ensure_order` only descends through `Childrens::Only` and never crosses a Join. + A Join's own `PhysicalOption` is `SortOption::None`. +- If a Sort / Aggregate sits between a leaf and the Join, that subtree is not `Filter* + TableScan`, so it cannot be a + probe inner side, and its ordering is only consumed inside the subtree. + +Conclusion: changing a leaf's access path in Greedy does not break any upper ordering dependency, and the `work` used +by the Hash shape contains no sort discount. + +## 7. Implementation Plan + +The cost model is the prerequisite for Greedy's decisions, so costs are unified first and decisions are handed over +second, in two commits to make regressions easy to locate. + +1. Unify costs (behavior only): introduce `rows_work` and `JoinImpl` / `InputCost`, fix the HashJoin, NLJ, and + MarkApply estimates, and change the Greedy score to the work done in this step. Existing join orders and explain / + slt expectations will change and need to be recalibrated as a whole. +2. Hand over decisions: extract the shared predicate function and `index_covering`; Greedy compares shapes, rewrites + probes directly, and marks the rest `join_decided`; `ParameterizeInnerJoin` skips `join_decided` joins; Greedy's + trigger is relaxed to `can_expand` and it takes over NestLoop direction (`FORCE_NEST_LOOP_JOIN` regions only + compare NestLoop); remove `OrientNestedLoopJoin` and its batch. + +Tests (unit tests via `make test`, slt via `make test-slt`): + +- `rows_work` and each `JoinImpl::work`: Full join, zero-column rows, saturating arithmetic. +- Greedy: large outer × small unique inner chooses hash; small outer × large unique inner chooses probe and merges + it first; the break-even point; the cheapest of several usable unique indexes; a composite unique index plus + constant equalities; default row counts without statistics. +- Downstream rules: `join_decided` joins are not rewritten; unmarked joins take the fallback. +- NestLoop direction: cross pairs inside a Greedy region, regions rooted at Cross, and `FORCE_NEST_LOOP_JOIN` regions + all reach the same direction as the existing `OrientNestedLoopJoin` cases (migrate its unit tests), and only NLJ + appears under `FORCE_NEST_LOOP_JOIN`. +- Coverage: when the scan chose a covering index and another covering Unique can be probed, the probe is priced with + `seeks = 1` and chosen, the rewritten IndexScan carries `covered_deserializers`, and execution skips the table + lookup. +- Correctness: after a probe, all `on` conditions, the join filter, and static filters are kept (covered by slt). + +## 8. Future Work + +- `rescan_work` for MarkApply with `materialize` enabled, and the deviation where Greedy decides NLJ direction from + the pre-parameterization / pre-materialization estimate of a subquery-style MarkApply in the inner side (3.10). +- Probes on non-unique indexes: the executor must support emitting multiple rows per outer row, and resetting the + inner side per row is expensive, so evaluate case by case; `rows_per_lookup` would then be estimated from the + index NDV. +- Joint NDV and uniqueness bound for composite join keys: under the hash shape, estimate rows with any PK/Unique bound + by the join keys, not only the index chosen during implementation (row count is a property of the data, not of + which index the scan uses). Single-column keys already get NDV from column histograms, so the benefit is only for + composite keys; do it when a concrete case appears. +- I/O modeling. From 0f46019624bc66fa0f9040d521a84c6b45cd46df Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 12:50:52 +0800 Subject: [PATCH 49/62] test: gate constant propagation tests off wasm32 --- src/optimizer/rule/normalization/constant_propagation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/optimizer/rule/normalization/constant_propagation.rs b/src/optimizer/rule/normalization/constant_propagation.rs index e47c4239..444afdb0 100644 --- a/src/optimizer/rule/normalization/constant_propagation.rs +++ b/src/optimizer/rule/normalization/constant_propagation.rs @@ -355,7 +355,7 @@ impl NormalizationRule for PropagateJoinConstants { } } -#[cfg(test)] +#[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::{eq_operands, for_each_conjunct, table_column}; use super::{PropagateFilterConstants, PropagateJoinConstants}; From 7176d7f7eb24c4d7c345b3c22b4be88e9bf392fa Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 14:23:39 +0800 Subject: [PATCH 50/62] refactor: split IndexProbeJoin out of MarkApply Inner-join probes shared MarkApplyOperator through a MarkApplyKind::InnerJoin variant, which forced kind checks across the executor, schema, costing and rewrite rules. Give them a dedicated IndexProbeJoin operator and executor, make the MarkApply output column required, and fold the quantified ANY scan over its Eq(x) and Eq(NULL) ranges into one pass. --- src/execution/dql/join/index_probe_join.rs | 284 ++++++++++++++++ src/execution/dql/join/mod.rs | 26 ++ src/execution/dql/mark_apply.rs | 315 +++--------------- src/execution/mod.rs | 12 + src/optimizer/heuristic/optimizer.rs | 22 +- .../rule/implementation/dql/mark_apply.rs | 18 +- src/optimizer/rule/implementation/mod.rs | 11 +- .../rule/normalization/column_pruning.rs | 43 +-- .../normalization/compilation_in_advance.rs | 1 + .../rule/normalization/guard_mark_apply.rs | 4 - src/optimizer/rule/normalization/mod.rs | 2 + .../rule/normalization/parameterized_index.rs | 51 ++- src/orm/mod.rs | 2 +- src/planner/mod.rs | 6 +- src/planner/operator/index_probe_join.rs | 36 ++ src/planner/operator/mark_apply.rs | 24 +- src/planner/operator/mod.rs | 69 +++- src/planner/operator/visitor.rs | 24 +- src/planner/operator/visitor_mut.rs | 23 +- tests/slt/join.slt | 2 +- 20 files changed, 564 insertions(+), 411 deletions(-) create mode 100644 src/execution/dql/join/index_probe_join.rs create mode 100644 src/planner/operator/index_probe_join.rs diff --git a/src/execution/dql/join/index_probe_join.rs b/src/execution/dql/join/index_probe_join.rs new file mode 100644 index 00000000..7cb16d0a --- /dev/null +++ b/src/execution/dql/join/index_probe_join.rs @@ -0,0 +1,284 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::errors::DatabaseError; +use crate::execution::dql::join::predicates_matched; +use crate::execution::{ + build_read, ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor, +}; +use crate::planner::operator::index_probe_join::IndexProbeJoinOperator; +use crate::planner::{ExecMetaArena, LogicalPlan, MetaArena}; +use crate::storage::Transaction; +use crate::types::index::RuntimeIndexProbe; + +pub struct IndexProbeJoin<'a> { + op: &'a IndexProbeJoinOperator, + left_input: ExecId, + right_input: ExecId, +} + +impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for IndexProbeJoin<'a> { + type Input = (&'a IndexProbeJoinOperator, &'a LogicalPlan, &'a LogicalPlan); + + fn into_executor( + (op, left_input, right_input): Self::Input, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut (dyn MetaArena + 'a), + cache: ExecutionContext<'_>, + transaction: &T, + ) -> ExecId { + let left_input = build_read(arena, plan_arena, left_input, cache, transaction); + let right_input = build_read(arena, plan_arena, right_input, cache, transaction); + arena.push(ExecNode::IndexProbeJoin(Self { + op, + left_input, + right_input, + })) + } +} + +impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for IndexProbeJoin<'a> { + fn reset( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + arena.reset(self.left_input, plan_arena) + } + + fn next_tuple( + &mut self, + arena: &mut ExecArena<'a, T>, + plan_arena: &mut ExecMetaArena, + ) -> Result<(), DatabaseError> { + while arena.next_tuple(self.left_input, plan_arena)? { + let mut left = arena.take_tuple(); + let key = plan_arena + .expression(self.op.probe) + .eval(plan_arena, Some(&left))? + .into_owned(); + if key.has_null() { + arena.release(left); + continue; + } + arena.push_runtime_probe(RuntimeIndexProbe::Eq(key)); + arena.reset(self.right_input, plan_arena)?; + if arena.next_tuple(self.right_input, plan_arena)? + && predicates_matched(&self.op.predicates, &left, arena.result_tuple(), plan_arena)? + { + let mut right = arena.take_tuple(); + let pk = left.take_pk().or_else(|| right.take_pk()); + let joined = arena.concat(left, right, pk); + arena.produce(joined); + return Ok(()); + } + arena.release(left); + } + arena.finish(); + Ok(()) + } +} + +// GRCOV_EXCL_START +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::*; + use crate::catalog::{ColumnCatalog, ColumnDesc, ColumnRef}; + use crate::execution::{execute_input, ExecArena}; + use crate::expression::{BinaryOperator, ScalarExpression}; + use crate::planner::operator::values::ValuesOperator; + use crate::planner::operator::Operator; + use crate::planner::test::PlanArenaTestExt; + use crate::planner::{Childrens, ExprRef, PlanArena, TableArenaCell}; + use crate::storage::rocksdb::RocksStorage; + use crate::storage::{StatisticsMetaCache, Storage, TableCache, ViewCache}; + use crate::types::evaluator::binary_create; + use crate::types::value::DataValue; + use crate::types::{CharLengthUnits, LogicalType}; + use std::borrow::Cow; + use tempfile::TempDir; + + fn build_values( + arena: &mut PlanArena, + columns: Vec<(&str, LogicalType)>, + rows: Vec>, + ) -> LogicalPlan { + let schema_ref = columns + .into_iter() + .map(|(name, ty)| { + arena.alloc_column(ColumnCatalog::new( + name.to_string(), + true, + ColumnDesc::new(ty, None, true, None).unwrap(), + )) + }) + .collect(); + LogicalPlan::new( + Operator::Values(ValuesOperator::new( + arena.alloc_expression_rows(&rows), + rows.len(), + schema_ref, + )), + Childrens::None, + ) + } + + fn build_equality( + arena: &mut PlanArena, + left: (ColumnRef, usize), + right: (ColumnRef, usize), + ) -> Result { + let left_expr = arena.alloc_expression(ScalarExpression::column_expr(left.0, left.1)); + let right_expr = arena.alloc_expression(ScalarExpression::column_expr(right.0, right.1)); + Ok(arena.alloc_expression(ScalarExpression::Binary { + op: BinaryOperator::Eq, + left_expr, + right_expr, + evaluator: Some(binary_create( + Cow::Owned(LogicalType::Integer), + BinaryOperator::Eq, + )?), + ty: LogicalType::Boolean, + })) + } + + fn build_test_storage() -> Result< + ( + TableCache, + ViewCache, + StatisticsMetaCache, + TempDir, + RocksStorage, + ), + DatabaseError, + > { + let temp_dir = TempDir::new().expect("unable to create temporary working directory"); + let storage = RocksStorage::new(temp_dir.path())?; + Ok(( + TableCache::default(), + ViewCache::default(), + StatisticsMetaCache::default(), + temp_dir, + storage, + )) + } + + // Values ignores runtime probes, so it stands in for the unique lookup and the pushed + // probes stay on the stack for inspection. + #[test] + fn index_probe_join_moves_single_match_and_reuses_right_slots() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut plan_arena = PlanArena::new(&table_arena); + let mut left = build_values( + &mut plan_arena, + vec![ + ("left_key", LogicalType::Integer), + ( + "payload", + LogicalType::Varchar(None, CharLengthUnits::Characters), + ), + ], + vec![ + vec![DataValue::Int32(2), DataValue::from("first".to_string())], + vec![DataValue::Null, DataValue::from("null".to_string())], + vec![DataValue::Int32(99), DataValue::from("missing".to_string())], + vec![DataValue::Int32(2), DataValue::from("again".to_string())], + ], + ); + let mut right = build_values( + &mut plan_arena, + vec![ + ("right_key", LogicalType::Integer), + ("flag", LogicalType::Boolean), + ], + vec![vec![DataValue::Int32(2), DataValue::Boolean(true)]], + ); + let left_column = left.output_schema(&mut plan_arena)[0]; + let right_schema = right.output_schema(&mut plan_arena).clone(); + let equality = build_equality(&mut plan_arena, (left_column, 0), (right_schema[0], 2))?; + let residual = + plan_arena.alloc_expression(ScalarExpression::column_expr(right_schema[1], 3)); + let probe = plan_arena.alloc_expression(ScalarExpression::column_expr(left_column, 0)); + let op = IndexProbeJoinOperator::new(probe, vec![equality, residual]); + let (table_cache, view_cache, meta_cache, _temp_dir, storage) = build_test_storage()?; + let transaction = storage.transaction()?; + let cache = crate::execution::empty_context(&table_cache, &view_cache, &meta_cache); + let mut arena = ExecArena::with_capacity(0); + arena.init_context(cache, &transaction); + let root = >::into_executor( + (&op, &left, &right), + &mut arena, + &mut plan_arena, + cache, + &transaction, + ); + let address = arena.nodes.items.as_ptr(); + let count = arena.nodes.items.len(); + assert_eq!(count, 3); + let mut plan_arena = ExecMetaArena::new(plan_arena); + for payload in ["first", "again"] { + assert!(arena.next_tuple(root, &mut plan_arena)?); + assert_eq!( + arena.take_tuple(), + vec![ + DataValue::Int32(2), + DataValue::from(payload.to_string()), + DataValue::Int32(2), + DataValue::Boolean(true) + ] + ); + assert_eq!(arena.nodes.items.as_ptr(), address); + assert_eq!(arena.nodes.items.len(), count); + } + assert!(!arena.next_tuple(root, &mut plan_arena)?); + // The NULL key is skipped without probing. + assert_eq!( + arena.runtime_probe_stack, + [2, 99, 2].map(|key| RuntimeIndexProbe::Eq(DataValue::Int32(key))) + ); + Ok(()) + } + + #[test] + fn index_probe_join_skips_a_rejected_unique_match() -> Result<(), DatabaseError> { + let table_arena = TableArenaCell::default(); + let mut plan_arena = PlanArena::new(&table_arena); + let mut left = build_values( + &mut plan_arena, + vec![("left_key", LogicalType::Integer)], + vec![vec![DataValue::Int32(1)]], + ); + let mut right = build_values( + &mut plan_arena, + vec![("flag", LogicalType::Boolean)], + vec![vec![DataValue::Boolean(false)]], + ); + let left_column = left.output_schema(&mut plan_arena)[0]; + let right_column = right.output_schema(&mut plan_arena)[0]; + let predicate = plan_arena.alloc_expression(ScalarExpression::column_expr(right_column, 1)); + let probe = plan_arena.alloc_expression(ScalarExpression::column_expr(left_column, 0)); + let op = IndexProbeJoinOperator::new(probe, vec![predicate]); + let (table_cache, view_cache, meta_cache, _temp_dir, storage) = build_test_storage()?; + let transaction = storage.transaction()?; + let mut executor = execute_input::<_, IndexProbeJoin>( + (&op, &left, &right), + crate::execution::empty_context(&table_cache, &view_cache, &meta_cache), + plan_arena, + &transaction, + ); + assert!(executor.next_tuple()?.is_none()); + Ok(()) + } +} +// GRCOV_EXCL_STOP diff --git a/src/execution/dql/join/mod.rs b/src/execution/dql/join/mod.rs index d15176f2..4d61c41f 100644 --- a/src/execution/dql/join/mod.rs +++ b/src/execution/dql/join/mod.rs @@ -12,10 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::errors::DatabaseError; use crate::planner::operator::join::JoinType; +use crate::planner::{ExprRef, MetaArena}; +use crate::types::tuple::{SplitTupleRef, TupleImpl}; +use crate::types::value::DataValue; mod hash; pub(crate) mod hash_join; +pub(crate) mod index_probe_join; pub(crate) mod nested_loop_join; pub fn joins_nullable(join_type: &JoinType) -> (bool, bool) { @@ -27,3 +32,24 @@ pub fn joins_nullable(join_type: &JoinType) -> (bool, bool) { JoinType::Cross => (true, true), } } + +/// Evaluates join predicates bound to the `left ++ right` layout; NULL counts as no match. +pub(crate) fn predicates_matched( + predicates: &[ExprRef], + left_tuple: &TupleImpl, + right_tuple: &TupleImpl, + plan_arena: &(dyn MetaArena + '_), +) -> Result { + let values = SplitTupleRef::new(left_tuple, right_tuple); + for predicate in predicates { + match *plan_arena + .expression(*predicate) + .eval(plan_arena, Some(&values))? + { + DataValue::Boolean(true) => {} + DataValue::Boolean(false) | DataValue::Null => return Ok(false), + _ => return Err(DatabaseError::InvalidType), + } + } + Ok(true) +} diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index 7cd73e26..b84ae323 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; +use crate::execution::dql::join::predicates_matched; use crate::execution::runs::{Runs, RunsBuilder, RunsCursor}; #[cfg(feature = "spill")] use crate::execution::spill::runs::SpillRuns; @@ -30,14 +31,6 @@ use crate::types::value::DataValue; use std::borrow::Cow; use std::cmp::Ordering; -#[derive(PartialEq, Eq)] -enum QuantifiedPredicateOutcome { - True, - False, - Null, - Skip, -} - #[cfg(feature = "spill")] type KeysBuilder = SpillRuns; #[cfg(not(feature = "spill"))] @@ -168,11 +161,6 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { arena.finish(); return Ok(()); }; - if matches!(self.op.kind, MarkApplyKind::InnerJoin) { - self.next_join_tuple(&input, arena, plan_arena)?; - self.right_input = Some(input); - return Ok(()); - } if !arena.next_tuple(self.left_input, plan_arena)? { // Return the temporarily owned cache so reset can reuse it after EOF. if self.op.materialize { @@ -199,41 +187,6 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { } impl<'a> MarkApply<'a> { - fn next_join_tuple( - &mut self, - input: &RightInput, - arena: &mut ExecArena<'a, T>, - plan_arena: &mut ExecMetaArena, - ) -> Result<(), DatabaseError> { - while arena.next_tuple(self.left_input, plan_arena)? { - let mut left = arena.take_tuple(); - let value = self.parameterized_probe_value(&left, plan_arena)?; - if value.as_ref().is_some_and(DataValue::has_null) { - arena.release(left); - continue; - } - let root = input.reset(arena, plan_arena, self.runtime_probe_for(value))?; - // A full unique-key probe cannot have another match, so consume the left row. - if arena.next_tuple(root, plan_arena)? - && Self::predicates_matched( - self.op.predicates(), - &left, - arena.result_tuple(), - plan_arena, - )? - { - let mut right = arena.take_tuple(); - let pk = left.take_pk().or_else(|| right.take_pk()); - let joined = arena.concat(left, right, pk); - arena.produce(joined); - return Ok(()); - } - arena.release(left); - } - arena.finish(); - Ok(()) - } - fn runtime_probe_for(&self, param_value: Option) -> Option { self.op.parameterized_probe()?; match param_value { @@ -277,16 +230,13 @@ impl<'a> MarkApply<'a> { ) -> Result<(RightInput, DataValue), DatabaseError> { let probe = self.parameterized_probe_value(left_tuple, plan_arena)?; let value = match self.op.kind { - MarkApplyKind::InnerJoin => unreachable!("inner join streams tuples"), MarkApplyKind::Exists => { let predicates = self.op.predicates(); let runtime_probe = self.runtime_probe_for(probe); input .scan(arena, plan_arena, runtime_probe, |right, meta| { - Ok( - Self::predicates_matched(predicates, left_tuple, right, meta)? - .then_some(DataValue::Boolean(true)), - ) + Ok(predicates_matched(predicates, left_tuple, right, meta)? + .then_some(DataValue::Boolean(true))) })? .unwrap_or(DataValue::Boolean(false)) } @@ -296,42 +246,12 @@ impl<'a> MarkApply<'a> { self.materialize_keys(root, arena, plan_arena, left_tuple)?, ); } - match (&input, probe) { - (RightInput::Materialized(keys), _) => { + match &input { + RightInput::Materialized(keys) => { self.lookup_keys(keys, quantifier, plan_arena, left_tuple)? } - (_, Some(probe_value)) - if matches!(quantifier, MarkApplyQuantifier::Any) - && !probe_value.is_null() => - { - let value = self.scan_quantified_right_input( - &input, - arena, - plan_arena, - Some(probe_value), - quantifier, - left_tuple, - )?; - if value == DataValue::Boolean(true) { - value - } else if self - .scan_quantified_right_input( - &input, - arena, - plan_arena, - Some(DataValue::Null), - quantifier, - left_tuple, - )? - .is_null() - { - DataValue::Null - } else { - DataValue::Boolean(false) - } - } _ => self.scan_quantified_right_input( - &input, arena, plan_arena, None, quantifier, left_tuple, + &input, arena, plan_arena, probe, quantifier, left_tuple, )?, } } @@ -339,7 +259,6 @@ impl<'a> MarkApply<'a> { Ok((input, value)) } - /// Splits the single materializable predicate into `left_expr op right_expr`. fn quantified_comparison<'p>( predicates: &[ExprRef], plan_arena: &'p (dyn MetaArena + '_), @@ -361,7 +280,6 @@ impl<'a> MarkApply<'a> { Ok((*op, *left_expr, *right_expr, evaluator)) } - /// Reads the whole RHS once, keeping only its comparison keys. fn materialize_keys( &self, root: ExecId, @@ -462,92 +380,69 @@ impl<'a> MarkApply<'a> { ) -> Result { let mut saw_null = false; let predicates = self.op.predicates(); - let runtime_probe = self.runtime_probe_for(probe); - let value = input.scan(arena, plan_arena, runtime_probe, |right, meta| { - match Self::quantified_predicate_outcome(predicates, left_tuple, right, meta)? { - QuantifiedPredicateOutcome::True - if matches!(quantifier, MarkApplyQuantifier::Any) => - { - return Ok(Some(DataValue::Boolean(true))); - } - QuantifiedPredicateOutcome::False - if matches!(quantifier, MarkApplyQuantifier::All) => - { - return Ok(Some(DataValue::Boolean(false))); + // One fold across all probed ranges, so the mark is the quantifier over their union. + for runtime_probe in self.quantified_probes(probe) { + let value = input.scan(arena, plan_arena, runtime_probe, |right, meta| { + match Self::quantified_row_value(predicates, left_tuple, right, meta)? { + Some(DataValue::Boolean(true)) + if matches!(quantifier, MarkApplyQuantifier::Any) => + { + return Ok(Some(DataValue::Boolean(true))); + } + Some(DataValue::Boolean(false)) + if matches!(quantifier, MarkApplyQuantifier::All) => + { + return Ok(Some(DataValue::Boolean(false))); + } + Some(DataValue::Null) => saw_null = true, + _ => {} } - QuantifiedPredicateOutcome::Null => saw_null = true, - _ => {} + Ok(None) + })?; + if let Some(value) = value { + return Ok(value); } - Ok(None) - })?; - Ok(value.unwrap_or(if saw_null { + } + Ok(if saw_null { DataValue::Null } else { DataValue::Boolean(matches!(quantifier, MarkApplyQuantifier::All)) - })) - } - - fn predicates_matched( - predicates: &[ExprRef], - left_tuple: &TupleImpl, - right_tuple: &TupleImpl, - plan_arena: &(dyn MetaArena + '_), - ) -> Result { - let values = SplitTupleRef::new(left_tuple, right_tuple); - for predicate in predicates { - match *plan_arena - .expression(*predicate) - .eval(plan_arena, Some(&values))? - { - DataValue::Boolean(true) => {} - DataValue::Boolean(false) | DataValue::Null => return Ok(false), - _ => return Err(DatabaseError::InvalidType), - } - } - Ok(true) + }) } - fn quantified_predicate_outcome( - predicates: &[ExprRef], - left_tuple: &TupleImpl, - right_tuple: &TupleImpl, - plan_arena: &(dyn MetaArena + '_), - ) -> Result { - match Self::eval_predicates(predicates, left_tuple, right_tuple, plan_arena)? { - Some(DataValue::Boolean(true)) => Ok(QuantifiedPredicateOutcome::True), - Some(DataValue::Boolean(false)) => Ok(QuantifiedPredicateOutcome::False), - Some(DataValue::Null) => Ok(QuantifiedPredicateOutcome::Null), - None => Ok(QuantifiedPredicateOutcome::Skip), - Some(_) => Err(DatabaseError::InvalidType), - } + fn quantified_probes( + &self, + probe: Option, + ) -> impl Iterator> { + let (first, null_keys) = match probe.filter(|value| !value.is_null()) { + Some(key) => ( + Some(RuntimeIndexProbe::Eq(key)), + Some(Some(RuntimeIndexProbe::Eq(DataValue::Null))), + ), + None => (self.runtime_probe_for(None), None), + }; + std::iter::once(first).chain(null_keys) } - - fn eval_predicates( + fn quantified_row_value( predicates: &[ExprRef], left_tuple: &TupleImpl, right_tuple: &TupleImpl, plan_arena: &(dyn MetaArena + '_), ) -> Result, DatabaseError> { - let values = SplitTupleRef::new(left_tuple, right_tuple); - // probe_predicate is in predicate, always first - let (probe_predicate, correlated_predicates) = + let (comparison, correlated_predicates) = predicates.split_first().ok_or(DatabaseError::InvalidType)?; - for predicate in correlated_predicates { - match *plan_arena - .expression(*predicate) - .eval(plan_arena, Some(&values))? - { - DataValue::Boolean(true) => {} - DataValue::Boolean(false) | DataValue::Null => return Ok(None), - _ => return Err(DatabaseError::InvalidType), - } + if !predicates_matched(correlated_predicates, left_tuple, right_tuple, plan_arena)? { + return Ok(None); + } + let values = SplitTupleRef::new(left_tuple, right_tuple); + match plan_arena + .expression(*comparison) + .eval(plan_arena, Some(&values))? + .into_owned() + { + value @ (DataValue::Boolean(_) | DataValue::Null) => Ok(Some(value)), + _ => Err(DatabaseError::InvalidType), } - Ok(Some( - plan_arena - .expression(*probe_predicate) - .eval(plan_arena, Some(&values))? - .into_owned(), - )) } } @@ -567,7 +462,7 @@ mod tests { use crate::types::evaluator::{binary_create, cast_create}; use crate::types::index::RuntimeIndexProbe; use crate::types::tuple::Tuple; - use crate::types::{CharLengthUnits, LogicalType}; + use crate::types::LogicalType; use std::borrow::Cow; use tempfile::TempDir; @@ -656,106 +551,6 @@ mod tests { })) } - #[test] - fn inner_join_apply_moves_single_match_and_reuses_right_slots() -> Result<(), DatabaseError> { - let table_arena = crate::planner::TableArenaCell::default(); - let mut plan_arena = crate::planner::PlanArena::new(&table_arena); - let mut left = build_values_with_schema( - &mut plan_arena, - vec![ - ("left_key", LogicalType::Integer), - ( - "payload", - LogicalType::Varchar(None, CharLengthUnits::Characters), - ), - ], - vec![ - vec![DataValue::Int32(2), DataValue::from("first".to_string())], - vec![DataValue::Null, DataValue::from("null".to_string())], - vec![DataValue::Int32(99), DataValue::from("missing".to_string())], - vec![DataValue::Int32(2), DataValue::from("again".to_string())], - ], - ); - let mut right = build_values_with_schema( - &mut plan_arena, - vec![ - ("right_key", LogicalType::Integer), - ("flag", LogicalType::Boolean), - ], - vec![vec![DataValue::Int32(2), DataValue::Boolean(true)]], - ); - let left_column = left.output_schema(&mut plan_arena)[0]; - let right_schema = right.output_schema(&mut plan_arena).clone(); - let equality = - build_equality_predicate(&mut plan_arena, left_column, 0, right_schema[0], 2)?; - let residual = - plan_arena.alloc_expression(ScalarExpression::column_expr(right_schema[1], 3)); - let probe = plan_arena.alloc_expression(ScalarExpression::column_expr(left_column, 0)); - let mut op = MarkApplyOperator::new_inner_join(vec![equality, residual], probe); - // A single Values row stands in for the unique lookup, without a runtime probe. - op.set_parameterized_probe(None); - let (table_cache, view_cache, meta_cache, _temp_dir, storage) = build_test_storage()?; - let transaction = storage.transaction()?; - let cache = crate::execution::empty_context(&table_cache, &view_cache, &meta_cache); - let mut arena = ExecArena::with_capacity(0); - arena.init_context(cache, &transaction); - let root = >::into_executor( - (&op, &left, &right), - &mut arena, - &mut plan_arena, - cache, - &transaction, - ); - let address = arena.nodes.items.as_ptr(); - let count = arena.nodes.items.len(); - assert_eq!(count, 3); - let mut plan_arena = ExecMetaArena::new(plan_arena); - for payload in ["first", "again"] { - assert!(arena.next_tuple(root, &mut plan_arena)?); - assert_eq!( - arena.take_tuple(), - vec![ - DataValue::Int32(2), - DataValue::from(payload.to_string()), - DataValue::Int32(2), - DataValue::Boolean(true) - ] - ); - assert_eq!(arena.nodes.items.as_ptr(), address); - assert_eq!(arena.nodes.items.len(), count); - } - assert!(!arena.next_tuple(root, &mut plan_arena)?); - Ok(()) - } - - #[test] - fn inner_join_apply_skips_a_rejected_unique_match() -> Result<(), DatabaseError> { - let table_arena = crate::planner::TableArenaCell::default(); - let mut plan_arena = crate::planner::PlanArena::new(&table_arena); - let mut left = build_values(&mut plan_arena, "left_key", vec![vec![DataValue::Int32(1)]]); - let mut right = build_values_with_schema( - &mut plan_arena, - vec![("flag", LogicalType::Boolean)], - vec![vec![DataValue::Boolean(false)]], - ); - let left_column = left.output_schema(&mut plan_arena)[0]; - let right_column = right.output_schema(&mut plan_arena)[0]; - let predicate = plan_arena.alloc_expression(ScalarExpression::column_expr(right_column, 1)); - let probe = plan_arena.alloc_expression(ScalarExpression::column_expr(left_column, 0)); - let mut op = MarkApplyOperator::new_inner_join(vec![predicate], probe); - op.set_parameterized_probe(None); - let (table_cache, view_cache, meta_cache, _temp_dir, storage) = build_test_storage()?; - let transaction = storage.transaction()?; - let mut executor = execute_input::<_, MarkApply>( - (&op, &left, &right), - crate::execution::empty_context(&table_cache, &view_cache, &meta_cache), - plan_arena, - &transaction, - ); - assert!(executor.next_tuple()?.is_none()); - Ok(()) - } - #[test] fn mark_exists_apply_appends_boolean_match_column() -> Result<(), DatabaseError> { let table_arena = crate::planner::TableArenaCell::default(); diff --git a/src/execution/mod.rs b/src/execution/mod.rs index 7fb60ea5..907a2060 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -62,6 +62,7 @@ use crate::execution::dql::filter::Filter; use crate::execution::dql::function_scan::FunctionScan; use crate::execution::dql::index_scan::IndexScan; use crate::execution::dql::join::hash_join::HashJoin; +use crate::execution::dql::join::index_probe_join::IndexProbeJoin; use crate::execution::dql::limit::Limit; use crate::execution::dql::projection::Projection; use crate::execution::dql::recursive_cte::{RecursiveCte, RecursiveInput, RecursiveScan}; @@ -226,6 +227,7 @@ pub(crate) enum ExecNode<'a, T: Transaction + 'a> { Insert(Insert<'a>), Limit(Limit), MarkApply(MarkApply<'a>), + IndexProbeJoin(IndexProbeJoin<'a>), NestedLoopJoin(NestedLoopJoin), Projection(Projection<'a>), RecursiveCte(RecursiveCte<'a, T>), @@ -348,6 +350,9 @@ impl<'a, T: Transaction + 'a> ExecNode<'a, T> { ExecNode::MarkApply(exec) => { as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) } + ExecNode::IndexProbeJoin(exec) => { + as ExecutorNode<'a, T>>::reset(exec, arena, plan_arena) + } ExecNode::NestedLoopJoin(exec) => { >::reset(exec, arena, plan_arena) } @@ -493,6 +498,9 @@ impl<'a, T: Transaction + 'a> ExecNode<'a, T> { ExecNode::MarkApply(exec) => { as ExecutorNode<'a, T>>::next_tuple(exec, arena, plan_arena) } + ExecNode::IndexProbeJoin(exec) => { + as ExecutorNode<'a, T>>::next_tuple(exec, arena, plan_arena) + } ExecNode::NestedLoopJoin(exec) => { >::next_tuple(exec, arena, plan_arena) } @@ -978,6 +986,10 @@ where let (left, right) = plan.childrens.twins(); read!(MarkApply<'a>, (op, left, right)) } + Operator::IndexProbeJoin(op) => { + let (left, right) = plan.childrens.twins(); + read!(IndexProbeJoin<'a>, (op, left, right)) + } Operator::Join(op) => { let use_hash_join = matches!( &op.on, diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index 127aeb89..56f91d47 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -25,7 +25,6 @@ use crate::optimizer::rule::normalization::{ apply_annotated_post_rules, apply_scan_order_hint, constant_calculation_current, evaluator_bind_current, NormalizationRuleImpl, OrderHintKind, ScanOrderHint, WholeTreePassKind, }; -use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::planner::{Childrens, LogicalPlan, PlanArena}; @@ -523,16 +522,11 @@ impl ImplementationRuleIndex { Operator::Limit(_) if self.contains(ImplementationRuleImpl::Limit) => Some( PhysicalOption::new(PlanImpl::Limit, SortOption::Follow, RowCountOption::Unknown), ), - Operator::MarkApply(op) if self.contains(ImplementationRuleImpl::MarkApply) => { - let rows = if matches!(op.kind, MarkApplyKind::InnerJoin) { - RowCountOption::Unknown - } else { - RowCountOption::Follow - }; + Operator::MarkApply(_) if self.contains(ImplementationRuleImpl::MarkApply) => { Some(PhysicalOption::new( PlanImpl::MarkApply, SortOption::Follow, - rows, + RowCountOption::Follow, )) } Operator::Project(_) if self.contains(ImplementationRuleImpl::Projection) => { @@ -704,7 +698,7 @@ mod tests { use crate::optimizer::rule::normalization::NormalizationRuleImpl; use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::limit::LimitOperator; - use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyOperator}; + use crate::planner::operator::mark_apply::MarkApplyOperator; use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::sort::SortField; use crate::planner::operator::{ @@ -774,16 +768,6 @@ mod tests { let implementations = ImplementationRuleIndex::new(&[ImplementationRuleImpl::MarkApply]); plan.physical_option = implementations.direct_physical_option(&plan.operator); assert_eq!(plan.estimated_rows(), Some(12)); - let Operator::MarkApply(op) = &mut plan.operator else { - unreachable!() - }; - op.kind = MarkApplyKind::InnerJoin; - plan.physical_option = implementations.direct_physical_option(&plan.operator); - assert_eq!( - plan.physical_option.as_ref().unwrap().row_count, - RowCountOption::Unknown - ); - assert_eq!(plan.estimated_rows(), None); Ok(()) } diff --git a/src/optimizer/rule/implementation/dql/mark_apply.rs b/src/optimizer/rule/implementation/dql/mark_apply.rs index 31c7b682..d5698e7a 100644 --- a/src/optimizer/rule/implementation/dql/mark_apply.rs +++ b/src/optimizer/rule/implementation/dql/mark_apply.rs @@ -17,7 +17,6 @@ use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{keep_best_physical_option, BestPhysicalOption}; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; -use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::planner::{LogicalPlan, PlanArena}; use std::sync::LazyLock; @@ -43,18 +42,17 @@ impl ImplementationRule for MarkApplyImplementation { _: &StatisticMetaLoader<'_>, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { - let Operator::MarkApply(op) = &plan.operator else { + if !matches!(plan.operator, Operator::MarkApply(_)) { return Ok(()); - }; - // A mark adds one value to each left row; INNER JOIN may duplicate rows. - let rows = if matches!(op.kind, MarkApplyKind::InnerJoin) { - RowCountOption::Unknown - } else { - RowCountOption::Follow - }; + } + // A mark adds one value to each left row. keep_best_physical_option( best_physical_option, - PhysicalOption::new(PlanImpl::MarkApply, SortOption::Follow, rows), + PhysicalOption::new( + PlanImpl::MarkApply, + SortOption::Follow, + RowCountOption::Follow, + ), None, ); Ok(()) diff --git a/src/optimizer/rule/implementation/mod.rs b/src/optimizer/rule/implementation/mod.rs index 40f08eee..165c33c3 100644 --- a/src/optimizer/rule/implementation/mod.rs +++ b/src/optimizer/rule/implementation/mod.rs @@ -102,6 +102,8 @@ impl ImplementationRuleRootTag { Operator::Join(_) => Some(Self::Join), Operator::Limit(_) => Some(Self::Limit), Operator::MarkApply(_) => Some(Self::MarkApply), + // The join planner sets its physical option when building it. + Operator::IndexProbeJoin(_) => None, Operator::Project(_) => Some(Self::Project), Operator::ScalarQueryInit(_) => Some(Self::ScalarQueryInit), Operator::ScalarSubquery(_) => Some(Self::ScalarSubquery), @@ -324,7 +326,6 @@ mod tests { ImplementationRuleImpl, ImplementationRuleRootTag, }; use crate::planner::operator::function_scan::FunctionScanOperator; - use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::top_k::TopKOperator; use crate::planner::operator::{ Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption, @@ -442,16 +443,10 @@ mod tests { with_operator_from_sql( "select * from t1 where exists(select * from t2)", |op| matches!(op, Operator::MarkApply(_)), - |mut operator, arena| { + |operator, arena| { let option = best_option(ImplementationRuleImpl::MarkApply, &operator, arena)?; assert_eq!(option.plan, PlanImpl::MarkApply); assert_eq!(option.row_count, RowCountOption::Follow); - let Operator::MarkApply(op) = &mut operator else { - unreachable!() - }; - op.kind = MarkApplyKind::InnerJoin; - let option = best_option(ImplementationRuleImpl::MarkApply, &operator, arena)?; - assert_eq!(option.row_count, RowCountOption::Unknown); Ok(()) }, )?; diff --git a/src/optimizer/rule/normalization/column_pruning.rs b/src/optimizer/rule/normalization/column_pruning.rs index ed26dfc3..a932b571 100644 --- a/src/optimizer/rule/normalization/column_pruning.rs +++ b/src/optimizer/rule/normalization/column_pruning.rs @@ -142,13 +142,8 @@ impl ColumnPruning { op: &'a crate::planner::operator::mark_apply::MarkApplyOperator, _children: &mut ChildrenVisit<'a>, ) -> Result<(), DatabaseError> { - if !matches!( - op.kind, - crate::planner::operator::mark_apply::MarkApplyKind::InnerJoin - ) { - self.referenced_columns - .insert(*op.output_column(), self.arena); - } + self.referenced_columns + .insert(*op.output_column(), self.arena); Ok(()) } @@ -275,18 +270,6 @@ impl ColumnPruning { .visit_operator(operator, None, &mut ChildrenVisitMut::empty()) } - fn remap_exprs_after_child_change<'a>( - exprs: impl IntoIterator, - removed_positions: &[usize], - remapped_exprs: &mut HashSet, - arena: &mut PlanArena<'_>, - ) -> Result<(), DatabaseError> { - if removed_positions.is_empty() { - return Ok(()); - } - remap_exprs_positions(exprs, removed_positions, remapped_exprs, arena) - } - fn apply_only_child( referenced_columns: ReferencedColumns, all_referenced: bool, @@ -522,6 +505,7 @@ impl ColumnPruning { | Operator::Limit(_) | Operator::ScalarQueryInit(_) | Operator::MarkApply(_) + | Operator::IndexProbeJoin(_) | Operator::ScalarSubquery(_) | Operator::Join(_) | Operator::Filter(_) @@ -559,7 +543,11 @@ impl ColumnPruning { )?; changed |= outcome.changed; outcome.removed_positions.truncate(main_removed_end); - } else if matches!(operator, Operator::MarkApply(_)) { + } else if matches!( + operator, + Operator::MarkApply(_) | Operator::IndexProbeJoin(_) + ) { + // Both inputs stay intact, so probe/predicate positions need no remapping. Self::extend_operator_referenced_columns( operator, &mut required_columns, @@ -665,21 +653,6 @@ impl ColumnPruning { ); } } - } else if let Operator::MarkApply(op) = operator { - Self::offset_removed_positions( - &mut outcome.removed_positions - [right_removed_start..right_removed_end], - old_left_outputs_len, - ); - let removed_positions = - &outcome.removed_positions[left_removed_start..right_removed_end]; - Self::remap_exprs_after_child_change( - op.predicates.iter_mut().chain(op.guard.iter_mut()), - removed_positions, - &mut outcome.remapped_exprs, - arena, - )?; - outcome.removed_positions.truncate(right_removed_start); } else { Self::offset_removed_positions( &mut outcome.removed_positions diff --git a/src/optimizer/rule/normalization/compilation_in_advance.rs b/src/optimizer/rule/normalization/compilation_in_advance.rs index 9e183ea8..4cdcda2f 100644 --- a/src/optimizer/rule/normalization/compilation_in_advance.rs +++ b/src/optimizer/rule/normalization/compilation_in_advance.rs @@ -47,6 +47,7 @@ impl EvaluatorBind { plan.operator, Operator::ScalarQueryInit(_) | Operator::MarkApply(_) + | Operator::IndexProbeJoin(_) | Operator::Join(_) | Operator::Union(_) | Operator::SetMembership(_) diff --git a/src/optimizer/rule/normalization/guard_mark_apply.rs b/src/optimizer/rule/normalization/guard_mark_apply.rs index 7ba5e84a..46d39871 100644 --- a/src/optimizer/rule/normalization/guard_mark_apply.rs +++ b/src/optimizer/rule/normalization/guard_mark_apply.rs @@ -19,7 +19,6 @@ use crate::expression::{BinaryOperator, ScalarExpression, TypeCast, UnaryOperato use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::{only_child, only_child_mut}; -use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::Operator; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; use crate::types::evaluator::BinaryEvaluatorRef; @@ -53,9 +52,6 @@ impl NormalizationRule for GuardMarkApply { while let (Operator::MarkApply(op), Childrens::Twins { left, .. }) = (&input.operator, input.childrens.as_ref()) { - if matches!(op.kind, MarkApplyKind::InnerJoin) { - break; - } markers.push(*op.output_column()); input = left; } diff --git a/src/optimizer/rule/normalization/mod.rs b/src/optimizer/rule/normalization/mod.rs index 33d18fa7..6e80de89 100644 --- a/src/optimizer/rule/normalization/mod.rs +++ b/src/optimizer/rule/normalization/mod.rs @@ -121,6 +121,8 @@ impl NormalizationRuleRootTag { match operator { Operator::Aggregate(_) => Some(Self::Aggregate), Operator::MarkApply(_) => Some(Self::MarkApply), + // Built after the rules that could rewrite it; nothing normalizes it further. + Operator::IndexProbeJoin(_) => None, Operator::ScalarQueryInit(_) => Some(Self::Any), Operator::Filter(_) => Some(Self::Filter), Operator::Join(_) => Some(Self::Join), diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index e5e77807..1046d498 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -20,8 +20,9 @@ use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::rule::normalization::pushdown_predicates::{index_covering, is_index_covers}; use crate::planner::operator::filter::FilterOperator; +use crate::planner::operator::index_probe_join::IndexProbeJoinOperator; use crate::planner::operator::join::{JoinCondition, JoinType}; -use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyOperator, MarkApplyQuantifier}; +use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyQuantifier}; use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::{ @@ -46,9 +47,7 @@ impl NormalizationRule for ParameterizeMarkApply { _loader: Option<&StatisticMetaLoader<'_>>, ) -> Result { let (op, new_probe) = match (&mut plan.operator, plan.childrens.as_mut()) { - (Operator::MarkApply(op), Childrens::Twins { left, right }) - if !matches!(op.kind, MarkApplyKind::InnerJoin) => - { + (Operator::MarkApply(op), Childrens::Twins { left, right }) => { let probe = find_parameterized_probe( op.kind, op.predicates(), @@ -97,7 +96,7 @@ fn find_parameterized_probe( Ok(None) } } - MarkApplyKind::InnerJoin | MarkApplyKind::Quantified(MarkApplyQuantifier::All) => Ok(None), + MarkApplyKind::Quantified(MarkApplyQuantifier::All) => Ok(None), } } @@ -447,7 +446,7 @@ fn use_probe_index(plan: &mut LogicalPlan, position: usize, arena: &PlanArena<'_ } } -/// Builds the inner `MarkApply` probing `right` with `index` for every `left` row. `on` holds +/// Builds the `IndexProbeJoin` probing `right` with `index` for every `left` row. `on` holds /// the join keys bound to each side; keys not used by the probe are checked after it. pub(crate) fn probe_join( mut left: LogicalPlan, @@ -482,13 +481,13 @@ pub(crate) fn probe_join( })); } let mut apply = LogicalPlan::new( - Operator::MarkApply(MarkApplyOperator::new_inner_join(predicates, probe)), + Operator::IndexProbeJoin(IndexProbeJoinOperator::new(probe, predicates)), Childrens::Twins { left: Box::new(left), right: Box::new(right), }, ); - let mut option = PhysicalOption::new(PlanImpl::MarkApply, SortOption::Follow, row_count); + let mut option = PhysicalOption::new(PlanImpl::IndexProbeJoin, SortOption::Follow, row_count); option.lookup_work = Some(index.lookup_work); apply.physical_option = Some(option); Ok(apply) @@ -751,7 +750,6 @@ mod inner_join_tests { use crate::optimizer::rule::normalization::join_reorder::GreedyJoinReorder; use crate::planner::operator::join::JoinOperator; use crate::planner::operator::limit::LimitOperator; - use crate::planner::operator::mark_apply::MarkApplyKind; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::WorkCost; use crate::planner::TableArenaCell; @@ -1095,7 +1093,7 @@ mod inner_join_tests { } assert!(ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); let option = plan.physical_option.as_ref().unwrap(); - assert_eq!(option.plan, PlanImpl::MarkApply); + assert_eq!(option.plan, PlanImpl::IndexProbeJoin); assert_eq!(option.cost, None); assert_eq!(plan.estimated_rows(), rows); let Childrens::Twins { right, .. } = plan.childrens.as_ref() else { @@ -1193,7 +1191,7 @@ mod inner_join_tests { let Childrens::Twins { left, .. } = join.childrens.as_ref() else { unreachable!() }; - assert!(matches!(left.operator, Operator::MarkApply(_))); + assert!(matches!(left.operator, Operator::IndexProbeJoin(_))); Ok(()) } @@ -1272,15 +1270,11 @@ mod inner_join_tests { } else { &plan }; - let Operator::MarkApply(apply) = &apply_plan.operator else { - panic!("expected inner join apply") + let Operator::IndexProbeJoin(apply) = &apply_plan.operator else { + panic!("expected index probe join") }; - assert_eq!(apply.kind, MarkApplyKind::InnerJoin); - assert!(apply.predicates().is_empty()); - assert_eq!( - apply.parameterized_probe().copied(), - Some(if swap { r } else { l }) - ); + assert!(apply.predicates.is_empty()); + assert_eq!(apply.probe, if swap { r } else { l }); let Childrens::Twins { left, right } = apply_plan.childrens.as_ref() else { unreachable!() }; @@ -1352,13 +1346,12 @@ mod inner_join_tests { let mut plan = join(left, right, vec![(l0, r1), (l1, r2)], None); assert!(ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); - let Operator::MarkApply(apply) = &plan.operator else { - panic!("expected apply without projection") + let Operator::IndexProbeJoin(apply) = &plan.operator else { + panic!("expected index probe join without projection") }; - assert_eq!(apply.kind, MarkApplyKind::InnerJoin); - assert!(apply.predicates().is_empty()); + assert!(apply.predicates.is_empty()); assert_eq!( - arena.expression(*apply.parameterized_probe().unwrap()), + arena.expression(apply.probe), &ScalarExpression::Tuple(vec![constant, l1, l0]) ); assert_eq!((position(l0, &arena), position(l1, &arena)), (0, 1)); @@ -1467,17 +1460,17 @@ mod inner_join_tests { let Childrens::Only(apply_plan) = project.childrens.as_ref() else { unreachable!() }; - let Operator::MarkApply(apply) = &apply_plan.operator else { - panic!("expected apply") + let Operator::IndexProbeJoin(apply) = &apply_plan.operator else { + panic!("expected index probe join") }; assert_eq!(apply_plan.estimated_rows(), Some(30)); assert_eq!( apply_plan.physical_option.as_ref().unwrap().plan, - PlanImpl::MarkApply + PlanImpl::IndexProbeJoin ); - assert_eq!(apply.parameterized_probe(), Some(&r)); + assert_eq!(apply.probe, r); assert_eq!(position(r, &arena), 0); - assert!(apply.predicates().is_empty()); + assert!(apply.predicates.is_empty()); let Childrens::Twins { left, right } = apply_plan.childrens.as_ref() else { unreachable!() }; diff --git a/src/orm/mod.rs b/src/orm/mod.rs index 8f8dfa0b..2c91a07a 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -4438,7 +4438,7 @@ mod tests { inner_using_plan, concat!( "Projection [orm_unit_users.id] [Project => (Sort Option: Follow)] ", - "InnerJoinApply [MarkApply => (Sort Option: Follow)] ", + "IndexProbeJoin [IndexProbeJoin => (Sort Option: Follow)] ", "TableScan orm_unit_orders -> [orm_unit_orders.id] [SeqScan => (Sort Option: None)] ", "TableScan orm_unit_users -> [orm_unit_users.id] [IndexScan By pk_index => Probe ? Covered => (Sort Option: OrderBy: (orm_unit_users.id Asc Nulls Last) ignore_prefix_len: 0)]" ), diff --git a/src/planner/mod.rs b/src/planner/mod.rs index b54feb95..fe391cf3 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -372,11 +372,9 @@ impl LogicalPlan { } _ => unreachable!(), }, - Operator::MarkApply(op) - if matches!(op.kind, operator::mark_apply::MarkApplyKind::InnerJoin) => - { + Operator::IndexProbeJoin(_) => { let Childrens::Twins { left, right } = childrens else { - unreachable!("inner apply requires two inputs") + unreachable!("index probe join requires two inputs") }; let mut schema = left.output_schema(arena).clone(); schema.extend_from_slice(right.output_schema(arena)); diff --git a/src/planner/operator/index_probe_join.rs b/src/planner/operator/index_probe_join.rs new file mode 100644 index 00000000..51b3abb2 --- /dev/null +++ b/src/planner/operator/index_probe_join.rs @@ -0,0 +1,36 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::planner::ExprRef; +use kite_sql_serde_macros::ReferenceSerialization; +use std::fmt; +use std::fmt::Formatter; + +#[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] +pub struct IndexProbeJoinOperator { + pub probe: ExprRef, + pub predicates: Vec, +} + +impl IndexProbeJoinOperator { + pub(crate) fn new(probe: ExprRef, predicates: Vec) -> Self { + Self { probe, predicates } + } +} + +impl fmt::Display for IndexProbeJoinOperator { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "IndexProbeJoin") + } +} diff --git a/src/planner/operator/mark_apply.rs b/src/planner/operator/mark_apply.rs index ec2defbd..0b39fa69 100644 --- a/src/planner/operator/mark_apply.rs +++ b/src/planner/operator/mark_apply.rs @@ -29,14 +29,13 @@ pub enum MarkApplyQuantifier { pub enum MarkApplyKind { Exists, Quantified(MarkApplyQuantifier), - InnerJoin, } #[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] pub struct MarkApplyOperator { pub kind: MarkApplyKind, pub predicates: Vec, - output_column: Option, + output_column: ColumnRef, pub parameterized_probe: Option, pub(crate) materialize: bool, pub(crate) guard: Option, @@ -47,7 +46,7 @@ impl MarkApplyOperator { Self { kind: MarkApplyKind::Exists, predicates, - output_column: Some(output_column), + output_column, parameterized_probe: None, materialize: false, guard: None, @@ -81,7 +80,7 @@ impl MarkApplyOperator { Self { kind: MarkApplyKind::Quantified(quantifier), predicates, - output_column: Some(output_column), + output_column, parameterized_probe: None, materialize: false, guard: None, @@ -132,21 +131,7 @@ impl MarkApplyOperator { } pub fn output_column(&self) -> &ColumnRef { - self.output_column - .as_ref() - .expect("marker apply output column") - } - - // The RHS must be a full primary/unique-key point probe, yielding at most one row. - pub(crate) fn new_inner_join(predicates: Vec, probe: ExprRef) -> Self { - Self { - kind: MarkApplyKind::InnerJoin, - predicates, - output_column: None, - parameterized_probe: Some(probe), - materialize: false, - guard: None, - } + &self.output_column } pub fn parameterized_probe(&self) -> Option<&ExprRef> { @@ -161,7 +146,6 @@ impl MarkApplyOperator { impl fmt::Display for MarkApplyOperator { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self.kind { - MarkApplyKind::InnerJoin => write!(f, "InnerJoinApply"), MarkApplyKind::Exists => write!(f, "MarkExistsApply"), MarkApplyKind::Quantified(MarkApplyQuantifier::Any) => write!(f, "MarkAnyApply"), MarkApplyKind::Quantified(MarkApplyQuantifier::All) => write!(f, "MarkAllApply"), diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 2d854497..320cd7d4 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -33,6 +33,7 @@ pub mod drop_table; pub mod drop_view; pub mod filter; pub mod function_scan; +pub mod index_probe_join; pub mod insert; pub mod join; pub mod limit; @@ -81,6 +82,7 @@ use crate::planner::operator::drop_index::DropIndexOperator; use crate::planner::operator::drop_table::DropTableOperator; use crate::planner::operator::drop_view::DropViewOperator; use crate::planner::operator::function_scan::FunctionScanOperator; +use crate::planner::operator::index_probe_join::IndexProbeJoinOperator; use crate::planner::operator::insert::InsertOperator; use crate::planner::operator::join::JoinCondition; use crate::planner::operator::set_membership::SetMembershipOperator; @@ -143,6 +145,8 @@ pub enum Operator { #[cfg(feature = "copy")] CopyToFile(CopyToFileOperator), Window(window::WindowOperator), + // Appended to keep the serialized indices of earlier variants stable. + IndexProbeJoin(IndexProbeJoinOperator), } #[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] @@ -291,6 +295,35 @@ impl PhysicalOption { &self.sort_option } + /// Probes `right` once per `left` row; without a planned `lookup_work`, one probe costs + /// the right input's startup plus resetting it and reading the matched row. + fn index_probe_work( + &self, + left: &LogicalPlan, + right: &LogicalPlan, + rows: usize, + cost: &WorkCost, + ) -> Option { + let right_cost = right.physical_option.as_ref()?.cost?; + let probed = InputCost { + rows: 1, + width: right_cost.width, + work: right_cost.startup?, + }; + let lookup_work = self.lookup_work.unwrap_or_else(|| { + probed + .work + .saturating_add(PROBE_RESET_WORK) + .saturating_add(rows_work(probed.rows, probed.width)) + }); + Some(JoinImpl::IndexProbe { lookup_work }.work( + &input_cost(left)?, + &probed, + rows, + cost.width, + )) + } + /// Join formulas: RFC 0001 §4.2 (`rfc/0001-greedy-join-reorder-owns-join-decisions.md`); /// probe `lookup_work`: §4.3. pub(crate) fn calculate_work( @@ -308,25 +341,13 @@ impl PhysicalOption { (PlanImpl::MarkApply, Operator::MarkApply(op), Childrens::Twins { left, right }) if op.parameterized_probe().is_some() && !op.materialize => { - let right_cost = right.physical_option.as_ref()?.cost?; - let probed = InputCost { - rows: 1, - width: right_cost.width, - work: right_cost.startup?, - }; - let lookup_work = self.lookup_work.unwrap_or_else(|| { - probed - .work - .saturating_add(PROBE_RESET_WORK) - .saturating_add(rows_work(probed.rows, probed.width)) - }); - Some(JoinImpl::IndexProbe { lookup_work }.work( - &input_cost(left)?, - &probed, - rows?, - cost.width, - )) + self.index_probe_work(left, right, rows?, cost) } + ( + PlanImpl::IndexProbeJoin, + Operator::IndexProbeJoin(_), + Childrens::Twins { left, right }, + ) => self.index_probe_work(left, right, rows?, cost), (PlanImpl::HashJoin, Operator::Join(_), Childrens::Twins { left, right }) => Some( JoinImpl::Hash.work(&input_cost(left)?, &input_cost(right)?, rows?, cost.width), ), @@ -441,6 +462,12 @@ impl PhysicalOption { Some(left.saturating_add(right)) }) } + ( + PlanImpl::IndexProbeJoin, + Operator::IndexProbeJoin(_), + Childrens::Twins { left, right }, + ) => startup_work(left) + .and_then(|left| startup_work(right).map(|right| left.saturating_add(right))), (PlanImpl::ScalarQueryInit, _, Childrens::Twins { left, right }) => startup_work(left) .and_then(|left| work(right).map(|right| left.saturating_add(right))), _ => None, @@ -504,6 +531,8 @@ pub enum PlanImpl { CopyToFile, Analyze, Window, + // Appended to keep the serialized indices of earlier variants stable. + IndexProbeJoin, } impl Explain for ColumnRef { @@ -546,6 +575,7 @@ macro_rules! impl_display_explain { impl_display_explain!( MarkApplyOperator, + IndexProbeJoinOperator, ScalarSubqueryOperator, FunctionScanOperator, LimitOperator, @@ -575,6 +605,7 @@ impl Explain for Operator { Operator::Aggregate(op) => Explain::fmt(op, arena, f), Operator::ScalarQueryInit(op) => Explain::fmt(op, arena, f), Operator::MarkApply(op) => Explain::fmt(op, arena, f), + Operator::IndexProbeJoin(op) => Explain::fmt(op, arena, f), Operator::Filter(op) => Explain::fmt(op, arena, f), Operator::Join(op) => Explain::fmt(op, arena, f), Operator::Project(op) => Explain::fmt(op, arena, f), @@ -968,6 +999,7 @@ impl Explain for PlanImpl { PlanImpl::StreamDistinct => f.write_str("StreamDistinct"), PlanImpl::ScalarQueryInit => f.write_str("ScalarQueryInit"), PlanImpl::MarkApply => f.write_str("MarkApply"), + PlanImpl::IndexProbeJoin => f.write_str("IndexProbeJoin"), PlanImpl::Filter => f.write_str("Filter"), PlanImpl::HashJoin => f.write_str("HashJoin"), PlanImpl::NestLoopJoin => f.write_str("NestLoopJoin"), @@ -1194,6 +1226,7 @@ mod tests { (PlanImpl::StreamDistinct, "StreamDistinct"), (PlanImpl::ScalarQueryInit, "ScalarQueryInit"), (PlanImpl::MarkApply, "MarkApply"), + (PlanImpl::IndexProbeJoin, "IndexProbeJoin"), (PlanImpl::Filter, "Filter"), (PlanImpl::HashJoin, "HashJoin"), (PlanImpl::NestLoopJoin, "NestLoopJoin"), diff --git a/src/planner/operator/visitor.rs b/src/planner/operator/visitor.rs index 5cbfa351..095bf79e 100644 --- a/src/planner/operator/visitor.rs +++ b/src/planner/operator/visitor.rs @@ -109,6 +109,14 @@ pub trait OperatorVisitor<'a>: Sized { Ok(()) } + fn visit_index_probe_join( + &mut self, + _op: &'a IndexProbeJoinOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { + Ok(()) + } + fn visit_filter( &mut self, _op: &'a FilterOperator, @@ -384,6 +392,18 @@ impl<'a, V: ExprVisitor, A: MetaArena + ?Sized> OperatorVisitor<'a> Ok(()) } + fn visit_index_probe_join( + &mut self, + op: &'a IndexProbeJoinOperator, + _children: &mut ChildrenVisit<'a>, + ) -> Result<(), DatabaseError> { + ExprVisitor::visit(self.visitor, op.probe, self.arena)?; + for expr in &op.predicates { + ExprVisitor::visit(self.visitor, *expr, self.arena)?; + } + Ok(()) + } + fn visit_filter( &mut self, op: &'a FilterOperator, @@ -539,6 +559,7 @@ pub fn walk_operator<'a, V: OperatorVisitor<'a>>( Operator::Aggregate(op) => visitor.visit_aggregate(op, children), Operator::ScalarQueryInit(op) => visitor.visit_scalar_query_init(op, children), Operator::MarkApply(op) => visitor.visit_mark_apply(op, children), + Operator::IndexProbeJoin(op) => visitor.visit_index_probe_join(op, children), Operator::Filter(op) => visitor.visit_filter(op, children), Operator::Join(op) => visitor.visit_join(op, children), Operator::Project(op) => visitor.visit_project(op, children), @@ -654,6 +675,7 @@ pub(crate) mod tests { value: expr(0), }), Operator::MarkApply(mark_apply), + Operator::IndexProbeJoin(IndexProbeJoinOperator::new(expr(16), vec![expr(3)])), Operator::Filter(FilterOperator { predicate: expr(5), is_optimized: false, @@ -1019,7 +1041,7 @@ pub(crate) mod tests { for operator in &operators { visitor.visit_operator(operator, None, &mut ChildrenVisit::empty())?; } - assert_eq!(counter.0, 22); // Includes the Values row expression. + assert_eq!(counter.0, 24); // Includes the Values row expression. Ok(()) } diff --git a/src/planner/operator/visitor_mut.rs b/src/planner/operator/visitor_mut.rs index c8bf54f8..4b9166be 100644 --- a/src/planner/operator/visitor_mut.rs +++ b/src/planner/operator/visitor_mut.rs @@ -114,6 +114,14 @@ pub trait OperatorVisitorMut<'a>: Sized { Ok(()) } + fn visit_index_probe_join( + &mut self, + _op: &'a mut IndexProbeJoinOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { + Ok(()) + } + fn visit_filter( &mut self, _op: &'a mut FilterOperator, @@ -401,6 +409,18 @@ impl<'a, V: ExprVisitorMut, A: MetaArena + ?Sized> OperatorVisitorMut<'a> Ok(()) } + fn visit_index_probe_join( + &mut self, + op: &'a mut IndexProbeJoinOperator, + _children: &mut ChildrenVisitMut<'a>, + ) -> Result<(), DatabaseError> { + ExprVisitorMut::visit(self.visitor, &mut op.probe, self.arena)?; + for expr in &mut op.predicates { + ExprVisitorMut::visit(self.visitor, expr, self.arena)?; + } + Ok(()) + } + fn visit_filter( &mut self, op: &'a mut FilterOperator, @@ -566,6 +586,7 @@ pub fn walk_mut_operator<'a, V: OperatorVisitorMut<'a>>( Operator::Aggregate(op) => visitor.visit_aggregate(op, children), Operator::ScalarQueryInit(op) => visitor.visit_scalar_query_init(op, children), Operator::MarkApply(op) => visitor.visit_mark_apply(op, children), + Operator::IndexProbeJoin(op) => visitor.visit_index_probe_join(op, children), Operator::Filter(op) => visitor.visit_filter(op, children), Operator::Join(op) => visitor.visit_join(op, children), Operator::Project(op) => visitor.visit_project(op, children), @@ -694,7 +715,7 @@ mod tests { visitor.visit_operator(operator, None, &mut ChildrenVisitMut::empty())?; } } - assert_eq!(counter.0, 22); // Includes the Values row expression. + assert_eq!(counter.0, 24); // Includes the Values row expression. Ok(()) } diff --git a/tests/slt/join.slt b/tests/slt/join.slt index 9075001c..c0f264ac 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -782,7 +782,7 @@ analyze table jp_small; query T explain select jp_small.id, jp_big.c2 from jp_small join jp_big on jp_small.c1 = jp_big.id; ---- -Projection [jp_small.id, jp_big.c2] [Project => (Sort Option: Follow)] InnerJoinApply [MarkApply => (Sort Option: Follow)] TableScan jp_small -> [jp_small.id, jp_small.c1] [SeqScan => (Sort Option: None)] TableScan jp_big -> [jp_big.id, jp_big.c2] [IndexScan By pk_index => Probe ? => (Sort Option: OrderBy: (jp_big.id Asc Nulls Last) ignore_prefix_len: 0)] +Projection [jp_small.id, jp_big.c2] [Project => (Sort Option: Follow)] IndexProbeJoin [IndexProbeJoin => (Sort Option: Follow)] TableScan jp_small -> [jp_small.id, jp_small.c1] [SeqScan => (Sort Option: None)] TableScan jp_big -> [jp_big.id, jp_big.c2] [IndexScan By pk_index => Probe ? => (Sort Option: OrderBy: (jp_big.id Asc Nulls Last) ignore_prefix_len: 0)] query II rowsort select jp_small.id, jp_big.c2 from jp_small join jp_big on jp_small.c1 = jp_big.id; From 317acdfffd414b28793a4fd1b0c8eb1dd735c312 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 15:03:58 +0800 Subject: [PATCH 51/62] refactor(spill): encode TupleImpl through the Option and Vec codecs --- src/execution/spill/codec.rs | 75 +++++++++++++++--------------------- src/types/tuple.rs | 10 ++++- 2 files changed, 40 insertions(+), 45 deletions(-) diff --git a/src/execution/spill/codec.rs b/src/execution/spill/codec.rs index 4f8c98dd..dce65171 100644 --- a/src/execution/spill/codec.rs +++ b/src/execution/spill/codec.rs @@ -210,12 +210,7 @@ impl SpillCodec for DataValue { impl SpillCodec for Vec { fn encode(&self, writer: &mut W) -> Result<(), DatabaseError> { - let len: u32 = self.len().try_into()?; - writer.write_all(&len.to_le_bytes())?; - for value in self { - value.encode(writer)?; - } - Ok(()) + encode_seq(self.len(), self.iter(), writer) } fn decode(reader: &mut R) -> Result { @@ -230,11 +225,7 @@ impl SpillCodec for Vec { } fn encoded_len(&self) -> Result { - let _: u32 = self.len().try_into()?; - self.iter().try_fold( - size_of::(), - |len, value| Ok(len + value.encoded_len()?), - ) + seq_encoded_len(self.len(), self.iter()) } fn estimated_size(&self) -> usize { @@ -255,10 +246,7 @@ impl SpillCodec for Option { writer.write_all(&[1])?; value.encode(writer) } - None => { - writer.write_all(&[0])?; - Ok(()) - } + None => Ok(writer.write_all(&[0])?), } } @@ -330,23 +318,10 @@ impl SpillCodec for Tuple { } impl SpillCodec for TupleImpl { + // Same layout as `Tuple`, so a spilled Concat decodes back as a flat tuple. fn encode(&self, writer: &mut W) -> Result<(), DatabaseError> { - if let TupleImpl::Tuple(tuple) = self { - return tuple.encode(writer); - } - match self.pk() { - Some(pk) => { - writer.write_all(&[1])?; - pk.encode(writer)?; - } - None => writer.write_all(&[0])?, - } - let len: u32 = self.len().try_into()?; - writer.write_all(&len.to_le_bytes())?; - for value in self.iter() { - value.encode(writer)?; - } - Ok(()) + self.pk_slot().encode(writer)?; + encode_seq(self.len(), self.iter(), writer) } fn decode(reader: &mut R) -> Result { @@ -354,18 +329,7 @@ impl SpillCodec for TupleImpl { } fn encoded_len(&self) -> Result { - if let TupleImpl::Tuple(tuple) = self { - return tuple.encoded_len(); - } - let _: u32 = self.len().try_into()?; - let pk_len = match self.pk() { - Some(pk) => 1 + pk.encoded_len()?, - None => 1, - }; - self.iter() - .try_fold(pk_len + size_of::(), |len, value| { - Ok(len + value.encoded_len()?) - }) + Ok(self.pk_slot().encoded_len()? + seq_encoded_len(self.len(), self.iter())?) } fn estimated_size(&self) -> usize { @@ -383,6 +347,31 @@ impl SpillCodec for TupleImpl { } } +#[inline] +fn encode_seq<'v, T: SpillCodec + 'v, W: Write>( + len: usize, + values: impl IntoIterator, + writer: &mut W, +) -> Result<(), DatabaseError> { + let len: u32 = len.try_into()?; + writer.write_all(&len.to_le_bytes())?; + values + .into_iter() + .try_for_each(|value| value.encode(writer)) +} + +#[inline] +fn seq_encoded_len<'v, T: SpillCodec + 'v>( + len: usize, + values: impl IntoIterator, +) -> Result { + let _: u32 = len.try_into()?; + values.into_iter().try_fold( + size_of::(), + |len, value| Ok(len + value.encoded_len()?), + ) +} + fn estimated_dynamic_value_size(value: &DataValue) -> usize { match value { DataValue::Utf8 { value, .. } => value.capacity(), diff --git a/src/types/tuple.rs b/src/types/tuple.rs index e89054e6..e1621587 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -345,9 +345,15 @@ impl TupleImpl { #[inline] pub fn pk(&self) -> Option<&TupleId> { + self.pk_slot().as_ref() + } + + /// The stored `Option` itself, for callers that encode it as a whole. + #[inline] + pub(crate) fn pk_slot(&self) -> &Option { match self { - TupleImpl::Tuple(tuple) => tuple.pk.as_ref(), - TupleImpl::Concat(concat) => concat.pk.as_ref(), + TupleImpl::Tuple(tuple) => &tuple.pk, + TupleImpl::Concat(concat) => &concat.pk, } } From 6c4f30befd6e8218e0cde3383bbee7f781dc7acc Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 15:03:58 +0800 Subject: [PATCH 52/62] refactor(expression): split IN point checks so the range detacher scans args once --- src/expression/range_detacher.rs | 11 +++++--- src/expression/simplify.rs | 44 +++++++++++++++++--------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/expression/range_detacher.rs b/src/expression/range_detacher.rs index ac7a916e..1eff00a0 100644 --- a/src/expression/range_detacher.rs +++ b/src/expression/range_detacher.rs @@ -523,7 +523,10 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { args, .. } => { - let Some(column) = expr.constant_in_column(&args, self.arena) else { + let Some((column, ty)) = expr + .in_point_column(self.arena) + .filter(|_| !args.is_empty()) + else { return Ok(None); }; if !self.matches_column(column) { @@ -531,8 +534,8 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { } let mut values = Vec::with_capacity(args.len()); for arg in args { - let ScalarExpression::Constant(value) = self.arena.expression(arg) else { - unreachable!(); + let Some(value) = arg.in_point(ty, self.arena) else { + return Ok(None); }; if value.is_null() { // NOT IN with NULL cannot be true in a Filter; positive @@ -548,7 +551,7 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { if negated { return Ok(None); } - // constant_in_column guarantees comparable values in one domain. + // `in_point` keeps every value in the column's type, so they are comparable. values.sort_unstable_by(|left, right| left.partial_cmp(right).unwrap()); values.dedup(); let range = match values.len() { diff --git a/src/expression/simplify.rs b/src/expression/simplify.rs index b7627513..18041394 100644 --- a/src/expression/simplify.rs +++ b/src/expression/simplify.rs @@ -203,7 +203,9 @@ impl ExprVisitorMut for Simplify { // Keep point lists compact without changing pairwise coercion or // repeated evaluation of non-column operands in the old rewrite. if args.len() > IN_EXPANSION_LIMIT - && arg_expr.constant_in_column(args, arena).is_some() + && arg_expr.in_point_column(arena).is_some_and(|(_, ty)| { + args.iter().all(|arg| arg.in_point(ty, arena).is_some()) + }) { return Ok(true); } @@ -511,11 +513,10 @@ impl Simplify { } impl ExprRef { - pub(crate) fn constant_in_column( + pub(crate) fn in_point_column( mut self, - args: &[ExprRef], arena: &A, - ) -> Option { + ) -> Option<(ColumnRef, &LogicalType)> { while let ScalarExpression::Alias { expr, .. } = arena.expression(self) { self = *expr; } @@ -523,23 +524,26 @@ impl ExprRef { return None; }; let ty = arena.column(*column).datatype(); - // Floating key order need not agree with SQL equality for NaNs/zeros. - if args.is_empty() - || matches!( - ty, - LogicalType::Tuple(_) | LogicalType::Float | LogicalType::Double - ) - { - return None; + (!matches!( + ty, + LogicalType::Tuple(_) | LogicalType::Float | LogicalType::Double + )) + .then_some((*column, ty)) + } + + pub(crate) fn in_point<'v, A: MetaArena + ?Sized>( + self, + ty: &LogicalType, + arena: &'v A, + ) -> Option<&'v DataValue> { + match arena.expression(self) { + ScalarExpression::Constant(value) + if !value.has_parameter() && (value.is_null() || value.logical_type() == *ty) => + { + Some(value) + } + _ => None, } - args.iter() - .all(|arg| match arena.expression(*arg) { - ScalarExpression::Constant(value) if !value.has_parameter() => { - value.is_null() || value.logical_type() == *ty - } - _ => false, - }) - .then_some(*column) } pub(crate) fn unpack_val(self, arena: &A) -> Option { From 9f0a00b0154dcabaa51b44fd27a1ecc0f110d0f7 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 15:03:58 +0800 Subject: [PATCH 53/62] perf(optimizer): reuse a histogram buffer across the hint pass and trim redundant tests --- src/execution/dml/analyze.rs | 29 +- src/optimizer/core/histogram.rs | 117 +++++-- src/optimizer/core/rule.rs | 6 + src/optimizer/core/statistics_meta.rs | 12 +- src/optimizer/heuristic/optimizer.rs | 139 +------- .../rule/implementation/dql/filter.rs | 297 ++++-------------- src/optimizer/rule/implementation/dql/join.rs | 1 + .../rule/implementation/dql/mark_apply.rs | 1 + src/optimizer/rule/implementation/dql/sort.rs | 1 + .../rule/implementation/dql/table_scan.rs | 9 +- .../rule/implementation/dql/top_k.rs | 1 + .../rule/implementation/dql/window.rs | 1 + src/optimizer/rule/implementation/macros.rs | 1 + src/optimizer/rule/implementation/mod.rs | 21 +- 14 files changed, 238 insertions(+), 398 deletions(-) diff --git a/src/execution/dml/analyze.rs b/src/execution/dml/analyze.rs index 862d9e98..5a7f54bd 100644 --- a/src/execution/dml/analyze.rs +++ b/src/execution/dml/analyze.rs @@ -406,8 +406,12 @@ mod test { assert_eq!(unindexed.values_len(), 0); assert_eq!(unindexed.null_count(), 101); assert_eq!(unindexed.distinct_values_len(), 0); - let expected_frequency = - loader.collect_count(&table_name, 1, &Range::Eq(DataValue::Int32(7)))?; + let expected_frequency = loader.collect_count( + &table_name, + 1, + &Range::Eq(DataValue::Int32(7)), + &mut Vec::new(), + )?; // Reload the stored parts instead of reusing ANALYZE's in-memory cache. drop(kite_sql); @@ -421,7 +425,12 @@ mod test { assert_eq!(meta.histogram().buckets_len(), buckets); } assert_eq!( - loader.collect_count(&table_name, 1, &Range::Eq(DataValue::Int32(7)))?, + loader.collect_count( + &table_name, + 1, + &Range::Eq(DataValue::Int32(7)), + &mut Vec::new() + )?, expected_frequency ); assert_eq!( @@ -540,7 +549,12 @@ mod test { assert!(loader.load(&table_name, 1)?.is_some()); assert_eq!( loader - .collect_count(&table_name, 1, &Range::Eq(DataValue::Int32(7)))? + .collect_count( + &table_name, + 1, + &Range::Eq(DataValue::Int32(7)), + &mut Vec::new() + )? .map(|(rows, _)| rows), Some(1) ); @@ -562,7 +576,12 @@ mod test { assert!(loader.load(&table_name, 1)?.is_some()); assert_eq!( loader - .collect_count(&table_name, 1, &Range::Eq(DataValue::Int32(7)))? + .collect_count( + &table_name, + 1, + &Range::Eq(DataValue::Int32(7)), + &mut Vec::new() + )? .map(|(rows, _)| rows), Some(1) ); diff --git a/src/optimizer/core/histogram.rs b/src/optimizer/core/histogram.rs index d8512daa..2eb9e5f7 100644 --- a/src/optimizer/core/histogram.rs +++ b/src/optimizer/core/histogram.rs @@ -28,7 +28,6 @@ use ordered_float::OrderedFloat; use std::borrow::Cow; use std::cmp; use std::collections::Bound; -use std::mem; use std::sync::OnceLock; const ANALYZE_STATISTICS_CONFIDENCE: f64 = 0.95; @@ -472,6 +471,7 @@ impl Histogram { ranges: &[Range], sketch: &CountMinSketch, top_n: &ColumnTopN, + buf: &mut Vec, ) -> Result { if self.buckets.is_empty() || ranges.is_empty() { return Ok(0); @@ -494,31 +494,24 @@ impl Histogram { let mut count = 0; let mut binary_i = 0; let mut bucket_i = 0; - let mut bucket_idxs = Vec::new(); - let mut buf = Vec::new(); while bucket_i < self.buckets.len() && binary_i < ranges.len() { let is_dummy = self._collect_count( ranges, &mut binary_i, &mut bucket_i, - &mut bucket_idxs, &mut count, sketch, top_n, comparator, - &mut buf, + buf, )?; if is_dummy { return Ok(0); } } - Ok(bucket_idxs - .iter() - .map(|idx| self.buckets[*idx].count as usize) - .sum::() - + count) + Ok(count) } #[allow(clippy::too_many_arguments)] @@ -527,7 +520,6 @@ impl Histogram { ranges: &[Range], binary_i: &mut usize, bucket_i: &mut usize, - bucket_idxs: &mut Vec, count: &mut usize, sketch: &CountMinSketch, top_n: &ColumnTopN, @@ -555,7 +547,9 @@ impl Histogram { && (is_under(comparator, &bucket.upper, max, false)? || is_eq(&bucket.upper, max)) { - bucket_idxs.push(mem::replace(bucket_i, *bucket_i + 1)); + // A fully covered bucket counts its rows, without the NULLs added above. + *count += bucket.count as usize; + *bucket_i += 1; } else if is_above(comparator, &bucket.lower, max, false)? { *binary_i += 1; } else if is_under(comparator, &bucket.upper, min, true)? { @@ -1020,12 +1014,22 @@ mod tests { max: Bound::Excluded(DataValue::Tuple(upper)), }; assert_eq!( - histogram.collect_count(&[scope(prefix.clone(), prefix.clone())], &sketch, &top_n)?, + histogram.collect_count( + &[scope(prefix.clone(), prefix.clone())], + &sketch, + &top_n, + &mut Vec::new() + )?, 100 ); let longer = vec![parameter(1), parameter(2), parameter(3)]; assert_eq!( - histogram.collect_count(&[scope(longer.clone(), longer)], &sketch, &top_n)?, + histogram.collect_count( + &[scope(longer.clone(), longer)], + &sketch, + &top_n, + &mut Vec::new() + )?, 10 ); let mut lower = prefix.clone(); @@ -1033,16 +1037,31 @@ mod tests { let mut upper = prefix.clone(); upper.push(parameter(4)); assert_eq!( - histogram.collect_count(&[scope(lower.clone(), prefix)], &sketch, &top_n)?, + histogram.collect_count( + &[scope(lower.clone(), prefix)], + &sketch, + &top_n, + &mut Vec::new() + )?, 25 ); assert_eq!( - histogram.collect_count(&[scope(lower.clone(), upper.clone())], &sketch, &top_n)?, + histogram.collect_count( + &[scope(lower.clone(), upper.clone())], + &sketch, + &top_n, + &mut Vec::new() + )?, 2 ); let full = DataValue::Tuple(vec![parameter(1), parameter(2), parameter(3), parameter(4)]); assert_eq!( - histogram.collect_count(&[Range::Eq(full.clone())], &sketch, &top_n)?, + histogram.collect_count( + &[Range::Eq(full.clone())], + &sketch, + &top_n, + &mut Vec::new() + )?, 1 ); assert_eq!( @@ -1052,7 +1071,8 @@ mod tests { max: Bound::Included(full), }], &sketch, - &top_n + &top_n, + &mut Vec::new() )?, 0 ); @@ -1060,7 +1080,7 @@ mod tests { lower.push(parameter(5)); upper.push(parameter(5)); assert_eq!( - histogram.collect_count(&[scope(lower, upper)], &sketch, &top_n)?, + histogram.collect_count(&[scope(lower, upper)], &sketch, &top_n, &mut Vec::new())?, 2 ); histogram.buckets[0].upper = DataValue::Tuple(vec![ @@ -1069,7 +1089,12 @@ mod tests { ]); let nested = vec![DataValue::Tuple(vec![parameter(1), parameter(2)])]; assert_eq!( - histogram.collect_count(&[scope(nested.clone(), nested)], &sketch, &top_n)?, + histogram.collect_count( + &[scope(nested.clone(), nested)], + &sketch, + &top_n, + &mut Vec::new() + )?, 100 ); Ok(()) @@ -1305,7 +1330,7 @@ mod tests { ), ] { assert_eq!( - histogram.collect_count(&[range], &sketch, &top_n)?, + histogram.collect_count(&[range], &sketch, &top_n, &mut Vec::new())?, expected ); } @@ -1353,6 +1378,7 @@ mod tests { ], &sketch, &top_n, + &mut Vec::new(), )?; assert_eq!(count_1, 9); @@ -1364,6 +1390,7 @@ mod tests { }], &sketch, &top_n, + &mut Vec::new(), )?; assert_eq!(count_2, 11); @@ -1375,6 +1402,7 @@ mod tests { }], &sketch, &top_n, + &mut Vec::new(), )?; assert_eq!(count_3, 7); @@ -1386,6 +1414,7 @@ mod tests { }], &sketch, &top_n, + &mut Vec::new(), )?; assert_eq!(count_4, 12); @@ -1397,6 +1426,7 @@ mod tests { }], &sketch, &top_n, + &mut Vec::new(), )?; assert_eq!(count_5, 8); @@ -1408,6 +1438,7 @@ mod tests { }], &sketch, &top_n, + &mut Vec::new(), )?; assert_eq!(count_6, 12); @@ -1419,6 +1450,7 @@ mod tests { }], &sketch, &top_n, + &mut Vec::new(), )?; assert_eq!(count_7, 13); @@ -1430,6 +1462,7 @@ mod tests { }], &sketch, &top_n, + &mut Vec::new(), )?; assert_eq!(count_8, 13); @@ -1441,6 +1474,7 @@ mod tests { }], &sketch, &top_n, + &mut Vec::new(), )?; assert_eq!(count_9, 13); @@ -1452,6 +1486,7 @@ mod tests { }], &sketch, &top_n, + &mut Vec::new(), )?; assert_eq!(count_10, 2); @@ -1463,6 +1498,7 @@ mod tests { }], &sketch, &top_n, + &mut Vec::new(), )?; assert_eq!(count_11, 2); @@ -1512,7 +1548,12 @@ mod tests { let top_n = ColumnTopN::default(); assert_eq!( - histogram.collect_count(&[Range::Eq(DataValue::Int32(1))], &sketch, &top_n)?, + histogram.collect_count( + &[Range::Eq(DataValue::Int32(1))], + &sketch, + &top_n, + &mut Vec::new() + )?, 70 ); @@ -1538,7 +1579,12 @@ mod tests { sketch.add(&DataValue::Int32(1), 1000); assert_eq!( - histogram.collect_count(&[Range::Eq(DataValue::Int32(1))], &sketch, &top_n)?, + histogram.collect_count( + &[Range::Eq(DataValue::Int32(1))], + &sketch, + &top_n, + &mut Vec::new() + )?, 70 ); @@ -1566,7 +1612,12 @@ mod tests { assert!(estimated_count.saturating_sub(error_bound) <= average_count); assert_eq!( - histogram.collect_count(&[Range::Eq(DataValue::Int32(7))], &sketch, &top_n)?, + histogram.collect_count( + &[Range::Eq(DataValue::Int32(7))], + &sketch, + &top_n, + &mut Vec::new() + )?, average_count ); @@ -1597,7 +1648,12 @@ mod tests { assert!(estimated_count.saturating_sub(error_bound) > average_count); assert_eq!( - histogram.collect_count(&[Range::Eq(DataValue::Int32(7))], &sketch, &top_n)?, + histogram.collect_count( + &[Range::Eq(DataValue::Int32(7))], + &sketch, + &top_n, + &mut Vec::new() + )?, estimated_count ); @@ -1631,7 +1687,12 @@ mod tests { assert!(expected < entry.count()); assert_eq!( - histogram.collect_count(&[Range::Eq(DataValue::Int32(7))], &sketch, &top_n)?, + histogram.collect_count( + &[Range::Eq(DataValue::Int32(7))], + &sketch, + &top_n, + &mut Vec::new() + )?, expected ); @@ -1658,13 +1719,13 @@ mod tests { min: Bound::Excluded(DataValue::Tuple(vec![DataValue::Int32(0)])), max: Bound::Excluded(DataValue::Tuple(vec![DataValue::Int32(8)])), }]; - let clean_count = histogram.collect_count(&ranges, &sketch, &top_n)?; + let clean_count = histogram.collect_count(&ranges, &sketch, &top_n, &mut Vec::new())?; sketch.increment(&DataValue::Tuple(vec![DataValue::Int32(0)])); sketch.increment(&DataValue::Tuple(vec![DataValue::Int32(8)])); assert_eq!( - histogram.collect_count(&ranges, &sketch, &top_n)?, + histogram.collect_count(&ranges, &sketch, &top_n, &mut Vec::new())?, clean_count ); diff --git a/src/optimizer/core/rule.rs b/src/optimizer/core/rule.rs index e6550b47..386cdbff 100644 --- a/src/optimizer/core/rule.rs +++ b/src/optimizer/core/rule.rs @@ -62,12 +62,18 @@ pub fn keep_best_physical_option( } } +#[derive(Default)] +pub struct HintState { + pub(crate) histogram_buf: Vec, +} + pub trait ImplementationRule: MatchPattern { fn update_best_option( &self, plan: &LogicalPlan, arena: &PlanArena, loader: &StatisticMetaLoader<'_>, + state: &mut HintState, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError>; } diff --git a/src/optimizer/core/statistics_meta.rs b/src/optimizer/core/statistics_meta.rs index 672fe6ed..27042787 100644 --- a/src/optimizer/core/statistics_meta.rs +++ b/src/optimizer/core/statistics_meta.rs @@ -56,6 +56,7 @@ impl<'a> StatisticMetaLoader<'a> { table_name: &TableName, index_id: IndexId, range: &Range, + buf: &mut Vec, ) -> Result, DatabaseError> { let Some(entry) = self.load(table_name, index_id)? else { return Ok(None); @@ -68,7 +69,7 @@ impl<'a> StatisticMetaLoader<'a> { entry .histogram() - .collect_count(ranges, entry.sketch(), entry.top_n()) + .collect_count(ranges, entry.sketch(), entry.top_n(), buf) .map(|rows| Some((rows, entry.histogram().distinct_values_len()))) } } @@ -228,11 +229,16 @@ mod tests { ); let loader = StatisticMetaLoader::new(&cache); assert_eq!( - loader.collect_count(&index.table_name, index.id, &Range::Eq(DataValue::Int32(7)))?, + loader.collect_count( + &index.table_name, + index.id, + &Range::Eq(DataValue::Int32(7)), + &mut Vec::new() + )?, Some((1, histogram.distinct_values_len())) ); assert_eq!( - loader.collect_count(&index.table_name, 1, &Range::Dummy)?, + loader.collect_count(&index.table_name, 1, &Range::Dummy, &mut Vec::new())?, None ); assert_eq!(histogram, index_statistics.histogram); diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index 56f91d47..2a9cefc7 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -14,7 +14,7 @@ use crate::errors::DatabaseError; use crate::optimizer::core::rule::{ - BestPhysicalOption, ImplementationRule, MatchPattern, NormalizationRule, + BestPhysicalOption, HintState, ImplementationRule, MatchPattern, NormalizationRule, }; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::heuristic::batch::{ @@ -82,6 +82,7 @@ impl<'a> HepOptimizer<'a> { self.implementation_index, &apply_no_sort_hints, &apply_no_stream_aggregate_hints, + &mut HintState::default(), arena, )?; } @@ -260,6 +261,7 @@ impl<'a> HepOptimizer<'a> { implementation_index: &ImplementationRuleIndex, inherited_sort_hints: &'plan ScanHintApplier<'plan>, inherited_stream_aggregate_hints: &'plan ScanHintApplier<'plan>, + state: &mut HintState, arena: &mut PlanArena, ) -> Result<(), DatabaseError> { if let Operator::TableScan(scan_op) = &mut plan.operator { @@ -284,6 +286,7 @@ impl<'a> HepOptimizer<'a> { implementation_index, child_sort_hints, child_stream_aggregate_hints, + state, arena, ), Childrens::Twins { left, right } => { @@ -293,6 +296,7 @@ impl<'a> HepOptimizer<'a> { implementation_index, child_sort_hints, child_stream_aggregate_hints, + state, arena, )?; Self::annotate_hints_and_physical_options( @@ -301,6 +305,7 @@ impl<'a> HepOptimizer<'a> { implementation_index, child_sort_hints, child_stream_aggregate_hints, + state, arena, ) } @@ -315,7 +320,7 @@ impl<'a> HepOptimizer<'a> { } else { let mut best_physical_option: BestPhysicalOption = None; for rule in implementation_index.for_matching_operator(&plan.operator) { - rule.update_best_option(plan, arena, loader, &mut best_physical_option)?; + rule.update_best_option(plan, arena, loader, state, &mut best_physical_option)?; } if let Some((option, _)) = best_physical_option { plan.physical_option = Some(option); @@ -683,32 +688,22 @@ impl ImplementationRuleIndex { #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::binder::{Binder, BinderContext}; - use crate::catalog::{ColumnCatalog, ColumnDesc}; use crate::db::DataBaseBuilder; use crate::errors::DatabaseError; use crate::expression::range_detacher::Range; use crate::expression::ScalarExpression; - use crate::optimizer::core::rule::ImplementationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::heuristic::batch::{HepBatch, HepBatchStrategy, HepMatchOrder}; - use crate::optimizer::heuristic::optimizer::{ - HepOptimizer, HepOptimizerPipeline, ImplementationRuleIndex, - }; + use crate::optimizer::heuristic::optimizer::{HepOptimizer, HepOptimizerPipeline}; use crate::optimizer::rule::implementation::ImplementationRuleImpl; use crate::optimizer::rule::normalization::NormalizationRuleImpl; use crate::planner::operator::filter::FilterOperator; - use crate::planner::operator::limit::LimitOperator; - use crate::planner::operator::mark_apply::MarkApplyOperator; - use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::sort::SortField; - use crate::planner::operator::{ - Operator, PhysicalOption, PlanImpl, RowCountOption, SortOption, - }; + use crate::planner::operator::{Operator, PlanImpl, SortOption}; use crate::planner::{Childrens, LogicalPlan, PlanArena, TableArenaCell}; - use crate::storage::{StatisticsMetaCache, Storage, Transaction}; + use crate::storage::{Storage, Transaction}; use crate::types::index::IndexLookup; use crate::types::value::DataValue; - use crate::types::LogicalType; use std::ops::Bound; use tempfile::TempDir; @@ -722,55 +717,6 @@ mod tests { plan } - #[test] - fn filter_fallback_and_row_preserving_fast_paths_follow_input_row_count( - ) -> Result<(), DatabaseError> { - let table_arena = TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - let mut plan = filter_chain(1, &mut arena); - let Childrens::Only(child) = plan.childrens.as_mut() else { - unreachable!() - }; - child.physical_option = Some(PhysicalOption::new( - PlanImpl::Dummy, - SortOption::None, - RowCountOption::Estimated { - rows: 12, - ndv: None, - }, - )); - let implementations = ImplementationRuleIndex::new(&[ImplementationRuleImpl::Filter]); - assert!(implementations - .direct_physical_option(&plan.operator) - .is_none()); - let cache = StatisticsMetaCache::default(); - let mut best = None; - ImplementationRuleImpl::Filter.update_best_option( - &plan, - &arena, - &StatisticMetaLoader::new(&cache), - &mut best, - )?; - plan.physical_option = best.map(|(option, _)| option); - assert_eq!( - plan.physical_option.as_ref().unwrap().row_count, - RowCountOption::Follow - ); - assert_eq!(plan.estimated_rows(), Some(12)); - - let marker = arena.alloc_column(ColumnCatalog::new( - "mark".to_string(), - true, - ColumnDesc::new(LogicalType::Boolean, None, false, None)?, - )); - let right = LogicalPlan::new(Operator::Dummy, Childrens::None); - let mut plan = MarkApplyOperator::build_exists(plan, right, marker, vec![]); - let implementations = ImplementationRuleIndex::new(&[ImplementationRuleImpl::MarkApply]); - plan.physical_option = implementations.direct_physical_option(&plan.operator); - assert_eq!(plan.estimated_rows(), Some(12)); - Ok(()) - } - #[test] fn local_rule_order_and_sibling_application() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); @@ -877,71 +823,6 @@ mod tests { Ok(()) } - #[test] - fn once_does_not_reapply_a_rule_to_its_rewritten_children() -> Result<(), DatabaseError> { - let table_arena = TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - for (strategy, expected_depth) in [ - (HepBatchStrategy::once_topdown(), 1), - (HepBatchStrategy::fix_point_topdown(1), 3), - ] { - let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::None); - for _ in 0..3 { - plan = LogicalPlan::new( - Operator::Project(ProjectOperator::new(Vec::new())), - Childrens::Only(Box::new(plan)), - ); - } - plan = LimitOperator::build(None, Some(5), plan); - let pipeline = HepOptimizerPipeline::builder() - .before_batch( - "Limit Project Transpose", - strategy, - &[NormalizationRuleImpl::LimitProjectTranspose], - ) - .build(); - let result = pipeline.instantiate(plan).find_best(None, &mut arena)?; - let mut cursor = &result; - for _ in 0..expected_depth { - assert!(matches!(cursor.operator, Operator::Project(_))); - cursor = cursor.childrens.only(); - } - assert!(matches!(cursor.operator, Operator::Limit(_))); - } - Ok(()) - } - - #[test] - fn bottomup_fix_point_respects_iteration_limit() -> Result<(), DatabaseError> { - let table_arena = TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - for (iterations, expected_depth) in [(0, 0), (1, 1), (2, 2), (30, 3)] { - let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::None); - for _ in 0..3 { - plan = LogicalPlan::new( - Operator::Project(ProjectOperator::new(Vec::new())), - Childrens::Only(Box::new(plan)), - ); - } - plan = LimitOperator::build(None, Some(5), plan); - let pipeline = HepOptimizerPipeline::builder() - .before_batch( - "Limit Project Transpose", - HepBatchStrategy::fix_point_bottomup(iterations), - &[NormalizationRuleImpl::LimitProjectTranspose], - ) - .build(); - let result = pipeline.instantiate(plan).find_best(None, &mut arena)?; - let mut cursor = &result; - for _ in 0..expected_depth { - assert!(matches!(cursor.operator, Operator::Project(_))); - cursor = cursor.childrens.only(); - } - assert!(matches!(cursor.operator, Operator::Limit(_))); - } - Ok(()) - } - #[test] fn test_find_best_selects_cheapest_scan() -> Result<(), DatabaseError> { let temp_dir = TempDir::new().expect("unable to create temporary working directory"); diff --git a/src/optimizer/rule/implementation/dql/filter.rs b/src/optimizer/rule/implementation/dql/filter.rs index d0c5e046..65390320 100644 --- a/src/optimizer/rule/implementation/dql/filter.rs +++ b/src/optimizer/rule/implementation/dql/filter.rs @@ -18,7 +18,7 @@ use crate::expression::visitor::ExprVisitor; use crate::expression::{BinaryOperator, ScalarExpression}; use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{ - keep_best_physical_option, BestPhysicalOption, ImplementationRule, MatchPattern, + keep_best_physical_option, BestPhysicalOption, HintState, ImplementationRule, MatchPattern, }; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::scan_column; @@ -47,6 +47,7 @@ impl ImplementationRule for FilterImplementation { plan: &LogicalPlan, arena: &PlanArena, loader: &StatisticMetaLoader<'_>, + _: &mut HintState, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { let rows = estimate_filter_rows(plan, arena, loader)?; @@ -212,15 +213,11 @@ mod tests { use crate::catalog::{ColumnCatalog, ColumnDesc}; use crate::expression::AliasType; use crate::optimizer::core::histogram::{HistogramMetaBuilder, DEFAULT_HLL_PRECISION}; - use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; - use crate::optimizer::rule::implementation::ImplementationRuleImpl; use crate::planner::operator::filter::FilterOperator; - use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::TableArenaCell; use crate::storage::StatisticsMetaCache; - use crate::types::index::{IndexInfo, IndexMeta, IndexType}; use crate::types::LogicalType; fn input(arena: &mut PlanArena, rows: usize) -> (LogicalPlan, [ExprRef; 2]) { @@ -301,117 +298,77 @@ mod tests { }) } + fn estimate( + arena: &PlanArena, + cache: &StatisticsMetaCache, + predicate: ExprRef, + input: &LogicalPlan, + ) -> Result, DatabaseError> { + let plan = FilterOperator::build(predicate, input.clone(), false); + estimate_filter_rows(&plan, arena, &StatisticMetaLoader::new(cache)) + } + #[test] - fn point_estimates_deduplicate_in_and_or_values() -> Result<(), DatabaseError> { + fn point_predicates_use_column_stats() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let (scan, keys) = input(&mut arena, 100); let cache = statistics(100, 0, 10); - let loader = StatisticMetaLoader::new(&cache); let eq = equal(&mut arena, keys[0], DataValue::Int32(1)); + let value = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(1))); + let reversed = binary(&mut arena, BinaryOperator::Eq, value, keys[0]); + let values = [1, 1, 2].map(DataValue::Int32); let list = in_list( &mut arena, keys[0], - &[ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Null, - ], + &[&values[..], &[DataValue::Null]].concat(), false, ); let union = binary(&mut arena, BinaryOperator::Or, eq, list); - for (predicate, expected) in [(eq, 10), (list, 20), (union, 20)] { - let plan = FilterOperator::build(predicate, scan.clone(), false); - assert_eq!( - estimate_filter_rows(&plan, &arena, &loader)?, - Some(expected) - ); - } - let value = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(1))); - let reversed = binary(&mut arena, BinaryOperator::Eq, value, keys[0]); - assert_eq!( - estimate_filter_rows( - &FilterOperator::build(reversed, scan.clone(), false), - &arena, - &loader - )?, - Some(10) - ); let many = in_list( &mut arena, keys[0], &(0..20).map(DataValue::Int32).collect::>(), false, ); - assert_eq!( - estimate_filter_rows(&FilterOperator::build(many, scan, false), &arena, &loader)?, - Some(100) - ); - Ok(()) - } - - #[test] - fn point_estimates_account_for_nulls_empty_inputs_and_missing_stats( - ) -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (scan, keys) = input(&mut arena, 100); - let eq = equal(&mut arena, keys[0], DataValue::Int32(1)); let null = equal(&mut arena, keys[0], DataValue::Null); - let mixed = in_list( - &mut arena, - keys[0], - &[DataValue::Null, DataValue::Int32(1)], - false, - ); - let all_null = in_list(&mut arena, keys[0], &[DataValue::Null], false); - for (non_null, nulls, ndv, expected) in [(75, 25, 5, 15), (0, 100, 0, 0), (0, 0, 0, 0)] { - let cache = statistics(non_null, nulls, ndv); - let loader = StatisticMetaLoader::new(&cache); - assert_eq!( - estimate_filter_rows( - &FilterOperator::build(eq, scan.clone(), false), - &arena, - &loader - )?, - Some(expected) - ); - assert_eq!( - estimate_filter_rows( - &FilterOperator::build(mixed, scan.clone(), false), - &arena, - &loader - )?, - Some(expected) - ); - for predicate in [null, all_null] { - assert_eq!( - estimate_filter_rows( - &FilterOperator::build(predicate, scan.clone(), false), - &arena, - &loader - )?, - Some(0) - ); - } + // Duplicates and NULLs do not count as points; selectivity is capped at 1. + for (predicate, rows) in [ + (eq, 10), + (reversed, 10), + (list, 20), + (union, 20), + (many, 100), + (null, 0), + ] { + assert_eq!(estimate(&arena, &cache, predicate, &scan)?, Some(rows)); } - let empty_cache = StatisticsMetaCache::default(); - let loader = StatisticMetaLoader::new(&empty_cache); - let plan = FilterOperator::build(eq, scan, false); - let mut best = None; - FilterImplementation.update_best_option(&plan, &arena, &loader, &mut best)?; - assert_eq!(best.unwrap().0.row_count, RowCountOption::Follow); - let (empty, _) = input(&mut arena, 0); - let cache = statistics(100, 0, 10); + // NULLs in the column scale the estimate by the non-NULL fraction. assert_eq!( - estimate_filter_rows( - &FilterOperator::build(eq, empty, false), - &arena, - &StatisticMetaLoader::new(&cache) - )?, - Some(0) + estimate(&arena, &statistics(75, 25, 5), eq, &scan)?, + Some(15) + ); + let (empty, _) = input(&mut arena, 0); + assert_eq!(estimate(&arena, &cache, eq, &empty)?, Some(0)); + + // A column renamed by a projection still resolves to the scan's statistics. + let alias = arena.alloc_expression(ScalarExpression::Alias { + expr: keys[0], + alias: AliasType::Name("alias".into()), + }); + let column = alias.output_column_ref(&mut arena); + let key = arena.alloc_expression(ScalarExpression::column_expr(column, 0)); + let mut project = LogicalPlan::new( + Operator::Project(ProjectOperator::new(vec![alias])), + Childrens::Only(Box::new(scan)), ); + project.physical_option = Some(PhysicalOption::new( + PlanImpl::Project, + SortOption::Follow, + RowCountOption::Follow, + )); + let aliased = equal(&mut arena, key, DataValue::Int32(1)); + assert_eq!(estimate(&arena, &cache, aliased, &project)?, Some(10)); Ok(()) } @@ -419,71 +376,27 @@ mod tests { fn unsupported_predicates_and_restricted_inputs_keep_follow() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); - let (mut scan, keys) = input(&mut arena, 100); + let (scan, keys) = input(&mut arena, 100); let cache = statistics(100, 0, 10); - let loader = StatisticMetaLoader::new(&cache); let eq = equal(&mut arena, keys[0], DataValue::Int32(1)); let other = equal(&mut arena, keys[1], DataValue::Int32(2)); let multi_column = binary(&mut arena, BinaryOperator::Or, eq, other); - let and = binary(&mut arena, BinaryOperator::And, eq, eq); let column_eq = binary(&mut arena, BinaryOperator::Eq, keys[0], keys[1]); let not_in = in_list(&mut arena, keys[0], &[DataValue::Int32(1)], true); let wrong_type = equal(&mut arena, keys[0], DataValue::Int64(1)); - let cast = arena.alloc_expression(ScalarExpression::TypeCast { - expr: keys[0], - ty: LogicalType::Integer, - evaluator: None, - }); - let cast_eq = equal(&mut arena, cast, DataValue::Int32(1)); - for predicate in [multi_column, and, column_eq, not_in, wrong_type, cast_eq] { - assert_eq!( - estimate_filter_rows( - &FilterOperator::build(predicate, scan.clone(), false), - &arena, - &loader - )?, - None - ); + for predicate in [multi_column, column_eq, not_in, wrong_type] { + assert_eq!(estimate(&arena, &cache, predicate, &scan)?, None); } - scan.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; - assert_eq!( - estimate_filter_rows( - &FilterOperator::build(eq, scan.clone(), false), - &arena, - &loader - )?, - None - ); - scan.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { - rows: 100, - ndv: None, - }; - let Operator::TableScan(op) = &mut scan.operator else { + + // Inputs already restricted by a limit or an earlier estimate are not reduced again. + let mut limited_scan = scan.clone(); + let Operator::TableScan(op) = &mut limited_scan.operator else { unreachable!() }; op.limit = (Some(1), Some(5)); - assert_eq!( - estimate_filter_rows( - &FilterOperator::build(eq, scan.clone(), false), - &arena, - &loader - )?, - None - ); - let limited = LogicalPlan::new( - Operator::Limit(LimitOperator { - offset: None, - limit: Some(5), - }), - Childrens::Only(Box::new(scan)), - ); - assert_eq!( - estimate_filter_rows(&FilterOperator::build(eq, limited, false), &arena, &loader)?, - None - ); - let (scan, _) = input(&mut arena, 100); - let mut estimated = FilterOperator::build(eq, scan, false); - estimated.physical_option = Some(PhysicalOption::new( + assert_eq!(estimate(&arena, &cache, eq, &limited_scan)?, None); + let mut filtered = FilterOperator::build(eq, scan.clone(), false); + filtered.physical_option = Some(PhysicalOption::new( PlanImpl::Filter, SortOption::Follow, RowCountOption::Estimated { @@ -491,94 +404,20 @@ mod tests { ndv: None, }, )); - assert_eq!( - estimate_filter_rows( - &FilterOperator::build(eq, estimated, false), - &arena, - &loader - )?, - None - ); - Ok(()) - } + assert_eq!(estimate(&arena, &cache, eq, &filtered)?, None); - #[test] - fn point_selectivity_is_not_applied_again_to_index_output() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (mut scan, keys) = input(&mut arena, 10); - let meta = arena.alloc_index(IndexMeta { - id: 1, - table_name: "t".into(), - name: "idx".into(), - column_ids: vec![0], - ty: IndexType::Normal, - pk_ty: LogicalType::Integer, - value_ty: LogicalType::Integer, - }); - scan.physical_option.as_mut().unwrap().plan = PlanImpl::IndexScan(Box::new(IndexInfo { - meta, - sort_option: SortOption::None, - lookup: None, - residual_predicate: None, - covered_deserializers: None, - cover_mapping: None, - sort_elimination_hint: None, - stream_aggregate_hint: None, - })); - let predicate = equal(&mut arena, keys[0], DataValue::Int32(1)); - let cache = statistics(100, 0, 10); - let mut plan = FilterOperator::build(predicate, scan, false); + // Without column statistics the rule falls back to the input row count. + let plan = FilterOperator::build(eq, scan, false); let mut best = None; + let empty_cache = StatisticsMetaCache::default(); FilterImplementation.update_best_option( &plan, &arena, - &StatisticMetaLoader::new(&cache), + &StatisticMetaLoader::new(&empty_cache), + &mut HintState::default(), &mut best, )?; - plan.physical_option = best.map(|(option, _)| option); - assert_eq!( - plan.physical_option.as_ref().unwrap().row_count, - RowCountOption::Follow - ); - assert_eq!(plan.estimated_rows(), Some(10)); - Ok(()) - } - - #[test] - fn direct_aliases_and_dynamic_implementation_path_use_column_stats() -> Result<(), DatabaseError> - { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let (scan, keys) = input(&mut arena, 100); - let alias = arena.alloc_expression(ScalarExpression::Alias { - expr: keys[0], - alias: AliasType::Name("alias".into()), - }); - let column = alias.output_column_ref(&mut arena); - let key = arena.alloc_expression(ScalarExpression::column_expr(column, 0)); - let mut project = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![alias])), - Childrens::Only(Box::new(scan)), - ); - project.physical_option = Some(PhysicalOption::new( - PlanImpl::Project, - SortOption::Follow, - RowCountOption::Follow, - )); - let eq = equal(&mut arena, key, DataValue::Int32(1)); - let plan = FilterOperator::build(eq, project, false); - let cache = statistics(100, 0, 10); - let loader = StatisticMetaLoader::new(&cache); - assert_eq!(estimate_filter_rows(&plan, &arena, &loader)?, Some(10)); - let pipeline = HepOptimizerPipeline::builder() - .implementations(&[ImplementationRuleImpl::Filter]) - .build(); - let mut plan = pipeline - .instantiate(plan) - .find_best(Some(&loader), &mut arena)?; - assert_eq!(plan.estimated_rows(), Some(10)); - assert_eq!(plan.estimated_startup_work(&mut arena)?, Some(0)); + assert_eq!(best.unwrap().0.row_count, RowCountOption::Follow); Ok(()) } } diff --git a/src/optimizer/rule/implementation/dql/join.rs b/src/optimizer/rule/implementation/dql/join.rs index b9671d25..e63d8641 100644 --- a/src/optimizer/rule/implementation/dql/join.rs +++ b/src/optimizer/rule/implementation/dql/join.rs @@ -39,6 +39,7 @@ impl ImplementationRule for JoinImplementation { plan: &LogicalPlan, _: &crate::planner::PlanArena, _: &StatisticMetaLoader<'_>, + _: &mut crate::optimizer::core::rule::HintState, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { let Operator::Join(operator) = &plan.operator else { diff --git a/src/optimizer/rule/implementation/dql/mark_apply.rs b/src/optimizer/rule/implementation/dql/mark_apply.rs index d5698e7a..ec778e06 100644 --- a/src/optimizer/rule/implementation/dql/mark_apply.rs +++ b/src/optimizer/rule/implementation/dql/mark_apply.rs @@ -40,6 +40,7 @@ impl ImplementationRule for MarkApplyImplementation { plan: &LogicalPlan, _: &PlanArena, _: &StatisticMetaLoader<'_>, + _: &mut crate::optimizer::core::rule::HintState, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { if !matches!(plan.operator, Operator::MarkApply(_)) { diff --git a/src/optimizer/rule/implementation/dql/sort.rs b/src/optimizer/rule/implementation/dql/sort.rs index 9202ed16..c92c1900 100644 --- a/src/optimizer/rule/implementation/dql/sort.rs +++ b/src/optimizer/rule/implementation/dql/sort.rs @@ -39,6 +39,7 @@ impl ImplementationRule for SortImplementation { plan: &LogicalPlan, _: &crate::planner::PlanArena, _: &StatisticMetaLoader<'_>, + _: &mut crate::optimizer::core::rule::HintState, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { if let Operator::Sort(op) = &plan.operator { diff --git a/src/optimizer/rule/implementation/dql/table_scan.rs b/src/optimizer/rule/implementation/dql/table_scan.rs index 53a09b8b..3ba4b987 100644 --- a/src/optimizer/rule/implementation/dql/table_scan.rs +++ b/src/optimizer/rule/implementation/dql/table_scan.rs @@ -43,6 +43,7 @@ impl ImplementationRule for SeqScanImplementation { plan: &LogicalPlan, arena: &PlanArena, loader: &StatisticMetaLoader<'_>, + _: &mut crate::optimizer::core::rule::HintState, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { if let Operator::TableScan(scan_op) = &plan.operator { @@ -88,6 +89,7 @@ impl ImplementationRule for IndexScanImplementation { plan: &LogicalPlan, arena: &PlanArena, loader: &StatisticMetaLoader<'_>, + state: &mut crate::optimizer::core::rule::HintState, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { if let Operator::TableScan(scan_op) = &plan.operator { @@ -102,7 +104,12 @@ impl ImplementationRule for IndexScanImplementation { let index_meta = arena.index(index_info.meta); // A PK/Unique equality range has an exact row count even without statistics. let counted = loader - .collect_count(&scan_op.table_name, index_meta.id, range)? + .collect_count( + &scan_op.table_name, + index_meta.id, + range, + &mut state.histogram_buf, + )? .or_else(|| unique_eq_row_count(index_meta.ty, range).map(|rows| (rows, rows))); if let Some((mut row_count, distinct_count)) = counted { row_count = adjust_index_row_count(index_meta.ty, range, row_count); diff --git a/src/optimizer/rule/implementation/dql/top_k.rs b/src/optimizer/rule/implementation/dql/top_k.rs index 76ebf242..310d0a2c 100644 --- a/src/optimizer/rule/implementation/dql/top_k.rs +++ b/src/optimizer/rule/implementation/dql/top_k.rs @@ -39,6 +39,7 @@ impl ImplementationRule for TopKImplementation { plan: &LogicalPlan, _: &crate::planner::PlanArena, _: &StatisticMetaLoader<'_>, + _: &mut crate::optimizer::core::rule::HintState, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { if let Operator::TopK(op) = &plan.operator { diff --git a/src/optimizer/rule/implementation/dql/window.rs b/src/optimizer/rule/implementation/dql/window.rs index ef28895f..1e3186b1 100644 --- a/src/optimizer/rule/implementation/dql/window.rs +++ b/src/optimizer/rule/implementation/dql/window.rs @@ -39,6 +39,7 @@ impl ImplementationRule for WindowImplementation { plan: &LogicalPlan, _: &crate::planner::PlanArena, _: &StatisticMetaLoader<'_>, + _: &mut crate::optimizer::core::rule::HintState, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { if let Operator::Window(op) = &plan.operator { diff --git a/src/optimizer/rule/implementation/macros.rs b/src/optimizer/rule/implementation/macros.rs index 14d4289f..e0835533 100644 --- a/src/optimizer/rule/implementation/macros.rs +++ b/src/optimizer/rule/implementation/macros.rs @@ -27,6 +27,7 @@ macro_rules! single_mapping { _: &$crate::planner::LogicalPlan, _: &$crate::planner::PlanArena, _: &StatisticMetaLoader<'_>, + _: &mut $crate::optimizer::core::rule::HintState, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { //TODO: CostModel diff --git a/src/optimizer/rule/implementation/mod.rs b/src/optimizer/rule/implementation/mod.rs index 165c33c3..6e4e5501 100644 --- a/src/optimizer/rule/implementation/mod.rs +++ b/src/optimizer/rule/implementation/mod.rs @@ -19,7 +19,9 @@ pub(crate) mod macros; use crate::errors::DatabaseError; use crate::optimizer::core::pattern::Pattern; -use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; +use crate::optimizer::core::rule::{ + BestPhysicalOption, HintState, ImplementationRule, MatchPattern, +}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::rule::implementation::ddl::add_column::AddColumnImplementation; use crate::optimizer::rule::implementation::ddl::change_column::ChangeColumnImplementation; @@ -264,11 +266,18 @@ impl ImplementationRule for ImplementationRuleImpl { plan: &LogicalPlan, arena: &PlanArena, loader: &StatisticMetaLoader<'_>, + state: &mut HintState, best_physical_option: &mut BestPhysicalOption, ) -> Result<(), DatabaseError> { macro_rules! update { ($implementation:expr) => { - $implementation.update_best_option(plan, arena, loader, best_physical_option)? + $implementation.update_best_option( + plan, + arena, + loader, + state, + best_physical_option, + )? }; } @@ -383,7 +392,13 @@ mod tests { let loader = StatisticMetaLoader::new(&statistics_cache); let mut best = None; let plan = LogicalPlan::new(operator.clone(), Childrens::None); - rule.update_best_option(&plan, arena, &loader, &mut best)?; + rule.update_best_option( + &plan, + arena, + &loader, + &mut crate::optimizer::core::rule::HintState::default(), + &mut best, + )?; Ok(best .expect("implementation rule should produce an option") From 41357d2bb6a56275c54cd3a7070e4aa0f621968c Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 15:03:58 +0800 Subject: [PATCH 54/62] docs: ask agents to write only necessary tests --- AGENT.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/AGENT.md b/AGENT.md index 0276a327..e59d87c8 100644 --- a/AGENT.md +++ b/AGENT.md @@ -77,6 +77,18 @@ PRs that modify logic but leave obvious test gaps untouched may be rejected. --- +### 2.4 Write Only Necessary Tests + +Coverage means each behavior is tested once, not that every case gets its own test. + +- Test each distinct branch or behavior once, at the layer that owns it +- Do not repeat in a driver or caller what the rule or module already tests +- Group related cases into one table-driven test instead of near-identical tests +- Skip tests for trivial control flow (simple loops, early returns, plain delegation) +- When touching a test module, delete tests made redundant by others + +--- + ## 3. Code Simplicity Rules ### 3.1 Prefer Simple Code From 15d6723f747d5673179a56f9028bb2d4c4e373aa Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 16:23:02 +0800 Subject: [PATCH 55/62] refactor(optimizer): price plans on demand and trim redundant tests - Replace the cached PhysicalOption cost with LogicalPlan::price, so a cost always reflects the current rows; drop WorkCost and clear_work_cost. - Join reorder inspect only collects schemas and keys; inner join rows were discarded by the rebuild. - Guard MarkApply walks referenced columns through the operator visitor instead of collecting them first. - Return an error instead of panicking on a loaded column without an id. - Drop tests already covered elsewhere and table-drive near-duplicates. --- src/db.rs | 14 +- .../rule/normalization/elimination.rs | 134 ++----- .../rule/normalization/guard_mark_apply.rs | 243 ++++--------- .../rule/normalization/join_reorder.rs | 326 ++++-------------- .../rule/normalization/parameterized_index.rs | 154 +-------- src/planner/mod.rs | 238 ++++--------- src/planner/operator/mod.rs | 200 ++++++----- src/planner/operator/visitor.rs | 81 ----- src/serdes/physical_option.rs | 87 ++--- src/types/value.rs | 148 +++----- 10 files changed, 432 insertions(+), 1193 deletions(-) diff --git a/src/db.rs b/src/db.rs index 499e8798..f340d137 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1057,13 +1057,13 @@ impl Database { } } for column in table.columns() { - let column_id = self - .state - .table_arena - .borrow() - .column(*column) - .id() - .unwrap(); + let column_id = { + let table_arena = self.state.table_arena.borrow(); + let column = table_arena.column(*column); + column + .id() + .ok_or_else(|| DatabaseError::invalid_column(column.name()))? + }; if let Some(meta) = transaction.column_statistics( &mut table_codec, name.as_ref(), diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index 481bca9d..66681eb1 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -167,12 +167,10 @@ fn lift_cross_project( let mut join = plan.take(); join.reset_output_schema_cache(); - join.clear_work_cost(); *project.childrens = Childrens::Only(Box::new(join)); project.physical_option = None; project.reset_output_schema_cache(); - project.clear_work_cost(); *plan = project; Ok(true) @@ -267,14 +265,12 @@ impl NormalizationRule for EliminateProject { std::mem::swap(&mut parent.childrens, &mut project.childrens); parent.reset_output_schema_cache(); - parent.clear_work_cost(); *project.childrens = Childrens::Only(Box::new(parent)); project.physical_option = None; *plan = project; } plan.reset_output_schema_cache(); - plan.clear_work_cost(); Ok(true) } } @@ -990,47 +986,6 @@ mod tests { Ok(()) } - #[test] - fn repeated_refs_are_copied_without_mutating_sources_or_revisiting_replacements( - ) -> Result<(), DatabaseError> { - let table = build_t1_table()?; - let mut arena = PlanArena::new(&table.table_arena); - let scan = table.plan_with_arena("select c1, c2 from t1", &mut arena)?; - let (input, replacement) = source(&mut arena, scan); - let reference = column(&mut arena, replacement, 0); - let expr = binary(&mut arena, reference, BinaryOperator::Plus, 2); - let mut plan = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![expr, expr])), - Childrens::Only(Box::new(input)), - ); - - assert!(EliminateProject.apply(&mut plan, &mut arena, None)?); - let Operator::Project(op) = &plan.operator else { - unreachable!() - }; - for rewritten in &op.exprs { - let ScalarExpression::Binary { left_expr, .. } = rewritten.unpack_alias_ref(&arena) - else { - unreachable!() - }; - assert_eq!(*left_expr, replacement); - assert_ne!(*rewritten, expr); - } - let ScalarExpression::Binary { left_expr, .. } = arena.expression(expr) else { - unreachable!() - }; - assert_eq!(*left_expr, reference); - let ScalarExpression::Binary { left_expr, .. } = arena.expression(replacement) else { - unreachable!() - }; - let ScalarExpression::ColumnRef { position, .. } = left_expr.unpack_alias_ref(&arena) - else { - unreachable!() - }; - assert_eq!(*position, 1); - Ok(()) - } - #[test] fn mapping_transfer_preserves_shared_source_expressions_across_plan_nodes( ) -> Result<(), DatabaseError> { @@ -1167,13 +1122,19 @@ mod tests { } #[test] - fn cross_join_uses_new_left_width_for_mapped_and_unchanged_right_inputs( - ) -> Result<(), DatabaseError> { - for (right_slots, expected) in [(Some(&[1, 0][..]), [2, 4, 3]), (None, [2, 3, 4])] { + fn cross_join_remaps_by_new_left_width() -> Result<(), DatabaseError> { + // (left slots, right slots, outer slots, expected positions); an empty left + // mapping must not be treated as identity. + let cases: [(&[usize], Option<&[usize]>, &[usize], &[usize]); 3] = [ + (&[2], Some(&[1, 0]), &[0, 1, 2], &[2, 4, 3]), + (&[2], None, &[0, 1, 2], &[2, 3, 4]), + (&[], None, &[0, 1], &[3, 4]), + ]; + for (left_slots, right_slots, outer_slots, expected) in cases { let table_arena = crate::planner::TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); let left = values(&mut arena, &["l0", "l1", "l2"]); - let left = permutation(&mut arena, left, &[2]); + let left = permutation(&mut arena, left, left_slots); let right = values(&mut arena, &["r0", "r1"]); let right = match right_slots { Some(slots) => permutation(&mut arena, right, slots), @@ -1181,50 +1142,27 @@ mod tests { }; let cross = JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); - let mut plan = permutation(&mut arena, cross, &[0, 1, 2]); + let mut plan = permutation(&mut arena, cross, outer_slots); assert!(eliminate_projects(&mut plan, &mut arena)?); assert_eq!(plan.explain(&mut arena, 0).matches("Projection").count(), 1); let Operator::Project(op) = &plan.operator else { unreachable!() }; - for (expr, expected) in op.exprs.iter().zip(expected) { - let ScalarExpression::ColumnRef { position, .. } = expr.unpack_alias_ref(&arena) - else { - unreachable!() - }; - assert_eq!(*position, expected); - } + let positions: Vec<_> = op + .exprs + .iter() + .map(|expr| match expr.unpack_alias_ref(&arena) { + ScalarExpression::ColumnRef { position, .. } => *position, + _ => unreachable!(), + }) + .collect(); + assert_eq!(positions, expected); assert!(!eliminate_projects(&mut plan, &mut arena)?); } Ok(()) } - #[test] - fn cross_join_distinguishes_empty_mapping_from_identity() -> Result<(), DatabaseError> { - let table_arena = crate::planner::TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - let left = values(&mut arena, &["l0", "l1", "l2"]); - let left = permutation(&mut arena, left, &[]); - let right = values(&mut arena, &["r0", "r1"]); - let cross = JoinOperator::build(left, right, JoinCondition::None, JoinType::Cross, false); - let mut plan = permutation(&mut arena, cross, &[0, 1]); - - assert!(eliminate_projects(&mut plan, &mut arena)?); - assert_eq!(plan.explain(&mut arena, 0).matches("Projection").count(), 1); - let Operator::Project(op) = &plan.operator else { - unreachable!() - }; - for (expr, expected) in op.exprs.iter().zip([3, 4]) { - let ScalarExpression::ColumnRef { position, .. } = expr.unpack_alias_ref(&arena) else { - unreachable!() - }; - assert_eq!(*position, expected); - } - assert!(!eliminate_projects(&mut plan, &mut arena)?); - Ok(()) - } - #[test] fn cross_join_inlines_computed_right_output_without_changing_source( ) -> Result<(), DatabaseError> { @@ -1283,38 +1221,6 @@ mod tests { Ok(()) } - #[test] - fn test_collapse_project_with_alias() -> Result<(), DatabaseError> { - let table_state = build_t1_table()?; - let mut arena = PlanArena::new(&table_state.table_arena); - let plan = table_state - .plan_with_arena("select t.x from (select c1 as x from t1) t", &mut arena)?; - let original = plan.clone(); - let original_child = original.childrens.pop_only(); - assert!(matches!(original_child.operator, Operator::Project(_))); - let original_grandchild = original_child.childrens.pop_only(); - assert!(matches!(original_grandchild.operator, Operator::Project(_))); - - let pipeline = HepOptimizerPipeline::builder() - .before_batch( - "test_collapse_project_with_alias", - HepBatchStrategy::fix_point_bottomup(10), - &[NormalizationRuleImpl::EliminateProject], - ) - .build(); - let best_plan = pipeline.instantiate(plan).find_best(None, &mut arena)?; - if let Operator::Project(op) = &best_plan.operator { - assert_eq!(op.exprs.len(), 1); - } else { - unreachable!("Should be a project operator"); - } - - let scan_op = best_plan.childrens.pop_only(); - assert!(matches!(scan_op.operator, Operator::TableScan(_))); - - Ok(()) - } - #[test] fn test_collapse_project_remaps_reordered_passthrough_positions() -> Result<(), DatabaseError> { let table_arena = crate::planner::TableArenaCell::default(); diff --git a/src/optimizer/rule/normalization/guard_mark_apply.rs b/src/optimizer/rule/normalization/guard_mark_apply.rs index 46d39871..1251bc18 100644 --- a/src/optimizer/rule/normalization/guard_mark_apply.rs +++ b/src/optimizer/rule/normalization/guard_mark_apply.rs @@ -20,7 +20,7 @@ use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::optimizer::plan_utils::{only_child, only_child_mut}; use crate::planner::operator::Operator; -use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; +use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; use crate::types::evaluator::BinaryEvaluatorRef; use crate::types::value::DataValue; use crate::types::LogicalType; @@ -75,30 +75,24 @@ impl NormalizationRule for GuardMarkApply { } // Do not skip values consumed by another apply, even if Filter does not need them. let mut consumed = Vec::new(); - let mut columns = Vec::new(); let mut input = mark.as_ref(); - for (index, _) in markers.iter().enumerate() { - columns.clear(); + for index in 0..markers.len() { let (Operator::MarkApply(op), Childrens::Twins { left, right }) = (&input.operator, input.childrens.as_ref()) else { unreachable!() }; - for expr in op.predicates.iter().chain(op.parameterized_probe.iter()) { - expr.all_referenced_columns(arena, |_, column, _| { - columns.push(*column); - true - })?; - } - right.visit_column_refs(arena, &mut |column| columns.push(*column))?; - for marker in &markers[index + 1..] { - if columns - .iter() - .any(|column| arena.same_column(*column, *marker)) - { + let later = &markers[index + 1..]; + let mut consume = |arena: &dyn MetaArena, column: &ColumnRef| { + if let Some(marker) = later.iter().find(|m| arena.same_column(*column, **m)) { consumed.push(*marker); } + true + }; + for expr in op.predicates.iter().chain(op.parameterized_probe.iter()) { + expr.all_referenced_columns(arena, |arena, column, _| consume(arena, column))?; } + right.visit_referenced_columns(arena, &mut |arena, column| consume(arena, column))?; input = left; } let mut changed = false; @@ -726,78 +720,10 @@ mod tests { } #[test] - fn and_or_guards_preserve_three_valued_results() -> Result<(), DatabaseError> { - for op in [BinaryOperator::And, BinaryOperator::Or] { - for marker_first in [false, true] { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let mut fixture = Fixture::new(&mut arena, op, marker_first)?; - assert!(GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); - let guard = fixture.mark().guard.unwrap(); - assert_ne!(guard, fixture.flag); - assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); - EvaluatorBind.apply(&mut fixture.plan, &mut arena, None)?; - let predicate = fixture.predicate(); - for flag in [ - DataValue::Boolean(false), - DataValue::Boolean(true), - DataValue::Null, - ] { - let left = Tuple::new(None, vec![flag.clone()]); - let guard_value = arena - .expression(guard) - .eval(&arena, Some(&left))? - .into_owned(); - let expected = match (&op, &flag) { - (BinaryOperator::Or, DataValue::Boolean(value)) => { - DataValue::Boolean(!value) - } - _ => flag.clone(), - }; - assert_eq!(guard_value, expected); - for marker in [ - DataValue::Boolean(false), - DataValue::Boolean(true), - DataValue::Null, - ] { - let original = Tuple::new(None, vec![flag.clone(), marker.clone()]); - let guarded = Tuple::new( - None, - vec![ - flag.clone(), - if guard_value == DataValue::Boolean(false) { - DataValue::Null - } else { - marker - }, - ], - ); - assert_eq!( - arena.expression(predicate).eval(&arena, Some(&original))?, - arena.expression(predicate).eval(&arena, Some(&guarded))?, - ); - } - } - } - } - Ok(()) - } - - #[test] - fn marker_needed_by_projection_or_another_branch_is_not_guarded() -> Result<(), DatabaseError> { + fn marker_in_another_branch_is_not_guarded() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let mut fixture = Fixture::new(&mut arena, BinaryOperator::Or, false)?; - let Operator::Project(project) = &mut fixture.plan.operator else { - unreachable!() - }; - project.exprs.push(fixture.marker); - assert!(!GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); - assert!(fixture.mark().guard.is_none()); - let Operator::Project(project) = &mut fixture.plan.operator else { - unreachable!() - }; - project.exprs.pop(); let guarded_branch = fixture.predicate(); let predicate = arena.alloc_expression(ScalarExpression::Binary { op: BinaryOperator::Or, @@ -952,38 +878,75 @@ mod tests { } #[test] - fn numeric_guards_accept_only_safe_operands() -> Result<(), DatabaseError> { + fn simple_guard_whitelist() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); - let number = arena.alloc_column(ColumnCatalog::new( - "number".into(), - true, - ColumnDesc::new(LogicalType::Bigint, None, false, None)?, - )); - let number = arena.alloc_expression(ScalarExpression::column_expr(number, 0)); + let mut column = |name: &str, ty, position| -> Result { + let column = arena.alloc_column(ColumnCatalog::new( + name.into(), + true, + ColumnDesc::new(ty, None, false, None)?, + )); + Ok(arena.alloc_expression(ScalarExpression::column_expr(column, position))) + }; + let flag = column("flag", LogicalType::Boolean, 0)?; + let number = column("number", LogicalType::Bigint, 1)?; let zero = arena.alloc_expression(DataValue::Int64(0).into()); - let widening = arena.alloc_expression(ScalarExpression::TypeCast { - expr: number, - ty: LogicalType::Double, + let mut alloc = |expr| arena.alloc_expression(expr); + let cast = |expr, ty| ScalarExpression::TypeCast { + expr, + ty, evaluator: None, - }); - let narrowing = arena.alloc_expression(ScalarExpression::TypeCast { - expr: number, - ty: LogicalType::Integer, + }; + let not = |expr, ty| ScalarExpression::Unary { + op: UnaryOperator::Not, + expr, evaluator: None, - }); - let arithmetic = arena.alloc_expression(ScalarExpression::Binary { + ty, + }; + let widening = alloc(cast(number, LogicalType::Double)); + let narrowing = alloc(cast(number, LogicalType::Integer)); + let divide = alloc(ScalarExpression::Binary { op: BinaryOperator::Divide, left_expr: number, right_expr: zero, evaluator: None, ty: LogicalType::Bigint, }); + let not_number = alloc(not(number, LogicalType::Bigint)); + let mut cases = vec![ + (flag, true), + (number, false), + (alloc(not(flag, LogicalType::Boolean)), true), + ( + alloc(ScalarExpression::Alias { + expr: flag, + alias: AliasType::Expr(number), + }), + true, + ), + ( + alloc(ScalarExpression::IsNull { + negated: true, + expr: number, + }), + true, + ), + // IS NULL only accepts a plain column or constant operand. + ( + alloc(ScalarExpression::IsNull { + negated: false, + expr: not_number, + }), + false, + ), + ]; + // Comparisons accept numeric operands and widening casts, never failing calculations. for (operand, valid) in [ (number, true), (widening, true), (narrowing, false), - (arithmetic, false), + (divide, false), ] { for op in [ BinaryOperator::Eq, @@ -993,90 +956,24 @@ mod tests { BinaryOperator::Lt, BinaryOperator::LtEq, ] { - let predicate = arena.alloc_expression(ScalarExpression::Binary { + let comparison = alloc(ScalarExpression::Binary { op, left_expr: operand, right_expr: zero, evaluator: None, ty: LogicalType::Boolean, }); - let mut visitor = SimpleGuardVisitor { - valid: true, - context: GuardContext::Predicate, - }; - visitor.visit(predicate, &arena)?; - assert_eq!(visitor.valid, valid); + cases.push((comparison, valid)); } } - Ok(()) - } - - #[test] - fn null_checks_and_aliases_keep_the_guard_whitelist() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let mut fixture = Fixture::new(&mut arena, BinaryOperator::And, false)?; - let number_column = arena.alloc_column(ColumnCatalog::new( - "number".to_string(), - true, - ColumnDesc::new(LogicalType::Integer, None, false, None)?, - )); - let number = arena.alloc_expression(ScalarExpression::column_expr(number_column, 0)); - let is_null = arena.alloc_expression(ScalarExpression::IsNull { - negated: false, - expr: number, - }); - let is_not_null = arena.alloc_expression(ScalarExpression::IsNull { - negated: true, - expr: number, - }); - let not_flag = arena.alloc_expression(ScalarExpression::Unary { - op: UnaryOperator::Not, - expr: fixture.flag, - evaluator: None, - ty: LogicalType::Boolean, - }); - let alias = arena.alloc_expression(ScalarExpression::Alias { - expr: fixture.flag, - alias: AliasType::Expr(number), - }); - let not_number = arena.alloc_expression(ScalarExpression::Unary { - op: UnaryOperator::Not, - expr: number, - evaluator: None, - ty: LogicalType::Integer, - }); - let nested = arena.alloc_expression(ScalarExpression::IsNull { - negated: false, - expr: not_number, - }); - let assert_case = |expr, expected| -> Result<(), DatabaseError> { + for (expr, valid) in cases { let mut visitor = SimpleGuardVisitor { valid: true, context: GuardContext::Predicate, }; visitor.visit(expr, &arena)?; - assert_eq!(visitor.valid, expected); - Ok(()) - }; - assert_case(fixture.flag, true)?; - assert_case(number, false)?; - assert_case(is_null, true)?; - assert_case(is_not_null, true)?; - assert_case(not_flag, true)?; - assert_case(alias, true)?; - assert_case(nested, false)?; - let predicate = fixture.predicate(); - let alias = arena.alloc_expression(ScalarExpression::Alias { - expr: fixture.flag, - alias: AliasType::Name("flag_alias".to_string()), - }); - let ScalarExpression::Binary { left_expr, .. } = &mut *arena.expression_mut(predicate) - else { - unreachable!() - }; - *left_expr = alias; - assert!(GuardMarkApply.apply(&mut fixture.plan, &mut arena, None)?); + assert_eq!(visitor.valid, valid); + } Ok(()) } } diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 322db6d5..3107981d 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -311,114 +311,74 @@ fn region_inputs(plan: &LogicalPlan, forced: bool) -> usize { region_inputs(left, forced) + region_inputs(right, forced) } -// Collects input schemas and join keys -// returns `None` when any input lacks a row estimate. +// Collects input schemas and join keys; returns whether every input has a row estimate. +// Joins inside the region are rebuilt by the reorder, so their own rows are not estimated here. fn inspect<'plan>( plan: &'plan mut LogicalPlan, schemas: &mut Vec<&'plan Schema>, keys: &mut JoinKeys, arena: &mut PlanArena, - loader: Option<&StatisticMetaLoader<'_>>, forced: bool, -) -> Result, DatabaseError> { - if can_expand(plan, forced) { - let (Operator::Join(join), Childrens::Twins { left, right }) = - (&plan.operator, plan.childrens.as_mut()) - else { - unreachable!() - }; - let left_stats = index_statistics(left); - let right_stats = index_statistics(right); - let on = match &join.on { - JoinCondition::On { on, .. } => on.as_slice(), - JoinCondition::None => &[], - }; - let ndvs = column_ndv(left, right, on.iter().copied(), loader, arena)?; - let start = schemas.len(); - let left_rows = inspect(left, schemas, keys, arena, loader, forced)?; - let middle = schemas.len(); - let right_rows = inspect(right, schemas, keys, arena, loader, forced)?; +) -> Result { + if !can_expand(plan, forced) { + let estimated = plan.estimated_rows().is_some(); + schemas.push(plan.output_schema(arena)); + return Ok(estimated); + } + let (Operator::Join(join), Childrens::Twins { left, right }) = + (&plan.operator, plan.childrens.as_mut()) + else { + unreachable!() + }; + let on = match &join.on { + JoinCondition::On { on, .. } => on.as_slice(), + JoinCondition::None => &[], + }; + let start = schemas.len(); + let left_estimated = inspect(left, schemas, keys, arena, forced)?; + let middle = schemas.len(); + let right_estimated = inspect(right, schemas, keys, arena, forced)?; - for &(left, right) in on { - let mut endpoints = [None, None]; - for (endpoint, (expr, range)) in endpoints - .iter_mut() - .zip([(left, start..middle), (right, middle..schemas.len())]) - { - for index in range { - if key_in_schema(expr, schemas[index], arena)? { - *endpoint = Some(index); - break; - } - } - if endpoint.is_none() { - return Err(DatabaseError::InvalidPlan(format!( - "join key `{}` is not produced by any join input", - expr.output_name(arena) - ))); + for &(left, right) in on { + let mut endpoints = [None, None]; + for (endpoint, (expr, range)) in endpoints + .iter_mut() + .zip([(left, start..middle), (right, middle..schemas.len())]) + { + for index in range { + if key_in_schema(expr, schemas[index], arena)? { + *endpoint = Some(index); + break; } } - if endpoints[0] == endpoints[1] { + if endpoint.is_none() { return Err(DatabaseError::InvalidPlan(format!( - "join key `{} = {}` compares columns of the same join input", - left.output_name(arena), - right.output_name(arena) + "join key `{}` is not produced by any join input", + expr.output_name(arena) ))); } - // SAFETY: The loop above returns early if either endpoint is None. - let [l, r] = endpoints.map(Option::unwrap); - keys.insert(1u64 << l, 1u64 << r, (left, right)); } - let (Some(left_rows), Some(right_rows)) = (left_rows, right_rows) else { - return Ok(None); - }; - let rows = join_rows( - &JoinInputStatistics { - rows: left_rows, - schema: if middle == start + 1 { - schemas[start] - } else { - &[] - }, - index: left_stats.map(|(index, ndv)| (arena.index(index), ndv)), - }, - &JoinInputStatistics { - rows: right_rows, - schema: if schemas.len() == middle + 1 { - schemas[middle] - } else { - &[] - }, - index: right_stats.map(|(index, ndv)| (arena.index(index), ndv)), - }, - || on.iter().copied(), - ndvs, - arena, - ); - if let Some(option) = &mut plan.physical_option { - if option.row_count != (RowCountOption::Estimated { rows, ndv: None }) { - option.row_count = RowCountOption::Estimated { - rows: rows, - ndv: None, - }; - option.cost = None; - } + if endpoints[0] == endpoints[1] { + return Err(DatabaseError::InvalidPlan(format!( + "join key `{} = {}` compares columns of the same join input", + left.output_name(arena), + right.output_name(arena) + ))); } - Ok(Some(rows)) - } else { - let rows = plan.estimated_rows(); - schemas.push(plan.output_schema(arena)); - Ok(rows) + // SAFETY: The loop above returns early if either endpoint is None. + let [l, r] = endpoints.map(Option::unwrap); + keys.insert(1u64 << l, 1u64 << r, (left, right)); } + Ok(left_estimated && right_estimated) } /// An input's cost, read once at its (default) rows when its estimate is incomplete. -fn input_cost( +fn input_cost_or_default( input: &mut LogicalPlan, rows: usize, arena: &mut PlanArena, ) -> Result { - Ok(match input.estimated_cost(arena)? { + Ok(match input.price(arena).and_then(|cost| cost.input()) { Some(cost) => cost, None => { let width = input.output_schema(arena).len(); @@ -447,7 +407,7 @@ fn flatten( } else { // Without statistics an input is assumed to have the default rows (RFC 0001 §3.9). let rows = plan.estimated_rows().unwrap_or(DEFAULT_TABLE_ROWS); - let cost = input_cost(&mut plan, rows, arena)?; + let cost = input_cost_or_default(&mut plan, rows, arena)?; if leaves .get(*start) .is_some_and(|(_, best, _)| (cost.rows, cost.work) < (best.rows, best.work)) @@ -558,7 +518,7 @@ impl NormalizationRule for GreedyJoinReorder { let mut keys = JoinKeys::default(); // Inputs without statistics are priced with default rows, but joins built from them keep // an unknown row count for later rules. - let estimated = inspect(plan, &mut schemas, &mut keys, arena, loader, forced)?.is_some(); + let estimated = inspect(plan, &mut schemas, &mut keys, arena, forced)?; // Each original column keeps its input bit and position within its current join. let mut positions = Vec::with_capacity(original_schema.len()); for (input, schema) in schemas.iter().enumerate() { @@ -742,7 +702,7 @@ impl NormalizationRule for GreedyJoinReorder { join.physical_option = Some(option); join }; - current_cost = input_cost(&mut current, product, arena)?; + current_cost = input_cost_or_default(&mut current, product, arena)?; current_mask |= leaf_mask; } *plan = project_join_output( @@ -762,10 +722,6 @@ mod tests { use crate::catalog::{ColumnCatalog, ColumnDesc}; use crate::expression::BinaryOperator; use crate::optimizer::core::histogram::{HistogramMetaBuilder, DEFAULT_HLL_PRECISION}; - use crate::optimizer::heuristic::batch::HepBatchStrategy; - use crate::optimizer::heuristic::optimizer::HepOptimizerPipeline; - use crate::optimizer::rule::normalization::NormalizationRuleImpl; - use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::TableArenaCell; @@ -926,7 +882,7 @@ mod tests { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let (mut left, l) = input("l", 1000, &mut arena); - let (mut right, r) = input("r", 200, &mut arena); + let (right, r) = input("r", 200, &mut arena); let mut cache = StatisticsMetaCache::default(); for (table, rows) in [("l", 1000), ("r", 200)] { let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; @@ -976,58 +932,6 @@ mod tests { )?, None ); - let mut builder = HistogramMetaBuilder::new(DEFAULT_HLL_PRECISION)?; - for value in 0..200 { - builder.append(&DataValue::Int32(value % 100)); - } - cache.columns.insert(("r".into(), 0), builder.build()); - let loader = StatisticMetaLoader::new(&cache); - let left_schema = left.output_schema(&mut arena); - let right_schema = right.output_schema(&mut arena); - assert_eq!( - join_rows( - &JoinInputStatistics { - rows: 1000, - schema: left_schema, - index: None - }, - &JoinInputStatistics { - rows: 200, - schema: right_schema, - index: None - }, - || std::iter::once((l, r)), - Some((100, 100)), - &arena - ), - 2000 - ); - let mut join = JoinOperator::build( - left, - right, - JoinCondition::On { - on: vec![(l, r)], - filter: None, - }, - JoinType::Inner, - false, - ); - join.physical_option = Some(PhysicalOption::new( - PlanImpl::HashJoin, - SortOption::None, - RowCountOption::Unknown, - )); - let pipeline = HepOptimizerPipeline::builder() - .after_batch( - "column statistics", - HepBatchStrategy::fix_point_topdown(1), - &[NormalizationRuleImpl::GreedyJoinReorder], - ) - .build(); - let plan = pipeline - .instantiate(join) - .find_best(Some(&loader), &mut arena)?; - assert_eq!(plan.estimated_rows(), Some(2000)); Ok(()) } @@ -1226,7 +1130,7 @@ mod tests { } #[test] - fn startup_unknown_and_overflow_do_not_become_zero() -> Result<(), DatabaseError> { + fn unknown_startup_does_not_become_zero() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let (mut scan, key) = input("source", 16, &mut arena); @@ -1246,22 +1150,14 @@ mod tests { SortOption::None, RowCountOption::Follow, )); - assert_eq!(sort.estimated_startup_work(&mut arena)?, None); - assert_eq!(sort.estimated_cost(&mut arena)?.map(|cost| cost.work), None); - - let (a, _) = input("a", usize::MAX, &mut arena); - let (b, _) = input("b", 2, &mut arena); - let mut cross = JoinOperator::build(a, b, JoinCondition::None, JoinType::Cross, false); - cross.physical_option = Some(PhysicalOption::new( - PlanImpl::NestLoopJoin, - SortOption::None, - RowCountOption::Unknown, - )); + assert_eq!(sort.price(&mut arena).and_then(|cost| cost.startup), None); assert_eq!( - cross.estimated_cost(&mut arena)?.map(|cost| cost.work), - Some(usize::MAX) + sort.price(&mut arena) + .and_then(|cost| cost.input()) + .map(|cost| cost.work), + None ); - assert_eq!(cross.estimated_rows(), Some(usize::MAX)); + Ok(()) } @@ -1272,10 +1168,13 @@ mod tests { let mut arena = PlanArena::new(&table_arena); let (mut left, _) = input("a", 100, &mut arena); let mut right = materialized_right(&mut arena); - assert_eq!(left.estimated_startup_work(&mut arena)?, Some(0)); + assert_eq!( + left.price(&mut arena).and_then(|cost| cost.startup), + Some(0) + ); // The 10-row build side is read (50), stored whole (50) and hashed. assert_eq!( - right.estimated_startup_work(&mut arena)?, + right.price(&mut arena).and_then(|cost| cost.startup), Some(100 + 10 * crate::planner::operator::HASH_BUILD_WORK) ); let mut plan = @@ -1429,45 +1328,6 @@ mod tests { Ok(()) } - #[test] - fn connected_pair_precedes_a_smaller_cartesian_product() -> Result<(), DatabaseError> { - let table_arena = TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - let mut plan = three_inputs(&mut arena); - if let Childrens::Twins { left, .. } = plan.childrens.as_mut() { - if let Childrens::Twins { left, right } = left.childrens.as_mut() { - left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { - rows: 10, - ndv: None, - }; - right.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { - rows: 1000, - ndv: None, - }; - } - } - let schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan.output_schema(&mut arena), &schema); - // AB is merged first despite its larger product; C then builds against it. - let join = plan.childrens.only(); - let (c, ab) = join.childrens.twins(); - assert_eq!(ab.referenced_table(), vec!["a".into(), "b".into()]); - assert_eq!(c.referenced_table(), vec!["c".into()]); - assert_eq!(ab.estimated_rows(), Some(10000)); - for join in [join, ab] { - let Operator::Join(op) = &join.operator else { - unreachable!() - }; - let JoinCondition::On { on, .. } = &op.on else { - unreachable!() - }; - assert_eq!(op.join_type, JoinType::Inner); - assert_eq!(on.len(), 1); - } - Ok(()) - } - #[test] fn unestimated_region_defers_cartesian_products_by_input_order() -> Result<(), DatabaseError> { // FROM a, c, d, b WHERE a = b AND c = b: FROM order crosses a, c and d @@ -1537,70 +1397,10 @@ mod tests { } #[test] - fn rebuilding_join_region_preserves_parent_and_input_subtrees() -> Result<(), DatabaseError> { - let table_arena = TableArenaCell::default(); - let mut arena = PlanArena::new(&table_arena); - let mut join = three_inputs(&mut arena); - let Childrens::Twins { right, .. } = join.childrens.as_mut() else { - unreachable!() - }; - let predicate = - arena.alloc_expression(ScalarExpression::Constant(DataValue::Boolean(true))); - let mut filter = FilterOperator::build(predicate, right.take(), false); - filter.physical_option = Some(PhysicalOption::new( - PlanImpl::Filter, - SortOption::Follow, - RowCountOption::Follow, - )); - **right = filter; - let schema = join.output_schema(&mut arena).clone(); - let exprs = schema - .iter() - .enumerate() - .map(|(position, column)| { - arena.alloc_expression(ScalarExpression::column_expr(*column, position)) - }) - .collect(); - let mut plan = LogicalPlan::new( - Operator::Project(ProjectOperator::new(exprs)), - Childrens::Only(Box::new(join)), - ); - let original_parent = plan.operator.clone(); - let Childrens::Only(join) = plan.childrens.as_mut() else { - unreachable!() - }; - assert!(GreedyJoinReorder.apply(join, &mut arena, None)?); - assert_eq!(plan.operator, original_parent); - assert_eq!(plan.output_schema(&mut arena), &schema); - // Greedy restores the region's output order with its own Project below the parent. - let (bc, _) = plan.childrens.only().childrens.only().childrens.twins(); - let (_, filter) = bc.childrens.twins(); - assert!(matches!(filter.operator, Operator::Filter(_))); - assert_eq!(filter.childrens.only().referenced_table(), vec!["c".into()]); - assert_eq!(filter.estimated_rows(), Some(20)); - Ok(()) - } - - #[test] - fn unknown_inputs_are_rebuilt_but_outer_joins_keep_original_order() -> Result<(), DatabaseError> - { + fn outer_joins_keep_original_order() -> Result<(), DatabaseError> { let table_arena = TableArenaCell::default(); let mut arena = PlanArena::new(&table_arena); let mut plan = three_inputs(&mut arena); - if let Childrens::Twins { right, .. } = plan.childrens.as_mut() { - right.physical_option.as_mut().unwrap().row_count = RowCountOption::Unknown; - } - let mut outer = plan.clone(); - // Unknown inputs are rebuilt with default rows, but joins keep unknown row counts. - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); - let (c, ab) = plan.childrens.only().childrens.twins(); - assert_eq!(c.referenced_table(), vec!["c".into()]); - let mut tables = ab.referenced_table(); - tables.sort(); - assert_eq!(tables, vec!["a".into(), "b".into()]); - assert_eq!(plan.row_count_option(), None); - - let mut plan = outer.take(); if let Operator::Join(op) = &mut plan.operator { op.join_type = JoinType::LeftOuter; } diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 1046d498..52cb67eb 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -747,11 +747,9 @@ mod inner_join_tests { use crate::catalog::{ColumnCatalog, ColumnDesc}; use crate::expression::range_detacher::Range; use crate::expression::{AliasType, UnaryOperator}; - use crate::optimizer::rule::normalization::join_reorder::GreedyJoinReorder; use crate::planner::operator::join::JoinOperator; use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::table_scan::TableScanOperator; - use crate::planner::operator::WorkCost; use crate::planner::TableArenaCell; use crate::types::index::{IndexInfo, IndexMeta, IndexType}; use crate::types::tuple::Tuple; @@ -1085,16 +1083,10 @@ mod inner_join_tests { SortOption::None, RowCountOption::Estimated { rows, ndv: None }, )); - plan.physical_option.as_mut().unwrap().cost = Some(WorkCost { - width: 1, - startup: Some(9999), - total: Some(9999), - }); } assert!(ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); let option = plan.physical_option.as_ref().unwrap(); assert_eq!(option.plan, PlanImpl::IndexProbeJoin); - assert_eq!(option.cost, None); assert_eq!(plan.estimated_rows(), rows); let Childrens::Twins { right, .. } = plan.childrens.as_ref() else { unreachable!() @@ -1104,137 +1096,14 @@ mod inner_join_tests { right.physical_option.as_ref().unwrap().row_count, RowCountOption::Unknown ); - assert_eq!(plan.estimated_startup_work(&mut arena)?, Some(0)); + assert_eq!( + plan.price(&mut arena).and_then(|cost| cost.startup), + Some(0) + ); } Ok(()) } - #[test] - fn greedy_decided_join_is_not_parameterized() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let left = scan(&mut arena, "outer", &[LogicalType::Integer], &[]); - let right = unique_scan(&mut arena, "inner", &[LogicalType::Integer], &[&[0]]); - let l = column(&left, 0, &mut arena); - let r = column(&right, 0, &mut arena); - let mut plan = join(left, right, vec![(l, r)], None); - let mut option = PhysicalOption::new( - PlanImpl::HashJoin, - SortOption::None, - RowCountOption::Unknown, - ); - option.join_decided = true; - plan.physical_option = Some(option); - let original = plan.clone(); - assert!(!ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan, original); - Ok(()) - } - - #[test] - fn parameterized_join_allows_parent_nested_loop_orientation() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let mut left = scan(&mut arena, "outer", &[LogicalType::Integer], &[]); - let mut right = unique_scan(&mut arena, "inner", &[LogicalType::Integer], &[&[0]]); - let l = column(&left, 0, &mut arena); - let r = column(&right, 0, &mut arena); - left.physical_option = Some(PhysicalOption::new( - PlanImpl::SeqScan, - SortOption::None, - RowCountOption::Estimated { - rows: 129, - ndv: None, - }, - )); - right.physical_option = Some(PhysicalOption::new( - PlanImpl::SeqScan, - SortOption::None, - RowCountOption::Estimated { - rows: 120, - ndv: None, - }, - )); - let mut apply = join(left, right, vec![(l, r)], None); - apply.physical_option = Some(PhysicalOption::new( - PlanImpl::HashJoin, - SortOption::None, - RowCountOption::Estimated { - rows: 10, - ndv: None, - }, - )); - assert!(ParameterizeInnerJoin.apply(&mut apply, &mut arena, None)?); - let mut other = scan(&mut arena, "other", &[LogicalType::Integer], &[]); - other.physical_option = Some(PhysicalOption::new( - PlanImpl::SeqScan, - SortOption::None, - RowCountOption::Estimated { - rows: 200, - ndv: None, - }, - )); - let mut plan = - JoinOperator::build(other, apply, JoinCondition::None, JoinType::Cross, false); - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::NestLoopJoin, - SortOption::None, - RowCountOption::Unknown, - )); - let schema = plan.output_schema(&mut arena).clone(); - assert!(GreedyJoinReorder.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan.output_schema(&mut arena), &schema); - assert_eq!(plan.estimated_rows(), Some(2000)); - let Childrens::Only(join) = plan.childrens.as_ref() else { - panic!("expected output-restoring projection") - }; - let Childrens::Twins { left, .. } = join.childrens.as_ref() else { - unreachable!() - }; - assert!(matches!(left.operator, Operator::IndexProbeJoin(_))); - Ok(()) - } - - #[test] - fn constant_only_probe_leaves_static_scan_and_join_unchanged() -> Result<(), DatabaseError> { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let left = scan(&mut arena, "outer", &[LogicalType::Integer], &[]); - let mut right = unique_scan( - &mut arena, - "inner", - &[LogicalType::Integer, LogicalType::Integer], - &[&[1]], - ); - let l = column(&left, 0, &mut arena); - let r = column(&right, 0, &mut arena); - let static_key = column(&right, 1, &mut arena); - let constant = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(782))); - let predicate = binary(&mut arena, BinaryOperator::Eq, static_key, constant); - let Operator::TableScan(scan) = &mut right.operator else { - unreachable!() - }; - let info = &mut scan.index_infos[0]; - info.lookup = Some(IndexLookup::Static(Range::Eq(DataValue::Int32(782)))); - info.residual_predicate = Some(predicate); - right.physical_option = Some(PhysicalOption::new( - PlanImpl::IndexScan(Box::new(info.clone())), - SortOption::None, - RowCountOption::Unknown, - )); - // Keep the join-key boundary intact across multiple filter layers. - let passthrough = arena.alloc_expression(ScalarExpression::from(true)); - let right = FilterOperator::build(passthrough, right, false); - let right = FilterOperator::build(predicate, right, false); - let mut plan = join(left, right, vec![(l, r)], None); - let before = plan.clone(); - - assert!(!ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?); - assert_eq!(plan, before); - assert_eq!((position(l, &arena), position(r, &arena)), (0, 0)); - Ok(()) - } - #[test] fn constant_only_index_does_not_block_outer_key_probe() -> Result<(), DatabaseError> { for swap in [false, true] { @@ -1426,7 +1295,10 @@ mod inner_join_tests { plan.physical_option.as_ref().unwrap().row_count, RowCountOption::Follow ); - assert_eq!(plan.estimated_startup_work(&mut arena)?, Some(0)); + assert_eq!( + plan.price(&mut arena).and_then(|cost| cost.startup), + Some(0) + ); assert_eq!(plan.output_schema(&mut arena), &schema); let Operator::Filter(filter) = &plan.operator else { panic!("join filter must remain above projection") @@ -1495,6 +1367,7 @@ mod inner_join_tests { "outer_join", "normal_index", "composite_index", + "join_decided", ] { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); @@ -1533,6 +1406,15 @@ mod inner_join_tests { }; join.join_type = JoinType::LeftOuter; } + if case == "join_decided" { + let mut option = PhysicalOption::new( + PlanImpl::HashJoin, + SortOption::None, + RowCountOption::Unknown, + ); + option.join_decided = true; + plan.physical_option = Some(option); + } let before = plan.clone(); assert!( !ParameterizeInnerJoin.apply(&mut plan, &mut arena, None)?, diff --git a/src/planner/mod.rs b/src/planner/mod.rs index fe391cf3..ce72e0fd 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -26,9 +26,7 @@ use crate::planner::operator::set_membership::SetMembershipOperator; use crate::planner::operator::union::UnionOperator; use crate::planner::operator::values::ValuesOperator; use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; -use crate::planner::operator::{ - input_cost, InputCost, Operator, PhysicalOption, RowCountOption, WorkCost, -}; +use crate::planner::operator::{ChildCosts, Operator, PhysicalOption, PlanCost, RowCountOption}; use kite_sql_serde_macros::ReferenceSerialization; use std::fmt; use std::hash::{Hash, Hasher}; @@ -155,43 +153,6 @@ impl<'a> Iterator for ChildrensIter<'a> { } } -struct StartupWorkVisitor<'r, A> { - arena: &'r mut A, -} - -impl<'a, A: MetaArena> OperatorVisitorMut<'a> for StartupWorkVisitor<'_, A> { - fn visit_plan(&mut self, plan: &'a mut LogicalPlan) -> Result<(), DatabaseError> { - if plan - .physical_option - .as_ref() - .is_none_or(|option| option.cost.is_some()) - { - return Ok(()); - } - let width = plan.output_schema(self.arena).len(); - let rows = plan.estimated_rows(); - let LogicalPlan { - operator, - childrens, - physical_option, - .. - } = plan; - let Some(option) = physical_option.as_mut() else { - return Ok(()); - }; - OperatorVisitorMut::<'_>::visit_children(self, childrens)?; - // Children are cached first, so this only prices the current node. - let mut cost = WorkCost { - width, - startup: option.calculate_startup_work(operator, childrens), - total: None, - }; - cost.total = option.calculate_work(operator, childrens, rows, &cost); - option.cost = Some(cost); - Ok(()) - } -} - #[derive(Debug)] pub struct LogicalPlan { pub(crate) operator: Operator, @@ -262,29 +223,26 @@ impl LogicalPlan { // Pass-local memoization for NLJ orientation. New physical options start // empty; orientation preserves child rows/startup, so their cache is reusable. - pub(crate) fn estimated_startup_work( - &mut self, - arena: &mut A, - ) -> Result, DatabaseError> { - StartupWorkVisitor { arena }.visit_plan(self)?; - Ok(self - .physical_option - .as_ref() - .and_then(|option| option.cost?.startup)) - } - - pub(crate) fn estimated_cost( - &mut self, - arena: &mut A, - ) -> Result, DatabaseError> { - self.estimated_startup_work(arena)?; - Ok(input_cost(self)) - } - - pub(crate) fn clear_work_cost(&mut self) { - if let Some(option) = self.physical_option.as_mut() { - option.cost = None; - } + pub(crate) fn price(&mut self, arena: &mut A) -> Option { + self.physical_option.as_ref()?; + let width = self.output_schema(arena).len(); + let rows = self.estimated_rows(); + let children = match self.childrens.as_mut() { + Childrens::None => ChildCosts::None, + Childrens::Only(child) => ChildCosts::Only(child.price(arena)), + Childrens::Twins { left, right } => { + ChildCosts::Twins(left.price(arena), right.price(arena)) + } + }; + let option = self.physical_option.as_ref()?; + let mut cost = PlanCost { + rows, + width, + startup: option.calculate_startup_work(&self.operator, &children), + total: None, + }; + cost.total = option.calculate_work(&self.operator, &children, rows, &cost); + Some(cost) } pub fn referenced_table(&self) -> Vec { @@ -302,6 +260,14 @@ impl LogicalPlan { tables } + pub(crate) fn visit_referenced_columns( + &self, + arena: &A, + f: &mut impl FnMut(&A, &crate::catalog::ColumnRef) -> bool, + ) -> Result { + Operator::referenced_columns(operator::ColumnVisitRoot::Plan(self), arena, f) + } + pub(crate) fn visit_column_refs( &self, arena: &mut A, @@ -311,14 +277,10 @@ impl LogicalPlan { A: MetaArena + ?Sized, F: FnMut(&crate::catalog::ColumnRef) + ?Sized, { - self.operator - .visit_referenced_columns(arena, &mut |_, column| { - f(column); - true - })?; - for child in self.childrens.iter() { - child.visit_column_refs(arena, f)?; - } + self.visit_referenced_columns(&*arena, &mut |_, column| { + f(column); + true + })?; Ok(()) } @@ -700,101 +662,6 @@ mod tests { assert_eq!(plan.estimated_rows(), None); } - #[test] - fn startup_cache_reuses_known_and_unknown_estimates_for_the_round() -> Result<(), DatabaseError> - { - let tables = TableArenaCell::default(); - let mut arena = PlanArena::new(&tables); - let id = column("id", LogicalType::Integer, &mut arena); - let key = arena.alloc_expression(ScalarExpression::column_expr(id, 0)); - let mut scan = table_scan("users", vec![id]); - scan.physical_option = Some(PhysicalOption::new( - PlanImpl::SeqScan, - SortOption::None, - RowCountOption::Unknown, - )); - let mut sort = LogicalPlan::new( - Operator::Sort(SortOperator { - sort_fields: vec![SortField { - expr: key, - asc: true, - nulls_first: false, - }], - }), - Childrens::Only(Box::new(scan)), - ); - sort.physical_option = Some(PhysicalOption::new( - PlanImpl::Sort, - SortOption::None, - RowCountOption::Follow, - )); - assert_eq!(sort.estimated_startup_work(&mut arena)?, None); - assert_eq!( - sort.physical_option - .as_ref() - .unwrap() - .cost - .map(|cost| cost.startup), - Some(None) - ); - assert_eq!(sort.estimated_startup_work(&mut arena)?, None); - assert_eq!( - sort.physical_option - .as_ref() - .unwrap() - .cost - .map(|cost| cost.startup), - Some(None) - ); - - // A new physical selection starts the next round with empty caches. - let Childrens::Only(scan) = sort.childrens.as_mut() else { - unreachable!() - }; - scan.physical_option = Some(PhysicalOption::new( - PlanImpl::SeqScan, - SortOption::None, - RowCountOption::Estimated { - rows: 16, - ndv: None, - }, - )); - sort.physical_option = Some(PhysicalOption::new( - PlanImpl::Sort, - SortOption::None, - RowCountOption::Follow, - )); - // 16 one-column rows (5 each) + 64 comparisons * 2 operator units. - assert_eq!(sort.estimated_startup_work(&mut arena)?, Some(208)); - assert_eq!( - sort.physical_option - .as_ref() - .unwrap() - .cost - .map(|cost| cost.startup), - Some(Some(208)) - ); - let Childrens::Only(scan) = sort.childrens.as_mut() else { - unreachable!() - }; - assert_eq!( - scan.physical_option - .as_ref() - .unwrap() - .cost - .map(|cost| cost.startup), - Some(Some(0)) - ); - // A cached parent does not walk its children again in this round. - scan.clear_work_cost(); - assert_eq!(sort.estimated_startup_work(&mut arena)?, Some(208)); - let Childrens::Only(scan) = sort.childrens.as_mut() else { - unreachable!() - }; - assert_eq!(scan.physical_option.as_ref().unwrap().cost, None); - Ok(()) - } - #[test] fn filter_and_projection_work_preserve_input_without_blocking() -> Result<(), DatabaseError> { for output_rows in [0, 8, 98] { @@ -821,9 +688,15 @@ mod tests { }, )); assert_eq!(filter.estimated_rows(), Some(output_rows)); - assert_eq!(filter.estimated_startup_work(&mut arena)?, Some(0)); assert_eq!( - filter.estimated_cost(&mut arena)?.map(|cost| cost.work), + filter.price(&mut arena).and_then(|cost| cost.startup), + Some(0) + ); + assert_eq!( + filter + .price(&mut arena) + .and_then(|cost| cost.input()) + .map(|cost| cost.work), Some(392) ); let mut project = LogicalPlan::new( @@ -835,9 +708,15 @@ mod tests { SortOption::Follow, RowCountOption::Follow, )); - assert_eq!(project.estimated_startup_work(&mut arena)?, Some(0)); assert_eq!( - project.estimated_cost(&mut arena)?.map(|cost| cost.work), + project.price(&mut arena).and_then(|cost| cost.startup), + Some(0) + ); + assert_eq!( + project + .price(&mut arena) + .and_then(|cost| cost.input()) + .map(|cost| cost.work), Some(392) ); } @@ -884,9 +763,14 @@ mod tests { )); // 98 one-column input rows (5 each) + 24 comparisons * 2 operator units, // then 8 output rows. - assert_eq!(sort.estimated_startup_work(&mut arena)?, Some(538)); assert_eq!( - sort.estimated_cost(&mut arena)?.map(|cost| cost.work), + sort.price(&mut arena).and_then(|cost| cost.startup), + Some(538) + ); + assert_eq!( + sort.price(&mut arena) + .and_then(|cost| cost.input()) + .map(|cost| cost.work), Some(578) ); let mut unknown = table_scan("unknown", vec![]); @@ -901,9 +785,15 @@ mod tests { SortOption::Follow, RowCountOption::Estimated { rows: 8, ndv: None }, )); - assert_eq!(filter.estimated_startup_work(&mut arena)?, Some(0)); assert_eq!( - filter.estimated_cost(&mut arena)?.map(|cost| cost.work), + filter.price(&mut arena).and_then(|cost| cost.startup), + Some(0) + ); + assert_eq!( + filter + .price(&mut arena) + .and_then(|cost| cost.input()) + .map(|cost| cost.work), None ); Ok(()) diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 320cd7d4..be1fd133 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -92,8 +92,8 @@ use crate::planner::operator::truncate::TruncateOperator; use crate::planner::operator::union::UnionOperator; use crate::planner::operator::update::UpdateOperator; use crate::planner::operator::values::ValuesOperator; -use crate::planner::operator::visitor::{ChildrenVisit, OperatorVisitor}; -use crate::planner::{fmt_explain_list, Childrens, Explain, ExprRef, LogicalPlan, MetaArena}; +use crate::planner::operator::visitor::{walk_plan, ChildrenVisit, OperatorVisitor}; +use crate::planner::{fmt_explain_list, Explain, ExprRef, LogicalPlan, MetaArena}; use crate::types::index::{IndexInfo, IndexMetaRef}; use kite_sql_serde_macros::ReferenceSerialization; use std::hash::{Hash, Hasher}; @@ -123,6 +123,7 @@ pub enum Operator { Union(UnionOperator), RecursiveCte(RecursiveCteOperator), RecursiveScan(RecursiveScanOperator), + IndexProbeJoin(IndexProbeJoinOperator), // DML Insert(InsertOperator), Update(UpdateOperator), @@ -145,8 +146,6 @@ pub enum Operator { #[cfg(feature = "copy")] CopyToFile(CopyToFileOperator), Window(window::WindowOperator), - // Appended to keep the serialized indices of earlier variants stable. - IndexProbeJoin(IndexProbeJoinOperator), } #[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] @@ -181,8 +180,6 @@ pub(crate) const PROBE_RESET_WORK: usize = CPU_ROW_WORK; // costs ~7 scanned rows per build/probe pair (RFC 0001 §4.2). pub(crate) const HASH_BUILD_WORK: usize = 6 * CPU_ROW_WORK; -/// Work of producing `rows` rows of `width` columns. The fixed per-row part keeps -/// zero-column rows (e.g. the input of `count(*)`) from being free. pub(crate) fn rows_work(rows: usize, width: usize) -> usize { rows.saturating_mul(CPU_ROW_WORK.saturating_add(width.saturating_mul(CPU_OPERATOR_WORK))) } @@ -195,28 +192,17 @@ fn hash_build_work(build: &InputCost) -> usize { pub(crate) struct InputCost { pub(crate) rows: usize, pub(crate) width: usize, - /// Work of producing all rows once, including the input's own inputs. pub(crate) work: usize, } -/// Join implementations priced from input statistics alone, so the join reorder can price -/// joins it has not built yet. `left` is the hash build side, the nested loop outer side and -/// the index probe outer side. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum JoinImpl { Hash, - /// `full` re-reads the inner input once more to emit its unmatched rows. - NestLoop { - full: bool, - }, - /// The probed (right) input is never read whole; each outer row pays `lookup_work`. - IndexProbe { - lookup_work: usize, - }, + NestLoop { full: bool }, + IndexProbe { lookup_work: usize }, } impl JoinImpl { - /// Total work, including reading both inputs. pub(crate) fn work( self, left: &InputCost, @@ -247,34 +233,44 @@ impl JoinImpl { } } -pub(crate) fn input_cost(input: &LogicalPlan) -> Option { - let cost = input.physical_option.as_ref()?.cost?; - Some(InputCost { - rows: input.estimated_rows()?, - width: cost.width, - work: cost.total?, - }) -} - -/// A node's cached work estimates, filled by `StartupWorkVisitor` and invalidated as a whole. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct WorkCost { - /// Output column count. +pub(crate) struct PlanCost { + pub(crate) rows: Option, pub(crate) width: usize, - /// Work before the first row; `None` when unknown. pub(crate) startup: Option, - /// Work of producing every row; `None` when unknown. pub(crate) total: Option, } +impl PlanCost { + pub(crate) fn input(&self) -> Option { + Some(InputCost { + rows: self.rows?, + width: self.width, + work: self.total?, + }) + } +} + +/// The inputs' costs, shaped like `Childrens`; `None` marks an input that cannot be priced. +pub(crate) enum ChildCosts { + None, + Only(Option), + Twins(Option, Option), +} + +fn input(cost: &Option) -> Option { + cost.as_ref()?.input() +} + +fn startup(cost: &Option) -> Option { + cost.as_ref()?.startup +} + #[derive(Debug, Clone)] pub struct PhysicalOption { pub plan: PlanImpl, pub(crate) sort_option: SortOption, pub(crate) row_count: RowCountOption, - /// `None` until computed. - pub(crate) cost: Option, - /// Per-lookup work of a parameterized probe, priced with index statistics. pub(crate) lookup_work: Option, pub(crate) join_decided: bool, } @@ -285,7 +281,6 @@ impl PhysicalOption { plan, sort_option, row_count, - cost: None, lookup_work: None, join_decided: false, } @@ -295,16 +290,14 @@ impl PhysicalOption { &self.sort_option } - /// Probes `right` once per `left` row; without a planned `lookup_work`, one probe costs - /// the right input's startup plus resetting it and reading the matched row. fn index_probe_work( &self, - left: &LogicalPlan, - right: &LogicalPlan, + left: &Option, + right: &Option, rows: usize, - cost: &WorkCost, + cost: &PlanCost, ) -> Option { - let right_cost = right.physical_option.as_ref()?.cost?; + let right_cost = (*right)?; let probed = InputCost { rows: 1, width: right_cost.width, @@ -316,12 +309,7 @@ impl PhysicalOption { .saturating_add(PROBE_RESET_WORK) .saturating_add(rows_work(probed.rows, probed.width)) }); - Some(JoinImpl::IndexProbe { lookup_work }.work( - &input_cost(left)?, - &probed, - rows, - cost.width, - )) + Some(JoinImpl::IndexProbe { lookup_work }.work(&input(left)?, &probed, rows, cost.width)) } /// Join formulas: RFC 0001 §4.2 (`rfc/0001-greedy-join-reorder-owns-join-decisions.md`); @@ -329,16 +317,16 @@ impl PhysicalOption { pub(crate) fn calculate_work( &self, operator: &Operator, - childrens: &Childrens, + children: &ChildCosts, rows: Option, - cost: &WorkCost, + cost: &PlanCost, ) -> Option { - match (&self.plan, operator, childrens) { - (PlanImpl::Filter, Operator::Filter(_), Childrens::Only(input)) - | (PlanImpl::Project, Operator::Project(_), Childrens::Only(input)) => { - Some(input_cost(input)?.work) + match (&self.plan, operator, children) { + (PlanImpl::Filter, Operator::Filter(_), ChildCosts::Only(child)) + | (PlanImpl::Project, Operator::Project(_), ChildCosts::Only(child)) => { + Some(input(child)?.work) } - (PlanImpl::MarkApply, Operator::MarkApply(op), Childrens::Twins { left, right }) + (PlanImpl::MarkApply, Operator::MarkApply(op), ChildCosts::Twins(left, right)) if op.parameterized_probe().is_some() && !op.materialize => { self.index_probe_work(left, right, rows?, cost) @@ -346,12 +334,12 @@ impl PhysicalOption { ( PlanImpl::IndexProbeJoin, Operator::IndexProbeJoin(_), - Childrens::Twins { left, right }, + ChildCosts::Twins(left, right), ) => self.index_probe_work(left, right, rows?, cost), - (PlanImpl::HashJoin, Operator::Join(_), Childrens::Twins { left, right }) => Some( - JoinImpl::Hash.work(&input_cost(left)?, &input_cost(right)?, rows?, cost.width), - ), - (PlanImpl::NestLoopJoin, Operator::Join(join), Childrens::Twins { left, right }) => { + (PlanImpl::HashJoin, Operator::Join(_), ChildCosts::Twins(left, right)) => { + Some(JoinImpl::Hash.work(&input(left)?, &input(right)?, rows?, cost.width)) + } + (PlanImpl::NestLoopJoin, Operator::Join(join), ChildCosts::Twins(left, right)) => { let (outer, inner) = if join.join_type == join::JoinType::RightOuter { (right, left) } else { @@ -361,12 +349,7 @@ impl PhysicalOption { JoinImpl::NestLoop { full: join.join_type == join::JoinType::Full, } - .work( - &input_cost(outer)?, - &input_cost(inner)?, - rows?, - cost.width, - ), + .work(&input(outer)?, &input(inner)?, rows?, cost.width), ) } _ => Some(cost.startup?.saturating_add(rows_work(rows?, cost.width))), @@ -376,15 +359,14 @@ impl PhysicalOption { pub(crate) fn calculate_startup_work( &self, operator: &Operator, - childrens: &Childrens, + children: &ChildCosts, ) -> Option { - let startup_work = |input: &LogicalPlan| input.physical_option.as_ref()?.cost?.startup; - let work = |input: &LogicalPlan| Some(input_cost(input)?.work); - match (&self.plan, operator, childrens) { + let work = |child: &Option| Some(input(child)?.work); + match (&self.plan, operator, children) { ( PlanImpl::SeqScan | PlanImpl::IndexScan(_) | PlanImpl::Values | PlanImpl::Dummy, _, - Childrens::None, + ChildCosts::None, ) => Some(0), ( PlanImpl::Project @@ -393,29 +375,29 @@ impl PhysicalOption { | PlanImpl::StreamAggregate | PlanImpl::StreamDistinct, _, - Childrens::Only(input), - ) => startup_work(input), - (PlanImpl::HashJoin, _, Childrens::Twins { left, right }) => { - input_cost(left).and_then(|build| { + ChildCosts::Only(child), + ) => startup(child), + (PlanImpl::HashJoin, _, ChildCosts::Twins(left, right)) => { + input(left).and_then(|build| { Some( build .work .saturating_add(hash_build_work(&build)) - .saturating_add(startup_work(right)?), + .saturating_add(startup(right)?), ) }) } - (PlanImpl::NestLoopJoin, _, Childrens::Twins { left, right }) => startup_work(left) - .and_then(|left| startup_work(right).map(|right| left.saturating_add(right))), - (PlanImpl::Sort | PlanImpl::TopK | PlanImpl::Window, _, Childrens::Only(input)) => { + (PlanImpl::NestLoopJoin, _, ChildCosts::Twins(left, right)) => startup(left) + .and_then(|left| startup(right).map(|right| left.saturating_add(right))), + (PlanImpl::Sort | PlanImpl::TopK | PlanImpl::Window, _, ChildCosts::Only(child)) => { let keys = match operator { Operator::Sort(op) => op.sort_fields.len(), Operator::TopK(op) => op.sort_fields.len(), Operator::Window(op) => op.sort_fields.len(), _ => unreachable!(), }; - work(input).and_then(|read| { - let rows = input.estimated_rows()?; + work(child).and_then(|read| { + let rows = child.as_ref()?.rows?; let comparisons = if rows <= 1 { 0 } else { @@ -435,7 +417,7 @@ impl PhysicalOption { ( PlanImpl::SimpleAggregate | PlanImpl::HashAggregate, Operator::Aggregate(op), - Childrens::Only(input), + ChildCosts::Only(child), ) => { let per_row = op .agg_calls @@ -448,16 +430,16 @@ impl PhysicalOption { } else { 0 }); - work(input).and_then(|read| { - Some(read.saturating_add(input.estimated_rows()?.saturating_mul(per_row))) + work(child).and_then(|read| { + Some(read.saturating_add(child.as_ref()?.rows?.saturating_mul(per_row))) }) } - (PlanImpl::MarkApply, Operator::MarkApply(op), Childrens::Twins { left, right }) => { - startup_work(left).and_then(|left| { + (PlanImpl::MarkApply, Operator::MarkApply(op), ChildCosts::Twins(left, right)) => { + startup(left).and_then(|left| { let right = if op.materialize { work(right) } else { - startup_work(right) + startup(right) }?; Some(left.saturating_add(right)) }) @@ -465,17 +447,18 @@ impl PhysicalOption { ( PlanImpl::IndexProbeJoin, Operator::IndexProbeJoin(_), - Childrens::Twins { left, right }, - ) => startup_work(left) - .and_then(|left| startup_work(right).map(|right| left.saturating_add(right))), - (PlanImpl::ScalarQueryInit, _, Childrens::Twins { left, right }) => startup_work(left) - .and_then(|left| work(right).map(|right| left.saturating_add(right))), + ChildCosts::Twins(left, right), + ) => startup(left) + .and_then(|left| startup(right).map(|right| left.saturating_add(right))), + (PlanImpl::ScalarQueryInit, _, ChildCosts::Twins(left, right)) => { + startup(left).and_then(|left| work(right).map(|right| left.saturating_add(right))) + } _ => None, } } } -// Cost caches and Greedy's join decision are pass-local, not plan semantics or wire data. +// Probe lookup work and Greedy's join decision are pass-local, not plan semantics or wire data. impl PartialEq for PhysicalOption { fn eq(&self, other: &Self) -> bool { self.plan == other.plan @@ -647,11 +630,26 @@ impl Explain for Operator { } } +/// Where a referenced-column visit starts: one operator, or a plan and all its inputs. +pub(crate) enum ColumnVisitRoot<'a> { + Operator(&'a Operator), + Plan(&'a LogicalPlan), +} + impl Operator { pub fn visit_referenced_columns( &self, arena: &A, f: &mut impl FnMut(&A, &ColumnRef) -> bool, + ) -> Result { + Self::referenced_columns(ColumnVisitRoot::Operator(self), arena, f) + } + + /// Calls `f` for each referenced column until it returns false; returns whether it ran to the end. + pub(crate) fn referenced_columns( + root: ColumnVisitRoot<'_>, + arena: &A, + f: &mut impl FnMut(&A, &ColumnRef) -> bool, ) -> Result { struct ReferencedColumnVisitor<'a, A: ?Sized, F> { arena: &'a A, @@ -701,6 +699,13 @@ impl Operator { A: MetaArena + ?Sized, F: FnMut(&A, &ColumnRef) -> bool, { + fn visit_plan(&mut self, plan: &'operator LogicalPlan) -> Result<(), DatabaseError> { + if self.keep_going { + walk_plan(self, plan)?; + } + Ok(()) + } + fn visit_aggregate( &mut self, op: &'operator AggregateOperator, @@ -954,7 +959,12 @@ impl Operator { f, keep_going: true, }; - visitor.visit_operator(self, None, &mut ChildrenVisit::empty())?; + match root { + ColumnVisitRoot::Operator(operator) => { + visitor.visit_operator(operator, None, &mut ChildrenVisit::empty())? + } + ColumnVisitRoot::Plan(plan) => visitor.visit_plan(plan)?, + } Ok(visitor.keep_going) } diff --git a/src/planner/operator/visitor.rs b/src/planner/operator/visitor.rs index 095bf79e..e60699c3 100644 --- a/src/planner/operator/visitor.rs +++ b/src/planner/operator/visitor.rs @@ -943,87 +943,6 @@ pub(crate) mod tests { Ok(()) } - #[test] - fn physical_option_error_stops_read_only_plan_traversal() { - struct FailOnPhysical(usize); - - impl<'a> OperatorVisitor<'a> for FailOnPhysical { - fn visit_dummy(&mut self) -> Result<(), DatabaseError> { - self.0 += 1; - Ok(()) - } - - fn visit_physical_option( - &mut self, - _option: &'a PhysicalOption, - ) -> Result<(), DatabaseError> { - Err(DatabaseError::invalid_column("physical")) - } - } - - let child = LogicalPlan::new(Operator::Dummy, Childrens::None); - let mut plan = LogicalPlan::new(Operator::Dummy, Childrens::Only(Box::new(child))); - plan.physical_option = Some(PhysicalOption::new( - PlanImpl::Dummy, - SortOption::None, - RowCountOption::Unknown, - )); - let mut visitor = FailOnPhysical(0); - assert!(visitor.visit_plan(&plan).is_err()); - assert_eq!(visitor.0, 1); - } - - #[test] - fn traverses_read_only_plans_in_input_order() -> Result<(), DatabaseError> { - #[derive(Default)] - struct VisitOrder(Vec<&'static str>); - - impl<'a> OperatorVisitor<'a> for VisitOrder { - fn visit_project( - &mut self, - _op: &'a ProjectOperator, - _children: &mut ChildrenVisit<'a>, - ) -> Result<(), DatabaseError> { - self.0.push("project"); - Ok(()) - } - - fn visit_join( - &mut self, - _op: &'a JoinOperator, - _children: &mut ChildrenVisit<'a>, - ) -> Result<(), DatabaseError> { - self.0.push("join"); - Ok(()) - } - - fn visit_dummy(&mut self) -> Result<(), DatabaseError> { - self.0.push("dummy"); - Ok(()) - } - } - - let right = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![])), - Childrens::Only(Box::new(LogicalPlan::new(Operator::Dummy, Childrens::None))), - ); - let join = JoinOperator::build( - LogicalPlan::new(Operator::Dummy, Childrens::None), - right, - JoinCondition::None, - JoinType::Cross, - false, - ); - let plan = LogicalPlan::new( - Operator::Project(ProjectOperator::new(vec![])), - Childrens::Only(Box::new(join)), - ); - let mut visitor = VisitOrder::default(); - visitor.visit_plan(&plan)?; - assert_eq!(visitor.0, ["project", "join", "dummy", "project", "dummy"]); - Ok(()) - } - #[test] fn dispatches_all_variants_and_visits_expressions() -> Result<(), DatabaseError> { struct NoopVisitor; diff --git a/src/serdes/physical_option.rs b/src/serdes/physical_option.rs index 9d26fd31..503bdcf1 100644 --- a/src/serdes/physical_option.rs +++ b/src/serdes/physical_option.rs @@ -84,7 +84,6 @@ impl ReferenceSerialization for PhysicalOption { plan: PlanImpl::decode(reader, context, reference_tables, arena)?, sort_option: SortOption::decode(reader, context, reference_tables, arena)?, row_count: RowCountOption::decode(reader, context, reference_tables, arena)?, - cost: None, lookup_work: None, join_decided: false, }) @@ -101,7 +100,6 @@ mod tests { use super::*; use crate::expression::ScalarExpression; use crate::planner::operator::sort::SortField; - use crate::planner::operator::WorkCost; use crate::planner::{PlanArena, TableArenaCell}; #[cfg(all(not(feature = "rocksdb"), feature = "lmdb"))] use crate::storage::lmdb::LmdbTransaction as TestTransaction; @@ -146,7 +144,6 @@ mod tests { &mut arena, )?; assert_eq!(decoded, option); - assert_eq!(decoded.cost, None); assert!(reader.is_empty()); } } @@ -154,7 +151,7 @@ mod tests { } #[test] - fn physical_option_keeps_wire_order_and_discards_startup_cache() -> Result<(), DatabaseError> { + fn physical_option_keeps_wire_order() -> Result<(), DatabaseError> { let tables = TableArenaCell::default(); let mut arena = PlanArena::new(&tables); let key = arena.alloc_expression(ScalarExpression::Constant(DataValue::Int32(1))); @@ -178,7 +175,7 @@ mod tests { ndv: Some(37), }; for is_direct in [false, true] { - // This is the pre-cache wire layout: exactly three ordered fields. + // The wire layout is exactly three ordered fields. let mut expected = Vec::new(); let mut reference_tables = ReferenceTables::new(); option @@ -190,50 +187,42 @@ mod tests { option .row_count .encode(&mut expected, is_direct, &mut reference_tables, &arena)?; - for cache in [None, Some(None), Some(Some(192))] { - option.cost = cache.map(|startup| WorkCost { - width: 1, - startup, - total: startup, - }); - let mut encoded = Vec::new(); - let mut references = ReferenceTables::new(); - option.encode(&mut encoded, is_direct, &mut references, &arena)?; - assert_eq!(encoded, expected); - assert_eq!(references, reference_tables); - let mut reader = encoded.as_slice(); - let decoded = PhysicalOption::decode::, _, _>( - &mut reader, - None, - &references, - &mut arena, - )?; - assert_eq!(decoded.plan, PlanImpl::Sort); - assert_eq!( - decoded.row_count, - RowCountOption::Estimated { - rows: 42, - ndv: None - } - ); - assert_eq!(decoded.cost, None); - let SortOption::OrderBy { - fields, - ignore_prefix_len, - } = decoded.sort_option - else { - unreachable!() - }; - assert_eq!(ignore_prefix_len, 1); - assert_eq!(fields.len(), 1); - assert!(!fields[0].asc); - assert!(fields[0].nulls_first); - assert_eq!( - arena.expression(fields[0].expr), - &ScalarExpression::Constant(DataValue::Int32(1)) - ); - assert!(reader.is_empty()); - } + let mut encoded = Vec::new(); + let mut references = ReferenceTables::new(); + option.encode(&mut encoded, is_direct, &mut references, &arena)?; + assert_eq!(encoded, expected); + assert_eq!(references, reference_tables); + let mut reader = encoded.as_slice(); + let decoded = PhysicalOption::decode::, _, _>( + &mut reader, + None, + &references, + &mut arena, + )?; + assert_eq!(decoded.plan, PlanImpl::Sort); + assert_eq!( + decoded.row_count, + RowCountOption::Estimated { + rows: 42, + ndv: None + } + ); + let SortOption::OrderBy { + fields, + ignore_prefix_len, + } = decoded.sort_option + else { + unreachable!() + }; + assert_eq!(ignore_prefix_len, 1); + assert_eq!(fields.len(), 1); + assert!(!fields[0].asc); + assert!(fields[0].nulls_first); + assert_eq!( + arena.expression(fields[0].expr), + &ScalarExpression::Constant(DataValue::Int32(1)) + ); + assert!(reader.is_empty()); } Ok(()) } diff --git a/src/types/value.rs b/src/types/value.rs index bee185e8..0ff77c2f 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -2473,41 +2473,72 @@ mod test { } #[test] - fn memcomparable_skip_keeps_following_values_aligned() -> Result<(), DatabaseError> { - let cases = [ - (LogicalType::Integer, DataValue::Int32(-42)), + fn memcomparable_skip_stops_at_the_value_boundary() -> Result<(), DatabaseError> { + // Fixed-width scalars, at their initial value. + let mut cases: Vec<_> = [ + LogicalType::Boolean, + LogicalType::Tinyint, + LogicalType::Smallint, + LogicalType::Integer, + LogicalType::Bigint, + LogicalType::UTinyint, + LogicalType::USmallint, + LogicalType::UInteger, + LogicalType::UBigint, + LogicalType::Float, + LogicalType::Double, + LogicalType::Date, + LogicalType::DateTime, + LogicalType::Time(Some(3)), + LogicalType::TimeStamp(Some(6), true), + ] + .into_iter() + .map(|ty| (DataValue::init(&ty), ty)) + .collect(); + // Variable payloads around the 8-byte group boundary. + for length in [0, 7, 8, 9, 65] { + cases.push(( + DataValue::Binary { + value: vec![0xff; length], + len: None, + }, + LogicalType::Varbinary(None), + )); + } + cases.extend([ ( - LogicalType::Varchar(None, CharLengthUnits::Characters), DataValue::Utf8 { value: "汉字abcdefgh".into(), ty: Utf8Type::Variable(None), unit: CharLengthUnits::Characters, }, + LogicalType::Varchar(None, CharLengthUnits::Characters), ), ( - LogicalType::Char(16, CharLengthUnits::Octets), DataValue::Utf8 { value: "0123456789abcdef".into(), ty: Utf8Type::Fixed(16), unit: CharLengthUnits::Octets, }, + LogicalType::Char(16, CharLengthUnits::Octets), ), ( - LogicalType::Varbinary(None), - DataValue::Binary { - value: vec![0xff; 19], - len: None, - }, - ), - ( + DataValue::Tuple(vec![DataValue::Int32(1), DataValue::Null]), LogicalType::Tuple(vec![ LogicalType::Integer, LogicalType::Varchar(None, CharLengthUnits::Characters), ]), - DataValue::Tuple(vec![DataValue::Int32(1), DataValue::Null]), ), - ]; - for (ty, value) in cases { + ]); + #[cfg(feature = "decimal")] + for text in ["0", "-0.001", "79228162514264337593543950335"] { + cases.push(( + DataValue::Decimal(text.parse::().unwrap()), + LogicalType::Decimal(None, None), + )); + } + + for (value, ty) in cases { for value in [value, DataValue::Null] { let mut bytes = Vec::new(); value.memcomparable_encode(&mut bytes)?; @@ -2515,73 +2546,16 @@ mod test { DataValue::Int32(99).memcomparable_encode(&mut bytes)?; let mut reader = Cursor::new(bytes.as_slice()); DataValue::memcomparable_skip(&mut reader, &ty)?; - assert_eq!(reader.position() as usize, boundary); + assert_eq!(reader.position() as usize, boundary, "{ty:?}"); assert_eq!( DataValue::memcomparable_decode(&mut reader, &LogicalType::Integer)?, DataValue::Int32(99) ); - assert_eq!(reader.position() as usize, bytes.len()); } } Ok(()) } - #[test] - fn memcomparable_skip_matches_all_scalar_payload_widths() -> Result<(), DatabaseError> { - for ty in [ - LogicalType::Boolean, - LogicalType::Tinyint, - LogicalType::Smallint, - LogicalType::Integer, - LogicalType::Bigint, - LogicalType::UTinyint, - LogicalType::USmallint, - LogicalType::UInteger, - LogicalType::UBigint, - LogicalType::Float, - LogicalType::Double, - LogicalType::Date, - LogicalType::DateTime, - LogicalType::Time(Some(3)), - LogicalType::TimeStamp(Some(6), true), - ] { - let mut bytes = Vec::new(); - DataValue::init(&ty).memcomparable_encode(&mut bytes)?; - let boundary = bytes.len(); - DataValue::Int32(99).memcomparable_encode(&mut bytes)?; - let mut reader = Cursor::new(bytes.as_slice()); - DataValue::memcomparable_skip(&mut reader, &ty)?; - assert_eq!(reader.position() as usize, boundary, "{ty:?}"); - assert_eq!( - DataValue::memcomparable_decode(&mut reader, &LogicalType::Integer)?, - DataValue::Int32(99) - ); - } - Ok(()) - } - - #[test] - fn memcomparable_skip_matches_variable_group_boundaries() -> Result<(), DatabaseError> { - for length in [0, 1, 7, 8, 9, 16, 65] { - let value = DataValue::Binary { - value: vec![0xff; length], - len: None, - }; - let mut bytes = Vec::new(); - value.memcomparable_encode(&mut bytes)?; - let mut reader = Cursor::new(bytes.as_slice()); - assert_eq!( - DataValue::memcomparable_decode(&mut reader, &LogicalType::Varbinary(None))?, - value - ); - assert_eq!(reader.position() as usize, bytes.len()); - reader.set_position(0); - DataValue::memcomparable_skip(&mut reader, &LogicalType::Varbinary(None))?; - assert_eq!(reader.position() as usize, bytes.len()); - } - Ok(()) - } - #[test] fn memcomparable_skip_rejects_truncation_and_invalid_markers() { assert!(DataValue::memcomparable_skip( @@ -2613,34 +2587,6 @@ mod test { } } - #[cfg(feature = "decimal")] - #[test] - fn memcomparable_skip_decimal_uses_variable_payload() -> Result<(), DatabaseError> { - for text in [ - "0", - "1", - "-1", - "123.456", - "-0.001", - "79228162514264337593543950335", - "0.0000000000000000000000000001", - ] { - let value = DataValue::Decimal(text.parse::().unwrap()); - let mut bytes = Vec::new(); - value.memcomparable_encode(&mut bytes)?; - let boundary = bytes.len(); - DataValue::Int32(99).memcomparable_encode(&mut bytes)?; - let mut reader = Cursor::new(bytes.as_slice()); - DataValue::memcomparable_skip(&mut reader, &LogicalType::Decimal(None, None))?; - assert_eq!(reader.position() as usize, boundary); - assert_eq!( - DataValue::memcomparable_decode(&mut reader, &LogicalType::Integer)?, - DataValue::Int32(99) - ); - } - Ok(()) - } - #[test] fn test_memcomparable_decode_rejects_invalid_strings() { let mut invalid_marker = vec![NOTNULL_TAG]; From d2dee1e9812d8378ce5bd933b7017947df585520 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 16:23:02 +0800 Subject: [PATCH 56/62] fix: resolve clippy lints and propagate binder extend errors --- src/binder/parser.rs | 2 +- src/binder/select.rs | 2 +- src/db.rs | 22 ++------- src/optimizer/heuristic/batch.rs | 1 + .../rule/normalization/combine_operators.rs | 6 +-- .../rule/normalization/elimination.rs | 16 +++---- .../rule/normalization/join_reorder.rs | 48 ++++++++++--------- .../rule/normalization/pushdown_predicates.rs | 2 +- .../rule/normalization/simplification.rs | 4 +- src/orm/mod.rs | 2 +- 10 files changed, 48 insertions(+), 57 deletions(-) diff --git a/src/binder/parser.rs b/src/binder/parser.rs index c1e3d9c1..365bba83 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -1718,7 +1718,7 @@ impl TryFrom<&Value> for DataValue { )); } let mut bytes = Vec::with_capacity(hex.len() / 2); - for pair in hex.as_bytes().chunks_exact(2) { + for pair in hex.as_bytes().as_chunks::<2>().0 { let Some(high) = (pair[0] as char).to_digit(16) else { return Err(DatabaseError::InvalidValue("invalid hex digit".to_string())); }; diff --git a/src/binder/select.rs b/src/binder/select.rs index 38b2aace..79d817ac 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -280,7 +280,7 @@ where join_type: JoinType, constraint: JoinConstraintInput, ) -> Result { - self.binder.extend(right_context, self.arena); + self.binder.extend(right_context, self.arena)?; self.plan = self .binder .bind_join_plans(self.plan, right_plan, join_type, constraint, self.arena)?; diff --git a/src/db.rs b/src/db.rs index f340d137..f602f57f 100644 --- a/src/db.rs +++ b/src/db.rs @@ -70,6 +70,8 @@ use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +/// Output schema, execution-time arena, and executor of a built statement. +pub(crate) type Execution<'a, A, T> = (Schema, ExecMetaArena, Executor<'a, T>); pub(crate) type ScalaFunctions = HashMap>; pub(crate) type TableFunctions = HashMap; @@ -750,14 +752,7 @@ impl State { transaction: &'a mut S::TransactionType<'txn>, plan: PlanInput<'a>, mut plan_arena: A, - ) -> Result< - ( - Schema, - ExecMetaArena, - Executor<'a, S::TransactionType<'txn>>, - ), - DatabaseError, - > + ) -> Result>, DatabaseError> where S: 'txn, { @@ -785,14 +780,7 @@ impl State { transaction: &'a mut S::TransactionType<'txn>, params: A, build: F, - ) -> Result< - ( - Schema, - ExecMetaArena>, - Executor<'a, S::TransactionType<'txn>>, - ), - DatabaseError, - > + ) -> Result, S::TransactionType<'txn>>, DatabaseError> where S: 'txn, A: AsRef<[(usize, LogicalType)]>, @@ -1598,7 +1586,7 @@ pub(crate) mod test { Operator::Values(ValuesOperator { rows, row_count: values.len(), - schema_ref: vec![column].into(), + schema_ref: vec![column], }), Childrens::None, ), diff --git a/src/optimizer/heuristic/batch.rs b/src/optimizer/heuristic/batch.rs index 8269e350..fb351496 100644 --- a/src/optimizer/heuristic/batch.rs +++ b/src/optimizer/heuristic/batch.rs @@ -148,6 +148,7 @@ impl HepBatchStrategy { Self::Once(HepMatchOrder::TopDown) } + #[cfg(test)] pub fn once_bottomup() -> Self { Self::Once(HepMatchOrder::BottomUp) } diff --git a/src/optimizer/rule/normalization/combine_operators.rs b/src/optimizer/rule/normalization/combine_operators.rs index f8d6edb8..12a91bd3 100644 --- a/src/optimizer/rule/normalization/combine_operators.rs +++ b/src/optimizer/rule/normalization/combine_operators.rs @@ -40,8 +40,6 @@ impl NormalizationRule for CombineFilter { return Ok(false); } }; - let parent_filter = parent_filter; - let cursor = match only_child_mut(plan) { Some(child) => child, None => { @@ -68,11 +66,11 @@ impl NormalizationRule for CombineFilter { child_op.having = having || child_op.having; child_op.is_optimized = false; - return Ok(replace_with_only_child(plan)); + Ok(replace_with_only_child(plan)) } _ => { plan.operator = Operator::Filter(parent_filter); - return Ok(false); + Ok(false) } } } diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index 66681eb1..901126df 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -1125,7 +1125,8 @@ mod tests { fn cross_join_remaps_by_new_left_width() -> Result<(), DatabaseError> { // (left slots, right slots, outer slots, expected positions); an empty left // mapping must not be treated as identity. - let cases: [(&[usize], Option<&[usize]>, &[usize], &[usize]); 3] = [ + type Case<'a> = (&'a [usize], Option<&'a [usize]>, &'a [usize], &'a [usize]); + let cases: [Case; 3] = [ (&[2], Some(&[1, 0]), &[0, 1, 2], &[2, 4, 3]), (&[2], None, &[0, 1, 2], &[2, 3, 4]), (&[], None, &[0, 1], &[3, 4]), @@ -1472,13 +1473,12 @@ mod tests { let mut plan = build_filter_with_selected_index(&mut arena, predicate, residual); if through_init { let scan = plan.childrens.pop_only(); - plan.childrens = - Box::new(Childrens::Only(Box::new(ScalarQueryInitOperator::build( - scan, - LogicalPlan::new(Operator::Dummy, Childrens::None), - value, - vec![], - )))); + *plan.childrens = Childrens::Only(Box::new(ScalarQueryInitOperator::build( + scan, + LogicalPlan::new(Operator::Dummy, Childrens::None), + value, + vec![], + ))); } let input = plan.childrens.only().clone(); assert!(rule.apply(&mut plan, &mut arena, None)?); diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index 3107981d..4a1fd94f 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -556,7 +556,7 @@ impl NormalizationRule for GreedyJoinReorder { schema: leaf_schema, index: leaf_stats.map(|(index, ndv)| (arena.index(index), ndv)), }, - &candidate_keys, + candidate_keys, ndvs, arena, ) @@ -752,10 +752,7 @@ mod tests { plan.physical_option = Some(PhysicalOption::new( PlanImpl::SeqScan, SortOption::None, - RowCountOption::Estimated { - rows: rows, - ndv: None, - }, + RowCountOption::Estimated { rows, ndv: None }, )); (plan, key) } @@ -798,10 +795,8 @@ mod tests { fn malformed_regions_are_planning_errors() { // (outer join key, expected message). Each side of a key is only searched in its own // input range, so a left column used on the right is reported as not produced. - let cases: [( - fn(ExprRef, ExprRef, &mut PlanArena) -> (ExprRef, ExprRef), - &str, - ); 2] = [ + type OuterKey = fn(ExprRef, ExprRef, &mut PlanArena) -> (ExprRef, ExprRef); + let cases: [(OuterKey, &str); 2] = [ (|a, _, _| (a, a), "is not produced by any join input"), ( |_, c, arena| (input("z", 1, arena).1, c), @@ -893,15 +888,24 @@ mod tests { } let loader = StatisticMetaLoader::new(&cache); assert_eq!( - column_ndv(&left, &right, &[(l, r)], Some(&loader), &arena)?, + column_ndv(&left, &right, [(l, r)], Some(&loader), &arena)?, Some((100, 100)) ); - assert_eq!(column_ndv(&left, &right, &[(l, r)], None, &arena)?, None); - assert_eq!(column_ndv(&left, &right, &[], Some(&loader), &arena)?, None); + assert_eq!(column_ndv(&left, &right, [(l, r)], None, &arena)?, None); + assert_eq!( + column_ndv( + &left, + &right, + [] as [(ExprRef, ExprRef); 0], + Some(&loader), + &arena + )?, + None + ); left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { rows: 8, ndv: None }; assert_eq!( - column_ndv(&left, &right, &[(l, r)], Some(&loader), &arena)?, + column_ndv(&left, &right, [(l, r)], Some(&loader), &arena)?, Some((8, 100)) ); left.physical_option.as_mut().unwrap().row_count = RowCountOption::Estimated { @@ -915,7 +919,7 @@ mod tests { column_ndv( &left, &right, - &[(l, r)], + [(l, r)], Some(&StatisticMetaLoader::new(&cache)), &arena )?, @@ -926,7 +930,7 @@ mod tests { column_ndv( &left, &right, - &[(l, r)], + [(l, r)], Some(&StatisticMetaLoader::new(&cache)), &arena )?, @@ -953,12 +957,12 @@ mod tests { let right_schema = right.output_schema(&mut arena); let l_input = JoinInputStatistics { rows: 1000, - schema: &left_schema, + schema: left_schema, index: Some((arena.index(li), Some(100))), }; let r_input = JoinInputStatistics { rows: 200, - schema: &right_schema, + schema: right_schema, index: Some((arena.index(ri), Some(100))), }; assert_eq!( @@ -973,7 +977,7 @@ mod tests { unique_right.ty = IndexType::Unique; let r_input = JoinInputStatistics { rows: 200, - schema: &right_schema, + schema: right_schema, index: Some((&unique_right, None)), }; assert_eq!( @@ -984,7 +988,7 @@ mod tests { unique_left.ty = IndexType::PrimaryKey { is_multiple: false }; let l_input = JoinInputStatistics { rows: 1000, - schema: &left_schema, + schema: left_schema, index: Some((&unique_left, None)), }; assert_eq!( @@ -994,12 +998,12 @@ mod tests { unique_right.column_ids.push(1); let r_input = JoinInputStatistics { rows: 200, - schema: &right_schema, + schema: right_schema, index: Some((&unique_right, Some(100))), }; let l_input = JoinInputStatistics { rows: 1000, - schema: &left_schema, + schema: left_schema, index: Some((arena.index(li), Some(100))), }; assert_eq!( @@ -1008,7 +1012,7 @@ mod tests { ); let r_input = JoinInputStatistics { rows: 200, - schema: &right_schema, + schema: right_schema, index: Some((arena.index(ri), Some(100))), }; assert_eq!( diff --git a/src/optimizer/rule/normalization/pushdown_predicates.rs b/src/optimizer/rule/normalization/pushdown_predicates.rs index 7ca092d8..ec8a2aa1 100644 --- a/src/optimizer/rule/normalization/pushdown_predicates.rs +++ b/src/optimizer/rule/normalization/pushdown_predicates.rs @@ -1271,7 +1271,7 @@ mod tests { for predicate in ["c1 = c3 or c2 = c4", "c1 < c3", "c1 > 0 and c3 < 9"] { let mut arena = PlanArena::new(&tables.table_arena); let plan = tables.plan_with_arena( - &format!("select * from t1, t2 where {predicate}"), + format!("select * from t1, t2 where {predicate}"), &mut arena, )?; let plan = apply_pipeline( diff --git a/src/optimizer/rule/normalization/simplification.rs b/src/optimizer/rule/normalization/simplification.rs index 3d824201..26a76493 100644 --- a/src/optimizer/rule/normalization/simplification.rs +++ b/src/optimizer/rule/normalization/simplification.rs @@ -674,7 +674,7 @@ mod test { for negated in ["", "not "] { let mut arena = PlanArena::new(&table_state.table_arena); let plan = table_state.plan_with_arena( - &format!("select * from t1 where c1 {negated}in ({list})"), + format!("select * from t1 where c1 {negated}in ({list})"), &mut arena, )?; let Operator::Filter(filter) = plan.childrens.pop_only().operator else { @@ -703,7 +703,7 @@ mod test { ] { let mut arena = PlanArena::new(&table_state.table_arena); let plan = table_state - .plan_with_arena(&format!("select * from t1 where {predicate}"), &mut arena)?; + .plan_with_arena(format!("select * from t1 where {predicate}"), &mut arena)?; let Operator::Filter(filter) = plan.childrens.pop_only().operator else { unreachable!(); }; diff --git a/src/orm/mod.rs b/src/orm/mod.rs index 2c91a07a..81ca0d76 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -2065,7 +2065,7 @@ where bind_orm_source(&mut right_binder, source, Some(join_type), self.arena)?; (right_plan, right_binder.context) }; - self.binder.extend(right_context, self.arena); + self.binder.extend(right_context, self.arena)?; let on = with_query_bind_step!(self.binder, QueryBindStep::From, { let mut scope = self.expr_scope(); let handle = scope.handle(); From c5fcfa03b161c3a0875dfcbcf6e8b5d56c06b37e Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 25 Oct 2026 16:23:02 +0800 Subject: [PATCH 57/62] ci: run clippy and fix the DeepWiki README badge --- .github/workflows/ci.yml | 13 +++++++++++++ README.md | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a1089d3..5f74713a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,19 @@ jobs: - name: Run cargo fmt run: make fmt + # 2.1 + clippy: + name: Rust clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + + - name: Run cargo clippy + run: make clippy # 3 e2e: name: Rust e2e sqllogictest diff --git a/README.md b/README.md index 12f731dc..16b7b782 100755 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@

CI Codecov - Ask DeepWiki + Ask DeepWiki Discord From 3594b02ce1600f3086a48cda16e6cc63793fec9d Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 26 Oct 2026 06:00:00 +0800 Subject: [PATCH 58/62] feat: add total(), group_concat() and SQLite-style text sums --- src/binder/expr.rs | 23 ++++++- src/execution/dql/aggregate/avg.rs | 5 +- src/execution/dql/aggregate/group_concat.rs | 74 +++++++++++++++++++++ src/execution/dql/aggregate/mod.rs | 6 ++ src/execution/dql/aggregate/sum.rs | 61 ++++++++++++++++- src/execution/dql/aggregate/total.rs | 67 +++++++++++++++++++ src/expression/agg.rs | 10 +++ tests/slt/aggregation.slt | 22 +++--- 8 files changed, 254 insertions(+), 14 deletions(-) create mode 100644 src/execution/dql/aggregate/group_concat.rs create mode 100644 src/execution/dql/aggregate/total.rs diff --git a/src/binder/expr.rs b/src/binder/expr.rs index 0ac1b9b5..f6e16ed9 100644 --- a/src/binder/expr.rs +++ b/src/binder/expr.rs @@ -514,7 +514,13 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> if args.len() != 1 { return Err(DatabaseError::MisMatch("number of sum() parameters", "1")); } - args[0].return_type(arena).into_owned() + let ty = args[0].return_type(arena).into_owned(); + // Text is summed by its numeric prefix, as in SQLite. + if matches!(ty, LogicalType::Varchar(..) | LogicalType::Char(..)) { + LogicalType::Double + } else { + ty + } } AggKind::Min => { if args.len() != 1 { @@ -534,6 +540,21 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> } LogicalType::Double } + AggKind::Total => { + if args.len() != 1 { + return Err(DatabaseError::MisMatch("number of total() parameters", "1")); + } + LogicalType::Double + } + AggKind::GroupConcat => { + if args.len() != 1 { + return Err(DatabaseError::MisMatch( + "number of group_concat() parameters", + "1", + )); + } + LogicalType::Varchar(None, CharLengthUnits::Characters) + } }; Ok(ScalarExpression::AggCall { distinct: is_distinct, diff --git a/src/execution/dql/aggregate/avg.rs b/src/execution/dql/aggregate/avg.rs index b9afbac2..dd068541 100644 --- a/src/execution/dql/aggregate/avg.rs +++ b/src/execution/dql/aggregate/avg.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::execution::dql::aggregate::sum::SumAccumulator; +use crate::execution::dql::aggregate::sum::{text_as_number, SumAccumulator}; use crate::execution::dql::aggregate::Accumulator; use crate::expression::BinaryOperator; use crate::types::evaluator::binary_create; @@ -48,13 +48,14 @@ impl Accumulator for AvgAccumulator { return Ok(()); } } + let value = text_as_number(value); let acc = if let Some(ref mut inner) = self.inner { inner } else { self.inner .get_or_insert(SumAccumulator::new(Cow::Owned(value.logical_type()))?) }; - acc.update_value(value)?; + acc.update_value(&value)?; self.count += 1; self.result = DataValue::Null; } diff --git a/src/execution/dql/aggregate/group_concat.rs b/src/execution/dql/aggregate/group_concat.rs new file mode 100644 index 00000000..07264efe --- /dev/null +++ b/src/execution/dql/aggregate/group_concat.rs @@ -0,0 +1,74 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::errors::DatabaseError; +use crate::execution::dql::aggregate::Accumulator; +use crate::types::value::{DataValue, Utf8Type}; +use crate::types::CharLengthUnits; +use std::collections::HashSet; +use std::fmt::Write; + +/// SQLite `group_concat()`: non-NULL values in input order joined with `,`; NULL when there are none. +pub struct GroupConcatAccumulator { + distinct_values: Option>, + result: DataValue, +} + +impl GroupConcatAccumulator { + pub fn new(distinct: bool) -> Self { + Self { + distinct_values: distinct.then(HashSet::new), + result: DataValue::Null, + } + } +} + +impl Accumulator for GroupConcatAccumulator { + fn update_value(&mut self, value: &DataValue) -> Result<(), DatabaseError> { + if value.is_null() { + return Ok(()); + } + if let Some(values) = &mut self.distinct_values { + if !values.insert(value.clone()) { + return Ok(()); + } + } + match &mut self.result { + DataValue::Utf8 { value: text, .. } => { + text.push(','); + let _ = write!(text, "{value}"); + } + result => { + *result = DataValue::Utf8 { + value: value.to_string(), + ty: Utf8Type::Variable(None), + unit: CharLengthUnits::Characters, + } + } + } + Ok(()) + } + + fn evaluate(&mut self) -> Result<(), DatabaseError> { + Ok(()) + } + + fn result(&self) -> &DataValue { + &self.result + } + + fn result_owned(self: Box) -> DataValue { + self.result + } +} diff --git a/src/execution/dql/aggregate/mod.rs b/src/execution/dql/aggregate/mod.rs index 87c685b5..eb783dc2 100644 --- a/src/execution/dql/aggregate/mod.rs +++ b/src/execution/dql/aggregate/mod.rs @@ -14,18 +14,22 @@ mod avg; mod count; +mod group_concat; pub mod hash_agg; mod min_max; pub mod simple_agg; pub mod stream_agg; pub mod stream_distinct; mod sum; +mod total; use crate::errors::DatabaseError; use crate::execution::dql::aggregate::avg::AvgAccumulator; use crate::execution::dql::aggregate::count::{CountAccumulator, DistinctCountAccumulator}; +use crate::execution::dql::aggregate::group_concat::GroupConcatAccumulator; use crate::execution::dql::aggregate::min_max::MinMaxAccumulator; use crate::execution::dql::aggregate::sum::{DistinctSumAccumulator, SumAccumulator}; +use crate::execution::dql::aggregate::total::TotalAccumulator; use crate::expression::agg::AggKind; use crate::expression::ScalarExpression; use crate::iter_ext::Itertools; @@ -65,6 +69,8 @@ pub(crate) fn create_accumulator( (AggKind::Min, _) => Box::new(MinMaxAccumulator::new(false)), (AggKind::Max, _) => Box::new(MinMaxAccumulator::new(true)), (AggKind::Avg, distinct) => Box::new(AvgAccumulator::new(distinct)), + (AggKind::Total, distinct) => Box::new(TotalAccumulator::new(distinct)), + (AggKind::GroupConcat, distinct) => Box::new(GroupConcatAccumulator::new(distinct)), }) } diff --git a/src/execution/dql/aggregate/sum.rs b/src/execution/dql/aggregate/sum.rs index 1e220167..0cdc5ea0 100644 --- a/src/execution/dql/aggregate/sum.rs +++ b/src/execution/dql/aggregate/sum.rs @@ -21,6 +21,46 @@ use crate::types::LogicalType; use std::borrow::Cow; use std::collections::HashSet; +/// Text in a numeric aggregate is read as its longest numeric prefix (0 without one), as SQLite does. +pub(crate) fn text_as_number(value: &DataValue) -> Cow<'_, DataValue> { + match value { + DataValue::Utf8 { value: text, .. } => { + Cow::Owned(DataValue::Float64(numeric_prefix(text).into())) + } + _ => Cow::Borrowed(value), + } +} + +fn numeric_prefix(text: &str) -> f64 { + let text = text.trim_start(); + let bytes = text.as_bytes(); + let digits = |mut i: usize| { + while bytes.get(i).is_some_and(u8::is_ascii_digit) { + i += 1; + } + i + }; + let start = usize::from(matches!(bytes.first(), Some(b'+' | b'-'))); + let mut end = digits(start); + let mut has_digits = end > start; + if bytes.get(end) == Some(&b'.') { + let fraction = digits(end + 1); + has_digits |= fraction > end + 1; + end = fraction; + } + if !has_digits { + return 0.0; + } + if matches!(bytes.get(end), Some(b'e' | b'E')) { + let sign = end + 1 + usize::from(matches!(bytes.get(end + 1), Some(b'+' | b'-'))); + let exponent = digits(sign); + if exponent > sign { + end = exponent; + } + } + text[..end].parse().unwrap_or(0.0) +} + pub struct SumAccumulator { result: DataValue, evaluator: BinaryEvaluatorRef, @@ -41,12 +81,13 @@ impl SumAccumulator { impl Accumulator for SumAccumulator { fn update_value(&mut self, value: &DataValue) -> Result<(), DatabaseError> { if !value.is_null() { + let value = text_as_number(value); if self.result.is_null() { - self.result = value.clone(); + self.result = value.into_owned(); } else { self.result = self .evaluator - .binary_eval(Cow::Borrowed(&self.result), value)?; + .binary_eval(Cow::Borrowed(&self.result), &value)?; } } @@ -100,6 +141,22 @@ impl Accumulator for DistinctSumAccumulator { mod tests { use super::*; + #[test] + fn text_reads_its_numeric_prefix() { + for (text, expected) in [ + ("10", 10.0), + (" -1.5e2x", -150.0), + ("+.5", 0.5), + ("3.", 3.0), + ("7e", 7.0), + ("true", 0.0), + (".", 0.0), + ("", 0.0), + ] { + assert_eq!(numeric_prefix(text), expected, "{text:?}"); + } + } + #[test] fn sum_results() -> Result<(), DatabaseError> { let mut accumulator = SumAccumulator::new(Cow::Borrowed(&LogicalType::Integer))?; diff --git a/src/execution/dql/aggregate/total.rs b/src/execution/dql/aggregate/total.rs new file mode 100644 index 00000000..ac8c27fa --- /dev/null +++ b/src/execution/dql/aggregate/total.rs @@ -0,0 +1,67 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::errors::DatabaseError; +use crate::execution::dql::aggregate::sum::text_as_number; +use crate::execution::dql::aggregate::Accumulator; +use crate::types::value::DataValue; +use crate::types::LogicalType; +use std::collections::HashSet; + +/// SQLite `total()`: always a double, and 0.0 rather than NULL when nothing is summed. +pub struct TotalAccumulator { + distinct_values: Option>, + result: DataValue, +} + +impl TotalAccumulator { + pub fn new(distinct: bool) -> Self { + Self { + distinct_values: distinct.then(HashSet::new), + result: DataValue::Float64(0.0.into()), + } + } +} + +impl Accumulator for TotalAccumulator { + fn update_value(&mut self, value: &DataValue) -> Result<(), DatabaseError> { + if value.is_null() { + return Ok(()); + } + if let Some(values) = &mut self.distinct_values { + if !values.insert(value.clone()) { + return Ok(()); + } + } + let DataValue::Float64(value) = text_as_number(value) + .into_owned() + .cast(&LogicalType::Double)? + else { + unreachable!("cast to Double yields Float64"); + }; + let DataValue::Float64(total) = &mut self.result else { + unreachable!("total is always Float64"); + }; + *total = (total.0 + value.0).into(); + Ok(()) + } + + fn result(&self) -> &DataValue { + &self.result + } + + fn result_owned(self: Box) -> DataValue { + self.result + } +} diff --git a/src/expression/agg.rs b/src/expression/agg.rs index 13198aef..8f8b4e7a 100644 --- a/src/expression/agg.rs +++ b/src/expression/agg.rs @@ -21,6 +21,10 @@ pub enum AggKind { Min, Sum, Count, + /// SQLite `total()`: a float sum that is 0.0, not NULL, over no rows. + Total, + /// SQLite `group_concat()`: non-NULL values joined with `,`. + GroupConcat, } impl AggKind { @@ -31,6 +35,8 @@ impl AggKind { "min" => Some(Self::Min), "sum" => Some(Self::Sum), "count" => Some(Self::Count), + "total" => Some(Self::Total), + "group_concat" => Some(Self::GroupConcat), _ => None, } } @@ -42,6 +48,8 @@ impl AggKind { Self::Min => "min", Self::Sum => "sum", Self::Count => "count", + Self::Total => "total", + Self::GroupConcat => "group_concat", } } @@ -52,6 +60,8 @@ impl AggKind { AggKind::Min => false, AggKind::Sum => true, AggKind::Count => true, + AggKind::Total => true, + AggKind::GroupConcat => true, } } } diff --git a/tests/slt/aggregation.slt b/tests/slt/aggregation.slt index 811b89c6..c9529f93 100644 --- a/tests/slt/aggregation.slt +++ b/tests/slt/aggregation.slt @@ -105,17 +105,21 @@ create table ss(id int primary key, v varchar); statement ok insert into ss values (1, '10'), (2, 'x'); -statement error -select sum(v) from ss; - -statement error -select sum(distinct v) from ss; +# Text is summed by its numeric prefix (0 without one), as in SQLite. +query RRRRR +select sum(v), sum(distinct v), avg(v), sum('a'), total(v) from ss; +---- +10.0 10.0 5.0 0.0 10.0 -statement error -select avg(v) from ss; +query RT +select total(v), group_concat(v) from ss where id > 2; +---- +0.0 NULL -statement error -select sum('a') from ss; +query TT +select group_concat(v), group_concat(distinct id % 2) from ss; +---- +10,x 1,0 statement ok drop table ss; From 249185f36891297e3fcffc4157974adec41c1766 Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 26 Oct 2026 06:00:00 +0800 Subject: [PATCH 59/62] ci: run the SQLite sqllogictest corpus as a required job --- .github/workflows/ci.yml | 13 +++++++++++++ Makefile | 2 +- tests/kitesql-testing | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f74713a..7f41b525 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,19 @@ jobs: - name: Run sqllogictest suite run: make test-slt + # 3.1 + sqlite-slt: + name: SQLite sqllogictest corpus + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + + - name: Run SQLite sqllogictest corpus + run: make test-slt-sqlite # 4 coverage: name: Rust coverage diff --git a/Makefile b/Makefile index beb5a053..9184ba38 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,7 @@ test-slt: ## Run the external SQLite corpus (select1–5 excluded unless --include-select is passed). test-slt-sqlite: - $(CARGO) run -p sqllogictest-test --bin sqlite -- --path '$(SQLITE_LOGIC_PATH)' $(SQLITE_LOGIC_ARGS) + $(CARGO) run --release -p sqllogictest-test --bin sqlite -- --path '$(SQLITE_LOGIC_PATH)' $(SQLITE_LOGIC_ARGS) ## Convenience target to run every suite in sequence. test-all: test test-wasm test-slt test-python diff --git a/tests/kitesql-testing b/tests/kitesql-testing index 3b5449a5..54eed09e 160000 --- a/tests/kitesql-testing +++ b/tests/kitesql-testing @@ -1 +1 @@ -Subproject commit 3b5449a531ca1e29caf605b4431c29a24015abe3 +Subproject commit 54eed09e149d10da06ba4ae7ffd04b743e0b7363 From f3d974b0b6882d3327413b1fb64bbd124e776c41 Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 26 Oct 2026 06:00:00 +0800 Subject: [PATCH 60/62] chore: bump to 0.6.0 and refresh TPC-C results --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- README.md | 9 ++++----- kite_sql_serde_macros/Cargo.toml | 2 +- tpcc/README.md | 11 +++++------ 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf0640c2..15984401 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1135,7 +1135,7 @@ dependencies = [ [[package]] name = "kite_sql" -version = "0.5.0" +version = "0.6.0" dependencies = [ "bumpalo", "chrono", @@ -1164,7 +1164,7 @@ dependencies = [ [[package]] name = "kite_sql_serde_macros" -version = "0.2.4" +version = "0.2.5" dependencies = [ "darling", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 7cf94757..00406ada 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "kite_sql" -version = "0.5.0" +version = "0.6.0" edition = "2021" build = "build.rs" authors = ["Kould ", "Xwg "] @@ -56,7 +56,7 @@ required-features = ["pprof"] bumpalo = { version = "3", default-features = false, features = ["collections"] } ordered-float = { version = "4" } paste = { version = "1" } -kite_sql_serde_macros = { version = "0.2.4", path = "kite_sql_serde_macros" } +kite_sql_serde_macros = { version = "0.2.5", path = "kite_sql_serde_macros" } # Optional dependencies for features comfy-table = { version = "7", default-features = false, optional = true } diff --git a/README.md b/README.md index 16b7b782..ec954831 100755 --- a/README.md +++ b/README.md @@ -131,14 +131,13 @@ print(list(db.run("select * from demo"))) ## TPC-C `make tpcc` runs the benchmark (`--backend rocksdb|lmdb`); `make tpcc-dual` cross-checks every statement against SQLite. -720-second single-thread run on an i9-13900HX (32 GB, KIOXIA EXCERIA PLUS G3), every backend pinned to the same P-core. Latencies are p90 in µs, including commit. +720-second single-thread run on an i9-13900HX (32 GB, KIOXIA EXCERIA PLUS G3), every backend pinned to the same P-core and run without fsync on commit. Latencies are p90 in µs, including commit. | Backend | TpmC | New-Order | Payment | Order-Status | Delivery | Stock-Level | | --- | ---: | ---: | ---: | ---: | ---: | ---: | -| KiteSQL LMDB | 159616 | 315 | 81 | 44 | 435 | 332 | -| KiteSQL RocksDB | 48035 | 459 | 258 | 200 | 11055 | 704 | -| SQLite balanced | 67102 | 283 | 67 | 48 | 338 | 473 | -| SQLite practical | 67153 | 336 | 66 | 41 | 427 | 339 | +| KiteSQL LMDB | 165956 | 303 | 75 | 43 | 436 | 318 | +| KiteSQL RocksDB | 50739 | 392 | 188 | 198 | 11743 | 812 | +| SQLite (WAL, `synchronous=OFF`) | 191484 | 274 | 41 | 33 | 343 | 268 | 👉 [Details and how to reproduce](tpcc/README.md) diff --git a/kite_sql_serde_macros/Cargo.toml b/kite_sql_serde_macros/Cargo.toml index 3a3c6ba7..f04aa0c2 100644 --- a/kite_sql_serde_macros/Cargo.toml +++ b/kite_sql_serde_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kite_sql_serde_macros" -version = "0.2.4" +version = "0.2.5" edition = "2021" description = "Derive macros for KiteSQL" documentation = "https://docs.rs/kite_sql_serde_macros/latest/kite_sql_serde_macros/" diff --git a/tpcc/README.md b/tpcc/README.md index b60481d6..ca9936e1 100644 --- a/tpcc/README.md +++ b/tpcc/README.md @@ -50,15 +50,14 @@ Local 720-second comparison on the machine above: | Backend | TpmC | New-Order p90 (µs) | Payment p90 (µs) | Order-Status p90 (µs) | Delivery p90 (µs) | Stock-Level p90 (µs) | | --- | ---: | ---: | ---: | ---: | ---: | ---: | -| KiteSQL LMDB | 159616 | 315 | 81 | 44 | 435 | 332 | -| KiteSQL RocksDB | 48035 | 459 | 258 | 200 | 11055 | 704 | -| SQLite balanced | 67102 | 283 | 67 | 48 | 338 | 473 | -| SQLite practical | 67153 | 336 | 66 | 41 | 427 | 339 | +| KiteSQL LMDB | 165956 | 303 | 75 | 43 | 436 | 318 | +| KiteSQL RocksDB | 50739 | 392 | 188 | 198 | 11743 | 812 | +| SQLite | 191484 | 274 | 41 | 33 | 343 | 268 | -- Run dates: `2026-10-01`; results: `2026-10-01_17-12-31`. Latency is measured in microseconds and includes commit. +- Run dates: `2026-10-25`; results: `2026-10-25_16-30-15`. Latency is measured in microseconds and includes commit. - Every row is pinned to `cpu8`, the highest-frequency P-core on this machine (5.4 GHz). - All rows use `--num-ware 1`, `--max-retry 5`, and TPCC's default 720-second measure time. -- SQLite rows use the `balanced` and `practical` profiles respectively. +- All rows run without `--sync`, so commits are not fsynced; SQLite uses WAL with `synchronous = OFF`. ## Historical raw outputs — 2026-07-11 From bbb1cc0065581caa848054fb6ab87789fe7d069a Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 26 Oct 2026 06:00:00 +0800 Subject: [PATCH 61/62] perf: materialize uncorrelated quantified subqueries without estimates --- .../normalization/materialize_mark_apply.rs | 59 +++++++++---------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/src/optimizer/rule/normalization/materialize_mark_apply.rs b/src/optimizer/rule/normalization/materialize_mark_apply.rs index b3ef4f9b..a49936e3 100644 --- a/src/optimizer/rule/normalization/materialize_mark_apply.rs +++ b/src/optimizer/rule/normalization/materialize_mark_apply.rs @@ -51,13 +51,15 @@ impl NormalizationRule for MaterializeMarkApply { if op.materialize || !Self::can_materialize(op, arena) { return Ok(false); } - let (Some(left_rows), Some(right_rows)) = (left.estimated_rows(), right.estimated_rows()) - else { - return Ok(false); + // Streaming rebuilds the RHS once per left row, which nested subqueries multiply, + // so materialize by default. Stream only when estimates show the cache would not + // pay off: no repeated left input, or fewer rebuilds than retained RHS rows. + let stream = match (left.estimated_rows(), right.estimated_rows()) { + (Some(left_rows), _) if left_rows <= 1 => true, + (Some(left_rows), Some(right_rows)) => left_rows < right_rows, + _ => false, }; - // Prefer reuse when rebuilds are at least as numerous as retained rows, - // but avoid eager RHS evaluation when there is no repeated left input. - if left_rows <= 1 || left_rows < right_rows { + if stream { return Ok(false); } op.materialize = true; @@ -130,11 +132,14 @@ mod tests { }, ); for (left_rows, right_rows, materialize) in [ - (0, 2, false), - (1, 0, false), - (2, 8, false), - (2, 2, true), - (8, 2, true), + (Some(0), Some(2), false), + (Some(1), Some(0), false), + (Some(1), None, false), + (Some(2), Some(8), false), + (Some(2), Some(2), true), + (Some(8), Some(2), true), + (None, None, true), + (Some(8), None, true), ] { let Operator::MarkApply(op) = &mut plan.operator else { unreachable!() @@ -143,22 +148,17 @@ mod tests { let Childrens::Twins { left, right } = plan.childrens.as_mut() else { unreachable!() }; - left.physical_option = Some(PhysicalOption::new( - PlanImpl::Values, - SortOption::None, - RowCountOption::Estimated { - rows: left_rows, - ndv: None, - }, - )); - right.physical_option = Some(PhysicalOption::new( - PlanImpl::Values, - SortOption::None, - RowCountOption::Estimated { - rows: right_rows, - ndv: None, - }, - )); + let estimated = |rows: Option| { + rows.map(|rows| { + PhysicalOption::new( + PlanImpl::Values, + SortOption::None, + RowCountOption::Estimated { rows, ndv: None }, + ) + }) + }; + left.physical_option = estimated(left_rows); + right.physical_option = estimated(right_rows); assert_eq!( MaterializeMarkApply.apply(&mut plan, &mut arena, None)?, materialize @@ -192,11 +192,6 @@ mod tests { unreachable!() }; op.predicates[0] = predicate; - let Childrens::Twins { right, .. } = plan.childrens.as_mut() else { - unreachable!() - }; - right.physical_option = None; - assert!(!MaterializeMarkApply.apply(&mut plan, &mut arena, None)?); Ok(()) } } From 4844c6fed2277166b909402d682a56b58f95810f Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 26 Oct 2026 06:00:00 +0800 Subject: [PATCH 62/62] feat: read-only transactions and transaction type renames - StorageTransaction::transaction_with_isolation and Database::new_transaction take a read_only flag; LMDB read-only transactions skip the single-writer lock and reject writes. - run(), prepared plans and ORM reads pick read-only transactions by statement or plan classification. - CommandType moves to the planner; Operator::command_type classifies plans the same way as SQL statements (COPY .. TO is DQL). - Rename the storage Transaction trait to StorageTransaction and DBTransaction to Transaction. --- examples/transaction.rs | 4 +- kite_sql_serde_macros/src/projection.rs | 2 +- .../src/reference_serialization.rs | 4 +- src/bin/shell.rs | 12 +- src/binder/aggregate.rs | 4 +- src/binder/alter_table.rs | 4 +- src/binder/analyze.rs | 4 +- src/binder/copy.rs | 2 +- src/binder/create_index.rs | 4 +- src/binder/create_table.rs | 4 +- src/binder/create_view.rs | 4 +- src/binder/delete.rs | 4 +- src/binder/describe.rs | 4 +- src/binder/distinct.rs | 4 +- src/binder/drop_index.rs | 4 +- src/binder/drop_table.rs | 4 +- src/binder/drop_view.rs | 4 +- src/binder/explain.rs | 4 +- src/binder/expr.rs | 4 +- src/binder/insert.rs | 4 +- src/binder/mod.rs | 19 ++- src/binder/parser.rs | 71 ++++++---- src/binder/select.rs | 46 +++---- src/binder/show_table.rs | 4 +- src/binder/show_view.rs | 4 +- src/binder/truncate.rs | 4 +- src/binder/update.rs | 4 +- src/binder/window.rs | 4 +- src/db.rs | 103 ++++++++------ src/db/prepared.rs | 20 ++- src/execution/ddl/add_column.rs | 6 +- src/execution/ddl/change_column.rs | 6 +- src/execution/ddl/create_index.rs | 6 +- src/execution/ddl/create_table.rs | 6 +- src/execution/ddl/create_view.rs | 6 +- src/execution/ddl/drop_column.rs | 6 +- src/execution/ddl/drop_index.rs | 6 +- src/execution/ddl/drop_table.rs | 6 +- src/execution/ddl/drop_view.rs | 6 +- src/execution/ddl/mod.rs | 8 +- src/execution/ddl/truncate.rs | 6 +- src/execution/dml/analyze.rs | 10 +- src/execution/dml/copy_from_file.rs | 6 +- src/execution/dml/copy_to_file.rs | 6 +- src/execution/dml/delete.rs | 6 +- src/execution/dml/insert.rs | 6 +- src/execution/dml/update.rs | 6 +- src/execution/dql/aggregate/hash_agg.rs | 6 +- src/execution/dql/aggregate/simple_agg.rs | 6 +- src/execution/dql/aggregate/stream_agg.rs | 6 +- .../dql/aggregate/stream_distinct.rs | 6 +- src/execution/dql/describe.rs | 6 +- src/execution/dql/dummy.rs | 6 +- src/execution/dql/explain.rs | 6 +- src/execution/dql/external_sort.rs | 6 +- src/execution/dql/filter.rs | 6 +- src/execution/dql/function_scan.rs | 6 +- src/execution/dql/index_scan.rs | 10 +- src/execution/dql/join/hash_join.rs | 8 +- src/execution/dql/join/index_probe_join.rs | 6 +- src/execution/dql/join/nested_loop_join.rs | 8 +- src/execution/dql/limit.rs | 6 +- src/execution/dql/mark_apply.rs | 16 +-- src/execution/dql/projection.rs | 6 +- src/execution/dql/recursive_cte.rs | 14 +- src/execution/dql/scalar_query_init.rs | 10 +- src/execution/dql/scalar_subquery.rs | 6 +- src/execution/dql/seq_scan.rs | 8 +- src/execution/dql/set_membership.rs | 6 +- src/execution/dql/show_table.rs | 8 +- src/execution/dql/show_view.rs | 8 +- src/execution/dql/sort.rs | 6 +- src/execution/dql/top_k.rs | 6 +- src/execution/dql/union.rs | 6 +- src/execution/dql/values.rs | 6 +- src/execution/dql/window.rs | 6 +- src/execution/mod.rs | 44 +++--- src/expression/mod.rs | 2 +- src/optimizer/core/cm_sketch.rs | 4 +- src/optimizer/heuristic/optimizer.rs | 2 +- src/orm/README.md | 2 +- src/orm/dml.rs | 2 +- src/orm/dql.rs | 8 +- src/orm/mod.rs | 104 +++++++------- src/planner/mod.rs | 2 +- src/planner/operator/mod.rs | 63 +++++++++ src/planner/plan_keeper.rs | 9 ++ src/planner/scalar_query_ref.rs | 4 +- src/serdes/boolean.rs | 4 +- src/serdes/bound.rs | 4 +- src/serdes/btree_map.rs | 4 +- src/serdes/char.rs | 4 +- src/serdes/char_length_units.rs | 4 +- src/serdes/column.rs | 8 +- src/serdes/data_value.rs | 6 +- src/serdes/evaluator.rs | 12 +- src/serdes/expression.rs | 4 +- src/serdes/function.rs | 6 +- src/serdes/hasher.rs | 4 +- src/serdes/index.rs | 4 +- src/serdes/mod.rs | 8 +- src/serdes/num.rs | 8 +- src/serdes/option.rs | 4 +- src/serdes/pair.rs | 4 +- src/serdes/path_buf.rs | 4 +- src/serdes/phantom.rs | 4 +- src/serdes/physical_option.rs | 6 +- src/serdes/ptr.rs | 4 +- src/serdes/slice.rs | 4 +- src/serdes/string.rs | 6 +- src/serdes/trim.rs | 4 +- src/serdes/vec.rs | 4 +- src/storage/lmdb.rs | 128 ++++++++++++++++-- src/storage/memory.rs | 5 +- src/storage/mod.rs | 71 +++++----- src/storage/rocksdb.rs | 8 +- src/storage/table_codec.rs | 24 ++-- tests/macros-test/src/main.rs | 6 +- tpcc/src/backend/dual.rs | 8 +- tpcc/src/backend/kitesql_lmdb.rs | 6 +- tpcc/src/backend/kitesql_rocksdb.rs | 14 +- tpcc/src/main.rs | 2 +- 122 files changed, 782 insertions(+), 541 deletions(-) diff --git a/examples/transaction.rs b/examples/transaction.rs index 7732a289..8e8bdee8 100644 --- a/examples/transaction.rs +++ b/examples/transaction.rs @@ -42,7 +42,7 @@ mod app { // Optimistic transactions are currently backed by RocksDB. let mut database = DataBaseBuilder::path(EXAMPLE_DB_PATH).build_optimistic()?; database.ddl("create table if not exists t1 (c1 int primary key, c2 int)")?; - let mut transaction = database.new_transaction()?; + let mut transaction = database.new_transaction(false)?; transaction .run("insert into t1 values(0, 0), (1, 1)")? @@ -66,7 +66,7 @@ mod app { assert!(iter.next_tuple(|_, _| ())?.is_none()); iter.done()?; - let mut tx2 = database.new_transaction()?; + let mut tx2 = database.new_transaction(false)?; tx2.run("update t1 set c2 = 99 where c1 = 0")?.done()?; let mut c2_iter = database.run("select c2 from t1 where c1 = 0")?; assert_eq!( diff --git a/kite_sql_serde_macros/src/projection.rs b/kite_sql_serde_macros/src/projection.rs index 29b7079f..45fb0312 100644 --- a/kite_sql_serde_macros/src/projection.rs +++ b/kite_sql_serde_macros/src/projection.rs @@ -107,7 +107,7 @@ pub(crate) fn handle(ast: DeriveInput) -> Result { relation: &str, ) -> ::std::result::Result<::std::vec::Vec<::kite_sql::planner::ExprRef>, ::kite_sql::errors::DatabaseError> where - T: ::kite_sql::storage::Transaction, + T: ::kite_sql::storage::StorageTransaction, A: AsRef<[(usize, ::kite_sql::types::LogicalType)]>, { Ok(::std::vec![ diff --git a/kite_sql_serde_macros/src/reference_serialization.rs b/kite_sql_serde_macros/src/reference_serialization.rs index 087331d7..8cc7b38d 100644 --- a/kite_sql_serde_macros/src/reference_serialization.rs +++ b/kite_sql_serde_macros/src/reference_serialization.rs @@ -148,7 +148,7 @@ pub(crate) fn handle(ast: DeriveInput) -> Result { Ok(()) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, Tx>>, reference_tables: &crate::serdes::ReferenceTables, @@ -229,7 +229,7 @@ pub(crate) fn handle(ast: DeriveInput) -> Result { Ok(()) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, Tx>>, reference_tables: &crate::serdes::ReferenceTables, diff --git a/src/bin/shell.rs b/src/bin/shell.rs index b31dadfa..f0b153d3 100644 --- a/src/bin/shell.rs +++ b/src/bin/shell.rs @@ -15,7 +15,7 @@ #[cfg(not(target_arch = "wasm32"))] mod native { use comfy_table::{Cell, Table}; - use kite_sql::db::{DBTransaction, DataBaseBuilder, Database, ResultIter}; + use kite_sql::db::{DataBaseBuilder, Database, ResultIter, Transaction}; use kite_sql::errors::DatabaseError; use kite_sql::storage::rocksdb::RocksStorage; use rustyline::config::Configurer; @@ -46,7 +46,7 @@ Metacommands: .views Show views .schema NAME Describe a table or view -Transaction commands: +StorageTransaction commands: BEGIN COMMIT ROLLBACK @@ -224,7 +224,7 @@ Transaction commands: fn run_sql<'a>( database: &Database, - tx: &mut Option>, + tx: &mut Option>, sql: &str, ) -> Result<(), DatabaseError> { let started = Instant::now(); @@ -240,7 +240,7 @@ Transaction commands: fn handle_meta_command<'a>( command: &str, database: &Database, - tx: &mut Option>, + tx: &mut Option>, ) -> Result { let trimmed = command.trim(); if trimmed.is_empty() { @@ -283,7 +283,7 @@ Transaction commands: fn handle_command<'a>( command: &str, database: &'a Database, - tx: &mut Option>, + tx: &mut Option>, ) -> Result { let trimmed = command.trim(); if trimmed.is_empty() { @@ -302,7 +302,7 @@ Transaction commands: if tx.is_some() { eprintln!("transaction already started"); } else { - *tx = Some(database.new_transaction()?); + *tx = Some(database.new_transaction(false)?); println!("OK"); } return Ok(Mode::Continue); diff --git a/src/binder/aggregate.rs b/src/binder/aggregate.rs index c3997afb..63538cdb 100644 --- a/src/binder/aggregate.rs +++ b/src/binder/aggregate.rs @@ -18,7 +18,7 @@ use crate::expression::visitor::{walk_expr, ExprVisitor}; use crate::expression::visitor_mut::{walk_mut_expr, ExprVisitorMut}; use crate::planner::MetaArena; use crate::planner::{ExprRef, LogicalPlan, PlanArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; use crate::{ expression::ScalarExpression, @@ -41,7 +41,7 @@ impl ExprVisitor for AggregateCallCollector<'_> { } } -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub fn bind_aggregate( &mut self, children: LogicalPlan, diff --git a/src/binder/alter_table.rs b/src/binder/alter_table.rs index 2cb1d303..c55c2d87 100644 --- a/src/binder/alter_table.rs +++ b/src/binder/alter_table.rs @@ -22,10 +22,10 @@ use crate::planner::operator::alter_table::change_column::{ use crate::planner::operator::alter_table::drop_column::DropColumnOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_add_column( &mut self, table_name: TableName, diff --git a/src/binder/analyze.rs b/src/binder/analyze.rs index edcd560a..19be98fc 100644 --- a/src/binder/analyze.rs +++ b/src/binder/analyze.rs @@ -19,10 +19,10 @@ use crate::planner::operator::analyze::AnalyzeOperator; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_analyze( &mut self, table_name: TableName, diff --git a/src/binder/copy.rs b/src/binder/copy.rs index 0e292ac2..5829c04a 100644 --- a/src/binder/copy.rs +++ b/src/binder/copy.rs @@ -65,7 +65,7 @@ impl FromStr for ExtSource { } } -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(super) fn bind_copy_to_file( &mut self, target: ExtSource, diff --git a/src/binder/create_index.rs b/src/binder/create_index.rs index 5fdabdef..b9f0d39b 100644 --- a/src/binder/create_index.rs +++ b/src/binder/create_index.rs @@ -19,11 +19,11 @@ use crate::planner::operator::create_index::CreateIndexOperator; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::index::IndexType; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_create_index_source( &mut self, table_name: TableName, diff --git a/src/binder/create_table.rs b/src/binder/create_table.rs index 31d22512..d84d7b3c 100644 --- a/src/binder/create_table.rs +++ b/src/binder/create_table.rs @@ -18,11 +18,11 @@ use crate::errors::DatabaseError; use crate::planner::operator::create_table::CreateTableOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; use std::collections::HashSet; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { // TODO: TableConstraint pub(crate) fn bind_create_table( &mut self, diff --git a/src/binder/create_view.rs b/src/binder/create_view.rs index 7f49753c..d25b7a9c 100644 --- a/src/binder/create_view.rs +++ b/src/binder/create_view.rs @@ -20,10 +20,10 @@ use crate::expression::{AliasType, ScalarExpression}; use crate::planner::operator::create_view::CreateViewOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, ExprRef, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_create_view( &mut self, view_name: TableName, diff --git a/src/binder/delete.rs b/src/binder/delete.rs index 4eb6617b..0419fb3c 100644 --- a/src/binder/delete.rs +++ b/src/binder/delete.rs @@ -18,10 +18,10 @@ use crate::errors::DatabaseError; use crate::planner::operator::delete::DeleteOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_delete( &mut self, table_name: TableName, diff --git a/src/binder/describe.rs b/src/binder/describe.rs index 3729331d..d6f58d7d 100644 --- a/src/binder/describe.rs +++ b/src/binder/describe.rs @@ -18,10 +18,10 @@ use crate::errors::DatabaseError; use crate::planner::operator::describe::DescribeOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_describe( &mut self, table_name: TableName, diff --git a/src/binder/distinct.rs b/src/binder/distinct.rs index 9608f800..ff8f4c2f 100644 --- a/src/binder/distinct.rs +++ b/src/binder/distinct.rs @@ -20,10 +20,10 @@ use crate::planner::operator::aggregate::AggregateOperator; use crate::planner::operator::sort::SortField; use crate::planner::MetaArena; use crate::planner::{ExprRef, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub fn bind_distinct( &mut self, children: LogicalPlan, diff --git a/src/binder/drop_index.rs b/src/binder/drop_index.rs index 149b61ea..20f76e24 100644 --- a/src/binder/drop_index.rs +++ b/src/binder/drop_index.rs @@ -18,10 +18,10 @@ use crate::errors::DatabaseError; use crate::planner::operator::drop_index::DropIndexOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_drop_index( &mut self, table_name: TableName, diff --git a/src/binder/drop_table.rs b/src/binder/drop_table.rs index 047c90fc..ea2e0950 100644 --- a/src/binder/drop_table.rs +++ b/src/binder/drop_table.rs @@ -18,10 +18,10 @@ use crate::errors::DatabaseError; use crate::planner::operator::drop_table::DropTableOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_drop_table( &mut self, table_name: TableName, diff --git a/src/binder/drop_view.rs b/src/binder/drop_view.rs index 936c6490..87eab8b5 100644 --- a/src/binder/drop_view.rs +++ b/src/binder/drop_view.rs @@ -18,10 +18,10 @@ use crate::errors::DatabaseError; use crate::planner::operator::drop_view::DropViewOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_drop_view( &mut self, view_name: TableName, diff --git a/src/binder/explain.rs b/src/binder/explain.rs index 0bd109af..811e3f87 100644 --- a/src/binder/explain.rs +++ b/src/binder/explain.rs @@ -16,10 +16,10 @@ use crate::binder::Binder; use crate::errors::DatabaseError; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_explain(&mut self, plan: LogicalPlan) -> Result { Ok(LogicalPlan::new( Operator::Explain, diff --git a/src/binder/expr.rs b/src/binder/expr.rs index f6e16ed9..d1906d17 100644 --- a/src/binder/expr.rs +++ b/src/binder/expr.rs @@ -25,7 +25,7 @@ use crate::expression::{AliasType, ScalarExpression, TypeCast}; use crate::planner::operator::mark_apply::MarkApplyQuantifier; use crate::planner::operator::scalar_subquery::ScalarSubqueryOperator; use crate::planner::{ExprRef, LogicalPlan, MetaArena, PlanArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::value::{DataValue, Utf8Type}; use crate::types::{CharLengthUnits, LogicalType}; @@ -37,7 +37,7 @@ macro_rules! try_default { }; } -impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> { +impl<'a, T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> { pub(crate) fn bind_temp_table( &mut self, expr: ScalarExpression, diff --git a/src/binder/insert.rs b/src/binder/insert.rs index 124561d7..0778edb1 100644 --- a/src/binder/insert.rs +++ b/src/binder/insert.rs @@ -19,11 +19,11 @@ use crate::planner::operator::insert::InsertOperator; use crate::planner::operator::values::ValuesOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, ExprRef, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple::Schema; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_insert_values( &mut self, table_name: TableName, diff --git a/src/binder/mod.rs b/src/binder/mod.rs index 43e1ba34..61189d7a 100644 --- a/src/binder/mod.rs +++ b/src/binder/mod.rs @@ -51,8 +51,9 @@ mod truncate; mod update; mod window; +pub use crate::planner::operator::CommandType; #[cfg(feature = "parser")] -pub use parser::{command_type, CommandType, Statement}; +pub use parser::{command_type, Statement}; #[cfg(feature = "orm")] pub use select::{BindPlanFrom, BindPlanSelectList}; #[cfg(feature = "orm")] @@ -67,7 +68,7 @@ use crate::expression::{ScalarExpression, TypeCast}; use crate::planner::operator::join::JoinType; use crate::planner::operator::mark_apply::MarkApplyQuantifier; use crate::planner::{ExprRef, LogicalPlan, PlanArena, PlanRef, ScalarQueryRef}; -use crate::storage::{TableCache, Transaction, ViewCache}; +use crate::storage::{StorageTransaction, TableCache, ViewCache}; use crate::types::tuple::Schema; use crate::types::LogicalType; @@ -258,7 +259,7 @@ pub(crate) struct BoundScalarQuery { param_bindings: Vec<(ScalarQueryRef, ExprRef)>, } -pub struct BinderContext<'a, T: Transaction> { +pub struct BinderContext<'a, T: StorageTransaction> { pub(crate) scala_functions: &'a ScalaFunctions, pub(crate) table_functions: &'a TableFunctions, pub(crate) table_cache: &'a TableCache, @@ -325,7 +326,7 @@ impl Source<'_> { } } -impl<'a, T: Transaction> BinderContext<'a, T> { +impl<'a, T: StorageTransaction> BinderContext<'a, T> { pub fn new( table_cache: &'a TableCache, view_cache: &'a ViewCache, @@ -766,7 +767,7 @@ impl<'a, T: Transaction> BinderContext<'a, T> { } } -pub struct Binder<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> { +pub struct Binder<'a, 'parent, T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>> { pub(crate) context: BinderContext<'a, T>, pub(crate) args: &'a A, pub(crate) force_spill: bool, @@ -775,7 +776,9 @@ pub struct Binder<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> pub(crate) parent: Option<&'parent BinderContext<'a, T>>, } -impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'parent, T, A> { +impl<'a, 'parent, T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>> + Binder<'a, 'parent, T, A> +{ pub fn new( context: BinderContext<'a, T>, args: &'a A, @@ -846,7 +849,9 @@ pub mod test { use crate::errors::DatabaseError; use crate::planner::{LogicalPlan, PlanArena, TableArenaCell}; use crate::storage::rocksdb::RocksStorage; - use crate::storage::{table_codec::TableCodec, Storage, TableCache, Transaction, ViewCache}; + use crate::storage::{ + table_codec::TableCodec, Storage, StorageTransaction, TableCache, ViewCache, + }; use crate::types::ColumnId; use crate::types::LogicalType::Integer; use std::path::PathBuf; diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 365bba83..b684c7b3 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -21,7 +21,7 @@ use super::{is_valid_identifier, Binder, CteBinding, QueryBindStep, SetOperatorK #[cfg(feature = "copy")] use crate::binder::copy::{ExtSource, FileFormat}; use crate::catalog::{ColumnCatalog, ColumnDesc, ColumnRef, TableName}; -use crate::db::{BindSource, DBTransaction, Database, DatabaseIter, State, TransactionIter}; +use crate::db::{BindSource, Database, DatabaseIter, State, Transaction, TransactionIter}; use crate::errors::{DatabaseError, SqlErrorSpan}; use crate::expression; use crate::expression::agg::AggKind; @@ -36,10 +36,10 @@ use crate::planner::operator::mark_apply::MarkApplyQuantifier; use crate::planner::operator::project::ProjectOperator; use crate::planner::operator::recursive_cte::{RecursiveCteOperator, RecursiveScanOperator}; use crate::planner::operator::sort::SortField; -use crate::planner::operator::Operator; +use crate::planner::operator::{CommandType, Operator}; use crate::planner::MetaArena; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; -use crate::storage::{Storage, Transaction}; +use crate::storage::{Storage, StorageTransaction}; use crate::types::value::{DataValue, Utf8Type}; use crate::types::{CharLengthUnits, ColumnId, LogicalType}; pub(super) use sqlparser::ast::{ @@ -63,14 +63,6 @@ use std::slice; /// Parsed SQL statement type used by KiteSQL SQL frontend APIs. pub type Statement = sqlparser::ast::Statement; -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum CommandType { - DQL, - DML, - DDL, - Analyze, -} - pub(crate) trait AttachSpanSource { fn sql_error_span(self) -> Option; } @@ -134,6 +126,8 @@ pub fn command_type(stmt: &Statement) -> Result { | Statement::Delete(_) | Statement::Insert(_) => Ok(CommandType::DML), #[cfg(feature = "copy")] + Statement::Copy { to: true, .. } => Ok(CommandType::DQL), + #[cfg(feature = "copy")] Statement::Copy { .. } => Ok(CommandType::DML), stmt => Err(DatabaseError::UnsupportedStmt(stmt.to_string())), } @@ -158,6 +152,15 @@ impl State { } } +/// Statements that only read storage can run in a read transaction. +fn statement_is_read_only(statement: &Statement) -> Result { + Ok(match statement { + // Locking reads (`FOR UPDATE`) are treated as writes. + Statement::Query(query) => query.locks.is_empty(), + _ => command_type(statement)?.is_read_only(), + }) +} + fn statement_mutates_catalog_or_statistics(statement: &Statement) -> Result { Ok(matches!( command_type(statement)?, @@ -265,9 +268,15 @@ impl Database { .with_sql_context(sql)); } + let read_only = statements + .iter() + .try_fold(true, |read_only, stmt| { + Ok::<_, DatabaseError>(read_only && statement_is_read_only(stmt)?) + }) + .map_err(|err| err.with_sql_context(sql))?; let transaction = Box::into_raw(Box::new( self.storage - .transaction_with_isolation(self.transaction_isolation)?, + .transaction_with_isolation(self.transaction_isolation, read_only)?, )); let mut statements = statements.into_iter().peekable(); @@ -310,7 +319,7 @@ impl Database { } } -impl<'txn, S: Storage> DBTransaction<'txn, S> { +impl<'txn, S: Storage> Transaction<'txn, S> { /// Runs SQL inside the current transaction and returns the final result iterator. pub fn run<'a, T: AsRef>( &'a mut self, @@ -334,7 +343,7 @@ impl<'txn, S: Storage> DBTransaction<'txn, S> { .ok_or_else(|| DatabaseError::EmptyStatement.with_sql_context(sql))?; for statement in statements { - BindSource::execute(&mut *self, |state, tx| { + BindSource::execute(&mut *self, false, |state, tx| { state.build_plan(&[], tx, |binder, arena| binder.bind(&statement, arena)) }) .map_err(|err| err.with_sql_context(sql))? @@ -342,7 +351,7 @@ impl<'txn, S: Storage> DBTransaction<'txn, S> { .map_err(|err| err.with_sql_context(sql))?; } - BindSource::execute(self, |state, tx| { + BindSource::execute(self, false, |state, tx| { state.build_plan(&[], tx, |binder, arena| binder.bind(&last_statement, arena)) }) .map_err(|err| err.with_sql_context(sql)) @@ -351,7 +360,7 @@ impl<'txn, S: Storage> DBTransaction<'txn, S> { struct BindStatementStart<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { binder: &'s mut Binder<'a, 'b, T, A>, @@ -390,7 +399,7 @@ impl ExprVisitorMut for UpdateExprTargetRemapper<'_> { impl<'s, 'a: 'b, 'b, 'arena, T, A> BindStatementStart<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn statement(self, stmt: &Statement) -> Result { @@ -1409,7 +1418,7 @@ impl BindStatementComplete { impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanStart<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { #[allow(clippy::wrong_self_convention)] @@ -1442,7 +1451,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanFrom<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn select_list_from_sql( @@ -1459,7 +1468,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanSelectList<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn where_sql( @@ -1481,7 +1490,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanFiltered<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn aggregate_sql( @@ -1588,7 +1597,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> super::select::BindPlanWindowed<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn distinct_sql( @@ -1601,7 +1610,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanProjected<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn select_into_sql( @@ -2063,7 +2072,9 @@ fn copy_file_format(options: Vec) -> Result> Binder<'a, 'parent, T, A> { +impl<'a, 'parent, T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>> + Binder<'a, 'parent, T, A> +{ fn bind_table_ref_sql( &mut self, from: &TableWithJoins, @@ -3499,6 +3510,16 @@ mod tests { command_type(&database.prepare_all("truncate table t")?.pop().unwrap())?, CommandType::DML ); + #[cfg(feature = "copy")] + for (sql, expected) in [ + ("copy t to 'out.csv'", CommandType::DQL), + ("copy t from 'in.csv'", CommandType::DML), + ] { + assert_eq!( + command_type(&database.prepare_all(sql)?.pop().unwrap())?, + expected + ); + } let err = command_type(&database.prepare_all("start transaction")?.pop().unwrap()).unwrap_err(); @@ -3522,7 +3543,7 @@ mod tests { ); assert_unsupported(expect_err(database.ddl("select 1")), "`Database::ddl`"); - let transaction = database.new_transaction()?; + let transaction = database.new_transaction(false)?; assert_unsupported( expect_err(transaction.prepare_sql("analyze table t", params)), "DDL and ANALYZE", diff --git a/src/binder/select.rs b/src/binder/select.rs index 79d817ac..f7327bc5 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -44,7 +44,7 @@ use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::operator::union::UnionOperator; use crate::planner::operator::visitor_mut::{OperatorExprVisitorMut, OperatorVisitorMut}; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena, ScalarQueryRef}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple::Schema; use crate::types::{ColumnId, LogicalType}; @@ -118,7 +118,7 @@ impl ExprVisitorMut for ProjectionOutputBinder<'_> { pub(crate) struct BindPlanStart<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) binder: &'s mut Binder<'a, 'b, T, A>, @@ -127,7 +127,7 @@ where pub struct BindPlanFrom<'s, 'a, 'b, 'arena, T, A, M = ()> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) binder: &'s mut Binder<'a, 'b, T, A>, @@ -138,7 +138,7 @@ where pub struct BindPlanSelectList<'s, 'a, 'b, 'arena, T, A, M = ()> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) binder: &'s mut Binder<'a, 'b, T, A>, @@ -150,7 +150,7 @@ where pub(crate) struct BindPlanFiltered<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(super) binder: &'s mut Binder<'a, 'b, T, A>, @@ -161,7 +161,7 @@ where pub(crate) struct BindPlanAggregated<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { binder: &'s mut Binder<'a, 'b, T, A>, @@ -174,7 +174,7 @@ where pub(crate) struct BindPlanHaving<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { binder: &'s mut Binder<'a, 'b, T, A>, @@ -186,7 +186,7 @@ where pub(crate) struct BindPlanWindowed<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { binder: &'s mut Binder<'a, 'b, T, A>, @@ -198,7 +198,7 @@ where pub(crate) struct BindPlanDistinct<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { binder: &'s mut Binder<'a, 'b, T, A>, @@ -210,7 +210,7 @@ where pub(crate) struct BindPlanSorted<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { binder: &'s mut Binder<'a, 'b, T, A>, @@ -221,7 +221,7 @@ where pub(crate) struct BindPlanProjected<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { plan: LogicalPlan, @@ -251,7 +251,7 @@ pub(crate) enum JoinConstraintInput { impl<'s, 'a: 'b, 'b, 'arena, T, A, M> BindPlanFrom<'s, 'a, 'b, 'arena, T, A, M> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { #[cfg(feature = "orm")] @@ -303,7 +303,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A, M> BindPlanSelectList<'s, 'a, 'b, 'arena, T, A, M> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { #[cfg(feature = "orm")] @@ -452,7 +452,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanStart<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { #[allow(clippy::wrong_self_convention)] @@ -471,7 +471,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A, M> BindPlanSelectList<'s, 'a, 'b, 'arena, T, A, M> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn filter_expr( @@ -495,7 +495,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanFiltered<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn aggregate( @@ -608,7 +608,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanAggregated<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn having( @@ -630,7 +630,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanHaving<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn window( @@ -688,7 +688,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanWindowed<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn distinct( @@ -740,7 +740,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanDistinct<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn order_by( @@ -761,7 +761,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanSorted<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn project( @@ -782,7 +782,7 @@ where impl<'s, 'a: 'b, 'b, 'arena, T, A> BindPlanProjected<'s, 'a, 'b, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub(crate) fn insert_into( @@ -810,7 +810,7 @@ impl BindPlanComplete { } } -impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b, T, A> { +impl<'a: 'b, 'b, T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b, T, A> { pub(crate) fn bind_scalar_queries( &mut self, mut plan: LogicalPlan, diff --git a/src/binder/show_table.rs b/src/binder/show_table.rs index c6a0891c..f230dfa9 100644 --- a/src/binder/show_table.rs +++ b/src/binder/show_table.rs @@ -16,10 +16,10 @@ use crate::binder::Binder; use crate::errors::DatabaseError; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_show_tables(&mut self) -> Result { Ok(LogicalPlan::new(Operator::ShowTable, Childrens::None)) } diff --git a/src/binder/show_view.rs b/src/binder/show_view.rs index dc90320e..7bf26dba 100644 --- a/src/binder/show_view.rs +++ b/src/binder/show_view.rs @@ -16,10 +16,10 @@ use crate::binder::Binder; use crate::errors::DatabaseError; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_show_views(&mut self) -> Result { Ok(LogicalPlan::new(Operator::ShowView, Childrens::None)) } diff --git a/src/binder/truncate.rs b/src/binder/truncate.rs index f3e1611d..8ae366d0 100644 --- a/src/binder/truncate.rs +++ b/src/binder/truncate.rs @@ -18,10 +18,10 @@ use crate::errors::DatabaseError; use crate::planner::operator::truncate::TruncateOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_truncate( &mut self, table_name: TableName, diff --git a/src/binder/update.rs b/src/binder/update.rs index bab3520b..1e9b8b4a 100644 --- a/src/binder/update.rs +++ b/src/binder/update.rs @@ -18,10 +18,10 @@ use crate::errors::DatabaseError; use crate::planner::operator::update::UpdateOperator; use crate::planner::operator::Operator; use crate::planner::{Childrens, ExprRef, LogicalPlan}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_update( &mut self, table_name: TableName, diff --git a/src/binder/window.rs b/src/binder/window.rs index 63a5edf5..176fa1c5 100644 --- a/src/binder/window.rs +++ b/src/binder/window.rs @@ -24,7 +24,7 @@ use crate::planner::operator::window::WindowOperator; use crate::planner::operator::Operator; use crate::planner::MetaArena; use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::LogicalType; struct WindowCollector { @@ -93,7 +93,7 @@ struct WindowGroup { output_columns: Vec, } -impl> Binder<'_, '_, T, A> { +impl> Binder<'_, '_, T, A> { pub(crate) fn bind_window_function( &mut self, kind: WindowFunctionKind, diff --git a/src/db.rs b/src/db.rs index f602f57f..87d9d0cc 100644 --- a/src/db.rs +++ b/src/db.rs @@ -54,7 +54,7 @@ use crate::storage::memory::MemoryStorage; use crate::storage::rocksdb::{OptimisticRocksStorage, RocksStorage, StorageConfig}; use crate::storage::table_codec::TableCodec; use crate::storage::{ - CheckpointableStorage, StatisticsMetaCache, Storage, TableCache, Transaction, + CheckpointableStorage, StatisticsMetaCache, Storage, StorageTransaction, TableCache, TransactionIsolationLevel, ViewCache, }; use crate::types::tuple::{Schema, SchemaView, TupleImpl}; @@ -84,12 +84,15 @@ pub enum CatalogKind { pub(crate) trait BindSource<'a> { type Iter: ResultIter; - type Transaction: Transaction; + type Transaction: StorageTransaction; type Storage: Storage; + /// `read_only` lets a fresh transaction use the storage's read path; executors + /// that already hold a transaction ignore it. fn execute>, A: MetaArena + 'a, F>( self, + read_only: bool, build: F, ) -> Result where @@ -586,7 +589,7 @@ fn optimizer_pipeline() -> HepOptimizerPipeline { .build() } -fn statement_stamp( +fn statement_stamp( transaction: &mut T, plan: &LogicalPlan, ) -> Result { @@ -700,7 +703,7 @@ impl State { Ok(()) } - pub(crate) fn build_plan<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>, F>( + pub(crate) fn build_plan<'a, T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, F>( &'a self, params: A, transaction: &T, @@ -963,7 +966,7 @@ impl Database { { let transaction = Box::into_raw(Box::new( self.storage - .transaction_with_isolation(self.transaction_isolation)?, + .transaction_with_isolation(self.transaction_isolation, false)?, )); let state = std::ptr::from_mut(&mut self.state); let (schema, plan_arena, executor) = @@ -1022,7 +1025,10 @@ impl Database { Ok(()) } CatalogKind::Table(name) => { - let transaction = self.storage.transaction()?; + let transaction = self.storage.transaction_with_isolation( + self.storage.default_transaction_isolation(), + true, + )?; let mut table_codec = TableCodec::default(); let table = transaction .load_table( @@ -1077,7 +1083,10 @@ impl Database { Ok(()) } CatalogKind::View(name) => { - let transaction = self.storage.transaction()?; + let transaction = self.storage.transaction_with_isolation( + self.storage.default_transaction_isolation(), + true, + )?; let mut table_codec = TableCodec::default(); let view = transaction .load_view( @@ -1099,15 +1108,18 @@ impl Database { /// Opens a new explicit transaction. /// /// Statements executed through the returned transaction share the same - /// transactional context until [`DBTransaction::commit`] is called. - pub fn new_transaction(&self) -> Result, DatabaseError> { + /// transactional context until [`Transaction::commit`] is called. + /// A `read_only` transaction rejects statements that write; on LMDB it runs + /// alongside the single writer instead of waiting for it. + pub fn new_transaction(&self, read_only: bool) -> Result, DatabaseError> { let transaction = self .storage - .transaction_with_isolation(self.transaction_isolation)?; + .transaction_with_isolation(self.transaction_isolation, read_only)?; - Ok(DBTransaction { + Ok(Transaction { inner: transaction, state: &self.state, + read_only, }) } @@ -1136,6 +1148,7 @@ impl<'a, S: Storage> BindSource<'a> for &'a Database { fn execute>, A: MetaArena + 'a, F>( self, + read_only: bool, build: F, ) -> Result where @@ -1143,7 +1156,7 @@ impl<'a, S: Storage> BindSource<'a> for &'a Database { { let transaction = Box::into_raw(Box::new( self.storage - .transaction_with_isolation(self.transaction_isolation)?, + .transaction_with_isolation(self.transaction_isolation, read_only)?, )); let result = (|| { let transaction = unsafe { &mut *transaction }; @@ -1178,7 +1191,7 @@ impl<'a, S: Storage> BindSource<'a> for &'a Database { { let mut transaction = self .storage - .transaction_with_isolation(self.transaction_isolation)?; + .transaction_with_isolation(self.transaction_isolation, true)?; transaction.begin_statement_scope()?; let (plan, mut arena) = self.state.build_plan(params, &transaction, build)?; Ok(plan.explain(&mut arena, 0)) @@ -1335,12 +1348,13 @@ impl ResultIter for DatabaseIter<'_, S> { } /// Explicit transaction handle created by [`Database::new_transaction`]. -pub struct DBTransaction<'a, S: Storage + 'a> { +pub struct Transaction<'a, S: Storage + 'a> { pub(crate) inner: S::TransactionType<'a>, pub(crate) state: &'a State, + read_only: bool, } -impl<'txn, S: Storage> DBTransaction<'txn, S> { +impl<'txn, S: Storage> Transaction<'txn, S> { /// Commits the current transaction. pub fn commit(self) -> Result<(), DatabaseError> { self.inner.commit()?; @@ -1349,7 +1363,7 @@ impl<'txn, S: Storage> DBTransaction<'txn, S> { } } -impl<'a, 'txn, S: Storage> BindSource<'a> for &'a mut DBTransaction<'txn, S> { +impl<'a, 'txn, S: Storage> BindSource<'a> for &'a mut Transaction<'txn, S> { type Iter = TransactionIter<'a, S::TransactionType<'txn>>; type Transaction = S::TransactionType<'txn>; @@ -1357,6 +1371,7 @@ impl<'a, 'txn, S: Storage> BindSource<'a> for &'a mut DBTransaction<'txn, S> { fn execute>, A: MetaArena + 'a, F>( self, + _read_only: bool, build: F, ) -> Result where @@ -1364,10 +1379,16 @@ impl<'a, 'txn, S: Storage> BindSource<'a> for &'a mut DBTransaction<'txn, S> { { self.inner.begin_statement_scope()?; let (plan, arena) = build(self.state, &self.inner)?; + let plan = plan.into(); + if self.read_only && !plan.root().operator.command_type().is_read_only() { + return Err(DatabaseError::UnsupportedStmt( + "cannot write in a read-only transaction".into(), + )); + } let transaction = std::ptr::from_mut(&mut self.inner); let (schema, arena, executor) = self.state - .execute(unsafe { &mut *transaction }, plan.into(), arena)?; + .execute(unsafe { &mut *transaction }, plan, arena)?; Ok(TransactionIter::new( schema, arena.boxed(), @@ -1391,13 +1412,13 @@ impl<'a, 'txn, S: Storage> BindSource<'a> for &'a mut DBTransaction<'txn, S> { } } -struct TransactionGuard<'a, T: Transaction + 'a> { +struct TransactionGuard<'a, T: StorageTransaction + 'a> { executor: Option>, transaction: *mut T, statement_scope_active: bool, } -impl TransactionGuard<'_, T> { +impl TransactionGuard<'_, T> { #[inline] fn finish_statement_scope( &mut self, @@ -1415,14 +1436,18 @@ impl TransactionGuard<'_, T> { } } -impl Drop for TransactionGuard<'_, T> { +impl Drop for TransactionGuard<'_, T> { fn drop(&mut self) { let _ = self.finish_statement_scope(&mut Vec::new()); } } /// Raw result iterator returned by transaction execution APIs. -pub struct TransactionIter<'a, T: Transaction + 'a, A: MetaArena + 'a = Box> { +pub struct TransactionIter< + 'a, + T: StorageTransaction + 'a, + A: MetaArena + 'a = Box, +> { // Drop execution before the metadata arena it borrows from. guard: TransactionGuard<'a, T>, plan_arena: ExecMetaArena, @@ -1430,7 +1455,7 @@ pub struct TransactionIter<'a, T: Transaction + 'a, A: MetaArena + 'a = Box, } -impl<'a, T: Transaction + 'a, A: MetaArena + 'a> TransactionIter<'a, T, A> { +impl<'a, T: StorageTransaction + 'a, A: MetaArena + 'a> TransactionIter<'a, T, A> { pub(crate) fn new( schema: Schema, plan_arena: ExecMetaArena, @@ -1493,7 +1518,7 @@ impl<'a, T: Transaction + 'a, A: MetaArena + 'a> TransactionIter<'a, T, A> { } } -impl<'a, T: Transaction + 'a> TransactionIter<'a, T, PlanArena<'a>> { +impl<'a, T: StorageTransaction + 'a> TransactionIter<'a, T, PlanArena<'a>> { fn done_with_ddl_apply(mut self) -> Result<(PlanArena<'a>, Vec), DatabaseError> { while self.next_tuple(|_, _| ())?.is_some() {} let Self { @@ -1507,7 +1532,7 @@ impl<'a, T: Transaction + 'a> TransactionIter<'a, T, PlanArena<'a>> { } } -impl ResultIter for TransactionIter<'_, T, A> { +impl ResultIter for TransactionIter<'_, T, A> { fn schema(&self, f: impl FnOnce(&SchemaView<'_, '_>) -> R) -> R { TransactionIter::schema(self, f) } @@ -1541,7 +1566,7 @@ pub(crate) mod test { use crate::planner::{Childrens, ExprRef, LogicalPlan, PlanArena, TableArenaCell}; use crate::storage::memory::MemoryStorage; use crate::storage::{ - table_codec::TableCodec, StatisticsMetaCache, Storage, TableCache, Transaction, + table_codec::TableCodec, StatisticsMetaCache, Storage, StorageTransaction, TableCache, TransactionIsolationLevel, ViewCache, }; use crate::types::tuple::{Tuple, TupleImpl}; @@ -1772,7 +1797,7 @@ pub(crate) mod test { Ok(()) } - pub(crate) fn build_table( + pub(crate) fn build_table( table_cache: &mut TableCache, transaction: &mut T, plan_arena: &mut PlanArena, @@ -2376,8 +2401,8 @@ pub(crate) mod test { kite_sql.ddl("create table t1 (a int primary key, b int)")?; - let mut tx_1 = kite_sql.new_transaction()?; - let mut tx_2 = kite_sql.new_transaction()?; + let mut tx_1 = kite_sql.new_transaction(false)?; + let mut tx_2 = kite_sql.new_transaction(false)?; tx_1.run("insert into t1 values(0, 0)")?.done()?; tx_1.run("insert into t1 values(1, 1)")?.done()?; @@ -2407,7 +2432,7 @@ pub(crate) mod test { tx_1.commit()?; tx_2.commit()?; - let mut tx_3 = kite_sql.new_transaction()?; + let mut tx_3 = kite_sql.new_transaction(false)?; let res = tx_3.run("create table t2 (a int primary key, b int)"); assert!(res.is_err()); @@ -2421,7 +2446,7 @@ pub(crate) mod test { kite_sql.ddl("create table t_multi_tx (a int primary key, b int)")?; - let mut tx = kite_sql.new_transaction()?; + let mut tx = kite_sql.new_transaction(false)?; let mut iter = tx.run( "insert into t_multi_tx values(0, 0); insert into t_multi_tx values(1, 1); select * from t_multi_tx order by a", )?; @@ -2454,7 +2479,7 @@ pub(crate) mod test { kite_sql.ddl("create table t_iter_drop (a int primary key, b int)")?; - let mut tx = kite_sql.new_transaction()?; + let mut tx = kite_sql.new_transaction(false)?; tx.run("insert into t_iter_drop values (0, 0), (1, 1)")? .done()?; @@ -2503,8 +2528,8 @@ pub(crate) mod test { kite_sql.ddl("create table t_rc (a int primary key, b int)")?; kite_sql.run("insert into t_rc values (1, 10)")?.done()?; - let mut reader = kite_sql.new_transaction()?; - let mut writer = kite_sql.new_transaction()?; + let mut reader = kite_sql.new_transaction(false)?; + let mut writer = kite_sql.new_transaction(false)?; assert_eq!( read_single_i32(reader.run("select b from t_rc where a = 1")?)?, @@ -2546,8 +2571,8 @@ pub(crate) mod test { kite_sql.ddl("create table t_rr (a int primary key, b int)")?; kite_sql.run("insert into t_rr values (1, 10)")?.done()?; - let mut reader = kite_sql.new_transaction()?; - let mut writer = kite_sql.new_transaction()?; + let mut reader = kite_sql.new_transaction(false)?; + let mut writer = kite_sql.new_transaction(false)?; assert_eq!( read_single_i32(reader.run("select b from t_rr where a = 1")?)?, @@ -2573,8 +2598,8 @@ pub(crate) mod test { kite_sql.ddl("create table t1 (a int primary key, b int)")?; - let mut tx_1 = kite_sql.new_transaction()?; - let mut tx_2 = kite_sql.new_transaction()?; + let mut tx_1 = kite_sql.new_transaction(false)?; + let mut tx_2 = kite_sql.new_transaction(false)?; tx_1.run("insert into t1 values(0, 0)")?.done()?; tx_1.run("insert into t1 values(1, 1)")?.done()?; @@ -2609,7 +2634,7 @@ pub(crate) mod test { assert!(tx_2.commit().is_err()); - let mut tx_3 = kite_sql.new_transaction()?; + let mut tx_3 = kite_sql.new_transaction(false)?; let res = tx_3.run("create table t2 (a int primary key, b int)"); assert!(res.is_err()); @@ -2686,7 +2711,7 @@ pub(crate) mod test { vec![DataValue::Int32(7)] ); iter.done()?; - let mut tx = db.new_transaction()?; + let mut tx = db.new_transaction(false)?; let mut iter = tx.run(&sql)?; assert_eq!( iter.next_tuple(|_, row| row.clone())?.unwrap(), diff --git a/src/db/prepared.rs b/src/db/prepared.rs index a278f778..1929a12e 100644 --- a/src/db/prepared.rs +++ b/src/db/prepared.rs @@ -16,6 +16,7 @@ pub struct PreparedPlan<'db> { pub(crate) plan: LogicalPlan, pub(crate) arena: PlanArena<'db>, parameter_expressions: Vec, + pub(crate) read_only: bool, } impl<'db> PreparedPlan<'db> { @@ -40,7 +41,7 @@ impl Database { )); } let arena = prepared.bind_parameters(params.as_ref())?; - BindSource::execute(self, |_, _| { + BindSource::execute(self, prepared.read_only, |_, _| { Ok((PlanInput::Borrowed(&prepared.plan), arena)) }) } @@ -56,12 +57,12 @@ impl Database { let statement = self.state.parse_statement(sql)?; let transaction = self .storage - .transaction_with_isolation(self.transaction_isolation)?; + .transaction_with_isolation(self.transaction_isolation, true)?; self.state.prepare_plan(&statement, params, &transaction) } } -impl<'db, S: Storage> DBTransaction<'db, S> { +impl<'db, S: Storage> Transaction<'db, S> { /// Executes a prepared SQL or ORM plan inside this transaction. pub fn execute<'a>( &'a mut self, @@ -74,7 +75,7 @@ impl<'db, S: Storage> DBTransaction<'db, S> { )); } let arena = prepared.bind_parameters(params.as_ref())?; - BindSource::execute(self, |_, _| { + BindSource::execute(self, prepared.read_only, |_, _| { Ok((PlanInput::Borrowed(&prepared.plan), arena)) }) } @@ -114,7 +115,12 @@ impl State { }) } - pub(crate) fn prepare_plan_with<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>, F>( + pub(crate) fn prepare_plan_with< + 'a, + T: StorageTransaction, + A: AsRef<[(usize, LogicalType)]>, + F, + >( &'a self, params: A, transaction: &T, @@ -128,10 +134,12 @@ impl State { { let (plan, mut arena) = self.build_plan(params, transaction, build)?; let parameter_expressions = arena.parameter_expressions(); + let read_only = plan.operator.command_type().is_read_only(); Ok(PreparedPlan { plan, arena, parameter_expressions, + read_only, }) } } @@ -177,7 +185,7 @@ mod tests { assert!(iter.next_tuple(|_, _| ())?.is_none()); iter.done()?; } - let mut tx = db.new_transaction()?; + let mut tx = db.new_transaction(false)?; let mut iter = tx.execute(&plan, [(1, DataValue::Int64(7))])?; assert_eq!( iter.next_tuple(|_, row| row.clone())?.unwrap(), diff --git a/src/execution/ddl/add_column.rs b/src/execution/ddl/add_column.rs index 3c7c9ee7..535aface 100644 --- a/src/execution/ddl/add_column.rs +++ b/src/execution/ddl/add_column.rs @@ -20,7 +20,7 @@ use crate::execution::{ use crate::iter_ext::Itertools; use crate::planner::operator::alter_table::add_column::AddColumnOperator; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::index::{Index, IndexType}; use crate::types::value::DataValue; @@ -28,7 +28,7 @@ pub struct AddColumn<'a> { op: &'a AddColumnOperator, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for AddColumn<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for AddColumn<'a> { type Input = &'a AddColumnOperator; fn into_executor( @@ -42,7 +42,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for AddColumn<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for AddColumn<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for AddColumn<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/ddl/change_column.rs b/src/execution/ddl/change_column.rs index 1c1ae9a0..ce12e245 100644 --- a/src/execution/ddl/change_column.rs +++ b/src/execution/ddl/change_column.rs @@ -20,14 +20,14 @@ use crate::execution::{ use crate::iter_ext::Itertools; use crate::planner::operator::alter_table::change_column::{ChangeColumnOperator, NotNullChange}; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::value::DataValue; pub struct ChangeColumn<'a> { op: &'a ChangeColumnOperator, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for ChangeColumn<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for ChangeColumn<'a> { type Input = &'a ChangeColumnOperator; fn into_executor( @@ -41,7 +41,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for ChangeColumn<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ChangeColumn<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for ChangeColumn<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/ddl/create_index.rs b/src/execution/ddl/create_index.rs index 60b70990..d8197a84 100644 --- a/src/execution/ddl/create_index.rs +++ b/src/execution/ddl/create_index.rs @@ -21,7 +21,7 @@ use crate::expression::ScalarExpression; use crate::planner::operator::create_index::CreateIndexOperator; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::index::Index; use crate::types::tuple::Schema; use crate::types::value::DataValue; @@ -33,7 +33,7 @@ pub struct CreateIndex<'a> { input: ExecId, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for CreateIndex<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for CreateIndex<'a> { type Input = (&'a CreateIndexOperator, &'a LogicalPlan); fn into_executor( @@ -53,7 +53,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for CreateIndex<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CreateIndex<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for CreateIndex<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/ddl/create_table.rs b/src/execution/ddl/create_table.rs index 9f398f8f..cc90dafe 100644 --- a/src/execution/ddl/create_table.rs +++ b/src/execution/ddl/create_table.rs @@ -18,13 +18,13 @@ use crate::execution::{ }; use crate::planner::operator::create_table::CreateTableOperator; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; pub struct CreateTable<'a> { op: &'a CreateTableOperator, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for CreateTable<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for CreateTable<'a> { type Input = &'a CreateTableOperator; fn into_executor( @@ -38,7 +38,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for CreateTable<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CreateTable<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for CreateTable<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/ddl/create_view.rs b/src/execution/ddl/create_view.rs index f7184891..85d60576 100644 --- a/src/execution/ddl/create_view.rs +++ b/src/execution/ddl/create_view.rs @@ -18,13 +18,13 @@ use crate::execution::{ }; use crate::planner::operator::create_view::CreateViewOperator; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; pub struct CreateView<'a> { op: &'a CreateViewOperator, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for CreateView<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for CreateView<'a> { type Input = &'a CreateViewOperator; fn into_executor( @@ -38,7 +38,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for CreateView<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CreateView<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for CreateView<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/ddl/drop_column.rs b/src/execution/ddl/drop_column.rs index 6e1712ae..501d0620 100644 --- a/src/execution/ddl/drop_column.rs +++ b/src/execution/ddl/drop_column.rs @@ -20,13 +20,13 @@ use crate::execution::{ use crate::iter_ext::Itertools; use crate::planner::operator::alter_table::drop_column::DropColumnOperator; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; pub struct DropColumn<'a> { op: &'a DropColumnOperator, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for DropColumn<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for DropColumn<'a> { type Input = &'a DropColumnOperator; fn into_executor( @@ -40,7 +40,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for DropColumn<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for DropColumn<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for DropColumn<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/ddl/drop_index.rs b/src/execution/ddl/drop_index.rs index 1984a0b1..5a8f5e2a 100644 --- a/src/execution/ddl/drop_index.rs +++ b/src/execution/ddl/drop_index.rs @@ -18,13 +18,13 @@ use crate::execution::{ }; use crate::planner::operator::drop_index::DropIndexOperator; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; pub struct DropIndex<'a> { op: &'a DropIndexOperator, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for DropIndex<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for DropIndex<'a> { type Input = &'a DropIndexOperator; fn into_executor( @@ -38,7 +38,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for DropIndex<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for DropIndex<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for DropIndex<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/ddl/drop_table.rs b/src/execution/ddl/drop_table.rs index 3df6c183..9e6bf44b 100644 --- a/src/execution/ddl/drop_table.rs +++ b/src/execution/ddl/drop_table.rs @@ -18,13 +18,13 @@ use crate::execution::{ }; use crate::planner::operator::drop_table::DropTableOperator; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; pub struct DropTable<'a> { op: &'a DropTableOperator, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for DropTable<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for DropTable<'a> { type Input = &'a DropTableOperator; fn into_executor( @@ -38,7 +38,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for DropTable<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for DropTable<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for DropTable<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/ddl/drop_view.rs b/src/execution/ddl/drop_view.rs index ce2b1277..4a8718a1 100644 --- a/src/execution/ddl/drop_view.rs +++ b/src/execution/ddl/drop_view.rs @@ -18,13 +18,13 @@ use crate::execution::{ }; use crate::planner::operator::drop_view::DropViewOperator; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; pub struct DropView<'a> { op: &'a DropViewOperator, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for DropView<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for DropView<'a> { type Input = &'a DropViewOperator; fn into_executor( @@ -38,7 +38,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for DropView<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for DropView<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for DropView<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/ddl/mod.rs b/src/execution/ddl/mod.rs index 743e125d..49bb6ce9 100644 --- a/src/execution/ddl/mod.rs +++ b/src/execution/ddl/mod.rs @@ -26,7 +26,7 @@ pub(crate) mod truncate; use crate::catalog::TableName; use crate::errors::DatabaseError; use crate::storage::table_codec::TableCodec; -use crate::storage::{InnerIter, Transaction}; +use crate::storage::{InnerIter, StorageTransaction}; use crate::types::serialize::TupleValueSerializableImpl; use crate::types::tuple::{Tuple, TupleId, TupleImpl}; use crate::types::LogicalType; @@ -47,7 +47,7 @@ fn read_tuple_batch( batch_size: usize, ) -> Result where - T: Transaction, + T: StorageTransaction, D: FnMut() -> I, I: IntoIterator, { @@ -108,7 +108,7 @@ pub(crate) fn visit_table_in_batches( mut visit: F, ) -> Result<(), DatabaseError> where - T: Transaction, + T: StorageTransaction, D: FnMut() -> I, I: IntoIterator, F: FnMut(&TupleImpl) -> Result<(), DatabaseError>, @@ -160,7 +160,7 @@ pub(crate) fn rewrite_table_in_batches( mut after_write: G, ) -> Result<(), DatabaseError> where - T: Transaction, + T: StorageTransaction, D: FnMut() -> I, I: IntoIterator, S: FnMut() -> J, diff --git a/src/execution/ddl/truncate.rs b/src/execution/ddl/truncate.rs index ffc54ee5..65f2177e 100644 --- a/src/execution/ddl/truncate.rs +++ b/src/execution/ddl/truncate.rs @@ -18,13 +18,13 @@ use crate::execution::{ }; use crate::planner::operator::truncate::TruncateOperator; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; pub struct Truncate<'a> { op: &'a TruncateOperator, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for Truncate<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for Truncate<'a> { type Input = &'a TruncateOperator; fn into_executor( @@ -38,7 +38,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for Truncate<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Truncate<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Truncate<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dml/analyze.rs b/src/execution/dml/analyze.rs index 5a7f54bd..e7a3e839 100644 --- a/src/execution/dml/analyze.rs +++ b/src/execution/dml/analyze.rs @@ -28,7 +28,7 @@ use crate::optimizer::core::statistics_meta::IndexStatistics; use crate::planner::operator::analyze::AnalyzeOperator; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::{table_codec::TableCodec, Transaction}; +use crate::storage::{table_codec::TableCodec, StorageTransaction}; use crate::types::index::IndexId; use crate::types::tuple::TupleLike; use crate::types::value::{DataValue, Utf8Type}; @@ -46,7 +46,7 @@ pub struct Analyze<'a> { statistics_relative_error: Option>, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for Analyze<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for Analyze<'a> { type Input = (&'a AnalyzeOperator, &'a LogicalPlan); fn into_executor( @@ -82,7 +82,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for Analyze<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Analyze<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Analyze<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, @@ -176,7 +176,7 @@ struct State { } impl<'a> Analyze<'a> { - fn persist_statistics( + fn persist_statistics( table_name: &TableName, columns: Vec<(ColumnId, usize, HistogramMetaBuilder)>, builders: Vec, @@ -251,7 +251,7 @@ mod test { use crate::optimizer::core::cm_sketch::COUNT_MIN_SKETCH_STORAGE_PAGE_LEN; use crate::optimizer::core::histogram::{HistogramMetaBuilder, DEFAULT_HLL_PRECISION}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; - use crate::storage::{table_codec::TableCodec, InnerIter, Storage, Transaction}; + use crate::storage::{table_codec::TableCodec, InnerIter, Storage, StorageTransaction}; use crate::types::value::DataValue; use std::ops::Bound; use tempfile::TempDir; diff --git a/src/execution/dml/copy_from_file.rs b/src/execution/dml/copy_from_file.rs index 927d2a36..72cd8f19 100644 --- a/src/execution/dml/copy_from_file.rs +++ b/src/execution/dml/copy_from_file.rs @@ -20,7 +20,7 @@ use crate::execution::{ use crate::iter_ext::Itertools; use crate::planner::operator::copy_from_file::CopyFromFileOperator; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple::{Tuple, TupleImpl}; use crate::types::tuple_builder::TupleBuilder; use std::fs::File; @@ -30,7 +30,7 @@ pub struct CopyFromFile<'a> { op: &'a CopyFromFileOperator, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for CopyFromFile<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for CopyFromFile<'a> { type Input = &'a CopyFromFileOperator; fn into_executor( @@ -44,7 +44,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for CopyFromFile<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CopyFromFile<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for CopyFromFile<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dml/copy_to_file.rs b/src/execution/dml/copy_to_file.rs index 6cc39682..60a2fef5 100644 --- a/src/execution/dml/copy_to_file.rs +++ b/src/execution/dml/copy_to_file.rs @@ -21,7 +21,7 @@ use crate::iter_ext::Itertools; use crate::planner::operator::copy_to_file::CopyToFileOperator; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple_builder::TupleBuilder; pub struct CopyToFile<'a> { @@ -30,7 +30,7 @@ pub struct CopyToFile<'a> { input: Option, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for CopyToFile<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for CopyToFile<'a> { type Input = (&'a CopyToFileOperator, &'a LogicalPlan); fn into_executor( @@ -83,7 +83,7 @@ impl CopyToFile<'_> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for CopyToFile<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for CopyToFile<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dml/delete.rs b/src/execution/dml/delete.rs index 4140e36c..75e83700 100644 --- a/src/execution/dml/delete.rs +++ b/src/execution/dml/delete.rs @@ -20,7 +20,7 @@ use crate::execution::{ use crate::planner::operator::delete::DeleteOperator; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::index::{Index, IndexType}; use crate::types::tuple_builder::TupleBuilder; @@ -29,7 +29,7 @@ pub struct Delete<'a> { input: Option, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for Delete<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for Delete<'a> { type Input = (&'a DeleteOperator, &'a LogicalPlan); fn into_executor( @@ -50,7 +50,7 @@ impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for Delete<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Delete<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Delete<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dml/insert.rs b/src/execution/dml/insert.rs index 0d274505..037a6247 100644 --- a/src/execution/dml/insert.rs +++ b/src/execution/dml/insert.rs @@ -21,7 +21,7 @@ use crate::iter_ext::Itertools; use crate::planner::operator::insert::InsertOperator; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::index::Index; use crate::types::tuple::{Schema, Tuple, TupleImpl}; use crate::types::tuple_builder::TupleBuilder; @@ -37,7 +37,7 @@ pub struct Insert<'a> { is_mapping_by_name: bool, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for Insert<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for Insert<'a> { type Input = (&'a InsertOperator, &'a LogicalPlan); fn into_executor( @@ -91,7 +91,7 @@ impl Insert<'_> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Insert<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Insert<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dml/update.rs b/src/execution/dml/update.rs index 8fdd680e..55e1e215 100644 --- a/src/execution/dml/update.rs +++ b/src/execution/dml/update.rs @@ -22,7 +22,7 @@ use crate::planner::operator::update::UpdateOperator; use crate::planner::ExprRef; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::index::{Index, IndexMeta, IndexType}; use crate::types::tuple::{Schema, Tuple}; use crate::types::tuple_builder::TupleBuilder; @@ -36,7 +36,7 @@ pub struct Update<'a> { input: Option, } -impl<'a, T: Transaction + 'a> WriteExecutor<'a, T> for Update<'a> { +impl<'a, T: StorageTransaction + 'a> WriteExecutor<'a, T> for Update<'a> { type Input = (&'a UpdateOperator, &'a LogicalPlan); fn into_executor( @@ -87,7 +87,7 @@ impl Update<'_> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Update<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Update<'a> { fn next_tuple( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/aggregate/hash_agg.rs b/src/execution/dql/aggregate/hash_agg.rs index 5864dece..3c9a3693 100644 --- a/src/execution/dql/aggregate/hash_agg.rs +++ b/src/execution/dql/aggregate/hash_agg.rs @@ -23,7 +23,7 @@ use crate::planner::operator::aggregate::AggregateOperator; use crate::planner::ExprRef; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::value::DataValue; use std::collections::hash_map::IntoIter as HashMapIntoIter; use std::collections::HashMap; @@ -37,7 +37,7 @@ pub struct HashAggExecutor<'a> { output: Option, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for HashAggExecutor<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for HashAggExecutor<'a> { type Input = (&'a AggregateOperator, &'a LogicalPlan); fn into_executor( @@ -64,7 +64,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for HashAggExecutor<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for HashAggExecutor<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for HashAggExecutor<'a> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/aggregate/simple_agg.rs b/src/execution/dql/aggregate/simple_agg.rs index a975da0e..09966f67 100644 --- a/src/execution/dql/aggregate/simple_agg.rs +++ b/src/execution/dql/aggregate/simple_agg.rs @@ -22,14 +22,14 @@ use crate::planner::operator::aggregate::AggregateOperator; use crate::planner::ExprRef; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; pub struct SimpleAggExecutor<'a> { agg_calls: &'a [ExprRef], input: ExecId, returned: bool, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for SimpleAggExecutor<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for SimpleAggExecutor<'a> { type Input = (&'a AggregateOperator, &'a LogicalPlan); fn into_executor( @@ -48,7 +48,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for SimpleAggExecutor<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for SimpleAggExecutor<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for SimpleAggExecutor<'a> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/aggregate/stream_agg.rs b/src/execution/dql/aggregate/stream_agg.rs index 12768256..0a896921 100644 --- a/src/execution/dql/aggregate/stream_agg.rs +++ b/src/execution/dql/aggregate/stream_agg.rs @@ -23,7 +23,7 @@ use crate::planner::operator::aggregate::AggregateOperator; use crate::planner::ExprRef; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::value::DataValue; use std::mem; @@ -36,7 +36,7 @@ pub struct StreamAggExecutor<'a> { input: ExecId, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for StreamAggExecutor<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for StreamAggExecutor<'a> { type Input = (&'a AggregateOperator, &'a LogicalPlan); fn into_executor( @@ -64,7 +64,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for StreamAggExecutor<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for StreamAggExecutor<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for StreamAggExecutor<'a> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/aggregate/stream_distinct.rs b/src/execution/dql/aggregate/stream_distinct.rs index 5d946d4c..89e79938 100644 --- a/src/execution/dql/aggregate/stream_distinct.rs +++ b/src/execution/dql/aggregate/stream_distinct.rs @@ -20,7 +20,7 @@ use crate::planner::operator::aggregate::AggregateOperator; use crate::planner::ExprRef; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; @@ -30,7 +30,7 @@ pub struct StreamDistinctExecutor<'a> { last_keys: Option, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for StreamDistinctExecutor<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for StreamDistinctExecutor<'a> { type Input = (&'a AggregateOperator, &'a LogicalPlan); fn into_executor( @@ -49,7 +49,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for StreamDistinctExecutor<'a> } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for StreamDistinctExecutor<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for StreamDistinctExecutor<'a> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/describe.rs b/src/execution/dql/describe.rs index 77ee8470..6368653f 100644 --- a/src/execution/dql/describe.rs +++ b/src/execution/dql/describe.rs @@ -17,7 +17,7 @@ use crate::errors::DatabaseError; use crate::execution::{ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor}; use crate::planner::operator::describe::DescribeOperator; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::value::{DataValue, Utf8Type}; use crate::types::CharLengthUnits; use std::sync::LazyLock; @@ -56,7 +56,7 @@ impl From<&DescribeOperator> for Describe { } } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Describe { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for Describe { type Input = &'a DescribeOperator; fn into_executor( @@ -71,7 +71,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Describe { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Describe { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Describe { fn reset( &mut self, _arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/dummy.rs b/src/execution/dql/dummy.rs index 163d3150..a39ce217 100644 --- a/src/execution/dql/dummy.rs +++ b/src/execution/dql/dummy.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::execution::{ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor}; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple::Tuple; pub struct Dummy { @@ -30,7 +30,7 @@ impl Default for Dummy { } } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Dummy { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for Dummy { type Input = (); fn into_executor( @@ -45,7 +45,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Dummy { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Dummy { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Dummy { fn reset( &mut self, _arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/explain.rs b/src/execution/dql/explain.rs index d7582ec5..c8be66a8 100644 --- a/src/execution/dql/explain.rs +++ b/src/execution/dql/explain.rs @@ -16,7 +16,7 @@ use crate::errors::DatabaseError; use crate::execution::{ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor}; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::value::{DataValue, Utf8Type}; use crate::types::CharLengthUnits; @@ -25,7 +25,7 @@ pub struct Explain<'a> { emitted: bool, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Explain<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for Explain<'a> { type Input = &'a LogicalPlan; fn into_executor( @@ -42,7 +42,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Explain<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Explain<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Explain<'a> { fn reset( &mut self, _arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/external_sort.rs b/src/execution/dql/external_sort.rs index c3992f2a..9e181505 100644 --- a/src/execution/dql/external_sort.rs +++ b/src/execution/dql/external_sort.rs @@ -21,7 +21,7 @@ use crate::execution::{ use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::fs::File; use std::io::BufReader; use std::mem; @@ -49,7 +49,7 @@ pub struct ExternalSort<'a> { input: ExecId, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ExternalSort<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for ExternalSort<'a> { type Input = (&'a SortOperator, &'a LogicalPlan); fn into_executor( @@ -68,7 +68,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ExternalSort<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ExternalSort<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for ExternalSort<'a> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/filter.rs b/src/execution/dql/filter.rs index fc2a2932..766040ef 100644 --- a/src/execution/dql/filter.rs +++ b/src/execution/dql/filter.rs @@ -20,13 +20,13 @@ use crate::planner::operator::filter::FilterOperator; use crate::planner::ExprRef; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; pub struct Filter { predicate: ExprRef, input: ExecId, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Filter { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for Filter { type Input = (&'a FilterOperator, &'a LogicalPlan); fn into_executor( @@ -44,7 +44,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Filter { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Filter { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Filter { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/function_scan.rs b/src/execution/dql/function_scan.rs index 71185a65..cff849dd 100644 --- a/src/execution/dql/function_scan.rs +++ b/src/execution/dql/function_scan.rs @@ -17,7 +17,7 @@ use crate::execution::{ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNo use crate::expression::function::table::TableFunction; use crate::planner::operator::function_scan::FunctionScanOperator; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple::Tuple; pub struct FunctionScan<'a> { @@ -34,7 +34,7 @@ impl<'a> From<&'a FunctionScanOperator> for FunctionScan<'a> { } } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for FunctionScan<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for FunctionScan<'a> { type Input = &'a FunctionScanOperator; fn into_executor( @@ -49,7 +49,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for FunctionScan<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for FunctionScan<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for FunctionScan<'a> { fn reset( &mut self, _arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/index_scan.rs b/src/execution/dql/index_scan.rs index 155243cc..940b0b45 100644 --- a/src/execution/dql/index_scan.rs +++ b/src/execution/dql/index_scan.rs @@ -18,18 +18,18 @@ use crate::expression::range_detacher::{IndexRangeColumn, RangeDetacher}; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::SortOption; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::{IndexIter, IndexRanges, Iter, Transaction}; +use crate::storage::{IndexIter, IndexRanges, Iter, StorageTransaction}; use crate::types::index::{IndexInfo, IndexLookup}; use std::borrow::Cow; -pub(crate) struct IndexScan<'a, T: Transaction + 'a> { +pub(crate) struct IndexScan<'a, T: StorageTransaction + 'a> { op: &'a TableScanOperator, info: &'a IndexInfo, lookup: &'a IndexLookup, iter: Option>, } -impl<'a, T: Transaction + 'a> IndexScan<'a, T> { +impl<'a, T: StorageTransaction + 'a> IndexScan<'a, T> { pub(crate) fn new( op: &'a TableScanOperator, info: &'a IndexInfo, @@ -83,7 +83,7 @@ impl<'a, T: Transaction + 'a> IndexScan<'a, T> { } } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for IndexScan<'a, T> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for IndexScan<'a, T> { type Input = (&'a TableScanOperator, &'a IndexInfo, &'a IndexLookup); fn into_executor( @@ -97,7 +97,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for IndexScan<'a, T> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for IndexScan<'a, T> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for IndexScan<'a, T> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/join/hash_join.rs b/src/execution/dql/join/hash_join.rs index 3e8793ae..e50db76f 100644 --- a/src/execution/dql/join/hash_join.rs +++ b/src/execution/dql/join/hash_join.rs @@ -28,7 +28,7 @@ use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::ExprRef; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::bitset::BitSet; use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; @@ -143,7 +143,7 @@ impl HashJoin { Ok(()) } - fn initialize_build<'a, T: Transaction + 'a, A: MetaArena + 'a>( + fn initialize_build<'a, T: StorageTransaction + 'a, A: MetaArena + 'a>( &mut self, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, @@ -229,7 +229,7 @@ pub(crate) struct BuildState { pub(crate) tuples: Vec<(usize, TupleImpl)>, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for HashJoin { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for HashJoin { type Input = (&'a JoinOperator, &'a LogicalPlan, &'a LogicalPlan); fn into_executor( @@ -254,7 +254,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for HashJoin { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for HashJoin { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for HashJoin { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/join/index_probe_join.rs b/src/execution/dql/join/index_probe_join.rs index 7cb16d0a..fd7862db 100644 --- a/src/execution/dql/join/index_probe_join.rs +++ b/src/execution/dql/join/index_probe_join.rs @@ -19,7 +19,7 @@ use crate::execution::{ }; use crate::planner::operator::index_probe_join::IndexProbeJoinOperator; use crate::planner::{ExecMetaArena, LogicalPlan, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::index::RuntimeIndexProbe; pub struct IndexProbeJoin<'a> { @@ -28,7 +28,7 @@ pub struct IndexProbeJoin<'a> { right_input: ExecId, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for IndexProbeJoin<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for IndexProbeJoin<'a> { type Input = (&'a IndexProbeJoinOperator, &'a LogicalPlan, &'a LogicalPlan); fn into_executor( @@ -48,7 +48,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for IndexProbeJoin<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for IndexProbeJoin<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for IndexProbeJoin<'a> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/join/nested_loop_join.rs b/src/execution/dql/join/nested_loop_join.rs index 00ef1eff..7b3c5bea 100644 --- a/src/execution/dql/join/nested_loop_join.rs +++ b/src/execution/dql/join/nested_loop_join.rs @@ -24,7 +24,7 @@ use crate::execution::{ }; use crate::planner::operator::join::{JoinCondition, JoinOperator, JoinType}; use crate::planner::ExprRef; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::bitset::BitSet; use crate::types::tuple::{SplitTupleRef, TupleImpl, TupleLike}; use crate::types::value::DataValue; @@ -107,7 +107,7 @@ struct ActiveLeftState { first_matches: Vec, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for NestedLoopJoin { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for NestedLoopJoin { type Input = (&'a JoinOperator, &'a LogicalPlan, &'a LogicalPlan); fn into_executor( @@ -150,7 +150,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for NestedLoopJoin { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin { fn reset( &mut self, arena: &mut ExecArena<'a, T>, @@ -329,7 +329,7 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin { } impl NestedLoopJoin { - fn reset_right_input<'a, T: Transaction + 'a, A: MetaArena + 'a>( + fn reset_right_input<'a, T: StorageTransaction + 'a, A: MetaArena + 'a>( &mut self, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, diff --git a/src/execution/dql/limit.rs b/src/execution/dql/limit.rs index 8ac1cce9..1324f73f 100644 --- a/src/execution/dql/limit.rs +++ b/src/execution/dql/limit.rs @@ -19,7 +19,7 @@ use crate::execution::{ use crate::planner::operator::limit::LimitOperator; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; pub struct Limit { offset: Option, limit: Option, @@ -28,7 +28,7 @@ pub struct Limit { emitted: usize, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Limit { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for Limit { type Input = (&'a LimitOperator, &'a LogicalPlan); fn into_executor( @@ -49,7 +49,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Limit { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Limit { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Limit { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index b84ae323..4b889bd5 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -23,7 +23,7 @@ use crate::execution::{ use crate::expression::{BinaryOperator, ScalarExpression, TypeCast}; use crate::planner::operator::mark_apply::{MarkApplyKind, MarkApplyOperator, MarkApplyQuantifier}; use crate::planner::{ExecMetaArena, ExprRef, LogicalPlan, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::evaluator::BinaryEvaluatorRef; use crate::types::index::RuntimeIndexProbe; use crate::types::tuple::{SplitTupleRef, TupleImpl}; @@ -59,7 +59,7 @@ enum RightInput { } impl RightInput { - fn reset<'a, T: Transaction + 'a, A: MetaArena + 'a>( + fn reset<'a, T: StorageTransaction + 'a, A: MetaArena + 'a>( &self, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, @@ -76,7 +76,7 @@ impl RightInput { } // Return the first terminal predicate result, or None after exhausting the RHS. - fn scan<'a, T: Transaction + 'a, A: MetaArena + 'a>( + fn scan<'a, T: StorageTransaction + 'a, A: MetaArena + 'a>( &self, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, @@ -104,7 +104,7 @@ pub struct MarkApply<'a> { right_input: Option, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for MarkApply<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for MarkApply<'a> { type Input = (&'a MarkApplyOperator, &'a LogicalPlan, &'a LogicalPlan); fn into_executor( @@ -130,7 +130,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for MarkApply<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for MarkApply<'a> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, @@ -221,7 +221,7 @@ impl<'a> MarkApply<'a> { .transpose() } - fn mark_value( + fn mark_value( &self, mut input: RightInput, arena: &mut ExecArena<'a, T>, @@ -280,7 +280,7 @@ impl<'a> MarkApply<'a> { Ok((*op, *left_expr, *right_expr, evaluator)) } - fn materialize_keys( + fn materialize_keys( &self, root: ExecId, arena: &mut ExecArena<'a, T>, @@ -369,7 +369,7 @@ impl<'a> MarkApply<'a> { }) } - fn scan_quantified_right_input( + fn scan_quantified_right_input( &self, input: &RightInput, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/projection.rs b/src/execution/dql/projection.rs index eadf834b..769d787b 100644 --- a/src/execution/dql/projection.rs +++ b/src/execution/dql/projection.rs @@ -20,7 +20,7 @@ use crate::planner::operator::project::ProjectOperator; use crate::planner::ExprRef; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::value::DataValue; pub struct Projection<'a> { @@ -28,7 +28,7 @@ pub struct Projection<'a> { input: ExecId, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Projection<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for Projection<'a> { type Input = (&'a ProjectOperator, &'a LogicalPlan); fn into_executor( @@ -43,7 +43,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Projection<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Projection<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Projection<'a> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/recursive_cte.rs b/src/execution/dql/recursive_cte.rs index 69deed0e..634309fe 100644 --- a/src/execution/dql/recursive_cte.rs +++ b/src/execution/dql/recursive_cte.rs @@ -25,7 +25,7 @@ use crate::execution::{ use crate::planner::operator::recursive_cte::RecursiveScanOperator; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple::TupleImpl; use std::mem; @@ -209,7 +209,7 @@ enum RecursivePhase { Recursive, } -pub struct RecursiveCte<'a, T: Transaction + 'a> { +pub struct RecursiveCte<'a, T: StorageTransaction + 'a> { recursive_plan: &'a LogicalPlan, anchor_input: ExecId, recursive_arena: ExecArena<'a, T>, @@ -219,7 +219,7 @@ pub struct RecursiveCte<'a, T: Transaction + 'a> { phase: RecursivePhase, } -impl<'a, T: Transaction + 'a> RecursiveCte<'a, T> { +impl<'a, T: StorageTransaction + 'a> RecursiveCte<'a, T> { fn new( anchor_input: ExecId, recursive_plan: &'a LogicalPlan, @@ -259,7 +259,7 @@ impl<'a, T: Transaction + 'a> RecursiveCte<'a, T> { } } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for RecursiveCte<'a, T> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for RecursiveCte<'a, T> { type Input = (&'a LogicalPlan, &'a LogicalPlan); fn into_executor( @@ -280,7 +280,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for RecursiveCte<'a, T> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for RecursiveCte<'a, T> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for RecursiveCte<'a, T> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, @@ -338,7 +338,7 @@ pub struct RecursiveScan { input: RecursiveInput, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for RecursiveScan { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for RecursiveScan { type Input = &'a RecursiveScanOperator; fn into_executor( @@ -353,7 +353,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for RecursiveScan { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for RecursiveScan { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for RecursiveScan { fn reset( &mut self, _arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/scalar_query_init.rs b/src/execution/dql/scalar_query_init.rs index da640862..b5539442 100644 --- a/src/execution/dql/scalar_query_init.rs +++ b/src/execution/dql/scalar_query_init.rs @@ -20,7 +20,7 @@ use crate::expression::ScalarExpression; use crate::planner::operator::scalar_query_init::ScalarQueryInitOperator; use crate::planner::ExecMetaArena; use crate::planner::{ExprRef, LogicalPlan, MetaArena, ScalarQueryRef}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; @@ -43,7 +43,7 @@ pub struct ScalarQueryInit<'a> { } impl<'a> ScalarQueryInit<'a> { - fn evaluate_init( + fn evaluate_init( &self, arena: &mut ExecArena<'a, T>, plan_arena: &mut ExecMetaArena, @@ -61,7 +61,7 @@ impl<'a> ScalarQueryInit<'a> { Ok(()) } - pub(crate) fn build( + pub(crate) fn build( op: &'a ScalarQueryInitOperator, input: ExecId, init_plan: &'a LogicalPlan, @@ -89,7 +89,7 @@ impl<'a> ScalarQueryInit<'a> { } } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ScalarQueryInit<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for ScalarQueryInit<'a> { type Input = ( &'a ScalarQueryInitOperator, &'a LogicalPlan, @@ -108,7 +108,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ScalarQueryInit<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ScalarQueryInit<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for ScalarQueryInit<'a> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/scalar_subquery.rs b/src/execution/dql/scalar_subquery.rs index 24d1e387..80f3aa42 100644 --- a/src/execution/dql/scalar_subquery.rs +++ b/src/execution/dql/scalar_subquery.rs @@ -19,7 +19,7 @@ use crate::execution::{ use crate::planner::operator::scalar_subquery::ScalarSubqueryOperator; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::value::DataValue; pub struct ScalarSubquery { @@ -28,7 +28,7 @@ pub struct ScalarSubquery { returned: bool, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ScalarSubquery { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for ScalarSubquery { type Input = (&'a ScalarSubqueryOperator, &'a LogicalPlan); fn into_executor( @@ -48,7 +48,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ScalarSubquery { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ScalarSubquery { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for ScalarSubquery { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/seq_scan.rs b/src/execution/dql/seq_scan.rs index 4046893d..9c031ed7 100644 --- a/src/execution/dql/seq_scan.rs +++ b/src/execution/dql/seq_scan.rs @@ -16,14 +16,14 @@ use crate::errors::DatabaseError; use crate::execution::{ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor}; use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::{Iter, Transaction, TupleIter}; +use crate::storage::{Iter, StorageTransaction, TupleIter}; -pub(crate) struct SeqScan<'a, T: Transaction + 'a> { +pub(crate) struct SeqScan<'a, T: StorageTransaction + 'a> { op: &'a TableScanOperator, iter: Option>, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for SeqScan<'a, T> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for SeqScan<'a, T> { type Input = &'a TableScanOperator; fn into_executor( @@ -37,7 +37,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for SeqScan<'a, T> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for SeqScan<'a, T> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for SeqScan<'a, T> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/set_membership.rs b/src/execution/dql/set_membership.rs index 6eca9d99..b1d65932 100644 --- a/src/execution/dql/set_membership.rs +++ b/src/execution/dql/set_membership.rs @@ -19,7 +19,7 @@ use crate::execution::{ use crate::planner::operator::set_membership::SetMembershipKind; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple::TupleImpl; use std::collections::HashMap; @@ -31,7 +31,7 @@ pub struct SetMembership { built: bool, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for SetMembership { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for SetMembership { type Input = (SetMembershipKind, &'a LogicalPlan, &'a LogicalPlan); fn into_executor( @@ -53,7 +53,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for SetMembership { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for SetMembership { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for SetMembership { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/show_table.rs b/src/execution/dql/show_table.rs index 4666e6c7..71b8ffc7 100644 --- a/src/execution/dql/show_table.rs +++ b/src/execution/dql/show_table.rs @@ -15,15 +15,15 @@ use crate::errors::DatabaseError; use crate::execution::{ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor}; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::{TableIter, Transaction}; +use crate::storage::{StorageTransaction, TableIter}; use crate::types::value::{DataValue, Utf8Type}; use crate::types::CharLengthUnits; -pub struct ShowTables<'a, T: Transaction + 'a> { +pub struct ShowTables<'a, T: StorageTransaction + 'a> { pub(crate) metas: Option>, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ShowTables<'a, T> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for ShowTables<'a, T> { type Input = (); fn into_executor( @@ -37,7 +37,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ShowTables<'a, T> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ShowTables<'a, T> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for ShowTables<'a, T> { fn reset( &mut self, _arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/show_view.rs b/src/execution/dql/show_view.rs index 2fa98fbd..9f5bcfc3 100644 --- a/src/execution/dql/show_view.rs +++ b/src/execution/dql/show_view.rs @@ -15,15 +15,15 @@ use crate::errors::DatabaseError; use crate::execution::{ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNode, ReadExecutor}; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::{Transaction, ViewIter}; +use crate::storage::{StorageTransaction, ViewIter}; use crate::types::value::{DataValue, Utf8Type}; use crate::types::CharLengthUnits; -pub struct ShowViews<'a, T: Transaction + 'a> { +pub struct ShowViews<'a, T: StorageTransaction + 'a> { pub(crate) metas: Option>, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ShowViews<'a, T> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for ShowViews<'a, T> { type Input = (); fn into_executor( @@ -37,7 +37,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for ShowViews<'a, T> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for ShowViews<'a, T> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for ShowViews<'a, T> { fn reset( &mut self, _arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/sort.rs b/src/execution/dql/sort.rs index f7fe3a7d..389a6cc6 100644 --- a/src/execution/dql/sort.rs +++ b/src/execution/dql/sort.rs @@ -20,7 +20,7 @@ use crate::planner::operator::sort::{SortField, SortOperator}; use crate::planner::LogicalPlan; use crate::planner::ScalarQueryRef; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; use bumpalo::Bump; @@ -161,7 +161,7 @@ pub struct Sort<'a> { input: ExecId, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Sort<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for Sort<'a> { type Input = (&'a SortOperator, &'a LogicalPlan); fn into_executor( @@ -187,7 +187,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Sort<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Sort<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Sort<'a> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/top_k.rs b/src/execution/dql/top_k.rs index 57693917..5490d0d8 100644 --- a/src/execution/dql/top_k.rs +++ b/src/execution/dql/top_k.rs @@ -22,7 +22,7 @@ use crate::planner::operator::top_k::TopKOperator; use crate::planner::{ExecMetaArena, MetaArena}; use crate::planner::{LogicalPlan, ScalarQueryRef}; use crate::storage::table_codec::BumpBytes; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; use bumpalo::Bump; @@ -117,7 +117,7 @@ pub struct TopK<'a> { input: ExecId, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for TopK<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for TopK<'a> { type Input = (&'a TopKOperator, &'a LogicalPlan); fn into_executor( @@ -146,7 +146,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for TopK<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for TopK<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for TopK<'a> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/union.rs b/src/execution/dql/union.rs index 1891dfd6..753c992c 100644 --- a/src/execution/dql/union.rs +++ b/src/execution/dql/union.rs @@ -18,14 +18,14 @@ use crate::execution::{ }; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; pub struct Union { left_input: ExecId, right_input: ExecId, reading_left: bool, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Union { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for Union { type Input = (&'a LogicalPlan, &'a LogicalPlan); fn into_executor( @@ -45,7 +45,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Union { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Union { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Union { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/values.rs b/src/execution/dql/values.rs index 265587e2..e18ff9fa 100644 --- a/src/execution/dql/values.rs +++ b/src/execution/dql/values.rs @@ -17,7 +17,7 @@ use crate::execution::{ExecArena, ExecId, ExecNode, ExecutionContext, ExecutorNo use crate::planner::operator::values::ValuesOperator; use crate::planner::ExprRef; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::value::DataValue; pub struct Values<'a> { @@ -36,7 +36,7 @@ impl<'a> From<&'a ValuesOperator> for Values<'a> { } } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Values<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for Values<'a> { type Input = &'a ValuesOperator; fn into_executor( @@ -51,7 +51,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Values<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Values<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Values<'a> { fn reset( &mut self, _arena: &mut ExecArena<'a, T>, diff --git a/src/execution/dql/window.rs b/src/execution/dql/window.rs index caf7656c..e9e8900b 100644 --- a/src/execution/dql/window.rs +++ b/src/execution/dql/window.rs @@ -21,7 +21,7 @@ use crate::planner::operator::sort::SortField; use crate::planner::operator::window::WindowOperator; use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; @@ -84,7 +84,7 @@ pub struct Window<'a> { input: ExecId, } -impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for Window<'a> { +impl<'a, T: StorageTransaction + 'a> ReadExecutor<'a, T> for Window<'a> { type Input = (&'a WindowOperator, &'a LogicalPlan); fn into_executor( @@ -225,7 +225,7 @@ impl<'a> Window<'a> { } } -impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Window<'a> { +impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for Window<'a> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, diff --git a/src/execution/mod.rs b/src/execution/mod.rs index 907a2060..810dc761 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -82,7 +82,7 @@ use crate::planner::operator::{Operator, PhysicalOption, PlanImpl}; use crate::planner::MetaArena; use crate::planner::{ExecMetaArena, ExprRef, LogicalPlan, PlanKeeper}; use crate::storage::table_codec::TableCodec; -use crate::storage::{StatisticsMetaCache, TableCache, Transaction, ViewCache}; +use crate::storage::{StatisticsMetaCache, StorageTransaction, TableCache, ViewCache}; use crate::types::index::RuntimeIndexProbe; use crate::types::tuple::{ConcatPool, Input, Tuple, TupleId, TupleImpl}; @@ -164,7 +164,7 @@ impl RewriteExpression for ScalarExpression { } } -pub struct Executor<'a, T: Transaction + 'a> { +pub struct Executor<'a, T: StorageTransaction + 'a> { arena: ExecArena<'a, T>, root: ExecId, // Never read: it only keeps the plan that `arena` borrows from alive, and must stay @@ -173,7 +173,7 @@ pub struct Executor<'a, T: Transaction + 'a> { keeper: PlanKeeper<'a>, } -impl<'a, T: Transaction + 'a> Executor<'a, T> { +impl<'a, T: StorageTransaction + 'a> Executor<'a, T> { pub(crate) fn new(arena: ExecArena<'a, T>, root: ExecId, keeper: PlanKeeper<'a>) -> Self { Self { arena, @@ -198,7 +198,7 @@ impl<'a, T: Transaction + 'a> Executor<'a, T> { } #[allow(clippy::large_enum_variant)] -pub(crate) enum ExecNode<'a, T: Transaction + 'a> { +pub(crate) enum ExecNode<'a, T: StorageTransaction + 'a> { AddColumn(AddColumn<'a>), Analyze(Analyze<'a>), ChangeColumn(ChangeColumn<'a>), @@ -250,7 +250,7 @@ pub(crate) enum ExecNode<'a, T: Transaction + 'a> { Window(Window<'a>), } -pub(crate) trait ExecutorNode<'a, T: Transaction + 'a>: Sized { +pub(crate) trait ExecutorNode<'a, T: StorageTransaction + 'a>: Sized { // Reads restore their local state and reset inputs. Writes must not be replayed. fn reset( &mut self, @@ -269,7 +269,7 @@ pub(crate) trait ExecutorNode<'a, T: Transaction + 'a>: Sized { ) -> Result<(), DatabaseError>; } -impl<'a, T: Transaction + 'a> ExecNode<'a, T> { +impl<'a, T: StorageTransaction + 'a> ExecNode<'a, T> { fn reset( &mut self, arena: &mut ExecArena<'a, T>, @@ -567,13 +567,13 @@ impl<'a, T: Transaction + 'a> ExecNode<'a, T> { } } -struct ExecNodes<'a, T: Transaction + 'a> { +struct ExecNodes<'a, T: StorageTransaction + 'a> { items: Vec>>, pos: ExecId, executing: usize, } -impl<'a, T: Transaction + 'a> ExecNodes<'a, T> { +impl<'a, T: StorageTransaction + 'a> ExecNodes<'a, T> { #[cfg(test)] fn seek(&mut self, pos: ExecId) { assert!(pos <= self.items.len(), "node position out of bounds"); @@ -599,7 +599,7 @@ impl<'a, T: Transaction + 'a> ExecNodes<'a, T> { } } -pub(crate) struct ExecArena<'a, T: Transaction + 'a> { +pub(crate) struct ExecArena<'a, T: StorageTransaction + 'a> { nodes: ExecNodes<'a, T>, result: ExecResult, table_codec: TableCodec, @@ -610,7 +610,7 @@ pub(crate) struct ExecArena<'a, T: Transaction + 'a> { recursive_input: Option, } -pub(crate) struct ExecArenaLocalState<'b, 'a, T: Transaction + 'a> { +pub(crate) struct ExecArenaLocalState<'b, 'a, T: StorageTransaction + 'a> { transaction: *mut T, pub(crate) table_codec: &'b mut TableCodec, pub(crate) context: ExecutionContext<'a>, @@ -619,7 +619,7 @@ pub(crate) struct ExecArenaLocalState<'b, 'a, T: Transaction + 'a> { ddl_apply: &'b mut Vec, } -impl<'b, 'a, T: Transaction + 'a> ExecArenaLocalState<'b, 'a, T> { +impl<'b, 'a, T: StorageTransaction + 'a> ExecArenaLocalState<'b, 'a, T> { pub(crate) fn transaction(&self) -> &'a T { unsafe { &*self.transaction } } @@ -655,7 +655,7 @@ impl<'b, 'a, T: Transaction + 'a> ExecArenaLocalState<'b, 'a, T> { } } -impl<'a, T: Transaction + 'a> ExecArena<'a, T> { +impl<'a, T: StorageTransaction + 'a> ExecArena<'a, T> { pub(crate) fn set_statement_stamp(&mut self, stamp: u64) { self.table_codec.set_stamp(stamp); } @@ -678,7 +678,7 @@ impl<'a, T: Transaction + 'a> ExecArena<'a, T> { } } -impl<'a, T: Transaction + 'a> ExecArena<'a, T> { +impl<'a, T: StorageTransaction + 'a> ExecArena<'a, T> { pub(crate) fn init_context(&mut self, context: ExecutionContext<'a>, transaction: &'a T) { if let Some(current) = &self.context { debug_assert!(current.is_same_context(context)); @@ -900,7 +900,7 @@ impl<'a, T: Transaction + 'a> ExecArena<'a, T> { } } -pub(crate) trait ReadExecutor<'a, T: Transaction + 'a>: Sized { +pub(crate) trait ReadExecutor<'a, T: StorageTransaction + 'a>: Sized { type Input; fn into_executor( @@ -912,7 +912,7 @@ pub(crate) trait ReadExecutor<'a, T: Transaction + 'a>: Sized { ) -> ExecId; } -pub(crate) trait WriteExecutor<'a, T: Transaction + 'a>: Sized { +pub(crate) trait WriteExecutor<'a, T: StorageTransaction + 'a>: Sized { type Input; fn into_executor( @@ -932,7 +932,7 @@ pub(crate) fn build_read<'a, T>( transaction: &T, ) -> ExecId where - T: Transaction + 'a, + T: StorageTransaction + 'a, { macro_rules! read { ($executor:ty, $input:expr) => { @@ -1065,7 +1065,7 @@ pub(crate) fn build_write<'a, T>( transaction: &'a T, ) -> ExecId where - T: Transaction + 'a, + T: StorageTransaction + 'a, { arena.init_context(cache, transaction); let transaction_ref: &T = transaction; @@ -1139,12 +1139,12 @@ mod test_utils { ) } - pub(crate) struct TestExecutor<'a, T: Transaction + 'a> { + pub(crate) struct TestExecutor<'a, T: StorageTransaction + 'a> { executor: Executor<'a, T>, plan_arena: ExecMetaArena>, } - impl TestExecutor<'_, T> { + impl TestExecutor<'_, T> { pub(crate) fn next_tuple(&mut self) -> Result, DatabaseError> { self.executor.next_tuple(&mut self.plan_arena) } @@ -1157,7 +1157,7 @@ mod test_utils { transaction: &'a T, ) -> TestExecutor<'a, T> where - T: Transaction + 'a, + T: StorageTransaction + 'a, E: ReadExecutor<'a, T>, { let mut arena = ExecArena::with_capacity(0); @@ -1183,7 +1183,7 @@ mod test_utils { transaction: &'a T, ) -> TestExecutor<'a, T> where - T: Transaction + 'a, + T: StorageTransaction + 'a, E: WriteExecutor<'a, T>, { let mut arena = ExecArena::with_capacity(0); @@ -1201,7 +1201,7 @@ mod test_utils { } } - pub fn try_collect( + pub fn try_collect( executor: TestExecutor<'_, T>, ) -> Result, DatabaseError> { let mut executor = executor; diff --git a/src/expression/mod.rs b/src/expression/mod.rs index 134cd964..a58f2464 100644 --- a/src/expression/mod.rs +++ b/src/expression/mod.rs @@ -1110,7 +1110,7 @@ mod test { use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTables}; use crate::storage::rocksdb::RocksStorage; use crate::storage::rocksdb::RocksTransaction; - use crate::storage::{Storage, Transaction}; + use crate::storage::{Storage, StorageTransaction}; use crate::types::evaluator::{binary_create, cast_create, unary_create}; use crate::types::value::{DataValue, Utf8Type}; use crate::types::CharLengthUnits; diff --git a/src/optimizer/core/cm_sketch.rs b/src/optimizer/core/cm_sketch.rs index 76ee3a7d..32f4f530 100644 --- a/src/optimizer/core/cm_sketch.rs +++ b/src/optimizer/core/cm_sketch.rs @@ -17,7 +17,7 @@ use crate::expression::range_detacher::Range; use crate::planner::MetaArena; use crate::serdes::stable_hash::{StableHasher, CM_SKETCH_HASH_KEYS}; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::value::DataValue; use kite_sql_serde_macros::ReferenceSerialization; use std::borrow::Borrow; @@ -373,7 +373,7 @@ impl ReferenceSerialization for CountMinSketch { Ok(()) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index 2a9cefc7..2fdd21c4 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -701,7 +701,7 @@ mod tests { use crate::planner::operator::sort::SortField; use crate::planner::operator::{Operator, PlanImpl, SortOption}; use crate::planner::{Childrens, LogicalPlan, PlanArena, TableArenaCell}; - use crate::storage::{Storage, Transaction}; + use crate::storage::{Storage, StorageTransaction}; use crate::types::index::IndexLookup; use crate::types::value::DataValue; use std::ops::Bound; diff --git a/src/orm/README.md b/src/orm/README.md index d10e73f1..4041b524 100644 --- a/src/orm/README.md +++ b/src/orm/README.md @@ -88,7 +88,7 @@ let users = database.execute(&plan, [(1, DataValue::Int32(1))])? # Ok::<(), Box>(()) ``` -`DBTransaction::prepare` and `DBTransaction::execute` provide the same +`Transaction::prepare` and `Transaction::execute` provide the same workflow within an explicit transaction. The plan must be executed against the database that created it; DDL and ANALYZE cannot be prepared. diff --git a/src/orm/dml.rs b/src/orm/dml.rs index 4adb5cae..1cb39aa3 100644 --- a/src/orm/dml.rs +++ b/src/orm/dml.rs @@ -71,7 +71,7 @@ impl Database { } } -impl<'a, S: Storage> DBTransaction<'a, S> { +impl<'a, S: Storage> Transaction<'a, S> { /// Refreshes optimizer statistics for the model table inside the current transaction. pub fn analyze(&mut self) -> Result<(), DatabaseError> { orm_analyze::<_, M>(self) diff --git a/src/orm/dql.rs b/src/orm/dql.rs index f90b0509..fdd29195 100644 --- a/src/orm/dql.rs +++ b/src/orm/dql.rs @@ -22,7 +22,7 @@ impl Database { { let transaction = self .storage - .transaction_with_isolation(self.transaction_isolation)?; + .transaction_with_isolation(self.transaction_isolation, true)?; self.state .prepare_plan_with(params, &transaction, |binder, arena| { let mut context = OrmContext { binder, arena }; @@ -44,7 +44,7 @@ impl Database { >, ) -> Result, { - bind_orm_context(self, build) + bind_orm_context(self, false, build) } /// Explains a binder-backed plan built inside a closure. @@ -185,7 +185,7 @@ impl Database { } } -impl<'a, S: Storage> DBTransaction<'a, S> { +impl<'a, S: Storage> Transaction<'a, S> { /// Prepares an ORM-built plan for repeated execution inside this transaction. pub fn prepare( &self, @@ -228,7 +228,7 @@ impl<'a, S: Storage> DBTransaction<'a, S> { >, ) -> Result, { - bind_orm_context(self, build) + bind_orm_context(self, false, build) } /// Explains a binder-backed plan inside the current transaction. diff --git a/src/orm/mod.rs b/src/orm/mod.rs index 81ca0d76..02d8c1bb 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -6,7 +6,7 @@ use crate::binder::{ }; use crate::catalog::{ColumnCatalog, ColumnRef, TableCatalog, TableName}; use crate::db::{ - BindSource, DBTransaction, Database, DatabaseIter, OrmIter, ResultIter, TransactionIter, + BindSource, Database, DatabaseIter, OrmIter, ResultIter, Transaction, TransactionIter, }; use crate::errors::DatabaseError; pub use crate::expression::agg::AggKind; @@ -17,7 +17,7 @@ use crate::planner::operator::join::JoinType; use crate::planner::operator::mark_apply::MarkApplyQuantifier; use crate::planner::operator::sort::SortField; use crate::planner::{ExprRef, LogicalPlan, PlanArena}; -use crate::storage::{Storage, Transaction}; +use crate::storage::{Storage, StorageTransaction}; use crate::types::tuple::{SchemaView, TupleImpl}; use crate::types::value::DataValue; use crate::types::CharLengthUnits; @@ -71,7 +71,7 @@ impl OrmField { } } -/// One row returned by [`Database::describe`] or [`DBTransaction::describe`]. +/// One row returned by [`Database::describe`] or [`Transaction::describe`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DescribeColumn { pub field: String, @@ -222,7 +222,7 @@ impl FieldSort { #[doc(hidden)] pub trait BindOrmScalar<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn bind_scalar( @@ -233,7 +233,7 @@ where impl<'bind, 'parent, 'arena, T, A, M, V> BindOrmScalar<'bind, 'parent, 'arena, T, A> for Field where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn bind_scalar( @@ -246,7 +246,7 @@ where impl<'bind, 'parent, 'arena, T, A> BindOrmScalar<'bind, 'parent, 'arena, T, A> for ScalarExpression where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn bind_scalar( @@ -260,7 +260,7 @@ where impl<'bind, 'parent, 'arena, T, A> BindOrmScalar<'bind, 'parent, 'arena, T, A> for CtxExpression<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn bind_scalar( @@ -274,7 +274,7 @@ where #[doc(hidden)] pub trait BindOrmSort<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn bind_sort<'scope>( @@ -285,7 +285,7 @@ where impl<'bind, 'parent, 'arena, T, A, M, V> BindOrmSort<'bind, 'parent, 'arena, T, A> for Field where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn bind_sort<'scope>( @@ -299,7 +299,7 @@ where impl<'bind, 'parent, 'arena, T, A, M, V> BindOrmSort<'bind, 'parent, 'arena, T, A> for FieldSort where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn bind_sort<'scope>( @@ -315,7 +315,7 @@ where impl<'bind, 'parent, 'arena, T, A> BindOrmSort<'bind, 'parent, 'arena, T, A> for ScalarExpression where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn bind_sort<'scope>( @@ -328,7 +328,7 @@ where impl<'bind, 'parent, 'arena, T, A> BindOrmSort<'bind, 'parent, 'arena, T, A> for SortField where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn bind_sort<'scope>( @@ -342,7 +342,7 @@ where impl<'bind, 'parent, 'arena, T, A> BindOrmSort<'bind, 'parent, 'arena, T, A> for CtxExpression<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn bind_sort<'scope>( @@ -382,7 +382,7 @@ impl WindowSpec { impl<'bind, 'parent, 'arena, T, A> From> for ExprRef where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn from(expr: CtxExpression<'bind, 'parent, 'arena, T, A>) -> Self { @@ -420,7 +420,7 @@ where #[doc(hidden)] pub trait BindOrmScalarList<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn bind_scalar_list( @@ -435,7 +435,7 @@ macro_rules! impl_bind_orm_scalar_list { impl<'bind, 'parent, 'arena, Tx, Args, $($name),+> BindOrmScalarList<'bind, 'parent, 'arena, Tx, Args> for ($($name,)+) where - Tx: Transaction, + Tx: StorageTransaction, Args: AsRef<[(usize, LogicalType)]>, $($name: BindOrmScalar<'bind, 'parent, 'arena, Tx, Args>,)+ { @@ -487,7 +487,7 @@ macro_rules! impl_quantified_subquery_methods { #[allow(clippy::type_complexity)] struct ExprBindScopeHandle<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { binder: NonNull>, @@ -497,7 +497,7 @@ where impl<'bind, 'parent, 'arena, T, A> Clone for ExprBindScopeHandle<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn clone(&self) -> Self { @@ -507,14 +507,14 @@ where impl<'bind, 'parent, 'arena, T, A> Copy for ExprBindScopeHandle<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { } impl<'bind, 'parent, 'arena, T, A> ExprBindScopeHandle<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn new<'ctx>(scope: &ExprBindScope<'ctx, 'bind, 'parent, 'arena, T, A>) -> Self { @@ -691,7 +691,7 @@ where /// internal scope handle is private. pub struct CtxExpression<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { expr: ExprRef, @@ -700,7 +700,7 @@ where impl<'bind, 'parent, 'arena, T, A> CtxExpression<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub fn into_scalar(self) -> ExprRef { @@ -971,7 +971,7 @@ where impl<'bind, 'parent, 'arena, T, A> fmt::Debug for CtxExpression<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -981,7 +981,7 @@ where impl<'bind, 'parent, 'arena, T, A> Clone for CtxExpression<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn clone(&self) -> Self { @@ -994,7 +994,7 @@ where impl<'bind, 'parent, 'arena, T, A> PartialEq for CtxExpression<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn eq(&self, other: &Self) -> bool { @@ -1004,14 +1004,14 @@ where impl<'bind, 'parent, 'arena, T, A> Eq for CtxExpression<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { } impl<'bind, 'parent, 'arena, T, A> Hash for CtxExpression<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn hash(&self, state: &mut H) { @@ -1021,7 +1021,7 @@ where impl<'bind, 'parent, 'arena, T, A> IntoOrmExpression for CtxExpression<'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn into_orm_expression(self) -> OrmExpression { @@ -1029,7 +1029,11 @@ where } } -fn bind_orm_context<'a, E, F>(executor: E, build: F) -> Result +fn bind_orm_context<'a, E, F>( + executor: E, + read_only: bool, + build: F, +) -> Result where E: BindSource<'a>, F: for<'ctx, 'bind, 'parent, 'arena> FnOnce( @@ -1044,7 +1048,7 @@ where ) -> Result, { static EMPTY_BIND_PARAMS: &[(usize, LogicalType)] = &[]; - executor.execute(|state, tx| { + executor.execute(read_only, |state, tx| { state.build_plan(EMPTY_BIND_PARAMS, tx, |binder, arena| { let mut context = OrmContext { binder, arena }; build(&mut context) @@ -1075,12 +1079,12 @@ where /// Binder-backed ORM query context. /// -/// This context is created by [`Database::bind`] or [`DBTransaction::bind`]. Query construction inside +/// This context is created by [`Database::bind`] or [`Transaction::bind`]. Query construction inside /// the closure binds directly into [`ScalarExpression`] and [`LogicalPlan`] /// values; it does not build an ORM expression tree first. pub struct OrmContext<'ctx, 'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { binder: &'ctx mut Binder<'bind, 'parent, T, A>, @@ -1090,7 +1094,7 @@ where /// Narrow expression binding scope borrowed from an [`OrmContext`]. pub struct ExprBindScope<'ctx, 'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { binder: &'ctx mut Binder<'bind, 'parent, T, A>, @@ -1099,7 +1103,7 @@ where pub struct UpdateBindScope<'ctx, 'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { binder: &'ctx mut Binder<'bind, 'parent, T, A>, @@ -1110,7 +1114,7 @@ where impl<'ctx, 'bind, 'parent, 'arena, T, A> OrmContext<'ctx, 'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub fn from<'scope, M: Model>( @@ -1331,7 +1335,7 @@ where impl<'ctx, 'bind, 'parent, 'arena, T, A> ExprBindScope<'ctx, 'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { fn handle(&self) -> ExprBindScopeHandle<'bind, 'parent, 'arena, T, A> { @@ -1859,7 +1863,7 @@ where impl<'ctx, 'bind, 'parent, 'arena, T, A> UpdateBindScope<'ctx, 'bind, 'parent, 'arena, T, A> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { pub fn set_value(&mut self, field: Field, value: D) -> Result<(), DatabaseError> @@ -1955,7 +1959,7 @@ where impl<'scope_ctx, 'bind, 'parent, 'arena, T, A, M> BindPlanFrom<'scope_ctx, 'bind, 'parent, 'arena, T, A, M> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, M: Model, { @@ -2408,7 +2412,7 @@ where impl<'scope_ctx, 'bind, 'parent, 'arena, T, A, M> BindPlanSelectList<'scope_ctx, 'bind, 'parent, 'arena, T, A, M> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, M: Model, { @@ -2577,7 +2581,7 @@ pub trait Projection: FromQueryRow { relation: &str, ) -> Result, DatabaseError> where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>; } @@ -2595,7 +2599,7 @@ fn bind_orm_source<'bind, 'parent, 'arena, T, A>( arena: &mut PlanArena<'arena>, ) -> Result where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { let alias = orm_table_alias(&source); @@ -2609,7 +2613,7 @@ fn bind_orm_target_column<'bind, 'parent, 'arena, T, A>( arena: &mut PlanArena<'arena>, ) -> Result where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { match binder.bind_column_ref_by_name(None, column_name, Some(source_name), arena)? { @@ -2627,7 +2631,7 @@ fn bind_orm_insert_plan<'bind, 'parent, 'arena, T, A>( arena: &mut PlanArena<'arena>, ) -> Result where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, { let table_name: TableName = table_name.into(); @@ -2695,7 +2699,7 @@ fn bind_orm_insert_model<'bind, 'parent, 'arena, T, A, M>( arena: &mut PlanArena<'arena>, ) -> Result where - T: Transaction, + T: StorageTransaction, A: AsRef<[(usize, LogicalType)]>, M: Model, { @@ -3399,7 +3403,7 @@ fn extract_projected_tuple(tuple: &mut TupleImpl) -> Result, M: Model>(executor: E) -> Result<(), DatabaseError> { executor - .execute(|state, tx| { + .execute(false, |state, tx| { state.build_plan(&[], tx, |binder, arena| { binder.bind_analyze(M::table_name().into(), arena) }) @@ -3413,7 +3417,7 @@ fn orm_insert<'a, E: BindSource<'a>, M: Model>( ) -> Result<(), DatabaseError> { let params = model.params(); executor - .execute(|state, tx| { + .execute(false, |state, tx| { state.build_plan(&[], tx, |binder, arena| { bind_orm_insert_model::<_, _, M>(binder, params, arena) }) @@ -3427,7 +3431,7 @@ fn orm_get<'a, E: BindSource<'a>, M: Model>( ) -> Result, DatabaseError> { let primary_key = M::primary_key_field(); let key = key.to_data_value(); - extract_optional_model(bind_orm_context(executor, |ctx| { + extract_optional_model(bind_orm_context(executor, true, |ctx| { let plan: LogicalPlan = ctx .from::()? .filter(|expr| { @@ -3445,7 +3449,7 @@ fn orm_get<'a, E: BindSource<'a>, M: Model>( fn orm_list<'a, E: BindSource<'a>, M: Model>( executor: E, ) -> Result, DatabaseError> { - Ok(bind_orm_context(executor, |ctx| { + Ok(bind_orm_context(executor, true, |ctx| { let plan: LogicalPlan = ctx.from::()?.finish()?; Ok(plan) })? @@ -3886,7 +3890,7 @@ mod tests { assert_eq!(rows.len(), 1); assert_eq!(rows[0].name, expected); } - let mut tx = database.new_transaction()?; + let mut tx = database.new_transaction(false)?; let tx_plan = tx.prepare(&types, |ctx| { ctx.from::()? .filter(|e| e.column(OrmUnitUser::id())?.eq(e.param(1)?))? @@ -3962,7 +3966,7 @@ mod tests { .iter() .any(|column| column.field == "name")); - let mut tx = database.new_transaction()?; + let mut tx = database.new_transaction(false)?; tx.insert(&OrmUnitUser { id: 4, name: "Dora".to_string(), diff --git a/src/planner/mod.rs b/src/planner/mod.rs index ce72e0fd..637569c5 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -548,7 +548,7 @@ impl crate::serdes::ReferenceSerialization for LogicalPlan { ) } - fn decode( + fn decode( reader: &mut R, context: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &crate::serdes::ReferenceTables, diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index be1fd133..16198a88 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -636,7 +636,70 @@ pub(crate) enum ColumnVisitRoot<'a> { Plan(&'a LogicalPlan), } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CommandType { + DQL, + DML, + DDL, + Analyze, +} + +impl CommandType { + /// Only queries leave storage untouched. + pub fn is_read_only(self) -> bool { + matches!(self, CommandType::DQL) + } +} + impl Operator { + /// Classifies a plan by its root, matching `command_type` of SQL statements. + pub fn command_type(&self) -> CommandType { + match self { + Operator::Dummy + | Operator::Aggregate(_) + | Operator::ScalarQueryInit(_) + | Operator::MarkApply(_) + | Operator::Filter(_) + | Operator::Join(_) + | Operator::Project(_) + | Operator::ScalarSubquery(_) + | Operator::TableScan(_) + | Operator::FunctionScan(_) + | Operator::Sort(_) + | Operator::Limit(_) + | Operator::TopK(_) + | Operator::Values(_) + | Operator::ShowTable + | Operator::ShowView + | Operator::Explain + | Operator::Describe(_) + | Operator::SetMembership(_) + | Operator::Union(_) + | Operator::RecursiveCte(_) + | Operator::RecursiveScan(_) + | Operator::IndexProbeJoin(_) + | Operator::Window(_) => CommandType::DQL, + #[cfg(feature = "copy")] + Operator::CopyToFile(_) => CommandType::DQL, + Operator::Insert(_) + | Operator::Update(_) + | Operator::Delete(_) + | Operator::Truncate(_) => CommandType::DML, + #[cfg(feature = "copy")] + Operator::CopyFromFile(_) => CommandType::DML, + Operator::Analyze(_) => CommandType::Analyze, + Operator::AddColumn(_) + | Operator::ChangeColumn(_) + | Operator::DropColumn(_) + | Operator::CreateTable(_) + | Operator::CreateIndex(_) + | Operator::CreateView(_) + | Operator::DropTable(_) + | Operator::DropView(_) + | Operator::DropIndex(_) => CommandType::DDL, + } + } + pub fn visit_referenced_columns( &self, arena: &A, diff --git a/src/planner/plan_keeper.rs b/src/planner/plan_keeper.rs index 294f69e6..fcd1309c 100644 --- a/src/planner/plan_keeper.rs +++ b/src/planner/plan_keeper.rs @@ -20,6 +20,15 @@ pub(crate) enum PlanInput<'p> { Borrowed(&'p LogicalPlan), } +impl PlanInput<'_> { + pub(crate) fn root(&self) -> &LogicalPlan { + match self { + PlanInput::Owned(plan) => plan, + PlanInput::Borrowed(plan) => plan, + } + } +} + impl From for PlanInput<'_> { fn from(plan: LogicalPlan) -> Self { PlanInput::Owned(plan) diff --git a/src/planner/scalar_query_ref.rs b/src/planner/scalar_query_ref.rs index 51e13769..53550df7 100644 --- a/src/planner/scalar_query_ref.rs +++ b/src/planner/scalar_query_ref.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; /// A definition's position in an Arena namespace, independent of execution-local slots. @@ -45,7 +45,7 @@ impl ReferenceSerialization for ScalarQueryRef { self.is_outer_value.encode(writer, direct, tables, arena) } - fn decode( + fn decode( reader: &mut R, context: Option<&ReferenceDecodeContext<'_, T>>, tables: &ReferenceTables, diff --git a/src/serdes/boolean.rs b/src/serdes/boolean.rs index 7aaa6daf..244a81fe 100644 --- a/src/serdes/boolean.rs +++ b/src/serdes/boolean.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; impl ReferenceSerialization for bool { @@ -29,7 +29,7 @@ impl ReferenceSerialization for bool { if *self { 1u8 } else { 0u8 }.encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/bound.rs b/src/serdes/bound.rs index 62a8c6d8..833cf004 100644 --- a/src/serdes/bound.rs +++ b/src/serdes/bound.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; use std::ops::Bound; @@ -49,7 +49,7 @@ where Ok(()) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/btree_map.rs b/src/serdes/btree_map.rs index 5a51d497..cb66922e 100644 --- a/src/serdes/btree_map.rs +++ b/src/serdes/btree_map.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::collections::BTreeMap; use std::io::{Read, Write}; @@ -40,7 +40,7 @@ where Ok(()) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/char.rs b/src/serdes/char.rs index a97511d8..8d1fa46f 100644 --- a/src/serdes/char.rs +++ b/src/serdes/char.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; impl ReferenceSerialization for char { @@ -38,7 +38,7 @@ impl ReferenceSerialization for char { Ok(writer.write_all(&buf)?) } - fn decode( + fn decode( reader: &mut R, _: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, _: &ReferenceTables, diff --git a/src/serdes/char_length_units.rs b/src/serdes/char_length_units.rs index b5b7b19a..9aeb46d5 100644 --- a/src/serdes/char_length_units.rs +++ b/src/serdes/char_length_units.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::CharLengthUnits; use std::io::{Read, Write}; @@ -36,7 +36,7 @@ impl ReferenceSerialization for CharLengthUnits { Ok(()) } - fn decode( + fn decode( reader: &mut R, _: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, _: &ReferenceTables, diff --git a/src/serdes/column.rs b/src/serdes/column.rs index 071c5522..d6b2e571 100644 --- a/src/serdes/column.rs +++ b/src/serdes/column.rs @@ -16,7 +16,7 @@ use crate::catalog::{ColumnCatalog, ColumnRef, ColumnRelation}; use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::ColumnId; use std::io::{Read, Write}; @@ -33,7 +33,7 @@ impl ReferenceSerialization for ColumnRef { .encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, drive: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, @@ -94,7 +94,7 @@ impl ReferenceSerialization for ColumnRelation { Ok(()) } - fn decode( + fn decode( reader: &mut R, drive: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, @@ -141,7 +141,7 @@ pub(crate) mod test { use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTables}; use crate::storage::rocksdb::RocksStorage; use crate::storage::rocksdb::RocksTransaction; - use crate::storage::{Storage, Transaction}; + use crate::storage::{Storage, StorageTransaction}; use crate::types::value::DataValue; use crate::types::LogicalType; use std::io::{Cursor, Seek, SeekFrom}; diff --git a/src/serdes/data_value.rs b/src/serdes/data_value.rs index efbf94ba..223b1a33 100644 --- a/src/serdes/data_value.rs +++ b/src/serdes/data_value.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::value::DataValue; use crate::types::value::Utf8Type; use crate::types::CharLengthUnits; @@ -65,7 +65,7 @@ impl ReferenceSerialization for Utf8Type { } } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, @@ -275,7 +275,7 @@ impl ReferenceSerialization for DataValue { self.encode_reference_value(writer) } - fn decode( + fn decode( reader: &mut R, _: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, _: &ReferenceTables, diff --git a/src/serdes/evaluator.rs b/src/serdes/evaluator.rs index 4edd37ff..742dce8d 100644 --- a/src/serdes/evaluator.rs +++ b/src/serdes/evaluator.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::evaluator::{ BinaryEvaluatorParams, BinaryEvaluatorRef, CastEvaluatorParams, CastEvaluatorRef, UnaryEvaluatorRef, @@ -39,7 +39,7 @@ impl ReferenceSerialization for BinaryEvaluatorParams { } } - fn decode( + fn decode( reader: &mut R, context: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, @@ -99,7 +99,7 @@ impl ReferenceSerialization for CastEvaluatorParams { } } - fn decode( + fn decode( reader: &mut R, context: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, @@ -166,7 +166,7 @@ impl ReferenceSerialization for UnaryEvaluatorRef { self.pos.encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, context: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, @@ -195,7 +195,7 @@ impl ReferenceSerialization for BinaryEvaluatorRef { .encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, context: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, @@ -222,7 +222,7 @@ impl ReferenceSerialization for CastEvaluatorRef { .encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, context: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/expression.rs b/src/serdes/expression.rs index c4ead6d9..2aeda320 100644 --- a/src/serdes/expression.rs +++ b/src/serdes/expression.rs @@ -16,7 +16,7 @@ use crate::errors::DatabaseError; use crate::expression::ScalarExpression; use crate::planner::{ExprRef, MetaArena}; use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; impl ReferenceSerialization for ExprRef { @@ -32,7 +32,7 @@ impl ReferenceSerialization for ExprRef { .encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, context: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/function.rs b/src/serdes/function.rs index d262a9b6..607e2aab 100644 --- a/src/serdes/function.rs +++ b/src/serdes/function.rs @@ -18,7 +18,7 @@ use crate::expression::function::table::ArcTableFunctionImpl; use crate::expression::function::FunctionSummary; use crate::planner::MetaArena; use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; impl ReferenceSerialization for ArcScalarFunctionImpl { @@ -33,7 +33,7 @@ impl ReferenceSerialization for ArcScalarFunctionImpl { .encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, context: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, @@ -69,7 +69,7 @@ impl ReferenceSerialization for ArcTableFunctionImpl { .encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, context: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/hasher.rs b/src/serdes/hasher.rs index 737fbbfa..ea2ef0d2 100644 --- a/src/serdes/hasher.rs +++ b/src/serdes/hasher.rs @@ -16,7 +16,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::stable_hash::StableHasher; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; impl ReferenceSerialization for StableHasher { fn encode( @@ -31,7 +31,7 @@ impl ReferenceSerialization for StableHasher { key1.encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/index.rs b/src/serdes/index.rs index 9aef83a9..dda12f95 100644 --- a/src/serdes/index.rs +++ b/src/serdes/index.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use crate::types::index::{IndexMeta, IndexMetaRef}; use std::io::{Read, Write}; @@ -32,7 +32,7 @@ impl ReferenceSerialization for IndexMetaRef { .encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, drive: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/mod.rs b/src/serdes/mod.rs index 242cce54..e2092b6b 100644 --- a/src/serdes/mod.rs +++ b/src/serdes/mod.rs @@ -41,7 +41,7 @@ use crate::catalog::TableName; use crate::db::{ScalaFunctions, TableFunctions}; use crate::errors::DatabaseError; use crate::planner::MetaArena; -use crate::storage::{TableCache, Transaction}; +use crate::storage::{StorageTransaction, TableCache}; use std::io; use std::io::{Read, Write}; @@ -54,7 +54,7 @@ pub trait ReferenceSerialization { arena: &A, ) -> Result<(), DatabaseError>; - fn decode( + fn decode( reader: &mut R, context: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, @@ -64,13 +64,13 @@ pub trait ReferenceSerialization { Self: Sized; } -pub struct ReferenceDecodeContext<'a, T: Transaction> { +pub struct ReferenceDecodeContext<'a, T: StorageTransaction> { drive: Option<(&'a T, &'a TableCache)>, scala_functions: Option<&'a ScalaFunctions>, table_functions: Option<&'a TableFunctions>, } -impl<'a, T: Transaction> ReferenceDecodeContext<'a, T> { +impl<'a, T: StorageTransaction> ReferenceDecodeContext<'a, T> { pub fn new(drive: Option<(&'a T, &'a TableCache)>) -> Self { Self { drive, diff --git a/src/serdes/num.rs b/src/serdes/num.rs index 293bc196..df66d6bb 100644 --- a/src/serdes/num.rs +++ b/src/serdes/num.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use ordered_float::OrderedFloat; use std::io::Read; use std::io::Write; @@ -37,7 +37,7 @@ macro_rules! implement_num_serialization { Ok(()) } - fn decode( + fn decode( reader: &mut R, _: Option<&$crate::serdes::ReferenceDecodeContext<'_, T>>, _: &ReferenceTables, @@ -75,7 +75,7 @@ impl ReferenceSerialization for OrderedFloat { self.0.encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, @@ -96,7 +96,7 @@ impl ReferenceSerialization for usize { (*self as u32).encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/option.rs b/src/serdes/option.rs index 82e2427f..26770c6c 100644 --- a/src/serdes/option.rs +++ b/src/serdes/option.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; impl ReferenceSerialization for Option @@ -40,7 +40,7 @@ where Ok(()) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/pair.rs b/src/serdes/pair.rs index 3fd86c02..9b8b2b99 100644 --- a/src/serdes/pair.rs +++ b/src/serdes/pair.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; impl ReferenceSerialization for (A, B) @@ -37,7 +37,7 @@ where Ok(()) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/path_buf.rs b/src/serdes/path_buf.rs index 072dac6f..eda11243 100644 --- a/src/serdes/path_buf.rs +++ b/src/serdes/path_buf.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::path::PathBuf; use std::str::FromStr; @@ -33,7 +33,7 @@ impl ReferenceSerialization for PathBuf { .encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/phantom.rs b/src/serdes/phantom.rs index 33e01b8e..980a0acd 100644 --- a/src/serdes/phantom.rs +++ b/src/serdes/phantom.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; use std::marker::PhantomData; @@ -30,7 +30,7 @@ impl ReferenceSerialization for PhantomData { Ok(()) } - fn decode( + fn decode( _: &mut R, _: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, _: &ReferenceTables, diff --git a/src/serdes/physical_option.rs b/src/serdes/physical_option.rs index 503bdcf1..fd887ca1 100644 --- a/src/serdes/physical_option.rs +++ b/src/serdes/physical_option.rs @@ -16,7 +16,7 @@ use crate::errors::DatabaseError; use crate::planner::operator::{PhysicalOption, PlanImpl, RowCountOption, SortOption}; use crate::planner::MetaArena; use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; impl ReferenceSerialization for RowCountOption { @@ -38,7 +38,7 @@ impl ReferenceSerialization for RowCountOption { Ok(()) } - fn decode( + fn decode( reader: &mut R, context: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, @@ -74,7 +74,7 @@ impl ReferenceSerialization for PhysicalOption { .encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, context: Option<&ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/ptr.rs b/src/serdes/ptr.rs index e5301158..6837f517 100644 --- a/src/serdes/ptr.rs +++ b/src/serdes/ptr.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::serdes::DatabaseError; -use crate::serdes::Transaction; +use crate::serdes::StorageTransaction; use crate::serdes::{ReferenceSerialization, ReferenceTables}; use std::io::{Read, Write}; use std::sync::Arc; @@ -36,7 +36,7 @@ macro_rules! implement_ptr_serialization { .encode(writer, is_direct, reference_tables, arena) } - fn decode( + fn decode( reader: &mut R, drive: Option<&$crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/slice.rs b/src/serdes/slice.rs index 1d4f567b..488c1bf1 100644 --- a/src/serdes/slice.rs +++ b/src/serdes/slice.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; impl ReferenceSerialization for [V; 2] @@ -35,7 +35,7 @@ where Ok(()) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/string.rs b/src/serdes/string.rs index 0110b277..ee3cd754 100644 --- a/src/serdes/string.rs +++ b/src/serdes/string.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; use std::sync::Arc; @@ -34,7 +34,7 @@ impl ReferenceSerialization for String { Ok(()) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, @@ -63,7 +63,7 @@ impl ReferenceSerialization for Arc { Ok(()) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/serdes/trim.rs b/src/serdes/trim.rs index afbb42e1..f966dcd0 100644 --- a/src/serdes/trim.rs +++ b/src/serdes/trim.rs @@ -16,7 +16,7 @@ use crate::errors::DatabaseError; use crate::expression::TrimWhereField; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; impl ReferenceSerialization for TrimWhereField { @@ -37,7 +37,7 @@ impl ReferenceSerialization for TrimWhereField { Ok(()) } - fn decode( + fn decode( reader: &mut R, _: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, _: &ReferenceTables, diff --git a/src/serdes/vec.rs b/src/serdes/vec.rs index 5cbabd6c..c704dc6c 100644 --- a/src/serdes/vec.rs +++ b/src/serdes/vec.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; -use crate::storage::Transaction; +use crate::storage::StorageTransaction; use std::io::{Read, Write}; impl ReferenceSerialization for Vec @@ -37,7 +37,7 @@ where Ok(()) } - fn decode( + fn decode( reader: &mut R, drive: Option<&crate::serdes::ReferenceDecodeContext<'_, T>>, reference_tables: &ReferenceTables, diff --git a/src/storage/lmdb.rs b/src/storage/lmdb.rs index 86162b54..935284d9 100644 --- a/src/storage/lmdb.rs +++ b/src/storage/lmdb.rs @@ -16,11 +16,11 @@ use crate::errors::DatabaseError; use crate::storage::table_codec::Bytes; use crate::storage::{ bounds_contain, bytes_bound_as_slice, owned_bound, InnerIter, KeyValueRef, Storage, - Transaction, TransactionIsolationLevel, + StorageTransaction, TransactionIsolationLevel, }; use lmdb::{ - Cursor, Database, DatabaseFlags, Environment, EnvironmentFlags, RoCursor, RwTransaction, - Transaction as _, WriteFlags, + Cursor, Database, DatabaseFlags, Environment, EnvironmentFlags, RoCursor, RoTransaction, + RwTransaction, Transaction as _, WriteFlags, }; use std::collections::Bound; use std::fmt::{self, Display, Formatter}; @@ -99,7 +99,9 @@ impl LmdbStorage { let mut builder = Environment::new(); builder.set_map_size(config.map_size); - builder.set_flags(config.flags); + // Read transactions may outlive the statement call (result iterators) and + // several may be open on one thread, so they must not be bound to TLS slots. + builder.set_flags(config.flags | EnvironmentFlags::NO_TLS); if let Some(max_readers) = config.max_readers { builder.set_max_readers(max_readers); } @@ -128,10 +130,14 @@ impl Storage for LmdbStorage { fn transaction_with_isolation( &self, isolation: TransactionIsolationLevel, + read_only: bool, ) -> Result, DatabaseError> { self.validate_transaction_isolation(isolation)?; - let tx = self.env.begin_rw_txn()?; - + let tx = if read_only { + LmdbTxn::Ro(self.env.begin_ro_txn()?) + } else { + LmdbTxn::Rw(self.env.begin_rw_txn()?) + }; Ok(LmdbTransaction { tx, db: self.db, @@ -162,11 +168,49 @@ impl Storage for LmdbStorage { } pub struct LmdbTransaction<'env> { - tx: RwTransaction<'env>, + tx: LmdbTxn<'env>, db: Database, statements: u64, } +/// Read transactions do not take LMDB's single-writer lock. +enum LmdbTxn<'env> { + Ro(RoTransaction<'env>), + Rw(RwTransaction<'env>), +} + +impl<'env> LmdbTxn<'env> { + fn raw(&self) -> *mut lmdb_sys::MDB_txn { + match self { + LmdbTxn::Ro(tx) => tx.txn(), + LmdbTxn::Rw(tx) => tx.txn(), + } + } + + fn get(&self, db: Database, key: &[u8]) -> Result<&[u8], lmdb::Error> { + match self { + LmdbTxn::Ro(tx) => tx.get(db, &key), + LmdbTxn::Rw(tx) => tx.get(db, &key), + } + } + + fn open_ro_cursor(&self, db: Database) -> Result, lmdb::Error> { + match self { + LmdbTxn::Ro(tx) => tx.open_ro_cursor(db), + LmdbTxn::Rw(tx) => tx.open_ro_cursor(db), + } + } + + fn writable(&mut self) -> Result<&mut RwTransaction<'env>, DatabaseError> { + match self { + LmdbTxn::Ro(_) => Err(DatabaseError::UnsupportedStmt( + "cannot write in a read-only transaction".into(), + )), + LmdbTxn::Rw(tx) => Ok(tx), + } + } +} + pub struct LmdbIter<'txn> { cursor: RoCursor<'txn>, scan: Scan, @@ -276,7 +320,7 @@ fn cursor_get<'txn, C: Cursor<'txn>>( } } -impl Transaction for LmdbTransaction<'_> { +impl StorageTransaction for LmdbTransaction<'_> { type BorrowedBytes<'a> = &'a [u8] where @@ -300,7 +344,7 @@ impl Transaction for LmdbTransaction<'_> { const STATEMENT_BITS: u32 = 20; self.statements += 1; // SAFETY: `self.tx` is a live transaction handle owned by this value. - let txn_id = unsafe { lmdb_sys::mdb_txn_id(self.tx.txn()) } as u64; + let txn_id = unsafe { lmdb_sys::mdb_txn_id(self.tx.raw()) } as u64; if self.statements >= 1 << STATEMENT_BITS || txn_id >= 1 << (u64::BITS - STATEMENT_BITS) { return Err(DatabaseError::InvalidValue( "statement stamps exhausted".into(), @@ -313,7 +357,7 @@ impl Transaction for LmdbTransaction<'_> { &'a self, key: &[u8], ) -> Result>, DatabaseError> { - match self.tx.get(self.db, &key) { + match self.tx.get(self.db, key) { Ok(value) => Ok(Some(value)), Err(lmdb::Error::NotFound) => Ok(None), Err(err) => Err(err.into()), @@ -338,12 +382,13 @@ impl Transaction for LmdbTransaction<'_> { fn set(&mut self, key: &[u8], value: &[u8]) -> Result<(), DatabaseError> { self.tx + .writable()? .put(self.db, &key, &value, lmdb::WriteFlags::empty())?; Ok(()) } fn remove(&mut self, key: &[u8]) -> Result<(), DatabaseError> { - match self.tx.del(self.db, &key, None) { + match self.tx.writable()?.del(self.db, &key, None) { Ok(()) | Err(lmdb::Error::NotFound) => Ok(()), Err(err) => Err(err.into()), } @@ -372,7 +417,7 @@ impl Transaction for LmdbTransaction<'_> { } fn remove_range(&mut self, min: Bound<&[u8]>, max: Bound<&[u8]>) -> Result<(), DatabaseError> { - let mut cursor = self.tx.open_rw_cursor(self.db)?; + let mut cursor = self.tx.writable()?.open_rw_cursor(self.db)?; let mut scan = Scan::new(min, max, false); while scan.next(&cursor)?.is_some() { cursor.del(WriteFlags::empty())?; @@ -381,7 +426,10 @@ impl Transaction for LmdbTransaction<'_> { } fn commit(self) -> Result<(), DatabaseError> { - self.tx.commit()?; + match self.tx { + LmdbTxn::Ro(tx) => tx.commit()?, + LmdbTxn::Rw(tx) => tx.commit()?, + } Ok(()) } } @@ -443,6 +491,60 @@ mod tests { crate::storage::check_explicit_transaction_does_not_rescan_own_writes(db).unwrap(); } + #[test] + fn read_transactions_skip_the_writer_lock() { + use crate::storage::{Storage, StorageTransaction}; + let temp_dir = TempDir::new().expect("unable to create temporary working directory"); + let storage = LmdbStorage::new(temp_dir.path().join("kite_sql.lmdb")).unwrap(); + let isolation = storage.default_transaction_isolation(); + + // A writer holds LMDB's single write lock; readers still open on the same thread. + let mut writer = storage.transaction().unwrap(); + writer.set(b"k", b"v").unwrap(); + let mut reader = storage.transaction_with_isolation(isolation, true).unwrap(); + let second = storage.transaction_with_isolation(isolation, true).unwrap(); + assert!(reader.get_borrowed(b"k").unwrap().is_none()); + assert!(second.get_borrowed(b"k").unwrap().is_none()); + assert!(reader.set(b"k", b"x").is_err()); + assert!(reader.remove(b"k").is_err()); + writer.commit().unwrap(); + drop((reader, second)); + + let reader = storage.transaction_with_isolation(isolation, true).unwrap(); + assert_eq!(reader.get_borrowed(b"k").unwrap(), Some(&b"v"[..])); + } + + #[test] + fn read_statements_and_plans_use_read_transactions() { + let temp_dir = TempDir::new().expect("unable to create temporary working directory"); + let mut db = DataBaseBuilder::path(temp_dir.path()).build_lmdb().unwrap(); + db.ddl("create table t (a int primary key)").unwrap(); + db.run("insert into t values (1)").unwrap().done().unwrap(); + + // An open write transaction does not block reads. + let mut writer = db.new_transaction(false).unwrap(); + writer + .run("insert into t values (2)") + .unwrap() + .done() + .unwrap(); + let select = db.prepare_sql("select count(*) from t", &[]).unwrap(); + let insert = db.prepare_sql("insert into t values (3)", &[]).unwrap(); + assert!(select.read_only && !insert.read_only); + let mut first = db.run("select a from t").unwrap(); + let mut second = db.execute(&select, []).unwrap(); + let count = second.next_tuple(|_, t| t[0].to_string()).unwrap(); + assert_eq!(count.as_deref(), Some("1")); + assert!(first.next_tuple(|_, _| ()).unwrap().is_some()); + // An explicit read-only transaction reads beside the writer and rejects writes. + let mut reader = db.new_transaction(true).unwrap(); + assert!(reader.run("select a from t").unwrap().done().is_ok()); + assert!(reader.run("insert into t values (4)").is_err()); + assert!(reader.execute(&insert, []).is_err()); + drop((first, second, reader)); + writer.commit().unwrap(); + } + #[test] fn build_with_lmdb_storage() { let temp_dir = TempDir::new().expect("unable to create temporary working directory"); diff --git a/src/storage/memory.rs b/src/storage/memory.rs index 5612cc63..fde58037 100644 --- a/src/storage/memory.rs +++ b/src/storage/memory.rs @@ -15,7 +15,7 @@ use crate::errors::DatabaseError; use crate::storage::table_codec::Bytes; use crate::storage::{ - bytes_bound_as_slice, owned_bound, EmptyStorageMetrics, InnerIter, Storage, Transaction, + bytes_bound_as_slice, owned_bound, EmptyStorageMetrics, InnerIter, Storage, StorageTransaction, TransactionIsolationLevel, }; use std::cell::{Cell, Ref, RefCell}; @@ -45,6 +45,7 @@ impl Storage for MemoryStorage { fn transaction_with_isolation( &self, isolation: TransactionIsolationLevel, + _read_only: bool, ) -> Result, DatabaseError> { self.validate_transaction_isolation(isolation)?; Ok(MemoryTransaction { @@ -147,7 +148,7 @@ impl InnerIter for MemoryIter<'_> { } } -impl Transaction for MemoryTransaction { +impl StorageTransaction for MemoryTransaction { type BorrowedBytes<'a> = MemoryValue<'a> where diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 5f7212f5..92dfe441 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -121,17 +121,18 @@ impl Display for EmptyStorageMetrics { pub trait Storage: Clone { type Metrics: Display; - type TransactionType<'a>: Transaction + type TransactionType<'a>: StorageTransaction where Self: 'a; fn transaction(&self) -> Result, DatabaseError> { - self.transaction_with_isolation(self.default_transaction_isolation()) + self.transaction_with_isolation(self.default_transaction_isolation(), false) } fn transaction_with_isolation( &self, isolation: TransactionIsolationLevel, + read_only: bool, ) -> Result, DatabaseError>; fn default_transaction_isolation(&self) -> TransactionIsolationLevel { @@ -165,7 +166,7 @@ pub trait CheckpointableStorage: Storage { /// Optional bounds of the reader, of the form (offset, limit). pub(crate) type Bounds = (Option, Option); -pub trait Transaction: Sized { +pub trait StorageTransaction: Sized { type BorrowedBytes<'a>: AsRef<[u8]> where Self: 'a; @@ -1359,7 +1360,7 @@ fn encode_bound<'a>( bound: &Bound, is_upper: bool, buffer: &'a mut Bytes, - params: &IndexImplParams<'_, impl Transaction>, + params: &IndexImplParams<'_, impl StorageTransaction>, inner: &IndexImplEnum, ) -> Result, DatabaseError> { match bound { @@ -1375,7 +1376,7 @@ fn encode_bound<'a>( } } -trait IndexImpl { +trait IndexImpl { fn index_lookup_into( &self, table_codec: &mut TableCodec, @@ -1455,7 +1456,7 @@ impl TupleMapping { } } -struct IndexImplParams<'a, T: Transaction> { +struct IndexImplParams<'a, T: StorageTransaction> { index_meta: IndexMetaRef, meta_arena: &'a dyn MetaArena, table_name: TableName, @@ -1468,7 +1469,7 @@ struct IndexImplParams<'a, T: Transaction> { reverse: bool, } -impl IndexImplParams<'_, T> { +impl IndexImplParams<'_, T> { #[inline] pub(crate) fn index_meta(&self) -> &IndexMeta { self.meta_arena.index(self.index_meta) @@ -1502,18 +1503,18 @@ impl IndexImplParams<'_, T> { } } -enum IndexResult<'a, T: Transaction + 'a> { +enum IndexResult<'a, T: StorageTransaction + 'a> { Hit, Miss, Scope(RangeIter<'a, T>), } -pub enum RangeIter<'a, T: Transaction + 'a> { +pub enum RangeIter<'a, T: StorageTransaction + 'a> { Forward(T::IterType<'a>), Reverse(T::RevIterType<'a>), } -impl<'a, T: Transaction + 'a> RangeIter<'a, T> { +impl<'a, T: StorageTransaction + 'a> RangeIter<'a, T> { fn open( tx: &'a T, min: Bound<&[u8]>, @@ -1528,7 +1529,7 @@ impl<'a, T: Transaction + 'a> RangeIter<'a, T> { } } -impl<'a, T: Transaction + 'a> InnerIter for RangeIter<'a, T> { +impl<'a, T: StorageTransaction + 'a> InnerIter for RangeIter<'a, T> { fn try_next(&mut self) -> Result>, DatabaseError> { match self { RangeIter::Forward(iter) => iter.try_next(), @@ -1537,7 +1538,7 @@ impl<'a, T: Transaction + 'a> InnerIter for RangeIter<'a, T> { } } -impl IndexImpl for IndexImplEnum { +impl IndexImpl for IndexImplEnum { fn index_lookup_into( &self, table_codec: &mut TableCodec, @@ -1621,7 +1622,7 @@ impl IndexImpl for IndexImplEnum { } } -impl IndexImpl for PrimaryKeyIndexImpl { +impl IndexImpl for PrimaryKeyIndexImpl { fn index_lookup_into( &self, _: &mut TableCodec, @@ -1698,7 +1699,7 @@ impl IndexImpl for PrimaryKeyIndexImpl { } #[inline(always)] -fn secondary_index_lookup( +fn secondary_index_lookup( table_codec: &mut TableCodec, tuple: &mut Tuple, bytes: &[u8], @@ -1712,7 +1713,7 @@ fn secondary_index_lookup( } } -impl IndexImpl for UniqueIndexImpl { +impl IndexImpl for UniqueIndexImpl { fn index_lookup_into( &self, table_codec: &mut TableCodec, @@ -1780,7 +1781,7 @@ impl IndexImpl for UniqueIndexImpl { } } -impl IndexImpl for NormalIndexImpl { +impl IndexImpl for NormalIndexImpl { fn index_lookup_into( &self, table_codec: &mut TableCodec, @@ -1832,7 +1833,7 @@ impl IndexImpl for NormalIndexImpl { } } -impl IndexImpl for CompositeIndexImpl { +impl IndexImpl for CompositeIndexImpl { fn index_lookup_into( &self, table_codec: &mut TableCodec, @@ -1884,7 +1885,7 @@ impl IndexImpl for CompositeIndexImpl { } } -impl IndexImpl for CoveredIndexImpl { +impl IndexImpl for CoveredIndexImpl { fn index_lookup_into( &self, _: &mut TableCodec, @@ -1952,7 +1953,7 @@ impl IndexImpl for CoveredIndexImpl { } #[inline(always)] -fn eq_to_res_scope<'a, T: Transaction + 'a>( +fn eq_to_res_scope<'a, T: StorageTransaction + 'a>( table_codec: &mut TableCodec, tuple: &mut Tuple, index_impl: &impl IndexImpl, @@ -1974,7 +1975,7 @@ fn eq_to_res_scope<'a, T: Transaction + 'a>( Ok(IndexResult::Scope(iter)) } -pub struct TupleIter<'a, T: Transaction + 'a> { +pub struct TupleIter<'a, T: StorageTransaction + 'a> { bounds: IterBounds, pk_ty: Option, deserializers: Vec, @@ -1982,7 +1983,7 @@ pub struct TupleIter<'a, T: Transaction + 'a> { iter: T::IterType<'a>, } -impl<'a, T: Transaction + 'a> TupleIter<'a, T> { +impl<'a, T: StorageTransaction + 'a> TupleIter<'a, T> { pub(crate) fn reset( &mut self, tx: &'a T, @@ -1998,7 +1999,7 @@ impl<'a, T: Transaction + 'a> TupleIter<'a, T> { } } -impl<'a, T: Transaction + 'a> Iter for TupleIter<'a, T> { +impl<'a, T: StorageTransaction + 'a> Iter for TupleIter<'a, T> { fn next_tuple_into( &mut self, table_codec: &mut TableCodec, @@ -2135,7 +2136,7 @@ impl IterBounds { } } -pub struct IndexIter<'a, T: Transaction> { +pub struct IndexIter<'a, T: StorageTransaction> { bounds: IterBounds, params: IndexImplParams<'a, T>, inner: IndexImplEnum, @@ -2145,13 +2146,13 @@ pub struct IndexIter<'a, T: Transaction> { encode_max_buffer: Bytes, } -pub enum IndexIterState<'a, T: Transaction + 'a> { +pub enum IndexIterState<'a, T: StorageTransaction + 'a> { Init, Range(RangeIter<'a, T>), Over, } -impl<'a, T: Transaction> IndexIter<'a, T> { +impl<'a, T: StorageTransaction> IndexIter<'a, T> { pub(crate) fn reset(&mut self, ranges: IndexRanges<'a>, bounds: Bounds) { self.params.reverse = ranges.reverse; self.ranges = ranges; @@ -2173,7 +2174,7 @@ impl<'a, T: Transaction> IndexIter<'a, T> { } } -impl Iter for IndexIter<'_, T> { +impl Iter for IndexIter<'_, T> { fn next_tuple_into( &mut self, table_codec: &mut TableCodec, @@ -2300,11 +2301,11 @@ pub trait Iter { ) -> Result; } -pub struct TableIter<'a, T: Transaction + 'a> { +pub struct TableIter<'a, T: StorageTransaction + 'a> { iter: T::IterType<'a>, } -impl TableIter<'_, T> { +impl TableIter<'_, T> { pub fn try_next( &mut self, arena: &mut (impl MetaArena + ?Sized), @@ -2317,11 +2318,11 @@ impl TableIter<'_, T> { } } -pub struct ViewIter<'a, T: Transaction + 'a> { +pub struct ViewIter<'a, T: StorageTransaction + 'a> { iter: T::IterType<'a>, } -impl ViewIter<'_, T> { +impl ViewIter<'_, T> { pub fn try_next( &mut self, arena: &mut (impl MetaArena + ?Sized), @@ -2336,7 +2337,7 @@ impl ViewIter<'_, T> { // GRCOV_EXCL_START #[cfg(test)] -pub(crate) fn check_range_rev_matches_range( +pub(crate) fn check_range_rev_matches_range( tx: &mut T, ) -> Result<(), DatabaseError> { fn keys(mut iter: I) -> Result>, DatabaseError> { @@ -2415,7 +2416,7 @@ pub(crate) fn check_explicit_transaction_does_not_rescan_own_writes( mut db: crate::db::Database, ) -> Result<(), DatabaseError> { db.ddl("create table t (id int primary key, v int not null)")?; - let mut tx = db.new_transaction()?; + let mut tx = db.new_transaction(false)?; tx.run("insert into t values (1,1), (2,2), (3,3), (4,4), (5,5)")? .done()?; tx.run("update t set id = id + 1000 where id > 3 and id < 3000")? @@ -2437,7 +2438,7 @@ pub(crate) fn check_explicit_transaction_does_not_rescan_own_writes( } #[cfg(test)] -pub(crate) fn check_remove_range(tx: &mut T) -> Result<(), DatabaseError> { +pub(crate) fn check_remove_range(tx: &mut T) -> Result<(), DatabaseError> { let data: Vec<[u8; 4]> = (0u32..3000).map(u32::to_be_bytes).collect(); let [k0, k1, k10, k2500, k2999, k5000] = [0u32, 1, 10, 2500, 2999, 5000].map(u32::to_be_bytes); type Case<'a> = (Bound<&'a [u8]>, Bound<&'a [u8]>); @@ -2496,7 +2497,7 @@ mod test { use crate::storage::rocksdb::{RocksStorage, RocksTransaction}; use crate::storage::table_codec::TableCodec; use crate::storage::{ - IndexIter, InnerIter, Storage, TableCache, Transaction, TransactionIsolationLevel, + IndexIter, InnerIter, Storage, StorageTransaction, TableCache, TransactionIsolationLevel, }; use crate::types::index::{Index, IndexType}; use crate::types::tuple::Tuple; @@ -3111,7 +3112,7 @@ mod test { index_id }; let reader_tx = - storage.transaction_with_isolation(TransactionIsolationLevel::ReadCommitted)?; + storage.transaction_with_isolation(TransactionIsolationLevel::ReadCommitted, true)?; let tuple_id = { let mut index_iter = table_codec.with_index_bound("t1", index_id, |min, max| { reader_tx.range(Bound::Included(min), Bound::Included(max)) diff --git a/src/storage/rocksdb.rs b/src/storage/rocksdb.rs index f6491a1d..3ee90be6 100644 --- a/src/storage/rocksdb.rs +++ b/src/storage/rocksdb.rs @@ -14,7 +14,7 @@ use crate::errors::DatabaseError; use crate::storage::{ - CheckpointableStorage, InnerIter, Storage, Transaction, TransactionIsolationLevel, + CheckpointableStorage, InnerIter, Storage, StorageTransaction, TransactionIsolationLevel, }; #[cfg(feature = "unsafe_txdb_checkpoint")] use librocksdb_sys as ffi; @@ -446,6 +446,7 @@ impl Storage for OptimisticRocksStorage { fn transaction_with_isolation( &self, isolation: TransactionIsolationLevel, + _read_only: bool, ) -> Result, DatabaseError> { self.validate_transaction_isolation(isolation)?; let tx = if self.config.sync { @@ -502,6 +503,7 @@ impl Storage for RocksStorage { fn transaction_with_isolation( &self, isolation: TransactionIsolationLevel, + _read_only: bool, ) -> Result, DatabaseError> { self.validate_transaction_isolation(isolation)?; let tx = if self.config.sync { @@ -636,7 +638,7 @@ fn build_read_options( #[macro_export] macro_rules! impl_transaction { ($tx:ident, $iter:ident) => { - impl<'storage> Transaction for $tx<'storage> { + impl<'storage> StorageTransaction for $tx<'storage> { type BorrowedBytes<'a> = DBPinnableSlice<'a> where @@ -899,7 +901,7 @@ mod test { use crate::storage::table_codec::TableCodec; use crate::storage::{ IndexImplEnum, IndexImplParams, IndexIter, IndexIterState, InnerIter, IterBounds, - PrimaryKeyIndexImpl, Storage, Transaction, + PrimaryKeyIndexImpl, Storage, StorageTransaction, }; use crate::types::index::IndexType; use crate::types::tuple::Tuple; diff --git a/src/storage/table_codec.rs b/src/storage/table_codec.rs index 91166aec..83a3bd58 100644 --- a/src/storage/table_codec.rs +++ b/src/storage/table_codec.rs @@ -24,7 +24,7 @@ use crate::optimizer::core::top_n::ColumnTopN; use crate::planner::MetaArena; use crate::serdes::stable_hash::{StableHasher, TABLE_NAME_HASH_KEYS}; use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTables}; -use crate::storage::{TableCache, Transaction}; +use crate::storage::{StorageTransaction, TableCache}; use crate::types::index::{Index, IndexId, IndexMeta, IndexType, INDEX_ID_LEN}; use crate::types::serialize::TupleValueSerializableImpl; use crate::types::tuple::{Tuple, TupleId, TupleLike}; @@ -613,7 +613,7 @@ impl TableCodec { ) } - pub fn decode_column_statistics( + pub fn decode_column_statistics( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { @@ -869,7 +869,7 @@ impl TableCodec { index_meta.encode(value, true, reference_tables, arena) } - pub fn decode_index_meta( + pub fn decode_index_meta( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { @@ -927,7 +927,7 @@ impl TableCodec { col.encode(value, true, reference_tables, arena) } - pub fn decode_column( + pub fn decode_column( reader: &mut R, reference_tables: &ReferenceTables, arena: &mut (impl MetaArena + ?Sized), @@ -936,7 +936,7 @@ impl TableCodec { ColumnCatalog::decode::(reader, None, reference_tables, arena) } - pub fn decode_index_statistics( + pub fn decode_index_statistics( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { @@ -957,7 +957,7 @@ impl TableCodec { sketch_meta.encode(value, true, reference_tables, arena) } - pub fn decode_index_statistics_sketch_meta( + pub fn decode_index_statistics_sketch_meta( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { @@ -978,7 +978,7 @@ impl TableCodec { sketch_page.encode(value, true, reference_tables, arena) } - pub fn decode_index_statistics_sketch_page( + pub fn decode_index_statistics_sketch_page( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { @@ -1012,7 +1012,7 @@ impl TableCodec { IndexStatisticsCodecType::from_tag(tag) } - pub fn decode_index_statistics_bucket( + pub fn decode_index_statistics_bucket( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { @@ -1033,7 +1033,7 @@ impl TableCodec { top_n.encode(value, true, reference_tables, arena) } - pub fn decode_index_statistics_top_n( + pub fn decode_index_statistics_top_n( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { @@ -1076,7 +1076,7 @@ impl TableCodec { Ok(()) } - pub fn decode_view( + pub fn decode_view( bytes: &[u8], drive: (&T, &TableCache), scala_functions: &ScalaFunctions, @@ -1098,7 +1098,7 @@ impl TableCodec { View::decode(&mut cursor, Some(&context), &reference_tables, arena) } - pub fn decode_view_name( + pub fn decode_view_name( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { @@ -1116,7 +1116,7 @@ impl TableCodec { meta.encode(value, true, reference_tables, arena) } - pub fn decode_root_table( + pub fn decode_root_table( bytes: &[u8], arena: &mut (impl MetaArena + ?Sized), ) -> Result { diff --git a/tests/macros-test/src/main.rs b/tests/macros-test/src/main.rs index 174581e1..95721683 100644 --- a/tests/macros-test/src/main.rs +++ b/tests/macros-test/src/main.rs @@ -1491,7 +1491,7 @@ mod test { vec![(1, Some(100)), (1, Some(200)), (2, Some(300)), (3, None)] ); - let mut tx = database.new_transaction()?; + let mut tx = database.new_transaction(false)?; let in_tx = tx .bind(|ctx| { ctx.from::()? @@ -2840,7 +2840,7 @@ mod test { }); assert_eq!(set_query_ages, vec![None, Some(30), Some(18), Some(18)]); - let mut tx = database.new_transaction()?; + let mut tx = database.new_transaction(false)?; tx.insert_many([User { id: 4, name: "Dora".to_string(), @@ -2961,7 +2961,7 @@ mod test { "Aggregate [] -> Group By [users.id] [HashAggregate => (Sort Option: None)] Union: [users.id] Projection [users.id] [Project => (Sort Option: Follow)] TableScan users -> [users.id] [SeqScan => (Sort Option: None)] Projection [wallets.id] [Project => (Sort Option: Follow)] TableScan wallets -> [wallets.id] [SeqScan => (Sort Option: None)]" ); - let mut tx = database.new_transaction()?; + let mut tx = database.new_transaction(false)?; let tx_tables = tx.show_tables()?.collect::, _>>()?; assert!(tx_tables.iter().any(|name| name == "users")); let tx_describe = tx.describe::()?.collect::, _>>()?; diff --git a/tpcc/src/backend/dual.rs b/tpcc/src/backend/dual.rs index ed738bea..f87b4223 100644 --- a/tpcc/src/backend/dual.rs +++ b/tpcc/src/backend/dual.rs @@ -223,14 +223,14 @@ fn drain_sqlite_iter(mut iter: SqliteResult<'_, '_>) -> Result<(), TpccError> { Ok(()) } -fn drain_kitesql_iter( +fn drain_kitesql_iter( mut iter: KiteSqlTxnResult<'_, T>, ) -> Result<(), TpccError> { while iter.skip_next_tuple()? {} Ok(()) } -fn with_kitesql_nth( +fn with_kitesql_nth( mut iter: KiteSqlTxnResult<'_, T>, n: usize, visitor: &mut dyn FnMut(&TupleImpl) -> Result<(), TpccError>, @@ -244,7 +244,7 @@ fn with_kitesql_nth( .ok_or(TpccError::EmptyTuples) } -fn with_ordered_nth( +fn with_ordered_nth( mut kitesql_iter: KiteSqlTxnResult<'_, T>, mut sqlite_iter: SqliteResult<'_, '_>, sql: &'static str, @@ -303,7 +303,7 @@ fn with_ordered_nth( } } -fn drain_and_compare_ordered( +fn drain_and_compare_ordered( mut kitesql_iter: KiteSqlTxnResult<'_, T>, mut sqlite_iter: SqliteResult<'_, '_>, sql: &'static str, diff --git a/tpcc/src/backend/kitesql_lmdb.rs b/tpcc/src/backend/kitesql_lmdb.rs index 49fea50f..892aebd2 100644 --- a/tpcc/src/backend/kitesql_lmdb.rs +++ b/tpcc/src/backend/kitesql_lmdb.rs @@ -18,7 +18,7 @@ use super::{ StatementSpec, }; use crate::TpccError; -use kite_sql::db::{DBTransaction, DataBaseBuilder, Database}; +use kite_sql::db::{DataBaseBuilder, Database, Transaction}; use kite_sql::storage::lmdb::{LmdbStorage, LmdbTransaction as KiteSqlLmdbTransaction}; use kite_sql::types::tuple::TupleImpl; @@ -53,7 +53,7 @@ impl KiteSqlLmdbBackend { fn start_transaction(&self) -> Result, TpccError> { Ok(KiteSqlLmdbTransactionWrapper { - inner: self.database.new_transaction()?, + inner: self.database.new_transaction(false)?, }) } } @@ -88,7 +88,7 @@ impl SimpleExecutor for KiteSqlLmdbBackend { } pub struct KiteSqlLmdbTransactionWrapper<'a> { - inner: DBTransaction<'a, LmdbStorage>, + inner: Transaction<'a, LmdbStorage>, } impl<'a> KiteSqlLmdbTransactionWrapper<'a> { diff --git a/tpcc/src/backend/kitesql_rocksdb.rs b/tpcc/src/backend/kitesql_rocksdb.rs index 1ba92c9c..ed03f0bc 100644 --- a/tpcc/src/backend/kitesql_rocksdb.rs +++ b/tpcc/src/backend/kitesql_rocksdb.rs @@ -18,9 +18,9 @@ use super::{ }; use crate::TpccError; use kite_sql::binder::{command_type, CommandType}; -use kite_sql::db::{DBTransaction, DataBaseBuilder, Database, Statement, TransactionIter}; +use kite_sql::db::{DataBaseBuilder, Database, Statement, Transaction, TransactionIter}; use kite_sql::storage::rocksdb::{OptimisticRocksStorage, RocksStorage}; -use kite_sql::storage::{Storage, Transaction}; +use kite_sql::storage::{Storage, StorageTransaction}; use kite_sql::types::tuple::TupleImpl; pub type KiteSqlRocksDbBackend = KiteSqlRocksBackend; @@ -74,7 +74,7 @@ impl KiteSqlRocksBackend { fn start_transaction(&self) -> Result, TpccError> { Ok(KiteSqlRocksTransaction { - inner: self.database.new_transaction()?, + inner: self.database.new_transaction(false)?, }) } } @@ -154,14 +154,14 @@ pub(crate) fn execute_kitesql_batch( return Ok(()); } - let mut transaction = database.new_transaction()?; + let mut transaction = database.new_transaction(false)?; transaction.run(sql)?.done()?; transaction.commit()?; Ok(()) } pub struct KiteSqlRocksTransaction<'a, S: Storage> { - inner: DBTransaction<'a, S>, + inner: Transaction<'a, S>, } impl<'a, S: Storage> KiteSqlRocksTransaction<'a, S> { @@ -234,9 +234,9 @@ impl<'a, S: Storage> BackendTransaction for KiteSqlRocksTransaction<'a, S> { } } -pub struct KiteSqlTxnResult<'a, T: Transaction + 'a>(TransactionIter<'a, T>); +pub struct KiteSqlTxnResult<'a, T: StorageTransaction + 'a>(TransactionIter<'a, T>); -impl<'a, T: Transaction + 'a> KiteSqlTxnResult<'a, T> { +impl<'a, T: StorageTransaction + 'a> KiteSqlTxnResult<'a, T> { pub(crate) fn new(iter: TransactionIter<'a, T>) -> Self { Self(iter) } diff --git a/tpcc/src/main.rs b/tpcc/src/main.rs index 72c31821..e105fc5a 100644 --- a/tpcc/src/main.rs +++ b/tpcc/src/main.rs @@ -916,7 +916,7 @@ fn explain_tpcc() -> Result<(), DatabaseError> { ] { database.load(CatalogKind::Table(table_name.to_string().into()))?; } - let mut tx = database.new_transaction()?; + let mut tx = database.new_transaction(false)?; let (c_w_id, c_d_id, c_id, c_last, c_balance, c_data) = with_next_tuple( tx.run("SELECT c_w_id, c_d_id, c_id, c_last, c_balance, c_data FROM customer limit 1")?,