#![allow(dead_code)]
use std::sync::Arc;
use armdb::armour::Db;
use armdb::{FixedConfig, UniqueIndex, UniqueOwner, unique_index};
use armour_core::Fuid;
armour_core::const_hasher_or!(user_id, "USR", "dev-key-user_id", brand = "user");
type UserId = Fuid<user_id::Hasher>;
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
zerocopy::FromBytes,
zerocopy::IntoBytes,
zerocopy::Immutable,
zerocopy::KnownLayout,
)]
#[repr(C)]
pub struct UsernameKey([u8; 24]);
armdb::impl_key_zerocopy!(
UsernameKey,
armour_core::KeyScheme::Typed(&[armour_core::KeyType::Array(24)])
);
impl UsernameKey {
fn new(s: &str) -> Self {
let mut b = [0u8; 24];
b[..s.len()].copy_from_slice(s.as_bytes());
Self(b)
}
}
unique_index!(Usernames, key = UsernameKey, name = "usernames");
#[test]
fn namespace_opens_empty_and_reads_are_typed() {
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(Db::open_test(dir.path()).unwrap());
let ns = db
.open_unique_index::<Usernames>(FixedConfig::test())
.unwrap();
assert_eq!(ns.name(), "usernames");
assert!(ns.owner_of(&UsernameKey::new("rail")).is_none());
assert!(!ns.contains(&UsernameKey::new("rail")));
}
#[test]
fn row_and_owner_are_format_stable() {
assert_eq!(size_of::<armdb::UniqueRow>(), 80);
assert_eq!(armdb::UNIQUE_OWNER_KEY_CAP, 56);
}
#[derive(Clone, Debug, PartialEq, armour_core::GetType)]
struct UserOfEightBytes;
impl armdb::CollectionMeta for UserOfEightBytes {
type SelfId = [u8; 8];
const NAME: &'static str = "user_of_eight_bytes";
}
#[derive(Clone, Debug, PartialEq, armour_core::GetType)]
struct OtherCollectionWithSixteenByteKey;
impl armdb::CollectionMeta for OtherCollectionWithSixteenByteKey {
type SelfId = [u8; 16];
const NAME: &'static str = "other_collection_with_sixteen_byte_key";
}
#[derive(Clone, Debug, PartialEq, armour_core::GetType)]
struct UserOfEightBytesWithASixteenByteKey;
impl armdb::CollectionMeta for UserOfEightBytesWithASixteenByteKey {
type SelfId = [u8; 16];
const NAME: &'static str = "user_of_eight_bytes";
}
#[test]
fn owner_decode_succeeds_for_its_own_collection() {
let owner: UniqueOwner = armdb::testing::unique_owner_for::<UserOfEightBytes>(&[1u8; 8]);
assert!(owner.belongs_to::<UserOfEightBytes>().is_some());
}
#[test]
fn owner_decode_rejects_a_foreign_collection_tag() {
let owner = armdb::testing::unique_owner_for::<UserOfEightBytes>(&[1u8; 8]);
assert!(
owner
.belongs_to::<OtherCollectionWithSixteenByteKey>()
.is_none()
);
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
zerocopy::FromBytes,
zerocopy::IntoBytes,
zerocopy::Immutable,
zerocopy::KnownLayout,
)]
#[repr(C)]
pub struct EmailKey([u8; 32]);
armdb::impl_key_zerocopy!(
EmailKey,
armour_core::KeyScheme::Typed(&[armour_core::KeyType::Array(32)])
);
impl EmailKey {
fn new(s: &str) -> Self {
let mut b = [0u8; 32];
b[..s.len()].copy_from_slice(s.as_bytes());
Self(b)
}
}
unique_index!(Emails, key = EmailKey, name = "emails");
fn open_two_namespaces() -> (
tempfile::TempDir,
Arc<Db>,
Arc<UniqueIndex<Usernames>>,
Arc<UniqueIndex<Emails>>,
) {
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(Db::open_test(dir.path()).unwrap());
let usernames = db
.open_unique_index::<Usernames>(FixedConfig::test())
.unwrap();
let emails = db.open_unique_index::<Emails>(FixedConfig::test()).unwrap();
(dir, db, usernames, emails)
}
#[test]
fn sink_rejects_an_undeclared_namespace_and_honours_the_budget() {
let (_dir, _db, usernames, emails) = open_two_namespaces();
let targets = armdb::UniqueTargets::new().with(&usernames);
let mut sink =
armdb::testing::claim_sink(&targets, 2, 1024);
sink.claim(&usernames, UsernameKey::new("a")).unwrap();
let err = sink.claim(&emails, EmailKey::new("b")).unwrap_err();
assert!(matches!(
err,
armdb::DbError::UndeclaredUniqueTarget {
namespace: "emails"
}
));
sink.claim(&usernames, UsernameKey::new("c")).unwrap();
let err = sink.claim(&usernames, UsernameKey::new("d")).unwrap_err();
assert!(matches!(
err,
armdb::DbError::ProjectionBudgetExceeded { .. }
));
}
#[test]
fn owner_decode_rejects_a_key_length_mismatch() {
let owner = armdb::testing::unique_owner_for::<UserOfEightBytes>(&[1u8; 8]);
assert!(
owner
.belongs_to::<UserOfEightBytesWithASixteenByteKey>()
.is_none()
);
}
#[cfg(feature = "schema")]
use armdb::schema::{CollectionKind, SchemaRegistry};
#[cfg(feature = "schema")]
use armdb::{Config, CounterTargets, OnMissingTarget, RapiraCodec, TtlPolicy, Upsert};
#[cfg(feature = "schema")]
use armour_core::{GetRefs, GetType};
#[cfg(feature = "schema")]
use rapira::Rapira;
#[cfg(feature = "schema")]
#[derive(Clone, Debug, PartialEq, GetType, GetRefs, Rapira)]
struct User {
username: Option<String>,
}
#[cfg(feature = "schema")]
impl armdb::CollectionMeta for User {
type SelfId = UserId;
const NAME: &'static str = "task3_users";
}
#[cfg(feature = "schema")]
#[derive(
Clone,
Copy,
Debug,
Default,
PartialEq,
GetType,
GetRefs,
zerocopy::FromBytes,
zerocopy::IntoBytes,
zerocopy::Immutable,
zerocopy::KnownLayout,
)]
#[repr(C)]
struct UserTagMarker(pub u8);
#[cfg(feature = "schema")]
impl armdb::CollectionMeta for UserTagMarker {
type SelfId = UserId;
const NAME: &'static str = "task3_user_tags";
}
#[cfg(feature = "schema")]
#[derive(Clone, Debug, PartialEq, GetType, GetRefs, Rapira)]
struct UserStats {
active_count: u64,
}
#[cfg(feature = "schema")]
impl armdb::CollectionMeta for UserStats {
type SelfId = [u8; 8];
const NAME: &'static str = "task3_user_stats";
}
#[cfg(feature = "schema")]
type UserStatsMap = armdb::typed_map!(UserStats, RapiraCodec);
#[cfg(feature = "schema")]
fn open_namespace() -> (tempfile::TempDir, Arc<Db>, Arc<UniqueIndex<Usernames>>) {
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(Db::open_test(dir.path()).unwrap());
let usernames = db
.open_unique_index::<Usernames>(FixedConfig::test())
.unwrap();
(dir, db, usernames)
}
#[cfg(feature = "schema")]
fn open_ordinary_index(db: &Arc<Db>) -> Arc<armdb::zero_tree!(UserTagMarker)> {
db.open_zero_tree::<UserTagMarker, { size_of::<UserTagMarker>() }, _>(
Config::test(),
armdb::NoHook,
&[],
)
.unwrap()
}
#[cfg(feature = "schema")]
fn open_counter_target(db: &Arc<Db>) -> Arc<UserStatsMap> {
db.open_typed_map::<UserStats, RapiraCodec, _>(Config::test(), armdb::NoHook, &[])
.unwrap()
}
#[cfg(feature = "schema")]
fn counter_targets(counters: &Arc<UserStatsMap>) -> CounterTargets {
CounterTargets::new().with_counter(
counters,
"active_count",
OnMissingTarget::Skip,
|v: &UserStats| v.active_count,
|cur: Option<&UserStats>, n: u64| match cur {
Some(_) => Upsert::Put(UserStats { active_count: n }),
None => Upsert::Keep,
},
)
}
#[cfg(feature = "schema")]
#[test]
fn a_source_declares_claims_and_the_carrier_carries_all_three_halves() {
let (_dir, db, usernames) = open_namespace();
let idx = open_ordinary_index(&db); let counters = open_counter_target(&db);
let mut reg = SchemaRegistry::new();
reg.register(&idx, CollectionKind::SecondaryIndex);
reg.register_external(usernames.name(), CollectionKind::UniqueIndex);
let carrier = reg.secondary_index::<User, _>(
armdb::IndexTargets::new().with_index(&idx),
move |k: &UserId, _v: &User, sink| sink.row(&idx, *k, UserTagMarker(1)),
);
let carrier = reg.counted_on(carrier, counter_targets(&counters), |_k, _v, _sink| Ok(()));
let hook = reg.unique_index_on(
carrier,
armdb::UniqueTargets::new().with(&usernames),
move |_k, v: &User, sink| {
if let Some(u) = &v.username {
sink.claim(&usernames, UsernameKey::new(u))?;
}
Ok(())
},
);
assert_eq!(
hook.targets().len(),
1,
"the index half is genuinely non-empty"
);
assert!(
hook.counter_targets().is_some(),
"the counter half survived composition onto the carrier"
);
assert!(
hook.claim_targets().is_some(),
"the claim half survived composition onto the carrier"
);
let users = db
.open_typed_map::<User, RapiraCodec, _>(Config::test(), hook, &[])
.unwrap();
reg.register_entity(&users);
let built = reg.finish().unwrap();
assert_eq!(built.unique_decl_sources().len(), 1);
assert_eq!(built.counted_decl_sources().len(), 1);
}
#[cfg(feature = "schema")]
#[test]
#[should_panic(expected = "unique_index: UniqueTargets must declare at least one target")]
fn unique_index_rejects_empty_targets() {
let mut reg = SchemaRegistry::new();
let _ = reg.unique_index::<User, _>(armdb::UniqueTargets::new(), |_k, _v: &User, _sink| Ok(()));
}
#[cfg(feature = "schema")]
#[derive(Clone, Debug, PartialEq, GetType, GetRefs, Rapira)]
struct UserWithTtl {
username: Option<String>,
expires_at_ms: u64,
}
#[cfg(feature = "schema")]
impl armdb::CollectionMeta for UserWithTtl {
type SelfId = UserId;
const NAME: &'static str = "task5_users_with_ttl";
const TTL: Option<TtlPolicy<Self>> = Some(TtlPolicy {
revision: 1,
deadline: |row, _| (row.expires_at_ms > 0).then_some(row.expires_at_ms),
});
}
#[cfg(feature = "schema")]
#[test]
#[should_panic(
expected = "unique_index: a claiming source must not declare a TTL — a sweep runs outside any transaction, so an expiring row could not release its claim atomically"
)]
fn unique_index_rejects_a_ttl_carrying_source() {
let (_dir, _db, usernames) = open_namespace();
let mut reg = SchemaRegistry::new();
let _ = reg.unique_index::<UserWithTtl, _>(
armdb::UniqueTargets::new().with(&usernames),
move |_k, v: &UserWithTtl, sink| {
if let Some(u) = &v.username {
sink.claim(&usernames, UsernameKey::new(u))?;
}
Ok(())
},
);
}
#[cfg(feature = "schema")]
#[test]
fn open_managed_rejects_a_hand_assembled_ttl_carrying_claim_carrier() {
let (_dir, db, usernames) = open_namespace();
let hook = armdb::Projected::new(armdb::IndexTargets::new(), |_, _, _| Ok(())).and_unique(
armdb::UniqueTargets::new().with(&usernames),
move |_k, v: &UserWithTtl, sink| {
if let Some(u) = &v.username {
sink.claim(&usernames, UsernameKey::new(u))?;
}
Ok(())
},
);
let result = db.open_typed_map::<UserWithTtl, RapiraCodec, _>(Config::test(), hook, &[]);
let Err(err) = result else {
panic!("a claim carrier on a TTL-declaring collection must be rejected at open");
};
assert!(
matches!(
err,
armdb::DbError::ClaimingSourceHasTtl { collection } if collection == "task5_users_with_ttl"
),
"expected ClaimingSourceHasTtl, got {err:?}"
);
}
#[cfg(feature = "schema")]
#[test]
fn claim_carrier_rejects_a_direct_open() {
let (_dir, _db, usernames) = open_namespace();
let src_dir = tempfile::tempdir().unwrap();
let hook = armdb::Projected::new(armdb::IndexTargets::new(), |_, _, _| Ok(())).and_unique(
armdb::UniqueTargets::new().with(&usernames),
move |_k, v: &User, sink| {
if let Some(u) = &v.username {
sink.claim(&usernames, UsernameKey::new(u))?;
}
Ok(())
},
);
let err = armdb::TypedMap::<UserId, User, RapiraCodec, _>::open_hooked(
src_dir.path(),
Config::test(),
RapiraCodec,
hook,
)
.err()
.expect("a claim carrier has nowhere to join db.tx() from a direct open");
assert!(matches!(err, armdb::DbError::ClaimsRequireManagedOpen));
}
#[cfg(feature = "schema")]
#[test]
#[should_panic(expected = "unique_index: namespace `usernames` must not equal source `usernames`")]
fn unique_index_rejects_a_source_as_its_own_namespace() {
let (_dir, _db, usernames) = open_namespace();
let mut reg = SchemaRegistry::new();
let _ = reg.unique_index::<Usernames, _>(
armdb::UniqueTargets::new().with(&usernames),
|_k, _v: &Usernames, _sink| Ok(()),
);
}
#[cfg(feature = "schema")]
#[test]
#[should_panic(
expected = "unique_index: namespace `usernames` is already a projecting source (cascade forbidden)"
)]
fn unique_index_rejects_a_namespace_that_is_already_a_projecting_source() {
let (_dir, db, usernames) = open_namespace();
let idx = open_ordinary_index(&db);
let mut reg = SchemaRegistry::new();
let _ = reg.secondary_index::<Usernames, _>(
armdb::IndexTargets::new().with_index(&idx),
|_k, _v: &Usernames, _sink| Ok(()),
);
let _ = reg.unique_index::<User, _>(
armdb::UniqueTargets::new().with(&usernames),
|_k, _v: &User, _sink| Ok(()),
);
}
#[cfg(feature = "schema")]
#[test]
fn unique_index_allows_the_same_source_into_the_same_namespace_twice() {
let (_dir, _db, usernames) = open_namespace();
let mut reg = SchemaRegistry::new();
let _h1 = reg.unique_index::<User, _>(
armdb::UniqueTargets::new().with(&usernames),
|_k, _v: &User, _sink| Ok(()),
);
let _h2 = reg.unique_index::<User, _>(
armdb::UniqueTargets::new().with(&usernames),
|_k, _v: &User, _sink| Ok(()),
);
}
#[cfg(feature = "schema")]
#[test]
#[should_panic(expected = "and_unique: a claim half is already attached to this carrier")]
fn and_unique_panics_when_called_twice_on_one_carrier() {
let (_dir, _db, usernames, emails) = open_two_namespaces();
let _ = armdb::Projected::<UserId, User>::new(armdb::IndexTargets::new(), |_, _, _| Ok(()))
.and_unique(
armdb::UniqueTargets::new().with(&usernames),
|_k, _v: &User, _sink| Ok(()),
)
.and_unique(
armdb::UniqueTargets::new().with(&emails),
|_k, _v: &User, _sink| Ok(()),
);
}
#[cfg(feature = "schema")]
#[test]
#[should_panic(expected = "and_unique: a claim half is already attached to this carrier")]
fn unique_index_on_rejects_a_second_claim_declaration_for_one_source() {
let (_dir, _db, usernames, emails) = open_two_namespaces();
let mut reg = SchemaRegistry::new();
let hook = reg.unique_index::<User, _>(
armdb::UniqueTargets::new().with(&usernames),
|_k, _v: &User, _sink| Ok(()),
);
let _ = reg.unique_index_on(
hook,
armdb::UniqueTargets::new().with(&emails),
|_k, _v: &User, _sink| Ok(()),
);
}