...
This commit is contained in:
		
							
								
								
									
										15
									
								
								herodb/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								herodb/Cargo.toml
									
									
									
									
									
										Normal 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"] }
 | 
			
		||||
							
								
								
									
										25
									
								
								herodb/src/circle/models/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								herodb/src/circle/models/lib.rs
									
									
									
									
									
										Normal 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};
 | 
			
		||||
							
								
								
									
										8
									
								
								herodb/src/circle/models/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								herodb/src/circle/models/mod.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										147
									
								
								herodb/src/core/base.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										628
									
								
								herodb/src/core/db.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										70
									
								
								herodb/src/core/mod.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										36
									
								
								herodb/src/error.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										15
									
								
								herodb/src/lib.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										6
									
								
								herodb/src/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
// Export core module
 | 
			
		||||
pub mod core;
 | 
			
		||||
 | 
			
		||||
// Export zaz module
 | 
			
		||||
pub mod zaz;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										98
									
								
								herodb/src/zaz/DB_README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								herodb/src/zaz/DB_README.md
									
									
									
									
									
										Normal 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.
 | 
			
		||||
							
								
								
									
										160
									
								
								herodb/src/zaz/cmd/examples.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								herodb/src/zaz/cmd/examples.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										168
									
								
								herodb/src/zaz/db_tests.rs
									
									
									
									
									
										Normal 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");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										64
									
								
								herodb/src/zaz/examples.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								herodb/src/zaz/examples.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										9
									
								
								herodb/src/zaz/mod.rs
									
									
									
									
									
										Normal 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;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										236
									
								
								herodb/src/zaz/models/company.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								herodb/src/zaz/models/company.rs
									
									
									
									
									
										Normal 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");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								herodb/src/zaz/models/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								herodb/src/zaz/models/lib.rs
									
									
									
									
									
										Normal 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};
 | 
			
		||||
							
								
								
									
										172
									
								
								herodb/src/zaz/models/meeting.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								herodb/src/zaz/models/meeting.rs
									
									
									
									
									
										Normal 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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										155
									
								
								herodb/src/zaz/models/product.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								herodb/src/zaz/models/product.rs
									
									
									
									
									
										Normal 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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										146
									
								
								herodb/src/zaz/models/sale.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								herodb/src/zaz/models/sale.rs
									
									
									
									
									
										Normal 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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										78
									
								
								herodb/src/zaz/models/shareholder.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								herodb/src/zaz/models/shareholder.rs
									
									
									
									
									
										Normal 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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								herodb/src/zaz/models/user.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								herodb/src/zaz/models/user.rs
									
									
									
									
									
										Normal 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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										143
									
								
								herodb/src/zaz/models/vote.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								herodb/src/zaz/models/vote.rs
									
									
									
									
									
										Normal 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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										628
									
								
								herodb/src/zaz/tests/db_integration_test.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										628
									
								
								herodb/src/zaz/tests/db_integration_test.rs
									
									
									
									
									
										Normal 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!");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										265
									
								
								herodb/src/zaz/tests/transaction_test.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								herodb/src/zaz/tests/transaction_test.rs
									
									
									
									
									
										Normal 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(())
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user