mod error; mod location; mod lookup; mod backend; pub use error::Error; pub use location::Location; pub use lookup::LookupTable; use std::fs::File; use std::path::PathBuf; /// OurDB is a lightweight, efficient key-value database implementation that provides /// data persistence with history tracking capabilities. pub struct OurDB { /// Directory path for storage path: PathBuf, /// Whether to use auto-increment mode incremental_mode: bool, /// Maximum file size (default: 500MB) file_size: u32, /// Lookup table for mapping keys to locations lookup: LookupTable, /// Currently open file file: Option, /// Current file number file_nr: u16, /// Last used file number last_used_file_nr: u16, } /// Configuration for creating a new OurDB instance pub struct OurDBConfig { /// Directory path for storage pub path: PathBuf, /// Whether to use auto-increment mode pub incremental_mode: bool, /// Maximum file size (default: 500MB) pub file_size: Option, /// Lookup table key size pub keysize: Option, } /// Arguments for setting a value in OurDB pub struct OurDBSetArgs<'a> { /// ID for the record (optional in incremental mode) pub id: Option, /// Data to store pub data: &'a [u8], } impl OurDB { /// Creates a new OurDB instance with the given configuration pub fn new(config: OurDBConfig) -> Result { // Create directory if it doesn't exist std::fs::create_dir_all(&config.path)?; // Create lookup table let lookup_path = config.path.join("lookup"); std::fs::create_dir_all(&lookup_path)?; let lookup_config = lookup::LookupConfig { size: 1000000, // Default size keysize: config.keysize.unwrap_or(4), lookuppath: lookup_path.to_string_lossy().to_string(), incremental_mode: config.incremental_mode, }; let lookup = LookupTable::new(lookup_config)?; let mut db = OurDB { path: config.path, incremental_mode: config.incremental_mode, file_size: config.file_size.unwrap_or(500 * (1 << 20)), // 500MB default lookup, file: None, file_nr: 0, last_used_file_nr: 0, }; // Load existing metadata if available db.load()?; Ok(db) } /// Sets a value in the database /// /// In incremental mode: /// - If ID is provided, it updates an existing record /// - If ID is not provided, it creates a new record with auto-generated ID /// /// In key-value mode: /// - ID must be provided pub fn set(&mut self, args: OurDBSetArgs) -> Result { if self.incremental_mode { if let Some(id) = args.id { // This is an update let location = self.lookup.get(id)?; if location.position == 0 { return Err(Error::InvalidOperation( "Cannot set ID for insertions when incremental mode is enabled".to_string() )); } self.set_(id, location, args.data)?; Ok(id) } else { // This is an insert let id = self.lookup.get_next_id()?; self.set_(id, Location::default(), args.data)?; Ok(id) } } else { // Using key-value mode let id = args.id.ok_or_else(|| Error::InvalidOperation( "ID must be provided when incremental is disabled".to_string() ))?; let location = self.lookup.get(id)?; self.set_(id, location, args.data)?; Ok(id) } } /// Retrieves data stored at the specified key position pub fn get(&mut self, id: u32) -> Result, Error> { let location = self.lookup.get(id)?; self.get_(location) } /// Retrieves a list of previous values for the specified key /// /// The depth parameter controls how many historical values to retrieve (maximum) pub fn get_history(&mut self, id: u32, depth: u8) -> Result>, Error> { let mut result = Vec::new(); let mut current_location = self.lookup.get(id)?; // Traverse the history chain up to specified depth for _ in 0..depth { // Get current value let data = self.get_(current_location)?; result.push(data); // Try to get previous location match self.get_prev_pos_(current_location) { Ok(location) => { if location.position == 0 { break; } current_location = location; } Err(_) => break, } } Ok(result) } /// Deletes the data at the specified key position pub fn delete(&mut self, id: u32) -> Result<(), Error> { let location = self.lookup.get(id)?; self.delete_(id, location)?; self.lookup.delete(id)?; Ok(()) } /// Returns the next ID which will be used when storing in incremental mode pub fn get_next_id(&mut self) -> Result { if !self.incremental_mode { return Err(Error::InvalidOperation("Incremental mode is not enabled".to_string())); } self.lookup.get_next_id() } /// Closes the database, ensuring all data is saved pub fn close(&mut self) -> Result<(), Error> { self.save()?; self.close_(); Ok(()) } /// Destroys the database, removing all files pub fn destroy(&mut self) -> Result<(), Error> { let _ = self.close(); std::fs::remove_dir_all(&self.path)?; Ok(()) } // Helper methods fn lookup_dump_path(&self) -> PathBuf { self.path.join("lookup_dump.db") } fn load(&mut self) -> Result<(), Error> { let dump_path = self.lookup_dump_path(); if dump_path.exists() { self.lookup.import_sparse(&dump_path.to_string_lossy())?; } Ok(()) } fn save(&mut self) -> Result<(), Error> { self.lookup.export_sparse(&self.lookup_dump_path().to_string_lossy())?; Ok(()) } fn close_(&mut self) { self.file = None; } } #[cfg(test)] mod tests { use super::*; use std::env::temp_dir; use std::time::{SystemTime, UNIX_EPOCH}; fn get_temp_dir() -> PathBuf { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); temp_dir().join(format!("ourdb_test_{}", timestamp)) } #[test] fn test_basic_operations() { let temp_dir = get_temp_dir(); let config = OurDBConfig { path: temp_dir.clone(), incremental_mode: true, file_size: None, keysize: None, }; let mut db = OurDB::new(config).unwrap(); // Test set and get let test_data = b"Hello, OurDB!"; let id = db.set(OurDBSetArgs { id: None, data: test_data }).unwrap(); let retrieved = db.get(id).unwrap(); assert_eq!(retrieved, test_data); // Test update let updated_data = b"Updated data"; db.set(OurDBSetArgs { id: Some(id), data: updated_data }).unwrap(); let retrieved = db.get(id).unwrap(); assert_eq!(retrieved, updated_data); // Test history let history = db.get_history(id, 2).unwrap(); assert_eq!(history.len(), 2); assert_eq!(history[0], updated_data); assert_eq!(history[1], test_data); // Test delete db.delete(id).unwrap(); assert!(db.get(id).is_err()); // Clean up db.destroy().unwrap(); } }