Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: production ready borsh(with schema) example #817

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ redb1 = { version = "=1.0.0", package = "redb" }
redb2 = { version = "=2.0.0", package = "redb" }
serde = { version = "1.0", features = ["derive"] }
bincode = "1.3.3"
borsh = { version = "1.5.1", features = ["std", "unstable__schema"] }

# Just benchmarking dependencies
[target.'cfg(not(target_os = "wasi"))'.dev-dependencies]
Expand Down
178 changes: 178 additions & 0 deletions examples/borsh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//! Example of making any Borsh type to be Value or Key using BorshSchema

use core::fmt::Debug;
use core::{cmp::Ordering, ops::RangeInclusive};

use borsh::{
schema::{BorshSchemaContainer, Declaration},
BorshDeserialize, BorshSchema, BorshSerialize,
};
use redb::{Database, Error, Key, Range, TableDefinition, TypeName, Value};

#[derive(Debug, BorshDeserialize, BorshSerialize, BorshSchema, PartialEq, Eq, PartialOrd, Ord)]
struct SomeKey {
foo: String,
bar: i32,
}

#[derive(Debug, BorshDeserialize, BorshSerialize, BorshSchema, PartialEq)]
struct SomeValue {
foo: [f64; 3],
bar: bool,
}

const TABLE: TableDefinition<Borsh<SomeKey>, Borsh<SomeValue>> = TableDefinition::new("my_data");

fn main() -> Result<(), Error> {
let some_key = SomeKey {
foo: "hello world".to_string(),
bar: 42,
};
let some_value = SomeValue {
foo: [1., 2., 3.],
bar: true,
};
let lower = SomeKey {
foo: "a".to_string(),
bar: 42,
};
let upper = SomeKey {
foo: "z".to_string(),
bar: 42,
};

let db = Database::create("borsh.redb")?;
let write_txn = db.begin_write()?;
{
let mut table = write_txn.open_table(TABLE)?;

table.insert(&some_key, &some_value).unwrap();
}
write_txn.commit()?;

let read_txn = db.begin_read()?;
let table = read_txn.open_table(TABLE)?;

let mut iter: Range<Borsh<SomeKey>, Borsh<SomeValue>> = table.range(lower..upper).unwrap();
assert_eq!(iter.next().unwrap().unwrap().1.value(), some_value);
assert!(iter.next().is_none());

Ok(())
}

/// Wrapper type to handle keys and values using Borsh serialization
#[derive(Debug)]
pub struct Borsh<T>(pub T);

impl<'c, T> Value for Borsh<T>
where
T: Debug + BorshDeserialize + BorshSchema + BorshSerialize + 'c,
{
type SelfType<'a> = T
where
Self: 'a;

type AsBytes<'a> = Vec<u8>
where
Self: 'a;

/// Width of a fixed type, or None for variable width.
/// Uses Borsh schema to determine if the type is fixed width.
fn fixed_width<'a>() -> Option<usize> {
let schema = BorshSchemaContainer::for_type::<Self::SelfType<'c>>();
is_fixed_width(&schema, schema.declaration())
}

fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a>
where
Self: 'a,
{
BorshDeserialize::try_from_slice(data).unwrap()
}

fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a>
where
Self: 'a,
Self: 'b,
{
let mut writer = Vec::new();
value.serialize(&mut writer).unwrap();
writer
}

/// Globally unique identifier for this type.
/// Returns the type name Borsh schema declaration for `T`.
fn type_name() -> TypeName {
let schema = BorshSchemaContainer::for_type::<Self::SelfType<'c>>();
redb::TypeName::new(schema.declaration())
}
}

fn is_fixed_width(schema: &BorshSchemaContainer, declaration: &Declaration) -> Option<usize> {
use borsh::schema::Definition::*;
match schema
.get_definition(declaration)
.expect("must have definition")
{
Primitive(size) => Some(*size as usize),
Struct { fields } => match fields {
borsh::schema::Fields::NamedFields(fields) => {
are_fixed_width(schema, fields.iter().map(|(_, field)| field))
}
borsh::schema::Fields::UnnamedFields(fields) => are_fixed_width(schema, fields),
borsh::schema::Fields::Empty => Some(0),
},
// fixed sequence
Sequence {
length_width,
length_range,
elements,
} if is_fixed_width_sequence(length_range, length_width) => {
let fixed_witdh = is_fixed_width(schema, elements);
fixed_witdh.map(|width| width * *length_range.end() as usize)
}
Tuple { elements } => are_fixed_width(schema, elements),
Enum {
tag_width,
variants,
} => {
let max_width =
are_fixed_width(schema, variants.iter().map(|(_, _, element)| element))?;
Some(max_width + *tag_width as usize)
}
_ => None,
}
}

fn is_fixed_width_sequence(length_range: &RangeInclusive<u64>, length_width: &u8) -> bool {
length_range.end() == length_range.start() && *length_width == 0
}

fn are_fixed_width<'a>(
schema: &BorshSchemaContainer,
elements: impl IntoIterator<Item = &'a String>,
) -> Option<usize> {
let mut width = 0;
for element in elements {
if let Some(element_width) = is_fixed_width(schema, element) {
width += element_width;
} else {
return None;
}
}
Some(width)
}

impl<'c, T> Key for Borsh<T>
where
T: Debug + BorshDeserialize + Ord + BorshSerialize + BorshSchema + 'c,
{
/// Panics in case of deserialization error.
/// Data integrity should be guaranteed by the database
/// and data migration should be handled properly.
fn compare(mut data1: &[u8], mut data2: &[u8]) -> Ordering {
Self::SelfType::<'c>::deserialize(&mut data1)
.expect("valid data")
.cmp(&Self::SelfType::<'c>::deserialize(&mut data2).expect("valid data"))
}
}
1 change: 0 additions & 1 deletion rust-toolchain

This file was deleted.

2 changes: 2 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[toolchain]
channel = "1.67.0"
23 changes: 23 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# `nix-shell` to get examples and tests runnable without any manual rust or c-dependnecies installation on linux and mac
{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-24.05.tar.gz") { } }:
let
fenix = import (fetchTarball "https://github.com/nix-community/fenix/archive/main.tar.gz") { };
in
pkgs.mkShell rec {
name = "redb";
nativeBuildInputs = [
(fenix.fromToolchainFile { dir = ./.; })
pkgs.clang
] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.pkg-config ];

buildInputs = with pkgs; [
libclang.lib
rocksdb
] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
darwin.apple_sdk.frameworks.SystemConfiguration
];
shellHook = ''
export LIBCLANG_PATH="${pkgs.libclang.lib}/lib";
export ROCKSDB_LIB_DIR="${pkgs.rocksdb}/lib";
'';
}