Ver código fonte

Refactoring and minor improvements to query module.

Kestrel 7 meses atrás
pai
commit
fc86bdd417

+ 1 - 0
microrm/Cargo.toml

@@ -21,6 +21,7 @@ libsqlite3-sys = "0.28"
 serde = { version = "1.0" }
 serde_json = { version = "1.0" }
 time = "0.3"
+itertools = "0.12"
 
 microrm-macros = { path = "../microrm-macros", version = "0.4.0-rc.2" }
 log = "0.4.17"

+ 49 - 299
microrm/src/query.rs

@@ -1,11 +1,10 @@
+use itertools::Itertools;
+
 use crate::{
-    db::{Connection, StatementContext, StatementRow, Transaction},
+    db::{Connection, StatementContext, Transaction},
     schema::{
         datum::{Datum, QueryEquivalent, QueryEquivalentList},
-        entity::{
-            helpers::check_relation, Entity, EntityID, EntityPart, EntityPartList,
-            EntityPartVisitor,
-        },
+        entity::{Entity, EntityID, EntityPart, EntityPartList},
         index::Index,
         relation::{LocalSide, RelationData},
         IDMap, Stored,
@@ -16,171 +15,36 @@ use crate::{
 use std::collections::HashMap;
 use std::hash::{Hash, Hasher};
 
+pub(crate) mod base_queries;
 pub(crate) mod components;
+pub(crate) mod containers;
+use containers::*;
 
-pub(crate) fn insert<E: Entity>(conn: &Connection, value: &E) -> DBResult<E::ID> {
-    struct InsertQuery<E: Entity>(std::marker::PhantomData<E>);
-
-    conn.with_prepared(
-        std::any::TypeId::of::<InsertQuery<E>>(),
-        || {
-            let mut part_names = String::new();
-            let mut placeholders = String::new();
-            struct PartNameVisitor<'a, E: Entity>(
-                &'a mut String,
-                &'a mut String,
-                std::marker::PhantomData<E>,
-            );
-            impl<'a, E: Entity> EntityPartVisitor for PartNameVisitor<'a, E> {
-                type Entity = E;
-                fn visit<EP: EntityPart>(&mut self) {
-                    // if this is a set-relation, then we don't actually want to do anything
-                    // with it here; it doesn't have a column
-                    if check_relation::<EP>() {
-                        return;
-                    }
-
-                    if !self.0.is_empty() {
-                        self.0.push_str(", ");
-                        self.1.push_str(", ");
-                    }
-                    self.0.push('`');
-                    self.0.push_str(EP::part_name());
-                    self.0.push('`');
-                    self.1.push('?');
-                }
-            }
-
-            E::accept_part_visitor(&mut PartNameVisitor(
-                &mut part_names,
-                &mut placeholders,
-                Default::default(),
-            ));
-
-            format!(
-                "INSERT INTO `{}` ({}) VALUES ({}) RETURNING `id`",
-                E::entity_name(),
-                part_names,
-                placeholders
-            )
-        },
-        |mut ctx| {
-            struct PartBinder<'a, 'b, E: Entity>(
-                &'a mut StatementContext<'b>,
-                i32,
-                std::marker::PhantomData<E>,
-            );
-            impl<'a, 'b, E: Entity> EntityPartVisitor for PartBinder<'a, 'b, E> {
-                type Entity = E;
-                fn visit_datum<EP: EntityPart>(&mut self, datum: &EP::Datum) {
-                    // skip relations, as with the query preparation above
-                    if check_relation::<EP>() {
-                        return;
-                    }
-
-                    datum.bind_to(self.0, self.1);
-                    self.1 += 1;
-                }
-            }
-
-            value.accept_part_visitor_ref(&mut PartBinder(&mut ctx, 1, Default::default()));
-
-            ctx.run()?
-                .ok_or(Error::InternalError("No result row from INSERT query"))
-                .map(|r| <E::ID>::from_raw(r.read(0).expect("couldn't read resulting ID")))
-        },
-    )
+#[derive(Debug, Clone)]
+pub(crate) enum QueryPartData<'l> {
+    Owned(String),
+    Borrowed(&'l str),
 }
 
-pub(crate) fn insert_and_return<E: Entity>(conn: &Connection, mut value: E) -> DBResult<Stored<E>> {
-    let id = insert(conn, &value)?;
-
-    // update relation data in all fields
-    struct DatumWalker<'l, E: Entity>(&'l Connection, i64, std::marker::PhantomData<E>);
-    impl<'l, E: Entity> EntityPartVisitor for DatumWalker<'l, E> {
-        type Entity = E;
-        fn visit_datum_mut<EP: EntityPart>(&mut self, datum: &mut EP::Datum) {
-            datum.update_adata(RelationData {
-                conn: self.0.clone(),
-                part_name: EP::part_name(),
-                local_name: <EP::Entity as Entity>::entity_name(),
-                local_id: self.1,
-            });
+impl<'l> std::fmt::Display for QueryPartData<'l> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Owned(s) => f.write_str(s),
+            Self::Borrowed(s) => f.write_str(s),
         }
     }
-
-    value.accept_part_visitor_mut(&mut DatumWalker(conn, id.into_raw(), Default::default()));
-
-    Ok(Stored::new(conn.clone(), id, value))
 }
 
-pub(crate) fn update_entity<E: Entity>(conn: &Connection, value: &Stored<E>) -> DBResult<()> {
-    struct UpdateQuery<E: Entity>(std::marker::PhantomData<E>);
-
-    conn.with_prepared(
-        std::any::TypeId::of::<UpdateQuery<E>>(),
-        || {
-            let mut set_columns = String::new();
-            struct PartNameVisitor<'a, E: Entity>(&'a mut String, std::marker::PhantomData<E>);
-            impl<'a, E: Entity> EntityPartVisitor for PartNameVisitor<'a, E> {
-                type Entity = E;
-                fn visit<EP: EntityPart>(&mut self) {
-                    // if this is a set-relation, then we don't actually want to do anything
-                    // with it here; it doesn't have a column
-                    if check_relation::<EP>() {
-                        return;
-                    }
-
-                    if !self.0.is_empty() {
-                        self.0.push_str(", ");
-                    }
-                    self.0.push('`');
-                    self.0.push_str(EP::part_name());
-                    self.0.push_str("` = ?");
-                }
-            }
-
-            E::accept_part_visitor(&mut PartNameVisitor(&mut set_columns, Default::default()));
-            format!(
-                "UPDATE `{entity_name}` SET {set_columns} WHERE `id` = ?",
-                entity_name = E::entity_name()
-            )
-        },
-        |mut ctx| {
-            struct PartBinder<'a, 'b, E: Entity>(
-                &'a mut StatementContext<'b>,
-                &'a mut i32,
-                std::marker::PhantomData<E>,
-            );
-            impl<'a, 'b, E: Entity> EntityPartVisitor for PartBinder<'a, 'b, E> {
-                type Entity = E;
-                fn visit_datum<EP: EntityPart>(&mut self, datum: &EP::Datum) {
-                    // skip relations, as with the query preparation above
-                    if check_relation::<EP>() {
-                        return;
-                    }
-
-                    datum.bind_to(self.0, *self.1);
-                    *self.1 += 1;
-                }
-            }
-
-            // first bind all the updating clauses
-            let mut index = 1;
-            value.accept_part_visitor_ref(&mut PartBinder(
-                &mut ctx,
-                &mut index,
-                Default::default(),
-            ));
-
-            // then bind the id
-            value.id().bind_to(&mut ctx, index);
-
-            ctx.run()?;
+impl<'l> From<String> for QueryPartData<'l> {
+    fn from(value: String) -> Self {
+        Self::Owned(value)
+    }
+}
 
-            Ok(())
-        },
-    )
+impl<'l> From<&'l str> for QueryPartData<'l> {
+    fn from(value: &'l str) -> Self {
+        Self::Borrowed(value)
+    }
 }
 
 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
@@ -195,29 +59,29 @@ pub(crate) enum QueryPart {
 }
 
 #[derive(Debug)]
-pub struct Query {
-    parts: HashMap<QueryPart, Vec<String>>,
+pub struct Query<'l> {
+    parts: HashMap<QueryPart, Vec<QueryPartData<'l>>>,
 }
 
-impl Query {
+impl<'l> Query<'l> {
     pub(crate) fn new() -> Self {
         Self {
             parts: Default::default(),
         }
     }
 
-    pub(crate) fn attach(mut self, qp: QueryPart, val: String) -> Self {
-        self.attach_mut(qp, val);
+    pub(crate) fn attach<T: Into<QueryPartData<'l>>>(mut self, qp: QueryPart, val: T) -> Self {
+        self.attach_mut(qp, val.into());
         self
     }
 
-    pub(crate) fn replace(mut self, qp: QueryPart, val: String) -> Self {
+    pub(crate) fn replace<T: Into<QueryPartData<'l>>>(mut self, qp: QueryPart, val: T) -> Self {
         self.parts.remove(&qp);
-        self.attach(qp, val)
+        self.attach(qp, val.into())
     }
 
-    pub(crate) fn attach_mut(&mut self, qp: QueryPart, val: String) {
-        self.parts.entry(qp).or_default().push(val);
+    pub(crate) fn attach_mut<T: Into<QueryPartData<'l>>>(&mut self, qp: QueryPart, val: T) {
+        self.parts.entry(qp).or_default().push(val.into());
     }
 
     pub(crate) fn assemble(mut self) -> String {
@@ -225,33 +89,20 @@ impl Query {
 
         let columns_ = match self.parts.remove(&QueryPart::Columns) {
             None => String::new(),
-            Some(v) => v
-                .into_iter()
-                .reduce(|a, b| format!("{}, {}", a, b))
-                .unwrap(),
+            Some(v) => v.into_iter().join(","),
         };
 
         let from_ = match self.parts.remove(&QueryPart::From) {
             None => String::new(),
             Some(v) => {
-                format!(
-                    "FROM {}",
-                    v.into_iter()
-                        .reduce(|a, b| format!("{}, {}", a, b))
-                        .unwrap()
-                )
+                format!("FROM {}", v.into_iter().join(","))
             }
         };
 
         let set_ = match self.parts.remove(&QueryPart::Set) {
             None => String::new(),
             Some(v) => {
-                format!(
-                    "SET {}",
-                    v.into_iter()
-                        .reduce(|a, b| format!("{}, {}", a, b))
-                        .unwrap()
-                )
+                format!("SET {}", v.into_iter().join(","))
             }
         };
 
@@ -267,18 +118,13 @@ impl Query {
         let where_ = match self.parts.remove(&QueryPart::Where) {
             None => String::new(),
             Some(v) => {
-                format!(
-                    "WHERE {}",
-                    v.into_iter()
-                        .reduce(|a, b| format!("{} AND {}", a, b))
-                        .unwrap()
-                )
+                format!("WHERE {}", v.into_iter().join(" AND "))
             }
         };
 
         let trailing_ = match self.parts.remove(&QueryPart::Trailing) {
             None => String::new(),
-            Some(v) => v.into_iter().reduce(|a, b| format!("{} {}", a, b)).unwrap(),
+            Some(v) => v.into_iter().join(" "),
         };
 
         format!(
@@ -343,32 +189,6 @@ fn hash_of<T: Hash>(val: T) -> u64 {
     hasher.finish()
 }
 
-fn do_connect<Remote: Entity>(
-    rdata: &RelationData,
-    an: RelationNames,
-    remote_id: Remote::ID,
-) -> DBResult<()> {
-    rdata.conn.with_prepared(
-        hash_of(("connect", an.local_name, an.remote_name, an.part_name)),
-        || {
-            format!(
-                "insert into `{relation_name}` (`{local_field}`, `{remote_field}`) values (?, ?) returning (`id`)",
-                relation_name = an.relation_name(),
-                local_field = an.local_field,
-                remote_field = an.remote_field
-            )
-        },
-        |ctx| {
-            ctx.bind(1, rdata.local_id)?;
-            ctx.bind(2, remote_id.into_raw())?;
-
-            ctx.run()?
-                .ok_or(Error::ConstraintViolation("Relation entry uniqueness".to_string()))
-                .map(|_| ())
-        },
-    )
-}
-
 /// Relation map generic interface trait.
 pub trait RelationInterface: 'static {
     /// The type of the entity on the non-local end of the relation.
@@ -397,7 +217,7 @@ pub trait RelationInterface: 'static {
 
         let txn = Transaction::new(&rdata.conn)?;
 
-        do_connect::<Self::RemoteEntity>(rdata, an, remote_id)?;
+        base_queries::do_connect::<Self::RemoteEntity>(rdata, an, remote_id)?;
 
         txn.commit()
     }
@@ -463,9 +283,9 @@ impl<AI: RelationInterface> Insertable<AI::RemoteEntity> for AI {
         let txn = Transaction::new(&rdata.conn)?;
 
         // so first, into the remote table
-        let remote_id = insert(&rdata.conn, &value)?;
+        let remote_id = base_queries::insert(&rdata.conn, &value)?;
         // then the relation
-        do_connect::<AI::RemoteEntity>(rdata, an, remote_id)?;
+        base_queries::do_connect::<AI::RemoteEntity>(rdata, an, remote_id)?;
 
         txn.commit()?;
 
@@ -486,9 +306,9 @@ impl<AI: RelationInterface> Insertable<AI::RemoteEntity> for AI {
         let txn = Transaction::new(&rdata.conn)?;
 
         // so first, into the remote table
-        let remote = insert_and_return(&rdata.conn, value)?;
+        let remote = base_queries::insert_and_return(&rdata.conn, value)?;
         // then the relation
-        do_connect::<AI::RemoteEntity>(rdata, an, remote.id())?;
+        base_queries::do_connect::<AI::RemoteEntity>(rdata, an, remote.id())?;
 
         txn.commit()?;
 
@@ -496,76 +316,6 @@ impl<AI: RelationInterface> Insertable<AI::RemoteEntity> for AI {
     }
 }
 
-pub trait IDContainer<T: Entity>: 'static + IntoIterator<Item = T::ID> {
-    fn assemble_from(ctx: StatementContext<'_>) -> DBResult<Self>
-    where
-        Self: Sized;
-}
-
-pub trait OutputContainer<T: Entity>: 'static + IntoIterator<Item = Stored<T>> {
-    type IDContainer: IDContainer<T>;
-    type ReplacedEntity<N: Entity>: OutputContainer<N>;
-    fn assemble_from(conn: &Connection, stmt: StatementContext<'_>) -> DBResult<Self>
-    where
-        Self: Sized;
-}
-
-fn assemble_id<T: Entity>(row: StatementRow) -> T::ID {
-    <T::ID>::from_raw(row.read::<i64>(0).expect("couldn't read ID"))
-}
-
-fn assemble_single<T: Entity>(conn: &Connection, row: &mut StatementRow) -> Stored<T> {
-    let id = row.read::<i64>(0).expect("couldn't read ID");
-    let datum_list = <T::Parts>::build_datum_list(conn, row).expect("couldn't build datum list");
-    Stored::new(conn.clone(), T::ID::from_raw(id), T::build(datum_list))
-}
-
-impl<T: Entity> IDContainer<T> for Option<T::ID> {
-    fn assemble_from(ctx: StatementContext<'_>) -> DBResult<Self>
-    where
-        Self: Sized,
-    {
-        Ok(ctx.run()?.map(assemble_id::<T>))
-    }
-}
-
-impl<T: Entity> OutputContainer<T> for Option<Stored<T>> {
-    type IDContainer = Option<T::ID>;
-    type ReplacedEntity<N: Entity> = Option<Stored<N>>;
-
-    fn assemble_from(conn: &Connection, ctx: StatementContext<'_>) -> DBResult<Self>
-    where
-        Self: Sized,
-    {
-        Ok(ctx.run()?.map(|mut r| assemble_single(conn, &mut r)))
-    }
-}
-
-impl<T: Entity> IDContainer<T> for Vec<T::ID> {
-    fn assemble_from(ctx: StatementContext<'_>) -> DBResult<Self>
-    where
-        Self: Sized,
-    {
-        ctx.iter()
-            .map(|r| r.map(assemble_id::<T>))
-            .collect::<Result<Vec<_>, Error>>()
-    }
-}
-
-impl<T: Entity> OutputContainer<T> for Vec<Stored<T>> {
-    type IDContainer = Vec<T::ID>;
-    type ReplacedEntity<N: Entity> = Vec<Stored<N>>;
-
-    fn assemble_from(conn: &Connection, ctx: StatementContext<'_>) -> DBResult<Self>
-    where
-        Self: Sized,
-    {
-        ctx.iter()
-            .map(|r| r.map(|mut s| assemble_single(conn, &mut s)))
-            .collect::<Result<Vec<_>, Error>>()
-    }
-}
-
 /// Represents a searchable context of a given entity.
 pub trait Queryable: Clone {
     /// The entity that results from a search in this context.
@@ -812,8 +562,8 @@ impl<'a, T: Entity> Queryable for &'a IDMap<T> {
 
     fn build(&self) -> Query {
         Query::new()
-            .attach(QueryPart::Root, "SELECT DISTINCT".into())
-            .attach(QueryPart::Columns, "*".into())
+            .attach(QueryPart::Root, "SELECT DISTINCT")
+            .attach(QueryPart::Columns, "*")
             .attach(QueryPart::From, format!("`{}`", T::entity_name()))
     }
     fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {}
@@ -833,7 +583,7 @@ impl<'a, AI: RelationInterface> Queryable for &'a AI {
         let anames = RelationNames::collect(*self).unwrap();
         let relation_name = anames.relation_name();
         Query::new()
-            .attach(QueryPart::Root, "SELECT DISTINCT".into())
+            .attach(QueryPart::Root, "SELECT DISTINCT")
             .attach(QueryPart::Columns, format!("`{}`.*", anames.remote_name))
             .attach(QueryPart::From, format!("`{}`", relation_name))
             .attach(
@@ -882,8 +632,8 @@ impl<'a, E: Entity, EPL: EntityPartList<Entity = E>> Queryable for &'a Index<E,
 
     fn build(&self) -> Query {
         Query::new()
-            .attach(QueryPart::Root, "SELECT DISTINCT".into())
-            .attach(QueryPart::Columns, "*".into())
+            .attach(QueryPart::Root, "SELECT DISTINCT")
+            .attach(QueryPart::Columns, "*")
             .attach(QueryPart::From, format!("`{}`", E::entity_name()))
     }
     fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {}

+ 203 - 0
microrm/src/query/base_queries.rs

@@ -0,0 +1,203 @@
+use crate::{
+    db::{Connection, StatementContext},
+    schema::{
+        datum::Datum,
+        entity::{helpers::is_relation, Entity, EntityID, EntityPart, EntityPartVisitor},
+        relation::RelationData,
+        Stored,
+    },
+    DBResult, Error,
+};
+
+use super::RelationNames;
+
+pub(crate) fn insert<E: Entity>(conn: &Connection, value: &E) -> DBResult<E::ID> {
+    struct InsertQuery<E: Entity>(std::marker::PhantomData<E>);
+
+    conn.with_prepared(
+        std::any::TypeId::of::<InsertQuery<E>>(),
+        || {
+            let mut part_names = String::new();
+            let mut placeholders = String::new();
+            struct PartNameVisitor<'a, E: Entity>(
+                &'a mut String,
+                &'a mut String,
+                std::marker::PhantomData<E>,
+            );
+            impl<'a, E: Entity> EntityPartVisitor for PartNameVisitor<'a, E> {
+                type Entity = E;
+                fn visit<EP: EntityPart>(&mut self) {
+                    // if this is a set-relation, then we don't actually want to do anything
+                    // with it here; it doesn't have a column
+                    if is_relation::<EP>() {
+                        return;
+                    }
+
+                    if !self.0.is_empty() {
+                        self.0.push_str(", ");
+                        self.1.push_str(", ");
+                    }
+                    self.0.push('`');
+                    self.0.push_str(EP::part_name());
+                    self.0.push('`');
+                    self.1.push('?');
+                }
+            }
+
+            E::accept_part_visitor(&mut PartNameVisitor(
+                &mut part_names,
+                &mut placeholders,
+                Default::default(),
+            ));
+
+            format!(
+                "INSERT INTO `{}` ({}) VALUES ({}) RETURNING `id`",
+                E::entity_name(),
+                part_names,
+                placeholders
+            )
+        },
+        |mut ctx| {
+            struct PartBinder<'a, 'b, E: Entity>(
+                &'a mut StatementContext<'b>,
+                i32,
+                std::marker::PhantomData<E>,
+            );
+            impl<'a, 'b, E: Entity> EntityPartVisitor for PartBinder<'a, 'b, E> {
+                type Entity = E;
+                fn visit_datum<EP: EntityPart>(&mut self, datum: &EP::Datum) {
+                    // skip relations, as with the query preparation above
+                    if is_relation::<EP>() {
+                        return;
+                    }
+
+                    datum.bind_to(self.0, self.1);
+                    self.1 += 1;
+                }
+            }
+
+            value.accept_part_visitor_ref(&mut PartBinder(&mut ctx, 1, Default::default()));
+
+            ctx.run()?
+                .ok_or(Error::InternalError("No result row from INSERT query"))
+                .map(|r| <E::ID>::from_raw(r.read(0).expect("couldn't read resulting ID")))
+        },
+    )
+}
+
+pub(crate) fn insert_and_return<E: Entity>(conn: &Connection, mut value: E) -> DBResult<Stored<E>> {
+    let id = insert(conn, &value)?;
+
+    // update relation data in all fields
+    struct DatumWalker<'l, E: Entity>(&'l Connection, i64, std::marker::PhantomData<E>);
+    impl<'l, E: Entity> EntityPartVisitor for DatumWalker<'l, E> {
+        type Entity = E;
+        fn visit_datum_mut<EP: EntityPart>(&mut self, datum: &mut EP::Datum) {
+            datum.update_adata(RelationData {
+                conn: self.0.clone(),
+                part_name: EP::part_name(),
+                local_name: <EP::Entity as Entity>::entity_name(),
+                local_id: self.1,
+            });
+        }
+    }
+
+    value.accept_part_visitor_mut(&mut DatumWalker(conn, id.into_raw(), Default::default()));
+
+    Ok(Stored::new(conn.clone(), id, value))
+}
+
+pub(crate) fn update_entity<E: Entity>(conn: &Connection, value: &Stored<E>) -> DBResult<()> {
+    struct UpdateQuery<E: Entity>(std::marker::PhantomData<E>);
+
+    conn.with_prepared(
+        std::any::TypeId::of::<UpdateQuery<E>>(),
+        || {
+            let mut set_columns = String::new();
+            struct PartNameVisitor<'a, E: Entity>(&'a mut String, std::marker::PhantomData<E>);
+            impl<'a, E: Entity> EntityPartVisitor for PartNameVisitor<'a, E> {
+                type Entity = E;
+                fn visit<EP: EntityPart>(&mut self) {
+                    // if this is a set-relation, then we don't actually want to do anything
+                    // with it here; it doesn't have a column
+                    if is_relation::<EP>() {
+                        return;
+                    }
+
+                    if !self.0.is_empty() {
+                        self.0.push_str(", ");
+                    }
+                    self.0.push('`');
+                    self.0.push_str(EP::part_name());
+                    self.0.push_str("` = ?");
+                }
+            }
+
+            E::accept_part_visitor(&mut PartNameVisitor(&mut set_columns, Default::default()));
+            format!(
+                "UPDATE `{entity_name}` SET {set_columns} WHERE `id` = ?",
+                entity_name = E::entity_name()
+            )
+        },
+        |mut ctx| {
+            struct PartBinder<'a, 'b, E: Entity>(
+                &'a mut StatementContext<'b>,
+                &'a mut i32,
+                std::marker::PhantomData<E>,
+            );
+            impl<'a, 'b, E: Entity> EntityPartVisitor for PartBinder<'a, 'b, E> {
+                type Entity = E;
+                fn visit_datum<EP: EntityPart>(&mut self, datum: &EP::Datum) {
+                    // skip relations, as with the query preparation above
+                    if is_relation::<EP>() {
+                        return;
+                    }
+
+                    datum.bind_to(self.0, *self.1);
+                    *self.1 += 1;
+                }
+            }
+
+            // first bind all the updating clauses
+            let mut index = 1;
+            value.accept_part_visitor_ref(&mut PartBinder(
+                &mut ctx,
+                &mut index,
+                Default::default(),
+            ));
+
+            // then bind the id
+            value.id().bind_to(&mut ctx, index);
+
+            ctx.run()?;
+
+            Ok(())
+        },
+    )
+}
+
+pub(crate) fn do_connect<Remote: Entity>(
+    rdata: &RelationData,
+    an: RelationNames,
+    remote_id: Remote::ID,
+) -> DBResult<()> {
+    rdata.conn.with_prepared(
+        super::hash_of(("connect", an.local_name, an.remote_name, an.part_name)),
+        || {
+            format!(
+                "insert into `{relation_name}` (`{local_field}`, `{remote_field}`) values (?, ?) returning (`id`)",
+                relation_name = an.relation_name(),
+                local_field = an.local_field,
+                remote_field = an.remote_field
+            )
+        },
+        |ctx| {
+            ctx.bind(1, rdata.local_id)?;
+            ctx.bind(2, remote_id.into_raw())?;
+
+            ctx.run()?
+                .ok_or(Error::ConstraintViolation("Relation entry uniqueness".to_string()))
+                .map(|_| ())
+        },
+    )
+}

+ 8 - 10
microrm/src/query/components.rs

@@ -45,8 +45,8 @@ impl<E: Entity> Queryable for TableComponent<E> {
 
     fn build(&self) -> Query {
         Query::new()
-            .attach(QueryPart::Root, "SELECT DISTINCT".into())
-            .attach(QueryPart::Columns, "*".into())
+            .attach(QueryPart::Root, "SELECT DISTINCT")
+            .attach(QueryPart::Columns, "*")
             .attach(QueryPart::From, format!("`{}`", E::entity_name()))
     }
     fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {}
@@ -230,8 +230,8 @@ impl<
     fn build(&self) -> Query {
         let mut query = self.parent.build();
 
-        struct PartVisitor<'a, E: Entity>(&'a mut Query, std::marker::PhantomData<E>);
-        impl<'a, E: Entity> EntityPartVisitor for PartVisitor<'a, E> {
+        struct PartVisitor<'a, 'b, E: Entity>(&'a mut Query<'b>, std::marker::PhantomData<E>);
+        impl<'a, 'b, E: Entity> EntityPartVisitor for PartVisitor<'a, 'b, E> {
             type Entity = E;
             fn visit<EP: EntityPart>(&mut self) {
                 self.0.attach_mut(
@@ -286,9 +286,7 @@ impl<Parent: Queryable> Queryable for SingleComponent<Parent> {
     type StaticVersion = SingleComponent<Parent::StaticVersion>;
 
     fn build(&self) -> Query {
-        self.parent
-            .build()
-            .attach(QueryPart::Trailing, "LIMIT 1".into())
+        self.parent.build().attach(QueryPart::Trailing, "LIMIT 1")
     }
 
     fn bind(&self, stmt: &mut StatementContext, index: &mut i32) {
@@ -453,9 +451,9 @@ impl<FE: Entity, EP: EntityPart, Parent: Queryable> Queryable for ForeignCompone
         );
 
         Query::new()
-            .attach(QueryPart::Root, "SELECT DISTINCT".into())
-            .attach(QueryPart::Columns, "*".into())
-            .attach(QueryPart::From, FE::entity_name().into())
+            .attach(QueryPart::Root, "SELECT DISTINCT")
+            .attach(QueryPart::Columns, "*")
+            .attach(QueryPart::From, format!("`{}`", FE::entity_name()))
             .attach(
                 QueryPart::Where,
                 format!("`{}`.`id` = ({})", FE::entity_name(), subquery.assemble()),

+ 78 - 0
microrm/src/query/containers.rs

@@ -0,0 +1,78 @@
+use crate::{
+    db::{Connection, StatementContext, StatementRow},
+    schema::{
+        entity::{Entity, EntityID, EntityPartList},
+        Stored,
+    },
+    DBResult, Error,
+};
+
+pub trait IDContainer<T: Entity>: 'static + IntoIterator<Item = T::ID> {
+    fn assemble_from(ctx: StatementContext<'_>) -> DBResult<Self>
+    where
+        Self: Sized;
+}
+
+pub trait OutputContainer<T: Entity>: 'static + IntoIterator<Item = Stored<T>> {
+    type IDContainer: IDContainer<T>;
+    type ReplacedEntity<N: Entity>: OutputContainer<N>;
+    fn assemble_from(conn: &Connection, stmt: StatementContext<'_>) -> DBResult<Self>
+    where
+        Self: Sized;
+}
+
+fn assemble_id<T: Entity>(row: StatementRow) -> T::ID {
+    <T::ID>::from_raw(row.read::<i64>(0).expect("couldn't read ID"))
+}
+
+fn assemble_single<T: Entity>(conn: &Connection, row: &mut StatementRow) -> Stored<T> {
+    let id = row.read::<i64>(0).expect("couldn't read ID");
+    let datum_list = <T::Parts>::build_datum_list(conn, row).expect("couldn't build datum list");
+    Stored::new(conn.clone(), T::ID::from_raw(id), T::build(datum_list))
+}
+
+impl<T: Entity> IDContainer<T> for Option<T::ID> {
+    fn assemble_from(ctx: StatementContext<'_>) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        Ok(ctx.run()?.map(assemble_id::<T>))
+    }
+}
+
+impl<T: Entity> OutputContainer<T> for Option<Stored<T>> {
+    type IDContainer = Option<T::ID>;
+    type ReplacedEntity<N: Entity> = Option<Stored<N>>;
+
+    fn assemble_from(conn: &Connection, ctx: StatementContext<'_>) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        Ok(ctx.run()?.map(|mut r| assemble_single(conn, &mut r)))
+    }
+}
+
+impl<T: Entity> IDContainer<T> for Vec<T::ID> {
+    fn assemble_from(ctx: StatementContext<'_>) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        ctx.iter()
+            .map(|r| r.map(assemble_id::<T>))
+            .collect::<Result<Vec<_>, Error>>()
+    }
+}
+
+impl<T: Entity> OutputContainer<T> for Vec<Stored<T>> {
+    type IDContainer = Vec<T::ID>;
+    type ReplacedEntity<N: Entity> = Vec<Stored<N>>;
+
+    fn assemble_from(conn: &Connection, ctx: StatementContext<'_>) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        ctx.iter()
+            .map(|r| r.map(|mut s| assemble_single(conn, &mut s)))
+            .collect::<Result<Vec<_>, Error>>()
+    }
+}

+ 3 - 3
microrm/src/schema.rs

@@ -72,7 +72,7 @@ impl<T: Entity> Stored<T> {
     /// Synchronize the wrapped value with the corresponding database row.
     pub fn sync(&mut self) -> DBResult<()> {
         let txn = Transaction::new(&self.db)?;
-        query::update_entity(&self.db, self)?;
+        query::base_queries::update_entity(&self.db, self)?;
         txn.commit()
     }
 }
@@ -250,14 +250,14 @@ impl<T: Entity> Insertable<T> for IDMap<T> {
     /// Insert a new Entity into this map, and return its new ID.
     fn insert(&self, value: T) -> DBResult<T::ID> {
         let txn = Transaction::new(self.conn())?;
-        let out = query::insert(self.conn(), &value)?;
+        let out = query::base_queries::insert(self.conn(), &value)?;
         txn.commit()?;
         Ok(out)
     }
 
     fn insert_and_return(&self, value: T) -> DBResult<Stored<T>> {
         let txn = Transaction::new(self.conn())?;
-        let out = query::insert_and_return(self.conn(), value)?;
+        let out = query::base_queries::insert_and_return(self.conn(), value)?;
         txn.commit()?;
         Ok(out)
     }

+ 21 - 3
microrm/src/schema/entity/helpers.rs

@@ -1,15 +1,33 @@
-use crate::schema::entity::{Datum, Entity, EntityVisitor};
+use crate::schema::{
+    datum::{Datum, DatumDiscriminator},
+    entity::{Entity, EntityVisitor},
+    relation::Relation,
+};
 
 use super::EntityPart;
 
-pub fn check_relation<EP: EntityPart>() -> bool {
+pub fn is_relation<EP: EntityPart>() -> bool {
     struct Checker(bool);
+    impl DatumDiscriminator for Checker {
+        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_relation_map<E: Entity>(&mut self) {
+            self.0 = true;
+        }
+        fn visit_relation_range<R: Relation>(&mut self) {
+            self.0 = true;
+        }
+        fn visit_relation_domain<R: Relation>(&mut self) {
+            self.0 = true;
+        }
+    }
     impl EntityVisitor for Checker {
         fn visit<E: Entity>(&mut self) {
             self.0 = true;
         }
     }
     let mut checker = Checker(false);
-    <EP::Datum as Datum>::accept_entity_visitor(&mut checker);
+    <EP::Datum as Datum>::accept_discriminator(&mut checker);
     checker.0
 }