use std::mem::size_of;
use std::num::NonZeroU32;
use std::ops::{Bound, ControlFlow};
use std::sync::Arc;
#[cfg(feature = "rpc")]
use armour_rpc::protocol::WriteKey;
use armour_core::{KeyScheme, Typ};
#[cfg(feature = "rpc")]
use crate::access::HeldOps;
use crate::access::{Access, Wire};
#[cfg(feature = "rpc")]
use crate::armour::handler::{
KeyBytes, RangeCollector, ValueBytes, bound_to_key_bound, check_key_len, resolve_key,
};
use crate::armour::inspection::{
InspectionCollection, InspectionEntry, InspectionError, InspectionHandler, InspectionOperation,
InspectionPage, InspectionSchema, IterationDirection, NormalizedBounds, PageAccumulator,
ScanCapability, SnapshotAccumulator,
};
use crate::armour::seq::SeqGen;
use crate::error::{DbError, DbResult};
use crate::key::Key;
use crate::map_index::inspection_batch;
use crate::tree_meta::TreeMeta;
fn iteration_direction(reversed: bool) -> IterationDirection {
if reversed {
IterationDirection::Descending
} else {
IterationDirection::Ascending
}
}
fn bound_to_key_bound_owned<K: Key>(bound: &Bound<Vec<u8>>) -> DbResult<Bound<K>> {
match bound {
Bound::Included(b) => Ok(Bound::Included(K::from_bytes(b))),
Bound::Excluded(b) => Ok(Bound::Excluded(K::from_bytes(b))),
Bound::Unbounded => Ok(Bound::Unbounded),
}
}
pub(crate) struct Adapter<S, W> {
pub(crate) name: String,
pub(crate) typ_hash: u64,
pub(crate) ty: Typ,
pub(crate) key_scheme: KeyScheme,
pub(crate) version: u16,
pub(crate) access: Arc<S>,
pub(crate) wire: W,
pub(crate) seq: Arc<SeqGen>,
}
impl<S, W> Adapter<S, W> {
pub(crate) fn new(meta: &TreeMeta, access: Arc<S>, wire: W, seq: Arc<SeqGen>) -> Self {
Self {
name: meta.name.to_owned(),
typ_hash: meta.ty.h(),
ty: meta.ty,
key_scheme: meta.key_scheme,
version: meta.version,
access,
wire,
seq,
}
}
}
impl<S, W> InspectionHandler for Adapter<S, W>
where
S: Access,
W: Wire<S::Val>,
{
fn metadata(&self) -> Result<InspectionCollection, InspectionError> {
let scan = self.access.scan_capability();
Ok(InspectionCollection {
name: self.name.clone(),
schema: InspectionSchema {
version: self.version,
typ: self.ty,
key_scheme: self.key_scheme,
key_size: size_of::<S::Key>() as u32,
encoding: self.access.encoding(&self.ty),
},
storage: S::STORAGE,
count: Some(self.access.approx_len()),
scan,
direction: (scan == ScanCapability::Ordered)
.then(|| iteration_direction(self.access.reversed())),
})
}
fn get_raw(&self, key: &[u8]) -> DbResult<Option<Vec<u8>>> {
let key = S::Key::from_bytes(key);
if S::RAW_WIRE_BYTES && W::PASSTHROUGH {
return match self.access.stored_bytes(&key)? {
Some(raw) => {
self.wire.validate_stored(&raw)?;
Ok(Some(raw))
}
None => Ok(None),
};
}
self.access
.with_value(&key, |value| self.wire.encode(value))?
.transpose()
}
fn exact_count(&self) -> DbResult<u64> {
self.access.inspection_exact_count()
}
fn ordered_page(
&self,
bounds: &NormalizedBounds,
limit: NonZeroU32,
) -> DbResult<InspectionPage> {
if self.access.scan_capability() != ScanCapability::Ordered {
return Err(DbError::NotImplemented);
}
if bounds.empty {
return Ok(InspectionPage {
entries: vec![],
next_key: None,
});
}
let start = bound_to_key_bound_owned::<S::Key>(&bounds.start)?;
let end = bound_to_key_bound_owned::<S::Key>(&bounds.end)?;
let batch = inspection_batch(limit, self.access.inspection_shard_count());
let wire = &self.wire;
let mut page = PageAccumulator::new(limit);
self.access
.try_scan_range(start.as_ref(), end.as_ref(), batch, &mut |key, value| {
page.push_with(|| {
Ok(InspectionEntry {
key: key.as_bytes().to_vec(),
value: wire.encode(value)?,
})
})
})?;
Ok(page.finish())
}
fn unordered_snapshot(
&self,
max_entries: NonZeroU32,
) -> Result<Vec<InspectionEntry>, InspectionError> {
if self.access.scan_capability() != ScanCapability::UnorderedSnapshot {
return Err(InspectionError::UnsupportedOperation {
name: self.name.clone(),
operation: InspectionOperation::SnapshotUnordered,
});
}
let observed = self.access.approx_len();
let cap = max_entries.get();
if observed > u64::from(cap) {
return Err(InspectionError::SnapshotTooLarge {
limit: cap,
observed: Some(observed),
});
}
let mut acc = SnapshotAccumulator::new(max_entries);
let mut failure = None;
self.access.scan_all(&mut |row| {
let (key, value) = match row {
Ok(pair) => pair,
Err(error) => {
failure = Some(error);
return ControlFlow::Break(());
}
};
match self.wire.encode(value) {
Ok(bytes) => acc.push(InspectionEntry {
key: key.as_bytes().to_vec(),
value: bytes,
}),
Err(error) => {
failure = Some(error);
ControlFlow::Break(())
}
}
});
if let Some(error) = failure {
return Err(InspectionError::Database(error));
}
acc.finish()
}
}
#[cfg(feature = "rpc")]
impl<S, W> crate::armour::handler::RpcHandler for Adapter<S, W>
where
S: Access,
W: Wire<S::Val>,
{
fn name(&self) -> &str {
&self.name
}
fn info(&self) -> (u64, u16) {
(self.typ_hash, self.version)
}
fn schema(&self) -> armour_rpc::SchemaResponse {
armour_rpc::SchemaResponse {
name: self.name.clone(),
version: self.version,
typ: self.ty,
key_scheme: self.key_scheme,
}
}
fn scannable(&self) -> bool {
self.access.scan_capability() == ScanCapability::Ordered
}
fn get(&self, key: &[u8]) -> DbResult<Option<ValueBytes>> {
let key: S::Key = check_key_len(key)?;
if S::RAW_WIRE_BYTES && W::PASSTHROUGH {
return match self.access.stored_bytes(&key)? {
Some(raw) => {
self.wire.validate_stored(&raw)?;
Ok(Some(raw))
}
None => Ok(None),
};
}
self.access
.with_value(&key, |value| self.wire.encode(value))?
.transpose()
}
fn contains(&self, key: &[u8]) -> DbResult<bool> {
let key: S::Key = check_key_len(key)?;
self.access.contains(&key)
}
fn entry_len(&self, key: &[u8]) -> DbResult<Option<u32>> {
let key: S::Key = check_key_len(key)?;
self.access.entry_len(&key, &self.wire)
}
fn first(&self) -> DbResult<Option<(KeyBytes, ValueBytes)>> {
self.access
.with_first(|key, value| Ok((key.as_bytes().to_vec(), self.wire.encode(value)?)))?
.transpose()
}
fn last(&self) -> DbResult<Option<(KeyBytes, ValueBytes)>> {
self.access
.with_last(|key, value| Ok((key.as_bytes().to_vec(), self.wire.encode(value)?)))?
.transpose()
}
fn range(
&self,
start: Bound<KeyBytes>,
end: Bound<KeyBytes>,
limit: u32,
) -> DbResult<Vec<(KeyBytes, ValueBytes)>> {
if self.access.scan_capability() != ScanCapability::Ordered {
return Err(DbError::NotImplemented);
}
let start = bound_to_key_bound::<S::Key>(&start)?;
let end = bound_to_key_bound::<S::Key>(&end)?;
let wire = &self.wire;
let mut out = RangeCollector::new(limit);
self.access
.scan_range(start.as_ref(), end.as_ref(), &mut |key, value| {
out.push_with(|| Ok((key.as_bytes().to_vec(), wire.encode(value)?)))
})?;
Ok(out.rows())
}
fn range_keys(
&self,
start: Bound<KeyBytes>,
end: Bound<KeyBytes>,
limit: u32,
) -> DbResult<Vec<ValueBytes>> {
if self.access.scan_capability() != ScanCapability::Ordered {
return Err(DbError::NotImplemented);
}
let start = bound_to_key_bound::<S::Key>(&start)?;
let end = bound_to_key_bound::<S::Key>(&end)?;
let mut out = RangeCollector::new(limit);
self.access
.scan_keys(start.as_ref(), end.as_ref(), &mut |key| {
Ok(out.push_key(key.as_bytes().to_vec()))
})?;
Ok(out.keys())
}
fn write(&self, key: WriteKey, flag: Option<bool>, value: ValueBytes) -> DbResult<ValueBytes> {
let (typed_key, key_bytes) = resolve_key::<S::Key>(key, &self.seq, &self.name)?;
let decoded = self.wire.decode(&value)?;
match flag {
None => {
self.access.put(&typed_key, decoded)?;
Ok(key_bytes)
}
Some(false) => self.access.atomic_at(&typed_key, |shard| {
if shard.try_contains(&typed_key)? {
return Err(DbError::KeyExists);
}
shard.put(&typed_key, decoded)?;
Ok(key_bytes)
}),
Some(true) => self.access.atomic_at(&typed_key, |shard| {
if !shard.try_contains(&typed_key)? {
return Err(DbError::KeyNotFound);
}
shard.put(&typed_key, decoded)?;
Ok(key_bytes)
}),
}
}
fn remove(&self, key: &[u8], soft: bool) -> DbResult<()> {
if soft {
return Err(DbError::NotImplemented);
}
let key: S::Key = check_key_len(key)?;
self.access.delete(&key)
}
fn take(&self, key: &[u8], soft: bool) -> DbResult<Option<ValueBytes>> {
if soft {
return Err(DbError::NotImplemented);
}
let key: S::Key = check_key_len(key)?;
let wire = &self.wire;
self.access.atomic_at(&key, |shard| {
let Some(encoded) = shard.with_value(&key, |value| wire.encode(value))? else {
return Ok(None);
};
let encoded = encoded?;
shard.delete(&key)?;
Ok(Some(encoded))
})
}
fn count(&self, exact: bool) -> DbResult<u64> {
if exact {
self.access.rpc_exact_count()
} else {
Ok(self.access.approx_len())
}
}
fn apply_batch(&self, items: Vec<(KeyBytes, Option<ValueBytes>)>) -> DbResult<()> {
let decoded: Vec<(S::Key, Option<S::Val>)> = items
.into_iter()
.map(|(key, value)| {
if key.len() != size_of::<S::Key>() {
return Err(DbError::Client("invalid key length in batch"));
}
let value = value
.map(|bytes| self.wire.decode_batch_item(&bytes))
.transpose()?;
Ok((S::Key::from_bytes(&key), value))
})
.collect::<DbResult<_>>()?;
for (key, value) in decoded {
match value {
Some(value) => self.access.put(&key, value)?,
None => self.access.delete(&key)?,
}
}
Ok(())
}
}
#[cfg(all(test, feature = "typed-tree", feature = "rapira-codec"))]
mod tests {
use std::num::NonZeroU32;
use std::ops::Bound;
use armour_core::{CollectionShape, GetType, StorageBackend, StorageClass, ValueModel};
use rapira::Rapira;
use crate::access::{Adapter, CodecWire};
use crate::armour::Db;
use crate::armour::inspection::InspectionError;
use crate::armour::inspection::{
InspectionHandler, InspectionOperation, IterationDirection, ScanCapability, normalize_range,
};
use crate::tree_meta::TreeMeta;
use crate::{CollectionMeta, Config, NoHook, RapiraCodec};
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct AdapterRow {
n: u64,
}
impl CollectionMeta for AdapterRow {
type SelfId = [u8; 8];
const NAME: &'static str = "adapter_typed_tree";
const VERSION: u16 = 7;
}
fn key(id: u64) -> [u8; 8] {
id.to_be_bytes()
}
#[test]
fn adapter_reports_metadata_reads_and_pages_for_a_typed_tree() {
let dir = tempfile::tempdir().expect("tmp");
let db = Db::open_test(dir.path()).expect("db");
let cfg = Config::balanced()
.shard_count(2)
.hints(true)
.reversed(false)
.build();
let tree = db
.open_typed_tree::<AdapterRow, RapiraCodec, _>(cfg, NoHook, &[])
.expect("tree");
for id in 1..=5 {
tree.put(&key(id), AdapterRow { n: id }).expect("seed");
}
let adapter = Adapter::new(
&TreeMeta::of::<AdapterRow>(),
tree.clone(),
CodecWire(RapiraCodec),
db.seq.clone(),
);
let meta = adapter.metadata().expect("metadata");
assert_eq!(meta.name, AdapterRow::NAME);
assert_eq!(meta.schema.version, 7);
assert_eq!(meta.schema.key_size, 8);
assert_eq!(
meta.storage,
StorageClass {
model: ValueModel::Typed,
shape: CollectionShape::Tree,
backend: StorageBackend::Bitcask,
}
);
assert_eq!(meta.count, Some(5));
assert_eq!(meta.scan, ScanCapability::Ordered);
assert_eq!(meta.direction, Some(IterationDirection::Ascending));
let raw = adapter.get_raw(&key(3)).expect("get_raw").expect("present");
assert_eq!(raw, rapira::serialize(&AdapterRow { n: 3 }));
assert!(adapter.get_raw(&key(99)).expect("get_raw").is_none());
assert_eq!(adapter.exact_count().expect("exact count"), 5);
let bounds = normalize_range(
8,
IterationDirection::Ascending,
Bound::Unbounded,
Bound::Unbounded,
None,
)
.expect("bounds");
let page = adapter
.ordered_page(&bounds, NonZeroU32::new(2).unwrap())
.expect("page");
assert_eq!(
page.entries
.iter()
.map(|e| e.key.clone())
.collect::<Vec<_>>(),
vec![key(1).to_vec(), key(2).to_vec()]
);
assert_eq!(page.next_key, Some(key(2).to_vec()));
let bounds = normalize_range(
8,
IterationDirection::Ascending,
Bound::Unbounded,
Bound::Unbounded,
Some(&key(3)),
)
.expect("bounds");
let page = adapter
.ordered_page(&bounds, NonZeroU32::new(10).unwrap())
.expect("page");
assert_eq!(page.entries.len(), 2);
assert!(page.next_key.is_none());
let err = adapter
.unordered_snapshot(NonZeroU32::new(10).unwrap())
.unwrap_err();
assert!(matches!(
err,
InspectionError::UnsupportedOperation {
operation: InspectionOperation::SnapshotUnordered,
..
}
));
}
#[cfg(feature = "rpc")]
#[test]
fn adapter_rpc_read_write_count_and_batch_on_a_typed_tree() {
use armour_rpc::protocol::WriteKey;
use std::ops::Bound;
use crate::armour::handler::RpcHandler;
let dir = tempfile::tempdir().expect("tmp");
let db = Db::open_test(dir.path()).expect("db");
let cfg = Config::balanced()
.shard_count(2)
.hints(true)
.reversed(false)
.build();
let tree = db
.open_typed_tree::<AdapterRow, RapiraCodec, _>(cfg, NoHook, &[])
.expect("tree");
for id in 1..=5 {
tree.put(&key(id), AdapterRow { n: id }).expect("seed");
}
let adapter = Adapter::new(
&TreeMeta::of::<AdapterRow>(),
tree.clone(),
CodecWire(RapiraCodec),
db.seq.clone(),
);
assert_eq!(adapter.name(), AdapterRow::NAME);
assert_eq!(adapter.info(), (AdapterRow::TYPE.h(), 7));
assert!(adapter.scannable());
let encoded = rapira::serialize(&AdapterRow { n: 3 });
assert_eq!(adapter.get(&key(3)).expect("get"), Some(encoded.clone()));
assert!(adapter.get(&key(99)).expect("get").is_none());
assert!(adapter.contains(&key(3)).expect("contains"));
assert_eq!(
adapter.entry_len(&key(3)).expect("entry_len"),
Some(encoded.len() as u32)
);
assert!(matches!(
adapter.get(&[0u8; 4]),
Err(crate::DbError::KeyNotFound)
));
assert_eq!(
adapter.first().expect("first").map(|(k, _)| k),
Some(key(1).to_vec())
);
assert_eq!(
adapter.last().expect("last").map(|(k, _)| k),
Some(key(5).to_vec())
);
let all = adapter
.range(Bound::Unbounded, Bound::Unbounded, 0)
.expect("range");
assert_eq!(all.len(), 5);
let two = adapter
.range(Bound::Unbounded, Bound::Unbounded, 2)
.expect("range");
assert_eq!(
two.iter().map(|(k, _)| k.clone()).collect::<Vec<_>>(),
vec![key(1).to_vec(), key(2).to_vec()]
);
let keys = adapter
.range_keys(Bound::Unbounded, Bound::Unbounded, 3)
.expect("range_keys");
assert_eq!(keys.len(), 3);
let fresh = rapira::serialize(&AdapterRow { n: 42 });
assert!(matches!(
adapter.write(
WriteKey::Provided(key(3).to_vec()),
Some(false),
fresh.clone()
),
Err(crate::DbError::KeyExists)
));
assert!(matches!(
adapter.write(
WriteKey::Provided(key(90).to_vec()),
Some(true),
fresh.clone()
),
Err(crate::DbError::KeyNotFound)
));
assert!(!tree.contains(&key(90)));
adapter
.write(WriteKey::Provided(key(90).to_vec()), None, fresh)
.expect("unconditional write");
assert!(tree.contains(&key(90)));
assert_eq!(
adapter.take(&key(90), false).expect("take"),
Some(rapira::serialize(&AdapterRow { n: 42 }))
);
assert!(!tree.contains(&key(90)));
assert!(
adapter
.take(&key(90), false)
.expect("take absent")
.is_none()
);
assert!(matches!(
adapter.remove(&key(1), true),
Err(crate::DbError::NotImplemented)
));
assert!(matches!(
adapter.take(&key(1), true),
Err(crate::DbError::NotImplemented)
));
assert_eq!(adapter.count(false).expect("cheap count"), 5);
assert_eq!(adapter.count(true).expect("exact count"), 5);
let before = tree.len();
let err = adapter
.apply_batch(vec![
(
key(7).to_vec(),
Some(rapira::serialize(&AdapterRow { n: 7 })),
),
(key(8).to_vec(), Some(vec![0xff, 0xfe])),
])
.unwrap_err();
assert!(matches!(
err,
crate::DbError::Client("invalid value bytes in batch")
));
assert_eq!(tree.len(), before, "a failed batch must write nothing");
adapter
.apply_batch(vec![
(
key(7).to_vec(),
Some(rapira::serialize(&AdapterRow { n: 7 })),
),
(key(1).to_vec(), None),
])
.expect("good batch");
assert!(tree.contains(&key(7)));
assert!(!tree.contains(&key(1)));
}
#[cfg(feature = "rpc")]
#[test]
fn write_insert_only_uses_single_held_section() {
use armour_rpc::protocol::WriteKey;
use crate::armour::handler::RpcHandler;
let dir = tempfile::tempdir().expect("tmp");
let db = Db::open_test(dir.path()).expect("db");
let cfg = Config::balanced()
.shard_count(2)
.hints(true)
.reversed(false)
.build();
let tree = db
.open_typed_tree::<AdapterRow, RapiraCodec, _>(cfg, NoHook, &[])
.expect("tree");
tree.put(&key(1), AdapterRow { n: 1 }).expect("seed");
let adapter = Adapter::new(
&TreeMeta::of::<AdapterRow>(),
tree.clone(),
CodecWire(RapiraCodec),
db.seq.clone(),
);
let fresh = rapira::serialize(&AdapterRow { n: 99 });
assert!(matches!(
adapter.write(
WriteKey::Provided(key(1).to_vec()),
Some(false),
fresh.clone()
),
Err(crate::DbError::KeyExists)
));
assert!(matches!(
adapter.write(
WriteKey::Provided(key(2).to_vec()),
Some(true),
fresh.clone()
),
Err(crate::DbError::KeyNotFound)
));
let key_bytes = adapter
.write(WriteKey::Provided(key(2).to_vec()), Some(false), fresh)
.expect("insert-only on absent key");
assert_eq!(key_bytes, key(2).to_vec());
assert_eq!(
adapter.get(&key(2)).expect("get"),
Some(rapira::serialize(&AdapterRow { n: 99 }))
);
}
}
#[cfg(all(test, feature = "var-collections", feature = "rapira-codec"))]
mod var_corruption_tests {
use std::num::NonZeroU32;
use std::ops::Bound;
use armour_core::GetType;
use rapira::Rapira;
use crate::armour::inspection::InspectionError;
use crate::armour::{CountMode, Db};
use crate::{CollectionMeta, Config, NoHook, RapiraCodec};
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct CorruptVarTree {
n: u64,
}
impl CollectionMeta for CorruptVarTree {
type SelfId = [u8; 8];
const NAME: &'static str = "adapter_corrupt_var_tree";
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct CorruptVarMap {
n: u64,
}
impl CollectionMeta for CorruptVarMap {
type SelfId = [u8; 8];
const NAME: &'static str = "adapter_corrupt_var_map";
}
fn key(id: u64) -> [u8; 8] {
id.to_be_bytes()
}
fn iterable_config() -> Config {
Config::balanced()
.shard_count(2)
.hints(true)
.iterable(true)
.reversed(false)
.build()
}
#[test]
fn inspection_var_typed_tree_page_fails_and_exact_count_includes_bad_row() {
let dir = tempfile::tempdir().expect("tmp");
let db = Db::open_test(dir.path()).expect("db");
let tree = db
.open_var_typed_tree::<CorruptVarTree, RapiraCodec, _>(iterable_config(), NoHook, &[])
.expect("var tree");
tree.put(&key(1), &CorruptVarTree { n: 1 }).expect("seed");
tree.put(&key(3), &CorruptVarTree { n: 3 }).expect("seed");
tree.as_inner()
.put(&key(2), &[0xff, 0xfe, 0xfd])
.expect("poison");
let inspector = db.inspector();
let err = inspector
.range(
CorruptVarTree::NAME,
Bound::Unbounded,
Bound::Unbounded,
None,
NonZeroU32::new(10).unwrap(),
)
.unwrap_err();
assert!(
matches!(err, InspectionError::Database(_)),
"the strict Inspector scan must surface the corrupt row: {err:?}"
);
assert_eq!(
inspector
.count(CorruptVarTree::NAME, CountMode::Exact)
.expect("exact count"),
3,
"CountMode::Exact counts live keys, including the undecodable one"
);
}
#[test]
fn inspection_var_typed_map_page_fails_on_undecodable_row() {
let dir = tempfile::tempdir().expect("tmp");
let db = Db::open_test(dir.path()).expect("db");
let map = db
.open_var_typed_map::<CorruptVarMap, RapiraCodec, _>(iterable_config(), NoHook, &[])
.expect("var map");
for id in [1u64, 3, 4] {
map.put(&key(id), &CorruptVarMap { n: id }).expect("seed");
}
map.as_inner()
.put(&key(2), &[0xff, 0xfe, 0xfd])
.expect("poison");
let err = db
.inspector()
.range(
CorruptVarMap::NAME,
Bound::Unbounded,
Bound::Unbounded,
None,
NonZeroU32::new(10).unwrap(),
)
.unwrap_err();
assert!(
matches!(err, InspectionError::Database(_)),
"the strict Inspector scan must surface the corrupt row: {err:?}"
);
}
#[cfg(feature = "rpc")]
#[test]
fn write_precondition_on_var_typed_tree_is_existence_only() {
use armour_rpc::protocol::WriteKey;
use crate::access::{Adapter, CodecWire};
use crate::armour::handler::RpcHandler;
use crate::tree_meta::TreeMeta;
let dir = tempfile::tempdir().expect("tmp");
let db = Db::open_test(dir.path()).expect("db");
let tree = db
.open_var_typed_tree::<CorruptVarTree, RapiraCodec, _>(iterable_config(), NoHook, &[])
.expect("var tree");
tree.as_inner()
.put(&key(1), &[0xff, 0xfe, 0xfd])
.expect("poison");
let adapter = Adapter::new(
&TreeMeta::of::<CorruptVarTree>(),
tree.clone(),
CodecWire(RapiraCodec),
db.seq.clone(),
);
let fresh = rapira::serialize(&CorruptVarTree { n: 99 });
let err = adapter
.write(
WriteKey::Provided(key(1).to_vec()),
Some(false),
fresh.clone(),
)
.unwrap_err();
assert!(matches!(err, crate::DbError::KeyExists), "{err:?}");
assert_eq!(
tree.as_inner().get(&key(1)).map(|b| b.to_vec()),
Some(vec![0xff, 0xfe, 0xfd]),
"insert-only must not have overwritten the corrupt row"
);
adapter
.write(WriteKey::Provided(key(1).to_vec()), Some(true), fresh)
.expect("update-only over a present (if undecodable) row must succeed");
assert_eq!(
tree.get(&key(1)),
Some(CorruptVarTree { n: 99 }),
"update-only must have replaced the corrupt row"
);
}
}
#[cfg(all(test, feature = "fjall", feature = "rapira-codec"))]
mod lsm_corruption_tests {
use std::num::NonZeroU32;
use std::ops::Bound;
use armour_core::GetType;
use rapira::Rapira;
use crate::armour::Db;
use crate::armour::inspection::InspectionError;
use crate::lsm::{LsmConfig, LsmDbConfig};
use crate::{CollectionMeta, Key, NoHook, RapiraCodec};
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct CorruptLsm {
n: u64,
}
impl CollectionMeta for CorruptLsm {
type SelfId = [u8; 8];
const NAME: &'static str = "adapter_corrupt_lsm";
}
fn key(id: u64) -> [u8; 8] {
id.to_be_bytes()
}
#[test]
fn inspection_lsm_page_fails_on_undecodable_row() {
let dir = tempfile::tempdir().expect("tmp");
let db = Db::open_test(dir.path()).expect("db");
let tree = db
.open_lsm_tree::<CorruptLsm, RapiraCodec, _>(
Some(LsmDbConfig::default()),
LsmConfig {
reversed: false,
..LsmConfig::default()
},
NoHook,
&[],
)
.expect("lsm tree");
tree.put(&key(1), &CorruptLsm { n: 1 }).expect("seed");
tree.put(&key(3), &CorruptLsm { n: 3 }).expect("seed");
tree.keyspace()
.insert(key(2).as_bytes(), b"corrupt")
.expect("poison");
let err = db
.inspector()
.range(
CorruptLsm::NAME,
Bound::Unbounded,
Bound::Unbounded,
None,
NonZeroU32::new(10).unwrap(),
)
.unwrap_err();
assert!(
matches!(err, InspectionError::Database(_)),
"the strict Inspector scan must surface the corrupt row: {err:?}"
);
}
}