mirror of
https://github.com/aljazceru/pubky-core.git
synced 2026-01-03 22:34:21 +01:00
feat: Database and WriteTransaction
This commit is contained in:
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -207,7 +207,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kytes"
|
||||
name = "kytz"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
@@ -251,6 +251,7 @@ dependencies = [
|
||||
"proptest",
|
||||
"redb",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -444,18 +445,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.50"
|
||||
version = "1.0.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
|
||||
checksum = "b2cd5904763bad08ad5513ddbb12cf2ae273ca53fa9f68e843e236ec6dfccc09"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.50"
|
||||
version = "1.0.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
|
||||
checksum = "3dcf4a824cce0aeacd6f38ae6f24234c8e80d68632338ebaa1443b5df9e29e19"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -8,6 +8,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
blake3 = "1.5.0"
|
||||
redb = "1.4.0"
|
||||
thiserror = "1.0.53"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1.4.0"
|
||||
|
||||
123
mast/src/db.rs
Normal file
123
mast/src/db.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
//! Kytz Database
|
||||
|
||||
use crate::node::Node;
|
||||
use crate::operations::read::{get_node, root_hash, root_node};
|
||||
use crate::operations::{insert, remove};
|
||||
use crate::{Hash, Result};
|
||||
|
||||
pub struct Database {
|
||||
pub(crate) inner: redb::Database,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Create a new in-memory database.
|
||||
pub fn in_memory() -> Self {
|
||||
let backend = redb::backends::InMemoryBackend::new();
|
||||
let inner = redb::Database::builder()
|
||||
.create_with_backend(backend)
|
||||
.unwrap();
|
||||
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub fn begin_write(&self) -> Result<WriteTransaction> {
|
||||
let txn = self.inner.begin_write().unwrap();
|
||||
WriteTransaction::new(txn)
|
||||
}
|
||||
|
||||
pub fn iter(&self, treap: &str) -> TreapIterator<'_> {
|
||||
// TODO: save tables instead of opening a new one on every next() call.
|
||||
TreapIterator::new(self, treap.to_string())
|
||||
}
|
||||
|
||||
// === Private Methods ===
|
||||
|
||||
pub(crate) fn get_node(&self, hash: &Option<Hash>) -> Option<Node> {
|
||||
get_node(&self, hash)
|
||||
}
|
||||
|
||||
pub(crate) fn root_hash(&self, treap: &str) -> Option<Hash> {
|
||||
root_hash(&self, treap)
|
||||
}
|
||||
|
||||
pub(crate) fn root(&self, treap: &str) -> Option<Node> {
|
||||
root_node(self, treap)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TreapIterator<'db> {
|
||||
db: &'db Database,
|
||||
treap: String,
|
||||
stack: Vec<Node>,
|
||||
}
|
||||
|
||||
impl<'db> TreapIterator<'db> {
|
||||
fn new(db: &'db Database, treap: String) -> Self {
|
||||
let mut iter = TreapIterator {
|
||||
db,
|
||||
treap: treap.clone(),
|
||||
stack: Vec::new(),
|
||||
};
|
||||
|
||||
if let Some(root) = db.root(&treap) {
|
||||
iter.push_left(root)
|
||||
};
|
||||
|
||||
iter
|
||||
}
|
||||
|
||||
fn push_left(&mut self, mut node: Node) {
|
||||
while let Some(left) = self.db.get_node(node.left()) {
|
||||
self.stack.push(node);
|
||||
node = left;
|
||||
}
|
||||
self.stack.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TreapIterator<'a> {
|
||||
type Item = Node;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.stack.pop() {
|
||||
Some(node) => {
|
||||
if let Some(right) = self.db.get_node(node.right()) {
|
||||
self.push_left(right)
|
||||
}
|
||||
|
||||
Some(node.clone())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WriteTransaction<'db> {
|
||||
inner: redb::WriteTransaction<'db>,
|
||||
}
|
||||
|
||||
impl<'db> WriteTransaction<'db> {
|
||||
pub(crate) fn new(inner: redb::WriteTransaction<'db>) -> Result<Self> {
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
treap: &str,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: impl AsRef<[u8]>,
|
||||
) -> Option<Node> {
|
||||
// TODO: validate key and value length.
|
||||
// key and value mast be less than 2^32 bytes.
|
||||
|
||||
insert(&mut self.inner, treap, key.as_ref(), value.as_ref())
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, treap: &str, key: impl AsRef<[u8]>) -> Option<Node> {
|
||||
remove(&mut self.inner, treap, key.as_ref())
|
||||
}
|
||||
|
||||
pub fn commit(self) -> Result<()> {
|
||||
self.inner.commit().map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
24
mast/src/error.rs
Normal file
24
mast/src/error.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
//! Main Crate Error
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
/// Mainline crate error enum.
|
||||
pub enum Error {
|
||||
/// For starter, to remove as code matures.
|
||||
#[error("Generic error: {0}")]
|
||||
Generic(String),
|
||||
/// For starter, to remove as code matures.
|
||||
#[error("Static error: {0}")]
|
||||
Static(&'static str),
|
||||
|
||||
#[error(transparent)]
|
||||
/// Transparent [std::io::Error]
|
||||
IO(#[from] std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
/// Transparent [redb::CommitError]
|
||||
CommitError(#[from] redb::CommitError),
|
||||
|
||||
#[error(transparent)]
|
||||
/// Error from `redb::TransactionError`.
|
||||
TransactionError(#[from] redb::TransactionError),
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
#![allow(unused)]
|
||||
|
||||
pub mod db;
|
||||
pub mod error;
|
||||
mod node;
|
||||
mod operations;
|
||||
pub mod treap;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
pub(crate) use blake3::{Hash, Hasher};
|
||||
pub(crate) const HASH_LEN: usize = 32;
|
||||
|
||||
pub const HASH_LEN: usize = 32;
|
||||
pub use db::Database;
|
||||
pub use error::Error;
|
||||
|
||||
// Alias Result to be the crate Result.
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use blake3::Hash;
|
||||
use redb::Table;
|
||||
|
||||
use super::search::binary_search_path;
|
||||
use super::{read::root_node_inner, search::binary_search_path, NODES_TABLE, ROOTS_TABLE};
|
||||
use crate::node::{Branch, Node};
|
||||
|
||||
// Watch this [video](https://youtu.be/NxRXhBur6Xs?si=GNwaUOfuGwr_tBKI&t=1763) for a good explanation of the unzipping algorithm.
|
||||
@@ -101,12 +101,17 @@ use crate::node::{Branch, Node};
|
||||
//
|
||||
|
||||
pub(crate) fn insert(
|
||||
nodes_table: &'_ mut Table<&'static [u8], (u64, &'static [u8])>,
|
||||
root: Option<Node>,
|
||||
write_txn: &mut redb::WriteTransaction,
|
||||
treap: &str,
|
||||
key: &[u8],
|
||||
value: &[u8],
|
||||
) -> Node {
|
||||
let mut path = binary_search_path(nodes_table, root, key);
|
||||
) -> Option<Node> {
|
||||
let mut roots_table = write_txn.open_table(ROOTS_TABLE).unwrap();
|
||||
let mut nodes_table = write_txn.open_table(NODES_TABLE).unwrap();
|
||||
|
||||
let old_root = root_node_inner(&roots_table, &nodes_table, treap);
|
||||
|
||||
let mut path = binary_search_path(&nodes_table, old_root, key);
|
||||
|
||||
let mut left_subtree: Option<Hash> = None;
|
||||
let mut right_subtree: Option<Hash> = None;
|
||||
@@ -114,7 +119,7 @@ pub(crate) fn insert(
|
||||
// Unzip the lower path to get left and right children of the inserted node.
|
||||
for (node, branch) in path.lower.iter_mut().rev() {
|
||||
// Decrement the old version.
|
||||
node.decrement_ref_count().save(nodes_table);
|
||||
node.decrement_ref_count().save(&mut nodes_table);
|
||||
|
||||
match branch {
|
||||
Branch::Right => {
|
||||
@@ -127,28 +132,27 @@ pub(crate) fn insert(
|
||||
}
|
||||
}
|
||||
|
||||
node.increment_ref_count().save(nodes_table);
|
||||
node.increment_ref_count().save(&mut nodes_table);
|
||||
}
|
||||
|
||||
let mut root;
|
||||
let mut new_root;
|
||||
|
||||
if let Some(mut found) = path.found {
|
||||
if found.value() == value {
|
||||
// There is really nothing to update. Skip traversing upwards.
|
||||
|
||||
return path.upper.first().map(|(n, _)| n.clone()).unwrap_or(found);
|
||||
return Some(found);
|
||||
}
|
||||
|
||||
// Decrement the old version.
|
||||
found.decrement_ref_count().save(nodes_table);
|
||||
found.decrement_ref_count().save(&mut nodes_table);
|
||||
|
||||
// Else, update the value and rehashe the node so that we can update the hashes upwards.
|
||||
found
|
||||
.set_value(value)
|
||||
.increment_ref_count()
|
||||
.save(nodes_table);
|
||||
.save(&mut nodes_table);
|
||||
|
||||
root = found
|
||||
new_root = found
|
||||
} else {
|
||||
// Insert the new node.
|
||||
let mut node = Node::new(key, value);
|
||||
@@ -156,29 +160,34 @@ pub(crate) fn insert(
|
||||
node.set_left_child(left_subtree)
|
||||
.set_right_child(right_subtree)
|
||||
.increment_ref_count()
|
||||
.save(nodes_table);
|
||||
.save(&mut nodes_table);
|
||||
|
||||
root = node
|
||||
new_root = node
|
||||
};
|
||||
|
||||
let mut upper_path = path.upper;
|
||||
|
||||
// Propagate the new hashes upwards if there are any nodes in the upper_path.
|
||||
while let Some((mut node, branch)) = upper_path.pop() {
|
||||
node.decrement_ref_count().save(nodes_table);
|
||||
node.decrement_ref_count().save(&mut nodes_table);
|
||||
|
||||
match branch {
|
||||
Branch::Left => node.set_left_child(Some(root.hash())),
|
||||
Branch::Right => node.set_right_child(Some(root.hash())),
|
||||
Branch::Left => node.set_left_child(Some(new_root.hash())),
|
||||
Branch::Right => node.set_right_child(Some(new_root.hash())),
|
||||
};
|
||||
|
||||
node.increment_ref_count().save(nodes_table);
|
||||
node.increment_ref_count().save(&mut nodes_table);
|
||||
|
||||
root = node;
|
||||
new_root = node;
|
||||
}
|
||||
|
||||
// Finally return the new root to be set to the root.
|
||||
root
|
||||
// Finally set the new root .
|
||||
roots_table
|
||||
.insert(treap.as_bytes(), new_root.hash().as_bytes().as_slice())
|
||||
.unwrap();
|
||||
|
||||
// No older value was found.
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
pub mod insert;
|
||||
pub mod read;
|
||||
pub mod remove;
|
||||
mod search;
|
||||
|
||||
pub(crate) use insert::insert;
|
||||
pub(crate) use remove::remove;
|
||||
|
||||
use redb::{ReadableTable, TableDefinition};
|
||||
|
||||
// Table: Nodes v0
|
||||
// stores all the hash treap nodes from all the treaps in the storage.
|
||||
//
|
||||
// Key: `[u8; 32]` # Node hash
|
||||
// Value: `(u64, [u8])` # (RefCount, EncodedNode)
|
||||
pub const NODES_TABLE: TableDefinition<&[u8], (u64, &[u8])> =
|
||||
TableDefinition::new("kytz:hash_treap:nodes:v0");
|
||||
|
||||
// Table: Roots v0
|
||||
// stores all the current roots for all treaps in the storage.
|
||||
//
|
||||
// Key: `[u8; 32]` # Treap name
|
||||
// Value: `[u8; 32]` # Hash
|
||||
pub const ROOTS_TABLE: TableDefinition<&[u8], &[u8]> =
|
||||
TableDefinition::new("kytz:hash_treap:roots:v0");
|
||||
|
||||
47
mast/src/operations/read.rs
Normal file
47
mast/src/operations/read.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use super::{ReadableTable, NODES_TABLE, ROOTS_TABLE};
|
||||
use crate::node::Node;
|
||||
use crate::{Database, Hash, HASH_LEN};
|
||||
|
||||
pub fn root_hash_inner(
|
||||
table: &'_ impl ReadableTable<&'static [u8], &'static [u8]>,
|
||||
treap: &str,
|
||||
) -> Option<Hash> {
|
||||
let existing = table.get(treap.as_bytes()).unwrap();
|
||||
existing.as_ref()?;
|
||||
|
||||
let hash = existing.unwrap();
|
||||
|
||||
let hash: [u8; HASH_LEN] = hash.value().try_into().expect("Invalid root hash");
|
||||
|
||||
Some(Hash::from_bytes(hash))
|
||||
}
|
||||
|
||||
pub fn root_node_inner(
|
||||
roots_table: &'_ impl ReadableTable<&'static [u8], &'static [u8]>,
|
||||
nodes_table: &'_ impl ReadableTable<&'static [u8], (u64, &'static [u8])>,
|
||||
treap: &str,
|
||||
) -> Option<Node> {
|
||||
root_hash_inner(roots_table, treap).and_then(|hash| Node::open(nodes_table, hash))
|
||||
}
|
||||
|
||||
pub fn get_node(db: &Database, hash: &Option<Hash>) -> Option<Node> {
|
||||
let read_txn = db.inner.begin_read().unwrap();
|
||||
let table = read_txn.open_table(NODES_TABLE).unwrap();
|
||||
|
||||
hash.and_then(|h| Node::open(&table, h))
|
||||
}
|
||||
|
||||
pub fn root_hash(db: &Database, treap: &str) -> Option<Hash> {
|
||||
let read_txn = db.inner.begin_read().unwrap();
|
||||
let table = read_txn.open_table(ROOTS_TABLE).unwrap();
|
||||
|
||||
root_hash_inner(&table, treap)
|
||||
}
|
||||
|
||||
pub fn root_node(db: &Database, treap: &str) -> Option<Node> {
|
||||
let read_txn = db.inner.begin_read().unwrap();
|
||||
let roots_table = read_txn.open_table(ROOTS_TABLE).unwrap();
|
||||
let nodes_table = read_txn.open_table(NODES_TABLE).unwrap();
|
||||
|
||||
root_node_inner(&roots_table, &nodes_table, treap)
|
||||
}
|
||||
@@ -1,41 +1,54 @@
|
||||
use blake3::Hash;
|
||||
use redb::Table;
|
||||
|
||||
use super::search::binary_search_path;
|
||||
use super::{read::root_node_inner, search::binary_search_path, NODES_TABLE, ROOTS_TABLE};
|
||||
use crate::node::{Branch, Node};
|
||||
|
||||
/// Removes the target node if it exists, and returns the new root and the removed node.
|
||||
pub(crate) fn remove(
|
||||
nodes_table: &'_ mut Table<&'static [u8], (u64, &'static [u8])>,
|
||||
root: Option<Node>,
|
||||
write_txn: &mut redb::WriteTransaction,
|
||||
treap: &str,
|
||||
key: &[u8],
|
||||
) -> (Option<Node>, Option<Node>) {
|
||||
let mut path = binary_search_path(nodes_table, root, key);
|
||||
) -> Option<Node> {
|
||||
let mut roots_table = write_txn.open_table(ROOTS_TABLE).unwrap();
|
||||
let mut nodes_table = write_txn.open_table(NODES_TABLE).unwrap();
|
||||
|
||||
let mut root = None;
|
||||
let old_root = root_node_inner(&roots_table, &nodes_table, treap);
|
||||
|
||||
let mut path = binary_search_path(&nodes_table, old_root, key);
|
||||
|
||||
let mut new_root = None;
|
||||
|
||||
if let Some(mut target) = path.found.clone() {
|
||||
root = zip(nodes_table, &mut target)
|
||||
new_root = zip(&mut nodes_table, &mut target)
|
||||
} else {
|
||||
// clearly the lower path has the highest node, and it won't be changed.
|
||||
root = path.lower.first().map(|(n, _)| n.clone());
|
||||
new_root = path.lower.first().map(|(n, _)| n.clone());
|
||||
}
|
||||
|
||||
// If there is an upper path, we propagate the hash updates upwards.
|
||||
while let Some((mut node, branch)) = path.upper.pop() {
|
||||
node.decrement_ref_count().save(nodes_table);
|
||||
node.decrement_ref_count().save(&mut nodes_table);
|
||||
|
||||
match branch {
|
||||
Branch::Left => node.set_left_child(root.map(|mut n| n.hash())),
|
||||
Branch::Right => node.set_right_child(root.map(|mut n| n.hash())),
|
||||
Branch::Left => node.set_left_child(new_root.map(|mut n| n.hash())),
|
||||
Branch::Right => node.set_right_child(new_root.map(|mut n| n.hash())),
|
||||
};
|
||||
|
||||
node.increment_ref_count().save(nodes_table);
|
||||
node.increment_ref_count().save(&mut nodes_table);
|
||||
|
||||
root = Some(node);
|
||||
new_root = Some(node);
|
||||
}
|
||||
|
||||
(root, path.found)
|
||||
if let Some(mut new_root) = new_root {
|
||||
roots_table
|
||||
.insert(treap.as_bytes(), new_root.hash().as_bytes().as_slice())
|
||||
.unwrap();
|
||||
} else {
|
||||
roots_table.remove(treap.as_bytes()).unwrap();
|
||||
}
|
||||
|
||||
path.found
|
||||
}
|
||||
|
||||
fn zip(
|
||||
|
||||
@@ -4,12 +4,9 @@ use std::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::node::Node;
|
||||
use crate::treap::HashTreap;
|
||||
use crate::Database;
|
||||
use crate::Hash;
|
||||
|
||||
use redb::backends::InMemoryBackend;
|
||||
use redb::Database;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Operation {
|
||||
Insert,
|
||||
@@ -56,27 +53,24 @@ impl std::fmt::Debug for Entry {
|
||||
}
|
||||
|
||||
pub fn test_operations(input: &[(Entry, Operation)], root_hash: Option<&str>) {
|
||||
let inmemory = InMemoryBackend::new();
|
||||
let db = Database::builder()
|
||||
.create_with_backend(inmemory)
|
||||
.expect("Failed to create DB");
|
||||
|
||||
let mut treap = HashTreap::new(&db, "test");
|
||||
let db = Database::in_memory();
|
||||
let mut txn = db.begin_write().unwrap();
|
||||
let treap = "test";
|
||||
|
||||
for (entry, operation) in input {
|
||||
match operation {
|
||||
Operation::Insert => treap.insert(&entry.key, &entry.value),
|
||||
Operation::Remove => {
|
||||
treap.remove(&entry.key);
|
||||
}
|
||||
}
|
||||
Operation::Insert => txn.insert(treap, &entry.key, &entry.value),
|
||||
Operation::Remove => txn.remove(treap, &entry.key),
|
||||
};
|
||||
}
|
||||
|
||||
txn.commit();
|
||||
|
||||
// Uncomment to see the graph
|
||||
// println!("{}", into_mermaid_graph(&treap));
|
||||
|
||||
let collected = treap
|
||||
.iter()
|
||||
let collected = db
|
||||
.iter(treap)
|
||||
.map(|n| {
|
||||
assert_eq!(
|
||||
*n.ref_count(),
|
||||
@@ -92,7 +86,7 @@ pub fn test_operations(input: &[(Entry, Operation)], root_hash: Option<&str>) {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
verify_ranks(&treap);
|
||||
verify_ranks(&db, treap);
|
||||
|
||||
let mut btree = BTreeMap::new();
|
||||
for (entry, operation) in input {
|
||||
@@ -117,28 +111,28 @@ pub fn test_operations(input: &[(Entry, Operation)], root_hash: Option<&str>) {
|
||||
assert_eq!(collected, expected, "{}", format!("Entries do not match"));
|
||||
|
||||
if root_hash.is_some() {
|
||||
assert_root(&treap, root_hash.unwrap());
|
||||
assert_root(&db, treap, root_hash.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that every node has higher rank than its children.
|
||||
fn verify_ranks(treap: &HashTreap) {
|
||||
fn verify_ranks(db: &Database, treap: &str) {
|
||||
assert!(
|
||||
verify_children_rank(treap, treap.root()),
|
||||
verify_children_rank(db, treap, db.root(treap)),
|
||||
"Ranks are not sorted correctly"
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_children_rank(treap: &HashTreap, node: Option<Node>) -> bool {
|
||||
fn verify_children_rank(db: &Database, treap: &str, node: Option<Node>) -> bool {
|
||||
match node {
|
||||
Some(n) => {
|
||||
let left_check = treap.get_node(n.left()).map_or(true, |left| {
|
||||
let left_check = db.get_node(n.left()).map_or(true, |left| {
|
||||
n.rank().as_bytes() > left.rank().as_bytes()
|
||||
&& verify_children_rank(treap, Some(left))
|
||||
&& verify_children_rank(db, treap, Some(left))
|
||||
});
|
||||
let right_check = treap.get_node(n.right()).map_or(true, |right| {
|
||||
let right_check = db.get_node(n.right()).map_or(true, |right| {
|
||||
n.rank().as_bytes() > right.rank().as_bytes()
|
||||
&& verify_children_rank(treap, Some(right))
|
||||
&& verify_children_rank(db, treap, Some(right))
|
||||
});
|
||||
|
||||
left_check && right_check
|
||||
@@ -147,11 +141,8 @@ fn verify_children_rank(treap: &HashTreap, node: Option<Node>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_root(treap: &HashTreap, expected_root_hash: &str) {
|
||||
let root_hash = treap
|
||||
.root()
|
||||
.map(|mut n| n.hash())
|
||||
.expect("Has root hash after insertion");
|
||||
fn assert_root(db: &Database, treap: &str, expected_root_hash: &str) {
|
||||
let root_hash = db.root_hash(treap).expect("Has root hash after insertion");
|
||||
|
||||
assert_eq!(
|
||||
root_hash,
|
||||
@@ -162,13 +153,13 @@ fn assert_root(treap: &HashTreap, expected_root_hash: &str) {
|
||||
|
||||
// === Visualize the treap to verify the structure ===
|
||||
|
||||
fn into_mermaid_graph(treap: &HashTreap) -> String {
|
||||
fn into_mermaid_graph(db: &Database, treap: &str) -> String {
|
||||
let mut graph = String::new();
|
||||
|
||||
graph.push_str("graph TD;\n");
|
||||
|
||||
if let Some(mut root) = treap.root() {
|
||||
build_graph_string(&treap, &mut root, &mut graph);
|
||||
if let Some(mut root) = db.root(treap) {
|
||||
build_graph_string(db, treap, &mut root, &mut graph);
|
||||
}
|
||||
|
||||
graph.push_str(&format!(
|
||||
@@ -178,28 +169,28 @@ fn into_mermaid_graph(treap: &HashTreap) -> String {
|
||||
graph
|
||||
}
|
||||
|
||||
fn build_graph_string(treap: &HashTreap, node: &mut Node, graph: &mut String) {
|
||||
fn build_graph_string(db: &Database, treap: &str, node: &mut Node, graph: &mut String) {
|
||||
let key = format_key(node.key());
|
||||
let node_label = format!("{}(({}))", node.hash(), key);
|
||||
|
||||
// graph.push_str(&format!("## START node {}\n", node_label));
|
||||
if let Some(mut child) = treap.get_node(node.left()) {
|
||||
if let Some(mut child) = db.get_node(node.left()) {
|
||||
let key = format_key(child.key());
|
||||
let child_label = format!("{}(({}))", child.hash(), key);
|
||||
|
||||
graph.push_str(&format!(" {} --l--> {};\n", node_label, child_label));
|
||||
build_graph_string(&treap, &mut child, graph);
|
||||
build_graph_string(db, treap, &mut child, graph);
|
||||
} else {
|
||||
graph.push_str(&format!(" {} -.-> {}l((l));\n", node_label, node.hash()));
|
||||
graph.push_str(&format!(" class {}l null;\n", node.hash()));
|
||||
}
|
||||
|
||||
if let Some(mut child) = treap.get_node(node.right()) {
|
||||
if let Some(mut child) = db.get_node(node.right()) {
|
||||
let key = format_key(child.key());
|
||||
let child_label = format!("{}(({}))", child.hash(), key);
|
||||
|
||||
graph.push_str(&format!(" {} --r--> {};\n", node_label, child_label));
|
||||
build_graph_string(&treap, &mut child, graph);
|
||||
build_graph_string(db, treap, &mut child, graph);
|
||||
} else {
|
||||
graph.push_str(&format!(" {} -.-> {}r((r));\n", node_label, node.hash()));
|
||||
graph.push_str(&format!(" class {}r null;\n", node.hash()));
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
use blake3::Hash;
|
||||
use redb::*;
|
||||
|
||||
use crate::{node::Node, HASH_LEN};
|
||||
|
||||
// TODO: test that order is correct
|
||||
// TODO: test that there are no extr anodes.
|
||||
|
||||
// Table: Nodes v0
|
||||
// stores all the hash treap nodes from all the treaps in the storage.
|
||||
//
|
||||
// Key: `[u8; 32]` # Node hash
|
||||
// Value: `(u64, [u8])` # (RefCount, EncodedNode)
|
||||
pub const NODES_TABLE: TableDefinition<&[u8], (u64, &[u8])> =
|
||||
TableDefinition::new("kytz:hash_treap:nodes:v0");
|
||||
|
||||
// Table: Roots v0
|
||||
// stores all the current roots for all treaps in the storage.
|
||||
//
|
||||
// Key: `[u8; 32]` # Treap name
|
||||
// Value: `[u8; 32]` # Hash
|
||||
pub const ROOTS_TABLE: TableDefinition<&[u8], &[u8]> =
|
||||
TableDefinition::new("kytz:hash_treap:roots:v0");
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HashTreap<'treap> {
|
||||
/// Redb database to store the nodes.
|
||||
pub(crate) db: &'treap Database,
|
||||
pub(crate) name: &'treap str,
|
||||
}
|
||||
|
||||
impl<'treap> HashTreap<'treap> {
|
||||
// TODO: add name to open from storage with.
|
||||
pub fn new(db: &'treap Database, name: &'treap str) -> Self {
|
||||
// Setup tables
|
||||
let write_tx = db.begin_write().unwrap();
|
||||
{
|
||||
let _table = write_tx.open_table(NODES_TABLE).unwrap();
|
||||
let _table = write_tx.open_table(ROOTS_TABLE).unwrap();
|
||||
}
|
||||
write_tx.commit().unwrap();
|
||||
|
||||
Self { name, db }
|
||||
}
|
||||
|
||||
// === Getters ===
|
||||
|
||||
/// Returns the root hash of the treap.
|
||||
pub fn root_hash(&self) -> Option<Hash> {
|
||||
let read_txn = self.db.begin_read().unwrap();
|
||||
let table = read_txn.open_table(ROOTS_TABLE).unwrap();
|
||||
|
||||
self.root_hash_inner(&table)
|
||||
}
|
||||
|
||||
// === Public Methods ===
|
||||
|
||||
pub fn insert(&mut self, key: &[u8], value: &[u8]) {
|
||||
// TODO: validate key and value length.
|
||||
// key and value mast be less than 2^32 bytes.
|
||||
|
||||
let write_txn = self.db.begin_write().unwrap();
|
||||
|
||||
{
|
||||
let mut roots_table = write_txn.open_table(ROOTS_TABLE).unwrap();
|
||||
let mut nodes_table = write_txn.open_table(NODES_TABLE).unwrap();
|
||||
|
||||
let old_root = self
|
||||
.root_hash_inner(&roots_table)
|
||||
.and_then(|hash| Node::open(&nodes_table, hash));
|
||||
|
||||
let mut new_root = crate::operations::insert(&mut nodes_table, old_root, key, value);
|
||||
|
||||
roots_table
|
||||
.insert(self.name.as_bytes(), new_root.hash().as_bytes().as_slice())
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
// Finally commit the changes to the storage.
|
||||
write_txn.commit().unwrap();
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &[u8]) -> Option<Box<[u8]>> {
|
||||
let write_txn = self.db.begin_write().unwrap();
|
||||
|
||||
let mut removed_node;
|
||||
|
||||
{
|
||||
let mut roots_table = write_txn.open_table(ROOTS_TABLE).unwrap();
|
||||
let mut nodes_table = write_txn.open_table(NODES_TABLE).unwrap();
|
||||
|
||||
let old_root = self
|
||||
.root_hash_inner(&roots_table)
|
||||
.and_then(|hash| Node::open(&nodes_table, hash));
|
||||
|
||||
let (new_root, old_node) = crate::operations::remove(&mut nodes_table, old_root, key);
|
||||
|
||||
removed_node = old_node;
|
||||
|
||||
if let Some(mut new_root) = new_root {
|
||||
roots_table
|
||||
.insert(self.name.as_bytes(), new_root.hash().as_bytes().as_slice())
|
||||
.unwrap();
|
||||
} else {
|
||||
roots_table.remove(self.name.as_bytes()).unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
// Finally commit the changes to the storage.
|
||||
write_txn.commit().unwrap();
|
||||
|
||||
removed_node.map(|node| node.value().to_vec().into_boxed_slice())
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> TreapIterator<'_> {
|
||||
TreapIterator::new(self)
|
||||
}
|
||||
|
||||
// === Private Methods ===
|
||||
|
||||
pub(crate) fn root(&self) -> Option<Node> {
|
||||
let read_txn = self.db.begin_read().unwrap();
|
||||
|
||||
let roots_table = read_txn.open_table(ROOTS_TABLE).unwrap();
|
||||
let nodes_table = read_txn.open_table(NODES_TABLE).unwrap();
|
||||
|
||||
self.root_hash_inner(&roots_table)
|
||||
.and_then(|hash| Node::open(&nodes_table, hash))
|
||||
}
|
||||
|
||||
fn root_hash_inner(
|
||||
&self,
|
||||
table: &'_ impl ReadableTable<&'static [u8], &'static [u8]>,
|
||||
) -> Option<Hash> {
|
||||
let existing = table.get(self.name.as_bytes()).unwrap();
|
||||
existing.as_ref()?;
|
||||
|
||||
let hash = existing.unwrap();
|
||||
|
||||
let hash: [u8; HASH_LEN] = hash.value().try_into().expect("Invalid root hash");
|
||||
|
||||
Some(Hash::from_bytes(hash))
|
||||
}
|
||||
|
||||
pub(crate) fn get_node(&self, hash: &Option<Hash>) -> Option<Node> {
|
||||
let read_txn = self.db.begin_read().unwrap();
|
||||
let table = read_txn.open_table(NODES_TABLE).unwrap();
|
||||
|
||||
hash.and_then(|h| Node::open(&table, h))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TreapIterator<'treap> {
|
||||
treap: &'treap HashTreap<'treap>,
|
||||
stack: Vec<Node>,
|
||||
}
|
||||
|
||||
impl<'a> TreapIterator<'a> {
|
||||
fn new(treap: &'a HashTreap<'a>) -> Self {
|
||||
let mut iter = TreapIterator {
|
||||
treap,
|
||||
stack: Vec::new(),
|
||||
};
|
||||
|
||||
if let Some(root) = treap.root() {
|
||||
iter.push_left(root)
|
||||
};
|
||||
|
||||
iter
|
||||
}
|
||||
|
||||
fn push_left(&mut self, mut node: Node) {
|
||||
while let Some(left) = self.treap.get_node(node.left()) {
|
||||
self.stack.push(node);
|
||||
node = left;
|
||||
}
|
||||
self.stack.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TreapIterator<'a> {
|
||||
type Item = Node;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.stack.pop() {
|
||||
Some(node) => {
|
||||
if let Some(right) = self.treap.get_node(node.right()) {
|
||||
self.push_left(right)
|
||||
}
|
||||
|
||||
Some(node.clone())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user