use storekey::{Decode, Encode};
use crate::{
error::{Error, Result},
store::index_encoding::{CanonicalKey, IndexKeyValue, nominal_key_bytes},
};
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,
},
}
#[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(7)?;
writer.write_u8(if unique { 2 } else { 3 })?;
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,
},
}
}
}
#[allow(dead_code)]
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>(),
)
}
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_CHUNK_RANGE_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_range_suffix(table_id: u64, row_id: u64) -> Vec<u8> {
let prefix = inline_chunk_range_table_prefix(table_id);
let encoded = Key::Inline(InlineKey::ChunkRange {
table_id,
row_id_end: row_id,
})
.encode();
encoded[prefix.len()..].to_vec()
}
const INDEX_KIND_PREFIX_LEN: usize = 2;
const INDEX_ENTRY_PREFIX_LEN: usize = INDEX_KIND_PREFIX_LEN + size_of::<u64>();
pub(crate) fn index_entry_bytes(values: &[Option<IndexKeyValue>]) -> u64 {
let bytes = INDEX_ENTRY_PREFIX_LEN + nominal_key_bytes(values) + size_of::<u64>();
bytes as u64
}
#[allow(dead_code)]
#[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)
}
#[allow(dead_code)]
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) 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_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_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_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))
);
}
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, row_id_end)| {
Key::Inline(InlineKey::ChunkRange {
table_id,
row_id_end,
})
}),
arb_index().prop_map(Key::Index),
any::<u64>().prop_map(|snapshot_id| Key::Changelog { snapshot_id }),
]
}
proptest! {
#[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));
}
}
}
}