use std::cell::RefCell;
use std::collections::HashMap;
use std::ops::Bound;
use std::path::Path;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use redb::ReadableDatabase as _;
use redb::{Database, Durability, ReadableTable, TableDefinition, TableHandle};
use crate::error::TalaDbError;
pub type KvPairs = Vec<(Vec<u8>, Vec<u8>)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScanFlow {
Continue,
Stop,
}
pub type ScanFn<'f> = &'f mut dyn FnMut(&[u8], &[u8]) -> Result<ScanFlow, TalaDbError>;
pub enum KvOp<'a> {
Put(&'a [u8], &'a [u8]),
Delete(&'a [u8]),
}
pub trait StorageBackend: Send + Sync {
fn begin_write(&self) -> Result<Box<dyn WriteTxn + '_>, TalaDbError>;
fn begin_read(&self) -> Result<Box<dyn ReadTxn + '_>, TalaDbError>;
fn compact(&self) -> Result<(), TalaDbError> {
Ok(())
}
fn set_durability(&self, _eventual: bool) {}
fn flush(&self) -> Result<(), TalaDbError> {
Ok(())
}
}
pub trait WriteTxn {
fn put(&mut self, table: &str, key: &[u8], value: &[u8]) -> Result<(), TalaDbError>;
fn delete(&mut self, table: &str, key: &[u8]) -> Result<Option<Vec<u8>>, TalaDbError>;
fn get(&self, table: &str, key: &[u8]) -> Result<Option<Vec<u8>>, TalaDbError>;
fn range(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
) -> Result<KvPairs, TalaDbError>;
fn scan(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
f: ScanFn<'_>,
) -> Result<(), TalaDbError> {
for (k, v) in self.range(table, start, end)? {
if f(&k, &v)? == ScanFlow::Stop {
break;
}
}
Ok(())
}
fn commit(self: Box<Self>) -> Result<(), TalaDbError>;
fn list_tables(&self) -> Result<Vec<String>, TalaDbError>;
fn delete_table(&mut self, table: &str) -> Result<bool, TalaDbError>;
fn count_entries(&self, table: &str) -> Result<u64, TalaDbError> {
Ok(self.range(table, Bound::Unbounded, Bound::Unbounded)?.len() as u64)
}
fn get_many(&self, table: &str, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, TalaDbError> {
keys.iter().map(|k| self.get(table, k)).collect()
}
fn apply_batch(&mut self, table: &str, ops: &[KvOp<'_>]) -> Result<(), TalaDbError> {
for op in ops {
match op {
KvOp::Put(k, v) => self.put(table, k, v)?,
KvOp::Delete(k) => {
self.delete(table, k)?;
}
}
}
Ok(())
}
}
pub trait ReadTxn {
fn get(&self, table: &str, key: &[u8]) -> Result<Option<Vec<u8>>, TalaDbError>;
fn range(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
) -> Result<KvPairs, TalaDbError>;
fn scan_all(&self, table: &str) -> Result<KvPairs, TalaDbError>;
fn list_tables(&self) -> Result<Vec<String>, TalaDbError>;
fn count_entries(&self, table: &str) -> Result<u64, TalaDbError>;
fn get_many(&self, table: &str, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, TalaDbError> {
keys.iter().map(|k| self.get(table, k)).collect()
}
fn scan(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
f: ScanFn<'_>,
) -> Result<(), TalaDbError> {
for (k, v) in self.range(table, start, end)? {
if f(&k, &v)? == ScanFlow::Stop {
break;
}
}
Ok(())
}
}
pub(crate) struct WriteView<'a>(pub &'a dyn WriteTxn);
impl ReadTxn for WriteView<'_> {
fn get(&self, table: &str, key: &[u8]) -> Result<Option<Vec<u8>>, TalaDbError> {
self.0.get(table, key)
}
fn range(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
) -> Result<KvPairs, TalaDbError> {
self.0.range(table, start, end)
}
fn scan(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
f: ScanFn<'_>,
) -> Result<(), TalaDbError> {
self.0.scan(table, start, end, f)
}
fn scan_all(&self, table: &str) -> Result<KvPairs, TalaDbError> {
self.0.range(table, Bound::Unbounded, Bound::Unbounded)
}
fn list_tables(&self) -> Result<Vec<String>, TalaDbError> {
self.0.list_tables()
}
fn count_entries(&self, table: &str) -> Result<u64, TalaDbError> {
self.0.count_entries(table)
}
fn get_many(&self, table: &str, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, TalaDbError> {
self.0.get_many(table, keys)
}
}
const REVISION_TABLE: &str = "meta::revisions";
pub(crate) fn revision(txn: &dyn ReadTxn, key: &str) -> Result<u64, TalaDbError> {
match txn.get(REVISION_TABLE, key.as_bytes())? {
None => Ok(0),
Some(bytes) => {
Ok(u64::from_le_bytes(bytes.try_into().map_err(|_| {
TalaDbError::Storage("invalid revision counter".into())
})?))
}
}
}
pub(crate) fn bump_revision(txn: &mut dyn WriteTxn, key: &str) -> Result<u64, TalaDbError> {
let next = revision(&WriteView(txn), key)?
.checked_add(1)
.ok_or_else(|| TalaDbError::Storage("revision counter exhausted".into()))?;
txn.put(REVISION_TABLE, key.as_bytes(), &next.to_le_bytes())?;
Ok(next)
}
pub struct RedbBackend {
db: Arc<Mutex<Database>>,
eventual: AtomicBool,
}
impl RedbBackend {
pub fn open(path: &Path) -> Result<Self, TalaDbError> {
#[cfg(all(feature = "legacy-migration", not(target_arch = "wasm32")))]
crate::migrate::migrate_file_if_needed(path)?;
let db = Database::create(path).map_err(|e| {
let msg = e.to_string();
if crate::migrate::is_legacy_format_error(&msg) {
TalaDbError::Storage(crate::migrate::LEGACY_BACKEND_HELP.to_string())
} else {
TalaDbError::from(e)
}
})?;
Ok(Self {
db: Arc::new(Mutex::new(db)),
eventual: AtomicBool::new(false),
})
}
pub fn open_in_memory() -> Result<Self, TalaDbError> {
let db = Database::builder().create_with_backend(redb::backends::InMemoryBackend::new())?;
Ok(Self {
db: Arc::new(Mutex::new(db)),
eventual: AtomicBool::new(false),
})
}
pub fn open_with_redb_backend<B: redb::StorageBackend + 'static>(
backend: B,
) -> Result<Self, TalaDbError> {
let db = Database::builder()
.create_with_backend(backend)
.map_err(|e| {
let msg = e.to_string();
if crate::migrate::is_legacy_format_error(&msg) {
TalaDbError::Storage(crate::migrate::LEGACY_BACKEND_HELP.to_string())
} else {
TalaDbError::from(e)
}
})?;
Ok(Self {
db: Arc::new(Mutex::new(db)),
eventual: AtomicBool::new(false),
})
}
}
impl StorageBackend for RedbBackend {
fn begin_write(&self) -> Result<Box<dyn WriteTxn + '_>, TalaDbError> {
let mut txn = self
.db
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.begin_write()?;
let wanted = if self.eventual.load(Ordering::Relaxed) {
Durability::None
} else {
Durability::Immediate
};
txn.set_durability(wanted)
.map_err(|e| TalaDbError::Storage(format!("could not set write durability: {e}")))?;
Ok(Box::new(RedbWriteTxn { txn }))
}
fn begin_read(&self) -> Result<Box<dyn ReadTxn + '_>, TalaDbError> {
let txn = self
.db
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.begin_read()?;
Ok(Box::new(RedbReadTxn {
txn,
open: RefCell::new(HashMap::new()),
}))
}
fn compact(&self) -> Result<(), TalaDbError> {
self.db
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.compact()
.map_err(|e| TalaDbError::Storage(e.to_string()))?;
Ok(())
}
fn set_durability(&self, eventual: bool) {
self.eventual.store(eventual, Ordering::Relaxed);
}
fn flush(&self) -> Result<(), TalaDbError> {
let mut txn = self
.db
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.begin_write()?;
txn.set_durability(Durability::Immediate)
.map_err(|e| TalaDbError::Storage(format!("could not force durable flush: {e}")))?;
txn.commit()?;
Ok(())
}
}
struct RedbWriteTxn {
txn: redb::WriteTransaction,
}
const MAX_INTERNED_NAMES: usize = 4096;
fn intern_name(name: &str) -> Result<&'static str, TalaDbError> {
use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
static INTERNED: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
let set = INTERNED.get_or_init(|| Mutex::new(HashSet::new()));
let mut guard = set.lock().unwrap_or_else(|p| {
tracing::warn!("intern_name: mutex was poisoned; recovering");
p.into_inner()
});
if let Some(&existing) = guard.get(name) {
return Ok(existing);
}
if guard.len() >= MAX_INTERNED_NAMES {
return Err(TalaDbError::InvalidOperation(format!(
"exceeded {MAX_INTERNED_NAMES} interned table names; avoid dynamically \
generating unbounded collection or index names"
)));
}
let leaked: &'static str = Box::leak(name.to_string().into_boxed_str());
guard.insert(leaked);
Ok(leaked)
}
fn table_def(
name: &str,
) -> Result<TableDefinition<'static, &'static [u8], &'static [u8]>, TalaDbError> {
Ok(TableDefinition::new(intern_name_cached(name)?))
}
thread_local! {
static LOCAL_NAMES: RefCell<HashMap<Box<str>, &'static str>> =
RefCell::new(HashMap::new());
}
fn intern_name_cached(name: &str) -> Result<&'static str, TalaDbError> {
if let Some(hit) = LOCAL_NAMES.with(|c| c.borrow().get(name).copied()) {
return Ok(hit);
}
let interned = intern_name(name)?;
LOCAL_NAMES.with(|c| c.borrow_mut().insert(name.into(), interned));
Ok(interned)
}
impl WriteTxn for RedbWriteTxn {
fn count_entries(&self, table: &str) -> Result<u64, TalaDbError> {
use redb::ReadableTableMetadata;
match self.txn.open_table(table_def(table)?) {
Ok(tbl) => Ok(tbl.len()?),
Err(redb::TableError::TableDoesNotExist(_)) => Ok(0),
Err(e) => Err(e.into()),
}
}
fn put(&mut self, table: &str, key: &[u8], value: &[u8]) -> Result<(), TalaDbError> {
let mut tbl = self.txn.open_table(table_def(table)?)?;
tbl.insert(key, value)?;
Ok(())
}
fn delete(&mut self, table: &str, key: &[u8]) -> Result<Option<Vec<u8>>, TalaDbError> {
let mut tbl = self.txn.open_table(table_def(table)?)?;
let old = tbl.remove(key)?.map(|v| v.value().to_vec());
Ok(old)
}
fn get(&self, table: &str, key: &[u8]) -> Result<Option<Vec<u8>>, TalaDbError> {
match self.txn.open_table(table_def(table)?) {
Ok(tbl) => Ok(tbl.get(key)?.map(|v| v.value().to_vec())),
Err(redb::TableError::TableDoesNotExist(_)) => Ok(None),
Err(e) => Err(e.into()),
}
}
fn range(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
) -> Result<KvPairs, TalaDbError> {
match self.txn.open_table(table_def(table)?) {
Ok(tbl) => {
let iter = tbl.range::<&[u8]>((start, end))?;
let mut out = Vec::new();
for entry in iter {
let (k, v) = entry?;
out.push((k.value().to_vec(), v.value().to_vec()));
}
Ok(out)
}
Err(redb::TableError::TableDoesNotExist(_)) => Ok(vec![]),
Err(e) => Err(e.into()),
}
}
fn scan(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
f: ScanFn<'_>,
) -> Result<(), TalaDbError> {
let tbl = match self.txn.open_table(table_def(table)?) {
Ok(tbl) => tbl,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(()),
Err(e) => return Err(e.into()),
};
for entry in tbl.range::<&[u8]>((start, end))? {
let (k, v) = entry?;
if f(k.value(), v.value())? == ScanFlow::Stop {
break;
}
}
Ok(())
}
fn commit(self: Box<Self>) -> Result<(), TalaDbError> {
self.txn.commit()?;
Ok(())
}
fn list_tables(&self) -> Result<Vec<String>, TalaDbError> {
Ok(self
.txn
.list_tables()?
.map(|t| t.name().to_string())
.collect())
}
fn delete_table(&mut self, table: &str) -> Result<bool, TalaDbError> {
Ok(self.txn.delete_table(table_def(table)?)?)
}
fn get_many(&self, table: &str, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, TalaDbError> {
match self.txn.open_table(table_def(table)?) {
Ok(tbl) => {
let mut out = Vec::with_capacity(keys.len());
for key in keys {
out.push(tbl.get(*key)?.map(|v| v.value().to_vec()));
}
Ok(out)
}
Err(redb::TableError::TableDoesNotExist(_)) => Ok(vec![None; keys.len()]),
Err(e) => Err(e.into()),
}
}
fn apply_batch(&mut self, table: &str, ops: &[KvOp<'_>]) -> Result<(), TalaDbError> {
if ops.is_empty() {
return Ok(());
}
let mut tbl = self.txn.open_table(table_def(table)?)?;
for op in ops {
match op {
KvOp::Put(k, v) => {
tbl.insert(*k, *v)?;
}
KvOp::Delete(k) => {
tbl.remove(*k)?;
}
}
}
Ok(())
}
}
type RedbTable = redb::ReadOnlyTable<&'static [u8], &'static [u8]>;
struct RedbReadTxn {
txn: redb::ReadTransaction,
open: RefCell<HashMap<Box<str>, Option<Rc<RedbTable>>>>,
}
impl RedbReadTxn {
fn table(&self, name: &str) -> Result<Option<Rc<RedbTable>>, TalaDbError> {
if let Some(hit) = self.open.borrow().get(name) {
return Ok(hit.clone());
}
let opened = match self.txn.open_table(table_def(name)?) {
Ok(tbl) => Some(Rc::new(tbl)),
Err(redb::TableError::TableDoesNotExist(_)) => None,
Err(e) => return Err(e.into()),
};
self.open.borrow_mut().insert(name.into(), opened.clone());
Ok(opened)
}
}
impl ReadTxn for RedbReadTxn {
fn list_tables(&self) -> Result<Vec<String>, TalaDbError> {
let names = self
.txn
.list_tables()?
.map(|t| t.name().to_string())
.collect();
Ok(names)
}
fn get(&self, table: &str, key: &[u8]) -> Result<Option<Vec<u8>>, TalaDbError> {
match self.table(table)? {
Some(tbl) => Ok(tbl.get(key)?.map(|v| v.value().to_vec())),
None => Ok(None),
}
}
fn get_many(&self, table: &str, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, TalaDbError> {
let Some(tbl) = self.table(table)? else {
return Ok(vec![None; keys.len()]);
};
let mut out = Vec::with_capacity(keys.len());
for key in keys {
out.push(tbl.get(*key)?.map(|v| v.value().to_vec()));
}
Ok(out)
}
fn range(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
) -> Result<KvPairs, TalaDbError> {
let Some(tbl) = self.table(table)? else {
return Ok(vec![]);
};
let mut out = Vec::new();
for entry in tbl.range::<&[u8]>((start, end))? {
let (k, v) = entry?;
out.push((k.value().to_vec(), v.value().to_vec()));
}
Ok(out)
}
fn scan(
&self,
table: &str,
start: Bound<&[u8]>,
end: Bound<&[u8]>,
f: ScanFn<'_>,
) -> Result<(), TalaDbError> {
let Some(tbl) = self.table(table)? else {
return Ok(());
};
for entry in tbl.range::<&[u8]>((start, end))? {
let (k, v) = entry?;
if f(k.value(), v.value())? == ScanFlow::Stop {
break;
}
}
Ok(())
}
fn scan_all(&self, table: &str) -> Result<KvPairs, TalaDbError> {
let Some(tbl) = self.table(table)? else {
return Ok(vec![]);
};
let mut out = Vec::new();
for entry in tbl.iter()? {
let (k, v) = entry?;
out.push((k.value().to_vec(), v.value().to_vec()));
}
Ok(out)
}
fn count_entries(&self, table: &str) -> Result<u64, TalaDbError> {
use redb::ReadableTableMetadata;
match self.table(table)? {
Some(tbl) => Ok(tbl.len()?),
None => Ok(0),
}
}
}