...
This commit is contained in:
parent
cf6c52a2bc
commit
30dade3d06
15
herodb/Cargo.toml
Normal file
15
herodb/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "herodb"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "A database library built on top of sled with model support"
|
||||||
|
license = "MIT"
|
||||||
|
authors = ["HeroCode Team"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sled = "0.34.7"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
uuid = { version = "1.3", features = ["v4", "serde"] }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
25
herodb/src/circle/models/lib.rs
Normal file
25
herodb/src/circle/models/lib.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
pub mod user;
|
||||||
|
pub mod vote;
|
||||||
|
pub mod company;
|
||||||
|
pub mod meeting;
|
||||||
|
pub mod product;
|
||||||
|
pub mod sale;
|
||||||
|
pub mod shareholder;
|
||||||
|
// pub mod db; // Moved to src/zaz/db
|
||||||
|
// pub mod migration; // Removed
|
||||||
|
|
||||||
|
// Re-export all model types for convenience
|
||||||
|
pub use user::User;
|
||||||
|
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
|
||||||
|
pub use company::{Company, CompanyStatus, BusinessType};
|
||||||
|
pub use meeting::Meeting;
|
||||||
|
pub use product::{Product, Currency, ProductComponent, ProductType, ProductStatus};
|
||||||
|
pub use sale::Sale;
|
||||||
|
pub use shareholder::Shareholder;
|
||||||
|
|
||||||
|
// Re-export database components
|
||||||
|
// pub use db::{DB, DBError, DBResult, Model, ModelMetadata}; // Removed old DB re-exports
|
||||||
|
pub use crate::db::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel}; // Re-export Sled DB components
|
||||||
|
|
||||||
|
// Re-export migration components - Removed
|
||||||
|
// pub use migration::{Migrator, MigrationError, MigrationResult};
|
8
herodb/src/circle/models/mod.rs
Normal file
8
herodb/src/circle/models/mod.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Declare the models submodule
|
||||||
|
#[path = "models/lib.rs"] // Tell compiler where to find models module source
|
||||||
|
pub mod models;
|
||||||
|
|
||||||
|
// Declare the db submodule with the new database implementation
|
||||||
|
#[path = "db/mod.rs"] // Tell compiler where to find db module source
|
||||||
|
pub mod db;
|
||||||
|
|
147
herodb/src/core/base.rs
Normal file
147
herodb/src/core/base.rs
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
use bincode;
|
||||||
|
use brotli::{CompressorReader, Decompressor};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sled;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::path::Path;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Errors that can occur during Sled database operations
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum SledDBError {
|
||||||
|
#[error("Sled database error: {0}")]
|
||||||
|
SledError(#[from] sled::Error),
|
||||||
|
#[error("Serialization/Deserialization error: {0}")]
|
||||||
|
SerdeError(#[from] bincode::Error),
|
||||||
|
#[error("Compression/Decompression error: {0}")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
#[error("Record not found for ID: {0}")]
|
||||||
|
NotFound(String),
|
||||||
|
#[error("Type mismatch during deserialization")]
|
||||||
|
TypeError,
|
||||||
|
#[error("General database error: {0}")]
|
||||||
|
GeneralError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result type for Sled DB operations
|
||||||
|
pub type SledDBResult<T> = Result<T, SledDBError>;
|
||||||
|
|
||||||
|
/// Trait for models that can be stored in the Sled database.
|
||||||
|
/// Requires `Serialize` and `Deserialize` for the underlying storage mechanism.
|
||||||
|
pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized {
|
||||||
|
/// Serializes and compresses the instance using bincode and brotli.
|
||||||
|
fn dump(&self) -> SledDBResult<Vec<u8>> {
|
||||||
|
let encoded: Vec<u8> = bincode::serialize(self)?;
|
||||||
|
|
||||||
|
let mut compressed = Vec::new();
|
||||||
|
// Default Brotli parameters: quality 5, lgwin 22 (window size)
|
||||||
|
const BROTLI_QUALITY: u32 = 5;
|
||||||
|
const BROTLI_LGWIN: u32 = 22;
|
||||||
|
const BUFFER_SIZE: usize = 4096; // 4KB buffer
|
||||||
|
|
||||||
|
let mut compressor = CompressorReader::new(
|
||||||
|
&encoded[..],
|
||||||
|
BUFFER_SIZE,
|
||||||
|
BROTLI_QUALITY,
|
||||||
|
BROTLI_LGWIN
|
||||||
|
);
|
||||||
|
compressor.read_to_end(&mut compressed)?;
|
||||||
|
|
||||||
|
Ok(compressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes and decompresses data from bytes into an instance.
|
||||||
|
fn load_from_bytes(data: &[u8]) -> SledDBResult<Self> {
|
||||||
|
let mut decompressed = Vec::new();
|
||||||
|
const BUFFER_SIZE: usize = 4096; // 4KB buffer
|
||||||
|
|
||||||
|
let mut decompressor = Decompressor::new(data, BUFFER_SIZE);
|
||||||
|
decompressor.read_to_end(&mut decompressed)?;
|
||||||
|
|
||||||
|
let decoded: Self = bincode::deserialize(&decompressed)?;
|
||||||
|
Ok(decoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait identifying a model suitable for the Sled database.
|
||||||
|
/// The 'static lifetime bound is required for type identification via Any
|
||||||
|
pub trait SledModel: Storable + Debug + Clone + Send + Sync + 'static {
|
||||||
|
/// Returns the unique ID for this model instance, used as the key in Sled.
|
||||||
|
fn get_id(&self) -> String;
|
||||||
|
|
||||||
|
/// Returns a prefix used for this model type in the Sled database.
|
||||||
|
/// Helps to logically separate different model types.
|
||||||
|
fn db_prefix() -> &'static str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A generic database layer on top of Sled.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SledDB<T: SledModel> {
|
||||||
|
db: sled::Db,
|
||||||
|
_phantom: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: SledModel> SledDB<T> {
|
||||||
|
/// Opens or creates a Sled database at the specified path.
|
||||||
|
pub fn open<P: AsRef<Path>>(path: P) -> SledDBResult<Self> {
|
||||||
|
let db = sled::open(path)?;
|
||||||
|
Ok(Self {
|
||||||
|
db,
|
||||||
|
_phantom: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates the full Sled key using the model's prefix and ID.
|
||||||
|
fn get_full_key(id: &str) -> Vec<u8> {
|
||||||
|
format!("{}:{}", T::db_prefix(), id).into_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts or updates a model instance in the database.
|
||||||
|
pub fn insert(&self, model: &T) -> SledDBResult<()> {
|
||||||
|
let key = Self::get_full_key(&model.get_id());
|
||||||
|
let value = model.dump()?;
|
||||||
|
self.db.insert(key, value)?;
|
||||||
|
// Optionally force a disk flush for durability, but it impacts performance.
|
||||||
|
// self.db.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves a model instance by its ID.
|
||||||
|
pub fn get(&self, id: &str) -> SledDBResult<T> {
|
||||||
|
let key = Self::get_full_key(id);
|
||||||
|
match self.db.get(&key)? {
|
||||||
|
Some(ivec) => T::load_from_bytes(&ivec),
|
||||||
|
None => Err(SledDBError::NotFound(id.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a model instance by its ID.
|
||||||
|
pub fn delete(&self, id: &str) -> SledDBResult<()> {
|
||||||
|
let key = Self::get_full_key(id);
|
||||||
|
match self.db.remove(&key)? {
|
||||||
|
Some(_) => Ok(()),
|
||||||
|
None => Err(SledDBError::NotFound(id.to_string())),
|
||||||
|
}
|
||||||
|
// Optionally flush after delete
|
||||||
|
// self.db.flush()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists all models of this type.
|
||||||
|
/// Warning: This can be inefficient for large datasets as it loads all models into memory.
|
||||||
|
pub fn list(&self) -> SledDBResult<Vec<T>> {
|
||||||
|
let prefix = format!("{}:", T::db_prefix());
|
||||||
|
let mut models = Vec::new();
|
||||||
|
for result in self.db.scan_prefix(prefix.as_bytes()) {
|
||||||
|
let (_key, value) = result?;
|
||||||
|
models.push(T::load_from_bytes(&value)?);
|
||||||
|
}
|
||||||
|
Ok(models)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides access to the underlying Sled Db instance for advanced operations.
|
||||||
|
pub fn raw_db(&self) -> &sled::Db {
|
||||||
|
&self.db
|
||||||
|
}
|
||||||
|
}
|
628
herodb/src/core/db.rs
Normal file
628
herodb/src/core/db.rs
Normal file
@ -0,0 +1,628 @@
|
|||||||
|
|
||||||
|
use crate::zaz::models::*;
|
||||||
|
use std::any::TypeId;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
|
||||||
|
/// Main DB manager that automatically handles all root models
|
||||||
|
pub struct DB {
|
||||||
|
db_path: PathBuf,
|
||||||
|
user_db: SledDB<User>,
|
||||||
|
company_db: SledDB<Company>,
|
||||||
|
meeting_db: SledDB<Meeting>,
|
||||||
|
product_db: SledDB<Product>,
|
||||||
|
sale_db: SledDB<Sale>,
|
||||||
|
vote_db: SledDB<Vote>,
|
||||||
|
shareholder_db: SledDB<Shareholder>,
|
||||||
|
|
||||||
|
// Type map for generic operations
|
||||||
|
type_map: HashMap<TypeId, Box<dyn AnyDbOperations>>,
|
||||||
|
|
||||||
|
// Locks to ensure thread safety for key areas
|
||||||
|
_write_locks: Arc<Mutex<HashMap<String, bool>>>,
|
||||||
|
|
||||||
|
// Transaction state
|
||||||
|
transaction: RwLock<Option<TransactionState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DB {
|
||||||
|
/// Create a new DB instance with all model databases
|
||||||
|
pub fn new<P: Into<PathBuf>>(base_path: P) -> SledDBResult<Self> {
|
||||||
|
let base_path = base_path.into();
|
||||||
|
|
||||||
|
// Ensure base directory exists
|
||||||
|
if !base_path.exists() {
|
||||||
|
std::fs::create_dir_all(&base_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create individual database instances for each model type
|
||||||
|
let user_db = SledDB::open(base_path.join("user"))?;
|
||||||
|
let company_db = SledDB::open(base_path.join("company"))?;
|
||||||
|
let meeting_db = SledDB::open(base_path.join("meeting"))?;
|
||||||
|
let product_db = SledDB::open(base_path.join("product"))?;
|
||||||
|
let sale_db = SledDB::open(base_path.join("sale"))?;
|
||||||
|
let vote_db = SledDB::open(base_path.join("vote"))?;
|
||||||
|
let shareholder_db = SledDB::open(base_path.join("shareholder"))?;
|
||||||
|
|
||||||
|
// Create type map for generic operations
|
||||||
|
let mut type_map: HashMap<TypeId, Box<dyn AnyDbOperations>> = HashMap::new();
|
||||||
|
type_map.insert(TypeId::of::<User>(), Box::new(user_db.clone()));
|
||||||
|
type_map.insert(TypeId::of::<Company>(), Box::new(company_db.clone()));
|
||||||
|
type_map.insert(TypeId::of::<Meeting>(), Box::new(meeting_db.clone()));
|
||||||
|
type_map.insert(TypeId::of::<Product>(), Box::new(product_db.clone()));
|
||||||
|
type_map.insert(TypeId::of::<Sale>(), Box::new(sale_db.clone()));
|
||||||
|
type_map.insert(TypeId::of::<Vote>(), Box::new(vote_db.clone()));
|
||||||
|
type_map.insert(TypeId::of::<Shareholder>(), Box::new(shareholder_db.clone()));
|
||||||
|
|
||||||
|
let _write_locks = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
let transaction = RwLock::new(None);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
db_path: base_path,
|
||||||
|
user_db,
|
||||||
|
company_db,
|
||||||
|
meeting_db,
|
||||||
|
product_db,
|
||||||
|
sale_db,
|
||||||
|
vote_db,
|
||||||
|
shareholder_db,
|
||||||
|
type_map,
|
||||||
|
_write_locks,
|
||||||
|
transaction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction-related methods
|
||||||
|
|
||||||
|
/// Begin a new transaction
|
||||||
|
pub fn begin_transaction(&self) -> SledDBResult<()> {
|
||||||
|
let mut tx = self.transaction.write().unwrap();
|
||||||
|
if tx.is_some() {
|
||||||
|
return Err(SledDBError::GeneralError("Transaction already in progress".into()));
|
||||||
|
}
|
||||||
|
*tx = Some(TransactionState::new());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a transaction is active
|
||||||
|
pub fn has_active_transaction(&self) -> bool {
|
||||||
|
let tx = self.transaction.read().unwrap();
|
||||||
|
tx.is_some() && tx.as_ref().unwrap().active
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a set operation with the serialized data - bypass transaction check
|
||||||
|
fn apply_set_operation(&self, model_type: TypeId, serialized: &[u8]) -> SledDBResult<()> {
|
||||||
|
// User model
|
||||||
|
if model_type == TypeId::of::<User>() {
|
||||||
|
let model: User = bincode::deserialize(serialized)?;
|
||||||
|
// Access the database operations directly to avoid transaction recursion
|
||||||
|
if let Some(db_ops) = self.type_map.get(&TypeId::of::<User>()) {
|
||||||
|
return db_ops.insert_any(&model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Company model
|
||||||
|
else if model_type == TypeId::of::<Company>() {
|
||||||
|
let model: Company = bincode::deserialize(serialized)?;
|
||||||
|
if let Some(db_ops) = self.type_map.get(&TypeId::of::<Company>()) {
|
||||||
|
return db_ops.insert_any(&model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Meeting model
|
||||||
|
else if model_type == TypeId::of::<Meeting>() {
|
||||||
|
let model: Meeting = bincode::deserialize(serialized)?;
|
||||||
|
if let Some(db_ops) = self.type_map.get(&TypeId::of::<Meeting>()) {
|
||||||
|
return db_ops.insert_any(&model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Product model
|
||||||
|
else if model_type == TypeId::of::<Product>() {
|
||||||
|
let model: Product = bincode::deserialize(serialized)?;
|
||||||
|
if let Some(db_ops) = self.type_map.get(&TypeId::of::<Product>()) {
|
||||||
|
return db_ops.insert_any(&model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sale model
|
||||||
|
else if model_type == TypeId::of::<Sale>() {
|
||||||
|
let model: Sale = bincode::deserialize(serialized)?;
|
||||||
|
if let Some(db_ops) = self.type_map.get(&TypeId::of::<Sale>()) {
|
||||||
|
return db_ops.insert_any(&model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Vote model
|
||||||
|
else if model_type == TypeId::of::<Vote>() {
|
||||||
|
let model: Vote = bincode::deserialize(serialized)?;
|
||||||
|
if let Some(db_ops) = self.type_map.get(&TypeId::of::<Vote>()) {
|
||||||
|
return db_ops.insert_any(&model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Shareholder model
|
||||||
|
else if model_type == TypeId::of::<Shareholder>() {
|
||||||
|
let model: Shareholder = bincode::deserialize(serialized)?;
|
||||||
|
if let Some(db_ops) = self.type_map.get(&TypeId::of::<Shareholder>()) {
|
||||||
|
return db_ops.insert_any(&model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(SledDBError::TypeError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commit the current transaction, applying all operations
|
||||||
|
pub fn commit_transaction(&self) -> SledDBResult<()> {
|
||||||
|
let mut tx_guard = self.transaction.write().unwrap();
|
||||||
|
|
||||||
|
if let Some(tx_state) = tx_guard.take() {
|
||||||
|
if !tx_state.active {
|
||||||
|
return Err(SledDBError::GeneralError("Transaction not active".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute all operations in the transaction
|
||||||
|
for op in tx_state.operations {
|
||||||
|
match op {
|
||||||
|
DbOperation::Set { model_type, serialized } => {
|
||||||
|
self.apply_set_operation(model_type, &serialized)?;
|
||||||
|
},
|
||||||
|
DbOperation::Delete { model_type, id } => {
|
||||||
|
let db_ops = self.type_map.get(&model_type)
|
||||||
|
.ok_or_else(|| SledDBError::TypeError)?;
|
||||||
|
db_ops.delete(&id)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(SledDBError::GeneralError("No active transaction".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rollback the current transaction, discarding all operations
|
||||||
|
pub fn rollback_transaction(&self) -> SledDBResult<()> {
|
||||||
|
let mut tx = self.transaction.write().unwrap();
|
||||||
|
if tx.is_none() {
|
||||||
|
return Err(SledDBError::GeneralError("No active transaction".into()));
|
||||||
|
}
|
||||||
|
*tx = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the path to the database
|
||||||
|
pub fn path(&self) -> &PathBuf {
|
||||||
|
&self.db_path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic methods that work with any supported model type
|
||||||
|
|
||||||
|
/// Insert a model instance into its appropriate database based on type
|
||||||
|
pub fn set<T: SledModel>(&self, model: &T) -> SledDBResult<()> {
|
||||||
|
// Try to acquire a write lock on the transaction
|
||||||
|
let mut tx_guard = self.transaction.write().unwrap();
|
||||||
|
|
||||||
|
// Check if there's an active transaction
|
||||||
|
if let Some(tx_state) = tx_guard.as_mut() {
|
||||||
|
if tx_state.active {
|
||||||
|
// Serialize the model for later use
|
||||||
|
let serialized = bincode::serialize(model)?;
|
||||||
|
|
||||||
|
// Record a Set operation in the transaction
|
||||||
|
tx_state.operations.push(DbOperation::Set {
|
||||||
|
model_type: TypeId::of::<T>(),
|
||||||
|
serialized,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here, either there's no transaction or it's not active
|
||||||
|
// Drop the write lock before doing a direct database operation
|
||||||
|
drop(tx_guard);
|
||||||
|
|
||||||
|
// Execute directly
|
||||||
|
match self.type_map.get(&TypeId::of::<T>()) {
|
||||||
|
Some(db_ops) => db_ops.insert_any(model),
|
||||||
|
None => Err(SledDBError::TypeError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the transaction state for the given type and id
|
||||||
|
fn check_transaction<T: SledModel>(&self, id: &str) -> Option<Result<Option<T>, SledDBError>> {
|
||||||
|
// Try to acquire a read lock on the transaction
|
||||||
|
let tx_guard = self.transaction.read().unwrap();
|
||||||
|
|
||||||
|
if let Some(tx_state) = tx_guard.as_ref() {
|
||||||
|
if !tx_state.active {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let type_id = TypeId::of::<T>();
|
||||||
|
let id_str = id.to_string();
|
||||||
|
|
||||||
|
// Process operations in reverse order (last operation wins)
|
||||||
|
for op in tx_state.operations.iter().rev() {
|
||||||
|
match op {
|
||||||
|
// First check if this ID has been deleted in the transaction
|
||||||
|
DbOperation::Delete { model_type, id: op_id } => {
|
||||||
|
if *model_type == type_id && op_id == id {
|
||||||
|
// Return NotFound error for deleted records
|
||||||
|
return Some(Err(SledDBError::NotFound(id.to_string())));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Then check if it has been set in the transaction
|
||||||
|
DbOperation::Set { model_type, serialized } => {
|
||||||
|
if *model_type == type_id {
|
||||||
|
// Deserialize to check the ID
|
||||||
|
if let Ok(model) = bincode::deserialize::<T>(serialized) {
|
||||||
|
if model.get_id() == id_str {
|
||||||
|
return Some(Ok(Some(model)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not found in transaction (continue to database)
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a model instance by its ID and type
|
||||||
|
pub fn get<T: SledModel>(&self, id: &str) -> SledDBResult<T> {
|
||||||
|
// First check if there's a pending value in the current transaction
|
||||||
|
if let Some(tx_result) = self.check_transaction::<T>(id) {
|
||||||
|
match tx_result {
|
||||||
|
Ok(Some(model)) => return Ok(model),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
Ok(None) => {} // Should never happen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no pending value, look up from the database
|
||||||
|
match self.type_map.get(&TypeId::of::<T>()) {
|
||||||
|
Some(db_ops) => {
|
||||||
|
let result_any = db_ops.get_any(id)?;
|
||||||
|
// We expect the result to be of type T since we looked it up by TypeId
|
||||||
|
match result_any.downcast::<T>() {
|
||||||
|
Ok(t) => Ok(*t),
|
||||||
|
Err(_) => Err(SledDBError::TypeError),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Err(SledDBError::TypeError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a model instance by its ID and type
|
||||||
|
pub fn delete<T: SledModel>(&self, id: &str) -> SledDBResult<()> {
|
||||||
|
// Try to acquire a write lock on the transaction
|
||||||
|
let mut tx_guard = self.transaction.write().unwrap();
|
||||||
|
|
||||||
|
// Check if there's an active transaction
|
||||||
|
if let Some(tx_state) = tx_guard.as_mut() {
|
||||||
|
if tx_state.active {
|
||||||
|
// Record a Delete operation in the transaction
|
||||||
|
tx_state.operations.push(DbOperation::Delete {
|
||||||
|
model_type: TypeId::of::<T>(),
|
||||||
|
id: id.to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here, either there's no transaction or it's not active
|
||||||
|
// Drop the write lock before doing a direct database operation
|
||||||
|
drop(tx_guard);
|
||||||
|
|
||||||
|
// Execute directly
|
||||||
|
match self.type_map.get(&TypeId::of::<T>()) {
|
||||||
|
Some(db_ops) => db_ops.delete(id),
|
||||||
|
None => Err(SledDBError::TypeError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all model instances of a specific type
|
||||||
|
pub fn list<T: SledModel>(&self) -> SledDBResult<Vec<T>> {
|
||||||
|
// Look up the correct DB operations for type T in our type map
|
||||||
|
match self.type_map.get(&TypeId::of::<T>()) {
|
||||||
|
Some(db_ops) => {
|
||||||
|
let result_any = db_ops.list_any()?;
|
||||||
|
// We expect the result to be of type Vec<T> since we looked it up by TypeId
|
||||||
|
match result_any.downcast::<Vec<T>>() {
|
||||||
|
Ok(vec_t) => Ok(*vec_t),
|
||||||
|
Err(_) => Err(SledDBError::TypeError),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Err(SledDBError::TypeError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods to get each specific database
|
||||||
|
|
||||||
|
pub fn user_db(&self) -> &SledDB<User> {
|
||||||
|
&self.user_db
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn company_db(&self) -> &SledDB<Company> {
|
||||||
|
&self.company_db
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn meeting_db(&self) -> &SledDB<Meeting> {
|
||||||
|
&self.meeting_db
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn product_db(&self) -> &SledDB<Product> {
|
||||||
|
&self.product_db
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sale_db(&self) -> &SledDB<Sale> {
|
||||||
|
&self.sale_db
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vote_db(&self) -> &SledDB<Vote> {
|
||||||
|
&self.vote_db
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shareholder_db(&self) -> &SledDB<Shareholder> {
|
||||||
|
&self.shareholder_db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The as_type function is no longer needed with our type-map based implementation
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use chrono::Utc;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_your_writes() {
|
||||||
|
// Create a temporary directory for the test
|
||||||
|
let dir = tempdir().expect("Failed to create temp dir");
|
||||||
|
let db = DB::new(dir.path()).expect("Failed to create DB");
|
||||||
|
|
||||||
|
// Create a user
|
||||||
|
let user = User::new(
|
||||||
|
10,
|
||||||
|
"Original User".to_string(),
|
||||||
|
"original@example.com".to_string(),
|
||||||
|
"password".to_string(),
|
||||||
|
"Original Corp".to_string(),
|
||||||
|
"User".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert the user directly (no transaction)
|
||||||
|
db.set(&user).expect("Failed to insert user");
|
||||||
|
|
||||||
|
// Begin a transaction
|
||||||
|
db.begin_transaction().expect("Failed to begin transaction");
|
||||||
|
|
||||||
|
// Verify we can read the original user
|
||||||
|
let original = db.get::<User>(&user.id.to_string()).expect("Failed to get original user");
|
||||||
|
assert_eq!(original.name, "Original User");
|
||||||
|
|
||||||
|
// Create a modified user with the same ID
|
||||||
|
let modified_user = User::new(
|
||||||
|
10,
|
||||||
|
"Modified User".to_string(),
|
||||||
|
"modified@example.com".to_string(),
|
||||||
|
"new_password".to_string(),
|
||||||
|
"Modified Corp".to_string(),
|
||||||
|
"Admin".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the user in the transaction
|
||||||
|
db.set(&modified_user).expect("Failed to update user in transaction");
|
||||||
|
|
||||||
|
// Verify we can read our own writes within the transaction
|
||||||
|
let in_transaction = db.get::<User>(&user.id.to_string()).expect("Failed to get user from transaction");
|
||||||
|
assert_eq!(in_transaction.name, "Modified User");
|
||||||
|
assert_eq!(in_transaction.email, "modified@example.com");
|
||||||
|
|
||||||
|
// Create a new user that only exists in the transaction
|
||||||
|
let new_user = User::new(
|
||||||
|
20,
|
||||||
|
"Transaction Only User".to_string(),
|
||||||
|
"tx@example.com".to_string(),
|
||||||
|
"password".to_string(),
|
||||||
|
"TX Corp".to_string(),
|
||||||
|
"Admin".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the new user in the transaction
|
||||||
|
db.set(&new_user).expect("Failed to add new user in transaction");
|
||||||
|
|
||||||
|
// Verify we can read the new user within the transaction
|
||||||
|
let new_in_tx = db.get::<User>(&new_user.id.to_string()).expect("Failed to get new user from transaction");
|
||||||
|
assert_eq!(new_in_tx.name, "Transaction Only User");
|
||||||
|
|
||||||
|
// Delete a user in the transaction and verify it appears deleted within the transaction
|
||||||
|
db.delete::<User>(&user.id.to_string()).expect("Failed to delete user in transaction");
|
||||||
|
match db.get::<User>(&user.id.to_string()) {
|
||||||
|
Err(SledDBError::NotFound(_)) => (), // Expected result
|
||||||
|
Ok(_) => panic!("User should appear deleted within transaction"),
|
||||||
|
Err(e) => panic!("Unexpected error: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rollback the transaction
|
||||||
|
db.rollback_transaction().expect("Failed to rollback transaction");
|
||||||
|
|
||||||
|
// Verify the original user is still available and unchanged after rollback
|
||||||
|
let after_rollback = db.get::<User>(&user.id.to_string()).expect("Failed to get user after rollback");
|
||||||
|
assert_eq!(after_rollback.name, "Original User");
|
||||||
|
|
||||||
|
// Verify the transaction-only user doesn't exist after rollback
|
||||||
|
assert!(db.get::<User>(&new_user.id.to_string()).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transactions() {
|
||||||
|
// Create a temporary directory for the test
|
||||||
|
let dir = tempdir().expect("Failed to create temp dir");
|
||||||
|
let db = DB::new(dir.path()).expect("Failed to create DB");
|
||||||
|
|
||||||
|
// Create a sample user and company for testing
|
||||||
|
let user = User::new(
|
||||||
|
1,
|
||||||
|
"Transaction Test User".to_string(),
|
||||||
|
"tx@example.com".to_string(),
|
||||||
|
"password".to_string(),
|
||||||
|
"Test Corp".to_string(),
|
||||||
|
"Admin".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let incorporation_date = Utc::now();
|
||||||
|
let company = Company::new(
|
||||||
|
1,
|
||||||
|
"Transaction Test Corp".to_string(),
|
||||||
|
"TX123".to_string(),
|
||||||
|
incorporation_date,
|
||||||
|
"12-31".to_string(),
|
||||||
|
"tx@corp.com".to_string(),
|
||||||
|
"123-456-7890".to_string(),
|
||||||
|
"www.testcorp.com".to_string(),
|
||||||
|
"123 Test St".to_string(),
|
||||||
|
BusinessType::Global,
|
||||||
|
"Tech".to_string(),
|
||||||
|
"A test company for transactions".to_string(),
|
||||||
|
CompanyStatus::Active,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test successful transaction (multiple operations committed at once)
|
||||||
|
{
|
||||||
|
// Start a transaction
|
||||||
|
db.begin_transaction().expect("Failed to begin transaction");
|
||||||
|
assert!(db.has_active_transaction());
|
||||||
|
|
||||||
|
// Perform multiple operations within the transaction
|
||||||
|
db.set(&user).expect("Failed to add user to transaction");
|
||||||
|
db.set(&company).expect("Failed to add company to transaction");
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
db.commit_transaction().expect("Failed to commit transaction");
|
||||||
|
assert!(!db.has_active_transaction());
|
||||||
|
|
||||||
|
// Verify both operations were applied
|
||||||
|
let retrieved_user: User = db.get(&user.id.to_string()).expect("Failed to get user after commit");
|
||||||
|
let retrieved_company: Company = db.get(&company.id.to_string()).expect("Failed to get company after commit");
|
||||||
|
|
||||||
|
assert_eq!(user.name, retrieved_user.name);
|
||||||
|
assert_eq!(company.name, retrieved_company.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test transaction rollback
|
||||||
|
{
|
||||||
|
// Create another user that should not be persisted
|
||||||
|
let temp_user = User::new(
|
||||||
|
2,
|
||||||
|
"Temporary User".to_string(),
|
||||||
|
"temp@example.com".to_string(),
|
||||||
|
"password".to_string(),
|
||||||
|
"Temp Corp".to_string(),
|
||||||
|
"Temp".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
db.begin_transaction().expect("Failed to begin transaction");
|
||||||
|
|
||||||
|
// Add the temporary user
|
||||||
|
db.set(&temp_user).expect("Failed to add temporary user to transaction");
|
||||||
|
|
||||||
|
// Perform a delete operation in the transaction
|
||||||
|
db.delete::<Company>(&company.id.to_string()).expect("Failed to delete company in transaction");
|
||||||
|
|
||||||
|
// Rollback the transaction - should discard all operations
|
||||||
|
db.rollback_transaction().expect("Failed to rollback transaction");
|
||||||
|
assert!(!db.has_active_transaction());
|
||||||
|
|
||||||
|
// Verify the temporary user was not added
|
||||||
|
match db.get::<User>(&temp_user.id.to_string()) {
|
||||||
|
Err(SledDBError::NotFound(_)) => (), // Expected outcome
|
||||||
|
Ok(_) => panic!("Temporary user should not exist after rollback"),
|
||||||
|
Err(e) => panic!("Unexpected error: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the company was not deleted
|
||||||
|
let company_still_exists = db.get::<Company>(&company.id.to_string()).is_ok();
|
||||||
|
assert!(company_still_exists, "Company should still exist after transaction rollback");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generic_db_operations() {
|
||||||
|
// Create a temporary directory for the test
|
||||||
|
let dir = tempdir().expect("Failed to create temp dir");
|
||||||
|
let db = DB::new(dir.path()).expect("Failed to create DB");
|
||||||
|
|
||||||
|
// Test simple transaction functionality
|
||||||
|
assert!(!db.has_active_transaction());
|
||||||
|
db.begin_transaction().expect("Failed to begin transaction");
|
||||||
|
assert!(db.has_active_transaction());
|
||||||
|
db.rollback_transaction().expect("Failed to rollback transaction");
|
||||||
|
assert!(!db.has_active_transaction());
|
||||||
|
|
||||||
|
// Create a sample user
|
||||||
|
let user = User::new(
|
||||||
|
1,
|
||||||
|
"Test User".to_string(),
|
||||||
|
"test@example.com".to_string(),
|
||||||
|
"password".to_string(),
|
||||||
|
"Test Corp".to_string(),
|
||||||
|
"Admin".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert the user
|
||||||
|
db.set(&user).expect("Failed to insert user");
|
||||||
|
|
||||||
|
// Get the user
|
||||||
|
let retrieved_user: User = db.get(&user.id.to_string()).expect("Failed to get user");
|
||||||
|
assert_eq!(user.name, retrieved_user.name);
|
||||||
|
|
||||||
|
// Create a sample company
|
||||||
|
let incorporation_date = Utc::now();
|
||||||
|
let company = Company::new(
|
||||||
|
1,
|
||||||
|
"Test Corp".to_string(),
|
||||||
|
"REG123".to_string(),
|
||||||
|
incorporation_date,
|
||||||
|
"12-31".to_string(),
|
||||||
|
"test@corp.com".to_string(),
|
||||||
|
"123-456-7890".to_string(),
|
||||||
|
"www.testcorp.com".to_string(),
|
||||||
|
"123 Test St".to_string(),
|
||||||
|
BusinessType::Global,
|
||||||
|
"Tech".to_string(),
|
||||||
|
"A test company".to_string(),
|
||||||
|
CompanyStatus::Active,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert the company
|
||||||
|
db.set(&company).expect("Failed to insert company");
|
||||||
|
|
||||||
|
// Get the company
|
||||||
|
let retrieved_company: Company = db.get(&company.id.to_string())
|
||||||
|
.expect("Failed to get company");
|
||||||
|
assert_eq!(company.name, retrieved_company.name);
|
||||||
|
|
||||||
|
// List all companies
|
||||||
|
let companies: Vec<Company> = db.list().expect("Failed to list companies");
|
||||||
|
assert_eq!(companies.len(), 1);
|
||||||
|
assert_eq!(companies[0].name, company.name);
|
||||||
|
|
||||||
|
// List all users
|
||||||
|
let users: Vec<User> = db.list().expect("Failed to list users");
|
||||||
|
assert_eq!(users.len(), 1);
|
||||||
|
assert_eq!(users[0].name, user.name);
|
||||||
|
|
||||||
|
// Delete the company
|
||||||
|
db.delete::<Company>(&company.id.to_string())
|
||||||
|
.expect("Failed to delete company");
|
||||||
|
|
||||||
|
// Try to get the deleted company (should fail)
|
||||||
|
match db.get::<Company>(&company.id.to_string()) {
|
||||||
|
Err(SledDBError::NotFound(_)) => (),
|
||||||
|
_ => panic!("Expected NotFound error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
herodb/src/core/mod.rs
Normal file
70
herodb/src/core/mod.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
mod base;
|
||||||
|
|
||||||
|
pub use base::*;
|
||||||
|
|
||||||
|
use std::any::TypeId;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
|
||||||
|
/// Represents a single database operation in a transaction
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum DbOperation {
|
||||||
|
Set {
|
||||||
|
model_type: TypeId,
|
||||||
|
serialized: Vec<u8>,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
model_type: TypeId,
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trait for type-erased database operations
|
||||||
|
trait AnyDbOperations: Send + Sync {
|
||||||
|
fn delete(&self, id: &str) -> SledDBResult<()>;
|
||||||
|
fn get_any(&self, id: &str) -> SledDBResult<Box<dyn std::any::Any>>;
|
||||||
|
fn list_any(&self) -> SledDBResult<Box<dyn std::any::Any>>;
|
||||||
|
fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation of AnyDbOperations for any SledDB<T>
|
||||||
|
impl<T: SledModel> AnyDbOperations for SledDB<T> {
|
||||||
|
fn delete(&self, id: &str) -> SledDBResult<()> {
|
||||||
|
self.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_any(&self, id: &str) -> SledDBResult<Box<dyn std::any::Any>> {
|
||||||
|
let result = self.get(id)?;
|
||||||
|
Ok(Box::new(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_any(&self) -> SledDBResult<Box<dyn std::any::Any>> {
|
||||||
|
let result = self.list()?;
|
||||||
|
Ok(Box::new(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()> {
|
||||||
|
// Downcast to the specific type T
|
||||||
|
match model.downcast_ref::<T>() {
|
||||||
|
Some(t) => self.insert(t),
|
||||||
|
None => Err(SledDBError::TypeError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction state for DB operations
|
||||||
|
pub struct TransactionState {
|
||||||
|
operations: Vec<DbOperation>,
|
||||||
|
active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransactionState {
|
||||||
|
/// Create a new transaction state
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
operations: Vec::new(),
|
||||||
|
active: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
herodb/src/error.rs
Normal file
36
herodb/src/error.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Error types for HeroDB operations
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Error from the underlying sled database
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
Database(#[from] sled::Error),
|
||||||
|
|
||||||
|
/// Error during serialization or deserialization
|
||||||
|
#[error("Serialization error: {0}")]
|
||||||
|
Serialization(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
/// Error when a requested item is not found
|
||||||
|
#[error("Item not found: {0}")]
|
||||||
|
NotFound(String),
|
||||||
|
|
||||||
|
/// Error when an item already exists
|
||||||
|
#[error("Item already exists: {0}")]
|
||||||
|
AlreadyExists(String),
|
||||||
|
|
||||||
|
/// Error when a model validation fails
|
||||||
|
#[error("Validation error: {0}")]
|
||||||
|
Validation(String),
|
||||||
|
|
||||||
|
/// Error when a transaction fails
|
||||||
|
#[error("Transaction error: {0}")]
|
||||||
|
Transaction(String),
|
||||||
|
|
||||||
|
/// Other errors
|
||||||
|
#[error("Other error: {0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result type for HeroDB operations
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
15
herodb/src/lib.rs
Normal file
15
herodb/src/lib.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//! HeroDB: A database library built on top of sled with model support
|
||||||
|
//!
|
||||||
|
//! This library provides a simple interface for working with a sled-based database
|
||||||
|
//! and includes support for defining and working with data models.
|
||||||
|
|
||||||
|
mod db;
|
||||||
|
mod error;
|
||||||
|
mod model;
|
||||||
|
|
||||||
|
pub use db::{Database, Collection};
|
||||||
|
pub use error::Error;
|
||||||
|
pub use model::{Model, ModelId, Timestamp};
|
||||||
|
|
||||||
|
/// Re-export sled for advanced usage
|
||||||
|
pub use sled;
|
6
herodb/src/mod.rs
Normal file
6
herodb/src/mod.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Export core module
|
||||||
|
pub mod core;
|
||||||
|
|
||||||
|
// Export zaz module
|
||||||
|
pub mod zaz;
|
||||||
|
|
98
herodb/src/zaz/DB_README.md
Normal file
98
herodb/src/zaz/DB_README.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# Zaz DB System
|
||||||
|
|
||||||
|
The Zaz DB system is a new implementation that provides automatic database persistence for all root models in the system.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- Each root model (User, Company, Meeting, Product, Sale, Vote, Shareholder) is stored in its own database file
|
||||||
|
- The DB system uses Sled, a high-performance embedded database
|
||||||
|
- Each model is automatically serialized with Bincode and compressed with Brotli
|
||||||
|
- The DB system provides generic methods that work with any model type
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/zaz/
|
||||||
|
├── db/
|
||||||
|
│ ├── base.rs # Core traits and SledDB implementation
|
||||||
|
│ └── mod.rs # Main DB implementation that handles all models
|
||||||
|
└── models/
|
||||||
|
├── user.rs
|
||||||
|
├── company.rs
|
||||||
|
├── meeting.rs
|
||||||
|
├── product.rs
|
||||||
|
├── sale.rs
|
||||||
|
├── vote.rs
|
||||||
|
├── shareholder.rs
|
||||||
|
└── lib.rs # Re-exports all models
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::db::core::DB;
|
||||||
|
use crate::zaz::models::*;
|
||||||
|
|
||||||
|
// Create a DB instance (handles all model types)
|
||||||
|
let db = DB::new("/path/to/db").expect("Failed to create DB");
|
||||||
|
|
||||||
|
// --- User Example ---
|
||||||
|
let user = User::new(
|
||||||
|
1,
|
||||||
|
"John Doe".to_string(),
|
||||||
|
"john@example.com".to_string(),
|
||||||
|
"password123".to_string(),
|
||||||
|
"ACME Corp".to_string(),
|
||||||
|
"Admin".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert user (DB automatically detects the type)
|
||||||
|
db.set(&user).expect("Failed to insert user");
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
let retrieved_user: User = db.get(&user.id.to_string())
|
||||||
|
.expect("Failed to get user");
|
||||||
|
|
||||||
|
// List all users
|
||||||
|
let users: Vec<User> = db.list().expect("Failed to list users");
|
||||||
|
|
||||||
|
// Delete user
|
||||||
|
db.delete::<User>(&user.id.to_string()).expect("Failed to delete user");
|
||||||
|
|
||||||
|
// --- Company Example ---
|
||||||
|
let company = Company::new(
|
||||||
|
1,
|
||||||
|
"ACME Corporation".to_string(),
|
||||||
|
"REG12345".to_string(),
|
||||||
|
Utc::now(),
|
||||||
|
"12-31".to_string(),
|
||||||
|
// other fields...
|
||||||
|
);
|
||||||
|
|
||||||
|
// Similar operations for company and other models
|
||||||
|
|
||||||
|
// --- Direct Database Access ---
|
||||||
|
// You can also access the specific database for a model type directly
|
||||||
|
let user_db = db.user_db();
|
||||||
|
let company_db = db.company_db();
|
||||||
|
// etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. **Automatic Type Handling**: The DB system automatically detects the model type and routes operations to the appropriate database
|
||||||
|
2. **Generic Interface**: Same methods work with any model type
|
||||||
|
3. **Persistence**: All models are automatically persisted to disk
|
||||||
|
4. **Performance**: Fast serialization with Bincode and efficient compression with Brotli
|
||||||
|
5. **Storage Separation**: Each model type has its own database file, making maintenance easier
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- Each model implements the `SledModel` trait which provides the necessary methods for database operations
|
||||||
|
- The `Storable` trait handles serialization and deserialization
|
||||||
|
- The DB uses separate Sled databases for each model type to ensure proper separation of concerns
|
||||||
|
- Type-safe operations are ensured through Rust's type system
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See the `examples.rs` file for complete examples of how to use the DB system.
|
160
herodb/src/zaz/cmd/examples.rs
Normal file
160
herodb/src/zaz/cmd/examples.rs
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
//! Examples demonstrating how to use the new DB implementation
|
||||||
|
|
||||||
|
use crate::db::core::DB;
|
||||||
|
use crate::db::zaz::models::*;
|
||||||
|
use crate::db::zaz::models::shareholder::ShareholderType;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::fs;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
/// Creates a simple temporary directory
|
||||||
|
fn create_temp_dir() -> std::io::Result<PathBuf> {
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let random_name = format!("db-example-{}", std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis());
|
||||||
|
let path = temp_dir.join(random_name);
|
||||||
|
fs::create_dir_all(&path)?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Example demonstrating basic CRUD operations with the DB
|
||||||
|
pub fn run_db_examples() -> Result<(), String> {
|
||||||
|
println!("Running DB examples...");
|
||||||
|
|
||||||
|
// Create a temporary directory for the DB (or use a permanent one)
|
||||||
|
let db_path = create_temp_dir().map_err(|e| format!("Failed to create temp dir: {}", e))?;
|
||||||
|
println!("Using DB path: {:?}", db_path);
|
||||||
|
|
||||||
|
// Create a DB instance
|
||||||
|
let db = DB::new(db_path).map_err(|e| format!("Failed to create DB: {}", e))?;
|
||||||
|
|
||||||
|
// --- User Example ---
|
||||||
|
println!("\nRunning User example:");
|
||||||
|
let user = User::new(
|
||||||
|
1,
|
||||||
|
"John Doe".to_string(),
|
||||||
|
"john@example.com".to_string(),
|
||||||
|
"password123".to_string(),
|
||||||
|
"ACME Corp".to_string(),
|
||||||
|
"Admin".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert user
|
||||||
|
db.set(&user).map_err(|e| format!("Failed to insert user: {}", e))?;
|
||||||
|
println!("Inserted user: {}", user.name);
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
let retrieved_user: User = db.get(&user.id.to_string())
|
||||||
|
.map_err(|e| format!("Failed to get user: {}", e))?;
|
||||||
|
println!("Retrieved user: {} ({})", retrieved_user.name, retrieved_user.email);
|
||||||
|
|
||||||
|
// --- Company Example ---
|
||||||
|
println!("\nRunning Company example:");
|
||||||
|
let company = Company::new(
|
||||||
|
1,
|
||||||
|
"ACME Corporation".to_string(),
|
||||||
|
"REG12345".to_string(),
|
||||||
|
Utc::now(),
|
||||||
|
"12-31".to_string(),
|
||||||
|
"info@acme.com".to_string(),
|
||||||
|
"555-123-4567".to_string(),
|
||||||
|
"www.acme.com".to_string(),
|
||||||
|
"123 Main St, Metropolis".to_string(),
|
||||||
|
BusinessType::Global,
|
||||||
|
"Technology".to_string(),
|
||||||
|
"A leading technology company".to_string(),
|
||||||
|
CompanyStatus::Active,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert company
|
||||||
|
db.set(&company).map_err(|e| format!("Failed to insert company: {}", e))?;
|
||||||
|
println!("Inserted company: {}", company.name);
|
||||||
|
|
||||||
|
// Get company
|
||||||
|
let retrieved_company: Company = db.get(&company.id.to_string())
|
||||||
|
.map_err(|e| format!("Failed to get company: {}", e))?;
|
||||||
|
println!("Retrieved company: {} ({})", retrieved_company.name, retrieved_company.registration_number);
|
||||||
|
|
||||||
|
// --- Shareholder Example ---
|
||||||
|
println!("\nRunning Shareholder example:");
|
||||||
|
// Create the shareholder directly
|
||||||
|
let shareholder = Shareholder {
|
||||||
|
id: 1,
|
||||||
|
company_id: company.id,
|
||||||
|
user_id: user.id,
|
||||||
|
name: "John Doe".to_string(),
|
||||||
|
shares: 1000.0,
|
||||||
|
percentage: 25.0,
|
||||||
|
type_: ShareholderType::Individual, // Use the shared enum via re-export
|
||||||
|
since: Utc::now(),
|
||||||
|
created_at: Utc::now(),
|
||||||
|
updated_at: Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert shareholder
|
||||||
|
db.set(&shareholder).map_err(|e| format!("Failed to insert shareholder: {}", e))?;
|
||||||
|
println!("Inserted shareholder: {} ({}%)", shareholder.name, shareholder.percentage);
|
||||||
|
|
||||||
|
// Get shareholder
|
||||||
|
let retrieved_shareholder: Shareholder = db.get(&shareholder.id.to_string())
|
||||||
|
.map_err(|e| format!("Failed to get shareholder: {}", e))?;
|
||||||
|
println!("Retrieved shareholder: {} ({} shares)", retrieved_shareholder.name, retrieved_shareholder.shares);
|
||||||
|
|
||||||
|
// --- List Example ---
|
||||||
|
println!("\nListing all entities:");
|
||||||
|
|
||||||
|
let users: Vec<User> = db.list().map_err(|e| format!("Failed to list users: {}", e))?;
|
||||||
|
println!("Found {} users", users.len());
|
||||||
|
for user in &users {
|
||||||
|
println!("- User: {}", user.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let companies: Vec<Company> = db.list().map_err(|e| format!("Failed to list companies: {}", e))?;
|
||||||
|
println!("Found {} companies", companies.len());
|
||||||
|
for company in &companies {
|
||||||
|
println!("- Company: {}", company.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let shareholders: Vec<Shareholder> = db.list()
|
||||||
|
.map_err(|e| format!("Failed to list shareholders: {}", e))?;
|
||||||
|
println!("Found {} shareholders", shareholders.len());
|
||||||
|
for shareholder in &shareholders {
|
||||||
|
println!("- Shareholder: {} ({}%)", shareholder.name, shareholder.percentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Delete Example ---
|
||||||
|
println!("\nDeleting entities:");
|
||||||
|
|
||||||
|
// Delete shareholder
|
||||||
|
db.delete::<Shareholder>(&shareholder.id.to_string())
|
||||||
|
.map_err(|e| format!("Failed to delete shareholder: {}", e))?;
|
||||||
|
println!("Deleted shareholder: {}", shareholder.name);
|
||||||
|
|
||||||
|
// Delete company
|
||||||
|
db.delete::<Company>(&company.id.to_string())
|
||||||
|
.map_err(|e| format!("Failed to delete company: {}", e))?;
|
||||||
|
println!("Deleted company: {}", company.name);
|
||||||
|
|
||||||
|
// Delete user
|
||||||
|
db.delete::<User>(&user.id.to_string())
|
||||||
|
.map_err(|e| format!("Failed to delete user: {}", e))?;
|
||||||
|
println!("Deleted user: {}", user.name);
|
||||||
|
|
||||||
|
// Verify deletion
|
||||||
|
let users_after_delete: Vec<User> = db.list()
|
||||||
|
.map_err(|e| format!("Failed to list users after delete: {}", e))?;
|
||||||
|
println!("Users remaining: {}", users_after_delete.len());
|
||||||
|
|
||||||
|
let companies_after_delete: Vec<Company> = db.list()
|
||||||
|
.map_err(|e| format!("Failed to list companies after delete: {}", e))?;
|
||||||
|
println!("Companies remaining: {}", companies_after_delete.len());
|
||||||
|
|
||||||
|
let shareholders_after_delete: Vec<Shareholder> = db.list()
|
||||||
|
.map_err(|e| format!("Failed to list shareholders after delete: {}", e))?;
|
||||||
|
println!("Shareholders remaining: {}", shareholders_after_delete.len());
|
||||||
|
|
||||||
|
println!("\nDB examples completed successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
168
herodb/src/zaz/db_tests.rs
Normal file
168
herodb/src/zaz/db_tests.rs
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
//! Integration tests for zaz database module
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use sled;
|
||||||
|
use bincode;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Test model for database operations
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
struct User {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
balance: f64,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
fn new(id: u32, name: String, email: String, balance: f64) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
balance,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test basic CRUD operations
|
||||||
|
#[test]
|
||||||
|
fn test_basic_crud() {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
let temp_dir = tempdir().expect("Failed to create temp directory");
|
||||||
|
println!("Created temporary directory at: {:?}", temp_dir.path());
|
||||||
|
|
||||||
|
// Open a sled database in the temporary directory
|
||||||
|
let db = sled::open(temp_dir.path().join("users")).expect("Failed to open database");
|
||||||
|
println!("Opened database at: {:?}", temp_dir.path().join("users"));
|
||||||
|
|
||||||
|
// CREATE a user
|
||||||
|
let user = User::new(1, "Test User".to_string(), "test@example.com".to_string(), 100.0);
|
||||||
|
let user_key = user.id.to_string();
|
||||||
|
let user_value = bincode::serialize(&user).expect("Failed to serialize user");
|
||||||
|
db.insert(user_key.as_bytes(), user_value).expect("Failed to insert user");
|
||||||
|
db.flush().expect("Failed to flush database");
|
||||||
|
println!("Created user: {} ({})", user.name, user.email);
|
||||||
|
|
||||||
|
// READ the user
|
||||||
|
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
|
||||||
|
assert!(result.is_some(), "User should exist");
|
||||||
|
if let Some(data) = result {
|
||||||
|
let retrieved_user: User = bincode::deserialize(&data).expect("Failed to deserialize user");
|
||||||
|
println!("Retrieved user: {} ({})", retrieved_user.name, retrieved_user.email);
|
||||||
|
assert_eq!(user, retrieved_user, "Retrieved user should match original");
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPDATE the user
|
||||||
|
let updated_user = User::new(1, "Updated User".to_string(), "updated@example.com".to_string(), 150.0);
|
||||||
|
let updated_value = bincode::serialize(&updated_user).expect("Failed to serialize updated user");
|
||||||
|
db.insert(user_key.as_bytes(), updated_value).expect("Failed to update user");
|
||||||
|
db.flush().expect("Failed to flush database");
|
||||||
|
println!("Updated user: {} ({})", updated_user.name, updated_user.email);
|
||||||
|
|
||||||
|
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
|
||||||
|
if let Some(data) = result {
|
||||||
|
let retrieved_user: User = bincode::deserialize(&data).expect("Failed to deserialize user");
|
||||||
|
assert_eq!(updated_user, retrieved_user, "Retrieved user should match updated version");
|
||||||
|
} else {
|
||||||
|
panic!("User should exist after update");
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE the user
|
||||||
|
db.remove(user_key.as_bytes()).expect("Failed to delete user");
|
||||||
|
db.flush().expect("Failed to flush database");
|
||||||
|
println!("Deleted user");
|
||||||
|
|
||||||
|
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
|
||||||
|
assert!(result.is_none(), "User should be deleted");
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
drop(db);
|
||||||
|
temp_dir.close().expect("Failed to cleanup temporary directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test transaction-like behavior with multiple operations
|
||||||
|
#[test]
|
||||||
|
fn test_transaction_behavior() {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
let temp_dir = tempdir().expect("Failed to create temp directory");
|
||||||
|
println!("Created temporary directory at: {:?}", temp_dir.path());
|
||||||
|
|
||||||
|
// Open a sled database in the temporary directory
|
||||||
|
let db = sled::open(temp_dir.path().join("tx_test")).expect("Failed to open database");
|
||||||
|
println!("Opened transaction test database at: {:?}", temp_dir.path().join("tx_test"));
|
||||||
|
|
||||||
|
// Create initial users
|
||||||
|
let user1 = User::new(1, "User One".to_string(), "one@example.com".to_string(), 100.0);
|
||||||
|
let user2 = User::new(2, "User Two".to_string(), "two@example.com".to_string(), 50.0);
|
||||||
|
|
||||||
|
// Insert initial users
|
||||||
|
db.insert(user1.id.to_string().as_bytes(), bincode::serialize(&user1).unwrap()).unwrap();
|
||||||
|
db.insert(user2.id.to_string().as_bytes(), bincode::serialize(&user2).unwrap()).unwrap();
|
||||||
|
db.flush().unwrap();
|
||||||
|
println!("Inserted initial users");
|
||||||
|
|
||||||
|
// Simulate a transaction - transfer 25.0 from user1 to user2
|
||||||
|
println!("Starting transaction simulation: transfer 25.0 from user1 to user2");
|
||||||
|
|
||||||
|
// Create transaction workspace
|
||||||
|
let mut tx_workspace = HashMap::new();
|
||||||
|
|
||||||
|
// Retrieve current state
|
||||||
|
if let Some(data) = db.get(user1.id.to_string().as_bytes()).unwrap() {
|
||||||
|
let user: User = bincode::deserialize(&data).unwrap();
|
||||||
|
tx_workspace.insert(user1.id.to_string(), user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(data) = db.get(user2.id.to_string().as_bytes()).unwrap() {
|
||||||
|
let user: User = bincode::deserialize(&data).unwrap();
|
||||||
|
tx_workspace.insert(user2.id.to_string(), user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify both users in the transaction
|
||||||
|
let mut updated_user1 = tx_workspace.get(&user1.id.to_string()).unwrap().clone();
|
||||||
|
let mut updated_user2 = tx_workspace.get(&user2.id.to_string()).unwrap().clone();
|
||||||
|
|
||||||
|
updated_user1.balance -= 25.0;
|
||||||
|
updated_user2.balance += 25.0;
|
||||||
|
|
||||||
|
// Update the workspace
|
||||||
|
tx_workspace.insert(user1.id.to_string(), updated_user1);
|
||||||
|
tx_workspace.insert(user2.id.to_string(), updated_user2);
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
println!("Committing transaction");
|
||||||
|
for (key, user) in tx_workspace {
|
||||||
|
let user_bytes = bincode::serialize(&user).unwrap();
|
||||||
|
db.insert(key.as_bytes(), user_bytes).unwrap();
|
||||||
|
}
|
||||||
|
db.flush().unwrap();
|
||||||
|
|
||||||
|
// Verify the results
|
||||||
|
if let Some(data) = db.get(user1.id.to_string().as_bytes()).unwrap() {
|
||||||
|
let final_user1: User = bincode::deserialize(&data).unwrap();
|
||||||
|
assert_eq!(final_user1.balance, 75.0, "User1 balance should be 75.0");
|
||||||
|
println!("Verified user1 balance is now {}", final_user1.balance);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(data) = db.get(user2.id.to_string().as_bytes()).unwrap() {
|
||||||
|
let final_user2: User = bincode::deserialize(&data).unwrap();
|
||||||
|
assert_eq!(final_user2.balance, 75.0, "User2 balance should be 75.0");
|
||||||
|
println!("Verified user2 balance is now {}", final_user2.balance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
drop(db);
|
||||||
|
temp_dir.close().expect("Failed to cleanup temporary directory");
|
||||||
|
}
|
||||||
|
}
|
64
herodb/src/zaz/examples.rs
Normal file
64
herodb/src/zaz/examples.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Examples for using the Zaz database
|
||||||
|
|
||||||
|
use crate::db::core::DB;
|
||||||
|
use crate::db::zaz::models::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
/// Run a simple example of the DB operations
|
||||||
|
pub fn run_db_examples() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("Running Zaz DB examples...");
|
||||||
|
|
||||||
|
// Create a temp DB path
|
||||||
|
let db_path = PathBuf::from("/tmp/zaz-examples");
|
||||||
|
std::fs::create_dir_all(&db_path)?;
|
||||||
|
|
||||||
|
// Create DB instance
|
||||||
|
let db = DB::new(&db_path)?;
|
||||||
|
|
||||||
|
// Example 1: User operations
|
||||||
|
println!("\n--- User Examples ---");
|
||||||
|
let user = User::new(
|
||||||
|
1,
|
||||||
|
"John Doe".to_string(),
|
||||||
|
"john@example.com".to_string(),
|
||||||
|
"secure123".to_string(),
|
||||||
|
"Example Corp".to_string(),
|
||||||
|
"User".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
db.set(&user)?;
|
||||||
|
println!("Inserted user: {}", user.name);
|
||||||
|
|
||||||
|
let retrieved_user = db.get::<User>(&user.id.to_string())?;
|
||||||
|
println!("Retrieved user: {} ({})", retrieved_user.name, retrieved_user.email);
|
||||||
|
|
||||||
|
// Example 2: Company operations
|
||||||
|
println!("\n--- Company Examples ---");
|
||||||
|
let company = Company::new(
|
||||||
|
1,
|
||||||
|
"Example Corp".to_string(),
|
||||||
|
"EX123456".to_string(),
|
||||||
|
Utc::now(),
|
||||||
|
"12-31".to_string(),
|
||||||
|
"info@example.com".to_string(),
|
||||||
|
"123-456-7890".to_string(),
|
||||||
|
"www.example.com".to_string(),
|
||||||
|
"123 Example St, Example City".to_string(),
|
||||||
|
BusinessType::Global,
|
||||||
|
"Technology".to_string(),
|
||||||
|
"An example company".to_string(),
|
||||||
|
CompanyStatus::Active,
|
||||||
|
);
|
||||||
|
|
||||||
|
db.set(&company)?;
|
||||||
|
println!("Inserted company: {}", company.name);
|
||||||
|
|
||||||
|
let companies = db.list::<Company>()?;
|
||||||
|
println!("Found {} companies", companies.len());
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
std::fs::remove_dir_all(db_path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
9
herodb/src/zaz/mod.rs
Normal file
9
herodb/src/zaz/mod.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Declare the models submodule
|
||||||
|
#[path = "models/lib.rs"] // Tell compiler where to find models module source
|
||||||
|
pub mod models;
|
||||||
|
|
||||||
|
|
||||||
|
// Declare the examples module for the new DB implementation
|
||||||
|
#[path = "examples.rs"] // Tell compiler where to find the examples module
|
||||||
|
pub mod examples;
|
||||||
|
|
236
herodb/src/zaz/models/company.rs
Normal file
236
herodb/src/zaz/models/company.rs
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
use crate::db::core::{SledModel, Storable, SledDB, SledDBError}; // Import from new location
|
||||||
|
use super::shareholder::Shareholder; // Use super:: for sibling module
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
/// CompanyStatus represents the status of a company
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum CompanyStatus {
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
Suspended,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BusinessType represents the type of a business
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum BusinessType {
|
||||||
|
Coop,
|
||||||
|
Single,
|
||||||
|
Twin,
|
||||||
|
Starter,
|
||||||
|
Global,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Company represents a company registered in the Freezone
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] // Added PartialEq
|
||||||
|
pub struct Company {
|
||||||
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub registration_number: String,
|
||||||
|
pub incorporation_date: DateTime<Utc>,
|
||||||
|
pub fiscal_year_end: String,
|
||||||
|
pub email: String,
|
||||||
|
pub phone: String,
|
||||||
|
pub website: String,
|
||||||
|
pub address: String,
|
||||||
|
pub business_type: BusinessType,
|
||||||
|
pub industry: String,
|
||||||
|
pub description: String,
|
||||||
|
pub status: CompanyStatus,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
// Removed shareholders property
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storable trait provides default dump/load using bincode/brotli
|
||||||
|
impl Storable for Company {}
|
||||||
|
|
||||||
|
// SledModel requires get_id and db_prefix
|
||||||
|
impl SledModel for Company {
|
||||||
|
fn get_id(&self) -> String {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_prefix() -> &'static str {
|
||||||
|
"company" // Prefix for company records in Sled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Company {
|
||||||
|
/// Create a new company with default timestamps
|
||||||
|
pub fn new(
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
registration_number: String,
|
||||||
|
incorporation_date: DateTime<Utc>,
|
||||||
|
fiscal_year_end: String,
|
||||||
|
email: String,
|
||||||
|
phone: String,
|
||||||
|
website: String,
|
||||||
|
address: String,
|
||||||
|
business_type: BusinessType,
|
||||||
|
industry: String,
|
||||||
|
description: String,
|
||||||
|
status: CompanyStatus,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
registration_number,
|
||||||
|
incorporation_date,
|
||||||
|
fiscal_year_end,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
website,
|
||||||
|
address,
|
||||||
|
business_type,
|
||||||
|
industry,
|
||||||
|
description,
|
||||||
|
status,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a shareholder to the company, saving it to the Shareholder's SledDB
|
||||||
|
pub fn add_shareholder(
|
||||||
|
&mut self,
|
||||||
|
db: &SledDB<Shareholder>, // Pass in the Shareholder's SledDB
|
||||||
|
mut shareholder: Shareholder,
|
||||||
|
) -> Result<(), SledDBError> {
|
||||||
|
shareholder.company_id = self.id; // Set the company_id
|
||||||
|
db.insert(&shareholder)?; // Insert the shareholder into its own DB
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed dump and load_from_bytes methods, now provided by Storable trait
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::db::zaz::db::{SledDB, SledDBError, SledModel};
|
||||||
|
use crate::db::zaz::models::shareholder::{Shareholder, ShareholderType};
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_company_sled_crud() {
|
||||||
|
// 1. Setup: Create a temporary directory for the Sled DB
|
||||||
|
let dir = tempdir().expect("Failed to create temp dir");
|
||||||
|
let db_path = dir.path();
|
||||||
|
let company_db: SledDB<Company> = SledDB::open(db_path.join("company")).expect("Failed to open Company Sled DB");
|
||||||
|
let shareholder_db: SledDB<Shareholder> = SledDB::open(db_path.join("shareholder")).expect("Failed to open Shareholder Sled DB");
|
||||||
|
|
||||||
|
// 2. Create a sample Company
|
||||||
|
let incorporation_date = Utc::now();
|
||||||
|
let mut company1 = Company::new(
|
||||||
|
1,
|
||||||
|
"Test Corp".to_string(),
|
||||||
|
"REG123".to_string(),
|
||||||
|
incorporation_date,
|
||||||
|
"12-31".to_string(),
|
||||||
|
"test@corp.com".to_string(),
|
||||||
|
"123-456-7890".to_string(),
|
||||||
|
"www.testcorp.com".to_string(),
|
||||||
|
"123 Test St".to_string(),
|
||||||
|
BusinessType::Global,
|
||||||
|
"Tech".to_string(),
|
||||||
|
"A test company".to_string(),
|
||||||
|
CompanyStatus::Active,
|
||||||
|
);
|
||||||
|
|
||||||
|
let company_id = company1.get_id();
|
||||||
|
|
||||||
|
// 3. Create and add a shareholder to the company
|
||||||
|
let now = Utc::now();
|
||||||
|
// Define shareholder properties separately
|
||||||
|
let shareholder_id = 1;
|
||||||
|
let shareholder_name = "Dummy Shareholder".to_string();
|
||||||
|
|
||||||
|
// Create the shareholder
|
||||||
|
let shareholder = Shareholder::new(
|
||||||
|
shareholder_id,
|
||||||
|
0, // company_id will be set by add_shareholder
|
||||||
|
0, // user_id
|
||||||
|
shareholder_name.clone(),
|
||||||
|
100.0, // shares
|
||||||
|
10.0, // percentage
|
||||||
|
ShareholderType::Individual,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the shareholder
|
||||||
|
company1.add_shareholder(&shareholder_db, shareholder).expect("Failed to add shareholder");
|
||||||
|
|
||||||
|
// 4. Insert the company
|
||||||
|
company_db.insert(&company1).expect("Failed to insert company");
|
||||||
|
|
||||||
|
// 5. Get and Assert
|
||||||
|
let retrieved_company = company_db.get(&company_id).expect("Failed to get company");
|
||||||
|
assert_eq!(company1, retrieved_company, "Retrieved company does not match original");
|
||||||
|
|
||||||
|
// 6. List and Assert
|
||||||
|
let all_companies = company_db.list().expect("Failed to list companies");
|
||||||
|
assert_eq!(all_companies.len(), 1, "Should be one company in the list");
|
||||||
|
assert_eq!(all_companies[0], company1, "List should contain the inserted company");
|
||||||
|
|
||||||
|
// 7. Delete
|
||||||
|
company_db.delete(&company_id).expect("Failed to delete company");
|
||||||
|
|
||||||
|
// 8. Get after delete and Assert NotFound
|
||||||
|
match company_db.get(&company_id) {
|
||||||
|
Err(SledDBError::NotFound(id)) => {
|
||||||
|
assert_eq!(id, company_id, "NotFound error should contain the correct ID");
|
||||||
|
}
|
||||||
|
Ok(_) => panic!("Should not have found the company after deletion"),
|
||||||
|
Err(e) => panic!("Unexpected error after delete: {:?}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. List after delete
|
||||||
|
let companies_after_delete = company_db.list().expect("Failed to list companies after delete");
|
||||||
|
assert!(companies_after_delete.is_empty(), "List should be empty after deletion");
|
||||||
|
|
||||||
|
// 10. Check if shareholder exists in shareholder db
|
||||||
|
let retrieved_shareholder = shareholder_db.get(&shareholder_id.to_string()).expect("Failed to get shareholder");
|
||||||
|
assert_eq!(shareholder_id, retrieved_shareholder.id, "Retrieved shareholder should have the correct ID");
|
||||||
|
assert_eq!(shareholder_name, retrieved_shareholder.name, "Retrieved shareholder should have the correct name");
|
||||||
|
assert_eq!(1, retrieved_shareholder.company_id, "Retrieved shareholder should have company_id set to 1");
|
||||||
|
|
||||||
|
// Temporary directory `dir` is automatically removed when it goes out of scope here.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dump_load() {
|
||||||
|
// Create a sample Company
|
||||||
|
let incorporation_date = Utc::now();
|
||||||
|
let original_company = Company::new(
|
||||||
|
2,
|
||||||
|
"DumpLoad Test".to_string(),
|
||||||
|
"DL987".to_string(),
|
||||||
|
incorporation_date,
|
||||||
|
"06-30".to_string(),
|
||||||
|
"dump@load.com".to_string(),
|
||||||
|
"987-654-3210".to_string(),
|
||||||
|
"www.dumpload.com".to_string(),
|
||||||
|
"456 DumpLoad Ave".to_string(),
|
||||||
|
BusinessType::Coop,
|
||||||
|
"Testing".to_string(),
|
||||||
|
"Testing dump and load".to_string(),
|
||||||
|
CompanyStatus::Active,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dump (serialize + compress)
|
||||||
|
let dumped_data = original_company.dump().expect("Failed to dump company");
|
||||||
|
assert!(!dumped_data.is_empty(), "Dumped data should not be empty");
|
||||||
|
|
||||||
|
// Load (decompress + deserialize)
|
||||||
|
let loaded_company = Company::load_from_bytes(&dumped_data).expect("Failed to load company from bytes");
|
||||||
|
|
||||||
|
// Assert equality
|
||||||
|
assert_eq!(original_company, loaded_company, "Loaded company should match the original");
|
||||||
|
}
|
||||||
|
}
|
25
herodb/src/zaz/models/lib.rs
Normal file
25
herodb/src/zaz/models/lib.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
pub mod user;
|
||||||
|
pub mod vote;
|
||||||
|
pub mod company;
|
||||||
|
pub mod meeting;
|
||||||
|
pub mod product;
|
||||||
|
pub mod sale;
|
||||||
|
pub mod shareholder;
|
||||||
|
// pub mod db; // Moved to src/zaz/db
|
||||||
|
// pub mod migration; // Removed
|
||||||
|
|
||||||
|
// Re-export all model types for convenience
|
||||||
|
pub use user::User;
|
||||||
|
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
|
||||||
|
pub use company::{Company, CompanyStatus, BusinessType};
|
||||||
|
pub use meeting::Meeting;
|
||||||
|
pub use product::{Product, Currency, ProductComponent, ProductType, ProductStatus};
|
||||||
|
pub use sale::Sale;
|
||||||
|
pub use shareholder::Shareholder;
|
||||||
|
|
||||||
|
// Re-export database components
|
||||||
|
// pub use db::{DB, DBError, DBResult, Model, ModelMetadata}; // Removed old DB re-exports
|
||||||
|
pub use crate::db::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; // Re-export Sled DB components
|
||||||
|
|
||||||
|
// Re-export migration components - Removed
|
||||||
|
// pub use migration::{Migrator, MigrationError, MigrationResult};
|
172
herodb/src/zaz/models/meeting.rs
Normal file
172
herodb/src/zaz/models/meeting.rs
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::db::core::{SledModel, Storable}; // Import Sled traits from new location
|
||||||
|
// use std::collections::HashMap; // Removed unused import
|
||||||
|
|
||||||
|
// use super::db::Model; // Removed old Model trait import
|
||||||
|
|
||||||
|
/// MeetingStatus represents the status of a meeting
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum MeetingStatus {
|
||||||
|
Scheduled,
|
||||||
|
Completed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AttendeeRole represents the role of an attendee in a meeting
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum AttendeeRole {
|
||||||
|
Coordinator,
|
||||||
|
Member,
|
||||||
|
Secretary,
|
||||||
|
Participant,
|
||||||
|
Advisor,
|
||||||
|
Admin,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AttendeeStatus represents the status of an attendee's participation
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum AttendeeStatus {
|
||||||
|
Confirmed,
|
||||||
|
Pending,
|
||||||
|
Declined,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attendee represents an attendee of a board meeting
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Attendee {
|
||||||
|
pub id: u32,
|
||||||
|
pub meeting_id: u32,
|
||||||
|
pub user_id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub role: AttendeeRole,
|
||||||
|
pub status: AttendeeStatus,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Attendee {
|
||||||
|
/// Create a new attendee with default values
|
||||||
|
pub fn new(
|
||||||
|
id: u32,
|
||||||
|
meeting_id: u32,
|
||||||
|
user_id: u32,
|
||||||
|
name: String,
|
||||||
|
role: AttendeeRole,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
meeting_id,
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
role,
|
||||||
|
status: AttendeeStatus::Pending,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the status of an attendee
|
||||||
|
pub fn update_status(&mut self, status: AttendeeStatus) {
|
||||||
|
self.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Meeting represents a board meeting of a company or other meeting
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Meeting {
|
||||||
|
pub id: u32,
|
||||||
|
pub company_id: u32,
|
||||||
|
pub title: String,
|
||||||
|
pub date: DateTime<Utc>,
|
||||||
|
pub location: String,
|
||||||
|
pub description: String,
|
||||||
|
pub status: MeetingStatus,
|
||||||
|
pub minutes: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
pub attendees: Vec<Attendee>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed old Model trait implementation
|
||||||
|
|
||||||
|
impl Meeting {
|
||||||
|
/// Create a new meeting with default values
|
||||||
|
pub fn new(
|
||||||
|
id: u32,
|
||||||
|
company_id: u32,
|
||||||
|
title: String,
|
||||||
|
date: DateTime<Utc>,
|
||||||
|
location: String,
|
||||||
|
description: String,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
company_id,
|
||||||
|
title,
|
||||||
|
date,
|
||||||
|
location,
|
||||||
|
description,
|
||||||
|
status: MeetingStatus::Scheduled,
|
||||||
|
minutes: String::new(),
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
attendees: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an attendee to the meeting
|
||||||
|
pub fn add_attendee(&mut self, attendee: Attendee) {
|
||||||
|
// Make sure the attendee's meeting_id matches this meeting
|
||||||
|
assert_eq!(self.id, attendee.meeting_id, "Attendee meeting_id must match meeting id");
|
||||||
|
|
||||||
|
// Check if the attendee already exists
|
||||||
|
if !self.attendees.iter().any(|a| a.id == attendee.id) {
|
||||||
|
self.attendees.push(attendee);
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the status of the meeting
|
||||||
|
pub fn update_status(&mut self, status: MeetingStatus) {
|
||||||
|
self.status = status;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the meeting minutes
|
||||||
|
pub fn update_minutes(&mut self, minutes: String) {
|
||||||
|
self.minutes = minutes;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find an attendee by user ID
|
||||||
|
pub fn find_attendee_by_user_id(&self, user_id: u32) -> Option<&Attendee> {
|
||||||
|
self.attendees.iter().find(|a| a.user_id == user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find an attendee by user ID (mutable version)
|
||||||
|
pub fn find_attendee_by_user_id_mut(&mut self, user_id: u32) -> Option<&mut Attendee> {
|
||||||
|
self.attendees.iter_mut().find(|a| a.user_id == user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all confirmed attendees
|
||||||
|
pub fn confirmed_attendees(&self) -> Vec<&Attendee> {
|
||||||
|
self.attendees
|
||||||
|
.iter()
|
||||||
|
.filter(|a| a.status == AttendeeStatus::Confirmed)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait (provides default dump/load)
|
||||||
|
impl Storable for Meeting {}
|
||||||
|
|
||||||
|
// Implement SledModel trait
|
||||||
|
impl SledModel for Meeting {
|
||||||
|
fn get_id(&self) -> String {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_prefix() -> &'static str {
|
||||||
|
"meeting"
|
||||||
|
}
|
||||||
|
}
|
155
herodb/src/zaz/models/product.rs
Normal file
155
herodb/src/zaz/models/product.rs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
use chrono::{DateTime, Utc, Duration};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::db::core::{SledModel, Storable}; // Import Sled traits from new location
|
||||||
|
|
||||||
|
/// Currency represents a monetary value with amount and currency code
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Currency {
|
||||||
|
pub amount: f64,
|
||||||
|
pub currency_code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Currency {
|
||||||
|
/// Create a new currency with amount and code
|
||||||
|
pub fn new(amount: f64, currency_code: String) -> Self {
|
||||||
|
Self {
|
||||||
|
amount,
|
||||||
|
currency_code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ProductType represents the type of a product
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum ProductType {
|
||||||
|
Product,
|
||||||
|
Service,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ProductStatus represents the status of a product
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum ProductStatus {
|
||||||
|
Available,
|
||||||
|
Unavailable,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ProductComponent represents a component of a product
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ProductComponent {
|
||||||
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub quantity: i32,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProductComponent {
|
||||||
|
/// Create a new product component with default timestamps
|
||||||
|
pub fn new(id: u32, name: String, description: String, quantity: i32) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
quantity,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Product represents a product or service offered by the Freezone
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Product {
|
||||||
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub price: Currency,
|
||||||
|
pub type_: ProductType,
|
||||||
|
pub category: String,
|
||||||
|
pub status: ProductStatus,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
pub max_amount: u16, // means allows us to define how many max of this there are
|
||||||
|
pub purchase_till: DateTime<Utc>,
|
||||||
|
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
|
||||||
|
pub components: Vec<ProductComponent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed old Model trait implementation
|
||||||
|
|
||||||
|
impl Product {
|
||||||
|
/// Create a new product with default timestamps
|
||||||
|
pub fn new(
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
price: Currency,
|
||||||
|
type_: ProductType,
|
||||||
|
category: String,
|
||||||
|
status: ProductStatus,
|
||||||
|
max_amount: u16,
|
||||||
|
validity_days: i64, // How many days the product is valid after purchase
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
// Default: purchasable for 1 year, active for specified validity days after purchase
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
price,
|
||||||
|
type_,
|
||||||
|
category,
|
||||||
|
status,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
max_amount,
|
||||||
|
purchase_till: now + Duration::days(365),
|
||||||
|
active_till: now + Duration::days(validity_days),
|
||||||
|
components: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a component to this product
|
||||||
|
pub fn add_component(&mut self, component: ProductComponent) {
|
||||||
|
self.components.push(component);
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the purchase availability timeframe
|
||||||
|
pub fn set_purchase_period(&mut self, purchase_till: DateTime<Utc>) {
|
||||||
|
self.purchase_till = purchase_till;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the active timeframe
|
||||||
|
pub fn set_active_period(&mut self, active_till: DateTime<Utc>) {
|
||||||
|
self.active_till = active_till;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the product is available for purchase
|
||||||
|
pub fn is_purchasable(&self) -> bool {
|
||||||
|
self.status == ProductStatus::Available && Utc::now() <= self.purchase_till
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the product is still active (for services)
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
Utc::now() <= self.active_till
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait (provides default dump/load)
|
||||||
|
impl Storable for Product {}
|
||||||
|
|
||||||
|
// Implement SledModel trait
|
||||||
|
impl SledModel for Product {
|
||||||
|
fn get_id(&self) -> String {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_prefix() -> &'static str {
|
||||||
|
"product"
|
||||||
|
}
|
||||||
|
}
|
146
herodb/src/zaz/models/sale.rs
Normal file
146
herodb/src/zaz/models/sale.rs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
use super::product::Currency; // Use super:: for sibling module
|
||||||
|
use crate::db::core::{SledModel, Storable}; // Import Sled traits from new location
|
||||||
|
// use super::db::Model; // Removed old Model trait import
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
// use std::collections::HashMap; // Removed unused import
|
||||||
|
|
||||||
|
/// SaleStatus represents the status of a sale
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum SaleStatus {
|
||||||
|
Pending,
|
||||||
|
Completed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SaleItem represents an item in a sale
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SaleItem {
|
||||||
|
pub id: u32,
|
||||||
|
pub sale_id: u32,
|
||||||
|
pub product_id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub quantity: i32,
|
||||||
|
pub unit_price: Currency,
|
||||||
|
pub subtotal: Currency,
|
||||||
|
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SaleItem {
|
||||||
|
/// Create a new sale item
|
||||||
|
pub fn new(
|
||||||
|
id: u32,
|
||||||
|
sale_id: u32,
|
||||||
|
product_id: u32,
|
||||||
|
name: String,
|
||||||
|
quantity: i32,
|
||||||
|
unit_price: Currency,
|
||||||
|
active_till: DateTime<Utc>,
|
||||||
|
) -> Self {
|
||||||
|
// Calculate subtotal
|
||||||
|
let amount = unit_price.amount * quantity as f64;
|
||||||
|
let subtotal = Currency {
|
||||||
|
amount,
|
||||||
|
currency_code: unit_price.currency_code.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
sale_id,
|
||||||
|
product_id,
|
||||||
|
name,
|
||||||
|
quantity,
|
||||||
|
unit_price,
|
||||||
|
subtotal,
|
||||||
|
active_till,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sale represents a sale of products or services
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Sale {
|
||||||
|
pub id: u32,
|
||||||
|
pub company_id: u32,
|
||||||
|
pub buyer_name: String,
|
||||||
|
pub buyer_email: String,
|
||||||
|
pub total_amount: Currency,
|
||||||
|
pub status: SaleStatus,
|
||||||
|
pub sale_date: DateTime<Utc>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
pub items: Vec<SaleItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed old Model trait implementation
|
||||||
|
|
||||||
|
impl Sale {
|
||||||
|
/// Create a new sale with default timestamps
|
||||||
|
pub fn new(
|
||||||
|
id: u32,
|
||||||
|
company_id: u32,
|
||||||
|
buyer_name: String,
|
||||||
|
buyer_email: String,
|
||||||
|
currency_code: String,
|
||||||
|
status: SaleStatus,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
company_id,
|
||||||
|
buyer_name,
|
||||||
|
buyer_email,
|
||||||
|
total_amount: Currency { amount: 0.0, currency_code },
|
||||||
|
status,
|
||||||
|
sale_date: now,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
items: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an item to the sale and update the total amount
|
||||||
|
pub fn add_item(&mut self, item: SaleItem) {
|
||||||
|
// Make sure the item's sale_id matches this sale
|
||||||
|
assert_eq!(self.id, item.sale_id, "Item sale_id must match sale id");
|
||||||
|
|
||||||
|
// Update the total amount
|
||||||
|
if self.items.is_empty() {
|
||||||
|
// First item, initialize the total amount with the same currency
|
||||||
|
self.total_amount = Currency {
|
||||||
|
amount: item.subtotal.amount,
|
||||||
|
currency_code: item.subtotal.currency_code.clone(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Add to the existing total
|
||||||
|
// (Assumes all items have the same currency)
|
||||||
|
self.total_amount.amount += item.subtotal.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the item to the list
|
||||||
|
self.items.push(item);
|
||||||
|
|
||||||
|
// Update the sale timestamp
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the status of the sale
|
||||||
|
pub fn update_status(&mut self, status: SaleStatus) {
|
||||||
|
self.status = status;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait (provides default dump/load)
|
||||||
|
impl Storable for Sale {}
|
||||||
|
|
||||||
|
// Implement SledModel trait
|
||||||
|
impl SledModel for Sale {
|
||||||
|
fn get_id(&self) -> String {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_prefix() -> &'static str {
|
||||||
|
"sale"
|
||||||
|
}
|
||||||
|
}
|
78
herodb/src/zaz/models/shareholder.rs
Normal file
78
herodb/src/zaz/models/shareholder.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
use crate::db::core::{SledModel, Storable}; // Import Sled traits
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
// use std::collections::HashMap; // Removed unused import
|
||||||
|
|
||||||
|
// use super::db::Model; // Removed old Model trait import
|
||||||
|
|
||||||
|
/// ShareholderType represents the type of shareholder
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum ShareholderType {
|
||||||
|
Individual,
|
||||||
|
Corporate,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shareholder represents a shareholder of a company
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] // Added PartialEq
|
||||||
|
pub struct Shareholder {
|
||||||
|
pub id: u32,
|
||||||
|
pub company_id: u32,
|
||||||
|
pub user_id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub shares: f64,
|
||||||
|
pub percentage: f64,
|
||||||
|
pub type_: ShareholderType,
|
||||||
|
pub since: DateTime<Utc>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed old Model trait implementation
|
||||||
|
|
||||||
|
impl Shareholder {
|
||||||
|
/// Create a new shareholder with default timestamps
|
||||||
|
pub fn new(
|
||||||
|
id: u32,
|
||||||
|
company_id: u32,
|
||||||
|
user_id: u32,
|
||||||
|
name: String,
|
||||||
|
shares: f64,
|
||||||
|
percentage: f64,
|
||||||
|
type_: ShareholderType,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
company_id,
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
shares,
|
||||||
|
percentage,
|
||||||
|
type_,
|
||||||
|
since: now,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the shares owned by this shareholder
|
||||||
|
pub fn update_shares(&mut self, shares: f64, percentage: f64) {
|
||||||
|
self.shares = shares;
|
||||||
|
self.percentage = percentage;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait (provides default dump/load)
|
||||||
|
impl Storable for Shareholder {}
|
||||||
|
|
||||||
|
// Implement SledModel trait
|
||||||
|
impl SledModel for Shareholder {
|
||||||
|
fn get_id(&self) -> String {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_prefix() -> &'static str {
|
||||||
|
"shareholder"
|
||||||
|
}
|
||||||
|
}
|
57
herodb/src/zaz/models/user.rs
Normal file
57
herodb/src/zaz/models/user.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::db::core::{SledModel, Storable}; // Import Sled traits from new location
|
||||||
|
// use std::collections::HashMap; // Removed unused import
|
||||||
|
|
||||||
|
/// User represents a user in the Freezone Manager system
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
pub company: String, // here its just a best effort
|
||||||
|
pub role: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed old Model trait implementation
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
/// Create a new user with default timestamps
|
||||||
|
pub fn new(
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
company: String,
|
||||||
|
role: String,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
company,
|
||||||
|
role,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait (provides default dump/load)
|
||||||
|
impl Storable for User {}
|
||||||
|
|
||||||
|
// Implement SledModel trait
|
||||||
|
impl SledModel for User {
|
||||||
|
fn get_id(&self) -> String {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_prefix() -> &'static str {
|
||||||
|
"user"
|
||||||
|
}
|
||||||
|
}
|
143
herodb/src/zaz/models/vote.rs
Normal file
143
herodb/src/zaz/models/vote.rs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::db::core::{SledModel, Storable}; // Import Sled traits from new location
|
||||||
|
// use std::collections::HashMap; // Removed unused import
|
||||||
|
|
||||||
|
// use super::db::Model; // Removed old Model trait import
|
||||||
|
|
||||||
|
/// VoteStatus represents the status of a vote
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum VoteStatus {
|
||||||
|
Open,
|
||||||
|
Closed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vote represents a voting item in the Freezone
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Vote {
|
||||||
|
pub id: u32,
|
||||||
|
pub company_id: u32,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub start_date: DateTime<Utc>,
|
||||||
|
pub end_date: DateTime<Utc>,
|
||||||
|
pub status: VoteStatus,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
pub options: Vec<VoteOption>,
|
||||||
|
pub ballots: Vec<Ballot>,
|
||||||
|
pub private_group: Vec<u32>, // user id's only people who can vote
|
||||||
|
}
|
||||||
|
|
||||||
|
/// VoteOption represents an option in a vote
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct VoteOption {
|
||||||
|
pub id: u8,
|
||||||
|
pub vote_id: u32,
|
||||||
|
pub text: String,
|
||||||
|
pub count: i32,
|
||||||
|
pub min_valid: i32, // min votes we need to make total vote count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The vote as done by the user
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Ballot {
|
||||||
|
pub id: u32,
|
||||||
|
pub vote_id: u32,
|
||||||
|
pub user_id: u32,
|
||||||
|
pub vote_option_id: u8,
|
||||||
|
pub shares_count: i32,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed old Model trait implementation
|
||||||
|
|
||||||
|
impl Vote {
|
||||||
|
/// Create a new vote with default timestamps
|
||||||
|
pub fn new(
|
||||||
|
id: u32,
|
||||||
|
company_id: u32,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
start_date: DateTime<Utc>,
|
||||||
|
end_date: DateTime<Utc>,
|
||||||
|
status: VoteStatus,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
company_id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
status,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
options: Vec::new(),
|
||||||
|
ballots: Vec::new(),
|
||||||
|
private_group: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a voting option to this vote
|
||||||
|
pub fn add_option(&mut self, text: String, min_valid: i32) -> &VoteOption {
|
||||||
|
let id = if self.options.is_empty() {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
self.options.iter().map(|o| o.id).max().unwrap_or(0) + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let option = VoteOption {
|
||||||
|
id,
|
||||||
|
vote_id: self.id,
|
||||||
|
text,
|
||||||
|
count: 0,
|
||||||
|
min_valid,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.options.push(option);
|
||||||
|
self.options.last().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a ballot to this vote
|
||||||
|
pub fn add_ballot(&mut self, user_id: u32, vote_option_id: u8, shares_count: i32) -> &Ballot {
|
||||||
|
let id = if self.ballots.is_empty() {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
self.ballots.iter().map(|b| b.id).max().unwrap_or(0) + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let ballot = Ballot {
|
||||||
|
id,
|
||||||
|
vote_id: self.id,
|
||||||
|
user_id,
|
||||||
|
vote_option_id,
|
||||||
|
shares_count,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the vote count for the selected option
|
||||||
|
if let Some(option) = self.options.iter_mut().find(|o| o.id == vote_option_id) {
|
||||||
|
option.count += shares_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ballots.push(ballot);
|
||||||
|
self.ballots.last().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait (provides default dump/load)
|
||||||
|
impl Storable for Vote {}
|
||||||
|
|
||||||
|
// Implement SledModel trait
|
||||||
|
impl SledModel for Vote {
|
||||||
|
fn get_id(&self) -> String {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_prefix() -> &'static str {
|
||||||
|
"vote"
|
||||||
|
}
|
||||||
|
}
|
628
herodb/src/zaz/tests/db_integration_test.rs
Normal file
628
herodb/src/zaz/tests/db_integration_test.rs
Normal file
@ -0,0 +1,628 @@
|
|||||||
|
//! Integration tests for the zaz database module
|
||||||
|
|
||||||
|
use sled;
|
||||||
|
use bincode;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
/// Test the basic database functionality
|
||||||
|
#[test]
|
||||||
|
fn test_basic_database_operations() {
|
||||||
|
match run_comprehensive_test() {
|
||||||
|
Ok(_) => println!("All tests passed successfully!"),
|
||||||
|
Err(e) => panic!("Error running tests: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_comprehensive_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// User model
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
struct User {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
company: String,
|
||||||
|
role: String,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
fn new(
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
company: String,
|
||||||
|
role: String,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
company,
|
||||||
|
role,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Company model
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
enum BusinessType {
|
||||||
|
Local,
|
||||||
|
National,
|
||||||
|
Global,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
enum CompanyStatus {
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
Pending,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
struct Company {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
registration_number: String,
|
||||||
|
registration_date: DateTime<Utc>,
|
||||||
|
fiscal_year_end: String,
|
||||||
|
email: String,
|
||||||
|
phone: String,
|
||||||
|
website: String,
|
||||||
|
address: String,
|
||||||
|
business_type: BusinessType,
|
||||||
|
industry: String,
|
||||||
|
description: String,
|
||||||
|
status: CompanyStatus,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Company {
|
||||||
|
fn new(
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
registration_number: String,
|
||||||
|
registration_date: DateTime<Utc>,
|
||||||
|
fiscal_year_end: String,
|
||||||
|
email: String,
|
||||||
|
phone: String,
|
||||||
|
website: String,
|
||||||
|
address: String,
|
||||||
|
business_type: BusinessType,
|
||||||
|
industry: String,
|
||||||
|
description: String,
|
||||||
|
status: CompanyStatus,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
registration_number,
|
||||||
|
registration_date,
|
||||||
|
fiscal_year_end,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
website,
|
||||||
|
address,
|
||||||
|
business_type,
|
||||||
|
industry,
|
||||||
|
description,
|
||||||
|
status,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
let temp_dir = tempdir()?;
|
||||||
|
println!("Using temporary directory: {:?}", temp_dir.path());
|
||||||
|
|
||||||
|
println!("\n--- Testing User operations ---");
|
||||||
|
test_user_operations(temp_dir.path())?;
|
||||||
|
|
||||||
|
println!("\n--- Testing Company operations ---");
|
||||||
|
test_company_operations(temp_dir.path())?;
|
||||||
|
|
||||||
|
println!("\n--- Testing Transaction Simulation ---");
|
||||||
|
test_transaction_simulation(temp_dir.path())?;
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
drop(temp_dir);
|
||||||
|
|
||||||
|
println!("All comprehensive tests completed successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_user_operations(base_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// User model (duplicate for scope)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
struct User {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
company: String,
|
||||||
|
role: String,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
fn new(
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
company: String,
|
||||||
|
role: String,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
company,
|
||||||
|
role,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the user database
|
||||||
|
let db = sled::open(base_path.join("users"))?;
|
||||||
|
println!("Opened user database at: {:?}", base_path.join("users"));
|
||||||
|
|
||||||
|
// Create a test user
|
||||||
|
let user = User::new(
|
||||||
|
100,
|
||||||
|
"Test User".to_string(),
|
||||||
|
"test@example.com".to_string(),
|
||||||
|
"password123".to_string(),
|
||||||
|
"Test Company".to_string(),
|
||||||
|
"Admin".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert the user
|
||||||
|
let user_id = user.id.to_string();
|
||||||
|
let user_bytes = bincode::serialize(&user)?;
|
||||||
|
db.insert(user_id.as_bytes(), user_bytes)?;
|
||||||
|
db.flush()?;
|
||||||
|
println!("Inserted user: {}", user.name);
|
||||||
|
|
||||||
|
// Retrieve the user
|
||||||
|
if let Some(data) = db.get(user_id.as_bytes())? {
|
||||||
|
let retrieved_user: User = bincode::deserialize(&data)?;
|
||||||
|
println!("Retrieved user: {}", retrieved_user.name);
|
||||||
|
assert_eq!(user.name, retrieved_user.name);
|
||||||
|
assert_eq!(user.email, retrieved_user.email);
|
||||||
|
} else {
|
||||||
|
return Err("Failed to retrieve user".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the user
|
||||||
|
let updated_user = User::new(
|
||||||
|
100,
|
||||||
|
"Updated User".to_string(),
|
||||||
|
"updated@example.com".to_string(),
|
||||||
|
"newpassword".to_string(),
|
||||||
|
"New Company".to_string(),
|
||||||
|
"SuperAdmin".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let updated_bytes = bincode::serialize(&updated_user)?;
|
||||||
|
db.insert(user_id.as_bytes(), updated_bytes)?;
|
||||||
|
db.flush()?;
|
||||||
|
println!("Updated user: {}", updated_user.name);
|
||||||
|
|
||||||
|
// Retrieve the updated user
|
||||||
|
if let Some(data) = db.get(user_id.as_bytes())? {
|
||||||
|
let retrieved_user: User = bincode::deserialize(&data)?;
|
||||||
|
println!("Retrieved updated user: {}", retrieved_user.name);
|
||||||
|
assert_eq!(updated_user.name, retrieved_user.name);
|
||||||
|
assert_eq!(updated_user.email, retrieved_user.email);
|
||||||
|
} else {
|
||||||
|
return Err("Failed to retrieve updated user".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the user
|
||||||
|
db.remove(user_id.as_bytes())?;
|
||||||
|
db.flush()?;
|
||||||
|
println!("Deleted user: {}", user.name);
|
||||||
|
|
||||||
|
// Try to retrieve the deleted user (should fail)
|
||||||
|
let result = db.get(user_id.as_bytes())?;
|
||||||
|
assert!(result.is_none(), "User should be deleted");
|
||||||
|
println!("Verified user was deleted");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_company_operations(base_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Company model (duplicate for scope)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
enum BusinessType {
|
||||||
|
Local,
|
||||||
|
National,
|
||||||
|
Global,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
enum CompanyStatus {
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
Pending,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
struct Company {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
registration_number: String,
|
||||||
|
registration_date: DateTime<Utc>,
|
||||||
|
fiscal_year_end: String,
|
||||||
|
email: String,
|
||||||
|
phone: String,
|
||||||
|
website: String,
|
||||||
|
address: String,
|
||||||
|
business_type: BusinessType,
|
||||||
|
industry: String,
|
||||||
|
description: String,
|
||||||
|
status: CompanyStatus,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Company {
|
||||||
|
fn new(
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
registration_number: String,
|
||||||
|
registration_date: DateTime<Utc>,
|
||||||
|
fiscal_year_end: String,
|
||||||
|
email: String,
|
||||||
|
phone: String,
|
||||||
|
website: String,
|
||||||
|
address: String,
|
||||||
|
business_type: BusinessType,
|
||||||
|
industry: String,
|
||||||
|
description: String,
|
||||||
|
status: CompanyStatus,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
registration_number,
|
||||||
|
registration_date,
|
||||||
|
fiscal_year_end,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
website,
|
||||||
|
address,
|
||||||
|
business_type,
|
||||||
|
industry,
|
||||||
|
description,
|
||||||
|
status,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the company database
|
||||||
|
let db = sled::open(base_path.join("companies"))?;
|
||||||
|
println!("Opened company database at: {:?}", base_path.join("companies"));
|
||||||
|
|
||||||
|
// Create a test company
|
||||||
|
let company = Company::new(
|
||||||
|
100,
|
||||||
|
"Test Corp".to_string(),
|
||||||
|
"TEST123".to_string(),
|
||||||
|
Utc::now(),
|
||||||
|
"12-31".to_string(),
|
||||||
|
"test@corp.com".to_string(),
|
||||||
|
"123-456-7890".to_string(),
|
||||||
|
"www.testcorp.com".to_string(),
|
||||||
|
"123 Test St".to_string(),
|
||||||
|
BusinessType::Global,
|
||||||
|
"Technology".to_string(),
|
||||||
|
"A test company".to_string(),
|
||||||
|
CompanyStatus::Active,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert the company
|
||||||
|
let company_id = company.id.to_string();
|
||||||
|
let company_bytes = bincode::serialize(&company)?;
|
||||||
|
db.insert(company_id.as_bytes(), company_bytes)?;
|
||||||
|
db.flush()?;
|
||||||
|
println!("Inserted company: {}", company.name);
|
||||||
|
|
||||||
|
// Retrieve the company
|
||||||
|
if let Some(data) = db.get(company_id.as_bytes())? {
|
||||||
|
let retrieved_company: Company = bincode::deserialize(&data)?;
|
||||||
|
println!("Retrieved company: {}", retrieved_company.name);
|
||||||
|
assert_eq!(company.name, retrieved_company.name);
|
||||||
|
} else {
|
||||||
|
return Err("Failed to retrieve company".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all companies
|
||||||
|
let mut companies = Vec::new();
|
||||||
|
for item in db.iter() {
|
||||||
|
let (_key, value) = item?;
|
||||||
|
let company: Company = bincode::deserialize(&value)?;
|
||||||
|
companies.push(company);
|
||||||
|
}
|
||||||
|
println!("Found {} companies", companies.len());
|
||||||
|
assert_eq!(companies.len(), 1);
|
||||||
|
|
||||||
|
// Delete the company
|
||||||
|
db.remove(company_id.as_bytes())?;
|
||||||
|
db.flush()?;
|
||||||
|
println!("Deleted company: {}", company.name);
|
||||||
|
|
||||||
|
// List companies again (should be empty)
|
||||||
|
let mut companies = Vec::new();
|
||||||
|
for item in db.iter() {
|
||||||
|
let (_key, value) = item?;
|
||||||
|
let company: Company = bincode::deserialize(&value)?;
|
||||||
|
companies.push(company);
|
||||||
|
}
|
||||||
|
assert_eq!(companies.len(), 0);
|
||||||
|
println!("Verified company was deleted");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_transaction_simulation(base_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// User model (duplicate for scope)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
struct User {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
company: String,
|
||||||
|
role: String,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
fn new(
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
company: String,
|
||||||
|
role: String,
|
||||||
|
) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
company,
|
||||||
|
role,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the user database
|
||||||
|
let db = sled::open(base_path.join("tx_users"))?;
|
||||||
|
println!("Opened transaction test database at: {:?}", base_path.join("tx_users"));
|
||||||
|
|
||||||
|
// Add a user outside of transaction
|
||||||
|
let user = User::new(
|
||||||
|
200,
|
||||||
|
"Transaction Test".to_string(),
|
||||||
|
"tx@example.com".to_string(),
|
||||||
|
"password".to_string(),
|
||||||
|
"TX Corp".to_string(),
|
||||||
|
"User".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let user_id = user.id.to_string();
|
||||||
|
let user_bytes = bincode::serialize(&user)?;
|
||||||
|
db.insert(user_id.as_bytes(), user_bytes)?;
|
||||||
|
db.flush()?;
|
||||||
|
println!("Added initial user: {}", user.name);
|
||||||
|
|
||||||
|
// Since sled doesn't have explicit transaction support like the DB mock in the original code,
|
||||||
|
// we'll simulate transaction behavior by:
|
||||||
|
// 1. Making changes in memory
|
||||||
|
// 2. Only writing to the database when we "commit"
|
||||||
|
println!("Simulating transaction operations...");
|
||||||
|
|
||||||
|
// Create in-memory copy of our data (transaction workspace)
|
||||||
|
let mut tx_workspace = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
// Retrieve initial state from db
|
||||||
|
if let Some(data) = db.get(user_id.as_bytes())? {
|
||||||
|
let retrieved_user: User = bincode::deserialize(&data)?;
|
||||||
|
tx_workspace.insert(user_id.clone(), retrieved_user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user in transaction workspace
|
||||||
|
let updated_user = User::new(
|
||||||
|
200,
|
||||||
|
"Updated in TX".to_string(),
|
||||||
|
"updated@example.com".to_string(),
|
||||||
|
"newpass".to_string(),
|
||||||
|
"New Corp".to_string(),
|
||||||
|
"Admin".to_string(),
|
||||||
|
);
|
||||||
|
tx_workspace.insert(user_id.clone(), updated_user.clone());
|
||||||
|
println!("Updated user in transaction workspace");
|
||||||
|
|
||||||
|
// Add new user in transaction workspace
|
||||||
|
let new_user = User::new(
|
||||||
|
201,
|
||||||
|
"New in TX".to_string(),
|
||||||
|
"new@example.com".to_string(),
|
||||||
|
"password".to_string(),
|
||||||
|
"New Corp".to_string(),
|
||||||
|
"User".to_string(),
|
||||||
|
);
|
||||||
|
let new_user_id = new_user.id.to_string();
|
||||||
|
tx_workspace.insert(new_user_id.clone(), new_user.clone());
|
||||||
|
println!("Added new user in transaction workspace");
|
||||||
|
|
||||||
|
// Verify the transaction workspace state
|
||||||
|
let tx_user = tx_workspace.get(&user_id).unwrap();
|
||||||
|
assert_eq!(tx_user.name, "Updated in TX");
|
||||||
|
println!("Verified transaction changes are visible within workspace");
|
||||||
|
|
||||||
|
// Simulate a rollback by discarding our workspace without writing to db
|
||||||
|
println!("Rolled back transaction (discarded workspace without writing to db)");
|
||||||
|
|
||||||
|
// Verify original user is unchanged in the database
|
||||||
|
if let Some(data) = db.get(user_id.as_bytes())? {
|
||||||
|
let original: User = bincode::deserialize(&data)?;
|
||||||
|
assert_eq!(original.name, "Transaction Test");
|
||||||
|
println!("Verified original user is unchanged after rollback");
|
||||||
|
} else {
|
||||||
|
return Err("Failed to retrieve user after rollback".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify new user was not added to the database
|
||||||
|
let result = db.get(new_user_id.as_bytes())?;
|
||||||
|
assert!(result.is_none());
|
||||||
|
println!("Verified new user was not added after rollback");
|
||||||
|
|
||||||
|
// Test commit transaction
|
||||||
|
println!("Simulating a new transaction...");
|
||||||
|
|
||||||
|
// Create new transaction workspace
|
||||||
|
let mut tx_workspace = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
// Retrieve current state from db
|
||||||
|
if let Some(data) = db.get(user_id.as_bytes())? {
|
||||||
|
let retrieved_user: User = bincode::deserialize(&data)?;
|
||||||
|
tx_workspace.insert(user_id.clone(), retrieved_user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user in new transaction workspace
|
||||||
|
let committed_user = User::new(
|
||||||
|
200,
|
||||||
|
"Committed Update".to_string(),
|
||||||
|
"commit@example.com".to_string(),
|
||||||
|
"commit_pass".to_string(),
|
||||||
|
"Commit Corp".to_string(),
|
||||||
|
"Manager".to_string(),
|
||||||
|
);
|
||||||
|
tx_workspace.insert(user_id.clone(), committed_user.clone());
|
||||||
|
println!("Updated user in new transaction");
|
||||||
|
|
||||||
|
// Commit the transaction by writing the workspace changes to the database
|
||||||
|
println!("Committing transaction by writing changes to database");
|
||||||
|
for (key, user) in tx_workspace {
|
||||||
|
let user_bytes = bincode::serialize(&user)?;
|
||||||
|
db.insert(key.as_bytes(), user_bytes)?;
|
||||||
|
}
|
||||||
|
db.flush()?;
|
||||||
|
|
||||||
|
// Verify changes persisted to the database
|
||||||
|
if let Some(data) = db.get(user_id.as_bytes())? {
|
||||||
|
let final_user: User = bincode::deserialize(&data)?;
|
||||||
|
assert_eq!(final_user.name, "Committed Update");
|
||||||
|
println!("Verified changes persisted after commit");
|
||||||
|
} else {
|
||||||
|
return Err("Failed to retrieve user after commit".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test the basic CRUD functionality with a single model
|
||||||
|
#[test]
|
||||||
|
fn test_simple_db() {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
let temp_dir = tempdir().expect("Failed to create temp directory");
|
||||||
|
println!("Created temporary directory at: {:?}", temp_dir.path());
|
||||||
|
|
||||||
|
// Create a test user
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
struct User {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
fn new(id: u32, name: String, email: String) -> Self {
|
||||||
|
Self { id, name, email }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open a sled database in the temporary directory
|
||||||
|
let db = sled::open(temp_dir.path().join("simple_users")).expect("Failed to open database");
|
||||||
|
println!("Opened database at: {:?}", temp_dir.path().join("simple_users"));
|
||||||
|
|
||||||
|
// CREATE: Create a user
|
||||||
|
let user = User::new(1, "Simple User".to_string(), "simple@example.com".to_string());
|
||||||
|
let user_key = user.id.to_string();
|
||||||
|
let user_value = bincode::serialize(&user).expect("Failed to serialize user");
|
||||||
|
db.insert(user_key.as_bytes(), user_value).expect("Failed to insert user");
|
||||||
|
db.flush().expect("Failed to flush database");
|
||||||
|
println!("Created user: {} ({})", user.name, user.email);
|
||||||
|
|
||||||
|
// READ: Retrieve the user
|
||||||
|
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
|
||||||
|
assert!(result.is_some(), "User should exist");
|
||||||
|
if let Some(data) = result {
|
||||||
|
let retrieved_user: User = bincode::deserialize(&data).expect("Failed to deserialize user");
|
||||||
|
println!("Retrieved user: {} ({})", retrieved_user.name, retrieved_user.email);
|
||||||
|
assert_eq!(user, retrieved_user, "Retrieved user should match original");
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPDATE: Update the user
|
||||||
|
let updated_user = User::new(1, "Updated User".to_string(), "updated@example.com".to_string());
|
||||||
|
let updated_value = bincode::serialize(&updated_user).expect("Failed to serialize updated user");
|
||||||
|
db.insert(user_key.as_bytes(), updated_value).expect("Failed to update user");
|
||||||
|
db.flush().expect("Failed to flush database");
|
||||||
|
println!("Updated user: {} ({})", updated_user.name, updated_user.email);
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
|
||||||
|
assert!(result.is_some(), "Updated user should exist");
|
||||||
|
if let Some(data) = result {
|
||||||
|
let retrieved_user: User = bincode::deserialize(&data).expect("Failed to deserialize user");
|
||||||
|
println!("Retrieved updated user: {} ({})", retrieved_user.name, retrieved_user.email);
|
||||||
|
assert_eq!(updated_user, retrieved_user, "Retrieved user should match updated version");
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE: Delete the user
|
||||||
|
db.remove(user_key.as_bytes()).expect("Failed to delete user");
|
||||||
|
db.flush().expect("Failed to flush database");
|
||||||
|
println!("Deleted user");
|
||||||
|
|
||||||
|
// Verify deletion
|
||||||
|
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
|
||||||
|
assert!(result.is_none(), "User should be deleted");
|
||||||
|
println!("Verified user deletion");
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
drop(db);
|
||||||
|
temp_dir.close().expect("Failed to cleanup temporary directory");
|
||||||
|
|
||||||
|
println!("Simple DB test completed successfully!");
|
||||||
|
}
|
265
herodb/src/zaz/tests/transaction_test.rs
Normal file
265
herodb/src/zaz/tests/transaction_test.rs
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
//! Transaction tests for the zaz database module
|
||||||
|
|
||||||
|
use sled;
|
||||||
|
use bincode;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Test the transaction-like behavior capabilities
|
||||||
|
#[test]
|
||||||
|
fn test_transaction_operations() {
|
||||||
|
match run_transaction_test() {
|
||||||
|
Ok(_) => println!("All transaction tests passed successfully!"),
|
||||||
|
Err(e) => panic!("Error in transaction tests: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_transaction_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
let temp_dir = tempdir()?;
|
||||||
|
println!("Using temporary directory: {:?}", temp_dir.path());
|
||||||
|
|
||||||
|
test_basic_transactions(temp_dir.path())?;
|
||||||
|
test_rollback_behavior(temp_dir.path())?;
|
||||||
|
test_concurrent_operations(temp_dir.path())?;
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
drop(temp_dir);
|
||||||
|
|
||||||
|
println!("All transaction tests completed successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User model for testing
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
struct User {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
balance: f64,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
fn new(id: u32, name: String, email: String, balance: f64) -> Self {
|
||||||
|
let now = Utc::now();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
balance,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test basic transaction functionality
|
||||||
|
fn test_basic_transactions(base_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Open the test database
|
||||||
|
let db = sled::open(base_path.join("basic_tx"))?;
|
||||||
|
println!("Opened basic transaction test database at: {:?}", base_path.join("basic_tx"));
|
||||||
|
|
||||||
|
// Create initial users
|
||||||
|
let user1 = User::new(
|
||||||
|
1,
|
||||||
|
"User One".to_string(),
|
||||||
|
"one@example.com".to_string(),
|
||||||
|
100.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let user2 = User::new(
|
||||||
|
2,
|
||||||
|
"User Two".to_string(),
|
||||||
|
"two@example.com".to_string(),
|
||||||
|
50.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert initial users
|
||||||
|
db.insert(user1.id.to_string().as_bytes(), bincode::serialize(&user1)?)?;
|
||||||
|
db.insert(user2.id.to_string().as_bytes(), bincode::serialize(&user2)?)?;
|
||||||
|
db.flush()?;
|
||||||
|
println!("Inserted initial users");
|
||||||
|
|
||||||
|
// Simulate a transaction - transfer 25.0 from user1 to user2
|
||||||
|
println!("Starting transaction simulation: transfer 25.0 from user1 to user2");
|
||||||
|
|
||||||
|
// Create transaction workspace
|
||||||
|
let mut tx_workspace = HashMap::new();
|
||||||
|
|
||||||
|
// Retrieve current state
|
||||||
|
if let Some(data) = db.get(user1.id.to_string().as_bytes())? {
|
||||||
|
let user: User = bincode::deserialize(&data)?;
|
||||||
|
tx_workspace.insert(user1.id.to_string(), user);
|
||||||
|
} else {
|
||||||
|
return Err("Failed to find user1".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(data) = db.get(user2.id.to_string().as_bytes())? {
|
||||||
|
let user: User = bincode::deserialize(&data)?;
|
||||||
|
tx_workspace.insert(user2.id.to_string(), user);
|
||||||
|
} else {
|
||||||
|
return Err("Failed to find user2".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify both users in the transaction
|
||||||
|
let mut updated_user1 = tx_workspace.get(&user1.id.to_string()).unwrap().clone();
|
||||||
|
let mut updated_user2 = tx_workspace.get(&user2.id.to_string()).unwrap().clone();
|
||||||
|
|
||||||
|
updated_user1.balance -= 25.0;
|
||||||
|
updated_user2.balance += 25.0;
|
||||||
|
|
||||||
|
// Update the workspace
|
||||||
|
tx_workspace.insert(user1.id.to_string(), updated_user1);
|
||||||
|
tx_workspace.insert(user2.id.to_string(), updated_user2);
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
println!("Committing transaction");
|
||||||
|
for (key, user) in tx_workspace {
|
||||||
|
let user_bytes = bincode::serialize(&user)?;
|
||||||
|
db.insert(key.as_bytes(), user_bytes)?;
|
||||||
|
}
|
||||||
|
db.flush()?;
|
||||||
|
|
||||||
|
// Verify the results
|
||||||
|
if let Some(data) = db.get(user1.id.to_string().as_bytes())? {
|
||||||
|
let final_user1: User = bincode::deserialize(&data)?;
|
||||||
|
assert_eq!(final_user1.balance, 75.0, "User1 balance should be 75.0");
|
||||||
|
println!("Verified user1 balance is now {}", final_user1.balance);
|
||||||
|
} else {
|
||||||
|
return Err("Failed to find user1 after transaction".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(data) = db.get(user2.id.to_string().as_bytes())? {
|
||||||
|
let final_user2: User = bincode::deserialize(&data)?;
|
||||||
|
assert_eq!(final_user2.balance, 75.0, "User2 balance should be 75.0");
|
||||||
|
println!("Verified user2 balance is now {}", final_user2.balance);
|
||||||
|
} else {
|
||||||
|
return Err("Failed to find user2 after transaction".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
drop(db);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test transaction rollback functionality
|
||||||
|
fn test_rollback_behavior(base_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Open the test database
|
||||||
|
let db = sled::open(base_path.join("rollback_tx"))?;
|
||||||
|
println!("Opened rollback test database at: {:?}", base_path.join("rollback_tx"));
|
||||||
|
|
||||||
|
// Create initial user
|
||||||
|
let user = User::new(
|
||||||
|
1,
|
||||||
|
"Rollback Test".to_string(),
|
||||||
|
"rollback@example.com".to_string(),
|
||||||
|
100.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert initial user
|
||||||
|
db.insert(user.id.to_string().as_bytes(), bincode::serialize(&user)?)?;
|
||||||
|
db.flush()?;
|
||||||
|
println!("Inserted initial user with balance: {}", user.balance);
|
||||||
|
|
||||||
|
// Simulate a transaction that shouldn't be committed
|
||||||
|
println!("Starting transaction that will be rolled back");
|
||||||
|
|
||||||
|
// Create transaction workspace (we'd track in memory)
|
||||||
|
let mut updated_user = user.clone();
|
||||||
|
updated_user.balance = 0.0; // Drastic change
|
||||||
|
|
||||||
|
// Do NOT commit changes to the database (simulating rollback)
|
||||||
|
println!("Rolling back transaction (by not writing changes)");
|
||||||
|
|
||||||
|
// Verify the original data is intact
|
||||||
|
if let Some(data) = db.get(user.id.to_string().as_bytes())? {
|
||||||
|
let final_user: User = bincode::deserialize(&data)?;
|
||||||
|
assert_eq!(final_user.balance, 100.0, "User balance should remain 100.0");
|
||||||
|
println!("Verified user balance is still {} after rollback", final_user.balance);
|
||||||
|
} else {
|
||||||
|
return Err("Failed to find user after rollback".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
drop(db);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test multiple operations that might happen concurrently
|
||||||
|
fn test_concurrent_operations(base_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Open the test database
|
||||||
|
let db = sled::open(base_path.join("concurrent_tx"))?;
|
||||||
|
println!("Opened concurrent operations test database at: {:?}", base_path.join("concurrent_tx"));
|
||||||
|
|
||||||
|
// Create initial user
|
||||||
|
let user = User::new(
|
||||||
|
1,
|
||||||
|
"Concurrent Test".to_string(),
|
||||||
|
"concurrent@example.com".to_string(),
|
||||||
|
100.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert initial user
|
||||||
|
db.insert(user.id.to_string().as_bytes(), bincode::serialize(&user)?)?;
|
||||||
|
db.flush()?;
|
||||||
|
println!("Inserted initial user with balance: {}", user.balance);
|
||||||
|
|
||||||
|
// Simulate two concurrent transactions
|
||||||
|
// Transaction 1: Add 50 to balance
|
||||||
|
println!("Starting simulated concurrent transaction 1: Add 50 to balance");
|
||||||
|
|
||||||
|
// Read current state for TX1
|
||||||
|
let mut tx1_user = user.clone();
|
||||||
|
if let Some(data) = db.get(user.id.to_string().as_bytes())? {
|
||||||
|
tx1_user = bincode::deserialize(&data)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction 2: Subtract 30 from balance
|
||||||
|
println!("Starting simulated concurrent transaction 2: Subtract 30 from balance");
|
||||||
|
|
||||||
|
// Read current state for TX2 (same starting point)
|
||||||
|
let mut tx2_user = user.clone();
|
||||||
|
if let Some(data) = db.get(user.id.to_string().as_bytes())? {
|
||||||
|
tx2_user = bincode::deserialize(&data)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify in TX1
|
||||||
|
tx1_user.balance += 50.0;
|
||||||
|
|
||||||
|
// Modify in TX2
|
||||||
|
tx2_user.balance -= 30.0;
|
||||||
|
|
||||||
|
// Commit TX1 first
|
||||||
|
println!("Committing TX1");
|
||||||
|
db.insert(user.id.to_string().as_bytes(), bincode::serialize(&tx1_user)?)?;
|
||||||
|
db.flush()?;
|
||||||
|
|
||||||
|
// Now commit TX2 (would overwrite TX1 in naive implementation)
|
||||||
|
println!("Committing TX2");
|
||||||
|
db.insert(user.id.to_string().as_bytes(), bincode::serialize(&tx2_user)?)?;
|
||||||
|
db.flush()?;
|
||||||
|
|
||||||
|
// Verify the final state (last write wins, so should be TX2's value)
|
||||||
|
if let Some(data) = db.get(user.id.to_string().as_bytes())? {
|
||||||
|
let final_user: User = bincode::deserialize(&data)?;
|
||||||
|
assert_eq!(final_user.balance, 70.0, "Final balance should be 70.0 (TX2 overwrote TX1)");
|
||||||
|
println!("Final user balance is {} after both transactions", final_user.balance);
|
||||||
|
|
||||||
|
// In a real implementation with better concurrency control, you'd expect:
|
||||||
|
// println!("In a proper ACID system, this would have been 120.0 (100.0 - 30.0 + 50.0)");
|
||||||
|
} else {
|
||||||
|
return Err("Failed to find user after concurrent transactions".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
drop(db);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user