# TST Integration Plan for HeroDB ## Overview This document outlines the plan for adding generic functionality to the `herodb/src/db` module to use the Ternary Search Tree (TST) for storing objects with prefixed IDs and implementing a generic list function to retrieve all objects with a specific prefix. ## Current Architecture Currently: - Each model has a `db_prefix()` method that returns a string prefix (e.g., "vote" for Vote objects) - Objects are stored in OurDB with numeric IDs - The `list()` method in `OurDbStore` is not implemented ## Implementation Plan ### 1. Create a TST-based Index Manager (herodb/src/db/tst_index.rs) Create a new module that manages TST instances for different model prefixes: ```rust use crate::db::error::{DbError, DbResult}; use std::path::{Path, PathBuf}; use tst::TST; /// Manages TST-based indexes for model objects pub struct TSTIndexManager { /// Base path for TST databases base_path: PathBuf, /// Map of model prefixes to their TST instances tst_instances: std::collections::HashMap, } impl TSTIndexManager { /// Creates a new TST index manager pub fn new>(base_path: P) -> DbResult { let base_path = base_path.as_ref().to_path_buf(); // Create directory if it doesn't exist std::fs::create_dir_all(&base_path).map_err(DbError::IoError)?; Ok(Self { base_path, tst_instances: std::collections::HashMap::new(), }) } /// Gets or creates a TST instance for a model prefix pub fn get_tst(&mut self, prefix: &str) -> DbResult<&mut TST> { if !self.tst_instances.contains_key(prefix) { // Create a new TST instance for this prefix let tst_path = self.base_path.join(format!("{}_tst", prefix)); let tst_path_str = tst_path.to_string_lossy().to_string(); // Create the TST let tst = TST::new(&tst_path_str, false) .map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?; // Insert it into the map self.tst_instances.insert(prefix.to_string(), tst); } // Return a mutable reference to the TST Ok(self.tst_instances.get_mut(prefix).unwrap()) } /// Adds or updates an object in the TST index pub fn set(&mut self, prefix: &str, id: u32, data: Vec) -> DbResult<()> { // Get the TST for this prefix let tst = self.get_tst(prefix)?; // Create the key in the format prefix_id let key = format!("{}_{}", prefix, id); // Set the key-value pair in the TST tst.set(&key, data) .map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?; Ok(()) } /// Removes an object from the TST index pub fn delete(&mut self, prefix: &str, id: u32) -> DbResult<()> { // Get the TST for this prefix let tst = self.get_tst(prefix)?; // Create the key in the format prefix_id let key = format!("{}_{}", prefix, id); // Delete the key from the TST tst.delete(&key) .map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?; Ok(()) } /// Lists all objects with a given prefix pub fn list(&mut self, prefix: &str) -> DbResult)>> { // Get the TST for this prefix let tst = self.get_tst(prefix)?; // Get all keys with this prefix let keys = tst.list(prefix) .map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?; // Get all values for these keys let mut result = Vec::with_capacity(keys.len()); for key in keys { // Extract the ID from the key (format: prefix_id) let id_str = key.split('_').nth(1).ok_or_else(|| { DbError::GeneralError(format!("Invalid key format: {}", key)) })?; let id = id_str.parse::().map_err(|_| { DbError::GeneralError(format!("Invalid ID in key: {}", key)) })?; // Get the value from the TST let data = tst.get(&key) .map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?; result.push((id, data)); } Ok(result) } } ``` ### 2. Update DB Module (herodb/src/db/mod.rs) Add the new module to the db module: ```rust pub mod db; pub mod error; pub mod macros; pub mod model; pub mod model_methods; pub mod store; pub mod tst_index; // Add the new module pub use db::DB; pub use db::DBBuilder; pub use error::{DbError, DbResult}; pub use model::Model; pub use model::Storable; ``` ### 3. Modify DB Struct (herodb/src/db/db.rs) Update the DB struct to include the TST index manager: ```rust /// Main DB manager that automatically handles all models #[derive(Clone, CustomType)] pub struct DB { db_path: PathBuf, // Type map for generic operations type_map: HashMap>>, // TST index manager tst_index: Arc>, // Transaction state transaction: Arc>>, } ``` ### 4. Extend Transaction Handling Extend the `DbOperation` enum to include model prefix and ID information: ```rust #[derive(Debug, Clone)] enum DbOperation { Set { model_type: TypeId, serialized: Vec, model_prefix: String, // Add model prefix model_id: u32, // Add model ID }, Delete { model_type: TypeId, id: u32, model_prefix: String, // Add model prefix }, } ``` ### 5. Update Transaction Recording Modify the `set` and `delete` methods to record model prefix and ID in the transaction: ```rust pub fn set(&self, model: &T) -> DbResult<()> { // 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 = model.to_bytes()?; // Record a Set operation in the transaction with prefix and ID tx_state.operations.push(DbOperation::Set { model_type: TypeId::of::(), serialized: serialized.clone(), model_prefix: T::db_prefix().to_string(), model_id: model.get_id(), }); return Ok(()); } } // ... rest of the method ... } pub fn delete(&self, id: u32) -> DbResult<()> { // 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 with prefix tx_state.operations.push(DbOperation::Delete { model_type: TypeId::of::(), id, model_prefix: T::db_prefix().to_string(), }); return Ok(()); } } // ... rest of the method ... } ``` ### 6. Update Transaction Commit Modify the `commit_transaction` method to update both OurDB and the TST index: ```rust pub fn commit_transaction(&self) -> DbResult<()> { let mut tx_guard = self.transaction.write().unwrap(); if let Some(tx_state) = tx_guard.take() { if !tx_state.active { return Err(DbError::TransactionError("Transaction not active".into())); } // Create a backup of the transaction state in case we need to rollback let backup = tx_state.clone(); // Try to execute all operations let result = (|| { for op in &tx_state.operations { match op { DbOperation::Set { model_type, serialized, model_prefix, model_id, } => { // Apply to OurDB self.apply_set_operation(*model_type, serialized)?; // Apply to TST index let mut tst_index = self.tst_index.write().unwrap(); tst_index.set(model_prefix, *model_id, serialized.clone())?; } DbOperation::Delete { model_type, id, model_prefix, } => { // Apply to OurDB let db_ops = self .type_map .get(model_type) .ok_or_else(|| DbError::TypeError)?; let mut db_ops_guard = db_ops.write().unwrap(); db_ops_guard.delete(*id)?; // Apply to TST index let mut tst_index = self.tst_index.write().unwrap(); tst_index.delete(model_prefix, *id)?; } } } Ok(()) })(); // If any operation failed, restore the transaction state if result.is_err() { *tx_guard = Some(backup); return result; } Ok(()) } else { Err(DbError::TransactionError("No active transaction".into())) } } ``` ### 7. Implement List Method Implement the `list` method to use the TST's prefix search: ```rust pub fn list(&self) -> DbResult> { // Get the prefix for this model type let prefix = T::db_prefix(); // Use the TST index to get all objects with this prefix let mut tst_index = self.tst_index.write().unwrap(); let items = tst_index.list(prefix)?; // Deserialize the objects let mut result = Vec::with_capacity(items.len()); for (_, data) in items { let model = T::from_bytes(&data)?; result.push(model); } Ok(result) } ``` ### 8. Add Recovery Mechanism Add a method to synchronize the TST index with OurDB in case they get out of sync: ```rust pub fn synchronize_tst_index(&self) -> DbResult<()> { // Get all models from OurDB let models = self.list_from_ourdb::()?; // Clear the TST index for this model type let mut tst_index = self.tst_index.write().unwrap(); let prefix = T::db_prefix(); // Rebuild the TST index for model in models { let id = model.get_id(); let data = model.to_bytes()?; tst_index.set(prefix, id, data)?; } Ok(()) } // Helper method to list models directly from OurDB (not using TST) fn list_from_ourdb(&self) -> DbResult> { match self.type_map.get(&TypeId::of::()) { Some(db_ops) => { let db_ops_guard = db_ops.read().unwrap(); let result_any = db_ops_guard.list()?; match result_any.downcast::>() { Ok(vec_t) => Ok(*vec_t), Err(_) => Err(DbError::TypeError), } } None => Err(DbError::TypeError), } } ``` ## Implementation Flow ```mermaid sequenceDiagram participant Client participant DB participant TransactionState participant OurDbStore participant TSTIndexManager participant TST Client->>DB: begin_transaction() DB->>TransactionState: create new transaction Client->>DB: set(model) DB->>TransactionState: record Set operation with prefix and ID Client->>DB: delete(model) DB->>TransactionState: record Delete operation with prefix and ID Client->>DB: commit_transaction() DB->>TransactionState: get all operations loop For each operation alt Set operation DB->>OurDbStore: apply_set_operation() DB->>TSTIndexManager: set(prefix, id, data) TSTIndexManager->>TST: set(key, data) else Delete operation DB->>OurDbStore: delete(id) DB->>TSTIndexManager: delete(prefix, id) TSTIndexManager->>TST: delete(key) end end alt Success DB-->>Client: Ok(()) else Error DB->>TransactionState: restore transaction state DB-->>Client: Err(error) end Client->>DB: list() DB->>TSTIndexManager: list(prefix) TSTIndexManager->>TST: list(prefix) TST-->>TSTIndexManager: keys TSTIndexManager->>TST: get(key) for each key TST-->>TSTIndexManager: data TSTIndexManager-->>DB: (id, data) pairs DB->>DB: deserialize data to models DB-->>Client: Vec ``` ## Testing Strategy 1. Create unit tests for the TST index manager 2. Test the list functionality with different model types 3. Test transaction handling (commit and rollback) 4. Test error recovery mechanisms 5. Test edge cases (empty database, large datasets) ## Implementation Steps 1. Add TST dependency to herodb/Cargo.toml 2. Create the tst_index.rs module 3. Update the DB module to include the TST index manager 4. Extend the transaction handling 5. Implement the list method 6. Add tests for the new functionality 7. Update documentation ## Considerations 1. **Performance**: The TST operations add overhead to insert/delete operations, but provide efficient list functionality. 2. **Consistency**: The enhanced transaction handling ensures consistency between OurDB and the TST index. 3. **Error Handling**: Proper error handling and recovery mechanisms are essential for maintaining data integrity. 4. **Backward Compatibility**: The implementation should maintain backward compatibility with existing code.