armdb 0.8.0

sharded bitcask key-value storage optimized for NVMe
Documentation
#![cfg(all(feature = "armour", feature = "typed-tree", feature = "rapira-codec"))]

use armdb::armour::{Db, TypedMigration};
use armdb::{CollectionMeta, Config, MigrateAction, NoHook, RapiraCodec, TtlPolicy};
use armour_core::GetType;
use rapira::Rapira;
use tempfile::tempdir;

#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct Row {
    expires_at_ms: u64,
}

impl CollectionMeta for Row {
    type SelfId = [u8; 8];
    const NAME: &'static str = "ttl_bootstrap_rows";
    const TTL: Option<TtlPolicy<Self>> = Some(TtlPolicy {
        revision: 1,
        deadline: |row, _| (row.expires_at_ms > 0).then_some(row.expires_at_ms),
    });
}

#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct MigRow {
    expires_at_ms: u64,
}

impl CollectionMeta for MigRow {
    type SelfId = [u8; 8];
    const NAME: &'static str = "ttl_bootstrap_mig_rows";
    const VERSION: u16 = 2;
    const TTL: Option<TtlPolicy<Self>> = Some(TtlPolicy {
        revision: 1,
        deadline: |row, _| (row.expires_at_ms > 0).then_some(row.expires_at_ms),
    });
}

fn key(id: u64) -> [u8; 8] {
    id.to_be_bytes()
}

fn patch_stored_version(db: &Db, name: &str) {
    db.db_info_update(|info| {
        info.collections.get_mut(name).unwrap().version = 1;
    })
    .unwrap();
}

#[test]
fn reopen_rebuilds_from_live_state() {
    let dir = tempdir().unwrap();
    {
        let db = Db::open_test(dir.path()).unwrap();
        let tree = db
            .open_typed_tree::<Row, RapiraCodec, NoHook>(Config::test(), NoHook, &[])
            .unwrap();
        tree.put(&key(1), Row { expires_at_ms: 100 }).unwrap();
        tree.put(&key(2), Row { expires_at_ms: 200 }).unwrap();
    }
    let db = Db::open_test(dir.path()).unwrap();
    let tree = db
        .open_typed_tree::<Row, RapiraCodec, NoHook>(Config::test(), NoHook, &[])
        .unwrap();
    assert_eq!(tree.ttl_debug_len(), 2);
    assert_eq!(tree.ttl_debug_deadline(&key(1)), Some(100));
    assert_eq!(tree.ttl_debug_deadline(&key(2)), Some(200));
}

fn v1_to_v2(k: &[u8; 8], _row: &MigRow) -> MigrateAction<MigRow> {
    let id = u64::from_be_bytes(*k);
    match id {
        1 => MigrateAction::Update(MigRow { expires_at_ms: 500 }),
        2 => MigrateAction::Delete,
        _ => MigrateAction::Keep,
    }
}

const MIGRATIONS: &[TypedMigration<[u8; 8], MigRow>] = &[(1, v1_to_v2)];

#[test]
fn migration_update_and_delete_leave_correct_index() {
    let dir = tempdir().unwrap();
    {
        let db = Db::open_test(dir.path()).unwrap();
        let tree = db
            .open_typed_tree::<MigRow, RapiraCodec, NoHook>(Config::test(), NoHook, &[])
            .unwrap();
        tree.put(&key(1), MigRow { expires_at_ms: 100 }).unwrap();
        tree.put(&key(2), MigRow { expires_at_ms: 200 }).unwrap();
        tree.put(&key(3), MigRow { expires_at_ms: 300 }).unwrap();
        patch_stored_version(&db, MigRow::NAME);
    }
    let db = Db::open_test(dir.path()).unwrap();
    let tree = db
        .open_typed_tree::<MigRow, RapiraCodec, NoHook>(Config::test(), NoHook, MIGRATIONS)
        .unwrap();
    assert_eq!(tree.ttl_debug_len(), 2);
    assert_eq!(tree.ttl_debug_deadline(&key(1)), Some(500));
    assert_eq!(tree.ttl_debug_deadline(&key(2)), None);
    assert_eq!(tree.ttl_debug_deadline(&key(3)), Some(300));
}