diff --git a/CLAUDE.md b/CLAUDE.md index 752117b..1580fb5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,6 +7,14 @@ working, or if the notes become outdated. Rust bindings for libmdbx (MDBX database). Crate name: `signet-libmdbx`. +## Crate Mandates + +- You MUST NOT expose raw pointers to MDBX types outside of unsafe modules. +- You MUST maintain zero-copy semantics for read operations in all new + interfaces. +- You MUST read and respect `SAFETY` comments throughout the codebase. +- You MUST NOT introduce new dependencies without approval. + ## MDBX Synchronization Model When making changes to this codebase you MUST remember and conform to the MDBX @@ -14,15 +22,6 @@ synchronization model for transactions and cursors. Access to raw pointers MUST be mediated via the `TxAccess` trait. The table below summarizes the transaction types and their access models. -| Transaction Type | Thread Safety | Access Model | enforced by Rust type system? | -| ---------------- | ------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -| Read-Only (RO) | !Sync + Send | Access MUST be totally ordered and non-concurrent | No | -| Read-Write (RW) | !Sync + !Send | Access MUST be totally ordered, and non-concurrent. Only the creating thread may manage TX lifecycle | No | -| Transaction | Sync + Send | Multi-threaded wrapper using Mutex and Arc to share a RO or RW transaction safely across threads | Yes, via synchronization wrappers | -| TxUnsync | !Sync + Send | Single-threaded RO transaction without synchronization overhead | Yes, via required &mut or & access | -| TxUnsync | !Sync + !Send | Single-threaded RW transaction without synchronization overhead | Yes, &self enforces via required ownership and !Send + !Sync bound | -| Cursors | [Inherited] | Cursors borrow a Tx. The cursor CANNOT outlive the tx, and must reap its pointer on drop | Yes, via lifetimes | - ## Key Types - `Environment` - Database environment (in `src/sys/environment.rs`) @@ -105,6 +104,7 @@ docker build -t mdbx-linux-tests . && docker run --rm mdbx-linux-tests ``` This SHOULD be run alongside local tests and linting, especially for changes that: + - Modify build configuration - Add new dependencies - Change platform-specific code diff --git a/Cargo.toml b/Cargo.toml index 301b5f5..fbb9726 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "signet-libmdbx" description = "Idiomatic and safe MDBX wrapper" -version = "0.6.0" +version = "0.7.0" edition = "2024" rust-version = "1.92" license = "MIT OR Apache-2.0" @@ -57,3 +57,7 @@ harness = false [[bench]] name = "transaction" harness = false + +[[bench]] +name = "iter" +harness = false diff --git a/benches/iter.rs b/benches/iter.rs new file mode 100644 index 0000000..0d1203c --- /dev/null +++ b/benches/iter.rs @@ -0,0 +1,122 @@ +#![allow(missing_docs)] +mod utils; + +use crate::utils::{create_ro_sync, create_ro_unsync}; +use criterion::{Criterion, criterion_group, criterion_main}; +use signet_libmdbx::{DatabaseFlags, Environment, WriteFlags}; +use std::hint::black_box; +use tempfile::{TempDir, tempdir}; + +const VALUE_SIZE: usize = 100; +const NUM_VALUES: u32 = 2000; + +const DB_NAME: &str = "dupfixed_bench"; + +/// Setup a DUPFIXED database with NUM_VALUES 100-byte values under a single key. +fn setup_dupfixed_db() -> (TempDir, Environment) { + let dir = tempdir().unwrap(); + let env = Environment::builder().set_max_dbs(1).open(dir.path()).unwrap(); + + let txn = env.begin_rw_unsync().unwrap(); + let db = + txn.create_db(Some(DB_NAME), DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED).unwrap(); + + // Insert NUM_VALUES with incrementing content + for i in 0..NUM_VALUES { + let mut value = [0u8; VALUE_SIZE]; + value[..4].copy_from_slice(&i.to_le_bytes()); + txn.put(db, b"key", value, WriteFlags::empty()).unwrap(); + } + txn.commit().unwrap(); + + (dir, env) +} + +/// Benchmark: iter_dupfixed (batched page fetching). +fn bench_iter_dupfixed(c: &mut Criterion) { + let (_dir, env) = setup_dupfixed_db(); + let txn = create_ro_unsync(&env); + let db = txn.open_db(Some(DB_NAME)).unwrap(); + + c.bench_function("unsync::iter::dupfixed::batched", |b| { + b.iter(|| { + let mut cursor = txn.cursor(db).unwrap(); + let mut count = 0u32; + for result in cursor.iter_dupfixed_start::<[u8; 3], VALUE_SIZE>().unwrap() { + let (_key, value) = result.unwrap(); + black_box(value); + count += 1; + } + assert_eq!(count, NUM_VALUES); + }) + }); +} + +/// Benchmark: simple next() iteration. +fn bench_iter_simple(c: &mut Criterion) { + let (_dir, env) = setup_dupfixed_db(); + let txn = create_ro_unsync(&env); + let db = txn.open_db(Some(DB_NAME)).unwrap(); + + c.bench_function("unsync::iter::dupfixed::simple_next", |b| { + b.iter(|| { + let mut cursor = txn.cursor(db).unwrap(); + let mut count = 0u32; + for result in cursor.iter_start::<[u8; 3], [u8; VALUE_SIZE]>().unwrap() { + let (_key, value) = result.unwrap(); + black_box(value); + count += 1; + } + assert_eq!(count, NUM_VALUES); + }) + }); +} + +/// Benchmark: iter_dupfixed (batched page fetching). +fn bench_iter_dupfixed_sync(c: &mut Criterion) { + let (_dir, env) = setup_dupfixed_db(); + let txn = create_ro_sync(&env); + let db = txn.open_db(Some(DB_NAME)).unwrap(); + + c.bench_function("sync::iter::dupfixed::batched", |b| { + b.iter(|| { + let mut cursor = txn.cursor(db).unwrap(); + let mut count = 0u32; + for result in cursor.iter_dupfixed_start::<[u8; 3], VALUE_SIZE>().unwrap() { + let (_key, value) = result.unwrap(); + black_box(value); + count += 1; + } + assert_eq!(count, NUM_VALUES); + }) + }); +} + +/// Benchmark: simple next() iteration. +fn bench_iter_simple_sync(c: &mut Criterion) { + let (_dir, env) = setup_dupfixed_db(); + let txn = create_ro_sync(&env); + let db = txn.open_db(Some(DB_NAME)).unwrap(); + + c.bench_function("sync::iter::dupfixed::simple_next", |b| { + b.iter(|| { + let mut cursor = txn.cursor(db).unwrap(); + let mut count = 0u32; + for result in cursor.iter_start::<[u8; 3], [u8; VALUE_SIZE]>().unwrap() { + let (_key, value) = result.unwrap(); + black_box(value); + count += 1; + } + assert_eq!(count, NUM_VALUES); + }) + }); +} + +criterion_group! { + name = benches; + config = Criterion::default(); + targets = bench_iter_dupfixed, bench_iter_simple, + bench_iter_dupfixed_sync, bench_iter_simple_sync, +} + +criterion_main!(benches); diff --git a/benches/utils.rs b/benches/utils.rs index a4da0e6..681c40f 100644 --- a/benches/utils.rs +++ b/benches/utils.rs @@ -4,7 +4,7 @@ use signet_libmdbx::{ Environment, WriteFlags, ffi::{MDBX_TXN_RDONLY, MDBX_env, MDBX_txn, mdbx_txn_begin_ex}, - tx::{RoTxSync, RoTxUnsync, RwTxSync, RwTxUnsync}, + tx::aliases::{RoTxSync, RoTxUnsync, RwTxSync, RwTxUnsync}, }; use std::ptr; use tempfile::{TempDir, tempdir}; diff --git a/src/lib.rs b/src/lib.rs index 46ed117..9e43782 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,9 +160,8 @@ pub mod sys; pub use sys::{Environment, EnvironmentBuilder, Geometry, Info, Stat}; pub mod tx; -pub use tx::{ - CommitLatency, Cursor, Database, Ro, RoSync, Rw, RwSync, TransactionKind, TxSync, TxUnsync, -}; +pub use tx::aliases::{TxSync, TxUnsync}; +pub use tx::{CommitLatency, Cursor, Database, Ro, RoSync, Rw, RwSync, TransactionKind}; #[cfg(test)] mod test { diff --git a/src/sys/environment.rs b/src/sys/environment.rs index 47b185a..7ce6926 100644 --- a/src/sys/environment.rs +++ b/src/sys/environment.rs @@ -3,7 +3,7 @@ use crate::{ error::{MdbxError, MdbxResult, ReadResult, mdbx_result}, flags::EnvironmentFlags, sys::txn_manager::{LifecycleHandle, RwSyncLifecycle}, - tx::{RoTxSync, RoTxUnsync, RwTxSync, RwTxUnsync}, + tx::aliases::{RoTxSync, RoTxUnsync, RwTxSync, RwTxUnsync}, }; use byteorder::{ByteOrder, NativeEndian}; use mem::size_of; diff --git a/src/tx/access.rs b/src/tx/access.rs index a9c973a..f0dec56 100644 --- a/src/tx/access.rs +++ b/src/tx/access.rs @@ -130,7 +130,7 @@ impl Drop for PtrUnsync { /// This type is used internally to manage transaction access in the [`TxSync`] /// transaction API. Users typically don't interact with this type directly. /// -/// [`TxSync`]: crate::tx::TxSync +/// [`TxSync`]: crate::tx::aliases::TxSync #[derive(Debug)] pub struct PtrSync { /// Raw pointer to the MDBX transaction. diff --git a/src/tx/aliases.rs b/src/tx/aliases.rs new file mode 100644 index 0000000..a374bbe --- /dev/null +++ b/src/tx/aliases.rs @@ -0,0 +1,126 @@ +//! Public type aliases for transactions, cursors, and iterators. + +use crate::{ + Ro, RoSync, Rw, RwSync, + tx::{ + PtrSync, PtrUnsync, + cursor::Cursor, + r#impl::Tx, + iter::{Iter, IterDupFixed, IterDupFixedOfKey}, + }, +}; +use std::{borrow::Cow, sync::Arc}; + +// --- Transaction aliases --- + +/// Transaction type for synchronized access. +pub type TxSync = Tx>; + +/// Transaction type for unsynchronized access. +pub type TxUnsync = Tx; + +/// A synchronized read-only transaction. +pub type RoTxSync = TxSync; + +/// A synchronized read-write transaction. +pub type RwTxSync = TxSync; + +/// An unsynchronized read-only transaction. +pub type RoTxUnsync = TxUnsync; + +/// An unsynchronized read-write transaction. +pub type RwTxUnsync = TxUnsync; + +// SAFETY: +// - RoTxSync and RwTxSync use Arc which is Send and Sync. +// - K::Cache is ALWAYS Send +// - TxMeta is ALWAYS Send +// - Moving an RO transaction between threads is safe as long as no concurrent +// access occurs, which is guaranteed by being !Sync. +// +// NB: Send is correctly derived for RoTxSync and RwTxSync UNTIL +// you unsafe impl Sync for RoTxUnsync below. This is a quirk I did not know +// about. +unsafe impl Send for RoTxSync {} +unsafe impl Send for RwTxSync {} +unsafe impl Send for RoTxUnsync {} + +// --- Cursor aliases --- + +/// A read-only cursor for a synchronized transaction. +pub type RoCursorSync<'tx> = Cursor<'tx, RoSync>; + +/// A read-write cursor for a synchronized transaction. +pub type RwCursorSync<'tx> = Cursor<'tx, RwSync>; + +/// A read-only cursor for an unsynchronized transaction. +pub type RoCursorUnsync<'tx> = Cursor<'tx, Ro>; + +/// A read-write cursor for an unsynchronized transaction. +pub type RwCursorUnsync<'tx> = Cursor<'tx, Rw>; + +// --- Iterator aliases --- + +/// Iterates over KV pairs in an MDBX database. +pub type IterKeyVals<'tx, 'cur, K, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = + Iter<'tx, 'cur, K, Key, Value, { ffi::MDBX_NEXT }>; + +/// An iterator over the key/value pairs in an MDBX `DUPSORT` with duplicate +/// keys, yielding the first value for each key. +/// +/// See the [`Iter`] documentation for more details. +pub type IterDupKeys<'tx, 'cur, K, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = + Iter<'tx, 'cur, K, Key, Value, { ffi::MDBX_NEXT_NODUP }>; + +/// An iterator over the key/value pairs in an MDBX `DUPSORT`, yielding each +/// duplicate value for a specific key. +pub type IterDupVals<'tx, 'cur, K, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = + Iter<'tx, 'cur, K, Key, Value, { ffi::MDBX_NEXT_DUP }>; + +/// A key-value iterator for a synchronized read-only transaction. +pub type RoIterSync<'tx, 'cur, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = + IterKeyVals<'tx, 'cur, RoSync, Key, Value>; + +/// A key-value iterator for a synchronized read-write transaction. +pub type RwIterSync<'tx, 'cur, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = + IterKeyVals<'tx, 'cur, RwSync, Key, Value>; + +/// A key-value iterator for an unsynchronized read-only transaction. +pub type RoIterUnsync<'tx, 'cur, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = + IterKeyVals<'tx, 'cur, Ro, Key, Value>; + +/// A key-value iterator for an unsynchronized read-write transaction. +pub type RwIterUnsync<'tx, 'cur, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = + IterKeyVals<'tx, 'cur, Rw, Key, Value>; + +/// A flattening DUPFIXED iterator for a synchronized read-only transaction. +pub type RoDupFixedIterSync<'tx, 'cur, Key = Cow<'tx, [u8]>, const VALUE_SIZE: usize = 0> = + IterDupFixed<'tx, 'cur, RoSync, Key, VALUE_SIZE>; + +/// A flattening DUPFIXED iterator for a synchronized read-write transaction. +pub type RwDupFixedIterSync<'tx, 'cur, Key = Cow<'tx, [u8]>, const VALUE_SIZE: usize = 0> = + IterDupFixed<'tx, 'cur, RwSync, Key, VALUE_SIZE>; + +/// A flattening DUPFIXED iterator for an unsynchronized read-only transaction. +pub type RoDupFixedIterUnsync<'tx, 'cur, Key = Cow<'tx, [u8]>, const VALUE_SIZE: usize = 0> = + IterDupFixed<'tx, 'cur, Ro, Key, VALUE_SIZE>; + +/// A flattening DUPFIXED iterator for an unsynchronized read-write transaction. +pub type RwDupFixedIterUnsync<'tx, 'cur, Key = Cow<'tx, [u8]>, const VALUE_SIZE: usize = 0> = + IterDupFixed<'tx, 'cur, Rw, Key, VALUE_SIZE>; + +/// A single-key DUPFIXED iterator for a synchronized read-only transaction. +pub type RoDupFixedIterOfKeySync<'tx, 'cur, const VALUE_SIZE: usize = 0> = + IterDupFixedOfKey<'tx, 'cur, RoSync, VALUE_SIZE>; + +/// A single-key DUPFIXED iterator for a synchronized read-write transaction. +pub type RwDupFixedIterOfKeySync<'tx, 'cur, const VALUE_SIZE: usize = 0> = + IterDupFixedOfKey<'tx, 'cur, RwSync, VALUE_SIZE>; + +/// A single-key DUPFIXED iterator for an unsynchronized read-only transaction. +pub type RoDupFixedIterOfKeyUnsync<'tx, 'cur, const VALUE_SIZE: usize = 0> = + IterDupFixedOfKey<'tx, 'cur, Ro, VALUE_SIZE>; + +/// A single-key DUPFIXED iterator for an unsynchronized read-write transaction. +pub type RwDupFixedIterOfKeyUnsync<'tx, 'cur, const VALUE_SIZE: usize = 0> = + IterDupFixedOfKey<'tx, 'cur, Rw, VALUE_SIZE>; diff --git a/src/tx/assertions.rs b/src/tx/assertions.rs index 6bc4e6d..b9ec034 100644 --- a/src/tx/assertions.rs +++ b/src/tx/assertions.rs @@ -9,7 +9,7 @@ #![allow(dead_code)] -use crate::flags::DatabaseFlags; +use crate::DatabaseFlags; /// Debug assertion that validates key size constraints. /// @@ -190,3 +190,23 @@ fn debug_assert_append_dup_order(flags: DatabaseFlags, last_dup: Option<&[u8]>, #[cfg(not(debug_assertions))] let _ = (flags, last_dup, new_data); } + +/// Debug assertion that validates DUP_SORT flag is set. +#[inline(always)] +#[track_caller] +pub(crate) fn debug_assert_dup_sort(flags: DatabaseFlags) { + debug_assert!( + flags.contains(DatabaseFlags::DUP_SORT), + "Operation requires DUP_SORT database flag" + ); +} + +/// Debug assertion that validates DUP_FIXED flag is set. +#[inline(always)] +#[track_caller] +pub(crate) fn debug_assert_dup_fixed(flags: DatabaseFlags) { + debug_assert!( + flags.contains(DatabaseFlags::DUP_FIXED), + "Operation requires DUP_FIXED database flag" + ); +} diff --git a/src/tx/cache.rs b/src/tx/cache.rs index 858a850..ff236bc 100644 --- a/src/tx/cache.rs +++ b/src/tx/cache.rs @@ -11,8 +11,8 @@ //! - [`SharedCache`]: A thread-safe cache using `Arc>` for //! synchronized transactions. //! -//! [`TxSync`]: crate::tx::TxSync -//! [`TxUnsync`]: crate::tx::TxUnsync +//! [`TxSync`]: crate::tx::aliases::TxSync +//! [`TxUnsync`]: crate::tx::aliases::TxUnsync use crate::Database; use parking_lot::RwLock; diff --git a/src/tx/cursor.rs b/src/tx/cursor.rs index 7c4672a..1fa315e 100644 --- a/src/tx/cursor.rs +++ b/src/tx/cursor.rs @@ -1,32 +1,25 @@ use crate::{ - Database, MdbxError, ReadResult, TableObject, TransactionKind, codec_try_optional, + Database, ReadError, ReadResult, TableObject, TransactionKind, codec_try_optional, error::{MdbxResult, mdbx_result}, flags::*, tx::{ - TxPtrAccess, assertions, - iter::{Iter, IterDup, IterDupVals, IterKeyVals}, + TxPtrAccess, + aliases::{IterDupVals, IterKeyVals}, + iter::{Iter, IterDup, IterDupFixed, IterDupFixedOfKey}, kind::WriteMarker, }, }; use ffi::{ MDBX_FIRST, MDBX_FIRST_DUP, MDBX_GET_BOTH, MDBX_GET_BOTH_RANGE, MDBX_GET_CURRENT, MDBX_GET_MULTIPLE, MDBX_LAST, MDBX_LAST_DUP, MDBX_NEXT, MDBX_NEXT_DUP, MDBX_NEXT_MULTIPLE, - MDBX_NEXT_NODUP, MDBX_PREV, MDBX_PREV_DUP, MDBX_PREV_MULTIPLE, MDBX_PREV_NODUP, MDBX_SET, - MDBX_SET_KEY, MDBX_SET_LOWERBOUND, MDBX_SET_RANGE, MDBX_cursor_op, + MDBX_NEXT_NODUP, MDBX_PREV, MDBX_PREV_DUP, MDBX_PREV_MULTIPLE, MDBX_PREV_NODUP, + MDBX_SEEK_AND_GET_MULTIPLE, MDBX_SET, MDBX_SET_KEY, MDBX_SET_LOWERBOUND, MDBX_SET_RANGE, + MDBX_cursor_op, }; use std::{ffi::c_void, fmt, marker::PhantomData, ptr}; -/// A read-only cursor for a synchronized transaction. -pub type RoCursorSync<'tx> = Cursor<'tx, crate::RoSync>; - -/// A read-write cursor for a synchronized transaction. -pub type RwCursorSync<'tx> = Cursor<'tx, crate::RwSync>; - -/// A read-only cursor for an unsynchronized transaction. -pub type RoCursorUnsync<'tx> = Cursor<'tx, crate::Ro>; - -/// A read-write cursor for an unsynchronized transaction. -pub type RwCursorUnsync<'tx> = Cursor<'tx, crate::Rw>; +#[cfg(debug_assertions)] +use crate::tx::assertions; /// A cursor for navigating the items within a database. /// @@ -117,26 +110,6 @@ where == ffi::MDBX_RESULT_TRUE } - /// Validates that the database has the DUP_SORT flag set. - #[inline(always)] - fn require_dup_sort(&self) -> MdbxResult<()> { - self.db - .flags() - .contains(DatabaseFlags::DUP_SORT) - .then_some(()) - .ok_or(MdbxError::RequiresDupSort) - } - - /// Validates that the database has the DUP_FIXED flag set. - #[inline(always)] - fn require_dup_fixed(&self) -> MdbxResult<()> { - self.db - .flags() - .contains(DatabaseFlags::DUP_FIXED) - .then_some(()) - .ok_or(MdbxError::RequiresDupFixed) - } - /// Retrieves a key/data pair from the cursor. Depending on the cursor op, /// the current key may be returned. fn get( @@ -226,39 +199,33 @@ where } /// [`DatabaseFlags::DUP_SORT`]-only: Position at first data item of current key. - /// - /// Returns [`MdbxError::RequiresDupSort`] if the database does not have the - /// [`DatabaseFlags::DUP_SORT`] flag set. pub fn first_dup(&mut self) -> ReadResult> where Value: TableObject<'tx>, { - self.require_dup_sort()?; + #[cfg(debug_assertions)] + assertions::debug_assert_dup_sort(self.db_flags()); self.get_value(None, None, MDBX_FIRST_DUP) } /// [`DatabaseFlags::DUP_SORT`]-only: Position at key/data pair. - /// - /// Returns [`MdbxError::RequiresDupSort`] if the database does not have the - /// [`DatabaseFlags::DUP_SORT`] flag set. pub fn get_both(&mut self, k: &[u8], v: &[u8]) -> ReadResult> where Value: TableObject<'tx>, { - self.require_dup_sort()?; + #[cfg(debug_assertions)] + assertions::debug_assert_dup_sort(self.db_flags()); self.get_value(Some(k), Some(v), MDBX_GET_BOTH) } /// [`DatabaseFlags::DUP_SORT`]-only: Position at given key and at first data greater than or /// equal to specified data. - /// - /// Returns [`MdbxError::RequiresDupSort`] if the database does not have the - /// [`DatabaseFlags::DUP_SORT`] flag set. pub fn get_both_range(&mut self, k: &[u8], v: &[u8]) -> ReadResult> where Value: TableObject<'tx>, { - self.require_dup_sort()?; + #[cfg(debug_assertions)] + assertions::debug_assert_dup_sort(self.db_flags()); self.get_value(Some(k), Some(v), MDBX_GET_BOTH_RANGE) } @@ -273,17 +240,33 @@ where /// [`DatabaseFlags::DUP_FIXED`]-only: Return up to a page of duplicate data items from current /// cursor position. Move cursor to prepare for [`Self::next_multiple()`]. - /// - /// Returns [`MdbxError::RequiresDupFixed`] if the database does not have the - /// [`DatabaseFlags::DUP_FIXED`] flag set. pub fn get_multiple(&mut self) -> ReadResult> where Value: TableObject<'tx>, { - self.require_dup_fixed()?; + #[cfg(debug_assertions)] + assertions::debug_assert_dup_fixed(self.db_flags()); self.get_value(None, None, MDBX_GET_MULTIPLE) } + /// [`DatabaseFlags::DUP_FIXED`]-only: Seek to the given key and return up to a page of + /// duplicate data items. Move cursor to prepare for [`Self::next_multiple()`]. + pub fn seek_and_get_multiple( + &mut self, + key: &[u8], + ) -> ReadResult> + where + Key: TableObject<'tx>, + Value: TableObject<'tx>, + { + #[cfg(debug_assertions)] + { + assertions::debug_assert_dup_fixed(self.db_flags()); + assertions::debug_assert_integer_key(self.db_flags(), key); + } + self.get_full(Some(key), None, MDBX_SEEK_AND_GET_MULTIPLE) + } + /// Position at last key/data item. pub fn last(&mut self) -> ReadResult> where @@ -294,14 +277,12 @@ where } /// [`DatabaseFlags::DUP_SORT`]-only: Position at last data item of current key. - /// - /// Returns [`MdbxError::RequiresDupSort`] if the database does not have the - /// [`DatabaseFlags::DUP_SORT`] flag set. pub fn last_dup(&mut self) -> ReadResult> where Value: TableObject<'tx>, { - self.require_dup_sort()?; + #[cfg(debug_assertions)] + assertions::debug_assert_dup_sort(self.db_flags()); self.get_value(None, None, MDBX_LAST_DUP) } @@ -316,29 +297,25 @@ where } /// [`DatabaseFlags::DUP_SORT`]-only: Position at next data item of current key. - /// - /// Returns [`MdbxError::RequiresDupSort`] if the database does not have the - /// [`DatabaseFlags::DUP_SORT`] flag set. pub fn next_dup(&mut self) -> ReadResult> where Key: TableObject<'tx>, Value: TableObject<'tx>, { - self.require_dup_sort()?; + #[cfg(debug_assertions)] + assertions::debug_assert_dup_sort(self.db_flags()); self.get_full(None, None, MDBX_NEXT_DUP) } /// [`DatabaseFlags::DUP_FIXED`]-only: Return up to a page of duplicate data items from next /// cursor position. Move cursor to prepare for `MDBX_NEXT_MULTIPLE`. - /// - /// Returns [`MdbxError::RequiresDupFixed`] if the database does not have the - /// [`DatabaseFlags::DUP_FIXED`] flag set. pub fn next_multiple(&mut self) -> ReadResult> where Key: TableObject<'tx>, Value: TableObject<'tx>, { - self.require_dup_fixed()?; + #[cfg(debug_assertions)] + assertions::debug_assert_dup_fixed(self.db_flags()); self.get_full(None, None, MDBX_NEXT_MULTIPLE) } @@ -361,15 +338,13 @@ where } /// [`DatabaseFlags::DUP_SORT`]-only: Position at previous data item of current key. - /// - /// Returns [`MdbxError::RequiresDupSort`] if the database does not have the - /// [`DatabaseFlags::DUP_SORT`] flag set. pub fn prev_dup(&mut self) -> ReadResult> where Key: TableObject<'tx>, Value: TableObject<'tx>, { - self.require_dup_sort()?; + #[cfg(debug_assertions)] + assertions::debug_assert_dup_sort(self.db_flags()); self.get_full(None, None, MDBX_PREV_DUP) } @@ -383,11 +358,14 @@ where } /// Position at specified key. + /// + /// For DupSort-ed databases, positions at first data item of the key. pub fn set(&mut self, key: &[u8]) -> ReadResult> where Value: TableObject<'tx>, { - assertions::debug_assert_integer_key(self.db.flags(), key); + #[cfg(debug_assertions)] + assertions::debug_assert_integer_key(self.db_flags(), key); self.get_value(Some(key), None, MDBX_SET) } @@ -397,7 +375,8 @@ where Key: TableObject<'tx>, Value: TableObject<'tx>, { - assertions::debug_assert_integer_key(self.db.flags(), key); + #[cfg(debug_assertions)] + assertions::debug_assert_integer_key(self.db_flags(), key); self.get_full(Some(key), None, MDBX_SET_KEY) } @@ -407,21 +386,20 @@ where Key: TableObject<'tx>, Value: TableObject<'tx>, { - assertions::debug_assert_integer_key(self.db.flags(), key); + #[cfg(debug_assertions)] + assertions::debug_assert_integer_key(self.db_flags(), key); self.get_full(Some(key), None, MDBX_SET_RANGE) } /// [`DatabaseFlags::DUP_FIXED`]-only: Position at previous page and return up to a page of /// duplicate data items. - /// - /// Returns [`MdbxError::RequiresDupFixed`] if the database does not have the - /// [`DatabaseFlags::DUP_FIXED`] flag set. pub fn prev_multiple(&mut self) -> ReadResult> where Key: TableObject<'tx>, Value: TableObject<'tx>, { - self.require_dup_fixed()?; + #[cfg(debug_assertions)] + assertions::debug_assert_dup_fixed(self.db_flags()); self.get_full(None, None, MDBX_PREV_MULTIPLE) } @@ -442,7 +420,8 @@ where Key: TableObject<'tx>, Value: TableObject<'tx>, { - assertions::debug_assert_integer_key(self.db.flags(), key); + #[cfg(debug_assertions)] + assertions::debug_assert_integer_key(self.db_flags(), key); let (k, v, found) = codec_try_optional!(self.get(Some(key), None, MDBX_SET_LOWERBOUND)); Ok(Some((found, k.unwrap(), v))) @@ -552,7 +531,7 @@ where Ok(None) | Err(_) => return IterDup::end_from_ref(self), } } - IterDup::from_ref(self) + IterDup::::from_ref(self) } /// Iterate over duplicate database items starting from the beginning of the @@ -584,7 +563,7 @@ where Value: TableObject<'tx>, { let Some(first) = self.set_range(key)? else { - return Ok(IterDup::end_from_ref(self)); + return Ok(IterDup::::end_from_ref(self)); }; Ok(IterDup::from_ref_with(self, first)) @@ -607,6 +586,185 @@ where Ok(IterDupVals::from_ref_with(self, first)) } + + /// [`DatabaseFlags::DUP_FIXED`]-only: Iterate over all fixed-size duplicate + /// values starting from the beginning of the database. + /// + /// This iterator efficiently fetches pages of fixed-size values and yields + /// them individually, providing a flattened view of the DUPFIXED table. + /// + /// The `VALUE_SIZE` const generic must match the fixed size of values in + /// the database. + /// + /// Returns [`crate::MdbxError::RequiresDupFixed`] if the database does not have the + /// [`DatabaseFlags::DUP_FIXED`] flag set. + /// + /// # Correctness + /// + /// The `VALUE_SIZE` const generic must exactly match the fixed value size + /// in the database. See [`IterDupFixed`] for details on mismatch behavior. + /// + /// # Example + /// + /// ```no_run + /// # use signet_libmdbx::{Environment, DatabaseFlags, WriteFlags}; + /// # use std::path::Path; + /// # let env = Environment::builder().open(Path::new("/tmp/ex")).unwrap(); + /// let txn = env.begin_ro_sync().unwrap(); + /// let db = txn.open_db(None).unwrap(); + /// let mut cursor = txn.cursor(db).unwrap(); + /// + /// // Iterate over 8-byte values + /// for result in cursor.iter_dupfixed_start::, 8>().unwrap() { + /// let (key, value) = result.unwrap(); + /// println!("{:?} => {:?}", key, value); + /// } + /// ``` + pub fn iter_dupfixed_start<'cur, Key, const VALUE_SIZE: usize>( + &'cur mut self, + ) -> ReadResult> + where + 'tx: 'cur, + Key: TableObject<'tx> + Clone, + { + #[cfg(debug_assertions)] + { + assertions::debug_assert_dup_fixed(self.db_flags()); + assert!(VALUE_SIZE > 0, "VALUE_SIZE must be non-zero"); + } + + // Position at first key + let Some((_key, _value)) = self.first::()? else { + return Ok(IterDupFixed::end_from_ref(self)); + }; + + // Get first page of values for current key + let Some(page) = self.get_multiple::>()? else { + return Ok(IterDupFixed::end_from_ref(self)); + }; + + // Re-fetch the key since get_multiple doesn't return it + let Some((key, _)) = self.get_current::()? else { + return Ok(IterDupFixed::end_from_ref(self)); + }; + + Ok(IterDupFixed::from_ref_with(self, key, page)) + } + + /// [`DatabaseFlags::DUP_FIXED`]-only: Iterate over all fixed-size duplicate + /// values starting from the given key or the first key greater than it. + /// + /// This iterator efficiently fetches pages of fixed-size values and yields + /// them individually, providing a flattened view of the DUPFIXED table. + /// + /// The `VALUE_SIZE` const generic must match the fixed size of values in + /// the database. + /// + /// # Correctness + /// + /// The `VALUE_SIZE` const generic must exactly match the fixed value size + /// in the database. See [`IterDupFixed`] for details on mismatch behavior. + /// + /// # Example + /// + /// ```no_run + /// # use signet_libmdbx::{Environment, DatabaseFlags, WriteFlags}; + /// # use std::path::Path; + /// # let env = Environment::builder().open(Path::new("/tmp/ex")).unwrap(); + /// let txn = env.begin_ro_sync().unwrap(); + /// let db = txn.open_db(None).unwrap(); + /// let mut cursor = txn.cursor(db).unwrap(); + /// + /// // Iterate over 8-byte values starting from key "start" + /// for result in cursor.iter_dupfixed_from::, 8>(b"start").unwrap() { + /// let (key, value) = result.unwrap(); + /// println!("{:?} => {:?}", key, value); + /// } + /// ``` + pub fn iter_dupfixed_from<'cur, Key, const VALUE_SIZE: usize>( + &'cur mut self, + key: &[u8], + ) -> ReadResult> + where + 'tx: 'cur, + Key: TableObject<'tx> + Clone, + { + #[cfg(debug_assertions)] + { + assertions::debug_assert_dup_fixed(self.db_flags()); + assert!(VALUE_SIZE > 0, "VALUE_SIZE must be non-zero"); + } + + // Position at first key >= the requested key + let Some((found_key, _)) = self.set_range::(key)? else { + return Ok(IterDupFixed::end_from_ref(self)); + }; + + // Get first page for this key + let Some(page) = self.get_multiple::>()? else { + return Ok(IterDupFixed::end_from_ref(self)); + }; + + Ok(IterDupFixed::from_ref_with(self, found_key, page)) + } + + /// [`DatabaseFlags::DUP_FIXED`]-only: Iterate over all fixed-size duplicate + /// values for a specific key. + /// + /// Unlike [`Self::iter_dupfixed_start`] and [`Self::iter_dupfixed_from`] + /// which iterate across all keys, this iterator only yields values for + /// the specified key. When all values for that key are exhausted, + /// iteration stops. + /// + /// The `VALUE_SIZE` const generic must match the fixed size of values in + /// the database. + /// + /// # Correctness + /// + /// The `VALUE_SIZE` const generic must exactly match the fixed value size + /// in the database. See [`IterDupFixedOfKey`] for details on mismatch + /// behavior. + /// + /// # Example + /// + /// ```no_run + /// # use signet_libmdbx::{Environment, DatabaseFlags, WriteFlags}; + /// # use std::path::Path; + /// # let env = Environment::builder().open(Path::new("/tmp/ex")).unwrap(); + /// let txn = env.begin_ro_sync().unwrap(); + /// let db = txn.open_db(None).unwrap(); + /// let mut cursor = txn.cursor(db).unwrap(); + /// + /// // Iterate over 8-byte values for a specific key + /// for result in cursor.iter_dupfixed_of::<8>(b"my_key").unwrap() { + /// let value: [u8; 8] = result.unwrap(); + /// println!("value: {:?}", value); + /// } + /// ``` + /// + /// [`IterDupFixedOfKey`]: crate::tx::iter::IterDupFixedOfKey + pub fn iter_dupfixed_of<'cur, const VALUE_SIZE: usize>( + &'cur mut self, + key: &[u8], + ) -> ReadResult> + where + 'tx: 'cur, + { + #[cfg(debug_assertions)] + { + assertions::debug_assert_dup_fixed(self.db_flags()); + assertions::debug_assert_integer_key(self.db_flags(), key); + assert!(VALUE_SIZE > 0, "VALUE_SIZE must be non-zero"); + } + + let Some((_key, page)) = + self.seek_and_get_multiple::<(), std::borrow::Cow<'tx, [u8]>>(key)? + else { + return Ok(IterDupFixedOfKey::end_from_ref(self)); + }; + + Ok(IterDupFixedOfKey::from_ref_with(self, page)) + } } impl<'tx, K: TransactionKind + WriteMarker> Cursor<'tx, K> { @@ -640,13 +798,7 @@ impl<'tx, K: TransactionKind + WriteMarker> Cursor<'tx, K> { .map(drop) } - /// Deletes the current key/data pair. - /// - /// ### Flags - /// - /// [`WriteFlags::NO_DUP_DATA`] may be used to delete all data items for the - /// current key, if the database was opened with [`DatabaseFlags::DUP_SORT`]. - pub fn del(&mut self, flags: WriteFlags) -> MdbxResult<()> { + fn del_inner(&mut self, flags: WriteFlags) -> MdbxResult<()> { mdbx_result( self.access .with_txn_ptr(|_| unsafe { ffi::mdbx_cursor_del(self.cursor, flags.bits()) }), @@ -654,6 +806,52 @@ impl<'tx, K: TransactionKind + WriteMarker> Cursor<'tx, K> { .map(drop) } + /// Deletes the current key/data pair. + /// + /// In order to delete all data items for a key in a + /// [`DatabaseFlags::DUP_SORT`] database, see [`Cursor::del_all_dups`]. + pub fn del(&mut self) -> MdbxResult<()> { + self.del_inner(WriteFlags::CURRENT) + } + + /// Deletes all duplicate data items for the current key. + /// + /// This is a [`DatabaseFlags::DUP_SORT`]-only operation that efficiently + /// removes all values associated with the current key in a single call. + /// + /// The cursor must be positioned at a valid key before calling this method. + /// After deletion, the cursor position is undefined. + pub fn del_all_dups(&mut self) -> MdbxResult<()> { + #[cfg(debug_assertions)] + assertions::debug_assert_dup_sort(self.db_flags()); + self.del_inner(WriteFlags::ALLDUPS) + } + + /// Delete all duplicate data items for the specified key. + /// + /// This is a [`DatabaseFlags::DUP_SORT`]-only operation that efficiently + /// removes all values associated with the given key in a single call. + /// + /// If the key does not exist, no action is taken. + pub fn del_all_dups_of(&mut self, key: &[u8]) -> MdbxResult<()> { + #[cfg(debug_assertions)] + assertions::debug_assert_dup_sort(self.db_flags()); + + // Position at the key. Convert the error to MdbxResult. + let found = self.set::<()>(key).map_err(|e| match e { + ReadError::Mdbx(e) => e, + _ => unreachable!("() can always be decoded"), + })?; + + if found.is_none() { + // Key not found, nothing to delete + return Ok(()); + } + + // Delete all duplicates for the current key + self.del_inner(WriteFlags::ALLDUPS) + } + /// Appends a key/data pair to the end of the database. /// /// The key must be greater than all existing keys (or less than, for @@ -700,13 +898,11 @@ impl<'tx, K: TransactionKind + WriteMarker> Cursor<'tx, K> { /// than, for [`DatabaseFlags::REVERSE_DUP`] tables). This is more efficient /// than [`Cursor::put`] when adding duplicates in sorted order. /// - /// Returns [`MdbxError::RequiresDupSort`] if the database does not have the - /// [`DatabaseFlags::DUP_SORT`] flag set. - /// /// In debug builds, this method asserts that the data ordering constraint /// is satisfied. pub fn append_dup(&mut self, key: &[u8], data: &[u8]) -> MdbxResult<()> { - self.require_dup_sort()?; + #[cfg(debug_assertions)] + assertions::debug_assert_dup_sort(self.db_flags()); let key_val: ffi::MDBX_val = ffi::MDBX_val { iov_len: key.len(), iov_base: key.as_ptr() as *mut c_void }; diff --git a/src/tx/impl.rs b/src/tx/impl.rs index 9e9b609..70e3513 100644 --- a/src/tx/impl.rs +++ b/src/tx/impl.rs @@ -3,10 +3,11 @@ use crate::{ Ro, Rw, Stat, TableObject, TransactionKind, WriteFlags, error::mdbx_result, sys::txn_manager::{Begin, Commit, CommitLatencyPtr, RawTxPtr}, + tx::aliases::{RoTxSync, RoTxUnsync, RwTxUnsync}, tx::{ PtrSync, PtrUnsync, TxPtrAccess, cache::{Cache, CachedDb}, - kind::{RoSync, RwSync, SyncKind, WriteMarker, WriterKind}, + kind::{RoSync, SyncKind, WriteMarker, WriterKind}, ops, }, }; @@ -22,41 +23,8 @@ use std::{ }; use tracing::{debug_span, instrument, warn}; -/// Transaction type for synchronized access. -pub type TxSync = Tx>; - -/// Transaction type for unsynchronized access. -pub type TxUnsync = Tx; - -/// A synchronized read-only transaction. -pub type RoTxSync = TxSync; - -/// A synchronized read-write transaction. -pub type RwTxSync = TxSync; - -/// An unsynchronized read-only transaction. -pub type RoTxUnsync = TxUnsync; - -// SAFETY: -// - RoTxSync and RwTxSync use Arc which is Send and Sync. -// - K::Cache is ALWAYS Send -// - TxMeta is ALWAYS Send -// - Moving an RO transaction between threads is safe as long as no concurrent -// access occurs, which is guaranteed by being !Sync. -// -// NB: Send is correctly derived for RoTxSync and RwTxSync UNTIL -// you unsafe impl Sync for RoTxUnsync below. This is a quirk I did not know -// about. -unsafe impl Send for RoTxSync {} -unsafe impl Send for RwTxSync {} -unsafe impl Send for RoTxUnsync {} - -// // SAFETY: RoTxUnsync cannot be shared between threads, but can be moved. -// // This satisfies MDBX's requirements for read-only transactions. -// unsafe impl Send for RoTxUnsync {} - -/// An unsynchronized read-write transaction. -pub type RwTxUnsync = TxUnsync; +#[cfg(debug_assertions)] +use crate::tx::assertions; /// Meta-data for a transaction. #[derive(Clone)] @@ -73,8 +41,12 @@ impl fmt::Debug for TxMeta { /// An MDBX transaction. /// -/// Prefer using the [`TxSync`] or [`TxUnsync`] type aliases, unless -/// specifically implementing generic code over all four transaction kinds. +/// Prefer using the [`TxSync`] or +/// [`TxUnsync`] type aliases, unless specifically +/// implementing generic code over all four transaction kinds. +/// +/// [`TxSync`]: crate::tx::aliases::TxSync +/// [`TxUnsync`]: crate::tx::aliases::TxUnsync pub struct Tx::Access> { txn: U, @@ -330,7 +302,7 @@ impl Tx { /// /// The key must be greater than all existing keys (or less than, for /// [`DatabaseFlags::REVERSE_KEY`] tables). This is more efficient than - /// [`TxSync::put`] when adding data in sorted order. + /// [`Tx::put`] when adding data in sorted order. /// /// In debug builds, this method asserts that the key ordering constraint is /// satisfied. @@ -359,10 +331,7 @@ impl Tx { /// /// The data must be greater than all existing data for this key (or less /// than, for [`DatabaseFlags::REVERSE_DUP`] tables). This is more efficient - /// than [`TxSync::put`] when adding duplicates in sorted order. - /// - /// Returns [`MdbxError::RequiresDupSort`] if the database does not have the - /// [`DatabaseFlags::DUP_SORT`] flag set. + /// than [`Tx::put`] when adding duplicates in sorted order. /// /// In debug builds, this method asserts that the data ordering constraint /// is satisfied. @@ -372,9 +341,9 @@ impl Tx { key: impl AsRef<[u8]>, data: impl AsRef<[u8]>, ) -> MdbxResult<()> { - if !db.flags().contains(DatabaseFlags::DUP_SORT) { - return Err(MdbxError::RequiresDupSort); - } + #[cfg(debug_assertions)] + assertions::debug_assert_dup_sort(db.flags()); + let key = key.as_ref(); let data = data.as_ref(); @@ -431,7 +400,7 @@ impl Tx { /// Reserves space for a value of the given length at the given key, and /// calls the given closure with a mutable slice to write into. /// - /// This is a safe wrapper around [`TxSync::reserve`]. + /// This is a safe wrapper around [`Tx::reserve`]. pub fn with_reservation( &self, db: Database, @@ -611,7 +580,7 @@ impl Tx> where K: TransactionKind> + WriteMarker, { - /// Begins a new [`RwTxSync`] transaction. + /// Begins a new [`RwTxSync`](crate::tx::aliases::RwTxSync) transaction. pub fn begin(env: Environment) -> MdbxResult { let mut warned = false; let txn = loop { @@ -686,6 +655,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::tx::aliases::{RoTxSync, RwTxSync, TxUnsync}; use tempfile::tempdir; #[test] diff --git a/src/tx/iter.rs b/src/tx/iter.rs deleted file mode 100644 index 96636b6..0000000 --- a/src/tx/iter.rs +++ /dev/null @@ -1,394 +0,0 @@ -//! Iterator types for traversing MDBX databases. -//! -//! This module provides lending iterators over key-value pairs in MDBX -//! databases. The iterators support both borrowed and owned access patterns. -//! -//! # Iterator Types -//! -//! - [`Iter`]: Base iterator with configurable cursor operation -//! - [`IterKeyVals`]: Iterates over all key-value pairs (`MDBX_NEXT`) -//! - [`IterDupKeys`]: For `DUPSORT` databases, yields first value per key -//! - [`IterDupVals`]: For `DUPSORT` databases, yields all values for one key -//! - [`IterDup`]: Nested iteration over `DUPSORT` databases -//! -//! # Borrowing vs Owning -//! -//! Iterators provide two ways to access data: -//! -//! - [`borrow_next()`](Iter::borrow_next): Returns data potentially borrowed -//! from the database. Requires the `Key` and `Value` types to implement -//! [`TableObject<'tx>`](crate::TableObject). This can avoid allocations -//! when using `Cow<'tx, [u8]>`. -//! -//! - [`owned_next()`](Iter::owned_next): Returns owned data. Requires -//! [`TableObjectOwned`]. Always safe but may allocate. -//! -//! The standard [`Iterator`] trait is implemented via `owned_next()`. -//! -//! # Dirty Page Handling -//! -//! In read-write transactions, database pages may be "dirty" (modified but -//! not yet committed). The behavior of `Cow<[u8]>` depends on the -//! `return-borrowed` feature: -//! -//! - **With `return-borrowed`**: Always returns `Cow::Borrowed`, even for -//! dirty pages. This is faster but the data may change if the transaction -//! modifies it later. -//! -//! - **Without `return-borrowed`** (default): Dirty pages are copied to -//! `Cow::Owned`. This is safer but allocates more. -//! -//! # Example -//! -//! ```no_run -//! # use signet_libmdbx::Environment; -//! # use std::path::Path; -//! # let env = Environment::builder().open(Path::new("/tmp/iter_example")).unwrap(); -//! let txn = env.begin_ro_sync().unwrap(); -//! let db = txn.open_db(None).unwrap(); -//! let mut cursor = txn.cursor(db).unwrap(); -//! -//! // Iterate using the standard Iterator trait (owned) -//! for result in cursor.iter_start::, Vec>().unwrap() { -//! let (key, value) = result.expect("decode error"); -//! println!("{:?} => {:?}", key, value); -//! } -//! ``` - -use crate::{ - Cursor, MdbxError, ReadResult, TableObject, TableObjectOwned, TransactionKind, - error::mdbx_result, tx::TxPtrAccess, -}; -use std::{borrow::Cow, marker::PhantomData, ptr}; - -/// A key-value iterator for a synchronized read-only transaction. -pub type RoIterSync<'tx, 'cur, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = - IterKeyVals<'tx, 'cur, crate::RoSync, Key, Value>; - -/// A key-value iterator for a synchronized read-write transaction. -pub type RwIterSync<'tx, 'cur, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = - IterKeyVals<'tx, 'cur, crate::RwSync, Key, Value>; - -/// A key-value iterator for an unsynchronized read-only transaction. -pub type RoIterUnsync<'tx, 'cur, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = - IterKeyVals<'tx, 'cur, crate::Ro, Key, Value>; - -/// A key-value iterator for an unsynchronized read-write transaction. -pub type RwIterUnsync<'tx, 'cur, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = - IterKeyVals<'tx, 'cur, crate::Rw, Key, Value>; - -/// Iterates over KV pairs in an MDBX database. -pub type IterKeyVals<'tx, 'cur, K, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = - Iter<'tx, 'cur, K, Key, Value, { ffi::MDBX_NEXT }>; - -/// An iterator over the key/value pairs in an MDBX `DUPSORT` with duplicate -/// keys, yielding the first value for each key. -/// -/// See the [`Iter`] documentation for more details. -pub type IterDupKeys<'tx, 'cur, K, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = - Iter<'tx, 'cur, K, Key, Value, { ffi::MDBX_NEXT_NODUP }>; - -/// An iterator over the key/value pairs in an MDBX `DUPSORT`, yielding each -/// duplicate value for a specific key. -pub type IterDupVals<'tx, 'cur, K, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> = - Iter<'tx, 'cur, K, Key, Value, { ffi::MDBX_NEXT_DUP }>; - -/// An iterator over the key/value pairs in an MDBX database. -/// -/// The iteration order is determined by the `OP` const generic parameter. -/// Usually -/// -/// This is a lending iterator, meaning that the key and values are borrowed -/// from the underlying cursor when possible. This allows for more efficient -/// iteration without unnecessary allocations, and can be used to create -/// deserializing iterators and other higher-level abstractions. -/// -/// Whether borrowing is possible depends on the implementation of -/// [`TableObject`] for both `Key` and `Value`. -pub struct Iter< - 'tx, - 'cur, - K: TransactionKind, - Key = Cow<'tx, [u8]>, - Value = Cow<'tx, [u8]>, - const OP: u32 = { ffi::MDBX_NEXT }, -> { - cursor: Cow<'cur, Cursor<'tx, K>>, - /// Pre-fetched value from cursor positioning, yielded before calling FFI. - pending: Option<(Key, Value)>, - /// When true, the iterator is exhausted and will always return `None`. - exhausted: bool, - _marker: PhantomData (Key, Value)>, -} - -impl core::fmt::Debug for Iter<'_, '_, K, Key, Value, OP> -where - K: TransactionKind, - Key: core::fmt::Debug, - Value: core::fmt::Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Iter").finish() - } -} - -impl<'tx: 'cur, 'cur, K, Key, Value, const OP: u32> Iter<'tx, 'cur, K, Key, Value, OP> -where - K: TransactionKind, -{ - /// Create a new iterator from the given cursor, starting at the given - /// position. - pub(crate) fn new(cursor: Cow<'cur, Cursor<'tx, K>>) -> Self { - Iter { cursor, pending: None, exhausted: false, _marker: PhantomData } - } - - /// Create a new iterator from a mutable reference to the given cursor, - pub(crate) fn from_ref(cursor: &'cur mut Cursor<'tx, K>) -> Self { - Self::new(Cow::Borrowed(cursor)) - } - - /// Create a new iterator that is already exhausted. - /// - /// Iteration will immediately return `None`. - pub(crate) fn new_end(cursor: Cow<'cur, Cursor<'tx, K>>) -> Self { - Iter { cursor, pending: None, exhausted: true, _marker: PhantomData } - } - - /// Create a new, exhausted iterator from a mutable reference to the given - /// cursor. This is usually used as a placeholder when no items are to be - /// yielded. - pub(crate) fn end_from_ref(cursor: &'cur mut Cursor<'tx, K>) -> Self { - Self::new_end(Cow::Borrowed(cursor)) - } - - /// Create a new iterator from the given cursor, first yielding the - /// provided key/value pair. - pub(crate) fn new_with(cursor: Cow<'cur, Cursor<'tx, K>>, first: (Key, Value)) -> Self { - Iter { cursor, pending: Some(first), exhausted: false, _marker: PhantomData } - } - - /// Create a new iterator from a mutable reference to the given cursor, - /// first yielding the provided key/value pair. - pub(crate) fn from_ref_with(cursor: &'cur mut Cursor<'tx, K>, first: (Key, Value)) -> Self { - Self::new_with(Cow::Borrowed(cursor), first) - } - - /// Create a new iterator from an owned cursor, first yielding the - /// provided key/value pair. - pub(crate) fn from_owned_with(cursor: Cursor<'tx, K>, first: (Key, Value)) -> Self { - Self::new_with(Cow::Owned(cursor), first) - } -} - -impl Iter<'_, '_, K, Key, Value, OP> -where - K: TransactionKind, - Key: TableObjectOwned, - Value: TableObjectOwned, -{ - /// Own the next key/value pair from the iterator. - pub fn owned_next(&mut self) -> ReadResult> { - if self.exhausted { - return Ok(None); - } - if let Some(v) = self.pending.take() { - return Ok(Some(v)); - } - self.execute_op() - } -} - -impl<'tx: 'cur, 'cur, K, Key, Value, const OP: u32> Iter<'tx, 'cur, K, Key, Value, OP> -where - K: TransactionKind, - Key: TableObject<'tx>, - Value: TableObject<'tx>, -{ - /// Execute the MDBX operation and decode the result. - /// - /// Returns `Ok(Some((key, value)))` if a key/value pair was found, - /// `Ok(None)` if no more key/value pairs are available, or `Err` on error. - fn execute_op(&self) -> ReadResult> { - let mut key = ffi::MDBX_val { iov_len: 0, iov_base: ptr::null_mut() }; - let mut data = ffi::MDBX_val { iov_len: 0, iov_base: ptr::null_mut() }; - - self.cursor.access().with_txn_ptr(|txn| { - let res = - unsafe { ffi::mdbx_cursor_get(self.cursor.cursor(), &mut key, &mut data, OP) }; - - match res { - ffi::MDBX_SUCCESS => { - // SAFETY: decode_val checks for dirty writes and copies if needed. - // The lifetime 'tx guarantees the Cow cannot outlive the transaction. - unsafe { - let key = TableObject::decode_val::(txn, key)?; - let data = TableObject::decode_val::(txn, data)?; - Ok(Some((key, data))) - } - } - ffi::MDBX_NOTFOUND | ffi::MDBX_ENODATA | ffi::MDBX_RESULT_TRUE => Ok(None), - other => Err(MdbxError::from_err_code(other).into()), - } - }) - } - - /// Borrow the next key/value pair from the iterator. - /// - /// Returns `Ok(Some((key, value)))` if a key/value pair was found, - /// `Ok(None)` if no more key/value pairs are available, or `Err` on DB - /// access error. - pub fn borrow_next(&mut self) -> ReadResult> { - if self.exhausted { - return Ok(None); - } - if let Some(v) = self.pending.take() { - return Ok(Some(v)); - } - self.execute_op() - } -} - -impl Iterator for Iter<'_, '_, K, Key, Value, OP> -where - K: TransactionKind, - Key: TableObjectOwned, - Value: TableObjectOwned, -{ - type Item = ReadResult<(Key, Value)>; - - fn next(&mut self) -> Option { - self.owned_next().transpose() - } -} - -/// An iterator over the key/value pairs in an MDBX database with duplicate -/// keys. -pub struct IterDup<'tx, 'cur, K: TransactionKind, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> { - inner: IterDupKeys<'tx, 'cur, K, Key, Value>, -} - -impl<'tx, 'cur, K, Key, Value> core::fmt::Debug for IterDup<'tx, 'cur, K, Key, Value> -where - K: TransactionKind, - Key: core::fmt::Debug, - Value: core::fmt::Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("IterDup").finish() - } -} - -impl<'tx, 'cur, K, Key, Value> IterDup<'tx, 'cur, K, Key, Value> -where - K: TransactionKind, -{ - /// Create a new iterator from the given cursor, starting at the given - /// position. - pub(crate) fn new(cursor: Cow<'cur, Cursor<'tx, K>>) -> Self { - IterDup { inner: IterDupKeys::new(cursor) } - } - - /// Create a new iterator from a mutable reference to the given cursor, - pub(crate) fn from_ref(cursor: &'cur mut Cursor<'tx, K>) -> Self { - Self::new(Cow::Borrowed(cursor)) - } - - /// Create a new iterator from an owned cursor. - pub fn from_owned(cursor: Cursor<'tx, K>) -> Self { - Self::new(Cow::Owned(cursor)) - } - - /// Create a new iterator from the given cursor, the inner iterator will - /// first yield the provided key/value pair. - pub(crate) fn new_with(cursor: Cow<'cur, Cursor<'tx, K>>, first: (Key, Value)) -> Self { - IterDup { inner: Iter::new_with(cursor, first) } - } - - /// Create a new iterator from a mutable reference to the given cursor, - /// first yielding the provided key/value pair. - pub fn from_ref_with(cursor: &'cur mut Cursor<'tx, K>, first: (Key, Value)) -> Self { - Self::new_with(Cow::Borrowed(cursor), first) - } - - /// Create a new iterator from the given cursor, with no items to yield. - pub fn new_end(cursor: Cow<'cur, Cursor<'tx, K>>) -> Self { - IterDup { inner: Iter::new_end(cursor) } - } - - /// Create a new iterator from a mutable reference to the given cursor, with - /// no items to yield. - pub fn end_from_ref(cursor: &'cur mut Cursor<'tx, K>) -> Self { - Self::new_end(Cow::Borrowed(cursor)) - } - - /// Create a new iterator from an owned cursor, with no items to yield. - pub fn end_from_owned(cursor: Cursor<'tx, K>) -> Self { - Self::new_end(Cow::Owned(cursor)) - } -} - -impl<'tx: 'cur, 'cur, K, Key, Value> IterDup<'tx, 'cur, K, Key, Value> -where - K: TransactionKind, - Key: TableObject<'tx>, - Value: TableObject<'tx>, -{ - /// Borrow the next key/value pair from the iterator. - pub fn borrow_next(&mut self) -> ReadResult>> { - // We want to use Cursor::new_at_position to create a new cursor, - // but the kv pair may be borrowed from the inner cursor, so we need to - // store the references first. This is just to avoid borrow checker - // issues in the unsafe block. - let cursor_ptr = self.inner.cursor.as_ref().cursor(); - - // SAFETY: the access lives as long as self.inner.cursor, and the cursor op - // we perform does not invalidate the data borrowed from the inner - // cursor in borrow_next. - let access = self.inner.cursor.access(); - - // The next will be the FIRST KV pair for the NEXT key in the DUPSORT - match self.inner.borrow_next()? { - Some((key, value)) => { - // SAFETY: the access is valid as per above. The FFI calls here do - // not invalidate any data borrowed from the inner cursor. - // - // This is an inlined version of Cursor::new_at_position. - let db = self.inner.cursor.as_ref().db(); - let dup_cursor = access.with_txn_ptr(move |_| unsafe { - let new_cursor = ffi::mdbx_cursor_create(ptr::null_mut()); - let res = ffi::mdbx_cursor_copy(cursor_ptr, new_cursor); - mdbx_result(res)?; - Ok::<_, MdbxError>(Cursor::new_raw(access, new_cursor, db)) - })?; - - Ok(Some(IterDupVals::from_owned_with(dup_cursor, (key, value)))) - } - None => Ok(None), - } - } -} - -impl<'tx: 'cur, 'cur, K, Key, Value> IterDup<'tx, 'cur, K, Key, Value> -where - K: TransactionKind, - Key: TableObjectOwned, - Value: TableObjectOwned, -{ - /// Own the next key/value pair from the iterator. - pub fn owned_next(&mut self) -> ReadResult>> { - self.borrow_next() - } -} - -impl<'tx: 'cur, 'cur, K, Key, Value> Iterator for IterDup<'tx, 'cur, K, Key, Value> -where - K: TransactionKind, - Key: TableObjectOwned, - Value: TableObjectOwned, -{ - type Item = ReadResult>; - - fn next(&mut self) -> Option { - self.owned_next().transpose() - } -} diff --git a/src/tx/iter/base.rs b/src/tx/iter/base.rs new file mode 100644 index 0000000..baa3c1d --- /dev/null +++ b/src/tx/iter/base.rs @@ -0,0 +1,174 @@ +//! Base iterator implementation for MDBX cursors. + +use crate::{ + Cursor, MdbxError, ReadResult, TableObject, TableObjectOwned, TransactionKind, tx::TxPtrAccess, +}; +use std::{borrow::Cow, marker::PhantomData, ptr}; + +/// An iterator over the key/value pairs in an MDBX database. +/// +/// The iteration order is determined by the `OP` const generic parameter. +/// Usually +/// +/// This is a lending iterator, meaning that the key and values are borrowed +/// from the underlying cursor when possible. This allows for more efficient +/// iteration without unnecessary allocations, and can be used to create +/// deserializing iterators and other higher-level abstractions. +/// +/// Whether borrowing is possible depends on the implementation of +/// [`TableObject`] for both `Key` and `Value`. +pub struct Iter< + 'tx, + 'cur, + K: TransactionKind, + Key = Cow<'tx, [u8]>, + Value = Cow<'tx, [u8]>, + const OP: u32 = { ffi::MDBX_NEXT }, +> { + pub(crate) cursor: Cow<'cur, Cursor<'tx, K>>, + /// Pre-fetched value from cursor positioning, yielded before calling FFI. + pending: Option<(Key, Value)>, + /// When true, the iterator is exhausted and will always return `None`. + exhausted: bool, + _marker: PhantomData (Key, Value)>, +} + +impl core::fmt::Debug for Iter<'_, '_, K, Key, Value, OP> +where + K: TransactionKind, + Key: core::fmt::Debug, + Value: core::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Iter").finish() + } +} + +impl<'tx: 'cur, 'cur, K, Key, Value, const OP: u32> Iter<'tx, 'cur, K, Key, Value, OP> +where + K: TransactionKind, +{ + /// Create a new iterator from the given cursor, starting at the given + /// position. + pub(crate) fn new(cursor: Cow<'cur, Cursor<'tx, K>>) -> Self { + Iter { cursor, pending: None, exhausted: false, _marker: PhantomData } + } + + /// Create a new iterator from a mutable reference to the given cursor, + pub(crate) fn from_ref(cursor: &'cur mut Cursor<'tx, K>) -> Self { + Self::new(Cow::Borrowed(cursor)) + } + + /// Create a new iterator that is already exhausted. + /// + /// Iteration will immediately return `None`. + pub(crate) fn new_end(cursor: Cow<'cur, Cursor<'tx, K>>) -> Self { + Iter { cursor, pending: None, exhausted: true, _marker: PhantomData } + } + + /// Create a new, exhausted iterator from a mutable reference to the given + /// cursor. This is usually used as a placeholder when no items are to be + /// yielded. + pub(crate) fn end_from_ref(cursor: &'cur mut Cursor<'tx, K>) -> Self { + Self::new_end(Cow::Borrowed(cursor)) + } + + /// Create a new iterator from the given cursor, first yielding the + /// provided key/value pair. + pub(crate) fn new_with(cursor: Cow<'cur, Cursor<'tx, K>>, first: (Key, Value)) -> Self { + Iter { cursor, pending: Some(first), exhausted: false, _marker: PhantomData } + } + + /// Create a new iterator from a mutable reference to the given cursor, + /// first yielding the provided key/value pair. + pub(crate) fn from_ref_with(cursor: &'cur mut Cursor<'tx, K>, first: (Key, Value)) -> Self { + Self::new_with(Cow::Borrowed(cursor), first) + } + + /// Create a new iterator from an owned cursor, first yielding the + /// provided key/value pair. + pub(crate) fn from_owned_with(cursor: Cursor<'tx, K>, first: (Key, Value)) -> Self { + Self::new_with(Cow::Owned(cursor), first) + } +} + +impl Iter<'_, '_, K, Key, Value, OP> +where + K: TransactionKind, + Key: TableObjectOwned, + Value: TableObjectOwned, +{ + /// Own the next key/value pair from the iterator. + pub fn owned_next(&mut self) -> ReadResult> { + if self.exhausted { + return Ok(None); + } + if let Some(v) = self.pending.take() { + return Ok(Some(v)); + } + self.execute_op() + } +} + +impl<'tx: 'cur, 'cur, K, Key, Value, const OP: u32> Iter<'tx, 'cur, K, Key, Value, OP> +where + K: TransactionKind, + Key: TableObject<'tx>, + Value: TableObject<'tx>, +{ + /// Execute the MDBX operation and decode the result. + /// + /// Returns `Ok(Some((key, value)))` if a key/value pair was found, + /// `Ok(None)` if no more key/value pairs are available, or `Err` on error. + fn execute_op(&self) -> ReadResult> { + let mut key = ffi::MDBX_val { iov_len: 0, iov_base: ptr::null_mut() }; + let mut data = ffi::MDBX_val { iov_len: 0, iov_base: ptr::null_mut() }; + + self.cursor.access().with_txn_ptr(|txn| { + let res = + unsafe { ffi::mdbx_cursor_get(self.cursor.cursor(), &mut key, &mut data, OP) }; + + match res { + ffi::MDBX_SUCCESS => { + // SAFETY: decode_val checks for dirty writes and copies if needed. + // The lifetime 'tx guarantees the Cow cannot outlive the transaction. + unsafe { + let key = TableObject::decode_val::(txn, key)?; + let data = TableObject::decode_val::(txn, data)?; + Ok(Some((key, data))) + } + } + ffi::MDBX_NOTFOUND | ffi::MDBX_ENODATA | ffi::MDBX_RESULT_TRUE => Ok(None), + other => Err(MdbxError::from_err_code(other).into()), + } + }) + } + + /// Borrow the next key/value pair from the iterator. + /// + /// Returns `Ok(Some((key, value)))` if a key/value pair was found, + /// `Ok(None)` if no more key/value pairs are available, or `Err` on DB + /// access error. + pub fn borrow_next(&mut self) -> ReadResult> { + if self.exhausted { + return Ok(None); + } + if let Some(v) = self.pending.take() { + return Ok(Some(v)); + } + self.execute_op() + } +} + +impl Iterator for Iter<'_, '_, K, Key, Value, OP> +where + K: TransactionKind, + Key: TableObjectOwned, + Value: TableObjectOwned, +{ + type Item = ReadResult<(Key, Value)>; + + fn next(&mut self) -> Option { + self.owned_next().transpose() + } +} diff --git a/src/tx/iter/dup.rs b/src/tx/iter/dup.rs new file mode 100644 index 0000000..97ed489 --- /dev/null +++ b/src/tx/iter/dup.rs @@ -0,0 +1,144 @@ +//! Iterator for DUPSORT databases with nested iteration. + +use crate::{ + Cursor, MdbxError, ReadResult, TableObject, TableObjectOwned, TransactionKind, + error::mdbx_result, + tx::{ + TxPtrAccess, + aliases::{IterDupKeys, IterDupVals}, + iter::Iter, + }, +}; +use std::{borrow::Cow, ptr}; + +/// An iterator over the key/value pairs in an MDBX database with duplicate +/// keys. +pub struct IterDup<'tx, 'cur, K: TransactionKind, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> { + inner: IterDupKeys<'tx, 'cur, K, Key, Value>, +} + +impl<'tx, 'cur, K, Key, Value> core::fmt::Debug for IterDup<'tx, 'cur, K, Key, Value> +where + K: TransactionKind, + Key: core::fmt::Debug, + Value: core::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IterDup").finish() + } +} + +impl<'tx: 'cur, 'cur, K, Key, Value> IterDup<'tx, 'cur, K, Key, Value> +where + K: TransactionKind, +{ + /// Create a new iterator from the given cursor, starting at the given + /// position. + pub(crate) fn new(cursor: Cow<'cur, Cursor<'tx, K>>) -> Self { + IterDup { inner: IterDupKeys::new(cursor) } + } + + /// Create a new iterator from a mutable reference to the given cursor, + pub(crate) fn from_ref(cursor: &'cur mut Cursor<'tx, K>) -> Self { + Self::new(Cow::Borrowed(cursor)) + } + + /// Create a new iterator from an owned cursor. + pub fn from_owned(cursor: Cursor<'tx, K>) -> Self { + Self::new(Cow::Owned(cursor)) + } + + /// Create a new iterator from the given cursor, the inner iterator will + /// first yield the provided key/value pair. + pub(crate) fn new_with(cursor: Cow<'cur, Cursor<'tx, K>>, first: (Key, Value)) -> Self { + IterDup { inner: Iter::new_with(cursor, first) } + } + + /// Create a new iterator from a mutable reference to the given cursor, + /// first yielding the provided key/value pair. + pub fn from_ref_with(cursor: &'cur mut Cursor<'tx, K>, first: (Key, Value)) -> Self { + Self::new_with(Cow::Borrowed(cursor), first) + } + + /// Create a new iterator from the given cursor, with no items to yield. + pub fn new_end(cursor: Cow<'cur, Cursor<'tx, K>>) -> Self { + IterDup { inner: Iter::new_end(cursor) } + } + + /// Create a new iterator from a mutable reference to the given cursor, with + /// no items to yield. + pub fn end_from_ref(cursor: &'cur mut Cursor<'tx, K>) -> Self { + Self::new_end(Cow::Borrowed(cursor)) + } + + /// Create a new iterator from an owned cursor, with no items to yield. + pub fn end_from_owned(cursor: Cursor<'tx, K>) -> Self { + Self::new_end(Cow::Owned(cursor)) + } +} + +impl<'tx, 'cur, K, Key, Value> IterDup<'tx, 'cur, K, Key, Value> +where + K: TransactionKind, + Key: TableObject<'tx>, + Value: TableObject<'tx>, +{ + /// Borrow the next key/value pair from the iterator. + pub fn borrow_next(&mut self) -> ReadResult>> { + // We want to use Cursor::new_at_position to create a new cursor, + // but the kv pair may be borrowed from the inner cursor, so we need to + // store the references first. This is just to avoid borrow checker + // issues in the unsafe block. + let cursor_ptr = self.inner.cursor.as_ref().cursor(); + + // SAFETY: the access lives as long as self.inner.cursor, and the cursor op + // we perform does not invalidate the data borrowed from the inner + // cursor in borrow_next. + let access = self.inner.cursor.access(); + + // The next will be the FIRST KV pair for the NEXT key in the DUPSORT + match self.inner.borrow_next()? { + Some((key, value)) => { + // SAFETY: the access is valid as per above. The FFI calls here do + // not invalidate any data borrowed from the inner cursor. + // + // This is an inlined version of Cursor::new_at_position. + let db = self.inner.cursor.as_ref().db(); + let dup_cursor = access.with_txn_ptr(move |_| unsafe { + let new_cursor = ffi::mdbx_cursor_create(ptr::null_mut()); + let res = ffi::mdbx_cursor_copy(cursor_ptr, new_cursor); + mdbx_result(res)?; + Ok::<_, MdbxError>(Cursor::new_raw(access, new_cursor, db)) + })?; + + Ok(Some(IterDupVals::from_owned_with(dup_cursor, (key, value)))) + } + None => Ok(None), + } + } +} + +impl<'tx: 'cur, 'cur, K, Key, Value> IterDup<'tx, 'cur, K, Key, Value> +where + K: TransactionKind, + Key: TableObjectOwned, + Value: TableObjectOwned, +{ + /// Own the next key/value pair from the iterator. + pub fn owned_next(&mut self) -> ReadResult>> { + self.borrow_next() + } +} + +impl<'tx: 'cur, 'cur, K, Key, Value> Iterator for IterDup<'tx, 'cur, K, Key, Value> +where + K: TransactionKind, + Key: TableObjectOwned, + Value: TableObjectOwned, +{ + type Item = ReadResult>; + + fn next(&mut self) -> Option { + self.owned_next().transpose() + } +} diff --git a/src/tx/iter/dupfixed.rs b/src/tx/iter/dupfixed.rs new file mode 100644 index 0000000..0890ace --- /dev/null +++ b/src/tx/iter/dupfixed.rs @@ -0,0 +1,274 @@ +//! Flattening iterator for DUPFIXED tables. + +use crate::{Cursor, ReadResult, TableObject, TransactionKind}; +use std::{borrow::Cow, marker::PhantomData}; + +/// A flattening iterator over DUPFIXED tables. +/// +/// This iterator efficiently iterates over DUPFIXED tables by fetching pages +/// of fixed-size values and yielding them individually. DUPFIXED databases +/// store duplicate values with a fixed size, allowing MDBX to pack multiple +/// values per page. +/// +/// # Type Parameters +/// +/// - `'tx`: The transaction lifetime +/// - `'cur`: The cursor lifetime +/// - `K`: The transaction kind marker +/// - `Key`: The key type (must implement [`TableObject`]) +/// - `VALUE_SIZE`: The fixed size of each value in bytes +/// +/// # Correctness +/// +/// The `VALUE_SIZE` const generic **must** match the fixed value size stored +/// in the database. MDBX does not validate this at runtime. If mismatched: +/// +/// - **Too large**: Values are skipped; the iterator yields fewer items than +/// exist, potentially with misaligned data. +/// - **Too small**: The iterator yields more items than exist, each containing +/// partial or corrupted data from adjacent values. +/// - **Zero**: Causes an infinite loop (caught by debug assertion). +/// +/// The correct value size is determined by the size of values written to the +/// DUPFIXED database. All values under a given key must have the same size. +/// +/// # Zero-Copy Operation +/// +/// When possible, this iterator avoids copying data: +/// - In read-only transactions, values are borrowed directly from memory-mapped pages +/// - In read-write transactions with clean pages, values are also borrowed +/// - Only dirty pages (modified but not committed) require copying +/// +/// # Example +/// +/// ```no_run +/// # use signet_libmdbx::{Environment, DatabaseFlags, WriteFlags}; +/// # use std::path::Path; +/// # let env = Environment::builder().open(Path::new("/tmp/dupfixed_example")).unwrap(); +/// // Create a DUPFIXED database +/// let txn = env.begin_rw_sync().unwrap(); +/// let db = txn.create_db(Some("my_cool_db"), DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED).unwrap(); +/// +/// // Insert fixed-size values (4 bytes each) +/// txn.put(db, b"key", &1u32.to_le_bytes(), WriteFlags::empty()).unwrap(); +/// txn.put(db, b"key", &2u32.to_le_bytes(), WriteFlags::empty()).unwrap(); +/// txn.put(db, b"key", &3u32.to_le_bytes(), WriteFlags::empty()).unwrap(); +/// txn.commit().unwrap(); +/// +/// // Iterate over values +/// let txn = env.begin_ro_sync().unwrap(); +/// let db = txn.open_db(Some("my_cool_db")).unwrap(); +/// let mut cursor = txn.cursor(db).unwrap(); +/// +/// for result in cursor.iter_dupfixed_start::, 4>().unwrap() { +/// let (key, value) = result.unwrap(); +/// let num = u32::from_le_bytes(value); +/// println!("{:?} => {}", key, num); +/// } +/// ``` +pub struct IterDupFixed< + 'tx, + 'cur, + K: TransactionKind, + Key = Cow<'tx, [u8]>, + const VALUE_SIZE: usize = 0, +> { + cursor: Cow<'cur, Cursor<'tx, K>>, + /// The current key being iterated. + current_key: Option, + /// The current page of values. + current_page: Cow<'tx, [u8]>, + /// Current offset into the page, incremented as values are yielded. + page_offset: usize, + /// When true, the iterator is exhausted and will always return `None`. + exhausted: bool, + _marker: PhantomData Key>, +} + +impl core::fmt::Debug for IterDupFixed<'_, '_, K, Key, VALUE_SIZE> +where + K: TransactionKind, + Key: core::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let remaining = self.current_page.len().saturating_sub(self.page_offset) / VALUE_SIZE; + f.debug_struct("IterDupFixed") + .field("exhausted", &self.exhausted) + .field("remaining_in_page", &remaining) + .finish() + } +} + +impl<'tx: 'cur, 'cur, K, Key, const VALUE_SIZE: usize> IterDupFixed<'tx, 'cur, K, Key, VALUE_SIZE> +where + K: TransactionKind, +{ + /// Create a new, exhausted iterator. + /// + /// Iteration will immediately return `None`. + pub(crate) fn new_end(cursor: Cow<'cur, Cursor<'tx, K>>) -> Self { + IterDupFixed { + cursor, + current_key: None, + current_page: Cow::Borrowed(&[]), + page_offset: 0, + exhausted: true, + _marker: PhantomData, + } + } + + /// Create a new, exhausted iterator from a mutable reference to the cursor. + pub(crate) fn end_from_ref(cursor: &'cur mut Cursor<'tx, K>) -> Self { + Self::new_end(Cow::Borrowed(cursor)) + } + + /// Create a new iterator with the given initial key and page. + pub(crate) fn new_with( + cursor: Cow<'cur, Cursor<'tx, K>>, + key: Key, + page: Cow<'tx, [u8]>, + ) -> Self { + IterDupFixed { + cursor, + current_key: Some(key), + current_page: page, + page_offset: 0, + exhausted: false, + _marker: PhantomData, + } + } + + /// Create a new iterator from a mutable reference with initial key and page. + pub(crate) fn from_ref_with( + cursor: &'cur mut Cursor<'tx, K>, + key: Key, + page: Cow<'tx, [u8]>, + ) -> Self { + Self::new_with(Cow::Borrowed(cursor), key, page) + } +} + +impl<'tx: 'cur, 'cur, K, Key, const VALUE_SIZE: usize> IterDupFixed<'tx, 'cur, K, Key, VALUE_SIZE> +where + K: TransactionKind, + Key: TableObject<'tx> + Clone, +{ + /// Consume the next value from the current page. + /// + /// Returns `Some(Cow<'tx, [u8]>)` containing exactly `VALUE_SIZE` bytes, + /// or `None` if the page is exhausted. + fn consume_value(&mut self) -> Option> { + let end = self.page_offset.checked_add(VALUE_SIZE)?; + if end > self.current_page.len() { + return None; + } + + let start = self.page_offset; + self.page_offset = end; + + match &self.current_page { + Cow::Borrowed(slice) => Some(Cow::Borrowed(&slice[start..end])), + Cow::Owned(vec) => Some(Cow::Owned(vec[start..end].to_vec())), + } + } + + /// Fetch the next page of values. + /// + /// First tries `next_multiple` to get more pages for the current key. + /// If that fails, moves to the next key with `next_nodup` and fetches + /// its first page with `get_multiple`. + /// + /// Returns `Ok(true)` if a new page was fetched, `Ok(false)` if exhausted. + fn fetch_next_page(&mut self) -> ReadResult { + let cursor = self.cursor.to_mut(); + + // Try to get next page for current key + if let Some((key, page)) = cursor.next_multiple::>()? { + self.current_key = Some(key); + self.current_page = page; + self.page_offset = 0; + return Ok(true); + } + + // No more pages for current key, move to next key + if cursor.next_nodup::()?.is_none() { + self.exhausted = true; + return Ok(false); + } + + // Get first page for new key + let Some(page) = cursor.get_multiple::>()? else { + self.exhausted = true; + return Ok(false); + }; + + // Re-fetch the key since get_multiple doesn't return it + let Some((key, _)) = cursor.get_current::()? else { + self.exhausted = true; + return Ok(false); + }; + + self.current_key = Some(key); + self.current_page = page; + self.page_offset = 0; + Ok(true) + } + + /// Borrow the next key/value pair from the iterator. + /// + /// Returns `Ok(Some((key, value)))` where: + /// - `key` is cloned from the current key + /// - `value` is a `Cow<'tx, [u8]>` of exactly `VALUE_SIZE` bytes + /// + /// Returns `Ok(None)` when the iterator is exhausted. + pub fn borrow_next(&mut self) -> ReadResult)>> { + if self.exhausted { + return Ok(None); + } + + // Try to consume from current page + if let Some(value) = self.consume_value() { + // Key is cloned for each value - cheap for Cow<[u8]>, may allocate + // for decoded types + let key = self.current_key.clone().expect("key should be set when page is non-empty"); + return Ok(Some((key, value))); + } + + // Current page exhausted, fetch next page + if !self.fetch_next_page()? { + return Ok(None); + } + + // Consume first value from new page + let value = self.consume_value().expect("freshly fetched page should have values"); + let key = self.current_key.clone().expect("key should be set after fetch"); + Ok(Some((key, value))) + } + + /// Get the next key/value pair as owned data. + /// + /// Returns `Ok(Some((key, [u8; VALUE_SIZE])))` where the value is copied + /// into a fixed-size array. + pub fn owned_next(&mut self) -> ReadResult> { + self.borrow_next().map(|opt| { + opt.map(|(key, value)| { + let mut arr = [0u8; VALUE_SIZE]; + arr.copy_from_slice(&value); + (key, arr) + }) + }) + } +} + +impl<'tx: 'cur, 'cur, K, Key, const VALUE_SIZE: usize> Iterator + for IterDupFixed<'tx, 'cur, K, Key, VALUE_SIZE> +where + K: TransactionKind, + Key: TableObject<'tx> + Clone, +{ + type Item = ReadResult<(Key, [u8; VALUE_SIZE])>; + + fn next(&mut self) -> Option { + self.owned_next().transpose() + } +} diff --git a/src/tx/iter/dupfixed_key.rs b/src/tx/iter/dupfixed_key.rs new file mode 100644 index 0000000..480bacc --- /dev/null +++ b/src/tx/iter/dupfixed_key.rs @@ -0,0 +1,199 @@ +//! Single-key flattening iterator for DUPFIXED tables. + +use crate::{Cursor, ReadResult, TransactionKind}; +use std::{borrow::Cow, marker::PhantomData}; + +/// A single-key flattening iterator over DUPFIXED tables. +/// +/// Unlike [`IterDupFixed`](super::IterDupFixed) which iterates across all keys, +/// this iterator only yields values for a single key. When all values for that +/// key are exhausted, iteration stops. +/// +/// # Type Parameters +/// +/// - `'tx`: The transaction lifetime +/// - `'cur`: The cursor lifetime +/// - `K`: The transaction kind marker +/// - `VALUE_SIZE`: The fixed size of each value in bytes +/// +/// # Correctness +/// +/// The `VALUE_SIZE` const generic **must** match the fixed value size stored +/// in the database. MDBX does not validate this at runtime. If mismatched: +/// +/// - **Too large**: Values are skipped; the iterator yields fewer items than +/// exist, potentially with misaligned data. +/// - **Too small**: The iterator yields more items than exist, each containing +/// partial or corrupted data from adjacent values. +/// - **Zero**: Causes an infinite loop (caught by debug assertion). +/// +/// The correct value size is determined by the size of values written to the +/// DUPFIXED database. All values under a given key must have the same size. +/// +/// # Zero-Copy Operation +/// +/// When possible, this iterator avoids copying data: +/// - In read-only transactions, values are borrowed directly from memory-mapped pages +/// - In read-write transactions with clean pages, values are also borrowed +/// - Only dirty pages (modified but not committed) require copying +pub struct IterDupFixedOfKey<'tx, 'cur, K: TransactionKind, const VALUE_SIZE: usize = 0> { + cursor: Cow<'cur, Cursor<'tx, K>>, + /// The current page of values. + current_page: Cow<'tx, [u8]>, + /// Current offset into the page, incremented as values are yielded. + page_offset: usize, + /// When true, the iterator is exhausted and will always return `None`. + exhausted: bool, + _marker: PhantomData ()>, +} + +impl core::fmt::Debug for IterDupFixedOfKey<'_, '_, K, VALUE_SIZE> +where + K: TransactionKind, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let remaining = self.current_page.len().saturating_sub(self.page_offset) / VALUE_SIZE; + f.debug_struct("IterDupFixedOfKey") + .field("exhausted", &self.exhausted) + .field("remaining_in_page", &remaining) + .finish() + } +} + +impl<'tx: 'cur, 'cur, K, const VALUE_SIZE: usize> IterDupFixedOfKey<'tx, 'cur, K, VALUE_SIZE> +where + K: TransactionKind, +{ + /// Create a new, exhausted iterator. + /// + /// Iteration will immediately return `None`. + pub(crate) fn new_end(cursor: Cow<'cur, Cursor<'tx, K>>) -> Self { + IterDupFixedOfKey { + cursor, + current_page: Cow::Borrowed(&[]), + page_offset: 0, + exhausted: true, + _marker: PhantomData, + } + } + + /// Create a new, exhausted iterator from a mutable reference to the cursor. + pub(crate) fn end_from_ref(cursor: &'cur mut Cursor<'tx, K>) -> Self { + Self::new_end(Cow::Borrowed(cursor)) + } + + /// Create a new iterator with the given initial page. + pub(crate) fn new_with(cursor: Cow<'cur, Cursor<'tx, K>>, page: Cow<'tx, [u8]>) -> Self { + IterDupFixedOfKey { + cursor, + current_page: page, + page_offset: 0, + exhausted: false, + _marker: PhantomData, + } + } + + /// Create a new iterator from a mutable reference with initial page. + pub(crate) fn from_ref_with(cursor: &'cur mut Cursor<'tx, K>, page: Cow<'tx, [u8]>) -> Self { + Self::new_with(Cow::Borrowed(cursor), page) + } +} + +impl<'tx: 'cur, 'cur, K, const VALUE_SIZE: usize> IterDupFixedOfKey<'tx, 'cur, K, VALUE_SIZE> +where + K: TransactionKind, +{ + /// Consume the next value from the current page. + /// + /// Returns `Some(Cow<'tx, [u8]>)` containing exactly `VALUE_SIZE` bytes, + /// or `None` if the page is exhausted. + fn consume_value(&mut self) -> Option> { + let end = self.page_offset.checked_add(VALUE_SIZE)?; + if end > self.current_page.len() { + return None; + } + + let start = self.page_offset; + self.page_offset = end; + + match &self.current_page { + Cow::Borrowed(slice) => Some(Cow::Borrowed(&slice[start..end])), + Cow::Owned(vec) => Some(Cow::Owned(vec[start..end].to_vec())), + } + } + + /// Fetch the next page of values for the current key. + /// + /// Unlike + /// [`IterDupFixed::fetch_next_page`](crate::tx::aliases::IterDupFixed), + /// this does NOT move to the next key when pages are exhausted. It simply + /// returns `Ok(false)` to signal exhaustion. + /// + /// Returns `Ok(true)` if a new page was fetched, `Ok(false)` if exhausted. + fn fetch_next_page(&mut self) -> ReadResult { + let cursor = self.cursor.to_mut(); + + // Try to get next page for current key + if let Some((_key, page)) = cursor.next_multiple::<(), Cow<'tx, [u8]>>()? { + self.current_page = page; + self.page_offset = 0; + return Ok(true); + } + + // No more pages for this key - done + self.exhausted = true; + Ok(false) + } + + /// Borrow the next value from the iterator. + /// + /// Returns `Ok(Some(value))` where `value` is a `Cow<'tx, [u8]>` of exactly + /// `VALUE_SIZE` bytes. + /// + /// Returns `Ok(None)` when the iterator is exhausted. + pub fn borrow_next(&mut self) -> ReadResult>> { + if self.exhausted { + return Ok(None); + } + + // Try to consume from current page + if let Some(value) = self.consume_value() { + return Ok(Some(value)); + } + + // Current page exhausted, fetch next page + if !self.fetch_next_page()? { + return Ok(None); + } + + // Consume first value from new page + let value = self.consume_value().expect("freshly fetched page should have values"); + Ok(Some(value)) + } + + /// Get the next value as owned data. + /// + /// Returns `Ok(Some([u8; VALUE_SIZE]))` where the value is copied + /// into a fixed-size array. + pub fn owned_next(&mut self) -> ReadResult> { + self.borrow_next().map(|opt| { + opt.map(|value| { + let mut arr = [0u8; VALUE_SIZE]; + arr.copy_from_slice(&value); + arr + }) + }) + } +} + +impl<'tx: 'cur, 'cur, K, const VALUE_SIZE: usize> Iterator + for IterDupFixedOfKey<'tx, 'cur, K, VALUE_SIZE> +where + K: TransactionKind, +{ + type Item = ReadResult<[u8; VALUE_SIZE]>; + + fn next(&mut self) -> Option { + self.owned_next().transpose() + } +} diff --git a/src/tx/iter/mod.rs b/src/tx/iter/mod.rs new file mode 100644 index 0000000..e8107b5 --- /dev/null +++ b/src/tx/iter/mod.rs @@ -0,0 +1,68 @@ +//! Iterator types for traversing MDBX databases. +//! +//! This module provides lending iterators over key-value pairs in MDBX +//! databases. The iterators support both borrowed and owned access patterns. +//! +//! # Iterator Types +//! +//! - [`Iter`]: Base iterator with configurable cursor operation +//! - [`IterKeyVals`]: Iterates over all key-value pairs (`MDBX_NEXT`) +//! - [`IterDupKeys`]: For `DUPSORT` databases, yields first value per key +//! - [`IterDupVals`]: For `DUPSORT` databases, yields all values for one key +//! - [`IterDup`]: Nested iteration over `DUPSORT` databases +//! +//! # Borrowing vs Owning +//! +//! Iterators provide two ways to access data: +//! +//! - [`borrow_next()`](Iter::borrow_next): Returns data potentially borrowed +//! from the database. Requires the `Key` and `Value` types to implement +//! [`TableObject<'tx>`](crate::TableObject). This can avoid allocations +//! when using `Cow<'tx, [u8]>`. +//! +//! - [`owned_next()`](Iter::owned_next): Returns owned data. Requires +//! [`TableObjectOwned`](crate::TableObjectOwned). Always safe but may allocate. +//! +//! The standard [`Iterator`] trait is implemented via `owned_next()`. +//! +//! # Dirty Page Handling +//! +//! In read-write transactions, database pages may be "dirty" (modified but +//! not yet committed). The behavior of `Cow<[u8]>` depends on the +//! `return-borrowed` feature: +//! +//! - **With `return-borrowed`**: Always returns `Cow::Borrowed`, even for +//! dirty pages. This is faster but the data may change if the transaction +//! modifies it later. +//! +//! - **Without `return-borrowed`** (default): Dirty pages are copied to +//! `Cow::Owned`. This is safer but allocates more. +//! +//! # Example +//! +//! ```no_run +//! # use signet_libmdbx::Environment; +//! # use std::path::Path; +//! # let env = Environment::builder().open(Path::new("/tmp/iter_example")).unwrap(); +//! let txn = env.begin_ro_sync().unwrap(); +//! let db = txn.open_db(None).unwrap(); +//! let mut cursor = txn.cursor(db).unwrap(); +//! +//! // Iterate using the standard Iterator trait (owned) +//! for result in cursor.iter_start::, Vec>().unwrap() { +//! let (key, value) = result.expect("decode error"); +//! println!("{:?} => {:?}", key, value); +//! } +//! ``` + +mod base; +pub use base::Iter; + +mod dup; +pub use dup::IterDup; + +mod dupfixed; +pub use dupfixed::IterDupFixed; + +mod dupfixed_key; +pub use dupfixed_key::IterDupFixedOfKey; diff --git a/src/tx/mod.rs b/src/tx/mod.rs index 45c0069..570cb6c 100644 --- a/src/tx/mod.rs +++ b/src/tx/mod.rs @@ -2,8 +2,8 @@ //! //! # Core Types (re-exported at crate root) //! -//! - [`TxSync`] - Thread-safe synchronized transaction -//! - [`TxUnsync`] - Single-threaded unsynchronized transaction +//! - [`aliases::TxSync`] - Thread-safe synchronized transaction +//! - [`aliases::TxUnsync`] - Single-threaded unsynchronized transaction //! - [`Cursor`] - Database cursor for navigating entries //! - [`Database`] - Handle to an opened database //! - [`Ro`], [`Rw`], [`RoSync`], [`RwSync`] - Transaction kind markers @@ -11,12 +11,15 @@ //! //! # Type Aliases //! -//! Convenience aliases for common transaction/cursor configurations: -//! - [`RoTxSync`], [`RwTxSync`] - Synchronized transactions -//! - [`RoTxUnsync`], [`RwTxUnsync`] - Unsynchronized transactions -//! - [`RoCursorSync`], [`RwCursorSync`] - Cursors for synchronized transactions -//! - [`RoCursorUnsync`], [`RwCursorUnsync`] - Cursors for unsynchronized +//! Convenience aliases for common transaction/cursor/iterator configurations +//! are available in [`aliases`]: +//! - [`aliases::RoTxSync`], [`aliases::RwTxSync`] - Synchronized transactions +//! - [`aliases::RoTxUnsync`], [`aliases::RwTxUnsync`] - Unsynchronized //! transactions +//! - [`aliases::RoCursorSync`], [`aliases::RwCursorSync`] - Cursors for +//! synchronized transactions +//! - [`aliases::RoCursorUnsync`], [`aliases::RwCursorUnsync`] - Cursors for +//! unsynchronized transactions //! //! # Advanced: Writing Generic Code //! @@ -30,16 +33,17 @@ mod assertions; mod access; pub use access::{PtrSync, PtrUnsync, TxPtrAccess}; +pub mod aliases; + pub mod cache; mod cursor; -pub use cursor::{Cursor, RoCursorSync, RoCursorUnsync, RwCursorSync, RwCursorUnsync}; +pub use cursor::Cursor; mod database; pub use database::Database; pub mod iter; -pub use iter::{RoIterSync, RoIterUnsync, RwIterSync, RwIterUnsync}; mod kind; pub use kind::{Ro, RoSync, Rw, RwSync, SyncKind, TransactionKind, WriteMarker, WriterKind}; @@ -51,4 +55,4 @@ pub use lat::CommitLatency; pub mod ops; mod r#impl; -pub use r#impl::{RoTxSync, RoTxUnsync, RwTxSync, RwTxUnsync, Tx, TxSync, TxUnsync}; +pub use r#impl::Tx; diff --git a/src/tx/ops.rs b/src/tx/ops.rs index 391c24c..1d47935 100644 --- a/src/tx/ops.rs +++ b/src/tx/ops.rs @@ -399,7 +399,7 @@ pub(crate) unsafe fn debug_assert_append( let pagesize = unsafe { get_pagesize(txn) }; // SAFETY: Caller guarantees txn and dbi are valid. let last_key = unsafe { get_last_key(txn, dbi) }; - super::assertions::debug_assert_append(pagesize, flags, key, data, last_key.as_deref()); + crate::tx::assertions::debug_assert_append(pagesize, flags, key, data, last_key.as_deref()); } /// All-in-one append_dup assertion: opens cursor, gets last dup, asserts, closes cursor. @@ -420,5 +420,5 @@ pub(crate) unsafe fn debug_assert_append_dup( let pagesize = unsafe { get_pagesize(txn) }; // SAFETY: Caller guarantees txn and dbi are valid. let last_dup = unsafe { get_last_dup(txn, dbi, key) }; - super::assertions::debug_assert_append_dup(pagesize, flags, key, data, last_dup.as_deref()); + crate::tx::assertions::debug_assert_append_dup(pagesize, flags, key, data, last_dup.as_deref()); } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 97acf7b..cfc8a93 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -7,7 +7,10 @@ use signet_libmdbx::{ Cursor, Database, DatabaseFlags, Environment, MdbxResult, ReadResult, Ro, RoSync, Rw, RwSync, Stat, TableObject, TransactionKind, TxSync, TxUnsync, WriteFlags, ffi, - tx::{RoTxSync, RoTxUnsync, RwTxSync, RwTxUnsync, WriteMarker}, + tx::{ + WriteMarker, + aliases::{RoTxSync, RoTxUnsync, RwTxSync, RwTxUnsync}, + }, }; /// Trait for read-write transaction operations used in tests. diff --git a/tests/cursor.rs b/tests/cursor.rs index 291e5d3..2a8215e 100644 --- a/tests/cursor.rs +++ b/tests/cursor.rs @@ -2,7 +2,7 @@ mod common; use common::{TestRoTxn, TestRwTxn, V1Factory, V2Factory}; use signet_libmdbx::{ - Cursor, DatabaseFlags, Environment, MdbxError, MdbxResult, ObjectLength, ReadError, ReadResult, + Cursor, DatabaseFlags, Environment, MdbxError, MdbxResult, ObjectLength, ReadResult, TransactionKind, WriteFlags, }; use std::{borrow::Cow, hint::black_box}; @@ -96,11 +96,11 @@ fn test_get_dup_impl( assert_eq!(cursor.get_both_range(b"key2", b"val").unwrap(), Some(*b"val1")); assert_eq!(cursor.last().unwrap(), Some((*b"key2", *b"val3"))); - cursor.del(WriteFlags::empty()).unwrap(); + cursor.del().unwrap(); assert_eq!(cursor.last().unwrap(), Some((*b"key2", *b"val2"))); - cursor.del(WriteFlags::empty()).unwrap(); + cursor.del().unwrap(); assert_eq!(cursor.last().unwrap(), Some((*b"key2", *b"val1"))); - cursor.del(WriteFlags::empty()).unwrap(); + cursor.del().unwrap(); assert_eq!(cursor.last().unwrap(), Some((*b"key1", *b"val3"))); } @@ -458,7 +458,7 @@ fn test_iter_del_get_impl( assert_eq!(cursor.set(b"a").unwrap(), Some(*b"1")); - cursor.del(WriteFlags::empty()).unwrap(); + cursor.del().unwrap(); assert_eq!( cursor @@ -508,7 +508,7 @@ fn test_put_del_impl( Some((Cow::Borrowed(b"key2" as &[u8]), Cow::Borrowed(b"val2" as &[u8]))) ); - cursor.del(WriteFlags::empty()).unwrap(); + cursor.del().unwrap(); assert_eq!( cursor.get_current().unwrap(), Some((Cow::Borrowed(b"key3" as &[u8]), Cow::Borrowed(b"val3" as &[u8]))) @@ -529,91 +529,9 @@ fn test_put_del_v2() { test_put_del_impl(V2Factory::begin_rw, V2Factory::begin_ro); } -fn test_dup_sort_validation_on_non_dupsort_db_impl( - begin_rw: impl Fn(&Environment) -> MdbxResult, - _begin_ro: impl Fn(&Environment) -> MdbxResult, -) where - RwTx: TestRwTxn, - RoTx: TestRoTxn, -{ - let dir = tempdir().unwrap(); - let env = Environment::builder().open(dir.path()).unwrap(); - - let txn = begin_rw(&env).unwrap(); - let db = txn.open_db(None).unwrap(); // Non-DUPSORT database - txn.put(db, b"key1", b"val1", WriteFlags::empty()).unwrap(); - - let mut cursor = txn.cursor(db).unwrap(); - cursor.first::<(), ()>().unwrap(); // Position cursor - - // These should return RequiresDupSort error - let err = cursor.first_dup::<()>().unwrap_err(); - assert!(matches!(err, ReadError::Mdbx(MdbxError::RequiresDupSort))); - - let err = cursor.last_dup::<()>().unwrap_err(); - assert!(matches!(err, ReadError::Mdbx(MdbxError::RequiresDupSort))); - - let err = cursor.next_dup::<(), ()>().unwrap_err(); - assert!(matches!(err, ReadError::Mdbx(MdbxError::RequiresDupSort))); - - let err = cursor.prev_dup::<(), ()>().unwrap_err(); - assert!(matches!(err, ReadError::Mdbx(MdbxError::RequiresDupSort))); - - let err = cursor.get_both::<()>(b"key1", b"val1").unwrap_err(); - assert!(matches!(err, ReadError::Mdbx(MdbxError::RequiresDupSort))); - - let err = cursor.get_both_range::<()>(b"key1", b"val").unwrap_err(); - assert!(matches!(err, ReadError::Mdbx(MdbxError::RequiresDupSort))); -} - -#[test] -fn test_dup_sort_validation_on_non_dupsort_db_v1() { - test_dup_sort_validation_on_non_dupsort_db_impl(V1Factory::begin_rw, V1Factory::begin_ro); -} - -#[test] -fn test_dup_sort_validation_on_non_dupsort_db_v2() { - test_dup_sort_validation_on_non_dupsort_db_impl(V2Factory::begin_rw, V2Factory::begin_ro); -} - -fn test_dup_fixed_validation_on_non_dupfixed_db_impl( - begin_rw: impl Fn(&Environment) -> MdbxResult, - _begin_ro: impl Fn(&Environment) -> MdbxResult, -) where - RwTx: TestRwTxn, - RoTx: TestRoTxn, -{ - let dir = tempdir().unwrap(); - let env = Environment::builder().open(dir.path()).unwrap(); - - let txn = begin_rw(&env).unwrap(); - // Create DUPSORT but NOT DUPFIXED database - let db = txn.create_db(None, DatabaseFlags::DUP_SORT).unwrap(); - txn.put(db, b"key1", b"val1", WriteFlags::empty()).unwrap(); - - let mut cursor = txn.cursor(db).unwrap(); - cursor.first::<(), ()>().unwrap(); // Position cursor - - // These should return RequiresDupFixed error - let err = cursor.get_multiple::<()>().unwrap_err(); - assert!(matches!(err, ReadError::Mdbx(MdbxError::RequiresDupFixed))); - - let err = cursor.next_multiple::<(), ()>().unwrap_err(); - assert!(matches!(err, ReadError::Mdbx(MdbxError::RequiresDupFixed))); - - let err = cursor.prev_multiple::<(), ()>().unwrap_err(); - assert!(matches!(err, ReadError::Mdbx(MdbxError::RequiresDupFixed))); -} - -#[test] -fn test_dup_fixed_validation_on_non_dupfixed_db_v1() { - test_dup_fixed_validation_on_non_dupfixed_db_impl(V1Factory::begin_rw, V1Factory::begin_ro); -} - -#[test] -fn test_dup_fixed_validation_on_non_dupfixed_db_v2() { - test_dup_fixed_validation_on_non_dupfixed_db_impl(V2Factory::begin_rw, V2Factory::begin_ro); -} +// NOTE: DUP_SORT and DUP_FIXED validation tests have been moved to the debug-only +// module below. In debug builds, these validations panic via debug_assert! +// In release builds, the checks are skipped and MDBX will return errors. fn test_dup_sort_methods_work_on_dupsort_db_impl( begin_rw: impl Fn(&Environment) -> MdbxResult, @@ -936,31 +854,441 @@ fn test_append_dup_v2() { test_append_dup_impl(V2Factory::begin_rw, V2Factory::begin_ro); } -fn test_append_dup_requires_dupsort_impl(begin_rw: impl Fn(&Environment) -> MdbxResult) -where +// NOTE: append_dup DUP_SORT validation tests have been moved to the debug-only +// module below. In debug builds, these validations panic via debug_assert! +// In release builds, the checks are skipped and MDBX will return errors. + +// ============================================================================= +// DUPFIXED Iterator Tests +// ============================================================================= + +fn test_iter_dupfixed_basic_impl( + begin_rw: impl Fn(&Environment) -> MdbxResult, + begin_ro: impl Fn(&Environment) -> MdbxResult, +) where RwTx: TestRwTxn, + RoTx: TestRoTxn, { let dir = tempdir().unwrap(); let env = Environment::builder().open(dir.path()).unwrap(); - // Try append_dup on non-DUPSORT database + // Create DUPFIXED database with 4-byte values let txn = begin_rw(&env).unwrap(); - let db = txn.open_db(None).unwrap(); // Non-DUPSORT database + let db = txn.create_db(None, DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED).unwrap(); + + // Insert values for key1 + txn.put(db, b"key1", &1u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.put(db, b"key1", &2u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.put(db, b"key1", &3u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + + // Insert values for key2 + txn.put(db, b"key2", &10u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.put(db, b"key2", &20u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + + txn.commit().unwrap(); + + // Read back using the iterator + let txn = begin_ro(&env).unwrap(); + let db = txn.open_db(None).unwrap(); let mut cursor = txn.cursor(db).unwrap(); - // Should return RequiresDupSort error - let err = cursor.append_dup(b"key", b"value").unwrap_err(); - assert!(matches!(err, MdbxError::RequiresDupSort)); + let results: Vec<(Vec, [u8; 4])> = + cursor.iter_dupfixed_start::, 4>().unwrap().map(|r| r.unwrap()).collect(); + + assert_eq!(results.len(), 5); + assert_eq!(results[0], (b"key1".to_vec(), 1u32.to_le_bytes())); + assert_eq!(results[1], (b"key1".to_vec(), 2u32.to_le_bytes())); + assert_eq!(results[2], (b"key1".to_vec(), 3u32.to_le_bytes())); + assert_eq!(results[3], (b"key2".to_vec(), 10u32.to_le_bytes())); + assert_eq!(results[4], (b"key2".to_vec(), 20u32.to_le_bytes())); } #[test] -fn test_append_dup_requires_dupsort_v1() { - test_append_dup_requires_dupsort_impl(V1Factory::begin_rw); +fn test_iter_dupfixed_basic_v1() { + test_iter_dupfixed_basic_impl(V1Factory::begin_rw, V1Factory::begin_ro); } #[test] -fn test_append_dup_requires_dupsort_v2() { - test_append_dup_requires_dupsort_impl(V2Factory::begin_rw); +fn test_iter_dupfixed_basic_v2() { + test_iter_dupfixed_basic_impl(V2Factory::begin_rw, V2Factory::begin_ro); +} + +fn test_iter_dupfixed_from_impl( + begin_rw: impl Fn(&Environment) -> MdbxResult, + begin_ro: impl Fn(&Environment) -> MdbxResult, +) where + RwTx: TestRwTxn, + RoTx: TestRoTxn, +{ + let dir = tempdir().unwrap(); + let env = Environment::builder().open(dir.path()).unwrap(); + + // Create DUPFIXED database with 4-byte values + let txn = begin_rw(&env).unwrap(); + let db = txn.create_db(None, DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED).unwrap(); + + txn.put(db, b"aaa", &1u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.put(db, b"bbb", &2u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.put(db, b"ccc", &3u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.put(db, b"ddd", &4u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + + txn.commit().unwrap(); + + // Start from "bbb" + let txn = begin_ro(&env).unwrap(); + let db = txn.open_db(None).unwrap(); + let mut cursor = txn.cursor(db).unwrap(); + + let results: Vec<(Vec, [u8; 4])> = + cursor.iter_dupfixed_from::, 4>(b"bbb").unwrap().map(|r| r.unwrap()).collect(); + + assert_eq!(results.len(), 3); + assert_eq!(results[0], (b"bbb".to_vec(), 2u32.to_le_bytes())); + assert_eq!(results[1], (b"ccc".to_vec(), 3u32.to_le_bytes())); + assert_eq!(results[2], (b"ddd".to_vec(), 4u32.to_le_bytes())); +} + +#[test] +fn test_iter_dupfixed_from_v1() { + test_iter_dupfixed_from_impl(V1Factory::begin_rw, V1Factory::begin_ro); +} + +#[test] +fn test_iter_dupfixed_from_v2() { + test_iter_dupfixed_from_impl(V2Factory::begin_rw, V2Factory::begin_ro); +} + +fn test_iter_dupfixed_empty_impl( + begin_rw: impl Fn(&Environment) -> MdbxResult, + begin_ro: impl Fn(&Environment) -> MdbxResult, +) where + RwTx: TestRwTxn, + RoTx: TestRoTxn, +{ + let dir = tempdir().unwrap(); + let env = Environment::builder().open(dir.path()).unwrap(); + + // Create empty DUPFIXED database + let txn = begin_rw(&env).unwrap(); + txn.create_db(None, DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED).unwrap(); + txn.commit().unwrap(); + + let txn = begin_ro(&env).unwrap(); + let db = txn.open_db(None).unwrap(); + let mut cursor = txn.cursor(db).unwrap(); + + let results: Vec<(Vec, [u8; 4])> = + cursor.iter_dupfixed_start::, 4>().unwrap().map(|r| r.unwrap()).collect(); + + assert!(results.is_empty()); +} + +#[test] +fn test_iter_dupfixed_empty_v1() { + test_iter_dupfixed_empty_impl(V1Factory::begin_rw, V1Factory::begin_ro); +} + +#[test] +fn test_iter_dupfixed_empty_v2() { + test_iter_dupfixed_empty_impl(V2Factory::begin_rw, V2Factory::begin_ro); +} + +// NOTE: iter_dupfixed DUP_FIXED validation tests have been moved to the debug-only +// module below. In debug builds, these validations panic via debug_assert! +// In release builds, the checks are skipped and MDBX will return errors. + +fn test_iter_dupfixed_many_values_impl( + begin_rw: impl Fn(&Environment) -> MdbxResult, + begin_ro: impl Fn(&Environment) -> MdbxResult, +) where + RwTx: TestRwTxn, + RoTx: TestRoTxn, +{ + use std::collections::HashSet; + + let dir = tempdir().unwrap(); + let env = Environment::builder().open(dir.path()).unwrap(); + + // Create DUPFIXED database with many values to test page boundaries + let txn = begin_rw(&env).unwrap(); + let db = txn.create_db(None, DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED).unwrap(); + + // Insert 1000 values to ensure we span multiple pages + // Note: MDBX sorts duplicates lexicographically by bytes, not as integers. + // So u32 values will be sorted by their byte representation, not numerically. + for i in 0u32..1000 { + txn.put(db, b"key", &i.to_le_bytes(), WriteFlags::empty()).unwrap(); + } + + txn.commit().unwrap(); + + // Read back and verify all values are present (order may differ from insertion) + let txn = begin_ro(&env).unwrap(); + let db = txn.open_db(None).unwrap(); + let mut cursor = txn.cursor(db).unwrap(); + + let results: Vec<(Vec, [u8; 4])> = + cursor.iter_dupfixed_start::, 4>().unwrap().map(|r| r.unwrap()).collect(); + + // Verify count + assert_eq!(results.len(), 1000); + + // Verify all keys are "key" + for (key, _) in &results { + assert_eq!(key, b"key"); + } + + // Verify all 1000 values are present (regardless of order) + let values: HashSet = results.iter().map(|(_, v)| u32::from_le_bytes(*v)).collect(); + let expected: HashSet = (0u32..1000).collect(); + assert_eq!(values, expected); + + // Verify values are in lexicographic byte order (MDBX sorts duplicates this way) + for window in results.windows(2) { + let v1 = &window[0].1; + let v2 = &window[1].1; + assert!(v1 <= v2, "values not in sorted order: {:?} > {:?}", v1, v2); + } +} + +#[test] +fn test_iter_dupfixed_many_values_v1() { + test_iter_dupfixed_many_values_impl(V1Factory::begin_rw, V1Factory::begin_ro); +} + +#[test] +fn test_iter_dupfixed_many_values_v2() { + test_iter_dupfixed_many_values_impl(V2Factory::begin_rw, V2Factory::begin_ro); +} + +fn test_iter_dupfixed_from_nonexistent_key_impl( + begin_rw: impl Fn(&Environment) -> MdbxResult, + begin_ro: impl Fn(&Environment) -> MdbxResult, +) where + RwTx: TestRwTxn, + RoTx: TestRoTxn, +{ + let dir = tempdir().unwrap(); + let env = Environment::builder().open(dir.path()).unwrap(); + + let txn = begin_rw(&env).unwrap(); + let db = txn.create_db(None, DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED).unwrap(); + txn.put(db, b"aaa", &1u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.put(db, b"ccc", &2u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.commit().unwrap(); + + let txn = begin_ro(&env).unwrap(); + let db = txn.open_db(None).unwrap(); + let mut cursor = txn.cursor(db).unwrap(); + + // Start from "bbb" which doesn't exist - should find "ccc" + let results: Vec<(Vec, [u8; 4])> = + cursor.iter_dupfixed_from::, 4>(b"bbb").unwrap().map(|r| r.unwrap()).collect(); + + assert_eq!(results.len(), 1); + assert_eq!(results[0], (b"ccc".to_vec(), 2u32.to_le_bytes())); +} + +#[test] +fn test_iter_dupfixed_from_nonexistent_key_v1() { + test_iter_dupfixed_from_nonexistent_key_impl(V1Factory::begin_rw, V1Factory::begin_ro); +} + +#[test] +fn test_iter_dupfixed_from_nonexistent_key_v2() { + test_iter_dupfixed_from_nonexistent_key_impl(V2Factory::begin_rw, V2Factory::begin_ro); +} + +fn test_iter_dupfixed_from_past_end_impl( + begin_rw: impl Fn(&Environment) -> MdbxResult, + begin_ro: impl Fn(&Environment) -> MdbxResult, +) where + RwTx: TestRwTxn, + RoTx: TestRoTxn, +{ + let dir = tempdir().unwrap(); + let env = Environment::builder().open(dir.path()).unwrap(); + + let txn = begin_rw(&env).unwrap(); + let db = txn.create_db(None, DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED).unwrap(); + txn.put(db, b"aaa", &1u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.commit().unwrap(); + + let txn = begin_ro(&env).unwrap(); + let db = txn.open_db(None).unwrap(); + let mut cursor = txn.cursor(db).unwrap(); + + // Start from "zzz" which is past all keys + let results: Vec<(Vec, [u8; 4])> = + cursor.iter_dupfixed_from::, 4>(b"zzz").unwrap().map(|r| r.unwrap()).collect(); + + assert!(results.is_empty()); +} + +#[test] +fn test_iter_dupfixed_from_past_end_v1() { + test_iter_dupfixed_from_past_end_impl(V1Factory::begin_rw, V1Factory::begin_ro); +} + +#[test] +fn test_iter_dupfixed_from_past_end_v2() { + test_iter_dupfixed_from_past_end_impl(V2Factory::begin_rw, V2Factory::begin_ro); +} + +// ============================================================================= +// DUPFIXED Single-Key Iterator Tests (iter_dupfixed_of) +// ============================================================================= + +fn test_iter_dupfixed_of_impl( + begin_rw: impl Fn(&Environment) -> MdbxResult, + begin_ro: impl Fn(&Environment) -> MdbxResult, +) where + RwTx: TestRwTxn, + RoTx: TestRoTxn, +{ + let dir = tempdir().unwrap(); + let env = Environment::builder().open(dir.path()).unwrap(); + + // Create DUPFIXED database with multiple keys + let txn = begin_rw(&env).unwrap(); + let db = txn.create_db(None, DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED).unwrap(); + + // Insert values for key1 + txn.put(db, b"key1", &1u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.put(db, b"key1", &2u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.put(db, b"key1", &3u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + + // Insert values for key2 + txn.put(db, b"key2", &10u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.put(db, b"key2", &20u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + + // Insert values for key3 + txn.put(db, b"key3", &100u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + + txn.commit().unwrap(); + + // Test: iter_dupfixed_of should only return values for key2 + let txn = begin_ro(&env).unwrap(); + let db = txn.open_db(None).unwrap(); + let mut cursor = txn.cursor(db).unwrap(); + + let results: Vec<[u8; 4]> = + cursor.iter_dupfixed_of::<4>(b"key2").unwrap().map(|r| r.unwrap()).collect(); + + // Should only contain key2's values + assert_eq!(results.len(), 2); + assert_eq!(u32::from_le_bytes(results[0]), 10); + assert_eq!(u32::from_le_bytes(results[1]), 20); +} + +#[test] +fn test_iter_dupfixed_of_v1() { + test_iter_dupfixed_of_impl(V1Factory::begin_rw, V1Factory::begin_ro); +} + +#[test] +fn test_iter_dupfixed_of_v2() { + test_iter_dupfixed_of_impl(V2Factory::begin_rw, V2Factory::begin_ro); +} + +fn test_iter_dupfixed_of_nonexistent_key_impl( + begin_rw: impl Fn(&Environment) -> MdbxResult, + begin_ro: impl Fn(&Environment) -> MdbxResult, +) where + RwTx: TestRwTxn, + RoTx: TestRoTxn, +{ + let dir = tempdir().unwrap(); + let env = Environment::builder().open(dir.path()).unwrap(); + + // Create DUPFIXED database with some data + let txn = begin_rw(&env).unwrap(); + let db = txn.create_db(None, DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED).unwrap(); + txn.put(db, b"aaa", &1u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.put(db, b"ccc", &2u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + txn.commit().unwrap(); + + let txn = begin_ro(&env).unwrap(); + let db = txn.open_db(None).unwrap(); + let mut cursor = txn.cursor(db).unwrap(); + + // Seek nonexistent key "bbb" - should return empty iterator + let results: Vec<[u8; 4]> = + cursor.iter_dupfixed_of::<4>(b"bbb").unwrap().map(|r| r.unwrap()).collect(); + + assert!(results.is_empty()); +} + +#[test] +fn test_iter_dupfixed_of_nonexistent_key_v1() { + test_iter_dupfixed_of_nonexistent_key_impl(V1Factory::begin_rw, V1Factory::begin_ro); +} + +#[test] +fn test_iter_dupfixed_of_nonexistent_key_v2() { + test_iter_dupfixed_of_nonexistent_key_impl(V2Factory::begin_rw, V2Factory::begin_ro); +} + +fn test_iter_dupfixed_of_many_values_impl( + begin_rw: impl Fn(&Environment) -> MdbxResult, + begin_ro: impl Fn(&Environment) -> MdbxResult, +) where + RwTx: TestRwTxn, + RoTx: TestRoTxn, +{ + use std::collections::HashSet; + + let dir = tempdir().unwrap(); + let env = Environment::builder().open(dir.path()).unwrap(); + + // Create DUPFIXED database with many values to test page boundaries + let txn = begin_rw(&env).unwrap(); + let db = txn.create_db(None, DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED).unwrap(); + + // Insert values for "before" key + txn.put(db, b"before", &999u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + + // Insert 1000 values for target key to ensure we span multiple pages + for i in 0u32..1000 { + txn.put(db, b"target", &i.to_le_bytes(), WriteFlags::empty()).unwrap(); + } + + // Insert values for "after" key + txn.put(db, b"zafter", &888u32.to_le_bytes(), WriteFlags::empty()).unwrap(); + + txn.commit().unwrap(); + + // Read back using iter_dupfixed_of + let txn = begin_ro(&env).unwrap(); + let db = txn.open_db(None).unwrap(); + let mut cursor = txn.cursor(db).unwrap(); + + let results: Vec<[u8; 4]> = + cursor.iter_dupfixed_of::<4>(b"target").unwrap().map(|r| r.unwrap()).collect(); + + // Verify count - should be exactly 1000 + assert_eq!(results.len(), 1000); + + // Verify all 1000 values are present (regardless of order) + let values: HashSet = results.iter().map(|v| u32::from_le_bytes(*v)).collect(); + let expected: HashSet = (0u32..1000).collect(); + assert_eq!(values, expected); + + // Verify no values from "before" or "zafter" keys leaked in + for v in &results { + let num = u32::from_le_bytes(*v); + assert!(num < 1000, "unexpected value {num} from other key"); + } +} + +#[test] +fn test_iter_dupfixed_of_many_values_v1() { + test_iter_dupfixed_of_many_values_impl(V1Factory::begin_rw, V1Factory::begin_ro); +} + +#[test] +fn test_iter_dupfixed_of_many_values_v2() { + test_iter_dupfixed_of_many_values_impl(V2Factory::begin_rw, V2Factory::begin_ro); } // Debug assertion tests - only run in debug builds @@ -1148,4 +1476,65 @@ mod append_debug_tests { assert!(result.is_err()); assert!(matches!(result, Err(MdbxError::KeyMismatch))); } + + // DUP_SORT validation tests - these panic in debug builds + #[test] + #[should_panic(expected = "Operation requires DUP_SORT database flag")] + fn test_first_dup_on_non_dupsort_panics() { + let dir = tempdir().unwrap(); + let env = Environment::builder().open(dir.path()).unwrap(); + + let txn = env.begin_rw_sync().unwrap(); + let db = txn.open_db(None).unwrap(); + txn.put(db, b"key1", b"val1", WriteFlags::empty()).unwrap(); + + let mut cursor = txn.cursor(db).unwrap(); + cursor.first::<(), ()>().unwrap(); + let _ = cursor.first_dup::<()>(); + } + + #[test] + #[should_panic(expected = "Operation requires DUP_SORT database flag")] + fn test_append_dup_on_non_dupsort_panics() { + let dir = tempdir().unwrap(); + let env = Environment::builder().open(dir.path()).unwrap(); + + let txn = env.begin_rw_sync().unwrap(); + let db = txn.open_db(None).unwrap(); + let mut cursor = txn.cursor(db).unwrap(); + let _ = cursor.append_dup(b"key", b"value"); + } + + // DUP_FIXED validation tests - these panic in debug builds + #[test] + #[should_panic(expected = "Operation requires DUP_FIXED database flag")] + fn test_get_multiple_on_non_dupfixed_panics() { + let dir = tempdir().unwrap(); + let env = Environment::builder().open(dir.path()).unwrap(); + + let txn = env.begin_rw_sync().unwrap(); + let db = txn.create_db(None, DatabaseFlags::DUP_SORT).unwrap(); + txn.put(db, b"key1", b"val1", WriteFlags::empty()).unwrap(); + + let mut cursor = txn.cursor(db).unwrap(); + cursor.first::<(), ()>().unwrap(); + let _ = cursor.get_multiple::<()>(); + } + + #[test] + #[should_panic(expected = "Operation requires DUP_FIXED database flag")] + fn test_iter_dupfixed_on_non_dupfixed_panics() { + let dir = tempdir().unwrap(); + let env = Environment::builder().open(dir.path()).unwrap(); + + let txn = env.begin_rw_sync().unwrap(); + let db = txn.create_db(None, DatabaseFlags::DUP_SORT).unwrap(); + txn.put(db, b"key", b"value", WriteFlags::empty()).unwrap(); + txn.commit().unwrap(); + + let txn = env.begin_ro_sync().unwrap(); + let db = txn.open_db(None).unwrap(); + let mut cursor = txn.cursor(db).unwrap(); + let _ = cursor.iter_dupfixed_start::, 4>(); + } }