pub mod aggregate;
pub mod audit;
pub mod bm25;
pub mod collection;
pub mod config;
pub mod crypto;
pub mod document;
pub mod engine;
pub mod error;
pub mod fts;
pub mod index;
pub mod json;
pub mod json_depth;
pub mod migrate;
pub mod migration;
pub mod query;
pub mod time;
pub mod typed;
pub mod vector;
pub mod watch;
pub use aggregate::{Accumulator, GroupKey, Pipeline, Stage};
pub use audit::{AuditEntry, AuditOp, read_audit_log, read_audit_log_since};
pub use collection::{Collection, CollectionIndexInfo, Update};
pub use config::{DurabilityConfig, TalaDbConfig, load_auto, load_from_path};
#[cfg(feature = "encryption")]
pub use crypto::{
EncryptedBackend, EncryptionKey, MIN_PBKDF2_ITERATIONS, derive_key, migrate_encrypted_v0_to_v1,
rekey,
};
pub use document::{Document, Value, derive_doc_id};
pub use engine::{RedbBackend, StorageBackend};
pub use error::TalaDbError;
pub use migration::{BUILTIN_MIGRATIONS, CURRENT_SCHEMA_VERSION, Migration, run_migrations};
pub use query::Filter;
pub use query::options::{FindOptions, SortDirection, SortSpec};
pub use redb;
#[cfg(feature = "legacy-migration")]
pub use redb2;
pub use time::now_ms;
pub use typed::{Scored, TypedCollection, TypedWatch};
pub use ulid::Ulid;
pub use vector::{HnswOptions, VectorMetric, VectorSearchResult};
pub use serde_json;
pub fn open(path: impl AsRef<std::path::Path>) -> Result<Database, TalaDbError> {
Database::open(path.as_ref())
}
use std::path::Path;
use std::sync::Arc;
pub(crate) fn clamp_to_usize(value: u64) -> usize {
usize::try_from(value).unwrap_or(usize::MAX)
}
const SNAPSHOT_MAGIC: &[u8; 4] = b"TDBS";
const SNAPSHOT_VERSION: u32 = 1;
const MAX_SNAPSHOT_ENTRY_BYTES: usize = 64 * 1024 * 1024;
#[derive(Clone)]
pub struct Database {
backend: Arc<dyn StorageBackend>,
index_cache: collection::SharedIndexCache,
watch_registries:
Arc<std::sync::Mutex<std::collections::HashMap<String, watch::SharedRegistry>>>,
search_cache: search_cache::SharedSearchCache,
}
impl Database {
#[cfg(feature = "encryption")]
pub fn open_encrypted(path: &Path, passphrase: &str) -> Result<Self, TalaDbError> {
if passphrase.is_empty() {
return Err(TalaDbError::Config(
"encryption passphrase must not be empty".into(),
));
}
let salt_path = path.with_extension(format!(
"{}taladb-salt",
path.extension()
.and_then(|v| v.to_str())
.map(|v| format!("{v}."))
.unwrap_or_default()
));
let salt = match std::fs::read(&salt_path) {
Ok(salt) if salt.len() == 16 => salt,
Ok(_) => {
return Err(TalaDbError::Encryption(
"invalid encryption salt file".into(),
));
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let mut salt = vec![0u8; 16];
getrandom::fill(&mut salt).map_err(|e| TalaDbError::Encryption(e.to_string()))?;
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
use std::io::Write;
options
.open(&salt_path)
.and_then(|mut file| file.write_all(&salt))
.map_err(|e| {
TalaDbError::Storage(format!("cannot write encryption salt: {e}"))
})?;
salt
}
Err(e) => {
return Err(TalaDbError::Storage(format!(
"cannot read encryption salt: {e}"
)));
}
};
let raw: Arc<dyn StorageBackend> = Arc::new(RedbBackend::open(path)?);
let key = crypto::derive_key(passphrase, &salt, crypto::MIN_PBKDF2_ITERATIONS)?;
Self::open_with_backend(Box::new(crypto::EncryptedBackend::new(raw, key))).map_err(|e| {
match e {
TalaDbError::Encryption(detail) => TalaDbError::Encryption(format!(
"wrong passphrase, or the file was not encrypted with it ({detail})"
)),
other => other,
}
})
}
fn from_backend(backend: Arc<dyn StorageBackend>) -> Result<Self, TalaDbError> {
run_migrations(backend.as_ref(), BUILTIN_MIGRATIONS)?;
Ok(Self {
backend,
index_cache: collection::new_shared_index_cache(),
watch_registries: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
search_cache: search_cache::new_shared_search_cache(),
})
}
pub fn open(path: &Path) -> Result<Self, TalaDbError> {
Self::from_backend(Arc::new(RedbBackend::open(path)?))
}
pub fn open_with_backend(backend: Box<dyn StorageBackend>) -> Result<Self, TalaDbError> {
Self::from_backend(Arc::from(backend))
}
pub fn open_in_memory() -> Result<Self, TalaDbError> {
Self::from_backend(Arc::new(RedbBackend::open_in_memory()?))
}
pub fn open_with_migrations(
path: &Path,
migrations: &[Migration],
) -> Result<Self, TalaDbError> {
let db = Self::from_backend(Arc::new(RedbBackend::open(path)?))?;
run_migrations(db.backend.as_ref(), migrations)?;
Ok(db)
}
pub fn set_vector_cache_budget(&self, bytes: usize) {
self.search_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.set_budget(bytes);
}
pub fn vector_cache_stats(&self) -> VectorCacheStats {
self.search_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.stats()
}
pub fn set_vector_cache_adaptive(&self, memory_bytes: Option<u64>) -> Result<(), TalaDbError> {
self.search_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.set_adaptive(memory_bytes)
}
pub fn notify_memory_pressure(&self, pressure: MemoryPressure) {
self.search_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.notify_pressure(pressure);
}
pub fn backend(&self) -> &dyn StorageBackend {
self.backend.as_ref()
}
pub fn set_durability(&self, eventual: bool) {
self.backend.set_durability(eventual);
}
pub fn flush(&self) -> Result<(), TalaDbError> {
self.backend.flush()
}
pub fn collection(&self, name: &str) -> Result<Collection, TalaDbError> {
collection::validate_collection_name(name)?;
let registry = {
let mut map = self
.watch_registries
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Arc::clone(
map.entry(name.to_string())
.or_insert_with(watch::new_registry),
)
};
let col = Collection::new(name, Arc::clone(&self.backend))
.with_index_cache(Arc::clone(&self.index_cache))
.with_watch_registry(registry)
.with_search_cache(Arc::clone(&self.search_cache));
Ok(col)
}
pub fn rebuild_hnsw_indexes(&self) -> Result<(), TalaDbError> {
let txn = self.backend.begin_read()?;
let all = txn.scan_all(vector::META_HNSW_TABLE)?;
drop(txn);
for (k, _) in all {
let key_str = match std::str::from_utf8(&k) {
Ok(s) => s.to_string(),
Err(_) => continue,
};
let mut parts = key_str.splitn(2, "::");
let col_name = match parts.next() {
Some(s) => s.to_string(),
None => continue,
};
let field = match parts.next() {
Some(s) => s.to_string(),
None => continue,
};
self.collection(&col_name)?.upgrade_vector_index(&field)?;
}
Ok(())
}
pub fn compact(&self) -> Result<(), TalaDbError> {
self.backend.compact()
}
pub fn list_collection_names(&self) -> Result<Vec<String>, TalaDbError> {
let txn = self.backend.begin_read()?;
let mut names: Vec<String> = txn
.list_tables()?
.into_iter()
.filter_map(|t| t.strip_prefix("docs::").map(str::to_string))
.filter(|name| !name.starts_with('_'))
.collect();
names.sort();
Ok(names)
}
pub fn user_version(&self) -> Result<u32, TalaDbError> {
let rtxn = self.backend.begin_read()?;
match rtxn.get(
crate::index::META_USER_VERSION_TABLE,
crate::index::META_VERSION_KEY,
)? {
Some(bytes) => Ok(postcard::from_bytes(&bytes)?),
None => Ok(0),
}
}
pub fn set_user_version(&self, version: u32) -> Result<(), TalaDbError> {
let bytes = postcard::to_allocvec(&version)?;
let mut wtxn = self.backend.begin_write()?;
wtxn.put(
crate::index::META_USER_VERSION_TABLE,
crate::index::META_VERSION_KEY,
&bytes,
)?;
wtxn.commit()?;
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn export_snapshot(&self) -> Result<Vec<u8>, TalaDbError> {
self.export_snapshot_with_limit(usize::MAX)
}
pub fn export_snapshot_with_limit(&self, max_bytes: usize) -> Result<Vec<u8>, TalaDbError> {
if max_bytes < 12 {
return Err(TalaDbError::InvalidSnapshot);
}
let txn = self.backend.begin_read()?;
let table_names = txn.list_tables()?;
let mut buf = Vec::new();
buf.extend_from_slice(SNAPSHOT_MAGIC);
buf.extend_from_slice(&SNAPSHOT_VERSION.to_le_bytes());
buf.extend_from_slice(&len_prefix(table_names.len())?);
for name in &table_names {
let name_bytes = name.as_bytes();
if buf
.len()
.saturating_add(name_bytes.len())
.saturating_add(12)
> max_bytes
{
return Err(TalaDbError::Storage(
"snapshot exceeds configured byte limit".into(),
));
}
buf.extend_from_slice(&len_prefix(name_bytes.len())?);
buf.extend_from_slice(name_bytes);
buf.extend_from_slice(&txn.count_entries(name)?.to_le_bytes());
txn.scan(
name,
std::ops::Bound::Unbounded,
std::ops::Bound::Unbounded,
&mut |key, val| {
if buf
.len()
.saturating_add(key.len())
.saturating_add(val.len())
.saturating_add(8)
> max_bytes
{
return Err(TalaDbError::Storage(
"snapshot exceeds configured byte limit".into(),
));
}
buf.extend_from_slice(&len_prefix(key.len())?);
buf.extend_from_slice(key);
buf.extend_from_slice(&len_prefix(val.len())?);
buf.extend_from_slice(val);
Ok(crate::engine::ScanFlow::Continue)
},
)?;
}
Ok(buf)
}
pub fn restore_from_snapshot(data: &[u8]) -> Result<Self, TalaDbError> {
#[cfg(target_pointer_width = "32")]
const MAX_SNAPSHOT_SIZE: usize = 2 * 1024 * 1024 * 1024; #[cfg(not(target_pointer_width = "32"))]
const MAX_SNAPSHOT_SIZE: usize = 10 * 1024 * 1024 * 1024; if data.len() > MAX_SNAPSHOT_SIZE {
return Err(TalaDbError::InvalidSnapshot);
}
if data.len() < 12 || &data[..4] != SNAPSHOT_MAGIC {
return Err(TalaDbError::InvalidSnapshot);
}
let version = u32::from_le_bytes(
data[4..8]
.try_into()
.map_err(|_| TalaDbError::InvalidSnapshot)?,
);
if version != SNAPSHOT_VERSION {
return Err(TalaDbError::InvalidSnapshot);
}
let db = Self::open_in_memory()?;
let mut cursor: usize = 8;
let table_count = read_u32(data, &mut cursor)? as usize;
for _ in 0..table_count {
let name_len = read_u32(data, &mut cursor)? as usize;
if name_len == 0 {
return Err(TalaDbError::InvalidSnapshot);
}
let name = std::str::from_utf8(read_slice(data, &mut cursor, name_len)?)
.map_err(|_| TalaDbError::InvalidSnapshot)?
.to_string();
let entry_count = clamp_to_usize(read_u64(data, &mut cursor)?);
let mut wtxn = db.backend.begin_write()?;
for _ in 0..entry_count {
let key_len = read_u32(data, &mut cursor)? as usize;
if key_len > MAX_SNAPSHOT_ENTRY_BYTES {
return Err(TalaDbError::InvalidSnapshot);
}
let key = read_slice(data, &mut cursor, key_len)?.to_vec();
let val_len = read_u32(data, &mut cursor)? as usize;
if val_len > MAX_SNAPSHOT_ENTRY_BYTES {
return Err(TalaDbError::InvalidSnapshot);
}
let val = read_slice(data, &mut cursor, val_len)?.to_vec();
wtxn.put(&name, &key, &val)?;
}
wtxn.commit()?;
}
Ok(db)
}
}
fn len_prefix(len: usize) -> Result<[u8; 4], TalaDbError> {
u32::try_from(len)
.map(u32::to_le_bytes)
.map_err(|_| TalaDbError::InvalidSnapshot)
}
fn read_u32(data: &[u8], cursor: &mut usize) -> Result<u32, TalaDbError> {
let end = cursor.checked_add(4).ok_or(TalaDbError::InvalidSnapshot)?;
if end > data.len() {
return Err(TalaDbError::InvalidSnapshot);
}
let val = u32::from_le_bytes(
data[*cursor..end]
.try_into()
.map_err(|_| TalaDbError::InvalidSnapshot)?,
);
*cursor = end;
Ok(val)
}
fn read_u64(data: &[u8], cursor: &mut usize) -> Result<u64, TalaDbError> {
let end = cursor.checked_add(8).ok_or(TalaDbError::InvalidSnapshot)?;
if end > data.len() {
return Err(TalaDbError::InvalidSnapshot);
}
let val = u64::from_le_bytes(
data[*cursor..end]
.try_into()
.map_err(|_| TalaDbError::InvalidSnapshot)?,
);
*cursor = end;
Ok(val)
}
fn read_slice<'a>(data: &'a [u8], cursor: &mut usize, len: usize) -> Result<&'a [u8], TalaDbError> {
let end = cursor
.checked_add(len)
.ok_or(TalaDbError::InvalidSnapshot)?;
if end > data.len() {
return Err(TalaDbError::InvalidSnapshot);
}
let slice = &data[*cursor..end];
*cursor = end;
Ok(slice)
}
#[cfg(test)]
mod snapshot_encoding_tests {
use super::*;
#[test]
fn len_prefix_round_trips_and_rejects_the_unrepresentable() {
assert_eq!(len_prefix(0).unwrap(), 0u32.to_le_bytes());
assert_eq!(len_prefix(16).unwrap(), 16u32.to_le_bytes());
assert_eq!(
len_prefix(u32::MAX as usize).unwrap(),
u32::MAX.to_le_bytes()
);
#[cfg(not(target_pointer_width = "32"))]
{
let too_big = u32::MAX as usize + 1;
assert!(
matches!(len_prefix(too_big), Err(TalaDbError::InvalidSnapshot)),
"an unencodable length must be an error, not a saturated prefix"
);
}
}
#[test]
fn len_prefix_is_readable_by_the_snapshot_reader() {
let encoded = len_prefix(1234).unwrap();
let mut cursor = 0usize;
assert_eq!(read_u32(&encoded, &mut cursor).unwrap(), 1234);
assert_eq!(cursor, 4);
}
}
mod search_cache;
mod vector_graph;
pub use collection::{
VectorBuildProgress, VectorExecution, VectorIndexStatus, VectorQueryOptions, VectorQueryResult,
VectorSearchMode,
};
pub use search_cache::{MemoryPressure, VectorCachePolicy, VectorCacheStats};
pub use vector_graph::{GraphOptions, Quantization};
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
pub struct ReadmeDoctests;