use rocksdb::{DB, Options, WriteBatch, IteratorMode};
use std::path::PathBuf;
use crate::types::*;
#[derive(Debug)]
pub struct LSMTree {
db: DB,
data_dir: PathBuf,
}
impl LSMTree {
pub fn new(data_dir: PathBuf, memtable_size: usize, sstable_max_size: usize) -> Result<Self> {
std::fs::create_dir_all(&data_dir)?;
let mut opts = Options::default();
opts.create_if_missing(true);
opts.set_max_write_buffer_number(3);
opts.set_write_buffer_size(memtable_size * 1024 * 1024); opts.set_target_file_size_base(sstable_max_size * 1024 * 1024); opts.set_max_background_compactions(4);
opts.set_max_background_flushes(2);
let db = DB::open(&opts, &data_dir)
.map_err(|e| KotobaError::Storage(format!("Failed to open RocksDB: {}", e)))?;
Ok(Self {
db,
data_dir,
})
}
pub fn put(&mut self, key: String, value: Vec<u8>) -> Result<()> {
self.db.put(key, value)
.map_err(|e| KotobaError::Storage(format!("Failed to put data: {}", e)))?;
Ok(())
}
pub fn delete(&mut self, key: String) -> Result<()> {
self.db.delete(key)
.map_err(|e| KotobaError::Storage(format!("Failed to delete data: {}", e)))?;
Ok(())
}
pub fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
match self.db.get(key) {
Ok(Some(value)) => Ok(Some(value)),
Ok(None) => Ok(None),
Err(e) => Err(KotobaError::Storage(format!("Failed to get data: {}", e))),
}
}
pub fn compact(&mut self) -> Result<()> {
self.db.compact_range(None::<&[u8]>, None::<&[u8]>);
Ok(())
}
pub fn create_snapshot(&self, snapshot_id: &str) -> Result<()> {
let snapshot = self.db.snapshot();
let snapshot_dir = self.data_dir.join(format!("snapshot_{}", snapshot_id));
std::fs::create_dir_all(&snapshot_dir)?;
let iter = snapshot.iterator(IteratorMode::Start);
let mut batch = WriteBatch::default();
for item in iter {
let (key, value) = item?;
batch.put(key, value);
}
let mut opts = Options::default();
opts.create_if_missing(true);
let snapshot_db = DB::open(&opts, &snapshot_dir)?;
snapshot_db.write(batch)?;
Ok(())
}
pub fn restore_from_snapshot(&mut self, snapshot_id: &str) -> Result<()> {
let snapshot_dir = self.data_dir.join(format!("snapshot_{}", snapshot_id));
if !snapshot_dir.exists() {
return Err(KotobaError::Storage("Snapshot not found".to_string()));
}
self.clear_all()?;
let mut opts = Options::default();
opts.create_if_missing(false);
let snapshot_db = DB::open(&opts, &snapshot_dir)?;
let iter = snapshot_db.iterator(IteratorMode::Start);
for item in iter {
let (key, value) = item?;
self.db.put(key, value)?;
}
Ok(())
}
fn clear_all(&mut self) -> Result<()> {
let iter = self.db.iterator(IteratorMode::Start);
let keys: Vec<Vec<u8>> = iter.map(|item| item.unwrap().0).collect();
for key in keys {
self.db.delete(key)?;
}
Ok(())
}
pub fn cleanup(&mut self, cutoff_timestamp: u64) -> Result<()> {
Ok(())
}
pub fn stats(&self) -> LSMStats {
let total_entries = self.db.iterator(IteratorMode::Start).count() as usize;
let total_size = self.db
.property_value("rocksdb.estimate-live-data-size")
.unwrap_or(Some("0".to_string()))
.unwrap_or("0".to_string())
.parse::<u64>()
.unwrap_or(0);
LSMStats {
memtable_entries: 0, sstable_count: self.db
.property_value("rocksdb.num-files-at-level0")
.unwrap_or(Some("0".to_string()))
.unwrap_or("0".to_string())
.parse::<usize>()
.unwrap_or(0),
total_entries,
total_size,
}
}
}
#[derive(Debug, Clone)]
pub struct LSMStats {
pub memtable_entries: usize,
pub sstable_count: usize,
pub total_entries: usize,
pub total_size: u64,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_db() -> (LSMTree, TempDir) {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().to_path_buf();
let lsm_tree = LSMTree::new(db_path, 64, 128).unwrap();
(lsm_tree, temp_dir)
}
#[test]
fn test_put_and_get() {
let (mut lsm_tree, _temp_dir) = create_test_db();
lsm_tree.put("key1".to_string(), b"value1".to_vec()).unwrap();
lsm_tree.put("key2".to_string(), b"value2".to_vec()).unwrap();
assert_eq!(lsm_tree.get("key1").unwrap(), Some(b"value1".to_vec()));
assert_eq!(lsm_tree.get("key2").unwrap(), Some(b"value2".to_vec()));
assert_eq!(lsm_tree.get("key3").unwrap(), None);
}
#[test]
fn test_delete() {
let (mut lsm_tree, _temp_dir) = create_test_db();
lsm_tree.put("key1".to_string(), b"value1".to_vec()).unwrap();
assert_eq!(lsm_tree.get("key1").unwrap(), Some(b"value1".to_vec()));
lsm_tree.delete("key1".to_string()).unwrap();
assert_eq!(lsm_tree.get("key1").unwrap(), None);
}
#[test]
fn test_compaction() {
let (mut lsm_tree, _temp_dir) = create_test_db();
for i in 0..100 {
lsm_tree.put(format!("key{}", i), format!("value{}", i).into_bytes()).unwrap();
}
lsm_tree.compact().unwrap();
for i in 0..100 {
let expected = format!("value{}", i).into_bytes();
assert_eq!(lsm_tree.get(&format!("key{}", i)).unwrap(), Some(expected));
}
}
#[test]
fn test_stats() {
let (mut lsm_tree, _temp_dir) = create_test_db();
let stats = lsm_tree.stats();
assert_eq!(stats.memtable_entries, 0);
lsm_tree.put("key1".to_string(), b"value1".to_vec()).unwrap();
lsm_tree.put("key2".to_string(), b"value2".to_vec()).unwrap();
let stats = lsm_tree.stats();
assert!(stats.total_entries >= 2); }
#[test]
fn test_snapshot() {
let (mut lsm_tree, temp_dir) = create_test_db();
lsm_tree.put("key1".to_string(), b"value1".to_vec()).unwrap();
lsm_tree.put("key2".to_string(), b"value2".to_vec()).unwrap();
lsm_tree.create_snapshot("test_snapshot").unwrap();
lsm_tree.put("key1".to_string(), b"modified_value1".to_vec()).unwrap();
lsm_tree.delete("key2".to_string()).unwrap();
assert_eq!(lsm_tree.get("key1").unwrap(), Some(b"modified_value1".to_vec()));
assert_eq!(lsm_tree.get("key2").unwrap(), None);
lsm_tree.restore_from_snapshot("test_snapshot").unwrap();
assert_eq!(lsm_tree.get("key1").unwrap(), Some(b"value1".to_vec()));
assert_eq!(lsm_tree.get("key2").unwrap(), Some(b"value2".to_vec()));
}
}