iridium-db 0.4.0

A high-performance vector-graph hybrid storage and indexing engine
use super::*;
use std::fs;

#[test]
fn sstable_corrupt_entry_rejected() {
    let path = temp_path("sstable_corrupt_entry");
    let entries = vec![Entry {
        key: 1,
        version: 1,
        kind: EntryKind::FullNode,
        value: b"hello".to_vec(),
    }];
    write_sstable(&path, &entries).unwrap();

    // Flip a byte in the entry's value region.
    // Header: 8 + 4*5 + 8*2 = 44 bytes.
    // Entry layout: 8(key)+8(version)+1(kind)+4(len)+value+4(checksum)
    // Value starts at header_len + 8+8+1+4 = 44+21 = 65
    let mut raw = fs::read(&path).unwrap();
    raw[65] ^= 0xFF;
    fs::write(&path, &raw).unwrap();

    let result = read_sstable(&path);
    assert!(
        matches!(result, Err(SstableError::Corrupt(_))),
        "expected Corrupt error, got {:?}",
        result.map(|_| ())
    );
    fs::remove_file(&path).ok();
}

fn temp_path(name: &str) -> std::path::PathBuf {
    let mut dir = std::env::temp_dir();
    let stamp = format!(
        "{}_{}_{}",
        name,
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    );
    dir.push(format!("{}.sst", stamp));
    dir
}

fn sample_entries(count: usize) -> Vec<Entry> {
    (0..count)
        .map(|i| Entry {
            key: i as u64,
            version: (i * 10) as u64,
            kind: EntryKind::EdgeDelta,
            value: vec![i as u8, (i + 1) as u8],
        })
        .collect()
}

#[test]
fn sstable_round_trip() {
    let path = temp_path("sstable_round_trip");
    let entries = vec![
        Entry {
            key: 5,
            version: 1,
            kind: EntryKind::FullNode,
            value: b"alpha".to_vec(),
        },
        Entry {
            key: 2,
            version: 2,
            kind: EntryKind::EdgeDelta,
            value: b"beta".to_vec(),
        },
    ];

    let table = write_sstable(&path, &entries).unwrap();
    let loaded = read_sstable(&path).unwrap();

    assert_eq!(table.entries, loaded.entries);
    assert_eq!(table.fences, loaded.fences);
    assert_eq!(table.bloom.bits(), loaded.bloom.bits());

    fs::remove_file(path).ok();
}

#[test]
fn bloom_and_fence_metadata() {
    let path = temp_path("sstable_bloom_fence");
    let entries = sample_entries(130);
    let table = write_sstable(&path, &entries).unwrap();

    assert!(table.bloom.may_contain(5));
    assert!(table.bloom.may_contain(129));
    assert_eq!(table.fences.len(), 3);
    assert_eq!(table.fences[0].min_key, 0);
    assert_eq!(table.fences[0].max_key, 63);
    assert_eq!(table.fences[1].min_key, 64);
    assert_eq!(table.fences[1].max_key, 127);
    assert_eq!(table.fences[2].min_key, 128);
    assert_eq!(table.fences[2].max_key, 129);

    fs::remove_file(path).ok();
}

#[test]
fn sstable_is_page_aligned() {
    let path = temp_path("sstable_page_aligned");
    let entries = sample_entries(200);
    let table = write_sstable(&path, &entries).unwrap();

    assert_eq!(table.fences.len(), 4);

    let metadata = fs::metadata(&path).unwrap();
    assert_eq!(metadata.len() % PAGE_SIZE_BYTES, 0);

    let buffer = std::fs::read(&path).unwrap();
    let mut cursor = Cursor::new(buffer);
    let header = read_header(&mut cursor).unwrap();
    assert_eq!(header.bloom_offset % PAGE_SIZE_BYTES, 0);
    assert_eq!(header.fence_offset % PAGE_SIZE_BYTES, 0);

    fs::remove_file(path).ok();
}