Browse Source

Add semi-automatic unique index creation.

Kestrel 1 year ago
parent
commit
9ffeea33f8

+ 1 - 1
microrm-macros/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 [package]
 name = "microrm-macros"
 name = "microrm-macros"
-version = "0.2.5"
+version = "0.2.6"
 edition = "2021"
 edition = "2021"
 license = "BSD-4-Clause"
 license = "BSD-4-Clause"
 authors = ["Kestrel <kestrel@flying-kestrel.ca>"]
 authors = ["Kestrel <kestrel@flying-kestrel.ca>"]

+ 34 - 32
microrm-macros/src/entity.rs

@@ -4,6 +4,8 @@ use syn::{parse_macro_input, DeriveInput};
 
 
 use convert_case::{Case, Casing};
 use convert_case::{Case, Casing};
 
 
+use crate::index::generate_index_code;
+
 fn parse_fk(attrs: &[syn::Attribute]) -> bool {
 fn parse_fk(attrs: &[syn::Attribute]) -> bool {
     for attr in attrs {
     for attr in attrs {
         if attr.path.segments.len() == 1
         if attr.path.segments.len() == 1
@@ -170,8 +172,8 @@ pub(crate) fn derive(tokens: TokenStream) -> TokenStream {
     let mut value_references = Vec::new();
     let mut value_references = Vec::new();
     let mut build_clauses = Vec::new();
     let mut build_clauses = Vec::new();
 
 
-    // let mut foreign_keys = Vec::new();
-    // let mut foreign_key_impls = Vec::new();
+    // let mut constraints = Vec::new();
+    let mut unique_columns = Vec::new();
 
 
     let mut index: usize = 0;
     let mut index: usize = 0;
 
 
@@ -198,31 +200,9 @@ pub(crate) fn derive(tokens: TokenStream) -> TokenStream {
         index += 1;
         index += 1;
         build_clauses.push(quote! { #field_name: <#ty as #microrm_ref::model::Modelable>::build_from(stmt, #index)?.0 });
         build_clauses.push(quote! { #field_name: <#ty as #microrm_ref::model::Modelable>::build_from(stmt, #index)?.0 });
 
 
-        /*
-        if parse_fk(&name.attrs) {
-            let fk_struct_name = format_ident!("{}{}ForeignKey", struct_name, converted_case);
-            let ty = &name.ty;
-            foreign_keys.push(quote! {
-                &#fk_struct_name { col: #columns_name::#converted_case }
-            });
-            foreign_key_impls.push(quote!{
-                struct #fk_struct_name {
-                    col: #columns_name
-                }
-                impl #microrm_ref::entity::EntityForeignKey<#columns_name> for #fk_struct_name {
-                    /*
-                    fn local_column(&self) -> &#columns_name { &self.col }
-                    fn foreign_table_name(&self) -> &'static str {
-                        <<#ty as #microrm_ref::entity::EntityID>::Entity as #microrm_ref::entity::Entity>::table_name()
-                    }
-                    fn foreign_column_name(&self) -> &'static str {
-                        "id"
-                    }
-                    */
-                }
-            });
+        if name.attrs.iter().filter(|a| a.path.is_ident("microrm_unique")).count() > 0 {
+            unique_columns.push(original_case);
         }
         }
-        */
     }
     }
 
 
     let columns_array_name = format_ident!(
     let columns_array_name = format_ident!(
@@ -233,12 +213,37 @@ pub(crate) fn derive(tokens: TokenStream) -> TokenStream {
     let field_count = fields.named.iter().count();
     let field_count = fields.named.iter().count();
 
 
     let columns_name = format_ident!("_{}_columns", &input.ident.to_string().to_case(Case::Snake));
     let columns_name = format_ident!("_{}_columns", &input.ident.to_string().to_case(Case::Snake));
+    let constraints_mod = format_ident!("_{}_constraints", &input.ident.to_string().to_case(Case::Snake));
+
+    let (constraints, constraint_visitor) = if unique_columns.len() > 0 {
+        let instance_paths = unique_columns.iter().map(|id| {
+            let mut punc = syn::punctuated::Punctuated::new();
+            punc.push(format_ident!("super").into());
+            punc.push(format_ident!("{}", struct_name).into());
+            punc.push(format_ident!("{}", id.to_string().to_case(Case::UpperCamel)).into());
+            syn::ExprPath { attrs: vec![], qself: None, path: syn::Path { leading_colon: None, segments: punc } }
+        });
+
+        let index_ident = format_ident!("{}UniqueIndex", struct_name);
+        let index_impl = generate_index_code(index_ident.clone(), true, instance_paths, microrm_ref.clone());
+        (quote! {
+            pub mod #constraints_mod {
+                #index_impl
+            }
+        }, quote!{
+            cv.index::<#constraints_mod::#index_ident>();
+        })
+    }
+    else {
+        (quote! { }, quote!{ })
+    };
 
 
     quote!{
     quote!{
         // Related types for #struct_name
         // Related types for #struct_name
 
 
         #column_output
         #column_output
         #id_output
         #id_output
+        #constraints
 
 
         // Implementations for #struct_name
         // Implementations for #struct_name
         impl #microrm_ref::entity::Entity for #struct_name {
         impl #microrm_ref::entity::Entity for #struct_name {
@@ -270,12 +275,9 @@ pub(crate) fn derive(tokens: TokenStream) -> TokenStream {
                 Self::ID
                 Self::ID
             }
             }
 
 
-            /*fn foreign_keys() -> &'static [&'static dyn #microrm_ref::entity::EntityForeignKey<Self::Column>] {
-                &[#(#foreign_keys),*]
-            }*/
+            fn constraints<CV: #microrm_ref::entity::ConstraintVisitor>(cv: &mut CV) {
+                #constraint_visitor
+            }
         }
         }
-
-        // Foreign key struct implementations
-        // #(#foreign_key_impls)*
     }.into()
     }.into()
 }
 }

+ 13 - 12
microrm-macros/src/index.rs

@@ -24,18 +24,11 @@ impl syn::parse::Parse for MakeIndexParams {
     }
     }
 }
 }
 
 
-pub(crate) fn do_make_index(
-    tokens: TokenStream,
-    microrm_ref: proc_macro2::TokenStream,
-) -> TokenStream {
-    let input = parse_macro_input!(tokens as MakeIndexParams);
-
-    let index_struct_name = input.name;
+pub(crate) fn generate_index_code(name: syn::Ident, unique: bool, columns: impl Iterator<Item = syn::ExprPath> + Clone, microrm_ref: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
+    let index_struct_name = name;
     let index_sql_name = format!("{}", index_struct_name).to_case(Case::Snake);
     let index_sql_name = format!("{}", index_struct_name).to_case(Case::Snake);
 
 
-    let columns = input.columns.clone().into_iter();
-
-    let unique = input.unique.is_some();
+    let columns = columns.clone().into_iter();
 
 
     let first_column = columns.clone().next().unwrap();
     let first_column = columns.clone().next().unwrap();
 
 
@@ -43,7 +36,7 @@ pub(crate) fn do_make_index(
         "INDEX_COLUMN_NAMES_{}",
         "INDEX_COLUMN_NAMES_{}",
         index_struct_name.to_string().to_case(Case::ScreamingSnake)
         index_struct_name.to_string().to_case(Case::ScreamingSnake)
     );
     );
-    let column_count = columns.len();
+    let column_count = columns.clone().count();
 
 
     quote! {
     quote! {
         pub struct #index_struct_name ();
         pub struct #index_struct_name ();
@@ -73,5 +66,13 @@ pub(crate) fn do_make_index(
             }
             }
         }
         }
     }
     }
-    .into()
+}
+
+pub(crate) fn do_make_index(
+    tokens: TokenStream,
+    microrm_ref: proc_macro2::TokenStream,
+) -> TokenStream {
+    let input = parse_macro_input!(tokens as MakeIndexParams);
+
+    generate_index_code(input.name, input.unique.is_some(), input.columns.into_iter(), microrm_ref).into()
 }
 }

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

@@ -37,7 +37,8 @@ fn parse_microrm_ref(attrs: &[syn::Attribute]) -> proc_macro2::TokenStream {
 ///
 ///
 /// The following are understood on individual fields:
 /// The following are understood on individual fields:
 /// - `#[microrm_foreign]`: this is a foreign key (and the field must be of a type implementing `EntityID`)
 /// - `#[microrm_foreign]`: this is a foreign key (and the field must be of a type implementing `EntityID`)
-#[proc_macro_derive(Entity, attributes(microrm_internal, microrm_foreign))]
+/// - `#[microrm_unique]`: this field is part of a unique index constraint on this entity's table
+#[proc_macro_derive(Entity, attributes(microrm_internal, microrm_foreign, microrm_unique))]
 pub fn derive_entity(tokens: TokenStream) -> TokenStream {
 pub fn derive_entity(tokens: TokenStream) -> TokenStream {
     entity::derive(tokens)
     entity::derive(tokens)
 }
 }

+ 2 - 2
microrm/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 [package]
 name = "microrm"
 name = "microrm"
-version = "0.3.11"
+version = "0.3.12"
 edition = "2021"
 edition = "2021"
 license = "BSD-4-Clause"
 license = "BSD-4-Clause"
 authors = ["Kestrel <kestrel@flying-kestrel.ca>"]
 authors = ["Kestrel <kestrel@flying-kestrel.ca>"]
@@ -18,7 +18,7 @@ serde_bytes = { version = "0.11.6" }
 serde_json = { version = "1.0" }
 serde_json = { version = "1.0" }
 lazy_static = { version = "1.4.0" }
 lazy_static = { version = "1.4.0" }
 
 
-microrm-macros = { path = "../microrm-macros", version = "0.2.5" }
+microrm-macros = { path = "../microrm-macros", version = "0.2.6" }
 log = "0.4.17"
 log = "0.4.17"
 
 
 [dev-dependencies]
 [dev-dependencies]

+ 6 - 0
microrm/src/entity.rs

@@ -1,5 +1,9 @@
 use crate::model::Modelable;
 use crate::model::Modelable;
 
 
+pub trait ConstraintVisitor {
+    fn index<I: Index>(&mut self);
+}
+
 /// A database entity, aka a struct representing a row in a table
 /// A database entity, aka a struct representing a row in a table
 pub trait Entity: 'static + for<'de> serde::Deserialize<'de> + serde::Serialize {
 pub trait Entity: 'static + for<'de> serde::Deserialize<'de> + serde::Serialize {
     type ID: EntityID;
     type ID: EntityID;
@@ -16,6 +20,8 @@ pub trait Entity: 'static + for<'de> serde::Deserialize<'de> + serde::Serialize
         visit: &mut F,
         visit: &mut F,
     ) -> Result<(), E>;
     ) -> Result<(), E>;
 
 
+    fn constraints<CV: ConstraintVisitor>(_cv: &mut CV) {}
+
     fn build_from(stmt: &sqlite::Statement) -> sqlite::Result<Self>
     fn build_from(stmt: &sqlite::Statement) -> sqlite::Result<Self>
     where
     where
         Self: Sized;
         Self: Sized;

+ 19 - 6
microrm/src/schema.rs

@@ -1,6 +1,6 @@
 mod create;
 mod create;
 
 
-use crate::entity::{Entity, Index};
+use crate::entity::{Entity, Index, ConstraintVisitor};
 
 
 /// How we describe an entire schema
 /// How we describe an entire schema
 #[derive(Debug)]
 #[derive(Debug)]
@@ -21,6 +21,22 @@ impl Schema {
         let (drop, create) = create::sql_for_table::<E>();
         let (drop, create) = create::sql_for_table::<E>();
         self.drop.push(drop);
         self.drop.push(drop);
         self.create.push(create);
         self.create.push(create);
+
+        struct Visitor<'l> {
+            drop: &'l mut Vec<String>,
+            create: &'l mut Vec<String>,
+        }
+
+        impl<'l> ConstraintVisitor for Visitor<'l> {
+            fn index<I: Index>(&mut self) {
+                let (drop, create) = create::sql_for_index::<I>();
+                self.drop.push(drop);
+                self.create.push(create);
+            }
+        }
+
+        E::constraints(&mut Visitor { drop: &mut self.drop, create: &mut self.create });
+
         self
         self
     }
     }
 
 
@@ -31,11 +47,8 @@ impl Schema {
         self
         self
     }
     }
 
 
-    pub fn add<E: Entity>(mut self) -> Self {
-        let (drop, create) = create::sql_for_table::<E>();
-        self.drop.push(drop);
-        self.create.push(create);
-        self
+    pub fn add<E: Entity>(self) -> Self {
+        self.entity::<E>()
     }
     }
 
 
     pub fn drop(&self) -> &Vec<String> {
     pub fn drop(&self) -> &Vec<String> {

+ 38 - 0
microrm/src/schema/create.rs

@@ -221,4 +221,42 @@ mod test {
             r#"CREATE TABLE IF NOT EXISTS `enum_container` (`id` integer primary key,`before` integer,`e` text,`after` integer)"#.to_owned()
             r#"CREATE TABLE IF NOT EXISTS `enum_container` (`id` integer primary key,`before` integer,`e` text,`after` integer)"#.to_owned()
         )
         )
     }
     }
+
+    #[derive(serde::Serialize, serde::Deserialize, crate::Entity)]
+    #[microrm_internal]
+    pub struct WithUnique {
+        #[microrm_unique]
+        key: String,
+        value: String,
+    }
+
+    #[test]
+    fn test_unique_attr() {
+        use crate::entity::{Entity,Index,ConstraintVisitor};
+        struct Visitor<'l> {
+            drop: &'l mut Vec<String>,
+            create: &'l mut Vec<String>,
+        }
+
+        impl<'l> ConstraintVisitor for Visitor<'l> {
+            fn index<I: Index>(&mut self) {
+                let (drop, create) = super::sql_for_index::<I>();
+                self.drop.push(drop);
+                self.create.push(create);
+            }
+        }
+
+        let mut drop = Vec::new();
+        let mut create = Vec::new();
+
+        WithUnique::constraints(&mut Visitor { drop: &mut drop, create: &mut create });
+        assert_eq!(
+            create.first().map(|s| s.as_str()),
+            Some(r#"CREATE UNIQUE INDEX `with_unique_unique_index` ON `with_unique` (`key`)"#)
+        );
+        assert_eq!(
+            drop.first().map(|s| s.as_str()),
+            Some(r#"DROP INDEX IF EXISTS `with_unique_unique_index`"#)
+        )
+    }
 }
 }