Browse Source

Sketched out datum discrimination for type-specific behaviour.

Kestrel 1 year ago
parent
commit
522cf9ba00

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

@@ -68,9 +68,8 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
     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)
+            #item_name : #item_type :: build(conn.clone())
         }
     });
 

+ 4 - 0
microrm-macros/src/entity.rs

@@ -169,6 +169,10 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
                 let raw = <i64 as ::microrm::datum::Datum>::build_from(adata, stmt, index)?;
                 Ok((Self(raw.0), raw.1))
             }
+
+            fn accept_discriminator(d: &mut impl ::microrm::schema::DatumDiscriminator) where Self: Sized {
+                d.visit_entity_id::<#entity_ident>();
+            }
         }
 
         impl ::microrm::entity::Entity for #entity_ident {

+ 5 - 2
microrm/src/datum.rs

@@ -1,4 +1,4 @@
-use crate::{entity::EntityVisitor, schema::AssocData, DBResult};
+use crate::{entity::EntityVisitor, schema::{AssocData, DatumDiscriminator}, DBResult};
 
 // ----------------------------------------------------------------------
 // Datum and related types
@@ -18,6 +18,9 @@ pub trait Datum: 'static {
         Self: Sized;
 
     fn accept_entity_visitor(_: &mut impl EntityVisitor) {}
+    fn accept_discriminator(d: &mut impl DatumDiscriminator) where Self: Sized {
+        d.visit_bare_field::<Self>();
+    }
 }
 
 /// A fixed-length list of EntityDatums, usually a tuple.
@@ -25,7 +28,7 @@ pub trait DatumList {
     fn accept(&self, visitor: &mut impl DatumVisitor);
 }
 
-/// A walker for a EntityDatumList instance.
+/// A walker for a DatumList instance.
 pub trait DatumVisitor {
     fn visit<ED: Datum>(&mut self, datum: &ED);
 }

+ 0 - 1
microrm/src/entity.rs

@@ -45,7 +45,6 @@ pub trait EntityPartList: 'static {
 
     fn build_datum_list(
         conn: &DBConnection,
-        ctx: &'static str,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList>;
 

+ 23 - 32
microrm/src/entity/part_list.rs

@@ -3,11 +3,10 @@ use crate::{db::DBConnection, schema::AssocData, DBResult};
 use super::{Datum, Entity, EntityPart, EntityPartList, EntityPartVisitor};
 
 macro_rules! build_datum {
-    ($conn:ident, $ctx:ident, $base_rowid:ident,$stmt:ident,$idx:ident,$d:ident,$t:ident) => {
+    ($conn:ident, $base_rowid:ident,$stmt:ident,$idx:ident,$d:ident,$t:ident) => {
         let ($d, $idx) = <$t::Datum as Datum>::build_from(
             AssocData {
                 conn: $conn.clone(),
-                ctx: $ctx,
                 base_name: <$t::Entity as Entity>::entity_name(),
                 part_name: $t::part_name(),
                 base_rowid: $base_rowid,
@@ -23,7 +22,6 @@ impl EntityPartList for () {
 
     fn build_datum_list(
         _conn: &DBConnection,
-        _ctx: &'static str,
         _stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
         Ok(())
@@ -38,12 +36,11 @@ impl<P0: EntityPart> EntityPartList for P0 {
 
     fn build_datum_list(
         conn: &DBConnection,
-        ctx: &'static str,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
         let base_rowid: i64 = stmt.read(0)?;
         let idx = 1; // starting index is 1 since index 0 is the ID
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d0, P0);
+        build_datum!(conn, base_rowid, stmt, idx, d0, P0);
 
         let _ = idx;
         Ok(d0)
@@ -62,10 +59,9 @@ impl<P0: EntityPart> EntityPartList for (P0,) {
 
     fn build_datum_list(
         conn: &DBConnection,
-        ctx: &'static str,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
-        <P0 as EntityPartList>::build_datum_list(conn, ctx, stmt)
+        <P0 as EntityPartList>::build_datum_list(conn, stmt)
     }
 
     fn accept_part_visitor(v: &mut impl EntityPartVisitor) {
@@ -81,13 +77,12 @@ impl<P0: EntityPart, P1: EntityPart> EntityPartList for (P0, P1) {
 
     fn build_datum_list(
         conn: &DBConnection,
-        ctx: &'static str,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
         let base_rowid: i64 = stmt.read(0)?;
         let idx = 1; // starting index is 1 since index 0 is the ID
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d0, P0);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d1, P1);
+        build_datum!(conn, base_rowid, stmt, idx, d0, P0);
+        build_datum!(conn, base_rowid, stmt, idx, d1, P1);
 
         let _ = idx;
         Ok((d0, d1))
@@ -108,14 +103,13 @@ impl<P0: EntityPart, P1: EntityPart, P2: EntityPart> EntityPartList for (P0, P1,
 
     fn build_datum_list(
         conn: &DBConnection,
-        ctx: &'static str,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
         let base_rowid: i64 = stmt.read(0)?;
         let idx = 1; // starting index is 1 since index 0 is the ID
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d0, P0);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d1, P1);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d2, P2);
+        build_datum!(conn, base_rowid, stmt, idx, d0, P0);
+        build_datum!(conn, base_rowid, stmt, idx, d1, P1);
+        build_datum!(conn, base_rowid, stmt, idx, d2, P2);
 
         let _ = idx;
         Ok((d0, d1, d2))
@@ -140,15 +134,14 @@ impl<P0: EntityPart, P1: EntityPart, P2: EntityPart, P3: EntityPart> EntityPartL
 
     fn build_datum_list(
         conn: &DBConnection,
-        ctx: &'static str,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
         let base_rowid: i64 = stmt.read(0)?;
         let idx = 1; // starting index is 1 since index 0 is the ID
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d0, P0);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d1, P1);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d2, P2);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d3, P3);
+        build_datum!(conn, base_rowid, stmt, idx, d0, P0);
+        build_datum!(conn, base_rowid, stmt, idx, d1, P1);
+        build_datum!(conn, base_rowid, stmt, idx, d2, P2);
+        build_datum!(conn, base_rowid, stmt, idx, d3, P3);
 
         let _ = idx;
         Ok((d0, d1, d2, d3))
@@ -175,16 +168,15 @@ impl<P0: EntityPart, P1: EntityPart, P2: EntityPart, P3: EntityPart, P4: EntityP
 
     fn build_datum_list(
         conn: &DBConnection,
-        ctx: &'static str,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
         let base_rowid: i64 = stmt.read(0)?;
         let idx = 1; // starting index is 1 since index 0 is the ID
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d0, P0);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d1, P1);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d2, P2);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d3, P3);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d4, P4);
+        build_datum!(conn, base_rowid, stmt, idx, d0, P0);
+        build_datum!(conn, base_rowid, stmt, idx, d1, P1);
+        build_datum!(conn, base_rowid, stmt, idx, d2, P2);
+        build_datum!(conn, base_rowid, stmt, idx, d3, P3);
+        build_datum!(conn, base_rowid, stmt, idx, d4, P4);
 
         let _ = idx;
         Ok((d0, d1, d2, d3, d4))
@@ -226,17 +218,16 @@ impl<
 
     fn build_datum_list(
         conn: &DBConnection,
-        ctx: &'static str,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
         let base_rowid: i64 = stmt.read(0)?;
         let idx = 1; // starting index is 1 since index 0 is the ID
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d0, P0);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d1, P1);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d2, P2);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d3, P3);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d4, P4);
-        build_datum!(conn, ctx, base_rowid, stmt, idx, d5, P5);
+        build_datum!(conn, base_rowid, stmt, idx, d0, P0);
+        build_datum!(conn, base_rowid, stmt, idx, d1, P1);
+        build_datum!(conn, base_rowid, stmt, idx, d2, P2);
+        build_datum!(conn, base_rowid, stmt, idx, d3, P3);
+        build_datum!(conn, base_rowid, stmt, idx, d4, P4);
+        build_datum!(conn, base_rowid, stmt, idx, d5, P5);
 
         let _ = idx;
         Ok((d0, d1, d2, d3, d4, d5))

+ 12 - 17
microrm/src/query.rs

@@ -16,12 +16,11 @@ enum QueryType<'a> {
     InsertAssoc(&'a str),
 }
 
-fn query_hash<E: Entity>(ctx: &'static str, qtype: QueryType) -> u64 {
+fn query_hash<E: Entity>(qtype: QueryType) -> u64 {
     let mut hasher = std::collections::hash_map::DefaultHasher::new();
 
     qtype.hash(&mut hasher);
     std::any::TypeId::of::<E>().hash(&mut hasher);
-    ctx.hash(&mut hasher);
 
     hasher.finish()
 }
@@ -39,20 +38,19 @@ pub(crate) fn select_assoc<E: Entity>(map: &AssocMap<E>) -> DBResult<Vec<E>> {
     // LEFT JOIN `target_table_name` ON `assoc_table_name`.`target` = `target_table_name`.`rowid`
     // WHERE `base` = base_rowid
 
-    let ctx = adata.ctx;
     let base_name = adata.base_name;
     let target_name = E::entity_name();
     let part_name = adata.part_name;
 
     map.conn().with_prepared(
-        query_hash::<E>(map.ctx(), QueryType::SelectJoin(part_name)),
+        query_hash::<E>(QueryType::SelectJoin(part_name)),
         || {
             let assoc_name = format!("{base_name}_assoc_{part_name}_{target_name}");
             format!(
-                "select `{ctx}_{target_name}`.* from `{ctx}_{assoc_name}` \
-                    left join `{ctx}_{target_name}` on \
-                        `{ctx}_{assoc_name}`.`target` = `{ctx}_{target_name}`.`ID` \
-                    where `{ctx}_{assoc_name}`.`base` = ?"
+                "select `{target_name}`.* from `{assoc_name}` \
+                    left join `{target_name}` on \
+                        `{assoc_name}`.`target` = `{target_name}`.`ID` \
+                    where `{assoc_name}`.`base` = ?"
             )
         },
         |stmt| {
@@ -61,7 +59,7 @@ pub(crate) fn select_assoc<E: Entity>(map: &AssocMap<E>) -> DBResult<Vec<E>> {
             // now we grab the statement outputs
             let mut rows = vec![];
             while stmt.next()? == sqlite::State::Row {
-                let datum_list = <E::Parts>::build_datum_list(&map.conn(), map.ctx(), stmt)?;
+                let datum_list = <E::Parts>::build_datum_list(&map.conn(), stmt)?;
                 rows.push(E::build(datum_list));
             }
 
@@ -85,14 +83,13 @@ pub(crate) fn insert_assoc<E: Entity>(map: &AssocMap<E>, value: E) -> DBResult<(
 
     // second, the assoc table
     map.conn().with_prepared(
-        query_hash::<E>(map.ctx(), QueryType::InsertAssoc(adata.part_name)),
+        query_hash::<E>(QueryType::InsertAssoc(adata.part_name)),
         || {
-            let ctx = adata.ctx;
             let base_name = adata.base_name;
             let target_name = E::entity_name();
             let part_name = adata.part_name;
             let assoc_name = format!("{base_name}_assoc_{part_name}_{target_name}");
-            format!("insert into `{ctx}_{assoc_name}` (`base`, `target`) values (?, ?)")
+            format!("insert into `{assoc_name}` (`base`, `target`) values (?, ?)")
         },
         |stmt| {
             stmt.reset()?;
@@ -120,7 +117,7 @@ pub(crate) fn select_by<E: Entity, PL: EntityPartList>(
     by.accept(&mut ty);
 
     map.conn().with_prepared(
-        query_hash::<E>(map.ctx(), QueryType::Select(std::any::TypeId::of::<PL>())),
+        query_hash::<E>(QueryType::Select(std::any::TypeId::of::<PL>())),
         || {
             let mut conditions = String::new();
             struct BuildConditions<'a>(&'a mut String);
@@ -137,7 +134,6 @@ pub(crate) fn select_by<E: Entity, PL: EntityPartList>(
             }
             PL::accept_part_visitor(&mut BuildConditions(&mut conditions));
 
-            // CTX let table_name = format!("{}_{}", map.ctx(), E::entity_name());
             let table_name = format!("{}", E::entity_name());
             format!("select rowid, * from `{}` where {}", table_name, conditions)
         },
@@ -155,7 +151,7 @@ pub(crate) fn select_by<E: Entity, PL: EntityPartList>(
             // now we grab the statement outputs
             let mut rows = vec![];
             while stmt.next()? == sqlite::State::Row {
-                let datum_list = <E::Parts>::build_datum_list(&map.conn(), map.ctx(), stmt)?;
+                let datum_list = <E::Parts>::build_datum_list(&map.conn(), stmt)?;
                 rows.push(IDWrap::new(
                     <E::ID>::from_raw(stmt.read::<i64, _>(0)?),
                     E::build(datum_list),
@@ -174,9 +170,8 @@ pub(crate) fn insert<E: Entity>(
     value: E,
 ) -> DBResult<E::ID> {
     map.conn().with_prepared(
-        query_hash::<E>(map.ctx(), QueryType::Insert),
+        query_hash::<E>(QueryType::Insert),
         || {
-            // CTX let table_name = format!("{}_{}", map.ctx(), E::entity_name());
             let table_name = format!("{}", E::entity_name());
 
             let mut part_names = String::new();

+ 242 - 64
microrm/src/schema.rs

@@ -7,22 +7,17 @@ use crate::{
 use crate::{DBError, DBResult};
 
 mod build;
-mod entity;
+mod collect;
 pub(crate) mod meta;
+#[cfg(test)]
 mod tests;
 
+
 // ----------------------------------------------------------------------
-// Specification types
+// API types
 // ----------------------------------------------------------------------
 
-/// Trait for a type that represents a sqlite table that contains entities
-pub(crate) trait EntityMap {
-    type ContainedEntity: Entity;
-
-    fn conn(&self) -> &DBConnection;
-    fn ctx(&self) -> &'static str;
-}
-
+/// Wrapper struct that holds both an EntityID and an Entity itself.
 pub struct IDWrap<T: Entity> {
     id: T::ID,
     wrap: T,
@@ -67,69 +62,56 @@ impl<T: Entity> std::ops::DerefMut for IDWrap<T> {
     }
 }
 
-/// Table with EntityID-based lookup.
-pub struct IDMap<T: Entity> {
-    conn: DBConnection,
-    ctx: &'static str,
-    _ghost: std::marker::PhantomData<T>,
-}
 
-impl<T: Entity> EntityMap for IDMap<T> {
-    type ContainedEntity = T;
-    fn conn(&self) -> &DBConnection {
-        &self.conn
-    }
+// ----------------------------------------------------------------------
+// Entity field types
+// ----------------------------------------------------------------------
 
-    fn ctx(&self) -> &'static str {
-        self.ctx
-    }
+/// Visitor for allowing for type-specific behaviour
+pub trait DatumDiscriminator {
+    fn visit_entity_id<E: Entity>(&mut self);
+    fn visit_serialized<T: serde::Serialize + serde::de::DeserializeOwned>(&mut self);
+    fn visit_bare_field<T: Datum>(&mut self);
+    fn visit_assoc_map<E: Entity>(&mut self);
+    fn visit_assoc_domain<R: Relation>(&mut self);
+    fn visit_assoc_range<R: Relation>(&mut self);
 }
 
-impl<T: Entity> IDMap<T> {
-    pub fn build(db: DBConnection, ctx: &'static str) -> Self {
-        Self {
-            conn: db,
-            ctx,
-            _ghost: std::marker::PhantomData,
-        }
-    }
-
-    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,
-    ) -> DBResult<Option<IDWrap<T>>> {
-        query::select_by::<T, T::Uniques>(self, uniques).map(|mut v| {
-            if v.len() > 0 {
-                Some(v.remove(0))
-            } else {
-                None
-            }
-        })
-    }
-
-    pub fn insert(&self, value: T) -> DBResult<T::ID> {
-        query::insert(self, value)
-    }
+/// Represents an arbitrary relation between two types of entities.
+///
+/// In its most generic form, stores an arbitrary set of `(Domain::ID, Range::ID)` pairs used to
+/// define the relation. Can be restricted to be injective if this is desired. Doing so will incur
+/// a small runtime cost, since an extra index must be maintained.
+pub trait Relation: 'static {
+    type Domain: Entity;
+    type Range: Entity;
+
+    /// If true, then each Range-type entity can only be referred to by a single Domain-type
+    /// entity. This roughly translates to the relation being computationally-efficiently
+    /// invertible for a two-way map.
+    const INJECTIVE: bool = false;
+
+    const NAME: &'static str;
 }
 
-pub struct Index<T: Entity, Key: Datum> {
-    _conn: DBConnection,
-    _ghost: std::marker::PhantomData<(T, Key)>,
-}
+/// XXX: we want to be able to derive a string name from a Relation instance, preferably without
+/// another proc macro, and without relying on RTTI that could change between compilations. how?
+///
+/// -> can use a regular macro to automatically declare tag struct and impl Relation 
 
+/// Opaque data structure used for constructing `Assoc{Map,Domain,Range}` instances.
 pub struct AssocData {
     pub(crate) conn: DBConnection,
-    pub(crate) ctx: &'static str,
     pub(crate) base_name: &'static str,
     pub(crate) part_name: &'static str,
+    // XXX: for associations to work correctly, this needs to store a "direction" and allow for
+    // target_rowid to be specified instead
     pub(crate) base_rowid: i64,
 }
 
+/// Represents a simple one-to-many non-injective entity relationship.
+///
+/// This is a 'shortcut' type that doesn't require defining a tag type that implements `Relation`.
 pub struct AssocMap<T: Entity> {
     pub(crate) data: Option<AssocData>,
     _ghost: std::marker::PhantomData<T>,
@@ -167,10 +149,6 @@ impl<T: Entity> EntityMap for AssocMap<T> {
     fn conn(&self) -> &DBConnection {
         &self.data.as_ref().unwrap().conn
     }
-
-    fn ctx(&self) -> &'static str {
-        self.data.as_ref().unwrap().ctx
-    }
 }
 
 impl<T: Entity> Datum for AssocMap<T> {
@@ -182,6 +160,139 @@ impl<T: Entity> Datum for AssocMap<T> {
         v.visit::<T>();
     }
 
+    fn accept_discriminator(d: &mut impl DatumDiscriminator) where Self: Sized {
+        d.visit_assoc_map::<T>();
+    }
+
+    fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, _index: usize) {
+        unreachable!()
+    }
+
+    fn build_from<'a>(
+        adata: AssocData,
+        _stmt: &mut sqlite::Statement<'a>,
+        index: usize,
+    ) -> DBResult<(Self, usize)>
+    where
+        Self: Sized,
+    {
+        Ok((
+            Self {
+                data: Some(adata),
+                _ghost: Default::default(),
+            },
+            index,
+        ))
+    }
+}
+
+/// The domain part of a full Relation definition.
+pub struct AssocDomain<R: Relation> {
+    pub(crate) data: Option<AssocData>,
+    _ghost: std::marker::PhantomData<R>,
+}
+
+impl<R: Relation> Default for AssocDomain<R> {
+    fn default() -> Self {
+        Self::empty()
+    }
+}
+
+impl<R: Relation> AssocDomain<R> {
+    pub fn empty() -> Self {
+        Self {
+            data: None,
+            _ghost: Default::default(),
+        }
+    }
+}
+
+impl<R: Relation> EntityMap for AssocDomain<R> {
+    type ContainedEntity = R::Range;
+
+    fn conn(&self) -> &DBConnection {
+        &self.data.as_ref().unwrap().conn
+    }
+}
+
+impl<R: Relation> Datum for AssocDomain<R> {
+    fn sql_type() -> &'static str {
+        unreachable!()
+    }
+
+    fn accept_entity_visitor(v: &mut impl EntityVisitor) {
+        v.visit::<R::Domain>();
+    }
+
+    fn accept_discriminator(d: &mut impl DatumDiscriminator) where Self: Sized {
+        d.visit_assoc_domain::<R>();
+    }
+
+    fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, _index: usize) {
+        unreachable!()
+    }
+
+    fn build_from<'a>(
+        adata: AssocData,
+        _stmt: &mut sqlite::Statement<'a>,
+        index: usize,
+    ) -> DBResult<(Self, usize)>
+    where
+        Self: Sized,
+    {
+        Ok((
+            Self {
+                data: Some(adata),
+                _ghost: Default::default(),
+            },
+            index,
+        ))
+    }
+}
+
+/// The range part of a full Relation definition.
+pub struct AssocRange<R: Relation> {
+    pub(crate) data: Option<AssocData>,
+    _ghost: std::marker::PhantomData<R>,
+}
+
+impl<R: Relation> Default for AssocRange<R> {
+    fn default() -> Self {
+        Self::empty()
+    }
+}
+
+impl<R: Relation> AssocRange<R> {
+    pub fn empty() -> Self {
+        Self {
+            data: None,
+            _ghost: Default::default(),
+        }
+    }
+}
+
+
+impl<R: Relation> EntityMap for AssocRange<R> {
+    type ContainedEntity = R::Domain;
+
+    fn conn(&self) -> &DBConnection {
+        &self.data.as_ref().unwrap().conn
+    }
+}
+
+impl<R: Relation> Datum for AssocRange<R> {
+    fn sql_type() -> &'static str {
+        unreachable!()
+    }
+
+    fn accept_entity_visitor(v: &mut impl EntityVisitor) {
+        v.visit::<R::Domain>();
+    }
+
+    fn accept_discriminator(d: &mut impl DatumDiscriminator) where Self: Sized {
+        d.visit_assoc_range::<R>();
+    }
+
     fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, _index: usize) {
         unreachable!()
     }
@@ -194,7 +305,6 @@ impl<T: Entity> Datum for AssocMap<T> {
     where
         Self: Sized,
     {
-        // assuming that the stmt has the rowid as index 0
         Ok((
             Self {
                 data: Some(adata),
@@ -205,6 +315,7 @@ impl<T: Entity> Datum for AssocMap<T> {
     }
 }
 
+/// Stores an arbitrary Rust data type as serialized JSON in a string field.
 pub struct Serialized<T: serde::Serialize + serde::de::DeserializeOwned> {
     wrapped: T,
 }
@@ -266,6 +377,72 @@ impl<T: 'static + serde::Serialize + serde::de::DeserializeOwned> Datum for Seri
     }
 }
 
+// ----------------------------------------------------------------------
+// Database specification types
+// ----------------------------------------------------------------------
+
+/// Trait for a type that represents a sqlite table that contains entities.
+pub(crate) trait EntityMap {
+    type ContainedEntity: Entity;
+
+    fn conn(&self) -> &DBConnection;
+}
+
+/// Table with EntityID-based lookup.
+pub struct IDMap<T: Entity> {
+    conn: DBConnection,
+    _ghost: std::marker::PhantomData<T>,
+}
+
+impl<T: Entity> EntityMap for IDMap<T> {
+    type ContainedEntity = T;
+    fn conn(&self) -> &DBConnection {
+        &self.conn
+    }
+}
+
+impl<T: Entity> IDMap<T> {
+    pub fn build(db: DBConnection) -> Self {
+        Self {
+            conn: db,
+            _ghost: std::marker::PhantomData,
+        }
+    }
+
+    /// Look up an Entity in this map by ID.
+    pub fn by_id(&self, _id: T::ID) -> DBResult<Option<T>> {
+        // query::select_by
+        todo!()
+    }
+
+    /// Look up an Entity in this map by the unique-tagged fields.
+    ///
+    /// Fields are passed to this function in order of specification in the original `struct` definition.
+    pub fn lookup_unique(
+        &self,
+        uniques: &<<T as Entity>::Uniques as EntityPartList>::DatumList,
+    ) -> DBResult<Option<IDWrap<T>>> {
+        query::select_by::<T, T::Uniques>(self, uniques).map(|mut v| {
+            if v.len() > 0 {
+                Some(v.remove(0))
+            } else {
+                None
+            }
+        })
+    }
+
+    /// Insert a new Entity into this map, and return its new ID.
+    pub fn insert(&self, value: T) -> DBResult<T::ID> {
+        query::insert(self, value)
+    }
+}
+
+pub struct Index<T: Entity, Key: Datum> {
+    _conn: DBConnection,
+    _ghost: std::marker::PhantomData<(T, Key)>,
+}
+
+/// Represents a single root-level table or index in a database.
 pub trait DatabaseItem {
     fn item_key() -> &'static str;
     fn dependency_keys() -> &'static [&'static str];
@@ -299,6 +476,7 @@ impl<T: Entity> DatabaseSpec for IDMap<T> {
     }
 }
 
+/// A root structure for the database specification graph.
 pub trait Database {
     fn open_path<U: AsRef<str>>(uri: U) -> Result<Self, DBError>
     where

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

@@ -1,6 +1,6 @@
 use crate::{
     schema::{
-        entity::{EntityStateContainer, PartType},
+        collect::{EntityStateContainer, PartType},
         meta, DBConnection, Database, DatabaseItem, DatabaseItemVisitor,
     },
     DBResult,
@@ -74,7 +74,7 @@ impl DatabaseSchema {
 
         // check to see if the signature exists and matches
         metadb
-            .kv_metastore
+            .metastore
             .lookup_unique(&Self::SCHEMA_SIGNATURE_KEY.to_string())
             .ok()
             .flatten()
@@ -94,7 +94,7 @@ impl DatabaseSchema {
         }
 
         // store signature
-        metadb.kv_metastore.insert(meta::KV {
+        metadb.metastore.insert(meta::Meta {
             key: Self::SCHEMA_SIGNATURE_KEY.into(),
             value: format!("{}", self.signature),
         })?;
@@ -110,7 +110,7 @@ pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
         where
             Self: Sized,
         {
-            DI::accept_entity_visitor(&mut self.0.make_context(DI::item_key()));
+            DI::accept_entity_visitor(&mut self.0.make_context());
         }
     }
 
@@ -122,7 +122,6 @@ pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
     let mut tables = std::collections::HashMap::new();
 
     for state in iv.0.iter_states() {
-        // CTX let table_name = format!("{}_{}", state.context, state.name);
         let table_name = state.name.to_string();
         // we may end up visiting duplicate entities; skip them if so
         if tables.contains_key(&table_name) {
@@ -141,8 +140,8 @@ pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
                 }
                 PartType::Assoc(assoc_name) => {
                     let assoc_table_name = format!(
-                        "{}_{}_assoc_{}_{}",
-                        state.context, state.name, part.name, assoc_name
+                        "{}_assoc_{}_{}",
+                        state.name, part.name, assoc_name
                     );
                     let mut assoc_table = TableInfo::new(assoc_table_name.clone());
                     assoc_table.dependencies.push(table_name.clone());
@@ -158,7 +157,6 @@ pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
                     assoc_table.columns.push(ColumnInfo {
                         name: "target".into(),
                         ty: "int".into(),
-                        // CTX fkey: Some(format!("{}_{}(`id`)", state.context, assoc_name)),
                         fkey: Some(format!("{}(`id`)", assoc_name)),
                         unique: false,
                     });

+ 8 - 13
microrm/src/schema/entity.rs → microrm/src/schema/collect.rs

@@ -39,7 +39,6 @@ impl PartState {
 
 #[derive(Debug)]
 pub struct EntityState {
-    pub context: &'static str,
     pub name: &'static str,
     typeid: std::any::TypeId,
 
@@ -47,7 +46,7 @@ pub struct EntityState {
 }
 
 impl EntityState {
-    fn build<E: Entity>(context: &'static str) -> Self {
+    fn build<E: Entity>() -> Self {
         #[derive(Default)]
         struct PartVisitor(Vec<PartState>);
         impl EntityPartVisitor for PartVisitor {
@@ -60,7 +59,6 @@ impl EntityState {
         E::accept_part_visitor(&mut pv);
 
         Self {
-            context,
             name: E::entity_name(),
             typeid: std::any::TypeId::of::<E>(),
             parts: pv.0,
@@ -70,7 +68,7 @@ impl EntityState {
 
 #[derive(Default, Debug)]
 pub struct EntityStateContainer {
-    states: HashMap<(&'static str, &'static str), EntityState>,
+    states: HashMap<&'static str, EntityState>,
 }
 
 impl EntityStateContainer {
@@ -78,30 +76,27 @@ impl EntityStateContainer {
         self.states.values()
     }
 
-    pub fn make_context(&mut self, context: &'static str) -> EntityContext {
+    pub fn make_context(&mut self) -> EntityContext {
         EntityContext {
-            context,
             container: self,
         }
     }
 }
 
 pub struct EntityContext<'a> {
-    context: &'static str,
     container: &'a mut EntityStateContainer,
 }
 
 impl<'a> EntityVisitor for EntityContext<'a> {
     fn visit<E: Entity>(&mut self) {
         // three cases:
-        // 1. we haven't seen this entity in this context before
-        // 2. we've seen this entity in this context before
-        // 3. we haven't seen this entity in this context before, but we've seen one with an identical name in the same context
+        // 1. we haven't seen this entity
+        // 2. we've seen this entity before
 
         if self
             .container
             .states
-            .contains_key(&(self.context, E::entity_name()))
+            .contains_key(E::entity_name())
         {
             return;
         }
@@ -109,9 +104,9 @@ impl<'a> EntityVisitor for EntityContext<'a> {
         let entry = self
             .container
             .states
-            .entry((self.context, E::entity_name()));
+            .entry(E::entity_name());
 
-        let entry = entry.or_insert_with(|| EntityState::build::<E>(self.context));
+        let entry = entry.or_insert_with(EntityState::build::<E>);
         // sanity-check
         if entry.typeid != std::any::TypeId::of::<E>() {
             panic!("Identical entity name but different typeid!");

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

@@ -1,7 +1,7 @@
 use crate::schema::IDMap;
 
 #[derive(microrm_macros::Entity)]
-pub struct KV {
+pub struct Meta {
     #[unique]
     pub key: String,
     pub value: String,
@@ -9,5 +9,5 @@ pub struct KV {
 
 #[derive(microrm_macros::Database)]
 pub struct MetadataDB {
-    pub kv_metastore: IDMap<KV>,
+    pub metastore: IDMap<Meta>,
 }

+ 30 - 4
microrm/src/schema/tests.rs

@@ -1,5 +1,11 @@
 #![allow(unused)]
 
+fn open_test_db<DB: super::Database>(identifier: &'static str) -> DB {
+    let path = format!("/tmp/microrm-{identifier}.db");
+    let _ = std::fs::remove_file(path.as_str());
+    DB::open_path(path).expect("couldn't open database file")
+}
+
 mod manual_test_db {
     // simple hand-built database example
 
@@ -230,19 +236,28 @@ mod derive_tests {
 }
 
 mod mutual_relationship {
-    use crate::schema::{AssocMap, Database, IDMap};
+    use super::open_test_db;
+    use crate::schema::{AssocMap, Database, IDMap, AssocRange, AssocDomain};
     use microrm_macros::{Database, Entity};
 
+    struct CR;
+    impl microrm::schema::Relation for CR {
+        type Domain = Customer;
+        type Range = Receipt;
+
+        const NAME: &'static str = "CR";
+    }
+
     #[derive(Entity)]
     struct Customer {
         name: String,
-        receipts: AssocMap<Receipt>,
+        receipts: AssocDomain<CR>,
     }
 
     #[derive(Entity)]
     struct Receipt {
         value: usize,
-        customers: AssocMap<Customer>,
+        customers: AssocRange<CR>,
     }
 
     #[derive(Database)]
@@ -253,6 +268,17 @@ mod mutual_relationship {
 
     #[test]
     fn check_schema_creation() {
-        let db = ReceiptDB::open_path(":memory:").expect("couldn't open in-memory database");
+        let db = open_test_db::<ReceiptDB>("mutual_relationship_create");
+        // let db = ReceiptDB::open_path(":memory:").expect("couldn't open in-memory database");
+
+        let ca = db.customers.insert(Customer {
+            name: "customer A".to_string(),
+            receipts: Default::default(),
+        }).expect("couldn't insert customer record");
+
+        let ra = db.receipts.insert(Receipt {
+            value: 32usize,
+            customers: Default::default(),
+        }).expect("couldn't insert receipt record");
     }
 }