use async_trait::async_trait;
use crate::error::LiteError;
use nodedb_types::Namespace;
pub type KvPair = (Vec<u8>, Vec<u8>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrefixScanLimit {
Records,
Bytes,
}
#[derive(Debug, Default)]
pub struct PrefixScan {
pub entries: Vec<KvPair>,
pub limit: Option<PrefixScanLimit>,
}
pub(crate) fn check_prefix_cursor(
prefix: &[u8],
after_key: Option<&[u8]>,
) -> Result<(), LiteError> {
if after_key.is_some_and(|key| !key.starts_with(prefix)) {
return Err(LiteError::BadRequest {
detail: format!(
"scan cursor {after_key:?} excludes prefix {prefix:?}: use a matching cursor"
),
});
}
Ok(())
}
pub(crate) fn prefix_scan_budget_error(
prefix: &[u8],
after_key: Option<&[u8]>,
max_bytes: usize,
) -> LiteError {
LiteError::Backpressure {
detail: format!(
"scan row after {after_key:?} under prefix {prefix:?} exceeds {max_bytes} key-plus-value bytes: increase the byte budget"
),
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompactionOutcome {
pub reclaimed_pages: u64,
pub segments_repacked: u32,
pub file_bytes_freed: u64,
pub reclaimed_segments: u64,
pub segment_bytes_freed: u64,
}
#[derive(Debug, Clone)]
pub enum WriteOp {
Put {
ns: Namespace,
key: Vec<u8>,
value: Vec<u8>,
},
Delete { ns: Namespace, key: Vec<u8> },
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
pub trait StorageEngine: Send + Sync + 'static {
async fn get(&self, ns: Namespace, key: &[u8]) -> Result<Option<Vec<u8>>, LiteError>;
async fn put(&self, ns: Namespace, key: &[u8], value: &[u8]) -> Result<(), LiteError>;
async fn delete(&self, ns: Namespace, key: &[u8]) -> Result<(), LiteError>;
async fn scan_prefix(&self, ns: Namespace, prefix: &[u8]) -> Result<Vec<KvPair>, LiteError>;
async fn scan_prefix_bounded(
&self,
_ns: Namespace,
_prefix: &[u8],
limit: usize,
) -> Result<Vec<KvPair>, LiteError> {
if limit == 0 {
return Ok(Vec::new());
}
Err(LiteError::Unsupported {
detail: "bounded prefix scans require backend support".into(),
})
}
async fn scan_prefix_budgeted(
&self,
_ns: Namespace,
_prefix: &[u8],
max_records: usize,
_max_bytes: usize,
) -> Result<PrefixScan, LiteError> {
if max_records == 0 {
return Ok(PrefixScan::default());
}
Err(LiteError::Unsupported {
detail: "budgeted prefix scans require backend support".into(),
})
}
async fn scan_prefix_from_budgeted(
&self,
_ns: Namespace,
prefix: &[u8],
after_key: Option<&[u8]>,
max_records: usize,
_max_bytes: usize,
) -> Result<PrefixScan, LiteError> {
if max_records == 0 {
return Ok(PrefixScan::default());
}
check_prefix_cursor(prefix, after_key)?;
Err(LiteError::Unsupported {
detail: "budgeted prefix continuation requires backend support".into(),
})
}
async fn batch_write(&self, ops: &[WriteOp]) -> Result<(), LiteError>;
async fn count(&self, ns: Namespace) -> Result<u64, LiteError>;
async fn compact(&self) -> Result<CompactionOutcome, LiteError> {
Ok(CompactionOutcome::default())
}
async fn scan_range(
&self,
ns: Namespace,
start: &[u8],
limit: usize,
) -> Result<Vec<KvPair>, LiteError>;
async fn scan_range_bounded(
&self,
ns: Namespace,
start: Option<&[u8]>,
end: Option<&[u8]>,
limit: Option<usize>,
) -> Result<Vec<KvPair>, LiteError>;
#[cfg(not(target_arch = "wasm32"))]
fn as_vector_segment_ext(
&self,
) -> Option<&dyn crate::storage::vector_segment_ext::VectorSegmentExt> {
None
}
#[cfg(not(target_arch = "wasm32"))]
fn as_array_segment_ext(
&self,
) -> Option<&dyn crate::storage::array_segment_ext::ArraySegmentExt> {
None
}
#[cfg(not(target_arch = "wasm32"))]
fn as_fts_segment_ext(&self) -> Option<&dyn crate::storage::fts_segment_ext::FtsSegmentExt> {
None
}
#[cfg(not(target_arch = "wasm32"))]
fn as_columnar_segment_ext(
&self,
) -> Option<&dyn crate::storage::columnar_segment_ext::ColumnarSegmentExt> {
None
}
#[cfg(not(target_arch = "wasm32"))]
fn as_graph_segment_ext(
&self,
) -> Option<&dyn crate::storage::graph_segment_ext::GraphSegmentExt> {
None
}
#[cfg(not(target_arch = "wasm32"))]
fn as_spatial_segment_ext(
&self,
) -> Option<&dyn crate::storage::spatial_segment_ext::SpatialSegmentExt> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_op_debug() {
let op = WriteOp::Put {
ns: Namespace::Vector,
key: vec![1, 2],
value: vec![3, 4],
};
let dbg = format!("{op:?}");
assert!(dbg.contains("Put"));
assert!(dbg.contains("Vector"));
}
}