/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2026-2026 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use std::collections::BTreeMap;
#[cfg(feature = "sled-backend")]
use sled::{Transactional, transaction::ConflictableTransactionError};
use crate::{
Batch, Database, DatabaseOverlayState, DatabaseOverlayStateDiff, Error, Result, Tree,
TreeOverlay, TreeOverlayIter, TreeOverlayStateDiff,
};
/// An overlay on top of an entire [`Database`] which can span multiple trees
#[derive(Clone)]
pub struct DatabaseOverlay {
/// The [`Database`] that is being overlayed.
db: Database,
/// Current overlay cache state
pub state: DatabaseOverlayState,
/// Checkpointed cache state to revert to
checkpoint: DatabaseOverlayState,
}
impl DatabaseOverlay {
/// Instantiate a new [`DatabaseOverlay`] on top of a given
/// [`Database`].
/// Note: Provided protected trees don't have to be opened as
/// protected, as they are setup as protected here.
pub fn new(db: &Database, protected_tree_names: Vec) -> Result {
let initial_tree_names = db.tree_names()?;
Ok(Self {
db: db.clone(),
state: DatabaseOverlayState::new(
initial_tree_names.clone(),
protected_tree_names.clone(),
),
checkpoint: DatabaseOverlayState::new(initial_tree_names, protected_tree_names),
})
}
/// Create a new [`TreeOverlay`] on top of a given `tree_name`.
/// This function will also open a new tree inside `database`
/// regardless of if it has existed before, so for convenience, we
/// also provide [`DatabaseOverlay::purge_new_trees`] in case we
/// decide we don't want to write the batches, and drop the new
/// trees. Additionally, a boolean flag is passed to mark the
/// oppened tree as protected, meanning that it can't be removed
/// and its references will never be dropped.
pub fn open_tree(
&mut self,
name: &str,
#[cfg(feature = "fjall-backend")] create_options: impl FnOnce() -> fjall::KeyspaceCreateOptions,
protected: bool,
) -> Result<()> {
// Check if we have already opened this tree
if self.state.caches.contains_key(name) {
return Ok(());
}
// Open this tree in the database
let tree = self.db.open_tree(
name,
#[cfg(feature = "fjall-backend")]
create_options,
)?;
let mut cache = TreeOverlay::new(&tree);
// If we are reopenning a dropped tree, grab its cache
if let Some(diff) = self.state.dropped_trees.remove(name) {
cache.state = (&diff).into();
}
// In case it hasn't existed before, we also need to track it
// in `self.new_tree_names`.
let name = name.to_string();
if !self.state.initial_tree_names.contains(&name) {
self.state.new_tree_names.push(name.clone());
}
self.state.caches.insert(name.clone(), cache);
// Mark tree as protected if requested
if protected && !self.state.protected_tree_names.contains(&name) {
self.state.protected_tree_names.push(name);
}
Ok(())
}
/// Create a new [`TreeOverlay`] on top of a given `tree_name`.
/// This function will also open a new tree inside `database`,
/// using the default backend configuration, regardless of if it
/// has existed before, so for convenience, we also provide
/// [`DatabaseOverlay::purge_new_trees`] in case we decide we don't
/// want to write the batches, and drop the new trees.
/// Additionally, a boolean flag is passed to mark the oppened tree
/// as protected, meanning that it can't be removed and its
/// references will never be dropped.
pub fn open_tree_default(&mut self, name: &str, protected: bool) -> Result<()> {
#[cfg(feature = "sled-backend")]
{
self.open_tree(name, protected)
}
#[cfg(feature = "fjall-backend")]
{
self.open_tree(name, fjall::KeyspaceCreateOptions::default, protected)
}
}
/// Drop a tree from the overlay.
pub fn drop_tree(&mut self, name: &str) -> Result<()> {
// Check if tree is protected
let name = name.to_string();
if self.state.protected_tree_names.contains(&name) {
return Err(Error::ProtectedTreeDrop(name));
}
// Check if already removed
if self.state.dropped_trees.contains_key(&name) {
return Err(Error::CollectionNotFound(name));
}
// Check if its a new tree we created
if self.state.new_tree_names.contains(&name) {
self.state.new_tree_names.retain(|x| x != &name);
let tree = match self.get_cache(&name) {
Ok(cache) => &cache.tree,
_ => &self.db.open_tree_default(&name)?,
};
let diff = TreeOverlayStateDiff::new_dropped(tree);
self.state.caches.remove(&name);
self.state.dropped_trees.insert(name, diff);
return Ok(());
}
// Check if tree existed in the database
if !self.state.initial_tree_names.contains(&name) {
return Err(Error::CollectionNotFound(name));
}
let tree = match self.get_cache(&name) {
Ok(cache) => &cache.tree,
_ => &self.db.open_tree_default(&name)?,
};
let diff = TreeOverlayStateDiff::new_dropped(tree);
self.state.caches.remove(&name);
self.state.dropped_trees.insert(name, diff);
Ok(())
}
/// Drop newly created trees from the database. This is a
/// convenience function that should be used when we decide that we
/// don't want to apply any cache changes, and we want to revert
/// back to the initial state.
pub fn purge_new_trees(&self) -> Result<()> {
for i in &self.state.new_tree_names {
self.db.drop_tree(i)?;
}
Ok(())
}
/// Fetch the cache for a given tree.
fn get_cache(&self, name: &str) -> Result<&TreeOverlay> {
let name = name.to_string();
if self.state.dropped_trees.contains_key(&name) {
return Err(Error::CollectionNotFound(name));
}
if let Some(v) = self.state.caches.get(&name) {
return Ok(v);
}
Err(Error::CollectionNotFound(name))
}
/// Fetch a mutable reference to the cache for a given tree.
fn get_cache_mut(&mut self, name: &str) -> Result<&mut TreeOverlay> {
let name = name.to_string();
if self.state.dropped_trees.contains_key(&name) {
return Err(Error::CollectionNotFound(name));
}
if let Some(v) = self.state.caches.get_mut(&name) {
return Ok(v);
}
Err(Error::CollectionNotFound(name))
}
/// Fetch all our caches current [`Tree`] pointers.
pub fn get_state_trees(&self) -> BTreeMap {
// Grab our state tree pointers
let mut state_trees = BTreeMap::new();
for (name, cache) in self.state.caches.iter() {
state_trees.insert(name.clone(), cache.tree.clone());
}
state_trees
}
/// Returns `true` if the overlay contains a value for a specified
/// key in the specified tree cache.
pub fn contains_key(&self, name: &str, key: &[u8]) -> Result {
let cache = self.get_cache(name)?;
cache.contains_key(key)
}
/// Retrieve a value from the overlay if it exists in the specified
/// tree cache.
pub fn get(&self, name: &str, key: &[u8]) -> Result