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
Performance wise, check flamegraph. Instances are persisted completely structured by fields which means Redbit has slower write performance but blazing fast reads.
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 Nonce = u32;
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: Nonce,
}
#[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(PK, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct BlockPointer {
pub height: Height,
}
#[derive(PK, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct TxPointer {
#[parent]
pub block_pointer: BlockPointer,
pub tx_index: TxIndex,
}
#[derive(PK, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct UtxoPointer {
#[parent]
pub tx_pointer: TxPointer,
pub utxo_index: UtxoIndex,
}
#[derive(PK, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct AssetPointer {
#[parent]
pub utxo_pointer: UtxoPointer,
pub asset_index: AssetIndex,
}
And R/W entire instances efficiently using indexes and dictionaries examples/utxo/src/main.rs
:
use std::fs::File;
use pprof::ProfilerGuard;
use utxo::*;
fn demo() -> Result<(), DbEngineError> {
let db = redb::Database::create(std::env::temp_dir().join("my_db.redb"))?;
let blocks = get_blocks(10, 10, 20, 3);
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() {
let guard = ProfilerGuard::new(100).unwrap();
demo().unwrap();
if let Ok(report) = guard.report().build() {
let mut file = File::create(std::env::temp_dir().join("flamegraph.svg")).unwrap();
report.flamegraph(&mut file).unwrap();
println!("Flamegraph written to flamegraph.svg");
}
}