268 lines
7.8 KiB
Rust
268 lines
7.8 KiB
Rust
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<File>,
|
|
/// 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<u32>,
|
|
/// Lookup table key size
|
|
pub keysize: Option<u8>,
|
|
}
|
|
|
|
/// Arguments for setting a value in OurDB
|
|
pub struct OurDBSetArgs<'a> {
|
|
/// ID for the record (optional in incremental mode)
|
|
pub id: Option<u32>,
|
|
/// Data to store
|
|
pub data: &'a [u8],
|
|
}
|
|
|
|
impl OurDB {
|
|
/// Creates a new OurDB instance with the given configuration
|
|
pub fn new(config: OurDBConfig) -> Result<Self, Error> {
|
|
// 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<u32, Error> {
|
|
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<Vec<u8>, 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<Vec<Vec<u8>>, 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<u32, Error> {
|
|
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();
|
|
}
|
|
}
|