use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::{Arc, LazyLock};
use proptest::prelude::*;
use tessarikv_format::{Compression, TableMeta, TableOptions};
use tessarikv_types::{KeyspaceId, SequenceNumber, ValueKind};
use super::block_cache::BlockCache;
use super::read::{ScanRange, get_in, scan_in};
use super::table_blocks::open_table;
use super::tables::build;
use super::view::{TableFile, View};
use crate::Error;
use crate::memtable::Memtable;
use crate::storage::{MemoryStorage, Storage};
pub(super) type Op = (u8, Option<u16>);
pub(super) fn user_key(key: u8) -> [u8; 2] {
[b'k', key]
}
fn options() -> TableOptions {
TableOptions {
block_bytes: 64,
restart_interval: 2,
bloom_bits_per_key: 10,
compression: Compression::None,
}
}
fn fill(table: &Memtable, first: usize, ops: &[Op]) {
for (offset, (key, value)) in ops.iter().enumerate() {
let sequence = SequenceNumber::new(u64::try_from(first + offset).unwrap()).unwrap();
match value {
Some(value) => table
.insert(
sequence,
ValueKind::Value,
&user_key(*key),
&value.to_le_bytes(),
)
.unwrap(),
None => table
.insert(sequence, ValueKind::Delete, &user_key(*key), b"")
.unwrap(),
}
}
}
pub(super) fn block_on<T>(future: impl Future<Output = T>) -> T {
static RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.build()
.unwrap()
});
RUNTIME.block_on(future)
}
pub(super) fn cache_bytes() -> impl Strategy<Value = u64> {
prop_oneof![Just(0), Just(300), Just(1 << 20)]
}
pub(super) fn table_file(
bytes: Vec<u8>,
built: &super::tables::BuiltTable,
id: u64,
cache: &Arc<BlockCache>,
) -> TableFile {
table_file_in(&MemoryStorage::new(), bytes, built, id, cache)
}
pub(super) fn table_file_in(
storage: &dyn Storage,
bytes: Vec<u8>,
built: &super::tables::BuiltTable,
id: u64,
cache: &Arc<BlockCache>,
) -> TableFile {
let meta = TableMeta {
keyspace: 0,
level: 0,
id,
size: u64::try_from(bytes.len()).unwrap(),
smallest: built.smallest.clone(),
largest: built.largest.clone(),
min_sequence: built.min_sequence,
max_sequence: built.max_sequence,
};
let path = PathBuf::from(format!("tables/{id}.sst"));
let mut written = storage.create(&path).unwrap();
written.append(&bytes).unwrap();
drop(written);
let (table, blocks) = open_table(storage, &path, id, cache, None).unwrap();
TableFile {
meta: Arc::new(meta),
table,
blocks,
}
}
pub(super) fn built(
table: &Memtable,
options: tessarikv_format::TableOptions,
) -> (super::tables::BuiltTable, Vec<u8>) {
let mut bytes = Vec::new();
let built = build(table, options, None, &mut |piece| {
bytes.extend_from_slice(piece);
Ok(())
})
.unwrap();
(built, bytes)
}
pub(super) fn file(table: &Memtable, id: u64, cache: &Arc<BlockCache>) -> TableFile {
file_in(&MemoryStorage::new(), table, id, cache)
}
pub(super) fn file_in(
storage: &dyn Storage,
table: &Memtable,
id: u64,
cache: &Arc<BlockCache>,
) -> TableFile {
let (built, bytes) = built(table, options());
table_file_in(storage, bytes, &built, id, cache)
}
pub(super) fn from(start: &[u8], limit: usize) -> ScanRange<'_> {
ScanRange {
start,
end: None,
limit,
}
}
pub(super) fn model(ops: &[Op], count: usize) -> BTreeMap<Vec<u8>, Vec<u8>> {
let mut state = BTreeMap::new();
for (key, value) in ops.iter().take(count) {
match value {
Some(value) => state.insert(user_key(*key).to_vec(), value.to_le_bytes().to_vec()),
None => state.remove(user_key(*key).as_slice()),
};
}
state
}
pub(super) fn unflushed(ops: &[Op], first: usize, second: usize) -> View {
let view = View::new(0, Memtable::new());
fill(view.table(0, 0).unwrap(), 1, &ops[..first]);
let view = view.with_next_table();
fill(view.table(0, 1).unwrap(), first + 1, &ops[first..second]);
let view = view.with_next_table();
fill(view.table(0, 2).unwrap(), second + 1, &ops[second..]);
view
}
proptest! {
#[test]
fn reads_through_table_files_answer_as_the_writes_say(
ops in proptest::collection::vec((0_u8..12, proptest::option::of(any::<u16>())), 3..60),
cuts in (any::<prop::sample::Index>(), any::<prop::sample::Index>()),
capacity in cache_bytes(),
) {
let cache = Arc::new(BlockCache::new(capacity, tessarikv_constants::DEFAULT_BLOCK_BYTES));
let first = 1 + cuts.0.index(ops.len() - 2);
let second = first + 1 + cuts.1.index(ops.len() - first - 1);
let before = unflushed(&ops, first, second);
let oldest = file(before.table(0, 0).unwrap(), 1, &cache);
let older = file(before.table(0, 1).unwrap(), 2, &cache);
let after = before.after_flush(0, vec![oldest]).unwrap().after_flush(1, vec![older]).unwrap();
prop_assert_eq!(after.memtables(0).count(), 1);
prop_assert_eq!(after.files(0).count(), 2);
let default = KeyspaceId::DEFAULT;
for snapshot in 0..=ops.len() {
let at = SequenceNumber::new(u64::try_from(snapshot).unwrap()).unwrap();
let expected = model(&ops, snapshot);
for key in 0_u8..13 {
let key = user_key(key);
prop_assert_eq!(
block_on(get_in(&after, default, &key, at, true, None)).unwrap(),
expected.get(key.as_slice()).cloned(),
"key {:?} at {}", key, snapshot
);
}
let listed: Vec<_> = expected.into_iter().collect();
let whole = block_on(scan_in(&after, default, from(b"", usize::MAX), at, true)).unwrap();
prop_assert_eq!(&whole.entries, &listed);
let mut paged = Vec::new();
let mut start = Vec::new();
loop {
let page = block_on(scan_in(&after, default, from(&start, 3), at, true)).unwrap();
paged.extend(page.entries);
match page.next {
Some(next) => start = next,
None => break,
}
}
prop_assert_eq!(paged, listed);
}
}
}
#[tokio::test]
async fn a_damaged_table_file_fails_the_scan_instead_of_shortening_it() {
let table = Memtable::new();
let ops: Vec<Op> = (0..12).map(|key| (key, Some(u16::from(key)))).collect();
fill(&table, 1, &ops);
let (built, mut bytes) = built(&table, options());
bytes[1] ^= 0xff;
let view = View::new(0, Memtable::new())
.with_next_table()
.after_flush(
0,
vec![table_file(
bytes,
&built,
1,
&Arc::new(BlockCache::new(0, tessarikv_constants::DEFAULT_BLOCK_BYTES)),
)],
)
.unwrap();
let at = SequenceNumber::new(100).unwrap();
let scanned = scan_in(&view, KeyspaceId::DEFAULT, from(b"", usize::MAX), at, true).await;
assert!(
matches!(scanned, Err(Error::Corruption { what: "table", .. })),
"{scanned:?}"
);
}