tegdb 0.5.0

The name TegridyDB (short for TegDB) is inspired by the Tegridy Farm in South Park and tries to correct some of the wrong database implementations, such as null support, implicit conversion support, etc.
Documentation
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use tegdb::log::STORAGE_FORMAT_VERSION;
use tegdb::storage_engine::StorageEngine;
use tempfile::TempDir;

/// Test that current storage headers can be read correctly
#[test]
fn test_current_header_version() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test.teg");

    // Create a new database (should use the current storage version)
    let mut engine = StorageEngine::new(db_path.clone()).unwrap();
    engine.set(b"key1", b"value1".to_vec()).unwrap();
    engine.flush().unwrap();
    drop(engine);

    // Read header and verify the version matches the constant
    let mut file = File::open(&db_path).unwrap();
    let mut header = vec![0u8; 64];
    file.read_exact(&mut header).unwrap();

    // Check version bytes [6..8)
    let version = u16::from_be_bytes([header[6], header[7]]);
    assert_eq!(
        version, STORAGE_FORMAT_VERSION,
        "New databases should use the current storage version"
    );

    // Check valid_data_end is present [21..29)
    let valid_data_end = u64::from_be_bytes([
        header[21], header[22], header[23], header[24], header[25], header[26], header[27],
        header[28],
    ]);
    assert!(
        valid_data_end >= 64,
        "valid_data_end should be at least header size"
    );
    assert!(
        valid_data_end < 1024,
        "valid_data_end should be reasonable for small test"
    );

    // Re-open and verify data
    let engine = StorageEngine::new(db_path).unwrap();
    let value = engine.get(b"key1").unwrap();
    assert_eq!(value.as_ref(), b"value1");
}

/// Simulate a version 1 file and verify it is rejected
#[test]
fn test_version_1_rejected() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_v1.teg");

    // Manually create a version 1 header
    let mut file = OpenOptions::new()
        .create(true)
        .truncate(true)
        .write(true)
        .read(true)
        .open(&db_path)
        .unwrap();

    let mut header = vec![0u8; 64];
    // magic [0..6)
    header[0..6].copy_from_slice(b"TEGDB\0");
    // version [6..8) = 1
    header[6..8].copy_from_slice(&1u16.to_be_bytes());
    // flags [8..12) = 0
    header[8..12].copy_from_slice(&0u32.to_be_bytes());
    // max_key [12..16)
    header[12..16].copy_from_slice(&1024u32.to_be_bytes());
    // max_val [16..20)
    header[16..20].copy_from_slice(&(256 * 1024u32).to_be_bytes());
    // endianness [20] = 1
    header[20] = 1u8;
    // [21..64) reserved = 0 (no valid_data_end for version 1)

    file.write_all(&header).unwrap();

    // Write a simple entry
    let key = b"test_key";
    let value = b"test_value";
    let key_len = key.len() as u32;
    let value_len = value.len() as u32;

    file.write_all(&key_len.to_be_bytes()).unwrap();
    file.write_all(&value_len.to_be_bytes()).unwrap();
    file.write_all(key).unwrap();
    file.write_all(value).unwrap();
    file.sync_all().unwrap();
    drop(file);

    // Opening should now fail
    let result = StorageEngine::new(db_path);
    assert!(result.is_err(), "Version 1 files should be rejected");
}

/// Test that header magic is validated
#[test]
fn test_invalid_magic_rejected() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_bad.teg");

    // Create file with bad magic
    let mut file = File::create(&db_path).unwrap();
    let mut header = vec![0u8; 64];
    header[0..6].copy_from_slice(b"BADMAG"); // Wrong magic
    header[6..8].copy_from_slice(&2u16.to_be_bytes());
    header[20] = 1u8; // endianness
    file.write_all(&header).unwrap();
    drop(file);

    // Should fail to open
    let result = StorageEngine::new(db_path);
    assert!(result.is_err(), "Should reject invalid magic");
}

/// Test that unsupported version is rejected
#[test]
fn test_unsupported_version_rejected() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_v99.teg");

    // Create file with future version
    let mut file = File::create(&db_path).unwrap();
    let mut header = vec![0u8; 64];
    header[0..6].copy_from_slice(b"TEGDB\0");
    header[6..8].copy_from_slice(&99u16.to_be_bytes()); // Future version
    header[20] = 1u8; // endianness
    file.write_all(&header).unwrap();
    drop(file);

    // Should fail to open
    let result = StorageEngine::new(db_path);
    assert!(result.is_err(), "Should reject unsupported version");
}

/// Test valid_data_end is updated correctly
#[test]
fn test_valid_data_end_updates() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test.teg");

    // Create engine and insert data
    let engine = StorageEngine::new(db_path.clone()).unwrap();

    // Initial valid_data_end (should be header size)
    drop(engine);
    let mut file = File::open(&db_path).unwrap();
    let mut header = vec![0u8; 64];
    file.read_exact(&mut header).unwrap();
    let initial_end = u64::from_be_bytes([
        header[21], header[22], header[23], header[24], header[25], header[26], header[27],
        header[28],
    ]);
    assert_eq!(initial_end, 64);
    drop(file);

    // Insert some data
    let mut engine = StorageEngine::new(db_path.clone()).unwrap();
    engine.set(b"key1", b"value1".to_vec()).unwrap();
    engine.flush().unwrap();
    drop(engine);

    // valid_data_end should have increased
    let mut file = File::open(&db_path).unwrap();
    let mut header = vec![0u8; 64];
    file.read_exact(&mut header).unwrap();
    let after_insert_end = u64::from_be_bytes([
        header[21], header[22], header[23], header[24], header[25], header[26], header[27],
        header[28],
    ]);
    assert!(
        after_insert_end > initial_end,
        "valid_data_end should increase after insert"
    );
}