This commit is contained in:
2025-04-20 08:15:12 +02:00
parent e3ec26a6ef
commit a8ef07bb3f
10 changed files with 425 additions and 147 deletions

View File

@@ -1,7 +1,7 @@
[package]
name = "herodb"
version = "0.1.0"
edition = "2024"
edition = "2021"
description = "A database library built on top of ourdb with model support"
license = "MIT"
authors = ["HeroCode Team"]

View File

@@ -0,0 +1,83 @@
use herodb::db::{DB, DBBuilder, Model};
use serde::{Deserialize, Serialize};
use chrono::Utc;
// Define a simple Product model
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SimpleProduct {
id: u32,
name: String,
description: String,
price: f64,
created_at: chrono::DateTime<Utc>,
}
// Implement the Model trait for SimpleProduct
impl Model for SimpleProduct {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"simple_product"
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("OurDB Minimal Example");
println!("====================\n");
// Create a temporary directory for the database
let db_path = std::env::temp_dir().join("herodb_minimal_example");
std::fs::create_dir_all(&db_path)?;
println!("Creating database at: {}", db_path.display());
// Create a new database with SimpleProduct model registered
let db = DBBuilder::new(db_path.clone())
.register_model::<SimpleProduct>()
.build()?;
println!("Database created successfully");
// Create a product
let product = SimpleProduct {
id: 1,
name: "Test Product".to_string(),
description: "A test product for our minimal OurDB example".to_string(),
price: 99.99,
created_at: Utc::now(),
};
println!("\nCreated product: {}", product.name);
println!("Product ID: {}", product.get_id());
// Insert the product into the database
db.set(&product)?;
println!("Product saved to database");
// Retrieve the product from the database
let retrieved_product = db.get::<SimpleProduct>(product.get_id())?;
println!("\nRetrieved product from database:");
println!(" Name: {}", retrieved_product.name);
println!(" Description: {}", retrieved_product.description);
println!(" Price: ${}", retrieved_product.price);
// Delete the product
db.delete::<SimpleProduct>(product.get_id())?;
println!("\nProduct deleted from database");
// Try to retrieve the deleted product (should fail)
match db.get::<SimpleProduct>(product.get_id()) {
Ok(_) => println!("Product still exists (unexpected)"),
Err(e) => println!("Verified deletion: {}", e),
}
println!("\nExample completed successfully!");
// Clean up
std::fs::remove_dir_all(&db_path)?;
println!("Cleaned up database directory");
Ok(())
}

View File

@@ -21,6 +21,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a currency for pricing
let usd = CurrencyBuilder::new()
.id(1) // Add an ID for the currency
.amount(99.99)
.currency_code("USD")
.build()

View File

@@ -1,144 +0,0 @@
use bincode;
use brotli::{CompressorReader, Decompressor};
use rhai::CustomType;
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
}
}

View File

@@ -1,4 +1,6 @@
ere/// Macro to implement typed access methods on the DB struct for a given model
//! Macros for implementing model methods
/// Macro to implement typed access methods on the DB struct for a given model
#[macro_export]
macro_rules! impl_model_methods {
($model:ty, $singular:ident, $plural:ident) => {

View File

@@ -1,4 +1,4 @@
s using// Export the error module
// Export the error module
pub mod error;
pub use error::{DbError, DbResult};
@@ -14,5 +14,8 @@ pub use store::{DbOperations, OurDbStore};
pub mod db;
pub use db::{DB, DBBuilder, ModelRegistration, ModelRegistrar};
// Export the base module (compatibility layer for migration)
pub mod base;
// Export macros for model methods
pub mod macros;