use std::marker::PhantomData;
use noxu_bind::EntryBinding;
use noxu_db::{Database, OperationStatus, Transaction};
use crate::error::{CollectionError, Result};
use crate::internal::encode_key;
use crate::stored_iterator::StoredIterator;
pub struct StoredKeySet<'db, K, KB>
where
KB: EntryBinding<K>,
{
db: &'db Database,
key_binding: KB,
read_only: bool,
_marker: PhantomData<fn() -> K>,
}
impl<'db, K, KB> StoredKeySet<'db, K, KB>
where
KB: EntryBinding<K>,
{
pub fn new(db: &'db Database, key_binding: KB) -> Self {
StoredKeySet { db, key_binding, read_only: false, _marker: PhantomData }
}
pub fn new_read_only(db: &'db Database, key_binding: KB) -> Self {
StoredKeySet { db, key_binding, read_only: true, _marker: PhantomData }
}
pub fn is_read_only(&self) -> bool {
self.read_only
}
pub fn database(&self) -> &'db Database {
self.db
}
pub fn key_binding(&self) -> &KB {
&self.key_binding
}
pub fn add(&self, txn: Option<&Transaction>, key: &K) -> Result<bool> {
if self.read_only {
return Err(CollectionError::ReadOnly);
}
let key_entry = encode_key(&self.key_binding, key)?;
let mut data_entry = noxu_db::DatabaseEntry::new();
let already = matches!(
self.db.get(txn, &key_entry, &mut data_entry)?,
OperationStatus::Success,
);
let empty = noxu_db::DatabaseEntry::from_bytes(b"");
self.db.put(txn, &key_entry, &empty)?;
Ok(!already)
}
pub fn contains(&self, txn: Option<&Transaction>, key: &K) -> Result<bool> {
let key_entry = encode_key(&self.key_binding, key)?;
let mut data_entry = noxu_db::DatabaseEntry::new();
match self.db.get(txn, &key_entry, &mut data_entry)? {
OperationStatus::Success => Ok(true),
_ => Ok(false),
}
}
pub fn remove(&self, txn: Option<&Transaction>, key: &K) -> Result<bool> {
if self.read_only {
return Err(CollectionError::ReadOnly);
}
let key_entry = encode_key(&self.key_binding, key)?;
let mut data_entry = noxu_db::DatabaseEntry::new();
let present = matches!(
self.db.get(txn, &key_entry, &mut data_entry)?,
OperationStatus::Success,
);
if present {
self.db.delete(txn, &key_entry)?;
}
Ok(present)
}
pub fn len(&self, _txn: Option<&Transaction>) -> Result<usize> {
let n = self.db.count()?;
Ok(usize::try_from(n).unwrap_or(usize::MAX))
}
pub fn is_empty(&self, txn: Option<&Transaction>) -> Result<bool> {
Ok(self.len(txn)? == 0)
}
pub fn iter(&self, txn: Option<&Transaction>) -> Result<StoredIterator<K>> {
use crate::internal::{ScanDirection, StartKey, scan_records};
use noxu_bind::ByteArrayBinding;
let value_binding = ByteArrayBinding;
let items = scan_records::<K, Vec<u8>, KB, ByteArrayBinding, K, _>(
self.db,
txn,
StartKey::None,
ScanDirection::Forward,
&self.key_binding,
&value_binding,
|k, _v| k,
)?;
Ok(StoredIterator::from_vec(items))
}
pub fn clear(&self, txn: Option<&Transaction>) -> Result<()> {
if self.read_only {
return Err(CollectionError::ReadOnly);
}
let mut cursor = self.db.open_cursor(txn, None)?;
let mut key = noxu_db::DatabaseEntry::new();
let mut data = noxu_db::DatabaseEntry::new();
while let OperationStatus::Success =
cursor.get(&mut key, &mut data, noxu_db::Get::First, None)?
{
cursor.delete()?;
}
cursor.close()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use noxu_bind::IntBinding;
use noxu_db::{DatabaseConfig, Environment, EnvironmentConfig};
use tempfile::TempDir;
fn setup() -> (TempDir, Environment, noxu_db::Database) {
let td = TempDir::new().unwrap();
let env = Environment::open(
EnvironmentConfig::new(td.path().to_path_buf())
.with_allow_create(true)
.with_transactional(true),
)
.unwrap();
let db = env
.open_database(
None,
"kset",
&DatabaseConfig::new()
.with_allow_create(true)
.with_transactional(true),
)
.unwrap();
(td, env, db)
}
#[test]
fn add_and_contains() {
let (_td, _env, db) = setup();
let set: StoredKeySet<'_, i32, _> = StoredKeySet::new(&db, IntBinding);
assert!(set.add(None, &1).unwrap());
assert!(!set.add(None, &1).unwrap()); assert!(set.contains(None, &1).unwrap());
assert!(!set.contains(None, &2).unwrap());
}
#[test]
fn remove_returns_presence() {
let (_td, _env, db) = setup();
let set: StoredKeySet<'_, i32, _> = StoredKeySet::new(&db, IntBinding);
set.add(None, &1).unwrap();
assert!(set.remove(None, &1).unwrap());
assert!(!set.remove(None, &1).unwrap()); assert!(!set.contains(None, &1).unwrap());
}
#[test]
fn iter_yields_keys_in_order() {
let (_td, _env, db) = setup();
let set: StoredKeySet<'_, i32, _> = StoredKeySet::new(&db, IntBinding);
for i in [3, 1, 2] {
set.add(None, &i).unwrap();
}
let keys: Vec<i32> =
set.iter(None).unwrap().map(Result::unwrap).collect();
assert_eq!(keys, vec![1, 2, 3]);
}
#[test]
fn clear_empties() {
let (_td, _env, db) = setup();
let set: StoredKeySet<'_, i32, _> = StoredKeySet::new(&db, IntBinding);
for i in 0..5 {
set.add(None, &i).unwrap();
}
assert_eq!(set.len(None).unwrap(), 5);
set.clear(None).unwrap();
assert_eq!(set.len(None).unwrap(), 0);
}
#[test]
fn participates_in_user_txn() {
let (_td, env, db) = setup();
let set: StoredKeySet<'_, i32, _> = StoredKeySet::new(&db, IntBinding);
let txn = env.begin_transaction(None).unwrap();
set.add(Some(&txn), &7).unwrap();
assert!(set.contains(Some(&txn), &7).unwrap());
txn.abort().unwrap();
assert!(!set.contains(None, &7).unwrap());
}
#[test]
fn read_only_rejects_writes() {
let (_td, _env, db) = setup();
let set: StoredKeySet<'_, i32, _> =
StoredKeySet::new_read_only(&db, IntBinding);
assert!(matches!(set.add(None, &1), Err(CollectionError::ReadOnly)));
assert!(matches!(set.remove(None, &1), Err(CollectionError::ReadOnly)));
assert!(matches!(set.clear(None), Err(CollectionError::ReadOnly)));
}
}