Browse Source

Reorganization.

Kestrel 1 year ago
parent
commit
bd10cd25e8

+ 7 - 1
microrm/src/entity.rs

@@ -3,6 +3,7 @@ use std::{fmt::Debug, hash::Hash};
 use crate::{db::DBConnection, DBResult};
 
 mod datum;
+pub(crate) mod helpers;
 
 /// Integral identifier for an entity.
 pub trait EntityID: 'static + PartialEq + Hash + PartialOrd + Debug + Copy {
@@ -26,6 +27,7 @@ pub trait EntityDatum: 'static {
     fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, index: usize);
     fn build_from<'a>(
         conn: &DBConnection,
+        ctx: &'static str,
         stmt: &mut sqlite::Statement<'a>,
         index: usize,
     ) -> DBResult<(Self, usize)>
@@ -38,7 +40,11 @@ pub trait EntityDatum: 'static {
 /// A fixed-length list of EntityDatums, usually a tuple.
 pub trait EntityDatumList {
     fn accept(&self, visitor: &mut impl EntityDatumVisitor);
-    fn build_from<'a>(conn: &DBConnection, stmt: &mut sqlite::Statement<'a>) -> DBResult<Self>
+    fn build_from<'a>(
+        conn: &DBConnection,
+        ctx: &'static str,
+        stmt: &mut sqlite::Statement<'a>,
+    ) -> DBResult<Self>
     where
         Self: Sized;
 }

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

@@ -14,6 +14,7 @@ impl EntityDatum for String {
 
     fn build_from<'a>(
         _: &DBConnection,
+        _: &'static str,
         stmt: &mut sqlite::Statement<'a>,
         index: usize,
     ) -> DBResult<(Self, usize)>
@@ -36,6 +37,7 @@ impl EntityDatum for usize {
 
     fn build_from<'a>(
         _: &DBConnection,
+        _: &'static str,
         stmt: &mut sqlite::Statement<'a>,
         index: usize,
     ) -> DBResult<(Self, usize)>
@@ -57,6 +59,7 @@ impl EntityDatum for isize {
 
     fn build_from<'a>(
         _: &DBConnection,
+        _: &'static str,
         stmt: &mut sqlite::Statement<'a>,
         index: usize,
     ) -> DBResult<(Self, usize)>
@@ -78,6 +81,7 @@ impl EntityDatum for u64 {
 
     fn build_from<'a>(
         _: &DBConnection,
+        _: &'static str,
         stmt: &mut sqlite::Statement<'a>,
         index: usize,
     ) -> DBResult<(Self, usize)>
@@ -99,6 +103,7 @@ impl EntityDatum for i64 {
 
     fn build_from<'a>(
         _: &DBConnection,
+        _: &'static str,
         stmt: &mut sqlite::Statement<'a>,
         index: usize,
     ) -> DBResult<(Self, usize)>
@@ -120,6 +125,7 @@ impl<T: EntityDatum> EntityDatum for Option<T> {
 
     fn build_from<'a>(
         _: &DBConnection,
+        _: &'static str,
         _stmt: &mut sqlite::Statement<'a>,
         _index: usize,
     ) -> DBResult<(Self, usize)>

+ 41 - 17
microrm/src/entity/datum_list.rs

@@ -4,7 +4,11 @@ use crate::{db::DBConnection, DBResult};
 impl EntityDatumList for () {
     fn accept(&self, _: &mut impl EntityDatumVisitor) {}
 
-    fn build_from<'a>(_conn: &DBConnection, _stmt: &mut sqlite::Statement<'a>) -> DBResult<Self>
+    fn build_from<'a>(
+        _conn: &DBConnection,
+        _ctx: &'static str,
+        _stmt: &mut sqlite::Statement<'a>,
+    ) -> DBResult<Self>
     where
         Self: Sized,
     {
@@ -17,11 +21,15 @@ impl<T: EntityDatum> EntityDatumList for T {
         visitor.visit(self);
     }
 
-    fn build_from<'a>(conn: &DBConnection, stmt: &mut sqlite::Statement<'a>) -> DBResult<Self>
+    fn build_from<'a>(
+        conn: &DBConnection,
+        ctx: &'static str,
+        stmt: &mut sqlite::Statement<'a>,
+    ) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok(T::build_from(conn, stmt, 0)?.0)
+        Ok(T::build_from(conn, ctx, stmt, 0)?.0)
     }
 }
 
@@ -30,11 +38,15 @@ impl<T0: EntityDatum> EntityDatumList for (T0,) {
         visitor.visit(&self.0);
     }
 
-    fn build_from<'a>(conn: &DBConnection, stmt: &mut sqlite::Statement<'a>) -> DBResult<Self>
+    fn build_from<'a>(
+        conn: &DBConnection,
+        ctx: &'static str,
+        stmt: &mut sqlite::Statement<'a>,
+    ) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok((T0::build_from(conn, stmt, 0)?.0,))
+        Ok((T0::build_from(conn, ctx, stmt, 0)?.0,))
     }
 }
 
@@ -44,12 +56,16 @@ impl<T0: EntityDatum, T1: EntityDatum> EntityDatumList for (T0, T1) {
         visitor.visit(&self.1);
     }
 
-    fn build_from<'a>(conn: &DBConnection, stmt: &mut sqlite::Statement<'a>) -> DBResult<Self>
+    fn build_from<'a>(
+        conn: &DBConnection,
+        ctx: &'static str,
+        stmt: &mut sqlite::Statement<'a>,
+    ) -> DBResult<Self>
     where
         Self: Sized,
     {
-        let (d0, idx) = T0::build_from(conn, stmt, 0)?;
-        let (d1, _) = T1::build_from(conn, stmt, idx)?;
+        let (d0, idx) = T0::build_from(conn, ctx, stmt, 0)?;
+        let (d1, _) = T1::build_from(conn, ctx, stmt, idx)?;
         Ok((d0, d1))
     }
 }
@@ -61,13 +77,17 @@ impl<T0: EntityDatum, T1: EntityDatum, T2: EntityDatum> EntityDatumList for (T0,
         visitor.visit(&self.2);
     }
 
-    fn build_from<'a>(conn: &DBConnection, stmt: &mut sqlite::Statement<'a>) -> DBResult<Self>
+    fn build_from<'a>(
+        conn: &DBConnection,
+        ctx: &'static str,
+        stmt: &mut sqlite::Statement<'a>,
+    ) -> DBResult<Self>
     where
         Self: Sized,
     {
-        let (d0, idx) = T0::build_from(conn, stmt, 0)?;
-        let (d1, idx) = T1::build_from(conn, stmt, idx)?;
-        let (d2, _) = T2::build_from(conn, stmt, idx)?;
+        let (d0, idx) = T0::build_from(conn, ctx, stmt, 0)?;
+        let (d1, idx) = T1::build_from(conn, ctx, stmt, idx)?;
+        let (d2, _) = T2::build_from(conn, ctx, stmt, idx)?;
         Ok((d0, d1, d2))
     }
 }
@@ -82,14 +102,18 @@ impl<T0: EntityDatum, T1: EntityDatum, T2: EntityDatum, T3: EntityDatum> EntityD
         visitor.visit(&self.3);
     }
 
-    fn build_from<'a>(conn: &DBConnection, stmt: &mut sqlite::Statement<'a>) -> DBResult<Self>
+    fn build_from<'a>(
+        conn: &DBConnection,
+        ctx: &'static str,
+        stmt: &mut sqlite::Statement<'a>,
+    ) -> DBResult<Self>
     where
         Self: Sized,
     {
-        let (d0, idx) = T0::build_from(conn, stmt, 0)?;
-        let (d1, idx) = T1::build_from(conn, stmt, idx)?;
-        let (d2, idx) = T2::build_from(conn, stmt, idx)?;
-        let (d3, _) = T3::build_from(conn, stmt, idx)?;
+        let (d0, idx) = T0::build_from(conn, ctx, stmt, 0)?;
+        let (d1, idx) = T1::build_from(conn, ctx, stmt, idx)?;
+        let (d2, idx) = T2::build_from(conn, ctx, stmt, idx)?;
+        let (d3, _) = T3::build_from(conn, ctx, stmt, idx)?;
         Ok((d0, d1, d2, d3))
     }
 }

+ 15 - 0
microrm/src/entity/helpers.rs

@@ -0,0 +1,15 @@
+use crate::entity::{Entity, EntityDatum, EntityVisitor};
+
+use super::EntityPart;
+
+pub fn check_assoc<EP: EntityPart>() -> bool {
+    struct Checker(bool);
+    impl EntityVisitor for Checker {
+        fn visit<E: Entity>(&mut self) {
+            self.0 = true;
+        }
+    }
+    let mut checker = Checker(false);
+    <EP::Datum as EntityDatum>::accept_entity_visitor(&mut checker);
+    checker.0
+}

+ 23 - 16
microrm/src/query.rs

@@ -1,3 +1,4 @@
+use crate::entity::helpers::check_assoc;
 use crate::DBResult;
 use crate::{
     entity::{
@@ -24,18 +25,6 @@ fn query_hash<E: Entity>(ctx: &'static str, qtype: QueryType) -> u64 {
     hasher.finish()
 }
 
-struct CollectColumns<'a>(&'a mut String);
-impl<'a> EntityPartVisitor for CollectColumns<'a> {
-    fn visit<EP: EntityPart>(&mut self) {
-        if self.0.len() != 0 {
-            self.0.push_str(", ");
-        }
-        self.0.push_str("`");
-        self.0.push_str(EP::part_name());
-        self.0.push_str("`");
-    }
-}
-
 pub fn select_by<E: Entity, PL: EntityPartList>(
     map: &IDMap<E>,
     by: &PL::DatumList,
@@ -68,7 +57,7 @@ pub fn select_by<E: Entity, PL: EntityPartList>(
             PL::accept_part_visitor(&mut BuildConditions(&mut conditions));
 
             let table_name = format!("{}_{}", map.ctx(), E::entity_name());
-            format!("select * from `{}` where {}", table_name, conditions)
+            format!("select rowid, * from `{}` where {}", table_name, conditions)
         },
         |stmt| {
             stmt.reset()?;
@@ -79,15 +68,22 @@ pub fn select_by<E: Entity, PL: EntityPartList>(
                     self.1 += 1
                 }
             }
+            // note that this indexing starts at 1
             by.accept(&mut BindDatum(stmt, 1));
 
             // now we grab the statement outputs
-
+            let mut rows = vec![];
             while stmt.next()? == sqlite::State::Row {
-                println!("Read row!");
+                let datum_list =
+                    <<E::Parts as EntityPartList>::DatumList as EntityDatumList>::build_from(
+                        &map.conn(),
+                        map.ctx(),
+                        stmt,
+                    )?;
+                rows.push(E::build(datum_list));
             }
 
-            Ok(vec![])
+            Ok(rows)
         },
     )
 }
@@ -103,6 +99,12 @@ pub fn insert<E: Entity>(map: &IDMap<E>, value: &E) -> DBResult<()> {
             struct PartNameVisitor<'a>(&'a mut String, &'a mut String);
             impl<'a> EntityPartVisitor for PartNameVisitor<'a> {
                 fn visit<EP: EntityPart>(&mut self) {
+                    // if this is a set-association, then we don't actually want to do anything
+                    // with it here; it doesn't have a column
+                    if check_assoc::<EP>() {
+                        return;
+                    }
+
                     if self.0.len() != 0 {
                         self.0.push_str(", ");
                         self.1.push_str(", ");
@@ -127,6 +129,11 @@ pub fn insert<E: Entity>(map: &IDMap<E>, value: &E) -> DBResult<()> {
             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, datum: &EP::Datum) {
+                    // skip associations, as with the query preparation above
+                    if check_assoc::<EP>() {
+                        return;
+                    }
+
                     datum.bind_to(self.0, self.1);
                     self.1 += 1;
                 }

+ 31 - 213
microrm/src/schema.rs

@@ -5,218 +5,11 @@ use crate::{
 };
 use crate::{DBError, DBResult};
 
+mod build;
 mod entity;
 pub(crate) mod meta;
 mod tests;
 
-#[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);
-
-        // check to see if the signature exists and matches
-        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) {
-        for query in self.queries.iter() {
-            db.execute_raw(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_raw(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),
-            })
-            .expect("couldn't set schema signature");
-    }
-}
-
-pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
-    struct IV(entity::EntityStateContainer);
-
-    impl DatabaseItemVisitor for IV {
-        fn visit<DI: DatabaseItem>(&mut self)
-        where
-            Self: Sized,
-        {
-            DI::accept_entity_visitor(&mut self.0.make_context(DI::item_key()));
-        }
-    }
-
-    let mut iv = IV(entity::EntityStateContainer::default());
-
-    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);
-    }
-
-    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,
-    }
-}
-
 // ----------------------------------------------------------------------
 // Specification types
 // ----------------------------------------------------------------------
@@ -244,6 +37,11 @@ impl<T: Entity> IDMap<T> {
         self.ctx
     }
 
+    pub fn by_id(&self, _id: T::ID) -> DBResult<Option<T>> {
+        // query::select_by
+        todo!()
+    }
+
     pub fn lookup_unique(
         &self,
         uniques: &<<T as Entity>::Uniques as EntityPartList>::DatumList,
@@ -267,13 +65,21 @@ pub struct Index<T: Entity, Key: EntityDatum> {
     _ghost: std::marker::PhantomData<(T, Key)>,
 }
 
+pub(crate) struct AssocData {
+    pub(crate) conn: DBConnection,
+    pub(crate) ctx: &'static str,
+    pub(crate) base_rowid: Option<i64>,
+}
+
 pub struct AssocSet<T: Entity> {
+    data: Option<AssocData>,
     _ghost: std::marker::PhantomData<T>,
 }
 
 impl<T: Entity> AssocSet<T> {
     pub fn empty() -> Self {
         Self {
+            data: None,
             _ghost: Default::default(),
         }
     }
@@ -293,14 +99,26 @@ impl<T: Entity> EntityDatum for AssocSet<T> {
     }
 
     fn build_from<'a>(
-        _conn: &DBConnection,
-        _stmt: &mut sqlite::Statement<'a>,
-        _index: usize,
+        conn: &DBConnection,
+        ctx: &'static str,
+        stmt: &mut sqlite::Statement<'a>,
+        index: usize,
     ) -> DBResult<(Self, usize)>
     where
         Self: Sized,
     {
-        todo!()
+        // assuming that the stmt has the rowid as index 0
+        Ok((
+            Self {
+                data: Some(AssocData {
+                    conn: conn.clone(),
+                    ctx,
+                    base_rowid: stmt.read(0)?,
+                }),
+                _ghost: Default::default(),
+            },
+            index,
+        ))
     }
 }
 
@@ -343,7 +161,7 @@ pub trait Database {
         Self: Sized,
     {
         let conn = Connection::open(uri)?;
-        let schema = collect_from_database::<Self>();
+        let schema = build::collect_from_database::<Self>();
         if !schema.check(conn.clone()) {
             schema.create(conn.clone())
         }

+ 212 - 0
microrm/src/schema/build.rs

@@ -0,0 +1,212 @@
+use crate::schema::{
+    entity::{EntityStateContainer, PartType},
+    meta, DBConnection, Database, DatabaseItem, DatabaseItemVisitor,
+};
+
+#[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(crate) 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);
+
+        // check to see if the signature exists and matches
+        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) {
+        for query in self.queries.iter() {
+            db.execute_raw(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_raw(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),
+            })
+            .expect("couldn't set schema signature");
+    }
+}
+
+pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
+    struct IV(EntityStateContainer);
+
+    impl DatabaseItemVisitor for IV {
+        fn visit<DI: DatabaseItem>(&mut self)
+        where
+            Self: Sized,
+        {
+            DI::accept_entity_visitor(&mut self.0.make_context(DI::item_key()));
+        }
+    }
+
+    let mut iv = IV(EntityStateContainer::default());
+
+    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 {
+                PartType::Datum(dtype) => {
+                    table.columns.push(ColumnInfo {
+                        name: part.name,
+                        ty: dtype.into(),
+                        fkey: None,
+                        unique: false, // XXX
+                    })
+                }
+                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);
+    }
+
+    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,
+    }
+}

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

@@ -140,7 +140,7 @@ mod derive_tests {
 
     #[test]
     fn collect_test() {
-        microrm::schema::collect_from_database::<PeopleDB>();
+        microrm::schema::build::collect_from_database::<PeopleDB>();
     }
 
     #[test]
@@ -150,8 +150,7 @@ mod derive_tests {
 
     #[test]
     fn check_for_inserted() {
-        let db = PeopleDB::open_path(std::path::Path::new(":memory:").to_str().unwrap())
-            .expect("couldn't open database");
+        let db = PeopleDB::open_path("/tmp/schema.db").expect("couldn't open database");
         let name_string = "name_here".to_string();
         // check that it isn't in the database before we insert it
         assert!(db
@@ -161,17 +160,19 @@ mod derive_tests {
             .flatten()
             .is_none());
 
-        db.people.insert(&Person {
-            name: name_string.clone(),
-            roles: AssocSet::empty(),
-        });
+        db.people
+            .insert(&Person {
+                name: name_string.clone(),
+                roles: AssocSet::empty(),
+            })
+            .expect("failed to insert");
 
-        // check that it isn't in the database before we insert it
+        // check that it is in the database after we insert it
         assert!(db
             .people
             .lookup_unique(&name_string)
             .ok()
             .flatten()
-            .is_none());
+            .is_some());
     }
 }