use crate::backend::{BackendPartition, BackendWriteOp, StorageBackend};
use crate::config::VantaConfig;
use crate::error::{Result, VantaError};
use fjall::{Database, Keyspace, KeyspaceCreateOptions, PersistMode};
use std::path::Path;
use tracing::info;
pub(crate) struct FjallBackend {
db: Database,
default: Keyspace,
tombstone_storage: Keyspace,
compressed_archive: Keyspace,
tombstones: Keyspace,
namespace_index: Keyspace,
payload_index: Keyspace,
text_index: Keyspace,
internal_metadata: Keyspace,
}
impl FjallBackend {
pub(crate) fn open(path: &str, _config: &VantaConfig) -> Result<Self> {
let db = Database::builder(path)
.open()
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))?;
let default = db
.keyspace("default", KeyspaceCreateOptions::default)
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))?;
let tombstone_storage = db
.keyspace("tombstone_storage", KeyspaceCreateOptions::default)
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))?;
let compressed_archive = db
.keyspace("compressed_archive", KeyspaceCreateOptions::default)
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))?;
let tombstones = db
.keyspace("tombstones", KeyspaceCreateOptions::default)
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))?;
let namespace_index = db
.keyspace("namespace_index", KeyspaceCreateOptions::default)
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))?;
let payload_index = db
.keyspace("payload_index", KeyspaceCreateOptions::default)
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))?;
let text_index = db
.keyspace("text_index", KeyspaceCreateOptions::default)
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))?;
let internal_metadata = db
.keyspace("internal_metadata", KeyspaceCreateOptions::default)
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))?;
info!("Fjall database opened at '{}'", path);
Ok(Self {
db,
default,
tombstone_storage,
compressed_archive,
tombstones,
namespace_index,
payload_index,
text_index,
internal_metadata,
})
}
fn keyspace(&self, partition: BackendPartition) -> &Keyspace {
match partition {
BackendPartition::Default => &self.default,
BackendPartition::TombstoneStorage => &self.tombstone_storage,
BackendPartition::CompressedArchive => &self.compressed_archive,
BackendPartition::Tombstones => &self.tombstones,
BackendPartition::NamespaceIndex => &self.namespace_index,
BackendPartition::PayloadIndex => &self.payload_index,
BackendPartition::TextIndex => &self.text_index,
BackendPartition::InternalMetadata => &self.internal_metadata,
}
}
}
impl StorageBackend for FjallBackend {
fn put(&self, partition: BackendPartition, key: &[u8], value: &[u8]) -> Result<()> {
self.keyspace(partition)
.insert(key, value)
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))
}
fn get(&self, partition: BackendPartition, key: &[u8]) -> Result<Option<Vec<u8>>> {
self.keyspace(partition)
.get(key)
.map(|opt| opt.map(|slice| slice.to_vec()))
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))
}
fn delete(&self, partition: BackendPartition, key: &[u8]) -> Result<()> {
self.keyspace(partition)
.remove(key)
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))
}
fn write_batch(&self, ops: Vec<BackendWriteOp>) -> Result<()> {
let mut batch = self.db.batch();
for op in ops {
match op {
BackendWriteOp::Put {
partition,
key,
value,
} => {
batch.insert(self.keyspace(partition), key, value);
}
BackendWriteOp::Delete { partition, key } => {
batch.remove(self.keyspace(partition), key);
}
}
}
batch
.commit()
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))
}
fn scan(&self, partition: BackendPartition) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
let ks = self.keyspace(partition);
let mut result = Vec::new();
for item in ks.iter() {
let kv = item
.into_inner()
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))?;
result.push((kv.0.to_vec(), kv.1.to_vec()));
}
Ok(result)
}
fn scan_prefix(
&self,
partition: BackendPartition,
prefix: &[u8],
) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
let ks = self.keyspace(partition);
let mut result = Vec::new();
for item in ks.range(prefix..) {
let kv = item
.into_inner()
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))?;
if !kv.0.starts_with(prefix) {
break;
}
result.push((kv.0.to_vec(), kv.1.to_vec()));
}
Ok(result)
}
fn flush(&self) -> Result<()> {
self.db
.persist(PersistMode::SyncAll)
.map_err(|e| VantaError::IoError(std::io::Error::other(e.to_string())))
}
fn checkpoint(&self, _path: &Path) -> Result<()> {
Err(VantaError::Execution(
"Checkpoint not supported by FjallBackend: Fjall does not expose a \
point-in-time snapshot-to-disk API equivalent to RocksDB checkpoints"
.to_string(),
))
}
fn compact(&self) {
}
fn capabilities(&self) -> crate::backend::BackendCapabilities {
crate::backend::BackendCapabilities {
supports_checkpoint: false,
supports_manual_compaction: false,
kind: crate::backend::BackendKind::Fjall,
}
}
}