use noxu_db::{
DatabaseConfig, DatabaseEntry, EnvironmentConfig, Get, OperationStatus, Put,
};
use tempfile::TempDir;
const NUM_RECS: u32 = 50;
fn open_env_db(
dir: &TempDir,
name: &str,
dups: bool,
) -> (noxu_db::Environment, noxu_db::Database) {
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)
.with_transactional(true)
.with_sorted_duplicates(dups);
let db = env.open_database(None, name, &db_cfg).unwrap();
(env, db)
}
fn ikey(i: u32) -> DatabaseEntry {
DatabaseEntry::from_bytes(&i.to_be_bytes())
}
#[test]
fn database_put_existing_overwrite_round_trip() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_db(&dir, "put_existing", false);
let txn = env.begin_transaction(None).unwrap();
for i in (1..=NUM_RECS).rev() {
let k = ikey(i);
let d = ikey(i);
let s = db.put(Some(&txn), &k, &d).unwrap();
assert_eq!(s, OperationStatus::Success);
let mut out = DatabaseEntry::new();
let s = db.get(Some(&txn), &k, &mut out).unwrap();
assert_eq!(s, OperationStatus::Success);
assert_eq!(out.get_data().unwrap(), d.get_data().unwrap());
let s = db.put(Some(&txn), &k, &d).unwrap();
assert_eq!(s, OperationStatus::Success);
let mut c = db.open_cursor(Some(&txn), None).unwrap();
let mut sk = ikey(i);
let mut sd = ikey(i);
let s = c.get(&mut sk, &mut sd, Get::SearchBoth, None).unwrap();
assert_eq!(s, OperationStatus::Success);
assert_eq!(sd.get_data().unwrap(), d.get_data().unwrap());
}
txn.commit().unwrap();
}
#[test]
fn database_zero_length_data_round_trip_with_recovery() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_path_buf();
{
let (env, db) = open_env_db(&dir, "zero_len", false);
let txn = env.begin_transaction(None).unwrap();
for i in (1..=NUM_RECS).rev() {
let k = ikey(i);
let d = DatabaseEntry::from_bytes(&[]);
let s = db.put(Some(&txn), &k, &d).unwrap();
assert_eq!(s, OperationStatus::Success);
let mut out = DatabaseEntry::new();
let s = db.get(Some(&txn), &k, &mut out).unwrap();
assert_eq!(s, OperationStatus::Success);
assert!(out.get_data().is_some_and(|b| b.is_empty()));
}
txn.commit().unwrap();
drop(db);
drop(env);
}
let env_cfg = EnvironmentConfig::new(path)
.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).with_transactional(true);
let db = env.open_database(None, "zero_len", &db_cfg).unwrap();
let txn = env.begin_transaction(None).unwrap();
for i in (1..=NUM_RECS).rev() {
let k = ikey(i);
let mut out = DatabaseEntry::new();
let s = db.get(Some(&txn), &k, &mut out).unwrap();
assert_eq!(s, OperationStatus::Success);
assert!(
out.get_data().is_some_and(|b| b.is_empty()),
"zero-length data must survive recovery for key {i}"
);
}
txn.commit().unwrap();
drop(db);
drop(env);
}
#[test]
fn database_delete_non_dup() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_db(&dir, "del_nodup", false);
let txn = env.begin_transaction(None).unwrap();
for i in (1..=NUM_RECS).rev() {
db.put(Some(&txn), &ikey(i), &ikey(i)).unwrap();
}
for i in (1..=NUM_RECS).rev() {
let k = ikey(i);
let s = db.delete(Some(&txn), &k).unwrap();
assert_eq!(s, OperationStatus::Success, "first delete on key {i}");
let mut out = DatabaseEntry::new();
let s = db.get(Some(&txn), &k, &mut out).unwrap();
assert_eq!(s, OperationStatus::NotFound, "get after delete on key {i}");
let s = db.delete(Some(&txn), &k).unwrap();
assert_eq!(s, OperationStatus::NotFound, "second delete on key {i}");
}
txn.commit().unwrap();
}
#[test]
fn database_delete_with_dups_removes_all() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_db(&dir, "del_dup", true);
let txn = env.begin_transaction(None).unwrap();
const NUM_DUPS: u32 = 4;
for i in (1..=NUM_RECS).rev() {
db.put(Some(&txn), &ikey(i), &ikey(i)).unwrap();
for j in 0..NUM_DUPS {
db.put(Some(&txn), &ikey(i), &ikey(i + j)).unwrap();
}
}
txn.commit().unwrap();
let txn = env.begin_transaction(None).unwrap();
for i in (1..=NUM_RECS).rev() {
let s = db.delete(Some(&txn), &ikey(i)).unwrap();
assert_eq!(s, OperationStatus::Success);
let mut out = DatabaseEntry::new();
let s = db.get(Some(&txn), &ikey(i), &mut out).unwrap();
assert_eq!(s, OperationStatus::NotFound);
let s = db.delete(Some(&txn), &ikey(i)).unwrap();
assert_eq!(s, OperationStatus::NotFound);
}
txn.commit().unwrap();
}
#[test]
fn database_delete_abort_restores_record() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_db(&dir, "del_abort", false);
{
let t = env.begin_transaction(None).unwrap();
for i in (1..=NUM_RECS).rev() {
db.put(Some(&t), &ikey(i), &ikey(i)).unwrap();
}
t.commit().unwrap();
}
let delkey = NUM_RECS / 2;
let txn = env.begin_transaction(None).unwrap();
let s = db.delete(Some(&txn), &ikey(delkey)).unwrap();
assert_eq!(s, OperationStatus::Success);
txn.abort().unwrap();
let t2 = env.begin_transaction(None).unwrap();
let mut out = DatabaseEntry::new();
let s = db.get(Some(&t2), &ikey(delkey), &mut out).unwrap();
assert_eq!(
s,
OperationStatus::Success,
"record must reappear after delete is aborted"
);
assert_eq!(out.get_data().unwrap(), ikey(delkey).get_data().unwrap());
t2.commit().unwrap();
}
#[test]
fn database_put_duplicate_creates_distinct_dups() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_db(&dir, "put_dup", true);
let txn = env.begin_transaction(None).unwrap();
let mut expected_records = 0u64;
for i in (1..=NUM_RECS).rev() {
db.put(Some(&txn), &ikey(i), &ikey(i)).unwrap();
expected_records += 1;
db.put(Some(&txn), &ikey(i), &ikey(i * 2)).unwrap();
expected_records += 1;
}
txn.commit().unwrap();
assert_eq!(
db.count().unwrap(),
expected_records,
"count() must reflect total dups, not unique keys"
);
}
#[test]
fn database_put_no_dup_data_rejects_exact_pair() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_db(&dir, "put_no_dup_data", true);
let txn = env.begin_transaction(None).unwrap();
let mut c = db.open_cursor(Some(&txn), None).unwrap();
for i in (1..=NUM_RECS).rev() {
let k = ikey(i);
let d = ikey(i);
let s = c.put(&k, &d, Put::NoDupData).unwrap();
assert_eq!(
s,
OperationStatus::Success,
"first NoDupData on (k{i},d{i})"
);
let s = c.put(&k, &d, Put::NoDupData).unwrap();
assert_eq!(
s,
OperationStatus::KeyExists,
"duplicate NoDupData on (k{i},d{i})"
);
let d2 = ikey(i + 1);
let s = c.put(&k, &d2, Put::NoDupData).unwrap();
assert_eq!(
s,
OperationStatus::Success,
"different-data NoDupData on (k{i},d{}) ",
i + 1
);
}
drop(c);
txn.commit().unwrap();
}
#[test]
fn database_put_no_overwrite_no_dups() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_db(&dir, "no_overwrite_nodup", false);
let txn = env.begin_transaction(None).unwrap();
for i in (1..=NUM_RECS).rev() {
let k = ikey(i);
let d = ikey(i);
let s = db.put_no_overwrite(Some(&txn), &k, &d).unwrap();
assert_eq!(s, OperationStatus::Success, "first NoOverwrite on k{i}");
let s = db.put_no_overwrite(Some(&txn), &k, &d).unwrap();
assert_eq!(s, OperationStatus::KeyExists, "second NoOverwrite on k{i}");
}
txn.commit().unwrap();
}
#[test]
fn database_count_returns_record_count() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_db(&dir, "count", false);
let txn = env.begin_transaction(None).unwrap();
for i in (1..=NUM_RECS).rev() {
db.put(Some(&txn), &ikey(i), &ikey(i)).unwrap();
}
let c = db.count().unwrap();
assert_eq!(c, NUM_RECS as u64);
txn.commit().unwrap();
}
#[test]
fn database_config_snapshot_after_open() {
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 cfg_a =
DatabaseConfig::new().with_allow_create(true).with_transactional(true);
let db_a = env.open_database(None, "foo", &cfg_a).unwrap();
let stored = db_a.get_config().clone();
assert!(stored.allow_create);
assert!(!stored.sorted_duplicates);
assert!(stored.transactional);
let mut other = stored;
other.sorted_duplicates = true;
let _ = &other;
assert!(!db_a.get_config().sorted_duplicates);
db_a.close().unwrap();
drop(env);
}
#[test]
fn database_config_is_transactional() {
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 txn = env.begin_transaction(None).unwrap();
let cfg =
DatabaseConfig::new().with_allow_create(true).with_transactional(true);
let db = env.open_database(Some(&txn), "testDB2", &cfg).unwrap();
assert!(db.get_config().transactional);
db.put(
None,
&DatabaseEntry::from_bytes(&[0]),
&DatabaseEntry::from_bytes(&[0]),
)
.unwrap();
db.put(
Some(&txn),
&DatabaseEntry::from_bytes(&[1]),
&DatabaseEntry::from_bytes(&[1]),
)
.unwrap();
let mut out = DatabaseEntry::new();
let s =
db.get(Some(&txn), &DatabaseEntry::from_bytes(&[1]), &mut out).unwrap();
assert_eq!(s, OperationStatus::Success);
txn.commit().unwrap();
let mut out = DatabaseEntry::new();
let s = db.get(None, &DatabaseEntry::from_bytes(&[1]), &mut out).unwrap();
assert_eq!(s, OperationStatus::Success);
db.close().unwrap();
}
#[test]
fn database_config_open_read_only_rejects_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 cfg_rw =
DatabaseConfig::new().with_allow_create(true).with_transactional(true);
let db = env.open_database(None, "testDB2", &cfg_rw).unwrap();
db.put(
None,
&DatabaseEntry::from_bytes(&[0]),
&DatabaseEntry::from_bytes(&[0]),
)
.unwrap();
db.close().unwrap();
let cfg_ro =
DatabaseConfig::new().with_transactional(true).with_read_only(true);
let db_ro = env.open_database(None, "testDB2", &cfg_ro).unwrap();
assert!(db_ro.get_config().read_only);
assert!(db_ro.get_config().transactional);
let mut out = DatabaseEntry::new();
let s =
db_ro.get(None, &DatabaseEntry::from_bytes(&[0]), &mut out).unwrap();
assert_eq!(s, OperationStatus::Success);
assert_eq!(out.data(), &[0]);
let r = db_ro.put(
None,
&DatabaseEntry::from_bytes(&[1]),
&DatabaseEntry::from_bytes(&[1]),
);
assert!(r.is_err(), "put on read-only db must fail; got {:?}", r);
let mut c = db_ro.open_cursor(None, None).unwrap();
let mut k = DatabaseEntry::new();
let mut d = DatabaseEntry::new();
let s = c.get(&mut k, &mut d, Get::First, None).unwrap();
assert_eq!(s, OperationStatus::Success);
let r = c.delete();
assert!(r.is_err(), "cursor delete on read-only db must fail; got {:?}", r);
drop(c);
db_ro.close().unwrap();
}
#[test]
fn multi_env_open_close_test_multi_open_close() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_path_buf();
const N_RECORDS: u32 = 100;
const N_ITERS: u32 = 8;
const DATA_SIZE: usize = 1024;
{
let cfg = EnvironmentConfig::new(path.clone())
.with_allow_create(true)
.with_transactional(true);
let env = noxu_db::Environment::open(cfg).unwrap();
let db_cfg = DatabaseConfig::new()
.with_allow_create(true)
.with_transactional(true);
let db =
env.open_database(None, "MultiEnvOpenCloseTest", &db_cfg).unwrap();
let value = vec![0u8; DATA_SIZE];
let txn = env.begin_transaction(None).unwrap();
for i in 0..N_RECORDS {
let key = ikey(i);
let val = DatabaseEntry::from_bytes(&value);
db.put(Some(&txn), &key, &val).unwrap();
}
txn.commit().unwrap();
db.close().unwrap();
drop(env);
}
for _ in 0..N_ITERS {
let cfg = EnvironmentConfig::new(path.clone())
.with_transactional(true)
.with_allow_create(true);
let env = noxu_db::Environment::open(cfg).unwrap();
let db_cfg = DatabaseConfig::new()
.with_transactional(true)
.with_allow_create(true);
let db =
env.open_database(None, "MultiEnvOpenCloseTest", &db_cfg).unwrap();
for i in 0..N_RECORDS {
let mut out = DatabaseEntry::new();
let s = db.get(None, &ikey(i), &mut out).unwrap();
assert_eq!(
OperationStatus::Success,
s,
"k={i} should survive reopen"
);
}
db.close().unwrap();
drop(env);
}
}
#[test]
fn database_txn_cursor_on_non_txn_db_rejected() {
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, "non_txn_db", &db_cfg).unwrap();
let txn = env.begin_transaction(None).unwrap();
let result = db.open_cursor(Some(&txn), None);
assert!(
result.is_err(),
"opening a transactional cursor on a non-transactional database must fail"
);
txn.abort().unwrap();
}
#[test]
fn database_put_no_overwrite_in_dup_db_txn() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_db(&dir, "pno_dup_txn", true);
for i in (1..=10u32).rev() {
let txn = env.begin_transaction(None).unwrap();
let k = ikey(i);
let d = ikey(i);
assert_eq!(
db.put_no_overwrite(Some(&txn), &k, &d).unwrap(),
OperationStatus::Success
);
assert_eq!(
db.put_no_overwrite(Some(&txn), &k, &d).unwrap(),
OperationStatus::KeyExists
);
let d2 = ikey(i << 1);
assert_eq!(
db.put(Some(&txn), &k, &d2).unwrap(),
OperationStatus::Success
);
let d3 = ikey(i << 2);
assert_eq!(
db.put_no_overwrite(Some(&txn), &k, &d3).unwrap(),
OperationStatus::KeyExists,
"key already has dups; put_no_overwrite of same key must return KeyExists"
);
assert_eq!(
db.delete(Some(&txn), &k).unwrap(),
OperationStatus::Success
);
assert_eq!(
db.put_no_overwrite(Some(&txn), &k, &d3).unwrap(),
OperationStatus::Success
);
txn.commit().unwrap();
}
}
#[test]
fn database_put_no_overwrite_in_dup_db_no_txn() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_db(&dir, "pno_dup_no_txn", true);
for i in (1..=10u32).rev() {
let k = ikey(i);
let d = ikey(i);
assert_eq!(
db.put_no_overwrite(None, &k, &d).unwrap(),
OperationStatus::Success
);
assert_eq!(
db.put_no_overwrite(None, &k, &d).unwrap(),
OperationStatus::KeyExists
);
let d2 = ikey(i << 1);
assert_eq!(db.put(None, &k, &d2).unwrap(), OperationStatus::Success);
let d3 = ikey(i << 2);
assert_eq!(
db.put_no_overwrite(None, &k, &d3).unwrap(),
OperationStatus::KeyExists,
"key already has dups; put_no_overwrite must return KeyExists"
);
assert_eq!(db.delete(None, &k).unwrap(), OperationStatus::Success);
assert_eq!(
db.put_no_overwrite(None, &k, &d3).unwrap(),
OperationStatus::Success
);
}
}
#[test]
fn database_count_empty_returns_zero() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_db(&dir, "count_empty", false);
assert_eq!(db.count().unwrap(), 0);
}
#[test]
fn database_count_with_deleted_entries() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_db(&dir, "count_del", false);
let txn = env.begin_transaction(None).unwrap();
for i in 0..NUM_RECS {
db.put(Some(&txn), &ikey(i), &ikey(i)).unwrap();
}
txn.commit().unwrap();
assert_eq!(db.count().unwrap() as u32, NUM_RECS);
let txn = env.begin_transaction(None).unwrap();
for i in 0..(NUM_RECS / 2) {
db.delete(Some(&txn), &ikey(i)).unwrap();
}
txn.commit().unwrap();
assert_eq!(db.count().unwrap() as u32, NUM_RECS - NUM_RECS / 2);
}
#[test]
fn database_count_dups_counts_each_dup() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_db(&dir, "count_dups", true);
let txn = env.begin_transaction(None).unwrap();
let k = DatabaseEntry::from_bytes(b"k");
for i in 0u32..7 {
db.put(Some(&txn), &k, &DatabaseEntry::from_bytes(&i.to_be_bytes()))
.unwrap();
}
txn.commit().unwrap();
assert_eq!(db.count().unwrap(), 7);
}
#[test]
fn database_close_idempotent() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_db(&dir, "close_unop", false);
db.close().unwrap();
let _ = db.close();
let names = env.get_database_names().unwrap();
assert!(names.iter().any(|n| n == "close_unop"));
}
#[test]
fn environment_read_only_rejects_db_name_ops() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_path_buf();
{
let cfg = EnvironmentConfig::new(path.clone())
.with_allow_create(true)
.with_transactional(true);
let env = noxu_db::Environment::open(cfg).unwrap();
let dbcfg = DatabaseConfig::new()
.with_allow_create(true)
.with_transactional(true);
let db = env.open_database(None, "db1", &dbcfg).unwrap();
let txn = env.begin_transaction(None).unwrap();
db.put(
Some(&txn),
&DatabaseEntry::from_bytes(&[0u8; 10]),
&DatabaseEntry::from_bytes(&[0u8; 10]),
)
.unwrap();
txn.commit().unwrap();
assert_eq!(db.count().unwrap(), 1);
drop(db);
drop(env);
}
let cfg = EnvironmentConfig::new(path)
.with_read_only(true)
.with_transactional(true);
let env = noxu_db::Environment::open(cfg).unwrap();
assert!(env.truncate_database(None, "db1").is_err());
assert!(env.remove_database(None, "db1").is_err());
assert!(env.rename_database(None, "db1", "db2").is_err());
let dbcfg =
DatabaseConfig::new().with_read_only(true).with_transactional(true);
let db = env.open_database(None, "db1", &dbcfg).unwrap();
assert_eq!(db.count().unwrap(), 1);
}
#[test]
fn environment_checkpoint_forces_durability() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_path_buf();
{
let cfg = EnvironmentConfig::new(path.clone())
.with_allow_create(true)
.with_transactional(true);
let env = noxu_db::Environment::open(cfg).unwrap();
let dbcfg = DatabaseConfig::new()
.with_allow_create(true)
.with_transactional(true);
let db = env.open_database(None, "flush", &dbcfg).unwrap();
let txn = env.begin_transaction(None).unwrap();
for i in 0..NUM_RECS {
db.put(Some(&txn), &ikey(i), &ikey(i)).unwrap();
}
txn.commit().unwrap();
drop(db);
drop(env);
}
let cfg = EnvironmentConfig::new(path)
.with_allow_create(true)
.with_transactional(true);
let env = noxu_db::Environment::open(cfg).unwrap();
let dbcfg =
DatabaseConfig::new().with_allow_create(true).with_transactional(true);
let db = env.open_database(None, "flush", &dbcfg).unwrap();
let txn = env.begin_transaction(None).unwrap();
for i in 0..NUM_RECS {
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(Some(&txn), &ikey(i), &mut out).unwrap(),
OperationStatus::Success,
"key {i} must survive checkpoint + reopen"
);
assert_eq!(out.get_data().unwrap(), ikey(i).get_data().unwrap());
}
txn.commit().unwrap();
}
#[test]
fn environment_checkpoint_after_commit_loses_data() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_path_buf();
{
let cfg = EnvironmentConfig::new(path.clone())
.with_allow_create(true)
.with_transactional(true);
let env = noxu_db::Environment::open(cfg).unwrap();
let dbcfg = DatabaseConfig::new()
.with_allow_create(true)
.with_transactional(true);
let db = env.open_database(None, "ckp_loss", &dbcfg).unwrap();
let txn = env.begin_transaction(None).unwrap();
for i in 0..NUM_RECS {
db.put(Some(&txn), &ikey(i), &ikey(i)).unwrap();
}
txn.commit().unwrap();
env.checkpoint(None).unwrap();
drop(db);
drop(env);
}
let cfg = EnvironmentConfig::new(path)
.with_allow_create(true)
.with_transactional(true);
let env = noxu_db::Environment::open(cfg).unwrap();
let dbcfg =
DatabaseConfig::new().with_allow_create(true).with_transactional(true);
let db = env.open_database(None, "ckp_loss", &dbcfg).unwrap();
let txn = env.begin_transaction(None).unwrap();
for i in 0..NUM_RECS {
let mut out = DatabaseEntry::new();
assert_eq!(
db.get(Some(&txn), &ikey(i), &mut out).unwrap(),
OperationStatus::Success,
"key {i} must survive checkpoint + reopen"
);
}
txn.commit().unwrap();
}
#[test]
fn environment_open_reserved_name_db_rejected() {
let dir = TempDir::new().unwrap();
let cfg = EnvironmentConfig::new(dir.path().to_path_buf())
.with_allow_create(true)
.with_transactional(true);
let env = noxu_db::Environment::open(cfg).unwrap();
let dbcfg =
DatabaseConfig::new().with_allow_create(true).with_transactional(true);
let result = env.open_database(None, "", &dbcfg);
assert!(
result.is_err(),
"opening a database with an empty / reserved name must fail; got Ok"
);
}