use std::error::Error;
use std::path::Path;
use crate::branch::{
BranchRefStore, BranchRegistry, CommitDurability, CommitRequest, commit_branch, create_branch,
};
use crate::shard::router::{SHARD_STORE_DIR, SHARD_WAL_FILE};
use crate::store::{DiskStore, MemoryStore, NodeStore};
use crate::tree::{Hash, LeafNode, Node, TreePolicy, batch_mutate_owned};
use crate::wal::{DurableWal, FsyncPolicy, WalRecovery};
use super::super::DatabaseConfig;
use super::super::config::{DEFAULT_INTERNAL_TARGET_BYTES, DEFAULT_LEAF_TARGET_BYTES, finalize_v1};
pub(super) type Entries = Vec<(Vec<u8>, Vec<u8>)>;
pub(super) type Res<T> = Result<T, Box<dyn Error>>;
pub(super) fn oversized(fill: u8) -> Vec<u8> {
vec![b'A' + (fill % 26); DEFAULT_LEAF_TARGET_BYTES as usize]
}
pub(super) fn oversized_shards(shards: usize, per_shard: usize) -> Vec<Entries> {
(0..shards)
.map(|shard| {
(0..per_shard)
.map(|index| {
let fill = (shard * per_shard + index) as u8;
(
format!("k{shard:02}-{index:02}").into_bytes(),
oversized(fill),
)
})
.collect()
})
.collect()
}
pub(super) fn config_for(data_dir: &Path, shard_count: usize) -> DatabaseConfig {
DatabaseConfig {
data_dir: data_dir.to_path_buf(),
shard_count,
distributed: None,
executor_threads: None,
}
}
fn shard_store_dir(data_dir: &Path, shard_id: usize) -> std::path::PathBuf {
data_dir
.join(format!("shard-{shard_id}"))
.join(SHARD_STORE_DIR)
}
fn shard_wal_path(data_dir: &Path, shard_id: usize) -> std::path::PathBuf {
data_dir
.join(format!("shard-{shard_id}"))
.join(SHARD_WAL_FILE)
}
fn build_root<S: NodeStore + ?Sized>(
store: &mut S,
entries: &Entries,
policy: TreePolicy,
) -> Res<Hash>
where
S::Error: 'static,
{
let empty = store.put(&Node::Leaf(LeafNode::new(Vec::new())?))?;
if entries.is_empty() {
return Ok(empty);
}
let batch: Vec<(Vec<u8>, Option<Vec<u8>>)> = entries
.iter()
.cloned()
.map(|(key, value)| (key, Some(value)))
.collect();
Ok(batch_mutate_owned(store, empty, batch, policy)?)
}
fn write_shard_tree(
data_dir: &Path,
shard_id: usize,
entries: &Entries,
policy: TreePolicy,
) -> Res<Hash> {
let shard_dir = data_dir.join(format!("shard-{shard_id}"));
std::fs::create_dir_all(&shard_dir)?;
let mut store = DiskStore::new(shard_store_dir(data_dir, shard_id))?;
let root = build_root(&mut store, entries, policy)?;
store.sync_dirty_dirs()?;
let mut wal = DurableWal::new(shard_wal_path(data_dir, shard_id), FsyncPolicy::CommitOnly)?;
wal.commit(root)?;
Ok(root)
}
pub(super) fn build_v1_fixture(data_dir: &Path, shard_entries: &[Entries]) -> Res<Vec<Hash>> {
std::fs::create_dir_all(data_dir)?;
finalize_v1(&config_for(data_dir, shard_entries.len()))?;
let mut roots = Vec::with_capacity(shard_entries.len());
for (shard_id, entries) in shard_entries.iter().enumerate() {
roots.push(write_shard_tree(
data_dir,
shard_id,
entries,
TreePolicy::V1_DEFAULT,
)?);
}
Ok(roots)
}
pub(super) fn fresh_build(entries: &Entries, policy: TreePolicy) -> Res<Hash> {
let mut store = MemoryStore::new();
build_root(&mut store, entries, policy)
}
pub(super) fn v2_default() -> TreePolicy {
TreePolicy::v2(DEFAULT_LEAF_TARGET_BYTES, DEFAULT_INTERNAL_TARGET_BYTES)
}
pub(super) fn read_shard_root(data_dir: &Path, shard_id: usize) -> Res<Option<Hash>> {
let store = DiskStore::new(shard_store_dir(data_dir, shard_id))?;
let recovered = WalRecovery::recover_path(shard_wal_path(data_dir, shard_id), &store)?;
Ok(recovered.committed_root())
}
pub(super) fn read_all_roots(data_dir: &Path, shard_count: usize) -> Res<Vec<Option<Hash>>> {
(0..shard_count)
.map(|shard_id| read_shard_root(data_dir, shard_id))
.collect()
}
fn config_json(data_dir: &Path) -> Res<serde_json::Value> {
let bytes = std::fs::read(data_dir.join("config.json"))?;
Ok(serde_json::from_slice(&bytes)?)
}
pub(super) fn format_version(data_dir: &Path) -> Res<Option<u64>> {
Ok(config_json(data_dir)?
.get("format_version")
.and_then(serde_json::Value::as_u64))
}
pub(super) fn is_fenced(data_dir: &Path) -> Res<bool> {
Ok(config_json(data_dir)?
.get("migration_fence")
.is_some_and(|fence| !fence.is_null()))
}
fn branches_dir(data_dir: &Path) -> std::path::PathBuf {
data_dir.join("branches")
}
pub(super) fn build_v1_branch(data_dir: &Path, shard_id: usize, puts: &Entries) -> Res<()> {
let mut store = DiskStore::new(shard_store_dir(data_dir, shard_id))?;
let anchor = store.put(&Node::Leaf(LeafNode::new(Vec::new())?))?;
std::fs::create_dir_all(branches_dir(data_dir))?;
let mut refs = BranchRefStore::open(branches_dir(data_dir))?;
let registry = BranchRegistry::new();
let branch = create_branch("main", [(shard_id, anchor)], &mut refs, ®istry, 10)?;
for (key, value) in puts {
branch.put(shard_id, key.clone(), value.clone())?;
}
commit_branch(
&branch,
&mut store,
®istry,
CommitRequest {
durability: CommitDurability::Durable { refs: &mut refs },
extra_parents: &[],
timestamp: 20,
},
TreePolicy::V1_DEFAULT,
)?;
Ok(())
}
pub(super) fn read_branch_head(data_dir: &Path, name: &str, shard_id: usize) -> Res<Hash> {
let refs = BranchRefStore::open(branches_dir(data_dir))?;
let record = refs.get(name).ok_or("branch record not found")?;
let shard = record
.shards
.iter()
.find(|shard| shard.shard_id == shard_id)
.ok_or("shard not present in branch record")?;
Ok(shard.head)
}