use std::collections::HashSet;
use bytes::Bytes;
use nodedb_types::Namespace;
use pagedb::vfs::Vfs;
use crate::error::LiteError;
use crate::storage::engine::{CompactionOutcome, WriteOp};
use crate::storage::pagedb_storage::keys::{KeyBuf, prefix_key};
use crate::storage::pagedb_storage::types::PagedbStorage;
impl<V: Vfs + Clone> PagedbStorage<V> {
pub(super) async fn get_rows(
&self,
ns: Namespace,
key: &[u8],
) -> Result<Option<Vec<u8>>, LiteError> {
let composite = KeyBuf::new(ns, key);
let txn = self.db.begin_read().await.map_err(LiteError::from)?;
txn.get(composite.as_slice())
.await
.map(|opt| opt.map(|v| v.to_vec()))
.map_err(LiteError::from)
}
pub(super) async fn put_rows(
&self,
ns: Namespace,
key: &[u8],
value: &[u8],
) -> Result<(), LiteError> {
let composite = prefix_key(ns, key);
let mut txn = self.db.begin_write().await.map_err(LiteError::from)?;
txn.put(&composite, value).await.map_err(LiteError::from)?;
txn.commit().await.map(|_| ()).map_err(LiteError::from)
}
pub(super) async fn delete_rows(&self, ns: Namespace, key: &[u8]) -> Result<(), LiteError> {
let composite = prefix_key(ns, key);
let mut txn = self.db.begin_write().await.map_err(LiteError::from)?;
txn.delete(&composite).await.map_err(LiteError::from)?;
txn.commit().await.map(|_| ()).map_err(LiteError::from)
}
pub(super) async fn batch_write_rows(&self, ops: &[WriteOp]) -> Result<(), LiteError> {
if ops.is_empty() {
return Ok(());
}
let mut txn = self.db.begin_write().await.map_err(LiteError::from)?;
let all_keys: Vec<Vec<u8>> = ops
.iter()
.map(|op| match op {
WriteOp::Put { ns, key, .. } => prefix_key(*ns, key),
WriteOp::Delete { ns, key } => prefix_key(*ns, key),
})
.collect();
let unique_count = all_keys.iter().collect::<HashSet<_>>().len();
if unique_count < all_keys.len() {
for (op, composite) in ops.iter().zip(all_keys) {
match op {
WriteOp::Put { value, .. } => {
txn.put(&composite, value).await.map_err(LiteError::from)?;
}
WriteOp::Delete { .. } => {
txn.delete(&composite).await.map_err(LiteError::from)?;
}
}
}
} else {
let mut puts: Vec<(Bytes, Bytes)> = Vec::new();
let mut deletes: Vec<Vec<u8>> = Vec::new();
for (op, composite) in ops.iter().zip(all_keys) {
match op {
WriteOp::Put { value, .. } => {
puts.push((Bytes::from(composite), Bytes::from(value.clone())));
}
WriteOp::Delete { .. } => {
deletes.push(composite);
}
}
}
puts.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
deletes.sort_unstable();
if !puts.is_empty() {
txn.put_batch(puts).await.map_err(LiteError::from)?;
}
if !deletes.is_empty() {
txn.delete_batch(deletes).await.map_err(LiteError::from)?;
}
}
txn.commit().await.map(|_| ()).map_err(LiteError::from)
}
pub(super) async fn count_rows(&self, ns: Namespace) -> Result<u64, LiteError> {
let ns_prefix = vec![ns as u8];
let txn = self.db.begin_read().await.map_err(LiteError::from)?;
let raw = txn.scan_prefix(&ns_prefix).await.map_err(LiteError::from)?;
Ok(raw.len() as u64)
}
pub(super) async fn compact_rows(&self) -> Result<CompactionOutcome, LiteError> {
let stats = self.db.compact_now().await.map_err(LiteError::from)?;
let gc = self.db.gc_now().await.map_err(LiteError::from)?;
Ok(CompactionOutcome {
reclaimed_pages: stats.main_db_pages_reclaimed,
segments_repacked: stats.segments_repacked,
file_bytes_freed: stats.bytes_truncated,
reclaimed_segments: gc.reclaimed_segments,
segment_bytes_freed: gc.reclaimed_bytes,
})
}
}
#[cfg(test)]
mod tests {
use pagedb::vfs::memory::MemVfs;
use nodedb_types::Namespace;
use crate::storage::engine::{StorageEngine, WriteOp};
use crate::storage::pagedb_storage::types::PagedbStorage;
async fn make_storage() -> PagedbStorage<MemVfs> {
PagedbStorage::open_in_memory().await.unwrap()
}
#[tokio::test]
async fn put_get_roundtrip() {
let s = make_storage().await;
s.put(Namespace::Vector, b"v1", b"hello").await.unwrap();
let val = s.get(Namespace::Vector, b"v1").await.unwrap();
assert_eq!(val.as_deref(), Some(b"hello".as_slice()));
}
#[tokio::test]
async fn get_missing_returns_none() {
let s = make_storage().await;
let val = s.get(Namespace::Vector, b"nope").await.unwrap();
assert!(val.is_none());
}
#[tokio::test]
async fn put_overwrites() {
let s = make_storage().await;
s.put(Namespace::Graph, b"k", b"first").await.unwrap();
s.put(Namespace::Graph, b"k", b"second").await.unwrap();
let val = s.get(Namespace::Graph, b"k").await.unwrap();
assert_eq!(val.as_deref(), Some(b"second".as_slice()));
}
#[tokio::test]
async fn delete_removes_key() {
let s = make_storage().await;
s.put(Namespace::Crdt, b"k", b"val").await.unwrap();
s.delete(Namespace::Crdt, b"k").await.unwrap();
assert!(s.get(Namespace::Crdt, b"k").await.unwrap().is_none());
}
#[tokio::test]
async fn delete_nonexistent_is_noop() {
let s = make_storage().await;
s.delete(Namespace::Meta, b"ghost").await.unwrap();
}
#[tokio::test]
async fn namespaces_are_isolated() {
let s = make_storage().await;
s.put(Namespace::Vector, b"k", b"vec").await.unwrap();
s.put(Namespace::Graph, b"k", b"graph").await.unwrap();
assert_eq!(
s.get(Namespace::Vector, b"k").await.unwrap().as_deref(),
Some(b"vec".as_slice())
);
assert_eq!(
s.get(Namespace::Graph, b"k").await.unwrap().as_deref(),
Some(b"graph".as_slice())
);
}
#[tokio::test]
async fn batch_write_atomic() {
let s = make_storage().await;
s.put(Namespace::Crdt, b"to_delete", b"old").await.unwrap();
s.batch_write(&[
WriteOp::Put {
ns: Namespace::Crdt,
key: b"new1".to_vec(),
value: b"val1".to_vec(),
},
WriteOp::Put {
ns: Namespace::Crdt,
key: b"new2".to_vec(),
value: b"val2".to_vec(),
},
WriteOp::Delete {
ns: Namespace::Crdt,
key: b"to_delete".to_vec(),
},
])
.await
.unwrap();
assert!(s.get(Namespace::Crdt, b"new1").await.unwrap().is_some());
assert!(s.get(Namespace::Crdt, b"new2").await.unwrap().is_some());
assert!(
s.get(Namespace::Crdt, b"to_delete")
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn batch_write_same_key_put_then_delete() {
let s = make_storage().await;
s.batch_write(&[
WriteOp::Put {
ns: Namespace::Meta,
key: b"clash".to_vec(),
value: b"written".to_vec(),
},
WriteOp::Delete {
ns: Namespace::Meta,
key: b"clash".to_vec(),
},
])
.await
.unwrap();
assert!(s.get(Namespace::Meta, b"clash").await.unwrap().is_none());
}
#[tokio::test]
async fn batch_write_same_key_delete_then_put() {
let s = make_storage().await;
s.put(Namespace::Meta, b"exists", b"old").await.unwrap();
s.batch_write(&[
WriteOp::Delete {
ns: Namespace::Meta,
key: b"exists".to_vec(),
},
WriteOp::Put {
ns: Namespace::Meta,
key: b"exists".to_vec(),
value: b"new".to_vec(),
},
])
.await
.unwrap();
assert_eq!(
s.get(Namespace::Meta, b"exists").await.unwrap().as_deref(),
Some(b"new".as_slice())
);
}
#[tokio::test]
async fn batch_write_empty_is_noop() {
let s = make_storage().await;
s.batch_write(&[]).await.unwrap();
}
#[tokio::test]
async fn count_entries() {
let s = make_storage().await;
assert_eq!(s.count(Namespace::Vector).await.unwrap(), 0);
s.put(Namespace::Vector, b"v1", b"a").await.unwrap();
s.put(Namespace::Vector, b"v2", b"b").await.unwrap();
s.put(Namespace::Graph, b"g1", b"c").await.unwrap();
assert_eq!(s.count(Namespace::Vector).await.unwrap(), 2);
assert_eq!(s.count(Namespace::Graph).await.unwrap(), 1);
assert_eq!(s.count(Namespace::Crdt).await.unwrap(), 0);
}
#[tokio::test]
async fn large_value_roundtrip() {
let s = make_storage().await;
let large = vec![0xABu8; 1_000_000];
s.put(Namespace::Vector, b"hnsw:layer0", &large)
.await
.unwrap();
let val = s.get(Namespace::Vector, b"hnsw:layer0").await.unwrap();
assert_eq!(val.unwrap().len(), 1_000_000);
}
#[tokio::test]
async fn compact_mem_is_ok_noop() {
let s = make_storage().await;
s.put(Namespace::Vector, b"v1", b"hello").await.unwrap();
s.put(Namespace::Graph, b"g1", b"world").await.unwrap();
let outcome = s.compact().await.unwrap();
assert_eq!(
s.get(Namespace::Vector, b"v1").await.unwrap().as_deref(),
Some(b"hello".as_slice())
);
let _ = outcome.reclaimed_pages;
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn compact_default_disk_is_ok() {
use pagedb::vfs::DefaultVfs;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("compact-test.db");
let s = PagedbStorage::<DefaultVfs>::open(
&path,
crate::storage::encryption::Encryption::Plaintext,
)
.await
.unwrap();
for i in 0u32..200 {
let key = i.to_be_bytes();
s.put(Namespace::Meta, &key, &vec![0xCDu8; 512])
.await
.unwrap();
}
for i in 0u32..150 {
let key = i.to_be_bytes();
s.delete(Namespace::Meta, &key).await.unwrap();
}
let outcome = s.compact().await.unwrap();
let survivor = 175u32.to_be_bytes();
assert!(s.get(Namespace::Meta, &survivor).await.unwrap().is_some());
let _ = (
outcome.reclaimed_pages,
outcome.segments_repacked,
outcome.file_bytes_freed,
);
}
}