Forráskód Böngészése

Reorganization and rustfmt pass.

Kestrel 1 éve
szülő
commit
b5eb620c07

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

@@ -7,7 +7,7 @@ fn type_to_expression_context_type(ty: &syn::Type) -> proc_macro2::TokenStream {
         let args = &seg.arguments;
         let args = &seg.arguments;
         match seg.arguments.is_empty() {
         match seg.arguments.is_empty() {
             true => quote! { #ident },
             true => quote! { #ident },
-            false => quote! { #ident :: #args }
+            false => quote! { #ident :: #args },
         }
         }
     }
     }
 
 
@@ -50,12 +50,12 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
 
 
         quote! {
         quote! {
             struct #item_combined_name;
             struct #item_combined_name;
-            impl ::microrm::db::DatabaseItem for #item_combined_name {
+            impl ::microrm::schema::DatabaseItem for #item_combined_name {
                 fn item_key() -> &'static str { #item_base_name }
                 fn item_key() -> &'static str { #item_base_name }
                 fn dependency_keys() -> &'static [&'static str] { &[] } // TODO
                 fn dependency_keys() -> &'static [&'static str] { &[] } // TODO
 
 
                 fn accept_entity_visitor(visitor: &mut impl ::microrm::entity::EntityVisitor) {
                 fn accept_entity_visitor(visitor: &mut impl ::microrm::entity::EntityVisitor) {
-                    <#item_type as ::microrm::db::DatabaseSpec>::accept_entity_visitor(visitor);
+                    <#item_type as ::microrm::schema::DatabaseSpec>::accept_entity_visitor(visitor);
                 }
                 }
             }
             }
             v.visit::<#item_combined_name>();
             v.visit::<#item_combined_name>();
@@ -71,18 +71,16 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
         }
         }
     });
     });
 
 
-    let out = quote! {
-        impl ::microrm::db::Database for #db_ident {
+    quote! {
+        impl ::microrm::schema::Database for #db_ident {
             fn build(conn: ::microrm::db::DBConnection) -> Self where Self: Sized {
             fn build(conn: ::microrm::db::DBConnection) -> Self where Self: Sized {
                 Self { #(#build_method),* }
                 Self { #(#build_method),* }
             }
             }
 
 
-            fn accept_item_visitor(v: &mut impl ::microrm::db::DatabaseItemVisitor) {
+            fn accept_item_visitor(v: &mut impl ::microrm::schema::DatabaseItemVisitor) {
                 #(#visit_items)*
                 #(#visit_items)*
             }
             }
         }
         }
     }
     }
-    .into();
-    println!("out: {}", out);
-    out
+    .into()
 }
 }

+ 12 - 5
microrm-macros/src/entity.rs

@@ -74,20 +74,27 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let unique_ident = format_ident!("unique");
     let unique_ident = format_ident!("unique");
 
 
     // collect list of unique parts
     // 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 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() {
     let uniques_list = match unique_parts.len() {
         0 => quote! { () },
         0 => quote! { () },
         1 => {
         1 => {
             let uty = &unique_parts.first().as_ref().unwrap().1;
             let uty = &unique_parts.first().as_ref().unwrap().1;
             quote! { #uty }
             quote! { #uty }
-        },
+        }
         _ => {
         _ => {
             let utys = unique_parts.iter().map(|part| &part.1);
             let utys = unique_parts.iter().map(|part| &part.1);
             quote! { ( #(#utys),* ) }
             quote! { ( #(#utys),* ) }
-        },
+        }
     };
     };
 
 
     let entity_name = entity_ident.to_string().to_case(Case::Snake);
     let entity_name = entity_ident.to_string().to_case(Case::Snake);

+ 58 - 313
microrm/src/db.rs

@@ -1,325 +1,70 @@
-use crate::{entity::{Entity, EntityDatum, EntityVisitor, EntityPartVisitor, EntityPart}, schema::collect_from_database, query};
-
-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> 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: DBConnection,
-    ctx: &'static str,
-    _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
+use crate::{
+    entity::{Entity, EntityDatum, EntityPart, EntityPartVisitor, EntityVisitor},
+    query,
+    schema::collect_from_database,
+};
+use crate::{DBError, DBResult};
+
+use std::{
+    collections::HashMap,
+    sync::{Arc, Mutex, Weak},
+};
+
+// ----------------------------------------------------------------------
+// sqlite layer types
+// ----------------------------------------------------------------------
+
+pub(crate) type DBConnection = std::sync::Arc<Connection>;
+
+pub struct Connection {
+    conn: &'static mut sqlite::ConnectionThreadSafe,
+    statement_cache: Mutex<HashMap<u64, sqlite::Statement<'static>>>,
+}
+
+impl Connection {
+    pub fn open<U: AsRef<str>>(uri: U) -> Result<DBConnection, DBError> {
+        match sqlite::Connection::open_thread_safe_with_flags(
+            uri.as_ref(),
+            sqlite::OpenFlags::new()
+                .with_create()
+                .with_full_mutex()
+                .with_read_write(),
+        ) {
+            Ok(conn) => {
+                let conn = Box::leak(Box::new(conn));
+                Ok(Arc::new(Self {
+                    conn,
+                    statement_cache: Default::default(),
+                }))
+            }
+            Err(e) => Err(DBError::Sqlite(e)),
         }
         }
     }
     }
 
 
-    pub(crate) fn conn(&self) -> &DBConnection {
-        &self.conn
-    }
-
-    pub(crate) fn ctx(&self) -> &'static str {
-        self.ctx
-    }
-
-    pub fn lookup_unique(&self, uniques: &T::Uniques) -> DBResult<T> {
-        None.into()
+    pub(crate) fn execute_raw(&self, sql: &str) -> Result<(), DBError> {
+        Ok(self.conn.execute(sql)?)
     }
     }
 
 
-    pub fn insert(&self, value: &T) -> Result<(), DBError> {
-        query::insert(self, value)
-    }
-}
-
-pub struct Index<T: Entity, Key: EntityDatum> {
-    _conn: DBConnection,
-    _ghost: std::marker::PhantomData<(T, Key)>,
-}
-
-pub struct AssocSet<T: Entity> {
-    _ghost: std::marker::PhantomData<T>,
-}
-
-impl<T: Entity> EntityDatum for AssocSet<T> {
-    fn sql_type() -> &'static str { unreachable!() }
-
-    fn accept_entity_visitor(v: &mut impl EntityVisitor) {
-        v.visit::<T>();
-    }
-
-    fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, index: usize) {
+    pub(crate) fn with_prepared<R>(
+        &self,
+        hash_key: u64,
+        build_query: impl Fn() -> String,
+        run_query: impl Fn(&mut sqlite::Statement<'static>) -> DBResult<R>,
+    ) -> DBResult<R> {
         todo!()
         todo!()
     }
     }
 }
 }
 
 
-pub trait DatabaseItem {
-    fn item_key() -> &'static str;
-    fn dependency_keys() -> &'static [&'static str];
-
-    fn is_index() -> bool {
-        false
-    }
-    fn index_over() -> &'static str {
-        unreachable!()
-    }
-    fn index_columns() -> &'static [&'static str] {
-        unreachable!()
-    }
-
-    fn accept_entity_visitor(visitor: &mut impl EntityVisitor);
-}
-
-pub trait DatabaseItemVisitor {
-    fn visit<DI: DatabaseItem>(&mut self)
-    where
-        Self: Sized;
-}
-
-pub trait DatabaseSpec {
-    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>()
-    }
-}
-
-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) => {
-                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_tests {
-    #![allow(unused)]
-
-    use super::*;
-    use crate::entity::{EntityPart, EntityPartVisitor, EntityID};
-    // simple hand-built database example
-
-    struct SimpleEntity {
-        name: String,
-    }
-
-    #[derive(Clone, Copy, PartialEq, PartialOrd, Debug, Hash)]
-    struct SimpleEntityID(usize);
-
-    impl EntityID for SimpleEntityID {
-        type Entity = SimpleEntity;
-
-        fn from_raw(id: usize) -> Self { Self(id) }
-        fn into_raw(self) -> usize { self.0 }
-    }
-
-    // normally this would be invisible, but for testing purposes we expose it
-    impl SimpleEntity {
-        // pub const Name: SimpleEntityName = SimpleEntityName;
-    }
-    struct SimpleEntityName;
-    impl EntityPart for SimpleEntityName {
-        type Datum = String;
-        type Entity = SimpleEntity;
-        fn part_name() -> &'static str {
-            "name"
+impl Drop for Connection {
+    fn drop(&mut self) {
+        // first we drop the statement cache
+        self.statement_cache
+            .lock()
+            .expect("couldn't get statement cache lock")
+            .clear();
+        // then we drop the connection
+        unsafe {
+            drop(Box::from_raw(self.conn));
         }
         }
     }
     }
-
-    impl Entity for SimpleEntity {
-        type Uniques = String;
-        type ID = SimpleEntityID;
-
-        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 {
-        strings: IDMap<SimpleEntity>,
-    }
-
-    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,
-        {
-            struct SimpleDatabaseStringsItem;
-            impl DatabaseItem for SimpleDatabaseStringsItem {
-                fn item_key() -> &'static str {
-                    "strings"
-                }
-                fn dependency_keys() -> &'static [&'static str] {
-                    &[]
-                }
-
-                fn accept_entity_visitor(visitor: &mut impl EntityVisitor) {
-                    visitor.visit::<SimpleEntity>();
-                }
-            }
-
-            visitor.visit::<SimpleDatabaseStringsItem>();
-        }
-    }
-
-    #[test]
-    fn part_visitor() {
-        struct V {
-            v: Vec<std::any::TypeId>,
-        }
-        impl EntityPartVisitor for V {
-            fn visit<EP: EntityPart>(&mut self) {
-                self.v.push(std::any::TypeId::of::<EP>());
-            }
-        }
-
-        let mut vis = V { v: vec![] };
-        SimpleEntity::accept_part_visitor(&mut vis);
-        assert_eq!(
-            vis.v.as_slice(),
-            &[std::any::TypeId::of::<SimpleEntityName>()]
-        );
-    }
-}
-
-#[cfg(test)]
-mod derive_tests {
-    #![allow(unused)]
-
-    use microrm_macros::{Database, Entity};
-    use super::{IDMap, AssocSet, Database};
-
-    #[derive(Entity)]
-    struct Role {
-        title: String,
-        permissions: String,
-    }
-
-    #[derive(Entity)]
-    struct Person {
-        #[unique]
-        name: String,
-        roles: AssocSet<Role>,
-    }
-
-    #[derive(Database)]
-    struct PeopleDB {
-        people: IDMap<Person>,
-    }
-
-    #[test]
-    fn collect_test() {
-        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"));
-    }
-}
-
-/*
-// pub trait
-
-pub struct Skill { }
-impl Entity for Skill {}
-
-pub struct MenuItem {
-    name: String,
-    required_skills: Set<Skill>,
-    cost: f32,
-}
-
-impl Entity for MenuItem {}
-
-struct EmployeeShiftMap;
-
-pub struct Employee {
-    assigned_shifts: NamedMap<EmployeeShiftMap, Shift>,
-}
-
-impl Entity for Employee {}
-
-pub struct Shift {
-    name: String,
-    employees: Set<Employee>,
-}
-
-impl Entity for Shift {}
-
-pub struct FoodTruckSchema {
-    menu: Collection<MenuItem>,
 }
 }
-*/

+ 3 - 3
microrm/src/entity.rs

@@ -1,4 +1,4 @@
-use std::{hash::Hash, fmt::Debug};
+use std::{fmt::Debug, hash::Hash};
 
 
 mod datum;
 mod datum;
 
 
@@ -48,8 +48,8 @@ pub trait EntityPartVisitor {
 
 
 /// A single database entity, aka an object type that gets its own table.
 /// A single database entity, aka an object type that gets its own table.
 pub trait Entity: 'static {
 pub trait Entity: 'static {
-    type Uniques : EntityDatumList;
-    type ID : EntityID<Entity = Self>;
+    type Uniques: EntityDatumList;
+    type ID: EntityID<Entity = Self>;
 
 
     fn entity_name() -> &'static str;
     fn entity_name() -> &'static str;
     fn accept_part_visitor(visitor: &mut impl EntityPartVisitor);
     fn accept_part_visitor(visitor: &mut impl EntityPartVisitor);

+ 11 - 7
microrm/src/entity/datum.rs

@@ -1,4 +1,4 @@
-use super::{EntityDatum,EntityDatumList};
+use super::{EntityDatum, EntityDatumList};
 
 
 impl EntityDatum for String {
 impl EntityDatum for String {
     fn sql_type() -> &'static str {
     fn sql_type() -> &'static str {
@@ -6,7 +6,8 @@ impl EntityDatum for String {
     }
     }
 
 
     fn bind_to<'a>(&self, stmt: &mut sqlite::Statement<'a>, index: usize) {
     fn bind_to<'a>(&self, stmt: &mut sqlite::Statement<'a>, index: usize) {
-        stmt.bind((index, self.as_str())).expect("couldn't bind string");
+        stmt.bind((index, self.as_str()))
+            .expect("couldn't bind string");
     }
     }
 }
 }
 
 
@@ -16,7 +17,8 @@ impl EntityDatum for usize {
     }
     }
 
 
     fn bind_to<'a>(&self, stmt: &mut sqlite::Statement<'a>, index: usize) {
     fn bind_to<'a>(&self, stmt: &mut sqlite::Statement<'a>, index: usize) {
-        stmt.bind((index, *self as i64)).expect("couldn't bind usize as i64");
+        stmt.bind((index, *self as i64))
+            .expect("couldn't bind usize as i64");
     }
     }
 }
 }
 
 
@@ -61,7 +63,7 @@ impl<T: EntityDatum> EntityDatum for Option<T> {
 }
 }
 
 
 impl EntityDatumList for () {
 impl EntityDatumList for () {
-    fn accept(&self, _: &mut impl super::EntityDatumListVisitor) { }
+    fn accept(&self, _: &mut impl super::EntityDatumListVisitor) {}
 }
 }
 
 
 impl<T: EntityDatum> EntityDatumList for T {
 impl<T: EntityDatum> EntityDatumList for T {
@@ -76,14 +78,14 @@ impl<T0: EntityDatum> EntityDatumList for (T0,) {
     }
     }
 }
 }
 
 
-impl<T0: EntityDatum, T1: EntityDatum> EntityDatumList for (T0,T1) {
+impl<T0: EntityDatum, T1: EntityDatum> EntityDatumList for (T0, T1) {
     fn accept(&self, visitor: &mut impl super::EntityDatumListVisitor) {
     fn accept(&self, visitor: &mut impl super::EntityDatumListVisitor) {
         visitor.visit(&self.0);
         visitor.visit(&self.0);
         visitor.visit(&self.1);
         visitor.visit(&self.1);
     }
     }
 }
 }
 
 
-impl<T0: EntityDatum, T1: EntityDatum, T2: EntityDatum> EntityDatumList for (T0,T1,T2) {
+impl<T0: EntityDatum, T1: EntityDatum, T2: EntityDatum> EntityDatumList for (T0, T1, T2) {
     fn accept(&self, visitor: &mut impl super::EntityDatumListVisitor) {
     fn accept(&self, visitor: &mut impl super::EntityDatumListVisitor) {
         visitor.visit(&self.0);
         visitor.visit(&self.0);
         visitor.visit(&self.1);
         visitor.visit(&self.1);
@@ -91,7 +93,9 @@ impl<T0: EntityDatum, T1: EntityDatum, T2: EntityDatum> EntityDatumList for (T0,
     }
     }
 }
 }
 
 
-impl<T0: EntityDatum, T1: EntityDatum, T2: EntityDatum, T3: EntityDatum> EntityDatumList for (T0,T1,T2,T3) {
+impl<T0: EntityDatum, T1: EntityDatum, T2: EntityDatum, T3: EntityDatum> EntityDatumList
+    for (T0, T1, T2, T3)
+{
     fn accept(&self, visitor: &mut impl super::EntityDatumListVisitor) {
     fn accept(&self, visitor: &mut impl super::EntityDatumListVisitor) {
         visitor.visit(&self.0);
         visitor.visit(&self.0);
         visitor.visit(&self.1);
         visitor.visit(&self.1);

+ 55 - 2
microrm/src/lib.rs

@@ -3,10 +3,63 @@ extern crate self as microrm;
 
 
 pub mod db;
 pub mod db;
 pub mod entity;
 pub mod entity;
-pub mod schema;
 mod query;
 mod query;
+pub mod schema;
 
 
 pub mod prelude {
 pub mod prelude {
-    pub use microrm_macros::{Database,Entity};
+    pub use microrm_macros::{Database, Entity};
     // pub use crate::entity::
     // pub use crate::entity::
 }
 }
+
+// ----------------------------------------------------------------------
+// Generically-useful database types
+// ----------------------------------------------------------------------
+
+#[derive(Debug)]
+pub enum DBError {
+    EmptyResult,
+    LogicError(&'static str),
+    Sqlite(sqlite::Error),
+}
+
+impl From<sqlite::Error> for DBError {
+    fn from(value: sqlite::Error) -> Self {
+        Self::Sqlite(value)
+    }
+}
+
+/*
+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> From<Option<T>> for DBResult<T> {
+    fn from(value: Option<T>) -> Self {
+        match value {
+            Some(v) => Self::Ok(v),
+            None => Self::Empty,
+        }
+    }
+}
+*/
+pub type DBResult<T> = Result<T, DBError>;

+ 45 - 69
microrm/src/query.rs

@@ -1,4 +1,9 @@
-use crate::{entity::{Entity, EntityPartVisitor, EntityPart, EntityDatum}, db::{IDMap, DBError, DBConnection}};
+use crate::{
+    db::DBConnection,
+    entity::{Entity, EntityDatum, EntityPart, EntityPartVisitor},
+    schema::IDMap,
+};
+use crate::{DBError, DBResult};
 use std::hash::{Hash, Hasher};
 use std::hash::{Hash, Hasher};
 
 
 #[derive(Hash)]
 #[derive(Hash)]
@@ -16,78 +21,49 @@ fn query_hash<E: Entity>(ctx: &'static str, qtype: QueryType) -> u64 {
     hasher.finish()
     hasher.finish()
 }
 }
 
 
-pub struct Query {
-    stmt: std::sync::Arc<sqlite::Statement<'static>>,
-}
-
-impl Query {
-    
-}
-
-pub(crate) struct QueryCache {
-    conn: DBConnection,
-    queries: std::collections::HashMap<u64, Query>,
-}
-
-impl QueryCache {
-    pub fn new(conn: DBConnection) -> Self {
-        Self {
-            conn,
-            queries: Default::default(),
-        }
-    }
-
-    fn with_query(&mut self, hash: u64, build: impl Fn(&DBConnection) -> Query) -> &mut Query {
-        self.queries.entry(hash).or_insert_with(|| build(&self.conn))
-    }
-
-    pub fn insert<E: Entity>(&mut self, map: &IDMap<E>, value: &E) -> Result<(), DBError> {
-        let query = self.with_query(query_hash::<E>(map.ctx(), QueryType::Insert), move |conn| {
-            let stmt = conn.prepare("").expect("couldn't prepare query");
-
-            Query {
-                stmt: std::sync::Arc::new(stmt),
+pub fn insert<E: Entity>(map: &IDMap<E>, value: &E) -> DBResult<()> {
+    map.conn().with_prepared(
+        query_hash::<E>(map.ctx(), QueryType::Insert),
+        || {
+            let table_name = format!("{}_{}", map.ctx(), E::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("?");
+                }
             }
             }
-        });
-        todo!()
-    }
-}
-
-pub fn insert<E: Entity>(map: &IDMap<E>, value: &E) -> Result<(), DBError> {
-    let table_name = format!("{}_{}", map.ctx(), E::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(", ");
+            E::accept_part_visitor(&mut PartNameVisitor(&mut part_names, &mut placeholders));
+
+            format!(
+                "insert into `{}` ({}) values ({})",
+                table_name, part_names, placeholders
+            )
+        },
+        |prepared| {
+            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;
+                }
             }
             }
-            self.0.push_str("`");
-            self.0.push_str(EP::part_name());
-            self.0.push_str("`");
-            self.1.push_str("?");
-        }
-    }
-
-    E::accept_part_visitor(&mut PartNameVisitor(&mut part_names, &mut placeholders));
-
-    let query_string = format!("insert into `{}` ({}) values ({})", table_name, part_names, placeholders);
-
-    let mut prepared = map.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));
+            value.accept_part_visitor_ref(&mut PartBinder(prepared, 1));
 
 
-    prepared.next();
+            prepared.next()?;
 
 
-    Ok(())
+            DBResult::Ok(())
+        },
+    )
 }
 }

+ 136 - 11
microrm/src/schema.rs

@@ -1,7 +1,13 @@
-use crate::db::{Database, DatabaseItem, DatabaseItemVisitor, DBConnection};
+use crate::{
+    db::{Connection, DBConnection},
+    entity::{Entity, EntityDatum, EntityVisitor},
+    query,
+};
+use crate::{DBError, DBResult};
 
 
 mod entity;
 mod entity;
 pub(crate) mod meta;
 pub(crate) mod meta;
+mod tests;
 
 
 #[derive(Debug)]
 #[derive(Debug)]
 struct ColumnInfo {
 struct ColumnInfo {
@@ -65,28 +71,36 @@ impl DatabaseSchema {
         let metadb = meta::MetadataDB::build(db.clone());
         let metadb = meta::MetadataDB::build(db.clone());
 
 
         // check to see if the signature exists and matches
         // 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)
+        metadb
+            .kv_metastore
+            .lookup_unique(&Self::SCHEMA_SIGNATURE_KEY.to_string())
+            .ok()
+            .flatten()
+            .map(|kv| kv.value.parse::<u64>().unwrap_or(0) == self.signature)
+            .unwrap_or(false)
     }
     }
 
 
     pub fn create(&self, db: DBConnection) {
     pub fn create(&self, db: DBConnection) {
         for query in self.queries.iter() {
         for query in self.queries.iter() {
-            db.execute(query).expect("Couldn't run creation query!");
+            db.execute_raw(query).expect("Couldn't run creation query!");
         }
         }
 
 
         // attempt to use connection as a MetadataDB database
         // attempt to use connection as a MetadataDB database
         let metadb = meta::MetadataDB::build(db.clone());
         let metadb = meta::MetadataDB::build(db.clone());
 
 
         for query in collect_from_database::<meta::MetadataDB>().queries.iter() {
         for query in collect_from_database::<meta::MetadataDB>().queries.iter() {
-            db.execute(query).expect("Couldn't run MetadataDB creation query!");
+            db.execute_raw(query)
+                .expect("Couldn't run MetadataDB creation query!");
         }
         }
 
 
         // store signature
         // store signature
-        metadb.kv_metastore.insert(&meta::KV {
-            key: Self::SCHEMA_SIGNATURE_KEY.into(),
-            value: format!("{}", self.signature)
-        });
+        metadb
+            .kv_metastore
+            .insert(&meta::KV {
+                key: Self::SCHEMA_SIGNATURE_KEY.into(),
+                value: format!("{}", self.signature),
+            })
+            .expect("couldn't set schema signature");
     }
     }
 }
 }
 
 
@@ -202,6 +216,117 @@ pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
 
 
     DatabaseSchema {
     DatabaseSchema {
         signature: signature_hasher.finish(),
         signature: signature_hasher.finish(),
-        queries
+        queries,
+    }
+}
+
+// ----------------------------------------------------------------------
+// Specification types
+// ----------------------------------------------------------------------
+
+pub struct IDMap<T: Entity> {
+    conn: DBConnection,
+    ctx: &'static str,
+    _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(crate) fn conn(&self) -> &DBConnection {
+        &self.conn
+    }
+
+    pub(crate) fn ctx(&self) -> &'static str {
+        self.ctx
+    }
+
+    pub fn lookup_unique(&self, uniques: &T::Uniques) -> DBResult<Option<T>> {
+        // TODO
+        Ok(None)
+    }
+
+    pub fn insert(&self, value: &T) -> DBResult<()> {
+        query::insert(self, value)
+    }
+}
+
+pub struct Index<T: Entity, Key: EntityDatum> {
+    _conn: DBConnection,
+    _ghost: std::marker::PhantomData<(T, Key)>,
+}
+
+pub struct AssocSet<T: Entity> {
+    _ghost: std::marker::PhantomData<T>,
+}
+
+impl<T: Entity> EntityDatum for AssocSet<T> {
+    fn sql_type() -> &'static str {
+        unreachable!()
     }
     }
+
+    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];
+
+    fn is_index() -> bool {
+        false
+    }
+    fn index_over() -> &'static str {
+        unreachable!()
+    }
+    fn index_columns() -> &'static [&'static str] {
+        unreachable!()
+    }
+
+    fn accept_entity_visitor(visitor: &mut impl EntityVisitor);
+}
+
+pub trait DatabaseItemVisitor {
+    fn visit<DI: DatabaseItem>(&mut self)
+    where
+        Self: Sized;
+}
+
+pub trait DatabaseSpec {
+    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>()
+    }
+}
+
+pub trait Database {
+    fn open_path<U: AsRef<str>>(uri: U) -> Result<Self, DBError>
+    where
+        Self: Sized,
+    {
+        Connection::open(uri).map(Self::build)
+    }
+
+    #[doc(hidden)]
+    fn build(conn: DBConnection) -> Self
+    where
+        Self: Sized;
+
+    fn accept_item_visitor(visitor: &mut impl DatabaseItemVisitor)
+    where
+        Self: Sized;
 }
 }

+ 1 - 1
microrm/src/schema/meta.rs

@@ -1,4 +1,4 @@
-use crate::db::IDMap;
+use crate::schema::IDMap;
 
 
 #[derive(microrm_macros::Entity)]
 #[derive(microrm_macros::Entity)]
 pub struct KV {
 pub struct KV {

+ 175 - 0
microrm/src/schema/tests.rs

@@ -0,0 +1,175 @@
+mod manual_test_db {
+    // simple hand-built database example
+    #![allow(unused)]
+
+    use crate::db::DBConnection;
+    use crate::entity::{Entity, EntityID, EntityPart, EntityPartVisitor, EntityVisitor};
+    use crate::schema::{Database, DatabaseItem, DatabaseItemVisitor, IDMap};
+
+    struct SimpleEntity {
+        name: String,
+    }
+
+    #[derive(Clone, Copy, PartialEq, PartialOrd, Debug, Hash)]
+    struct SimpleEntityID(usize);
+
+    impl EntityID for SimpleEntityID {
+        type Entity = SimpleEntity;
+
+        fn from_raw(id: usize) -> Self {
+            Self(id)
+        }
+        fn into_raw(self) -> usize {
+            self.0
+        }
+    }
+
+    struct SimpleEntityName;
+    impl EntityPart for SimpleEntityName {
+        type Datum = String;
+        type Entity = SimpleEntity;
+        fn part_name() -> &'static str {
+            "name"
+        }
+    }
+
+    impl Entity for SimpleEntity {
+        type Uniques = String;
+        type ID = SimpleEntityID;
+
+        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 {
+        strings: IDMap<SimpleEntity>,
+    }
+
+    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,
+        {
+            struct SimpleDatabaseStringsItem;
+            impl DatabaseItem for SimpleDatabaseStringsItem {
+                fn item_key() -> &'static str {
+                    "strings"
+                }
+                fn dependency_keys() -> &'static [&'static str] {
+                    &[]
+                }
+
+                fn accept_entity_visitor(visitor: &mut impl EntityVisitor) {
+                    visitor.visit::<SimpleEntity>();
+                }
+            }
+
+            visitor.visit::<SimpleDatabaseStringsItem>();
+        }
+    }
+
+    #[test]
+    fn part_visitor() {
+        struct V {
+            v: Vec<std::any::TypeId>,
+        }
+        impl EntityPartVisitor for V {
+            fn visit<EP: EntityPart>(&mut self) {
+                self.v.push(std::any::TypeId::of::<EP>());
+            }
+        }
+
+        let mut vis = V { v: vec![] };
+        SimpleEntity::accept_part_visitor(&mut vis);
+        assert_eq!(
+            vis.v.as_slice(),
+            &[std::any::TypeId::of::<SimpleEntityName>()]
+        );
+    }
+}
+
+mod derive_tests {
+    #![allow(unused)]
+
+    use crate::schema::{AssocSet, Database, IDMap};
+    use microrm_macros::{Database, Entity};
+
+    #[derive(Entity)]
+    struct Role {
+        title: String,
+        permissions: String,
+    }
+
+    #[derive(Entity)]
+    struct Person {
+        #[unique]
+        name: String,
+        roles: AssocSet<Role>,
+    }
+
+    #[derive(Database)]
+    struct PeopleDB {
+        people: IDMap<Person>,
+    }
+
+    #[test]
+    fn collect_test() {
+        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").to_str().unwrap());
+    }
+}
+
+/*
+// pub trait
+
+pub struct Skill { }
+impl Entity for Skill {}
+
+pub struct MenuItem {
+    name: String,
+    required_skills: Set<Skill>,
+    cost: f32,
+}
+
+impl Entity for MenuItem {}
+
+struct EmployeeShiftMap;
+
+pub struct Employee {
+    assigned_shifts: NamedMap<EmployeeShiftMap, Shift>,
+}
+
+impl Entity for Employee {}
+
+pub struct Shift {
+    name: String,
+    employees: Set<Employee>,
+}
+
+impl Entity for Shift {}
+
+pub struct FoodTruckSchema {
+    menu: Collection<MenuItem>,
+}
+*/