Skip to main content
TAPLE Core provides a trait-based database abstraction that allows you to implement custom storage backends for subjects, events, and other protocol data.

Database Traits

Two traits define the database interface: DatabaseManager for creating collections and DatabaseCollection for data operations.

DatabaseManager

Manages database collections (from core/src/database/mod.rs:11-21):
pub trait DatabaseManager<C>: Sync + Send
where
    C: DatabaseCollection,
{
    /// Default constructor (mainly for testing)
    fn default() -> Self;
    
    /// Creates a database collection
    /// # Arguments
    /// - identifier: The identifier of the collection
    fn create_collection(&self, identifier: &str) -> C;
}

DatabaseCollection

Provides key-value operations with iteration support (from core/src/database/mod.rs:23-37):
pub trait DatabaseCollection: Sync + Send {
    /// Retrieves the value associated with the given key
    fn get(&self, key: &str) -> Result<Vec<u8>, Error>;
    
    /// Associates the given value with the given key
    fn put(&self, key: &str, data: Vec<u8>) -> Result<(), Error>;
    
    /// Removes the value associated with the given key
    fn del(&self, key: &str) -> Result<(), Error>;
    
    /// Returns an iterator over the key-value pairs
    fn iter<'a>(
        &'a self,
        reverse: bool,
        prefix: String,
    ) -> Box<dyn Iterator<Item = (String, Vec<u8>)> + 'a>;
}

Reference Implementation: MemoryManager

TAPLE includes an in-memory implementation for testing and development.

MemoryManager Structure

From core/src/database/memory.rs:40-71:
pub struct MemoryManager {
    data: RwLock<HashMap<String, Arc<DataStore>>>,
}

impl MemoryManager {
    pub fn new() -> Self {
        Self {
            data: RwLock::new(HashMap::new()),
        }
    }
}

impl DatabaseManager<MemoryCollection> for MemoryManager {
    fn default() -> Self {
        Self {
            data: RwLock::new(HashMap::new()),
        }
    }

    fn create_collection(&self, _identifier: &str) -> MemoryCollection {
        let mut lock = self.data.write().unwrap();
        let db: Arc<DataStore> = match lock.get("") {
            Some(map) => map.clone(),
            None => {
                let db: Arc<DataStore> = Arc::new(DataStore::new());
                lock.insert("".to_string(), db.clone());
                db
            }
        };
        MemoryCollection { data: db }
    }
}

MemoryCollection Implementation

From core/src/database/memory.rs:74-110:
pub struct MemoryCollection {
    data: Arc<DataStore>,
}

impl DatabaseCollection for MemoryCollection {
    fn get(&self, key: &str) -> Result<Vec<u8>, Error> {
        let lock = self.data._get_inner_read_lock();
        let Some(data) = lock.get(key) else {
            return Err(Error::EntryNotFound);
        };
        Ok(data.clone())
    }

    fn put(&self, key: &str, data: Vec<u8>) -> Result<(), Error> {
        let mut lock = self.data._get_inner_write_lock();
        lock.insert(key.to_string(), data);
        Ok(())
    }

    fn del(&self, key: &str) -> Result<(), Error> {
        let mut lock = self.data._get_inner_write_lock();
        lock.remove(key);
        Ok(())
    }

    fn iter<'a>(
        &'a self,
        reverse: bool,
        prefix: String,
    ) -> Box<dyn Iterator<Item = (String, Vec<u8>)> + 'a> {
        if reverse {
            Box::new(self.data.rev_iter(prefix))
        } else {
            Box::new(self.data.iter(prefix))
        }
    }
}

DataStore with Iterators

From core/src/database/memory.rs:9-37:
pub struct DataStore {
    data: RwLock<BTreeMap<String, Vec<u8>>>,
}

impl DataStore {
    fn new() -> Self {
        Self {
            data: RwLock::new(BTreeMap::new()),
        }
    }

    fn _get_inner_read_lock<'a>(&'a self) -> RwLockReadGuard<'a, BTreeMap<String, Vec<u8>>> {
        self.data.read().unwrap()
    }

    fn _get_inner_write_lock<'a>(&'a self) -> RwLockWriteGuard<'a, BTreeMap<String, Vec<u8>>> {
        self.data.write().unwrap()
    }

    fn iter(&self, prefix: String) -> MemoryIterator {
        MemoryIterator::new(&self, prefix)
    }

    fn rev_iter(&self, prefix: String) -> RevMemoryIterator {
        RevMemoryIterator::new(&self, prefix)
    }
}

Database Wrapper (DB)

The DB struct provides a high-level interface over database collections for storing TAPLE-specific data.

DB Structure

From core/src/database/db.rs:28-60:
pub struct DB<C: DatabaseCollection> {
    signature_db: SignatureDb<C>,
    subject_db: SubjectDb<C>,
    event_db: EventDb<C>,
    prevalidated_event_db: PrevalidatedEventDb<C>,
    event_request_db: EventRequestDb<C>,
    request_db: RequestDb<C>,
    controller_id_db: ControllerIdDb<C>,
    validation_db: ValidationDb<C>,
    contract_db: ContractDb<C>,
    witness_signatures_db: WitnessSignaturesDb<C>,
    subject_by_governance_db: SubjectByGovernanceDb<C>,
    keys_db: KeysDb<C>,
    preauthorized_subjects_and_providers_db: PreauthorizedSbujectsAndProovidersDb<C>,
    lce_validation_proofs_db: LceValidationProofs<C>,
    approvals_db: ApprovalsDb<C>,
}

Initialization

From core/src/database/db.rs:62-97:
impl<C: DatabaseCollection> DB<C> {
    pub fn new<M: DatabaseManager<C>>(manager: Arc<M>) -> Self {
        let signature_db = SignatureDb::new(&manager);
        let subject_db = SubjectDb::new(&manager);
        let event_db = EventDb::new(&manager);
        let prevalidated_event_db = PrevalidatedEventDb::new(&manager);
        let event_request_db = EventRequestDb::new(&manager);
        let request_db = RequestDb::new(&manager);
        let controller_id_db = ControllerIdDb::new(&manager);
        let validation_db = ValidationDb::new(&manager);
        let contract_db = ContractDb::new(&manager);
        let witness_signatures_db = WitnessSignaturesDb::new(&manager);
        let subject_by_governance_db = SubjectByGovernanceDb::new(&manager);
        let transfer_events_db = KeysDb::new(&manager);
        let preauthorized_subjects_and_providers_db =
            PreauthorizedSbujectsAndProovidersDb::new(&manager);
        let lce_validation_proofs_db = LceValidationProofs::new(&manager);
        let approvals_db = ApprovalsDb::new(&manager);
        
        Self {
            signature_db,
            subject_db,
            event_db,
            prevalidated_event_db,
            event_request_db,
            request_db,
            controller_id_db,
            validation_db,
            contract_db,
            witness_signatures_db,
            subject_by_governance_db,
            keys_db: transfer_events_db,
            preauthorized_subjects_and_providers_db,
            lce_validation_proofs_db,
            approvals_db,
        }
    }
}

Example Operations

From core/src/database/db.rs:129-155:
// Get a subject by ID
pub fn get_subject(&self, subject_id: &DigestIdentifier) -> Result<Subject, Error> {
    self.subject_db.get_subject(subject_id)
}

// Store a subject
pub fn set_subject(
    &self,
    subject_id: &DigestIdentifier,
    subject: Subject,
) -> Result<(), Error> {
    self.subject_db.set_subject(subject_id, subject)
}

// Get subjects with pagination
pub fn get_subjects(
    &self,
    from: Option<String>,
    quantity: isize,
) -> Result<Vec<Subject>, Error> {
    self.subject_db.get_subjects(from, quantity)
}

// Delete a subject
pub fn del_subject(&self, subject_id: &DigestIdentifier) -> Result<(), Error> {
    self.subject_db.del_subject(subject_id)
}

Testing Your Implementation

TAPLE provides a test macro to validate database implementations.

Test Macro

From core/src/database/mod.rs:47-203:
#[macro_export]
macro_rules! test_database_manager_trait {
    ($name:ident: $type:ty: $type2:ty) => {
        mod $name {
            use super::*;
            use borsh::{BorshDeserialize, BorshSerialize};

            #[derive(BorshSerialize, BorshDeserialize, Clone, PartialEq, Eq, Debug)]
            struct Data {
                id: usize,
                value: String,
            }

            #[test]
            fn basic_operations_test() {
                let db = <$type>::default();
                let first_collection: $type2 = db.create_collection("first");
                
                // Test PUT operation
                let result = first_collection.put("a", data[0].clone());
                assert!(result.is_ok());
                
                // Test GET operation
                let result = first_collection.get("a");
                assert!(result.is_ok());
                assert_eq!(result.unwrap(), data[0]);
                
                // Test DEL operation
                let result = first_collection.del("a");
                assert!(result.is_ok());
                
                // Verify deletion
                let result = first_collection.get("a");
                assert!(result.is_err());
            }

            #[test]
            fn iterator_test() {
                let db = <$type>::default();
                let first_collection: $type2 = db.create_collection("first");
                
                // Add test data
                build_state(&first_collection);
                
                // Test forward iteration
                let mut iter = first_collection.iter(false, "".to_string());
                for i in 0..3 {
                    let (key, val) = iter.next().unwrap();
                    assert_eq!(keys[i], key);
                    assert_eq!(data[i], val);
                }
                assert!(iter.next().is_none());
            }

            #[test]
            fn rev_iterator_test() {
                // Test reverse iteration
                let mut iter = first_collection.iter(true, "".to_string());
                for i in (0..3).rev() {
                    let (key, val) = iter.next().unwrap();
                    assert_eq!(keys[i], key);
                    assert_eq!(data[i], val);
                }
                assert!(iter.next().is_none());
            }
        }
    };
}
Use the macro to test your implementation:
test_database_manager_trait! {
    unit_test_my_database:MyDatabaseManager:MyCollection
}

Implementation Guide

Step 1: Define Your Types

pub struct MyDatabaseManager {
    // Your internal state
}

pub struct MyCollection {
    // Your collection state
}

Step 2: Implement DatabaseManager

impl DatabaseManager<MyCollection> for MyDatabaseManager {
    fn default() -> Self {
        // Initialize with defaults
        Self { /* ... */ }
    }

    fn create_collection(&self, identifier: &str) -> MyCollection {
        // Create or get collection
        MyCollection { /* ... */ }
    }
}

Step 3: Implement DatabaseCollection

impl DatabaseCollection for MyCollection {
    fn get(&self, key: &str) -> Result<Vec<u8>, Error> {
        // Retrieve value or return Error::EntryNotFound
    }

    fn put(&self, key: &str, data: Vec<u8>) -> Result<(), Error> {
        // Store value
    }

    fn del(&self, key: &str) -> Result<(), Error> {
        // Delete value
    }

    fn iter<'a>(
        &'a self,
        reverse: bool,
        prefix: String,
    ) -> Box<dyn Iterator<Item = (String, Vec<u8>)> + 'a> {
        // Return iterator over matching keys
    }
}

Step 4: Use with TAPLE

let database = MyDatabaseManager::new();
let (node, api) = Node::build(settings, database)?;

Requirements

Thread Safety

  • Both traits require Sync + Send
  • All operations must be thread-safe
  • Use appropriate locking mechanisms (RwLock, Mutex, etc.)

Iterator Constraints

  • Iterators must filter by prefix
  • Support both forward and reverse iteration
  • Must be valid for lifetime 'a
  • Return (String, Vec<u8>) tuples

Error Handling

  • Return Error::EntryNotFound for missing keys
  • Return Error::SerializeError for serialization failures
  • Propagate underlying database errors appropriately
The MemoryManager implementation is suitable only for testing and development. For production use, implement a persistent backend like RocksDB, LevelDB, or SQLite.

Performance Considerations

Optimize Hot Paths

  • get, put, and del are called frequently
  • Iterator performance affects pagination and queries
  • Consider caching for frequently accessed data

Batch Operations

While not in the trait, consider implementing:
  • Batch writes for multiple puts
  • Transactions for atomic updates
  • Bulk delete operations

Storage Layout

TAPLE creates these collections:
  • signatures - Event signatures
  • subjects - Subject state
  • events - Event history
  • contracts - Compiled smart contracts
  • approvals - Approval requests
  • And more (see DB struct)
Plan your storage layout accordingly.

Example: LevelDB Backend

use leveldb::database::Database;
use leveldb::kv::KV;
use leveldb::options::{Options, ReadOptions, WriteOptions};

pub struct LevelDbManager {
    path: PathBuf,
}

pub struct LevelDbCollection {
    db: Arc<Database<i32>>,
}

impl DatabaseManager<LevelDbCollection> for LevelDbManager {
    fn default() -> Self {
        Self {
            path: PathBuf::from("./taple_db"),
        }
    }

    fn create_collection(&self, identifier: &str) -> LevelDbCollection {
        let mut options = Options::new();
        options.create_if_missing = true;
        
        let path = self.path.join(identifier);
        let db = Database::open(&path, options).unwrap();
        
        LevelDbCollection {
            db: Arc::new(db),
        }
    }
}

impl DatabaseCollection for LevelDbCollection {
    fn get(&self, key: &str) -> Result<Vec<u8>, Error> {
        let read_opts = ReadOptions::new();
        self.db
            .get(read_opts, key.as_bytes())
            .map_err(|_| Error::DatabaseError)??
            .ok_or(Error::EntryNotFound)
    }

    fn put(&self, key: &str, data: Vec<u8>) -> Result<(), Error> {
        let write_opts = WriteOptions::new();
        self.db
            .put(write_opts, key.as_bytes(), &data)
            .map_err(|_| Error::DatabaseError)
    }

    fn del(&self, key: &str) -> Result<(), Error> {
        let write_opts = WriteOptions::new();
        self.db
            .delete(write_opts, key.as_bytes())
            .map_err(|_| Error::DatabaseError)
    }

    fn iter<'a>(
        &'a self,
        reverse: bool,
        prefix: String,
    ) -> Box<dyn Iterator<Item = (String, Vec<u8>)> + 'a> {
        // Implement LevelDB iterator with prefix filtering
        todo!()
    }
}

Build docs developers (and LLMs) love