Skip to content

Commit d47b085

Browse files
committed
fix #3617, dont panic on invalid index range scans
1 parent cb56bdc commit d47b085

File tree

4 files changed

+109
-11
lines changed

4 files changed

+109
-11
lines changed

crates/datastore/src/locking_tx_datastore/state_view.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,15 +374,19 @@ pub enum IterByColRangeMutTx<'a, R: RangeBounds<AlgebraicValue>> {
374374

375375
/// When the column has an index.
376376
Index(IndexScanRanged<'a>),
377+
378+
/// When the range itself is empty.
379+
RangeEmpty,
377380
}
378381

379382
impl<'a, R: RangeBounds<AlgebraicValue>> Iterator for IterByColRangeMutTx<'a, R> {
380383
type Item = RowRef<'a>;
381384

382385
fn next(&mut self) -> Option<Self::Item> {
383386
match self {
384-
IterByColRangeMutTx::Scan(range) => range.next(),
385-
IterByColRangeMutTx::Index(range) => range.next(),
387+
Self::Scan(range) => range.next(),
388+
Self::Index(range) => range.next(),
389+
Self::RangeEmpty => None,
386390
}
387391
}
388392
}

crates/sats/src/proptest.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,10 @@ use proptest::{
1515

1616
const SIZE: usize = 16;
1717

18-
/// Generates leaf (i.e. non-compound) `AlgebraicType`s.
19-
///
20-
/// These are types which do not contain other types,
21-
/// i.e. bools, integers, floats, strings and units.
18+
/// Generates primitive `AlgebraicType`s.
2219
///
23-
/// All other generated `AlgebraicType`s are compound,
24-
/// i.e. contain at least one child `AlgebraicType`,
25-
/// and thus require recursive generation.
26-
fn generate_non_compound_algebraic_type() -> impl Strategy<Value = AlgebraicType> {
20+
/// These are bool, the integer types, and the float types.
21+
pub fn generate_primitive_algebraic_type() -> impl Strategy<Value = AlgebraicType> {
2722
prop_oneof![
2823
Just(AlgebraicType::Bool),
2924
Just(AlgebraicType::U8),
@@ -40,6 +35,20 @@ fn generate_non_compound_algebraic_type() -> impl Strategy<Value = AlgebraicType
4035
Just(AlgebraicType::I256),
4136
Just(AlgebraicType::F32),
4237
Just(AlgebraicType::F64),
38+
]
39+
}
40+
41+
/// Generates leaf (i.e. non-compound) `AlgebraicType`s.
42+
///
43+
/// These are types which do not contain other types,
44+
/// i.e. bools, integers, floats, strings and units.
45+
///
46+
/// All other generated `AlgebraicType`s are compound,
47+
/// i.e. contain at least one child `AlgebraicType`,
48+
/// and thus require recursive generation.
49+
fn generate_non_compound_algebraic_type() -> impl Strategy<Value = AlgebraicType> {
50+
prop_oneof![
51+
generate_primitive_algebraic_type(),
4352
Just(AlgebraicType::String),
4453
Just(AlgebraicType::unit()),
4554
]

crates/table/src/table.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2671,7 +2671,6 @@ pub(crate) mod test {
26712671
fn index_size_reporting_matches_slow_implementations_two_column((ty, vals) in generate_typed_row_vec(128, 2048)) {
26722672
prop_assume!(ty.elements.len() >= 2);
26732673

2674-
26752674
test_index_size_reporting(ty, vals, ColList::from([ColId(0), ColId(1)]))?;
26762675
}
26772676
}

crates/table/src/table_index/mod.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ impl Iterator for TableIndexPointIter<'_> {
9191
///
9292
/// See module docs for info about specialization.
9393
enum TypedIndexRangeIter<'a> {
94+
/// The range itself provided was empty.
95+
RangeEmpty,
96+
9497
// All the non-unique btree index iterators.
9598
BtreeBool(BtreeIndexRangeIter<'a, bool>),
9699
BtreeU8(BtreeIndexRangeIter<'a, u8>),
@@ -137,6 +140,8 @@ impl Iterator for TypedIndexRangeIter<'_> {
137140
type Item = RowPointer;
138141
fn next(&mut self) -> Option<Self::Item> {
139142
match self {
143+
Self::RangeEmpty => None,
144+
140145
Self::BtreeBool(this) => this.next().copied(),
141146
Self::BtreeU8(this) => this.next().copied(),
142147
Self::BtreeI8(this) => this.next().copied(),
@@ -827,6 +832,24 @@ impl TypedIndex {
827832
}
828833

829834
fn seek_range(&self, range: &impl RangeBounds<AlgebraicValue>) -> TypedIndexRangeIter<'_> {
835+
// Copied from `RangeBounds::is_empty` as it's unstable.
836+
// TODO(centril): replace once stable.
837+
fn is_empty<T: PartialOrd>(bounds: &impl RangeBounds<T>) -> bool {
838+
use core::ops::Bound::*;
839+
!match (bounds.start_bound(), bounds.end_bound()) {
840+
(Unbounded, _) | (_, Unbounded) => true,
841+
(Included(start), Excluded(end))
842+
| (Excluded(start), Included(end))
843+
| (Excluded(start), Excluded(end)) => start < end,
844+
(Included(start), Included(end)) => start <= end,
845+
}
846+
}
847+
848+
// Ensure we don't panic inside `BTreeMap::seek_range`.
849+
if is_empty(range) {
850+
return RangeEmpty;
851+
}
852+
830853
fn mm_iter_at_type<'a, T: Ord>(
831854
this: &'a BtreeIndex<T>,
832855
range: &impl RangeBounds<AlgebraicValue>,
@@ -1355,10 +1378,12 @@ mod test {
13551378
use crate::page_pool::PagePool;
13561379
use crate::{blob_store::HashMapBlobStore, table::test::table};
13571380
use core::ops::Bound::*;
1381+
use decorum::Total;
13581382
use proptest::prelude::*;
13591383
use proptest::{collection::vec, test_runner::TestCaseResult};
13601384
use spacetimedb_data_structures::map::HashMap;
13611385
use spacetimedb_primitives::ColId;
1386+
use spacetimedb_sats::proptest::{generate_algebraic_value, generate_primitive_algebraic_type};
13621387
use spacetimedb_sats::{
13631388
product,
13641389
proptest::{generate_product_value, generate_row_type},
@@ -1406,6 +1431,34 @@ mod test {
14061431
index.is_unique().then(|| index.seek_point(row))
14071432
}
14081433

1434+
fn successor_of_primitive(av: &AlgebraicValue) -> Option<AlgebraicValue> {
1435+
use AlgebraicValue::*;
1436+
match av {
1437+
Min | Max | Sum(_) | Product(_) | Array(_) | String(_) => unimplemented!(),
1438+
1439+
Bool(false) => Some(Bool(true)),
1440+
Bool(true) => None,
1441+
I8(x) => x.checked_add(1).map(I8),
1442+
U8(x) => x.checked_add(1).map(U8),
1443+
I16(x) => x.checked_add(1).map(I16),
1444+
U16(x) => x.checked_add(1).map(U16),
1445+
I32(x) => x.checked_add(1).map(I32),
1446+
U32(x) => x.checked_add(1).map(U32),
1447+
I64(x) => x.checked_add(1).map(I64),
1448+
U64(x) => x.checked_add(1).map(U64),
1449+
I128(x) => x.0.checked_add(1).map(Packed).map(I128),
1450+
U128(x) => x.0.checked_add(1).map(Packed).map(U128),
1451+
I256(x) => x.checked_add(1.into()).map(Box::new).map(I256),
1452+
U256(x) => x.checked_add(1u8.into()).map(Box::new).map(U256),
1453+
F32(x) => Some(F32(Total::from_inner(x.into_inner().next_up()))),
1454+
F64(x) => Some(F64(Total::from_inner(x.into_inner().next_up()))),
1455+
}
1456+
}
1457+
1458+
fn gen_primitive_ty_and_val() -> impl Strategy<Value = (AlgebraicType, AlgebraicValue)> {
1459+
generate_primitive_algebraic_type().prop_flat_map(|ty| (Just(ty.clone()), generate_algebraic_value(ty)))
1460+
}
1461+
14091462
proptest! {
14101463
#![proptest_config(ProptestConfig { max_shrink_iters: 0x10000000, ..Default::default() })]
14111464
#[test]
@@ -1560,5 +1613,38 @@ mod test {
15601613
test_seek(&index, &val_to_ptr, (Excluded(V(prev)), Excluded(V(needle))), [])?;
15611614
test_seek(&index, &val_to_ptr, (Excluded(V(prev)), Excluded(V(next))), [needle])?;
15621615
}
1616+
1617+
#[test]
1618+
fn empty_range_scans_dont_panic(((ty, val), is_unique) in (gen_primitive_ty_and_val(), any::<bool>())) {
1619+
let succ = successor_of_primitive(&val);
1620+
prop_assume!(succ.is_some());
1621+
let succ = succ.unwrap();
1622+
1623+
// Construct the index.
1624+
let row_ty = ProductType::from([ty.clone()]);
1625+
let mut index = new_index(&row_ty, &[0].into(), is_unique);
1626+
1627+
// Construct the table and add `val` as a row.
1628+
let mut table = table(row_ty);
1629+
let pool = PagePool::new_for_test();
1630+
let mut blob_store = HashMapBlobStore::default();
1631+
let pv = product![val.clone()];
1632+
let row_ref = table.insert(&pool, &mut blob_store, &pv).unwrap().1;
1633+
1634+
// Add the row to the index.
1635+
unsafe { index.check_and_insert(row_ref).unwrap(); }
1636+
1637+
// Seek the empty ranges.
1638+
let rows = index.seek_range(&(&succ..&val)).collect::<Vec<_>>();
1639+
assert_eq!(rows, []);
1640+
let rows = index.seek_range(&(&succ..=&val)).collect::<Vec<_>>();
1641+
assert_eq!(rows, []);
1642+
let rows = index.seek_range(&(Excluded(&succ), Included(&val))).collect::<Vec<_>>();
1643+
assert_eq!(rows, []);
1644+
let rows = index.seek_range(&(Excluded(&succ), Excluded(&val))).collect::<Vec<_>>();
1645+
assert_eq!(rows, []);
1646+
let rows = index.seek_range(&(Excluded(&val), Excluded(&val))).collect::<Vec<_>>();
1647+
assert_eq!(rows, []);
1648+
}
15631649
}
15641650
}

0 commit comments

Comments
 (0)