Skip to content

Commit 4f80aa7

Browse files
authored
feat: GET_MULTIPLE iterators (#6)
* feat: GET_MULTIPLE iterators * chore: cleanup * feat: del_all_dups * feat: del_all_dups * chore: bump version * fix: tests
1 parent fc03851 commit 4f80aa7

File tree

22 files changed

+1963
-665
lines changed

22 files changed

+1963
-665
lines changed

CLAUDE.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,21 @@ working, or if the notes become outdated.
77

88
Rust bindings for libmdbx (MDBX database). Crate name: `signet-libmdbx`.
99

10+
## Crate Mandates
11+
12+
- You MUST NOT expose raw pointers to MDBX types outside of unsafe modules.
13+
- You MUST maintain zero-copy semantics for read operations in all new
14+
interfaces.
15+
- You MUST read and respect `SAFETY` comments throughout the codebase.
16+
- You MUST NOT introduce new dependencies without approval.
17+
1018
## MDBX Synchronization Model
1119

1220
When making changes to this codebase you MUST remember and conform to the MDBX
1321
synchronization model for transactions and cursors. Access to raw pointers MUST
1422
be mediated via the `TxAccess` trait. The table below summarizes the
1523
transaction types and their access models.
1624

17-
| Transaction Type | Thread Safety | Access Model | enforced by Rust type system? |
18-
| ---------------- | ------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
19-
| Read-Only (RO) | !Sync + Send | Access MUST be totally ordered and non-concurrent | No |
20-
| Read-Write (RW) | !Sync + !Send | Access MUST be totally ordered, and non-concurrent. Only the creating thread may manage TX lifecycle | No |
21-
| Transaction<K> | Sync + Send | Multi-threaded wrapper using Mutex and Arc to share a RO or RW transaction safely across threads | Yes, via synchronization wrappers |
22-
| TxUnsync<RO> | !Sync + Send | Single-threaded RO transaction without synchronization overhead | Yes, via required &mut or & access |
23-
| TxUnsync<RW> | !Sync + !Send | Single-threaded RW transaction without synchronization overhead | Yes, &self enforces via required ownership and !Send + !Sync bound |
24-
| Cursors | [Inherited] | Cursors borrow a Tx. The cursor CANNOT outlive the tx, and must reap its pointer on drop | Yes, via lifetimes |
25-
2625
## Key Types
2726

2827
- `Environment` - Database environment (in `src/sys/environment.rs`)
@@ -105,6 +104,7 @@ docker build -t mdbx-linux-tests . && docker run --rm mdbx-linux-tests
105104
```
106105

107106
This SHOULD be run alongside local tests and linting, especially for changes that:
107+
108108
- Modify build configuration
109109
- Add new dependencies
110110
- Change platform-specific code

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "signet-libmdbx"
33
description = "Idiomatic and safe MDBX wrapper"
4-
version = "0.6.0"
4+
version = "0.7.0"
55
edition = "2024"
66
rust-version = "1.92"
77
license = "MIT OR Apache-2.0"
@@ -57,3 +57,7 @@ harness = false
5757
[[bench]]
5858
name = "transaction"
5959
harness = false
60+
61+
[[bench]]
62+
name = "iter"
63+
harness = false

benches/iter.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#![allow(missing_docs)]
2+
mod utils;
3+
4+
use crate::utils::{create_ro_sync, create_ro_unsync};
5+
use criterion::{Criterion, criterion_group, criterion_main};
6+
use signet_libmdbx::{DatabaseFlags, Environment, WriteFlags};
7+
use std::hint::black_box;
8+
use tempfile::{TempDir, tempdir};
9+
10+
const VALUE_SIZE: usize = 100;
11+
const NUM_VALUES: u32 = 2000;
12+
13+
const DB_NAME: &str = "dupfixed_bench";
14+
15+
/// Setup a DUPFIXED database with NUM_VALUES 100-byte values under a single key.
16+
fn setup_dupfixed_db() -> (TempDir, Environment) {
17+
let dir = tempdir().unwrap();
18+
let env = Environment::builder().set_max_dbs(1).open(dir.path()).unwrap();
19+
20+
let txn = env.begin_rw_unsync().unwrap();
21+
let db =
22+
txn.create_db(Some(DB_NAME), DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED).unwrap();
23+
24+
// Insert NUM_VALUES with incrementing content
25+
for i in 0..NUM_VALUES {
26+
let mut value = [0u8; VALUE_SIZE];
27+
value[..4].copy_from_slice(&i.to_le_bytes());
28+
txn.put(db, b"key", value, WriteFlags::empty()).unwrap();
29+
}
30+
txn.commit().unwrap();
31+
32+
(dir, env)
33+
}
34+
35+
/// Benchmark: iter_dupfixed (batched page fetching).
36+
fn bench_iter_dupfixed(c: &mut Criterion) {
37+
let (_dir, env) = setup_dupfixed_db();
38+
let txn = create_ro_unsync(&env);
39+
let db = txn.open_db(Some(DB_NAME)).unwrap();
40+
41+
c.bench_function("unsync::iter::dupfixed::batched", |b| {
42+
b.iter(|| {
43+
let mut cursor = txn.cursor(db).unwrap();
44+
let mut count = 0u32;
45+
for result in cursor.iter_dupfixed_start::<[u8; 3], VALUE_SIZE>().unwrap() {
46+
let (_key, value) = result.unwrap();
47+
black_box(value);
48+
count += 1;
49+
}
50+
assert_eq!(count, NUM_VALUES);
51+
})
52+
});
53+
}
54+
55+
/// Benchmark: simple next() iteration.
56+
fn bench_iter_simple(c: &mut Criterion) {
57+
let (_dir, env) = setup_dupfixed_db();
58+
let txn = create_ro_unsync(&env);
59+
let db = txn.open_db(Some(DB_NAME)).unwrap();
60+
61+
c.bench_function("unsync::iter::dupfixed::simple_next", |b| {
62+
b.iter(|| {
63+
let mut cursor = txn.cursor(db).unwrap();
64+
let mut count = 0u32;
65+
for result in cursor.iter_start::<[u8; 3], [u8; VALUE_SIZE]>().unwrap() {
66+
let (_key, value) = result.unwrap();
67+
black_box(value);
68+
count += 1;
69+
}
70+
assert_eq!(count, NUM_VALUES);
71+
})
72+
});
73+
}
74+
75+
/// Benchmark: iter_dupfixed (batched page fetching).
76+
fn bench_iter_dupfixed_sync(c: &mut Criterion) {
77+
let (_dir, env) = setup_dupfixed_db();
78+
let txn = create_ro_sync(&env);
79+
let db = txn.open_db(Some(DB_NAME)).unwrap();
80+
81+
c.bench_function("sync::iter::dupfixed::batched", |b| {
82+
b.iter(|| {
83+
let mut cursor = txn.cursor(db).unwrap();
84+
let mut count = 0u32;
85+
for result in cursor.iter_dupfixed_start::<[u8; 3], VALUE_SIZE>().unwrap() {
86+
let (_key, value) = result.unwrap();
87+
black_box(value);
88+
count += 1;
89+
}
90+
assert_eq!(count, NUM_VALUES);
91+
})
92+
});
93+
}
94+
95+
/// Benchmark: simple next() iteration.
96+
fn bench_iter_simple_sync(c: &mut Criterion) {
97+
let (_dir, env) = setup_dupfixed_db();
98+
let txn = create_ro_sync(&env);
99+
let db = txn.open_db(Some(DB_NAME)).unwrap();
100+
101+
c.bench_function("sync::iter::dupfixed::simple_next", |b| {
102+
b.iter(|| {
103+
let mut cursor = txn.cursor(db).unwrap();
104+
let mut count = 0u32;
105+
for result in cursor.iter_start::<[u8; 3], [u8; VALUE_SIZE]>().unwrap() {
106+
let (_key, value) = result.unwrap();
107+
black_box(value);
108+
count += 1;
109+
}
110+
assert_eq!(count, NUM_VALUES);
111+
})
112+
});
113+
}
114+
115+
criterion_group! {
116+
name = benches;
117+
config = Criterion::default();
118+
targets = bench_iter_dupfixed, bench_iter_simple,
119+
bench_iter_dupfixed_sync, bench_iter_simple_sync,
120+
}
121+
122+
criterion_main!(benches);

benches/utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use signet_libmdbx::{
55
Environment, WriteFlags,
66
ffi::{MDBX_TXN_RDONLY, MDBX_env, MDBX_txn, mdbx_txn_begin_ex},
7-
tx::{RoTxSync, RoTxUnsync, RwTxSync, RwTxUnsync},
7+
tx::aliases::{RoTxSync, RoTxUnsync, RwTxSync, RwTxUnsync},
88
};
99
use std::ptr;
1010
use tempfile::{TempDir, tempdir};

src/lib.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,8 @@ pub mod sys;
160160
pub use sys::{Environment, EnvironmentBuilder, Geometry, Info, Stat};
161161

162162
pub mod tx;
163-
pub use tx::{
164-
CommitLatency, Cursor, Database, Ro, RoSync, Rw, RwSync, TransactionKind, TxSync, TxUnsync,
165-
};
163+
pub use tx::aliases::{TxSync, TxUnsync};
164+
pub use tx::{CommitLatency, Cursor, Database, Ro, RoSync, Rw, RwSync, TransactionKind};
166165

167166
#[cfg(test)]
168167
mod test {

src/sys/environment.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::{
33
error::{MdbxError, MdbxResult, ReadResult, mdbx_result},
44
flags::EnvironmentFlags,
55
sys::txn_manager::{LifecycleHandle, RwSyncLifecycle},
6-
tx::{RoTxSync, RoTxUnsync, RwTxSync, RwTxUnsync},
6+
tx::aliases::{RoTxSync, RoTxUnsync, RwTxSync, RwTxUnsync},
77
};
88
use byteorder::{ByteOrder, NativeEndian};
99
use mem::size_of;

src/tx/access.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ impl Drop for PtrUnsync {
130130
/// This type is used internally to manage transaction access in the [`TxSync`]
131131
/// transaction API. Users typically don't interact with this type directly.
132132
///
133-
/// [`TxSync`]: crate::tx::TxSync
133+
/// [`TxSync`]: crate::tx::aliases::TxSync
134134
#[derive(Debug)]
135135
pub struct PtrSync {
136136
/// Raw pointer to the MDBX transaction.

src/tx/aliases.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//! Public type aliases for transactions, cursors, and iterators.
2+
3+
use crate::{
4+
Ro, RoSync, Rw, RwSync,
5+
tx::{
6+
PtrSync, PtrUnsync,
7+
cursor::Cursor,
8+
r#impl::Tx,
9+
iter::{Iter, IterDupFixed, IterDupFixedOfKey},
10+
},
11+
};
12+
use std::{borrow::Cow, sync::Arc};
13+
14+
// --- Transaction aliases ---
15+
16+
/// Transaction type for synchronized access.
17+
pub type TxSync<K> = Tx<K, Arc<PtrSync>>;
18+
19+
/// Transaction type for unsynchronized access.
20+
pub type TxUnsync<K> = Tx<K, PtrUnsync>;
21+
22+
/// A synchronized read-only transaction.
23+
pub type RoTxSync = TxSync<RoSync>;
24+
25+
/// A synchronized read-write transaction.
26+
pub type RwTxSync = TxSync<RwSync>;
27+
28+
/// An unsynchronized read-only transaction.
29+
pub type RoTxUnsync = TxUnsync<Ro>;
30+
31+
/// An unsynchronized read-write transaction.
32+
pub type RwTxUnsync = TxUnsync<Rw>;
33+
34+
// SAFETY:
35+
// - RoTxSync and RwTxSync use Arc<PtrSync> which is Send and Sync.
36+
// - K::Cache is ALWAYS Send
37+
// - TxMeta is ALWAYS Send
38+
// - Moving an RO transaction between threads is safe as long as no concurrent
39+
// access occurs, which is guaranteed by being !Sync.
40+
//
41+
// NB: Send is correctly derived for RoTxSync and RwTxSync UNTIL
42+
// you unsafe impl Sync for RoTxUnsync below. This is a quirk I did not know
43+
// about.
44+
unsafe impl Send for RoTxSync {}
45+
unsafe impl Send for RwTxSync {}
46+
unsafe impl Send for RoTxUnsync {}
47+
48+
// --- Cursor aliases ---
49+
50+
/// A read-only cursor for a synchronized transaction.
51+
pub type RoCursorSync<'tx> = Cursor<'tx, RoSync>;
52+
53+
/// A read-write cursor for a synchronized transaction.
54+
pub type RwCursorSync<'tx> = Cursor<'tx, RwSync>;
55+
56+
/// A read-only cursor for an unsynchronized transaction.
57+
pub type RoCursorUnsync<'tx> = Cursor<'tx, Ro>;
58+
59+
/// A read-write cursor for an unsynchronized transaction.
60+
pub type RwCursorUnsync<'tx> = Cursor<'tx, Rw>;
61+
62+
// --- Iterator aliases ---
63+
64+
/// Iterates over KV pairs in an MDBX database.
65+
pub type IterKeyVals<'tx, 'cur, K, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> =
66+
Iter<'tx, 'cur, K, Key, Value, { ffi::MDBX_NEXT }>;
67+
68+
/// An iterator over the key/value pairs in an MDBX `DUPSORT` with duplicate
69+
/// keys, yielding the first value for each key.
70+
///
71+
/// See the [`Iter`] documentation for more details.
72+
pub type IterDupKeys<'tx, 'cur, K, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> =
73+
Iter<'tx, 'cur, K, Key, Value, { ffi::MDBX_NEXT_NODUP }>;
74+
75+
/// An iterator over the key/value pairs in an MDBX `DUPSORT`, yielding each
76+
/// duplicate value for a specific key.
77+
pub type IterDupVals<'tx, 'cur, K, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> =
78+
Iter<'tx, 'cur, K, Key, Value, { ffi::MDBX_NEXT_DUP }>;
79+
80+
/// A key-value iterator for a synchronized read-only transaction.
81+
pub type RoIterSync<'tx, 'cur, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> =
82+
IterKeyVals<'tx, 'cur, RoSync, Key, Value>;
83+
84+
/// A key-value iterator for a synchronized read-write transaction.
85+
pub type RwIterSync<'tx, 'cur, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> =
86+
IterKeyVals<'tx, 'cur, RwSync, Key, Value>;
87+
88+
/// A key-value iterator for an unsynchronized read-only transaction.
89+
pub type RoIterUnsync<'tx, 'cur, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> =
90+
IterKeyVals<'tx, 'cur, Ro, Key, Value>;
91+
92+
/// A key-value iterator for an unsynchronized read-write transaction.
93+
pub type RwIterUnsync<'tx, 'cur, Key = Cow<'tx, [u8]>, Value = Cow<'tx, [u8]>> =
94+
IterKeyVals<'tx, 'cur, Rw, Key, Value>;
95+
96+
/// A flattening DUPFIXED iterator for a synchronized read-only transaction.
97+
pub type RoDupFixedIterSync<'tx, 'cur, Key = Cow<'tx, [u8]>, const VALUE_SIZE: usize = 0> =
98+
IterDupFixed<'tx, 'cur, RoSync, Key, VALUE_SIZE>;
99+
100+
/// A flattening DUPFIXED iterator for a synchronized read-write transaction.
101+
pub type RwDupFixedIterSync<'tx, 'cur, Key = Cow<'tx, [u8]>, const VALUE_SIZE: usize = 0> =
102+
IterDupFixed<'tx, 'cur, RwSync, Key, VALUE_SIZE>;
103+
104+
/// A flattening DUPFIXED iterator for an unsynchronized read-only transaction.
105+
pub type RoDupFixedIterUnsync<'tx, 'cur, Key = Cow<'tx, [u8]>, const VALUE_SIZE: usize = 0> =
106+
IterDupFixed<'tx, 'cur, Ro, Key, VALUE_SIZE>;
107+
108+
/// A flattening DUPFIXED iterator for an unsynchronized read-write transaction.
109+
pub type RwDupFixedIterUnsync<'tx, 'cur, Key = Cow<'tx, [u8]>, const VALUE_SIZE: usize = 0> =
110+
IterDupFixed<'tx, 'cur, Rw, Key, VALUE_SIZE>;
111+
112+
/// A single-key DUPFIXED iterator for a synchronized read-only transaction.
113+
pub type RoDupFixedIterOfKeySync<'tx, 'cur, const VALUE_SIZE: usize = 0> =
114+
IterDupFixedOfKey<'tx, 'cur, RoSync, VALUE_SIZE>;
115+
116+
/// A single-key DUPFIXED iterator for a synchronized read-write transaction.
117+
pub type RwDupFixedIterOfKeySync<'tx, 'cur, const VALUE_SIZE: usize = 0> =
118+
IterDupFixedOfKey<'tx, 'cur, RwSync, VALUE_SIZE>;
119+
120+
/// A single-key DUPFIXED iterator for an unsynchronized read-only transaction.
121+
pub type RoDupFixedIterOfKeyUnsync<'tx, 'cur, const VALUE_SIZE: usize = 0> =
122+
IterDupFixedOfKey<'tx, 'cur, Ro, VALUE_SIZE>;
123+
124+
/// A single-key DUPFIXED iterator for an unsynchronized read-write transaction.
125+
pub type RwDupFixedIterOfKeyUnsync<'tx, 'cur, const VALUE_SIZE: usize = 0> =
126+
IterDupFixedOfKey<'tx, 'cur, Rw, VALUE_SIZE>;

src/tx/assertions.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
1010
#![allow(dead_code)]
1111

12-
use crate::flags::DatabaseFlags;
12+
use crate::DatabaseFlags;
1313

1414
/// Debug assertion that validates key size constraints.
1515
///
@@ -190,3 +190,23 @@ fn debug_assert_append_dup_order(flags: DatabaseFlags, last_dup: Option<&[u8]>,
190190
#[cfg(not(debug_assertions))]
191191
let _ = (flags, last_dup, new_data);
192192
}
193+
194+
/// Debug assertion that validates DUP_SORT flag is set.
195+
#[inline(always)]
196+
#[track_caller]
197+
pub(crate) fn debug_assert_dup_sort(flags: DatabaseFlags) {
198+
debug_assert!(
199+
flags.contains(DatabaseFlags::DUP_SORT),
200+
"Operation requires DUP_SORT database flag"
201+
);
202+
}
203+
204+
/// Debug assertion that validates DUP_FIXED flag is set.
205+
#[inline(always)]
206+
#[track_caller]
207+
pub(crate) fn debug_assert_dup_fixed(flags: DatabaseFlags) {
208+
debug_assert!(
209+
flags.contains(DatabaseFlags::DUP_FIXED),
210+
"Operation requires DUP_FIXED database flag"
211+
);
212+
}

src/tx/cache.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
//! - [`SharedCache`]: A thread-safe cache using `Arc<RwLock<...>>` for
1212
//! synchronized transactions.
1313
//!
14-
//! [`TxSync`]: crate::tx::TxSync
15-
//! [`TxUnsync`]: crate::tx::TxUnsync
14+
//! [`TxSync`]: crate::tx::aliases::TxSync
15+
//! [`TxUnsync`]: crate::tx::aliases::TxUnsync
1616
1717
use crate::Database;
1818
use parking_lot::RwLock;

0 commit comments

Comments
 (0)