use std::mem::size_of;
use std::ops::Bound;
use armour_core::{CollectionShape, StorageBackend, StorageClass, Typ, ValueModel};
use crate::access::{Access, FallibleRowFn, KeyFn, RowFn, Wire};
use crate::armour::inspection::ScanCapability;
use crate::codec::{Codec, InspectionEncoding};
use crate::error::{DbError, DbResult};
use crate::key::Key;
use crate::lsm::{LsmShard, LsmTree, map_fjall};
use crate::{CollectionMeta, TypedWriteHook};
impl<K, T, C, H> LsmTree<K, T, C, H>
where
K: Key + Ord + 'static,
T: CollectionMeta<SelfId = K> + Send + Sync + 'static,
C: Codec<T> + Clone + Send + Sync + 'static,
H: TypedWriteHook<K, T> + 'static,
{
fn walk_rows(&self, warn: bool, f: FallibleRowFn<'_, K, T>) {
let codec = self.codec();
for guard in self.keyspace().iter() {
let (raw_key, raw_value) = match map_fjall(guard.into_inner()) {
Ok(pair) => pair,
Err(error) => {
if warn {
tracing::warn!(
collection = T::NAME,
error = %error,
"schema scan: fjall iter error in LsmTree, skipping entry — relations not checked"
);
}
if f(Err(error)).is_break() {
break;
}
continue;
}
};
if raw_key.len() != size_of::<K>() {
if warn {
tracing::warn!(
collection = T::NAME,
len = raw_key.len(),
"schema scan: bad key length in LsmTree, skipping entry — relations not checked"
);
}
if f(Err(DbError::CorruptedEntry { offset: 0 })).is_break() {
break;
}
continue;
}
let key = K::from_bytes(raw_key.as_ref());
let value = match codec.decode_from(raw_value.as_ref()) {
Ok(value) => value,
Err(error) => {
if warn {
tracing::warn!(
collection = T::NAME,
"schema scan: decode error in LsmTree, skipping entry — relations not checked"
);
}
if f(Err(error)).is_break() {
break;
}
continue;
}
};
if f(Ok((&key, &value))).is_break() {
break;
}
}
}
}
impl<K, T, C, H> Access for LsmTree<K, T, C, H>
where
K: Key + Ord + 'static,
T: CollectionMeta<SelfId = K> + Send + Sync + 'static,
C: Codec<T> + Clone + Send + Sync + 'static,
H: TypedWriteHook<K, T> + 'static,
{
type Key = K;
type Val = T;
type Shard<'a>
= LsmShard<'a, K, T, C, H>
where
Self: 'a;
const STORAGE: StorageClass = StorageClass {
model: ValueModel::Lsm,
shape: CollectionShape::Tree,
backend: StorageBackend::Lsm,
};
const RAW_WIRE_BYTES: bool = true;
fn encoding(&self, _ty: &Typ) -> InspectionEncoding {
C::INSPECTION_ENCODING
}
fn approx_len(&self) -> u64 {
self.len() as u64
}
fn rpc_exact_count(&self) -> DbResult<u64> {
Ok(map_fjall(self.keyspace().len())? as u64)
}
fn inspection_exact_count(&self) -> DbResult<u64> {
Ok(map_fjall(self.keyspace().len())? as u64)
}
fn scan_capability(&self) -> ScanCapability {
ScanCapability::Ordered
}
fn reversed(&self) -> bool {
self.config().reversed
}
fn with_value<R>(&self, key: &K, f: impl FnOnce(&T) -> R) -> DbResult<Option<R>> {
Ok(self.try_get(key)?.map(|value| f(&value)))
}
fn contains(&self, key: &K) -> DbResult<bool> {
Ok(LsmTree::contains(self, key))
}
fn stored_bytes(&self, key: &K) -> DbResult<Option<Vec<u8>>> {
Ok(self.get_raw(key)?.map(|slice| slice.to_vec()))
}
fn entry_len<W: Wire<T>>(&self, key: &K, _wire: &W) -> DbResult<Option<u32>> {
Ok(self.entry_len(key))
}
fn with_first<R>(&self, f: impl FnOnce(&K, &T) -> R) -> DbResult<Option<R>> {
Ok(self.first().map(|(key, value)| f(&key, &value)))
}
fn with_last<R>(&self, f: impl FnOnce(&K, &T) -> R) -> DbResult<Option<R>> {
Ok(self.last().map(|(key, value)| f(&key, &value)))
}
fn scan_range(&self, start: Bound<&K>, end: Bound<&K>, f: RowFn<'_, K, T>) -> DbResult<()> {
for (key, value) in self.range_bounds(start, end) {
if f(&key, &value)?.is_break() {
break;
}
}
Ok(())
}
fn try_scan_range(
&self,
start: Bound<&K>,
end: Bound<&K>,
_batch: usize,
f: RowFn<'_, K, T>,
) -> DbResult<()> {
for row in self.try_range_bounds(start, end) {
let (key, value) = row?;
if f(&key, &value)?.is_break() {
break;
}
}
Ok(())
}
fn scan_keys(&self, start: Bound<&K>, end: Bound<&K>, f: KeyFn<'_, K>) -> DbResult<()> {
for (key, _) in self.range_bounds(start, end) {
if f(&key)?.is_break() {
break;
}
}
Ok(())
}
fn scan_all(&self, f: FallibleRowFn<'_, K, T>) {
self.walk_rows(true, f);
}
fn undecodable_rows(&self) -> u64 {
let mut n = 0u64;
self.walk_rows(false, &mut |row| {
if row.is_err() {
n += 1;
}
std::ops::ControlFlow::Continue(())
});
n
}
fn put(&self, key: &K, value: T) -> DbResult<()> {
self.put(key, &value)
}
fn put_ref(&self, key: &K, value: &T) -> DbResult<()> {
self.put(key, value)
}
fn delete(&self, key: &K) -> DbResult<()> {
self.delete(key).map(drop)
}
fn atomic_at<R>(
&self,
key: &K,
f: impl FnOnce(&mut Self::Shard<'_>) -> DbResult<R>,
) -> DbResult<R> {
self.atomic(key, f)
}
}
#[cfg(test)]
mod tests {
use std::ops::{Bound, ControlFlow};
use armour_core::{Fuid, GetRefs, GetType};
use rapira::Rapira;
use crate::access::{Access, CodecWire};
use crate::codec::Codec;
use crate::held_ops::HeldOps;
use crate::lsm::{LsmConfig, LsmTree};
use crate::{CollectionMeta, Key, NoHook, RapiraCodec};
armour_core::const_hasher_or!(
access_lsm_id,
"ACCESS_LSM",
"dev-key-access-lsm",
brand = "ACCESS_LSM"
);
type RowId = Fuid<access_lsm_id::Hasher>;
#[derive(Clone, Debug, PartialEq, GetType, GetRefs, Rapira)]
struct Row {
value: u64,
}
impl CollectionMeta for Row {
type SelfId = RowId;
const NAME: &'static str = "access_lsm_rows";
}
fn key(value: u64) -> RowId {
RowId::from_be_bytes(value.to_be_bytes())
}
fn tree() -> (tempfile::TempDir, LsmTree<RowId, Row, RapiraCodec, NoHook>) {
let dir = tempfile::tempdir().expect("tmp");
let db = fjall::Database::builder(dir.path())
.open()
.expect("fjall db");
let tree = LsmTree::open(db, LsmConfig::default(), RapiraCodec, NoHook).expect("lsm tree");
(dir, tree)
}
#[test]
fn lsm_access_stored_bytes_and_native_entry_len() {
let (_dir, tree) = tree();
let first = Row { value: 11 };
let second = Row { value: 22 };
tree.put(&key(1), &first).expect("put first");
tree.put(&key(2), &second).expect("put second");
let raw = RapiraCodec.encode(&first).expect("encode");
assert!(<LsmTree<RowId, Row, RapiraCodec, NoHook> as Access>::RAW_WIRE_BYTES);
assert_eq!(
Access::stored_bytes(&tree, &key(1)).expect("stored bytes"),
Some(raw.clone())
);
assert_eq!(
Access::entry_len(&tree, &key(1), &CodecWire(RapiraCodec)).expect("entry len"),
Some(raw.len() as u32)
);
assert_eq!(Access::rpc_exact_count(&tree).expect("RPC count"), 2);
assert_eq!(
Access::inspection_exact_count(&tree).expect("inspection count"),
2
);
assert_eq!(Access::approx_len(&tree), 2);
Access::atomic_at(&tree, &key(3), |shard| {
HeldOps::put(shard, &key(3), Row { value: 33 })?;
assert!(HeldOps::try_contains(shard, &key(3))?);
assert_eq!(
HeldOps::with_value(shard, &key(3), |value| value.value)?,
Some(33)
);
HeldOps::delete(shard, &key(3))
})
.expect("held operations");
assert!(!tree.contains(&key(3)));
}
#[test]
fn lsm_scan_all_reports_undecodable_rows() {
let (_dir, tree) = tree();
tree.put(&key(1), &Row { value: 11 }).expect("put good row");
tree.keyspace()
.insert(key(2).as_bytes(), b"corrupt")
.expect("inject corrupt row");
let mut good = 0;
let mut bad = 0;
Access::scan_all(&tree, &mut |row| {
match row {
Ok((_key, value)) => {
assert_eq!(value.value, 11);
good += 1;
}
Err(_) => bad += 1,
}
ControlFlow::Continue(())
});
assert_eq!((good, bad), (1, 1));
let mut lenient = 0;
Access::scan_range(
&tree,
Bound::Unbounded,
Bound::Unbounded,
&mut |_key, _value| {
lenient += 1;
Ok(ControlFlow::Continue(()))
},
)
.expect("lenient range scan");
assert_eq!(lenient, 1);
assert_eq!(Access::undecodable_rows(&tree), 1);
}
#[test]
fn lsm_facade_contains_is_lenient_while_held_shard_is_strict() {
let (_dir, tree) = tree();
tree.put(&key(1), &Row { value: 11 }).expect("put");
assert!(Access::contains(&tree, &key(1)).expect("facade contains"));
assert!(!Access::contains(&tree, &key(2)).expect("facade contains"));
assert_eq!(
Access::contains(&tree, &key(1)).ok(),
Some(LsmTree::contains(&tree, &key(1))),
"facade must answer from the lenient handler method"
);
Access::atomic_at(&tree, &key(1), |shard| {
assert!(HeldOps::try_contains(shard, &key(1))?);
assert!(!HeldOps::try_contains(shard, &key(2))?);
Ok(())
})
.expect("held shard");
}
}