use std::ops::Bound;
use storekey::{Decode, Encode};
use crate::{
error::{Error, Result},
store::index_encoding::CanonicalKey,
};
pub(crate) const TAG_PREFIX_LEN: usize = 1;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)]
pub(crate) enum Key {
Sys(SysKey),
Snapshot {
snapshot_id: u64,
},
Current(CurrentKey),
History(HistoryKey),
Inline(InlineKey),
Index(IndexKey),
SchemaVersion {
table_id: u64,
begin_snapshot: u64,
},
Changelog {
snapshot_id: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)]
pub(crate) enum IndexKey {
Unique {
index_id: u64,
key: CanonicalKey,
},
Multi {
index_id: u64,
key: CanonicalKey,
row_id: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)]
pub(crate) enum SysKey {
Format,
Head,
Migration,
MaintenanceStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)]
pub(crate) enum CurrentKey {
Entity(EntityKey),
GcFile {
data_file_id: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)]
pub(crate) struct HistoryKey {
pub(crate) entity: EntityKey,
pub(crate) end_snapshot: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)]
pub(crate) enum EntityKey {
Schema {
schema_id: u64,
},
Table {
table_id: u64,
},
View {
view_id: u64,
},
Column {
table_id: u64,
column_id: u64,
},
Partition {
table_id: u64,
partition_id: u64,
},
File {
table_id: u64,
data_file_id: u64,
},
DeleteFile {
table_id: u64,
delete_file_id: u64,
},
FileColumnStats {
table_id: u64,
data_file_id: u64,
column_id: u64,
},
TableStats {
table_id: u64,
},
TableColumnStats {
table_id: u64,
column_id: u64,
},
Tag {
object_id: u64,
},
Option {
scope_kind: u64,
scope_id: u64,
},
Sort {
table_id: u64,
sort_id: u64,
},
Macro {
macro_id: u64,
},
Mapping {
table_id: u64,
mapping_id: u64,
},
Index {
table_id: u64,
index_id: u64,
},
}
impl EntityKey {
pub(crate) const fn is_versioned(self) -> bool {
!matches!(
self,
Self::FileColumnStats { .. }
| Self::TableStats { .. }
| Self::TableColumnStats { .. }
| Self::Option { .. }
| Self::Tag { .. }
| Self::Mapping { .. }
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum EntityKind {
Schema,
Table,
View,
Column,
Partition,
File,
DeleteFile,
FileColumnStats,
TableStats,
TableColumnStats,
Tag,
Option,
Sort,
Macro,
Mapping,
}
impl EntityKind {
const fn sample(self) -> EntityKey {
match self {
Self::Schema => EntityKey::Schema { schema_id: 0 },
Self::Table => EntityKey::Table { table_id: 0 },
Self::View => EntityKey::View { view_id: 0 },
Self::Column => EntityKey::Column {
table_id: 0,
column_id: 0,
},
Self::Partition => EntityKey::Partition {
table_id: 0,
partition_id: 0,
},
Self::File => EntityKey::File {
table_id: 0,
data_file_id: 0,
},
Self::DeleteFile => EntityKey::DeleteFile {
table_id: 0,
delete_file_id: 0,
},
Self::FileColumnStats => EntityKey::FileColumnStats {
table_id: 0,
data_file_id: 0,
column_id: 0,
},
Self::TableStats => EntityKey::TableStats { table_id: 0 },
Self::TableColumnStats => EntityKey::TableColumnStats {
table_id: 0,
column_id: 0,
},
Self::Tag => EntityKey::Tag { object_id: 0 },
Self::Option => EntityKey::Option {
scope_kind: 0,
scope_id: 0,
},
Self::Sort => EntityKey::Sort {
table_id: 0,
sort_id: 0,
},
Self::Macro => EntityKey::Macro { macro_id: 0 },
Self::Mapping => EntityKey::Mapping {
table_id: 0,
mapping_id: 0,
},
}
}
pub(crate) const fn is_versioned(self) -> bool {
self.sample().is_versioned()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)]
pub(crate) enum InlineKey {
Schema {
table_id: u64,
schema_version: u64,
},
Live(InlineOperation),
Arch(InlineOperation),
FileDeleteTable {
table_id: u64,
},
ChunkRange {
table_id: u64,
row_id_end: u64,
},
SchemaDropped {
table_id: u64,
schema_version: u64,
},
ChunkLocator {
table_id: u64,
row_id_end: u64,
schema_version: u64,
begin_snapshot: u64,
chunk_seq: u64,
},
RowTombstone {
table_id: u64,
row_id: u64,
end_snapshot: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)]
pub(crate) enum InlineOperation {
Insert {
table_id: u64,
schema_version: u64,
begin_snapshot: u64,
chunk_seq: u64,
},
InlineDelete {
table_id: u64,
row_id: u64,
},
FileDelete {
table_id: u64,
data_file_id: u64,
row_id: u64,
},
}
impl Key {
pub(crate) const fn current(entity: EntityKey) -> Self {
Self::Current(CurrentKey::Entity(entity))
}
pub(crate) const fn history(entity: EntityKey, end_snapshot: u64) -> Self {
Self::History(HistoryKey {
entity,
end_snapshot,
})
}
pub(crate) fn encode(&self) -> Vec<u8> {
#[allow(clippy::expect_used)]
storekey::encode_vec(self).expect("storekey encode into a Vec cannot fail")
}
pub(crate) fn decode(bytes: &[u8]) -> Result<Self> {
storekey::decode(bytes).map_err(|err| Error::Corruption(format!("key: {err}")))
}
}
pub(crate) fn encode_index_entry(
index_id: u64,
unique: bool,
key: &CanonicalKey,
row_id: u64,
) -> Vec<u8> {
let mut bytes = Vec::new();
let mut writer = storekey::Writer::new(&mut bytes);
let result = (|| -> std::result::Result<(), storekey::EncodeError> {
writer.write_u8(INDEX_SUBSPACE_TAG)?;
writer.write_u8(index_kind_tag(unique))?;
writer.write_u64(index_id)?;
<CanonicalKey as Encode<()>>::encode(key, &mut writer)?;
if !unique {
writer.write_u64(row_id)?;
}
Ok(())
})();
#[allow(clippy::expect_used)]
result.expect("storekey encode into a Vec cannot fail");
bytes
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Subspace {
System,
Snapshot,
Current,
History,
Inline,
Index,
SchemaVersion,
Changelog,
}
impl Subspace {
pub(crate) const ALL: [Self; 8] = [
Self::System,
Self::Snapshot,
Self::Current,
Self::History,
Self::Inline,
Self::Index,
Self::SchemaVersion,
Self::Changelog,
];
const fn sample(self) -> Key {
match self {
Self::System => Key::Sys(SysKey::Format),
Self::Snapshot => Key::Snapshot { snapshot_id: 0 },
Self::Current => Key::current(EntityKey::Schema { schema_id: 0 }),
Self::History => Key::history(EntityKey::Schema { schema_id: 0 }, 0),
Self::Inline => Key::Inline(InlineKey::Schema {
table_id: 0,
schema_version: 0,
}),
Self::Index => Key::Index(IndexKey::Unique {
index_id: 0,
key: CanonicalKey::empty(),
}),
Self::SchemaVersion => Key::SchemaVersion {
table_id: 0,
begin_snapshot: 0,
},
Self::Changelog => Key::Changelog { snapshot_id: 0 },
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TableScopedKind {
Column,
Partition,
File,
DeleteFile,
FileColumnStats,
TableStats,
TableColumnStats,
Sort,
Mapping,
Index,
}
impl TableScopedKind {
#[allow(dead_code)]
const fn sample(self, table_id: u64) -> EntityKey {
match self {
Self::Column => EntityKey::Column {
table_id,
column_id: 0,
},
Self::Partition => EntityKey::Partition {
table_id,
partition_id: 0,
},
Self::File => EntityKey::File {
table_id,
data_file_id: 0,
},
Self::DeleteFile => EntityKey::DeleteFile {
table_id,
delete_file_id: 0,
},
Self::FileColumnStats => EntityKey::FileColumnStats {
table_id,
data_file_id: 0,
column_id: 0,
},
Self::TableStats => EntityKey::TableStats { table_id },
Self::TableColumnStats => EntityKey::TableColumnStats {
table_id,
column_id: 0,
},
Self::Sort => EntityKey::Sort {
table_id,
sort_id: 0,
},
Self::Mapping => EntityKey::Mapping {
table_id,
mapping_id: 0,
},
Self::Index => EntityKey::Index {
table_id,
index_id: 0,
},
}
}
}
const CUR_KIND_PREFIX_LEN: usize = 3;
const HISTORY_KIND_PREFIX_LEN: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InlineOperationKind {
Insert,
InlineDelete,
FileDelete,
}
impl InlineOperationKind {
const fn sample(self, table_id: u64) -> InlineOperation {
match self {
Self::Insert => InlineOperation::Insert {
table_id,
schema_version: 0,
begin_snapshot: 0,
chunk_seq: 0,
},
Self::InlineDelete => InlineOperation::InlineDelete {
table_id,
row_id: 0,
},
Self::FileDelete => InlineOperation::FileDelete {
table_id,
data_file_id: 0,
row_id: 0,
},
}
}
}
const INLINE_LIVE_KIND_PREFIX_LEN: usize = 3;
fn prefix_of(key: &Key, len: usize) -> Vec<u8> {
let mut bytes = key.encode();
bytes.truncate(len);
bytes
}
pub(crate) fn inline_live_table_prefix(kind: InlineOperationKind, table_id: u64) -> Vec<u8> {
prefix_of(
&Key::Inline(InlineKey::Live(kind.sample(table_id))),
INLINE_LIVE_KIND_PREFIX_LEN + size_of::<u64>(),
)
}
pub(crate) fn inline_live_kind_prefix(kind: InlineOperationKind) -> Vec<u8> {
prefix_of(
&Key::Inline(InlineKey::Live(kind.sample(0))),
INLINE_LIVE_KIND_PREFIX_LEN,
)
}
pub(crate) fn inline_row_tombstone_table_prefix(table_id: u64) -> Vec<u8> {
prefix_of(
&Key::Inline(InlineKey::RowTombstone {
table_id,
row_id: 0,
end_snapshot: 0,
}),
2 + size_of::<u64>(),
)
}
const INLINE_SCHEMA_PREFIX_LEN: usize = 2;
pub(crate) fn inline_schema_table_prefix(table_id: u64) -> Vec<u8> {
prefix_of(
&Key::Inline(InlineKey::Schema {
table_id,
schema_version: 0,
}),
INLINE_SCHEMA_PREFIX_LEN + size_of::<u64>(),
)
}
pub(crate) fn inline_schema_prefix() -> Vec<u8> {
prefix_of(
&Key::Inline(InlineKey::Schema {
table_id: 0,
schema_version: 0,
}),
INLINE_SCHEMA_PREFIX_LEN,
)
}
const INLINE_SCHEMA_DROPPED_PREFIX_LEN: usize = 2;
pub(crate) fn inline_schema_dropped_table_prefix(table_id: u64) -> Vec<u8> {
prefix_of(
&Key::Inline(InlineKey::SchemaDropped {
table_id,
schema_version: 0,
}),
INLINE_SCHEMA_DROPPED_PREFIX_LEN + size_of::<u64>(),
)
}
pub(crate) fn inline_schema_dropped_prefix() -> Vec<u8> {
prefix_of(
&Key::Inline(InlineKey::SchemaDropped {
table_id: 0,
schema_version: 0,
}),
INLINE_SCHEMA_DROPPED_PREFIX_LEN,
)
}
const INLINE_CHUNK_RANGE_PREFIX_LEN: usize = 2;
const INLINE_CHUNK_LOCATOR_PREFIX_LEN: usize = 2;
pub(crate) fn inline_chunk_range_table_prefix(table_id: u64) -> Vec<u8> {
prefix_of(
&Key::Inline(InlineKey::ChunkRange {
table_id,
row_id_end: 0,
}),
INLINE_CHUNK_RANGE_PREFIX_LEN + size_of::<u64>(),
)
}
pub(crate) fn inline_chunk_locator_table_prefix(table_id: u64) -> Vec<u8> {
prefix_of(
&Key::Inline(InlineKey::ChunkLocator {
table_id,
row_id_end: 0,
schema_version: 0,
begin_snapshot: 0,
chunk_seq: 0,
}),
INLINE_CHUNK_LOCATOR_PREFIX_LEN + size_of::<u64>(),
)
}
pub(crate) fn inline_chunk_locator_suffix(table_id: u64, row_id: u64) -> Vec<u8> {
Key::Inline(InlineKey::ChunkLocator {
table_id,
row_id_end: row_id,
schema_version: 0,
begin_snapshot: 0,
chunk_seq: 0,
})
.encode()
.split_off(INLINE_CHUNK_LOCATOR_PREFIX_LEN + size_of::<u64>())
}
pub(crate) const INDEX_SUBSPACE_TAG: u8 = 7;
pub(crate) const INDEX_UNIQUE_TAG: u8 = 2;
pub(crate) const INDEX_MULTI_TAG: u8 = 3;
const INDEX_KIND_PREFIX_LEN: usize = 2;
pub(crate) const INDEX_ENTRY_PREFIX_LEN: usize = INDEX_KIND_PREFIX_LEN + size_of::<u64>();
pub(crate) const fn index_kind_tag(unique: bool) -> u8 {
if unique {
INDEX_UNIQUE_TAG
} else {
INDEX_MULTI_TAG
}
}
#[cfg(test)]
pub(crate) fn index_entry_bytes(
values: &[Option<crate::store::index_encoding::IndexKeyValue>],
) -> u64 {
let bytes = INDEX_ENTRY_PREFIX_LEN
+ crate::store::index_encoding::nominal_key_bytes(values)
+ size_of::<u64>();
bytes as u64
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum IndexKind {
Unique,
Multi,
}
pub(crate) fn index_kind_prefix(kind: IndexKind) -> Vec<u8> {
let key = match kind {
IndexKind::Unique => Key::Index(IndexKey::Unique {
index_id: 0,
key: CanonicalKey::empty(),
}),
IndexKind::Multi => Key::Index(IndexKey::Multi {
index_id: 0,
key: CanonicalKey::empty(),
row_id: 0,
}),
};
prefix_of(&key, INDEX_KIND_PREFIX_LEN)
}
pub(crate) fn index_index_prefix(kind: IndexKind, index_id: u64) -> Vec<u8> {
let key = match kind {
IndexKind::Unique => Key::Index(IndexKey::Unique {
index_id,
key: CanonicalKey::empty(),
}),
IndexKind::Multi => Key::Index(IndexKey::Multi {
index_id,
key: CanonicalKey::empty(),
row_id: 0,
}),
};
prefix_of(&key, INDEX_ENTRY_PREFIX_LEN)
}
pub(crate) fn index_multi_value_prefix(index_id: u64, value: &CanonicalKey) -> Vec<u8> {
let mut bytes = Key::Index(IndexKey::Multi {
index_id,
key: value.clone(),
row_id: 0,
})
.encode();
bytes.truncate(bytes.len() - size_of::<u64>());
bytes
}
pub(crate) fn index_value_suffix(kind: IndexKind, index_id: u64, value: &CanonicalKey) -> Vec<u8> {
let mut bytes = match kind {
IndexKind::Unique => Key::Index(IndexKey::Unique {
index_id,
key: value.clone(),
})
.encode(),
IndexKind::Multi => index_multi_value_prefix(index_id, value),
};
bytes.drain(..INDEX_ENTRY_PREFIX_LEN);
bytes
}
pub(crate) fn index_value_body(kind: IndexKind, index_id: u64, value: &CanonicalKey) -> Vec<u8> {
let mut body = index_value_suffix(kind, index_id, value);
body.pop();
body
}
pub(crate) fn index_value_above(
kind: IndexKind,
index_id: u64,
value: &CanonicalKey,
) -> Option<Vec<u8>> {
increment_prefix(index_value_body(kind, index_id, value))
}
pub(crate) fn increment_prefix(mut bytes: Vec<u8>) -> Option<Vec<u8>> {
while let Some(last) = bytes.last_mut() {
if *last != u8::MAX {
*last += 1;
return Some(bytes);
}
bytes.pop();
}
None
}
pub(crate) const SPLIT_SCAN_KINDS: [EntityKind; 5] = [
EntityKind::Column,
EntityKind::File,
EntityKind::DeleteFile,
EntityKind::FileColumnStats,
EntityKind::TableColumnStats,
];
const SPLIT_WIDTH: usize = 16;
pub(crate) type SuffixRange = (Bound<Vec<u8>>, Bound<Vec<u8>>);
fn within(lower: &Bound<Vec<u8>>, upper: &Bound<Vec<u8>>, suffix: &[u8]) -> bool {
let above = match lower {
Bound::Unbounded => true,
Bound::Included(start) => suffix >= start.as_slice(),
Bound::Excluded(start) => suffix > start.as_slice(),
};
let below = match upper {
Bound::Unbounded => true,
Bound::Included(end) => suffix <= end.as_slice(),
Bound::Excluded(end) => suffix < end.as_slice(),
};
above && below
}
pub(crate) fn split_between(
lower: Bound<Vec<u8>>,
upper: Bound<Vec<u8>>,
mut points: Vec<Vec<u8>>,
) -> Vec<SuffixRange> {
points.retain(|point| {
within(&lower, &upper, point) && !matches!(&lower, Bound::Included(start) if start == point)
});
points.sort_unstable();
points.dedup();
let mut ranges = Vec::with_capacity(points.len() + 1);
let mut start = lower;
for point in points {
ranges.push((start, Bound::Excluded(point.clone())));
start = Bound::Included(point);
}
ranges.push((start, upper));
ranges
}
pub(crate) fn even_points_between(low: &[u8], high: &[u8], parts: usize) -> Vec<Vec<u8>> {
let widen = |suffix: &[u8]| {
let mut bytes = [0_u8; SPLIT_WIDTH];
let taken = suffix.len().min(SPLIT_WIDTH);
bytes[..taken].copy_from_slice(&suffix[..taken]);
u128::from_be_bytes(bytes)
};
let (low, high) = (widen(low), widen(high));
let Some(step) = high
.checked_sub(low)
.map(|span| span / u128::try_from(parts.max(1)).unwrap_or(u128::MAX))
else {
return Vec::new();
};
if step == 0 {
return Vec::new();
}
(1..parts)
.map(|part| low + step * u128::try_from(part).unwrap_or(u128::MAX))
.map(|point| point.to_be_bytes().to_vec())
.collect()
}
pub(crate) fn subspace_prefix(subspace: Subspace) -> Vec<u8> {
prefix_of(&subspace.sample(), TAG_PREFIX_LEN)
}
#[allow(dead_code)]
pub(crate) fn current_table_prefix(kind: TableScopedKind, table_id: u64) -> Vec<u8> {
prefix_of(
&Key::current(kind.sample(table_id)),
CUR_KIND_PREFIX_LEN + size_of::<u64>(),
)
}
pub(crate) fn current_entity_kind_prefix(kind: EntityKind) -> Vec<u8> {
prefix_of(&Key::current(kind.sample()), CUR_KIND_PREFIX_LEN)
}
pub(crate) fn history_entity_kind_prefix(kind: EntityKind) -> Vec<u8> {
prefix_of(&Key::history(kind.sample(), 0), HISTORY_KIND_PREFIX_LEN)
}
pub(crate) fn current_gc_file_prefix() -> Vec<u8> {
prefix_of(&Key::Current(CurrentKey::GcFile { data_file_id: 0 }), 2)
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
use crate::store::index_encoding::{IndexKeyValue, IntWidth, encode_key};
fn be(v: u64) -> Vec<u8> {
v.to_be_bytes().to_vec()
}
#[test]
fn golden_sys_keys() {
assert_eq!(Key::Sys(SysKey::Format).encode(), vec![0x02, 0x02]);
assert_eq!(Key::Sys(SysKey::Head).encode(), vec![0x02, 0x03]);
assert_eq!(Key::Sys(SysKey::Migration).encode(), vec![0x02, 0x04]);
assert_eq!(
Key::Sys(SysKey::MaintenanceStatus).encode(),
vec![0x02, 0x05]
);
}
#[test]
fn golden_snapshot_key() {
let mut expect = vec![0x03];
expect.extend(be(42));
assert_eq!(Key::Snapshot { snapshot_id: 42 }.encode(), expect);
}
#[test]
fn golden_changelog_key() {
let mut expect = vec![0x09];
expect.extend(be(42));
assert_eq!(Key::Changelog { snapshot_id: 42 }.encode(), expect);
}
#[test]
fn golden_current_file_key() {
let mut expect = vec![0x04, 0x02, 0x07];
expect.extend(be(1));
expect.extend(be(2));
let key = Key::current(EntityKey::File {
table_id: 1,
data_file_id: 2,
});
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_current_sort_key() {
let mut expect = vec![0x04, 0x02, 0x0e];
expect.extend(be(7));
expect.extend(be(9));
let key = Key::current(EntityKey::Sort {
table_id: 7,
sort_id: 9,
});
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_current_macro_key() {
let mut expect = vec![0x04, 0x02, 0x0f];
expect.extend(be(3));
let key = Key::current(EntityKey::Macro { macro_id: 3 });
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_history_file_key_appends_end_snapshot() {
let mut expect = vec![0x05, 0x07];
expect.extend(be(1));
expect.extend(be(2));
expect.extend(be(9));
let key = Key::history(
EntityKey::File {
table_id: 1,
data_file_id: 2,
},
9,
);
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_history_macro_key_appends_end_snapshot() {
let mut expect = vec![0x05, 0x0f];
expect.extend(be(3));
expect.extend(be(9));
let key = Key::history(EntityKey::Macro { macro_id: 3 }, 9);
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_current_index_key() {
let mut expect = vec![0x04, 0x02, 0x11];
expect.extend(be(4));
expect.extend(be(7));
let key = Key::current(EntityKey::Index {
table_id: 4,
index_id: 7,
});
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_current_mapping_key() {
let mut expect = vec![0x04, 0x02, 0x10];
expect.extend(be(4));
expect.extend(be(21));
let key = Key::current(EntityKey::Mapping {
table_id: 4,
mapping_id: 21,
});
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_gcfile_key() {
let mut expect = vec![0x04, 0x03];
expect.extend(be(5));
let key = Key::Current(CurrentKey::GcFile { data_file_id: 5 });
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_inline_ins_key() {
let mut expect = vec![0x06, 0x03, 0x02];
for v in [7, 3, 11, 0] {
expect.extend(be(v));
}
let key = Key::Inline(InlineKey::Live(InlineOperation::Insert {
table_id: 7,
schema_version: 3,
begin_snapshot: 11,
chunk_seq: 0,
}));
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_inline_file_delete_table_key() {
let mut expect = vec![0x06, 0x05];
expect.extend(be(7));
let key = Key::Inline(InlineKey::FileDeleteTable { table_id: 7 });
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_inline_schema_dropped_key() {
let mut expect = vec![0x06, 0x07];
expect.extend(be(7));
expect.extend(be(3));
let key = Key::Inline(InlineKey::SchemaDropped {
table_id: 7,
schema_version: 3,
});
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_inline_chunk_range_key() {
let mut expect = vec![0x06, 0x06];
expect.extend(be(7));
expect.extend(be(29));
let key = Key::Inline(InlineKey::ChunkRange {
table_id: 7,
row_id_end: 29,
});
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_inline_chunk_locator_key() {
let mut expect = vec![0x06, 0x08];
expect.extend(be(7));
expect.extend(be(29));
expect.extend(be(3));
expect.extend(be(11));
expect.extend(be(2));
let key = Key::Inline(InlineKey::ChunkLocator {
table_id: 7,
row_id_end: 29,
schema_version: 3,
begin_snapshot: 11,
chunk_seq: 2,
});
assert_eq!(key.encode(), expect);
}
#[test]
fn golden_inline_locator_kinds_are_distinct() {
let legacy = Key::Inline(InlineKey::ChunkRange {
table_id: 7,
row_id_end: 29,
})
.encode();
let current = Key::Inline(InlineKey::ChunkLocator {
table_id: 7,
row_id_end: 29,
schema_version: 0,
begin_snapshot: 0,
chunk_seq: 0,
})
.encode();
assert_ne!(legacy[1], current[1]);
}
#[test]
fn golden_arch_kinds_are_distinct_from_live() {
let ops = [
InlineOperation::Insert {
table_id: 7,
schema_version: 3,
begin_snapshot: 11,
chunk_seq: 0,
},
InlineOperation::InlineDelete {
table_id: 7,
row_id: 9,
},
InlineOperation::FileDelete {
table_id: 7,
data_file_id: 2,
row_id: 9,
},
];
for op in ops {
let live = Key::Inline(InlineKey::Live(op)).encode();
let arch = Key::Inline(InlineKey::Arch(op)).encode();
assert_eq!(live[0], 0x06);
assert_eq!(arch[0], 0x06);
assert_ne!(live[1], arch[1]);
assert_eq!(live[2..], arch[2..]);
assert!(arch > live);
}
}
fn canon(values: &[IndexKeyValue]) -> CanonicalKey {
encode_key(values).unwrap()
}
#[test]
fn golden_index_unique_key_leads_with_subspace_and_index() {
let key = Key::Index(IndexKey::Unique {
index_id: 1,
key: canon(&[IndexKeyValue::Int {
value: 7,
width: IntWidth::I64,
}]),
});
let bytes = key.encode();
let mut prefix = vec![0x07, 0x02];
prefix.extend(be(1));
assert!(bytes.starts_with(&prefix), "{bytes:?}");
}
#[test]
fn borrowed_index_entry_encoder_matches_owned_key() {
let canonical = canon(&[
IndexKeyValue::Str("a\0\u{1}z".to_owned()),
IndexKeyValue::Bytes(vec![0, 1, 0xff]),
]);
for unique in [false, true] {
for row_id in [0, 1_u64 << 56, u64::MAX] {
let owned = if unique {
Key::Index(IndexKey::Unique {
index_id: 7,
key: canonical.clone(),
})
} else {
Key::Index(IndexKey::Multi {
index_id: 7,
key: canonical.clone(),
row_id,
})
};
assert_eq!(
encode_index_entry(7, unique, &canonical, row_id),
owned.encode()
);
}
}
}
#[test]
fn index_multi_key_roundtrips_when_the_row_id_leads_with_the_escape_byte() {
let values = [
vec![IndexKeyValue::Str("a".to_owned())],
vec![IndexKeyValue::Int {
value: 0,
width: IntWidth::I64,
}],
vec![IndexKeyValue::Bytes(vec![0, 1, 2])],
vec![],
];
for row_id in [1u64 << 56, (1 << 56) + 1, (1 << 57) - 1] {
for value in &values {
let key = Key::Index(IndexKey::Multi {
index_id: 1,
key: canon(value),
row_id,
});
assert_eq!(Key::decode(&key.encode()).unwrap(), key, "row_id {row_id}");
}
}
}
#[test]
fn golden_index_multi_value_framing_and_trailing_row_id() {
let key = Key::Index(IndexKey::Multi {
index_id: 1,
key: canon(&[IndexKeyValue::Bytes(vec![0x00, 0x01, 0x41])]),
row_id: 1 << 56,
});
let mut expected = vec![0x07, 0x03];
expected.extend(be(1));
expected.extend([
0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x41, 0x01, 0x00, 0x00, ]);
expected.extend(be(1 << 56));
assert_eq!(key.encode(), expected);
}
#[test]
fn golden_index_multi_key_leads_with_subspace_and_index() {
let key = Key::Index(IndexKey::Multi {
index_id: 1,
key: canon(&[IndexKeyValue::Int {
value: 7,
width: IntWidth::I64,
}]),
row_id: 3,
});
let bytes = key.encode();
let mut prefix = vec![0x07, 0x03];
prefix.extend(be(1));
assert!(bytes.starts_with(&prefix), "{bytes:?}");
assert!(bytes.ends_with(&be(3)), "{bytes:?}");
}
#[test]
fn index_composite_framing_distinct_at_key_level() {
let ab_c = Key::Index(IndexKey::Unique {
index_id: 1,
key: canon(&[
IndexKeyValue::Str("ab".into()),
IndexKeyValue::Str("c".into()),
]),
});
let a_bc = Key::Index(IndexKey::Unique {
index_id: 1,
key: canon(&[
IndexKeyValue::Str("a".into()),
IndexKeyValue::Str("bc".into()),
]),
});
assert_ne!(ab_c.encode(), a_bc.encode());
}
#[test]
fn index_tags_match_the_encoded_discriminants() {
let unique = Key::Index(IndexKey::Unique {
index_id: 9,
key: CanonicalKey::empty(),
})
.encode();
let multi = Key::Index(IndexKey::Multi {
index_id: 9,
key: CanonicalKey::empty(),
row_id: 0,
})
.encode();
assert_eq!(unique[0], INDEX_SUBSPACE_TAG);
assert_eq!(multi[0], INDEX_SUBSPACE_TAG);
assert_eq!(unique[1], INDEX_UNIQUE_TAG);
assert_eq!(multi[1], INDEX_MULTI_TAG);
assert_eq!(index_kind_tag(true), INDEX_UNIQUE_TAG);
assert_eq!(index_kind_tag(false), INDEX_MULTI_TAG);
assert_eq!(&unique[2..INDEX_ENTRY_PREFIX_LEN], &9_u64.to_be_bytes());
}
#[test]
fn index_index_prefix_covers_one_index_and_kind() {
let value = canon(&[IndexKeyValue::Str("v".into())]);
let unique = Key::Index(IndexKey::Unique {
index_id: 4,
key: value.clone(),
})
.encode();
assert!(unique.starts_with(&index_index_prefix(IndexKind::Unique, 4)));
assert!(!unique.starts_with(&index_index_prefix(IndexKind::Unique, 5)));
assert!(!unique.starts_with(&index_index_prefix(IndexKind::Multi, 4)));
let multi = Key::Index(IndexKey::Multi {
index_id: 4,
key: value,
row_id: 1,
})
.encode();
assert!(multi.starts_with(&index_index_prefix(IndexKind::Multi, 4)));
assert!(!multi.starts_with(&index_index_prefix(IndexKind::Unique, 4)));
}
#[test]
fn index_index_prefixes_sort_ascending_within_a_kind() {
for kind in [IndexKind::Unique, IndexKind::Multi] {
let kind_prefix = index_kind_prefix(kind);
let mut previous = index_index_prefix(kind, 0);
assert!(previous.starts_with(&kind_prefix));
for index_id in [1, 2, 255, 256, u64::MAX - 1, u64::MAX] {
let prefix = index_index_prefix(kind, index_id);
assert!(prefix.starts_with(&kind_prefix));
assert!(
prefix > previous,
"index {index_id} must sort after its predecessor in {kind:?}"
);
previous = prefix;
}
}
assert_ne!(
index_kind_prefix(IndexKind::Unique),
index_kind_prefix(IndexKind::Multi)
);
}
#[test]
fn index_multi_value_prefix_covers_all_row_ids_of_one_value() {
let value = canon(&[IndexKeyValue::Str("shared".into())]);
let prefix = index_multi_value_prefix(9, &value);
for row_id in [0, 1, u64::MAX] {
let key = Key::Index(IndexKey::Multi {
index_id: 9,
key: value.clone(),
row_id,
});
assert!(key.encode().starts_with(&prefix), "row_id {row_id}");
}
let other_value = canon(&[IndexKeyValue::Str("different".into())]);
assert!(
!Key::Index(IndexKey::Multi {
index_id: 9,
key: other_value,
row_id: 0,
})
.encode()
.starts_with(&prefix)
);
assert!(
!Key::Index(IndexKey::Multi {
index_id: 10,
key: value,
row_id: 0,
})
.encode()
.starts_with(&prefix)
);
}
#[test]
fn index_keys_roundtrip() {
let keys = [
Key::Index(IndexKey::Unique {
index_id: 5,
key: canon(&[IndexKeyValue::Str("hello".into())]),
}),
Key::Index(IndexKey::Multi {
index_id: 9,
key: canon(&[
IndexKeyValue::Int {
value: -3,
width: IntWidth::I32,
},
IndexKeyValue::Bool(true),
]),
row_id: 42,
}),
];
for key in keys {
let decoded = Key::decode(&key.encode()).unwrap();
assert_eq!(decoded, key);
}
}
#[test]
fn decode_roundtrips_representative_keys() {
let keys = [
Key::Sys(SysKey::Head),
Key::Snapshot {
snapshot_id: u64::MAX,
},
Key::current(EntityKey::Column {
table_id: 3,
column_id: 4,
}),
Key::history(
EntityKey::Option {
scope_kind: 2,
scope_id: 3,
},
8,
),
Key::Current(CurrentKey::GcFile { data_file_id: 0 }),
Key::Inline(InlineKey::Live(InlineOperation::FileDelete {
table_id: 1,
data_file_id: 2,
row_id: 3,
})),
Key::Inline(InlineKey::Arch(InlineOperation::FileDelete {
table_id: 1,
data_file_id: 2,
row_id: 3,
})),
Key::Inline(InlineKey::FileDeleteTable { table_id: 1 }),
];
for key in keys {
let decoded = Key::decode(&key.encode()).unwrap();
assert_eq!(decoded, key);
}
}
#[test]
fn decode_rejects_unknown_discriminants_and_bad_arity() {
assert!(Key::decode(&[0x00, 0x02]).is_err());
assert!(Key::decode(&[0x7f, 0x02]).is_err());
assert!(Key::decode(&[0x02, 0x7f]).is_err());
assert!(Key::decode(&[0x06, 0x7f]).is_err());
let mut short = Key::Snapshot { snapshot_id: 1 }.encode();
short.pop();
assert!(Key::decode(&short).is_err());
let mut long = Key::Sys(SysKey::Head).encode();
long.push(0);
assert!(Key::decode(&long).is_err());
assert!(Key::decode(&[]).is_err());
}
#[test]
fn prefixes_are_byte_prefixes_of_full_keys() {
let key = Key::current(EntityKey::File {
table_id: 1,
data_file_id: 2,
});
let bytes = key.encode();
assert!(bytes.starts_with(&subspace_prefix(Subspace::Current)));
assert!(bytes.starts_with(¤t_table_prefix(TableScopedKind::File, 1)));
assert!(!bytes.starts_with(¤t_table_prefix(TableScopedKind::File, 2)));
assert!(!bytes.starts_with(¤t_table_prefix(TableScopedKind::DeleteFile, 1)));
}
#[test]
fn entity_kind_prefixes_select_only_the_requested_kind() {
let current = Key::current(EntityKey::File {
table_id: 1,
data_file_id: 2,
})
.encode();
let history = Key::history(
EntityKey::File {
table_id: 1,
data_file_id: 2,
},
3,
)
.encode();
assert!(current.starts_with(¤t_entity_kind_prefix(EntityKind::File)));
assert!(history.starts_with(&history_entity_kind_prefix(EntityKind::File)));
assert!(!current.starts_with(¤t_entity_kind_prefix(EntityKind::Table)));
assert!(!history.starts_with(&history_entity_kind_prefix(EntityKind::Table)));
}
#[test]
fn table_scoped_prefixes_cover_all_kinds() {
let kinds = [
TableScopedKind::Column,
TableScopedKind::Partition,
TableScopedKind::File,
TableScopedKind::DeleteFile,
TableScopedKind::FileColumnStats,
TableScopedKind::TableStats,
TableScopedKind::TableColumnStats,
TableScopedKind::Sort,
];
for kind in kinds {
let key = Key::current(kind.sample(9));
let bytes = key.encode();
assert!(
bytes.starts_with(¤t_table_prefix(kind, 9)),
"{kind:?} prefix must match its own key"
);
assert!(
!bytes.starts_with(¤t_table_prefix(kind, 10)),
"{kind:?} prefix must not match another table"
);
}
}
#[test]
fn inline_live_table_prefixes_cover_all_operation_kinds() {
let kinds = [
InlineOperationKind::Insert,
InlineOperationKind::InlineDelete,
InlineOperationKind::FileDelete,
];
for kind in kinds {
let key = Key::Inline(InlineKey::Live(kind.sample(9)));
let bytes = key.encode();
assert!(
bytes.starts_with(&inline_live_table_prefix(kind, 9)),
"{kind:?} prefix must match its own key"
);
assert!(
!bytes.starts_with(&inline_live_table_prefix(kind, 10)),
"{kind:?} prefix must not match another table"
);
}
assert!(
!inline_live_table_prefix(InlineOperationKind::InlineDelete, 9)
.starts_with(&inline_live_table_prefix(InlineOperationKind::Insert, 9))
);
let arch = Key::Inline(InlineKey::Arch(InlineOperationKind::Insert.sample(9))).encode();
assert!(!arch.starts_with(&inline_live_table_prefix(InlineOperationKind::Insert, 9)));
}
#[test]
fn inline_schema_table_prefix_covers_all_versions() {
for schema_version in [0, 1, 42] {
let key = Key::Inline(InlineKey::Schema {
table_id: 9,
schema_version,
});
assert!(key.encode().starts_with(&inline_schema_table_prefix(9)));
}
let other_table = Key::Inline(InlineKey::Schema {
table_id: 10,
schema_version: 0,
});
assert!(
!other_table
.encode()
.starts_with(&inline_schema_table_prefix(9))
);
}
#[test]
fn inline_schema_dropped_prefixes_are_disjoint_from_live_schemas() {
for schema_version in [0, 1, 42] {
let key = Key::Inline(InlineKey::SchemaDropped {
table_id: 9,
schema_version,
});
assert!(
key.encode()
.starts_with(&inline_schema_dropped_table_prefix(9))
);
assert!(key.encode().starts_with(&inline_schema_dropped_prefix()));
assert!(!key.encode().starts_with(&inline_schema_prefix()));
}
let live = Key::Inline(InlineKey::Schema {
table_id: 9,
schema_version: 0,
});
assert!(!live.encode().starts_with(&inline_schema_dropped_prefix()));
let other_table = Key::Inline(InlineKey::SchemaDropped {
table_id: 10,
schema_version: 0,
});
assert!(
!other_table
.encode()
.starts_with(&inline_schema_dropped_table_prefix(9))
);
}
fn arb_entity() -> impl Strategy<Value = EntityKey> {
prop_oneof![
any::<u64>().prop_map(|schema_id| EntityKey::Schema { schema_id }),
any::<u64>().prop_map(|table_id| EntityKey::Table { table_id }),
any::<u64>().prop_map(|view_id| EntityKey::View { view_id }),
any::<(u64, u64)>().prop_map(|(table_id, column_id)| EntityKey::Column {
table_id,
column_id
}),
any::<(u64, u64)>().prop_map(|(table_id, partition_id)| EntityKey::Partition {
table_id,
partition_id
}),
any::<(u64, u64)>().prop_map(|(table_id, data_file_id)| EntityKey::File {
table_id,
data_file_id
}),
any::<(u64, u64)>().prop_map(|(table_id, delete_file_id)| EntityKey::DeleteFile {
table_id,
delete_file_id
}),
any::<(u64, u64, u64)>().prop_map(|(table_id, data_file_id, column_id)| {
EntityKey::FileColumnStats {
table_id,
data_file_id,
column_id,
}
}),
any::<u64>().prop_map(|table_id| EntityKey::TableStats { table_id }),
any::<(u64, u64)>().prop_map(|(table_id, column_id)| EntityKey::TableColumnStats {
table_id,
column_id
}),
any::<(u64, u64)>()
.prop_map(|(table_id, sort_id)| EntityKey::Sort { table_id, sort_id }),
any::<u64>().prop_map(|object_id| EntityKey::Tag { object_id }),
any::<(u64, u64)>().prop_map(|(scope_kind, scope_id)| EntityKey::Option {
scope_kind,
scope_id
}),
any::<u64>().prop_map(|macro_id| EntityKey::Macro { macro_id }),
any::<(u64, u64)>().prop_map(|(table_id, mapping_id)| EntityKey::Mapping {
table_id,
mapping_id
}),
any::<(u64, u64)>()
.prop_map(|(table_id, index_id)| EntityKey::Index { table_id, index_id }),
]
}
fn arb_inline_op() -> impl Strategy<Value = InlineOperation> {
prop_oneof![
any::<(u64, u64, u64, u64)>().prop_map(
|(table_id, schema_version, begin_snapshot, chunk_seq)| InlineOperation::Insert {
table_id,
schema_version,
begin_snapshot,
chunk_seq
}
),
any::<(u64, u64)>()
.prop_map(|(table_id, row_id)| InlineOperation::InlineDelete { table_id, row_id }),
any::<(u64, u64, u64)>().prop_map(|(table_id, data_file_id, row_id)| {
InlineOperation::FileDelete {
table_id,
data_file_id,
row_id,
}
}),
]
}
fn arb_canonical_key() -> impl Strategy<Value = CanonicalKey> {
proptest::collection::vec(proptest::collection::vec(any::<u8>(), 0..8), 0..4).prop_map(
|components| {
let values = components
.into_iter()
.map(IndexKeyValue::Bytes)
.collect::<Vec<_>>();
encode_key(&values).unwrap()
},
)
}
fn arb_index() -> impl Strategy<Value = IndexKey> {
prop_oneof![
(any::<u64>(), arb_canonical_key())
.prop_map(|(index_id, key)| IndexKey::Unique { index_id, key }),
(any::<u64>(), arb_canonical_key(), any::<u64>()).prop_map(
|(index_id, key, row_id)| {
IndexKey::Multi {
index_id,
key,
row_id,
}
}
),
]
}
fn arb_key() -> impl Strategy<Value = Key> {
prop_oneof![
Just(Key::Sys(SysKey::Format)),
Just(Key::Sys(SysKey::Head)),
Just(Key::Sys(SysKey::Migration)),
Just(Key::Sys(SysKey::MaintenanceStatus)),
any::<u64>().prop_map(|snapshot_id| Key::Snapshot { snapshot_id }),
arb_entity().prop_map(Key::current),
(arb_entity(), any::<u64>()).prop_map(|(entity, end)| Key::history(entity, end)),
any::<u64>().prop_map(|data_file_id| Key::Current(CurrentKey::GcFile { data_file_id })),
any::<(u64, u64)>().prop_map(|(table_id, schema_version)| {
Key::Inline(InlineKey::Schema {
table_id,
schema_version,
})
}),
arb_inline_op().prop_map(|op| Key::Inline(InlineKey::Live(op))),
arb_inline_op().prop_map(|op| Key::Inline(InlineKey::Arch(op))),
any::<u64>().prop_map(|table_id| Key::Inline(InlineKey::FileDeleteTable { table_id })),
any::<(u64, u64)>().prop_map(|(table_id, schema_version)| {
Key::Inline(InlineKey::SchemaDropped {
table_id,
schema_version,
})
}),
any::<(u64, u64)>().prop_map(|(table_id, row_id_end)| {
Key::Inline(InlineKey::ChunkRange {
table_id,
row_id_end,
})
}),
any::<(u64, u64, u64, u64, u64)>().prop_map(
|(table_id, row_id_end, schema_version, begin_snapshot, chunk_seq)| {
Key::Inline(InlineKey::ChunkLocator {
table_id,
row_id_end,
schema_version,
begin_snapshot,
chunk_seq,
})
},
),
arb_index().prop_map(Key::Index),
any::<(u64, u64, u64)>().prop_map(|(table_id, row_id, end_snapshot)| {
Key::Inline(InlineKey::RowTombstone {
table_id,
row_id,
end_snapshot,
})
}),
any::<u64>().prop_map(|snapshot_id| Key::Changelog { snapshot_id }),
]
}
proptest! {
#[test]
fn inline_tombstone_roundtrip_and_table_prefix(
table_id in any::<u64>(), row_id in any::<u64>(), end_snapshot in any::<u64>(),
) {
let key = Key::Inline(InlineKey::RowTombstone { table_id, row_id, end_snapshot });
let encoded = key.encode();
prop_assert_eq!(Key::decode(&encoded).unwrap(), key);
prop_assert!(encoded.starts_with(&inline_row_tombstone_table_prefix(table_id)));
prop_assert!(!encoded.starts_with(&inline_live_table_prefix(InlineOperationKind::InlineDelete, table_id)));
}
#[test]
fn borrowed_index_entry_encoding_matches_owned(
index_id in any::<u64>(),
key in arb_canonical_key(),
row_id in any::<u64>(),
unique in any::<bool>(),
) {
let owned = if unique {
Key::Index(IndexKey::Unique {
index_id,
key: key.clone(),
})
} else {
Key::Index(IndexKey::Multi {
index_id,
key: key.clone(),
row_id,
})
};
prop_assert_eq!(
encode_index_entry(index_id, unique, &key, row_id),
owned.encode()
);
}
#[test]
fn roundtrip(key in arb_key()) {
let decoded = Key::decode(&key.encode()).unwrap();
prop_assert_eq!(decoded, key);
}
#[test]
fn encoded_keys_are_unique(a in arb_key(), b in arb_key()) {
if a != b {
prop_assert_ne!(a.encode(), b.encode());
}
}
#[test]
fn encoding_preserves_order(a in arb_key(), b in arb_key()) {
prop_assert_eq!(a.cmp(&b), a.encode().cmp(&b.encode()));
}
#[test]
fn decode_arbitrary_bytes_never_panics(
bytes in proptest::collection::vec(any::<u8>(), 0..64),
) {
let _ = Key::decode(&bytes);
}
#[test]
fn decode_garbage_in_valid_subspace_never_panics(
subspace in 0x02u8..=0x07,
bytes in proptest::collection::vec(any::<u8>(), 0..64),
) {
let mut encoded = vec![subspace];
encoded.extend(&bytes);
let _ = Key::decode(&encoded);
}
#[test]
fn decode_corrupted_valid_key_never_panics(
key in arb_key(),
index in any::<proptest::sample::Index>(),
byte in any::<u8>(),
) {
let mut bytes = key.encode();
let position = index.index(bytes.len());
bytes[position] = byte;
let _ = Key::decode(&bytes);
}
}
mod index_value_bounds {
use proptest::prelude::*;
use super::*;
use crate::store::index_encoding::{
Direction, IndexKeyValue, IntWidth, NullOrder, encode_ordered_values,
};
fn arbitrary_value() -> impl Strategy<Value = IndexKeyValue> {
prop_oneof![
any::<i64>().prop_map(|value| IndexKeyValue::Int {
value: i128::from(value),
width: IntWidth::I64,
}),
any::<u32>().prop_map(|value| IndexKeyValue::UInt {
value: u128::from(value),
width: IntWidth::I32,
}),
any::<bool>().prop_map(IndexKeyValue::Bool),
".{0,8}".prop_map(IndexKeyValue::Str),
proptest::collection::vec(any::<u8>(), 0..8).prop_map(IndexKeyValue::Bytes),
]
}
fn encode(values: &[IndexKeyValue], descending: bool) -> CanonicalKey {
let direction = if descending {
Direction::Descending
} else {
Direction::Ascending
};
encode_ordered_values(
&values.iter().cloned().map(Some).collect::<Vec<_>>(),
&[direction, direction],
&[NullOrder::Last, NullOrder::Last],
)
.expect("values within the key size cap encode")
}
fn multi_entry(index_id: u64, canon: &CanonicalKey, row_id: u64) -> Vec<u8> {
let full = Key::Index(IndexKey::Multi {
index_id,
key: canon.clone(),
row_id,
})
.encode();
full[INDEX_ENTRY_PREFIX_LEN..].to_vec()
}
proptest! {
#[test]
fn above_a_prefix_outranks_every_extension(
leading in arbitrary_value(),
extension in arbitrary_value(),
row_id: u64,
descending: bool,
) {
let index_id = 7;
let prefix = encode(std::slice::from_ref(&leading), descending);
let extended = encode(&[leading, extension], descending);
for kind in [IndexKind::Unique, IndexKind::Multi] {
let bound = index_value_above(kind, index_id, &prefix)
.expect("a framed body is never all 0xff");
prop_assert!(bound > index_value_suffix(kind, index_id, &prefix));
prop_assert!(bound > index_value_suffix(kind, index_id, &extended));
}
prop_assert!(
index_value_above(IndexKind::Multi, index_id, &prefix)
.expect("a framed body is never all 0xff")
> multi_entry(index_id, &extended, row_id)
);
}
#[test]
fn above_a_prefix_stops_below_a_larger_value(
one in arbitrary_value(),
two in arbitrary_value(),
descending: bool,
) {
let index_id = 7;
let kind = IndexKind::Unique;
let first = encode(&[one], descending);
let second = encode(&[two], descending);
let (lower, higher) = if index_value_suffix(kind, index_id, &first)
< index_value_suffix(kind, index_id, &second)
{
(&first, &second)
} else {
(&second, &first)
};
prop_assume!(lower != higher);
let bound = index_value_above(kind, index_id, lower)
.expect("a framed body is never all 0xff");
prop_assert!(bound <= index_value_suffix(kind, index_id, higher));
}
}
}
fn arbitrary_bound() -> impl Strategy<Value = Bound<Vec<u8>>> {
prop_oneof![
Just(Bound::Unbounded),
prop::collection::vec(any::<u8>(), 0..20).prop_map(Bound::Included),
prop::collection::vec(any::<u8>(), 0..20).prop_map(Bound::Excluded),
]
}
proptest! {
#[test]
fn split_ranges_partition_the_outer_range(
lower in arbitrary_bound(),
upper in arbitrary_bound(),
points in prop::collection::vec(prop::collection::vec(any::<u8>(), 0..20), 0..12),
suffix in prop::collection::vec(any::<u8>(), 0..24),
) {
let ranges = split_between(lower.clone(), upper.clone(), points.clone());
let holders = ranges
.iter()
.filter(|range| within(&range.0, &range.1, &suffix))
.count();
let expected = usize::from(within(&lower, &upper, &suffix));
prop_assert_eq!(holders, expected, "{:?} in {:?}", suffix, ranges);
for point in &points {
if within(&lower, &upper, point) && lower != Bound::Included(point.clone()) {
prop_assert!(ranges.iter().any(|range| range.0 == Bound::Included(point.clone())));
}
}
}
#[test]
fn even_points_lie_between_the_extremes(
low in prop::collection::vec(any::<u8>(), 16..24),
high in prop::collection::vec(any::<u8>(), 16..24),
parts in 1_usize..12,
) {
let (low, high) = if low <= high { (low, high) } else { (high, low) };
let points = even_points_between(&low, &high, parts);
prop_assert!(points.len() < parts);
prop_assert!(points.windows(2).all(|pair| pair[0] < pair[1]));
for point in &points {
prop_assert!(point.as_slice() > low.as_slice(), "{:?} <= {:?}", point, low);
prop_assert!(point.as_slice() < high.as_slice(), "{:?} >= {:?}", point, high);
}
}
}
#[test]
fn even_points_split_between_the_extremes_present() {
let low = 7_u64
.to_be_bytes()
.iter()
.chain(&0_u64.to_be_bytes())
.copied()
.collect::<Vec<_>>();
let high = 7_u64
.to_be_bytes()
.iter()
.chain(&4_000_u64.to_be_bytes())
.copied()
.collect::<Vec<_>>();
let points = even_points_between(&low, &high, 4);
assert_eq!(points.len(), 3);
assert_eq!(&points[0][..8], &7_u64.to_be_bytes());
assert_eq!(&points[0][8..], &1_000_u64.to_be_bytes());
assert_eq!(&points[2][8..], &3_000_u64.to_be_bytes());
assert!(even_points_between(&low, &low, 4).is_empty());
assert_eq!(
split_between(Bound::Unbounded, Bound::Unbounded, Vec::new()),
vec![(Bound::Unbounded, Bound::Unbounded)]
);
}
}