use noxu_db::cursor::CursorState;
use noxu_db::{
DatabaseConfig, DatabaseEntry, EnvironmentConfig, Get, OperationStatus,
Put, TransactionConfig,
};
use tempfile::TempDir;
fn open_env_and_db(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, "cursor_test_db", &db_config).unwrap();
(env, db)
}
fn open_env_and_txn_db(
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).with_transactional(true);
let db = env.open_database(None, "cursor_txn_test_db", &db_config).unwrap();
(env, db)
}
fn kv(k: &[u8], v: &[u8]) -> (DatabaseEntry, DatabaseEntry) {
(DatabaseEntry::from_bytes(k), DatabaseEntry::from_bytes(v))
}
fn put_batch(db: &noxu_db::Database, pairs: &[(&[u8], &[u8])]) {
for (k, v) in pairs {
let (key, val) = kv(k, v);
db.put(None, &key, &val).unwrap();
}
}
#[test]
fn cursor_initial_state_not_initialized() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
let cursor = db.open_cursor(None, None).unwrap();
assert_eq!(cursor.get_state(), CursorState::NotInitialized);
}
#[test]
fn cursor_is_valid_before_positioning() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
let cursor = db.open_cursor(None, None).unwrap();
assert!(cursor.is_valid());
}
#[test]
fn cursor_is_read_write_by_default() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
let cursor = db.open_cursor(None, None).unwrap();
assert!(!cursor.is_read_only());
}
#[test]
fn cursor_state_initialized_after_first_get() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"k", b"v")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
cursor.get(&mut key, &mut data, Get::First, None).unwrap();
assert_eq!(cursor.get_state(), CursorState::Initialized);
}
#[test]
fn cursor_state_closed_after_close() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
let mut cursor = db.open_cursor(None, None).unwrap();
cursor.close().unwrap();
assert_eq!(cursor.get_state(), CursorState::Closed);
}
#[test]
fn cursor_first_on_empty_db_returns_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
let status = cursor.get(&mut key, &mut data, Get::First, None).unwrap();
assert_eq!(status, OperationStatus::NotFound);
}
#[test]
fn cursor_last_on_empty_db_returns_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
let status = cursor.get(&mut key, &mut data, Get::Last, None).unwrap();
assert_eq!(status, OperationStatus::NotFound);
}
#[test]
fn cursor_first_and_last_single_record() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"only", b"value")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::First, None).unwrap();
assert_eq!(s, OperationStatus::Success);
assert_eq!(key.data(), b"only");
let s = cursor.get(&mut key, &mut data, Get::Last, None).unwrap();
assert_eq!(s, OperationStatus::Success);
assert_eq!(key.data(), b"only");
}
#[test]
fn cursor_next_at_end_returns_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"a", b"1")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
cursor.get(&mut key, &mut data, Get::First, None).unwrap();
let s = cursor.get(&mut key, &mut data, Get::Next, None).unwrap();
assert_eq!(s, OperationStatus::NotFound);
}
#[test]
fn cursor_prev_at_beginning_returns_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"a", b"1")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
cursor.get(&mut key, &mut data, Get::First, None).unwrap();
let s = cursor.get(&mut key, &mut data, Get::Prev, None).unwrap();
assert_eq!(s, OperationStatus::NotFound);
}
#[test]
fn cursor_iterates_all_keys_forward() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"a", b"1"), (b"b", b"2"), (b"c", b"3")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
let mut collected = Vec::new();
let mut s = cursor.get(&mut key, &mut data, Get::First, None).unwrap();
while s == OperationStatus::Success {
collected.push(key.data().to_vec());
s = cursor.get(&mut key, &mut data, Get::Next, None).unwrap();
}
assert_eq!(collected, vec![b"a", b"b", b"c"]);
}
#[test]
fn cursor_iterates_all_keys_backward() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"a", b"1"), (b"b", b"2"), (b"c", b"3")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
let mut collected = Vec::new();
let mut s = cursor.get(&mut key, &mut data, Get::Last, None).unwrap();
while s == OperationStatus::Success {
collected.push(key.data().to_vec());
s = cursor.get(&mut key, &mut data, Get::Prev, None).unwrap();
}
assert_eq!(collected, vec![b"c", b"b", b"a"]);
}
#[test]
fn cursor_search_exact_key() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"aaa", b"v1"), (b"bbb", b"v2"), (b"ccc", b"v3")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"bbb");
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::Search, None).unwrap();
assert_eq!(s, OperationStatus::Success);
assert_eq!(data.data(), b"v2");
}
#[test]
fn cursor_search_missing_key_returns_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"a", b"1"), (b"c", b"3")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"b");
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::Search, None).unwrap();
assert_eq!(s, OperationStatus::NotFound);
}
#[test]
fn cursor_search_gte_positions_at_or_after() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"aaa", b"v1"), (b"ccc", b"v3"), (b"eee", b"v5")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"bbb");
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::SearchGte, None).unwrap();
assert_eq!(s, OperationStatus::Success);
assert_eq!(key.data(), b"ccc");
}
#[test]
fn cursor_search_gte_exact_key_matches() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"a", b"1"), (b"b", b"2")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"a");
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::SearchGte, None).unwrap();
assert_eq!(s, OperationStatus::Success);
assert_eq!(key.data(), b"a");
}
#[test]
fn cursor_current_returns_current_record() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"k1", b"v1"), (b"k2", b"v2")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"k1");
let mut data = DatabaseEntry::new();
cursor.get(&mut key, &mut data, Get::Search, None).unwrap();
let mut key2 = DatabaseEntry::new();
let mut data2 = DatabaseEntry::new();
let s = cursor.get(&mut key2, &mut data2, Get::Current, None).unwrap();
assert_eq!(s, OperationStatus::Success);
assert_eq!(key2.data(), b"k1");
assert_eq!(data2.data(), b"v1");
}
#[test]
fn cursor_put_overwrite_inserts_record() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
let mut cursor = db.open_cursor(None, None).unwrap();
let key = DatabaseEntry::from_bytes(b"new_key");
let val = DatabaseEntry::from_bytes(b"new_val");
let s = cursor.put(&key, &val, Put::Overwrite).unwrap();
assert_eq!(s, OperationStatus::Success);
cursor.close().unwrap();
let mut out = DatabaseEntry::new();
db.get(None, &key, &mut out).unwrap();
assert_eq!(out.data(), b"new_val");
}
#[test]
fn cursor_put_no_overwrite_returns_key_exists() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
let key = DatabaseEntry::from_bytes(b"k");
let v1 = DatabaseEntry::from_bytes(b"v1");
db.put(None, &key, &v1).unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let v2 = DatabaseEntry::from_bytes(b"v2");
let s = cursor.put(&key, &v2, Put::NoOverwrite).unwrap();
assert_eq!(s, OperationStatus::KeyExists);
}
#[test]
fn cursor_put_current_updates_current_record() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
let key = DatabaseEntry::from_bytes(b"k");
let v1 = DatabaseEntry::from_bytes(b"original");
db.put(None, &key, &v1).unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let mut search_key = DatabaseEntry::from_bytes(b"k");
let mut data = DatabaseEntry::new();
cursor.get(&mut search_key, &mut data, Get::Search, None).unwrap();
let new_val = DatabaseEntry::from_bytes(b"updated");
let s = cursor.put(&key, &new_val, Put::Current).unwrap();
assert_eq!(s, OperationStatus::Success);
cursor.close().unwrap();
let mut out = DatabaseEntry::new();
db.get(None, &key, &mut out).unwrap();
assert_eq!(out.data(), b"updated");
}
#[test]
fn cursor_delete_removes_current_record() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
let key = DatabaseEntry::from_bytes(b"to_delete");
let val = DatabaseEntry::from_bytes(b"v");
db.put(None, &key, &val).unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let mut search_key = DatabaseEntry::from_bytes(b"to_delete");
let mut data = DatabaseEntry::new();
cursor.get(&mut search_key, &mut data, Get::Search, None).unwrap();
cursor.delete().unwrap();
cursor.close().unwrap();
let mut out = DatabaseEntry::new();
let s = db.get(None, &key, &mut out).unwrap();
assert_eq!(s, OperationStatus::NotFound);
}
#[test]
fn cursor_delete_middle_record_leaves_others() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"a", b"1"), (b"b", b"2"), (b"c", b"3")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"b");
let mut data = DatabaseEntry::new();
cursor.get(&mut key, &mut data, Get::Search, None).unwrap();
cursor.delete().unwrap();
cursor.close().unwrap();
assert_eq!(db.count().unwrap(), 2);
}
#[test]
fn cursor_count_zero_before_positioning() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
let cursor = db.open_cursor(None, None).unwrap();
assert_eq!(cursor.count().unwrap(), 0);
}
#[test]
fn cursor_count_one_after_positioning() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(&db, &[(b"k", b"v")]);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
cursor.get(&mut key, &mut data, Get::First, None).unwrap();
assert_eq!(cursor.count().unwrap(), 1);
}
#[test]
fn cursor_iterates_100_records_in_order() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
let mut keys: Vec<u32> = (0..100).collect();
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
keys.sort_by_key(|k| {
let mut h = DefaultHasher::new();
k.hash(&mut h);
h.finish()
});
for i in &keys {
let key = format!("{:04}", i);
let val = format!("{}", i);
db.put(
None,
&DatabaseEntry::from_bytes(key.as_bytes()),
&DatabaseEntry::from_bytes(val.as_bytes()),
)
.unwrap();
}
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
let mut count = 0u32;
let mut prev_key: Option<Vec<u8>> = None;
let mut s = cursor.get(&mut key, &mut data, Get::First, None).unwrap();
while s == OperationStatus::Success {
if let Some(ref pk) = prev_key {
assert!(
key.data() > pk.as_slice(),
"keys must be strictly increasing"
);
}
prev_key = Some(key.data().to_vec());
count += 1;
s = cursor.get(&mut key, &mut data, Get::Next, None).unwrap();
}
assert_eq!(count, 100);
}
#[test]
fn cursor_keys_returned_in_lexicographic_order() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db(&dir);
put_batch(
&db,
&[
(b"zz", b"z"),
(b"aa", b"a"),
(b"mm", b"m"),
(b"bb", b"b"),
(b"yy", b"y"),
],
);
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
let mut keys: Vec<Vec<u8>> = Vec::new();
let mut s = cursor.get(&mut key, &mut data, Get::First, None).unwrap();
while s == OperationStatus::Success {
keys.push(key.data().to_vec());
s = cursor.get(&mut key, &mut data, Get::Next, None).unwrap();
}
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(
keys, sorted,
"iteration order must be lexicographically sorted"
);
}
fn open_env_and_db_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)
}
#[test]
fn cursor_search_gte_short_seed_under_long_prefix_does_not_panic() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "search_gte_short_seed");
for i in 0..1000u32 {
let mut key = Vec::new();
key.extend_from_slice(b"K\0");
key.extend_from_slice(b"the-bucket\0");
key.extend_from_slice(format!("object-{i:08}").as_bytes());
let value = format!("payload-{i}");
db.put(
None,
&DatabaseEntry::from_bytes(&key),
&DatabaseEntry::from_bytes(value.as_bytes()),
)
.unwrap();
}
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"K\0");
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::SearchGte, None).unwrap();
assert_eq!(
s,
OperationStatus::Success,
"SearchGte with a short seed under a long-prefix BIN must succeed"
);
let want = {
let mut k = Vec::new();
k.extend_from_slice(b"K\0");
k.extend_from_slice(b"the-bucket\0");
k.extend_from_slice(b"object-00000000");
k
};
assert_eq!(key.data(), want.as_slice());
}
#[test]
fn cursor_search_gte_seed_above_all_keys_returns_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "search_gte_above_all");
for i in 0..200u32 {
let mut key = Vec::new();
key.extend_from_slice(b"K\0");
key.extend_from_slice(b"the-bucket\0");
key.extend_from_slice(format!("object-{i:08}").as_bytes());
db.put(
None,
&DatabaseEntry::from_bytes(&key),
&DatabaseEntry::from_bytes(format!("v{i}").as_bytes()),
)
.unwrap();
}
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"L\0");
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::SearchGte, None).unwrap();
assert_eq!(s, OperationStatus::NotFound);
}
#[test]
fn cursor_search_gte_seed_below_all_keys_returns_first() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "search_gte_below_all");
for i in 0..500u32 {
let mut key = Vec::new();
key.extend_from_slice(b"M\0");
key.extend_from_slice(b"bucket\0");
key.extend_from_slice(format!("k-{i:06}").as_bytes());
db.put(
None,
&DatabaseEntry::from_bytes(&key),
&DatabaseEntry::from_bytes(format!("v{i}").as_bytes()),
)
.unwrap();
}
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"A");
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::SearchGte, None).unwrap();
assert_eq!(s, OperationStatus::Success);
let want = {
let mut k = Vec::new();
k.extend_from_slice(b"M\0");
k.extend_from_slice(b"bucket\0");
k.extend_from_slice(b"k-000000");
k
};
assert_eq!(key.data(), want.as_slice());
}
const FANOUT_DOUBLED: u32 = 256;
#[test]
fn cursor_search_gte_walks_to_next_bin_when_chosen_bin_is_below_seed() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "search_gte_cross_bin");
for i in 0..FANOUT_DOUBLED {
let k = format!("k-{i:08}");
db.put(
None,
&DatabaseEntry::from_bytes(k.as_bytes()),
&DatabaseEntry::from_bytes(format!("v{i}").as_bytes()),
)
.unwrap();
}
let mut cursor = db.open_cursor(None, None).unwrap();
let seed = b"k-00000099a";
let mut key = DatabaseEntry::from_bytes(seed);
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::SearchGte, None).unwrap();
assert_eq!(s, OperationStatus::Success);
assert_eq!(
key.data(),
b"k-00000100",
"SearchGte must return the smallest key strictly greater than \
the seed, even if that key lives in the next BIN"
);
}
#[test]
fn cursor_search_gte_past_last_key_returns_not_found_with_many_bins() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "search_gte_past_last");
for i in 0..FANOUT_DOUBLED {
let k = format!("a-{i:08}");
db.put(
None,
&DatabaseEntry::from_bytes(k.as_bytes()),
&DatabaseEntry::from_bytes(format!("v{i}").as_bytes()),
)
.unwrap();
}
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"z-this-is-after-everything");
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::SearchGte, None).unwrap();
assert_eq!(s, OperationStatus::NotFound);
}
#[test]
fn cursor_search_gte_long_prefix_seed_above_walks_to_next_bin() {
let dir = TempDir::new().unwrap();
let (_env, db) =
open_env_and_db_named(&dir, "search_gte_long_prefix_above");
for i in 0..FANOUT_DOUBLED {
let mut k = Vec::new();
if i < FANOUT_DOUBLED / 2 {
k.extend_from_slice(b"K\0");
k.extend_from_slice(b"alpha\0");
} else {
k.extend_from_slice(b"K\0");
k.extend_from_slice(b"omega\0");
}
k.extend_from_slice(format!("{i:08}").as_bytes());
db.put(
None,
&DatabaseEntry::from_bytes(&k),
&DatabaseEntry::from_bytes(format!("v{i}").as_bytes()),
)
.unwrap();
}
let mut cursor = db.open_cursor(None, None).unwrap();
let seed = b"K\0gamma\0";
let mut key = DatabaseEntry::from_bytes(seed);
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::SearchGte, None).unwrap();
assert_eq!(s, OperationStatus::Success);
let want = {
let mut k = Vec::new();
k.extend_from_slice(b"K\0");
k.extend_from_slice(b"omega\0");
k.extend_from_slice(format!("{:08}", FANOUT_DOUBLED / 2).as_bytes());
k
};
assert_eq!(
key.data(),
want.as_slice(),
"SearchGte across BIN boundary with case-3 prefix must land on \
the first omega-prefix key"
);
}
#[test]
fn cursor_search_gte_in_every_inter_key_gap_agrees_with_get_next() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "search_gte_inter_key_gaps");
const N: u32 = 1024; for i in 0..N {
let k = format!("k-{i:08}");
db.put(
None,
&DatabaseEntry::from_bytes(k.as_bytes()),
&DatabaseEntry::from_bytes(format!("v{i}").as_bytes()),
)
.unwrap();
}
let mut keys_in_order: Vec<Vec<u8>> = Vec::with_capacity(N as usize);
{
let mut cursor = db.open_cursor(None, None).unwrap();
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 {
keys_in_order.push(k.data().to_vec());
s = cursor.get(&mut k, &mut v, Get::Next, None).unwrap();
}
}
assert_eq!(keys_in_order.len(), N as usize);
for pair in keys_in_order.windows(2) {
let (k_a, k_b) = (&pair[0], &pair[1]);
let mut probe = k_a.clone();
probe.push(0); let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(&probe);
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::SearchGte, None).unwrap();
assert_eq!(
s,
OperationStatus::Success,
"SearchGte({:?}) returned NotFound; expected next key {:?}",
probe,
k_b
);
assert_eq!(
key.data(),
k_b.as_slice(),
"SearchGte({:?}) returned wrong next key",
probe
);
}
}
#[test]
fn cursor_search_gte_oracle_brute_force_small_random() {
use std::collections::BTreeSet;
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "search_gte_oracle");
let mut keys: BTreeSet<Vec<u8>> = BTreeSet::new();
let mut state: u64 = 0xC0FFEE_DEADBEEF_u64;
for _ in 0..FANOUT_DOUBLED + 50 {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let len = 4 + (state as usize % 13);
let bytes: Vec<u8> =
(0..len).map(|i| ((state >> (i * 4)) & 0xFF) as u8).collect();
keys.insert(bytes);
}
for k in &keys {
db.put(
None,
&DatabaseEntry::from_bytes(k),
&DatabaseEntry::from_bytes(b"v"),
)
.unwrap();
}
let mut cursor = db.open_cursor(None, None).unwrap();
let probes: Vec<Vec<u8>> = {
let mut p: Vec<Vec<u8>> = Vec::new();
for k in &keys {
p.push(k.clone());
let mut up = k.clone();
up.push(0);
p.push(up);
if k.len() > 1 {
p.push(k[..k.len() - 1].to_vec());
}
}
p.push(b"\xff".to_vec());
p
};
for probe in probes {
let want = keys.iter().find(|k| k.as_slice() >= probe.as_slice());
let mut key = DatabaseEntry::from_bytes(&probe);
let mut data = DatabaseEntry::new();
let status =
cursor.get(&mut key, &mut data, Get::SearchGte, None).unwrap();
match (want, status) {
(Some(w), OperationStatus::Success) => {
assert_eq!(
key.data(),
w.as_slice(),
"SearchGte({:02x?}) returned wrong key; oracle expects {:02x?}",
probe,
w
);
}
(None, OperationStatus::NotFound) => { }
(oracle, got) => panic!(
"SearchGte({:02x?}) disagreement: oracle={:?} got={:?}",
probe, oracle, got
),
}
}
}
mod prop_full_scan_order {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
cases: 64,
.. ProptestConfig::default()
})]
#[test]
fn cursor_full_scan_order_oracle_brute_force_small_random(
keys in prop::collection::btree_set(
prop::collection::vec(any::<u8>(), 1..=16),
1..=256,
),
) {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "prop_full_scan");
for k in &keys {
db.put(
None,
&DatabaseEntry::from_bytes(k),
&DatabaseEntry::from_bytes(b"v"),
).unwrap();
}
let mut cursor = db.open_cursor(None, None).unwrap();
let mut got_fwd: Vec<Vec<u8>> = Vec::new();
let mut k = DatabaseEntry::new();
let mut d = DatabaseEntry::new();
let mut s = cursor.get(&mut k, &mut d, Get::First, None).unwrap();
while s == OperationStatus::Success {
got_fwd.push(k.data().to_vec());
s = cursor.get(&mut k, &mut d, Get::Next, None).unwrap();
if got_fwd.len() > keys.len() + 1 {
prop_assert!(
false,
"forward scan returned more entries ({}) than were inserted ({})",
got_fwd.len(), keys.len(),
);
}
}
prop_assert_eq!(s, OperationStatus::NotFound);
let expected_fwd: Vec<Vec<u8>> = keys.iter().cloned().collect();
prop_assert_eq!(
&got_fwd, &expected_fwd,
"Get::First+Next did not return keys in lex-sorted order",
);
let mut got_rev: Vec<Vec<u8>> = Vec::new();
let mut s = cursor.get(&mut k, &mut d, Get::Last, None).unwrap();
while s == OperationStatus::Success {
got_rev.push(k.data().to_vec());
s = cursor.get(&mut k, &mut d, Get::Prev, None).unwrap();
if got_rev.len() > keys.len() + 1 {
prop_assert!(
false,
"reverse scan returned more entries ({}) than were inserted ({})",
got_rev.len(), keys.len(),
);
}
}
prop_assert_eq!(s, OperationStatus::NotFound);
let mut expected_rev = expected_fwd;
expected_rev.reverse();
prop_assert_eq!(
&got_rev, &expected_rev,
"Get::Last+Prev did not return keys in reverse-lex order",
);
}
}
}
#[test]
fn cursor_next_dup_on_non_dup_db_returns_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "next_dup_non_dup");
db.put(
None,
&DatabaseEntry::from_bytes(b"a"),
&DatabaseEntry::from_bytes(b"1"),
)
.unwrap();
db.put(
None,
&DatabaseEntry::from_bytes(b"b"),
&DatabaseEntry::from_bytes(b"2"),
)
.unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"a");
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::Search, None).unwrap();
assert_eq!(s, OperationStatus::Success);
let s = cursor.get(&mut key, &mut data, Get::NextDup, None).unwrap();
assert_eq!(
s,
OperationStatus::NotFound,
"Get::NextDup on non-dup DB must return NotFound; got key={:?}",
key.get_data()
);
}
#[test]
fn cursor_prev_dup_on_non_dup_db_returns_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "prev_dup_non_dup");
db.put(
None,
&DatabaseEntry::from_bytes(b"a"),
&DatabaseEntry::from_bytes(b"1"),
)
.unwrap();
db.put(
None,
&DatabaseEntry::from_bytes(b"b"),
&DatabaseEntry::from_bytes(b"2"),
)
.unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
let s = cursor.get(&mut key, &mut data, Get::Last, None).unwrap();
assert_eq!(s, OperationStatus::Success);
assert_eq!(key.data(), b"b");
let s = cursor.get(&mut key, &mut data, Get::PrevDup, None).unwrap();
assert_eq!(
s,
OperationStatus::NotFound,
"Get::PrevDup on non-dup DB must return NotFound; got key={:?}",
key.get_data()
);
}
#[test]
fn cursor_search_both_on_non_dup_db_validates_data() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "search_both_non_dup");
db.put(
None,
&DatabaseEntry::from_bytes(b"k"),
&DatabaseEntry::from_bytes(b"stored"),
)
.unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let mut k = DatabaseEntry::from_bytes(b"k");
let mut d = DatabaseEntry::from_bytes(b"different");
let s = cursor.get(&mut k, &mut d, Get::SearchBoth, None).unwrap();
assert_eq!(
s,
OperationStatus::NotFound,
"SearchBoth on non-dup DB must return NotFound when data mismatches"
);
let mut k = DatabaseEntry::from_bytes(b"k");
let mut d = DatabaseEntry::from_bytes(b"stored");
let s = cursor.get(&mut k, &mut d, Get::SearchBoth, None).unwrap();
assert_eq!(
s,
OperationStatus::Success,
"SearchBoth on non-dup DB must succeed when data matches"
);
assert_eq!(k.data(), b"k");
assert_eq!(d.data(), b"stored");
}
#[test]
fn cursor_search_both_on_non_dup_db_missing_key_still_not_found() {
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "search_both_missing_key");
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"missing");
let mut d = DatabaseEntry::from_bytes(b"anything");
let s = cursor.get(&mut k, &mut d, Get::SearchBoth, None).unwrap();
assert_eq!(s, OperationStatus::NotFound);
}
#[test]
fn cursor_search_lte_returns_unsupported_error() {
use noxu_db::NoxuError;
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "search_lte_unsupported");
db.put(
None,
&DatabaseEntry::from_bytes(b"a"),
&DatabaseEntry::from_bytes(b"1"),
)
.unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"a");
let mut data = DatabaseEntry::new();
let err = cursor
.get(&mut key, &mut data, Get::SearchLte, None)
.expect_err("Get::SearchLte must return an Unsupported error");
match err {
NoxuError::Unsupported(op) => {
assert!(
op.contains("SearchLte"),
"Unsupported message must name the operation; got {op:?}"
);
}
other => panic!("expected NoxuError::Unsupported, got {other:?}"),
}
}
#[test]
fn cursor_first_dup_returns_unsupported_error() {
use noxu_db::NoxuError;
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "first_dup_unsupported");
db.put(
None,
&DatabaseEntry::from_bytes(b"a"),
&DatabaseEntry::from_bytes(b"1"),
)
.unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"a");
let mut data = DatabaseEntry::new();
cursor.get(&mut key, &mut data, Get::Search, None).unwrap();
let err = cursor
.get(&mut key, &mut data, Get::FirstDup, None)
.expect_err("Get::FirstDup must return an Unsupported error");
assert!(
matches!(err, NoxuError::Unsupported(ref op) if op.contains("FirstDup"))
);
}
#[test]
fn cursor_last_dup_returns_unsupported_error() {
use noxu_db::NoxuError;
let dir = TempDir::new().unwrap();
let (_env, db) = open_env_and_db_named(&dir, "last_dup_unsupported");
db.put(
None,
&DatabaseEntry::from_bytes(b"a"),
&DatabaseEntry::from_bytes(b"1"),
)
.unwrap();
let mut cursor = db.open_cursor(None, None).unwrap();
let mut key = DatabaseEntry::from_bytes(b"a");
let mut data = DatabaseEntry::new();
cursor.get(&mut key, &mut data, Get::Search, None).unwrap();
let err = cursor
.get(&mut key, &mut data, Get::LastDup, None)
.expect_err("Get::LastDup must return an Unsupported error");
assert!(
matches!(err, NoxuError::Unsupported(ref op) if op.contains("LastDup"))
);
}
#[test]
fn cursor_with_txn_put_is_rolled_back_on_abort() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_and_txn_db(&dir);
let txn = env.begin_transaction(None).unwrap();
let mut cursor = db.open_cursor(Some(&txn), None).unwrap();
let (k, v) = kv(b"rolled-back-key", b"rolled-back-val");
let s = cursor.put(&k, &v, Put::Overwrite).unwrap();
assert_eq!(s, OperationStatus::Success);
cursor.close().unwrap();
txn.abort().unwrap();
let mut probe = db.open_cursor(None, None).unwrap();
let mut out_k = DatabaseEntry::new();
let mut out_v = DatabaseEntry::new();
let status = probe.get(&mut out_k, &mut out_v, Get::First, None).unwrap();
assert_eq!(
status,
OperationStatus::NotFound,
"cursor.put under an aborted txn must NOT persist; the txn argument \
to Database::open_cursor was being dropped (audit C1)"
);
}
#[test]
fn cursor_with_txn_put_invisible_via_get_after_abort() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_and_txn_db(&dir);
let txn = env.begin_transaction(None).unwrap();
let mut cursor = db.open_cursor(Some(&txn), None).unwrap();
let (k, v) = kv(b"k", b"v");
cursor.put(&k, &v, Put::Overwrite).unwrap();
cursor.close().unwrap();
txn.abort().unwrap();
let mut out = DatabaseEntry::new();
let status = db.get(None, &k, &mut out).unwrap();
assert_eq!(
status,
OperationStatus::NotFound,
"value written through a cursor opened with Some(&txn) must vanish \
on txn.abort() (audit C1)"
);
}
#[test]
fn cursor_with_txn_get_takes_read_lock_via_locker() {
let dir = TempDir::new().unwrap();
let (env, db) = open_env_and_txn_db(&dir);
let seed = env.begin_transaction(None).unwrap();
let (k, v0) = kv(b"locked", b"v0");
db.put(Some(&seed), &k, &v0).unwrap();
seed.commit().unwrap();
let txn_a = env.begin_transaction(None).unwrap();
let v1 = DatabaseEntry::from_bytes(b"v1");
db.put(Some(&txn_a), &k, &v1).unwrap();
let no_wait = TransactionConfig::new().with_no_wait(true);
let txn_b = env.begin_transaction(Some(&no_wait)).unwrap();
let mut cursor = db.open_cursor(Some(&txn_b), None).unwrap();
let mut search_k = DatabaseEntry::from_bytes(b"locked");
let mut out_v = DatabaseEntry::new();
let read_result = cursor.get(&mut search_k, &mut out_v, Get::Search, None);
assert!(
read_result.is_err(),
"cursor read under txn B with no-wait must conflict with txn A's \
write lock; pre-fix the txn argument was dropped and the cursor \
did not consult the lock manager (audit C1). got Ok({:?})",
read_result.ok()
);
cursor.close().unwrap();
let _ = txn_b.abort();
txn_a.abort().unwrap();
}
mod secondary_cursor_txn {
use noxu_db::{
CursorConfig, Database, DatabaseConfig, DatabaseEntry, Environment,
EnvironmentConfig, OperationStatus, SecondaryConfig, SecondaryDatabase,
SecondaryKeyCreator,
};
use noxu_sync::Mutex;
use std::sync::Arc;
use tempfile::TempDir;
struct FirstByte;
impl SecondaryKeyCreator for FirstByte {
fn create_secondary_key(
&self,
_db: &Database,
_key: &DatabaseEntry,
data: &DatabaseEntry,
result: &mut DatabaseEntry,
) -> bool {
if let Some(d) = data.get_data()
&& !d.is_empty()
{
result.set_data(&d[..1]);
return true;
}
false
}
}
fn open_pri_sec(
dir: &TempDir,
) -> (Environment, Arc<Mutex<Database>>, SecondaryDatabase) {
let env = Environment::open(
EnvironmentConfig::new(dir.path().to_path_buf())
.with_allow_create(true)
.with_transactional(true),
)
.unwrap();
let primary_db = env
.open_database(
None,
"pri",
&DatabaseConfig::new()
.with_allow_create(true)
.with_transactional(true),
)
.unwrap();
let primary = Arc::new(Mutex::new(primary_db));
let sec_db = env
.open_database(
None,
"sec",
&DatabaseConfig::new()
.with_allow_create(true)
.with_transactional(true)
.with_sorted_duplicates(true),
)
.unwrap();
let sec_config = SecondaryConfig::new()
.with_allow_create(true)
.with_key_creator(Box::new(FirstByte));
let secondary =
SecondaryDatabase::open(Arc::clone(&primary), sec_db, sec_config)
.unwrap();
(env, primary, secondary)
}
#[test]
fn sec_open_cursor_threads_txn_and_config() {
let dir = TempDir::new().unwrap();
let (env, primary, secondary) = open_pri_sec(&dir);
let pk = DatabaseEntry::from_bytes(b"pk1");
let pv = DatabaseEntry::from_bytes(b"Apple");
primary.lock().put(None, &pk, &pv).unwrap();
secondary.update_secondary(None, &pk, None, Some(&pv)).unwrap();
let txn = env.begin_transaction(None).unwrap();
let cfg = CursorConfig::new();
let mut cursor = secondary.open_cursor(Some(&txn), Some(&cfg)).unwrap();
let mut sec_key = DatabaseEntry::from_bytes(b"A");
let mut p_key = DatabaseEntry::new();
let mut data = DatabaseEntry::new();
let s = cursor.get_search_key(&sec_key, &mut p_key, &mut data).unwrap();
assert_eq!(s, OperationStatus::Success);
assert_eq!(p_key.data(), b"pk1");
assert_eq!(data.data(), b"Apple");
cursor.close().unwrap();
txn.commit().unwrap();
let txn2 = env.begin_transaction(None).unwrap();
let mut cursor2 = secondary.open_cursor(Some(&txn2), None).unwrap();
sec_key = DatabaseEntry::from_bytes(b"A");
let s =
cursor2.get_search_key(&sec_key, &mut p_key, &mut data).unwrap();
assert_eq!(s, OperationStatus::Success);
cursor2.close().unwrap();
txn2.abort().unwrap();
let mut probe = secondary.open_cursor(None, None).unwrap();
let s = probe
.get_search_key(
&DatabaseEntry::from_bytes(b"A"),
&mut p_key,
&mut data,
)
.unwrap();
assert_eq!(s, OperationStatus::Success);
probe.close().unwrap();
}
}