Răsfoiți Sursa

Add migratability for relation types.

Kestrel 4 zile în urmă
părinte
comite
f0c59f36f0

+ 39 - 0
microrm-macros/src/entity.rs

@@ -32,6 +32,17 @@ fn is_key(attrs: &[syn::Attribute]) -> bool {
     attrs.iter().filter(|a| a.path.is_ident("key")).count() > 0
 }
 
+fn is_migratable(attrs: &[syn::Attribute]) -> Option<syn::Meta> {
+    attrs
+        .iter()
+        .filter(|a| a.path.is_ident("migratable"))
+        .map(|a| a.parse_meta())
+        .next()
+        .into_iter()
+        .flatten()
+        .next()
+}
+
 pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let input: syn::DeriveInput = syn::parse_macro_input!(tokens);
 
@@ -202,6 +213,32 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let id_ident = format_ident!("{}ID", entity_ident);
     let id_part_ident = format_ident!("{}IDPart", entity_ident);
 
+    let migrate_impl = match is_migratable(&input.attrs) {
+        Some(syn::Meta::List(mlist)) => {
+            assert_eq!(mlist.nested.len(), 1);
+            let syn::NestedMeta::Meta(syn::Meta::Path(mpath)) = &mlist.nested[0] else {
+                panic!()
+            };
+            let field_names = parts.iter().map(|v| &v.0);
+            let field_types = parts.iter().map(|v| &v.1);
+            quote! {
+                impl ::microrm::schema::migration::MigratableEntity< #mpath > for #entity_ident {
+                    fn migrate(from: & #mpath) -> ::microrm::DBResult<Option<Self>> {
+                        Ok(Some(
+                            Self {
+                                #(
+                                    #field_names : <#field_types as ::microrm::schema::migration::MigratableDatum<_>>::migrate_datum (&from . #field_names )?
+                                ),*
+                            }
+                        ))
+                    }
+                }
+            }
+        },
+        Some(m) => panic!("migrate attr must be given a path: {m:?}"),
+        None => quote! {},
+    };
+
     quote! {
         #(#part_defs)*
 
@@ -308,6 +345,8 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
                 );*
             }
         }
+
+        #migrate_impl
     }
     .into()
 }

+ 1 - 1
microrm-macros/src/lib.rs

@@ -56,7 +56,7 @@ mod value;
 ///     sin: String,
 /// }
 /// ```
-#[proc_macro_derive(Entity, attributes(unique, elide, key))]
+#[proc_macro_derive(Entity, attributes(unique, elide, key, migratable))]
 pub fn derive_entity(tokens: TokenStream) -> TokenStream {
     entity::derive(tokens)
 }

+ 0 - 2
microrm/src/query.rs

@@ -1,6 +1,4 @@
 //! Database query interface.
-//!
-//!
 
 use crate::{
     db::{ConnectionLease, StatementContext, Transaction},

+ 85 - 7
microrm/src/schema/migration.rs

@@ -4,27 +4,91 @@
 
 use std::marker::PhantomData;
 
+use crate::prelude::*;
 use crate::query::Insertable;
 use crate::schema::{
     self,
     build::{generate_from_schema, generate_single_entity_table, GeneratedSchema},
+    datum::Datum,
     entity::{Entity, EntityID, EntityPart, EntityPartList, EntityPartVisitor},
+    relation::{RelationDomain, RelationExt, RelationMap, RelationRange},
     DatabaseItem, Schema, SentinelDatabaseItem,
 };
-use crate::{ConnectionLease, DBResult, Error};
+use crate::{ConnectionLease, DBResult, Error, IDMap};
+
+pub trait MigratableDatum<From: Datum>: Datum {
+    fn migrate_datum(from: &From) -> DBResult<Self>
+    where
+        Self: Sized;
+}
+
+impl MigratableDatum<String> for String {
+    fn migrate_datum(from: &String) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        Ok(from.clone())
+    }
+}
+
+impl MigratableDatum<bool> for bool {
+    fn migrate_datum(from: &bool) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        Ok(from.clone())
+    }
+}
+
+impl<R0: Relation, R1: Relation> MigratableDatum<RelationDomain<R0>> for RelationDomain<R1> {
+    fn migrate_datum(_from: &RelationDomain<R0>) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        R0::try_coerce::<R1>()?;
+        Ok(RelationDomain::<R1>::default())
+    }
+}
+
+impl<R0: Relation, R1: Relation> MigratableDatum<RelationRange<R0>> for RelationRange<R1> {
+    fn migrate_datum(_from: &RelationRange<R0>) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        R0::try_coerce::<R1>()?;
+        Ok(RelationRange::<R1>::default())
+    }
+}
+
+impl<E0: Entity, E1: Entity> MigratableDatum<RelationMap<E0>> for RelationMap<E1> {
+    fn migrate_datum(_from: &RelationMap<E0>) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        if E0::entity_name() == E1::entity_name() {
+            Ok(RelationMap::default())
+        } else {
+            panic!(
+                "Cannot coerce a migration of a map between {} and {}",
+                std::any::type_name::<E0>(),
+                std::any::type_name::<E1>()
+            );
+        }
+    }
+}
 
 pub trait MigratableEntity<From: Entity>: 'static + Entity {
-    fn migrate(from: From) -> DBResult<Option<Self>>
+    fn migrate(from: &From) -> DBResult<Option<Self>>
     where
         Self: Sized;
 }
 
-impl<T: 'static + Entity> MigratableEntity<T> for T {
-    fn migrate(from: T) -> DBResult<Option<Self>>
+impl<T: 'static + Entity + Clone> MigratableEntity<T> for T {
+    fn migrate(from: &T) -> DBResult<Option<Self>>
     where
         Self: Sized,
     {
-        Ok(Some(from))
+        Ok(Some(from.clone()))
     }
 }
 
@@ -35,7 +99,6 @@ pub struct MigrationContext<'a, 'b> {
     in_progress: Vec<(&'static str, &'static str)>,
 }
 
-// impl<'a, 'b, 'c, From: DatabaseItem, Into: DatabaseItem> MigrationContext<'a, 'b, 'c, From
 impl<'a, 'b> MigrationContext<'a, 'b> {
     pub fn in_progress<E: Entity>(&mut self) -> DBResult<MigrateMap<E>> {
         // TODO: check that this is, like, part of the new schema...
@@ -53,6 +116,22 @@ impl<'a, 'b> MigrationContext<'a, 'b> {
         })
     }
 
+    pub fn migrate_entity<E0: Entity, E1: Entity>(&mut self) -> DBResult<MigrateMap<E1>>
+    where
+        E1: MigratableEntity<E0>,
+    {
+        let mm = self.in_progress::<E1>()?;
+
+        for e0 in IDMap::<E0>::default().get(self.lease)? {
+            let Some(e1) = <E1 as MigratableEntity<E0>>::migrate(e0.as_ref())? else {
+                continue;
+            };
+            mm.insert_matching(self.lease, e1, e0)?;
+        }
+
+        Ok(mm)
+    }
+
     pub fn lease(&mut self) -> &mut ConnectionLease<'b> {
         self.lease
     }
@@ -344,7 +423,6 @@ impl<OE: Entity> Default for MigrateMap<OE> {
 
 impl<OE: Entity> Insertable<OE> for MigrateMap<OE> {
     fn insert(&self, lease: &mut ConnectionLease, value: OE) -> DBResult<OE::ID> {
-        use crate::IDMap;
         let id = IDMap::<IPEntity<OE>>::insert(&IDMap::build(), lease, IPEntity(value))?;
         Ok(<OE::ID>::from_raw(id.0))
     }

+ 27 - 0
microrm/src/schema/relation.rs

@@ -30,6 +30,33 @@ pub trait Relation: 'static {
     const NAME: &'static str;
 }
 
+pub(crate) trait RelationExt: Relation {
+    fn try_coerce<Q: Relation>() -> DBResult<()>
+    where
+        Self: Sized,
+    {
+        if Self::NAME == Q::NAME
+            && Self::Domain::entity_name() == Q::Domain::entity_name()
+            && Self::Range::entity_name() == Q::Range::entity_name()
+            && Self::INJECTIVE == Q::INJECTIVE
+        {
+            Ok(())
+        } else {
+            panic!(
+                "Bad relation migration: types {} and {} have different semantics. Name: {}/{}, Domain: {}/{}, Range: {}/{}, injective: {}/{}",
+                std::any::type_name::<Self>(),
+                std::any::type_name::<Q>(),
+                Self::NAME, Q::NAME,
+                Self::Domain::entity_name(), Q::Domain::entity_name(),
+                Self::Range::entity_name(), Q::Range::entity_name(),
+                Self::INJECTIVE, Q::INJECTIVE
+            );
+        }
+    }
+}
+
+impl<R: Relation> RelationExt for R {}
+
 /// Enumeration used to represent which side of a relation a [`Relation`] trait implementation is representing.
 #[derive(Debug)]
 pub enum LocalSide {

+ 180 - 4
microrm/tests/migration.rs

@@ -85,14 +85,14 @@ impl MigratableItem<Schema1> for Schema2b {
     }
 }
 
-type Schemas = (Schema1, Schema2);
+type Schemas12 = (Schema1, Schema2);
 
 #[test]
 fn run_empty_migration() {
     let (pool, _db): (_, Schema2) = common::open_test_db!();
 
     let mut lease = pool.acquire().unwrap();
-    run_migration::<Schemas>(&mut lease).expect("migration result");
+    run_migration::<Schemas12>(&mut lease).expect("migration result");
 }
 
 #[test]
@@ -100,7 +100,7 @@ fn run_simple_migration() {
     let (pool, _db): (_, Schema1) = common::open_test_db!();
 
     let mut lease = pool.acquire().unwrap();
-    run_migration::<Schemas>(&mut lease).expect("migration result");
+    run_migration::<Schemas12>(&mut lease).expect("migration result");
 }
 
 #[test]
@@ -118,7 +118,7 @@ fn check_simple_migration() {
         )
         .expect("insert Schema1 kv");
 
-    run_migration::<Schemas>(&mut lease).expect("migration result");
+    run_migration::<Schemas12>(&mut lease).expect("migration result");
 
     let db2 = Schema2::default();
     let migrated = db2
@@ -153,3 +153,179 @@ fn check_incorrect_migration() {
         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(Default, Schema)]
+pub struct Schema4 {
+    pub category: microrm::IDMap<schema4::Category>,
+    pub book: microrm::IDMap<schema4::Book>,
+}
+
+#[derive(Default, 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(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 lease = pool.acquire().unwrap();
+
+    // add some very simple test data
+    let cat_a = db
+        .category
+        .insert_and_return(
+            &mut lease,
+            schema4::Category {
+                name: "A".to_string(),
+                books: Default::default(),
+            },
+        )
+        .expect("category A");
+    let cat_b = db
+        .category
+        .insert_and_return(
+            &mut lease,
+            schema4::Category {
+                name: "B".to_string(),
+                books: Default::default(),
+            },
+        )
+        .expect("category B");
+
+    cat_a
+        .books
+        .insert(
+            &mut lease,
+            schema4::Book {
+                title: "0".into(),
+                author: "a".into(),
+                category: Default::default(),
+            },
+        )
+        .expect("book 0");
+
+    cat_a
+        .books
+        .insert(
+            &mut lease,
+            schema4::Book {
+                title: "2".into(),
+                author: "b".into(),
+                category: Default::default(),
+            },
+        )
+        .expect("book 2");
+
+    cat_b
+        .books
+        .insert(
+            &mut lease,
+            schema4::Book {
+                title: "1".into(),
+                author: "c".into(),
+                category: Default::default(),
+            },
+        )
+        .expect("book 1");
+
+    // now perform the migration
+    run_migration::<(Schema4, Schema5)>(&mut lease).expect("migration");
+
+    // do some quick integrity checks
+    let ndb = Schema5::default();
+
+    let ncat_a = ndb
+        .category
+        .with(schema5::Category::Name, "A")
+        .first()
+        .get(&mut lease)
+        .expect("ndb get")
+        .expect("ndb get");
+    let ncat_b = ndb
+        .category
+        .with(schema5::Category::Name, "B")
+        .first()
+        .get(&mut lease)
+        .expect("ndb get")
+        .expect("ndb get");
+
+    assert_eq!(ncat_a.books.count(&mut lease).ok(), Some(2));
+    assert_eq!(ncat_b.books.count(&mut lease).ok(), Some(1));
+}