Переглянути джерело

Successful progress up to simple insertion queries.

Kestrel 1 рік тому
батько
коміт
ea661f6dbd

+ 41 - 9
microrm-macros/src/database.rs

@@ -1,6 +1,27 @@
 use convert_case::{Case, Casing};
 use quote::{format_ident, quote};
 
+fn type_to_expression_context_type(ty: &syn::Type) -> proc_macro2::TokenStream {
+    fn handle_path_segment(seg: &syn::PathSegment) -> proc_macro2::TokenStream {
+        let ident = &seg.ident;
+        let args = &seg.arguments;
+        match seg.arguments.is_empty() {
+            true => quote! { #ident },
+            false => quote! { #ident :: #args }
+        }
+    }
+
+    match ty {
+        syn::Type::Path(path) => {
+            let new_segments = path.path.segments.iter().map(handle_path_segment);
+            quote! {
+                #(#new_segments)::*
+            }
+        }
+        _ => todo!(),
+    }
+}
+
 pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let input: syn::DeriveInput = syn::parse_macro_input!(tokens);
 
@@ -19,11 +40,9 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
         panic!("Can only derive Database on data structs with named fields!");
     };
 
-    println!("items: {:?}", &items);
-
     let db_ident = input.ident;
 
-    let items = items.iter().map(|field| {
+    let visit_items = items.iter().map(|field| {
         let item_combined_name =
             format_ident!("{}{}", db_ident, field.0.to_string().to_case(Case::Snake));
         let item_base_name = &field.0.to_string();
@@ -31,22 +50,35 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
 
         quote! {
             struct #item_combined_name;
-            impl crate::db::DatabaseItem for #item_combined_name {
+            impl ::microrm::db::DatabaseItem for #item_combined_name {
                 fn item_key() -> &'static str { #item_base_name }
                 fn dependency_keys() -> &'static [&'static str] { &[] } // TODO
 
-                fn accept_entity_visitor(visitor: &mut impl crate::entity::EntityVisitor) {
-                    <#item_type as crate::db::DatabaseSpec>::accept_entity_visitor(visitor);
+                fn accept_entity_visitor(visitor: &mut impl ::microrm::entity::EntityVisitor) {
+                    <#item_type as ::microrm::db::DatabaseSpec>::accept_entity_visitor(visitor);
                 }
             }
             v.visit::<#item_combined_name>();
         }
     });
 
+    let build_method = items.iter().map(|field| {
+        let item_name = &field.0;
+        let item_type = type_to_expression_context_type(&field.1);
+        let item_name_str = item_name.to_string();
+        quote! {
+            #item_name : #item_type :: build(conn.clone(), #item_name_str)
+        }
+    });
+
     let out = quote! {
-        impl crate::db::Database for #db_ident {
-            fn accept_item_visitor(v: &mut impl crate::db::DatabaseItemVisitor) {
-                #(#items)*
+        impl ::microrm::db::Database for #db_ident {
+            fn build(conn: ::microrm::db::DBConnection) -> Self where Self: Sized {
+                Self { #(#build_method),* }
+            }
+
+            fn accept_item_visitor(v: &mut impl ::microrm::db::DatabaseItemVisitor) {
+                #(#visit_items)*
             }
         }
     }

+ 73 - 9
microrm-macros/src/entity.rs

@@ -13,7 +13,7 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
         fields
             .named
             .into_iter()
-            .map(|f| (f.ident.unwrap(), f.ty))
+            .map(|f| (f.ident.unwrap(), f.ty, f.attrs))
             .collect::<Vec<_>>()
     } else {
         panic!("Can only derive Entity on data structs with named fields!");
@@ -21,34 +21,98 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
 
     let entity_ident = input.ident;
 
-    let part_bodies = parts.iter().map(|part| {
-        let part_combined_name = format_ident!(
+    let make_combined_name = |part: &(syn::Ident, syn::Type, _)| {
+        format_ident!(
             "{}{}",
             entity_ident,
             part.0.to_string().to_case(Case::Snake)
-        );
+        )
+    };
+
+    let vis = input.vis;
+
+    let part_defs = parts.iter().map(|part| {
+        let part_combined_name = make_combined_name(&part);
         let part_base_name = &part.0.to_string();
         let part_type = &part.1;
         quote! {
-            struct #part_combined_name;
-            impl crate::entity::EntityPart for #part_combined_name {
+            #vis struct #part_combined_name;
+            impl ::microrm::entity::EntityPart for #part_combined_name {
                 type Datum = #part_type;
+                type Entity = #entity_ident;
                 fn part_name() -> &'static str {
                     #part_base_name
                 }
             }
+        }
+    });
+
+    let part_visit = parts.iter().map(|part| {
+        let part_combined_name = make_combined_name(&part);
+        quote! {
             v.visit::<#part_combined_name>();
         }
     });
 
+    let part_ref_visit = parts.iter().map(|part| {
+        let part_combined_name = make_combined_name(&part);
+        let field = &part.0;
+        let field_name = part.0.to_string();
+        quote! {
+            v.visit_datum::<#part_combined_name>(#field_name, &self.#field);
+        }
+    });
+
+    let part_names = parts.iter().map(|part| {
+        let part_combined_name = make_combined_name(&part);
+        let part_camel_name = format_ident!("{}", part.0.to_string().to_case(Case::UpperCamel));
+        quote! {
+            pub const #part_camel_name : #part_combined_name = #part_combined_name;
+        }
+    });
+
+    let unique_ident = format_ident!("unique");
+
+    // collect list of unique parts
+    let unique_parts = parts.iter().filter(|part| {
+        part.2.iter().any(|attr| attr.parse_meta().map(|a| a.path().is_ident(&unique_ident)).unwrap_or(false))
+    }).collect::<Vec<_>>();
+
+    let uniques_list = match unique_parts.len() {
+        0 => quote! { () },
+        1 => {
+            let uty = &unique_parts.first().as_ref().unwrap().1;
+            quote! { #uty }
+        },
+        _ => {
+            let utys = unique_parts.iter().map(|part| &part.1);
+            quote! { ( #(#utys),* ) }
+        },
+    };
+
     let entity_name = entity_ident.to_string().to_case(Case::Snake);
+    
 
     quote! {
-        impl crate::entity::Entity for #entity_ident {
+        #(#part_defs)*
+
+        impl #entity_ident {
+            #(#part_names)*
+        }
+
+        impl ::microrm::entity::Entity for #entity_ident {
+            type Uniques = #uniques_list;
+
             fn entity_name() -> &'static str { #entity_name }
-            fn accept_part_visitor(v: &mut impl crate::entity::EntityPartVisitor) {
+            fn accept_part_visitor(v: &mut impl ::microrm::entity::EntityPartVisitor) {
+                #(
+                    #part_visit
+                );*
+            }
+
+            fn accept_part_visitor_ref(&self, v: &mut impl ::microrm::entity::EntityPartVisitor) {
                 #(
-                    #part_bodies
+                    #part_ref_visit
                 );*
             }
         }

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

@@ -3,7 +3,7 @@ use proc_macro::TokenStream;
 mod database;
 mod entity;
 
-#[proc_macro_derive(Entity)]
+#[proc_macro_derive(Entity, attributes(unique))]
 pub fn derive_entity(tokens: TokenStream) -> TokenStream {
     entity::derive(tokens)
 }

+ 5 - 3
microrm/Cargo.toml

@@ -21,12 +21,14 @@ lazy_static = { version = "1.4.0" }
 microrm-macros = { path = "../microrm-macros", version = "0.2.5" }
 log = "0.4.17"
 
+topological-sort = { version = "0.2" }
+
 [dev-dependencies]
 criterion = "0.5"
 rand = "0.8.5"
 stats_alloc = "0.1.10"
 async-std = "1.11"
 
-[[bench]]
-name = "simple_in_memory"
-harness = false
+# [[bench]]
+# name = "simple_in_memory"
+# harness = false

+ 184 - 28
microrm/src/db.rs

@@ -1,26 +1,143 @@
-use crate::entity::{Entity, EntityDatum, EntityVisitor};
+use crate::{entity::{Entity, EntityDatum, EntityVisitor, EntityPartVisitor, EntityPart}, schema::collect_from_database};
 
-pub struct Unique<T: EntityDatum> {
-    _ghost: std::marker::PhantomData<T>,
+pub(crate) type DBConnection = std::sync::Arc<sqlite::ConnectionThreadSafe>;
+
+pub enum DBError {
+    EmptyResult,
+    SQLite(sqlite::Error),
+}
+
+pub enum DBResult<T> {
+    Ok(T),
+    Empty,
+    Error(sqlite::Error),
+}
+
+impl<T> DBResult<T> {
+    pub fn empty_ok(self) -> Result<Option<T>, DBError> {
+        match self {
+            Self::Ok(t) => Ok(Some(t)),
+            Self::Empty => Ok(None),
+            Self::Error(err) => Err(DBError::SQLite(err)),
+        }
+    }
+
+    pub fn empty_err(self) -> Result<T, DBError> {
+        match self {
+            Self::Ok(t) => Ok(t),
+            Self::Empty => Err(DBError::EmptyResult),
+            Self::Error(err) => Err(DBError::SQLite(err)),
+        }
+    }
 }
 
-impl<T: EntityDatum> EntityDatum for Unique<T> {}
+impl<T> From<Option<T>> for DBResult<T> {
+    fn from(value: Option<T>) -> Self {
+        match value {
+            Some(v) => Self::Ok(v),
+            None => Self::Empty,
+        }
+    }
+}
 
 pub struct IDMap<T: Entity> {
-    conn: std::cell::OnceCell<std::sync::Arc<sqlite::ConnectionThreadSafe>>,
+    conn: DBConnection,
+    ctx: &'static str,
     _ghost: std::marker::PhantomData<T>,
 }
 
-pub struct AssocSet<T: Entity> {
-    conn: std::cell::OnceCell<std::sync::Arc<sqlite::ConnectionThreadSafe>>,
-    _ghost: std::marker::PhantomData<T>,
+impl<T: Entity> IDMap<T> {
+    pub fn build(db: DBConnection, ctx: &'static str) -> Self {
+        Self {
+            conn: db,
+            ctx,
+            _ghost: std::marker::PhantomData
+        }
+    }
+
+    pub fn lookup_unique(&self, uniques: &T::Uniques) -> DBResult<T> {
+        None.into()
+    }
+
+    pub fn insert(&self, value: &T) -> Result<(), DBError> {
+        println!("inserting value into IDMap; context is {} and name is {}", self.ctx, T::entity_name());
+        let table_name = format!("{}_{}", self.ctx, T::entity_name());
+
+        let mut part_names = String::new();
+        let mut placeholders = String::new();
+        struct PartNameVisitor<'a>(&'a mut String, &'a mut String);
+        impl<'a> EntityPartVisitor for PartNameVisitor<'a> {
+            fn visit<EP: EntityPart>(&mut self) {
+                if self.0.len() != 0 {
+                    self.0.push_str(", ");
+                    self.1.push_str(", ");
+                }
+                self.0.push_str("`");
+                self.0.push_str(EP::part_name());
+                self.0.push_str("`");
+                self.1.push_str("?");
+            }
+        }
+
+        T::accept_part_visitor(&mut PartNameVisitor(&mut part_names, &mut placeholders));
+
+        let query_string = format!("insert into `{}` ({}) values ({})", table_name, part_names, placeholders);
+        println!("query_string: {}", query_string);
+
+        let mut prepared = self.conn.prepare(query_string).expect("couldn't prepare statement");
+        struct PartBinder<'a, 'b>(&'a mut sqlite::Statement<'b>, usize);
+        impl<'a, 'b> EntityPartVisitor for PartBinder<'a, 'b> {
+            fn visit_datum<EP: EntityPart>(&mut self, _ctx: &'static str, datum: &EP::Datum) {
+                datum.bind_to(self.0, self.1);
+                self.1 += 1;
+            }
+        }
+
+        value.accept_part_visitor_ref(&mut PartBinder(&mut prepared, 1));
+
+        prepared.next();
+
+        // struct BuildQuery
+        
+        // we're going to do a dumb thing for now
+        // TODO: make this better
+        // self.conn.execute(format!("insert into {} values ({})
+        Ok(())
+    }
 }
 
 pub struct Index<T: Entity, Key: EntityDatum> {
-    conn: std::cell::OnceCell<std::sync::Arc<sqlite::ConnectionThreadSafe>>,
+    _conn: DBConnection,
     _ghost: std::marker::PhantomData<(T, Key)>,
 }
 
+/*pub struct Unique<T: EntityDatum> {
+    _ghost: std::marker::PhantomData<T>,
+}
+
+impl<T: EntityDatum> EntityDatum for Unique<T> {
+    fn sql_type() -> &'static str { T::sql_type() }
+    fn is_unique() -> bool { true }
+}*/
+
+pub struct AssocSet<T: Entity> {
+    _ghost: std::marker::PhantomData<T>,
+}
+
+impl<T: Entity> EntityDatum for AssocSet<T> {
+    // foreign key over IDs
+    fn sql_type() -> &'static str { "integer" }
+
+    fn accept_entity_visitor(v: &mut impl EntityVisitor) {
+        v.visit::<T>();
+    }
+
+
+    fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, index: usize) {
+        todo!()
+    }
+}
+
 pub trait DatabaseItem {
     fn item_key() -> &'static str;
     fn dependency_keys() -> &'static [&'static str];
@@ -45,36 +162,53 @@ pub trait DatabaseItemVisitor {
 }
 
 pub trait DatabaseSpec {
-    fn give_connection(&mut self, conn: std::sync::Arc<sqlite::ConnectionThreadSafe>);
     fn accept_entity_visitor(visitor: &mut impl EntityVisitor);
 }
 
 impl<T: Entity> DatabaseSpec for IDMap<T> {
-    fn give_connection(&mut self, conn: std::sync::Arc<sqlite::ConnectionThreadSafe>) {
-        self.conn
-            .set(conn)
-            .ok()
-            .expect("couldn't set once_cell with sqlite connection!");
-    }
     fn accept_entity_visitor(visitor: &mut impl EntityVisitor) {
         visitor.visit::<T>()
     }
 }
 
 pub trait Database {
+    fn open_path<'a, P: Into<&'a std::path::Path>>(path: P) -> Result<Self, DBError> where Self: Sized {
+        match sqlite::Connection::open_thread_safe_with_flags(path.into(), sqlite::OpenFlags::new().with_create().with_full_mutex().with_read_write()) {
+            Ok(conn) => {
+                let conn = std::sync::Arc::new(conn);
+                let schema = collect_from_database::<Self>();
+                if !schema.check(conn.clone()) {
+                    schema.create(conn.clone());
+                }
+                Ok(Self::build(conn))
+            },
+            Err(e) => {
+                /*std::io::Error::new(std::io::ErrorKind::
+                e.message*/
+                println!("e: {:?}", e);
+                todo!("connection failed")
+            },
+        }
+    }
+
+    #[doc(hidden)]
+    fn build(conn: DBConnection) -> Self where Self: Sized;
+
     fn accept_item_visitor(visitor: &mut impl DatabaseItemVisitor)
     where
         Self: Sized;
 }
 
 #[cfg(test)]
-mod simple_test {
+mod simple_tests {
+    #![allow(unused)]
+
     use super::*;
     use crate::entity::{EntityPart, EntityPartVisitor};
     // simple hand-built database example
 
     struct SimpleEntity {
-        name: Unique<String>,
+        name: String,
     }
 
     // normally this would be invisible, but for testing purposes we expose it
@@ -84,21 +218,23 @@ mod simple_test {
     struct SimpleEntityName;
     impl EntityPart for SimpleEntityName {
         type Datum = String;
+        type Entity = SimpleEntity;
         fn part_name() -> &'static str {
             "name"
         }
-        fn is_unique() -> bool {
-            true
-        }
     }
 
     impl Entity for SimpleEntity {
+        type Uniques = String;
         fn entity_name() -> &'static str {
             "simple_entity"
         }
         fn accept_part_visitor(visitor: &mut impl EntityPartVisitor) {
             visitor.visit::<SimpleEntityName>();
         }
+        fn accept_part_visitor_ref(&self, visitor: &mut impl EntityPartVisitor) {
+            visitor.visit_datum::<SimpleEntityName>("name", &self.name);
+        }
     }
 
     struct SimpleDatabase {
@@ -106,6 +242,10 @@ mod simple_test {
     }
 
     impl Database for SimpleDatabase {
+        fn build(conn: DBConnection) -> Self where Self: Sized {
+            Self { strings: IDMap::build(conn, "strings") }
+        }
+
         fn accept_item_visitor(visitor: &mut impl DatabaseItemVisitor)
         where
             Self: Sized,
@@ -149,23 +289,39 @@ mod simple_test {
 }
 
 #[cfg(test)]
-mod derive_test {
+mod derive_tests {
+    #![allow(unused)]
+
     use microrm_macros::{Database, Entity};
+    use super::{IDMap, AssocSet, Database};
 
-    use super::IDMap;
     #[derive(Entity)]
-    struct SimpleEntity {
-        name: super::Unique<String>,
+    struct Role {
+        title: String,
+        permissions: String,
+    }
+
+    #[derive(Entity)]
+    struct Person {
+        #[unique]
+        name: String,
+        roles: AssocSet<Role>,
     }
 
     #[derive(Database)]
-    struct SimpleDB {
-        entities: IDMap<SimpleEntity>,
+    struct PeopleDB {
+        people: IDMap<Person>,
     }
 
     #[test]
     fn collect_test() {
-        crate::schema::collect_from_database::<SimpleDB>();
+        microrm::schema::collect_from_database::<PeopleDB>();
+    }
+
+    #[test]
+    fn open_test() {
+        // PeopleDB::open_path(std::path::Path::new(":memory:"));
+        PeopleDB::open_path(std::path::Path::new("/tmp/schema.db"));
     }
 }
 

+ 21 - 18
microrm/src/entity.rs

@@ -1,42 +1,45 @@
+mod datum;
+
 /// Represents a data field in an Entity
 pub trait EntityDatum: 'static {
     fn sql_type() -> &'static str;
+
+    fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, index: usize);
+
+    fn accept_entity_visitor(_: &mut impl EntityVisitor) {}
 }
 
-impl EntityDatum for String {
-    fn sql_type() -> &'static str {
-        "text"
-    }
+/// A fixed-length list of EntityDatums.
+pub trait EntityDatumList {
+    fn accept(&self, visitor: &mut impl EntityDatumListVisitor);
 }
 
-/// A single data field in an Entity
+/// A walker for a EntityDatumList instance.
+pub trait EntityDatumListVisitor {
+    fn visit<ED: EntityDatum>(&mut self, datum: &ED);
+}
+
+/// A single data field in an Entity, automatically derived via `#[derive(Entity)]`.
 pub trait EntityPart: 'static {
     type Datum: EntityDatum;
+    type Entity: Entity;
 
     fn part_name() -> &'static str;
-    fn is_unique() -> bool {
-        false
-    }
-    fn is_assoc() -> bool {
-        false
-    }
-    fn assoc_name() -> &'static str {
-        unreachable!()
-    }
-    fn visit_assoc(_: &mut impl EntityVisitor) {
-        unreachable!()
-    }
 }
 
 /// Visitor for traversing all `EntityPart`s in an `Entity`
 pub trait EntityPartVisitor {
-    fn visit<EP: EntityPart>(&mut self);
+    fn visit<EP: EntityPart>(&mut self) {}
+    fn visit_datum<EP: EntityPart>(&mut self, _ctx: &'static str, _datum: &EP::Datum) {}
 }
 
 /// A single database entity, aka an object type that gets its own table
 pub trait Entity: 'static {
+    type Uniques : EntityDatumList;
+
     fn entity_name() -> &'static str;
     fn accept_part_visitor(visitor: &mut impl EntityPartVisitor);
+    fn accept_part_visitor_ref(&self, visitor: &mut impl EntityPartVisitor);
 }
 
 /// Visitor for traversing all `Entity`s in a container

+ 102 - 0
microrm/src/entity/datum.rs

@@ -0,0 +1,102 @@
+use super::{EntityDatum,EntityDatumList};
+
+impl EntityDatum for String {
+    fn sql_type() -> &'static str {
+        "text"
+    }
+
+    fn bind_to<'a>(&self, stmt: &mut sqlite::Statement<'a>, index: usize) {
+        stmt.bind((index, self.as_str())).expect("couldn't bind string");
+    }
+}
+
+impl EntityDatum for usize {
+    fn sql_type() -> &'static str {
+        "int"
+    }
+
+    fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, index: usize) {
+        todo!()
+    }
+}
+
+impl EntityDatum for isize {
+    fn sql_type() -> &'static str {
+        "int"
+    }
+
+    fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, index: usize) {
+        todo!()
+    }
+}
+
+impl EntityDatum for u64 {
+    fn sql_type() -> &'static str {
+        "int"
+    }
+
+    fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, index: usize) {
+        todo!()
+    }
+}
+
+impl EntityDatum for i64 {
+    fn sql_type() -> &'static str {
+        "int"
+    }
+
+    fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, index: usize) {
+        todo!()
+    }
+}
+
+impl<T: EntityDatum> EntityDatum for Option<T> {
+    fn sql_type() -> &'static str {
+        T::sql_type()
+    }
+
+    fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, index: usize) {
+        todo!()
+    }
+}
+
+impl EntityDatumList for () {
+    fn accept(&self, _: &mut impl super::EntityDatumListVisitor) { }
+}
+
+impl<T: EntityDatum> EntityDatumList for T {
+    fn accept(&self, visitor: &mut impl super::EntityDatumListVisitor) {
+        visitor.visit(self);
+    }
+}
+
+impl<T0: EntityDatum> EntityDatumList for (T0,) {
+    fn accept(&self, visitor: &mut impl super::EntityDatumListVisitor) {
+        visitor.visit(&self.0);
+    }
+}
+
+impl<T0: EntityDatum, T1: EntityDatum> EntityDatumList for (T0,T1) {
+    fn accept(&self, visitor: &mut impl super::EntityDatumListVisitor) {
+        visitor.visit(&self.0);
+        visitor.visit(&self.1);
+    }
+}
+
+impl<T0: EntityDatum, T1: EntityDatum, T2: EntityDatum> EntityDatumList for (T0,T1,T2) {
+    fn accept(&self, visitor: &mut impl super::EntityDatumListVisitor) {
+        visitor.visit(&self.0);
+        visitor.visit(&self.1);
+        visitor.visit(&self.2);
+    }
+}
+
+impl<T0: EntityDatum, T1: EntityDatum, T2: EntityDatum, T3: EntityDatum> EntityDatumList for (T0,T1,T2,T3) {
+    fn accept(&self, visitor: &mut impl super::EntityDatumListVisitor) {
+        visitor.visit(&self.0);
+        visitor.visit(&self.1);
+        visitor.visit(&self.2);
+        visitor.visit(&self.3);
+    }
+}
+

+ 3 - 0
microrm/src/lib.rs

@@ -1,3 +1,6 @@
+// to make the proc_macros work inside the microrm crate; needed for tests and the metaschema.
+extern crate self as microrm;
+
 pub mod db;
 pub mod entity;
 pub mod schema;

+ 0 - 0
microrm/src/query.rs


+ 187 - 3
microrm/src/schema.rs

@@ -1,9 +1,96 @@
-use crate::db::{Database, DatabaseItem, DatabaseItemVisitor};
-use crate::entity::{Entity, EntityVisitor};
+use crate::db::{Database, DatabaseItem, DatabaseItemVisitor, DBConnection};
 
 mod entity;
+pub(crate) mod meta;
 
-pub fn collect_from_database<DB: Database>() {
+#[derive(Debug)]
+struct ColumnInfo {
+    name: &'static str,
+    ty: String,
+    fkey: Option<String>,
+    unique: bool,
+}
+
+#[derive(Debug)]
+struct TableInfo {
+    table_name: String,
+    columns: Vec<ColumnInfo>,
+    dependencies: Vec<String>,
+}
+
+impl TableInfo {
+    fn new(name: String) -> Self {
+        TableInfo {
+            table_name: name,
+            columns: vec![],
+            dependencies: vec![],
+        }
+    }
+
+    fn build_creation_query(&self) -> String {
+        let columns = self.columns.iter().map(|col| {
+            format!(
+                ", `{}` {}{}",
+                col.name,
+                col.ty,
+                if col.unique { "unique" } else { "" }
+            )
+        });
+        let fkeys = self.columns.iter().filter_map(|col| {
+            Some(format!(
+                ", foreign key(`{}`) references {}",
+                col.name,
+                col.fkey.as_ref()?
+            ))
+        });
+
+        format!(
+            "create table {} (`id` integer primary key{}{});",
+            self.table_name,
+            columns.collect::<String>(),
+            fkeys.collect::<String>()
+        )
+    }
+}
+
+pub struct DatabaseSchema {
+    signature: u64,
+    queries: Vec<String>,
+}
+
+impl DatabaseSchema {
+    const SCHEMA_SIGNATURE_KEY: &'static str = "schema_signature";
+    pub fn check(&self, db: DBConnection) -> bool {
+        // attempt to use connection as a MetadataDB database
+        let metadb = meta::MetadataDB::build(db.clone());
+
+        // check to see if the signature exists and matches
+        metadb.kv_metastore.lookup_unique(&Self::SCHEMA_SIGNATURE_KEY.to_string()).empty_err().map(|kv| {
+            kv.value.parse::<u64>().unwrap_or(0) == self.signature
+        }).unwrap_or(false)
+    }
+
+    pub fn create(&self, db: DBConnection) {
+        for query in self.queries.iter() {
+            db.execute(query).expect("Couldn't run creation query!");
+        }
+
+        // attempt to use connection as a MetadataDB database
+        let metadb = meta::MetadataDB::build(db.clone());
+
+        for query in collect_from_database::<meta::MetadataDB>().queries.iter() {
+            db.execute(query).expect("Couldn't run MetadataDB creation query!");
+        }
+
+        // store signature
+        metadb.kv_metastore.insert(&meta::KV {
+            key: Self::SCHEMA_SIGNATURE_KEY.into(),
+            value: format!("{}", self.signature)
+        });
+    }
+}
+
+pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
     struct IV(entity::EntityStateContainer);
 
     impl DatabaseItemVisitor for IV {
@@ -19,5 +106,102 @@ pub fn collect_from_database<DB: Database>() {
 
     DB::accept_item_visitor(&mut iv);
 
+    // now to turn all that into a set of tables
+    let mut tables = std::collections::HashMap::new();
+
+    for state in iv.0.iter_states() {
+        let table_name = format!("{}_{}", state.context, state.name);
+        // we may end up visiting duplicate entities; skip them if so
+        if tables.contains_key(&table_name) {
+            continue;
+        }
+        let mut table = TableInfo::new(table_name.clone());
+        for part in state.parts.iter() {
+            match part.ty {
+                entity::PartType::Datum(dtype) => {
+                    table.columns.push(ColumnInfo {
+                        name: part.name,
+                        ty: dtype.into(),
+                        fkey: None,
+                        unique: false, // XXX
+                    })
+                }
+                entity::PartType::Assoc(assoc_name) => {
+                    let assoc_table_name = format!(
+                        "{}_{}_assoc_{}_{}",
+                        state.context, state.name, part.name, assoc_name
+                    );
+                    let mut assoc_table = TableInfo::new(assoc_table_name.clone());
+                    assoc_table.dependencies.push(table_name.clone());
+                    assoc_table
+                        .dependencies
+                        .push(format!("{}_{}", state.context, assoc_name));
+
+                    assoc_table.columns.push(ColumnInfo {
+                        name: "base".into(),
+                        ty: "int".into(),
+                        fkey: Some(format!("{}(`id`)", table_name)),
+                        unique: false,
+                    });
+
+                    assoc_table.columns.push(ColumnInfo {
+                        name: "target".into(),
+                        ty: "int".into(),
+                        fkey: Some(format!("{}_{}(`id`)", state.context, assoc_name)),
+                        unique: false,
+                    });
+
+                    tables.insert(assoc_table_name, assoc_table);
+                }
+            }
+        }
+
+        tables.insert(table_name, table);
+    }
+
     println!("collected base info from database: {:?}", iv.0);
+    println!("derived table info: {:?}", tables);
+
+    let mut tsort: topological_sort::TopologicalSort<&str> =
+        topological_sort::TopologicalSort::new();
+    for table in tables.values() {
+        tsort.insert(table.table_name.as_str());
+
+        for dep in table.dependencies.iter() {
+            tsort.add_dependency(dep.as_str(), table.table_name.as_str());
+        }
+    }
+
+    // this must be a stable hash function, so we very explicitly want to use a SipHasher with
+    // known parameters
+    #[allow(deprecated)]
+    let mut signature_hasher = std::hash::SipHasher::new();
+    use std::hash::{Hash, Hasher};
+
+    let mut queries = vec![];
+
+    loop {
+        let mut table_list = tsort.pop_all();
+        if table_list.len() == 0 {
+            break;
+        }
+        // bring into stable ordering
+        table_list.sort();
+
+        for table_name in table_list.into_iter() {
+            let table = tables.get(table_name).unwrap();
+            let create_sql = table.build_creation_query();
+
+            table_name.hash(&mut signature_hasher);
+            create_sql.hash(&mut signature_hasher);
+            queries.push(create_sql);
+        }
+    }
+
+    // TODO: generate index schemas here
+
+    DatabaseSchema {
+        signature: signature_hasher.finish(),
+        queries
+    }
 }

+ 23 - 12
microrm/src/schema/entity.rs

@@ -10,17 +10,27 @@ pub enum PartType {
 
 #[derive(Debug)]
 pub struct PartState {
-    name: &'static str,
-    ty: PartType,
+    pub name: &'static str,
+    pub ty: PartType,
 }
 
 impl PartState {
     fn build<EP: EntityPart>() -> Self {
+        struct AssocCheck(Option<&'static str>);
+        impl EntityVisitor for AssocCheck {
+            fn visit<E: Entity>(&mut self) {
+                self.0 = Some(E::entity_name());
+            }
+        }
+
+        let mut acheck = AssocCheck(None);
+        EP::Datum::accept_entity_visitor(&mut acheck);
+
         PartState {
             name: EP::part_name(),
-            ty: match EP::is_assoc() {
-                true => PartType::Assoc(EP::assoc_name()),
-                false => PartType::Datum(EP::Datum::sql_type()),
+            ty: match acheck.0 {
+                Some(aname) => PartType::Assoc(aname),
+                None => PartType::Datum(EP::Datum::sql_type()),
             },
         }
     }
@@ -28,11 +38,11 @@ impl PartState {
 
 #[derive(Debug)]
 pub struct EntityState {
-    context: &'static str,
-    name: &'static str,
+    pub context: &'static str,
+    pub name: &'static str,
     typeid: std::any::TypeId,
 
-    parts: Vec<PartState>,
+    pub parts: Vec<PartState>,
 }
 
 impl EntityState {
@@ -63,6 +73,10 @@ pub struct EntityStateContainer {
 }
 
 impl EntityStateContainer {
+    pub fn iter_states(&self) -> impl Iterator<Item = &EntityState> {
+        self.states.values()
+    }
+
     pub fn make_context(&mut self, context: &'static str) -> EntityContext {
         EntityContext {
             context,
@@ -78,7 +92,6 @@ pub struct EntityContext<'a> {
 
 impl<'a> EntityVisitor for EntityContext<'a> {
     fn visit<E: Entity>(&mut self) {
-        println!("visiting entity in context");
         let entry = self
             .container
             .states
@@ -97,9 +110,7 @@ impl<'a> EntityVisitor for EntityContext<'a> {
         struct RecursiveVisitor<'a, 'b>(&'a mut EntityContext<'b>);
         impl<'a, 'b> EntityPartVisitor for RecursiveVisitor<'a, 'b> {
             fn visit<EP: EntityPart>(&mut self) {
-                if EP::is_assoc() {
-                    EP::visit_assoc(self.0)
-                }
+                EP::Datum::accept_entity_visitor(self.0);
             }
         }
 

+ 13 - 0
microrm/src/schema/meta.rs

@@ -0,0 +1,13 @@
+use crate::db::IDMap;
+
+#[derive(microrm_macros::Entity)]
+pub struct KV {
+    #[unique]
+    pub key: String,
+    pub value: String,
+}
+
+#[derive(microrm_macros::Database)]
+pub struct MetadataDB {
+    pub kv_metastore: IDMap<KV>,
+}