This commit is contained in:
2025-04-04 10:59:51 +02:00
parent b37b9da8b5
commit 32f6d87454
35 changed files with 227 additions and 261 deletions

View File

@@ -0,0 +1,91 @@
# HeroDB Architecture
This document explains the architecture of HeroDB, focusing on the separation between model definitions and database logic.
## Core Principles
1. **Separation of Concerns**: The DB core should not know about specific models
2. **Registration-Based System**: Models get registered with the DB through a factory pattern
3. **Type-Safety**: Despite the separation, we maintain full type safety
## Components
### Core Module
The `core` module provides the database foundation without knowing about specific models:
- `SledModel` trait: Defines the interface models must implement
- `Storable` trait: Provides serialization/deserialization capabilities
- `SledDB<T>`: Generic database wrapper for any model type
- `DB`: Main database manager that holds registered models
- `DBBuilder`: Builder for creating a DB with registered models
### Zaz Module
The `zaz` module contains domain-specific models and factories:
- `models`: Defines specific model types like User, Company, etc.
- `factory`: Provides functions to create a DB with zaz models registered
## Using the DB
### Option 1: Factory Function
The easiest way to create a DB with all zaz models is to use the factory:
```rust
use herodb::zaz::create_zaz_db;
// Create a DB with all zaz models registered
let db = create_zaz_db("/path/to/db")?;
// Use the DB with specific model types
let user = User::new(...);
db.set(&user)?;
let retrieved: User = db.get(&id)?;
```
### Option 2: Builder Pattern
For more control, use the builder pattern to register only the models you need:
```rust
use herodb::core::{DBBuilder, DB};
use herodb::zaz::models::{User, Company};
// Create a DB with only User and Company models
let db = DBBuilder::new("/path/to/db")
.register_model::<User>()
.register_model::<Company>()
.build()?;
```
### Option 3: Dynamic Registration
You can also register models with an existing DB:
```rust
use herodb::core::DB;
use herodb::zaz::models::User;
// Create an empty DB
let mut db = DB::new("/path/to/db")?;
// Register the User model
db.register::<User>()?;
```
## Benefits of this Architecture
1. **Modularity**: The core DB code doesn't need to change when models change
2. **Extensibility**: New model types can be added without modifying core DB code
3. **Flexibility**: Different modules can define and use their own models with the same DB code
4. **Type Safety**: Full compile-time type checking is maintained
## Implementation Details
The key to this architecture is the combination of generic types and trait objects:
- `SledDB<T>` provides type-safe operations for specific model types
- `AnyDbOperations` trait allows type-erased operations through a common interface
- `TypeId` mapping enables runtime lookup of the correct DB for a given model type

View File

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

View File

@@ -0,0 +1,64 @@
// Examples for using the Zaz database
use crate::zaz::models::*;
use crate::zaz::factory::create_zaz_db;
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 = create_zaz_db(&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(())
}

View File

@@ -0,0 +1,33 @@
//! Factory module for creating a DB with all zaz models registered
use crate::core::{DB, DBBuilder, SledDBResult};
use crate::zaz::models::*;
use std::path::PathBuf;
/// Create a new DB instance with all zaz models registered
pub fn create_zaz_db<P: Into<PathBuf>>(path: P) -> SledDBResult<DB> {
// Using the builder pattern to register all models
DBBuilder::new(path)
.register_model::<User>()
.register_model::<Company>()
.register_model::<Meeting>()
.register_model::<Product>()
.register_model::<Sale>()
.register_model::<Vote>()
.register_model::<Shareholder>()
.build()
}
/// Register all zaz models with an existing DB instance
pub fn register_zaz_models(db: &mut DB) -> SledDBResult<()> {
// Dynamically register all zaz models
db.register::<User>()?;
db.register::<Company>()?;
db.register::<Meeting>()?;
db.register::<Product>()?;
db.register::<Sale>()?;
db.register::<Vote>()?;
db.register::<Shareholder>()?;
Ok(())
}