cesiumdb 0.1.0

Blazing fast, persistent key-value store for Rust
use cesiumdb::{
    Db,
    DbOptions,
};
use tempfile::TempDir;

/// Tests that L0 key ranges are properly tracked during flush.
///
/// This test verifies that:
/// 1. L0 segments created by flush have their key ranges stored
/// 2. Key ranges persist across database restart (recovered from manifest)
/// 3. Data can be read correctly after recovery
#[test]
fn test_l0_key_ranges_persist_across_flush() {
    let temp_dir = TempDir::new().unwrap();
    let mut opts = DbOptions::new();
    opts.data_dir(temp_dir.path().to_path_buf())
        .memtable_size(512 * 1024) // 512KB memtable
        .max_memtables(2);

    let db = Db::open(opts);

    // Insert keys "a" to "z"
    for c in b'a'..=b'z' {
        db.put(&[c], b"value").unwrap();
    }

    db.sync().unwrap();

    // Verify data is readable
    for c in b'a'..=b'z' {
        let result = db.get(&[c]).unwrap();
        assert!(result.is_some(), "Key {} should be present", c as char);
        assert_eq!(&result.unwrap()[..], b"value");
    }

    db.close().unwrap();
}

/// Tests that L0 key ranges are correctly recovered from manifest after
/// restart.
///
/// This test verifies that:
/// 1. Key ranges written to manifest during flush
/// 2. VersionEdit::apply() correctly uses key ranges from manifest
/// 3. L0 segments can be read after recovery
#[test]
fn test_l0_key_ranges_recovered_from_manifest() {
    let temp_dir = TempDir::new().unwrap();
    let mut opts = DbOptions::new();
    opts.data_dir(temp_dir.path().to_path_buf())
        .memtable_size(512 * 1024)
        .max_memtables(2);

    // First session: write and flush data
    {
        let mut opts1 = DbOptions::new();
        opts1
            .data_dir(temp_dir.path().to_path_buf())
            .memtable_size(512 * 1024)
            .max_memtables(2);
        let db = Db::open(opts1);
        db.put(b"m", b"value_m").unwrap();
        db.put(b"p", b"value_p").unwrap();
        db.put(b"x", b"value_x").unwrap();
        db.sync().unwrap();
        db.close().unwrap();
    }

    // Second session: reopen and verify data
    {
        let mut opts2 = DbOptions::new();
        opts2
            .data_dir(temp_dir.path().to_path_buf())
            .memtable_size(512 * 1024)
            .max_memtables(2);
        let db = Db::open(opts2);

        // Verify all keys are readable after recovery
        let result_m = db.get(b"m").unwrap();
        assert!(
            result_m.is_some(),
            "Key 'm' should be present after recovery"
        );
        assert_eq!(&result_m.unwrap()[..], b"value_m");

        let result_p = db.get(b"p").unwrap();
        assert!(
            result_p.is_some(),
            "Key 'p' should be present after recovery"
        );
        assert_eq!(&result_p.unwrap()[..], b"value_p");

        let result_x = db.get(b"x").unwrap();
        assert!(
            result_x.is_some(),
            "Key 'x' should be present after recovery"
        );
        assert_eq!(&result_x.unwrap()[..], b"value_x");

        db.close().unwrap();
    }
}

/// Tests that multiple L0 segments have correct key ranges after multiple
/// flushes.
#[test]
fn test_multiple_l0_segments_have_key_ranges() {
    let temp_dir = TempDir::new().unwrap();
    let mut opts = DbOptions::new();
    opts.data_dir(temp_dir.path().to_path_buf())
        .memtable_size(512 * 1024)
        .max_memtables(2);

    let db = Db::open(opts);

    // Create multiple L0 segments with different key ranges
    // Batch 1: keys starting with "a"
    for i in 0..1000 {
        let key = format!("a_key_{:05}", i);
        db.put(key.as_bytes(), b"value_a").unwrap();
    }
    db.sync().unwrap();

    // Batch 2: keys starting with "m"
    for i in 0..1000 {
        let key = format!("m_key_{:05}", i);
        db.put(key.as_bytes(), b"value_m").unwrap();
    }
    db.sync().unwrap();

    // Batch 3: keys starting with "z"
    for i in 0..1000 {
        let key = format!("z_key_{:05}", i);
        db.put(key.as_bytes(), b"value_z").unwrap();
    }
    db.sync().unwrap();

    // Verify all data is readable
    for i in 0..1000 {
        let key_a = format!("a_key_{:05}", i);
        let result = db.get(key_a.as_bytes()).unwrap();
        assert!(result.is_some(), "Key {} should be present", key_a);

        let key_m = format!("m_key_{:05}", i);
        let result = db.get(key_m.as_bytes()).unwrap();
        assert!(result.is_some(), "Key {} should be present", key_m);

        let key_z = format!("z_key_{:05}", i);
        let result = db.get(key_z.as_bytes()).unwrap();
        assert!(result.is_some(), "Key {} should be present", key_z);
    }

    db.close().unwrap();
}

/// Tests that L0→L1 compaction works correctly with key ranges.
///
/// This test verifies that compaction can find overlapping L1 segments
/// using the L0 key ranges.
///
/// NOTE: This test is temporarily disabled until Bug 1 (empty key ranges in
/// CompactionInput) is fixed by Agent A. Once Bug 1 is complete, this test
/// should pass.
#[test]
#[ignore]
fn test_l0_compaction_uses_key_ranges() {
    let temp_dir = TempDir::new().unwrap();
    let mut opts = DbOptions::new();
    opts.data_dir(temp_dir.path().to_path_buf())
        .memtable_size(256 * 1024) // Smaller memtable for more L0 files
        .max_memtables(2);

    let db = Db::open(opts);

    // Create many L0 segments to trigger compaction
    for batch in 0..10 {
        for i in 0..500 {
            let key = format!("key_batch{:02}_item{:05}", batch, i);
            db.put(key.as_bytes(), b"test_value").unwrap();
        }
        db.sync().unwrap();
    }

    // Trigger manual compaction
    db.compact().unwrap();

    // Wait for compaction to complete
    std::thread::sleep(std::time::Duration::from_secs(5));

    // Verify all data is still readable after compaction
    for batch in 0..10 {
        for i in 0..500 {
            let key = format!("key_batch{:02}_item{:05}", batch, i);
            let result = db.get(key.as_bytes()).unwrap();
            assert!(
                result.is_some(),
                "Key {} should be present after compaction",
                key
            );
            assert_eq!(
                &result.unwrap()[..],
                b"test_value",
                "Value mismatch for key {} after compaction",
                key
            );
        }
    }

    db.close().unwrap();
}