Browse Source

Add index specification interface and schema generation.

Kestrel 7 months ago
parent
commit
084ad066b8

+ 3 - 15
microrm-macros/src/database.rs

@@ -1,5 +1,4 @@
-use convert_case::{Case, Casing};
-use quote::{format_ident, quote};
+use quote::{quote, ToTokens};
 
 fn type_to_expression_context_type(ty: &syn::Type) -> proc_macro2::TokenStream {
     fn handle_path_segment(seg: &syn::PathSegment) -> proc_macro2::TokenStream {
@@ -18,7 +17,7 @@ fn type_to_expression_context_type(ty: &syn::Type) -> proc_macro2::TokenStream {
                 #(#new_segments)::*
             }
         }
-        _ => todo!(),
+        v => v.into_token_stream(),
     }
 }
 
@@ -43,21 +42,10 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let db_ident = input.ident;
 
     let visit_items = items.iter().map(|field| {
-        let item_combined_name = format_ident!(
-            "{}{}ItemType",
-            db_ident,
-            field.0.to_string().to_case(Case::UpperCamel)
-        );
         let item_type = &field.1;
 
         quote! {
-            struct #item_combined_name;
-            impl ::microrm::schema::DatabaseItem for #item_combined_name {
-                fn accept_entity_visitor(visitor: &mut impl ::microrm::schema::entity::EntityVisitor) {
-                    <#item_type as ::microrm::schema::DatabaseSpec>::accept_entity_visitor(visitor);
-                }
-            }
-            v.visit::<#item_combined_name>();
+            <#item_type as ::microrm::schema::DatabaseItem>::accept_item_visitor(v);
         }
     });
 

+ 16 - 1
microrm-macros/src/entity.rs

@@ -79,7 +79,7 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
         .cloned()
         .collect::<Vec<_>>();
 
-    let part_defs = parts.iter().map(|part| {
+    let part_defs = parts.iter().enumerate().map(|(index, part)| {
         let part_combined_name = make_combined_name(part);
         let part_base_ident = &part.0;
         let part_base_name = &part.0.to_string();
@@ -109,6 +109,11 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
                     &from.#part_base_ident
                 }
             }
+
+            impl ::microrm::schema::index::IndexedEntityPart<#index> for #entity_ident {
+                type Entity = #entity_ident;
+                type Part = #part_combined_name;
+            }
         }
     });
 
@@ -143,6 +148,15 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
         }
     });
 
+    let part_indices = parts.iter().enumerate().map(|(i, part)| {
+        let part_index_name =
+            format_ident!("_{}_INDEX", part.0.to_string().to_case(Case::UpperSnake));
+        quote! {
+            #[doc(hidden)]
+            pub const #part_index_name : usize = #i;
+        }
+    });
+
     let build_struct = parts
         .iter()
         .enumerate()
@@ -190,6 +204,7 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
 
         impl #entity_ident {
             #(#part_names)*
+            #(#part_indices)*
         }
 
         #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]

+ 33 - 0
microrm-macros/src/index.rs

@@ -0,0 +1,33 @@
+use convert_case::{Case, Casing};
+use proc_macro::TokenStream;
+use quote::{format_ident, quote, ToTokens};
+
+pub fn index_cols(tokens: TokenStream) -> TokenStream {
+    type MT = syn::punctuated::Punctuated<syn::ExprPath, syn::token::Comma>;
+    let paths = syn::parse_macro_input!(tokens with MT::parse_terminated);
+
+    let mut part_list = proc_macro2::TokenStream::new();
+
+    let entity_path = {
+        let mut epath = paths.first().unwrap().clone();
+        epath.path.segments.pop();
+        let v = epath.path.segments.pop().unwrap().into_value();
+        epath.path.segments.push(v);
+        epath
+    };
+
+    for mut path in paths.into_iter() {
+        let last_seg = path.path.segments.last_mut().unwrap();
+        last_seg.ident = format_ident!(
+            "_{}_INDEX",
+            last_seg.ident.to_string().to_case(Case::UpperSnake)
+        );
+
+        quote! { ::microrm::schema::index::IndexSignifier< { #path } > , }
+            .to_tokens(&mut part_list);
+    }
+
+    let out = quote! { <#entity_path as ::microrm::schema::index::IndexPartList<#entity_path,(#part_list)>>::PartList }.into();
+
+    out
+}

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

@@ -4,6 +4,7 @@ use proc_macro::TokenStream;
 
 mod database;
 mod entity;
+mod index;
 
 /// `Entity` trait derivation procedural macro.
 ///
@@ -72,3 +73,8 @@ pub fn derive_entity(tokens: TokenStream) -> TokenStream {
 pub fn derive_database(tokens: TokenStream) -> TokenStream {
     database::derive(tokens)
 }
+
+#[proc_macro]
+pub fn index_cols(tokens: TokenStream) -> TokenStream {
+    index::index_cols(tokens)
+}

+ 12 - 20
microrm/src/schema.rs

@@ -30,6 +30,9 @@ pub mod entity;
 /// Types related to inter-entity relationships.
 pub mod relation;
 
+/// Types related to indexes.
+pub mod index;
+
 mod build;
 mod collect;
 pub(crate) mod meta;
@@ -260,37 +263,26 @@ impl<T: Entity> Insertable<T> for IDMap<T> {
     }
 }
 
-/// Search index definition.
-pub struct Index<T: Entity, Keys: EntityPartList> {
-    _conn: Connection,
-    _ghost: std::marker::PhantomData<(T, Keys)>,
+impl<E: Entity> DatabaseItem for IDMap<E> {
+    fn accept_item_visitor(visitor: &mut impl DatabaseItemVisitor) {
+        visitor.visit_idmap::<E>();
+    }
 }
 
 /// Represents a single root-level table or index in a database.
 pub trait DatabaseItem {
     /// Accept an entity visitor for entity discovery.
-    fn accept_entity_visitor(visitor: &mut impl EntityVisitor);
+    fn accept_item_visitor(visitor: &mut impl DatabaseItemVisitor);
 }
 
 /// Visitor trait for iterating across the types in a [`DatabaseSpec`].
 pub trait DatabaseItemVisitor {
-    ///
-    fn visit<DI: DatabaseItem>(&mut self)
+    fn visit_idmap<T: Entity>(&mut self)
+    where
+        Self: Sized;
+    fn visit_index<T: Entity, PL: EntityPartList<Entity = T>>(&mut self)
     where
         Self: Sized;
-}
-
-/// Trait representing a type that can be used as a field in a type implementing [`Database`] via
-/// the derivation macro.
-pub trait DatabaseSpec {
-    /// Accept an entity visitor.
-    fn accept_entity_visitor(visitor: &mut impl EntityVisitor);
-}
-
-impl<T: Entity> DatabaseSpec for IDMap<T> {
-    fn accept_entity_visitor(visitor: &mut impl EntityVisitor) {
-        visitor.visit::<T>()
-    }
 }
 
 /// A root structure for the database specification graph.

+ 52 - 6
microrm/src/schema/build.rs

@@ -2,7 +2,8 @@ use crate::{
     prelude::*,
     schema::{
         collect::{EntityStateContainer, PartType},
-        meta, Connection, Database, DatabaseItem, DatabaseItemVisitor,
+        entity::{Entity, EntityPart, EntityPartList, EntityPartVisitor, EntityVisitor},
+        meta, Connection, DatabaseItemVisitor,
     },
     DBResult,
 };
@@ -60,6 +61,26 @@ impl TableInfo {
     }
 }
 
+#[derive(Debug, Hash)]
+struct IndexInfo {
+    table_name: String,
+    columns: Vec<String>,
+    unique: bool,
+}
+
+impl IndexInfo {
+    fn build_creation_query(&self) -> String {
+        let index_name = format!("index_{}_{}", self.table_name, self.columns.iter().cloned().reduce(|a,b| format!("{a}_{b}")).unwrap());
+
+        format!(
+            "create {unique} index `{index_name}` on `{table_name}`({cols})",
+            table_name = self.table_name,
+            unique = if self.unique { "unique" } else { "" },
+            cols = self.columns.join(",")
+        )
+    }
+}
+
 pub(crate) struct DatabaseSchema {
     signature: u64,
     queries: Vec<String>,
@@ -113,18 +134,40 @@ impl DatabaseSchema {
 }
 
 pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
-    struct IV(EntityStateContainer);
+    struct IV(EntityStateContainer, Vec<IndexInfo>);
 
     impl DatabaseItemVisitor for IV {
-        fn visit<DI: DatabaseItem>(&mut self)
+        fn visit_idmap<E: Entity>(&mut self)
+        where
+            Self: Sized,
+        {
+            self.0.visit::<E>();
+        }
+
+        fn visit_index<T: Entity, PL: EntityPartList<Entity = T>>(&mut self)
         where
             Self: Sized,
         {
-            DI::accept_entity_visitor(&mut self.0.make_context());
+            struct PV<E: Entity>(Vec<String>, std::marker::PhantomData<E>);
+            impl<E: Entity> EntityPartVisitor for PV<E> {
+                type Entity = E;
+
+                fn visit<EP: EntityPart<Entity = Self::Entity>>(&mut self) {
+                    self.0.push(EP::part_name().to_string());
+                }
+            }
+            let mut pv = PV::<T>(Default::default(), Default::default());
+            PL::accept_part_visitor(&mut pv);
+
+            self.1.push(IndexInfo {
+                table_name: T::entity_name().to_string(),
+                columns: pv.0,
+                unique: true,
+            });
         }
     }
 
-    let mut iv = IV(EntityStateContainer::default());
+    let mut iv = IV(EntityStateContainer::default(), Default::default());
 
     DB::accept_item_visitor(&mut iv);
 
@@ -239,7 +282,10 @@ pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
         queries.push(create_sql);
     }
 
-    // TODO: generate index schemas here
+    for iinfo in iv.1.into_iter() {
+        iinfo.hash(&mut signature_hasher);
+        queries.push(iinfo.build_creation_query());
+    }
 
     DatabaseSchema {
         signature: signature_hasher.finish(),

+ 7 - 15
microrm/src/schema/collect.rs

@@ -166,27 +166,19 @@ impl EntityStateContainer {
     pub fn iter_states(&self) -> impl Iterator<Item = &EntityState> {
         self.states.values()
     }
-
-    pub fn make_context(&mut self) -> EntityContext {
-        EntityContext { container: self }
-    }
-}
-
-pub struct EntityContext<'a> {
-    container: &'a mut EntityStateContainer,
 }
 
-impl<'a> EntityVisitor for EntityContext<'a> {
+impl EntityVisitor for EntityStateContainer {
     fn visit<E: Entity>(&mut self) {
-        // three cases:
+        // cases:
         // 1. we haven't seen this entity
         // 2. we've seen this entity before
 
-        if self.container.states.contains_key(E::entity_name()) {
+        if self.states.contains_key(E::entity_name()) {
             return;
         }
 
-        let entry = self.container.states.entry(E::entity_name());
+        let entry = self.states.entry(E::entity_name());
 
         let entry = entry.or_insert_with(EntityState::build::<E>);
         // sanity-check
@@ -194,11 +186,11 @@ impl<'a> EntityVisitor for EntityContext<'a> {
             panic!("Identical entity name but different typeid!");
         }
 
-        struct RecursiveVisitor<'a, 'b, E: Entity>(
-            &'a mut EntityContext<'b>,
+        struct RecursiveVisitor<'a, E: Entity>(
+            &'a mut EntityStateContainer,
             std::marker::PhantomData<E>,
         );
-        impl<'a, 'b, E: Entity> EntityPartVisitor for RecursiveVisitor<'a, 'b, E> {
+        impl<'a, E: Entity> EntityPartVisitor for RecursiveVisitor<'a, E> {
             type Entity = E;
             fn visit<EP: EntityPart>(&mut self) {
                 EP::Datum::accept_entity_visitor(self.0);

+ 58 - 0
microrm/src/schema/index.rs

@@ -0,0 +1,58 @@
+#![allow(missing_docs)]
+
+use crate::db::Connection;
+
+use super::entity::{Entity, EntityPart, EntityPartList};
+
+/// Trait used to get entity part types by index, used for index schema generation.
+pub trait IndexedEntityPart<const N: usize> {
+    /// What entity is this part for?
+    type Entity: Entity;
+    /// The actual target part
+    type Part: EntityPart<Entity = Self::Entity>;
+}
+
+pub struct IndexSignifier<const N: usize>;
+
+pub trait IndexPartList<E: Entity, II> {
+    type PartList: EntityPartList<Entity = E>;
+}
+
+pub struct Index<E: Entity, EPL: EntityPartList<Entity = E>>(std::marker::PhantomData<(E, EPL)>);
+
+impl<E: Entity, EPL: EntityPartList<Entity = E>> Index<E, EPL> {
+    pub fn build(_conn: Connection) -> Self {
+        Self(std::marker::PhantomData)
+    }
+}
+
+impl<E: Entity, EPL: EntityPartList<Entity = E>> super::DatabaseItem for Index<E, EPL> {
+    fn accept_item_visitor(visitor: &mut impl super::DatabaseItemVisitor) {
+        visitor.visit_index::<E, EPL>();
+    }
+}
+
+macro_rules! entity_index {
+    ($($is:ident : $n:tt),+) => {
+        impl<E: Entity $( + IndexedEntityPart<$is, Entity = E> )*, $( const $is: usize ),*> IndexPartList<E, ( $( IndexSignifier<$is> ),*, )> for E {
+            type PartList = ( $( <E as IndexedEntityPart<$is>>::Part ),* ,);
+        }
+    }
+}
+
+entity_index!(N0:0);
+entity_index!(N0:0, N1:1);
+entity_index!(N0:0, N1:1, N2:2);
+entity_index!(N0:0, N1:1, N2:2, N3:3);
+entity_index!(N0:0, N1:1, N2:2, N3:3, N4:4);
+entity_index!(N0:0, N1:1, N2:2, N3:3, N4:4, N5:5);
+entity_index!(N0:0, N1:1, N2:2, N3:3, N4:4, N5:5, N6:6);
+entity_index!(N0:0, N1:1, N2:2, N3:3, N4:4, N5:5, N6:6, N7:7);
+entity_index!(N0:0, N1:1, N2:2, N3:3, N4:4, N5:5, N6:6, N7:7, N8:8);
+entity_index!(N0:0, N1:1, N2:2, N3:3, N4:4, N5:5, N6:6, N7:7, N8:8, N9:9);
+entity_index!(N0:0, N1:1, N2:2, N3:3, N4:4, N5:5, N6:6, N7:7, N8:8, N9:9, N10:10);
+entity_index!(N0:0, N1:1, N2:2, N3:3, N4:4, N5:5, N6:6, N7:7, N8:8, N9:9, N10:10, N11:11);
+entity_index!(N0:0, N1:1, N2:2, N3:3, N4:4, N5:5, N6:6, N7:7, N8:8, N9:9, N10:10, N11:11, N12:12);
+entity_index!(N0:0, N1:1, N2:2, N3:3, N4:4, N5:5, N6:6, N7:7, N8:8, N9:9, N10:10, N11:11, N12:12, N13:13);
+entity_index!(N0:0, N1:1, N2:2, N3:3, N4:4, N5:5, N6:6, N7:7, N8:8, N9:9, N10:10, N11:11, N12:12, N13:13, N14:14);
+entity_index!(N0:0, N1:1, N2:2, N3:3, N4:4, N5:5, N6:6, N7:7, N8:8, N9:9, N10:10, N11:11, N12:12, N13:13, N14:14, N15:15);

+ 29 - 9
microrm/src/schema/tests.rs

@@ -143,14 +143,7 @@ mod manual_test_db {
         where
             Self: Sized,
         {
-            struct SimpleDatabaseStringsItem;
-            impl DatabaseItem for SimpleDatabaseStringsItem {
-                fn accept_entity_visitor(visitor: &mut impl EntityVisitor) {
-                    visitor.visit::<SimpleEntity>();
-                }
-            }
-
-            visitor.visit::<SimpleDatabaseStringsItem>();
+            <IDMap<SimpleEntity> as DatabaseItem>::accept_item_visitor(visitor);
         }
     }
 
@@ -885,7 +878,7 @@ mod injective_test {
             .expect("couldn't connect a2 and b2");
 
         // we can't claim that Homer wrote the Aeneid because it's an injective relationship and
-        // only one Author can claim the Book as their work
+        // in this model, only one Author can claim the Book as their work
         match a1.works.connect_to(b2_id) {
             Err(microrm::Error::ConstraintViolation(_)) => {
                 // all good
@@ -899,3 +892,30 @@ mod injective_test {
         }
     }
 }
+
+mod index_test {
+    use microrm::prelude::*;
+    use microrm_macros::index_cols;
+    use test_log::test;
+
+    use microrm::schema::index::Index;
+
+    #[derive(Entity)]
+    struct TwoWay {
+        up: String,
+        left: String,
+        right: String,
+    }
+
+    #[derive(Database)]
+    struct TWDB {
+        entries: microrm::IDMap<TwoWay>,
+        // left_index: Index<TwoWay, index_cols![TwoWay::left]>,
+        right_index: Index<TwoWay, index_cols![TwoWay::right]>,
+    }
+
+    #[test]
+    fn db_construction() {
+        let db = TWDB::open_path(":memory:").expect("couldn't open in-memory db");
+    }
+}