wedb_embed 0.1.0

Embedded Kvrocks-compatible storage engine for WeDb
Documentation
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()));

    // HSETNX
    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()));

    // HDEL
    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")])?;

    // 1. HEXPIRE
    let res = db.hexpire("hexp", &["f1", "f2"], 100, HExpire::None)?;
    assert_eq!(res, vec![1, 1]);

    // 2. HTTL / HPTTL
    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);

    // 3. HEXPIRETIME / HPEXPIRETIME
    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);

    // 4. 条件检查 (NX / XX / GT / LT)
    let nx_res = db.hexpire("hexp", &["f1", "f3"], 200, HExpire::Nx)?;
    assert_eq!(nx_res, vec![0, 1]); // f1 已有 TTL 不满足 NX,f3 持久满足 NX

    let xx_res = db.hexpire("hexp", &["f1", "f2"], 300, HExpire::Xx)?;
    assert_eq!(xx_res, vec![1, 1]); // f1, f2 均有 TTL 满足 XX

    // GT 条件测试
    let gt_fail = db.hexpire("hexp", &["f1"], 50, HExpire::Gt)?;
    assert_eq!(gt_fail, vec![0]); // 50 < 300, 失败
    let gt_succ = db.hexpire("hexp", &["f1"], 500, HExpire::Gt)?;
    assert_eq!(gt_succ, vec![1]); // 500 > 300, 成功

    // LT 条件测试
    let lt_fail = db.hexpire("hexp", &["f1"], 600, HExpire::Lt)?;
    assert_eq!(lt_fail, vec![0]); // 600 > 500, 失败
    let lt_succ = db.hexpire("hexp", &["f1"], 400, HExpire::Lt)?;
    assert_eq!(lt_succ, vec![1]); // 400 < 500, 成功

    // 5. HPERSIST
    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]);

    // 对已持久化字段再次 hpersist 返回 -1
    let persist_again = db.hpersist("hexp", &["f1", "f2", "f3"])?;
    assert_eq!(persist_again, vec![-1, -1, -1]);

    // 6. 立即过期删除 (seconds <= 0)
    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();

    // 1. HSETEX
    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);

    // Fnx 遇到已有字段失败
    assert!(!db.set_fields_with_expire("h_ex_key", &[("a", "1_new")], setex_opts)?);

    // 便捷 hsetex 接口
    assert!(db.hsetex("h_ex_key2", &[("c", "100")], &[HSetEx::Ex(30), HSetEx::Fnx])?);
    assert_eq!(db.hlen("h_ex_key2")?, 1);

    // 2. HGETEX (持久化)
    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]);

    // 便捷 hgetex 接口
    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]);

    // 3. HGETDEL
    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")],
    )?;

    // HRANDFIELD
    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());

    // HRANGEBYLEX
    let spec = RangeLexSpec {
        min: b"f1".to_vec(),
        max: b"f3".to_vec(),
        minex: false,
        maxex: true, // [f1, f3) -> f1, f2
        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");

    // Reversed range
    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");

    // HSCAN
    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())?;

    // 1. 测试 HINCRBY 保持现有 LiveTTL 字段过期时间
    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);

    // 2. 测试 HINCRBYFLOAT 保持现有 LiveTTL 字段过期时间
    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);

    // 3. 测试 HSTRLEN 对不存在或已过期字段返回 0
    assert_eq!(db.hstrlen("h_inc_ttl", "nonexistent")?, 0);
    assert_eq!(db.hstrlen("h_inc_ttl", "counter")?, 2);

    // 立即过期后 HSTRLEN 查不到
    db.hexpire("h_inc_ttl", &["counter"], 0, HExpire::None)?;
    assert_eq!(db.hstrlen("h_inc_ttl", "counter")?, 0);

    Ok(())
}