...
This commit is contained in:
91
herodb/src/cmd/dbexample/DB_README.md
Normal file
91
herodb/src/cmd/dbexample/DB_README.md
Normal 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
|
168
herodb/src/cmd/dbexample/db_tests.rs
Normal file
168
herodb/src/cmd/dbexample/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/cmd/dbexample/examples.rs
Normal file
64
herodb/src/cmd/dbexample/examples.rs
Normal 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(())
|
||||
}
|
33
herodb/src/cmd/dbexample/factory.rs
Normal file
33
herodb/src/cmd/dbexample/factory.rs
Normal 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(())
|
||||
}
|
Reference in New Issue
Block a user