use test_log::test;
use microrm::prelude::*;
use microrm::schema::migration::*;
mod common;
mod schema1 {
use microrm::prelude::*;
#[derive(Entity)]
pub struct KVEntity {
#[key]
pub k: usize,
pub v: String,
}
}
mod schema2 {
use microrm::prelude::*;
#[derive(Entity)]
pub struct KVEntity {
#[key]
pub k: String,
pub v: String,
}
}
#[derive(Schema)]
pub struct Schema1 {
pub kv: microrm::IDMap<schema1::KVEntity>,
}
#[derive(Schema)]
pub struct Schema2 {
pub kv: microrm::IDMap<schema2::KVEntity>,
}
#[derive(Schema)]
pub struct Schema2b {
pub kv: microrm::IDMap<schema2::KVEntity>,
}
#[derive(Schema)]
pub struct Schema1i {
pub kv: microrm::IDMap<schema1::KVEntity>,
pub kidx: microrm::UniqueIndex<
schema1::KVEntity,
microrm_macros::index_cols![schema1::KVEntity::K, schema1::KVEntity::V],
>,
}
#[derive(Schema)]
pub struct Schema2i {
pub kv: microrm::IDMap<schema2::KVEntity>,
pub kidx: microrm::UniqueIndex<
schema2::KVEntity,
microrm_macros::index_cols![schema2::KVEntity::K, schema2::KVEntity::V],
>,
}
impl MigratableEntity<schema1::KVEntity> for schema2::KVEntity {
fn migrate(from: &schema1::KVEntity) -> microrm::DBResult<Option<Self>>
where
Self: Sized,
{
Ok(Some(schema2::KVEntity {
k: from.k.to_string(),
v: from.v.clone(),
}))
}
}
impl MigratableItem<Schema1> for Schema2 {
fn run_migration(old_schema: &Schema1, ctx: &mut MigrationContext) -> microrm::DBResult<()>
where
Self: Sized,
{
let ipkv2 = ctx.in_progress::<schema2::KVEntity>()?;
for kv in old_schema.kv.get(ctx.txn())? {
ipkv2.insert_matching(
ctx.txn(),
schema2::KVEntity {
k: kv.k.to_string(),
v: kv.v.clone(),
},
kv,
)?;
}
ipkv2.insert(
ctx.txn(),
schema2::KVEntity {
k: "s".to_string(),
v: "v".to_string(),
},
)?;
Ok(())
}
}
impl MigratableItem<Schema1> for Schema2b {
fn run_migration(_old: &Schema1, _ctx: &mut MigrationContext) -> microrm::DBResult<()>
where
Self: Sized,
{
Ok(())
}
}
impl MigratableItem<Schema1i> for Schema2i {
fn run_migration(_old: &Schema1i, ctx: &mut MigrationContext) -> microrm::DBResult<()>
where
Self: Sized,
{
ctx.migrate_entity::<schema1::KVEntity, schema2::KVEntity>()?;
Ok(())
}
}
type Schemas12 = (Schema1, Schema2);
#[test]
fn run_empty_migration() {
let (pool, _db): (_, Schema2) = common::open_test_db!();
run_migration::<Schemas12>(&pool).expect("migration result");
}
#[test]
fn run_simple_migration() {
let (pool, _db): (_, Schema1) = common::open_test_db!();
run_migration::<Schemas12>(&pool).expect("migration result");
}
#[test]
fn run_simple_migration_with_index() {
let (pool, _db): (_, Schema1i) = common::open_test_db!();
run_migration::<(Schema1i, Schema2i)>(&pool).expect("migration result");
}
#[test]
fn check_simple_migration() {
let (pool, db): (_, Schema1) = common::open_test_db!();
let mut txn = pool.start().unwrap();
db.kv
.insert(
&mut txn,
schema1::KVEntity {
k: 42,
v: "sixtimesnine".to_string(),
},
)
.expect("insert Schema1 kv");
txn.commit().unwrap();
let db2 = run_migration::<Schemas12>(&pool).expect("migration result");
let mut txn = pool.start().unwrap();
let migrated = db2
.kv
.with(schema2::KVEntity::K, "42")
.first()
.get(&mut txn)
.expect("get migrated entity")
.expect("no migrated entity");
assert_eq!(migrated.k, "42");
assert_eq!(migrated.v, "sixtimesnine");
let migrated = db2
.kv
.with(schema2::KVEntity::K, "s")
.first()
.get(&mut txn)
.expect("get migration-inserted entity")
.expect("no migration-inserted entity");
assert_eq!(migrated.k, "s");
assert_eq!(migrated.v, "v");
}
#[test]
fn check_incorrect_migration() {
let (pool, _db): (_, Schema1) = common::open_test_db!();
let Err(microrm::Error::ConsistencyError(_msg)) = run_migration::<(Schema1, Schema2b)>(&pool)
else {
panic!("incorrect migration succeeded");
};
}
mod schema4 {
use microrm::prelude::*;
pub struct BookCategory;
impl microrm::Relation for BookCategory {
type Domain = Category;
type Range = Book;
const NAME: &'static str = "book_category";
}
#[derive(Entity)]
pub struct Category {
#[key]
pub name: String,
pub books: microrm::RelationDomain<BookCategory>,
}
#[derive(Entity)]
pub struct Book {
pub title: String,
pub author: String,
pub category: microrm::RelationRange<BookCategory>,
}
}
mod schema5 {
use microrm::prelude::*;
pub struct BookCategory;
impl microrm::Relation for BookCategory {
type Domain = Category;
type Range = Book;
const NAME: &'static str = "book_category";
}
#[derive(Entity)]
#[migratable(super::schema4::Category)]
pub struct Category {
#[key]
pub name: String,
pub books: microrm::RelationDomain<BookCategory>,
}
#[derive(Entity)]
pub struct Book {
pub title: String,
pub author: String,
pub isbn: Option<String>,
pub category: microrm::RelationRange<super::schema4::BookCategory>,
}
}
#[derive(Schema)]
pub struct Schema4 {
pub category: microrm::IDMap<schema4::Category>,
pub book: microrm::IDMap<schema4::Book>,
}
#[derive(Schema)]
pub struct Schema5 {
pub category: microrm::IDMap<schema5::Category>,
pub book: microrm::IDMap<schema5::Book>,
}
impl MigratableEntity<schema4::Book> for schema5::Book {
fn migrate(from: &schema4::Book) -> microrm::DBResult<Option<schema5::Book>> {
Ok(Some(schema5::Book {
title: from.title.clone(),
author: from.author.clone(),
isbn: None,
category: Default::default(),
}))
}
}
impl MigratableItem<Schema4> for Schema5 {
fn run_migration(_old: &Schema4, ctx: &mut MigrationContext) -> microrm::DBResult<()> {
ctx.migrate_entity::<schema4::Category, schema5::Category>()?;
ctx.migrate_entity::<schema4::Book, schema5::Book>()?;
Ok(())
}
}
#[test]
fn check_relation_preservation() {
let (pool, db): (_, Schema4) = common::open_test_db!();
let mut txn = pool.start().unwrap();
let cat_a = db
.category
.insert_and_return(
&mut txn,
schema4::Category {
name: "A".to_string(),
books: Default::default(),
},
)
.expect("category A");
let cat_b = db
.category
.insert_and_return(
&mut txn,
schema4::Category {
name: "B".to_string(),
books: Default::default(),
},
)
.expect("category B");
cat_a
.books
.insert(
&mut txn,
schema4::Book {
title: "0".into(),
author: "a".into(),
category: Default::default(),
},
)
.expect("book 0");
cat_a
.books
.insert(
&mut txn,
schema4::Book {
title: "2".into(),
author: "b".into(),
category: Default::default(),
},
)
.expect("book 2");
cat_b
.books
.insert(
&mut txn,
schema4::Book {
title: "1".into(),
author: "c".into(),
category: Default::default(),
},
)
.expect("book 1");
txn.commit().unwrap();
let ndb = run_migration::<(Schema4, Schema5)>(&pool).expect("migration");
let mut txn = pool.start().unwrap();
let ncat_a = ndb
.category
.with(schema5::Category::Name, "A")
.first()
.get(&mut txn)
.expect("ndb get")
.expect("ndb get");
let ncat_b = ndb
.category
.with(schema5::Category::Name, "B")
.first()
.get(&mut txn)
.expect("ndb get")
.expect("ndb get");
assert_eq!(ncat_a.books.count(&mut txn).ok(), Some(2));
assert_eq!(ncat_b.books.count(&mut txn).ok(), Some(1));
txn.commit().unwrap();
}
#[derive(Entity)]
pub struct FKChild {
name: String,
}
#[derive(Entity)]
pub struct FKParent {
target: FKChildID,
}
#[derive(Schema)]
pub struct FKSchema {
pub child: microrm::IDMap<FKChild>,
pub parent: microrm::IDMap<FKParent>,
}
#[derive(Schema)]
pub struct FKSchema2 {
pub child: microrm::IDMap<FKChild>,
pub parent: microrm::IDMap<FKParent>,
pub parent_index: microrm::UniqueIndex<FKParent, microrm_macros::index_cols!(FKParent::target)>,
}
impl MigratableItem<FKSchema> for FKSchema2 {
fn run_migration(from: &FKSchema, ctx: &mut MigrationContext) -> microrm::DBResult<()>
where
Self: Sized,
{
log::info!("running deliberate fk failure migration...");
from.child.delete(ctx.txn()).unwrap();
Ok(())
}
}
#[test]
fn deliberate_fk_failure() {
let (pool, db): (_, FKSchema) = common::open_test_db!();
let mut txn = pool.start().unwrap();
let child_id = db
.child
.insert(
&mut txn,
FKChild {
name: "name".to_string(),
},
)
.unwrap();
let parent = db
.parent
.insert_and_return(&mut txn, FKParent { target: child_id })
.unwrap();
let _child = db.child.by_id(&mut txn, parent.target).unwrap();
txn.commit().unwrap();
let Err(microrm::Error::ConsistencyError(_msg)) = run_migration::<(FKSchema, FKSchema2)>(&pool)
else {
unreachable!("Migration that violates foreign key constraint succeeded!")
};
}
struct Check<F: microrm::schema::entity::Entity, T: microrm::schema::entity::Entity>(
std::marker::PhantomData<(F, T)>,
);
impl<F: microrm::schema::entity::Entity, T: microrm::schema::migration::MigratableEntity<F>>
Check<F, T>
{
fn assert() {}
}
#[test]
fn multiple_migrations() {
#[derive(Entity)]
#[migratable(B)]
struct A {
_ghost: bool,
}
#[derive(Entity)]
#[migratable(A, C)]
struct B {
_ghost: bool,
}
#[derive(Entity)]
#[migratable(B)]
struct C {
_ghost: bool,
}
Check::<A, B>::assert();
Check::<B, A>::assert();
Check::<B, C>::assert();
Check::<C, B>::assert();
}
#[test]
fn semantic_difference() {
#[derive(Entity)]
struct E {
value: bool,
}
#[derive(Schema)]
#[name = "schema1"]
struct S1 {
e: microrm::IDMap<E>,
}
#[derive(Schema)]
#[name = "schema2"]
struct S2 {
e: microrm::IDMap<E>,
}
impl MigratableItem<S1> for S2 {
fn run_migration(old_schema: &S1, ctx: &mut MigrationContext) -> microrm::DBResult<()> {
for mut e in old_schema.e.get(ctx.txn()).unwrap() {
e.value = !e.value;
e.sync(ctx.txn()).unwrap();
}
Ok(())
}
}
let (pool, db): (_, S1) = common::open_test_db!();
let mut txn = pool.start().unwrap();
db.e.insert(&mut txn, E { value: false }).unwrap();
txn.commit().unwrap();
let db = run_migration::<(S1, S2)>(&pool).unwrap();
let mut txn = pool.start().unwrap();
assert!(db.e.first().get(&mut txn).unwrap().unwrap().value);
}