Redbit reads struct annotations and derives code necessary for persisting and querying structured data into/from redb using secondary indexes and dictionaries, let's say we want to persist Utxo into Redb using Redbit :
- âś… Achieving more advanced querying capabilities with embedded KV stores is non-trivial
- âś… Absence of any existing abstraction layer for structured data
- âś… Handwriting custom codecs on byte-level is tedious and painful
- âś… Querying and ranging by secondary index
- âś… Optional dictionaries for low cardinality fields
- âś… One-to-One and One-to-Many entities with cascade read/write/delete
- âś… All goodies including intuitive data ordering without writing custom codecs
Declare annotated Struct examples/utxo/src/lib.rs
:
mod data;
pub use data::*;
pub use redbit::*;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
pub type Amount = u64;
pub type Timestamp = u64;
pub type Height = u32;
pub type TxIndex = u16;
pub type UtxoIndex = u16;
pub type AssetIndex = u16;
pub type Datum = String;
pub type Address = String;
pub type AssetName = String;
pub type PolicyId = String;
pub type Hash = String;
#[derive(Entity, Debug, Clone, PartialEq, Eq)]
pub struct Block {
#[pk(range)]
pub id: BlockPointer,
#[one2one]
pub header: BlockHeader,
#[one2many]
pub transactions: Vec<Transaction>,
}
#[derive(Entity, Debug, Clone, PartialEq, Eq)]
pub struct BlockHeader {
#[pk(range)]
pub id: BlockPointer,
#[column(index)]
pub hash: Hash,
#[column(index, range)]
pub timestamp: Timestamp,
#[column(index)]
pub merkle_root: Hash,
#[column]
pub nonce: u32,
}
#[derive(Entity, Debug, Clone, PartialEq, Eq)]
pub struct Transaction {
#[pk(range)]
pub id: TxPointer,
#[column(index)]
pub hash: Hash,
#[one2many]
pub utxos: Vec<Utxo>,
}
#[derive(Entity, Debug, Clone, PartialEq, Eq)]
pub struct Utxo {
#[pk(range)]
pub id: UtxoPointer,
#[column]
pub amount: Amount,
#[column(index)]
pub datum: Datum,
#[column(index, dictionary)]
pub address: Address,
#[one2many]
pub assets: Vec<Asset>,
}
#[derive(Entity, Debug, Clone, PartialEq, Eq)]
pub struct Asset {
#[pk(range)]
pub id: AssetPointer,
#[column]
pub amount: Amount,
#[column(index, dictionary)]
pub name: AssetName,
#[column(index, dictionary)]
pub policy_id: PolicyId,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct BlockPointer {
pub height: Height,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct TxPointer {
pub block_pointer: BlockPointer,
pub tx_index: TxIndex,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct UtxoPointer {
pub tx_pointer: TxPointer,
pub utxo_index: UtxoIndex,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct AssetPointer {
pub utxo_pointer: UtxoPointer,
pub asset_index: AssetIndex,
}
impl PK<TxPointer> for BlockPointer {
fn fk_range(&self) -> (TxPointer, TxPointer) {
(TxPointer { block_pointer: self.clone(), tx_index: TxIndex::MIN }, TxPointer { block_pointer: self.next(), tx_index: TxIndex::MIN })
}
fn next(&self) -> Self {
BlockPointer{ height: self.height + 1 }
}
}
impl PK<UtxoPointer> for TxPointer {
fn fk_range(&self) -> (UtxoPointer, UtxoPointer) {
(UtxoPointer { tx_pointer: self.clone(), utxo_index: UtxoIndex::MIN }, UtxoPointer { tx_pointer: self.next(), utxo_index: UtxoIndex::MIN })
}
fn next(&self) -> Self {
TxPointer { block_pointer: self.block_pointer.clone(), tx_index: self.tx_index+1 }
}
}
impl PK<AssetPointer> for UtxoPointer {
fn fk_range(&self) -> (AssetPointer, AssetPointer) {
(AssetPointer { utxo_pointer: self.clone(), asset_index: AssetIndex::MIN }, AssetPointer { utxo_pointer: self.next(), asset_index: AssetIndex::MIN })
}
fn next(&self) -> Self {
UtxoPointer { tx_pointer: self.tx_pointer.clone(), utxo_index: self.utxo_index+1 }
}
}
And R/W entire instances efficiently using indexes and dictionaries examples/utxo/src/main.rs
:
use utxo::*;
fn demo() -> Result<(), DbEngineError> {
let db = redb::Database::create(std::env::temp_dir().join("my_db.redb"))?;
let blocks = get_blocks(10, 5, 5, 5);
println!("Persisting blocks:");
for block in blocks.iter() {
Block::store_and_commit(&db, block)?
}
let read_tx = db.begin_read()?;
println!("Querying blocks:");
let first_block = Block::first(&read_tx)?.unwrap();
let last_block = Block::last(&read_tx)?.unwrap();
Block::all(&read_tx)?;
Block::get(&read_tx, &first_block.id)?;
Block::range(&read_tx, &first_block.id, &last_block.id)?;
Block::get_transactions(&read_tx, &first_block.id)?;
Block::get_header(&read_tx, &first_block.id)?;
println!("Querying block headers:");
let first_block_header = BlockHeader::first(&read_tx)?.unwrap();
let last_block_header = BlockHeader::last(&read_tx)?.unwrap();
BlockHeader::all(&read_tx)?;
BlockHeader::get(&read_tx, &first_block_header.id)?;
BlockHeader::range(&read_tx, &first_block_header.id, &last_block_header.id)?;
BlockHeader::range_by_timestamp(&read_tx, &first_block_header.timestamp, &last_block_header.timestamp)?;
BlockHeader::get_by_hash(&read_tx, &first_block_header.hash)?;
BlockHeader::get_by_timestamp(&read_tx, &first_block_header.timestamp)?;
BlockHeader::get_by_merkle_root(&read_tx, &first_block_header.merkle_root)?;
println!("Querying transactions:");
let first_transaction = Transaction::first(&read_tx)?.unwrap();
let last_transaction = Transaction::last(&read_tx)?.unwrap();
Transaction::all(&read_tx)?;
Transaction::get(&read_tx, &first_transaction.id)?;
Transaction::get_by_hash(&read_tx, &first_transaction.hash)?;
Transaction::range(&read_tx, &first_transaction.id, &last_transaction.id)?;
Transaction::get_utxos(&read_tx, &first_transaction.id)?;
println!("Querying utxos:");
let first_utxo = Utxo::first(&read_tx)?.unwrap();
let last_utxo = Utxo::last(&read_tx)?.unwrap();
Utxo::all(&read_tx)?;
Utxo::get(&read_tx, &first_utxo.id)?;
Utxo::get_by_address(&read_tx, &first_utxo.address)?;
Utxo::get_by_datum(&read_tx, &first_utxo.datum)?;
Utxo::range(&read_tx, &first_utxo.id, &last_utxo.id)?;
Utxo::get_assets(&read_tx, &first_utxo.id)?;
println!("Querying assets:");
let first_asset = Asset::first(&read_tx)?.unwrap();
let last_asset = Asset::last(&read_tx)?.unwrap();
Asset::all(&read_tx)?;
Asset::get(&read_tx, &first_asset.id)?;
Asset::get_by_name(&read_tx, &first_asset.name)?;
Asset::get_by_policy_id(&read_tx, &first_asset.policy_id)?;
Asset::range(&read_tx, &first_asset.id, &last_asset.id)?;
println!("Deleting blocks:");
for block in blocks.iter() {
Block::delete_and_commit(&db, &block.id)?
}
Ok(())
}
fn main() {
demo().unwrap()
}