#![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, 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, 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, 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, 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}"
);
}