Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,21 @@ 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
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<K> | Sync + Send | Multi-threaded wrapper using Mutex and Arc to share a RO or RW transaction safely across threads | Yes, via synchronization wrappers |
| TxUnsync<RO> | !Sync + Send | Single-threaded RO transaction without synchronization overhead | Yes, via required &mut or & access |
| TxUnsync<RW> | !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`)
Expand Down Expand Up @@ -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
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -57,3 +57,7 @@ harness = false
[[bench]]
name = "transaction"
harness = false

[[bench]]
name = "iter"
harness = false
122 changes: 122 additions & 0 deletions benches/iter.rs
Original file line number Diff line number Diff line change
@@ -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);
2 changes: 1 addition & 1 deletion benches/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
5 changes: 2 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/sys/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/tx/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
126 changes: 126 additions & 0 deletions src/tx/aliases.rs
Original file line number Diff line number Diff line change
@@ -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<K> = Tx<K, Arc<PtrSync>>;

/// Transaction type for unsynchronized access.
pub type TxUnsync<K> = Tx<K, PtrUnsync>;

/// A synchronized read-only transaction.
pub type RoTxSync = TxSync<RoSync>;

/// A synchronized read-write transaction.
pub type RwTxSync = TxSync<RwSync>;

/// An unsynchronized read-only transaction.
pub type RoTxUnsync = TxUnsync<Ro>;

/// An unsynchronized read-write transaction.
pub type RwTxUnsync = TxUnsync<Rw>;

// SAFETY:
// - RoTxSync and RwTxSync use Arc<PtrSync> 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>;
22 changes: 21 additions & 1 deletion src/tx/assertions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

#![allow(dead_code)]

use crate::flags::DatabaseFlags;
use crate::DatabaseFlags;

/// Debug assertion that validates key size constraints.
///
Expand Down Expand Up @@ -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"
);
}
4 changes: 2 additions & 2 deletions src/tx/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
//! - [`SharedCache`]: A thread-safe cache using `Arc<RwLock<...>>` 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;
Expand Down
Loading