#![cfg(all(
feature = "armour",
feature = "schema",
feature = "typed-tree",
feature = "rapira-codec"
))]
use std::sync::Arc;
use armdb::armour::Db;
use armdb::{Config, FixedConfig, NoHook, Projected, RapiraCodec, UniqueIndex, UniqueTargets};
use armour_core::{Fuid, GetType};
use rapira::Rapira;
armour_core::const_hasher_or!(user_id, "USR", "dev-key-user_id", brand = "user");
type UserId = Fuid<user_id::Hasher>;
fn id(n: u64) -> UserId {
Fuid::from_be_bytes(n.to_be_bytes())
}
fn encode(user: &User) -> Vec<u8> {
rapira::serialize(user)
}
#[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)
}
}
armdb::unique_index!(Usernames, key = UsernameKey, name = "unique_tx_usernames");
fn open_usernames(db: &Arc<Db>) -> Arc<UniqueIndex<Usernames>> {
db.open_unique_index::<Usernames>(FixedConfig::test())
.expect("open usernames namespace")
}
#[derive(Clone, Debug, PartialEq, GetType, Rapira)]
struct User {
username: Option<String>,
avatar: u32,
}
impl armdb::CollectionMeta for User {
type SelfId = UserId;
const NAME: &'static str = "unique_tx_users";
}
type UserHook = Projected<UserId, User, NoHook>;
fn claim_hook_user(usernames: &Arc<UniqueIndex<Usernames>>) -> UserHook {
let targets = UniqueTargets::new().with(usernames);
let ns = Arc::clone(usernames);
Projected::<UserId, User>::new(armdb::IndexTargets::new(), |_, _, _| Ok(())).and_unique(
targets,
move |_k, v: &User, sink| {
if let Some(u) = &v.username {
sink.claim(&ns, UsernameKey::new(u))?;
}
Ok(())
},
)
}
type Coll = armdb::typed_map!(User, RapiraCodec, UserHook);
fn fixture() -> (
tempfile::TempDir,
Arc<Db>,
Arc<UniqueIndex<Usernames>>,
Arc<Coll>,
) {
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(Db::open_test(dir.path()).unwrap());
let usernames = open_usernames(&db);
let hook = claim_hook_user(&usernames);
let users = db
.open_typed_map::<User, RapiraCodec, _>(Config::test(), hook, &[])
.unwrap();
(dir, db, usernames, users)
}
fn tx_put(db: &Arc<Db>, users: &Arc<Coll>, key: UserId, username: Option<&str>) {
db.tx()
.put(
users,
&key,
&User {
username: username.map(str::to_string),
avatar: 1,
},
)
.unwrap()
.commit()
.unwrap();
}
armour_core::const_hasher_or!(channel_id, "CHN", "dev-key-channel_id", brand = "channel");
type ChannelId = Fuid<channel_id::Hasher>;
fn cid(n: u64) -> ChannelId {
Fuid::from_be_bytes(n.to_be_bytes())
}
#[derive(Clone, Debug, PartialEq, GetType, Rapira)]
struct Channel {
username: Option<String>,
}
impl armdb::CollectionMeta for Channel {
type SelfId = ChannelId;
const NAME: &'static str = "unique_tx_channels";
}
type ChannelHook = Projected<ChannelId, Channel, NoHook>;
fn claim_hook_channel(usernames: &Arc<UniqueIndex<Usernames>>) -> ChannelHook {
let targets = UniqueTargets::new().with(usernames);
let ns = Arc::clone(usernames);
Projected::<ChannelId, Channel>::new(armdb::IndexTargets::new(), |_, _, _| Ok(())).and_unique(
targets,
move |_k, v: &Channel, sink| {
if let Some(u) = &v.username {
sink.claim(&ns, UsernameKey::new(u))?;
}
Ok(())
},
)
}
type ChannelColl = armdb::typed_map!(Channel, RapiraCodec, ChannelHook);
fn shared_namespace_fixture() -> (
tempfile::TempDir,
Arc<Db>,
Arc<UniqueIndex<Usernames>>,
Arc<Coll>,
Arc<ChannelColl>,
) {
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(Db::open_test(dir.path()).unwrap());
let usernames = open_usernames(&db);
let users = db
.open_typed_map::<User, RapiraCodec, _>(Config::test(), claim_hook_user(&usernames), &[])
.unwrap();
let channels = db
.open_typed_map::<Channel, RapiraCodec, _>(
Config::test(),
claim_hook_channel(&usernames),
&[],
)
.unwrap();
(dir, db, usernames, users, channels)
}
#[test]
fn unique_tx_writes_the_owner_row() {
let (_dir, db, usernames, users) = fixture();
db.tx()
.put(
&users,
&id(1),
&User {
username: Some("rail".into()),
avatar: 1,
},
)
.unwrap()
.commit()
.unwrap();
let owner = usernames.owner_of(&UsernameKey::new("rail")).unwrap();
assert_eq!(owner.belongs_to::<User>(), Some(id(1)));
assert!(!owner.is_reservation());
}
#[test]
fn unique_tx_refuses_a_key_held_by_another_owner() {
let (_dir, db, usernames, users) = fixture();
tx_put(&db, &users, id(1), Some("rail"));
let err = db
.tx()
.put(
&users,
&id(2),
&User {
username: Some("rail".into()),
avatar: 1,
},
)
.unwrap()
.commit()
.unwrap_err();
assert!(
matches!(
err,
armdb::DbError::UniqueViolation {
namespace: "unique_tx_usernames",
..
}
),
"{err}"
);
assert!(users.get(&id(2)).is_none());
assert_eq!(
usernames
.owner_of(&UsernameKey::new("rail"))
.unwrap()
.belongs_to::<User>(),
Some(id(1))
);
}
#[test]
fn unique_tx_two_sources_claiming_one_key_conflict() {
let (_dir, db, usernames, users, channels) = shared_namespace_fixture();
let err = db
.tx()
.put(
&users,
&id(1),
&User {
username: Some("dup".into()),
avatar: 1,
},
)
.unwrap()
.put(
&channels,
&cid(1),
&Channel {
username: Some("dup".into()),
},
)
.unwrap()
.commit()
.unwrap_err();
assert!(
matches!(err, armdb::DbError::UniqueViolation { .. }),
"{err}"
);
assert!(users.get(&id(1)).is_none());
assert!(channels.get(&cid(1)).is_none());
assert_eq!(usernames.owner_of(&UsernameKey::new("dup")), None);
}
#[test]
fn unique_tx_hands_a_key_from_one_owner_to_another_in_one_transaction() {
let (_dir, db, usernames, users) = fixture();
tx_put(&db, &users, id(1), Some("rail"));
db.tx()
.put(
&users,
&id(1),
&User {
username: None,
avatar: 1,
},
) .unwrap()
.put(
&users,
&id(2),
&User {
username: Some("rail".into()),
avatar: 1,
},
) .unwrap()
.commit()
.unwrap();
assert_eq!(
usernames
.owner_of(&UsernameKey::new("rail"))
.unwrap()
.belongs_to::<User>(),
Some(id(2))
);
}
#[test]
fn unique_tx_hands_a_key_over_with_the_claim_listed_before_the_release() {
let (_dir, db, usernames, users) = fixture();
tx_put(&db, &users, id(1), Some("rail"));
db.tx()
.put(
&users,
&id(2),
&User {
username: Some("rail".into()),
avatar: 1,
},
) .unwrap()
.put(
&users,
&id(1),
&User {
username: None,
avatar: 1,
},
) .unwrap()
.commit()
.unwrap();
assert_eq!(
usernames
.owner_of(&UsernameKey::new("rail"))
.unwrap()
.belongs_to::<User>(),
Some(id(2))
);
}
#[test]
fn unique_tx_an_unrelated_field_update_keeps_the_claim() {
let (_dir, db, usernames, users) = fixture();
tx_put(&db, &users, id(1), Some("rail"));
db.tx()
.put(
&users,
&id(1),
&User {
username: Some("rail".into()),
avatar: 7,
},
)
.unwrap()
.commit()
.unwrap();
assert_eq!(
usernames
.owner_of(&UsernameKey::new("rail"))
.unwrap()
.belongs_to::<User>(),
Some(id(1))
);
}
#[test]
fn unique_tx_rename_via_put_releases_the_old_key_and_lets_it_be_reclaimed() {
let (_dir, db, usernames, users) = fixture();
tx_put(&db, &users, id(1), Some("rail"));
db.tx()
.put(
&users,
&id(1),
&User {
username: Some("new".into()),
avatar: 1,
},
)
.unwrap()
.commit()
.unwrap();
assert_eq!(usernames.owner_of(&UsernameKey::new("rail")), None);
assert_eq!(
usernames
.owner_of(&UsernameKey::new("new"))
.unwrap()
.belongs_to::<User>(),
Some(id(1))
);
tx_put(&db, &users, id(2), Some("rail"));
assert_eq!(
usernames
.owner_of(&UsernameKey::new("rail"))
.unwrap()
.belongs_to::<User>(),
Some(id(2))
);
}
#[test]
fn unique_tx_delete_releases_the_claim() {
let (_dir, db, usernames, users) = fixture();
tx_put(&db, &users, id(1), Some("rail"));
db.tx().delete(&users, &id(1)).unwrap().commit().unwrap();
assert_eq!(usernames.owner_of(&UsernameKey::new("rail")), None);
assert!(users.get(&id(1)).is_none());
tx_put(&db, &users, id(2), Some("rail"));
assert_eq!(
usernames
.owner_of(&UsernameKey::new("rail"))
.unwrap()
.belongs_to::<User>(),
Some(id(2))
);
}
#[test]
fn unique_tx_claims_carry_the_source_as_owner() {
let (_dir, _db, _usernames, users) = fixture();
let old = encode(&User {
username: Some("old".into()),
avatar: 1,
});
let new = encode(&User {
username: Some("new".into()),
avatar: 1,
});
let ops = armdb::testing::tx_claims_of(&users, &id(1), Some(&old), Some(&new)).unwrap();
assert_eq!(ops.len(), 2);
assert!(
ops.iter()
.all(|o| o.owner.belongs_to::<User>() == Some(id(1)))
);
assert_eq!(ops.iter().filter(|o| o.is_release()).count(), 1);
assert_eq!(ops.iter().filter(|o| o.is_claim()).count(), 1);
}
#[test]
fn unique_tx_preplans_every_declared_namespace() {
let (_dir, _db, usernames, users) = fixture();
let preplanned = armdb::testing::tx_preplan_names(&users).unwrap();
assert!(preplanned.contains(&usernames.name()));
}