use std::fmt;
use crate::store::NodeStore;
use crate::tree::{Cursor, Hash, RangeIter, TreeError};
#[derive(Debug)]
pub enum CheckoutError {
ReadOnly,
Tree(TreeError),
}
impl fmt::Display for CheckoutError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ReadOnly => write!(f, "checkout view is read-only"),
Self::Tree(error) => write!(f, "checkout traversal error: {error}"),
}
}
}
impl std::error::Error for CheckoutError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Tree(error) => Some(error),
Self::ReadOnly => None,
}
}
}
impl From<TreeError> for CheckoutError {
fn from(error: TreeError) -> Self {
Self::Tree(error)
}
}
#[derive(Debug)]
pub struct ReadOnlyView<'a, S: NodeStore + ?Sized> {
cursor: Cursor<'a, S>,
}
impl<S: NodeStore + ?Sized> ReadOnlyView<'_, S> {
pub const fn root_hash(&self) -> Hash {
self.cursor.root_hash()
}
pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, CheckoutError> {
self.cursor.get(key).map_err(CheckoutError::from)
}
pub fn range(&self, from: &[u8], to: &[u8]) -> RangeIter<'_, S> {
self.cursor.range(from, to)
}
pub const fn put(&self, _key: &[u8], _value: &[u8]) -> Result<(), CheckoutError> {
Err(CheckoutError::ReadOnly)
}
pub const fn delete(&self, _key: &[u8]) -> Result<(), CheckoutError> {
Err(CheckoutError::ReadOnly)
}
pub const fn commit(&self) -> Result<(), CheckoutError> {
Err(CheckoutError::ReadOnly)
}
}
pub const fn checkout<S: NodeStore + ?Sized>(store: &S, root_hash: Hash) -> ReadOnlyView<'_, S> {
ReadOnlyView {
cursor: Cursor::new(store, root_hash),
}
}
#[cfg(test)]
mod tests {
use super::{CheckoutError, checkout};
use crate::store::MemoryStore;
use crate::tree::{Hash, InternalNode, LeafNode, Node, TreeError, insert};
type TestResult = Result<(), TreeError>;
type Pairs = Vec<(Vec<u8>, Vec<u8>)>;
fn empty_root(store: &mut MemoryStore) -> Result<Hash, TreeError> {
let leaf = LeafNode::new(Vec::new())?;
Ok(store.put(&Node::Leaf(leaf)))
}
fn build_tree(store: &mut MemoryStore, entries: &[(&[u8], &[u8])]) -> Result<Hash, TreeError> {
let mut root = empty_root(store)?;
for (key, value) in entries {
root = insert(store, root, key, value)?;
}
Ok(root)
}
fn collect_range(
view: &super::ReadOnlyView<'_, MemoryStore>,
from: &[u8],
to: &[u8],
) -> Result<Pairs, TreeError> {
view.range(from, to).collect()
}
#[test]
fn checkout_get_reads_historical_state() -> Result<(), CheckoutError> {
let mut store = MemoryStore::new();
let root = build_tree(&mut store, &[(b"a", b"1"), (b"b", b"2")])?;
let view = checkout(&store, root);
assert_eq!(view.root_hash(), root);
assert_eq!(view.get(b"a")?, Some(b"1".to_vec()));
assert_eq!(view.get(b"b")?, Some(b"2".to_vec()));
assert_eq!(view.get(b"missing")?, None);
Ok(())
}
#[test]
fn checkout_does_not_observe_writes_after_the_checkout_point() -> Result<(), CheckoutError> {
let mut store = MemoryStore::new();
let root = build_tree(&mut store, &[(b"a", b"1")])?;
let later = insert(&mut store, root, b"b", b"2")?;
assert_ne!(later, root);
let view = checkout(&store, root);
assert_eq!(view.get(b"a")?, Some(b"1".to_vec()));
assert_eq!(view.get(b"b")?, None);
Ok(())
}
#[test]
fn checkout_range_returns_sorted_historical_entries() -> TestResult {
let mut store = MemoryStore::new();
let root = build_tree(
&mut store,
&[(b"a", b"1"), (b"c", b"3"), (b"b", b"2"), (b"d", b"4")],
)?;
let view = checkout(&store, root);
assert_eq!(
collect_range(&view, b"a", b"d")?,
vec![
(b"a".to_vec(), b"1".to_vec()),
(b"b".to_vec(), b"2".to_vec()),
(b"c".to_vec(), b"3".to_vec()),
]
);
Ok(())
}
#[test]
fn checkout_range_excludes_entries_written_after_checkout() -> TestResult {
let mut store = MemoryStore::new();
let root = build_tree(&mut store, &[(b"a", b"1"), (b"b", b"2")])?;
let _later = insert(&mut store, root, b"c", b"3")?;
let view = checkout(&store, root);
assert_eq!(
collect_range(&view, b"a", b"z")?,
vec![
(b"a".to_vec(), b"1".to_vec()),
(b"b".to_vec(), b"2".to_vec()),
]
);
Ok(())
}
fn two_leaf_tree(store: &mut MemoryStore) -> Result<Hash, TreeError> {
let left = LeafNode::new(vec![
(b"a".to_vec(), b"1".to_vec()),
(b"b".to_vec(), b"2".to_vec()),
])?;
let right = LeafNode::new(vec![
(b"m".to_vec(), b"3".to_vec()),
(b"n".to_vec(), b"4".to_vec()),
])?;
let left_hash = store.put(&Node::Leaf(left));
let right_hash = store.put(&Node::Leaf(right));
let root = InternalNode::new(vec![
(b"a".to_vec(), left_hash),
(b"m".to_vec(), right_hash),
])?;
Ok(store.put(&Node::Internal(root)))
}
#[test]
fn checkout_range_spans_multiple_leaves() -> TestResult {
let mut store = MemoryStore::new();
let root = two_leaf_tree(&mut store)?;
let view = checkout(&store, root);
assert_eq!(
collect_range(&view, b"a", b"z")?,
vec![
(b"a".to_vec(), b"1".to_vec()),
(b"b".to_vec(), b"2".to_vec()),
(b"m".to_vec(), b"3".to_vec()),
(b"n".to_vec(), b"4".to_vec()),
]
);
Ok(())
}
#[test]
fn checkout_with_missing_root_errors_on_read() {
let store = MemoryStore::new();
let absent = Hash::from_bytes([0xff; 32]);
let view = checkout(&store, absent);
assert!(matches!(
view.get(b"any"),
Err(CheckoutError::Tree(TreeError::MissingNode { .. }))
));
assert!(matches!(
view.range(b"a", b"z").next(),
Some(Err(TreeError::MissingNode { .. }))
));
}
#[test]
fn checkout_rejects_writes() -> TestResult {
let mut store = MemoryStore::new();
let root = build_tree(&mut store, &[(b"a", b"1")])?;
let view = checkout(&store, root);
assert!(matches!(view.put(b"x", b"y"), Err(CheckoutError::ReadOnly)));
assert!(matches!(view.delete(b"a"), Err(CheckoutError::ReadOnly)));
assert!(matches!(view.commit(), Err(CheckoutError::ReadOnly)));
Ok(())
}
}