use std::marker::PhantomData;
use std::sync::Mutex;
use noxu_bind::EntryBinding;
use noxu_db::{Database, DatabaseEntry, Get, OperationStatus, Transaction};
use crate::error::{CollectionError, Result};
use crate::internal::{decode_value, encode_value};
use crate::stored_iterator::StoredIterator;
pub struct StoredList<'db, V, VB>
where
VB: EntryBinding<V>,
{
db: &'db Database,
value_binding: VB,
next_index: Mutex<usize>,
read_only: bool,
_marker: PhantomData<fn() -> V>,
}
impl<'db, V, VB> StoredList<'db, V, VB>
where
VB: EntryBinding<V>,
{
pub fn new(db: &'db Database, value_binding: VB) -> Self {
StoredList {
db,
value_binding,
next_index: Mutex::new(0),
read_only: false,
_marker: PhantomData,
}
}
pub fn open(db: &'db Database, value_binding: VB) -> Result<Self> {
Self::open_with_txn(db, value_binding, None)
}
pub fn open_with_txn(
db: &'db Database,
value_binding: VB,
txn: Option<&Transaction>,
) -> Result<Self> {
let mut cursor = db.open_cursor(txn, None)?;
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
let next = match cursor.get(&mut key, &mut data, Get::Last, None)? {
OperationStatus::Success => {
let bytes = key.get_data().unwrap_or(&[]);
if bytes.len() != 8 {
let _ = cursor.close();
return Err(CollectionError::IllegalState(format!(
"StoredList::open: largest key is {} bytes; \
expected an 8-byte big-endian index. Database \
was not produced by StoredList; use \
StoredList::new explicitly if this is intentional.",
bytes.len()
)));
}
let mut buf = [0u8; 8];
buf.copy_from_slice(bytes);
let last = u64::from_be_bytes(buf);
last.saturating_add(1) as usize
}
_ => 0,
};
cursor.close()?;
Ok(StoredList {
db,
value_binding,
next_index: Mutex::new(next),
read_only: false,
_marker: PhantomData,
})
}
pub fn into_read_only(mut self) -> Self {
self.read_only = true;
self
}
pub fn is_read_only(&self) -> bool {
self.read_only
}
pub fn database(&self) -> &'db Database {
self.db
}
pub fn value_binding(&self) -> &VB {
&self.value_binding
}
pub fn index_to_key(index: usize) -> [u8; 8] {
(index as u64).to_be_bytes()
}
pub fn next_index(&self) -> usize {
*self.next_index.lock().unwrap()
}
pub fn push(&self, txn: Option<&Transaction>, value: &V) -> Result<usize> {
if self.read_only {
return Err(CollectionError::ReadOnly);
}
let mut next = self.next_index.lock().unwrap();
let index = *next;
let key_bytes = Self::index_to_key(index);
let key_entry = DatabaseEntry::from_bytes(&key_bytes);
let value_entry = encode_value(&self.value_binding, value)?;
self.db.put(txn, &key_entry, &value_entry)?;
*next = index + 1;
Ok(index)
}
pub fn get(
&self,
txn: Option<&Transaction>,
index: usize,
) -> Result<Option<V>> {
let key_bytes = Self::index_to_key(index);
let key_entry = DatabaseEntry::from_bytes(&key_bytes);
let mut data_entry = DatabaseEntry::new();
match self.db.get(txn, &key_entry, &mut data_entry)? {
OperationStatus::Success => {
Ok(Some(decode_value(&self.value_binding, &data_entry)?))
}
_ => Ok(None),
}
}
pub fn pop(&self, txn: Option<&Transaction>) -> Result<Option<V>> {
if self.read_only {
return Err(CollectionError::ReadOnly);
}
let mut next = self.next_index.lock().unwrap();
if *next == 0 {
return Ok(None);
}
let index = *next - 1;
let key_bytes = Self::index_to_key(index);
let key_entry = DatabaseEntry::from_bytes(&key_bytes);
let mut data_entry = DatabaseEntry::new();
let val = match self.db.get(txn, &key_entry, &mut data_entry)? {
OperationStatus::Success => {
Some(decode_value(&self.value_binding, &data_entry)?)
}
_ => None,
};
if val.is_some() {
self.db.delete(txn, &key_entry)?;
*next = index;
}
Ok(val)
}
pub fn remove(
&self,
txn: Option<&Transaction>,
index: usize,
) -> Result<Option<V>> {
if self.read_only {
return Err(CollectionError::ReadOnly);
}
let mut next = self.next_index.lock().unwrap();
if index >= *next {
return Ok(None);
}
let target_key_bytes = Self::index_to_key(index);
let target_key = DatabaseEntry::from_bytes(&target_key_bytes);
let mut target_data = DatabaseEntry::new();
let removed = match self.db.get(txn, &target_key, &mut target_data)? {
OperationStatus::Success => {
Some(decode_value(&self.value_binding, &target_data)?)
}
_ => None,
};
let high = *next; for src in (index + 1)..high {
let dst = src - 1;
let src_key_bytes = Self::index_to_key(src);
let dst_key_bytes = Self::index_to_key(dst);
let src_key = DatabaseEntry::from_bytes(&src_key_bytes);
let dst_key = DatabaseEntry::from_bytes(&dst_key_bytes);
let mut src_data = DatabaseEntry::new();
match self.db.get(txn, &src_key, &mut src_data)? {
OperationStatus::Success => {
let payload = src_data.get_data().unwrap_or(&[]).to_vec();
let dst_value = DatabaseEntry::from_vec(payload);
self.db.put(txn, &dst_key, &dst_value)?;
self.db.delete(txn, &src_key)?;
}
_ => {
self.db.delete(txn, &dst_key)?;
}
}
}
if index == high.saturating_sub(1) && removed.is_some() {
self.db.delete(txn, &target_key)?;
}
if removed.is_some() {
*next = high - 1;
}
Ok(removed)
}
pub fn len(&self, _txn: Option<&Transaction>) -> Result<usize> {
Ok(*self.next_index.lock().unwrap())
}
pub fn is_empty(&self, txn: Option<&Transaction>) -> Result<bool> {
Ok(self.len(txn)? == 0)
}
pub fn iter(&self, txn: Option<&Transaction>) -> Result<StoredIterator<V>> {
use crate::internal::{ScanDirection, StartKey, scan_records};
use noxu_bind::ByteArrayBinding;
let key_binding = ByteArrayBinding;
let items = scan_records::<Vec<u8>, V, ByteArrayBinding, VB, V, _>(
self.db,
txn,
StartKey::None,
ScanDirection::Forward,
&key_binding,
&self.value_binding,
|_k, v| v,
)?;
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 = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
while let OperationStatus::Success =
cursor.get(&mut key, &mut data, Get::First, None)?
{
cursor.delete()?;
}
cursor.close()?;
*self.next_index.lock().unwrap() = 0;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use noxu_bind::StringBinding;
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,
"list",
&DatabaseConfig::new().with_allow_create(true),
)
.unwrap();
(td, env, db)
}
#[test]
fn push_and_get() {
let (_td, _env, db) = setup();
let list: StoredList<'_, String, _> =
StoredList::new(&db, StringBinding);
assert_eq!(list.push(None, &"a".to_string()).unwrap(), 0);
assert_eq!(list.push(None, &"b".to_string()).unwrap(), 1);
assert_eq!(list.get(None, 0).unwrap(), Some("a".to_string()));
assert_eq!(list.get(None, 1).unwrap(), Some("b".to_string()));
assert_eq!(list.get(None, 99).unwrap(), None);
assert_eq!(list.len(None).unwrap(), 2);
}
#[test]
fn pop_returns_last() {
let (_td, _env, db) = setup();
let list: StoredList<'_, String, _> =
StoredList::new(&db, StringBinding);
list.push(None, &"a".to_string()).unwrap();
list.push(None, &"b".to_string()).unwrap();
assert_eq!(list.pop(None).unwrap(), Some("b".to_string()));
assert_eq!(list.next_index(), 1);
assert_eq!(list.pop(None).unwrap(), Some("a".to_string()));
assert_eq!(list.pop(None).unwrap(), None);
}
#[test]
fn remove_compacts_no_gaps() {
let (_td, _env, db) = setup();
let list: StoredList<'_, String, _> =
StoredList::new(&db, StringBinding);
for i in 0..10 {
list.push(None, &format!("v{i}")).unwrap();
}
assert_eq!(list.next_index(), 10);
for current_idx in 1..=5 {
let removed = list.remove(None, current_idx).unwrap();
assert!(
removed.is_some(),
"remove({}) must return Some",
current_idx,
);
}
assert_eq!(list.len(None).unwrap(), 5);
assert_eq!(list.next_index(), 5);
let collected: Vec<Option<String>> =
(0..5).map(|i| list.get(None, i).unwrap()).collect();
assert!(
collected.iter().all(|v| v.is_some()),
"expected dense list, got {:?}",
collected,
);
let expected = vec![
"v0".to_string(),
"v2".to_string(),
"v4".to_string(),
"v6".to_string(),
"v8".to_string(),
];
let actual: Vec<String> =
collected.into_iter().map(Option::unwrap).collect();
assert_eq!(actual, expected);
let via_iter: Vec<String> =
list.iter(None).unwrap().map(Result::unwrap).collect();
assert_eq!(via_iter, expected);
}
#[test]
fn remove_at_head_compacts() {
let (_td, _env, db) = setup();
let list: StoredList<'_, String, _> =
StoredList::new(&db, StringBinding);
for i in 0..3 {
list.push(None, &format!("v{i}")).unwrap();
}
let removed = list.remove(None, 0).unwrap();
assert_eq!(removed, Some("v0".to_string()));
assert_eq!(list.get(None, 0).unwrap(), Some("v1".to_string()));
assert_eq!(list.get(None, 1).unwrap(), Some("v2".to_string()));
assert_eq!(list.get(None, 2).unwrap(), None);
assert_eq!(list.next_index(), 2);
}
#[test]
fn remove_at_tail_compacts() {
let (_td, _env, db) = setup();
let list: StoredList<'_, String, _> =
StoredList::new(&db, StringBinding);
for i in 0..3 {
list.push(None, &format!("v{i}")).unwrap();
}
let removed = list.remove(None, 2).unwrap();
assert_eq!(removed, Some("v2".to_string()));
assert_eq!(list.get(None, 0).unwrap(), Some("v0".to_string()));
assert_eq!(list.get(None, 1).unwrap(), Some("v1".to_string()));
assert_eq!(list.get(None, 2).unwrap(), None);
assert_eq!(list.next_index(), 2);
}
#[test]
fn remove_out_of_range_returns_none() {
let (_td, _env, db) = setup();
let list: StoredList<'_, String, _> =
StoredList::new(&db, StringBinding);
list.push(None, &"a".to_string()).unwrap();
assert_eq!(list.remove(None, 5).unwrap(), None);
assert_eq!(list.next_index(), 1);
}
#[test]
fn remove_compaction_under_user_txn_is_atomic() {
let (_td, env, db) = setup();
let list: StoredList<'_, String, _> =
StoredList::new(&db, StringBinding);
for i in 0..5 {
list.push(None, &format!("v{i}")).unwrap();
}
let txn = env.begin_transaction(None).unwrap();
let removed = list.remove(Some(&txn), 1).unwrap();
assert_eq!(removed, Some("v1".to_string()));
assert_eq!(list.get(Some(&txn), 0).unwrap(), Some("v0".to_string()));
assert_eq!(list.get(Some(&txn), 1).unwrap(), Some("v2".to_string()));
txn.abort().unwrap();
let recovered =
StoredList::<String, _>::open(&db, StringBinding).unwrap();
assert_eq!(recovered.next_index(), 5);
assert_eq!(recovered.get(None, 1).unwrap(), Some("v1".to_string()));
}
#[test]
fn open_recovers_next_index_after_reopen() {
let td = TempDir::new().unwrap();
let path = td.path().to_path_buf();
{
let env = Environment::open(
EnvironmentConfig::new(path.clone()).with_allow_create(true),
)
.unwrap();
let db = env
.open_database(
None,
"reopen",
&DatabaseConfig::new().with_allow_create(true),
)
.unwrap();
let list: StoredList<'_, String, _> =
StoredList::new(&db, StringBinding);
for i in 0..3 {
list.push(None, &format!("v{i}")).unwrap();
}
let _ = db.close();
}
{
let env = Environment::open(
EnvironmentConfig::new(path).with_allow_create(true),
)
.unwrap();
let db = env
.open_database(
None,
"reopen",
&DatabaseConfig::new().with_allow_create(true),
)
.unwrap();
let list: StoredList<'_, String, _> =
StoredList::open(&db, StringBinding).unwrap();
assert_eq!(list.next_index(), 3);
assert_eq!(list.push(None, &"v3".to_string()).unwrap(), 3);
assert_eq!(
list.get(None, 0).unwrap(),
Some("v0".to_string()),
"existing entries must survive reopen",
);
}
}
#[test]
fn open_rejects_mixed_use_database() {
let (_td, _env, db) = setup();
let key = DatabaseEntry::from_bytes(b"not-an-index");
let val = DatabaseEntry::from_bytes(b"v");
db.put(None, &key, &val).unwrap();
let err = StoredList::<String, _>::open(&db, StringBinding)
.err()
.expect("open must fail");
assert!(matches!(err, CollectionError::IllegalState(_)));
}
#[test]
fn read_only_rejects_writes() {
let (_td, _env, db) = setup();
let list =
StoredList::<String, _>::new(&db, StringBinding).into_read_only();
assert!(matches!(
list.push(None, &"x".to_string()),
Err(CollectionError::ReadOnly)
));
assert!(matches!(list.pop(None), Err(CollectionError::ReadOnly)));
assert!(matches!(list.remove(None, 0), Err(CollectionError::ReadOnly)));
assert!(matches!(list.clear(None), Err(CollectionError::ReadOnly)));
}
#[test]
fn iter_yields_values_in_index_order() {
let (_td, _env, db) = setup();
let list: StoredList<'_, String, _> =
StoredList::new(&db, StringBinding);
for i in 0..5 {
list.push(None, &format!("v{i}")).unwrap();
}
let values: Vec<String> =
list.iter(None).unwrap().map(Result::unwrap).collect();
assert_eq!(
values,
vec![
"v0".to_string(),
"v1".to_string(),
"v2".to_string(),
"v3".to_string(),
"v4".to_string(),
],
);
}
}