Parcourir la source

Started implementing generic AssocInterface for all Assoc* types.

Kestrel il y a 1 an
Parent
commit
70aad8d50a

+ 0 - 2
microrm/src/lib.rs

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

+ 208 - 13
microrm/src/query.rs

@@ -1,5 +1,6 @@
+use crate::db::DBConnection;
 use crate::schema::entity::helpers::check_assoc;
-use crate::schema::{AssocMap, IDWrap};
+use crate::schema::{AssocData, IDWrap, LocalSide};
 use crate::{
     schema::datum::{Datum, DatumList, DatumVisitor},
     schema::entity::{Entity, EntityID, EntityPart, EntityPartList, EntityPartVisitor},
@@ -10,10 +11,11 @@ use std::hash::{Hash, Hasher};
 
 #[derive(Hash)]
 enum QueryType<'a> {
+    ByID(&'a str),
     Select(std::any::TypeId),
-    SelectJoin(&'a str),
+    SelectJoin(&'a str, &'a str, &'a str),
     Insert,
-    InsertAssoc(&'a str),
+    InsertAssoc(&'a str, &'a str, &'a str),
 }
 
 fn query_hash<E: Entity>(qtype: QueryType) -> u64 {
@@ -25,6 +27,7 @@ fn query_hash<E: Entity>(qtype: QueryType) -> u64 {
     hasher.finish()
 }
 
+/*
 pub(crate) fn select_assoc<E: Entity>(map: &AssocMap<E>) -> DBResult<Vec<E>> {
     let adata = map
         .data
@@ -38,7 +41,7 @@ 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 base_name = adata.base_name;
+    let base_name = adata.local_name;
     let target_name = E::entity_name();
     let part_name = adata.part_name;
 
@@ -54,7 +57,7 @@ pub(crate) fn select_assoc<E: Entity>(map: &AssocMap<E>) -> DBResult<Vec<E>> {
             )
         },
         |stmt| {
-            stmt.bind((1, adata.base_rowid))?;
+            stmt.bind((1, adata.local_id))?;
 
             // now we grab the statement outputs
             let mut rows = vec![];
@@ -67,7 +70,9 @@ pub(crate) fn select_assoc<E: Entity>(map: &AssocMap<E>) -> DBResult<Vec<E>> {
         },
     )
 }
+*/
 
+/*
 pub(crate) fn insert_assoc<E: Entity>(map: &AssocMap<E>, value: E) -> DBResult<()> {
     // we're doing two things:
     // - inserting the entity into the target table
@@ -85,15 +90,15 @@ pub(crate) fn insert_assoc<E: Entity>(map: &AssocMap<E>, value: E) -> DBResult<(
     map.conn().with_prepared(
         query_hash::<E>(QueryType::InsertAssoc(adata.part_name)),
         || {
-            let base_name = adata.base_name;
+            let local_name = adata.local_name;
             let target_name = E::entity_name();
             let part_name = adata.part_name;
-            let assoc_name = format!("{base_name}_assoc_{part_name}_{target_name}");
+            let assoc_name = format!("{local_name}_assoc_{part_name}_{target_name}");
             format!("insert into `{assoc_name}` (`base`, `target`) values (?, ?)")
         },
         |stmt| {
             stmt.reset()?;
-            stmt.bind((1, adata.base_rowid))?;
+            stmt.bind((1, adata.local_id))?;
             stmt.bind((2, target_id.into_raw()))?;
 
             stmt.next()?;
@@ -102,6 +107,39 @@ pub(crate) fn insert_assoc<E: Entity>(map: &AssocMap<E>, value: E) -> DBResult<(
         },
     )
 }
+*/
+
+pub(crate) fn by_id<ID: EntityID>(
+    conn: &DBConnection,
+    id: ID,
+) -> DBResult<Option<IDWrap<ID::Entity>>> {
+    conn.with_prepared(
+        query_hash::<ID::Entity>(QueryType::ByID(<ID::Entity>::entity_name())),
+        || {
+            format!(
+                "select * from {} where `id` = ?",
+                <ID::Entity>::entity_name()
+            )
+        },
+        |stmt| {
+            stmt.reset()?;
+            stmt.bind((1, id.into_raw()))?;
+
+            // now we grab the statement output
+            if stmt.next()? == sqlite::State::Row {
+                // read the ID column
+                let id = stmt.read::<i64, _>(0)?;
+                let datum_list = <<ID::Entity as Entity>::Parts>::build_datum_list(conn, stmt)?;
+                Ok(Some(IDWrap::new(
+                    ID::from_raw(id),
+                    <ID::Entity>::build(datum_list),
+                )))
+            } else {
+                Ok(None)
+            }
+        },
+    )
+}
 
 pub(crate) fn select_by<E: Entity, PL: EntityPartList>(
     map: &IDMap<E>,
@@ -165,11 +203,8 @@ pub(crate) fn select_by<E: Entity, PL: EntityPartList>(
     )
 }
 
-pub(crate) fn insert<E: Entity>(
-    map: &impl EntityMap<ContainedEntity = E>,
-    value: E,
-) -> DBResult<E::ID> {
-    map.conn().with_prepared(
+pub(crate) fn insert<E: Entity>(conn: &DBConnection, value: E) -> DBResult<E::ID> {
+    conn.with_prepared(
         query_hash::<E>(QueryType::Insert),
         || {
             let table_name = format!("{}", E::entity_name());
@@ -227,3 +262,163 @@ pub(crate) fn insert<E: Entity>(
         },
     )
 }
+
+pub(crate) struct AssocNames {
+    local_name: &'static str,
+    remote_name: &'static str,
+    part_name: &'static str,
+    dist_name: &'static str,
+    domain_name: &'static str,
+    range_name: &'static str,
+    local_field: &'static str,
+    remote_field: &'static str,
+}
+
+impl AssocNames {
+    fn collect<AI: AssocInterface>(iface: &AI) -> DBResult<AssocNames> {
+        let adata = iface.get_data()?;
+        let local_name = adata.local_name;
+        let remote_name = <AI::RemoteEntity>::entity_name();
+        let part_name = adata.part_name;
+        let dist_name = iface.get_distinguishing_name()?;
+
+        let (domain_name, range_name) = match AI::SIDE {
+            LocalSide::Domain => (local_name, remote_name),
+            LocalSide::Range => (remote_name, local_name),
+        };
+        let (local_field, remote_field) = match AI::SIDE {
+            LocalSide::Domain => ("domain", "range"),
+            LocalSide::Range => ("range", "domain"),
+        };
+        Ok(Self {
+            local_name,
+            remote_name,
+            part_name,
+            dist_name,
+            domain_name,
+            range_name,
+            local_field,
+            remote_field,
+        })
+    }
+
+    fn assoc_name(&self) -> String {
+        format!(
+            "{domain_name}_{range_name}_assoc_{dist_name}",
+            domain_name = self.domain_name,
+            range_name = self.range_name,
+            dist_name = self.dist_name
+        )
+    }
+}
+
+pub(crate) trait AssocInterface {
+    type RemoteEntity: Entity;
+    fn get_data(&self) -> DBResult<&AssocData>;
+    fn get_distinguishing_name(&self) -> DBResult<&'static str>;
+    const SIDE: LocalSide;
+
+    fn get_all(&self) -> DBResult<Vec<Self::RemoteEntity>>
+    where
+        Self: Sized,
+    {
+        let adata = self.get_data()?;
+
+        // equivalent SQL:
+        // SELECT
+        //     `range_table_name`.*
+        // FROM
+        //     `assoc_table_name`
+        // LEFT JOIN `range_table_name` ON `assoc_table_name`.`range` = `range_table_name`.`rowid`
+        // WHERE `domain` = domain_id
+
+        let an = AssocNames::collect::<Self>(&self)?;
+
+        adata.conn.with_prepared(
+            query_hash::<Self::RemoteEntity>(QueryType::SelectJoin(
+                an.local_name,
+                an.remote_name,
+                an.part_name,
+            )),
+            || {
+                format!(
+                    "select `{remote_name}`.* from `{assoc_name}` \
+                        left join `{remote_name}` on \
+                            `{assoc_name}`.`{remote_field}` = `{remote_name}`.`id` \
+                        where `{assoc_name}`.`{local_field}` = ?",
+                    assoc_name = an.assoc_name(),
+                    remote_name = an.remote_name,
+                    local_field = an.local_field,
+                    remote_field = an.remote_field,
+                )
+            },
+            |stmt| {
+                stmt.bind((1, adata.local_id))?;
+
+                // now we grab the statement outputs
+                let mut rows = vec![];
+                while stmt.next()? == sqlite::State::Row {
+                    let datum_list = <<Self::RemoteEntity as Entity>::Parts>::build_datum_list(
+                        &adata.conn,
+                        stmt,
+                    )?;
+                    rows.push(Self::RemoteEntity::build(datum_list));
+                }
+
+                Ok(rows)
+            },
+        )
+    }
+
+    fn associate_with(&self, remote_id: <Self::RemoteEntity as Entity>::ID) -> DBResult<()>
+    where
+        Self: Sized,
+    {
+        let adata = self.get_data()?;
+        let an = AssocNames::collect::<Self>(&self)?;
+
+        // second, add to the assoc table
+        adata.conn.with_prepared(
+            query_hash::<Self::RemoteEntity>(QueryType::InsertAssoc(
+                an.local_name,
+                an.remote_name,
+                an.part_name,
+            )),
+            || {
+                format!(
+                    "insert into `{assoc_name}` (`{local_field}`, `{remote_field}`) values (?, ?)",
+                    assoc_name = an.assoc_name(),
+                    local_field = an.local_field,
+                    remote_field = an.remote_field
+                )
+            },
+            |stmt| {
+                stmt.reset()?;
+                stmt.bind((1, adata.local_id))?;
+                stmt.bind((2, remote_id.into_raw()))?;
+
+                stmt.next()?;
+
+                Ok(())
+            },
+        )
+    }
+
+    fn insert(&self, value: Self::RemoteEntity) -> DBResult<<Self::RemoteEntity as Entity>::ID>
+    where
+        Self: Sized,
+    {
+        // we're doing two things:
+        // - inserting the entity into the target table
+        // - adding the association row into the assoc table
+
+        let adata = self.get_data()?;
+
+        // so first, the remote table
+        let remote_id = insert(&adata.conn, value)?;
+        // then the association
+        self.associate_with(remote_id)?;
+        // TODO: handle error case of associate_with() fails but insert() succeeds
+        Ok(remote_id)
+    }
+}

+ 73 - 16
microrm/src/schema.rs

@@ -1,6 +1,14 @@
+//! Schema specification
+//!
+//! Terminology used:
+//! - domain: one side of an association/relation, the "pointed-from" side for one-sided relations
+//! - range: one side of an association/relation, the "pointed-to" side for one-sided relations
+//! - local: the current side of an association
+//! - remote: the opposite side of an association
+
 use crate::{
     db::{Connection, DBConnection},
-    query,
+    query::{self, AssocInterface},
     schema::datum::Datum,
     schema::entity::{Entity, EntityPartList, EntityVisitor},
 };
@@ -95,19 +103,17 @@ pub trait Relation: 'static {
     const NAME: &'static str;
 }
 
-/// 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
+pub(crate) enum LocalSide {
+    Domain,
+    Range,
+}
 
 /// Opaque data structure used for constructing `Assoc{Map,Domain,Range}` instances.
 pub struct AssocData {
     pub(crate) conn: DBConnection,
-    pub(crate) base_name: &'static str,
+    pub(crate) local_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,
+    pub(crate) local_id: i64,
 }
 
 /// Represents a simple one-to-many non-injective entity relationship.
@@ -122,11 +128,31 @@ impl<T: Entity> std::fmt::Debug for AssocMap<T> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.write_fmt(format_args!(
             "AssocMap {{ id: {:?} }}",
-            self.data.as_ref().map(|d| d.base_rowid)
+            self.data.as_ref().map(|d| d.local_id)
         ))
     }
 }
 
+impl<T: Entity> AssocInterface for AssocMap<T> {
+    type RemoteEntity = T;
+    const SIDE: LocalSide = LocalSide::Domain;
+
+    fn get_distinguishing_name(&self) -> DBResult<&'static str> {
+        self.data
+            .as_ref()
+            .ok_or(DBError::LogicError(
+                "no distinguishing name for empty AssocMap",
+            ))
+            .map(|d| d.part_name)
+    }
+
+    fn get_data(&self) -> DBResult<&AssocData> {
+        self.data
+            .as_ref()
+            .ok_or(DBError::LogicError("Reading from unassigned AssocMap"))
+    }
+}
+
 impl<T: Entity> AssocMap<T> {
     pub fn empty() -> Self {
         Self {
@@ -135,13 +161,13 @@ impl<T: Entity> AssocMap<T> {
         }
     }
 
-    pub fn get_all(&self) -> DBResult<Vec<T>> {
+    /*pub fn get_all(&self) -> DBResult<Vec<T>> {
         query::select_assoc(self)
     }
 
     pub fn insert(&self, value: T) -> DBResult<()> {
         query::insert_assoc(self, value)
-    }
+    }*/
 }
 
 impl<T: Entity> EntityMap for AssocMap<T> {
@@ -211,6 +237,22 @@ impl<R: Relation> AssocDomain<R> {
     }
 }
 
+impl<R: Relation> AssocInterface for AssocDomain<R> {
+    type RemoteEntity = R::Range;
+    const SIDE: LocalSide = LocalSide::Domain;
+
+    fn get_distinguishing_name(&self) -> DBResult<&'static str> {
+        Ok(R::NAME)
+    }
+
+    fn get_data(&self) -> DBResult<&AssocData> {
+        self.data
+            .as_ref()
+            .ok_or(DBError::LogicError("Reading from unassigned AssocDomain"))
+    }
+}
+
+/*
 impl<R: Relation> EntityMap for AssocDomain<R> {
     type ContainedEntity = R::Range;
 
@@ -218,6 +260,7 @@ impl<R: Relation> EntityMap for AssocDomain<R> {
         &self.data.as_ref().unwrap().conn
     }
 }
+*/
 
 impl<R: Relation> Datum for AssocDomain<R> {
     fn sql_type() -> &'static str {
@@ -278,6 +321,21 @@ impl<R: Relation> AssocRange<R> {
     }
 }
 
+impl<R: Relation> AssocInterface for AssocRange<R> {
+    type RemoteEntity = R::Domain;
+    const SIDE: LocalSide = LocalSide::Range;
+
+    fn get_distinguishing_name(&self) -> DBResult<&'static str> {
+        Ok(R::NAME)
+    }
+
+    fn get_data(&self) -> DBResult<&AssocData> {
+        self.data
+            .as_ref()
+            .ok_or(DBError::LogicError("Reading from unassigned AssocRange"))
+    }
+}
+
 impl<R: Relation> EntityMap for AssocRange<R> {
     type ContainedEntity = R::Domain;
 
@@ -419,9 +477,8 @@ impl<T: Entity> IDMap<T> {
     }
 
     /// Look up an Entity in this map by ID.
-    pub fn by_id(&self, _id: T::ID) -> DBResult<Option<T>> {
-        // query::select_by
-        todo!()
+    pub fn by_id(&self, id: T::ID) -> DBResult<Option<IDWrap<T>>> {
+        query::by_id(&self.conn, id)
     }
 
     /// Look up an Entity in this map by the unique-tagged fields.
@@ -442,7 +499,7 @@ impl<T: Entity> IDMap<T> {
 
     /// 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)
+        query::insert(self.conn(), value)
     }
 }
 

+ 35 - 10
microrm/src/schema/build.rs

@@ -129,37 +129,62 @@ pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
         }
         let mut table = TableInfo::new(table_name.clone());
         for part in state.parts.iter() {
-            match part.ty {
+            match &part.ty {
                 PartType::Datum(dtype) => {
                     table.columns.push(ColumnInfo {
                         name: part.name,
-                        ty: dtype.into(),
+                        ty: dtype.to_string(),
                         fkey: None,
                         unique: false, // XXX
                     })
                 }
-                PartType::Assoc(assoc_name) => {
-                    let assoc_table_name =
-                        format!("{}_assoc_{}_{}", state.name, part.name, assoc_name);
+                PartType::AssocDomain {
+                    table_name: assoc_table_name,
+                    range_name,
+                } => {
                     let mut assoc_table = TableInfo::new(assoc_table_name.clone());
                     assoc_table.dependencies.push(table_name.clone());
-                    assoc_table.dependencies.push(format!("{}", assoc_name));
+                    assoc_table.dependencies.push(range_name.to_string());
 
                     assoc_table.columns.push(ColumnInfo {
-                        name: "base".into(),
+                        name: "domain".into(),
                         ty: "int".into(),
                         fkey: Some(format!("{}(`id`)", table_name)),
                         unique: false,
                     });
 
                     assoc_table.columns.push(ColumnInfo {
-                        name: "target".into(),
+                        name: "range".into(),
                         ty: "int".into(),
-                        fkey: Some(format!("{}(`id`)", assoc_name)),
+                        fkey: Some(format!("{}(`id`)", range_name)),
                         unique: false,
                     });
 
-                    tables.insert(assoc_table_name, assoc_table);
+                    tables.insert(assoc_table_name.clone(), assoc_table);
+                }
+                PartType::AssocRange {
+                    table_name: assoc_table_name,
+                    domain_name,
+                } => {
+                    let mut assoc_table = TableInfo::new(assoc_table_name.clone());
+                    assoc_table.dependencies.push(table_name.clone());
+                    assoc_table.dependencies.push(domain_name.to_string());
+
+                    assoc_table.columns.push(ColumnInfo {
+                        name: "domain".into(),
+                        ty: "int".into(),
+                        fkey: Some(format!("{}(`id`)", domain_name)),
+                        unique: false,
+                    });
+
+                    assoc_table.columns.push(ColumnInfo {
+                        name: "range".into(),
+                        ty: "int".into(),
+                        fkey: Some(format!("{}(`id`)", table_name)),
+                        unique: false,
+                    });
+
+                    tables.insert(assoc_table_name.clone(), assoc_table);
                 }
             }
         }

+ 77 - 7
microrm/src/schema/collect.rs

@@ -2,11 +2,19 @@ use std::collections::HashMap;
 
 use crate::schema::datum::Datum;
 use crate::schema::entity::{Entity, EntityPart, EntityPartVisitor, EntityVisitor};
+use crate::schema::{DatumDiscriminator, Relation};
 
 #[derive(Debug)]
 pub enum PartType {
     Datum(&'static str),
-    Assoc(&'static str),
+    AssocDomain {
+        table_name: String,
+        range_name: &'static str,
+    },
+    AssocRange {
+        table_name: String,
+        domain_name: &'static str,
+    },
 }
 
 #[derive(Debug)]
@@ -27,12 +35,74 @@ impl PartState {
         let mut acheck = AssocCheck(None);
         EP::Datum::accept_entity_visitor(&mut acheck);
 
-        PartState {
-            name: EP::part_name(),
-            ty: match acheck.0 {
-                Some(aname) => PartType::Assoc(aname),
-                None => PartType::Datum(EP::Datum::sql_type()),
-            },
+        struct Discriminator<EP: EntityPart> {
+            ty: Option<PartType>,
+            _ghost: std::marker::PhantomData<EP>,
+        }
+        impl<EP: EntityPart> DatumDiscriminator for Discriminator<EP> {
+            fn visit_entity_id<E: Entity>(&mut self) {
+                unreachable!()
+            }
+
+            fn visit_bare_field<T: Datum>(&mut self) {
+                self.ty = Some(PartType::Datum(T::sql_type()));
+            }
+
+            fn visit_serialized<T: serde::Serialize + serde::de::DeserializeOwned>(&mut self) {
+                self.ty = Some(PartType::Datum("text"));
+            }
+
+            fn visit_assoc_map<E: Entity>(&mut self) {
+                self.ty = Some(PartType::AssocDomain {
+                    table_name: format!(
+                        "{}_{}_assoc_{}",
+                        EP::Entity::entity_name(),
+                        E::entity_name(),
+                        EP::part_name()
+                    ),
+                    range_name: E::entity_name(),
+                });
+            }
+
+            fn visit_assoc_domain<R: Relation>(&mut self) {
+                self.ty = Some(PartType::AssocDomain {
+                    table_name: format!(
+                        "{}_{}_assoc_{}",
+                        R::Domain::entity_name(),
+                        R::Range::entity_name(),
+                        R::NAME
+                    ),
+                    range_name: R::Range::entity_name(),
+                });
+            }
+
+            fn visit_assoc_range<R: Relation>(&mut self) {
+                self.ty = Some(PartType::AssocRange {
+                    table_name: format!(
+                        "{}_{}_assoc_{}",
+                        R::Domain::entity_name(),
+                        R::Range::entity_name(),
+                        R::NAME
+                    ),
+                    domain_name: R::Domain::entity_name(),
+                });
+            }
+        }
+
+        let mut discrim = Discriminator::<EP> {
+            ty: None,
+            _ghost: Default::default(),
+        };
+
+        <EP::Datum>::accept_discriminator(&mut discrim);
+
+        if let Some(ty) = discrim.ty {
+            PartState {
+                name: EP::part_name(),
+                ty,
+            }
+        } else {
+            unreachable!("no PartType extracted from EntityPart")
         }
     }
 }

+ 30 - 30
microrm/src/schema/entity/part_list.rs

@@ -3,13 +3,13 @@ use crate::{db::DBConnection, schema::AssocData, DBResult};
 use super::{Datum, Entity, EntityPart, EntityPartList, EntityPartVisitor};
 
 macro_rules! build_datum {
-    ($conn:ident, $base_rowid:ident,$stmt:ident,$idx:ident,$d:ident,$t:ident) => {
+    ($conn:ident,$local_id:ident,$stmt:ident,$idx:ident,$d:ident,$t:ident) => {
         let ($d, $idx) = <$t::Datum as Datum>::build_from(
             AssocData {
                 conn: $conn.clone(),
-                base_name: <$t::Entity as Entity>::entity_name(),
+                local_name: <$t::Entity as Entity>::entity_name(),
                 part_name: $t::part_name(),
-                base_rowid: $base_rowid,
+                local_id: $local_id,
             },
             $stmt,
             $idx,
@@ -38,9 +38,9 @@ impl<P0: EntityPart> EntityPartList for P0 {
         conn: &DBConnection,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
-        let base_rowid: i64 = stmt.read(0)?;
+        let local_id: i64 = stmt.read(0)?;
         let idx = 1; // starting index is 1 since index 0 is the ID
-        build_datum!(conn, base_rowid, stmt, idx, d0, P0);
+        build_datum!(conn, local_id, stmt, idx, d0, P0);
 
         let _ = idx;
         Ok(d0)
@@ -79,10 +79,10 @@ impl<P0: EntityPart, P1: EntityPart> EntityPartList for (P0, P1) {
         conn: &DBConnection,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
-        let base_rowid: i64 = stmt.read(0)?;
+        let local_id: i64 = stmt.read(0)?;
         let idx = 1; // starting index is 1 since index 0 is the ID
-        build_datum!(conn, base_rowid, stmt, idx, d0, P0);
-        build_datum!(conn, base_rowid, stmt, idx, d1, P1);
+        build_datum!(conn, local_id, stmt, idx, d0, P0);
+        build_datum!(conn, local_id, stmt, idx, d1, P1);
 
         let _ = idx;
         Ok((d0, d1))
@@ -105,11 +105,11 @@ impl<P0: EntityPart, P1: EntityPart, P2: EntityPart> EntityPartList for (P0, P1,
         conn: &DBConnection,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
-        let base_rowid: i64 = stmt.read(0)?;
+        let local_id: i64 = stmt.read(0)?;
         let idx = 1; // starting index is 1 since index 0 is the ID
-        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, local_id, stmt, idx, d0, P0);
+        build_datum!(conn, local_id, stmt, idx, d1, P1);
+        build_datum!(conn, local_id, stmt, idx, d2, P2);
 
         let _ = idx;
         Ok((d0, d1, d2))
@@ -136,12 +136,12 @@ impl<P0: EntityPart, P1: EntityPart, P2: EntityPart, P3: EntityPart> EntityPartL
         conn: &DBConnection,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
-        let base_rowid: i64 = stmt.read(0)?;
+        let local_id: i64 = stmt.read(0)?;
         let idx = 1; // starting index is 1 since index 0 is the ID
-        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, local_id, stmt, idx, d0, P0);
+        build_datum!(conn, local_id, stmt, idx, d1, P1);
+        build_datum!(conn, local_id, stmt, idx, d2, P2);
+        build_datum!(conn, local_id, stmt, idx, d3, P3);
 
         let _ = idx;
         Ok((d0, d1, d2, d3))
@@ -170,13 +170,13 @@ impl<P0: EntityPart, P1: EntityPart, P2: EntityPart, P3: EntityPart, P4: EntityP
         conn: &DBConnection,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
-        let base_rowid: i64 = stmt.read(0)?;
+        let local_id: i64 = stmt.read(0)?;
         let idx = 1; // starting index is 1 since index 0 is the ID
-        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, local_id, stmt, idx, d0, P0);
+        build_datum!(conn, local_id, stmt, idx, d1, P1);
+        build_datum!(conn, local_id, stmt, idx, d2, P2);
+        build_datum!(conn, local_id, stmt, idx, d3, P3);
+        build_datum!(conn, local_id, stmt, idx, d4, P4);
 
         let _ = idx;
         Ok((d0, d1, d2, d3, d4))
@@ -220,14 +220,14 @@ impl<
         conn: &DBConnection,
         stmt: &mut sqlite::Statement<'static>,
     ) -> DBResult<Self::DatumList> {
-        let base_rowid: i64 = stmt.read(0)?;
+        let local_id: i64 = stmt.read(0)?;
         let idx = 1; // starting index is 1 since index 0 is the ID
-        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);
+        build_datum!(conn, local_id, stmt, idx, d0, P0);
+        build_datum!(conn, local_id, stmt, idx, d1, P1);
+        build_datum!(conn, local_id, stmt, idx, d2, P2);
+        build_datum!(conn, local_id, stmt, idx, d3, P3);
+        build_datum!(conn, local_id, stmt, idx, d4, P4);
+        build_datum!(conn, local_id, stmt, idx, d5, P5);
 
         let _ = idx;
         Ok((d0, d1, d2, d3, d4, d5))

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

@@ -124,6 +124,7 @@ mod manual_test_db {
 mod derive_tests {
     #![allow(unused)]
 
+    use crate::query::AssocInterface;
     use crate::schema::{AssocMap, Database, IDMap};
     use microrm_macros::{Database, Entity};
 
@@ -237,6 +238,7 @@ mod derive_tests {
 
 mod mutual_relationship {
     use super::open_test_db;
+    use crate::query::AssocInterface;
     use crate::schema::{AssocDomain, AssocMap, AssocRange, Database, IDMap};
     use microrm_macros::{Database, Entity};
 
@@ -286,5 +288,33 @@ mod mutual_relationship {
                 customers: Default::default(),
             })
             .expect("couldn't insert receipt record");
+
+        let rb = db
+            .receipts
+            .insert(Receipt {
+                value: 64usize,
+                customers: Default::default(),
+            })
+            .expect("couldn't insert receipt record");
+
+        let e_ca = db
+            .customers
+            .by_id(ca)
+            .expect("couldn't retrieve customer record")
+            .expect("no customer record");
+        e_ca.receipts
+            .associate_with(ra)
+            .expect("couldn't associate customer with receipt");
+        e_ca.receipts
+            .associate_with(rb)
+            .expect("couldn't associate customer with receipt");
+
+        assert_eq!(
+            e_ca.receipts
+                .get_all()
+                .expect("couldn't get receipts associated with customer")
+                .len(),
+            2
+        );
     }
 }