From 30dade3d06c9cd65ff988dc27b1cf4dba235f683 Mon Sep 17 00:00:00 2001 From: kristof Date: Thu, 3 Apr 2025 08:47:35 +0200 Subject: [PATCH] ... --- README.md | 3 - herodb/Cargo.toml | 15 + herodb/src/circle/models/lib.rs | 25 + herodb/src/circle/models/mod.rs | 8 + herodb/src/core/base.rs | 147 +++++ herodb/src/core/db.rs | 628 ++++++++++++++++++++ herodb/src/core/mod.rs | 70 +++ herodb/src/error.rs | 36 ++ herodb/src/lib.rs | 15 + herodb/src/mod.rs | 6 + herodb/src/zaz/DB_README.md | 98 +++ herodb/src/zaz/cmd/examples.rs | 160 +++++ herodb/src/zaz/db_tests.rs | 168 ++++++ herodb/src/zaz/examples.rs | 64 ++ herodb/src/zaz/mod.rs | 9 + herodb/src/zaz/models/company.rs | 236 ++++++++ herodb/src/zaz/models/lib.rs | 25 + herodb/src/zaz/models/meeting.rs | 172 ++++++ herodb/src/zaz/models/product.rs | 155 +++++ herodb/src/zaz/models/sale.rs | 146 +++++ herodb/src/zaz/models/shareholder.rs | 78 +++ herodb/src/zaz/models/user.rs | 57 ++ herodb/src/zaz/models/vote.rs | 143 +++++ herodb/src/zaz/tests/db_integration_test.rs | 628 ++++++++++++++++++++ herodb/src/zaz/tests/transaction_test.rs | 265 +++++++++ 25 files changed, 3354 insertions(+), 3 deletions(-) delete mode 100644 README.md create mode 100644 herodb/Cargo.toml create mode 100644 herodb/src/circle/models/lib.rs create mode 100644 herodb/src/circle/models/mod.rs create mode 100644 herodb/src/core/base.rs create mode 100644 herodb/src/core/db.rs create mode 100644 herodb/src/core/mod.rs create mode 100644 herodb/src/error.rs create mode 100644 herodb/src/lib.rs create mode 100644 herodb/src/mod.rs create mode 100644 herodb/src/zaz/DB_README.md create mode 100644 herodb/src/zaz/cmd/examples.rs create mode 100644 herodb/src/zaz/db_tests.rs create mode 100644 herodb/src/zaz/examples.rs create mode 100644 herodb/src/zaz/mod.rs create mode 100644 herodb/src/zaz/models/company.rs create mode 100644 herodb/src/zaz/models/lib.rs create mode 100644 herodb/src/zaz/models/meeting.rs create mode 100644 herodb/src/zaz/models/product.rs create mode 100644 herodb/src/zaz/models/sale.rs create mode 100644 herodb/src/zaz/models/shareholder.rs create mode 100644 herodb/src/zaz/models/user.rs create mode 100644 herodb/src/zaz/models/vote.rs create mode 100644 herodb/src/zaz/tests/db_integration_test.rs create mode 100644 herodb/src/zaz/tests/transaction_test.rs diff --git a/README.md b/README.md deleted file mode 100644 index dee70ec..0000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# db - -code for working with the databases inside hero \ No newline at end of file diff --git a/herodb/Cargo.toml b/herodb/Cargo.toml new file mode 100644 index 0000000..a5dad14 --- /dev/null +++ b/herodb/Cargo.toml @@ -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"] } diff --git a/herodb/src/circle/models/lib.rs b/herodb/src/circle/models/lib.rs new file mode 100644 index 0000000..d6e09b1 --- /dev/null +++ b/herodb/src/circle/models/lib.rs @@ -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}; diff --git a/herodb/src/circle/models/mod.rs b/herodb/src/circle/models/mod.rs new file mode 100644 index 0000000..9d917b9 --- /dev/null +++ b/herodb/src/circle/models/mod.rs @@ -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; + diff --git a/herodb/src/core/base.rs b/herodb/src/core/base.rs new file mode 100644 index 0000000..30da0fd --- /dev/null +++ b/herodb/src/core/base.rs @@ -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 = Result; + +/// 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> { + let encoded: Vec = 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 { + 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 { + db: sled::Db, + _phantom: PhantomData, +} + +impl SledDB { + /// Opens or creates a Sled database at the specified path. + pub fn open>(path: P) -> SledDBResult { + 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 { + 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 { + 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> { + 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 + } +} diff --git a/herodb/src/core/db.rs b/herodb/src/core/db.rs new file mode 100644 index 0000000..a7fddf5 --- /dev/null +++ b/herodb/src/core/db.rs @@ -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, + company_db: SledDB, + meeting_db: SledDB, + product_db: SledDB, + sale_db: SledDB, + vote_db: SledDB, + shareholder_db: SledDB, + + // Type map for generic operations + type_map: HashMap>, + + // Locks to ensure thread safety for key areas + _write_locks: Arc>>, + + // Transaction state + transaction: RwLock>, +} + +impl DB { + /// Create a new DB instance with all model databases + pub fn new>(base_path: P) -> SledDBResult { + 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> = HashMap::new(); + type_map.insert(TypeId::of::(), Box::new(user_db.clone())); + type_map.insert(TypeId::of::(), Box::new(company_db.clone())); + type_map.insert(TypeId::of::(), Box::new(meeting_db.clone())); + type_map.insert(TypeId::of::(), Box::new(product_db.clone())); + type_map.insert(TypeId::of::(), Box::new(sale_db.clone())); + type_map.insert(TypeId::of::(), Box::new(vote_db.clone())); + type_map.insert(TypeId::of::(), 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::() { + 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::()) { + return db_ops.insert_any(&model); + } + } + // Company model + else if model_type == TypeId::of::() { + let model: Company = bincode::deserialize(serialized)?; + if let Some(db_ops) = self.type_map.get(&TypeId::of::()) { + return db_ops.insert_any(&model); + } + } + // Meeting model + else if model_type == TypeId::of::() { + let model: Meeting = bincode::deserialize(serialized)?; + if let Some(db_ops) = self.type_map.get(&TypeId::of::()) { + return db_ops.insert_any(&model); + } + } + // Product model + else if model_type == TypeId::of::() { + let model: Product = bincode::deserialize(serialized)?; + if let Some(db_ops) = self.type_map.get(&TypeId::of::()) { + return db_ops.insert_any(&model); + } + } + // Sale model + else if model_type == TypeId::of::() { + let model: Sale = bincode::deserialize(serialized)?; + if let Some(db_ops) = self.type_map.get(&TypeId::of::()) { + return db_ops.insert_any(&model); + } + } + // Vote model + else if model_type == TypeId::of::() { + let model: Vote = bincode::deserialize(serialized)?; + if let Some(db_ops) = self.type_map.get(&TypeId::of::()) { + return db_ops.insert_any(&model); + } + } + // Shareholder model + else if model_type == TypeId::of::() { + let model: Shareholder = bincode::deserialize(serialized)?; + if let Some(db_ops) = self.type_map.get(&TypeId::of::()) { + 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(&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::(), + 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::()) { + 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(&self, id: &str) -> Option, 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::(); + 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::(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(&self, id: &str) -> SledDBResult { + // First check if there's a pending value in the current transaction + if let Some(tx_result) = self.check_transaction::(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::()) { + 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::() { + Ok(t) => Ok(*t), + Err(_) => Err(SledDBError::TypeError), + } + }, + None => Err(SledDBError::TypeError), + } + } + + /// Delete a model instance by its ID and type + pub fn delete(&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::(), + 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::()) { + Some(db_ops) => db_ops.delete(id), + None => Err(SledDBError::TypeError), + } + } + + /// List all model instances of a specific type + pub fn list(&self) -> SledDBResult> { + // Look up the correct DB operations for type T in our type map + match self.type_map.get(&TypeId::of::()) { + Some(db_ops) => { + let result_any = db_ops.list_any()?; + // We expect the result to be of type Vec since we looked it up by TypeId + match result_any.downcast::>() { + 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 { + &self.user_db + } + + pub fn company_db(&self) -> &SledDB { + &self.company_db + } + + pub fn meeting_db(&self) -> &SledDB { + &self.meeting_db + } + + pub fn product_db(&self) -> &SledDB { + &self.product_db + } + + pub fn sale_db(&self) -> &SledDB { + &self.sale_db + } + + pub fn vote_db(&self) -> &SledDB { + &self.vote_db + } + + pub fn shareholder_db(&self) -> &SledDB { + &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.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.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::(&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.id.to_string()).expect("Failed to delete user in transaction"); + match db.get::(&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.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::(&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.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::(&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.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 = 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 = 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.id.to_string()) + .expect("Failed to delete company"); + + // Try to get the deleted company (should fail) + match db.get::(&company.id.to_string()) { + Err(SledDBError::NotFound(_)) => (), + _ => panic!("Expected NotFound error"), + } + } +} diff --git a/herodb/src/core/mod.rs b/herodb/src/core/mod.rs new file mode 100644 index 0000000..8268cbd --- /dev/null +++ b/herodb/src/core/mod.rs @@ -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, + }, + 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>; + fn list_any(&self) -> SledDBResult>; + fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()>; +} + +// Implementation of AnyDbOperations for any SledDB +impl AnyDbOperations for SledDB { + fn delete(&self, id: &str) -> SledDBResult<()> { + self.delete(id) + } + + fn get_any(&self, id: &str) -> SledDBResult> { + let result = self.get(id)?; + Ok(Box::new(result)) + } + + fn list_any(&self) -> SledDBResult> { + 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::() { + Some(t) => self.insert(t), + None => Err(SledDBError::TypeError), + } + } +} + +/// Transaction state for DB operations +pub struct TransactionState { + operations: Vec, + active: bool, +} + +impl TransactionState { + /// Create a new transaction state + pub fn new() -> Self { + Self { + operations: Vec::new(), + active: true, + } + } +} diff --git a/herodb/src/error.rs b/herodb/src/error.rs new file mode 100644 index 0000000..162c9b4 --- /dev/null +++ b/herodb/src/error.rs @@ -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 = std::result::Result; \ No newline at end of file diff --git a/herodb/src/lib.rs b/herodb/src/lib.rs new file mode 100644 index 0000000..2758848 --- /dev/null +++ b/herodb/src/lib.rs @@ -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; diff --git a/herodb/src/mod.rs b/herodb/src/mod.rs new file mode 100644 index 0000000..1fcf8b2 --- /dev/null +++ b/herodb/src/mod.rs @@ -0,0 +1,6 @@ +// Export core module +pub mod core; + +// Export zaz module +pub mod zaz; + diff --git a/herodb/src/zaz/DB_README.md b/herodb/src/zaz/DB_README.md new file mode 100644 index 0000000..b15b5b2 --- /dev/null +++ b/herodb/src/zaz/DB_README.md @@ -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 = db.list().expect("Failed to list users"); + +// Delete user +db.delete::(&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. diff --git a/herodb/src/zaz/cmd/examples.rs b/herodb/src/zaz/cmd/examples.rs new file mode 100644 index 0000000..f76daeb --- /dev/null +++ b/herodb/src/zaz/cmd/examples.rs @@ -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 { + 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 = 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 = 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 = 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.id.to_string()) + .map_err(|e| format!("Failed to delete shareholder: {}", e))?; + println!("Deleted shareholder: {}", shareholder.name); + + // Delete company + db.delete::(&company.id.to_string()) + .map_err(|e| format!("Failed to delete company: {}", e))?; + println!("Deleted company: {}", company.name); + + // Delete user + db.delete::(&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 = 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 = 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 = 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(()) +} diff --git a/herodb/src/zaz/db_tests.rs b/herodb/src/zaz/db_tests.rs new file mode 100644 index 0000000..b29dece --- /dev/null +++ b/herodb/src/zaz/db_tests.rs @@ -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, + updated_at: DateTime, + } + + 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"); + } +} diff --git a/herodb/src/zaz/examples.rs b/herodb/src/zaz/examples.rs new file mode 100644 index 0000000..4950f8d --- /dev/null +++ b/herodb/src/zaz/examples.rs @@ -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> { + 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.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::()?; + println!("Found {} companies", companies.len()); + + // Clean up + std::fs::remove_dir_all(db_path)?; + + Ok(()) +} diff --git a/herodb/src/zaz/mod.rs b/herodb/src/zaz/mod.rs new file mode 100644 index 0000000..5a736d9 --- /dev/null +++ b/herodb/src/zaz/mod.rs @@ -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; + diff --git a/herodb/src/zaz/models/company.rs b/herodb/src/zaz/models/company.rs new file mode 100644 index 0000000..266f4c6 --- /dev/null +++ b/herodb/src/zaz/models/company.rs @@ -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, + 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, + pub updated_at: DateTime, + // 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, + 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, // 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 = SledDB::open(db_path.join("company")).expect("Failed to open Company Sled DB"); + let shareholder_db: SledDB = 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"); + } +} diff --git a/herodb/src/zaz/models/lib.rs b/herodb/src/zaz/models/lib.rs new file mode 100644 index 0000000..4fa6188 --- /dev/null +++ b/herodb/src/zaz/models/lib.rs @@ -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}; diff --git a/herodb/src/zaz/models/meeting.rs b/herodb/src/zaz/models/meeting.rs new file mode 100644 index 0000000..864f3f5 --- /dev/null +++ b/herodb/src/zaz/models/meeting.rs @@ -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, +} + +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, + pub location: String, + pub description: String, + pub status: MeetingStatus, + pub minutes: String, + pub created_at: DateTime, + pub updated_at: DateTime, + pub attendees: Vec, +} + +// 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, + 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" + } +} diff --git a/herodb/src/zaz/models/product.rs b/herodb/src/zaz/models/product.rs new file mode 100644 index 0000000..ed7e756 --- /dev/null +++ b/herodb/src/zaz/models/product.rs @@ -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, + pub updated_at: DateTime, +} + +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, + pub updated_at: DateTime, + pub max_amount: u16, // means allows us to define how many max of this there are + pub purchase_till: DateTime, + pub active_till: DateTime, // after this product no longer active if e.g. a service + pub components: Vec, +} + +// 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) { + self.purchase_till = purchase_till; + self.updated_at = Utc::now(); + } + + /// Update the active timeframe + pub fn set_active_period(&mut self, active_till: DateTime) { + 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" + } +} diff --git a/herodb/src/zaz/models/sale.rs b/herodb/src/zaz/models/sale.rs new file mode 100644 index 0000000..f5fb98c --- /dev/null +++ b/herodb/src/zaz/models/sale.rs @@ -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, // 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, + ) -> 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, + pub created_at: DateTime, + pub updated_at: DateTime, + pub items: Vec, +} + +// 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" + } +} diff --git a/herodb/src/zaz/models/shareholder.rs b/herodb/src/zaz/models/shareholder.rs new file mode 100644 index 0000000..5a2497a --- /dev/null +++ b/herodb/src/zaz/models/shareholder.rs @@ -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, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +// 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" + } +} diff --git a/herodb/src/zaz/models/user.rs b/herodb/src/zaz/models/user.rs new file mode 100644 index 0000000..5e64629 --- /dev/null +++ b/herodb/src/zaz/models/user.rs @@ -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, + pub updated_at: DateTime, +} + +// 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" + } +} diff --git a/herodb/src/zaz/models/vote.rs b/herodb/src/zaz/models/vote.rs new file mode 100644 index 0000000..75ff4e4 --- /dev/null +++ b/herodb/src/zaz/models/vote.rs @@ -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, + pub end_date: DateTime, + pub status: VoteStatus, + pub created_at: DateTime, + pub updated_at: DateTime, + pub options: Vec, + pub ballots: Vec, + pub private_group: Vec, // 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, +} + +// 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, + end_date: DateTime, + 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" + } +} diff --git a/herodb/src/zaz/tests/db_integration_test.rs b/herodb/src/zaz/tests/db_integration_test.rs new file mode 100644 index 0000000..7bc2e19 --- /dev/null +++ b/herodb/src/zaz/tests/db_integration_test.rs @@ -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> { + // 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, + updated_at: DateTime, + } + + 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, + fiscal_year_end: String, + email: String, + phone: String, + website: String, + address: String, + business_type: BusinessType, + industry: String, + description: String, + status: CompanyStatus, + created_at: DateTime, + updated_at: DateTime, + } + + impl Company { + fn new( + id: u32, + name: String, + registration_number: String, + registration_date: DateTime, + 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> { + // 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, + updated_at: DateTime, + } + + 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> { + // 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, + fiscal_year_end: String, + email: String, + phone: String, + website: String, + address: String, + business_type: BusinessType, + industry: String, + description: String, + status: CompanyStatus, + created_at: DateTime, + updated_at: DateTime, + } + + impl Company { + fn new( + id: u32, + name: String, + registration_number: String, + registration_date: DateTime, + 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> { + // 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, + updated_at: DateTime, + } + + 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!"); +} diff --git a/herodb/src/zaz/tests/transaction_test.rs b/herodb/src/zaz/tests/transaction_test.rs new file mode 100644 index 0000000..bbc5d0b --- /dev/null +++ b/herodb/src/zaz/tests/transaction_test.rs @@ -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> { + // 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, + updated_at: DateTime, +} + +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> { + // 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> { + // 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> { + // 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(()) +}