Kestrel il y a 1 semaine
Parent
commit
e147485d64

+ 40 - 0
microrm-macros/src/grouping.rs

@@ -0,0 +1,40 @@
+use quote::quote;
+
+pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    let input: syn::DeriveInput = syn::parse_macro_input!(tokens);
+
+    let items = if let syn::Data::Struct(syn::DataStruct {
+        struct_token: _,
+        fields: syn::Fields::Named(fields),
+        semi_token: _,
+    }) = input.data
+    {
+        fields
+            .named
+            .into_iter()
+            .map(|f| (f.ident.unwrap(), f.ty))
+            .collect::<Vec<_>>()
+    } else {
+        panic!("Can only derive Schema on data structs with named fields!");
+    };
+
+    let item_ident = input.ident;
+
+    let visit_items = items.iter().map(|field| {
+        let item_type = &field.1;
+
+        quote! {
+            <#item_type as ::microrm::schema::DatabaseItem>::accept_item_visitor(&<#item_type as Default>::default(), v);
+        }
+    });
+
+    quote! {
+        impl ::microrm::schema::DatabaseItem for #item_ident {
+            fn accept_item_visitor(&self, v: &mut impl ::microrm::schema::DatabaseItemVisitor) {
+                use ::microrm::schema::DatabaseItem;
+                #(#visit_items)*
+            }
+        }
+    }
+    .into()
+}

+ 10 - 0
microrm-macros/src/lib.rs

@@ -3,6 +3,7 @@
 use proc_macro::TokenStream;
 
 mod entity;
+mod grouping;
 mod index;
 mod schema;
 mod value;
@@ -75,6 +76,15 @@ pub fn derive_schema(tokens: TokenStream) -> TokenStream {
     schema::derive(tokens)
 }
 
+/// `DatabaseItem` trait derivation procedural macro.
+///
+/// This macro provides a `DatabaseItem` implementation that forwards onto all members of the
+/// struct, intended for use as a logical grouping of other elements.
+#[proc_macro_derive(Grouping)]
+pub fn derive_grouping(tokens: TokenStream) -> TokenStream {
+    grouping::derive(tokens)
+}
+
 /// Index columns specification macro.
 ///
 /// This macro uses the type indirection set up in microrm to take a list of struct fields, such as

+ 2 - 0
microrm/src/lib.rs

@@ -286,6 +286,8 @@ pub enum Error {
     JSON(serde_json::Error),
     /// One of the mutexes failed in a spectacular way.
     LockError(String),
+    /// An empty database was found when an existing one was expected.
+    EmptyDatabase,
 }
 
 impl std::fmt::Display for Error {

+ 25 - 1
microrm/src/schema.rs

@@ -32,6 +32,8 @@ mod build;
 mod collect;
 pub(crate) mod meta;
 
+pub mod migration;
+
 // ----------------------------------------------------------------------
 // API types
 // ----------------------------------------------------------------------
@@ -295,6 +297,28 @@ pub trait DatabaseItem {
     fn accept_item_visitor(&self, visitor: &mut impl DatabaseItemVisitor);
 }
 
+/// A special sentinel DatabaseItem for DatabaseItemList.
+pub struct SentinelDatabaseItem;
+impl DatabaseItem for SentinelDatabaseItem {
+    fn accept_item_visitor(&self, _visitor: &mut impl DatabaseItemVisitor) {}
+}
+
+/// Representation of a list of DatabaseItems.
+pub trait DatabaseItemList {
+    /// Head of the list.
+    type Head: DatabaseItem;
+    /// The rest of the list.
+    type Tail: DatabaseItemList;
+    /// Whether this is an empty list or not.
+    const EMPTY: bool = false;
+}
+
+impl DatabaseItemList for () {
+    type Head = SentinelDatabaseItem;
+    type Tail = ();
+    const EMPTY: bool = true;
+}
+
 /// Visitor trait for iterating across the types in a [`Schema`] tree.
 pub trait DatabaseItemVisitor {
     /// Visit an `IDMap<T>` type.
@@ -308,7 +332,7 @@ pub trait DatabaseItemVisitor {
 }
 
 /// A root structure for the database specification graph.
-pub trait Schema {
+pub trait Schema: 'static + Default {
     /// Install this schema into a database
     fn install(&self, lease: &mut ConnectionLease) -> DBResult<()>
     where

+ 12 - 3
microrm/src/schema/datum/datum_list.rs

@@ -9,7 +9,10 @@ impl DatumList for () {
     type ListHead = bool;
     type ListTail = ();
 
-    type RefList<'l> = () where Self: 'l;
+    type RefList<'l>
+        = ()
+    where
+        Self: 'l;
 
     fn list_head(&self) -> &Self::ListHead {
         unreachable!()
@@ -31,7 +34,10 @@ impl QueryEquivalentList<()> for () {}
 impl<T: Datum> DatumList for T {
     type ListHead = T;
     type ListTail = ();
-    type RefList<'l> = () where Self: 'l;
+    type RefList<'l>
+        = ()
+    where
+        Self: 'l;
 
     fn list_head(&self) -> &Self::ListHead {
         self
@@ -56,7 +62,10 @@ impl<T: ConcreteDatum, E: QueryEquivalent<T>> QueryEquivalentList<T> for E {}
 impl<T0: Datum> DatumList for (T0,) {
     type ListHead = T0;
     type ListTail = ();
-    type RefList<'l> = (&'l T0,) where Self: 'l;
+    type RefList<'l>
+        = (&'l T0,)
+    where
+        Self: 'l;
 
     fn list_head(&self) -> &Self::ListHead {
         &self.0

+ 155 - 0
microrm/src/schema/migration.rs

@@ -0,0 +1,155 @@
+#![allow(missing_docs)]
+
+use crate::schema::{
+    self, build::collect_from_database, entity::Entity, DatabaseItem, DatabaseItemVisitor, Schema,
+};
+use crate::{ConnectionLease, DBResult, Error};
+
+pub trait MigratableEntity<From: Entity>: 'static + Entity {
+    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>>
+    where
+        Self: Sized,
+    {
+        Ok(Some(from))
+    }
+}
+
+pub trait MigratableItem<From: DatabaseItem>: 'static + DatabaseItem {
+    fn run_migration(lease: &mut ConnectionLease) -> DBResult<()>;
+}
+
+/*
+pub trait MigratableSchema<From: Schema>: 'static + Schema {
+    fn run_migration(lease: &mut ConnectionLease) -> DBResult<()> {
+        let i = Self::default();
+
+        struct Visitor;
+        impl DatabaseItemVisitor for Visitor {
+            fn visit_idmap<T: Entity>(&mut self)
+            where
+                Self: Sized,
+            {
+                todo!()
+            }
+            fn visit_index<T: Entity, PL: schema::entity::EntityPartList<Entity = T>>(&mut self)
+            where
+                Self: Sized,
+            {
+                todo!()
+            }
+        }
+
+        let mut v = Visitor;
+        i.accept_item_visitor(&mut v);
+        todo!()
+    }
+}
+
+/// identity implementations
+// impl<T: 'static + Schema> MigratableSchema<T> for T { }
+
+*/
+
+#[derive(Default, microrm_macros::Schema)]
+pub struct EmptySchema {}
+
+impl MigratableItem<EmptySchema> for EmptySchema {
+    fn run_migration(_lease: &mut ConnectionLease) -> DBResult<()> { unreachable!() }
+}
+
+impl<T: 'static + DatabaseItem> MigratableItem<()> for T {
+    fn run_migration(_lease: &mut ConnectionLease) -> DBResult<()> { unreachable!() }
+}
+
+impl Schema for () {
+    fn accept_item_visitor(&self, _visitor: &mut impl schema::DatabaseItemVisitor)
+    where
+        Self: Sized,
+    {
+    }
+}
+
+pub trait SchemaList {
+    type Head: Schema + MigratableItem<<Self::Tail as SchemaList>::Head>;
+    type Tail: SchemaList;
+    const EMPTY: bool = false;
+}
+
+impl SchemaList for () {
+    type Head = ();
+    type Tail = ();
+    const EMPTY: bool = true;
+}
+
+impl<S0: Schema> SchemaList for (S0,)
+where
+    S0: MigratableItem<()>,
+{
+    type Head = S0;
+    type Tail = ();
+}
+
+impl<S0: Schema, S1: Schema> SchemaList for (S0, S1)
+where
+    S1: MigratableItem<S0>,
+{
+    type Head = S1;
+    type Tail = (S0,);
+}
+
+impl<S0: Schema, S1: Schema, S2: Schema> SchemaList for (S0, S1, S2)
+where
+    S1: MigratableItem<S0>,
+    S2: MigratableItem<S1>,
+{
+    type Head = S2;
+    type Tail = (S0, S1);
+}
+
+impl<S0: Schema, S1: Schema, S2: Schema, S3: Schema> SchemaList for (S0, S1, S2, S3)
+where
+    S1: MigratableItem<S0>,
+    S2: MigratableItem<S1>,
+    S3: MigratableItem<S2>,
+{
+    type Head = S3;
+    type Tail = (S0, S1, S2);
+}
+
+fn migration_helper<A: SchemaList>(lease: &mut ConnectionLease) -> DBResult<()> {
+    if A::EMPTY {
+        return Err(Error::IncompatibleSchema);
+    }
+
+    let built = collect_from_database(&<A::Head>::default());
+    match built.check(lease) {
+        Some(true) => Ok(()),
+        Some(false) => {
+            migration_helper::<A::Tail>(lease)?;
+
+            // migrate from (A::Tail::Head) to A::Head
+            type MigrateTo<A> = <A as SchemaList>::Head;
+            type MigrateFrom<A> = <<A as SchemaList>::Tail as SchemaList>::Head;
+            <MigrateTo<A> as MigratableItem<MigrateFrom<A>>>::run_migration(lease)
+        },
+        None => Err(Error::EmptyDatabase),
+    }
+}
+
+pub fn run_migration<A: SchemaList>(lease: &mut ConnectionLease) -> DBResult<()> {
+    // let metadb = meta::MetadataDB::default();
+
+    let _ = migration_helper::<A>(lease)?;
+
+    // easy case: see if the head is the most recent
+
+    // super::build::collect_from_database(
+    // metadb.metastore.get(
+    Ok(())
+}

+ 47 - 0
microrm/tests/migration.rs

@@ -0,0 +1,47 @@
+use test_log::test;
+
+use microrm::prelude::*;
+use microrm::schema::migration::*;
+
+mod common;
+
+#[derive(Entity)]
+struct KVEntity1 {
+    k: usize,
+    v: String,
+}
+
+#[derive(Default, Schema)]
+struct Schema1 {
+    kv: microrm::IDMap<KVEntity1>,
+}
+
+#[derive(Entity)]
+struct KVEntity2 {
+    k: String,
+    v: String,
+}
+
+#[derive(Default, Schema)]
+struct Schema2 {
+    kv: microrm::IDMap<KVEntity2>,
+}
+
+impl MigratableEntity<KVEntity1> for KVEntity2 {
+    fn migrate(from: KVEntity1) -> microrm::DBResult<Option<Self>>
+    where
+        Self: Sized,
+    {
+        todo!()
+    }
+}
+
+impl MigratableSchema<Schema1> for Schema2 {}
+
+#[test]
+fn run_empty_migration() {
+    let (pool, _db): (_, Schema1) = common::open_test_db!();
+
+    let mut lease = pool.acquire().unwrap();
+    run_migration::<(Schema1, Schema2)>(&mut lease).expect("migration result");
+}