diff --git a/Cargo.lock b/Cargo.lock index 3b61126c..936107d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1131,6 +1131,7 @@ dependencies = [ "comfy-table", "criterion", "csv", + "hashbrown 0.15.4", "indicatif", "js-sys", "kite_sql_serde_macros", diff --git a/Cargo.toml b/Cargo.toml index 72b3b437..104f6099 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ required-features = ["pprof"] [dependencies] bumpalo = { version = "3", default-features = false, features = ["collections"] } +hashbrown = { version = "0.15", default-features = false } ordered-float = { version = "4" } paste = { version = "1" } kite_sql_serde_macros = { version = "0.2.6", path = "kite_sql_serde_macros" } diff --git a/README.md b/README.md index 41e25f0f..b71f6c18 100755 --- a/README.md +++ b/README.md @@ -135,9 +135,9 @@ print(list(db.run("select * from demo"))) | Backend | TpmC | New-Order | Payment | Order-Status | Delivery | Stock-Level | | --- | ---: | ---: | ---: | ---: | ---: | ---: | -| KiteSQL LMDB | 187161 | 274 | 68 | 40 | 454 | 278 | -| KiteSQL RocksDB | 46291 | 374 | 184 | 201 | 14463 | 651 | -| SQLite (WAL, `synchronous=OFF`) | 179039 | 291 | 43 | 38 | 368 | 320 | +| KiteSQL LMDB | 209840 | 254 | 50 | 28 | 345 | 202 | +| KiteSQL RocksDB | 48049 | 354 | 85 | 146 | 14511 | 562 | +| SQLite (WAL, `synchronous=OFF`) | 192435 | 270 | 40 | 33 | 344 | 265 | 👉 [Details and how to reproduce](tpcc/README.md) diff --git a/src/db/program.rs b/src/db/program.rs index cd48fbed..9f9ebc3c 100644 --- a/src/db/program.rs +++ b/src/db/program.rs @@ -218,7 +218,7 @@ 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::range_detacher::{Range, RangeValue}; use crate::expression::{BinaryOperator, ScalarExpression}; use crate::planner::operator::filter::FilterOperator; use crate::planner::operator::sort::SortField; @@ -287,11 +287,11 @@ mod tests { }); let original = Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Int32(5)), + max: Bound::Excluded(DataValue::Int32(5).into()), }; let expected = Range::Scope { - min: Bound::Included(DataValue::Int32(3)), - max: Bound::Excluded(DataValue::Int32(5)), + min: Bound::Included(DataValue::Int32(3).into()), + max: Bound::Excluded(DataValue::Int32(5).into()), }; let sort = SortOption::OrderBy { fields: vec![SortField::new(col_expr, true, false)], @@ -349,7 +349,7 @@ mod tests { lookup: Some(lookup), residual_predicate: has_residual.then_some(predicate), sort_option: sort.clone(), - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, @@ -600,21 +600,61 @@ mod tests { Ok(()) } - #[test] - fn composite_index_ranges_bind_for_each_execution() -> Result<(), DatabaseError> { - fn index_range(plan: &LogicalPlan) -> Option<&Range> { - plan.physical_option - .as_ref() - .and_then(|option| match &option.plan { - PlanImpl::IndexScan(info) => match &info.lookup { - Some(IndexLookup::Static(range)) => Some(range), - _ => None, - }, + fn index_range(plan: &LogicalPlan) -> Option<&Range> { + plan.physical_option + .as_ref() + .and_then(|option| match &option.plan { + PlanImpl::IndexScan(info) => match &info.lookup { + Some(IndexLookup::Static(range)) => Some(range), _ => None, - }) - .or_else(|| plan.childrens.iter().find_map(index_range)) + }, + _ => None, + }) + .or_else(|| plan.childrens.iter().find_map(index_range)) + } + + #[test] + fn null_parameters_empty_index_ranges() -> Result<(), DatabaseError> { + let mut db = DataBaseBuilder::path(".") + .histogram_buckets(2) + .build_in_memory()?; + db.ddl("create table t(w int, k int, primary key(w,k))")?; + db.run("insert into t values(1,1),(1,2),(2,1),(2,2),(2,4)")? + .done()?; + db.analyze("t")?; + let ty = LogicalType::Integer; + let mut tx = db.new_transaction(false)?; + // A NULL bound used to reach storage as a NULL key: the composite scope below became + // `(2, NULL) .. (2, 5)`, an inverted range, and `w < NULL` scanned every row. + for (sql, params) in [ + ( + "select k from t where w = $1 and k < $2 and k >= $3", + vec![ + (1, DataValue::Int32(2)), + (2, DataValue::Int32(5)), + (3, DataValue::Null), + ], + ), + ("select k from t where w < $1", vec![(1, DataValue::Null)]), + ("select k from t where w = $1", vec![(1, DataValue::Null)]), + ] { + let mut plan = db.program_sql( + sql, + &(1..=params.len()) + .map(|id| (id, ty.clone())) + .collect::>(), + )?; + assert!(index_range(plan.executor.plan()).is_some(), "{sql}"); + let mut iter = plan.execute(&mut tx, ¶ms)?; + assert!(iter.next_tuple(|_, _| ())?.is_none(), "{sql}"); + iter.done()?; } + Ok(()) + } + + #[test] + fn composite_index_ranges_bind_for_each_execution() -> Result<(), DatabaseError> { let mut db = DataBaseBuilder::path(".") .histogram_buckets(2) .build_in_memory()?; @@ -694,6 +734,19 @@ mod tests { else { panic!("expected index scan") }; + // `$3 - 20` is computed when the range is bound, so the whole predicate is the range. + fn has_filter(plan: &LogicalPlan) -> bool { + matches!(plan.operator, Operator::Filter(_)) || plan.childrens.iter().any(has_filter) + } + assert!(matches!( + &info.lookup, + Some(IndexLookup::Static(Range::Scope { + min: Bound::Included(RangeValue::Computed(_)), + .. + })) + )); + assert!(info.residual_predicate.is_none()); + assert!(!has_filter(plan.executor.plan())); let tables = TableCache::default(); let context = &tables; let storage = MemoryStorage::new(); @@ -712,16 +765,41 @@ mod tests { assert_eq!( ranges.next(), Some(&Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(2), - DataValue::Int32(2), - ])), - max: Bound::Excluded(DataValue::Tuple(vec![ - DataValue::Int32(2), - DataValue::Int32(5), - ])), + min: Bound::Included( + DataValue::Tuple(vec![DataValue::Int32(2), DataValue::Int32(2),]).into() + ), + max: Bound::Excluded( + DataValue::Tuple(vec![DataValue::Int32(2), DataValue::Int32(5),]).into() + ), }) ); + drop(ranges); + + // The computed bound follows each execution's parameters, and a NULL one matches nothing. + let mut tx = db.new_transaction(false)?; + for (lower, expected) in [ + (DataValue::Int32(22), vec![2, 4]), + (DataValue::Int32(24), vec![4]), + (DataValue::Null, vec![]), + ] { + let mut iter = plan.execute( + &mut tx, + [ + (1, DataValue::Int32(2)), + (2, DataValue::Int32(5)), + (3, lower), + ], + )?; + let mut rows = Vec::new(); + while let Some(k) = iter.next_tuple(|_, row| row.value_at(0).clone())? { + rows.push(k); + } + let expected = expected + .into_iter() + .map(DataValue::Int32) + .collect::>(); + assert_eq!(rows, expected); + } Ok(()) } } diff --git a/src/execution/dml/analyze.rs b/src/execution/dml/analyze.rs index 5b44d6d0..6651af85 100644 --- a/src/execution/dml/analyze.rs +++ b/src/execution/dml/analyze.rs @@ -277,11 +277,11 @@ mod test { } fn test_hll_precision_is_configurable() -> Result<(), DatabaseError> { - let rows = 1000; + let (rows, distinct) = (1000, 500); let ndv_with = |precision: u8| -> Result { let mut builder = HistogramMetaBuilder::new(precision)?; for i in 0..rows { - builder.append(&DataValue::Int32(i)); + builder.append(&DataValue::Int32(i % distinct)); } Ok(builder.build().distinct_values_len()) }; @@ -289,13 +289,16 @@ mod test { // 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(", "); + let values = (0..rows) + .map(|i| format!("({i}, {})", i % distinct)) + .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.ddl("create table t1 (id int primary key, a int)")?; + kite_sql.ddl("create index idx_a on t1 (a)")?; kite_sql .run(format!("insert into t1 values {values}"))? .done()?; @@ -318,7 +321,7 @@ mod test { let loader = StatisticMetaLoader::new(kite_sql.state.meta_cache()); let column = loader.load_column(table_id, a_id).unwrap(); assert_eq!(column.distinct_values_len(), expected); - let index = loader.load(table_id, 0)?.unwrap(); + let index = loader.load(table_id, 1)?.unwrap(); assert_eq!(index.histogram().distinct_values_len(), expected); } Ok(()) @@ -438,7 +441,7 @@ mod test { let expected_frequency = loader.collect_count( table_id, 1, - &Range::Eq(DataValue::Int32(7)), + &Range::Eq(DataValue::Int32(7).into()), &mut Vec::new(), )?; @@ -457,7 +460,7 @@ mod test { loader.collect_count( table_id, 1, - &Range::Eq(DataValue::Int32(7)), + &Range::Eq(DataValue::Int32(7).into()), &mut Vec::new() )?, expected_frequency @@ -586,7 +589,7 @@ mod test { .collect_count( table_id, 1, - &Range::Eq(DataValue::Int32(7)), + &Range::Eq(DataValue::Int32(7).into()), &mut Vec::new() )? .map(|(rows, _)| rows), @@ -613,7 +616,7 @@ mod test { .collect_count( table_id, 1, - &Range::Eq(DataValue::Int32(7)), + &Range::Eq(DataValue::Int32(7).into()), &mut Vec::new() )? .map(|(rows, _)| rows), diff --git a/src/execution/dql/aggregate/avg.rs b/src/execution/dql/aggregate/avg.rs index dd068541..e3b10a06 100644 --- a/src/execution/dql/aggregate/avg.rs +++ b/src/execution/dql/aggregate/avg.rs @@ -15,15 +15,16 @@ use crate::errors::DatabaseError; use crate::execution::dql::aggregate::sum::{text_as_number, SumAccumulator}; use crate::execution::dql::aggregate::Accumulator; +use crate::execution::hash::FxHashSet; use crate::expression::BinaryOperator; use crate::types::evaluator::binary_create; use crate::types::value::DataValue; use crate::types::LogicalType; use std::borrow::Cow; -use std::collections::HashSet; +use std::mem; pub struct AvgAccumulator { - distinct_values: Option>, + distinct_values: Option>, inner: Option, count: usize, result: DataValue, @@ -32,7 +33,7 @@ pub struct AvgAccumulator { impl AvgAccumulator { pub fn new(distinct: bool) -> Self { Self { - distinct_values: distinct.then(HashSet::new), + distinct_values: distinct.then(FxHashSet::default), inner: None, count: 0, result: DataValue::Null, @@ -85,8 +86,17 @@ impl Accumulator for AvgAccumulator { &self.result } - fn result_owned(self: Box) -> DataValue { - self.result + fn take_result(&mut self) -> DataValue { + mem::replace(&mut self.result, DataValue::Null) + } + + fn reset(&mut self) { + if let Some(values) = &mut self.distinct_values { + values.clear(); + } + self.inner = None; + self.count = 0; + self.result = DataValue::Null; } } @@ -109,7 +119,7 @@ mod tests { assert_ne!(first, second); accumulator.evaluate()?; assert_eq!(&second, accumulator.result()); - assert_eq!(Box::new(accumulator).result_owned(), second); + assert_eq!(accumulator.take_result(), second); Ok(()) } @@ -133,10 +143,7 @@ mod tests { accumulator.update_value(&DataValue::Int32(3))?; accumulator.evaluate()?; - assert_eq!( - Box::new(accumulator).result_owned(), - DataValue::Float64(1.5.into()) - ); + assert_eq!(accumulator.take_result(), DataValue::Float64(1.5.into())); Ok(()) } @@ -145,16 +152,13 @@ mod tests { let mut empty = AvgAccumulator::new(false); empty.evaluate()?; assert_eq!(empty.result(), &DataValue::Null); - assert_eq!(Box::new(empty).result_owned(), DataValue::Null); + assert_eq!(empty.take_result(), DataValue::Null); let mut accumulator = AvgAccumulator::new(false); accumulator.update_value(&DataValue::UInt32(2))?; accumulator.update_value(&DataValue::UInt32(4))?; accumulator.evaluate()?; - assert_eq!( - Box::new(accumulator).result_owned(), - DataValue::Float64(3.0.into()) - ); + assert_eq!(accumulator.take_result(), DataValue::Float64(3.0.into())); Ok(()) } } diff --git a/src/execution/dql/aggregate/count.rs b/src/execution/dql/aggregate/count.rs index 38c0d743..622c2505 100644 --- a/src/execution/dql/aggregate/count.rs +++ b/src/execution/dql/aggregate/count.rs @@ -14,8 +14,9 @@ use crate::errors::DatabaseError; use crate::execution::dql::aggregate::Accumulator; +use crate::execution::hash::FxHashSet; use crate::types::value::DataValue; -use std::collections::HashSet; +use std::mem; pub struct CountAccumulator { result: DataValue, @@ -45,20 +46,24 @@ impl Accumulator for CountAccumulator { &self.result } - fn result_owned(self: Box) -> DataValue { - self.result + fn take_result(&mut self) -> DataValue { + mem::replace(&mut self.result, DataValue::Null) + } + + fn reset(&mut self) { + self.result = DataValue::Int32(0); } } pub struct DistinctCountAccumulator { - distinct_values: HashSet, + distinct_values: FxHashSet, result: DataValue, } impl DistinctCountAccumulator { pub fn new() -> Self { Self { - distinct_values: HashSet::default(), + distinct_values: FxHashSet::default(), result: DataValue::Int32(0), } } @@ -78,8 +83,13 @@ impl Accumulator for DistinctCountAccumulator { &self.result } - fn result_owned(self: Box) -> DataValue { - self.result + fn take_result(&mut self) -> DataValue { + mem::replace(&mut self.result, DataValue::Null) + } + + fn reset(&mut self) { + self.distinct_values.clear(); + self.result = DataValue::Int32(0); } } @@ -95,7 +105,7 @@ mod tests { accumulator.update_value(&value)?; } assert_eq!(accumulator.result(), &DataValue::Int32(2)); - assert_eq!(Box::new(accumulator).result_owned(), DataValue::Int32(2)); + assert_eq!(accumulator.take_result(), DataValue::Int32(2)); Ok(()) } @@ -106,7 +116,7 @@ mod tests { accumulator.update_value(&value)?; } assert_eq!(accumulator.result(), &DataValue::Int32(2)); - assert_eq!(Box::new(accumulator).result_owned(), DataValue::Int32(2)); + assert_eq!(accumulator.take_result(), DataValue::Int32(2)); Ok(()) } } diff --git a/src/execution/dql/aggregate/group_concat.rs b/src/execution/dql/aggregate/group_concat.rs index 07264efe..778e56a6 100644 --- a/src/execution/dql/aggregate/group_concat.rs +++ b/src/execution/dql/aggregate/group_concat.rs @@ -14,21 +14,22 @@ use crate::errors::DatabaseError; use crate::execution::dql::aggregate::Accumulator; +use crate::execution::hash::FxHashSet; use crate::types::value::{DataValue, Utf8Type}; use crate::types::CharLengthUnits; -use std::collections::HashSet; use std::fmt::Write; +use std::mem; /// SQLite `group_concat()`: non-NULL values in input order joined with `,`; NULL when there are none. pub struct GroupConcatAccumulator { - distinct_values: Option>, + distinct_values: Option>, result: DataValue, } impl GroupConcatAccumulator { pub fn new(distinct: bool) -> Self { Self { - distinct_values: distinct.then(HashSet::new), + distinct_values: distinct.then(FxHashSet::default), result: DataValue::Null, } } @@ -68,7 +69,14 @@ impl Accumulator for GroupConcatAccumulator { &self.result } - fn result_owned(self: Box) -> DataValue { - self.result + fn take_result(&mut self) -> DataValue { + mem::replace(&mut self.result, DataValue::Null) + } + + fn reset(&mut self) { + if let Some(values) = &mut self.distinct_values { + values.clear(); + } + self.result = DataValue::Null; } } diff --git a/src/execution/dql/aggregate/hash_agg.rs b/src/execution/dql/aggregate/hash_agg.rs index 0036957d..cf08dd19 100644 --- a/src/execution/dql/aggregate/hash_agg.rs +++ b/src/execution/dql/aggregate/hash_agg.rs @@ -14,8 +14,9 @@ use crate::errors::DatabaseError; use crate::execution::dql::aggregate::{ - create_accumulators, update_accumulators, write_aggregate_output, Accumulator, + push_accumulators, update_accumulators, write_aggregate_output, Accumulator, }; +use crate::execution::hash::FxBuildHasher; use crate::execution::{ build_read, ExecBuilder, ExecContext, ExecNode, ExecutorNode, ReadExecutor, }; @@ -25,15 +26,22 @@ use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::StorageTransaction; use crate::types::value::DataValue; -use std::collections::hash_map::IntoIter as HashMapIntoIter; -use std::collections::HashMap; +use hashbrown::hash_table::IntoIter as HashTableIntoIter; +use hashbrown::HashTable; +use std::hash::BuildHasher; +use std::mem; -type HashAggOutput = HashMapIntoIter, Vec>>; +/// Group indexes, which locate each group's keys and accumulators. +type HashAggOutput = HashTableIntoIter; pub struct HashAggExecutor<'a> { agg_calls: &'a [ExprRef], groupby_exprs: &'a [ExprRef], input: ExecNode<'a>, + // Every group's keys and accumulators back to back, in the order the groups appear: + // `groupby_exprs.len()` keys and `agg_calls.len()` accumulators per group. + keys: Vec, + accs: Vec>, output: Option, } @@ -57,6 +65,8 @@ impl<'a> ReadExecutor<'a> for HashAggExecutor<'a> { agg_calls, groupby_exprs, input, + keys: Vec::new(), + accs: Vec::new(), output: None, })) } @@ -68,6 +78,8 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for HashAggExecutor<'a> arena: &mut ExecContext<'_, 'a, '_, T>, plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { + self.keys.clear(); + self.accs.clear(); self.output = None; self.input.reset(arena, plan_arena) } @@ -77,16 +89,19 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for HashAggExecutor<'a> arena: &mut ExecContext<'_, 'a, '_, T>, plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { + let len = self.groupby_exprs.len(); + let acc_len = self.agg_calls.len(); if self.output.is_none() { - let mut group_hash_accs: HashMap, Vec>> = - HashMap::new(); - let mut group_keys = Vec::with_capacity(self.groupby_exprs.len()); + let hasher = FxBuildHasher::default(); + let mut groups: HashTable = HashTable::new(); + let mut group_count = 0; while self.input.next_tuple(arena, plan_arena)? { let tuple = arena.result_tuple(); - group_keys.clear(); + // Evaluate the key in place, and drop it again if its group exists. + let start = self.keys.len(); for expr in self.groupby_exprs { - group_keys.push( + self.keys.push( plan_arena .expression(*expr) .eval(plan_arena, Some(tuple))? @@ -94,24 +109,50 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for HashAggExecutor<'a> ); } - if let Some(accs) = group_hash_accs.get_mut(group_keys.as_slice()) { - update_accumulators(accs, self.agg_calls, tuple, plan_arena)?; - } else { - let mut accs = create_accumulators(self.agg_calls, plan_arena)?; - update_accumulators(&mut accs, self.agg_calls, tuple, plan_arena)?; - group_hash_accs.insert(std::mem::take(&mut group_keys), accs); - } + let keys = &self.keys; + let hash = hasher.hash_one(&keys[start..]); + let group = groups + .find(hash, |&group| keys[group * len..][..len] == keys[start..]) + .copied(); + let group = match group { + Some(group) => { + self.keys.truncate(start); + group + } + None => { + let group = group_count; + group_count += 1; + groups.insert_unique(hash, group, |&group| { + hasher.hash_one(&keys[group * len..][..len]) + }); + push_accumulators(&mut self.accs, self.agg_calls, plan_arena)?; + group + } + }; + update_accumulators( + &mut self.accs[group * acc_len..][..acc_len], + self.agg_calls, + tuple, + plan_arena, + )?; } - self.output = Some(group_hash_accs.into_iter()); + self.output = Some(groups.into_iter()); } - let Some((group_keys, accs)) = self.output.as_mut().and_then(Iterator::next) else { + let Some(group) = self.output.as_mut().and_then(Iterator::next) else { arena.finish(); return Ok(()); }; + let group_keys = self.keys[group * len..][..len] + .iter_mut() + .map(|value| mem::replace(value, DataValue::Null)); - write_aggregate_output(arena.output_tuple_mut(), accs, group_keys)?; + write_aggregate_output( + arena.output_tuple_mut(), + &mut self.accs[group * acc_len..][..acc_len], + group_keys, + )?; arena.resume(); Ok(()) } diff --git a/src/execution/dql/aggregate/min_max.rs b/src/execution/dql/aggregate/min_max.rs index bc88ffdb..48b8bf1d 100644 --- a/src/execution/dql/aggregate/min_max.rs +++ b/src/execution/dql/aggregate/min_max.rs @@ -18,6 +18,7 @@ use crate::expression::BinaryOperator; use crate::types::evaluator::binary_create; use crate::types::value::DataValue; use std::borrow::Cow; +use std::mem; pub struct MinMaxAccumulator { result: DataValue, @@ -64,8 +65,12 @@ impl Accumulator for MinMaxAccumulator { &self.result } - fn result_owned(self: Box) -> DataValue { - self.result + fn take_result(&mut self) -> DataValue { + mem::replace(&mut self.result, DataValue::Null) + } + + fn reset(&mut self) { + self.result = DataValue::Null; } } @@ -82,10 +87,7 @@ mod tests { accumulator.update_value(&value)?; } assert_eq!(accumulator.result(), &DataValue::Int32(expected)); - assert_eq!( - Box::new(accumulator).result_owned(), - DataValue::Int32(expected) - ); + assert_eq!(accumulator.take_result(), DataValue::Int32(expected)); } Ok(()) } diff --git a/src/execution/dql/aggregate/mod.rs b/src/execution/dql/aggregate/mod.rs index eb783dc2..eeaa01b7 100644 --- a/src/execution/dql/aggregate/mod.rs +++ b/src/execution/dql/aggregate/mod.rs @@ -32,7 +32,6 @@ use crate::execution::dql::aggregate::sum::{DistinctSumAccumulator, SumAccumulat use crate::execution::dql::aggregate::total::TotalAccumulator; use crate::expression::agg::AggKind; use crate::expression::ScalarExpression; -use crate::iter_ext::Itertools; use crate::planner::ExprRef; use crate::planner::MetaArena; use crate::types::tuple::{Tuple, TupleLike}; @@ -53,7 +52,9 @@ pub trait Accumulator { fn result(&self) -> &DataValue; - fn result_owned(self: Box) -> DataValue; + fn take_result(&mut self) -> DataValue; + + fn reset(&mut self); } pub(crate) fn create_accumulator( @@ -75,22 +76,35 @@ pub(crate) fn create_accumulator( } #[inline] -pub(crate) fn create_accumulators( +pub(crate) fn push_accumulators( + accs: &mut Vec>, + exprs: &[ExprRef], + arena: &(dyn MetaArena + '_), +) -> Result<(), DatabaseError> { + for expr in exprs { + let ScalarExpression::AggCall { + kind, ty, distinct, .. + } = arena.expression(*expr) + else { + unreachable!("push_accumulators called with non-aggregate expression {expr}") + }; + accs.push(create_accumulator(*kind, ty, *distinct)?); + } + Ok(()) +} + +pub(crate) fn prepare_accumulators( + accs: &mut Vec>, exprs: &[ExprRef], arena: &(dyn MetaArena + '_), -) -> Result>, DatabaseError> { - exprs - .iter() - .map(|expr| { - let ScalarExpression::AggCall { - kind, ty, distinct, .. - } = arena.expression(*expr) - else { - unreachable!("create_accumulators called with non-aggregate expression {expr}") - }; - create_accumulator(*kind, ty, *distinct) - }) - .try_collect() +) -> Result<(), DatabaseError> { + if accs.is_empty() { + return push_accumulators(accs, exprs, arena); + } + for acc in accs { + acc.reset(); + } + Ok(()) } pub(crate) fn update_accumulators( @@ -100,32 +114,40 @@ pub(crate) fn update_accumulators( arena: &(dyn MetaArena + '_), ) -> Result<(), DatabaseError> { for (acc, expr) in accs.iter_mut().zip(agg_calls.iter()) { - let ScalarExpression::AggCall { args, .. } = arena.expression(*expr) else { - unreachable!() - }; - if args.len() > 1 { - return Err(DatabaseError::UnsupportedStmt( - "currently aggregate functions only support a single Column as a parameter" - .to_string(), - )); - } - let value = arena.expression(args[0]).eval(arena, Some(tuple))?; + let value = aggregate_arg(*expr, tuple, arena)?; acc.update_value(&value)?; } Ok(()) } +pub(crate) fn aggregate_arg<'a, T: TupleLike + ?Sized>( + agg_call: ExprRef, + tuple: &'a T, + arena: &'a (dyn MetaArena + '_), +) -> Result, DatabaseError> { + let ScalarExpression::AggCall { args, .. } = arena.expression(agg_call) else { + unreachable!() + }; + if args.len() > 1 { + return Err(DatabaseError::UnsupportedStmt( + "currently aggregate functions only support a single Column as a parameter".to_string(), + )); + } + arena.expression(args[0]).eval(arena, Some(tuple)) +} + pub(crate) fn write_aggregate_output( output: &mut Tuple, - accs: Vec>, - group_keys: Vec, + accs: &mut [Box], + group_keys: impl IntoIterator, ) -> Result<(), DatabaseError> { + let group_keys = group_keys.into_iter(); output.pk = None; output.values.clear(); - output.values.reserve(accs.len() + group_keys.len()); - for mut acc in accs { + output.values.reserve(accs.len() + group_keys.size_hint().0); + for acc in accs { acc.evaluate()?; - output.values.push(acc.result_owned()); + output.values.push(acc.take_result()); } output.values.extend(group_keys); Ok(()) diff --git a/src/execution/dql/aggregate/simple_agg.rs b/src/execution/dql/aggregate/simple_agg.rs index df054454..a41b0924 100644 --- a/src/execution/dql/aggregate/simple_agg.rs +++ b/src/execution/dql/aggregate/simple_agg.rs @@ -13,11 +13,12 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::execution::dql::aggregate::create_accumulators; +use crate::execution::dql::aggregate::{ + prepare_accumulators, update_accumulators, write_aggregate_output, Accumulator, +}; use crate::execution::{ build_read, ExecBuilder, ExecContext, ExecNode, ExecutorNode, ReadExecutor, }; -use crate::expression::ScalarExpression; use crate::planner::operator::aggregate::AggregateOperator; use crate::planner::ExprRef; use crate::planner::LogicalPlan; @@ -26,6 +27,7 @@ use crate::storage::StorageTransaction; pub struct SimpleAggExecutor<'a> { agg_calls: &'a [ExprRef], input: ExecNode<'a>, + accs: Vec>, returned: bool, } @@ -41,6 +43,7 @@ impl<'a> ReadExecutor<'a> for SimpleAggExecutor<'a> { ExecNode::SimpleAgg(Box::new(SimpleAggExecutor { agg_calls, input, + accs: Vec::new(), returned: false, })) } @@ -66,36 +69,13 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for SimpleAggExecutor<' return Ok(()); } - let mut accs = create_accumulators(self.agg_calls, plan_arena)?; - + prepare_accumulators(&mut self.accs, self.agg_calls, plan_arena)?; while self.input.next_tuple(arena, plan_arena)? { let tuple = arena.result_tuple(); - for (acc, expr) in accs.iter_mut().zip(self.agg_calls.iter()) { - let ScalarExpression::AggCall { args, .. } = plan_arena.expression(*expr) else { - unreachable!() - }; - if args.len() > 1 { - return Err(DatabaseError::UnsupportedStmt( - "currently aggregate functions only support a single Column as a parameter" - .to_string(), - )); - } - - let value = plan_arena - .expression(args[0]) - .eval(plan_arena, Some(tuple))?; - acc.update_value(&value)?; - } + update_accumulators(&mut self.accs, self.agg_calls, tuple, plan_arena)?; } - let output = arena.output_tuple_mut(); - output.pk = None; - output.values.clear(); - output.values.reserve(accs.len()); - for mut acc in accs { - acc.evaluate()?; - output.values.push(acc.result_owned()); - } + write_aggregate_output(arena.output_tuple_mut(), &mut self.accs, [])?; self.returned = true; arena.resume(); Ok(()) diff --git a/src/execution/dql/aggregate/stream_agg.rs b/src/execution/dql/aggregate/stream_agg.rs index 38574967..f63d3138 100644 --- a/src/execution/dql/aggregate/stream_agg.rs +++ b/src/execution/dql/aggregate/stream_agg.rs @@ -14,7 +14,7 @@ use crate::errors::DatabaseError; use crate::execution::dql::aggregate::{ - create_accumulators, update_accumulators, write_aggregate_output, Accumulator, + aggregate_arg, prepare_accumulators, update_accumulators, write_aggregate_output, Accumulator, }; use crate::execution::{ build_read, ExecBuilder, ExecContext, ExecNode, ExecutorNode, ReadExecutor, @@ -25,14 +25,19 @@ use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::StorageTransaction; use crate::types::value::DataValue; +use std::borrow::Cow; use std::mem; // The optimizer selects this executor only when equal group keys are contiguous in the input. pub struct StreamAggExecutor<'a> { agg_calls: &'a [ExprRef], groupby_exprs: &'a [ExprRef], - group_keys: Option>, + // The keys of the open group, followed by the keys of the row being read. + group_keys: Vec, + in_group: bool, accs: Vec>, + carried: bool, + carried_args: Vec, input: ExecNode<'a>, } @@ -55,8 +60,11 @@ impl<'a> ReadExecutor<'a> for StreamAggExecutor<'a> { ExecNode::StreamAgg(Box::new(StreamAggExecutor { agg_calls, groupby_exprs, - group_keys: None, + group_keys: Vec::new(), + in_group: false, accs: Vec::new(), + carried: false, + carried_args: Vec::new(), input, })) } @@ -68,8 +76,10 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for StreamAggExecutor<' arena: &mut ExecContext<'_, 'a, '_, T>, plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { - self.group_keys = None; - self.accs.clear(); + self.group_keys.clear(); + self.in_group = false; + self.carried = false; + self.carried_args.clear(); self.input.reset(arena, plan_arena) } @@ -78,25 +88,32 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for StreamAggExecutor<' arena: &mut ExecContext<'_, 'a, '_, T>, plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { + let len = self.groupby_exprs.len(); + if mem::take(&mut self.carried) { + prepare_accumulators(&mut self.accs, self.agg_calls, plan_arena)?; + for (acc, arg) in self.accs.iter_mut().zip(&self.carried_args) { + acc.update_value(arg)?; + } + arena.bytes_pool().recycle(&mut self.carried_args); + } loop { if !self.input.next_tuple(arena, plan_arena)? { - let Some(group_keys) = self.group_keys.take() else { + if !mem::take(&mut self.in_group) { arena.finish(); return Ok(()); - }; + } write_aggregate_output( arena.output_tuple_mut(), - mem::take(&mut self.accs), - group_keys, + &mut self.accs, + self.group_keys.drain(..), )?; arena.resume(); return Ok(()); } let tuple = arena.result_tuple(); - let mut group_keys = Vec::with_capacity(self.groupby_exprs.len()); for expr in self.groupby_exprs { - group_keys.push( + self.group_keys.push( plan_arena .expression(*expr) .eval(plan_arena, Some(tuple))? @@ -104,24 +121,31 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for StreamAggExecutor<' ); } - match &mut self.group_keys { - None => { - self.accs = create_accumulators(self.agg_calls, plan_arena)?; - update_accumulators(&mut self.accs, self.agg_calls, tuple, plan_arena)?; - self.group_keys = Some(group_keys); - } - Some(current_keys) if current_keys == &group_keys => { - update_accumulators(&mut self.accs, self.agg_calls, tuple, plan_arena)?; - } - Some(current_keys) => { - let mut next_accs = create_accumulators(self.agg_calls, plan_arena)?; - 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.output_tuple_mut(), current_accs, group_keys)?; - arena.resume(); - return Ok(()); + if !self.in_group { + self.in_group = true; + prepare_accumulators(&mut self.accs, self.agg_calls, plan_arena)?; + update_accumulators(&mut self.accs, self.agg_calls, tuple, plan_arena)?; + } else if self.group_keys[..len] == self.group_keys[len..] { + self.group_keys.truncate(len); + update_accumulators(&mut self.accs, self.agg_calls, tuple, plan_arena)?; + } else { + let (tuple, pool) = arena.result_tuple_and_pool(); + for expr in self.agg_calls { + let arg = match aggregate_arg(*expr, tuple, plan_arena)? { + Cow::Owned(arg) => arg, + Cow::Borrowed(arg) => pool.clone_value(arg), + }; + self.carried_args.push(arg); } + self.carried = true; + // Draining the closed group's keys moves the row's keys to the front. + write_aggregate_output( + arena.output_tuple_mut(), + &mut self.accs, + self.group_keys.drain(..len), + )?; + arena.resume(); + return Ok(()); } } } diff --git a/src/execution/dql/aggregate/sum.rs b/src/execution/dql/aggregate/sum.rs index 0cdc5ea0..77adcfb0 100644 --- a/src/execution/dql/aggregate/sum.rs +++ b/src/execution/dql/aggregate/sum.rs @@ -14,12 +14,13 @@ use crate::errors::DatabaseError; use crate::execution::dql::aggregate::Accumulator; +use crate::execution::hash::FxHashSet; use crate::expression::BinaryOperator; use crate::types::evaluator::{binary_create, BinaryEvaluatorRef}; use crate::types::value::DataValue; use crate::types::LogicalType; use std::borrow::Cow; -use std::collections::HashSet; +use std::mem; /// 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> { @@ -98,20 +99,24 @@ impl Accumulator for SumAccumulator { &self.result } - fn result_owned(self: Box) -> DataValue { - self.result + fn take_result(&mut self) -> DataValue { + mem::replace(&mut self.result, DataValue::Null) + } + + fn reset(&mut self) { + self.result = DataValue::Null; } } pub struct DistinctSumAccumulator { - distinct_values: HashSet, + distinct_values: FxHashSet, inner: SumAccumulator, } impl DistinctSumAccumulator { pub fn new(ty: &LogicalType) -> Result { Ok(Self { - distinct_values: HashSet::default(), + distinct_values: FxHashSet::default(), inner: SumAccumulator::new(Cow::Borrowed(ty))?, }) } @@ -131,8 +136,13 @@ impl Accumulator for DistinctSumAccumulator { self.inner.result() } - fn result_owned(self: Box) -> DataValue { - self.inner.result + fn take_result(&mut self) -> DataValue { + self.inner.take_result() + } + + fn reset(&mut self) { + self.distinct_values.clear(); + self.inner.reset(); } } @@ -164,7 +174,7 @@ mod tests { accumulator.update_value(&value)?; } assert_eq!(accumulator.result(), &DataValue::Int32(5)); - assert_eq!(Box::new(accumulator).result_owned(), DataValue::Int32(5)); + assert_eq!(accumulator.take_result(), DataValue::Int32(5)); Ok(()) } @@ -175,7 +185,7 @@ mod tests { accumulator.update_value(&value)?; } assert_eq!(accumulator.result(), &DataValue::Int32(5)); - assert_eq!(Box::new(accumulator).result_owned(), DataValue::Int32(5)); + assert_eq!(accumulator.take_result(), DataValue::Int32(5)); Ok(()) } } diff --git a/src/execution/dql/aggregate/total.rs b/src/execution/dql/aggregate/total.rs index ac8c27fa..0467a04c 100644 --- a/src/execution/dql/aggregate/total.rs +++ b/src/execution/dql/aggregate/total.rs @@ -15,20 +15,21 @@ use crate::errors::DatabaseError; use crate::execution::dql::aggregate::sum::text_as_number; use crate::execution::dql::aggregate::Accumulator; +use crate::execution::hash::FxHashSet; use crate::types::value::DataValue; use crate::types::LogicalType; -use std::collections::HashSet; +use std::mem; /// SQLite `total()`: always a double, and 0.0 rather than NULL when nothing is summed. pub struct TotalAccumulator { - distinct_values: Option>, + distinct_values: Option>, result: DataValue, } impl TotalAccumulator { pub fn new(distinct: bool) -> Self { Self { - distinct_values: distinct.then(HashSet::new), + distinct_values: distinct.then(FxHashSet::default), result: DataValue::Float64(0.0.into()), } } @@ -61,7 +62,14 @@ impl Accumulator for TotalAccumulator { &self.result } - fn result_owned(self: Box) -> DataValue { - self.result + fn take_result(&mut self) -> DataValue { + mem::replace(&mut self.result, DataValue::Null) + } + + fn reset(&mut self) { + if let Some(values) = &mut self.distinct_values { + values.clear(); + } + self.result = DataValue::Float64(0.0.into()); } } diff --git a/src/execution/dql/index_scan.rs b/src/execution/dql/index_scan.rs index 33a4c754..b768ce96 100644 --- a/src/execution/dql/index_scan.rs +++ b/src/execution/dql/index_scan.rs @@ -149,7 +149,7 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for IndexScan<'a> { ranges, *with_pk, deserializers, - self.info.covered_deserializers.is_some(), + self.info.covered, self.info.cover_mapping.as_deref(), )?; state.iters[self.iter] = StorageIter::Index(Box::new(iter)); @@ -228,7 +228,7 @@ mod tests { sort_option: SortOption::None, lookup: None, residual_predicate: None, - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, diff --git a/src/execution/dql/join/hash/full_join.rs b/src/execution/dql/join/hash/full_join.rs index bf170efd..7b3d12c5 100644 --- a/src/execution/dql/join/hash/full_join.rs +++ b/src/execution/dql/join/hash/full_join.rs @@ -14,9 +14,8 @@ use crate::errors::DatabaseError; use crate::execution::dql::join::hash::{ - filter, join_rows, JoinProbeState, LeftDropState, LeftDropTuples, ProbeState, + filter, join_rows, BuildRow, JoinProbeState, LeftDropState, ProbeState, }; -use crate::execution::dql::join::hash_join::BuildState; use crate::planner::ExprRef; use crate::planner::MetaArena; use crate::types::bitset::BitSet; @@ -33,39 +32,13 @@ impl JoinProbeState for FullJoinState { fn probe_next( &mut self, probe_state: &mut ProbeState, - build_state: Option<&mut BuildState>, + build: &[BuildRow], filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), ) -> Result, DatabaseError> { - if probe_state.is_keys_has_null { - if probe_state.emitted_unmatched { - probe_state.finished = true; - return Ok(None); - } - probe_state.emitted_unmatched = true; - probe_state.finished = true; - 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 { - if probe_state.emitted_unmatched { - probe_state.finished = true; - return Ok(None); - } - probe_state.emitted_unmatched = true; - probe_state.finished = true; - 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() { - let (i, build_tuple) = &build_state.tuples[probe_state.index]; - probe_state.index += 1; + while let Some(i) = probe_state.next_row { + probe_state.next_row = build[i].next; + let build_tuple = &build[i].tuple; if let Some(filter_expr) = filter_expr { let full_values = SplitTupleRef::new(build_tuple, &probe_state.probe_tuple); @@ -73,7 +46,7 @@ impl JoinProbeState for FullJoinState { continue; } } - self.bits.insert(*i); + self.bits.insert(i); probe_state.produced = true; return Ok(Some(join_rows( build_tuple, @@ -101,25 +74,14 @@ impl JoinProbeState for FullJoinState { ) -> Result, DatabaseError> { let full_schema_len = self.right_schema_len + self.left_schema_len; - loop { - if let Some(LeftDropTuples { tuples }) = left_drop_state.current.as_mut() { - for (i, mut left_tuple) in tuples.by_ref() { - if self.bits.contains(i) { - continue; - } - left_tuple.resize(full_schema_len, DataValue::Null); - return Ok(Some(left_tuple)); - } - left_drop_state.current = None; + for (i, row) in left_drop_state.rows.by_ref() { + if self.bits.contains(i) { + continue; } - - let Some((_, state)) = left_drop_state.states.next() else { - return Ok(None); - }; - - left_drop_state.current = Some(LeftDropTuples { - tuples: state.tuples.into_iter(), - }); + let mut left_tuple = row.tuple; + left_tuple.resize(full_schema_len, DataValue::Null); + return Ok(Some(left_tuple)); } + Ok(None) } } diff --git a/src/execution/dql/join/hash/inner_join.rs b/src/execution/dql/join/hash/inner_join.rs index 10d168d1..3498d5c9 100644 --- a/src/execution/dql/join/hash/inner_join.rs +++ b/src/execution/dql/join/hash/inner_join.rs @@ -13,8 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::execution::dql::join::hash::{filter, join_rows, JoinProbeState, ProbeState}; -use crate::execution::dql::join::hash_join::BuildState; +use crate::execution::dql::join::hash::{filter, join_rows, BuildRow, JoinProbeState, ProbeState}; use crate::planner::ExprRef; use crate::planner::MetaArena; use crate::types::tuple::{SplitTupleRef, TupleImpl}; @@ -25,30 +24,20 @@ impl JoinProbeState for InnerJoinState { fn probe_next( &mut self, probe_state: &mut ProbeState, - build_state: Option<&mut BuildState>, + build: &[BuildRow], filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), ) -> Result, DatabaseError> { - if probe_state.is_keys_has_null { - probe_state.finished = true; - return Ok(None); - } - - let Some(build_state) = build_state else { - probe_state.finished = true; - return Ok(None); - }; - - while probe_state.index < build_state.tuples.len() { - let (_, build_tuple) = &build_state.tuples[probe_state.index]; - probe_state.index += 1; + while let Some(i) = probe_state.next_row { + probe_state.next_row = build[i].next; + let build_tuple = &build[i].tuple; if let Some(filter_expr) = filter_expr { let full_values = SplitTupleRef::new(build_tuple, &probe_state.probe_tuple); if !filter(&full_values, filter_expr, plan_arena)? { continue; } } - let is_last = probe_state.index == build_state.tuples.len(); + let is_last = probe_state.next_row.is_none(); if 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 5933732e..c9992c22 100644 --- a/src/execution/dql/join/hash/left_join.rs +++ b/src/execution/dql/join/hash/left_join.rs @@ -14,9 +14,8 @@ use crate::errors::DatabaseError; use crate::execution::dql::join::hash::{ - filter, join_rows, JoinProbeState, LeftDropState, LeftDropTuples, ProbeState, + filter, join_rows, BuildRow, JoinProbeState, LeftDropState, ProbeState, }; -use crate::execution::dql::join::hash_join::BuildState; use crate::planner::ExprRef; use crate::planner::MetaArena; use crate::types::bitset::BitSet; @@ -33,23 +32,13 @@ impl JoinProbeState for LeftJoinState { fn probe_next( &mut self, probe_state: &mut ProbeState, - build_state: Option<&mut BuildState>, + build: &[BuildRow], filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), ) -> Result, DatabaseError> { - if probe_state.is_keys_has_null { - probe_state.finished = true; - return Ok(None); - } - - let Some(build_state) = build_state else { - probe_state.finished = true; - return Ok(None); - }; - - while probe_state.index < build_state.tuples.len() { - let (i, build_tuple) = &build_state.tuples[probe_state.index]; - probe_state.index += 1; + while let Some(i) = probe_state.next_row { + probe_state.next_row = build[i].next; + let build_tuple = &build[i].tuple; if let Some(filter_expr) = filter_expr { let full_values = SplitTupleRef::new(build_tuple, &probe_state.probe_tuple); @@ -57,7 +46,7 @@ impl JoinProbeState for LeftJoinState { continue; } } - self.bits.insert(*i); + self.bits.insert(i); return Ok(Some(join_rows( build_tuple, &mut probe_state.probe_tuple, @@ -77,25 +66,14 @@ impl JoinProbeState for LeftJoinState { ) -> Result, DatabaseError> { let full_schema_len = self.right_schema_len + self.left_schema_len; - loop { - if let Some(LeftDropTuples { tuples }) = left_drop_state.current.as_mut() { - for (i, mut left_tuple) in tuples.by_ref() { - if self.bits.contains(i) { - continue; - } - left_tuple.resize(full_schema_len, DataValue::Null); - return Ok(Some(left_tuple)); - } - left_drop_state.current = None; + for (i, row) in left_drop_state.rows.by_ref() { + if self.bits.contains(i) { + continue; } - - let Some((_, state)) = left_drop_state.states.next() else { - return Ok(None); - }; - - left_drop_state.current = Some(LeftDropTuples { - tuples: state.tuples.into_iter(), - }); + let mut left_tuple = row.tuple; + left_tuple.resize(full_schema_len, DataValue::Null); + return Ok(Some(left_tuple)); } + Ok(None) } } diff --git a/src/execution/dql/join/hash/mod.rs b/src/execution/dql/join/hash/mod.rs index a1f28364..20efafae 100644 --- a/src/execution/dql/join/hash/mod.rs +++ b/src/execution/dql/join/hash/mod.rs @@ -22,37 +22,36 @@ use crate::execution::dql::join::hash::full_join::FullJoinState; use crate::execution::dql::join::hash::inner_join::InnerJoinState; use crate::execution::dql::join::hash::left_join::LeftJoinState; use crate::execution::dql::join::hash::right_join::RightJoinState; -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, TupleImpl, TupleLike}; use crate::types::value::DataValue; -use std::collections::hash_map::IntoIter as HashMapIntoIter; +use std::iter::Enumerate; +use std::vec::IntoIter; + +/// A build row. Build rows are kept in input order, and their index is the row number. +pub(crate) struct BuildRow { + pub(crate) tuple: TupleImpl, + pub(crate) next: Option, +} pub(crate) struct ProbeState { - pub(crate) is_keys_has_null: bool, pub(crate) probe_tuple: TupleImpl, - pub(crate) index: usize, + pub(crate) next_row: Option, pub(crate) produced: bool, pub(crate) finished: bool, pub(crate) emitted_unmatched: bool, } pub(crate) struct LeftDropState { - pub(crate) states: HashMapIntoIter, BuildState>, - pub(crate) current: Option, -} - -pub(crate) struct LeftDropTuples { - pub(crate) tuples: std::vec::IntoIter<(usize, TupleImpl)>, + pub(crate) rows: Enumerate>, } pub(crate) trait JoinProbeState { fn probe_next( &mut self, probe_state: &mut ProbeState, - build_state: Option<&mut BuildState>, + build: &[BuildRow], filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), ) -> Result, DatabaseError>; @@ -78,22 +77,22 @@ impl JoinProbeState for JoinProbeStateImpl { fn probe_next( &mut self, probe_state: &mut ProbeState, - build_state: Option<&mut BuildState>, + build: &[BuildRow], filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), ) -> Result, DatabaseError> { match self { JoinProbeStateImpl::Inner(state) => { - state.probe_next(probe_state, build_state, filter_expr, plan_arena) + state.probe_next(probe_state, build, filter_expr, plan_arena) } JoinProbeStateImpl::Left(state) => { - state.probe_next(probe_state, build_state, filter_expr, plan_arena) + state.probe_next(probe_state, build, filter_expr, plan_arena) } JoinProbeStateImpl::Right(state) => { - state.probe_next(probe_state, build_state, filter_expr, plan_arena) + state.probe_next(probe_state, build, filter_expr, plan_arena) } JoinProbeStateImpl::Full(state) => { - state.probe_next(probe_state, build_state, filter_expr, plan_arena) + state.probe_next(probe_state, build, filter_expr, plan_arena) } } } diff --git a/src/execution/dql/join/hash/right_join.rs b/src/execution/dql/join/hash/right_join.rs index 8f52a02e..db6e5048 100644 --- a/src/execution/dql/join/hash/right_join.rs +++ b/src/execution/dql/join/hash/right_join.rs @@ -13,8 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::execution::dql::join::hash::{filter, join_rows, JoinProbeState, ProbeState}; -use crate::execution::dql::join::hash_join::BuildState; +use crate::execution::dql::join::hash::{filter, join_rows, BuildRow, JoinProbeState, ProbeState}; use crate::planner::ExprRef; use crate::planner::MetaArena; use crate::types::tuple::{SplitTupleRef, TupleImpl}; @@ -28,39 +27,13 @@ impl JoinProbeState for RightJoinState { fn probe_next( &mut self, probe_state: &mut ProbeState, - build_state: Option<&mut BuildState>, + build: &[BuildRow], filter_expr: Option<&ExprRef>, plan_arena: &(dyn MetaArena + '_), ) -> Result, DatabaseError> { - if probe_state.is_keys_has_null { - if probe_state.emitted_unmatched { - probe_state.finished = true; - return Ok(None); - } - probe_state.emitted_unmatched = true; - probe_state.finished = true; - 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 { - if probe_state.emitted_unmatched { - probe_state.finished = true; - return Ok(None); - } - probe_state.emitted_unmatched = true; - probe_state.finished = true; - 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() { - let (_, build_tuple) = &build_state.tuples[probe_state.index]; - probe_state.index += 1; + while let Some(i) = probe_state.next_row { + probe_state.next_row = build[i].next; + let build_tuple = &build[i].tuple; if let Some(filter_expr) = filter_expr { let full_values = SplitTupleRef::new(build_tuple, &probe_state.probe_tuple); diff --git a/src/execution/dql/join/hash_join.rs b/src/execution/dql/join/hash_join.rs index 03d11d0e..85b40ec4 100644 --- a/src/execution/dql/join/hash_join.rs +++ b/src/execution/dql/join/hash_join.rs @@ -18,9 +18,9 @@ use crate::execution::dql::join::hash::inner_join::InnerJoinState; use crate::execution::dql::join::hash::left_join::LeftJoinState; use crate::execution::dql::join::hash::right_join::RightJoinState; use crate::execution::dql::join::hash::{ - JoinProbeState, JoinProbeStateImpl, LeftDropState, ProbeState, + BuildRow, JoinProbeState, JoinProbeStateImpl, LeftDropState, ProbeState, }; -use crate::execution::dql::sort::BumpVec; +use crate::execution::hash::FxBuildHasher; use crate::execution::{ build_read, ExecBuilder, ExecContext, ExecNode, ExecutorNode, ReadExecutor, }; @@ -32,9 +32,8 @@ use crate::storage::StorageTransaction; use crate::types::bitset::BitSet; use crate::types::tuple::TupleImpl; use crate::types::value::DataValue; -use bumpalo::Bump; -use std::collections::HashMap; -use std::mem::transmute; +use hashbrown::HashTable; +use std::hash::BuildHasher; pub struct HashJoin<'a> { state: HashJoinState, @@ -46,16 +45,42 @@ pub struct HashJoin<'a> { right_schema_len: usize, left_input: ExecNode<'a>, right_input: ExecNode<'a>, - bump: Box, init_error: Option, } +/// A distinct build key and the chain of its rows in `BuildTable::rows`. +struct BuildKey { + // Where the key starts in `BuildTable::keys`. + offset: usize, + first_row: usize, + last_row: usize, +} + +/// The build side: each distinct key once in `keys`, `on_left_keys.len()` values per key. +struct BuildTable { + hasher: FxBuildHasher, + table: HashTable, + keys: Vec, + rows: Vec, +} + +impl BuildTable { + fn first_row(&self, key: &[DataValue]) -> Option { + let keys = &self.keys; + self.table + .find(self.hasher.hash_one(key), |build_key| { + &keys[build_key.offset..][..key.len()] == key + }) + .map(|build_key| build_key.first_row) + } +} + enum HashJoinState { Build, Probe { - build_map: HashMap, BuildState>, + build_table: Box, join_impl: JoinProbeStateImpl, - probe_buf: BumpVec<'static, DataValue>, + probe_buf: Vec, probe_state: Option, }, LeftDrop { @@ -101,39 +126,20 @@ impl<'a> HashJoin<'a> { right_schema_len, left_input, right_input, - bump: Box::::default(), init_error, } } } impl<'a> HashJoin<'a> { - #[allow(clippy::mutable_key_type)] - fn own_bump_vec(buf: BumpVec<'_, DataValue>) -> BumpVec<'static, DataValue> { - unsafe { transmute::, BumpVec<'static, DataValue>>(buf) } - } - - #[allow(clippy::mutable_key_type)] - fn own_build_map( - build_map: HashMap, BuildState>, - ) -> HashMap, BuildState> { - unsafe { - transmute::< - HashMap, BuildState>, - HashMap, BuildState>, - >(build_map) - } - } - fn eval_keys( on_keys: &[ExprRef], tuple: &TupleImpl, - build_buf: &mut BumpVec<'_, DataValue>, + buf: &mut Vec, plan_arena: &(dyn MetaArena + '_), ) -> Result<(), DatabaseError> { - build_buf.clear(); for expr in on_keys { - build_buf.push( + buf.push( plan_arena .expression(*expr) .eval(plan_arena, Some(tuple))? @@ -155,31 +161,40 @@ impl<'a> HashJoin<'a> { // 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); - let mut build_count = 0usize; + let hasher = FxBuildHasher::default(); + let mut table: HashTable = HashTable::new(); + let mut keys = Vec::new(); + let mut rows: Vec = Vec::new(); + let len = self.on_left_keys.len(); while self.left_input.next_tuple(arena, plan_arena)? { 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) { - None => { - let key = std::mem::replace(&mut build_buf, BumpVec::new_in(&self.bump)); - build_map.insert( - Self::own_bump_vec(key), - BuildState { - tuples: vec![(build_count, tuple)], - }, - ); - } - Some(BuildState { tuples, .. }) => tuples.push((build_count, tuple)), + let row = rows.len(); + // Evaluate the key in place, and drop it again if the key is already there. + let start = keys.len(); + Self::eval_keys(&self.on_left_keys, &tuple, &mut keys, plan_arena)?; + let hash = hasher.hash_one(&keys[start..]); + let found = table.find_mut(hash, |build_key| { + keys[build_key.offset..][..len] == keys[start..] + }); + + if let Some(build_key) = found { + rows[build_key.last_row].next = Some(row); + build_key.last_row = row; + keys.truncate(start); + } else { + let build_key = BuildKey { + offset: start, + first_row: row, + last_row: row, + }; + table.insert_unique(hash, build_key, |build_key| { + hasher.hash_one(&keys[build_key.offset..][..len]) + }); } - build_count += 1; + rows.push(BuildRow { tuple, next: None }); } + let build_count = rows.len(); self.state = HashJoinState::Probe { join_impl: Self::create_join_impl( @@ -188,11 +203,13 @@ impl<'a> HashJoin<'a> { self.right_schema_len, build_count, ), - probe_buf: Self::own_bump_vec(BumpVec::with_capacity_in( - self.on_right_keys.len(), - &self.bump, - )), - build_map: Self::own_build_map(build_map), + probe_buf: Vec::with_capacity(self.on_right_keys.len()), + build_table: Box::new(BuildTable { + hasher, + table, + keys, + rows, + }), probe_state: None, }; Ok(()) @@ -222,13 +239,6 @@ impl<'a> HashJoin<'a> { } } -// 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, TupleImpl)>, -} - impl<'a> ReadExecutor<'a> for HashJoin<'a> { type Input = (&'a JoinOperator, &'a LogicalPlan, &'a LogicalPlan); @@ -259,7 +269,6 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for HashJoin<'a> { plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { self.state = HashJoinState::Build; - self.bump.reset(); self.left_input.reset(arena, plan_arena)?; self.right_input.reset(arena, plan_arena) } @@ -280,7 +289,7 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for HashJoin<'a> { match state { HashJoinState::Build => unreachable!("hash join must be initialized before probe"), HashJoinState::Probe { - mut build_map, + build_table, mut join_impl, mut probe_buf, mut probe_state, @@ -291,16 +300,22 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for HashJoin<'a> { break true; } let tuple = arena.take_tuple(); + probe_buf.clear(); Self::eval_keys( &self.on_right_keys, &tuple, &mut probe_buf, plan_arena, )?; + // A null key matches nothing. + let next_row = if probe_buf.iter().any(DataValue::is_null) { + None + } else { + build_table.first_row(&probe_buf) + }; probe_state = Some(ProbeState { - is_keys_has_null: probe_buf.iter().any(DataValue::is_null), probe_tuple: tuple, - index: 0, + next_row, produced: false, finished: false, emitted_unmatched: false, @@ -310,15 +325,10 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for HashJoin<'a> { let Some(probe) = probe_state.as_mut() else { continue; }; - let build_state = if probe.is_keys_has_null { - None - } else { - build_map.get_mut(&probe_buf) - }; if let Some(tuple) = join_impl.probe_next( probe, - build_state, + &build_table.rows, self.filter.as_ref(), plan_arena, )? { @@ -326,7 +336,7 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for HashJoin<'a> { probe_state = None; } self.state = HashJoinState::Probe { - build_map, + build_table, join_impl, probe_buf, probe_state, @@ -344,8 +354,7 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for HashJoin<'a> { state = HashJoinState::LeftDrop { join_impl, left_drop: LeftDropState { - states: build_map.into_iter(), - current: None, + rows: build_table.rows.into_iter().enumerate(), }, }; } diff --git a/src/execution/dql/set_membership.rs b/src/execution/dql/set_membership.rs index 70e5aaeb..b5577bc5 100644 --- a/src/execution/dql/set_membership.rs +++ b/src/execution/dql/set_membership.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; +use crate::execution::hash::FxBuildHasher; use crate::execution::{ build_read, ExecBuilder, ExecContext, ExecNode, ExecutorNode, ReadExecutor, }; @@ -21,13 +22,22 @@ use crate::planner::LogicalPlan; use crate::planner::{ExecMetaArena, MetaArena}; use crate::storage::StorageTransaction; use crate::types::tuple::TupleImpl; -use std::collections::HashMap; +use crate::types::value::DataValue; +use hashbrown::HashTable; +use std::hash::{BuildHasher, Hash, Hasher}; + +struct RightRow { + offset: usize, + count: usize, +} pub struct SetMembership<'a> { kind: SetMembershipKind, left_input: ExecNode<'a>, right_input: ExecNode<'a>, - right_counts: HashMap, + hasher: FxBuildHasher, + right_rows: Vec, + right_counts: HashTable, built: bool, } @@ -45,7 +55,9 @@ impl<'a> ReadExecutor<'a> for SetMembership<'a> { kind, left_input, right_input, - right_counts: HashMap::new(), + hasher: FxBuildHasher::default(), + right_rows: Vec::new(), + right_counts: HashTable::new(), built: false, })) } @@ -58,6 +70,7 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for SetMembership<'a> { plan_arena: &mut ExecMetaArena, ) -> Result<(), DatabaseError> { self.built = false; + self.right_rows.clear(); self.right_counts.clear(); self.left_input.reset(arena, plan_arena)?; self.right_input.reset(arena, plan_arena) @@ -70,11 +83,20 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for SetMembership<'a> { ) -> Result<(), DatabaseError> { if !self.built { while self.right_input.next_tuple(arena, plan_arena)? { - if let Some(count) = self.right_counts.get_mut(arena.result_tuple()) { - *count += 1; - } else { - self.right_counts.insert(arena.take_tuple(), 1); + let tuple = arena.result_tuple(); + let width = tuple.len(); + let hash = hash_row(&self.hasher, tuple.iter()); + if let Some(row) = self.find_mut(hash, tuple) { + row.count += 1; + continue; } + let offset = self.right_rows.len(); + self.right_rows.extend(tuple.iter().cloned()); + let (hasher, rows) = (&self.hasher, &self.right_rows); + self.right_counts + .insert_unique(hash, RightRow { offset, count: 1 }, |row| { + hash_row(hasher, &rows[row.offset..][..width]) + }); } self.built = true; } @@ -99,15 +121,30 @@ impl<'a, T: StorageTransaction + 'a> ExecutorNode<'a, T> for SetMembership<'a> { } } +fn hash_row<'v>(hasher: &FxBuildHasher, values: impl IntoIterator) -> u64 { + let mut state = hasher.build_hasher(); + for value in values { + value.hash(&mut state); + } + state.finish() +} + impl SetMembership<'_> { + fn find_mut(&mut self, hash: u64, tuple: &TupleImpl) -> Option<&mut RightRow> { + let rows = &self.right_rows; + self.right_counts.find_mut(hash, |row| { + rows[row.offset..][..tuple.len()].iter().eq(tuple.iter()) + }) + } + fn consume_right_match(&mut self, tuple: &TupleImpl) -> bool { - if let Some(count) = self.right_counts.get_mut(tuple) { - if *count > 0 { - *count -= 1; - return true; + let hash = hash_row(&self.hasher, tuple.iter()); + match self.find_mut(hash, tuple) { + Some(row) if row.count > 0 => { + row.count -= 1; + true } + _ => false, } - - false } } diff --git a/src/execution/hash.rs b/src/execution/hash.rs new file mode 100644 index 00000000..ad0a1864 --- /dev/null +++ b/src/execution/hash.rs @@ -0,0 +1,102 @@ +// 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. + +//! A fast hasher for the hash tables of execution operators. Unlike std's SipHash it does not +//! resist crafted collisions, which is acceptable for keys taken from the database's own rows. + +use std::collections::HashSet; +use std::hash::{BuildHasherDefault, Hasher}; + +pub(crate) type FxBuildHasher = BuildHasherDefault; +pub(crate) type FxHashSet = HashSet; + +const SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95; + +/// The multiply-rotate hash of rustc and Firefox, one 64-bit word at a time. +#[derive(Default, Clone, Copy)] +pub(crate) struct FxHasher { + hash: u64, +} + +impl FxHasher { + #[inline] + fn add(&mut self, word: u64) { + self.hash = (self.hash.rotate_left(5) ^ word).wrapping_mul(SEED); + } +} + +impl Hasher for FxHasher { + #[inline] + fn write(&mut self, bytes: &[u8]) { + let (words, rest) = bytes.as_chunks::<8>(); + for word in words { + self.add(u64::from_le_bytes(*word)); + } + if !rest.is_empty() { + let mut word = [0; 8]; + word[..rest.len()].copy_from_slice(rest); + self.add(u64::from_le_bytes(word)); + } + } + + #[inline] + fn write_u8(&mut self, i: u8) { + self.add(i as u64); + } + + #[inline] + fn write_u16(&mut self, i: u16) { + self.add(i as u64); + } + + #[inline] + fn write_u32(&mut self, i: u32) { + self.add(i as u64); + } + + #[inline] + fn write_u64(&mut self, i: u64) { + self.add(i); + } + + #[inline] + fn write_usize(&mut self, i: usize) { + self.add(i as u64); + } + + // The multiply leaves the best bits on top, while hash tables index with the low bits. + #[inline] + fn finish(&self) -> u64 { + self.hash.rotate_left(26) + } +} + +// GRCOV_EXCL_START +#[cfg(test)] +mod tests { + use super::FxBuildHasher; + use std::hash::BuildHasher; + + #[test] + fn hashes_every_byte() { + let hasher = FxBuildHasher::default(); + assert_eq!(hasher.hash_one("kite_sql_1"), hasher.hash_one("kite_sql_1")); + assert_ne!(hasher.hash_one("kite_sql_1"), hasher.hash_one("kite_sql_2")); + assert_ne!( + hasher.hash_one(&[1u8; 9][..]), + hasher.hash_one(&[1u8; 8][..]) + ); + } +} +// GRCOV_EXCL_STOP diff --git a/src/execution/mod.rs b/src/execution/mod.rs index 6e045bda..ffaf29b9 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -16,6 +16,7 @@ pub(crate) mod ddl; mod ddl_apply; pub(crate) mod dml; pub(crate) mod dql; +pub(crate) mod hash; pub(crate) mod runs; #[cfg(feature = "spill")] pub(crate) mod spill; @@ -79,6 +80,7 @@ use crate::planner::{ExecMetaArena, ExprRef, LogicalPlan, PlanArena, PlanKeeper} use crate::storage::table_codec::TableCodec; use crate::storage::{IndexIter, StorageTransaction, TableCache, TableIter, TupleIter, ViewIter}; use crate::types::index::RuntimeIndexProbe; +use crate::types::serialize::BytesPool; use crate::types::tuple::{ConcatPool, Input, Schema, Tuple, TupleId, TupleImpl}; use crate::types::value::DataValue; use std::borrow::Cow; @@ -786,6 +788,19 @@ impl<'c, 'a, 't, T: StorageTransaction + 'a> ExecContext<'c, 'a, 't, T> { &self.buffer.result.tuple } + #[inline] + pub(crate) fn bytes_pool(&mut self) -> &mut BytesPool { + self.buffer.table_codec.bytes_pool() + } + + #[inline] + pub(crate) fn result_tuple_and_pool(&mut self) -> (&TupleImpl, &mut BytesPool) { + ( + &self.buffer.result.tuple, + self.buffer.table_codec.bytes_pool(), + ) + } + #[inline] pub(crate) fn result_mut(&mut self) -> &mut TupleImpl { self.buffer.result_mut() diff --git a/src/expression/range_detacher.rs b/src/expression/range_detacher.rs index 298e6cae..b838a5cd 100644 --- a/src/expression/range_detacher.rs +++ b/src/expression/range_detacher.rs @@ -19,11 +19,12 @@ use crate::iter_ext::Itertools; use crate::planner::ExprRef; use crate::planner::MetaArena; use crate::planner::ScalarQueryRef; +use crate::types::evaluator::binary_create; use crate::types::index::IndexMetaRef; use crate::types::value::{DataValue, DataValueRef}; use crate::types::{ColumnId, LogicalType}; use kite_sql_serde_macros::ReferenceSerialization; -use std::borrow::Borrow; +use std::borrow::{Borrow, Cow}; use std::cmp::Ordering; use std::collections::Bound; use std::fmt::Formatter; @@ -33,11 +34,214 @@ use std::{fmt, mem}; /// Tips: The NotEq case is ignored because it makes expression composition very complex /// - [`Range::Scope`]: #[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] -pub enum Range { - Scope { min: Bound, max: Bound }, - Eq(T), +pub enum Range { + Scope { + min: Bound, + max: Bound, + }, + Eq(RangeValue), Dummy, - SortedRanges(Vec>), + SortedRanges(Vec), +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] +pub enum RangeValue { + Value(DataValue), + Computed(Box), +} + +#[derive(PartialEq)] +pub(crate) enum RangeValueRef<'a> { + Value(&'a DataValue), + Computed(&'a ComputedValue), +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] +pub struct ComputedValue { + pub(crate) prefix: Option>, + pub(crate) op: BinaryOperator, + pub(crate) left: DataValue, + pub(crate) right: DataValue, + pub(crate) eval_ty: LogicalType, + pub(crate) ty: LogicalType, +} + +impl From for RangeValue { + fn from(value: DataValue) -> Self { + RangeValue::Value(value) + } +} + +impl RangeValue { + const NULL: RangeValue = RangeValue::Value(DataValue::Null); + + pub(crate) fn value(&self) -> Result<&DataValue, DatabaseError> { + match self { + RangeValue::Value(value) => Ok(value), + RangeValue::Computed(_) => Err(DatabaseError::InvalidValue( + "range value used before its range is bound".to_string(), + )), + } + } + + pub(crate) fn bound(bound: &Bound) -> Result, DatabaseError> { + Ok(match bound { + Bound::Included(value) => Bound::Included(value.value()?), + Bound::Excluded(value) => Bound::Excluded(value.value()?), + Bound::Unbounded => Bound::Unbounded, + }) + } + + pub(crate) fn to_ref(&self) -> RangeValueRef<'_> { + match self { + RangeValue::Value(value) => RangeValueRef::Value(value), + RangeValue::Computed(computed) => RangeValueRef::Computed(computed), + } + } + + pub(crate) fn iter(&self) -> impl Iterator> + '_ { + let (values, computed) = match self { + RangeValue::Value(value) => (value.as_slice(), None), + RangeValue::Computed(computed) => ( + computed.prefix.as_deref().unwrap_or_default(), + Some(computed.as_ref()), + ), + }; + values + .iter() + .map(RangeValueRef::Value) + .chain(computed.map(RangeValueRef::Computed)) + } + + pub(crate) fn is_null(&self) -> bool { + matches!(self, RangeValue::Value(value) if value.is_null()) + } + + pub(crate) fn has_parameter(&self) -> bool { + match self { + RangeValue::Value(value) => value.has_parameter(), + RangeValue::Computed(_) => true, + } + } + + pub(crate) fn is_tuple(&self) -> bool { + match self { + RangeValue::Value(value) => value.is_tuple(), + RangeValue::Computed(computed) => computed.prefix.is_some(), + } + } + + fn cast(self, to: &LogicalType) -> Result { + Ok(match self { + RangeValue::Value(value) => RangeValue::Value(value.cast(to)?), + RangeValue::Computed(mut computed) => { + if &computed.ty != to { + computed.ty = to.clone(); + } + RangeValue::Computed(computed) + } + }) + } + + fn prefixed(self, prefix: &[&DataValue]) -> Self { + let mut values = prefix.iter().map(|value| (*value).clone()).collect_vec(); + match self { + RangeValue::Value(value) => { + values.push(value); + RangeValue::Value(DataValue::Tuple(values)) + } + RangeValue::Computed(mut computed) => { + values.extend(computed.prefix.take().into_iter().flatten()); + computed.prefix = Some(values); + RangeValue::Computed(computed) + } + } + } + + 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 { + RangeValue::Value(value) => value.bind_parameters(params, init_values), + RangeValue::Computed(computed) => { + let Some(mut value) = computed.compute(params, init_values)? else { + return Ok(true); + }; + let has_null = value.bind_parameters(params, init_values)?; + *self = RangeValue::Value(value); + Ok(has_null) + } + } + } + + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (RangeValue::Value(left), RangeValue::Value(right)) => left.partial_cmp(right), + _ => None, + } + } +} + +impl ComputedValue { + /// Computes the value, moving the operands out; `None` when the result is NULL. + fn compute<'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>, + { + let mut left = mem::replace(&mut self.left, DataValue::Null); + let mut right = mem::replace(&mut self.right, DataValue::Null); + left.bind_parameters(params, init_values)?; + right.bind_parameters(params, init_values)?; + let value = binary_create(Cow::Borrowed(&self.eval_ty), self.op)? + .binary_eval( + Cow::Owned(left.cast(&self.eval_ty)?), + &right.cast(&self.eval_ty)?, + )? + .cast(&self.ty)?; + if value.is_null() { + return Ok(None); + } + Ok(Some(match self.prefix.take() { + Some(mut values) => { + values.push(value); + DataValue::Tuple(values) + } + None => value, + })) + } +} + +impl fmt::Display for RangeValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + RangeValue::Value(value) => write!(f, "{value}"), + RangeValue::Computed(computed) => { + let ComputedValue { + prefix, + op, + left, + right, + .. + } = computed.as_ref(); + write!(f, "(")?; + for value in prefix.iter().flatten() { + write!(f, "{value}, ")?; + } + write!(f, "{left} {op} {right})") + } + } + } } #[derive(Debug, PartialEq, Eq, Clone, Hash, ReferenceSerialization)] @@ -106,65 +310,65 @@ impl TreeNode { } } -pub(crate) enum EqRangeItem<'a, T> { +pub(crate) enum EqRangeItem<'a> { Eq(&'a DataValue), - Sorted(&'a [Range]), + Sorted(&'a [Range]), } -pub(crate) trait EqRangeInput> { +pub(crate) trait EqRangeInput { fn len(&self) -> usize; - fn get(&self, level: usize) -> Option>; + fn get(&self, level: usize) -> Option>; } -impl> EqRangeInput for [Range] { +impl EqRangeInput for [Range] { fn len(&self) -> usize { - <[Range]>::len(self) + <[Range]>::len(self) } - fn get(&self, level: usize) -> Option> { - <[Range]>::get(self, level).and_then(|range| match range { - Range::Eq(value) => Some(EqRangeItem::Eq(value.borrow())), + fn get(&self, level: usize) -> Option> { + <[Range]>::get(self, level).and_then(|range| match range { + Range::Eq(RangeValue::Value(value)) => Some(EqRangeItem::Eq(value)), Range::SortedRanges(ranges) => Some(EqRangeItem::Sorted(ranges)), _ => None, }) } } -impl, const N: usize> EqRangeInput for [Range; N] { +impl EqRangeInput for [Range; N] { fn len(&self) -> usize { N } - fn get(&self, level: usize) -> Option> { - <[Range] as EqRangeInput>::get(self, level) + fn get(&self, level: usize) -> Option> { + <[Range] as EqRangeInput>::get(self, level) } } -impl> EqRangeInput for Vec> { +impl EqRangeInput for Vec { fn len(&self) -> usize { self.as_slice().len() } - fn get(&self, level: usize) -> Option> { - <[Range] as EqRangeInput>::get(self, level) + fn get(&self, level: usize) -> Option> { + <[Range] as EqRangeInput>::get(self, level) } } -impl EqRangeInput for [DataValue] { +impl EqRangeInput for [DataValue] { fn len(&self) -> usize { <[DataValue]>::len(self) } - fn get(&self, level: usize) -> Option> { + fn get(&self, level: usize) -> Option> { <[DataValue]>::get(self, level).map(EqRangeItem::Eq) } } -fn build_tree<'a, T: Borrow + 'a, I: EqRangeInput + ?Sized>( - ranges: &'a I, +fn build_tree( + ranges: &I, current_level: usize, -) -> Option> { - fn build_subtree<'a, T: Borrow + 'a, I: EqRangeInput + ?Sized>( +) -> Option> { + fn build_subtree<'a, I: EqRangeInput + ?Sized>( ranges: &'a I, value: &'a DataValue, current_level: usize, @@ -182,11 +386,11 @@ fn build_tree<'a, T: Borrow + 'a, I: EqRangeInput + ?Sized>( match ranges.get(current_level)? { EqRangeItem::Sorted(children) => { for child in children { - let Range::Eq(value) = child else { + let Range::Eq(RangeValue::Value(value)) = child else { return None; }; root.children - .push(build_subtree(ranges, value.borrow(), current_level)?); + .push(build_subtree(ranges, value, current_level)?); } } EqRangeItem::Eq(value) => { @@ -253,33 +457,21 @@ impl Range { } } - pub(crate) fn combining_eqs, I: EqRangeInput + ?Sized>( - &self, - eqs: &I, - ) -> Option { - fn merge_value(tuple: &[&DataValue], value: DataValue) -> DataValue { - let mut merge_tuple = Vec::with_capacity(tuple.len() + 1); - for value in tuple { - merge_tuple.push((*value).clone()); - } - merge_tuple.push(value); - - DataValue::Tuple(merge_tuple) - } + pub(crate) fn combining_eqs(&self, eqs: &I) -> Option { fn collect_tuple_range(result_ranges: &mut Vec, tuple: &[&DataValue], range: Range) { fn merge_value_on_bound( tuple: &[&DataValue], - bound: Bound, - ) -> Bound { + bound: Bound, + ) -> Bound { match bound { - Bound::Included(v) => Bound::Included(merge_value(tuple, v)), - Bound::Excluded(v) => Bound::Excluded(merge_value(tuple, v)), + Bound::Included(v) => Bound::Included(v.prefixed(tuple)), + Bound::Excluded(v) => Bound::Excluded(v.prefixed(tuple)), Bound::Unbounded => { if tuple.is_empty() { return Bound::Unbounded; } let values = tuple.iter().map(|v| (*v).clone()).collect_vec(); - Bound::Included(DataValue::Tuple(values)) + Bound::Included(DataValue::Tuple(values).into()) } } } @@ -289,7 +481,7 @@ impl Range { min: merge_value_on_bound(tuple, min), max: merge_value_on_bound(tuple, max), }), - Range::Eq(v) => result_ranges.push(Range::Eq(merge_value(tuple, v))), + Range::Eq(v) => result_ranges.push(Range::Eq(v.prefixed(tuple))), Range::Dummy => result_ranges.push(Range::Dummy), Range::SortedRanges(mut ranges) => { for range in &mut ranges { @@ -368,8 +560,10 @@ impl<'a, A: MetaArena + ?Sized> RangeDetacher<'a, IndexRangeColumn, A> { }; let prefix: &[DataValue] = if composite && prefix_len > 0 { let ( - Bound::Included(DataValue::Tuple(lower)) | Bound::Excluded(DataValue::Tuple(lower)), - Bound::Included(DataValue::Tuple(upper)) | Bound::Excluded(DataValue::Tuple(upper)), + Bound::Included(RangeValue::Value(DataValue::Tuple(lower))) + | Bound::Excluded(RangeValue::Value(DataValue::Tuple(lower))), + Bound::Included(RangeValue::Value(DataValue::Tuple(upper))) + | Bound::Excluded(RangeValue::Value(DataValue::Tuple(upper))), ) = (min, max) else { return Ok(None); @@ -555,8 +749,13 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { 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()), + 1 => Range::Eq(values.pop().unwrap().into()), + _ => Range::SortedRanges( + values + .into_iter() + .map(|value| Range::Eq(value.into())) + .collect(), + ), }; Some(DetachedPredicate::consumed(range)) } @@ -570,10 +769,10 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { return Ok(if negated { Some(DetachedPredicate::consumed(Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Null), + max: Bound::Excluded(RangeValue::NULL), })) } else { - Some(DetachedPredicate::consumed(Range::Eq(DataValue::Null))) + Some(DetachedPredicate::consumed(Range::Eq(RangeValue::NULL))) }); } @@ -629,17 +828,51 @@ 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(), - }) + // A bound NULL empties the range, so a value known only at execution cannot serve `<=>`, + // which matches NULL. + fn range_val(&self, expr: ExprRef, op: BinaryOperator) -> Option { + if let Some(value) = expr.unpack_val(self.arena) { + return (op != BinaryOperator::Spaceship || !value.has_parameter()) + .then_some(value.into()); + } + if op == BinaryOperator::Spaceship { + return None; + } + match self.arena.expression(expr) { + ScalarExpression::InitValue { id, ty } => Some( + DataValue::InitValue { + id: *id, + ty: ty.clone(), } - _ => None, - }) + .into(), + ), + ScalarExpression::Binary { + left_expr, + right_expr, + op: + value_op @ (BinaryOperator::Plus + | BinaryOperator::Minus + | BinaryOperator::Multiply + | BinaryOperator::Divide + | BinaryOperator::Modulo), + ty, + .. + } => { + let left = left_expr.unpack_val(self.arena)?.cast(ty).ok()?; + let right = right_expr.unpack_val(self.arena)?.cast(ty).ok()?; + (left.has_parameter() || right.has_parameter()).then(|| { + RangeValue::Computed(Box::new(ComputedValue { + prefix: None, + op: *value_op, + left, + right, + eval_ty: ty.clone(), + ty: ty.clone(), + })) + }) + } + _ => None, + } } #[allow(clippy::result_large_err)] @@ -649,10 +882,10 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { right_binary: Range, ) -> Result { fn process_exclude_bound_with_eq( - bound: Bound, - eq: &DataValue, + bound: Bound, + eq: &RangeValue, op: BinaryOperator, - ) -> Bound { + ) -> Bound { match bound { Bound::Excluded(bound_val) => { if &bound_val == eq && op == BinaryOperator::Or { @@ -665,7 +898,7 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { } } - fn bounds_have_parameter(bounds: &[&Bound]) -> bool { + fn bounds_have_parameter(bounds: &[&Bound]) -> bool { bounds.iter().any(|bound| match bound { Bound::Included(value) | Bound::Excluded(value) => value.has_parameter(), Bound::Unbounded => false, @@ -732,7 +965,7 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { return Err((Range::Scope { min, max }, Range::Eq(eq))); } - let unpack_bound = |bound_eq: Bound| match bound_eq { + let unpack_bound = |bound_eq: Bound| match bound_eq { Bound::Included(val) | Bound::Excluded(val) => val, _ => unreachable!(), }; @@ -1057,10 +1290,10 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { } fn or_scope_merge( - left_min: Bound, - left_max: Bound, - right_min: Bound, - right_max: Bound, + left_min: Bound, + left_max: Bound, + right_min: Bound, + right_max: Bound, ) -> Range { if matches!( Self::bound_compared(&left_max, &right_min, true, false), @@ -1116,10 +1349,10 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { #[allow(clippy::result_large_err)] fn and_scope_merge( - left_min: Bound, - left_max: Bound, - right_min: Bound, - right_max: Bound, + left_min: Bound, + left_max: Bound, + right_min: Bound, + right_max: Bound, ) -> Result { let min_order = Self::bound_compared(&left_min, &right_min, false, false); let max_order = Self::bound_compared(&left_max, &right_max, true, true); @@ -1166,27 +1399,27 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { self.column.matches(table_id, column_id, self.arena) } - fn bound_compared, R: Borrow>( + fn bound_compared, R: Borrow>( left_bound: &Bound, right_bound: &Bound, left_is_upper: bool, right_is_upper: bool, ) -> Option { fn range_value_cmp( - left: &DataValue, - right: &DataValue, + left: &RangeValue, + right: &RangeValue, left_is_upper: bool, right_is_upper: bool, ) -> Option { - match (left, right) { - (DataValue::Null, DataValue::Null) => Some(Ordering::Equal), - (DataValue::Null, _) => Some(Ordering::Greater), - (_, DataValue::Null) => Some(Ordering::Less), - _ => DataValueRef::Value(left).partial_cmp_with_bounds( - DataValueRef::Value(right), - left_is_upper, - right_is_upper, - ), + match (left.is_null(), right.is_null(), left, right) { + (true, true, ..) => Some(Ordering::Equal), + (true, false, ..) => Some(Ordering::Greater), + (false, true, ..) => Some(Ordering::Less), + (_, _, RangeValue::Value(left), RangeValue::Value(right)) => DataValueRef::Value( + left, + ) + .partial_cmp_with_bounds(DataValueRef::Value(right), left_is_upper, right_is_upper), + _ => None, } } fn is_min_then_reverse(is_min: bool, order: Ordering) -> Ordering { @@ -1231,7 +1464,7 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { &mut self, mut op: BinaryOperator, col: ColumnRef, - mut val: DataValue, + mut val: RangeValue, is_flip: bool, ) -> Result, DatabaseError> { if !self.matches_column(col) { @@ -1240,7 +1473,7 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { let column = self.arena.column(col); if val.is_null() { return Ok(match op { - BinaryOperator::Spaceship => Some(Range::Eq(DataValue::Null)), + BinaryOperator::Spaceship => Some(Range::Eq(RangeValue::NULL)), BinaryOperator::Eq | BinaryOperator::NotEq | BinaryOperator::Gt @@ -1264,7 +1497,7 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { BinaryOperator::Gt => Some(Range::Scope { min: Bound::Excluded(val.clone()), max: if column.nullable() { - Bound::Excluded(DataValue::Null) + Bound::Excluded(RangeValue::NULL) } else { Bound::Unbounded }, @@ -1276,7 +1509,7 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { BinaryOperator::GtEq => Some(Range::Scope { min: Bound::Included(val.clone()), max: if column.nullable() { - Bound::Excluded(DataValue::Null) + Bound::Excluded(RangeValue::NULL) } else { Bound::Unbounded }, @@ -1468,13 +1701,13 @@ mod test { DataValue::Int32(3), ], Range::SortedRanges(vec![ - Range::Eq(DataValue::Int32(1)), - Range::Eq(DataValue::Int32(3)), + Range::Eq(DataValue::Int32(1).into()), + Range::Eq(DataValue::Int32(3).into()), ]), ), ( vec![DataValue::Null, DataValue::Int32(1)], - Range::Eq(DataValue::Int32(1)), + Range::Eq(DataValue::Int32(1).into()), ), (vec![DataValue::Null], Range::Dummy), ] { @@ -1552,8 +1785,8 @@ mod test { assert_eq!( detached.range, Range::SortedRanges(vec![ - Range::Eq(DataValue::Int32(3)), - Range::Eq(DataValue::Int32(5)) + Range::Eq(DataValue::Int32(3).into()), + Range::Eq(DataValue::Int32(5).into()) ]) ); assert_eq!(detached.residual, Some(residual)); @@ -1607,7 +1840,7 @@ mod test { assert_eq!( detached.range, Range::Scope { - min: Bound::Excluded(DataValue::Int32(10)), + min: Bound::Excluded(DataValue::Int32(10).into()), max: Bound::Unbounded, } ); @@ -1622,7 +1855,7 @@ mod test { assert_eq!( residual_detached.range, Range::Scope { - min: Bound::Excluded(DataValue::Int32(20)), + min: Bound::Excluded(DataValue::Int32(20).into()), max: Bound::Unbounded, } ); @@ -1648,8 +1881,8 @@ mod test { assert_eq!( detached.range, Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), - max: Bound::Excluded(DataValue::Int32(8)), + min: Bound::Excluded(DataValue::Int32(0).into()), + max: Bound::Excluded(DataValue::Int32(8).into()), } ); assert_eq!(detached.residual, None); @@ -1667,29 +1900,29 @@ mod test { ( cmp_predicate(&mut plan_arena, column, BinaryOperator::Gt, 0), Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), - max: Bound::Excluded(DataValue::Null), + min: Bound::Excluded(DataValue::Int32(0).into()), + max: Bound::Excluded(DataValue::Null.into()), }, ), ( cmp_predicate(&mut plan_arena, column, BinaryOperator::GtEq, 0), Range::Scope { - min: Bound::Included(DataValue::Int32(0)), - max: Bound::Excluded(DataValue::Null), + min: Bound::Included(DataValue::Int32(0).into()), + max: Bound::Excluded(DataValue::Null.into()), }, ), ( cmp_predicate(&mut plan_arena, column, BinaryOperator::Lt, 8), Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Int32(8)), + max: Bound::Excluded(DataValue::Int32(8).into()), }, ), ( cmp_predicate(&mut plan_arena, column, BinaryOperator::LtEq, 8), Range::Scope { min: Bound::Unbounded, - max: Bound::Included(DataValue::Int32(8)), + max: Bound::Included(DataValue::Int32(8).into()), }, ), ( @@ -1703,7 +1936,7 @@ mod test { }, Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Null), + max: Bound::Excluded(DataValue::Null.into()), }, ), ]; @@ -1738,8 +1971,8 @@ mod test { assert_eq!( detached.range, Range::SortedRanges(vec![ - Range::Eq(DataValue::Int32(1)), - Range::Eq(DataValue::Int32(2)), + Range::Eq(DataValue::Int32(1).into()), + Range::Eq(DataValue::Int32(2).into()), ]) ); assert_eq!(detached.residual, None); @@ -1781,7 +2014,7 @@ mod test { .map(|detached| detached.range) .unwrap(); println!("c1 = 1 => {}", range); - assert_eq!(range, Range::Eq(DataValue::Int32(1))) + assert_eq!(range, Range::Eq(DataValue::Int32(1).into())) } { let plan = @@ -1796,7 +2029,7 @@ mod test { .map(|detached| detached.range) .unwrap(); println!("c1 = 1.0 => {}", range); - assert_eq!(range, Range::Eq(DataValue::Int32(1))) + assert_eq!(range, Range::Eq(DataValue::Int32(1).into())) } { let plan = @@ -1828,7 +2061,7 @@ mod test { assert_eq!( range, Range::Scope { - min: Bound::Excluded(DataValue::Int32(1)), + min: Bound::Excluded(DataValue::Int32(1).into()), max: Bound::Unbounded, } ) @@ -1849,7 +2082,7 @@ mod test { assert_eq!( range, Range::Scope { - min: Bound::Included(DataValue::Int32(1)), + min: Bound::Included(DataValue::Int32(1).into()), max: Bound::Unbounded, } ) @@ -1871,7 +2104,7 @@ mod test { range, Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Int32(1)), + max: Bound::Excluded(DataValue::Int32(1).into()), } ) } @@ -1892,7 +2125,7 @@ mod test { range, Range::Scope { min: Bound::Unbounded, - max: Bound::Included(DataValue::Int32(1)), + max: Bound::Included(DataValue::Int32(1).into()), } ) } @@ -1912,8 +2145,8 @@ mod test { assert_eq!( range, Range::Scope { - min: Bound::Included(DataValue::Int32(0)), - max: Bound::Excluded(DataValue::Int32(1)), + min: Bound::Included(DataValue::Int32(0).into()), + max: Bound::Excluded(DataValue::Int32(1).into()), } ) } @@ -1970,8 +2203,8 @@ mod test { assert_eq!( range, Range::SortedRanges(vec![ - Range::Eq(DataValue::Int32(0)), - Range::Eq(DataValue::Int32(1)), + Range::Eq(DataValue::Int32(0).into()), + Range::Eq(DataValue::Int32(1).into()), ]) ) } @@ -1988,7 +2221,7 @@ mod test { .map(|detached| detached.range) .unwrap(); println!("c1 = 1 and c1 = 1 => c1: {}", range); - assert_eq!(range, Range::Eq(DataValue::Int32(1))) + assert_eq!(range, Range::Eq(DataValue::Int32(1).into())) } { let plan = table_state @@ -2003,7 +2236,7 @@ mod test { .map(|detached| detached.range) .unwrap(); println!("c1 = 1 or c1 = 1 => c1: {}", range); - assert_eq!(range, Range::Eq(DataValue::Int32(1))) + assert_eq!(range, Range::Eq(DataValue::Int32(1).into())) } { @@ -2034,7 +2267,7 @@ mod test { .map(|detached| detached.range) .unwrap(); println!("c1 >= 1 and c1 = 1 => c1: {}", range); - assert_eq!(range, Range::Eq(DataValue::Int32(1))) + assert_eq!(range, Range::Eq(DataValue::Int32(1).into())) } { let plan = table_state @@ -2052,7 +2285,7 @@ mod test { assert_eq!( range, Range::Scope { - min: Bound::Included(DataValue::Int32(1)), + min: Bound::Included(DataValue::Int32(1).into()), max: Bound::Unbounded, } ) @@ -2073,7 +2306,7 @@ mod test { assert_eq!( range, Range::Scope { - min: Bound::Included(DataValue::Int32(1)), + min: Bound::Included(DataValue::Int32(1).into()), max: Bound::Unbounded, } ) @@ -2100,8 +2333,8 @@ mod test { assert_eq!( range, Range::Scope { - min: Bound::Excluded(DataValue::Int32(1)), - max: Bound::Excluded(DataValue::Int32(3)), + min: Bound::Excluded(DataValue::Int32(1).into()), + max: Bound::Excluded(DataValue::Int32(3).into()), } ) } @@ -2126,8 +2359,8 @@ mod test { assert_eq!( range, Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), - max: Bound::Excluded(DataValue::Int32(4)), + min: Bound::Excluded(DataValue::Int32(0).into()), + max: Bound::Excluded(DataValue::Int32(4).into()), } ) } @@ -2193,10 +2426,10 @@ mod test { assert_eq!( range, Range::SortedRanges(vec![ - Range::Eq(DataValue::Int32(0)), + Range::Eq(DataValue::Int32(0).into()), Range::Scope { - min: Bound::Excluded(DataValue::Int32(1)), - max: Bound::Excluded(DataValue::Int32(3)), + min: Bound::Excluded(DataValue::Int32(1).into()), + max: Bound::Excluded(DataValue::Int32(3).into()), } ]) ) @@ -2222,8 +2455,8 @@ mod test { assert_eq!( range, Range::Scope { - min: Bound::Included(DataValue::Int32(0)), - max: Bound::Excluded(DataValue::Int32(4)), + min: Bound::Included(DataValue::Int32(0).into()), + max: Bound::Excluded(DataValue::Int32(4).into()), } ) } @@ -2257,8 +2490,8 @@ mod test { assert_eq!( range, Range::Scope { - min: Bound::Included(DataValue::Int32(0)), - max: Bound::Included(DataValue::Int32(2)), + min: Bound::Included(DataValue::Int32(0).into()), + max: Bound::Included(DataValue::Int32(2).into()), } ) } @@ -2279,12 +2512,12 @@ mod test { range, Range::SortedRanges(vec![ Range::Scope { - min: Bound::Excluded(DataValue::Int32(1)), - max: Bound::Excluded(DataValue::Int32(2)), + min: Bound::Excluded(DataValue::Int32(1).into()), + max: Bound::Excluded(DataValue::Int32(2).into()), }, Range::Scope { - min: Bound::Excluded(DataValue::Int32(5)), - max: Bound::Excluded(DataValue::Int32(6)), + min: Bound::Excluded(DataValue::Int32(5).into()), + max: Bound::Excluded(DataValue::Int32(6).into()), }, ]) ) @@ -2305,12 +2538,12 @@ mod test { range, Range::SortedRanges(vec![ Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), - max: Bound::Excluded(DataValue::Int32(3)), + min: Bound::Excluded(DataValue::Int32(0).into()), + max: Bound::Excluded(DataValue::Int32(3).into()), }, Range::Scope { - min: Bound::Excluded(DataValue::Int32(4)), - max: Bound::Excluded(DataValue::Int32(7)), + min: Bound::Excluded(DataValue::Int32(4).into()), + max: Bound::Excluded(DataValue::Int32(7).into()), }, ]) ) @@ -2395,8 +2628,8 @@ mod test { assert_eq!( range, Range::Scope { - min: Bound::Included(DataValue::Int32(5)), - max: Bound::Excluded(DataValue::Int32(12)), + min: Bound::Included(DataValue::Int32(5).into()), + max: Bound::Excluded(DataValue::Int32(12).into()), } ) } @@ -2442,13 +2675,13 @@ mod test { assert_eq!( detach_c1("select * from t1 where c2 = 1 and c1 > 10")?, Some(Range::Scope { - min: Bound::Excluded(DataValue::Int32(10)), + min: Bound::Excluded(DataValue::Int32(10).into()), max: Bound::Unbounded, }) ); let negated_range = Some(Range::Scope { min: Bound::Unbounded, - max: Bound::Included(DataValue::Int32(10)), + max: Bound::Included(DataValue::Int32(10).into()), }); assert_eq!( detach_c1("select * from t1 where (c1 > 10) = false")?, @@ -2507,7 +2740,7 @@ mod test { .map(|detached| detached.range) .unwrap(); println!("c1 = null or c1 = 1 => c1: {}", range); - assert_eq!(range, Range::Eq(DataValue::Int32(1))) + assert_eq!(range, Range::Eq(DataValue::Int32(1).into())) } { let plan = table_state.plan_with_arena( @@ -2528,7 +2761,7 @@ mod test { range, Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Int32(5)), + max: Bound::Excluded(DataValue::Int32(5).into()), } ) } @@ -2550,8 +2783,8 @@ mod test { assert_eq!( range, Range::Scope { - min: Bound::Excluded(DataValue::Int32(1)), - max: Bound::Excluded(DataValue::Int32(5)), + min: Bound::Excluded(DataValue::Int32(1).into()), + max: Bound::Excluded(DataValue::Int32(5).into()), } ) } @@ -2638,7 +2871,7 @@ mod test { range, Some(Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Int32(5)), + max: Bound::Excluded(DataValue::Int32(5).into()), }) ) } @@ -2659,8 +2892,8 @@ mod test { assert_eq!( range, Some(Range::Scope { - min: Bound::Excluded(DataValue::Int32(1)), - max: Bound::Excluded(DataValue::Int32(5)), + min: Bound::Excluded(DataValue::Int32(1).into()), + max: Bound::Excluded(DataValue::Int32(5).into()), }) ) } @@ -2714,12 +2947,12 @@ mod test { range, Range::SortedRanges(vec![ Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), - max: Bound::Excluded(DataValue::Int32(3)), + min: Bound::Excluded(DataValue::Int32(0).into()), + max: Bound::Excluded(DataValue::Int32(3).into()), }, Range::Scope { - min: Bound::Excluded(DataValue::Int32(4)), - max: Bound::Excluded(DataValue::Int32(7)), + min: Bound::Excluded(DataValue::Int32(4).into()), + max: Bound::Excluded(DataValue::Int32(7).into()), } ]) ) @@ -2740,12 +2973,12 @@ mod test { range, Range::SortedRanges(vec![ Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), - max: Bound::Excluded(DataValue::Int32(3)), + min: Bound::Excluded(DataValue::Int32(0).into()), + max: Bound::Excluded(DataValue::Int32(3).into()), }, Range::Scope { - min: Bound::Excluded(DataValue::Int32(4)), - max: Bound::Excluded(DataValue::Int32(7)), + min: Bound::Excluded(DataValue::Int32(4).into()), + max: Bound::Excluded(DataValue::Int32(7).into()), } ]) ) @@ -2766,12 +2999,12 @@ mod test { range, Range::SortedRanges(vec![ Range::Scope { - min: Bound::Excluded(DataValue::Int32(1)), - max: Bound::Excluded(DataValue::Int32(2)), + min: Bound::Excluded(DataValue::Int32(1).into()), + max: Bound::Excluded(DataValue::Int32(2).into()), }, Range::Scope { - min: Bound::Excluded(DataValue::Int32(5)), - max: Bound::Excluded(DataValue::Int32(6)), + min: Bound::Excluded(DataValue::Int32(5).into()), + max: Bound::Excluded(DataValue::Int32(6).into()), } ]) ) @@ -2792,12 +3025,12 @@ mod test { range, Range::SortedRanges(vec![ Range::Scope { - min: Bound::Excluded(DataValue::Int32(1)), - max: Bound::Excluded(DataValue::Int32(2)), + min: Bound::Excluded(DataValue::Int32(1).into()), + max: Bound::Excluded(DataValue::Int32(2).into()), }, Range::Scope { - min: Bound::Excluded(DataValue::Int32(5)), - max: Bound::Excluded(DataValue::Int32(6)), + min: Bound::Excluded(DataValue::Int32(5).into()), + max: Bound::Excluded(DataValue::Int32(6).into()), } ]) ) @@ -2809,20 +3042,20 @@ mod test { #[test] fn test_to_tuple_range_some() { let eqs_ranges = vec![ - Range::Eq(DataValue::Int32(1)), + Range::Eq(DataValue::Int32(1).into()), Range::SortedRanges(vec![ - Range::Eq(DataValue::Null), - Range::Eq(DataValue::Int32(1)), - Range::Eq(DataValue::Int32(2)), + Range::Eq(DataValue::Null.into()), + Range::Eq(DataValue::Int32(1).into()), + Range::Eq(DataValue::Int32(2).into()), ]), Range::SortedRanges(vec![ - Range::Eq(DataValue::Int32(1)), - Range::Eq(DataValue::Int32(2)), + Range::Eq(DataValue::Int32(1).into()), + Range::Eq(DataValue::Int32(2).into()), ]), ]; let range = Range::Scope { - min: Bound::Included(DataValue::Int32(1)), + min: Bound::Included(DataValue::Int32(1).into()), max: Bound::Unbounded, } .combining_eqs(&eqs_ranges); @@ -2831,89 +3064,125 @@ mod test { range, Some(Range::SortedRanges(vec![ Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Null, - DataValue::Int32(1), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Null, - DataValue::Int32(1), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(1), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(1), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Null, - DataValue::Int32(2), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Null, - DataValue::Int32(2), - ])) + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(2), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(2), + ]) + .into() + ) }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(1), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(1), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(2), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(2), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(1), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(1), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(1), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(1), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(2), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(2), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(2), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(2), + ]) + .into() + ), }, ])) ); let range = Range::Scope { min: Bound::Unbounded, - max: Bound::Included(DataValue::Int32(1)), + max: Bound::Included(DataValue::Int32(1).into()), } .combining_eqs(&eqs_ranges); @@ -2921,89 +3190,125 @@ mod test { range, Some(Range::SortedRanges(vec![ Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Null, - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Null, - DataValue::Int32(1), - DataValue::Int32(1), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(1), + DataValue::Int32(1), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Null, - DataValue::Int32(2), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Null, - DataValue::Int32(2), - DataValue::Int32(1), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(2), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(2), + DataValue::Int32(1), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(1), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(1), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(2), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(1), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(2), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(1), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(1), - DataValue::Int32(1), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(1), + DataValue::Int32(1), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(2), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(2), - DataValue::Int32(1), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(2), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(2), + DataValue::Int32(1), + ]) + .into() + ), }, ])) ); let range = Range::Scope { - min: Bound::Included(DataValue::Int32(1)), - max: Bound::Included(DataValue::Int32(2)), + min: Bound::Included(DataValue::Int32(1).into()), + max: Bound::Included(DataValue::Int32(2).into()), } .combining_eqs(&eqs_ranges); @@ -3011,88 +3316,124 @@ mod test { range, Some(Range::SortedRanges(vec![ Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Null, - DataValue::Int32(1), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Null, - DataValue::Int32(1), - DataValue::Int32(2), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(1), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(1), + DataValue::Int32(2), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Null, - DataValue::Int32(2), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Null, - DataValue::Int32(2), - DataValue::Int32(2), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(2), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Null, + DataValue::Int32(2), + DataValue::Int32(2), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(2), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(2), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(2), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(2), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(1), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(1), - DataValue::Int32(2), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(1), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(1), + DataValue::Int32(2), + ]) + .into() + ), }, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(2), - DataValue::Int32(1), - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(2), - DataValue::Int32(2), - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(2), + DataValue::Int32(1), + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(2), + DataValue::Int32(2), + ]) + .into() + ), }, ])) ) @@ -3101,25 +3442,27 @@ mod test { #[test] fn test_to_tuple_range_none() -> Result<(), DatabaseError> { let range = Range::Scope { - min: Bound::Included(DataValue::Int32(2)), + min: Bound::Included(DataValue::Int32(2).into()), max: Bound::Unbounded, } .combining_eqs(&[ - Range::Eq(DataValue::Int32(7)), - Range::Eq(DataValue::Int32(10)), + Range::Eq(DataValue::Int32(7).into()), + Range::Eq(DataValue::Int32(10).into()), ]); assert_eq!( range, Some(Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(7), - DataValue::Int32(10), - DataValue::Int32(2) - ])), - max: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(7), - DataValue::Int32(10) - ])), + min: Bound::Included( + DataValue::Tuple(vec![ + DataValue::Int32(7), + DataValue::Int32(10), + DataValue::Int32(2) + ]) + .into() + ), + max: Bound::Included( + DataValue::Tuple(vec![DataValue::Int32(7), DataValue::Int32(10)]).into() + ), }) ); Ok(()) @@ -3144,36 +3487,34 @@ mod test { _ => None, }; - let mut point = Range::Eq(DataValue::Tuple(vec![DataValue::Int32(1), init(first)])); + let mut point = Range::Eq(DataValue::Tuple(vec![DataValue::Int32(1), init(first)]).into()); 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) - ])) + Range::Eq(DataValue::Tuple(vec![DataValue::Int32(1), DataValue::Int64(7)]).into()) ); - let mut null_point = Range::Eq(DataValue::Tuple(vec![DataValue::Null, init(second)])); + let mut null_point = + Range::Eq(DataValue::Tuple(vec![DataValue::Null, init(second)]).into()); null_point.bind_parameters(&|_| None, &get)?; assert_eq!(null_point, Range::Dummy); let mut ranges = Range::SortedRanges(vec![ - Range::Eq(init(first)), + Range::Eq(init(first).into()), Range::Scope { - min: Bound::Excluded(init(second)), - max: Bound::Excluded(DataValue::Null), + min: Bound::Excluded(init(second).into()), + max: Bound::Excluded(DataValue::Null.into()), }, ]); ranges.bind_parameters(&|_| None, &get)?; assert_eq!( ranges, - Range::SortedRanges(vec![Range::Eq(DataValue::Int64(7)), Range::Dummy]) + Range::SortedRanges(vec![Range::Eq(DataValue::Int64(7).into()), Range::Dummy]) ); - let mut unset = Range::Eq(init(first)); + let mut unset = Range::Eq(init(first).into()); assert!(unset.bind_parameters(&|_| None, &|_| None).is_err()); Ok(()) } @@ -3186,12 +3527,12 @@ mod test { ty: LogicalType::Integer, }; let left = Range::Scope { - min: Bound::Included(parameter(1)), + min: Bound::Included(parameter(1).into()), max: Bound::Unbounded, }; let right = Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(parameter(2)), + max: Bound::Excluded(parameter(2).into()), }; assert_eq!( RangeDetacher::::merge_binary( @@ -3217,12 +3558,12 @@ mod test { ranges, Range::SortedRanges(vec![ Range::Scope { - min: Bound::Included(low), + min: Bound::Included(low.into()), max: Bound::Unbounded }, Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(high) + max: Bound::Excluded(high.into()) }, ]) ); @@ -3231,13 +3572,13 @@ mod test { #[test] fn intersection_failure_restores_inputs_after_appending_a_match() { - let scope = |min, max| Range::Scope { - min: Bound::Included(min), - max: Bound::Included(max), + let scope = |min: DataValue, max: DataValue| Range::Scope { + min: Bound::Included(min.into()), + max: Bound::Included(max.into()), }; let left = scope(DataValue::Int32(1), DataValue::Int32(9)); let right = vec![ - Range::Eq(DataValue::Int32(2)), + Range::Eq(DataValue::Int32(2).into()), scope( DataValue::new_utf8("a".into()), DataValue::new_utf8("z".into()), @@ -3252,17 +3593,17 @@ mod test { #[test] fn test_range_merge_preserves_open_closed_boundaries() { let gt_one = Range::Scope { - min: Bound::Excluded(DataValue::Int32(1)), + min: Bound::Excluded(DataValue::Int32(1).into()), max: Bound::Unbounded, }; assert_eq!( RangeDetacher::::merge_binary( BinaryOperator::Or, gt_one.clone(), - Range::Eq(DataValue::Int32(1)), + Range::Eq(DataValue::Int32(1).into()), ), Ok(Range::Scope { - min: Bound::Included(DataValue::Int32(1)), + min: Bound::Included(DataValue::Int32(1).into()), max: Bound::Unbounded, }) ); @@ -3270,7 +3611,7 @@ mod test { RangeDetacher::::merge_binary( BinaryOperator::And, gt_one, - Range::Eq(DataValue::Int32(1)), + Range::Eq(DataValue::Int32(1).into()), ), Ok(Range::Dummy) ); @@ -3278,36 +3619,42 @@ mod test { let disjoint = RangeDetacher::::merge_binary( BinaryOperator::Or, Range::Scope { - min: Bound::Included(DataValue::Int32(1)), - max: Bound::Included(DataValue::Int32(2)), + min: Bound::Included(DataValue::Int32(1).into()), + max: Bound::Included(DataValue::Int32(2).into()), }, Range::Scope { - min: Bound::Included(DataValue::Int32(4)), - max: Bound::Included(DataValue::Int32(5)), + min: Bound::Included(DataValue::Int32(4).into()), + max: Bound::Included(DataValue::Int32(5).into()), }, ); assert_eq!( disjoint, Ok(Range::SortedRanges(vec![ Range::Scope { - min: Bound::Included(DataValue::Int32(1)), - max: Bound::Included(DataValue::Int32(2)), + min: Bound::Included(DataValue::Int32(1).into()), + max: Bound::Included(DataValue::Int32(2).into()), }, Range::Scope { - min: Bound::Included(DataValue::Int32(4)), - max: Bound::Included(DataValue::Int32(5)), + min: Bound::Included(DataValue::Int32(4).into()), + max: Bound::Included(DataValue::Int32(5).into()), }, ])) ); - let left = Range::Eq(DataValue::Parameter { - id: 1, - ty: LogicalType::Integer, - }); - let right = Range::Eq(DataValue::Parameter { - id: 2, - ty: LogicalType::Integer, - }); + let left = Range::Eq( + DataValue::Parameter { + id: 1, + ty: LogicalType::Integer, + } + .into(), + ); + let right = Range::Eq( + DataValue::Parameter { + id: 2, + ty: LogicalType::Integer, + } + .into(), + ); assert_eq!( RangeDetacher::::merge_binary( BinaryOperator::And, @@ -3321,80 +3668,83 @@ mod test { #[test] fn test_combining_eqs_builds_tuple_ranges_and_rejects_non_eq_prefixes() { let suffix = Range::Scope { - min: Bound::Included(DataValue::Int32(10)), - max: Bound::Excluded(DataValue::Int32(20)), + min: Bound::Included(DataValue::Int32(10).into()), + max: Bound::Excluded(DataValue::Int32(20).into()), }; let prefixes = [ - Range::Eq(DataValue::Int32(1)), + Range::Eq(DataValue::Int32(1).into()), Range::SortedRanges(vec![ - Range::Eq(DataValue::Int32(2)), - Range::Eq(DataValue::Int32(3)), + Range::Eq(DataValue::Int32(2).into()), + Range::Eq(DataValue::Int32(3).into()), ]), ]; let combined = suffix.combining_eqs(&prefixes).unwrap(); - let eq = |value| Range::Eq(DataValue::Int32(value)); + let eq = |value| Range::Eq(DataValue::Int32(value).into()); assert_eq!( eq(9).combining_eqs(&[ Range::SortedRanges(vec![eq(1), eq(2)]), Range::SortedRanges(vec![eq(3), eq(4)]), ]), Some(Range::SortedRanges(vec![ - Range::Eq(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(3), - DataValue::Int32(9) - ])), - Range::Eq(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(4), - DataValue::Int32(9) - ])), - Range::Eq(DataValue::Tuple(vec![ - DataValue::Int32(2), - DataValue::Int32(3), - DataValue::Int32(9) - ])), - Range::Eq(DataValue::Tuple(vec![ - DataValue::Int32(2), - DataValue::Int32(4), - DataValue::Int32(9) - ])), + Range::Eq( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(3), + DataValue::Int32(9) + ]) + .into() + ), + Range::Eq( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(4), + DataValue::Int32(9) + ]) + .into() + ), + Range::Eq( + DataValue::Tuple(vec![ + DataValue::Int32(2), + DataValue::Int32(3), + DataValue::Int32(9) + ]) + .into() + ), + Range::Eq( + DataValue::Tuple(vec![ + DataValue::Int32(2), + DataValue::Int32(4), + DataValue::Int32(9) + ]) + .into() + ), ])) ); - let first = DataValue::Int32(1); - let second = DataValue::Int32(2); - let borrowed = [Range::Eq(&first), Range::Eq(&second)]; assert_eq!( - suffix.combining_eqs(&borrowed), + suffix.combining_eqs(&[DataValue::Int32(1), DataValue::Int32(2)][..]), suffix.combining_eqs(&[ - Range::Eq(DataValue::Int32(1)), - Range::Eq(DataValue::Int32(2)), + Range::Eq(DataValue::Int32(1).into()), + Range::Eq(DataValue::Int32(2).into()), ]) ); - assert_eq!( - suffix.combining_eqs(&[DataValue::Int32(1), DataValue::Int32(2)][..]), - suffix.combining_eqs(&borrowed) - ); assert!(matches!(combined, Range::SortedRanges(ref ranges) if ranges.len() == 2)); assert!(prefixes[0].only_eq()); assert!(prefixes[1].only_eq()); let explicit_suffix = Range::Scope { - min: Bound::Included(DataValue::Int32(10)), - max: Bound::Excluded(DataValue::Int32(20)), + min: Bound::Included(DataValue::Int32(10).into()), + max: Bound::Excluded(DataValue::Int32(20).into()), } - .combining_eqs(&[Range::Eq(DataValue::Int32(1))]); + .combining_eqs(&[Range::Eq(DataValue::Int32(1).into())]); assert_eq!( explicit_suffix, Some(Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(10) - ],)), - max: Bound::Excluded(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(20) - ],)), + min: Bound::Included( + DataValue::Tuple(vec![DataValue::Int32(1), DataValue::Int32(10)],).into() + ), + max: Bound::Excluded( + DataValue::Tuple(vec![DataValue::Int32(1), DataValue::Int32(20)],).into() + ), }) ); @@ -3402,24 +3752,22 @@ mod test { min: Bound::Unbounded, max: Bound::Unbounded, } - .combining_eqs(&[Range::Eq(DataValue::Int32(1))]); + .combining_eqs(&[Range::Eq(DataValue::Int32(1).into())]); assert_eq!( unbounded_suffix, Some(Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(1)])), - max: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(1)])), + min: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(1)]).into()), + max: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(1)]).into()), }) ); let concrete = Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(10), - ])), - max: Bound::Excluded(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(20), - ])), + min: Bound::Included( + DataValue::Tuple(vec![DataValue::Int32(1), DataValue::Int32(10)]).into(), + ), + max: Bound::Excluded( + DataValue::Tuple(vec![DataValue::Int32(1), DataValue::Int32(20)]).into(), + ), }; assert_eq!( RangeDetacher::::merge_binary( @@ -3431,7 +3779,7 @@ mod test { ); assert!(suffix - .combining_eqs(&[Range::::Scope { + .combining_eqs(&[Range::Scope { min: Bound::Unbounded, max: Bound::Unbounded, }]) @@ -3444,7 +3792,7 @@ mod test { fn test_detach_and_over_or() -> Result<(), DatabaseError> { let table_state = build_t1_table()?; let mut plan_arena = crate::planner::PlanArena::new(&table_state.table_arena); - let eq = |v| Range::Eq(DataValue::Int32(v)); + let eq = |v| Range::Eq(DataValue::Int32(v).into()); let cases = [ ("c1 > 10 and (c1 = 1 or c1 = 20)", eq(20)), // `c1 > 3` must still be applied after it has been intersected with `c1 = 5`. @@ -3465,7 +3813,7 @@ mod test { ( "c1 > 3 and (c1 < 2 or c1 > 30)", Range::Scope { - min: Bound::Excluded(DataValue::Int32(30)), + min: Bound::Excluded(DataValue::Int32(30).into()), max: Bound::Unbounded, }, ), diff --git a/src/optimizer/core/cm_sketch.rs b/src/optimizer/core/cm_sketch.rs index 4c6c6503..3a911844 100644 --- a/src/optimizer/core/cm_sketch.rs +++ b/src/optimizer/core/cm_sketch.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::expression::range_detacher::Range; +use crate::expression::range_detacher::{Range, RangeValue}; use crate::planner::MetaArena; use crate::serdes::stable_hash::{StableHasher, CM_SKETCH_HASH_KEYS}; use crate::serdes::ReferenceSerialization; @@ -208,7 +208,7 @@ impl CountMinSketch { for range in ranges { count += match range { - Range::Eq(value) => self.estimate(value), + Range::Eq(RangeValue::Value(value)) => self.estimate(value), _ => 0, } } @@ -454,7 +454,7 @@ mod tests { } assert_eq!( cms.collect_count(&[ - Range::Eq(DataValue::Int32(300)), + Range::Eq(DataValue::Int32(300).into()), Range::Scope { min: Bound::Unbounded, max: Bound::Unbounded, diff --git a/src/optimizer/core/histogram.rs b/src/optimizer/core/histogram.rs index da11eed4..8205c4ae 100644 --- a/src/optimizer/core/histogram.rs +++ b/src/optimizer/core/histogram.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::expression::range_detacher::Range; +use crate::expression::range_detacher::{Range, RangeValue, RangeValueRef}; use crate::expression::BinaryOperator; use crate::optimizer::core::cm_sketch::CountMinSketch; use crate::optimizer::core::hll::HyperLogLog; @@ -41,7 +41,8 @@ pub(crate) const DEFAULT_HLL_PRECISION: u8 = 14; pub struct HistogramMetaBuilder { null_count: usize, values_len: usize, - hll: HyperLogLog, + // One per key prefix; the last counts the whole key. + hlls: Vec>, } pub struct HistogramBuilder { @@ -62,7 +63,7 @@ struct BoundComparator { #[derive(Debug, Clone, PartialEq, ReferenceSerialization)] pub struct HistogramMeta { - number_of_distinct_value: usize, + number_of_distinct_values: Vec, null_count: usize, values_len: usize, buckets_len: usize, @@ -111,7 +112,7 @@ impl HistogramMetaBuilder { Ok(Self { null_count: 0, values_len: 0, - hll: HyperLogLog::with_precision(hll_precision)?, + hlls: vec![HyperLogLog::with_precision(hll_precision)?], }) } @@ -121,18 +122,24 @@ impl HistogramMetaBuilder { false } else { self.values_len += 1; - self.hll.add(value); + let fields = value.as_slice(); + for (len, hll) in (1..=fields.len()).zip(&mut self.hlls) { + hll.add(&fields[..len]); + } true } } pub fn build(self) -> HistogramMeta { - HistogramMeta { - number_of_distinct_value: if self.values_len == 0 { + let distinct = |hll: &HyperLogLog| { + if self.values_len == 0 { 0 } else { - self.hll.estimate().clamp(1, self.values_len) - }, + hll.estimate().clamp(1, self.values_len) + } + }; + HistogramMeta { + number_of_distinct_values: self.hlls.iter().map(distinct).collect(), null_count: self.null_count, values_len: self.values_len, buckets_len: 0, @@ -147,9 +154,15 @@ impl HistogramBuilder { relative_error: f64, hll_precision: u8, ) -> Result { + let mut meta = HistogramMetaBuilder::new(hll_precision)?; + if let LogicalType::Tuple(fields) = &index_meta.value_ty { + for _ in 1..fields.len() { + meta.hlls.push(HyperLogLog::with_precision(hll_precision)?); + } + } Ok(Self { index_id: index_meta.id, - meta: HistogramMetaBuilder::new(hll_precision)?, + meta, // 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%. @@ -287,7 +300,7 @@ impl BoundComparator { fn is_under( comparator: &BoundComparator, value: &DataValue, - target: &Bound, + target: Bound<&DataValue>, is_min: bool, ) -> Result { let _is_under = |value: &DataValue, target: &DataValue, is_min: bool| { @@ -308,7 +321,7 @@ fn is_under( fn is_above( comparator: &BoundComparator, value: &DataValue, - target: &Bound, + target: Bound<&DataValue>, is_min: bool, ) -> Result { let _is_above = |value: &DataValue, target: &DataValue, is_min: bool| { @@ -388,7 +401,7 @@ impl Histogram { } pub fn distinct_values_len(&self) -> usize { - self.meta.number_of_distinct_value + self.meta.distinct_values_len() } pub fn buckets_len(&self) -> usize { @@ -396,7 +409,7 @@ impl Histogram { } fn average_count(&self) -> usize { - let distinct_values = self.meta.number_of_distinct_value; + let distinct_values = self.distinct_values_len(); if distinct_values == 0 || self.meta.values_len == 0 { return 0; } @@ -449,15 +462,20 @@ impl Histogram { let distinct = self.distinct_values_len().max(1) as f64; match range { Range::Dummy => 0.0, - Range::Eq(value) => DataValue::bound_selectivity( + Range::Eq(value) => bound_selectivity( Bound::Included(value), Bound::Included(value), &key_type, distinct, + &self.meta.number_of_distinct_values, + ), + Range::Scope { min, max } => bound_selectivity( + min.as_ref(), + max.as_ref(), + &key_type, + distinct, + &self.meta.number_of_distinct_values, ), - Range::Scope { min, max } => { - DataValue::bound_selectivity(min.as_ref(), max.as_ref(), &key_type, distinct) - } Range::SortedRanges(ranges) => ranges .iter() .map(|range| self.estimate_range_selectivity(range)) @@ -476,7 +494,7 @@ impl Histogram { if self.buckets.is_empty() || ranges.is_empty() { return Ok(0); } - if ranges.iter().any(Range::has_parameter) { + if ranges.iter().any(|range| range.has_parameter()) { let selectivity = ranges .iter() .map(|range| self.estimate_range_selectivity(range)) @@ -526,10 +544,12 @@ impl Histogram { comparator: &BoundComparator, buf: &mut Vec, ) -> Result { - let distinct_1 = OrderedFloat(1.0 / self.meta.number_of_distinct_value as f64); + let distinct_1 = OrderedFloat(1.0 / self.distinct_values_len() as f64); match &ranges[*binary_i] { Range::Scope { min, max } => { + // Ranges with parameters are estimated instead, so these bounds hold values. + let (min, max) = (RangeValue::bound(min)?, RangeValue::bound(max)?); let bucket = &self.buckets[*bucket_i]; let mut bucket_count = bucket.count as usize; if *bucket_i == 0 && scope_lower_includes_null(min) { @@ -538,7 +558,7 @@ impl Histogram { let mut temp_count = 0; - let is_eq = |value: &DataValue, target: &Bound| match target { + let is_eq = |value: &DataValue, target: Bound<&DataValue>| match target { Bound::Included(target) => target.eq(value), _ => false, }; @@ -630,7 +650,7 @@ impl Histogram { *count += if value.is_null() { self.meta.null_count } else { - self.top_n_count_or_fallback(value, sketch, top_n) + self.top_n_count_or_fallback(value.value()?, sketch, top_n) }; *binary_i += 1 } @@ -655,7 +675,7 @@ fn encoded_fraction( end.memcomparable_encode(buf)?; let upper_end = buf.len(); value.memcomparable_encode(buf)?; - if is_upper && matches!(value, DataValue::Tuple(_)) { + if is_upper && value.is_tuple() { buf.push(crate::storage::table_codec::BOUND_MAX_TAG); } let lower = &buf[..lower_end]; @@ -723,7 +743,84 @@ fn key_types_match(left: &LogicalType, right: &LogicalType) -> bool { } } -fn scope_lower_includes_null(min: &Bound) -> bool { +/// Estimates a range from its shape and the NDV. The bounds are only compared with each +/// other, so values known only at execution need no stand-in. +fn bound_selectivity( + min: Bound<&RangeValue>, + max: Bound<&RangeValue>, + key_type: &LogicalType, + distinct: f64, + prefix_distinct: &[usize], +) -> f64 { + match (min, max, key_type) { + ( + Bound::Included(lower) | Bound::Excluded(lower), + Bound::Included(upper) | Bound::Excluded(upper), + LogicalType::Tuple(fields), + ) if !fields.is_empty() && lower.is_tuple() && upper.is_tuple() => { + // NDV of the first `len` fields, spread evenly over them when not analyzed. + let prefix_ndv = |len: usize| match len.checked_sub(1) { + Some(i) if prefix_distinct.len() == fields.len() => { + prefix_distinct[i].max(1) as f64 + } + _ => distinct.powf(len as f64 / fields.len() as f64), + }; + let (mut lower, mut upper) = (lower.iter(), upper.iter()); + let mut divisor = 1.0; + for len in 0..fields.len() { + let (min, max, is_range) = match (lower.next(), upper.next()) { + (Some(lower), Some(upper)) => { + let is_range = lower != upper; + (Bound::Included(lower), Bound::Included(upper), is_range) + } + (Some(lower), None) => (Bound::Included(lower), Bound::Unbounded, true), + (None, Some(upper)) => (Bound::Unbounded, Bound::Included(upper), true), + (None, None) => return 1.0 / divisor, + }; + // Equal prefixes multiply to `1 / prefix_ndv(len + 1)`. + let field_distinct = (prefix_ndv(len + 1) / prefix_ndv(len)).max(1.0); + let selectivity = RangeValueRef::bound_selectivity(min, max, field_distinct); + if selectivity == 0.0 { + return 0.0; + } + divisor /= selectivity; + // Only the equal prefix and first differing field constrain + // a lexicographic range. Missing fields delimit the prefix. + if is_range { + return 1.0 / divisor; + } + } + // The whole key is equal, so an excluded bound matches nothing. + if (matches!(min, Bound::Excluded(_)) || matches!(max, Bound::Excluded(_))) + && lower.next().is_none() + && upper.next().is_none() + { + return 0.0; + } + 1.0 / divisor + } + (min, max, _) => RangeValueRef::bound_selectivity( + min.map(RangeValue::to_ref), + max.map(RangeValue::to_ref), + distinct, + ), + } +} + +impl RangeValueRef<'_> { + fn bound_selectivity(min: Bound, max: Bound, distinct: f64) -> f64 { + match (min, max) { + (Bound::Included(lower), Bound::Included(upper)) if lower == upper => 1.0 / distinct, + (Bound::Unbounded, Bound::Unbounded) => 1.0, + // SQLite 3.45.3 whereRangeScanEst fallback: 1/4 for one bound, + // 1/64 for two. These are heuristics, not bucket estimates. + (Bound::Unbounded, _) | (_, Bound::Unbounded) => 1.0 / 4.0, + _ => 1.0 / 64.0, + } + } +} + +fn scope_lower_includes_null(min: Bound<&DataValue>) -> bool { match min { Bound::Unbounded => true, Bound::Included(value) => value.is_null(), @@ -741,7 +838,7 @@ impl HistogramMeta { } pub fn distinct_values_len(&self) -> usize { - self.number_of_distinct_value + self.number_of_distinct_values.last().copied().unwrap_or(0) } pub fn buckets_len(&self) -> usize { @@ -753,7 +850,8 @@ impl HistogramMeta { #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::errors::DatabaseError; - use crate::expression::range_detacher::Range; + use crate::expression::range_detacher::{ComputedValue, Range, RangeValue}; + use crate::expression::BinaryOperator; use crate::optimizer::core::cm_sketch::CountMinSketch; use crate::optimizer::core::histogram::{ Bucket, HistogramBuilder, HistogramMetaBuilder, DEFAULT_HLL_PRECISION, @@ -807,6 +905,14 @@ mod tests { } } + fn tuple_index_meta(len: usize) -> IndexMeta { + IndexMeta { + value_ty: LogicalType::Tuple(vec![LogicalType::Integer; len]), + ty: IndexType::PrimaryKey { is_multiple: true }, + ..index_meta() + } + } + #[test] fn test_sort_tuples_on_histogram() -> Result<(), DatabaseError> { let mut builder = HistogramBuilder::new( @@ -1008,10 +1114,65 @@ mod tests { Ok(()) } + #[test] + fn parameterized_tuple_ranges_use_prefix_distinct_values() -> Result<(), DatabaseError> { + let mut builder = HistogramBuilder::new( + &tuple_index_meta(4), + DEFAULT_STATISTICS_RELATIVE_ERROR, + DEFAULT_HLL_PRECISION, + )?; + // Shaped like TPC-C order_line: (w, d, o, number) with 10 lines per order. + for d in 0..10 { + for o in 0..300 { + for number in 0..10 { + builder.append(DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(d), + DataValue::Int32(o), + DataValue::Int32(number), + ]))?; + } + } + } + let (histogram, sketch, top_n) = builder.build(10)?; + let distinct = &histogram.meta().number_of_distinct_values; + let near = |actual: usize, expected: usize| actual.abs_diff(expected) * 50 <= expected; + assert_eq!(distinct.len(), 4); + assert_eq!(distinct[..2], [1, 10]); + assert!(near(distinct[2], 3_000), "{distinct:?}"); + assert!(near(distinct[3], 30_000), "{distinct:?}"); + + let parameter = |id| DataValue::Parameter { + id, + ty: LogicalType::Integer, + }; + let prefix = DataValue::Tuple(vec![parameter(1), parameter(2), parameter(3)]); + let count = |range| histogram.collect_count(&[range], &sketch, &top_n, &mut Vec::new()); + // An even spread over the four fields would estimate 30_000^(1/4) ~ 13 rows. + let rows = count(Range::Scope { + min: Bound::Included(prefix.clone().into()), + max: Bound::Included(prefix.into()), + })?; + assert!((9..=11).contains(&rows), "{rows}"); + // The equal prefix then the range on `number`: 10 rows / 64, at least one. + let rows = count(Range::Scope { + min: Bound::Included( + DataValue::Tuple(vec![parameter(1), parameter(2), parameter(3), parameter(4)]) + .into(), + ), + max: Bound::Included( + DataValue::Tuple(vec![parameter(1), parameter(2), parameter(3), parameter(5)]) + .into(), + ), + })?; + assert_eq!(rows, 1); + Ok(()) + } + #[test] fn parameterized_tuple_ranges_accumulate_prefix_selectivity() -> Result<(), DatabaseError> { let mut builder = HistogramBuilder::new( - &index_meta(), + &tuple_index_meta(4), DEFAULT_STATISTICS_RELATIVE_ERROR, DEFAULT_HLL_PRECISION, )?; @@ -1019,17 +1180,17 @@ mod tests { builder.append(DataValue::Tuple(vec![DataValue::Int32(value); 4]))?; } let (mut histogram, sketch, top_n) = builder.build(10)?; - // Fixed metadata isolates the heuristic from HLL estimation error. + // Fixed metadata without prefix NDVs isolates the even-spread fallback. histogram.meta.values_len = 10_000; - histogram.meta.number_of_distinct_value = 10_000; + histogram.meta.number_of_distinct_values = vec![10_000]; let parameter = |id| DataValue::Parameter { id, ty: crate::types::LogicalType::Integer, }; let prefix = vec![parameter(1), parameter(2)]; let scope = |lower, upper| Range::Scope { - min: Bound::Excluded(DataValue::Tuple(lower)), - max: Bound::Excluded(DataValue::Tuple(upper)), + min: Bound::Excluded(DataValue::Tuple(lower).into()), + max: Bound::Excluded(DataValue::Tuple(upper).into()), }; assert_eq!( histogram.collect_count( @@ -1050,6 +1211,29 @@ mod tests { )?, 10 ); + // Computed last fields that differ are a range after the equal prefix, not a point. + let computed = |op| { + RangeValue::Computed(Box::new(ComputedValue { + prefix: Some(prefix.clone()), + op, + left: parameter(3), + right: DataValue::Int32(20), + eval_ty: LogicalType::Integer, + ty: LogicalType::Integer, + })) + }; + assert_eq!( + histogram.collect_count( + &[Range::Scope { + min: Bound::Excluded(computed(BinaryOperator::Minus)), + max: Bound::Excluded(computed(BinaryOperator::Plus)), + }], + &sketch, + &top_n, + &mut Vec::new() + )?, + 2 + ); let mut lower = prefix.clone(); lower.push(parameter(3)); let mut upper = prefix.clone(); @@ -1075,7 +1259,7 @@ mod tests { let full = DataValue::Tuple(vec![parameter(1), parameter(2), parameter(3), parameter(4)]); assert_eq!( histogram.collect_count( - &[Range::Eq(full.clone())], + &[Range::Eq(full.clone().into())], &sketch, &top_n, &mut Vec::new() @@ -1085,8 +1269,8 @@ mod tests { assert_eq!( histogram.collect_count( &[Range::Scope { - min: Bound::Excluded(full.clone()), - max: Bound::Included(full), + min: Bound::Excluded(full.clone().into()), + max: Bound::Included(full.into()), }], &sketch, &top_n, @@ -1323,29 +1507,61 @@ mod tests { id, ty: crate::types::LogicalType::Integer, }; + let computed = |op, id| { + RangeValue::Computed(Box::new(ComputedValue { + prefix: None, + op, + left: parameter(id), + right: DataValue::Int32(20), + eval_ty: LogicalType::Integer, + ty: LogicalType::Integer, + })) + }; for (range, expected) in [ ( - Range::Eq(parameter(1)), + Range::Eq(parameter(1).into()), 1_000usize.div_ceil(histogram.distinct_values_len()), ), ( Range::Scope { - min: Bound::Included(parameter(1)), + min: Bound::Included(parameter(1).into()), max: Bound::Unbounded, }, 250, ), ( Range::Scope { - min: Bound::Included(parameter(1)), - max: Bound::Excluded(parameter(2)), + min: Bound::Included(parameter(1).into()), + max: Bound::Excluded(parameter(2).into()), }, 16, ), ( - Range::SortedRanges(vec![Range::Eq(parameter(1)), Range::Eq(parameter(2))]), + Range::SortedRanges(vec![ + Range::Eq(parameter(1).into()), + Range::Eq(parameter(2).into()), + ]), 2 * 1_000usize.div_ceil(histogram.distinct_values_len()), ), + // Bounds computed differently are two unknowns, not one point. + ( + Range::Scope { + min: Bound::Included(computed(BinaryOperator::Minus, 1)), + max: Bound::Included(computed(BinaryOperator::Plus, 2)), + }, + 16, + ), + ( + Range::Scope { + min: Bound::Included(computed(BinaryOperator::Minus, 1)), + max: Bound::Included(computed(BinaryOperator::Minus, 1)), + }, + 1_000usize.div_ceil(histogram.distinct_values_len()), + ), + ( + Range::Eq(computed(BinaryOperator::Minus, 1)), + 1_000usize.div_ceil(histogram.distinct_values_len()), + ), ] { assert_eq!( histogram.collect_count(&[range], &sketch, &top_n, &mut Vec::new())?, @@ -1388,10 +1604,10 @@ mod tests { let count_1 = histogram.collect_count( &[ - Range::Eq(DataValue::Int32(2)), + Range::Eq(DataValue::Int32(2).into()), Range::Scope { - min: Bound::Included(DataValue::Int32(4)), - max: Bound::Excluded(DataValue::Int32(12)), + min: Bound::Included(DataValue::Int32(4).into()), + max: Bound::Excluded(DataValue::Int32(12).into()), }, ], &sketch, @@ -1403,7 +1619,7 @@ mod tests { let count_2 = histogram.collect_count( &[Range::Scope { - min: Bound::Included(DataValue::Int32(4)), + min: Bound::Included(DataValue::Int32(4).into()), max: Bound::Unbounded, }], &sketch, @@ -1415,7 +1631,7 @@ mod tests { let count_3 = histogram.collect_count( &[Range::Scope { - min: Bound::Excluded(DataValue::Int32(7)), + min: Bound::Excluded(DataValue::Int32(7).into()), max: Bound::Unbounded, }], &sketch, @@ -1428,7 +1644,7 @@ mod tests { let count_4 = histogram.collect_count( &[Range::Scope { min: Bound::Unbounded, - max: Bound::Included(DataValue::Int32(11)), + max: Bound::Included(DataValue::Int32(11).into()), }], &sketch, &top_n, @@ -1440,7 +1656,7 @@ mod tests { let count_5 = histogram.collect_count( &[Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Int32(8)), + max: Bound::Excluded(DataValue::Int32(8).into()), }], &sketch, &top_n, @@ -1451,7 +1667,7 @@ mod tests { let count_6 = histogram.collect_count( &[Range::Scope { - min: Bound::Included(DataValue::Int32(2)), + min: Bound::Included(DataValue::Int32(2).into()), max: Bound::Unbounded, }], &sketch, @@ -1463,7 +1679,7 @@ mod tests { let count_7 = histogram.collect_count( &[Range::Scope { - min: Bound::Excluded(DataValue::Int32(1)), + min: Bound::Excluded(DataValue::Int32(1).into()), max: Bound::Unbounded, }], &sketch, @@ -1476,7 +1692,7 @@ mod tests { let count_8 = histogram.collect_count( &[Range::Scope { min: Bound::Unbounded, - max: Bound::Included(DataValue::Int32(12)), + max: Bound::Included(DataValue::Int32(12).into()), }], &sketch, &top_n, @@ -1488,7 +1704,7 @@ mod tests { let count_9 = histogram.collect_count( &[Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Int32(13)), + max: Bound::Excluded(DataValue::Int32(13).into()), }], &sketch, &top_n, @@ -1499,8 +1715,8 @@ mod tests { let count_10 = histogram.collect_count( &[Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), - max: Bound::Excluded(DataValue::Int32(3)), + min: Bound::Excluded(DataValue::Int32(0).into()), + max: Bound::Excluded(DataValue::Int32(3).into()), }], &sketch, &top_n, @@ -1511,8 +1727,8 @@ mod tests { let count_11 = histogram.collect_count( &[Range::Scope { - min: Bound::Included(DataValue::Int32(1)), - max: Bound::Included(DataValue::Int32(2)), + min: Bound::Included(DataValue::Int32(1).into()), + max: Bound::Included(DataValue::Int32(2).into()), }], &sketch, &top_n, @@ -1567,7 +1783,7 @@ mod tests { assert_eq!( histogram.collect_count( - &[Range::Eq(DataValue::Int32(1))], + &[Range::Eq(DataValue::Int32(1).into())], &sketch, &top_n, &mut Vec::new() @@ -1598,7 +1814,7 @@ mod tests { assert_eq!( histogram.collect_count( - &[Range::Eq(DataValue::Int32(1))], + &[Range::Eq(DataValue::Int32(1).into())], &sketch, &top_n, &mut Vec::new() @@ -1631,7 +1847,7 @@ mod tests { assert_eq!( histogram.collect_count( - &[Range::Eq(DataValue::Int32(7))], + &[Range::Eq(DataValue::Int32(7).into())], &sketch, &top_n, &mut Vec::new() @@ -1667,7 +1883,7 @@ mod tests { assert_eq!( histogram.collect_count( - &[Range::Eq(DataValue::Int32(7))], + &[Range::Eq(DataValue::Int32(7).into())], &sketch, &top_n, &mut Vec::new() @@ -1706,7 +1922,7 @@ mod tests { assert_eq!( histogram.collect_count( - &[Range::Eq(DataValue::Int32(7))], + &[Range::Eq(DataValue::Int32(7).into())], &sketch, &top_n, &mut Vec::new() @@ -1720,7 +1936,7 @@ mod tests { #[test] fn test_collect_count_ignores_tuple_prefix_endpoint_count() -> Result<(), DatabaseError> { let mut builder = HistogramBuilder::new( - &index_meta(), + &tuple_index_meta(2), DEFAULT_STATISTICS_RELATIVE_ERROR, DEFAULT_HLL_PRECISION, )?; @@ -1734,8 +1950,8 @@ mod tests { let (histogram, mut sketch, top_n) = builder.build(5)?; let ranges = [Range::Scope { - min: Bound::Excluded(DataValue::Tuple(vec![DataValue::Int32(0)])), - max: Bound::Excluded(DataValue::Tuple(vec![DataValue::Int32(8)])), + min: Bound::Excluded(DataValue::Tuple(vec![DataValue::Int32(0)]).into()), + max: Bound::Excluded(DataValue::Tuple(vec![DataValue::Int32(8)]).into()), }]; let clean_count = histogram.collect_count(&ranges, &sketch, &top_n, &mut Vec::new())?; diff --git a/src/optimizer/core/statistics_meta.rs b/src/optimizer/core/statistics_meta.rs index 73520d0b..bf207b6f 100644 --- a/src/optimizer/core/statistics_meta.rs +++ b/src/optimizer/core/statistics_meta.rs @@ -227,7 +227,7 @@ mod tests { loader.collect_count( index.table_id, index.id, - &Range::Eq(DataValue::Int32(7)), + &Range::Eq(DataValue::Int32(7).into()), &mut Vec::new() )?, Some((1, histogram.distinct_values_len())) diff --git a/src/optimizer/heuristic/optimizer.rs b/src/optimizer/heuristic/optimizer.rs index b70f24c8..a8c9a4a7 100644 --- a/src/optimizer/heuristic/optimizer.rs +++ b/src/optimizer/heuristic/optimizer.rs @@ -914,9 +914,9 @@ mod tests { assert_eq!( index_info.lookup, Some(IndexLookup::Static(Range::SortedRanges(vec![ - Range::Eq(DataValue::Int32(2)), + Range::Eq(DataValue::Int32(2).into()), Range::Scope { - min: Bound::Excluded(DataValue::Int32(40)), + min: Bound::Excluded(DataValue::Int32(40).into()), max: Bound::Unbounded, } ]))) diff --git a/src/optimizer/rule/implementation/dql/table_scan.rs b/src/optimizer/rule/implementation/dql/table_scan.rs index 6d797b4e..bdc59c8c 100644 --- a/src/optimizer/rule/implementation/dql/table_scan.rs +++ b/src/optimizer/rule/implementation/dql/table_scan.rs @@ -115,8 +115,7 @@ impl ImplementationRule for IndexScanImplementation { 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 { .. }) + if !index_info.covered && !matches!(index_meta.ty, IndexType::PrimaryKey { .. }) { // need to return table query(non-covering index) row_count *= 2; diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index 905376be..3bb75ba0 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -1337,7 +1337,7 @@ mod tests { sort_option: sort_option.clone(), lookup: None, residual_predicate: None, - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, @@ -1421,7 +1421,7 @@ mod tests { sort_option: sort_option.clone(), lookup: None, residual_predicate: None, - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, @@ -1671,10 +1671,13 @@ mod tests { let c2 = make_sort_field(&mut arena, "c2"); for lookup in [ None, - Some(IndexLookup::Static(Range::Eq(DataValue::Parameter { - id: 1, - ty: LogicalType::Integer, - }))), + Some(IndexLookup::Static(Range::Eq( + DataValue::Parameter { + id: 1, + ty: LogicalType::Integer, + } + .into(), + ))), ] { let mut plan = build_plan( &mut arena, diff --git a/src/optimizer/rule/normalization/join_reorder.rs b/src/optimizer/rule/normalization/join_reorder.rs index b0ea80d7..fd50a3c2 100644 --- a/src/optimizer/rule/normalization/join_reorder.rs +++ b/src/optimizer/rule/normalization/join_reorder.rs @@ -789,7 +789,7 @@ mod tests { sort_option: SortOption::None, lookup: None, residual_predicate: None, - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, diff --git a/src/optimizer/rule/normalization/parameterized_index.rs b/src/optimizer/rule/normalization/parameterized_index.rs index 9c845b42..eb23e91e 100644 --- a/src/optimizer/rule/normalization/parameterized_index.rs +++ b/src/optimizer/rule/normalization/parameterized_index.rs @@ -429,10 +429,10 @@ fn use_probe_index(plan: &mut LogicalPlan, position: usize, arena: &PlanArena<'_ let info = &mut scan.index_infos[position]; info.lookup = Some(IndexLookup::Probe); info.residual_predicate = None; - (info.covered_deserializers, info.cover_mapping) = + (info.covered, info.cover_mapping) = match index_covering(scan.deserializers.read_columns(), info.meta, arena) { - Some((deserializers, mapping)) => (Some(deserializers), mapping), - None => (None, None), + Some(mapping) => (true, mapping), + None => (false, None), }; plan.physical_option = Some(PhysicalOption::new( PlanImpl::IndexScan(Box::new(info.clone())), @@ -802,7 +802,7 @@ mod inner_join_tests { lookup: None, residual_predicate: None, sort_option: SortOption::None, - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, @@ -1207,7 +1207,7 @@ mod inner_join_tests { 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.lookup = Some(IndexLookup::Static(Range::Eq(DataValue::Int32(7).into()))); info.residual_predicate = Some(residual); let index = info.meta; right.physical_option = Some(PhysicalOption::new( diff --git a/src/optimizer/rule/normalization/pushdown_predicates.rs b/src/optimizer/rule/normalization/pushdown_predicates.rs index b9013cfb..13356896 100644 --- a/src/optimizer/rule/normalization/pushdown_predicates.rs +++ b/src/optimizer/rule/normalization/pushdown_predicates.rs @@ -24,13 +24,11 @@ use crate::planner::operator::filter::FilterOperator; 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}; -use crate::types::serialize::TupleValueSerializableImpl; -use crate::types::value::DataValue; +use crate::planner::{Childrens, ExprRef, LogicalPlan, MetaArena, PlanArena}; +use crate::types::index::{IndexInfo, IndexLookup, IndexMeta, IndexMetaRef}; use crate::types::LogicalType; +use std::mem; use std::ops::Bound; -use std::{mem, slice}; const EMPTY_SCHEMA: [crate::catalog::ColumnRef; 0] = []; type ClassifiedJoinFilters = (Option, Option, Option, usize); @@ -334,7 +332,7 @@ impl NormalizationRule for PushPredicateIntoScan { meta, lookup, residual_predicate, - covered_deserializers, + covered, cover_mapping, sort_option, sort_elimination_hint: _, @@ -366,9 +364,9 @@ impl NormalizationRule for PushPredicateIntoScan { *lookup = Some(IndexLookup::Static(detached.range)); *residual_predicate = detached.residual; let columns = scan_op.deserializers.read_columns(); - (*covered_deserializers, *cover_mapping) = match index_covering(columns, *meta, arena) { - Some((deserializers, mapping)) => (Some(deserializers), mapping), - None => (None, None), + (*covered, *cover_mapping) = match index_covering(columns, *meta, arena) { + Some(mapping) => (true, mapping), + None => (false, None), }; } @@ -376,39 +374,26 @@ impl NormalizationRule for PushPredicateIntoScan { } } -/// 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. +/// Index-only read layout when `index` holds every scanned column: a scan-to-index slot +/// mapping when their orders differ. Slots after the index columns are primary key fields, +/// which the entry's value holds. pub(crate) fn index_covering( columns: impl Iterator, index: IndexMetaRef, arena: &PlanArena, -) -> Option<(Vec, Option>)> { +) -> Option>> { 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 = (0..index_meta.column_ids.len()) - .map(|idx| index_column_types[idx].skip_serializable()) - .collect::>(); let mut mapping_slots = Vec::with_capacity(index_meta.column_ids.len()); let mut needs_mapping = false; for (scan_idx, column) in columns.enumerate() { - let column = arena.column(column); // Not covered once any scanned column is missing from the index. - let idx = column.id().and_then(|id| { - index_meta - .column_ids - .iter() - .position(|column_id| *column_id == id) - })?; - deserializers[idx] = column.datatype().serializable(); + let idx = covering_idx(column, index_meta, arena)?; mapping_slots.push(idx); - needs_mapping |= scan_idx != idx; + needs_mapping |= scan_idx != idx || idx >= index_meta.column_ids.len(); } - Some((deserializers, needs_mapping.then_some(mapping_slots))) + Some(needs_mapping.then_some(mapping_slots)) } pub(crate) fn is_index_covers( @@ -417,12 +402,23 @@ pub(crate) fn is_index_covers( arena: &PlanArena, ) -> bool { let index_meta = arena.index(index); - columns.all(|column| { - arena - .column(column) - .id() - .is_some_and(|id| index_meta.column_ids.contains(&id)) - }) + columns.all(|column| covering_idx(column, index_meta, arena).is_some()) +} + +fn covering_idx(column: ColumnRef, index_meta: &IndexMeta, arena: &PlanArena) -> Option { + let id = arena.column(column).id()?; + if let Some(idx) = index_meta + .column_ids + .iter() + .position(|column_id| *column_id == id) + { + return Some(idx); + } + let primary_keys = arena.table(index_meta.table_id)?.primary_keys(); + let idx = primary_keys + .iter() + .position(|(_, pk)| arena.column(*pk).id() == Some(id))?; + Some(index_meta.column_ids.len() + idx) } impl PushPredicateIntoScan { @@ -471,12 +467,10 @@ impl PushPredicateIntoScan { if range.only_eq() && apply_column_count != column_count { fn eq_to_scope(range: Range) -> Range { match range { - Range::Eq(DataValue::Tuple(values)) => { - let min = Bound::Included(DataValue::Tuple(values.clone())); - let max = Bound::Included(DataValue::Tuple(values)); - - Range::Scope { min, max } - } + Range::Eq(value) if value.is_tuple() => Range::Scope { + min: Bound::Included(value.clone()), + max: Bound::Included(value), + }, Range::SortedRanges(mut ranges) => { for range in ranges.iter_mut() { let tmp = mem::replace(range, Range::Dummy); @@ -770,7 +764,7 @@ mod tests { assert_eq!( op.index_infos[0].lookup, Some(IndexLookup::Static(Range::Scope { - min: Bound::Excluded(DataValue::Int32(1)), + min: Bound::Excluded(DataValue::Int32(1).into()), max: Bound::Unbounded, })) ); @@ -802,7 +796,7 @@ mod tests { ( first_range, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(2)])), + min: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(2)]).into()), max: Bound::Unbounded, }, 0, @@ -810,17 +804,14 @@ mod tests { ( first_eq, Range::Scope { - min: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(2)])), - max: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(2)])), + min: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(2)]).into()), + max: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(2)]).into()), }, 1, ), ( full_eq, - Range::Eq(DataValue::Tuple(vec![ - DataValue::Int32(2), - DataValue::Int32(3), - ])), + Range::Eq(DataValue::Tuple(vec![DataValue::Int32(2), DataValue::Int32(3)]).into()), 2, ), ] { @@ -837,7 +828,7 @@ mod tests { }, lookup: None, residual_predicate: None, - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, @@ -917,17 +908,23 @@ mod tests { assert_eq!(ignore_prefix_len, 3); assert_eq!( detached.range, - Range::Eq(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2), - DataValue::Int32(3), - ],)) + Range::Eq( + DataValue::Tuple(vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(3), + ],) + .into() + ) ); let residual = detached.residual.expect("c4 predicate should remain"); let residual_detached = RangeDetacher::new(0, &4, &mut arena) .detach(residual)? .expect("residual should be the c4 predicate"); - assert_eq!(residual_detached.range, Range::Eq(DataValue::Int32(4))); + assert_eq!( + residual_detached.range, + Range::Eq(DataValue::Int32(4).into()) + ); assert_eq!(residual_detached.residual, None); Ok(()) @@ -978,18 +975,20 @@ mod tests { assert_eq!( detached.range, Range::Scope { - min: Bound::Excluded(DataValue::Tuple(vec![ - DataValue::Int32(1), - DataValue::Int32(2) - ],)), - max: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(1)])), + min: Bound::Excluded( + DataValue::Tuple(vec![DataValue::Int32(1), DataValue::Int32(2)],).into() + ), + max: Bound::Included(DataValue::Tuple(vec![DataValue::Int32(1)]).into()), } ); let residual = detached.residual.expect("c3 predicate should remain"); let residual_detached = RangeDetacher::new(0, &3, &mut arena) .detach(residual)? .expect("residual should be the c3 predicate"); - assert_eq!(residual_detached.range, Range::Eq(DataValue::Int32(3))); + assert_eq!( + residual_detached.range, + Range::Eq(DataValue::Int32(3).into()) + ); assert_eq!(residual_detached.residual, None); Ok(()) @@ -1065,7 +1064,7 @@ mod tests { }, lookup: None, residual_predicate: None, - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, @@ -1078,7 +1077,7 @@ mod tests { }, lookup: None, residual_predicate: None, - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, @@ -1119,32 +1118,13 @@ mod tests { // verify the first index (reordered scan columns) still uses mapping let reordered_index = &index_infos[0]; - let deserializers = reordered_index - .covered_deserializers - .as_ref() - .expect("expected covering deserializers"); - assert_eq!(deserializers.len(), 3); - assert_eq!( - deserializers[0], - arena.column(c2_ref).datatype().serializable(), - "first serializer should align with c2" - ); - assert_eq!( - deserializers[1], - c3.datatype().skip_serializable(), - "non-projected index column should be skipped" - ); - assert_eq!( - deserializers[2], - arena.column(c1_ref).datatype().serializable(), - "last serializer should align with c1" - ); + assert!(reordered_index.covered); let mapping = reordered_index.cover_mapping.as_deref(); assert_eq!(mapping, Some(&[2, 0][..])); // verify the second index matches scan order exactly so mapping is omitted let ordered_index = &index_infos[1]; - assert!(ordered_index.covered_deserializers.is_some()); + assert!(ordered_index.covered); assert!( ordered_index.cover_mapping.is_none(), "mapping should be None when index/scan order already match" diff --git a/src/optimizer/rule/normalization/simplification.rs b/src/optimizer/rule/normalization/simplification.rs index 44696a6a..2c793b95 100644 --- a/src/optimizer/rule/normalization/simplification.rs +++ b/src/optimizer/rule/normalization/simplification.rs @@ -195,7 +195,7 @@ mod test { range, Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Int32(-2)), + max: Bound::Excluded(DataValue::Int32(-2).into()), } ); } else { @@ -362,7 +362,7 @@ mod test { let mut arena = PlanArena::new(&table_state.table_arena); let expected = Some(Range::Scope { min: Bound::Unbounded, - max: Bound::Included(DataValue::Int32(10)), + max: Bound::Included(DataValue::Int32(10).into()), }); for sql in [ @@ -546,20 +546,20 @@ mod test { range_1_c1, Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Int32(-2)) + max: Bound::Excluded(DataValue::Int32(-2).into()) } ); assert_eq!( range_1_c2, Range::Scope { - min: Bound::Excluded(DataValue::Int32(2)), + min: Bound::Excluded(DataValue::Int32(2).into()), max: Bound::Unbounded } ); assert_eq!( range_2_c1, Range::Scope { - min: Bound::Excluded(DataValue::Int32(2)), + min: Bound::Excluded(DataValue::Int32(2).into()), max: Bound::Unbounded } ); @@ -567,27 +567,27 @@ mod test { range_2_c2, Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Int32(-2)) + max: Bound::Excluded(DataValue::Int32(-2).into()) } ); assert_eq!( range_3_c1, Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Int32(-1)) + max: Bound::Excluded(DataValue::Int32(-1).into()) } ); assert_eq!( range_3_c2, Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), + min: Bound::Excluded(DataValue::Int32(0).into()), max: Bound::Unbounded } ); assert_eq!( range_4_c1, Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), + min: Bound::Excluded(DataValue::Int32(0).into()), max: Bound::Unbounded } ); @@ -595,7 +595,7 @@ mod test { range_4_c2, Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Int32(-1)) + max: Bound::Excluded(DataValue::Int32(-1).into()) } ); @@ -644,7 +644,7 @@ mod test { assert_eq!( plan_filter(&plan_1, table_state.column_id_by_name("c1"), &mut arena)?, - Some(Range::Eq(DataValue::Null)) + Some(Range::Eq(DataValue::Null.into())) ); Ok(()) @@ -661,7 +661,7 @@ mod test { plan_filter(&plan_1, table_state.column_id_by_name("c1"), &mut arena)?, Some(Range::Scope { min: Bound::Unbounded, - max: Bound::Excluded(DataValue::Null), + max: Bound::Excluded(DataValue::Null.into()), }) ); @@ -736,9 +736,9 @@ mod test { assert_eq!( plan_filter(&plan_1, table_state.column_id_by_name("c1"), &mut arena)?, Some(Range::SortedRanges(vec![ - Range::Eq(DataValue::Int32(1)), - Range::Eq(DataValue::Int32(2)), - Range::Eq(DataValue::Int32(3)), + Range::Eq(DataValue::Int32(1).into()), + Range::Eq(DataValue::Int32(2).into()), + Range::Eq(DataValue::Int32(3).into()), ])) ); diff --git a/src/planner/arena.rs b/src/planner/arena.rs index 9d4eb066..4496adb0 100644 --- a/src/planner/arena.rs +++ b/src/planner/arena.rs @@ -1212,21 +1212,18 @@ impl<'a> ParamArena<'a> { self.scalar_query_count = parent.scalar_query_count; for (slot, &expr_ref) in self.parameter_expressions.iter().enumerate() { debug_assert_eq!(parent.arena_expression(expr_ref).param, Some(slot)); - let ScalarExpression::Constant(value @ DataValue::Parameter { id, .. }) = + let ScalarExpression::Constant(DataValue::Parameter { id, ty }) = parent.expression(expr_ref) else { unreachable!("parameter expression must be a single parameter constant"); }; let id = *id; - let mut value = value.clone(); - value.bind_parameters( - &|id| { - params - .iter() - .find_map(|(candidate, value)| (*candidate == id).then_some(value)) - }, - &|_| None, - )?; + let value = params + .iter() + .find_map(|(candidate, value)| (*candidate == id).then_some(value)) + .ok_or_else(|| DatabaseError::parameter_not_found(format!("${id}")))? + .clone() + .cast(ty)?; // reuse match self.expressions.get_mut(slot) { Some(( diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index 70411697..e35b7f14 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -1242,7 +1242,7 @@ mod tests { sort_option: SortOption::None, lookup: None, residual_predicate: None, - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, diff --git a/src/planner/operator/table_scan.rs b/src/planner/operator/table_scan.rs index 04c27990..f17f97f9 100644 --- a/src/planner/operator/table_scan.rs +++ b/src/planner/operator/table_scan.rs @@ -73,7 +73,7 @@ impl TableScanOperator { }, lookup: None, residual_predicate: None, - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, diff --git a/src/planner/operator/visitor.rs b/src/planner/operator/visitor.rs index a3d6965e..de08a31c 100644 --- a/src/planner/operator/visitor.rs +++ b/src/planner/operator/visitor.rs @@ -635,7 +635,7 @@ pub(crate) mod tests { }, lookup: None, residual_predicate: Some(residual_predicate), - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, diff --git a/src/serdes/mod.rs b/src/serdes/mod.rs index 3be086fd..e0dd73dd 100644 --- a/src/serdes/mod.rs +++ b/src/serdes/mod.rs @@ -161,16 +161,14 @@ mod tests { #[test] fn test_generic_range_serialization() -> Result<(), DatabaseError> { let range = Range::SortedRanges(vec![ - Range::Eq(DataValue::Int32(7)), + Range::Eq(DataValue::Int32(7).into()), Range::Scope { - min: Bound::Included(DataValue::Int32(8)), - max: Bound::Excluded(DataValue::Int32(12)), + min: Bound::Included(DataValue::Int32(8).into()), + max: Bound::Excluded(DataValue::Int32(12).into()), }, Range::SortedRanges(vec![Range::Dummy]), ]); assert_eq!(round_trip(range.clone())?, range); - let generic = Range::SortedRanges(vec![Range::Eq("prefix".to_string())]); - assert_eq!(round_trip(generic.clone())?, generic); Ok(()) } diff --git a/src/storage/memory.rs b/src/storage/memory.rs index 6b00b327..8acd391b 100644 --- a/src/storage/memory.rs +++ b/src/storage/memory.rs @@ -368,10 +368,10 @@ mod wasm_tests { .id(), (Some(0), None), pk_index, - &[Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), - max: Bound::Included(DataValue::Int32(2)), - }][..], + Range::Scope { + min: Bound::Excluded(DataValue::Int32(0).into()), + max: Bound::Included(DataValue::Int32(2).into()), + }, true, &deserializers, false, @@ -557,10 +557,10 @@ mod native_tests { .id(), (Some(0), None), pk_index, - &[Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), - max: Bound::Included(DataValue::Int32(2)), - }][..], + Range::Scope { + min: Bound::Excluded(DataValue::Int32(0).into()), + max: Bound::Included(DataValue::Int32(2).into()), + }, true, &deserializers, false, diff --git a/src/storage/mod.rs b/src/storage/mod.rs index a48edb3c..1395d868 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -23,7 +23,7 @@ use crate::catalog::view::View; use crate::catalog::{ColumnCatalog, ColumnRef, TableCatalog, TableId, TableMeta, TableName}; use crate::db::{ScalaFunctions, TableFunctions}; use crate::errors::DatabaseError; -use crate::expression::range_detacher::Range; +use crate::expression::range_detacher::{Range, RangeValue}; use crate::expression::TypeCast; use crate::iter_ext::Itertools; use crate::optimizer::core::cm_sketch::{ @@ -1316,18 +1316,10 @@ fn encode_bound_key(buffer: &mut Bytes, key: &[u8], is_upper: bool) { } } -#[inline] -fn index_values(value: &DataValue) -> &[DataValue] { - match value { - DataValue::Tuple(values) => values, - value => std::slice::from_ref(value), - } -} - #[inline] fn encode_bound<'a>( table_codec: &mut TableCodec, - bound: &Bound, + bound: &Bound, is_upper: bool, buffer: &'a mut Bytes, params: &IndexImplParams<'_, impl StorageTransaction>, @@ -1335,11 +1327,11 @@ fn encode_bound<'a>( ) -> Result, DatabaseError> { match bound { Bound::Included(val) => { - inner.bound_key(table_codec, params, val, is_upper, buffer)?; + inner.bound_key(table_codec, params, val.value()?, is_upper, buffer)?; Ok(Bound::Included(buffer.as_slice())) } Bound::Excluded(val) => { - inner.bound_key(table_codec, params, val, !is_upper, buffer)?; + inner.bound_key(table_codec, params, val.value()?, !is_upper, buffer)?; Ok(Bound::Excluded(buffer.as_slice())) } Bound::Unbounded => Ok(Bound::Unbounded), @@ -1412,6 +1404,9 @@ impl TupleMapping { let mut target_len = 0; for index_idx in scan_to_index { + if index_idx >= index_to_scan.len() { + index_to_scan.resize(index_idx + 1, usize::MAX); + } index_to_scan[index_idx] = target_len; target_len += 1; } @@ -1701,11 +1696,7 @@ impl IndexImpl for UniqueIndexImpl { _: &mut Bytes, _: &mut Bytes, ) -> Result, DatabaseError> { - let index = Index::new( - params.index_meta().id, - index_values(value), - IndexType::Unique, - ); + let index = Index::new(params.index_meta().id, value.as_slice(), IndexType::Unique); let Some(bytes) = table_codec.with_index(params.table_id, &index, None, |key, _| { params.tx.get_borrowed(key) })? @@ -1728,11 +1719,7 @@ impl IndexImpl for UniqueIndexImpl { is_upper: bool, out: &mut Bytes, ) -> Result<(), DatabaseError> { - let index = Index::new( - params.index_meta().id, - index_values(value), - IndexType::Unique, - ); + let index = Index::new(params.index_meta().id, value.as_slice(), IndexType::Unique); table_codec.with_index(params.table_id, &index, None, |key, _| { if index.values.len() < params.index_meta().column_ids.len() { @@ -1787,11 +1774,7 @@ impl IndexImpl for NormalIndexImpl { is_upper: bool, out: &mut Bytes, ) -> Result<(), DatabaseError> { - let index = Index::new( - params.index_meta().id, - index_values(value), - IndexType::Normal, - ); + let index = Index::new(params.index_meta().id, value.as_slice(), IndexType::Normal); table_codec.with_index(params.table_id, &index, None, |key, _| { encode_bound_key(out, key, is_upper); Ok(()) @@ -1841,7 +1824,7 @@ impl IndexImpl for CompositeIndexImpl { ) -> Result<(), DatabaseError> { let index = Index::new( params.index_meta().id, - index_values(value), + value.as_slice(), IndexType::Composite, ); table_codec.with_index(params.table_id, &index, None, |key, _| { @@ -1860,22 +1843,25 @@ impl IndexImpl for CoveredIndexImpl { value: &[u8], params: &IndexImplParams, ) -> Result<(), DatabaseError> { + let ty = params.value_ty(); match ¶ms.cover_mapping { - Some(mapping) => { - TableCodec::decode_index_key_into(tuple, key, params.value_ty(), &mapping.as_ref())? - } - None => { - TableCodec::decode_index_key_into(tuple, key, params.value_ty(), &OrderedMapping)? - } + Some(mapping) => TableCodec::decode_index_into( + tuple, + key, + value, + ty, + &mapping.as_ref(), + params.with_pk, + ), + None => TableCodec::decode_index_into( + tuple, + key, + value, + ty, + &OrderedMapping, + params.with_pk, + ), } - - let tuple_id = if params.with_pk { - Some(TableCodec::decode_index(value)?) - } else { - None - }; - tuple.pk = tuple_id; - Ok(()) } fn eq_to_res<'a>( @@ -1908,7 +1894,7 @@ impl IndexImpl for CoveredIndexImpl { ) -> Result<(), DatabaseError> { let index = Index::new( params.index_meta().id, - index_values(value), + value.as_slice(), params.index_meta().ty, ); table_codec.with_index(params.table_id, &index, None, |key, _| { @@ -2200,7 +2186,9 @@ impl<'a, T: StorageTransaction> IndexIter<'a, T> { 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, + (RangeSource::Owned(Range::Eq(value)), RuntimeIndexProbe::Eq(probe)) => { + *value = RangeValue::Value(probe) + } (source, probe) => *source = RangeSource::Owned(probe.into()), } self.rewind(bounds); @@ -2280,7 +2268,7 @@ impl Iter for IndexIter<'_, T> { match self.inner.eq_to_res( table_codec, tuple, - val, + val.value()?, &self.params, &mut self.encode_min_buffer, &mut self.encode_max_buffer, diff --git a/src/storage/rocksdb.rs b/src/storage/rocksdb.rs index b242e76f..ba2f2a6a 100644 --- a/src/storage/rocksdb.rs +++ b/src/storage/rocksdb.rs @@ -1072,10 +1072,10 @@ mod test { DataValue::Int32(4), ]; let ranges = [ - Range::Eq(DataValue::Int32(0)), + Range::Eq(DataValue::Int32(0).into()), Range::Scope { - min: Bound::Included(DataValue::Int32(2)), - max: Bound::Included(DataValue::Int32(4)), + min: Bound::Included(DataValue::Int32(2).into()), + max: Bound::Included(DataValue::Int32(4).into()), }, ]; let deserializers = crate::storage::TupleDeserializers::new(&table, &plan_arena, false); @@ -1145,10 +1145,10 @@ mod test { .id(), (Some(0), Some(1)), table.indexes[0], - &[Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), + Range::Scope { + min: Bound::Excluded(DataValue::Int32(0).into()), max: Bound::Unbounded, - }][..], + }, true, &deserializers, false, @@ -1185,10 +1185,10 @@ mod test { .id(), (Some(0), Some(1)), table.indexes[0], - &[Range::Scope { - min: Bound::Excluded(DataValue::Int32(3)), + Range::Scope { + min: Bound::Excluded(DataValue::Int32(3).into()), max: Bound::Unbounded, - }][..], + }, true, &deserializers, false, @@ -1233,8 +1233,8 @@ mod test { .ok_or(DatabaseError::InvalidIndex)?; let c1_ranges = [Range::Scope { - min: Bound::Excluded(DataValue::Int32(0)), - max: Bound::Excluded(DataValue::Int32(10)), + min: Bound::Excluded(DataValue::Int32(0).into()), + max: Bound::Excluded(DataValue::Int32(10).into()), }]; let deserializers = crate::storage::project_deserializers_for_test( kite_sql.state.names(), @@ -1412,7 +1412,7 @@ mod test { let target_pk = DataValue::Int32(3); let covered_value = DataValue::Int32(4); - let covered_ranges = [Range::Eq(covered_value.clone())]; + let covered_ranges = [Range::Eq(covered_value.clone().into())]; let mut table_codec = TableCodec::default(); transaction.remove_tuple(&mut table_codec, 0, &target_pk)?; diff --git a/src/storage/table_codec.rs b/src/storage/table_codec.rs index 1a036590..85ea586b 100644 --- a/src/storage/table_codec.rs +++ b/src/storage/table_codec.rs @@ -31,6 +31,7 @@ use crate::types::value::{DataValue, IndexKeyMapping}; use crate::types::{ColumnId, LogicalType}; use std::borrow::Borrow; use std::io::{Cursor, Read}; +use std::mem; use std::sync::LazyLock; pub(crate) const BOUND_MIN_TAG: u8 = u8::MIN; @@ -859,15 +860,27 @@ impl TableCodec { IndexMeta::decode::(&mut Cursor::new(bytes), None, arena) } - pub fn decode_index_key_into( + pub fn decode_index_into( tuple: &mut Tuple, - bytes: &[u8], + key: &[u8], + value: &[u8], ty: &LogicalType, mapping: &M, + with_pk: bool, ) -> Result<(), DatabaseError> { + fn mapping_value<'a, M: IndexKeyMapping>( + values: &'a mut [DataValue], + mapping: &M, + index_pos: usize, + ) -> Result, DatabaseError> { + mapping + .scan_index(index_pos) + .map(|scan_pos| values.get_mut(scan_pos).ok_or(DatabaseError::InvalidIndex)) + .transpose() + } // Hash + TypeTag + Bound Min + Index Id Len + Bound Min let start = TUPLE_KEY_PREFIX_LEN + INDEX_ID_LEN + KEY_BOUND_LEN; - let bytes = bytes.get(start..).ok_or(DatabaseError::InvalidIndex)?; + let bytes = key.get(start..).ok_or(DatabaseError::InvalidIndex)?; let mut reader = Cursor::new(bytes); let types = match ty { LogicalType::Tuple(types) => types.as_slice(), @@ -878,17 +891,32 @@ impl TableCodec { .values .resize(mapping.target_len(types.len()), DataValue::Null); for (index_pos, ty) in types.iter().enumerate() { - if let Some(scan_pos) = mapping.scan_index(index_pos) { - 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)?; + match mapping_value(&mut tuple.values, mapping, index_pos)? { + Some(target) => *target = DataValue::memcomparable_decode(&mut reader, ty)?, + None => DataValue::memcomparable_skip(&mut reader, ty)?, + } + } + + let has_pk_fields = mapping.has_pk_fields(types.len()); + if !with_pk && !has_pk_fields { + tuple.pk = None; + return Ok(()); + } + let mut tuple_id = Self::decode_index(value)?; + if has_pk_fields { + for (pk_pos, field) in tuple_id.as_mut_slice().iter_mut().enumerate() { + if let Some(target) = + mapping_value(&mut tuple.values, mapping, types.len() + pk_pos)? + { + *target = if with_pk { + field.clone() + } else { + mem::replace(field, DataValue::Null) + }; + } } } + tuple.pk = with_pk.then_some(tuple_id); Ok(()) } @@ -1117,9 +1145,10 @@ mod tests { ]; let index = Index::new(7, values.as_slice(), IndexType::Normal); let mut codec = TableCodec::default(); - let key = codec.with_index(0, &index, Some(&DataValue::Int32(1)), |key, _| { - Ok(key.to_vec()) - })?; + let (key, value) = + codec.with_index(0, &index, Some(&DataValue::Int32(1)), |key, value| { + Ok((key.to_vec(), value.to_vec())) + })?; let index_ty = LogicalType::Tuple(vec![ LogicalType::Integer, varchar.clone(), @@ -1131,7 +1160,7 @@ mod tests { 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)?; + TableCodec::decode_index_into(&mut tuple, &key, &[], &index_ty, &mapping, false)?; assert_eq!( tuple.values, vec![DataValue::Int32(99), DataValue::Int32(7)] @@ -1140,11 +1169,32 @@ mod tests { } 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)?; + TableCodec::decode_index_into(&mut tuple, &key, &[], &index_ty, &mapping, false)?; assert_eq!(tuple.values, vec![DataValue::Null]); - assert!( - TableCodec::decode_index_key_into(&mut tuple, &[], &index_ty, &OrderedMapping).is_err() - ); + + // A slot after the index fields reads the primary key from the value. + let mapping = TupleMappingRef::new(&[1, usize::MAX, usize::MAX, usize::MAX, 2, 0], 3); + for with_pk in [false, true] { + TableCodec::decode_index_into(&mut tuple, &key, &value, &index_ty, &mapping, with_pk)?; + assert_eq!( + tuple.values, + vec![ + DataValue::Int32(1), + DataValue::Int32(7), + DataValue::Int32(99) + ] + ); + assert_eq!(tuple.pk, with_pk.then_some(DataValue::Int32(1))); + } + assert!(TableCodec::decode_index_into( + &mut tuple, + &[], + &[], + &index_ty, + &OrderedMapping, + false + ) + .is_err()); Ok(()) } diff --git a/src/types/index.rs b/src/types/index.rs index 1a401b4e..afc93064 100644 --- a/src/types/index.rs +++ b/src/types/index.rs @@ -14,13 +14,12 @@ use crate::catalog::{TableCatalog, TableId}; use crate::errors::DatabaseError; -use crate::expression::range_detacher::Range; +use crate::expression::range_detacher::{Range, RangeValue}; use crate::expression::ScalarExpression; use crate::planner::operator::SortOption; 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}; @@ -68,8 +67,11 @@ 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 }, + RuntimeIndexProbe::Eq(value) => Range::Eq(value.into()), + RuntimeIndexProbe::Scope { min, max } => Range::Scope { + min: min.map(RangeValue::Value), + max: max.map(RangeValue::Value), + }, } } } @@ -86,7 +88,8 @@ pub struct IndexInfo { pub(crate) sort_option: SortOption, pub(crate) lookup: Option, pub(crate) residual_predicate: Option, - pub(crate) covered_deserializers: Option>, + // The index holds every scanned column, so no row is looked up. + pub(crate) covered: bool, pub(crate) cover_mapping: Option>, pub(crate) sort_elimination_hint: Option, pub(crate) stream_aggregate_hint: Option, @@ -186,7 +189,7 @@ impl Explain for IndexInfo { if self.is_reverse() { f.write_str(" Reverse")?; } - if self.covered_deserializers.is_some() { + if self.covered { f.write_str(" Covered")?; } Ok(()) @@ -222,7 +225,7 @@ mod tests { sort_option: SortOption::None, lookup, residual_predicate: None, - covered_deserializers: None, + covered: false, cover_mapping: None, sort_elimination_hint: None, stream_aggregate_hint: None, @@ -285,9 +288,11 @@ mod tests { probe.meta = index_ref; assert_eq!(probe.explain(&arena).to_string(), "idx_t => Probe ?"); - let mut info = index_info(Some(IndexLookup::Static(Range::Eq(DataValue::Int32(1))))); + let mut info = index_info(Some(IndexLookup::Static(Range::Eq( + DataValue::Int32(1).into(), + )))); info.meta = index_ref; - info.covered_deserializers = Some(vec![LogicalType::Integer.serializable()]); + info.covered = true; assert_eq!(info.explain(&arena).to_string(), "idx_t => 1 Covered"); let value = DataValue::Int32(1); @@ -319,7 +324,7 @@ mod tests { assert!(probes.contains(&RuntimeIndexProbe::Eq(DataValue::Int32(1)))); let mut lookups = HashSet::new(); - lookups.insert(IndexLookup::Static(Range::Eq(DataValue::Int32(1)))); + lookups.insert(IndexLookup::Static(Range::Eq(DataValue::Int32(1).into()))); lookups.insert(IndexLookup::Probe); assert!(lookups.contains(&IndexLookup::Probe)); @@ -368,7 +373,7 @@ mod tests { } for lookup in [ - IndexLookup::Static(Range::Eq(DataValue::Int32(1))), + IndexLookup::Static(Range::Eq(DataValue::Int32(1).into())), IndexLookup::Probe, ] { assert_eq!( @@ -392,7 +397,7 @@ mod tests { sort_option: SortOption::None, lookup: Some(IndexLookup::Probe), residual_predicate: None, - covered_deserializers: Some(vec![LogicalType::Integer.serializable()]), + covered: true, cover_mapping: Some(vec![0]), sort_elimination_hint: Some(IndexOrderHint::new(1)), stream_aggregate_hint: Some(IndexOrderHint::new(1)), diff --git a/src/types/value.rs b/src/types/value.rs index 76505d8a..cb971338 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -33,7 +33,7 @@ use std::hash::Hash; use std::io::Read; #[cfg(feature = "decimal")] use std::mem; -use std::{cmp, fmt}; +use std::{cmp, fmt, slice}; trait ReadByteExt: Read { fn read_u8(&mut self) -> std::io::Result { @@ -252,6 +252,10 @@ pub trait IndexKeyMapping { fn target_len(&self, fields_len: usize) -> usize; fn scan_index(&self, index_pos: usize) -> Option; + + fn has_pk_fields(&self, _fields_len: usize) -> bool { + false + } } #[derive(Clone, Copy)] @@ -307,6 +311,11 @@ impl IndexKeyMapping for TupleMappingRef<'_> { fn scan_index(&self, index_pos: usize) -> Option { TupleMappingRef::scan_index(self, index_pos) } + + #[inline] + fn has_pk_fields(&self, fields_len: usize) -> bool { + self.index_to_scan.len() > fields_len + } } macro_rules! generate_get_option { @@ -631,64 +640,6 @@ impl Hash for DataValue { } } impl DataValue { - /// Estimate bound selectivity using the complete index key type. - pub(crate) fn bound_selectivity( - min: std::ops::Bound<&Self>, - max: std::ops::Bound<&Self>, - key_type: &LogicalType, - distinct: f64, - ) -> f64 { - use std::ops::Bound; - - match (min, max, key_type) { - ( - Bound::Included(Self::Tuple(lower)) | Bound::Excluded(Self::Tuple(lower)), - Bound::Included(Self::Tuple(upper)) | Bound::Excluded(Self::Tuple(upper)), - LogicalType::Tuple(fields), - ) if !fields.is_empty() => { - if lower == upper - && lower.len() == fields.len() - && upper.len() == fields.len() - && (matches!(min, Bound::Excluded(_)) || matches!(max, Bound::Excluded(_))) - { - return 0.0; - } - // Approximate field NDV from full-key NDV; no per-field statistics. - let distinct = distinct.powf(1.0 / fields.len() as f64); - let mut divisor = 1.0; - for (i, field) in fields.iter().enumerate() { - let (min, max, is_range) = match (lower.get(i), upper.get(i)) { - (Some(lower), Some(upper)) => ( - Bound::Included(lower), - Bound::Included(upper), - lower != upper, - ), - (Some(lower), None) => (Bound::Included(lower), Bound::Unbounded, true), - (None, Some(upper)) => (Bound::Unbounded, Bound::Included(upper), true), - (None, None) => return 1.0 / divisor, - }; - let selectivity = Self::bound_selectivity(min, max, field, distinct); - if selectivity == 0.0 { - return 0.0; - } - divisor /= selectivity; - // Only the equal prefix and first differing field constrain - // a lexicographic range. Missing fields delimit the prefix. - if is_range { - return 1.0 / divisor; - } - } - 1.0 / divisor - } - (Bound::Included(lower), Bound::Included(upper), _) if lower == upper => 1.0 / distinct, - (Bound::Unbounded, Bound::Unbounded, _) => 1.0, - // SQLite 3.45.3 whereRangeScanEst fallback: 1/4 for one bound, - // 1/64 for two. These are heuristics, not bucket estimates. - (Bound::Unbounded, _, _) | (_, Bound::Unbounded, _) => 1.0 / 4.0, - _ => 1.0 / 64.0, - } - } - pub(crate) fn parameter_id(placeholder: &str) -> Option { let digits = placeholder.strip_prefix('$')?; if digits.is_empty() || digits.starts_with('0') { @@ -712,7 +663,7 @@ impl DataValue { return Err(DatabaseError::parameter_not_found(format!("${id}"))); }; *self = input.clone().cast(ty)?; - Ok(false) + Ok(self.is_null()) } DataValue::InitValue { id, ty } => { let Some(input) = init_values(*id) else { @@ -921,6 +872,27 @@ impl DataValue { matches!(self, DataValue::Null) } + #[inline] + pub(crate) fn is_tuple(&self) -> bool { + matches!(self, DataValue::Tuple(_)) + } + + #[inline] + pub(crate) fn as_slice(&self) -> &[DataValue] { + match self { + DataValue::Tuple(values) => values, + value => slice::from_ref(value), + } + } + + #[inline] + pub(crate) fn as_mut_slice(&mut self) -> &mut [DataValue] { + match self { + DataValue::Tuple(values) => values, + value => slice::from_mut(value), + } + } + #[inline] pub fn has_null(&self) -> bool { match self { diff --git a/tests/slt/where.slt b/tests/slt/where.slt index 0331c930..bd333692 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_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)] +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 Covered => (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_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)] +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 Covered => (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_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)] +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 Covered => (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 Covered => (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; @@ -914,7 +914,7 @@ select id from cp_b where c1 = c2 and c2 = 4; 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)] +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 Covered => (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; @@ -925,7 +925,7 @@ select cp_a.id, cp_b.id from cp_a left join cp_b on cp_a.c1 = cp_b.c1 where cp_a 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)] +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 Covered => (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; diff --git a/tests/slt/where_by_index_explain.slt b/tests/slt/where_by_index_explain.slt index 1e9349f6..a2bffb1c 100644 --- a/tests/slt/where_by_index_explain.slt +++ b/tests/slt/where_by_index_explain.slt @@ -128,12 +128,12 @@ Projection [t1.id, t1.c1, t1.c2] [Project => (Sort Option: Follow)] Filter (t1.c query T explain select * from t1 where c1 = 7 and c2 < 9; ---- -Projection [t1.id, t1.c1, t1.c2] [Project => (Sort Option: Follow)] Filter (t1.c2 < 9), Is Having: false [Filter => (Sort Option: Follow)] TableScan t1 -> [t1.id, t1.c1, t1.c2] [IndexScan By u_c1_index => 7 => (Sort Option: OrderBy: (t1.c1 Asc Nulls Last) ignore_prefix_len: 0)] +Projection [t1.id, t1.c1, t1.c2] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.id, t1.c1, t1.c2] [IndexScan By p_index => [(7), (7, 9)) Covered => (Sort Option: OrderBy: (t1.c1 Asc Nulls Last, t1.c2 Asc Nulls Last) ignore_prefix_len: 1)] query T explain select * from t1 where (c1 = 7 or c1 = 10) and c2 < 9; ---- -Projection [t1.id, t1.c1, t1.c2] [Project => (Sort Option: Follow)] Filter (t1.c2 < 9), Is Having: false [Filter => (Sort Option: Follow)] TableScan t1 -> [t1.id, t1.c1, t1.c2] [IndexScan By u_c1_index => 7, 10 => (Sort Option: OrderBy: (t1.c1 Asc Nulls Last) ignore_prefix_len: 0)] +Projection [t1.id, t1.c1, t1.c2] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.id, t1.c1, t1.c2] [IndexScan By p_index => [(7), (7, 9)), [(10), (10, 9)) Covered => (Sort Option: OrderBy: (t1.c1 Asc Nulls Last, t1.c2 Asc Nulls Last) ignore_prefix_len: 1)] query T explain select * from t1 where c1 is null and c2 is null; @@ -143,7 +143,7 @@ Projection [t1.id, t1.c1, t1.c2] [Project => (Sort Option: Follow)] Filter t1.c2 query T explain select * from t1 where c1 > 0 and c1 < 8; ---- -Projection [t1.id, t1.c1, t1.c2] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.id, t1.c1, t1.c2] [IndexScan By u_c1_index => (0, 8) => (Sort Option: OrderBy: (t1.c1 Asc Nulls Last) ignore_prefix_len: 0)] +Projection [t1.id, t1.c1, t1.c2] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.id, t1.c1, t1.c2] [IndexScan By p_index => ((0), (8)) Covered => (Sort Option: OrderBy: (t1.c1 Asc Nulls Last, t1.c2 Asc Nulls Last) ignore_prefix_len: 0)] query T explain select * from t1 where c2 > 0 and c2 < 9; @@ -182,7 +182,7 @@ Projection [t1.c1] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.c1] [I query T explain select c1, id from t1 where c1 < 10; ---- -Projection [t1.c1, t1.id] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.id, t1.c1] [IndexScan By u_c1_index => (-inf, 10) => (Sort Option: OrderBy: (t1.c1 Asc Nulls Last) ignore_prefix_len: 0)] +Projection [t1.c1, t1.id] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.id, t1.c1] [IndexScan By u_c1_index => (-inf, 10) Covered => (Sort Option: OrderBy: (t1.c1 Asc Nulls Last) ignore_prefix_len: 0)] statement ok drop index t1.u_c1_index; @@ -245,3 +245,35 @@ Projection [t_cover.c2, t_cover.c3] [Project => (Sort Option: Follow)] Filter (t statement ok drop table t_cover; + +statement ok +create table t_pk_cover(a int, b int, c int, d int, primary key (a, b)); + +statement ok +insert into t_pk_cover values (1, 10, 100, 0), (2, 20, 100, 0), (3, 30, 300, 0), (4, 40, 400, 0), (5, 50, 500, 0), (6, 60, 600, 0), (7, 70, 700, 0), (8, 80, 800, 0), (9, 90, 900, 0), (10, 100, 1000, 0), (11, 110, 1100, 0), (12, 120, 1200, 0); + +statement ok +create index idx_pk_cover on t_pk_cover (c); + +statement ok +analyze table t_pk_cover; + +# Primary key fields are read from the index entry, in any scan order. +query T +explain select b, c, a from t_pk_cover where c = 100; +---- +Projection [t_pk_cover.b, t_pk_cover.c, t_pk_cover.a] [Project => (Sort Option: Follow)] TableScan t_pk_cover -> [t_pk_cover.a, t_pk_cover.b, t_pk_cover.c] [IndexScan By idx_pk_cover => 100 Covered => (Sort Option: OrderBy: (t_pk_cover.c Asc Nulls Last) ignore_prefix_len: 0)] + +query III rowsort +select b, c, a from t_pk_cover where c = 100; +---- +10 100 1 +20 100 2 + +query I +select count(b) from t_pk_cover where c = 100; +---- +2 + +statement ok +drop table t_pk_cover; diff --git a/tpcc/README.md b/tpcc/README.md index 54aaf68e..a19317e7 100644 --- a/tpcc/README.md +++ b/tpcc/README.md @@ -72,11 +72,11 @@ 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 | 187161 | 274 | 68 | 40 | 454 | 278 | -| KiteSQL RocksDB | 46291 | 374 | 184 | 201 | 14463 | 651 | -| SQLite | 179039 | 291 | 43 | 38 | 368 | 320 | +| KiteSQL LMDB | 209840 | 254 | 50 | 28 | 345 | 202 | +| KiteSQL RocksDB | 48049 | 354 | 85 | 146 | 14511 | 562 | +| SQLite | 192435 | 270 | 40 | 33 | 344 | 265 | -- Run date: `2026-10-31`. Latency is measured in microseconds and includes commit. +- Run date: `2026-11-01`. 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. - All rows run without `--sync`, so commits are not fsynced; SQLite uses WAL with `synchronous = OFF`.