armdb 0.4.1

sharded bitcask key-value storage optimized for NVMe
Documentation
#![cfg(feature = "schema")]

use std::sync::Arc;

use armdb::armour::Db;
use armdb::schema::{Finding, SchemaRegistry, validate};
use armdb::{CollectionMeta, Config, NoHook, RapiraCodec, impl_key_zerocopy};
use armour_core::Fuid;
use armour_core::{CollectionKind, GetRefs, GetType};
use rapira::Rapira;
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};

armour_core::const_hasher_or!(
    s_user,
    "SUSR",
    "K_zxJmNfxOS4R5DfaA2Dl42iHC4LjNaPTfLha6VKQoU1xtk91gdYVSNyl_o5iw1jqxx2zTqYqrQ"
);
armour_core::const_hasher_or!(
    s_group,
    "SGRP",
    "au2voAmcasJn_OEcNTf9OZVhP0FEECk9l59i3On0yirc7FyIHMWeEQWqEui4bjEQtfqZkMRpejE"
);

type SUserId = Fuid<s_user::Hasher>;
type SGroupId = Fuid<s_group::Hasher>;

fn fuid_from_native<H>(v: u64) -> Fuid<H> {
    Fuid::from_be_bytes(v.to_be_bytes())
}

#[derive(Clone, Debug, PartialEq, GetType, GetRefs, Rapira)]
struct SUser {
    pub name: String,
    pub mentor: SUserId,
}
impl CollectionMeta for SUser {
    type SelfId = SUserId;
    const NAME: &'static str = "s_users";
}

#[derive(Clone, Debug, PartialEq, GetType, GetRefs, Rapira)]
struct SGroup {
    pub title: String,
    pub owner: SUserId,
    pub member_count: u32,
}
impl CollectionMeta for SGroup {
    type SelfId = SGroupId;
    const NAME: &'static str = "s_groups";
}

#[derive(
    Clone,
    Copy,
    Debug,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    FromBytes,
    IntoBytes,
    Immutable,
    KnownLayout,
    armour_core::GetRefs,
)]
#[repr(C)]
struct OwnerGroupKey {
    owner: SUserId,
    group: SGroupId,
}
impl_key_zerocopy!(
    OwnerGroupKey,
    armour_core::KeyScheme::Typed(&[armour_core::KeyType::Fuid, armour_core::KeyType::Fuid])
);

#[derive(
    Clone, Copy, Debug, PartialEq, GetType, GetRefs, FromBytes, IntoBytes, Immutable, KnownLayout,
)]
#[repr(C)]
struct OwnerGroupMarker(pub u8);

impl CollectionMeta for OwnerGroupMarker {
    type SelfId = OwnerGroupKey;
    const NAME: &'static str = "s_owner_groups";
}

#[test]
fn atomic_writes_keep_secondary_index_consistent() {
    let dir = tempfile::tempdir().expect("tmp");
    let db = Arc::new(Db::open_test(dir.path()).expect("db"));

    let idx = db
        .open_zero_tree::<OwnerGroupMarker, { std::mem::size_of::<OwnerGroupMarker>() }, _>(
            Config::default(),
            NoHook,
            &[],
        )
        .expect("open idx");

    let mut reg = SchemaRegistry::new();
    reg.register(&idx, CollectionKind::SecondaryIndex);

    let hook = reg.secondary_index::<SGroup, _, _>(idx.clone(), |k: &SGroupId, v: &SGroup| {
        (
            OwnerGroupKey {
                owner: v.owner,
                group: *k,
            },
            OwnerGroupMarker(1),
        )
    });

    let users = db
        .open_typed_map::<SUser, RapiraCodec, _>(Config::default(), NoHook, &[])
        .expect("open users");
    let groups = db
        .open_typed_map::<SGroup, RapiraCodec, _>(Config::default(), hook, &[])
        .expect("open groups");
    reg.register_entity(&users);
    reg.register_entity(&groups);

    let g = fuid_from_native::<s_group::Hasher>(1);
    let u1 = fuid_from_native::<s_user::Hasher>(10);
    users
        .insert(
            &u1,
            SUser {
                name: "owner".into(),
                mentor: u1,
            },
        )
        .expect("ins user");

    groups
        .atomic(&g, |s| {
            s.put(
                &g,
                SGroup {
                    title: "t".into(),
                    owner: u1,
                    member_count: 0,
                },
            )?;
            Ok(())
        })
        .expect("atomic put");

    let built = reg.finish().expect("finish");
    let report = validate(&db, &built);
    assert_eq!(
        report.errors().count(),
        0,
        "schema validate must be clean after atomic hook-backed write: {report}"
    );
    assert!(
        !report.findings.iter().any(|f| matches!(
            f,
            Finding::IndexMissing { .. } | Finding::IndexOrphan { .. }
        )),
        "secondary index must stay in sync: {report}"
    );
}