use std::ops::Range;
use std::path::PathBuf;
use crate::error::{Error, Result};
use crate::log::{KeyMap, Log, LogConfig, TX_COMMIT_MARKER};
use std::rc::Rc;
pub const DEFAULT_PREALLOCATE_SIZE_BYTES: u64 = 1024 * 1024; pub const DEFAULT_PREALLOCATE_SIZE_MB: u64 = 1;
pub const DEFAULT_INITIAL_CAPACITY_KEYS: usize = 1_000;
pub const DEFAULT_COMPACTION_THRESHOLD_BYTES: u64 = 10 * 1024 * 1024;
pub const DEFAULT_COMPACTION_THRESHOLD_RATIO: f64 = 0.5;
pub const DEFAULT_COMPACTION_THRESHOLD_RATIO_STR: &str = "0.5";
pub const DEFAULT_COMPACTION_RATIO: f64 = 2.0;
pub const DEFAULT_COMPACTION_RATIO_STR: &str = "2.0";
#[derive(Debug, Clone)]
pub struct EngineConfig {
pub max_key_size: usize,
pub max_value_size: usize,
pub auto_compact: bool,
pub initial_capacity: Option<usize>,
pub preallocate_size: Option<u64>,
pub compaction_threshold_ratio: f64,
pub compaction_ratio: f64,
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
max_key_size: crate::log::DEFAULT_MAX_KEY_SIZE,
max_value_size: crate::log::DEFAULT_MAX_VALUE_SIZE,
auto_compact: true,
initial_capacity: Some(DEFAULT_INITIAL_CAPACITY_KEYS),
preallocate_size: Some(DEFAULT_PREALLOCATE_SIZE_BYTES),
compaction_threshold_ratio: DEFAULT_COMPACTION_THRESHOLD_RATIO,
compaction_ratio: DEFAULT_COMPACTION_RATIO,
}
}
}
pub struct StorageEngine {
log: Log,
key_map: KeyMap,
config: EngineConfig,
identifier: String, active_data_size: u64,
}
type ScanResult<'a> = Box<dyn Iterator<Item = (Vec<u8>, Rc<[u8]>)> + 'a>;
impl StorageEngine {
pub fn new(path: PathBuf) -> Result<Self> {
Self::with_config(path, EngineConfig::default())
}
pub fn with_config(path: PathBuf, config: EngineConfig) -> Result<Self> {
let path = ensure_teg_extension(&path)?;
let path_str = path.to_string_lossy().to_string();
Self::with_config_and_identifier(path_str, config)
}
pub fn with_config_and_identifier(identifier: String, config: EngineConfig) -> Result<Self> {
let log_config = LogConfig {
max_key_size: config.max_key_size,
max_value_size: config.max_value_size,
initial_capacity: config.initial_capacity,
preallocate_size: config.preallocate_size,
};
let mut log = Log::new(identifier.clone(), &log_config)?;
let (key_map, active_data_size) = log.build_key_map(&log_config)?;
if let Some(cap) = config.initial_capacity {
if key_map.len() > cap {
return Err(Error::OutOfMemoryQuota { max_keys: cap });
}
}
let mut engine = Self {
log,
key_map,
config,
identifier,
active_data_size,
};
if engine.config.auto_compact {
engine.compact()?;
}
Ok(engine)
}
pub fn begin_transaction(&mut self) -> Transaction<'_> {
Transaction {
engine: self,
undo_log: None, finalized: false,
}
}
pub fn get(&self, key: &[u8]) -> Option<Rc<[u8]>> {
self.key_map.get(key).cloned()
}
pub fn set(&mut self, key: &[u8], value: Vec<u8>) -> Result<()> {
if key.len() > self.config.max_key_size {
return Err(Error::KeyTooLarge(key.len()));
}
if value.len() > self.config.max_value_size {
return Err(Error::ValueTooLarge(value.len()));
}
if value.is_empty() {
return self.del(key);
}
let is_new_key = !self.key_map.contains_key(key);
if is_new_key {
if let Some(cap) = self.config.initial_capacity {
if self.key_map.len() >= cap {
return Err(Error::OutOfMemoryQuota { max_keys: cap });
}
}
}
if let Some(existing) = self.key_map.get(key) {
if existing.as_ref() == value.as_slice() {
return Ok(());
}
}
self.log.write_entry(key, &value)?;
let value_len = value.len() as u64;
let shared = Rc::from(value.into_boxed_slice());
if let Some(old_val) = self.key_map.insert(key.to_vec(), shared) {
self.active_data_size -= old_val.len() as u64;
} else {
self.active_data_size += key.len() as u64;
self.active_data_size += (crate::log::LENGTH_FIELD_BYTES * 2) as u64;
}
self.active_data_size += value_len;
self.check_compaction_trigger()?;
Ok(())
}
pub fn del(&mut self, key: &[u8]) -> Result<()> {
if !self.key_map.contains_key(key) {
return Ok(());
}
self.log.write_entry(key, &[])?;
if let Some(old_val) = self.key_map.remove(key) {
self.active_data_size -= old_val.len() as u64;
self.active_data_size -= key.len() as u64;
self.active_data_size -= (crate::log::LENGTH_FIELD_BYTES * 2) as u64;
}
self.check_compaction_trigger()?;
Ok(())
}
pub fn scan(&self, range: Range<Vec<u8>>) -> Result<ScanResult<'_>> {
let iter = self
.key_map
.range(range)
.map(|(key, value)| (key.clone(), Rc::clone(value)));
Ok(Box::new(iter))
}
pub fn flush(&mut self) -> Result<()> {
self.log.sync_all()
}
pub fn compact(&mut self) -> Result<()> {
let tmp_identifier = format!("{}.new", self.current_identifier());
let (mut new_log, new_key_map) = self.construct_log(tmp_identifier.clone())?;
new_log.rename_to(self.current_identifier())?;
let new_size = new_log.current_size()?;
self.log = new_log;
self.key_map = new_key_map;
self.active_data_size = new_size;
Ok(())
}
fn check_compaction_trigger(&mut self) -> Result<()> {
if !self.config.auto_compact {
return Ok(());
}
let log_size = self.log.current_size()?;
let threshold_bytes = self.effective_compaction_threshold_bytes();
if log_size <= threshold_bytes {
return Ok(());
}
if self.active_data_size == 0 {
if log_size > threshold_bytes {
return self.compact();
}
return Ok(());
}
let ratio = log_size as f64 / self.active_data_size as f64;
if ratio > self.config.compaction_ratio {
self.compact()?;
}
Ok(())
}
fn effective_compaction_threshold_bytes(&self) -> u64 {
if let Some(prealloc) = self.config.preallocate_size {
let ratio = if self.config.compaction_threshold_ratio <= 0.0 {
DEFAULT_COMPACTION_THRESHOLD_RATIO
} else {
self.config.compaction_threshold_ratio
};
let computed = (prealloc as f64 * ratio).round() as u64;
return computed.max(1);
}
DEFAULT_COMPACTION_THRESHOLD_BYTES
}
fn current_identifier(&self) -> String {
self.identifier.clone()
}
pub fn len(&self) -> usize {
self.key_map.len()
}
pub fn is_empty(&self) -> bool {
self.key_map.is_empty()
}
fn construct_log(&mut self, identifier: String) -> Result<(Log, KeyMap)> {
let mut new_key_map = KeyMap::new();
let log_config = LogConfig {
max_key_size: self.config.max_key_size,
max_value_size: self.config.max_value_size,
initial_capacity: self.config.initial_capacity,
preallocate_size: self.config.preallocate_size,
};
let mut new_log = Log::new(identifier, &log_config)?;
for (key, value) in &self.key_map {
new_log.write_entry(key, value.as_ref())?;
new_key_map.insert(key.clone(), value.clone());
}
Ok((new_log, new_key_map))
}
}
fn ensure_teg_extension(path: &std::path::Path) -> Result<std::path::PathBuf> {
if let Ok(meta) = std::fs::metadata(path) {
if meta.is_dir() {
return Err(crate::error::Error::Other(format!(
"Path points to a directory, expected file: {}",
path.display()
)));
}
}
if path.extension().is_none() {
let mut with_ext = path.to_path_buf();
with_ext.set_extension("teg");
Ok(with_ext)
} else {
if path.extension().and_then(|s| s.to_str()) != Some("teg") {
return Err(crate::error::Error::Other(format!(
"Unsupported database file extension. Expected '.teg': {}",
path.display()
)));
}
Ok(path.to_path_buf())
}
}
impl Drop for StorageEngine {
fn drop(&mut self) {
let _ = self.flush();
}
}
struct UndoEntry {
key: Vec<u8>,
old_value: Option<Rc<[u8]>>, }
pub struct Transaction<'a> {
engine: &'a mut StorageEngine,
undo_log: Option<Vec<UndoEntry>>, finalized: bool, }
impl Transaction<'_> {
fn record_undo(&mut self, key: &[u8]) -> Option<Rc<[u8]>> {
let old_value = self.engine.key_map.get(key).cloned();
if self.undo_log.is_none() {
self.undo_log = Some(Vec::new());
}
self.undo_log.as_mut().unwrap().push(UndoEntry {
key: key.to_vec(),
old_value: old_value.clone(),
});
old_value
}
pub fn set(&mut self, key: &[u8], value: Vec<u8>) -> Result<()> {
if key.len() > self.engine.config.max_key_size {
return Err(Error::KeyTooLarge(key.len()));
}
if value.len() > self.engine.config.max_value_size {
return Err(Error::ValueTooLarge(value.len()));
}
if value.is_empty() {
return self.delete(key);
}
if let Some(existing) = self.engine.key_map.get(key) {
if existing.as_ref() == value.as_slice() {
return Ok(());
}
}
self.record_undo(key);
let result = self.engine.set(key, value);
if result.is_err() {
if let Some(ref mut log) = self.undo_log {
log.pop();
}
}
result
}
pub fn delete(&mut self, key: &[u8]) -> Result<()> {
if !self.engine.key_map.contains_key(key) {
return Ok(());
}
self.record_undo(key);
let result = self.engine.del(key);
if result.is_err() {
if let Some(ref mut log) = self.undo_log {
log.pop();
}
}
result
}
pub fn get(&self, key: &[u8]) -> Option<Rc<[u8]>> {
self.engine.get(key)
}
pub fn scan(&self, range: Range<Vec<u8>>) -> Result<ScanResult<'_>> {
self.engine.scan(range)
}
pub fn has_pending_operations(&self) -> bool {
!self.finalized && self.undo_log.as_ref().is_some_and(|log| !log.is_empty())
}
pub fn is_clean(&self) -> bool {
self.finalized || self.undo_log.as_ref().is_none_or(|log| log.is_empty())
}
pub fn is_finalized(&self) -> bool {
self.finalized
}
pub fn commit(&mut self) -> Result<()> {
if self.finalized {
return Err(Error::Other("Transaction already finalized".to_string()));
}
let has_writes = self.undo_log.as_ref().is_some_and(|log| !log.is_empty());
if has_writes {
self.engine.log.write_entry(TX_COMMIT_MARKER, &[])?;
if let Some(ref mut log) = self.undo_log {
log.clear();
}
}
self.finalized = true;
Ok(())
}
pub fn rollback(&mut self) -> Result<()> {
if self.finalized {
return Err(Error::Other("Transaction already finalized".to_string()));
}
let has_operations = self.undo_log.as_ref().is_some_and(|log| !log.is_empty());
if !has_operations {
self.finalized = true;
return Ok(());
}
if let Some(ref mut log) = self.undo_log {
for undo_entry in log.drain(..).rev() {
if let Some(old_value) = undo_entry.old_value {
self.engine.set(&undo_entry.key, old_value.to_vec())?;
} else {
self.engine.del(&undo_entry.key)?;
}
}
}
self.finalized = true;
Ok(())
}
}
impl Drop for Transaction<'_> {
fn drop(&mut self) {
if !self.finalized {
let _ = self.rollback(); }
}
}