This commit is contained in:
kristof 2025-04-03 08:47:35 +02:00
parent cf6c52a2bc
commit 30dade3d06
25 changed files with 3354 additions and 3 deletions

View File

@ -1,3 +0,0 @@
# db
code for working with the databases inside hero

15
herodb/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "herodb"
version = "0.1.0"
edition = "2024"
description = "A database library built on top of sled with model support"
license = "MIT"
authors = ["HeroCode Team"]
[dependencies]
sled = "0.34.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
uuid = { version = "1.3", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }

View File

@ -0,0 +1,25 @@
pub mod user;
pub mod vote;
pub mod company;
pub mod meeting;
pub mod product;
pub mod sale;
pub mod shareholder;
// pub mod db; // Moved to src/zaz/db
// pub mod migration; // Removed
// Re-export all model types for convenience
pub use user::User;
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
pub use company::{Company, CompanyStatus, BusinessType};
pub use meeting::Meeting;
pub use product::{Product, Currency, ProductComponent, ProductType, ProductStatus};
pub use sale::Sale;
pub use shareholder::Shareholder;
// Re-export database components
// pub use db::{DB, DBError, DBResult, Model, ModelMetadata}; // Removed old DB re-exports
pub use crate::db::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel}; // Re-export Sled DB components
// Re-export migration components - Removed
// pub use migration::{Migrator, MigrationError, MigrationResult};

View File

@ -0,0 +1,8 @@
// Declare the models submodule
#[path = "models/lib.rs"] // Tell compiler where to find models module source
pub mod models;
// Declare the db submodule with the new database implementation
#[path = "db/mod.rs"] // Tell compiler where to find db module source
pub mod db;

147
herodb/src/core/base.rs Normal file
View File

@ -0,0 +1,147 @@
use bincode;
use brotli::{CompressorReader, Decompressor};
use serde::{Deserialize, Serialize};
use sled;
use std::fmt::Debug;
use std::io::Read;
use std::marker::PhantomData;
use std::path::Path;
use thiserror::Error;
/// Errors that can occur during Sled database operations
#[derive(Error, Debug)]
pub enum SledDBError {
#[error("Sled database error: {0}")]
SledError(#[from] sled::Error),
#[error("Serialization/Deserialization error: {0}")]
SerdeError(#[from] bincode::Error),
#[error("Compression/Decompression error: {0}")]
IoError(#[from] std::io::Error),
#[error("Record not found for ID: {0}")]
NotFound(String),
#[error("Type mismatch during deserialization")]
TypeError,
#[error("General database error: {0}")]
GeneralError(String),
}
/// Result type for Sled DB operations
pub type SledDBResult<T> = Result<T, SledDBError>;
/// Trait for models that can be stored in the Sled database.
/// Requires `Serialize` and `Deserialize` for the underlying storage mechanism.
pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized {
/// Serializes and compresses the instance using bincode and brotli.
fn dump(&self) -> SledDBResult<Vec<u8>> {
let encoded: Vec<u8> = bincode::serialize(self)?;
let mut compressed = Vec::new();
// Default Brotli parameters: quality 5, lgwin 22 (window size)
const BROTLI_QUALITY: u32 = 5;
const BROTLI_LGWIN: u32 = 22;
const BUFFER_SIZE: usize = 4096; // 4KB buffer
let mut compressor = CompressorReader::new(
&encoded[..],
BUFFER_SIZE,
BROTLI_QUALITY,
BROTLI_LGWIN
);
compressor.read_to_end(&mut compressed)?;
Ok(compressed)
}
/// Deserializes and decompresses data from bytes into an instance.
fn load_from_bytes(data: &[u8]) -> SledDBResult<Self> {
let mut decompressed = Vec::new();
const BUFFER_SIZE: usize = 4096; // 4KB buffer
let mut decompressor = Decompressor::new(data, BUFFER_SIZE);
decompressor.read_to_end(&mut decompressed)?;
let decoded: Self = bincode::deserialize(&decompressed)?;
Ok(decoded)
}
}
/// Trait identifying a model suitable for the Sled database.
/// The 'static lifetime bound is required for type identification via Any
pub trait SledModel: Storable + Debug + Clone + Send + Sync + 'static {
/// Returns the unique ID for this model instance, used as the key in Sled.
fn get_id(&self) -> String;
/// Returns a prefix used for this model type in the Sled database.
/// Helps to logically separate different model types.
fn db_prefix() -> &'static str;
}
/// A generic database layer on top of Sled.
#[derive(Clone)]
pub struct SledDB<T: SledModel> {
db: sled::Db,
_phantom: PhantomData<T>,
}
impl<T: SledModel> SledDB<T> {
/// Opens or creates a Sled database at the specified path.
pub fn open<P: AsRef<Path>>(path: P) -> SledDBResult<Self> {
let db = sled::open(path)?;
Ok(Self {
db,
_phantom: PhantomData,
})
}
/// Generates the full Sled key using the model's prefix and ID.
fn get_full_key(id: &str) -> Vec<u8> {
format!("{}:{}", T::db_prefix(), id).into_bytes()
}
/// Inserts or updates a model instance in the database.
pub fn insert(&self, model: &T) -> SledDBResult<()> {
let key = Self::get_full_key(&model.get_id());
let value = model.dump()?;
self.db.insert(key, value)?;
// Optionally force a disk flush for durability, but it impacts performance.
// self.db.flush()?;
Ok(())
}
/// Retrieves a model instance by its ID.
pub fn get(&self, id: &str) -> SledDBResult<T> {
let key = Self::get_full_key(id);
match self.db.get(&key)? {
Some(ivec) => T::load_from_bytes(&ivec),
None => Err(SledDBError::NotFound(id.to_string())),
}
}
/// Deletes a model instance by its ID.
pub fn delete(&self, id: &str) -> SledDBResult<()> {
let key = Self::get_full_key(id);
match self.db.remove(&key)? {
Some(_) => Ok(()),
None => Err(SledDBError::NotFound(id.to_string())),
}
// Optionally flush after delete
// self.db.flush()?;
}
/// Lists all models of this type.
/// Warning: This can be inefficient for large datasets as it loads all models into memory.
pub fn list(&self) -> SledDBResult<Vec<T>> {
let prefix = format!("{}:", T::db_prefix());
let mut models = Vec::new();
for result in self.db.scan_prefix(prefix.as_bytes()) {
let (_key, value) = result?;
models.push(T::load_from_bytes(&value)?);
}
Ok(models)
}
/// Provides access to the underlying Sled Db instance for advanced operations.
pub fn raw_db(&self) -> &sled::Db {
&self.db
}
}

628
herodb/src/core/db.rs Normal file
View File

@ -0,0 +1,628 @@
use crate::zaz::models::*;
use std::any::TypeId;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, RwLock};
/// Main DB manager that automatically handles all root models
pub struct DB {
db_path: PathBuf,
user_db: SledDB<User>,
company_db: SledDB<Company>,
meeting_db: SledDB<Meeting>,
product_db: SledDB<Product>,
sale_db: SledDB<Sale>,
vote_db: SledDB<Vote>,
shareholder_db: SledDB<Shareholder>,
// Type map for generic operations
type_map: HashMap<TypeId, Box<dyn AnyDbOperations>>,
// Locks to ensure thread safety for key areas
_write_locks: Arc<Mutex<HashMap<String, bool>>>,
// Transaction state
transaction: RwLock<Option<TransactionState>>,
}
impl DB {
/// Create a new DB instance with all model databases
pub fn new<P: Into<PathBuf>>(base_path: P) -> SledDBResult<Self> {
let base_path = base_path.into();
// Ensure base directory exists
if !base_path.exists() {
std::fs::create_dir_all(&base_path)?;
}
// Create individual database instances for each model type
let user_db = SledDB::open(base_path.join("user"))?;
let company_db = SledDB::open(base_path.join("company"))?;
let meeting_db = SledDB::open(base_path.join("meeting"))?;
let product_db = SledDB::open(base_path.join("product"))?;
let sale_db = SledDB::open(base_path.join("sale"))?;
let vote_db = SledDB::open(base_path.join("vote"))?;
let shareholder_db = SledDB::open(base_path.join("shareholder"))?;
// Create type map for generic operations
let mut type_map: HashMap<TypeId, Box<dyn AnyDbOperations>> = HashMap::new();
type_map.insert(TypeId::of::<User>(), Box::new(user_db.clone()));
type_map.insert(TypeId::of::<Company>(), Box::new(company_db.clone()));
type_map.insert(TypeId::of::<Meeting>(), Box::new(meeting_db.clone()));
type_map.insert(TypeId::of::<Product>(), Box::new(product_db.clone()));
type_map.insert(TypeId::of::<Sale>(), Box::new(sale_db.clone()));
type_map.insert(TypeId::of::<Vote>(), Box::new(vote_db.clone()));
type_map.insert(TypeId::of::<Shareholder>(), Box::new(shareholder_db.clone()));
let _write_locks = Arc::new(Mutex::new(HashMap::new()));
let transaction = RwLock::new(None);
Ok(Self {
db_path: base_path,
user_db,
company_db,
meeting_db,
product_db,
sale_db,
vote_db,
shareholder_db,
type_map,
_write_locks,
transaction,
})
}
// Transaction-related methods
/// Begin a new transaction
pub fn begin_transaction(&self) -> SledDBResult<()> {
let mut tx = self.transaction.write().unwrap();
if tx.is_some() {
return Err(SledDBError::GeneralError("Transaction already in progress".into()));
}
*tx = Some(TransactionState::new());
Ok(())
}
/// Check if a transaction is active
pub fn has_active_transaction(&self) -> bool {
let tx = self.transaction.read().unwrap();
tx.is_some() && tx.as_ref().unwrap().active
}
/// Apply a set operation with the serialized data - bypass transaction check
fn apply_set_operation(&self, model_type: TypeId, serialized: &[u8]) -> SledDBResult<()> {
// User model
if model_type == TypeId::of::<User>() {
let model: User = bincode::deserialize(serialized)?;
// Access the database operations directly to avoid transaction recursion
if let Some(db_ops) = self.type_map.get(&TypeId::of::<User>()) {
return db_ops.insert_any(&model);
}
}
// Company model
else if model_type == TypeId::of::<Company>() {
let model: Company = bincode::deserialize(serialized)?;
if let Some(db_ops) = self.type_map.get(&TypeId::of::<Company>()) {
return db_ops.insert_any(&model);
}
}
// Meeting model
else if model_type == TypeId::of::<Meeting>() {
let model: Meeting = bincode::deserialize(serialized)?;
if let Some(db_ops) = self.type_map.get(&TypeId::of::<Meeting>()) {
return db_ops.insert_any(&model);
}
}
// Product model
else if model_type == TypeId::of::<Product>() {
let model: Product = bincode::deserialize(serialized)?;
if let Some(db_ops) = self.type_map.get(&TypeId::of::<Product>()) {
return db_ops.insert_any(&model);
}
}
// Sale model
else if model_type == TypeId::of::<Sale>() {
let model: Sale = bincode::deserialize(serialized)?;
if let Some(db_ops) = self.type_map.get(&TypeId::of::<Sale>()) {
return db_ops.insert_any(&model);
}
}
// Vote model
else if model_type == TypeId::of::<Vote>() {
let model: Vote = bincode::deserialize(serialized)?;
if let Some(db_ops) = self.type_map.get(&TypeId::of::<Vote>()) {
return db_ops.insert_any(&model);
}
}
// Shareholder model
else if model_type == TypeId::of::<Shareholder>() {
let model: Shareholder = bincode::deserialize(serialized)?;
if let Some(db_ops) = self.type_map.get(&TypeId::of::<Shareholder>()) {
return db_ops.insert_any(&model);
}
}
Err(SledDBError::TypeError)
}
/// Commit the current transaction, applying all operations
pub fn commit_transaction(&self) -> SledDBResult<()> {
let mut tx_guard = self.transaction.write().unwrap();
if let Some(tx_state) = tx_guard.take() {
if !tx_state.active {
return Err(SledDBError::GeneralError("Transaction not active".into()));
}
// Execute all operations in the transaction
for op in tx_state.operations {
match op {
DbOperation::Set { model_type, serialized } => {
self.apply_set_operation(model_type, &serialized)?;
},
DbOperation::Delete { model_type, id } => {
let db_ops = self.type_map.get(&model_type)
.ok_or_else(|| SledDBError::TypeError)?;
db_ops.delete(&id)?;
}
}
}
Ok(())
} else {
Err(SledDBError::GeneralError("No active transaction".into()))
}
}
/// Rollback the current transaction, discarding all operations
pub fn rollback_transaction(&self) -> SledDBResult<()> {
let mut tx = self.transaction.write().unwrap();
if tx.is_none() {
return Err(SledDBError::GeneralError("No active transaction".into()));
}
*tx = None;
Ok(())
}
/// Get the path to the database
pub fn path(&self) -> &PathBuf {
&self.db_path
}
// Generic methods that work with any supported model type
/// Insert a model instance into its appropriate database based on type
pub fn set<T: SledModel>(&self, model: &T) -> SledDBResult<()> {
// Try to acquire a write lock on the transaction
let mut tx_guard = self.transaction.write().unwrap();
// Check if there's an active transaction
if let Some(tx_state) = tx_guard.as_mut() {
if tx_state.active {
// Serialize the model for later use
let serialized = bincode::serialize(model)?;
// Record a Set operation in the transaction
tx_state.operations.push(DbOperation::Set {
model_type: TypeId::of::<T>(),
serialized,
});
return Ok(());
}
}
// If we got here, either there's no transaction or it's not active
// Drop the write lock before doing a direct database operation
drop(tx_guard);
// Execute directly
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => db_ops.insert_any(model),
None => Err(SledDBError::TypeError),
}
}
/// Check the transaction state for the given type and id
fn check_transaction<T: SledModel>(&self, id: &str) -> Option<Result<Option<T>, SledDBError>> {
// Try to acquire a read lock on the transaction
let tx_guard = self.transaction.read().unwrap();
if let Some(tx_state) = tx_guard.as_ref() {
if !tx_state.active {
return None;
}
let type_id = TypeId::of::<T>();
let id_str = id.to_string();
// Process operations in reverse order (last operation wins)
for op in tx_state.operations.iter().rev() {
match op {
// First check if this ID has been deleted in the transaction
DbOperation::Delete { model_type, id: op_id } => {
if *model_type == type_id && op_id == id {
// Return NotFound error for deleted records
return Some(Err(SledDBError::NotFound(id.to_string())));
}
},
// Then check if it has been set in the transaction
DbOperation::Set { model_type, serialized } => {
if *model_type == type_id {
// Deserialize to check the ID
if let Ok(model) = bincode::deserialize::<T>(serialized) {
if model.get_id() == id_str {
return Some(Ok(Some(model)));
}
}
}
}
}
}
}
// Not found in transaction (continue to database)
None
}
/// Get a model instance by its ID and type
pub fn get<T: SledModel>(&self, id: &str) -> SledDBResult<T> {
// First check if there's a pending value in the current transaction
if let Some(tx_result) = self.check_transaction::<T>(id) {
match tx_result {
Ok(Some(model)) => return Ok(model),
Err(e) => return Err(e),
Ok(None) => {} // Should never happen
}
}
// If no pending value, look up from the database
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
let result_any = db_ops.get_any(id)?;
// We expect the result to be of type T since we looked it up by TypeId
match result_any.downcast::<T>() {
Ok(t) => Ok(*t),
Err(_) => Err(SledDBError::TypeError),
}
},
None => Err(SledDBError::TypeError),
}
}
/// Delete a model instance by its ID and type
pub fn delete<T: SledModel>(&self, id: &str) -> SledDBResult<()> {
// Try to acquire a write lock on the transaction
let mut tx_guard = self.transaction.write().unwrap();
// Check if there's an active transaction
if let Some(tx_state) = tx_guard.as_mut() {
if tx_state.active {
// Record a Delete operation in the transaction
tx_state.operations.push(DbOperation::Delete {
model_type: TypeId::of::<T>(),
id: id.to_string(),
});
return Ok(());
}
}
// If we got here, either there's no transaction or it's not active
// Drop the write lock before doing a direct database operation
drop(tx_guard);
// Execute directly
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => db_ops.delete(id),
None => Err(SledDBError::TypeError),
}
}
/// List all model instances of a specific type
pub fn list<T: SledModel>(&self) -> SledDBResult<Vec<T>> {
// Look up the correct DB operations for type T in our type map
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
let result_any = db_ops.list_any()?;
// We expect the result to be of type Vec<T> since we looked it up by TypeId
match result_any.downcast::<Vec<T>>() {
Ok(vec_t) => Ok(*vec_t),
Err(_) => Err(SledDBError::TypeError),
}
},
None => Err(SledDBError::TypeError),
}
}
// Convenience methods to get each specific database
pub fn user_db(&self) -> &SledDB<User> {
&self.user_db
}
pub fn company_db(&self) -> &SledDB<Company> {
&self.company_db
}
pub fn meeting_db(&self) -> &SledDB<Meeting> {
&self.meeting_db
}
pub fn product_db(&self) -> &SledDB<Product> {
&self.product_db
}
pub fn sale_db(&self) -> &SledDB<Sale> {
&self.sale_db
}
pub fn vote_db(&self) -> &SledDB<Vote> {
&self.vote_db
}
pub fn shareholder_db(&self) -> &SledDB<Shareholder> {
&self.shareholder_db
}
}
// The as_type function is no longer needed with our type-map based implementation
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use tempfile::tempdir;
#[test]
fn test_read_your_writes() {
// Create a temporary directory for the test
let dir = tempdir().expect("Failed to create temp dir");
let db = DB::new(dir.path()).expect("Failed to create DB");
// Create a user
let user = User::new(
10,
"Original User".to_string(),
"original@example.com".to_string(),
"password".to_string(),
"Original Corp".to_string(),
"User".to_string(),
);
// Insert the user directly (no transaction)
db.set(&user).expect("Failed to insert user");
// Begin a transaction
db.begin_transaction().expect("Failed to begin transaction");
// Verify we can read the original user
let original = db.get::<User>(&user.id.to_string()).expect("Failed to get original user");
assert_eq!(original.name, "Original User");
// Create a modified user with the same ID
let modified_user = User::new(
10,
"Modified User".to_string(),
"modified@example.com".to_string(),
"new_password".to_string(),
"Modified Corp".to_string(),
"Admin".to_string(),
);
// Update the user in the transaction
db.set(&modified_user).expect("Failed to update user in transaction");
// Verify we can read our own writes within the transaction
let in_transaction = db.get::<User>(&user.id.to_string()).expect("Failed to get user from transaction");
assert_eq!(in_transaction.name, "Modified User");
assert_eq!(in_transaction.email, "modified@example.com");
// Create a new user that only exists in the transaction
let new_user = User::new(
20,
"Transaction Only User".to_string(),
"tx@example.com".to_string(),
"password".to_string(),
"TX Corp".to_string(),
"Admin".to_string(),
);
// Add the new user in the transaction
db.set(&new_user).expect("Failed to add new user in transaction");
// Verify we can read the new user within the transaction
let new_in_tx = db.get::<User>(&new_user.id.to_string()).expect("Failed to get new user from transaction");
assert_eq!(new_in_tx.name, "Transaction Only User");
// Delete a user in the transaction and verify it appears deleted within the transaction
db.delete::<User>(&user.id.to_string()).expect("Failed to delete user in transaction");
match db.get::<User>(&user.id.to_string()) {
Err(SledDBError::NotFound(_)) => (), // Expected result
Ok(_) => panic!("User should appear deleted within transaction"),
Err(e) => panic!("Unexpected error: {}", e),
}
// Rollback the transaction
db.rollback_transaction().expect("Failed to rollback transaction");
// Verify the original user is still available and unchanged after rollback
let after_rollback = db.get::<User>(&user.id.to_string()).expect("Failed to get user after rollback");
assert_eq!(after_rollback.name, "Original User");
// Verify the transaction-only user doesn't exist after rollback
assert!(db.get::<User>(&new_user.id.to_string()).is_err());
}
#[test]
fn test_transactions() {
// Create a temporary directory for the test
let dir = tempdir().expect("Failed to create temp dir");
let db = DB::new(dir.path()).expect("Failed to create DB");
// Create a sample user and company for testing
let user = User::new(
1,
"Transaction Test User".to_string(),
"tx@example.com".to_string(),
"password".to_string(),
"Test Corp".to_string(),
"Admin".to_string(),
);
let incorporation_date = Utc::now();
let company = Company::new(
1,
"Transaction Test Corp".to_string(),
"TX123".to_string(),
incorporation_date,
"12-31".to_string(),
"tx@corp.com".to_string(),
"123-456-7890".to_string(),
"www.testcorp.com".to_string(),
"123 Test St".to_string(),
BusinessType::Global,
"Tech".to_string(),
"A test company for transactions".to_string(),
CompanyStatus::Active,
);
// Test successful transaction (multiple operations committed at once)
{
// Start a transaction
db.begin_transaction().expect("Failed to begin transaction");
assert!(db.has_active_transaction());
// Perform multiple operations within the transaction
db.set(&user).expect("Failed to add user to transaction");
db.set(&company).expect("Failed to add company to transaction");
// Commit the transaction
db.commit_transaction().expect("Failed to commit transaction");
assert!(!db.has_active_transaction());
// Verify both operations were applied
let retrieved_user: User = db.get(&user.id.to_string()).expect("Failed to get user after commit");
let retrieved_company: Company = db.get(&company.id.to_string()).expect("Failed to get company after commit");
assert_eq!(user.name, retrieved_user.name);
assert_eq!(company.name, retrieved_company.name);
}
// Test transaction rollback
{
// Create another user that should not be persisted
let temp_user = User::new(
2,
"Temporary User".to_string(),
"temp@example.com".to_string(),
"password".to_string(),
"Temp Corp".to_string(),
"Temp".to_string(),
);
// Start a transaction
db.begin_transaction().expect("Failed to begin transaction");
// Add the temporary user
db.set(&temp_user).expect("Failed to add temporary user to transaction");
// Perform a delete operation in the transaction
db.delete::<Company>(&company.id.to_string()).expect("Failed to delete company in transaction");
// Rollback the transaction - should discard all operations
db.rollback_transaction().expect("Failed to rollback transaction");
assert!(!db.has_active_transaction());
// Verify the temporary user was not added
match db.get::<User>(&temp_user.id.to_string()) {
Err(SledDBError::NotFound(_)) => (), // Expected outcome
Ok(_) => panic!("Temporary user should not exist after rollback"),
Err(e) => panic!("Unexpected error: {}", e),
}
// Verify the company was not deleted
let company_still_exists = db.get::<Company>(&company.id.to_string()).is_ok();
assert!(company_still_exists, "Company should still exist after transaction rollback");
}
}
#[test]
fn test_generic_db_operations() {
// Create a temporary directory for the test
let dir = tempdir().expect("Failed to create temp dir");
let db = DB::new(dir.path()).expect("Failed to create DB");
// Test simple transaction functionality
assert!(!db.has_active_transaction());
db.begin_transaction().expect("Failed to begin transaction");
assert!(db.has_active_transaction());
db.rollback_transaction().expect("Failed to rollback transaction");
assert!(!db.has_active_transaction());
// Create a sample user
let user = User::new(
1,
"Test User".to_string(),
"test@example.com".to_string(),
"password".to_string(),
"Test Corp".to_string(),
"Admin".to_string(),
);
// Insert the user
db.set(&user).expect("Failed to insert user");
// Get the user
let retrieved_user: User = db.get(&user.id.to_string()).expect("Failed to get user");
assert_eq!(user.name, retrieved_user.name);
// Create a sample company
let incorporation_date = Utc::now();
let company = Company::new(
1,
"Test Corp".to_string(),
"REG123".to_string(),
incorporation_date,
"12-31".to_string(),
"test@corp.com".to_string(),
"123-456-7890".to_string(),
"www.testcorp.com".to_string(),
"123 Test St".to_string(),
BusinessType::Global,
"Tech".to_string(),
"A test company".to_string(),
CompanyStatus::Active,
);
// Insert the company
db.set(&company).expect("Failed to insert company");
// Get the company
let retrieved_company: Company = db.get(&company.id.to_string())
.expect("Failed to get company");
assert_eq!(company.name, retrieved_company.name);
// List all companies
let companies: Vec<Company> = db.list().expect("Failed to list companies");
assert_eq!(companies.len(), 1);
assert_eq!(companies[0].name, company.name);
// List all users
let users: Vec<User> = db.list().expect("Failed to list users");
assert_eq!(users.len(), 1);
assert_eq!(users[0].name, user.name);
// Delete the company
db.delete::<Company>(&company.id.to_string())
.expect("Failed to delete company");
// Try to get the deleted company (should fail)
match db.get::<Company>(&company.id.to_string()) {
Err(SledDBError::NotFound(_)) => (),
_ => panic!("Expected NotFound error"),
}
}
}

70
herodb/src/core/mod.rs Normal file
View File

@ -0,0 +1,70 @@
mod base;
pub use base::*;
use std::any::TypeId;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, RwLock};
/// Represents a single database operation in a transaction
#[derive(Debug, Clone)]
enum DbOperation {
Set {
model_type: TypeId,
serialized: Vec<u8>,
},
Delete {
model_type: TypeId,
id: String,
},
}
// Trait for type-erased database operations
trait AnyDbOperations: Send + Sync {
fn delete(&self, id: &str) -> SledDBResult<()>;
fn get_any(&self, id: &str) -> SledDBResult<Box<dyn std::any::Any>>;
fn list_any(&self) -> SledDBResult<Box<dyn std::any::Any>>;
fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()>;
}
// Implementation of AnyDbOperations for any SledDB<T>
impl<T: SledModel> AnyDbOperations for SledDB<T> {
fn delete(&self, id: &str) -> SledDBResult<()> {
self.delete(id)
}
fn get_any(&self, id: &str) -> SledDBResult<Box<dyn std::any::Any>> {
let result = self.get(id)?;
Ok(Box::new(result))
}
fn list_any(&self) -> SledDBResult<Box<dyn std::any::Any>> {
let result = self.list()?;
Ok(Box::new(result))
}
fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()> {
// Downcast to the specific type T
match model.downcast_ref::<T>() {
Some(t) => self.insert(t),
None => Err(SledDBError::TypeError),
}
}
}
/// Transaction state for DB operations
pub struct TransactionState {
operations: Vec<DbOperation>,
active: bool,
}
impl TransactionState {
/// Create a new transaction state
pub fn new() -> Self {
Self {
operations: Vec::new(),
active: true,
}
}
}

36
herodb/src/error.rs Normal file
View File

@ -0,0 +1,36 @@
use thiserror::Error;
/// Error types for HeroDB operations
#[derive(Error, Debug)]
pub enum Error {
/// Error from the underlying sled database
#[error("Database error: {0}")]
Database(#[from] sled::Error),
/// Error during serialization or deserialization
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
/// Error when a requested item is not found
#[error("Item not found: {0}")]
NotFound(String),
/// Error when an item already exists
#[error("Item already exists: {0}")]
AlreadyExists(String),
/// Error when a model validation fails
#[error("Validation error: {0}")]
Validation(String),
/// Error when a transaction fails
#[error("Transaction error: {0}")]
Transaction(String),
/// Other errors
#[error("Other error: {0}")]
Other(String),
}
/// Result type for HeroDB operations
pub type Result<T> = std::result::Result<T, Error>;

15
herodb/src/lib.rs Normal file
View File

@ -0,0 +1,15 @@
//! HeroDB: A database library built on top of sled with model support
//!
//! This library provides a simple interface for working with a sled-based database
//! and includes support for defining and working with data models.
mod db;
mod error;
mod model;
pub use db::{Database, Collection};
pub use error::Error;
pub use model::{Model, ModelId, Timestamp};
/// Re-export sled for advanced usage
pub use sled;

6
herodb/src/mod.rs Normal file
View File

@ -0,0 +1,6 @@
// Export core module
pub mod core;
// Export zaz module
pub mod zaz;

View File

@ -0,0 +1,98 @@
# Zaz DB System
The Zaz DB system is a new implementation that provides automatic database persistence for all root models in the system.
## Architecture
- Each root model (User, Company, Meeting, Product, Sale, Vote, Shareholder) is stored in its own database file
- The DB system uses Sled, a high-performance embedded database
- Each model is automatically serialized with Bincode and compressed with Brotli
- The DB system provides generic methods that work with any model type
## Directory Structure
```
src/zaz/
├── db/
│ ├── base.rs # Core traits and SledDB implementation
│ └── mod.rs # Main DB implementation that handles all models
└── models/
├── user.rs
├── company.rs
├── meeting.rs
├── product.rs
├── sale.rs
├── vote.rs
├── shareholder.rs
└── lib.rs # Re-exports all models
```
## Usage
```rust
use crate::db::core::DB;
use crate::zaz::models::*;
// Create a DB instance (handles all model types)
let db = DB::new("/path/to/db").expect("Failed to create DB");
// --- User Example ---
let user = User::new(
1,
"John Doe".to_string(),
"john@example.com".to_string(),
"password123".to_string(),
"ACME Corp".to_string(),
"Admin".to_string(),
);
// Insert user (DB automatically detects the type)
db.set(&user).expect("Failed to insert user");
// Get user
let retrieved_user: User = db.get(&user.id.to_string())
.expect("Failed to get user");
// List all users
let users: Vec<User> = db.list().expect("Failed to list users");
// Delete user
db.delete::<User>(&user.id.to_string()).expect("Failed to delete user");
// --- Company Example ---
let company = Company::new(
1,
"ACME Corporation".to_string(),
"REG12345".to_string(),
Utc::now(),
"12-31".to_string(),
// other fields...
);
// Similar operations for company and other models
// --- Direct Database Access ---
// You can also access the specific database for a model type directly
let user_db = db.user_db();
let company_db = db.company_db();
// etc.
```
## Benefits
1. **Automatic Type Handling**: The DB system automatically detects the model type and routes operations to the appropriate database
2. **Generic Interface**: Same methods work with any model type
3. **Persistence**: All models are automatically persisted to disk
4. **Performance**: Fast serialization with Bincode and efficient compression with Brotli
5. **Storage Separation**: Each model type has its own database file, making maintenance easier
## Implementation Notes
- Each model implements the `SledModel` trait which provides the necessary methods for database operations
- The `Storable` trait handles serialization and deserialization
- The DB uses separate Sled databases for each model type to ensure proper separation of concerns
- Type-safe operations are ensured through Rust's type system
## Examples
See the `examples.rs` file for complete examples of how to use the DB system.

View File

@ -0,0 +1,160 @@
//! Examples demonstrating how to use the new DB implementation
use crate::db::core::DB;
use crate::db::zaz::models::*;
use crate::db::zaz::models::shareholder::ShareholderType;
use std::path::PathBuf;
use std::fs;
use chrono::Utc;
/// Creates a simple temporary directory
fn create_temp_dir() -> std::io::Result<PathBuf> {
let temp_dir = std::env::temp_dir();
let random_name = format!("db-example-{}", std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis());
let path = temp_dir.join(random_name);
fs::create_dir_all(&path)?;
Ok(path)
}
/// Example demonstrating basic CRUD operations with the DB
pub fn run_db_examples() -> Result<(), String> {
println!("Running DB examples...");
// Create a temporary directory for the DB (or use a permanent one)
let db_path = create_temp_dir().map_err(|e| format!("Failed to create temp dir: {}", e))?;
println!("Using DB path: {:?}", db_path);
// Create a DB instance
let db = DB::new(db_path).map_err(|e| format!("Failed to create DB: {}", e))?;
// --- User Example ---
println!("\nRunning User example:");
let user = User::new(
1,
"John Doe".to_string(),
"john@example.com".to_string(),
"password123".to_string(),
"ACME Corp".to_string(),
"Admin".to_string(),
);
// Insert user
db.set(&user).map_err(|e| format!("Failed to insert user: {}", e))?;
println!("Inserted user: {}", user.name);
// Get user
let retrieved_user: User = db.get(&user.id.to_string())
.map_err(|e| format!("Failed to get user: {}", e))?;
println!("Retrieved user: {} ({})", retrieved_user.name, retrieved_user.email);
// --- Company Example ---
println!("\nRunning Company example:");
let company = Company::new(
1,
"ACME Corporation".to_string(),
"REG12345".to_string(),
Utc::now(),
"12-31".to_string(),
"info@acme.com".to_string(),
"555-123-4567".to_string(),
"www.acme.com".to_string(),
"123 Main St, Metropolis".to_string(),
BusinessType::Global,
"Technology".to_string(),
"A leading technology company".to_string(),
CompanyStatus::Active,
);
// Insert company
db.set(&company).map_err(|e| format!("Failed to insert company: {}", e))?;
println!("Inserted company: {}", company.name);
// Get company
let retrieved_company: Company = db.get(&company.id.to_string())
.map_err(|e| format!("Failed to get company: {}", e))?;
println!("Retrieved company: {} ({})", retrieved_company.name, retrieved_company.registration_number);
// --- Shareholder Example ---
println!("\nRunning Shareholder example:");
// Create the shareholder directly
let shareholder = Shareholder {
id: 1,
company_id: company.id,
user_id: user.id,
name: "John Doe".to_string(),
shares: 1000.0,
percentage: 25.0,
type_: ShareholderType::Individual, // Use the shared enum via re-export
since: Utc::now(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
// Insert shareholder
db.set(&shareholder).map_err(|e| format!("Failed to insert shareholder: {}", e))?;
println!("Inserted shareholder: {} ({}%)", shareholder.name, shareholder.percentage);
// Get shareholder
let retrieved_shareholder: Shareholder = db.get(&shareholder.id.to_string())
.map_err(|e| format!("Failed to get shareholder: {}", e))?;
println!("Retrieved shareholder: {} ({} shares)", retrieved_shareholder.name, retrieved_shareholder.shares);
// --- List Example ---
println!("\nListing all entities:");
let users: Vec<User> = db.list().map_err(|e| format!("Failed to list users: {}", e))?;
println!("Found {} users", users.len());
for user in &users {
println!("- User: {}", user.name);
}
let companies: Vec<Company> = db.list().map_err(|e| format!("Failed to list companies: {}", e))?;
println!("Found {} companies", companies.len());
for company in &companies {
println!("- Company: {}", company.name);
}
let shareholders: Vec<Shareholder> = db.list()
.map_err(|e| format!("Failed to list shareholders: {}", e))?;
println!("Found {} shareholders", shareholders.len());
for shareholder in &shareholders {
println!("- Shareholder: {} ({}%)", shareholder.name, shareholder.percentage);
}
// --- Delete Example ---
println!("\nDeleting entities:");
// Delete shareholder
db.delete::<Shareholder>(&shareholder.id.to_string())
.map_err(|e| format!("Failed to delete shareholder: {}", e))?;
println!("Deleted shareholder: {}", shareholder.name);
// Delete company
db.delete::<Company>(&company.id.to_string())
.map_err(|e| format!("Failed to delete company: {}", e))?;
println!("Deleted company: {}", company.name);
// Delete user
db.delete::<User>(&user.id.to_string())
.map_err(|e| format!("Failed to delete user: {}", e))?;
println!("Deleted user: {}", user.name);
// Verify deletion
let users_after_delete: Vec<User> = db.list()
.map_err(|e| format!("Failed to list users after delete: {}", e))?;
println!("Users remaining: {}", users_after_delete.len());
let companies_after_delete: Vec<Company> = db.list()
.map_err(|e| format!("Failed to list companies after delete: {}", e))?;
println!("Companies remaining: {}", companies_after_delete.len());
let shareholders_after_delete: Vec<Shareholder> = db.list()
.map_err(|e| format!("Failed to list shareholders after delete: {}", e))?;
println!("Shareholders remaining: {}", shareholders_after_delete.len());
println!("\nDB examples completed successfully!");
Ok(())
}

168
herodb/src/zaz/db_tests.rs Normal file
View File

@ -0,0 +1,168 @@
//! Integration tests for zaz database module
#[cfg(test)]
mod tests {
use sled;
use bincode;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::Path;
use tempfile::tempdir;
use std::collections::HashMap;
/// Test model for database operations
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct User {
id: u32,
name: String,
email: String,
balance: f64,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl User {
fn new(id: u32, name: String, email: String, balance: f64) -> Self {
let now = Utc::now();
Self {
id,
name,
email,
balance,
created_at: now,
updated_at: now,
}
}
}
/// Test basic CRUD operations
#[test]
fn test_basic_crud() {
// Create a temporary directory for testing
let temp_dir = tempdir().expect("Failed to create temp directory");
println!("Created temporary directory at: {:?}", temp_dir.path());
// Open a sled database in the temporary directory
let db = sled::open(temp_dir.path().join("users")).expect("Failed to open database");
println!("Opened database at: {:?}", temp_dir.path().join("users"));
// CREATE a user
let user = User::new(1, "Test User".to_string(), "test@example.com".to_string(), 100.0);
let user_key = user.id.to_string();
let user_value = bincode::serialize(&user).expect("Failed to serialize user");
db.insert(user_key.as_bytes(), user_value).expect("Failed to insert user");
db.flush().expect("Failed to flush database");
println!("Created user: {} ({})", user.name, user.email);
// READ the user
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
assert!(result.is_some(), "User should exist");
if let Some(data) = result {
let retrieved_user: User = bincode::deserialize(&data).expect("Failed to deserialize user");
println!("Retrieved user: {} ({})", retrieved_user.name, retrieved_user.email);
assert_eq!(user, retrieved_user, "Retrieved user should match original");
}
// UPDATE the user
let updated_user = User::new(1, "Updated User".to_string(), "updated@example.com".to_string(), 150.0);
let updated_value = bincode::serialize(&updated_user).expect("Failed to serialize updated user");
db.insert(user_key.as_bytes(), updated_value).expect("Failed to update user");
db.flush().expect("Failed to flush database");
println!("Updated user: {} ({})", updated_user.name, updated_user.email);
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
if let Some(data) = result {
let retrieved_user: User = bincode::deserialize(&data).expect("Failed to deserialize user");
assert_eq!(updated_user, retrieved_user, "Retrieved user should match updated version");
} else {
panic!("User should exist after update");
}
// DELETE the user
db.remove(user_key.as_bytes()).expect("Failed to delete user");
db.flush().expect("Failed to flush database");
println!("Deleted user");
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
assert!(result.is_none(), "User should be deleted");
// Clean up
drop(db);
temp_dir.close().expect("Failed to cleanup temporary directory");
}
/// Test transaction-like behavior with multiple operations
#[test]
fn test_transaction_behavior() {
// Create a temporary directory for testing
let temp_dir = tempdir().expect("Failed to create temp directory");
println!("Created temporary directory at: {:?}", temp_dir.path());
// Open a sled database in the temporary directory
let db = sled::open(temp_dir.path().join("tx_test")).expect("Failed to open database");
println!("Opened transaction test database at: {:?}", temp_dir.path().join("tx_test"));
// Create initial users
let user1 = User::new(1, "User One".to_string(), "one@example.com".to_string(), 100.0);
let user2 = User::new(2, "User Two".to_string(), "two@example.com".to_string(), 50.0);
// Insert initial users
db.insert(user1.id.to_string().as_bytes(), bincode::serialize(&user1).unwrap()).unwrap();
db.insert(user2.id.to_string().as_bytes(), bincode::serialize(&user2).unwrap()).unwrap();
db.flush().unwrap();
println!("Inserted initial users");
// Simulate a transaction - transfer 25.0 from user1 to user2
println!("Starting transaction simulation: transfer 25.0 from user1 to user2");
// Create transaction workspace
let mut tx_workspace = HashMap::new();
// Retrieve current state
if let Some(data) = db.get(user1.id.to_string().as_bytes()).unwrap() {
let user: User = bincode::deserialize(&data).unwrap();
tx_workspace.insert(user1.id.to_string(), user);
}
if let Some(data) = db.get(user2.id.to_string().as_bytes()).unwrap() {
let user: User = bincode::deserialize(&data).unwrap();
tx_workspace.insert(user2.id.to_string(), user);
}
// Modify both users in the transaction
let mut updated_user1 = tx_workspace.get(&user1.id.to_string()).unwrap().clone();
let mut updated_user2 = tx_workspace.get(&user2.id.to_string()).unwrap().clone();
updated_user1.balance -= 25.0;
updated_user2.balance += 25.0;
// Update the workspace
tx_workspace.insert(user1.id.to_string(), updated_user1);
tx_workspace.insert(user2.id.to_string(), updated_user2);
// Commit the transaction
println!("Committing transaction");
for (key, user) in tx_workspace {
let user_bytes = bincode::serialize(&user).unwrap();
db.insert(key.as_bytes(), user_bytes).unwrap();
}
db.flush().unwrap();
// Verify the results
if let Some(data) = db.get(user1.id.to_string().as_bytes()).unwrap() {
let final_user1: User = bincode::deserialize(&data).unwrap();
assert_eq!(final_user1.balance, 75.0, "User1 balance should be 75.0");
println!("Verified user1 balance is now {}", final_user1.balance);
}
if let Some(data) = db.get(user2.id.to_string().as_bytes()).unwrap() {
let final_user2: User = bincode::deserialize(&data).unwrap();
assert_eq!(final_user2.balance, 75.0, "User2 balance should be 75.0");
println!("Verified user2 balance is now {}", final_user2.balance);
}
// Clean up
drop(db);
temp_dir.close().expect("Failed to cleanup temporary directory");
}
}

View File

@ -0,0 +1,64 @@
// Examples for using the Zaz database
use crate::db::core::DB;
use crate::db::zaz::models::*;
use std::path::PathBuf;
use chrono::Utc;
/// Run a simple example of the DB operations
pub fn run_db_examples() -> Result<(), Box<dyn std::error::Error>> {
println!("Running Zaz DB examples...");
// Create a temp DB path
let db_path = PathBuf::from("/tmp/zaz-examples");
std::fs::create_dir_all(&db_path)?;
// Create DB instance
let db = DB::new(&db_path)?;
// Example 1: User operations
println!("\n--- User Examples ---");
let user = User::new(
1,
"John Doe".to_string(),
"john@example.com".to_string(),
"secure123".to_string(),
"Example Corp".to_string(),
"User".to_string(),
);
db.set(&user)?;
println!("Inserted user: {}", user.name);
let retrieved_user = db.get::<User>(&user.id.to_string())?;
println!("Retrieved user: {} ({})", retrieved_user.name, retrieved_user.email);
// Example 2: Company operations
println!("\n--- Company Examples ---");
let company = Company::new(
1,
"Example Corp".to_string(),
"EX123456".to_string(),
Utc::now(),
"12-31".to_string(),
"info@example.com".to_string(),
"123-456-7890".to_string(),
"www.example.com".to_string(),
"123 Example St, Example City".to_string(),
BusinessType::Global,
"Technology".to_string(),
"An example company".to_string(),
CompanyStatus::Active,
);
db.set(&company)?;
println!("Inserted company: {}", company.name);
let companies = db.list::<Company>()?;
println!("Found {} companies", companies.len());
// Clean up
std::fs::remove_dir_all(db_path)?;
Ok(())
}

9
herodb/src/zaz/mod.rs Normal file
View File

@ -0,0 +1,9 @@
// Declare the models submodule
#[path = "models/lib.rs"] // Tell compiler where to find models module source
pub mod models;
// Declare the examples module for the new DB implementation
#[path = "examples.rs"] // Tell compiler where to find the examples module
pub mod examples;

View File

@ -0,0 +1,236 @@
use crate::db::core::{SledModel, Storable, SledDB, SledDBError}; // Import from new location
use super::shareholder::Shareholder; // Use super:: for sibling module
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
/// CompanyStatus represents the status of a company
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompanyStatus {
Active,
Inactive,
Suspended,
}
/// BusinessType represents the type of a business
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BusinessType {
Coop,
Single,
Twin,
Starter,
Global,
}
/// Company represents a company registered in the Freezone
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] // Added PartialEq
pub struct Company {
pub id: u32,
pub name: String,
pub registration_number: String,
pub incorporation_date: DateTime<Utc>,
pub fiscal_year_end: String,
pub email: String,
pub phone: String,
pub website: String,
pub address: String,
pub business_type: BusinessType,
pub industry: String,
pub description: String,
pub status: CompanyStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
// Removed shareholders property
}
// Storable trait provides default dump/load using bincode/brotli
impl Storable for Company {}
// SledModel requires get_id and db_prefix
impl SledModel for Company {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"company" // Prefix for company records in Sled
}
}
impl Company {
/// Create a new company with default timestamps
pub fn new(
id: u32,
name: String,
registration_number: String,
incorporation_date: DateTime<Utc>,
fiscal_year_end: String,
email: String,
phone: String,
website: String,
address: String,
business_type: BusinessType,
industry: String,
description: String,
status: CompanyStatus,
) -> Self {
let now = Utc::now();
Self {
id,
name,
registration_number,
incorporation_date,
fiscal_year_end,
email,
phone,
website,
address,
business_type,
industry,
description,
status,
created_at: now,
updated_at: now,
}
}
/// Add a shareholder to the company, saving it to the Shareholder's SledDB
pub fn add_shareholder(
&mut self,
db: &SledDB<Shareholder>, // Pass in the Shareholder's SledDB
mut shareholder: Shareholder,
) -> Result<(), SledDBError> {
shareholder.company_id = self.id; // Set the company_id
db.insert(&shareholder)?; // Insert the shareholder into its own DB
self.updated_at = Utc::now();
Ok(())
}
// Removed dump and load_from_bytes methods, now provided by Storable trait
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::zaz::db::{SledDB, SledDBError, SledModel};
use crate::db::zaz::models::shareholder::{Shareholder, ShareholderType};
use tempfile::tempdir;
#[test]
fn test_company_sled_crud() {
// 1. Setup: Create a temporary directory for the Sled DB
let dir = tempdir().expect("Failed to create temp dir");
let db_path = dir.path();
let company_db: SledDB<Company> = SledDB::open(db_path.join("company")).expect("Failed to open Company Sled DB");
let shareholder_db: SledDB<Shareholder> = SledDB::open(db_path.join("shareholder")).expect("Failed to open Shareholder Sled DB");
// 2. Create a sample Company
let incorporation_date = Utc::now();
let mut company1 = Company::new(
1,
"Test Corp".to_string(),
"REG123".to_string(),
incorporation_date,
"12-31".to_string(),
"test@corp.com".to_string(),
"123-456-7890".to_string(),
"www.testcorp.com".to_string(),
"123 Test St".to_string(),
BusinessType::Global,
"Tech".to_string(),
"A test company".to_string(),
CompanyStatus::Active,
);
let company_id = company1.get_id();
// 3. Create and add a shareholder to the company
let now = Utc::now();
// Define shareholder properties separately
let shareholder_id = 1;
let shareholder_name = "Dummy Shareholder".to_string();
// Create the shareholder
let shareholder = Shareholder::new(
shareholder_id,
0, // company_id will be set by add_shareholder
0, // user_id
shareholder_name.clone(),
100.0, // shares
10.0, // percentage
ShareholderType::Individual,
);
// Add the shareholder
company1.add_shareholder(&shareholder_db, shareholder).expect("Failed to add shareholder");
// 4. Insert the company
company_db.insert(&company1).expect("Failed to insert company");
// 5. Get and Assert
let retrieved_company = company_db.get(&company_id).expect("Failed to get company");
assert_eq!(company1, retrieved_company, "Retrieved company does not match original");
// 6. List and Assert
let all_companies = company_db.list().expect("Failed to list companies");
assert_eq!(all_companies.len(), 1, "Should be one company in the list");
assert_eq!(all_companies[0], company1, "List should contain the inserted company");
// 7. Delete
company_db.delete(&company_id).expect("Failed to delete company");
// 8. Get after delete and Assert NotFound
match company_db.get(&company_id) {
Err(SledDBError::NotFound(id)) => {
assert_eq!(id, company_id, "NotFound error should contain the correct ID");
}
Ok(_) => panic!("Should not have found the company after deletion"),
Err(e) => panic!("Unexpected error after delete: {:?}", e),
}
// 9. List after delete
let companies_after_delete = company_db.list().expect("Failed to list companies after delete");
assert!(companies_after_delete.is_empty(), "List should be empty after deletion");
// 10. Check if shareholder exists in shareholder db
let retrieved_shareholder = shareholder_db.get(&shareholder_id.to_string()).expect("Failed to get shareholder");
assert_eq!(shareholder_id, retrieved_shareholder.id, "Retrieved shareholder should have the correct ID");
assert_eq!(shareholder_name, retrieved_shareholder.name, "Retrieved shareholder should have the correct name");
assert_eq!(1, retrieved_shareholder.company_id, "Retrieved shareholder should have company_id set to 1");
// Temporary directory `dir` is automatically removed when it goes out of scope here.
}
#[test]
fn test_dump_load() {
// Create a sample Company
let incorporation_date = Utc::now();
let original_company = Company::new(
2,
"DumpLoad Test".to_string(),
"DL987".to_string(),
incorporation_date,
"06-30".to_string(),
"dump@load.com".to_string(),
"987-654-3210".to_string(),
"www.dumpload.com".to_string(),
"456 DumpLoad Ave".to_string(),
BusinessType::Coop,
"Testing".to_string(),
"Testing dump and load".to_string(),
CompanyStatus::Active,
);
// Dump (serialize + compress)
let dumped_data = original_company.dump().expect("Failed to dump company");
assert!(!dumped_data.is_empty(), "Dumped data should not be empty");
// Load (decompress + deserialize)
let loaded_company = Company::load_from_bytes(&dumped_data).expect("Failed to load company from bytes");
// Assert equality
assert_eq!(original_company, loaded_company, "Loaded company should match the original");
}
}

View File

@ -0,0 +1,25 @@
pub mod user;
pub mod vote;
pub mod company;
pub mod meeting;
pub mod product;
pub mod sale;
pub mod shareholder;
// pub mod db; // Moved to src/zaz/db
// pub mod migration; // Removed
// Re-export all model types for convenience
pub use user::User;
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
pub use company::{Company, CompanyStatus, BusinessType};
pub use meeting::Meeting;
pub use product::{Product, Currency, ProductComponent, ProductType, ProductStatus};
pub use sale::Sale;
pub use shareholder::Shareholder;
// Re-export database components
// pub use db::{DB, DBError, DBResult, Model, ModelMetadata}; // Removed old DB re-exports
pub use crate::db::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; // Re-export Sled DB components
// Re-export migration components - Removed
// pub use migration::{Migrator, MigrationError, MigrationResult};

View File

@ -0,0 +1,172 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::db::core::{SledModel, Storable}; // Import Sled traits from new location
// use std::collections::HashMap; // Removed unused import
// use super::db::Model; // Removed old Model trait import
/// MeetingStatus represents the status of a meeting
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MeetingStatus {
Scheduled,
Completed,
Cancelled,
}
/// AttendeeRole represents the role of an attendee in a meeting
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AttendeeRole {
Coordinator,
Member,
Secretary,
Participant,
Advisor,
Admin,
}
/// AttendeeStatus represents the status of an attendee's participation
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AttendeeStatus {
Confirmed,
Pending,
Declined,
}
/// Attendee represents an attendee of a board meeting
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attendee {
pub id: u32,
pub meeting_id: u32,
pub user_id: u32,
pub name: String,
pub role: AttendeeRole,
pub status: AttendeeStatus,
pub created_at: DateTime<Utc>,
}
impl Attendee {
/// Create a new attendee with default values
pub fn new(
id: u32,
meeting_id: u32,
user_id: u32,
name: String,
role: AttendeeRole,
) -> Self {
Self {
id,
meeting_id,
user_id,
name,
role,
status: AttendeeStatus::Pending,
created_at: Utc::now(),
}
}
/// Update the status of an attendee
pub fn update_status(&mut self, status: AttendeeStatus) {
self.status = status;
}
}
/// Meeting represents a board meeting of a company or other meeting
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Meeting {
pub id: u32,
pub company_id: u32,
pub title: String,
pub date: DateTime<Utc>,
pub location: String,
pub description: String,
pub status: MeetingStatus,
pub minutes: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub attendees: Vec<Attendee>,
}
// Removed old Model trait implementation
impl Meeting {
/// Create a new meeting with default values
pub fn new(
id: u32,
company_id: u32,
title: String,
date: DateTime<Utc>,
location: String,
description: String,
) -> Self {
let now = Utc::now();
Self {
id,
company_id,
title,
date,
location,
description,
status: MeetingStatus::Scheduled,
minutes: String::new(),
created_at: now,
updated_at: now,
attendees: Vec::new(),
}
}
/// Add an attendee to the meeting
pub fn add_attendee(&mut self, attendee: Attendee) {
// Make sure the attendee's meeting_id matches this meeting
assert_eq!(self.id, attendee.meeting_id, "Attendee meeting_id must match meeting id");
// Check if the attendee already exists
if !self.attendees.iter().any(|a| a.id == attendee.id) {
self.attendees.push(attendee);
self.updated_at = Utc::now();
}
}
/// Update the status of the meeting
pub fn update_status(&mut self, status: MeetingStatus) {
self.status = status;
self.updated_at = Utc::now();
}
/// Update the meeting minutes
pub fn update_minutes(&mut self, minutes: String) {
self.minutes = minutes;
self.updated_at = Utc::now();
}
/// Find an attendee by user ID
pub fn find_attendee_by_user_id(&self, user_id: u32) -> Option<&Attendee> {
self.attendees.iter().find(|a| a.user_id == user_id)
}
/// Find an attendee by user ID (mutable version)
pub fn find_attendee_by_user_id_mut(&mut self, user_id: u32) -> Option<&mut Attendee> {
self.attendees.iter_mut().find(|a| a.user_id == user_id)
}
/// Get all confirmed attendees
pub fn confirmed_attendees(&self) -> Vec<&Attendee> {
self.attendees
.iter()
.filter(|a| a.status == AttendeeStatus::Confirmed)
.collect()
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for Meeting {}
// Implement SledModel trait
impl SledModel for Meeting {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"meeting"
}
}

View File

@ -0,0 +1,155 @@
use chrono::{DateTime, Utc, Duration};
use serde::{Deserialize, Serialize};
use crate::db::core::{SledModel, Storable}; // Import Sled traits from new location
/// Currency represents a monetary value with amount and currency code
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Currency {
pub amount: f64,
pub currency_code: String,
}
impl Currency {
/// Create a new currency with amount and code
pub fn new(amount: f64, currency_code: String) -> Self {
Self {
amount,
currency_code,
}
}
}
/// ProductType represents the type of a product
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProductType {
Product,
Service,
}
/// ProductStatus represents the status of a product
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProductStatus {
Available,
Unavailable,
}
/// ProductComponent represents a component of a product
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductComponent {
pub id: u32,
pub name: String,
pub description: String,
pub quantity: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl ProductComponent {
/// Create a new product component with default timestamps
pub fn new(id: u32, name: String, description: String, quantity: i32) -> Self {
let now = Utc::now();
Self {
id,
name,
description,
quantity,
created_at: now,
updated_at: now,
}
}
}
/// Product represents a product or service offered by the Freezone
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Product {
pub id: u32,
pub name: String,
pub description: String,
pub price: Currency,
pub type_: ProductType,
pub category: String,
pub status: ProductStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub max_amount: u16, // means allows us to define how many max of this there are
pub purchase_till: DateTime<Utc>,
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
pub components: Vec<ProductComponent>,
}
// Removed old Model trait implementation
impl Product {
/// Create a new product with default timestamps
pub fn new(
id: u32,
name: String,
description: String,
price: Currency,
type_: ProductType,
category: String,
status: ProductStatus,
max_amount: u16,
validity_days: i64, // How many days the product is valid after purchase
) -> Self {
let now = Utc::now();
// Default: purchasable for 1 year, active for specified validity days after purchase
Self {
id,
name,
description,
price,
type_,
category,
status,
created_at: now,
updated_at: now,
max_amount,
purchase_till: now + Duration::days(365),
active_till: now + Duration::days(validity_days),
components: Vec::new(),
}
}
/// Add a component to this product
pub fn add_component(&mut self, component: ProductComponent) {
self.components.push(component);
self.updated_at = Utc::now();
}
/// Update the purchase availability timeframe
pub fn set_purchase_period(&mut self, purchase_till: DateTime<Utc>) {
self.purchase_till = purchase_till;
self.updated_at = Utc::now();
}
/// Update the active timeframe
pub fn set_active_period(&mut self, active_till: DateTime<Utc>) {
self.active_till = active_till;
self.updated_at = Utc::now();
}
/// Check if the product is available for purchase
pub fn is_purchasable(&self) -> bool {
self.status == ProductStatus::Available && Utc::now() <= self.purchase_till
}
/// Check if the product is still active (for services)
pub fn is_active(&self) -> bool {
Utc::now() <= self.active_till
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for Product {}
// Implement SledModel trait
impl SledModel for Product {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"product"
}
}

View File

@ -0,0 +1,146 @@
use super::product::Currency; // Use super:: for sibling module
use crate::db::core::{SledModel, Storable}; // Import Sled traits from new location
// use super::db::Model; // Removed old Model trait import
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
// use std::collections::HashMap; // Removed unused import
/// SaleStatus represents the status of a sale
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SaleStatus {
Pending,
Completed,
Cancelled,
}
/// SaleItem represents an item in a sale
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaleItem {
pub id: u32,
pub sale_id: u32,
pub product_id: u32,
pub name: String,
pub quantity: i32,
pub unit_price: Currency,
pub subtotal: Currency,
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
}
impl SaleItem {
/// Create a new sale item
pub fn new(
id: u32,
sale_id: u32,
product_id: u32,
name: String,
quantity: i32,
unit_price: Currency,
active_till: DateTime<Utc>,
) -> Self {
// Calculate subtotal
let amount = unit_price.amount * quantity as f64;
let subtotal = Currency {
amount,
currency_code: unit_price.currency_code.clone(),
};
Self {
id,
sale_id,
product_id,
name,
quantity,
unit_price,
subtotal,
active_till,
}
}
}
/// Sale represents a sale of products or services
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Sale {
pub id: u32,
pub company_id: u32,
pub buyer_name: String,
pub buyer_email: String,
pub total_amount: Currency,
pub status: SaleStatus,
pub sale_date: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub items: Vec<SaleItem>,
}
// Removed old Model trait implementation
impl Sale {
/// Create a new sale with default timestamps
pub fn new(
id: u32,
company_id: u32,
buyer_name: String,
buyer_email: String,
currency_code: String,
status: SaleStatus,
) -> Self {
let now = Utc::now();
Self {
id,
company_id,
buyer_name,
buyer_email,
total_amount: Currency { amount: 0.0, currency_code },
status,
sale_date: now,
created_at: now,
updated_at: now,
items: Vec::new(),
}
}
/// Add an item to the sale and update the total amount
pub fn add_item(&mut self, item: SaleItem) {
// Make sure the item's sale_id matches this sale
assert_eq!(self.id, item.sale_id, "Item sale_id must match sale id");
// Update the total amount
if self.items.is_empty() {
// First item, initialize the total amount with the same currency
self.total_amount = Currency {
amount: item.subtotal.amount,
currency_code: item.subtotal.currency_code.clone(),
};
} else {
// Add to the existing total
// (Assumes all items have the same currency)
self.total_amount.amount += item.subtotal.amount;
}
// Add the item to the list
self.items.push(item);
// Update the sale timestamp
self.updated_at = Utc::now();
}
/// Update the status of the sale
pub fn update_status(&mut self, status: SaleStatus) {
self.status = status;
self.updated_at = Utc::now();
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for Sale {}
// Implement SledModel trait
impl SledModel for Sale {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"sale"
}
}

View File

@ -0,0 +1,78 @@
use crate::db::core::{SledModel, Storable}; // Import Sled traits
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
// use std::collections::HashMap; // Removed unused import
// use super::db::Model; // Removed old Model trait import
/// ShareholderType represents the type of shareholder
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ShareholderType {
Individual,
Corporate,
}
/// Shareholder represents a shareholder of a company
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] // Added PartialEq
pub struct Shareholder {
pub id: u32,
pub company_id: u32,
pub user_id: u32,
pub name: String,
pub shares: f64,
pub percentage: f64,
pub type_: ShareholderType,
pub since: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
// Removed old Model trait implementation
impl Shareholder {
/// Create a new shareholder with default timestamps
pub fn new(
id: u32,
company_id: u32,
user_id: u32,
name: String,
shares: f64,
percentage: f64,
type_: ShareholderType,
) -> Self {
let now = Utc::now();
Self {
id,
company_id,
user_id,
name,
shares,
percentage,
type_,
since: now,
created_at: now,
updated_at: now,
}
}
/// Update the shares owned by this shareholder
pub fn update_shares(&mut self, shares: f64, percentage: f64) {
self.shares = shares;
self.percentage = percentage;
self.updated_at = Utc::now();
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for Shareholder {}
// Implement SledModel trait
impl SledModel for Shareholder {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"shareholder"
}
}

View File

@ -0,0 +1,57 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::db::core::{SledModel, Storable}; // Import Sled traits from new location
// use std::collections::HashMap; // Removed unused import
/// User represents a user in the Freezone Manager system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: u32,
pub name: String,
pub email: String,
pub password: String,
pub company: String, // here its just a best effort
pub role: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
// Removed old Model trait implementation
impl User {
/// Create a new user with default timestamps
pub fn new(
id: u32,
name: String,
email: String,
password: String,
company: String,
role: String,
) -> Self {
let now = Utc::now();
Self {
id,
name,
email,
password,
company,
role,
created_at: now,
updated_at: now,
}
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for User {}
// Implement SledModel trait
impl SledModel for User {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"user"
}
}

View File

@ -0,0 +1,143 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::db::core::{SledModel, Storable}; // Import Sled traits from new location
// use std::collections::HashMap; // Removed unused import
// use super::db::Model; // Removed old Model trait import
/// VoteStatus represents the status of a vote
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VoteStatus {
Open,
Closed,
Cancelled,
}
/// Vote represents a voting item in the Freezone
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vote {
pub id: u32,
pub company_id: u32,
pub title: String,
pub description: String,
pub start_date: DateTime<Utc>,
pub end_date: DateTime<Utc>,
pub status: VoteStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub options: Vec<VoteOption>,
pub ballots: Vec<Ballot>,
pub private_group: Vec<u32>, // user id's only people who can vote
}
/// VoteOption represents an option in a vote
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoteOption {
pub id: u8,
pub vote_id: u32,
pub text: String,
pub count: i32,
pub min_valid: i32, // min votes we need to make total vote count
}
/// The vote as done by the user
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ballot {
pub id: u32,
pub vote_id: u32,
pub user_id: u32,
pub vote_option_id: u8,
pub shares_count: i32,
pub created_at: DateTime<Utc>,
}
// Removed old Model trait implementation
impl Vote {
/// Create a new vote with default timestamps
pub fn new(
id: u32,
company_id: u32,
title: String,
description: String,
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
status: VoteStatus,
) -> Self {
let now = Utc::now();
Self {
id,
company_id,
title,
description,
start_date,
end_date,
status,
created_at: now,
updated_at: now,
options: Vec::new(),
ballots: Vec::new(),
private_group: Vec::new(),
}
}
/// Add a voting option to this vote
pub fn add_option(&mut self, text: String, min_valid: i32) -> &VoteOption {
let id = if self.options.is_empty() {
1
} else {
self.options.iter().map(|o| o.id).max().unwrap_or(0) + 1
};
let option = VoteOption {
id,
vote_id: self.id,
text,
count: 0,
min_valid,
};
self.options.push(option);
self.options.last().unwrap()
}
/// Add a ballot to this vote
pub fn add_ballot(&mut self, user_id: u32, vote_option_id: u8, shares_count: i32) -> &Ballot {
let id = if self.ballots.is_empty() {
1
} else {
self.ballots.iter().map(|b| b.id).max().unwrap_or(0) + 1
};
let ballot = Ballot {
id,
vote_id: self.id,
user_id,
vote_option_id,
shares_count,
created_at: Utc::now(),
};
// Update the vote count for the selected option
if let Some(option) = self.options.iter_mut().find(|o| o.id == vote_option_id) {
option.count += shares_count;
}
self.ballots.push(ballot);
self.ballots.last().unwrap()
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for Vote {}
// Implement SledModel trait
impl SledModel for Vote {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"vote"
}
}

View File

@ -0,0 +1,628 @@
//! Integration tests for the zaz database module
use sled;
use bincode;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use tempfile::tempdir;
use std::fmt::{Display, Formatter};
/// Test the basic database functionality
#[test]
fn test_basic_database_operations() {
match run_comprehensive_test() {
Ok(_) => println!("All tests passed successfully!"),
Err(e) => panic!("Error running tests: {}", e),
}
}
fn run_comprehensive_test() -> Result<(), Box<dyn std::error::Error>> {
// User model
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct User {
id: u32,
name: String,
email: String,
password: String,
company: String,
role: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl User {
fn new(
id: u32,
name: String,
email: String,
password: String,
company: String,
role: String,
) -> Self {
let now = Utc::now();
Self {
id,
name,
email,
password,
company,
role,
created_at: now,
updated_at: now,
}
}
}
// Company model
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
enum BusinessType {
Local,
National,
Global,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
enum CompanyStatus {
Active,
Inactive,
Pending,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct Company {
id: u32,
name: String,
registration_number: String,
registration_date: DateTime<Utc>,
fiscal_year_end: String,
email: String,
phone: String,
website: String,
address: String,
business_type: BusinessType,
industry: String,
description: String,
status: CompanyStatus,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl Company {
fn new(
id: u32,
name: String,
registration_number: String,
registration_date: DateTime<Utc>,
fiscal_year_end: String,
email: String,
phone: String,
website: String,
address: String,
business_type: BusinessType,
industry: String,
description: String,
status: CompanyStatus,
) -> Self {
let now = Utc::now();
Self {
id,
name,
registration_number,
registration_date,
fiscal_year_end,
email,
phone,
website,
address,
business_type,
industry,
description,
status,
created_at: now,
updated_at: now,
}
}
}
// Create a temporary directory for testing
let temp_dir = tempdir()?;
println!("Using temporary directory: {:?}", temp_dir.path());
println!("\n--- Testing User operations ---");
test_user_operations(temp_dir.path())?;
println!("\n--- Testing Company operations ---");
test_company_operations(temp_dir.path())?;
println!("\n--- Testing Transaction Simulation ---");
test_transaction_simulation(temp_dir.path())?;
// Clean up
drop(temp_dir);
println!("All comprehensive tests completed successfully!");
Ok(())
}
fn test_user_operations(base_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
// User model (duplicate for scope)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct User {
id: u32,
name: String,
email: String,
password: String,
company: String,
role: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl User {
fn new(
id: u32,
name: String,
email: String,
password: String,
company: String,
role: String,
) -> Self {
let now = Utc::now();
Self {
id,
name,
email,
password,
company,
role,
created_at: now,
updated_at: now,
}
}
}
// Open the user database
let db = sled::open(base_path.join("users"))?;
println!("Opened user database at: {:?}", base_path.join("users"));
// Create a test user
let user = User::new(
100,
"Test User".to_string(),
"test@example.com".to_string(),
"password123".to_string(),
"Test Company".to_string(),
"Admin".to_string(),
);
// Insert the user
let user_id = user.id.to_string();
let user_bytes = bincode::serialize(&user)?;
db.insert(user_id.as_bytes(), user_bytes)?;
db.flush()?;
println!("Inserted user: {}", user.name);
// Retrieve the user
if let Some(data) = db.get(user_id.as_bytes())? {
let retrieved_user: User = bincode::deserialize(&data)?;
println!("Retrieved user: {}", retrieved_user.name);
assert_eq!(user.name, retrieved_user.name);
assert_eq!(user.email, retrieved_user.email);
} else {
return Err("Failed to retrieve user".into());
}
// Update the user
let updated_user = User::new(
100,
"Updated User".to_string(),
"updated@example.com".to_string(),
"newpassword".to_string(),
"New Company".to_string(),
"SuperAdmin".to_string(),
);
let updated_bytes = bincode::serialize(&updated_user)?;
db.insert(user_id.as_bytes(), updated_bytes)?;
db.flush()?;
println!("Updated user: {}", updated_user.name);
// Retrieve the updated user
if let Some(data) = db.get(user_id.as_bytes())? {
let retrieved_user: User = bincode::deserialize(&data)?;
println!("Retrieved updated user: {}", retrieved_user.name);
assert_eq!(updated_user.name, retrieved_user.name);
assert_eq!(updated_user.email, retrieved_user.email);
} else {
return Err("Failed to retrieve updated user".into());
}
// Delete the user
db.remove(user_id.as_bytes())?;
db.flush()?;
println!("Deleted user: {}", user.name);
// Try to retrieve the deleted user (should fail)
let result = db.get(user_id.as_bytes())?;
assert!(result.is_none(), "User should be deleted");
println!("Verified user was deleted");
Ok(())
}
fn test_company_operations(base_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
// Company model (duplicate for scope)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
enum BusinessType {
Local,
National,
Global,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
enum CompanyStatus {
Active,
Inactive,
Pending,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct Company {
id: u32,
name: String,
registration_number: String,
registration_date: DateTime<Utc>,
fiscal_year_end: String,
email: String,
phone: String,
website: String,
address: String,
business_type: BusinessType,
industry: String,
description: String,
status: CompanyStatus,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl Company {
fn new(
id: u32,
name: String,
registration_number: String,
registration_date: DateTime<Utc>,
fiscal_year_end: String,
email: String,
phone: String,
website: String,
address: String,
business_type: BusinessType,
industry: String,
description: String,
status: CompanyStatus,
) -> Self {
let now = Utc::now();
Self {
id,
name,
registration_number,
registration_date,
fiscal_year_end,
email,
phone,
website,
address,
business_type,
industry,
description,
status,
created_at: now,
updated_at: now,
}
}
}
// Open the company database
let db = sled::open(base_path.join("companies"))?;
println!("Opened company database at: {:?}", base_path.join("companies"));
// Create a test company
let company = Company::new(
100,
"Test Corp".to_string(),
"TEST123".to_string(),
Utc::now(),
"12-31".to_string(),
"test@corp.com".to_string(),
"123-456-7890".to_string(),
"www.testcorp.com".to_string(),
"123 Test St".to_string(),
BusinessType::Global,
"Technology".to_string(),
"A test company".to_string(),
CompanyStatus::Active,
);
// Insert the company
let company_id = company.id.to_string();
let company_bytes = bincode::serialize(&company)?;
db.insert(company_id.as_bytes(), company_bytes)?;
db.flush()?;
println!("Inserted company: {}", company.name);
// Retrieve the company
if let Some(data) = db.get(company_id.as_bytes())? {
let retrieved_company: Company = bincode::deserialize(&data)?;
println!("Retrieved company: {}", retrieved_company.name);
assert_eq!(company.name, retrieved_company.name);
} else {
return Err("Failed to retrieve company".into());
}
// List all companies
let mut companies = Vec::new();
for item in db.iter() {
let (_key, value) = item?;
let company: Company = bincode::deserialize(&value)?;
companies.push(company);
}
println!("Found {} companies", companies.len());
assert_eq!(companies.len(), 1);
// Delete the company
db.remove(company_id.as_bytes())?;
db.flush()?;
println!("Deleted company: {}", company.name);
// List companies again (should be empty)
let mut companies = Vec::new();
for item in db.iter() {
let (_key, value) = item?;
let company: Company = bincode::deserialize(&value)?;
companies.push(company);
}
assert_eq!(companies.len(), 0);
println!("Verified company was deleted");
Ok(())
}
fn test_transaction_simulation(base_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
// User model (duplicate for scope)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct User {
id: u32,
name: String,
email: String,
password: String,
company: String,
role: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl User {
fn new(
id: u32,
name: String,
email: String,
password: String,
company: String,
role: String,
) -> Self {
let now = Utc::now();
Self {
id,
name,
email,
password,
company,
role,
created_at: now,
updated_at: now,
}
}
}
// Open the user database
let db = sled::open(base_path.join("tx_users"))?;
println!("Opened transaction test database at: {:?}", base_path.join("tx_users"));
// Add a user outside of transaction
let user = User::new(
200,
"Transaction Test".to_string(),
"tx@example.com".to_string(),
"password".to_string(),
"TX Corp".to_string(),
"User".to_string(),
);
let user_id = user.id.to_string();
let user_bytes = bincode::serialize(&user)?;
db.insert(user_id.as_bytes(), user_bytes)?;
db.flush()?;
println!("Added initial user: {}", user.name);
// Since sled doesn't have explicit transaction support like the DB mock in the original code,
// we'll simulate transaction behavior by:
// 1. Making changes in memory
// 2. Only writing to the database when we "commit"
println!("Simulating transaction operations...");
// Create in-memory copy of our data (transaction workspace)
let mut tx_workspace = std::collections::HashMap::new();
// Retrieve initial state from db
if let Some(data) = db.get(user_id.as_bytes())? {
let retrieved_user: User = bincode::deserialize(&data)?;
tx_workspace.insert(user_id.clone(), retrieved_user);
}
// Update user in transaction workspace
let updated_user = User::new(
200,
"Updated in TX".to_string(),
"updated@example.com".to_string(),
"newpass".to_string(),
"New Corp".to_string(),
"Admin".to_string(),
);
tx_workspace.insert(user_id.clone(), updated_user.clone());
println!("Updated user in transaction workspace");
// Add new user in transaction workspace
let new_user = User::new(
201,
"New in TX".to_string(),
"new@example.com".to_string(),
"password".to_string(),
"New Corp".to_string(),
"User".to_string(),
);
let new_user_id = new_user.id.to_string();
tx_workspace.insert(new_user_id.clone(), new_user.clone());
println!("Added new user in transaction workspace");
// Verify the transaction workspace state
let tx_user = tx_workspace.get(&user_id).unwrap();
assert_eq!(tx_user.name, "Updated in TX");
println!("Verified transaction changes are visible within workspace");
// Simulate a rollback by discarding our workspace without writing to db
println!("Rolled back transaction (discarded workspace without writing to db)");
// Verify original user is unchanged in the database
if let Some(data) = db.get(user_id.as_bytes())? {
let original: User = bincode::deserialize(&data)?;
assert_eq!(original.name, "Transaction Test");
println!("Verified original user is unchanged after rollback");
} else {
return Err("Failed to retrieve user after rollback".into());
}
// Verify new user was not added to the database
let result = db.get(new_user_id.as_bytes())?;
assert!(result.is_none());
println!("Verified new user was not added after rollback");
// Test commit transaction
println!("Simulating a new transaction...");
// Create new transaction workspace
let mut tx_workspace = std::collections::HashMap::new();
// Retrieve current state from db
if let Some(data) = db.get(user_id.as_bytes())? {
let retrieved_user: User = bincode::deserialize(&data)?;
tx_workspace.insert(user_id.clone(), retrieved_user);
}
// Update user in new transaction workspace
let committed_user = User::new(
200,
"Committed Update".to_string(),
"commit@example.com".to_string(),
"commit_pass".to_string(),
"Commit Corp".to_string(),
"Manager".to_string(),
);
tx_workspace.insert(user_id.clone(), committed_user.clone());
println!("Updated user in new transaction");
// Commit the transaction by writing the workspace changes to the database
println!("Committing transaction by writing changes to database");
for (key, user) in tx_workspace {
let user_bytes = bincode::serialize(&user)?;
db.insert(key.as_bytes(), user_bytes)?;
}
db.flush()?;
// Verify changes persisted to the database
if let Some(data) = db.get(user_id.as_bytes())? {
let final_user: User = bincode::deserialize(&data)?;
assert_eq!(final_user.name, "Committed Update");
println!("Verified changes persisted after commit");
} else {
return Err("Failed to retrieve user after commit".into());
}
Ok(())
}
/// Test the basic CRUD functionality with a single model
#[test]
fn test_simple_db() {
// Create a temporary directory for testing
let temp_dir = tempdir().expect("Failed to create temp directory");
println!("Created temporary directory at: {:?}", temp_dir.path());
// Create a test user
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct User {
id: u32,
name: String,
email: String,
}
impl User {
fn new(id: u32, name: String, email: String) -> Self {
Self { id, name, email }
}
}
// Open a sled database in the temporary directory
let db = sled::open(temp_dir.path().join("simple_users")).expect("Failed to open database");
println!("Opened database at: {:?}", temp_dir.path().join("simple_users"));
// CREATE: Create a user
let user = User::new(1, "Simple User".to_string(), "simple@example.com".to_string());
let user_key = user.id.to_string();
let user_value = bincode::serialize(&user).expect("Failed to serialize user");
db.insert(user_key.as_bytes(), user_value).expect("Failed to insert user");
db.flush().expect("Failed to flush database");
println!("Created user: {} ({})", user.name, user.email);
// READ: Retrieve the user
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
assert!(result.is_some(), "User should exist");
if let Some(data) = result {
let retrieved_user: User = bincode::deserialize(&data).expect("Failed to deserialize user");
println!("Retrieved user: {} ({})", retrieved_user.name, retrieved_user.email);
assert_eq!(user, retrieved_user, "Retrieved user should match original");
}
// UPDATE: Update the user
let updated_user = User::new(1, "Updated User".to_string(), "updated@example.com".to_string());
let updated_value = bincode::serialize(&updated_user).expect("Failed to serialize updated user");
db.insert(user_key.as_bytes(), updated_value).expect("Failed to update user");
db.flush().expect("Failed to flush database");
println!("Updated user: {} ({})", updated_user.name, updated_user.email);
// Verify update
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
assert!(result.is_some(), "Updated user should exist");
if let Some(data) = result {
let retrieved_user: User = bincode::deserialize(&data).expect("Failed to deserialize user");
println!("Retrieved updated user: {} ({})", retrieved_user.name, retrieved_user.email);
assert_eq!(updated_user, retrieved_user, "Retrieved user should match updated version");
}
// DELETE: Delete the user
db.remove(user_key.as_bytes()).expect("Failed to delete user");
db.flush().expect("Failed to flush database");
println!("Deleted user");
// Verify deletion
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
assert!(result.is_none(), "User should be deleted");
println!("Verified user deletion");
// Clean up
drop(db);
temp_dir.close().expect("Failed to cleanup temporary directory");
println!("Simple DB test completed successfully!");
}

View File

@ -0,0 +1,265 @@
//! Transaction tests for the zaz database module
use sled;
use bincode;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::Path;
use tempfile::tempdir;
use std::collections::HashMap;
/// Test the transaction-like behavior capabilities
#[test]
fn test_transaction_operations() {
match run_transaction_test() {
Ok(_) => println!("All transaction tests passed successfully!"),
Err(e) => panic!("Error in transaction tests: {}", e),
}
}
fn run_transaction_test() -> Result<(), Box<dyn std::error::Error>> {
// Create a temporary directory for testing
let temp_dir = tempdir()?;
println!("Using temporary directory: {:?}", temp_dir.path());
test_basic_transactions(temp_dir.path())?;
test_rollback_behavior(temp_dir.path())?;
test_concurrent_operations(temp_dir.path())?;
// Clean up
drop(temp_dir);
println!("All transaction tests completed successfully!");
Ok(())
}
/// User model for testing
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct User {
id: u32,
name: String,
email: String,
balance: f64,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl User {
fn new(id: u32, name: String, email: String, balance: f64) -> Self {
let now = Utc::now();
Self {
id,
name,
email,
balance,
created_at: now,
updated_at: now,
}
}
}
/// Test basic transaction functionality
fn test_basic_transactions(base_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
// Open the test database
let db = sled::open(base_path.join("basic_tx"))?;
println!("Opened basic transaction test database at: {:?}", base_path.join("basic_tx"));
// Create initial users
let user1 = User::new(
1,
"User One".to_string(),
"one@example.com".to_string(),
100.0,
);
let user2 = User::new(
2,
"User Two".to_string(),
"two@example.com".to_string(),
50.0,
);
// Insert initial users
db.insert(user1.id.to_string().as_bytes(), bincode::serialize(&user1)?)?;
db.insert(user2.id.to_string().as_bytes(), bincode::serialize(&user2)?)?;
db.flush()?;
println!("Inserted initial users");
// Simulate a transaction - transfer 25.0 from user1 to user2
println!("Starting transaction simulation: transfer 25.0 from user1 to user2");
// Create transaction workspace
let mut tx_workspace = HashMap::new();
// Retrieve current state
if let Some(data) = db.get(user1.id.to_string().as_bytes())? {
let user: User = bincode::deserialize(&data)?;
tx_workspace.insert(user1.id.to_string(), user);
} else {
return Err("Failed to find user1".into());
}
if let Some(data) = db.get(user2.id.to_string().as_bytes())? {
let user: User = bincode::deserialize(&data)?;
tx_workspace.insert(user2.id.to_string(), user);
} else {
return Err("Failed to find user2".into());
}
// Modify both users in the transaction
let mut updated_user1 = tx_workspace.get(&user1.id.to_string()).unwrap().clone();
let mut updated_user2 = tx_workspace.get(&user2.id.to_string()).unwrap().clone();
updated_user1.balance -= 25.0;
updated_user2.balance += 25.0;
// Update the workspace
tx_workspace.insert(user1.id.to_string(), updated_user1);
tx_workspace.insert(user2.id.to_string(), updated_user2);
// Commit the transaction
println!("Committing transaction");
for (key, user) in tx_workspace {
let user_bytes = bincode::serialize(&user)?;
db.insert(key.as_bytes(), user_bytes)?;
}
db.flush()?;
// Verify the results
if let Some(data) = db.get(user1.id.to_string().as_bytes())? {
let final_user1: User = bincode::deserialize(&data)?;
assert_eq!(final_user1.balance, 75.0, "User1 balance should be 75.0");
println!("Verified user1 balance is now {}", final_user1.balance);
} else {
return Err("Failed to find user1 after transaction".into());
}
if let Some(data) = db.get(user2.id.to_string().as_bytes())? {
let final_user2: User = bincode::deserialize(&data)?;
assert_eq!(final_user2.balance, 75.0, "User2 balance should be 75.0");
println!("Verified user2 balance is now {}", final_user2.balance);
} else {
return Err("Failed to find user2 after transaction".into());
}
// Clean up
drop(db);
Ok(())
}
/// Test transaction rollback functionality
fn test_rollback_behavior(base_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
// Open the test database
let db = sled::open(base_path.join("rollback_tx"))?;
println!("Opened rollback test database at: {:?}", base_path.join("rollback_tx"));
// Create initial user
let user = User::new(
1,
"Rollback Test".to_string(),
"rollback@example.com".to_string(),
100.0,
);
// Insert initial user
db.insert(user.id.to_string().as_bytes(), bincode::serialize(&user)?)?;
db.flush()?;
println!("Inserted initial user with balance: {}", user.balance);
// Simulate a transaction that shouldn't be committed
println!("Starting transaction that will be rolled back");
// Create transaction workspace (we'd track in memory)
let mut updated_user = user.clone();
updated_user.balance = 0.0; // Drastic change
// Do NOT commit changes to the database (simulating rollback)
println!("Rolling back transaction (by not writing changes)");
// Verify the original data is intact
if let Some(data) = db.get(user.id.to_string().as_bytes())? {
let final_user: User = bincode::deserialize(&data)?;
assert_eq!(final_user.balance, 100.0, "User balance should remain 100.0");
println!("Verified user balance is still {} after rollback", final_user.balance);
} else {
return Err("Failed to find user after rollback".into());
}
// Clean up
drop(db);
Ok(())
}
/// Test multiple operations that might happen concurrently
fn test_concurrent_operations(base_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
// Open the test database
let db = sled::open(base_path.join("concurrent_tx"))?;
println!("Opened concurrent operations test database at: {:?}", base_path.join("concurrent_tx"));
// Create initial user
let user = User::new(
1,
"Concurrent Test".to_string(),
"concurrent@example.com".to_string(),
100.0,
);
// Insert initial user
db.insert(user.id.to_string().as_bytes(), bincode::serialize(&user)?)?;
db.flush()?;
println!("Inserted initial user with balance: {}", user.balance);
// Simulate two concurrent transactions
// Transaction 1: Add 50 to balance
println!("Starting simulated concurrent transaction 1: Add 50 to balance");
// Read current state for TX1
let mut tx1_user = user.clone();
if let Some(data) = db.get(user.id.to_string().as_bytes())? {
tx1_user = bincode::deserialize(&data)?;
}
// Transaction 2: Subtract 30 from balance
println!("Starting simulated concurrent transaction 2: Subtract 30 from balance");
// Read current state for TX2 (same starting point)
let mut tx2_user = user.clone();
if let Some(data) = db.get(user.id.to_string().as_bytes())? {
tx2_user = bincode::deserialize(&data)?;
}
// Modify in TX1
tx1_user.balance += 50.0;
// Modify in TX2
tx2_user.balance -= 30.0;
// Commit TX1 first
println!("Committing TX1");
db.insert(user.id.to_string().as_bytes(), bincode::serialize(&tx1_user)?)?;
db.flush()?;
// Now commit TX2 (would overwrite TX1 in naive implementation)
println!("Committing TX2");
db.insert(user.id.to_string().as_bytes(), bincode::serialize(&tx2_user)?)?;
db.flush()?;
// Verify the final state (last write wins, so should be TX2's value)
if let Some(data) = db.get(user.id.to_string().as_bytes())? {
let final_user: User = bincode::deserialize(&data)?;
assert_eq!(final_user.balance, 70.0, "Final balance should be 70.0 (TX2 overwrote TX1)");
println!("Final user balance is {} after both transactions", final_user.balance);
// In a real implementation with better concurrency control, you'd expect:
// println!("In a proper ACID system, this would have been 120.0 (100.0 - 30.0 + 50.0)");
} else {
return Err("Failed to find user after concurrent transactions".into());
}
// Clean up
drop(db);
Ok(())
}