use std::fs;
use std::path::{Path, PathBuf};
use crate::shard::router::{SHARD_STORE_DIR, SHARD_WAL_FILE, shard_dir, shard_index_for};
use crate::store::{DiskStore, StoreError};
use crate::tree::{Cursor, Hash, TreeError};
use crate::ttl::{TtlDecodeError, Visibility, visible_value};
use crate::wal::{WalError, WalRecovery};
use super::DatabaseError;
use super::config::read_config;
pub type ObservedEntry = (Vec<u8>, Vec<u8>);
#[derive(Debug)]
pub struct ReadOnlyDatabase {
data_dir: PathBuf,
shard_count: usize,
}
impl ReadOnlyDatabase {
pub fn open(path: impl AsRef<Path>) -> Result<Self, ObserverError> {
let data_dir = path.as_ref().to_path_buf();
let config = read_config(&data_dir).map_err(ObserverError::Config)?;
if config.shard_count == 0 {
return Err(ObserverError::Config(DatabaseError::InvalidShardCount));
}
Ok(Self {
data_dir,
shard_count: config.shard_count,
})
}
#[must_use]
pub const fn shard_count(&self) -> usize {
self.shard_count
}
#[must_use]
pub fn shard_for(&self, key: &[u8]) -> usize {
shard_index_for(key, self.shard_count)
}
pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, ObserverError> {
let Some((store, root)) = self.committed_shard_state(self.shard_for(key))? else {
return Ok(None);
};
let Some(encoded) = Cursor::new(&store, root).get(key)? else {
return Ok(None);
};
Ok(visible_value(&encoded)?.into_option())
}
pub fn range(&self, from: &[u8], to: &[u8]) -> Result<Vec<ObservedEntry>, ObserverError> {
if from >= to {
return Ok(Vec::new());
}
let Some((store, root)) = self.committed_shard_state(self.shard_for(from))? else {
return Ok(Vec::new());
};
let cursor = Cursor::new(&store, root);
let mut entries = Vec::new();
for item in cursor.range(from, to) {
let (key, encoded) = item?;
if let Visibility::Live(value) = visible_value(&encoded)? {
entries.push((key, value));
}
}
Ok(entries)
}
pub fn committed_root(&self, shard_id: usize) -> Result<Option<Hash>, ObserverError> {
if shard_id >= self.shard_count {
return Err(ObserverError::ShardOutOfRange {
shard_id,
shard_count: self.shard_count,
});
}
Ok(self
.committed_shard_state(shard_id)?
.map(|(_store, root)| root))
}
fn committed_shard_state(
&self,
shard_id: usize,
) -> Result<Option<(DiskStore, Hash)>, ObserverError> {
let shard_dir = shard_dir(&self.data_dir, shard_id);
let store_dir = shard_dir.join(SHARD_STORE_DIR);
match fs::metadata(&store_dir) {
Ok(metadata) if metadata.is_dir() => {}
Ok(_metadata) => {
return Err(ObserverError::Store(StoreError::NotADirectory {
path: store_dir,
}));
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(ObserverError::Store(StoreError::Io(error))),
}
let store = DiskStore::new(&store_dir)?;
let recovered = WalRecovery::recover_path(shard_dir.join(SHARD_WAL_FILE), &store)?;
Ok(recovered.committed_root().map(|root| (store, root)))
}
}
#[derive(Debug)]
pub enum ObserverError {
Config(DatabaseError),
Store(StoreError),
Wal(WalError),
Tree(TreeError),
Ttl(TtlDecodeError),
ShardOutOfRange { shard_id: usize, shard_count: usize },
}
impl std::fmt::Display for ObserverError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Config(error) => write!(formatter, "observer open failed: {error}"),
Self::Store(error) => write!(formatter, "observer store error: {error}"),
Self::Wal(error) => write!(formatter, "observer wal error: {error}"),
Self::Tree(error) => write!(formatter, "observer tree error: {error}"),
Self::Ttl(error) => write!(formatter, "observer ttl decode error: {error}"),
Self::ShardOutOfRange {
shard_id,
shard_count,
} => write!(
formatter,
"shard id {shard_id} out of range for shard_count {shard_count}"
),
}
}
}
impl std::error::Error for ObserverError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Config(error) => Some(error),
Self::Store(error) => Some(error),
Self::Wal(error) => Some(error),
Self::Tree(error) => Some(error),
Self::Ttl(error) => Some(error),
Self::ShardOutOfRange { .. } => None,
}
}
}
impl From<StoreError> for ObserverError {
fn from(error: StoreError) -> Self {
Self::Store(error)
}
}
impl From<WalError> for ObserverError {
fn from(error: WalError) -> Self {
Self::Wal(error)
}
}
impl From<TreeError> for ObserverError {
fn from(error: TreeError) -> Self {
Self::Tree(error)
}
}
impl From<TtlDecodeError> for ObserverError {
fn from(error: TtlDecodeError) -> Self {
Self::Ttl(error)
}
}