#![cfg(all(feature = "typed-tree", feature = "armour", feature = "rapira-codec"))]
use std::mem::size_of;
use std::sync::{Arc, Mutex};
use armdb::armour::{Db, TypedMigration};
use armdb::{CollectionMeta, Config, MigrateAction, NoHook, RapiraCodec, TypedWriteHook};
use armour_core::GetType;
use rapira::Rapira;
use tempfile::tempdir;
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
type InitLog = Arc<Mutex<Vec<([u8; 8], i64)>>>;
#[derive(Clone, Default)]
struct RecordingHook(InitLog);
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct StepItem {
value: i64,
}
impl CollectionMeta for StepItem {
type SelfId = [u8; 8];
const NAME: &'static str = "step_items";
const VERSION: u16 = 3;
}
impl TypedWriteHook<[u8; 8], StepItem> for RecordingHook {
const NEEDS_OLD_VALUE: bool = false;
const NEEDS_INIT: bool = true;
fn on_write(&self, _k: &[u8; 8], _o: Option<&StepItem>, _n: Option<&StepItem>) {}
fn on_init(&self, k: &[u8; 8], v: &StepItem) {
self.0.lock().unwrap().push((*k, v.value));
}
}
fn step1(_k: &[u8; 8], v: &StepItem) -> MigrateAction<StepItem> {
MigrateAction::Update(StepItem {
value: v.value * 10,
})
}
fn step2(_k: &[u8; 8], v: &StepItem) -> MigrateAction<StepItem> {
MigrateAction::Update(StepItem { value: v.value + 1 })
}
const MIGRATIONS: &[TypedMigration<[u8; 8], StepItem>] = &[(1, step1), (2, step2)];
fn assert_init_once(events: &[([u8; 8], i64)], n: u64) {
assert_eq!(events.len(), n as usize, "on_init count: {events:?}");
for i in 0..n {
let key = i.to_be_bytes();
let matches: Vec<_> = events.iter().filter(|(k, _)| *k == key).collect();
assert_eq!(
matches.len(),
1,
"key {i}: expected exactly one on_init, got {matches:?}"
);
assert_eq!(
matches[0].1,
(i as i64) * 10 + 1,
"key {i}: on_init must see the FINAL value"
);
}
}
fn patch_stored_version(db: &Db, name: &str) {
db.db_info_update(|info| {
info.collections.get_mut(name).unwrap().version = 1;
})
.unwrap();
}
#[test]
fn typed_tree_two_step_migration_fires_init_once_with_final_value() {
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let tree = db
.open_typed_tree::<StepItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
for i in 0u64..3 {
tree.put(&i.to_be_bytes(), StepItem { value: i as i64 })
.unwrap();
}
patch_stored_version(&db, StepItem::NAME);
}
let hook = RecordingHook::default();
let events = hook.0.clone();
let db = Db::open_test(dir.path()).unwrap();
let _tree = db
.open_typed_tree::<StepItem, RapiraCodec, _>(Config::test(), hook, MIGRATIONS)
.unwrap();
assert_init_once(&events.lock().unwrap(), 3);
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct StepMapItem {
value: i64,
}
impl CollectionMeta for StepMapItem {
type SelfId = [u8; 8];
const NAME: &'static str = "step_map_items";
const VERSION: u16 = 3;
}
impl TypedWriteHook<[u8; 8], StepMapItem> for RecordingHook {
const NEEDS_OLD_VALUE: bool = false;
const NEEDS_INIT: bool = true;
fn on_write(&self, _k: &[u8; 8], _o: Option<&StepMapItem>, _n: Option<&StepMapItem>) {}
fn on_init(&self, k: &[u8; 8], v: &StepMapItem) {
self.0.lock().unwrap().push((*k, v.value));
}
}
fn step1_map(_k: &[u8; 8], v: &StepMapItem) -> MigrateAction<StepMapItem> {
MigrateAction::Update(StepMapItem {
value: v.value * 10,
})
}
fn step2_map(_k: &[u8; 8], v: &StepMapItem) -> MigrateAction<StepMapItem> {
MigrateAction::Update(StepMapItem { value: v.value + 1 })
}
const MAP_MIGRATIONS: &[TypedMigration<[u8; 8], StepMapItem>] = &[(1, step1_map), (2, step2_map)];
#[test]
fn typed_map_two_step_migration_fires_init_once_with_final_value() {
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let map = db
.open_typed_map::<StepMapItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
for i in 0u64..3 {
map.put(&i.to_be_bytes(), StepMapItem { value: i as i64 })
.unwrap();
}
patch_stored_version(&db, StepMapItem::NAME);
}
let hook = RecordingHook::default();
let events = hook.0.clone();
let db = Db::open_test(dir.path()).unwrap();
let _map = db
.open_typed_map::<StepMapItem, RapiraCodec, _>(Config::test(), hook, MAP_MIGRATIONS)
.unwrap();
assert_init_once(&events.lock().unwrap(), 3);
}
#[cfg(feature = "var-collections")]
mod var_typed {
use super::*;
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct VarStepItem {
value: i64,
}
impl CollectionMeta for VarStepItem {
type SelfId = [u8; 8];
const NAME: &'static str = "var_step_items";
const VERSION: u16 = 3;
}
impl TypedWriteHook<[u8; 8], VarStepItem> for RecordingHook {
const NEEDS_OLD_VALUE: bool = false;
const NEEDS_INIT: bool = true;
fn on_write(&self, _k: &[u8; 8], _o: Option<&VarStepItem>, _n: Option<&VarStepItem>) {}
fn on_init(&self, k: &[u8; 8], v: &VarStepItem) {
self.0.lock().unwrap().push((*k, v.value));
}
}
fn var_step1(_k: &[u8; 8], v: &VarStepItem) -> MigrateAction<VarStepItem> {
MigrateAction::Update(VarStepItem {
value: v.value * 10,
})
}
fn var_step2(_k: &[u8; 8], v: &VarStepItem) -> MigrateAction<VarStepItem> {
MigrateAction::Update(VarStepItem { value: v.value + 1 })
}
const VAR_MIGRATIONS: &[TypedMigration<[u8; 8], VarStepItem>] =
&[(1, var_step1), (2, var_step2)];
#[test]
fn var_typed_tree_two_step_migration_fires_init_once_with_final_value() {
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let tree = db
.open_var_typed_tree::<VarStepItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
for i in 0u64..3 {
tree.put(&i.to_be_bytes(), &VarStepItem { value: i as i64 })
.unwrap();
}
patch_stored_version(&db, VarStepItem::NAME);
}
let hook = RecordingHook::default();
let events = hook.0.clone();
let db = Db::open_test(dir.path()).unwrap();
let _tree = db
.open_var_typed_tree::<VarStepItem, RapiraCodec, _>(
Config::test(),
hook,
VAR_MIGRATIONS,
)
.unwrap();
assert_init_once(&events.lock().unwrap(), 3);
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct VarStepMapItem {
value: i64,
}
impl CollectionMeta for VarStepMapItem {
type SelfId = [u8; 8];
const NAME: &'static str = "var_step_map_items";
const VERSION: u16 = 3;
}
impl TypedWriteHook<[u8; 8], VarStepMapItem> for RecordingHook {
const NEEDS_OLD_VALUE: bool = false;
const NEEDS_INIT: bool = true;
fn on_write(&self, _k: &[u8; 8], _o: Option<&VarStepMapItem>, _n: Option<&VarStepMapItem>) {
}
fn on_init(&self, k: &[u8; 8], v: &VarStepMapItem) {
self.0.lock().unwrap().push((*k, v.value));
}
}
fn var_map_step1(_k: &[u8; 8], v: &VarStepMapItem) -> MigrateAction<VarStepMapItem> {
MigrateAction::Update(VarStepMapItem {
value: v.value * 10,
})
}
fn var_map_step2(_k: &[u8; 8], v: &VarStepMapItem) -> MigrateAction<VarStepMapItem> {
MigrateAction::Update(VarStepMapItem { value: v.value + 1 })
}
const VAR_MAP_MIGRATIONS: &[TypedMigration<[u8; 8], VarStepMapItem>] =
&[(1, var_map_step1), (2, var_map_step2)];
#[test]
fn var_typed_map_two_step_migration_fires_init_once_with_final_value() {
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let map = db
.open_var_typed_map::<VarStepMapItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
for i in 0u64..3 {
map.put(&i.to_be_bytes(), &VarStepMapItem { value: i as i64 })
.unwrap();
}
patch_stored_version(&db, VarStepMapItem::NAME);
}
let hook = RecordingHook::default();
let events = hook.0.clone();
let db = Db::open_test(dir.path()).unwrap();
let _map = db
.open_var_typed_map::<VarStepMapItem, RapiraCodec, _>(
Config::test(),
hook,
VAR_MAP_MIGRATIONS,
)
.unwrap();
assert_init_once(&events.lock().unwrap(), 3);
}
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, GetType, FromBytes, IntoBytes, Immutable, KnownLayout,
)]
#[repr(C)]
struct ZeroStepItem {
value: i64,
}
impl CollectionMeta for ZeroStepItem {
type SelfId = [u8; 8];
const NAME: &'static str = "zero_step_items";
const VERSION: u16 = 3;
}
impl TypedWriteHook<[u8; 8], ZeroStepItem> for RecordingHook {
const NEEDS_OLD_VALUE: bool = false;
const NEEDS_INIT: bool = true;
fn on_write(&self, _k: &[u8; 8], _o: Option<&ZeroStepItem>, _n: Option<&ZeroStepItem>) {}
fn on_init(&self, k: &[u8; 8], v: &ZeroStepItem) {
self.0.lock().unwrap().push((*k, v.value));
}
}
fn zero_step1(_k: &[u8; 8], v: &ZeroStepItem) -> MigrateAction<ZeroStepItem> {
MigrateAction::Update(ZeroStepItem {
value: v.value * 10,
})
}
fn zero_step2(_k: &[u8; 8], v: &ZeroStepItem) -> MigrateAction<ZeroStepItem> {
MigrateAction::Update(ZeroStepItem { value: v.value + 1 })
}
const ZERO_MIGRATIONS: &[TypedMigration<[u8; 8], ZeroStepItem>] =
&[(1, zero_step1), (2, zero_step2)];
#[test]
fn zero_tree_two_step_migration_fires_init_once_with_final_value() {
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let tree = db
.open_zero_tree::<ZeroStepItem, { size_of::<ZeroStepItem>() }, _>(
Config::test(),
NoHook,
&[],
)
.unwrap();
for i in 0u64..3 {
tree.put(&i.to_be_bytes(), &ZeroStepItem { value: i as i64 })
.unwrap();
}
patch_stored_version(&db, ZeroStepItem::NAME);
}
let hook = RecordingHook::default();
let events = hook.0.clone();
let db = Db::open_test(dir.path()).unwrap();
let _tree = db
.open_zero_tree::<ZeroStepItem, { size_of::<ZeroStepItem>() }, _>(
Config::test(),
hook,
ZERO_MIGRATIONS,
)
.unwrap();
assert_init_once(&events.lock().unwrap(), 3);
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, GetType, FromBytes, IntoBytes, Immutable, KnownLayout,
)]
#[repr(C)]
struct ZeroStepMapItem {
value: i64,
}
impl CollectionMeta for ZeroStepMapItem {
type SelfId = [u8; 8];
const NAME: &'static str = "zero_step_map_items";
const VERSION: u16 = 3;
}
impl TypedWriteHook<[u8; 8], ZeroStepMapItem> for RecordingHook {
const NEEDS_OLD_VALUE: bool = false;
const NEEDS_INIT: bool = true;
fn on_write(&self, _k: &[u8; 8], _o: Option<&ZeroStepMapItem>, _n: Option<&ZeroStepMapItem>) {}
fn on_init(&self, k: &[u8; 8], v: &ZeroStepMapItem) {
self.0.lock().unwrap().push((*k, v.value));
}
}
fn zero_map_step1(_k: &[u8; 8], v: &ZeroStepMapItem) -> MigrateAction<ZeroStepMapItem> {
MigrateAction::Update(ZeroStepMapItem {
value: v.value * 10,
})
}
fn zero_map_step2(_k: &[u8; 8], v: &ZeroStepMapItem) -> MigrateAction<ZeroStepMapItem> {
MigrateAction::Update(ZeroStepMapItem { value: v.value + 1 })
}
const ZERO_MAP_MIGRATIONS: &[TypedMigration<[u8; 8], ZeroStepMapItem>] =
&[(1, zero_map_step1), (2, zero_map_step2)];
#[test]
fn zero_map_two_step_migration_fires_init_once_with_final_value() {
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let map = db
.open_zero_map::<ZeroStepMapItem, { size_of::<ZeroStepMapItem>() }, _>(
Config::test(),
NoHook,
&[],
)
.unwrap();
for i in 0u64..3 {
map.put(&i.to_be_bytes(), &ZeroStepMapItem { value: i as i64 })
.unwrap();
}
patch_stored_version(&db, ZeroStepMapItem::NAME);
}
let hook = RecordingHook::default();
let events = hook.0.clone();
let db = Db::open_test(dir.path()).unwrap();
let _map = db
.open_zero_map::<ZeroStepMapItem, { size_of::<ZeroStepMapItem>() }, _>(
Config::test(),
hook,
ZERO_MAP_MIGRATIONS,
)
.unwrap();
assert_init_once(&events.lock().unwrap(), 3);
}