use std::error::Error as StdError;
pub enum Op<'a> {
Put(&'a [u8], &'a [u8]),
Delete(&'a [u8]),
}
pub type KeyValue = (Vec<u8>, Vec<u8>);
pub trait Store: Send + Sync {
type Error: StdError + Send + Sync + 'static;
fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error>;
fn seek(&self, from: &[u8]) -> Result<Option<KeyValue>, Self::Error>;
fn seek_back(&self, upto: &[u8]) -> Result<Option<KeyValue>, Self::Error>;
fn commit(&self, ops: &[Op<'_>], durable: bool) -> Result<(), Self::Error>;
}
pub struct MemStore {
map: crate::sync::Mutex<std::collections::BTreeMap<Vec<u8>, Vec<u8>>>,
}
impl MemStore {
pub fn new() -> Self {
Self {
map: crate::sync::Mutex::new(std::collections::BTreeMap::new()),
}
}
}
impl Default for MemStore {
fn default() -> Self {
Self::new()
}
}
impl Store for MemStore {
type Error = std::convert::Infallible;
fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
Ok(self.map.lock().unwrap().get(key).cloned())
}
fn seek(&self, from: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
Ok(self
.map
.lock()
.unwrap()
.range(from.to_vec()..)
.next()
.map(|(k, v)| (k.clone(), v.clone())))
}
fn seek_back(&self, upto: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
Ok(self
.map
.lock()
.unwrap()
.range(..=upto.to_vec())
.next_back()
.map(|(k, v)| (k.clone(), v.clone())))
}
fn commit(&self, ops: &[Op<'_>], _durable: bool) -> Result<(), Self::Error> {
let mut map = self.map.lock().unwrap();
for op in ops {
match op {
Op::Put(k, v) => {
map.insert(k.to_vec(), v.to_vec());
}
Op::Delete(k) => {
map.remove(*k);
}
}
}
Ok(())
}
}
#[cfg(feature = "sled")]
pub struct SledStore {
db: sled::Db,
}
#[cfg(feature = "sled")]
impl SledStore {
pub fn open(path: impl AsRef<std::path::Path>) -> sled::Result<Self> {
Ok(Self {
db: sled::open(path)?,
})
}
pub fn from_db(db: sled::Db) -> Self {
Self { db }
}
}
#[cfg(feature = "sled")]
impl Store for SledStore {
type Error = sled::Error;
fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
Ok(self.db.get(key)?.map(|v| v.to_vec()))
}
fn seek(&self, from: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
match self.db.range(from.to_vec()..).next() {
Some(r) => {
let (k, v) = r?;
Ok(Some((k.to_vec(), v.to_vec())))
}
None => Ok(None),
}
}
fn seek_back(&self, upto: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
match self.db.range(..=upto.to_vec()).next_back() {
Some(r) => {
let (k, v) = r?;
Ok(Some((k.to_vec(), v.to_vec())))
}
None => Ok(None),
}
}
fn commit(&self, ops: &[Op<'_>], durable: bool) -> Result<(), Self::Error> {
let mut batch = sled::Batch::default();
for op in ops {
match op {
Op::Put(k, v) => batch.insert(*k, *v),
Op::Delete(k) => batch.remove(*k),
}
}
self.db.apply_batch(batch)?;
if durable {
self.db.flush()?;
}
Ok(())
}
}
#[cfg(feature = "redb")]
const REDB_TABLE: redb::TableDefinition<'static, &[u8], &[u8]> =
redb::TableDefinition::new("entries");
#[cfg(feature = "redb")]
pub struct RedbStore {
db: redb::Database,
}
#[cfg(feature = "redb")]
impl RedbStore {
#[allow(clippy::result_large_err)]
pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, redb::Error> {
let db = redb::Database::create(path)?;
let wtx = db.begin_write()?;
wtx.open_table(REDB_TABLE)?;
wtx.commit()?;
Ok(Self { db })
}
pub fn from_db(db: redb::Database) -> Self {
Self { db }
}
}
#[cfg(feature = "redb")]
impl Store for RedbStore {
type Error = redb::Error;
fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
let rtx = self.db.begin_read()?;
let table = rtx.open_table(REDB_TABLE)?;
Ok(table.get(key)?.map(|g| g.value().to_vec()))
}
fn seek(&self, from: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
let rtx = self.db.begin_read()?;
let table = rtx.open_table(REDB_TABLE)?;
match table.range::<&[u8]>(from..)?.next() {
Some(r) => {
let (k, v) = r?;
Ok(Some((k.value().to_vec(), v.value().to_vec())))
}
None => Ok(None),
}
}
fn seek_back(&self, upto: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
let rtx = self.db.begin_read()?;
let table = rtx.open_table(REDB_TABLE)?;
match table.range::<&[u8]>(..=upto)?.next_back() {
Some(r) => {
let (k, v) = r?;
Ok(Some((k.value().to_vec(), v.value().to_vec())))
}
None => Ok(None),
}
}
fn commit(&self, ops: &[Op<'_>], durable: bool) -> Result<(), Self::Error> {
let mut wtx = self.db.begin_write()?;
if !durable {
wtx.set_durability(redb::Durability::None);
}
{
let mut table = wtx.open_table(REDB_TABLE)?;
for op in ops {
match op {
Op::Put(k, v) => {
table.insert(*k, *v)?;
}
Op::Delete(k) => {
table.remove(*k)?;
}
}
}
}
wtx.commit()?;
Ok(())
}
}
#[cfg(feature = "rocksdb")]
pub struct RocksStore {
db: rocksdb::DB,
}
#[cfg(feature = "rocksdb")]
impl RocksStore {
pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, rocksdb::Error> {
Ok(Self {
db: rocksdb::DB::open_default(path)?,
})
}
pub fn from_db(db: rocksdb::DB) -> Self {
Self { db }
}
}
#[cfg(feature = "rocksdb")]
impl Store for RocksStore {
type Error = rocksdb::Error;
fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
self.db.get(key)
}
fn seek(&self, from: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
let mut iter = self.db.iterator(rocksdb::IteratorMode::From(
from,
rocksdb::Direction::Forward,
));
match iter.next() {
Some(Ok((k, v))) => Ok(Some((k.to_vec(), v.to_vec()))),
Some(Err(e)) => Err(e),
None => Ok(None),
}
}
fn seek_back(&self, upto: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
let mut iter = self.db.iterator(rocksdb::IteratorMode::From(
upto,
rocksdb::Direction::Reverse,
));
match iter.next() {
Some(Ok((k, v))) => Ok(Some((k.to_vec(), v.to_vec()))),
Some(Err(e)) => Err(e),
None => Ok(None),
}
}
fn commit(&self, ops: &[Op<'_>], durable: bool) -> Result<(), Self::Error> {
let mut batch = rocksdb::WriteBatch::default();
for op in ops {
match op {
Op::Put(k, v) => batch.put(*k, *v),
Op::Delete(k) => batch.delete(*k),
}
}
let mut opts = rocksdb::WriteOptions::default();
opts.set_sync(durable);
self.db.write_opt(batch, &opts)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mem_store_contract() {
contract(MemStore::new());
}
#[cfg(feature = "sled")]
#[test]
fn sled_store_contract() {
let dir = tempfile::tempdir().unwrap();
contract(SledStore::open(dir.path().join("db")).unwrap());
}
#[cfg(feature = "redb")]
#[test]
fn redb_store_contract() {
let dir = tempfile::tempdir().unwrap();
contract(RedbStore::open(dir.path().join("db.redb")).unwrap());
}
#[cfg(feature = "rocksdb")]
#[test]
fn rocksdb_store_contract() {
let dir = tempfile::tempdir().unwrap();
contract(RocksStore::open(dir.path().join("db")).unwrap());
}
fn contract<S: Store>(store: S) {
assert!(store.get(b"missing").unwrap().is_none());
assert!(store.seek(b"a").unwrap().is_none());
store
.commit(
&[
Op::Put(b"b", b"2"),
Op::Put(b"a", b"1"),
Op::Put(b"c", b"3"),
],
true,
)
.unwrap();
assert_eq!(store.get(b"a").unwrap().as_deref(), Some(&b"1"[..]));
assert_eq!(store.get(b"z").unwrap(), None);
let (k, v) = store.seek(b"a").unwrap().unwrap();
assert_eq!((k.as_slice(), v.as_slice()), (&b"a"[..], &b"1"[..]));
assert_eq!(store.seek(b"aa").unwrap().unwrap().0.as_slice(), b"b");
assert_eq!(store.seek_back(b"bz").unwrap().unwrap().0.as_slice(), b"b");
assert_eq!(
store.seek_back(b"\xff").unwrap().unwrap().0.as_slice(),
b"c"
);
store.commit(&[Op::Delete(b"b")], true).unwrap();
assert_eq!(store.get(b"b").unwrap(), None);
assert_eq!(store.seek(b"b").unwrap().unwrap().0.as_slice(), b"c");
}
}