From 992225a5c644dc9ec0cbd78f4b42abd338f462c4 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 11 Jan 2026 07:29:33 +0800 Subject: [PATCH 1/2] fix: ensure index cover mapping only derives after range pushdown --- Makefile | 13 +- src/execution/dql/index_scan.rs | 7 +- src/execution/mod.rs | 3 +- src/optimizer/core/memo.rs | 1 + .../rule/normalization/pushdown_predicates.rs | 212 ++++++++++++++++-- src/planner/operator/table_scan.rs | 1 + src/storage/memory.rs | 2 + src/storage/mod.rs | 76 +++++-- src/storage/rocksdb.rs | 66 ++++++ src/storage/table_codec.rs | 10 +- src/types/index.rs | 1 + src/types/value.rs | 125 ++++++++++- tpcc/src/main.rs | 12 + 13 files changed, 486 insertions(+), 43 deletions(-) diff --git a/Makefile b/Makefile index e7b10c88..b2ad2df3 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ CARGO ?= cargo WASM_PACK ?= wasm-pack SQLLOGIC_PATH ?= tests/slt/**/*.slt -.PHONY: test test-wasm test-slt test-all wasm-build +.PHONY: test test-wasm test-slt test-all wasm-build check tpcc ## Run default Rust tests in the current environment (non-WASM). test: @@ -19,7 +19,16 @@ test-wasm: ## Run the sqllogictest harness against the configured .slt suite. test-slt: - $(CARGO) run -p sqllogictest-test -- --path "$(SQLLOGIC_PATH)" + $(CARGO) run -p sqllogictest-test -- --path '$(SQLLOGIC_PATH)' ## Convenience target to run every suite in sequence. test-all: test test-wasm test-slt + +## Run formatting (check mode) and clippy linting together. +check: + $(CARGO) fmt --all -- --check + $(CARGO) clippy --all-targets --all-features -- -D warnings + +## Execute the TPCC workload example as a standalone command. +tpcc: + $(CARGO) run -p tpcc --release diff --git a/src/execution/dql/index_scan.rs b/src/execution/dql/index_scan.rs index 16bb4bd3..1fb046ba 100644 --- a/src/execution/dql/index_scan.rs +++ b/src/execution/dql/index_scan.rs @@ -25,6 +25,7 @@ pub(crate) struct IndexScan { index_by: IndexMetaRef, ranges: Vec, covered_deserializers: Option>, + cover_mapping: Option>, } impl @@ -33,14 +34,16 @@ impl IndexMetaRef, Range, Option>, + Option>, )> for IndexScan { fn from( - (op, index_by, range, covered_deserializers): ( + (op, index_by, range, covered_deserializers, cover_mapping): ( TableScanOperator, IndexMetaRef, Range, Option>, + Option>, ), ) -> Self { let ranges = match range { @@ -53,6 +56,7 @@ impl index_by, ranges, covered_deserializers, + cover_mapping, } } } @@ -83,6 +87,7 @@ impl<'a, T: Transaction + 'a> ReadExecutor<'a, T> for IndexScan { self.ranges, with_pk, self.covered_deserializers, + self.cover_mapping, ) ); diff --git a/src/execution/mod.rs b/src/execution/mod.rs index 84569c92..e1a9071a 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -139,9 +139,10 @@ pub fn build_read<'a, T: Transaction + 'a>( meta, range: Some(range), covered_deserializers, + cover_mapping, })) = plan.physical_option { - IndexScan::from((op, meta, range, covered_deserializers)) + IndexScan::from((op, meta, range, covered_deserializers, cover_mapping)) .execute(cache, transaction) } else { SeqScan::from(op).execute(cache, transaction) diff --git a/src/optimizer/core/memo.rs b/src/optimizer/core/memo.rs index f8a19c2b..0543ec10 100644 --- a/src/optimizer/core/memo.rs +++ b/src/optimizer/core/memo.rs @@ -274,6 +274,7 @@ mod tests { } ])), covered_deserializers: None, + cover_mapping: None, })) ); diff --git a/src/optimizer/rule/normalization/pushdown_predicates.rs b/src/optimizer/rule/normalization/pushdown_predicates.rs index f190b96b..f1ccdcb4 100644 --- a/src/optimizer/rule/normalization/pushdown_predicates.rs +++ b/src/optimizer/rule/normalization/pushdown_predicates.rs @@ -244,6 +244,7 @@ impl NormalizationRule for PushPredicateIntoScan { meta, range, covered_deserializers, + cover_mapping, } in &mut scan_op.index_infos { if range.is_some() { @@ -265,26 +266,37 @@ impl NormalizationRule for PushPredicateIntoScan { } changed = true; - let mut deserializers = Vec::with_capacity(meta.column_ids.len()); - let mut cover_count = 0; + *covered_deserializers = None; + *cover_mapping = None; + + // try index covered + let mut mapping_slots = vec![usize::MAX; scan_op.columns.len()]; + let mut needs_mapping = false; let index_column_types = match &meta.value_ty { LogicalType::Tuple(tys) => tys, ty => slice::from_ref(ty), }; - for (i, column_id) in meta.column_ids.iter().enumerate() { - for column in scan_op.columns.values() { - deserializers.push( - if column.id().map(|id| id == *column_id).unwrap_or(false) { - cover_count += 1; - column.datatype().serializable() - } else { - index_column_types[i].skip_serializable() - }, - ); + let mut deserializers = Vec::with_capacity(meta.column_ids.len()); + + for (idx, column_id) in meta.column_ids.iter().enumerate() { + if let Some((scan_idx, column)) = + scan_op.columns.values().enumerate().find(|(_, column)| { + column.id().map(|id| id == *column_id).unwrap_or(false) + }) + { + mapping_slots[scan_idx] = idx; + needs_mapping |= scan_idx != idx; + deserializers.push(column.datatype().serializable()); + } else { + deserializers.push(index_column_types[idx].skip_serializable()); } } - if cover_count == scan_op.columns.len() { + + if mapping_slots.iter().all(|slot| *slot != usize::MAX) { *covered_deserializers = Some(deserializers); + if needs_mapping { + *cover_mapping = Some(mapping_slots); + } } } return Ok(changed); @@ -354,17 +366,24 @@ impl PushPredicateIntoScan { #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use crate::binder::test::build_t1_table; + use crate::catalog::{ColumnCatalog, ColumnDesc, ColumnRef, TableName}; use crate::errors::DatabaseError; use crate::expression::range_detacher::Range; use crate::expression::{BinaryOperator, ScalarExpression}; use crate::optimizer::heuristic::batch::HepBatchStrategy; use crate::optimizer::heuristic::optimizer::HepOptimizer; use crate::optimizer::rule::normalization::NormalizationRuleImpl; + use crate::planner::operator::filter::FilterOperator; + use crate::planner::operator::table_scan::TableScanOperator; use crate::planner::operator::Operator; + use crate::planner::{Childrens, LogicalPlan}; use crate::storage::rocksdb::RocksTransaction; + use crate::types::index::{IndexInfo, IndexMeta, IndexType}; use crate::types::value::DataValue; use crate::types::LogicalType; - use std::collections::Bound; + use std::collections::{BTreeMap, Bound}; + use std::sync::Arc; + use ulid::Ulid; #[test] fn test_push_predicate_into_scan() -> Result<(), DatabaseError> { @@ -400,6 +419,171 @@ mod tests { Ok(()) } + #[test] + fn test_cover_mapping_matches_scan_order() -> Result<(), DatabaseError> { + let table_name: TableName = Arc::from("mock_table"); + let c1_id = Ulid::new(); + let c2_id = Ulid::new(); + let c3_id = Ulid::new(); + + let mut c1 = ColumnCatalog::new( + "c1".to_string(), + false, + ColumnDesc::new(LogicalType::Integer, Some(0), false, None)?, + ); + c1.set_ref_table(table_name.clone(), c1_id, false); + let c1_ref = ColumnRef::from(c1.clone()); + + let mut c2 = ColumnCatalog::new( + "c2".to_string(), + false, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, + ); + c2.set_ref_table(table_name.clone(), c2_id, false); + let c2_ref = ColumnRef::from(c2.clone()); + + let mut c3 = ColumnCatalog::new( + "c3".to_string(), + false, + ColumnDesc::new(LogicalType::Integer, None, false, None)?, + ); + c3.set_ref_table(table_name.clone(), c3_id, false); + + let mut columns = BTreeMap::new(); + columns.insert(0, c1_ref.clone()); + columns.insert(1, c2_ref.clone()); + + let index_meta_reordered = Arc::new(IndexMeta { + id: 0, + column_ids: vec![c2_id, c3_id, c1_id], + table_name: table_name.clone(), + pk_ty: LogicalType::Integer, + value_ty: LogicalType::Tuple(vec![ + LogicalType::Integer, + LogicalType::Integer, + LogicalType::Integer, + ]), + name: "idx_c2_c3_c1".to_string(), + ty: IndexType::Composite, + }); + let index_meta_aligned = Arc::new(IndexMeta { + id: 1, + column_ids: vec![c1_id, c2_id], + table_name: table_name.clone(), + pk_ty: LogicalType::Integer, + value_ty: LogicalType::Tuple(vec![LogicalType::Integer, LogicalType::Integer]), + name: "idx_c1_c2".to_string(), + ty: IndexType::Composite, + }); + + let scan_plan = LogicalPlan::new( + Operator::TableScan(TableScanOperator { + table_name: table_name.clone(), + primary_keys: vec![c1_id], + columns, + limit: (None, None), + index_infos: vec![ + IndexInfo { + meta: index_meta_reordered, + range: None, + covered_deserializers: None, + cover_mapping: None, + }, + IndexInfo { + meta: index_meta_aligned, + range: None, + covered_deserializers: None, + cover_mapping: None, + }, + ], + with_pk: false, + }), + Childrens::None, + ); + + let c1_gt = ScalarExpression::Binary { + op: BinaryOperator::Gt, + left_expr: Box::new(ScalarExpression::column_expr(c1_ref.clone())), + right_expr: Box::new(ScalarExpression::Constant(DataValue::Int32(0))), + evaluator: None, + ty: LogicalType::Boolean, + }; + let c2_gt = ScalarExpression::Binary { + op: BinaryOperator::Gt, + left_expr: Box::new(ScalarExpression::column_expr(c2_ref.clone())), + right_expr: Box::new(ScalarExpression::Constant(DataValue::Int32(0))), + evaluator: None, + ty: LogicalType::Boolean, + }; + let predicate = ScalarExpression::Binary { + op: BinaryOperator::And, + left_expr: Box::new(c1_gt), + right_expr: Box::new(c2_gt), + evaluator: None, + ty: LogicalType::Boolean, + }; + + let filter_plan = LogicalPlan::new( + Operator::Filter(FilterOperator { + predicate, + is_optimized: false, + having: false, + }), + Childrens::Only(Box::new(scan_plan)), + ); + + let best_plan = HepOptimizer::new(filter_plan) + .batch( + "push_cover_mapping".to_string(), + HepBatchStrategy::once_topdown(), + vec![NormalizationRuleImpl::PushPredicateIntoScan], + ) + .find_best::(None)?; + + let table_scan = best_plan.childrens.pop_only(); + if let Operator::TableScan(op) = &table_scan.operator { + let index_infos = &op.index_infos; + assert_eq!(index_infos.len(), 2); + + // 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], + 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], + c1_ref.datatype().serializable(), + "last serializer should align with c1" + ); + let mapping = reordered_index.cover_mapping.as_ref().map(|m| m.as_slice()); + 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.cover_mapping.is_none(), + "mapping should be None when index/scan order already match" + ); + } else { + unreachable!("expected table scan"); + } + + Ok(()) + } + #[test] fn test_push_predicate_through_join_in_left_join() -> Result<(), DatabaseError> { let table_state = build_t1_table()?; diff --git a/src/planner/operator/table_scan.rs b/src/planner/operator/table_scan.rs index 29078ff7..c9d2ca14 100644 --- a/src/planner/operator/table_scan.rs +++ b/src/planner/operator/table_scan.rs @@ -63,6 +63,7 @@ impl TableScanOperator { meta: meta.clone(), range: None, covered_deserializers: None, + cover_mapping: None, }) .collect_vec(); diff --git a/src/storage/memory.rs b/src/storage/memory.rs index a7ade776..66fd625e 100644 --- a/src/storage/memory.rs +++ b/src/storage/memory.rs @@ -233,6 +233,7 @@ mod wasm_tests { }], true, None, + None, )?; let mut result = Vec::new(); @@ -365,6 +366,7 @@ mod native_tests { }], true, None, + None, )?; let mut result = Vec::new(); diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 4b97b3be..0be65d38 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -29,7 +29,7 @@ use crate::storage::table_codec::{BumpBytes, Bytes, TableCodec}; use crate::types::index::{Index, IndexId, IndexMetaRef, IndexType}; use crate::types::serialize::TupleValueSerializableImpl; use crate::types::tuple::{Tuple, TupleId}; -use crate::types::value::DataValue; +use crate::types::value::{DataValue, TupleMappingRef}; use crate::types::{ColumnId, LogicalType}; use crate::utils::lru::SharedLruCache; use itertools::Itertools; @@ -114,6 +114,7 @@ pub trait Transaction: Sized { ranges: Vec, with_pk: bool, covered_deserializers: Option>, + cover_mapping_indices: Option>, ) -> Result, DatabaseError> { debug_assert!(columns.keys().all_unique()); let values_len = columns.len(); @@ -129,21 +130,32 @@ pub trait Transaction: Sized { columns.insert(*i, column.clone()); } } - let (inner, deserializers, remap_pk_indices) = - if let Some(deserializers) = covered_deserializers { - ( - IndexImplEnum::Covered(CoveredIndexImpl), - deserializers, - PrimaryKeyRemap::Covered, - ) - } else { - let (deserializers, remap_pk_indices) = - Self::create_deserializers(&columns, table, with_pk); - ( - IndexImplEnum::instance(index_meta.ty), - deserializers, - remap_pk_indices, - ) + let (inner, deserializers, remap_pk_indices, cover_mapping) = + match (covered_deserializers, cover_mapping_indices) { + (Some(deserializers), mapping) => { + let tuple_len = match &index_meta.value_ty { + LogicalType::Tuple(tys) => tys.len(), + _ => 1, + }; + let cover_mapping = mapping.map(|slots| TupleMapping::new(slots, tuple_len)); + + ( + IndexImplEnum::Covered(CoveredIndexImpl), + deserializers, + PrimaryKeyRemap::Covered, + cover_mapping, + ) + } + (None, _) => { + let (deserializers, remap_pk_indices) = + Self::create_deserializers(&columns, table, with_pk); + ( + IndexImplEnum::instance(index_meta.ty), + deserializers, + remap_pk_indices, + None, + ) + } }; Ok(IndexIter { @@ -157,6 +169,7 @@ pub trait Transaction: Sized { values_len, total_len: table.columns_len(), tx: self, + cover_mapping, }, inner, ranges: ranges.into_iter(), @@ -835,6 +848,29 @@ struct NormalIndexImpl; struct CompositeIndexImpl; struct CoveredIndexImpl; +struct TupleMapping { + index_to_scan: Vec, + target_len: usize, +} + +impl TupleMapping { + fn new(scan_to_index: Vec, tuple_len: usize) -> Self { + let mut index_to_scan = vec![usize::MAX; tuple_len]; + + for (scan_idx, index_idx) in scan_to_index.iter().enumerate() { + index_to_scan[*index_idx] = scan_idx; + } + TupleMapping { + index_to_scan, + target_len: scan_to_index.len(), + } + } + + fn as_ref(&self) -> TupleMappingRef<'_> { + TupleMappingRef::new(&self.index_to_scan, self.target_len) + } +} + struct IndexImplParams<'a, T: Transaction> { index_meta: IndexMetaRef, table_name: &'a str, @@ -842,6 +878,7 @@ struct IndexImplParams<'a, T: Transaction> { values_len: usize, total_len: usize, tx: &'a T, + cover_mapping: Option, } impl IndexImplParams<'_, T> { @@ -1119,7 +1156,11 @@ impl<'bytes, T: Transaction + 'bytes> IndexImpl<'bytes, T> for CoveredIndexImpl pk_indices: &PrimaryKeyRemap, params: &IndexImplParams, ) -> Result { - let key = TableCodec::decode_index_key(key, params.value_ty())?; + let mapping = params + .cover_mapping + .as_ref() + .map(|mapping| mapping.as_ref()); + let key = TableCodec::decode_index_key(key, params.value_ty(), mapping)?; let mut tuple_id = None; if matches!(pk_indices, PrimaryKeyRemap::Covered) { @@ -1769,6 +1810,7 @@ mod test { }], true, None, + None, ) } diff --git a/src/storage/rocksdb.rs b/src/storage/rocksdb.rs index 31598340..1e21ad29 100644 --- a/src/storage/rocksdb.rs +++ b/src/storage/rocksdb.rs @@ -388,6 +388,7 @@ mod test { tx: &transaction, values_len, total_len: 1, + cover_mapping: None, }, ranges: vec![ Range::Eq(DataValue::Int32(0)), @@ -441,6 +442,7 @@ mod test { }], true, None, + None, ) .unwrap(); @@ -467,6 +469,7 @@ mod test { }], true, None, + None, ) .unwrap(); @@ -489,12 +492,24 @@ mod test { kite_sql .run("insert into t1 (a, b) values (0, 0), (1, 1), (2, 2), (3, 4)")? .done()?; + kite_sql.run("create index idx_b_a on t1(b, a)")?.done()?; let mut transaction = kite_sql.storage.transaction().unwrap(); let table = transaction .table(kite_sql.state.table_cache(), "t1".to_string().into())? .unwrap() .clone(); + let columns_vec: Vec<_> = table.columns().cloned().collect(); + let a_cover_column = columns_vec + .iter() + .find(|column| column.name() == "a") + .unwrap() + .clone(); + let b_cover_column = columns_vec + .iter() + .find(|column| column.name() == "b") + .unwrap() + .clone(); let unique_index = table .indexes .iter() @@ -511,6 +526,56 @@ mod test { columns.insert(b_pos, b_column.clone()); let covered_deserializers = vec![b_column.datatype().serializable()]; + // ensure cover mapping can reorder index values to match scan order + let composite_index = table + .indexes + .iter() + .find(|index| index.name == "idx_b_a") + .unwrap() + .clone(); + let mut reordered_columns = BTreeMap::new(); + reordered_columns.insert(0, a_cover_column.clone()); + reordered_columns.insert(1, b_cover_column.clone()); + let reordered_deserializers = vec![ + a_cover_column.datatype().serializable(), + b_cover_column.datatype().serializable(), + ]; + let a_id = a_cover_column.id().unwrap(); + let b_id = b_cover_column.id().unwrap(); + let cover_mapping = vec![ + composite_index + .column_ids + .iter() + .position(|id| id == &a_id) + .unwrap(), + composite_index + .column_ids + .iter() + .position(|id| id == &b_id) + .unwrap(), + ]; + + let mut iter = transaction.read_by_index( + kite_sql.state.table_cache(), + "t1".to_string().into(), + (None, None), + reordered_columns, + composite_index, + vec![Range::Scope { + min: Bound::Unbounded, + max: Bound::Unbounded, + }], + false, + Some(reordered_deserializers), + Some(cover_mapping), + )?; + let first_tuple = iter.next_tuple()?.unwrap(); + assert_eq!( + first_tuple.values, + vec![DataValue::Int32(0), DataValue::Int32(0)] + ); + drop(iter); + let target_pk = DataValue::Int32(3); let covered_value = DataValue::Int32(4); transaction.remove_tuple("t1", &target_pk)?; @@ -524,6 +589,7 @@ mod test { vec![Range::Eq(covered_value.clone())], false, Some(covered_deserializers), + None, )?; let mut tuples = Vec::new(); diff --git a/src/storage/table_codec.rs b/src/storage/table_codec.rs index e7094dc6..dc14e5e4 100644 --- a/src/storage/table_codec.rs +++ b/src/storage/table_codec.rs @@ -20,7 +20,7 @@ use crate::storage::{PrimaryKeyRemap, TableCache, Transaction}; use crate::types::index::{Index, IndexId, IndexMeta, IndexType, INDEX_ID_LEN}; use crate::types::serialize::TupleValueSerializableImpl; use crate::types::tuple::{Tuple, TupleId}; -use crate::types::value::DataValue; +use crate::types::value::{DataValue, TupleMappingRef}; use crate::types::LogicalType; use bumpalo::Bump; use siphasher::sip::SipHasher; @@ -399,10 +399,14 @@ impl TableCodec { Ok(key_prefix) } - pub fn decode_index_key(bytes: &[u8], ty: &LogicalType) -> Result { + pub fn decode_index_key( + bytes: &[u8], + ty: &LogicalType, + mapping: Option>, + ) -> Result { // Hash + TypeTag + Bound Min + Index Id Len + Bound Min let start = 8 + 1 + 1 + 1 + INDEX_ID_LEN; - DataValue::memcomparable_decode(&mut Cursor::new(&bytes[start..]), ty) + DataValue::memcomparable_decode_mapping(&mut Cursor::new(&bytes[start..]), ty, mapping) } pub fn decode_index(bytes: &[u8]) -> Result { diff --git a/src/types/index.rs b/src/types/index.rs index 570c49e1..001e2b25 100644 --- a/src/types/index.rs +++ b/src/types/index.rs @@ -42,6 +42,7 @@ pub struct IndexInfo { pub(crate) meta: IndexMetaRef, pub(crate) range: Option, pub(crate) covered_deserializers: Option>, + pub(crate) cover_mapping: Option>, } #[derive(Debug, Clone, Eq, PartialEq, Hash, ReferenceSerialization)] diff --git a/src/types/value.rs b/src/types/value.rs index 3a9bf22d..667991d9 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -88,6 +88,76 @@ pub enum DataValue { Tuple(Vec, bool), } +#[derive(Clone, Copy)] +pub struct TupleMappingRef<'a> { + index_to_scan: &'a [usize], + target_len: usize, +} + +impl<'a> TupleMappingRef<'a> { + pub fn new(index_to_scan: &'a [usize], target_len: usize) -> Self { + TupleMappingRef { + index_to_scan, + target_len, + } + } + + #[inline] + pub fn target_len(&self) -> usize { + self.target_len + } + + #[inline] + pub fn scan_index(&self, index_pos: usize) -> Option { + self.index_to_scan.get(index_pos).copied().and_then(|slot| { + if slot == usize::MAX { + None + } else { + Some(slot) + } + }) + } +} + +enum TupleCollector<'a> { + Mapped { + mapping: TupleMappingRef<'a>, + values: Vec, + }, + Ordered(Vec), +} + +impl<'a> TupleCollector<'a> { + fn new(mapping: Option>, tuple_len: usize) -> Self { + if let Some(mapping) = mapping { + TupleCollector::Mapped { + values: vec![DataValue::Null; mapping.target_len()], + mapping, + } + } else { + TupleCollector::Ordered(Vec::with_capacity(tuple_len)) + } + } + + fn push(&mut self, index_pos: usize, value: DataValue) { + match self { + TupleCollector::Mapped { mapping, values } => { + if let Some(target_pos) = mapping.scan_index(index_pos) { + values[target_pos] = value; + } + } + TupleCollector::Ordered(values) => values.push(value), + } + } + + fn finish(self) -> Vec { + match self { + TupleCollector::Mapped { values, .. } => values, + TupleCollector::Ordered(values) => values, + } + } +} + macro_rules! generate_get_option { ($data_value:ident, $($prefix:ident : $variant:ident($field:ty)),*) => { impl $data_value { @@ -750,6 +820,14 @@ impl DataValue { pub fn memcomparable_decode( reader: &mut R, ty: &LogicalType, + ) -> Result { + Self::memcomparable_decode_mapping(reader, ty, None) + } + + pub fn memcomparable_decode_mapping( + reader: &mut R, + ty: &LogicalType, + tuple_mapping: Option>, ) -> Result { if reader.read_u8()? == 0u8 { return Ok(DataValue::Null); @@ -816,12 +894,13 @@ impl DataValue { }), LogicalType::Decimal(..) => Ok(DataValue::Decimal(Self::deserialize_decimal(reader)?)), LogicalType::Tuple(tys) => { - let mut values = Vec::with_capacity(tys.len()); + let mut collector = TupleCollector::new(tuple_mapping, tys.len()); - for ty in tys { - values.push(Self::memcomparable_decode(reader, ty)?); + for (index_pos, ty) in tys.iter().enumerate() { + let value = Self::memcomparable_decode_mapping(reader, ty, None)?; + collector.push(index_pos, value); } - Ok(DataValue::Tuple(values, false)) + Ok(DataValue::Tuple(collector.finish(), false)) } } } @@ -2155,7 +2234,7 @@ impl fmt::Debug for DataValue { mod test { use crate::errors::DatabaseError; use crate::storage::table_codec::BumpBytes; - use crate::types::value::{DataValue, Utf8Type}; + use crate::types::value::{DataValue, TupleMappingRef, Utf8Type}; use crate::types::LogicalType; use bumpalo::Bump; use ordered_float::OrderedFloat; @@ -2697,6 +2776,42 @@ mod test { Ok(()) } + #[test] + fn test_memcomparable_decode_mapping_orders_values() -> Result<(), DatabaseError> { + let arena = Bump::new(); + let mut key_tuple = BumpBytes::new_in(&arena); + + let value = DataValue::Tuple( + vec![ + DataValue::Int32(1), + DataValue::Int32(2), + DataValue::Int32(3), + ], + false, + ); + value.memcomparable_encode(&mut key_tuple)?; + + let ty = LogicalType::Tuple(vec![ + LogicalType::Integer, + LogicalType::Integer, + LogicalType::Integer, + ]); + let index_to_scan = vec![1, usize::MAX, 0]; + let mapping = TupleMappingRef::new(&index_to_scan, 2); + let decoded = DataValue::memcomparable_decode_mapping( + &mut Cursor::new(&key_tuple[..]), + &ty, + Some(mapping), + )?; + + assert_eq!( + decoded, + DataValue::Tuple(vec![DataValue::Int32(3), DataValue::Int32(1)], false) + ); + + Ok(()) + } + #[test] fn test_mem_comparable_utf8() -> Result<(), DatabaseError> { let arena = Bump::new(); diff --git a/tpcc/src/main.rs b/tpcc/src/main.rs index b741bbf2..fcf02fa9 100644 --- a/tpcc/src/main.rs +++ b/tpcc/src/main.rs @@ -26,6 +26,8 @@ use kite_sql::errors::DatabaseError; use kite_sql::storage::Storage; use rand::prelude::ThreadRng; use rand::Rng; +use std::fs; +use std::path::Path; use std::time::{Duration, Instant}; mod delivery; @@ -92,6 +94,10 @@ struct Args { // TODO: Support multi-threaded TPCC fn main() -> Result<(), TpccError> { let args = Args::parse(); + let db_path = Path::new(&args.path); + if db_path.exists() { + fs::remove_dir_all(db_path)?; + } let mut rng = rand::thread_rng(); let database = DataBaseBuilder::path(&args.path).build()?; @@ -327,6 +333,12 @@ pub enum TpccError { #[from] DatabaseError, ), + #[error("io error: {0}")] + Io( + #[source] + #[from] + std::io::Error, + ), #[error("tuples is empty")] EmptyTuples, #[error("maximum retries reached")] From adb2e09e07d46e2be1c47f843e5f31eae01c98a6 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 11 Jan 2026 07:49:04 +0800 Subject: [PATCH 2/2] chore: update tpcc doc --- README.md | 12 +-- src/types/value.rs | 2 + tpcc/README.md | 196 +++++++++++++++++++++------------------------ 3 files changed, 98 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index d62ab8ab..1145db45 100755 --- a/README.md +++ b/README.md @@ -90,13 +90,13 @@ run `cargo run -p tpcc --release` to run tpcc - Tips: TPC-C currently only supports single thread ```shell <90th Percentile RT (MaxRT)> - New-Order : 0.002 (0.018) - Payment : 0.001 (0.024) -Order-Status : 0.050 (0.067) - Delivery : 0.021 (0.030) - Stock-Level : 0.003 (0.005) + New-Order : 0.002 (0.005) + Payment : 0.001 (0.003) +Order-Status : 0.057 (0.088) + Delivery : 0.001 (0.001) + Stock-Level : 0.002 (0.006) -8101 Tpmc +11125 Tpmc ``` #### 👉[check more](tpcc/README.md) diff --git a/src/types/value.rs b/src/types/value.rs index 667991d9..6077463e 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -824,9 +824,11 @@ impl DataValue { Self::memcomparable_decode_mapping(reader, ty, None) } + #[inline] pub fn memcomparable_decode_mapping( reader: &mut R, ty: &LogicalType, + // for index cover mapping reduce one layer of conversion tuple_mapping: Option>, ) -> Result { if reader.read_u8()? == 0u8 { diff --git a/tpcc/README.md b/tpcc/README.md index 36cd971f..657cb215 100644 --- a/tpcc/README.md +++ b/tpcc/README.md @@ -6,11 +6,11 @@ run `cargo run -p tpcc --release` to run tpcc - KIOXIA-EXCERIA PLUS G3 SSD - Tips: TPCC currently only supports single thread ```shell -|New-Order| sc: 93779 lt: 0 fl: 926 -|Payment| sc: 93759 lt: 0 fl: 0 -|Order-Status| sc: 9376 lt: 0 fl: 417 -|Delivery| sc: 9375 lt: 0 fl: 0 -|Stock-Level| sc: 9375 lt: 0 fl: 0 +|New-Order| sc: 133498 lt: 0 fl: 1360 +|Payment| sc: 133473 lt: 0 fl: 0 +|Order-Status| sc: 13348 lt: 0 fl: 450 +|Delivery| sc: 13348 lt: 0 fl: 0 +|Stock-Level| sc: 13347 lt: 0 fl: 0 in 720 sec. (all must be [OK]) [transaction percentage] @@ -24,129 +24,113 @@ in 720 sec. Order-Status: 100.0 [OK] Delivery: 100.0 [OK] Stock-Level: 100.0 [OK] - New-Order Total: 93779 - Payment Total: 93759 - Order-Status Total: 9376 - Delivery Total: 9375 - Stock-Level Total: 9375 + New-Order Total: 133498 + Payment Total: 133473 + Order-Status Total: 13348 + Delivery Total: 13348 + Stock-Level Total: 13347 1.New-Order -0.001, 20973 -0.002, 71372 -0.003, 1306 -0.004, 15 +0.001, 83231 +0.002, 49784 +0.003, 36 +0.004, 4 0.005, 2 2.Payment -0.001, 90277 -0.002, 3307 -0.003, 11 -0.004, 3 +0.001, 133281 +0.002, 184 +0.003, 2 3.Order-Status -0.013, 24 -0.014, 108 -0.015, 189 -0.016, 207 -0.017, 201 -0.018, 207 -0.019, 221 -0.020, 198 -0.021, 187 -0.022, 163 -0.023, 175 -0.024, 259 -0.025, 298 -0.026, 300 -0.027, 239 -0.028, 207 -0.029, 165 -0.030, 142 -0.031, 135 -0.032, 205 -0.033, 303 -0.034, 270 -0.035, 272 -0.036, 170 -0.037, 138 -0.038, 156 -0.039, 175 -0.040, 193 -0.041, 207 -0.042, 211 -0.043, 264 -0.044, 304 -0.045, 265 -0.046, 224 -0.047, 172 -0.048, 176 -0.049, 172 -0.050, 207 -0.051, 238 -0.052, 269 -0.053, 219 -0.054, 242 -0.055, 156 -0.056, 134 -0.057, 94 -0.058, 88 -0.059, 75 -0.060, 49 -0.061, 13 -0.062, 4 -0.063, 1 -0.064, 1 -0.076, 1 -0.080, 1 -0.117, 1 -0.127, 1 -0.150, 1 -0.158, 1 -0.172, 1 -0.176, 1 +0.012, 31 +0.013, 265 +0.014, 332 +0.015, 307 +0.016, 296 +0.017, 284 +0.018, 303 +0.019, 415 +0.020, 386 +0.021, 382 +0.022, 252 +0.023, 228 +0.024, 264 +0.025, 249 +0.026, 268 +0.027, 253 +0.028, 246 +0.029, 277 +0.030, 253 +0.031, 237 +0.032, 289 +0.033, 172 +0.034, 192 +0.035, 268 +0.036, 266 +0.037, 276 +0.038, 243 +0.039, 223 +0.040, 216 +0.041, 225 +0.042, 248 +0.043, 193 +0.044, 174 +0.045, 307 +0.046, 305 +0.047, 246 +0.048, 213 +0.049, 267 +0.050, 197 +0.051, 182 +0.052, 207 +0.053, 84 +0.054, 42 +0.055, 54 +0.056, 102 +0.057, 156 +0.058, 165 +0.059, 199 +0.060, 195 +0.061, 173 +0.062, 141 +0.063, 102 +0.064, 56 +0.065, 29 +0.066, 8 +0.067, 6 +0.068, 2 +0.069, 3 +0.075, 1 +0.078, 1 4.Delivery -0.011, 117 -0.012, 483 -0.013, 659 -0.014, 704 -0.015, 790 -0.016, 911 -0.017, 969 -0.018, 974 -0.019, 895 -0.020, 927 -0.021, 782 -0.022, 542 -0.023, 362 -0.024, 209 -0.025, 45 -0.026, 1 -0.027, 3 +0.001, 11129 +0.002, 1 5.Stock-Level -0.001, 1815 -0.002, 3454 -0.003, 3646 -0.004, 377 -0.005, 28 +0.001, 5697 +0.002, 4669 +0.003, 494 +0.004, 8 +0.005, 1 <90th Percentile RT (MaxRT)> - New-Order : 0.002 (0.004) - Payment : 0.001 (0.025) -Order-Status : 0.053 (0.175) - Delivery : 0.022 (0.027) - Stock-Level : 0.003 (0.019) + New-Order : 0.002 (0.005) + Payment : 0.001 (0.003) +Order-Status : 0.057 (0.088) + Delivery : 0.001 (0.001) + Stock-Level : 0.002 (0.006) -7815 tpmC +11125 Tpmc ``` ## Explain