...
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