use noxu_db::{
CursorConfig, DatabaseConfig, DatabaseEntry, EnvironmentConfig, Get,
LockMode, OperationStatus, Put, TransactionConfig,
};
use tempfile::TempDir;
fn open(dir: &TempDir) -> (noxu_db::Environment, noxu_db::Database) {
let env_config = EnvironmentConfig::new(dir.path().to_path_buf())
.with_allow_create(true)
.with_transactional(true);
let env = noxu_db::Environment::open(env_config).unwrap();
let db_config = DatabaseConfig::new().with_allow_create(true);
let db = env.open_database(None, "test", &db_config).unwrap();
(env, db)
}
#[allow(dead_code)]
fn open_named(
dir: &TempDir,
name: &str,
) -> (noxu_db::Environment, noxu_db::Database) {
let env_config = EnvironmentConfig::new(dir.path().to_path_buf())
.with_allow_create(true)
.with_transactional(true);
let env = noxu_db::Environment::open(env_config).unwrap();
let db_config = DatabaseConfig::new().with_allow_create(true);
let db = env.open_database(None, name, &db_config).unwrap();
(env, db)
}
fn kv(k: u32, v: u32) -> (DatabaseEntry, DatabaseEntry) {
(
DatabaseEntry::from_bytes(&k.to_be_bytes()),
DatabaseEntry::from_bytes(&v.to_be_bytes()),
)
}
#[test]
fn database_txn_put_get_delete() {
let dir = TempDir::new().unwrap();
let (env, db) = open(&dir);
let txn = env.begin_transaction(None).unwrap();
let (k, v) = kv(1, 100);
assert_eq!(db.put(Some(&txn), &k, &v).unwrap(), OperationStatus::Success);
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(Some(&txn), &k, &mut out).unwrap(),
OperationStatus::Success
);
assert_eq!(out.data(), 100u32.to_be_bytes());
txn.commit().unwrap();
let mut out2 = DatabaseEntry::new();
assert_eq!(db.get(None, &k, &mut out2).unwrap(), OperationStatus::Success);
assert_eq!(out2.data(), 100u32.to_be_bytes());
}
#[test]
fn database_delete_nonexistent_returns_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
let k = DatabaseEntry::from_bytes(b"absent");
assert_eq!(db.delete(None, &k).unwrap(), OperationStatus::NotFound);
}
#[test]
fn database_put_replaces_existing_value() {
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
let k = DatabaseEntry::from_bytes(b"k");
db.put(None, &k, &DatabaseEntry::from_bytes(b"v1")).unwrap();
db.put(None, &k, &DatabaseEntry::from_bytes(b"v2")).unwrap();
let mut out = DatabaseEntry::new();
db.get(None, &k, &mut out).unwrap();
assert_eq!(out.data(), b"v2");
}
#[test]
fn database_count_after_insert_delete() {
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
assert_eq!(db.count().unwrap(), 0);
for i in 0u32..10 {
let (k, v) = kv(i, i);
db.put(None, &k, &v).unwrap();
}
assert_eq!(db.count().unwrap(), 10);
for i in 0u32..5 {
let (k, _) = kv(i, 0);
db.delete(None, &k).unwrap();
}
assert_eq!(db.count().unwrap(), 5);
}
#[test]
fn database_put_no_overwrite_returns_key_exists() {
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
let k = DatabaseEntry::from_bytes(b"k");
db.put(None, &k, &DatabaseEntry::from_bytes(b"original")).unwrap();
let status = db
.put_no_overwrite(None, &k, &DatabaseEntry::from_bytes(b"overwrite"))
.unwrap();
assert_eq!(status, OperationStatus::KeyExists);
let mut out = DatabaseEntry::new();
db.get(None, &k, &mut out).unwrap();
assert_eq!(out.data(), b"original");
}
#[test]
fn truncate_database_clears_all_records() {
let dir = TempDir::new().unwrap();
let (env, db) = open(&dir);
const N: u32 = 50;
for i in 0..N {
let (k, v) = kv(i, i * 2);
db.put(None, &k, &v).unwrap();
}
assert_eq!(db.count().unwrap(), N as u64);
db.close().unwrap();
let count_before = env.truncate_database(None, "test").unwrap();
assert_eq!(
count_before, N as u64,
"truncate must return pre-truncation count"
);
let db_cfg =
DatabaseConfig::new().with_allow_create(false).with_transactional(true);
let db2 = env.open_database(None, "test", &db_cfg).unwrap();
assert_eq!(db2.count().unwrap(), 0);
for i in 0..N {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let mut out = DatabaseEntry::new();
assert_eq!(
db2.get(None, &k, &mut out).unwrap(),
OperationStatus::NotFound,
"key {i} must be absent after truncate"
);
}
}
#[test]
fn truncate_then_add_records_works() {
let dir = TempDir::new().unwrap();
let (env, db) = open(&dir);
for i in 0u32..20 {
let (k, v) = kv(i, i);
db.put(None, &k, &v).unwrap();
}
db.close().unwrap();
env.truncate_database(None, "test").unwrap();
let db_cfg =
DatabaseConfig::new().with_allow_create(false).with_transactional(true);
let db = env.open_database(None, "test", &db_cfg).unwrap();
assert_eq!(db.count().unwrap(), 0);
for i in 100u32..110 {
let (k, v) = kv(i, i * 3);
db.put(None, &k, &v).unwrap();
}
assert_eq!(db.count().unwrap(), 10);
for i in 100u32..110 {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(None, &k, &mut out).unwrap(),
OperationStatus::Success
);
assert_eq!(out.data(), (i * 3).to_be_bytes());
}
}
#[test]
fn truncate_empty_database_returns_zero() {
let dir = TempDir::new().unwrap();
let (env, db) = open(&dir);
db.close().unwrap();
let count = env.truncate_database(None, "test").unwrap();
assert_eq!(count, 0);
}
#[test]
fn truncate_nonexistent_database_errors() {
let dir = TempDir::new().unwrap();
let (env, _db) = open(&dir);
let result = env.truncate_database(None, "nosuchdb");
assert!(result.is_err(), "truncate of non-existent DB must return error");
}
#[test]
fn read_uncommitted_sees_dirty_write() {
use std::sync::{Arc, Barrier};
use std::thread;
let dir = TempDir::new().unwrap();
let env_config = EnvironmentConfig::new(dir.path().to_path_buf())
.with_allow_create(true)
.with_transactional(true);
let env = Arc::new(noxu_db::Environment::open(env_config).unwrap());
let db_config = DatabaseConfig::new().with_allow_create(true);
let db = Arc::new(env.open_database(None, "test", &db_config).unwrap());
{
let txn = env.begin_transaction(None).unwrap();
db.put(
Some(&txn),
&DatabaseEntry::from_bytes(b"key"),
&DatabaseEntry::from_bytes(b"baseline"),
)
.unwrap();
txn.commit().unwrap();
}
let write_barrier = Arc::new(Barrier::new(2));
let read_barrier = Arc::new(Barrier::new(2));
let env_w = Arc::clone(&env);
let db_w = Arc::clone(&db);
let wb = Arc::clone(&write_barrier);
let rb = Arc::clone(&read_barrier);
let writer = thread::spawn(move || {
let txn = env_w.begin_transaction(None).unwrap();
db_w.put(
Some(&txn),
&DatabaseEntry::from_bytes(b"key"),
&DatabaseEntry::from_bytes(b"dirty"),
)
.unwrap();
wb.wait(); rb.wait(); txn.abort().unwrap();
});
write_barrier.wait();
let cursor_cfg = CursorConfig::read_uncommitted();
let mut cursor = db.open_cursor(None, Some(&cursor_cfg)).unwrap();
let mut key = DatabaseEntry::from_bytes(b"key");
let mut data = DatabaseEntry::new();
let status = cursor
.get(&mut key, &mut data, Get::Search, Some(LockMode::ReadUncommitted))
.unwrap();
assert_eq!(status, OperationStatus::Success);
assert!(
data.data() == b"dirty" || data.data() == b"baseline",
"ReadUncommitted must return some value without blocking"
);
cursor.close().unwrap();
read_barrier.wait(); writer.join().unwrap();
}
#[test]
fn read_uncommitted_cursor_config_no_blocking() {
let dir = TempDir::new().unwrap();
let (env, db) = open(&dir);
for i in 0u32..5 {
let (k, v) = kv(i, i * 10);
db.put(None, &k, &v).unwrap();
}
let cursor_cfg = CursorConfig::read_uncommitted();
let mut cursor = db.open_cursor(None, Some(&cursor_cfg)).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
let mut count = 0u32;
let mut status = cursor.get(&mut key, &mut data, Get::First, None).unwrap();
while status == OperationStatus::Success {
count += 1;
status = cursor.get(&mut key, &mut data, Get::Next, None).unwrap();
}
cursor.close().unwrap();
assert_eq!(count, 5, "ReadUncommitted cursor must scan all 5 records");
drop(db);
drop(env);
}
#[test]
fn large_scale_insert_search_scan_257() {
const N: u32 = 257;
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
for i in 0u32..N {
let (k, v) = kv(i, i * 3);
db.put(None, &k, &v).unwrap();
}
assert_eq!(db.count().unwrap(), N as u64);
for i in 0u32..N {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(None, &k, &mut out).unwrap(),
OperationStatus::Success,
"key {i} must be findable after {N} inserts"
);
assert_eq!(
out.data(),
(i * 3).to_be_bytes(),
"value mismatch for key {i}"
);
}
let mut cursor = db.open_cursor(None, None).unwrap();
let mut seen = Vec::new();
let mut k = DatabaseEntry::new();
let mut v = DatabaseEntry::new();
let mut s = cursor.get(&mut k, &mut v, Get::First, None).unwrap();
while s == OperationStatus::Success {
seen.push(u32::from_be_bytes(k.data().try_into().unwrap()));
s = cursor.get(&mut k, &mut v, Get::Next, None).unwrap();
}
cursor.close().unwrap();
assert_eq!(seen.len(), N as usize);
for (i, &val) in seen.iter().enumerate() {
assert_eq!(val, i as u32, "cursor must visit keys in ascending order");
}
}
#[test]
fn large_scale_10k_deep_tree_correctness() {
const N: u32 = 10_000;
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
for i in (0u32..N).rev() {
let (k, v) = kv(i, i.wrapping_mul(0x9e37_9117));
db.put(None, &k, &v).unwrap();
}
assert_eq!(
db.count().unwrap(),
N as u64,
"count must equal N after {N} inserts"
);
for i in (0u32..N).step_by(100) {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(None, &k, &mut out).unwrap(),
OperationStatus::Success,
"key {i} missing after 10K inserts"
);
let expected = i.wrapping_mul(0x9e37_9117);
assert_eq!(
out.data(),
expected.to_be_bytes(),
"wrong value for key {i}"
);
}
}
#[test]
fn large_scale_interleaved_insert_delete() {
const N: u32 = 500;
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
for i in 0u32..N {
let (k, v) = kv(i, i * 7);
db.put(None, &k, &v).unwrap();
}
for i in (1u32..N).step_by(2) {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
assert_eq!(db.delete(None, &k).unwrap(), OperationStatus::Success);
}
let expected_count = (N / 2) as u64; assert_eq!(db.count().unwrap(), expected_count);
for i in (0u32..N).step_by(2) {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(None, &k, &mut out).unwrap(),
OperationStatus::Success,
"even key {i} must survive delete of odd keys"
);
assert_eq!(out.data(), (i * 7).to_be_bytes());
}
for i in (1u32..N).step_by(2) {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(None, &k, &mut out).unwrap(),
OperationStatus::NotFound,
"odd key {i} must be absent after delete"
);
}
}
#[test]
fn recovery_across_checkpoint_boundary() {
const BATCH1: u32 = 100;
const BATCH2: u32 = 100;
let dir = TempDir::new().unwrap();
{
let env_config = EnvironmentConfig::new(dir.path().to_path_buf())
.with_allow_create(true)
.with_transactional(true)
.with_checkpointer_bytes_interval(1); let env = noxu_db::Environment::open(env_config).unwrap();
let db_cfg = DatabaseConfig::new().with_allow_create(true);
let db = env.open_database(None, "test", &db_cfg).unwrap();
for i in 0u32..BATCH1 {
let (k, v) = kv(i, i + 1000);
db.put(None, &k, &v).unwrap();
}
std::thread::sleep(std::time::Duration::from_millis(50));
for i in BATCH1..(BATCH1 + BATCH2) {
let (k, v) = kv(i, i + 2000);
db.put(None, &k, &v).unwrap();
}
drop(db);
drop(env);
}
{
let env_config = EnvironmentConfig::new(dir.path().to_path_buf())
.with_allow_create(true)
.with_transactional(true);
let env = noxu_db::Environment::open(env_config).unwrap();
let db_cfg = DatabaseConfig::new().with_allow_create(true);
let db = env.open_database(None, "test", &db_cfg).unwrap();
for i in 0u32..BATCH1 {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(None, &k, &mut out).unwrap(),
OperationStatus::Success,
"batch1 key {i} missing after recovery"
);
assert_eq!(out.data(), (i + 1000).to_be_bytes());
}
for i in BATCH1..(BATCH1 + BATCH2) {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(None, &k, &mut out).unwrap(),
OperationStatus::Success,
"batch2 key {i} missing after recovery"
);
assert_eq!(out.data(), (i + 2000).to_be_bytes());
}
}
}
#[test]
fn txn_abort_insert_not_visible() {
let dir = TempDir::new().unwrap();
let (env, db) = open(&dir);
let txn = env.begin_transaction(None).unwrap();
let (k, v) = kv(42, 999);
db.put(Some(&txn), &k, &v).unwrap();
txn.abort().unwrap();
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(None, &k, &mut out).unwrap(),
OperationStatus::NotFound,
"aborted insert must not be visible"
);
}
#[test]
fn txn_abort_update_restores_original_value() {
let dir = TempDir::new().unwrap();
let (env, db) = open(&dir);
let (k, v_orig) = kv(42, 100);
db.put(None, &k, &v_orig).unwrap();
let txn = env.begin_transaction(None).unwrap();
db.put(Some(&txn), &k, &DatabaseEntry::from_bytes(&200u32.to_be_bytes()))
.unwrap();
txn.abort().unwrap();
let mut out = DatabaseEntry::new();
assert_eq!(db.get(None, &k, &mut out).unwrap(), OperationStatus::Success);
assert_eq!(
out.data(),
100u32.to_be_bytes(),
"abort must restore pre-update value"
);
}
#[test]
fn txn_abort_delete_restores_record() {
let dir = TempDir::new().unwrap();
let (env, db) = open(&dir);
let (k, v) = kv(7, 777);
db.put(None, &k, &v).unwrap();
let txn = env.begin_transaction(None).unwrap();
db.delete(Some(&txn), &k).unwrap();
txn.abort().unwrap();
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(None, &k, &mut out).unwrap(),
OperationStatus::Success,
"aborted delete must restore the record"
);
assert_eq!(out.data(), 777u32.to_be_bytes());
}
#[test]
fn txn_abort_multiple_ops_restores_prior_state() {
let dir = TempDir::new().unwrap();
let (env, db) = open(&dir);
for i in 0u32..5 {
let (k, v) = kv(i, i);
db.put(None, &k, &v).unwrap();
}
let txn = env.begin_transaction(None).unwrap();
db.put(
Some(&txn),
&DatabaseEntry::from_bytes(&10u32.to_be_bytes()),
&DatabaseEntry::from_bytes(&10u32.to_be_bytes()),
)
.unwrap();
db.put(
Some(&txn),
&DatabaseEntry::from_bytes(&2u32.to_be_bytes()),
&DatabaseEntry::from_bytes(&99u32.to_be_bytes()),
)
.unwrap();
db.delete(Some(&txn), &DatabaseEntry::from_bytes(&4u32.to_be_bytes()))
.unwrap();
txn.abort().unwrap();
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(
None,
&DatabaseEntry::from_bytes(&10u32.to_be_bytes()),
&mut out
)
.unwrap(),
OperationStatus::NotFound
);
assert_eq!(
db.get(None, &DatabaseEntry::from_bytes(&2u32.to_be_bytes()), &mut out)
.unwrap(),
OperationStatus::Success
);
assert_eq!(out.data(), 2u32.to_be_bytes());
assert_eq!(
db.get(None, &DatabaseEntry::from_bytes(&4u32.to_be_bytes()), &mut out)
.unwrap(),
OperationStatus::Success
);
assert_eq!(out.data(), 4u32.to_be_bytes());
for i in [0u32, 1, 3] {
assert_eq!(
db.get(
None,
&DatabaseEntry::from_bytes(&i.to_be_bytes()),
&mut out
)
.unwrap(),
OperationStatus::Success
);
assert_eq!(out.data(), i.to_be_bytes());
}
}
#[test]
fn cursor_edge_empty_database_all_ops_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut k = DatabaseEntry::new();
let mut v = DatabaseEntry::new();
for op in [Get::First, Get::Last] {
assert_eq!(
cursor.get(&mut k, &mut v, op, None).unwrap(),
OperationStatus::NotFound,
"{op:?} on empty DB must return NotFound"
);
}
cursor.close().unwrap();
}
#[test]
fn cursor_edge_search_after_delete_returns_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
let (k, v) = kv(5, 50);
db.put(None, &k, &v).unwrap();
db.delete(None, &k).unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let mut search_k = DatabaseEntry::from_bytes(&5u32.to_be_bytes());
let mut out = DatabaseEntry::new();
assert_eq!(
cursor.get(&mut search_k, &mut out, Get::Search, None).unwrap(),
OperationStatus::NotFound
);
cursor.close().unwrap();
}
#[test]
fn cursor_edge_skip_deleted_records() {
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
for i in 0u32..10 {
let (k, v) = kv(i, i);
db.put(None, &k, &v).unwrap();
}
for del in [0u32, 5, 9] {
db.delete(None, &DatabaseEntry::from_bytes(&del.to_be_bytes()))
.unwrap();
}
let mut cursor = db.open_cursor(None, None).unwrap();
let mut k = DatabaseEntry::new();
let mut v = DatabaseEntry::new();
let mut seen: Vec<u32> = Vec::new();
let mut s = cursor.get(&mut k, &mut v, Get::First, None).unwrap();
while s == OperationStatus::Success {
seen.push(u32::from_be_bytes(k.data().try_into().unwrap()));
s = cursor.get(&mut k, &mut v, Get::Next, None).unwrap();
}
cursor.close().unwrap();
let expected: Vec<u32> =
(0u32..10).filter(|&x| x != 0 && x != 5 && x != 9).collect();
assert_eq!(seen, expected, "cursor must skip deleted keys");
}
#[test]
fn cursor_edge_current_after_delete_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
let (k, v) = kv(1, 10);
db.put(None, &k, &v).unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let mut ck = DatabaseEntry::from_bytes(&1u32.to_be_bytes());
let mut cv = DatabaseEntry::new();
assert_eq!(
cursor.get(&mut ck, &mut cv, Get::Search, None).unwrap(),
OperationStatus::Success
);
db.delete(None, &DatabaseEntry::from_bytes(&1u32.to_be_bytes())).unwrap();
let status = cursor.get(&mut ck, &mut cv, Get::Current, None).unwrap();
assert_eq!(
status,
OperationStatus::NotFound,
"Current on deleted slot must return NotFound"
);
cursor.close().unwrap();
}
#[test]
fn cursor_search_gte_edge_cases() {
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
for k in [10u32, 20, 30] {
db.put(
None,
&DatabaseEntry::from_bytes(&k.to_be_bytes()),
&DatabaseEntry::from_bytes(&k.to_be_bytes()),
)
.unwrap();
}
let mut cursor = db.open_cursor(None, None).unwrap();
let mut k = DatabaseEntry::from_bytes(&5u32.to_be_bytes());
let mut v = DatabaseEntry::new();
assert_eq!(
cursor.get(&mut k, &mut v, Get::SearchGte, None).unwrap(),
OperationStatus::Success
);
assert_eq!(k.data(), 10u32.to_be_bytes());
let mut k = DatabaseEntry::from_bytes(&20u32.to_be_bytes());
assert_eq!(
cursor.get(&mut k, &mut v, Get::SearchGte, None).unwrap(),
OperationStatus::Success
);
assert_eq!(k.data(), 20u32.to_be_bytes());
let mut k = DatabaseEntry::from_bytes(&31u32.to_be_bytes());
assert_eq!(
cursor.get(&mut k, &mut v, Get::SearchGte, None).unwrap(),
OperationStatus::NotFound
);
cursor.close().unwrap();
}
#[test]
fn read_committed_allows_non_repeatable_read() {
use std::sync::{Arc, Barrier};
use std::thread;
let dir = TempDir::new().unwrap();
let env_cfg = EnvironmentConfig::new(dir.path().to_path_buf())
.with_allow_create(true)
.with_transactional(true);
let env = Arc::new(noxu_db::Environment::open(env_cfg).unwrap());
let db_cfg = DatabaseConfig::new().with_allow_create(true);
let db = Arc::new(env.open_database(None, "test", &db_cfg).unwrap());
{
let txn = env.begin_transaction(None).unwrap();
db.put(
Some(&txn),
&DatabaseEntry::from_bytes(b"key"),
&DatabaseEntry::from_bytes(b"v1"),
)
.unwrap();
txn.commit().unwrap();
}
let barrier_read1_done = Arc::new(Barrier::new(2));
let barrier_write_done = Arc::new(Barrier::new(2));
let env2 = Arc::clone(&env);
let db2 = Arc::clone(&db);
let b1 = Arc::clone(&barrier_read1_done);
let b2 = Arc::clone(&barrier_write_done);
let writer = thread::spawn(move || {
b1.wait(); let txn = env2.begin_transaction(None).unwrap();
db2.put(
Some(&txn),
&DatabaseEntry::from_bytes(b"key"),
&DatabaseEntry::from_bytes(b"v2"),
)
.unwrap();
txn.commit().unwrap();
b2.wait(); });
let rc_cfg = TransactionConfig::read_committed();
let txn1 = env.begin_transaction(Some(&rc_cfg)).unwrap();
let mut out = DatabaseEntry::new();
db.get(Some(&txn1), &DatabaseEntry::from_bytes(b"key"), &mut out).unwrap();
assert_eq!(out.data(), b"v1", "first read must see v1");
barrier_read1_done.wait(); barrier_write_done.wait();
let status =
db.get(Some(&txn1), &DatabaseEntry::from_bytes(b"key"), &mut out);
assert!(status.is_ok(), "second read must not error under read-committed");
txn1.abort().unwrap();
writer.join().unwrap();
}
#[test]
fn serializable_isolation_repeatable_read() {
use std::sync::{Arc, Barrier};
use std::thread;
use std::time::Duration;
let dir = TempDir::new().unwrap();
let env_cfg = EnvironmentConfig::new(dir.path().to_path_buf())
.with_allow_create(true)
.with_transactional(true);
let env = Arc::new(noxu_db::Environment::open(env_cfg).unwrap());
let db_cfg = DatabaseConfig::new().with_allow_create(true);
let db = Arc::new(env.open_database(None, "test", &db_cfg).unwrap());
{
let txn = env.begin_transaction(None).unwrap();
db.put(
Some(&txn),
&DatabaseEntry::from_bytes(b"key"),
&DatabaseEntry::from_bytes(b"v1"),
)
.unwrap();
txn.commit().unwrap();
}
let txn1 = env.begin_transaction(None).unwrap();
let mut out = DatabaseEntry::new();
db.get(Some(&txn1), &DatabaseEntry::from_bytes(b"key"), &mut out).unwrap();
let first_read = out.data().to_vec();
assert_eq!(first_read, b"v1");
let barrier_started = Arc::new(Barrier::new(2));
let env2 = Arc::clone(&env);
let db2 = Arc::clone(&db);
let bs = Arc::clone(&barrier_started);
let writer = thread::spawn(move || {
let no_wait_cfg = TransactionConfig::new().with_no_wait(true);
let txn2 = env2.begin_transaction(Some(&no_wait_cfg)).unwrap();
bs.wait();
let result = db2.put(
Some(&txn2),
&DatabaseEntry::from_bytes(b"key"),
&DatabaseEntry::from_bytes(b"v2"),
);
let _ = txn2.abort();
result
});
barrier_started.wait();
std::thread::sleep(Duration::from_millis(20));
let mut out2 = DatabaseEntry::new();
db.get(Some(&txn1), &DatabaseEntry::from_bytes(b"key"), &mut out2).unwrap();
let second_read = out2.data().to_vec();
assert_eq!(
second_read, b"v1",
"serializable: second read must equal first read"
);
txn1.commit().unwrap();
let writer_result = writer.join().unwrap();
let _ = writer_result; }
#[test]
fn multiple_databases_fully_isolated() {
const N: u32 = 50;
let dir = TempDir::new().unwrap();
let env_cfg = EnvironmentConfig::new(dir.path().to_path_buf())
.with_allow_create(true)
.with_transactional(true);
let env = noxu_db::Environment::open(env_cfg).unwrap();
let db_cfg = DatabaseConfig::new().with_allow_create(true);
let db_a = env.open_database(None, "A", &db_cfg).unwrap();
let db_b = env.open_database(None, "B", &db_cfg).unwrap();
for i in 0u32..N {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
db_a.put(None, &k, &DatabaseEntry::from_bytes(b"A")).unwrap();
db_b.put(None, &k, &DatabaseEntry::from_bytes(b"B")).unwrap();
}
for i in 0u32..N {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let mut out = DatabaseEntry::new();
db_a.get(None, &k, &mut out).unwrap();
assert_eq!(out.data(), b"A");
db_b.get(None, &k, &mut out).unwrap();
assert_eq!(out.data(), b"B");
}
assert_eq!(db_a.count().unwrap(), N as u64);
assert_eq!(db_b.count().unwrap(), N as u64);
}
#[test]
fn recovery_1000_records_survive_reopen() {
const N: u32 = 1_000;
let dir = TempDir::new().unwrap();
{
let (_env, db) = open(&dir);
for i in 0u32..N {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let v = DatabaseEntry::from_bytes(&(i ^ 0xdead_beef).to_be_bytes());
db.put(None, &k, &v).unwrap();
}
}
{
let (_env, db) = open(&dir);
assert_eq!(
db.count().unwrap(),
N as u64,
"all {N} records must survive reopen"
);
for i in 0u32..N {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(None, &k, &mut out).unwrap(),
OperationStatus::Success,
"key {i} missing after recovery"
);
assert_eq!(
out.data(),
(i ^ 0xdead_beef).to_be_bytes(),
"value corruption detected for key {i}"
);
}
}
}
#[test]
fn recovery_updates_are_durable() {
const N: u32 = 500;
let dir = TempDir::new().unwrap();
{
let (_env, db) = open(&dir);
for i in 0u32..N {
let (k, v) = kv(i, i);
db.put(None, &k, &v).unwrap();
}
for i in 0u32..N {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let v = DatabaseEntry::from_bytes(&(i + 10_000).to_be_bytes());
db.put(None, &k, &v).unwrap();
}
}
{
let (_env, db) = open(&dir);
for i in 0u32..N {
let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(None, &k, &mut out).unwrap(),
OperationStatus::Success
);
assert_eq!(
out.data(),
(i + 10_000).to_be_bytes(),
"key {i}: must see updated value after recovery, not original"
);
}
}
}
#[test]
fn cursor_count_non_dup_key_is_one() {
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
db.put(
None,
&DatabaseEntry::from_bytes(b"k"),
&DatabaseEntry::from_bytes(b"v"),
)
.unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let mut k = DatabaseEntry::from_bytes(b"k");
let mut v = DatabaseEntry::new();
cursor.get(&mut k, &mut v, Get::Search, None).unwrap();
assert_eq!(cursor.count().unwrap(), 1);
cursor.close().unwrap();
}
#[test]
fn cursor_put_overwrite_replaces_value() {
let dir = TempDir::new().unwrap();
let (_env, db) = open(&dir);
db.put(
None,
&DatabaseEntry::from_bytes(b"k"),
&DatabaseEntry::from_bytes(b"v1"),
)
.unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let mut k = DatabaseEntry::from_bytes(b"k");
let mut v = DatabaseEntry::new();
cursor.get(&mut k, &mut v, Get::Search, None).unwrap();
let new_v = DatabaseEntry::from_bytes(b"v2");
cursor.put(&k, &new_v, Put::Overwrite).unwrap();
cursor.close().unwrap();
let mut out = DatabaseEntry::new();
db.get(None, &DatabaseEntry::from_bytes(b"k"), &mut out).unwrap();
assert_eq!(out.data(), b"v2");
}
#[test]
fn environment_stats_non_negative_after_writes() {
let dir = TempDir::new().unwrap();
let env_cfg = EnvironmentConfig::new(dir.path().to_path_buf())
.with_allow_create(true)
.with_transactional(true);
let env = noxu_db::Environment::open(env_cfg).unwrap();
let db_cfg = DatabaseConfig::new().with_allow_create(true);
let db = env.open_database(None, "test", &db_cfg).unwrap();
for i in 0u32..20 {
let (k, v) = kv(i, i);
db.put(None, &k, &v).unwrap();
}
let stats = env.get_stats().unwrap();
assert!(stats.log.n_sequential_writes > 0, "log writes must be counted");
assert!(stats.cache_size > 0, "cache_size must reflect configuration");
}