Skip to content
Closed
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@
existing tables report `TableTypeMismatch` and must be migrated. Structs whose fields are
all built-in types are unaffected.

## 4.X - Unreleased
* Deprecate `ReadOnlyTable::get()`, `ReadOnlyTable::range()`, `ReadOnlyMultimapTable::get()`,
and `ReadOnlyMultimapTable::range()` in favor of the `_owned` variants. Contrary to their
documentation, the `'static` access guards they return, or yield, do not keep the transaction
alive on their own: holding one after the table and iterator it came from have been dropped
allows concurrent writers to reclaim the referenced pages, which panics the writer's
`commit()` in debug builds. The deprecation warnings are gated behind the
`experimental-pre-api-5-deprecations` feature flag.

## 4.3.0 - 2026-XX-XX
* Add `Key::separator()`, which returns a short byte string that separates two keys, as a
`Cow` so it can also be synthesized rather than borrowed from the inputs. Internal btree
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ experimental_cursor = ["experimental-api-5"]
# Unstable additions and changes to the public API, planned for redb 5. May change incompatibly, or
# be removed, in any release
experimental-api-5 = []
# Deprecation warnings on APIs that redb 5 will remove. May change incompatibly, or be removed,
# in any release
experimental-pre-api-5-deprecations = []
Comment thread
cberner marked this conversation as resolved.

[profile.bench]
debug = true
Expand Down
3 changes: 3 additions & 0 deletions crates/redb-derive/tests/crate_attr_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ const OLD3_TABLE: redb3_0::TableDefinition<u32, Old3Value> = redb3_0::TableDefin
const NEW_TABLE: redb::TableDefinition<u32, NewValue> = redb::TableDefinition::new("new");

#[test]
// Reads through the inherent ReadOnlyTable::get(), which redb deprecates behind a feature flag.
// This crate cannot see that flag, so the allowance is unconditional.
#[allow(deprecated)]
fn derives_for_both_redb_versions() {
let old_file = create_tempfile();
let old_db = redb2_6::Database::create(old_file.path()).unwrap();
Expand Down
4 changes: 4 additions & 0 deletions crates/redb-derive/tests/derive_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ struct LifetimeNames<'a, 'b> {
reference2: &'b str,
}

// Reads through the inherent ReadOnlyTable::get(), which redb deprecates behind a feature flag.
// This crate cannot see that flag, so the allowance is unconditional.
#[allow(deprecated)]
fn test_key_helper<K: Key + 'static>(key: &<K as Value>::SelfType<'_>) {
let file = create_tempfile();
let db = Database::create(file.path()).unwrap();
Expand All @@ -73,6 +76,7 @@ fn test_key_helper<K: Key + 'static>(key: &<K as Value>::SelfType<'_>) {
assert_eq!(retrieved_value, 1);
}

#[allow(deprecated)]
fn test_value_helper<V: Value + 'static>(
value: <V as Value>::SelfType<'_>,
expected_type_name: &str,
Expand Down
41 changes: 37 additions & 4 deletions src/multimap_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1207,9 +1207,25 @@ impl<K: Key + 'static, V: Key + 'static> ReadOnlyMultimapTable<K, V> {
})
}

/// This method is like [`ReadableMultimapTable::get()`], but the iterator is reference counted and keeps the transaction
/// alive until it is dropped.
/// This method is like [`ReadableMultimapTable::get()`], but the iterator is `'static`
///
/// Note: contrary to what was previously documented, the guards yielded by the returned
/// iterator do not themselves keep the transaction alive. They stay valid only while this
/// table or the iterator does, since both hold the transaction open. If a guard outlives
/// them both, concurrent writers may reclaim the pages it references, which panics in debug
/// builds. Use [`ReadableMultimapTable::get()`] instead, whose guards borrow from the table
/// so that the compiler enforces this, or [`Self::get_owned()`] if the guards need to keep
/// the transaction alive on their own.
///
/// Enabling the `experimental-pre-api-5-deprecations` feature marks this method deprecated.
#[cfg(not(feature = "experimental-api-5"))]
#[cfg_attr(
feature = "experimental-pre-api-5-deprecations",
deprecated(
since = "4.2.0",
note = "the guards yielded by the iterator do not keep the transaction alive on their own, and can crash debug builds if they outlive both this table and the iterator; use ReadableMultimapTable::get(), or get_owned() if the guards need to keep the transaction alive"
)
)]
pub fn get<'a>(&self, key: impl Borrow<K::SelfType<'a>>) -> Result<MultimapValue<'static, V>> {
self.get_inner(key)
}
Expand Down Expand Up @@ -1253,9 +1269,26 @@ impl<K: Key + 'static, V: Key + 'static> ReadOnlyMultimapTable<K, V> {
))
}

/// This method is like [`ReadableMultimapTable::range()`], but the iterator is reference counted and keeps the transaction
/// alive until it is dropped.
/// This method is like [`ReadableMultimapTable::range()`], but the iterator is `'static`
///
/// Note: contrary to what was previously documented, the value guards yielded by the
/// [`MultimapValue`]s this iterator produces do not themselves keep the transaction alive.
/// They stay valid only while this table, the iterator, or that [`MultimapValue`] does,
/// since each of those holds the transaction open. If a guard outlives all three, concurrent
/// writers may reclaim the pages it references, which panics in debug builds. Use
/// [`ReadableMultimapTable::range()`] instead, whose guards borrow from the table so that
/// the compiler enforces this, or [`Self::range_owned()`] if the guards need to keep the
/// transaction alive on their own.
///
/// Enabling the `experimental-pre-api-5-deprecations` feature marks this method deprecated.
#[cfg(not(feature = "experimental-api-5"))]
#[cfg_attr(
feature = "experimental-pre-api-5-deprecations",
deprecated(
since = "4.2.0",
note = "the value guards yielded by the returned MultimapValues do not keep the transaction alive on their own, and can crash debug builds if they outlive this table, the iterator, and the MultimapValue; use ReadableMultimapTable::range(), or range_owned() if the guards need to keep the transaction alive"
)
)]
pub fn range<'a, KR>(&self, range: impl RangeBounds<KR>) -> Result<MultimapRange<'static, K, V>>
where
KR: Borrow<K::SelfType<'a>>,
Expand Down
42 changes: 37 additions & 5 deletions src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ pub trait ReadableTable<K: Key + 'static, V: Value + 'static>: ReadableTableMeta
///
/// let read_txn = db.begin_read()?;
/// let table = read_txn.open_table(TABLE)?;
/// let mut iter = table.range("a".."c")?;
/// let mut iter = ReadableTable::range(&table, "a".."c")?;
/// let (key, value) = iter.next().unwrap()?;
/// assert_eq!("a", key.value());
/// assert_eq!(0, value.value());
Expand Down Expand Up @@ -881,9 +881,25 @@ impl<K: Key + 'static, V: Value + 'static> ReadOnlyTable<K, V> {
})
}

/// This method is like [`ReadableTable::get()`], but the [`AccessGuard`] is reference counted
/// and keeps the transaction alive until it is dropped.
/// This method is like [`ReadableTable::get()`], but the guard is `'static`
///
/// Note: contrary to what was previously documented, the returned guard does not itself keep
/// the transaction alive. It stays valid only while this table does, since the table is what
/// holds the transaction open. If the guard outlives the table, concurrent writers may
/// reclaim the pages it references, which panics in debug builds. Use
/// [`ReadableTable::get()`] instead, whose guard borrows from the table so that the compiler
/// enforces this, or [`Self::get_owned()`] if the guard needs to keep the transaction alive
/// on its own.
///
/// Enabling the `experimental-pre-api-5-deprecations` feature marks this method deprecated.
#[cfg(not(feature = "experimental-api-5"))]
#[cfg_attr(
feature = "experimental-pre-api-5-deprecations",
deprecated(
since = "4.2.0",
note = "the returned guard does not keep the transaction alive on its own, and can crash debug builds if it outlives this table; use ReadableTable::get(), or get_owned() if the guard needs to keep the transaction alive"
)
)]
pub fn get<'a>(
&self,
key: impl Borrow<K::SelfType<'a>>,
Expand All @@ -903,9 +919,25 @@ impl<K: Key + 'static, V: Value + 'static> ReadOnlyTable<K, V> {
.map(|x| OwnedAccessGuard::new(x, self.transaction_guard.clone())))
}

/// This method is like [`ReadableTable::range()`], but the iterator is reference counted and keeps the transaction
/// alive until it is dropped.
/// This method is like [`ReadableTable::range()`], but the iterator is `'static`
///
/// Note: contrary to what was previously documented, the guards yielded by the returned
/// iterator do not themselves keep the transaction alive. They stay valid only while this
/// table or the iterator does, since both hold the transaction open. If a guard outlives
/// them both, concurrent writers may reclaim the pages it references, which panics in debug
/// builds. Use [`ReadableTable::range()`] instead, whose guards borrow from the table so
/// that the compiler enforces this, or [`Self::range_owned()`] if the guards need to keep
/// the transaction alive on their own.
///
/// Enabling the `experimental-pre-api-5-deprecations` feature marks this method deprecated.
#[cfg(not(feature = "experimental-api-5"))]
#[cfg_attr(
feature = "experimental-pre-api-5-deprecations",
deprecated(
since = "4.2.0",
note = "the guards yielded by the iterator do not keep the transaction alive on their own, and can crash debug builds if they outlive both this table and the iterator; use ReadableTable::range(), or range_owned() if the guards need to keep the transaction alive"
)
)]
pub fn range<'a, KR>(&self, range: impl RangeBounds<KR>) -> Result<Range<'static, K, V>>
where
KR: Borrow<K::SelfType<'a>>,
Expand Down
2 changes: 2 additions & 0 deletions src/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2814,6 +2814,7 @@ mod test {
// it completes. Verify the resulting contract: writes are refused, reads keep working, the
// shutdown is not recorded as clean, and reopening repairs the database.
#[test]
#[allow(deprecated)] // reads through the deprecated inherent ReadOnlyTable::get()
fn discarded_allocator_state_poisons_database() {
let tmpfile = crate::create_tempfile();
let db = Database::create(tmpfile.path()).unwrap();
Expand Down Expand Up @@ -2990,6 +2991,7 @@ mod test {
// close may record a clean shutdown again; a read-only open requires one
#[cfg(panic = "unwind")]
#[test]
#[allow(deprecated)] // reads through the deprecated inherent ReadOnlyTable::get()
fn check_integrity_clears_leak_latch() {
let tmpfile = crate::create_tempfile();
let mut db = Database::create(tmpfile.path()).unwrap();
Expand Down
2 changes: 2 additions & 0 deletions src/tree_store/page_store/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,7 @@ mod test {
// wrong, it reaches the assertion in MutateHelper::finish_deletion -- for the system root, via
// the commit every Database::drop makes. See https://github.com/cberner/redb/issues/1303
#[test]
#[allow(deprecated)] // reads through the deprecated inherent ReadOnlyTable::get()
fn check_integrity_recomputes_root_lengths() {
let tmpfile = crate::create_tempfile();
create_database_with_one_table(tmpfile.path());
Expand Down Expand Up @@ -1096,6 +1097,7 @@ mod test {
// A torn commit slot can carry an invalid page number. Repair must treat that as a bad primary
// and fall back to the secondary, as it does for a checksum mismatch.
#[test]
#[allow(deprecated)] // reads through the deprecated inherent ReadOnlyTable::get()
fn repair_falls_back_to_secondary_on_invalid_primary_root() {
let tmpfile = crate::create_tempfile();
{
Expand Down
11 changes: 11 additions & 0 deletions tests/backward_compatibility.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
// The deprecated ReadOnlyTable and ReadOnlyMultimapTable accessors are exercised throughout
// these tests; they remain covered until they are removed. Scoped to the configuration that
// deprecates them, so that unrelated deprecations are still reported in other builds.
#![cfg_attr(
all(
feature = "experimental-pre-api-5-deprecations",
not(feature = "experimental-api-5")
),
allow(deprecated)
)]

#[cfg(feature = "experimental-api-5")]
use redb::ReadableTable;
use redb::{ReadableDatabase, ReadableTableMetadata};
Expand Down
11 changes: 11 additions & 0 deletions tests/basic_tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
// The deprecated ReadOnlyTable and ReadOnlyMultimapTable accessors are exercised throughout
// these tests; they remain covered until they are removed. Scoped to the configuration that
// deprecates them, so that unrelated deprecations are still reported in other builds.
#![cfg_attr(
all(
feature = "experimental-pre-api-5-deprecations",
not(feature = "experimental-api-5")
),
allow(deprecated)
)]

use rand::random;
#[cfg(not(target_os = "wasi"))]
use redb::CommitError;
Expand Down
11 changes: 11 additions & 0 deletions tests/check_integrity_nondurable.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
// The deprecated ReadOnlyTable and ReadOnlyMultimapTable accessors are exercised throughout
// these tests; they remain covered until they are removed. Scoped to the configuration that
// deprecates them, so that unrelated deprecations are still reported in other builds.
#![cfg_attr(
all(
feature = "experimental-pre-api-5-deprecations",
not(feature = "experimental-api-5")
),
allow(deprecated)
)]

//! Tests for `check_integrity()` with a pending `Durability::None` commit. The check promotes such
//! a commit to durable when the live state verifies (so acknowledged data is not lost); it refuses
//! to promote when the backing file was externally truncated or extended, falling back to
Expand Down
2 changes: 2 additions & 0 deletions tests/corrupted_btree_descent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ fn opens_cleanly(path: &Path) -> bool {
// seeding its allocator assertions, and rejects the cycle there. This test is about the descent
// itself, which the read-only open reaches without that walk.
#[test]
// Reads through the deprecated inherent ReadOnlyTable::get() and range()
#[allow(deprecated)]
fn cyclic_branch_pointer_is_reported_rather_than_overflowing_the_stack() {
let tmpfile = create_tempfile();
{
Expand Down
11 changes: 11 additions & 0 deletions tests/crash_consistency.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
// The deprecated ReadOnlyTable and ReadOnlyMultimapTable accessors are exercised throughout
// these tests; they remain covered until they are removed. Scoped to the configuration that
// deprecates them, so that unrelated deprecations are still reported in other builds.
#![cfg_attr(
all(
feature = "experimental-pre-api-5-deprecations",
not(feature = "experimental-api-5")
),
allow(deprecated)
)]

//! Regression test for a crash during a transaction that grows the database file.
//!
//! `grow()` extends the file with `set_len` and a subsequent commit writes the grown layout into
Expand Down
11 changes: 11 additions & 0 deletions tests/integration_tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
// The deprecated ReadOnlyTable and ReadOnlyMultimapTable accessors are exercised throughout
// these tests; they remain covered until they are removed. Scoped to the configuration that
// deprecates them, so that unrelated deprecations are still reported in other builds.
#![cfg_attr(
all(
feature = "experimental-pre-api-5-deprecations",
not(feature = "experimental-api-5")
),
allow(deprecated)
)]

use rand::RngExt;
use rand::prelude::SliceRandom;
#[cfg(feature = "experimental-api-5")]
Expand Down
11 changes: 11 additions & 0 deletions tests/multimap_tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
// The deprecated ReadOnlyTable and ReadOnlyMultimapTable accessors are exercised throughout
// these tests; they remain covered until they are removed. Scoped to the configuration that
// deprecates them, so that unrelated deprecations are still reported in other builds.
#![cfg_attr(
all(
feature = "experimental-pre-api-5-deprecations",
not(feature = "experimental-api-5")
),
allow(deprecated)
)]

use redb::{
Database, MultimapTableDefinition, ReadableDatabase, ReadableMultimapTable,
ReadableTableMetadata, TableError, TransactionError,
Expand Down
1 change: 1 addition & 0 deletions tests/multithreading_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ mod multithreading_test {
// allocate and dirty pages concurrently, and removals of committed entries queue pages on the
// transaction's shared freed list from multiple threads.
#[test]
#[allow(deprecated)] // reads through the deprecated inherent ReadOnlyTable::get()
fn multithreaded_insert_and_remove() {
let tmpfile = create_tempfile();
let db = Database::create(tmpfile.path()).unwrap();
Expand Down
Loading