#![cfg(all(feature = "typed-tree", feature = "armour", feature = "rapira-codec"))]
use armdb::armour::Db;
use armdb::{
CollectionMeta, Config, DbError, MigrateAction, NoHook, RapiraCodec, SchemaMismatchKind,
};
use armour_core::GetType;
use rapira::Rapira;
use tempfile::tempdir;
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct ItemV1 {
value: u64,
}
impl CollectionMeta for ItemV1 {
type SelfId = [u8; 8];
const NAME: &'static str = "mig_test_items";
const VERSION: u16 = 1;
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct ItemV2 {
value: u64,
}
impl CollectionMeta for ItemV2 {
type SelfId = [u8; 8];
const NAME: &'static str = "mig_test_items";
const VERSION: u16 = 2;
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct ItemV3 {
value: u64,
}
impl CollectionMeta for ItemV3 {
type SelfId = [u8; 8];
const NAME: &'static str = "mig_test_items";
const VERSION: u16 = 3;
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct HashItemA {
value: u64,
}
impl CollectionMeta for HashItemA {
type SelfId = [u8; 8];
const NAME: &'static str = "mig_hash_items";
const VERSION: u16 = 1;
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct HashItemB {
value: u64,
extra: u32,
}
impl CollectionMeta for HashItemB {
type SelfId = [u8; 8];
const NAME: &'static str = "mig_hash_items";
const VERSION: u16 = 1;
}
fn key(id: u64) -> [u8; 8] {
id.to_be_bytes()
}
fn unwrap_err<T>(r: Result<T, DbError>) -> DbError {
match r {
Err(e) => e,
Ok(_) => panic!("expected Err, got Ok"),
}
}
#[test]
fn migration_multi_step_walks_intermediate_versions() {
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let tree = db
.open_typed_tree::<ItemV1, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
tree.put(&key(0), ItemV1 { value: 1 }).unwrap();
tree.put(&key(1), ItemV1 { value: 1 }).unwrap();
db.close().unwrap();
}
fn add_one_step(_k: &[u8; 8], v: &ItemV3) -> MigrateAction<ItemV3> {
MigrateAction::Update(ItemV3 { value: v.value + 1 })
}
#[allow(clippy::type_complexity)]
let migrations: &[(u16, fn(&[u8; 8], &ItemV3) -> MigrateAction<ItemV3>)] =
&[(1, add_one_step), (2, add_one_step)];
{
let db = Db::open_test(dir.path()).unwrap();
let tree = db
.open_typed_tree::<ItemV3, RapiraCodec, _>(Config::test(), NoHook, migrations)
.unwrap();
assert_eq!(
tree.get(&key(0)).unwrap().value,
3,
"entry 0 must reach v=3"
);
assert_eq!(
tree.get(&key(1)).unwrap().value,
3,
"entry 1 must reach v=3"
);
db.close().unwrap();
}
{
let db = Db::open_test(dir.path()).unwrap();
let tree = db
.open_typed_tree::<ItemV3, RapiraCodec, _>(Config::test(), NoHook, migrations)
.unwrap();
assert_eq!(tree.get(&key(0)).unwrap().value, 3, "no second migration");
assert_eq!(tree.get(&key(1)).unwrap().value, 3, "no second migration");
}
}
#[test]
fn migration_missing_step_returns_schema_mismatch() {
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let _tree = db
.open_typed_tree::<ItemV1, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
db.close().unwrap();
}
let db = Db::open_test(dir.path()).unwrap();
let err = unwrap_err(db.open_typed_tree::<ItemV3, RapiraCodec, _>(Config::test(), NoHook, &[]));
match err {
DbError::SchemaMismatch {
kind: SchemaMismatchKind::MissingStep { from },
..
} => {
assert_eq!(from, 1, "MissingStep should report from=1");
}
other => panic!("expected SchemaMismatch::MissingStep, got: {other}"),
}
}
#[test]
fn migration_downgrade_returns_schema_mismatch() {
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let _tree = db
.open_typed_tree::<ItemV3, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
db.close().unwrap();
}
let db = Db::open_test(dir.path()).unwrap();
let err = unwrap_err(db.open_typed_tree::<ItemV2, RapiraCodec, _>(Config::test(), NoHook, &[]));
match err {
DbError::SchemaMismatch {
kind: SchemaMismatchKind::Downgrade { stored, requested },
..
} => {
assert_eq!(stored, 3);
assert_eq!(requested, 2);
}
other => panic!("expected SchemaMismatch::Downgrade, got: {other}"),
}
}
#[test]
fn migration_typ_hash_drift_returns_schema_mismatch() {
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let _tree = db
.open_typed_tree::<HashItemA, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
db.close().unwrap();
}
let db = Db::open_test(dir.path()).unwrap();
let err =
unwrap_err(db.open_typed_tree::<HashItemB, RapiraCodec, _>(Config::test(), NoHook, &[]));
match err {
DbError::SchemaMismatch {
kind: SchemaMismatchKind::TypHash { stored, expected },
..
} => {
assert_ne!(stored, expected, "hashes must differ");
}
other => panic!("expected SchemaMismatch::TypHash, got: {other}"),
}
}
#[test]
fn migration_first_open_no_step_required() {
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let _tree = db
.open_typed_tree::<ItemV2, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
db.close().unwrap();
}
let db = Db::open_test(dir.path()).unwrap();
let stored = db.db_info.cloned().collections;
let info = stored
.get("mig_test_items")
.expect("collection info must exist");
assert_eq!(info.version, 2, "stored version must match V2::VERSION");
}
#[test]
fn migration_resumes_from_intermediate_version() {
let dir = tempdir().unwrap();
fn add_ten(_k: &[u8; 8], v: &ItemV3) -> MigrateAction<ItemV3> {
MigrateAction::Update(ItemV3 {
value: v.value + 10,
})
}
fn add_hundred(_k: &[u8; 8], v: &ItemV3) -> MigrateAction<ItemV3> {
MigrateAction::Update(ItemV3 {
value: v.value + 100,
})
}
#[allow(clippy::type_complexity)]
let migrations: &[(u16, fn(&[u8; 8], &ItemV3) -> MigrateAction<ItemV3>)] =
&[(1, add_ten), (2, add_hundred)];
{
let db = Db::open_test(dir.path()).unwrap();
let tree = db
.open_typed_tree::<ItemV1, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
tree.put(&key(0), ItemV1 { value: 1 }).unwrap();
db.db_info
.update(|info| {
if let Some(ci) = info.collections.get_mut(ItemV1::NAME) {
ci.version = 2;
}
})
.expect("test persist");
db.close().unwrap();
}
{
let db = Db::open_test(dir.path()).unwrap();
let tree = db
.open_typed_tree::<ItemV3, RapiraCodec, _>(Config::test(), NoHook, migrations)
.unwrap();
assert_eq!(
tree.get(&key(0)).unwrap().value,
101,
"only V2→V3 step should run; expected 1 + 100 = 101"
);
}
}