use std::collections::BTreeMap;
use proptest::prelude::*;
use tessarikv_types::{KeyspaceId, SequenceNumber, ValueKind};
use super::block_cache::BlockCache;
use super::iterator::DbIterator;
use super::read::{get_in, scan_in};
use super::read_tests::{block_on, cache_bytes, file, from, user_key};
use super::view::View;
use crate::ReadOptions;
use crate::memtable::Memtable;
#[derive(Debug, Clone)]
enum Write {
Put(u8, u16),
Delete(u8),
DeleteRange(u8, u8),
}
fn write() -> impl Strategy<Value = Write> {
prop_oneof![
3 => (0_u8..12, any::<u16>()).prop_map(|(key, value)| Write::Put(key, value)),
1 => (0_u8..12).prop_map(Write::Delete),
1 => (0_u8..12, 1_u8..6).prop_map(|(from, width)| Write::DeleteRange(from, from + width)),
]
}
fn fill(table: &Memtable, first: usize, writes: &[Write]) {
for (offset, write) in writes.iter().enumerate() {
let sequence = SequenceNumber::new(u64::try_from(first + offset).unwrap()).unwrap();
match write {
Write::Put(key, value) => table
.insert(
sequence,
ValueKind::Value,
&user_key(*key),
&value.to_le_bytes(),
)
.unwrap(),
Write::Delete(key) => table
.insert(sequence, ValueKind::Delete, &user_key(*key), b"")
.unwrap(),
Write::DeleteRange(from, to) => table
.insert(
sequence,
ValueKind::RangeDelete,
&user_key(*from),
&user_key(*to),
)
.unwrap(),
}
}
}
fn model(writes: &[Write], count: usize) -> BTreeMap<Vec<u8>, Vec<u8>> {
let mut state = BTreeMap::new();
for write in writes.iter().take(count) {
match write {
Write::Put(key, value) => {
state.insert(user_key(*key).to_vec(), value.to_le_bytes().to_vec());
}
Write::Delete(key) => {
state.remove(user_key(*key).as_slice());
}
Write::DeleteRange(from, to) => {
state.retain(|key: &Vec<u8>, _| {
key.as_slice() < user_key(*from).as_slice()
|| key.as_slice() >= user_key(*to).as_slice()
});
}
}
}
state
}
fn view(writes: &[Write], first: usize, second: usize, flushed: usize, capacity: u64) -> View {
let cache = std::sync::Arc::new(BlockCache::new(
capacity,
tessarikv_constants::DEFAULT_BLOCK_BYTES,
));
let mut view = View::new(0, Memtable::new());
fill(view.table(0, 0).unwrap(), 1, &writes[..first]);
view = view.with_next_table();
fill(view.table(0, 1).unwrap(), first + 1, &writes[first..second]);
view = view.with_next_table();
fill(view.table(0, 2).unwrap(), second + 1, &writes[second..]);
for epoch in 0..u8::try_from(flushed).unwrap() {
let table = file(view.table(0, epoch).unwrap(), u64::from(epoch) + 1, &cache);
view = view.after_flush(epoch, vec![table]).unwrap();
}
view
}
proptest! {
#[test]
fn range_deletes_hide_exactly_the_older_versions_in_their_range(
writes in proptest::collection::vec(write(), 3..40),
cuts in (any::<prop::sample::Index>(), any::<prop::sample::Index>()),
flushed in 0_usize..=2,
capacity in cache_bytes(),
) {
let first = 1 + cuts.0.index(writes.len() - 2);
let second = first + 1 + cuts.1.index(writes.len() - first - 1);
let view = view(&writes, first, second, flushed, capacity);
let default = KeyspaceId::DEFAULT;
for snapshot in 0..=writes.len() {
let at = SequenceNumber::new(u64::try_from(snapshot).unwrap()).unwrap();
let state = model(&writes, snapshot);
let listed: Vec<_> = state.clone().into_iter().collect();
for key in 0_u8..13 {
let key = user_key(key);
prop_assert_eq!(
block_on(get_in(&view, default, &key, at, true, None)).unwrap(),
state.get(key.as_slice()).cloned(),
"key {:?} at {}", key, snapshot
);
}
let page = block_on(scan_in(&view, default, from(b"", usize::MAX), at, true)).unwrap();
prop_assert_eq!(&page.entries, &listed);
let options = ReadOptions::default();
let mut iterator = DbIterator::new(&view, default, at, &options, None).unwrap();
let (forward, backward) = block_on(async {
let mut forward = Vec::new();
iterator.seek_to_first().await.unwrap();
while iterator.valid() && forward.len() <= listed.len() {
forward.push((iterator.key().to_vec(), iterator.value().to_vec()));
iterator.next().await.unwrap();
}
let mut backward = Vec::new();
iterator.seek_to_last().await.unwrap();
while iterator.valid() && backward.len() <= listed.len() {
backward.push((iterator.key().to_vec(), iterator.value().to_vec()));
iterator.prev().await.unwrap();
}
(forward, backward)
});
prop_assert_eq!(&forward, &listed);
prop_assert_eq!(backward, listed.iter().rev().cloned().collect::<Vec<_>>());
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_range_delete_survives_reopening_from_the_log_and_from_table_files() {
use std::path::Path;
use std::sync::Arc;
use super::Db;
use super::flush_tests::{PATH, key, until, value};
use crate::log::fault::FaultStorage;
use crate::storage::Storage as _;
use crate::{DbOptions, KeyspaceOptions, WriteBatch, WriteOptions};
let keyspace = KeyspaceOptions::builder()
.memtable_bytes(64 * 1024)
.build()
.unwrap();
let options = DbOptions::builder()
.default_keyspace(keyspace)
.build()
.unwrap();
let open = |storage: &FaultStorage| {
Db::open_with_storage(PATH, options.clone(), Arc::new(storage.clone()))
};
let put = |from: u32, to: u32| {
let mut batch = WriteBatch::new();
for index in from..to {
batch
.put(KeyspaceId::DEFAULT, &key(index), &value(index))
.unwrap();
}
batch
};
let check = |db: Db, later: bool| async move {
let later_keys = if later { 100..140 } else { 0..0 };
for index in (0..40).chain(later_keys) {
let read = db
.get(KeyspaceId::DEFAULT, &key(index), &ReadOptions::default())
.await
.unwrap();
let deleted = (5..15).contains(&index);
assert_eq!(read.is_none(), deleted, "key {index}");
}
db.close().await.unwrap();
};
let storage = FaultStorage::default();
let db = open(&storage).await.unwrap();
for start in (0..40).step_by(10) {
db.write(&put(start, start + 10), &WriteOptions::default())
.await
.unwrap();
}
let mut batch = WriteBatch::new();
batch
.delete_range(KeyspaceId::DEFAULT, &key(5), &key(15))
.unwrap();
let deleted_at = db.write(&batch, &WriteOptions::default()).await.unwrap();
for start in (100..140).step_by(10) {
db.write(&put(start, start + 10), &WriteOptions::default())
.await
.unwrap();
}
until("the range delete's table is flushed", || {
db.view
.load()
.files(0)
.any(|file| file.meta.max_sequence >= deleted_at.get())
})
.await;
db.close().await.unwrap();
check(open(&storage).await.unwrap(), true).await;
for name in storage.list(Path::new(PATH)).unwrap() {
if name.ends_with(".log") {
storage.remove(&Path::new(PATH).join(name)).unwrap();
}
}
check(open(&storage).await.unwrap(), false).await;
}