use aok::Void;
use tempfile::tempdir;
use wedb_embed::WeDb;
use wedb_embed::hash::{
HExpire, HGetEx, HSetEx, HashFetchType, HashFieldSetCondition, HashGetExOptions,
HashLengthMode, HashSetExOptions, RangeLexSpec, TTLAction,
};
#[ctor::ctor(unsafe)]
fn _log_init() {
log_init::init();
}
#[test]
fn test_hash_basic_ops() -> Void {
let dir = tempdir()?;
let db = WeDb::open(dir.path())?;
let fields = ["test-hash-key-1", "test-hash-key-2", "test-hash-key-3"];
let values = [
"hash-test-value-1",
"hash-test-value-2",
"hash-test-value-3",
];
let fvs: Vec<(&str, &str)> = fields.iter().copied().zip(values.iter().copied()).collect();
assert_eq!(db.hset("test_hash->key", &fvs)?, 3);
assert_eq!(db.hlen("test_hash->key")?, 3);
assert_eq!(
db.hlen_with_mode("test_hash->key", HashLengthMode::Approximate)?,
3
);
for (f, v) in &fvs {
assert_eq!(db.hget("test_hash->key", f)?, Some(v.as_bytes().to_vec()));
assert!(db.hexists("test_hash->key", f)?);
}
assert!(!db.hexists("test_hash->key", "nonexistent_field")?);
assert_eq!(
db.hset("test_hash->key", &[("dup", "v1"), ("dup", "v2")])?,
1
);
assert_eq!(db.hget("test_hash->key", "dup")?, Some(b"v2".to_vec()));
assert!(!db.hsetnx("test_hash->key", "dup", "v_new")?);
assert_eq!(db.hget("test_hash->key", "dup")?, Some(b"v2".to_vec()));
assert!(db.hsetnx("test_hash->key", "new_nx", "v_nx")?);
assert_eq!(db.hget("test_hash->key", "new_nx")?, Some(b"v_nx".to_vec()));
assert_eq!(
db.hdel("test_hash->key", &["test-hash-key-1", "dup", "new_nx"])?,
3
);
assert_eq!(db.hlen("test_hash->key")?, 2);
assert_eq!(db.hget("test_hash->key", "test-hash-key-1")?, None);
Ok(())
}
#[test]
fn test_hash_incrby_and_incrbyfloat() -> Void {
let dir = tempdir()?;
let db = WeDb::open(dir.path())?;
assert_eq!(db.hincrby("h_counter", "f1", 10)?, 10);
assert_eq!(db.hincrby("h_counter", "f1", 5)?, 15);
assert_eq!(db.hincrby("h_counter", "f1", -20)?, -5);
let f1 = db.hincrbyfloat("h_float", "f2", 1.5)?;
assert!((f1 - 1.5).abs() < 1e-6);
let f2 = db.hincrbyfloat("h_float", "f2", 3.25)?;
assert!((f2 - 4.75).abs() < 1e-6);
db.hset("h_err", &[("not_int", "abc"), ("with_space", " 123")])?;
assert!(db.hincrby("h_err", "not_int", 1).is_err());
assert!(db.hincrby("h_err", "with_space", 1).is_err());
assert!(db.hincrbyfloat("h_err", "not_int", 1.0).is_err());
db.hset("h_ovf", &[("max_int", &i64::MAX.to_string())])?;
assert!(db.hincrby("h_ovf", "max_int", 1).is_err());
Ok(())
}
#[test]
fn test_hash_mget_and_keys_vals_all() -> Void {
let dir = tempdir()?;
let db = WeDb::open(dir.path())?;
db.hset("h_batch", &[("k1", "v1"), ("k2", "v2"), ("k3", "v3")])?;
let mget = db.hmget("h_batch", &["k1", "k_missing", "k3"])?;
assert_eq!(
mget,
vec![Some(b"v1".to_vec()), None, Some(b"v3".to_vec()),]
);
let all = db.hgetall("h_batch")?;
assert_eq!(all.len(), 3);
let keys = db.hkeys("h_batch")?;
assert_eq!(keys.len(), 3);
let vals = db.hvals("h_batch")?;
assert_eq!(vals.len(), 3);
let only_keys = db.hget_all_with_type("h_batch", HashFetchType::OnlyKey)?;
assert_eq!(only_keys.len(), 3);
assert_eq!(only_keys[0].0, b"k1");
assert!(only_keys[0].1.is_empty());
let only_vals = db.hget_all_with_type("h_batch", HashFetchType::OnlyValue)?;
assert_eq!(only_vals.len(), 3);
assert!(only_vals[0].0.is_empty());
assert_eq!(only_vals[0].1, b"v1");
assert_eq!(db.hstrlen("h_batch", "k1")?, 2);
assert_eq!(db.hstrlen("h_batch", "nonexistent")?, 0);
Ok(())
}
#[test]
fn test_hash_field_expiration_complete() -> Void {
let dir = tempdir()?;
let db = WeDb::open(dir.path())?;
db.hset("hexp", &[("f1", "v1"), ("f2", "v2"), ("f3", "v3")])?;
let res = db.hexpire("hexp", &["f1", "f2"], 100, HExpire::None)?;
assert_eq!(res, vec![1, 1]);
let ttls = db.httl("hexp", &["f1", "f2", "f3", "f_none"])?;
assert!(ttls[0] > 0 && ttls[0] <= 100);
assert!(ttls[1] > 0 && ttls[1] <= 100);
assert_eq!(ttls[2], -1);
assert_eq!(ttls[3], -2);
let pttls = db.hpttl("hexp", &["f1", "f3"])?;
assert!(pttls[0] > 0);
assert_eq!(pttls[1], -1);
let extimes = db.hexpiretime("hexp", &["f1", "f3"])?;
assert!(extimes[0] > 0);
assert_eq!(extimes[1], -1);
let pextimes = db.hpexpiretime("hexp", &["f1", "f3"])?;
assert!(pextimes[0] > 0);
assert_eq!(pextimes[1], -1);
let nx_res = db.hexpire("hexp", &["f1", "f3"], 200, HExpire::Nx)?;
assert_eq!(nx_res, vec![0, 1]);
let xx_res = db.hexpire("hexp", &["f1", "f2"], 300, HExpire::Xx)?;
assert_eq!(xx_res, vec![1, 1]);
let gt_fail = db.hexpire("hexp", &["f1"], 50, HExpire::Gt)?;
assert_eq!(gt_fail, vec![0]); let gt_succ = db.hexpire("hexp", &["f1"], 500, HExpire::Gt)?;
assert_eq!(gt_succ, vec![1]);
let lt_fail = db.hexpire("hexp", &["f1"], 600, HExpire::Lt)?;
assert_eq!(lt_fail, vec![0]); let lt_succ = db.hexpire("hexp", &["f1"], 400, HExpire::Lt)?;
assert_eq!(lt_succ, vec![1]);
let persist_res = db.hpersist("hexp", &["f1", "f2", "f3", "f_none"])?;
assert_eq!(persist_res, vec![1, 1, 1, -2]);
assert_eq!(db.httl("hexp", &["f1", "f2", "f3"])?, vec![-1, -1, -1]);
let persist_again = db.hpersist("hexp", &["f1", "f2", "f3"])?;
assert_eq!(persist_again, vec![-1, -1, -1]);
let imm_res = db.hexpire("hexp", &["f1"], 0, HExpire::None)?;
assert_eq!(imm_res, vec![2]);
assert_eq!(db.hget("hexp", "f1")?, None);
assert_eq!(db.hlen("hexp")?, 2);
Ok(())
}
#[test]
fn test_hash_hgetdel_and_hsetex_hgetex() -> Void {
let dir = tempdir()?;
let db = WeDb::open(dir.path())?;
let now_ms = coarsetime::Clock::now_since_epoch().as_millis();
let setex_opts = HashSetExOptions {
condition: HashFieldSetCondition::Fnx,
ttl_action: TTLAction::Set,
expire_at_ms: now_ms + 10_000,
};
assert!(db.set_fields_with_expire("h_ex_key", &[("a", "1"), ("b", "2")], setex_opts)?);
assert_eq!(db.hlen("h_ex_key")?, 2);
assert!(!db.set_fields_with_expire("h_ex_key", &[("a", "1_new")], setex_opts)?);
assert!(db.hsetex("h_ex_key2", &[("c", "100")], &[HSetEx::Ex(30), HSetEx::Fnx])?);
assert_eq!(db.hlen("h_ex_key2")?, 1);
let getex_opts = HashGetExOptions {
ttl_action: TTLAction::Persist,
expire_at_ms: 0,
};
let gvals = db.get_fields_with_expire("h_ex_key", &["a", "nonexistent"], getex_opts)?;
assert_eq!(gvals, vec![Some(b"1".to_vec()), None]);
assert_eq!(db.httl("h_ex_key", &["a"])?, vec![-1]);
let g_val = db.hgetex("h_ex_key2", "c", Some(HGetEx::Persist))?;
assert_eq!(g_val, Some(b"100".to_vec()));
assert_eq!(db.httl("h_ex_key2", &["c"])?, vec![-1]);
let del_res = db.hgetdel("h_ex_key", &["a", "b", "c"])?;
assert_eq!(
del_res,
vec![Some(b"1".to_vec()), Some(b"2".to_vec()), None]
);
assert_eq!(db.hlen("h_ex_key")?, 0);
Ok(())
}
#[test]
fn test_hash_randfield_and_lex_and_scan() -> Void {
let dir = tempdir()?;
let db = WeDb::open(dir.path())?;
db.hset(
"h_scan_test",
&[("f1", "v1"), ("f2", "v2"), ("f3", "v3"), ("f4", "v4")],
)?;
let r1 = db.hrandfield("h_scan_test", 2, false)?;
assert_eq!(r1.len(), 2);
let r2 = db.hrandfield("h_scan_test", -3, true)?;
assert_eq!(r2.len(), 3);
assert!(r2[0].1.is_some());
let spec = RangeLexSpec {
min: b"f1".to_vec(),
max: b"f3".to_vec(),
minex: false,
maxex: true, min_infinite: false,
max_infinite: false,
offset: 0,
count: None,
reversed: false,
};
let lex_items = db.hrangebylex("h_scan_test", spec)?;
assert_eq!(lex_items.len(), 2);
assert_eq!(lex_items[0].0, b"f1");
assert_eq!(lex_items[1].0, b"f2");
let spec_rev = RangeLexSpec {
min: b"f1".to_vec(),
max: b"f4".to_vec(),
minex: false,
maxex: false,
min_infinite: false,
max_infinite: false,
offset: 0,
count: Some(2),
reversed: true,
};
let rev_items = db.hrangebylex("h_scan_test", spec_rev)?;
assert_eq!(rev_items.len(), 2);
assert_eq!(rev_items[0].0, b"f4");
assert_eq!(rev_items[1].0, b"f3");
let (cursor, scanned) = db.hscan("h_scan_test", 0, 10, Some(b"f*"))?;
assert_eq!(cursor, 0);
assert_eq!(scanned.len(), 4);
Ok(())
}
#[test]
fn test_hash_expired_key_cleanup_and_ttl_preservation() -> Void {
let dir = tempdir()?;
let db = WeDb::open(dir.path())?;
db.hset("h_inc_ttl", &[("counter", "10")])?;
db.hexpire("h_inc_ttl", &["counter"], 300, HExpire::None)?;
let ttl_before = db.httl("h_inc_ttl", &["counter"])?[0];
assert!(ttl_before > 0 && ttl_before <= 300);
let new_val = db.hincrby("h_inc_ttl", "counter", 5)?;
assert_eq!(new_val, 15);
let ttl_after = db.httl("h_inc_ttl", &["counter"])?[0];
assert!(ttl_after > 0 && ttl_after <= 300);
db.hset("h_inc_ttl", &[("float_val", "1.5")])?;
db.hexpire("h_inc_ttl", &["float_val"], 200, HExpire::None)?;
let f_val = db.hincrbyfloat("h_inc_ttl", "float_val", 2.25)?;
assert!((f_val - 3.75).abs() < 1e-6);
let f_ttl = db.httl("h_inc_ttl", &["float_val"])?[0];
assert!(f_ttl > 0 && f_ttl <= 200);
assert_eq!(db.hstrlen("h_inc_ttl", "nonexistent")?, 0);
assert_eq!(db.hstrlen("h_inc_ttl", "counter")?, 2);
db.hexpire("h_inc_ttl", &["counter"], 0, HExpire::None)?;
assert_eq!(db.hstrlen("h_inc_ttl", "counter")?, 0);
Ok(())
}