ソースを参照

Significant reworking of query interface to be more unified.

Kestrel 1 年間 前
コミット
ab06e1f3c2

+ 1 - 0
Cargo.toml

@@ -1,2 +1,3 @@
 [workspace]
+resolver = "2"
 members = ["microrm", "microrm-macros"]

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

@@ -63,9 +63,12 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
         let part_base_name = &part.0.to_string();
         let part_type = &part.1;
 
+        let placeholder = format!("${}_{}", entity_ident.to_string(), part_base_name);
+
         let unique = unique_parts.iter().any(|p| p.0 == part.0);
 
         quote! {
+            #[derive(Clone, Copy)]
             #vis struct #part_combined_name;
             impl ::microrm::schema::entity::EntityPart for #part_combined_name {
                 type Datum = #part_type;
@@ -73,6 +76,9 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
                 fn part_name() -> &'static str {
                     #part_base_name
                 }
+                fn placeholder() -> &'static str {
+                    #placeholder
+                }
                 fn unique() -> bool {
                     #unique
                 }
@@ -149,6 +155,15 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
             fn into_raw(self) -> i64 { self.0 }
         }
 
+        impl ::microrm::schema::entity::EntityPart for #id_ident {
+            type Datum = Self;
+            type Entity = #entity_ident;
+
+            fn unique() -> bool { false }
+            fn part_name() -> &'static str { "id" }
+            fn placeholder() -> &'static str { "TODO" }
+        }
+
         impl ::microrm::schema::datum::Datum for #id_ident {
             fn sql_type() -> &'static str {
                 <i64 as ::microrm::schema::datum::Datum>::sql_type()

+ 0 - 2
microrm/Cargo.toml

@@ -14,7 +14,6 @@ base64 = "0.13"
 sha2 = "0.10"
 sqlite = "0.33"
 serde = { version = "1.0", features = ["derive"] }
-serde_bytes = { version = "0.11.6" }
 serde_json = { version = "1.0" }
 lazy_static = { version = "1.4.0" }
 time = "0.3"
@@ -28,7 +27,6 @@ topological-sort = { version = "0.2" }
 criterion = "0.5"
 rand = "0.8.5"
 stats_alloc = "0.1.10"
-async-std = "1.11"
 
 # [[bench]]
 # name = "simple_in_memory"

+ 34 - 3
microrm/src/db.rs

@@ -12,6 +12,26 @@ pub(crate) struct CachedStatement {
     sql: String,
 }
 
+pub(crate) trait PreparedKey {
+    fn into_u64(self) -> u64;
+}
+
+impl PreparedKey for u64 {
+    fn into_u64(self) -> u64 {
+        self
+    }
+}
+
+impl PreparedKey for std::any::TypeId {
+    fn into_u64(self) -> u64 {
+        use std::hash::Hash;
+        use std::hash::Hasher;
+        let mut hasher = std::collections::hash_map::DefaultHasher::new();
+        self.hash(&mut hasher);
+        hasher.finish()
+    }
+}
+
 pub struct Connection {
     // we leak the ConnectionThreadSafe and make sure that the only references to it are stored in
     // statement_cache, so as long as we drop the statement_cache first there are no correctness
@@ -46,17 +66,22 @@ impl Connection {
 
     pub(crate) fn with_prepared<R>(
         &self,
-        hash_key: u64,
+        hash_key: impl PreparedKey,
         build_query: impl Fn() -> String,
         run_query: impl Fn(&mut sqlite::Statement<'static>) -> DBResult<R>,
     ) -> DBResult<R> {
-        match self.statement_cache.lock()?.entry(hash_key) {
+        match self.statement_cache.lock()?.entry(hash_key.into_u64()) {
             std::collections::hash_map::Entry::Vacant(e) => {
                 let sql = build_query();
 
+                let q: sqlite::Statement<'static> = self
+                    .conn
+                    .prepare(sql.as_str())
+                    .map_err(|e| DBError::from(e).sqlite_to_query(sql.as_str()))?;
+
+                println!("built SQL query: {sql}");
                 log::trace!("built SQL query: {sql}");
 
-                let q: sqlite::Statement<'static> = self.conn.prepare(sql.as_str())?;
                 let inserted = e.insert(CachedStatement {
                     sql: sql.clone(),
                     stmt: q,
@@ -82,3 +107,9 @@ impl Drop for Connection {
         }
     }
 }
+
+impl std::fmt::Debug for Connection {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str("microrm::Connection")
+    }
+}

+ 2 - 1
microrm/src/lib.rs

@@ -9,7 +9,8 @@ pub mod schema;
 pub use sqlite;
 
 pub mod prelude {
-    pub use crate::schema::entity::Entity;
+    pub use crate::query::{AssocInterface, Queryable};
+    // pub use crate::schema::entity::Entity;
     pub use crate::schema::{AssocMap, Database, IDMap};
     pub use microrm_macros::{Database, Entity};
 }

+ 347 - 345
microrm/src/query.rs

@@ -4,208 +4,18 @@ use crate::schema::{AssocData, IDWrap, LocalSide};
 use crate::{
     schema::datum::{Datum, DatumList, DatumVisitor},
     schema::entity::{Entity, EntityID, EntityPart, EntityPartList, EntityPartVisitor},
-    schema::{EntityMap, IDMap},
 };
 use crate::{DBError, DBResult};
+use std::collections::HashMap;
 use std::hash::{Hash, Hasher};
 
-#[derive(Hash)]
-enum QueryType<'a> {
-    ByID(&'a str),
-    Delete(std::any::TypeId),
-    DeleteAssoc(&'a str, &'a str, &'a str),
-    DeleteById,
-    Select(std::any::TypeId),
-    SelectJoin(&'a str, &'a str, &'a str),
-    SelectJoinFilter(&'a str, &'a str, &'a str, std::any::TypeId),
-    Insert,
-    InsertAssoc(&'a str, &'a str, &'a str),
-}
-
-fn bind_datum_to<DL: DatumList>(
-    stmt: &mut sqlite::Statement,
-    dl: &DL,
-    start_index: usize,
-) -> DBResult<()> {
-    struct BindDatum<'a, 'b>(&'a mut sqlite::Statement<'b>, usize);
-    impl<'a, 'b> DatumVisitor for BindDatum<'a, 'b> {
-        fn visit<ED: Datum>(&mut self, datum: &ED) {
-            datum.bind_to(self.0, self.1);
-            self.1 += 1
-        }
-    }
-    // note that this indexing starts at 1
-    dl.accept(&mut BindDatum(stmt, start_index));
-
-    Ok(())
-}
-
-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);
-
-    hasher.finish()
-}
-
-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 get_all<E: Entity>(conn: &DBConnection) -> DBResult<Vec<IDWrap<E>>> {
-    conn.with_prepared(
-        query_hash::<E>(QueryType::Select(std::any::TypeId::of::<E>())),
-        || format!("select * from `{}`", E::entity_name()),
-        |stmt| {
-            stmt.reset()?;
-
-            let mut rows = vec![];
-            while stmt.next()? == sqlite::State::Row {
-                let datum_list = <E::Parts>::build_datum_list(&conn, stmt)?;
-                rows.push(IDWrap::new(
-                    <E::ID>::from_raw(stmt.read::<i64, _>(0)?),
-                    E::build(datum_list),
-                ));
-            }
-
-            stmt.reset()?;
-
-            Ok(rows)
-        },
-    )
-}
-
-pub(crate) fn select_by<E: Entity, PL: EntityPartList>(
-    map: &IDMap<E>,
-    by: &PL::DatumList,
-) -> DBResult<Vec<IDWrap<E>>> {
-    map.conn().with_prepared(
-        query_hash::<E>(QueryType::Select(std::any::TypeId::of::<PL>())),
-        || {
-            let mut conditions = String::new();
-            struct BuildConditions<'a>(&'a mut String);
-            impl<'a> EntityPartVisitor for BuildConditions<'a> {
-                fn visit<EP: EntityPart>(&mut self) {
-                    if self.0.len() > 0 {
-                        self.0.push_str(" and ");
-                    }
-                    self.0.push_str("`");
-                    self.0.push_str(EP::part_name());
-                    self.0.push_str("`");
-                    self.0.push_str(" = ?");
-                }
-            }
-            PL::accept_part_visitor(&mut BuildConditions(&mut conditions));
-
-            let table_name = format!("{}", E::entity_name());
-            format!("select * from `{}` where {}", table_name, conditions)
-        },
-        |stmt| {
-            struct BindDatum<'a, 'b>(&'a mut sqlite::Statement<'b>, usize);
-            impl<'a, 'b> DatumVisitor for BindDatum<'a, 'b> {
-                fn visit<ED: Datum>(&mut self, datum: &ED) {
-                    datum.bind_to(self.0, self.1);
-                    self.1 += 1
-                }
-            }
-            // note that this indexing starts at 1
-            by.accept(&mut BindDatum(stmt, 1));
-
-            // now we grab the statement outputs
-            let mut rows = vec![];
-            while stmt.next()? == sqlite::State::Row {
-                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),
-                ));
-            }
-
-            stmt.reset()?;
-
-            Ok(rows)
-        },
-    )
-}
-
-pub(crate) fn delete_by_id<E: Entity>(conn: &DBConnection, id: E::ID) -> DBResult<()> {
-    conn.with_prepared(
-        query_hash::<E>(QueryType::DeleteById),
-        || format!("delete from `{}` where `id` = ?", E::entity_name()),
-        |stmt| {
-            stmt.reset()?;
-            stmt.bind((1, id.into_raw()))?;
-            stmt.next()?;
-            Ok(())
-        },
-    )
-}
-
-pub(crate) fn delete_by<E: Entity, PL: EntityPartList>(
-    conn: &DBConnection,
-    values: &PL::DatumList,
-) -> DBResult<()> {
-    conn.with_prepared(
-        query_hash::<E>(QueryType::Delete(std::any::TypeId::of::<PL>())),
-        || {
-            let mut conditions = String::new();
-            struct BuildConditions<'a>(&'a mut String);
-            impl<'a> EntityPartVisitor for BuildConditions<'a> {
-                fn visit<EP: EntityPart>(&mut self) {
-                    if self.0.len() > 0 {
-                        self.0.push_str(" and ");
-                    }
-                    self.0.push_str("`");
-                    self.0.push_str(EP::part_name());
-                    self.0.push_str("`");
-                    self.0.push_str(" = ?");
-                }
-            }
-            PL::accept_part_visitor(&mut BuildConditions(&mut conditions));
-
-            format!("delete from `{}` where {}", E::entity_name(), conditions)
-        },
-        |stmt| {
-            stmt.reset()?;
-            bind_datum_to(stmt, values, 1)?;
-            stmt.next()?;
-            Ok(())
-        },
-    )
-}
+pub(crate) mod components;
 
 pub(crate) fn insert<E: Entity>(conn: &DBConnection, value: E) -> DBResult<E::ID> {
+    struct InsertQuery<E: Entity>(std::marker::PhantomData<E>);
+
     conn.with_prepared(
-        query_hash::<E>(QueryType::Insert),
+        std::any::TypeId::of::<InsertQuery<E>>(),
         || {
             let table_name = format!("{}", E::entity_name());
 
@@ -234,7 +44,7 @@ pub(crate) fn insert<E: Entity>(conn: &DBConnection, value: E) -> DBResult<E::ID
             E::accept_part_visitor(&mut PartNameVisitor(&mut part_names, &mut placeholders));
 
             format!(
-                "insert into `{}` ({}) values ({}) returning `id`",
+                "INSERT INTO `{}` ({}) VALUES ({}) RETURNING `id`",
                 table_name, part_names, placeholders
             )
         },
@@ -263,6 +73,129 @@ pub(crate) fn insert<E: Entity>(conn: &DBConnection, value: E) -> DBResult<E::ID
     )
 }
 
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub(crate) enum QueryPart {
+    Root,
+    Columns,
+    From,
+    Set,
+    Join,
+    Where,
+    Trailing,
+}
+
+#[derive(Debug)]
+pub struct Query<'a> {
+    conn: &'a DBConnection,
+    parts: HashMap<QueryPart, Vec<String>>,
+}
+
+impl<'a> Query<'a> {
+    pub(crate) fn new(conn: &'a DBConnection) -> Self {
+        Self {
+            conn,
+            parts: Default::default(),
+        }
+    }
+
+    pub(crate) fn attach(mut self, qp: QueryPart, val: String) -> Self {
+        self.attach_mut(qp, val);
+        self
+    }
+
+    pub(crate) fn replace(mut self, qp: QueryPart, val: String) -> Self {
+        self.parts.remove(&qp);
+        self.attach(qp, val)
+    }
+
+    pub(crate) fn attach_mut(&mut self, qp: QueryPart, val: String) {
+        self.parts.entry(qp).or_default().push(val);
+    }
+
+    pub(crate) fn assemble(mut self) -> String {
+        let root = self.parts.remove(&QueryPart::Root).unwrap().remove(0);
+
+        let columns_ = match self.parts.remove(&QueryPart::Columns) {
+            None => String::new(),
+            Some(v) => {
+                format!(
+                    "{}",
+                    v.into_iter()
+                        .reduce(|a, b| format!("{}, {}", a, b))
+                        .unwrap()
+                )
+            }
+        };
+
+        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()
+                )
+            }
+        };
+
+        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()
+                )
+            }
+        };
+
+        let join_ = match self.parts.remove(&QueryPart::Join) {
+            None => String::new(),
+            Some(v) => {
+                format!(
+                    "INNER JOIN {}",
+                    v.into_iter().reduce(|a, b| format!("{} {}", a, b)).unwrap()
+                )
+            }
+        };
+
+        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()
+                )
+            }
+        };
+
+        let trailing_ = match self.parts.remove(&QueryPart::Trailing) {
+            None => String::new(),
+            Some(v) => {
+                format!(
+                    "{}",
+                    v.into_iter().reduce(|a, b| format!("{} {}", a, b)).unwrap()
+                )
+            }
+        };
+
+        println!(
+            "built SQL query: {} {} {} {} {} {} {}",
+            root, columns_, from_, set_, join_, where_, trailing_
+        );
+        // log::trace!("built SQL query: {} {} {}", root, set_, where_);
+
+        format!(
+            "{} {} {} {} {} {} {}",
+            root, columns_, from_, set_, join_, where_, trailing_
+        )
+    }
+}
+
 pub(crate) struct AssocNames {
     local_name: &'static str,
     remote_name: &'static str,
@@ -312,148 +245,19 @@ impl AssocNames {
     }
 }
 
-pub(crate) trait AssocInterface {
+fn hash_of<T: Hash>(val: T) -> u64 {
+    let mut hasher = std::collections::hash_map::DefaultHasher::new();
+    val.hash(&mut hasher);
+    hasher.finish()
+}
+
+pub trait AssocInterface: 'static {
     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<IDWrap<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 id = stmt.read::<i64, _>(0)?;
-                    let datum_list = <<Self::RemoteEntity as Entity>::Parts>::build_datum_list(
-                        &adata.conn,
-                        stmt,
-                    )?;
-                    rows.push(IDWrap::new(
-                        <Self::RemoteEntity as Entity>::ID::from_raw(id),
-                        Self::RemoteEntity::build(datum_list),
-                    ));
-                }
-
-                Ok(rows)
-            },
-        )
-    }
-
-    fn lookup_by<PL: EntityPartList>(
-        &self,
-        values: &PL::DatumList,
-    ) -> DBResult<Vec<IDWrap<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
-        //      AND `unique_1` = ?
-
-        let an = AssocNames::collect::<Self>(&self)?;
-
-        adata.conn.with_prepared(
-            query_hash::<Self::RemoteEntity>(QueryType::SelectJoinFilter(
-                an.local_name,
-                an.remote_name,
-                an.part_name,
-                std::any::TypeId::of::<<Self::RemoteEntity as Entity>::Uniques>(),
-            )),
-            || {
-                let mut conditions = String::new();
-                struct BuildConditions<'a>(&'a mut String);
-                impl<'a> EntityPartVisitor for BuildConditions<'a> {
-                    fn visit<EP: EntityPart>(&mut self) {
-                        self.0.push_str(" and ");
-                        self.0.push_str("`");
-                        self.0.push_str(EP::part_name());
-                        self.0.push_str("`");
-                        self.0.push_str(" = ?");
-                    }
-                }
-
-                <<Self::RemoteEntity as Entity>::Uniques>::accept_part_visitor(
-                    &mut BuildConditions(&mut conditions),
-                );
-
-                format!(
-                    "select `{remote_name}`.* from `{assoc_name}` \
-                        left join `{remote_name}` on \
-                            `{assoc_name}`.`{remote_field}` = `{remote_name}`.`id` \
-                        where `{assoc_name}`.`{local_field}` = ? {conditions}",
-                    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))?;
-                bind_datum_to(stmt, values, 2);
-
-                // now we grab the statement outputs
-                let mut rows = vec![];
-                while stmt.next()? == sqlite::State::Row {
-                    let id = stmt.read::<i64, _>(0)?;
-                    let datum_list = <<Self::RemoteEntity as Entity>::Parts>::build_datum_list(
-                        &adata.conn,
-                        stmt,
-                    )?;
-                    rows.push(IDWrap::new(
-                        <Self::RemoteEntity as Entity>::ID::from_raw(id),
-                        Self::RemoteEntity::build(datum_list),
-                    ));
-                }
-
-                Ok(rows)
-            },
-        )
-    }
-
-    fn associate_with(&self, remote_id: <Self::RemoteEntity as Entity>::ID) -> DBResult<()>
+    fn connect_to(&self, remote_id: <Self::RemoteEntity as Entity>::ID) -> DBResult<()>
     where
         Self: Sized,
     {
@@ -462,11 +266,7 @@ pub(crate) trait AssocInterface {
 
         // 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,
-            )),
+            hash_of(("connect", an.local_name, an.remote_name, an.part_name)),
             || {
                 format!(
                     "insert into `{assoc_name}` (`{local_field}`, `{remote_field}`) values (?, ?)",
@@ -487,7 +287,7 @@ pub(crate) trait AssocInterface {
         )
     }
 
-    fn dissociate_with(&self, remote_id: <Self::RemoteEntity as Entity>::ID) -> DBResult<()>
+    fn disconnect_from(&self, remote_id: <Self::RemoteEntity as Entity>::ID) -> DBResult<()>
     where
         Self: Sized,
     {
@@ -496,11 +296,7 @@ pub(crate) trait AssocInterface {
 
         // second, add to the assoc table
         adata.conn.with_prepared(
-            query_hash::<Self::RemoteEntity>(QueryType::DeleteAssoc(
-                an.local_name,
-                an.remote_name,
-                an.part_name,
-            )),
+            hash_of(("disconnect", an.local_name, an.remote_name, an.part_name)),
             || {
                 format!(
                     "delete from `{assoc_name}` where `{local_field}` = ? and `{remote_field}` = ?",
@@ -534,8 +330,214 @@ pub(crate) trait AssocInterface {
         // so first, the remote table
         let remote_id = insert(&adata.conn, value)?;
         // then the association
-        self.associate_with(remote_id)?;
+        self.connect_to(remote_id)?;
         // TODO: handle error case of associate_with() fails but insert() succeeds
         Ok(remote_id)
     }
 }
+
+// ----------------------------------------------------------------------
+// New query interface
+// ----------------------------------------------------------------------
+
+pub trait OutputContainer: 'static {
+    fn assemble_from(conn: &DBConnection, stmt: &mut sqlite::Statement<'static>) -> DBResult<Self>
+    where
+        Self: Sized;
+}
+
+impl<T: Entity> OutputContainer for Option<IDWrap<T>> {
+    fn assemble_from(conn: &DBConnection, stmt: &mut sqlite::Statement<'static>) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        if stmt.next()? == sqlite::State::Row {
+            let id = stmt.read::<i64, _>(0)?;
+            let datum_list = <T::Parts>::build_datum_list(conn, stmt)?;
+            Ok(Some(IDWrap::new(T::ID::from_raw(id), T::build(datum_list))))
+        } else {
+            Ok(None)
+        }
+    }
+}
+
+impl<T: Entity> OutputContainer for Vec<IDWrap<T>> {
+    fn assemble_from(conn: &DBConnection, stmt: &mut sqlite::Statement<'static>) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        let mut rows = vec![];
+        while stmt.next()? == sqlite::State::Row {
+            let id = stmt.read::<i64, _>(0)?;
+            let datum_list = <T::Parts>::build_datum_list(conn, stmt)?;
+            rows.push(IDWrap::new(T::ID::from_raw(id), T::build(datum_list)));
+        }
+
+        Ok(rows)
+    }
+}
+
+pub trait Queryable {
+    type EntityOutput: Entity;
+    type OutputContainer: OutputContainer;
+    type StaticVersion: Queryable + 'static;
+
+    fn build<'s, 'q: 's>(&'s self) -> Query<'s>;
+    fn bind(&self, stmt: &mut sqlite::Statement, index: &mut usize);
+
+    // ----------------------------------------------------------------------
+    // Verbs
+    // ----------------------------------------------------------------------
+    /// Count all entities in the current context.
+    ///
+    /// Returns the number of entities.
+    fn count(self) -> DBResult<usize>
+    where
+        Self: Sized,
+    {
+        todo!()
+    }
+    /// Get all entities in the current context.
+    fn get(self) -> DBResult<Self::OutputContainer>
+    where
+        Self: Sized,
+    {
+        let q = self.build();
+        q.conn.with_prepared(
+            std::any::TypeId::of::<Self::StaticVersion>(),
+            || self.build().assemble(),
+            |stmt| {
+                stmt.reset()?;
+
+                // starting index is 1
+                let mut index = 1;
+                self.bind(stmt, &mut index);
+
+                <Self::OutputContainer>::assemble_from(q.conn, stmt)
+            },
+        )
+    }
+    /// Delete all entities in the current context.
+    ///
+    /// Returns the number of entities deleted.
+    fn delete(self) -> DBResult<usize>
+    where
+        Self: Sized,
+    {
+        todo!()
+    }
+
+    // ----------------------------------------------------------------------
+    // Filtering methods
+    // ----------------------------------------------------------------------
+    /// Filter using the unique index on the entity.
+    fn unique(
+        self,
+        values: <<<Self::EntityOutput as Entity>::Uniques as EntityPartList>::DatumList as DatumList>::Ref<'_>,
+    ) -> impl Queryable<
+        EntityOutput = Self::EntityOutput,
+        OutputContainer = Option<IDWrap<Self::EntityOutput>>,
+    >
+    where
+        Self: Sized,
+    {
+        components::UniqueComponent::new(self, values)
+    }
+    /// Filter using an arbitrary column on the entity.
+    fn with<EP: EntityPart<Entity = Self::EntityOutput>>(
+        self,
+        part: EP,
+        value: &EP::Datum,
+    ) -> impl Queryable<EntityOutput = Self::EntityOutput, OutputContainer = Self::OutputContainer>
+    where
+        Self: Sized,
+    {
+        components::WithComponent::new(self, part, value)
+    }
+
+    /// Ask to return at most a single result
+    fn first(
+        self,
+    ) -> impl Queryable<
+        EntityOutput = Self::EntityOutput,
+        OutputContainer = Option<IDWrap<Self::EntityOutput>>,
+    >
+    where
+        Self: Sized,
+    {
+        components::SingleComponent::new(self)
+    }
+
+    // ----------------------------------------------------------------------
+    // Association-following and joining methods
+    // ----------------------------------------------------------------------
+    /// Join based on an existing association
+    fn join<AD: AssocInterface, EP: EntityPart<Entity = Self::EntityOutput, Datum = AD>>(
+        self,
+        part: EP,
+    ) -> impl Queryable<EntityOutput = AD::RemoteEntity>
+    where
+        Self: Sized,
+    {
+        components::JoinComponent::<AD::RemoteEntity, Self::EntityOutput, _, Self>::new(self, part)
+    }
+}
+
+// Generic implementation for all assoc specification types
+impl<'a, AI: AssocInterface> Queryable for &'a AI {
+    type EntityOutput = AI::RemoteEntity;
+    type OutputContainer = Vec<IDWrap<AI::RemoteEntity>>;
+    type StaticVersion = &'static AI;
+
+    fn build<'s, 'q: 's>(&'s self) -> Query<'s> {
+        unreachable!()
+    }
+
+    fn bind(&self, _stmt: &mut sqlite::Statement, _index: &mut usize) {
+        unreachable!()
+    }
+
+    fn count(self) -> DBResult<usize> {
+        components::AssocQueryable::new(self).count()
+    }
+
+    fn get(self) -> DBResult<Self::OutputContainer> {
+        components::AssocQueryable::new(self).get()
+    }
+
+    fn delete(self) -> DBResult<usize> {
+        components::AssocQueryable::new(self).delete()
+    }
+
+    fn unique(
+        self,
+        values: <<<Self::EntityOutput as Entity>::Uniques as EntityPartList>::DatumList as DatumList>::Ref<'_>,
+    ) -> impl Queryable<
+        EntityOutput = Self::EntityOutput,
+        OutputContainer = Option<IDWrap<Self::EntityOutput>>,
+    > {
+        components::AssocQueryable::new(self).unique(values)
+    }
+
+    fn with<EP: EntityPart<Entity = Self::EntityOutput>>(
+        self,
+        part: EP,
+        value: &EP::Datum,
+    ) -> impl Queryable<EntityOutput = Self::EntityOutput, OutputContainer = Self::OutputContainer>
+    {
+        components::AssocQueryable::new(self).with(part, value)
+    }
+
+    fn join<AD: AssocInterface, EP: EntityPart<Entity = Self::EntityOutput, Datum = AD>>(
+        self,
+        part: EP,
+    ) -> impl Queryable<EntityOutput = AD::RemoteEntity> {
+        components::AssocQueryable::new(self).join(part)
+    }
+}
+
+#[cfg(test)]
+mod query_build_test {
+    #[test]
+    fn simple_construction() {}
+}

+ 287 - 0
microrm/src/query/components.rs

@@ -0,0 +1,287 @@
+//! Component types for query construction.
+
+use crate::{
+    prelude::Queryable,
+    query::{AssocInterface, QueryPart},
+    schema::{
+        datum::{Datum, DatumList, DatumListRef, DatumVisitor},
+        entity::{Entity, EntityPart, EntityPartList, EntityPartVisitor},
+        IDMap, IDWrap,
+    },
+    DBResult,
+};
+
+use super::Query;
+
+/// Concrete implementation of Queryable for an IDMap
+pub(crate) struct MapQueryable<'a, E: Entity> {
+    map: &'a IDMap<E>,
+}
+
+impl<'a, E: Entity> MapQueryable<'a, E> {
+    pub fn new(map: &'a IDMap<E>) -> Self {
+        Self { map }
+    }
+}
+
+impl<'a, E: Entity> Queryable for MapQueryable<'a, E> {
+    type EntityOutput = E;
+    type OutputContainer = Vec<IDWrap<E>>;
+    type StaticVersion = MapQueryable<'static, E>;
+
+    fn build<'s, 'q: 's>(&'s self) -> Query<'s> {
+        Query::new(&self.map.conn())
+            .attach(QueryPart::Root, "SELECT".into())
+            .attach(QueryPart::Columns, "*".into())
+            .attach(QueryPart::From, format!("`{}`", E::entity_name()))
+    }
+    fn bind(&self, _stmt: &mut sqlite::Statement, _index: &mut usize) {}
+}
+
+/// Concrete implementation of Queryable for an IDMap
+pub(crate) struct AssocQueryable<'a, AI: AssocInterface> {
+    assoc: &'a AI,
+}
+
+impl<'a, AI: AssocInterface> AssocQueryable<'a, AI> {
+    pub fn new(assoc: &'a AI) -> Self {
+        Self { assoc }
+    }
+}
+
+impl<'a, AI: AssocInterface> Queryable for AssocQueryable<'a, AI> {
+    type EntityOutput = AI::RemoteEntity;
+    type OutputContainer = Vec<IDWrap<AI::RemoteEntity>>;
+    type StaticVersion = AssocQueryable<'static, AI>;
+
+    fn build<'s, 'q: 's>(&'s self) -> Query<'s> {
+        let adata = self
+            .assoc
+            .get_data()
+            .expect("building query for assoc with no data");
+        let anames = super::AssocNames::collect(self.assoc).unwrap();
+        let assoc_name = anames.assoc_name();
+        Query::new(&adata.conn)
+            .attach(QueryPart::Root, "SELECT".into())
+            .attach(QueryPart::Columns, format!("`{}`.*", anames.remote_name))
+            .attach(QueryPart::From, format!("`{}`", assoc_name))
+            .attach(
+                QueryPart::Join,
+                format!(
+                    "`{}` ON `{}`.`id` = `{}`.`{}`",
+                    anames.remote_name, anames.remote_name, assoc_name, anames.remote_field
+                ),
+            )
+            .attach(
+                QueryPart::Where,
+                format!("`{}`.`{}` = ?", assoc_name, anames.local_field),
+            )
+    }
+    fn bind(&self, stmt: &mut sqlite::Statement, index: &mut usize) {
+        let adata = self
+            .assoc
+            .get_data()
+            .expect("binding query for assoc with no data");
+
+        stmt.bind((*index, adata.local_id))
+            .expect("couldn't bind assoc id");
+        *index += 1;
+    }
+}
+
+/// Filter on a Datum
+pub(crate) struct WithComponent<'a, WEP: EntityPart, Parent: Queryable> {
+    datum: &'a WEP::Datum,
+    parent: Parent,
+    _ghost: std::marker::PhantomData<WEP>,
+}
+
+impl<'a, WEP: EntityPart, Parent: Queryable> WithComponent<'a, WEP, Parent> {
+    pub fn new(parent: Parent, part: WEP, datum: &'a WEP::Datum) -> Self {
+        Self {
+            datum,
+            parent,
+            _ghost: Default::default(),
+        }
+    }
+}
+
+impl<'a, WEP: EntityPart, Parent: Queryable> Queryable for WithComponent<'a, WEP, Parent> {
+    type EntityOutput = WEP::Entity;
+    type OutputContainer = Parent::OutputContainer;
+    type StaticVersion = WithComponent<'static, WEP, Parent::StaticVersion>;
+
+    fn build<'s, 'q: 's>(&'s self) -> Query<'s> {
+        self.parent.build().attach(
+            QueryPart::Where,
+            format!(
+                "`{}`.`{}` = ?",
+                <Self::EntityOutput>::entity_name(),
+                WEP::part_name()
+            ),
+        )
+    }
+    fn bind(&self, stmt: &mut sqlite::Statement, index: &mut usize) {
+        self.parent.bind(stmt, index);
+        self.datum.bind_to(stmt, *index);
+        *index += 1;
+    }
+}
+
+/// Filter on the unique index
+pub(crate) struct UniqueComponent<'a, E: Entity, Parent: Queryable> {
+    datum: <<E::Uniques as EntityPartList>::DatumList as DatumList>::Ref<'a>,
+    parent: Parent,
+}
+
+impl<'a, E: Entity, Parent: Queryable> UniqueComponent<'a, E, Parent> {
+    pub fn new(
+        parent: Parent,
+        datum: <<E::Uniques as EntityPartList>::DatumList as DatumList>::Ref<'a>,
+    ) -> Self {
+        Self { datum, parent }
+    }
+}
+
+impl<'a, E: Entity, Parent: Queryable> Queryable for UniqueComponent<'a, E, Parent> {
+    type EntityOutput = E;
+    type OutputContainer = Option<IDWrap<E>>;
+    type StaticVersion = UniqueComponent<'static, E, Parent::StaticVersion>;
+
+    fn build<'s, 'q: 's>(&'s self) -> Query<'s> {
+        let mut query = self.parent.build();
+
+        struct PartVisitor<'a, 'b>(&'a mut Query<'b>);
+        impl<'a, 'b> EntityPartVisitor for PartVisitor<'a, 'b> {
+            fn visit<EP: EntityPart>(&mut self) {
+                self.0.attach_mut(
+                    QueryPart::Where,
+                    format!(
+                        "`{}`.`{}` = ?",
+                        <EP::Entity>::entity_name(),
+                        EP::part_name()
+                    ),
+                );
+            }
+        }
+
+        <E::Uniques>::accept_part_visitor(&mut PartVisitor(&mut query));
+
+        query
+    }
+
+    fn bind(&self, stmt: &mut sqlite::Statement, index: &mut usize) {
+        self.parent.bind(stmt, index);
+
+        struct Visitor<'a, 'b>(&'a mut sqlite::Statement<'b>, &'a mut usize);
+        impl<'a, 'b> DatumVisitor for Visitor<'a, 'b> {
+            fn visit<ED: Datum>(&mut self, datum: &ED) {
+                datum.bind_to(self.0, *self.1);
+                *self.1 += 1;
+            }
+        }
+
+        self.datum.accept(&mut Visitor(stmt, index));
+    }
+}
+
+pub(crate) struct SingleComponent<Parent: Queryable> {
+    parent: Parent,
+}
+
+impl<Parent: Queryable> SingleComponent<Parent> {
+    pub fn new(parent: Parent) -> Self {
+        Self { parent }
+    }
+}
+
+impl<Parent: Queryable> Queryable for SingleComponent<Parent> {
+    type EntityOutput = Parent::EntityOutput;
+    type OutputContainer = Option<IDWrap<Self::EntityOutput>>;
+    type StaticVersion = SingleComponent<Parent::StaticVersion>;
+
+    fn build<'s, 'q: 's>(&'s self) -> Query<'s> {
+        self.parent
+            .build()
+            .attach(QueryPart::Trailing, "LIMIT 1".into())
+    }
+
+    fn bind(&self, stmt: &mut sqlite::Statement, index: &mut usize) {
+        self.parent.bind(stmt, index)
+    }
+}
+
+/// Join with another entity via an association
+pub(crate) struct JoinComponent<R: Entity, L: Entity, EP: EntityPart<Entity = L>, Parent: Queryable>
+{
+    parent: Parent,
+    _ghost: std::marker::PhantomData<(L, R, EP)>,
+}
+
+impl<R: Entity, L: Entity, EP: EntityPart<Entity = L>, Parent: Queryable>
+    JoinComponent<R, L, EP, Parent>
+{
+    pub fn new(parent: Parent, _part: EP) -> Self {
+        Self {
+            parent,
+            _ghost: Default::default(),
+        }
+    }
+}
+
+impl<R: Entity, L: Entity, EP: EntityPart<Entity = L>, Parent: Queryable> Queryable
+    for JoinComponent<R, L, EP, Parent>
+{
+    type EntityOutput = R;
+    type OutputContainer = Vec<IDWrap<R>>;
+    type StaticVersion = JoinComponent<R, L, EP, Parent::StaticVersion>;
+
+    fn build<'s, 'q: 's>(&'s self) -> Query<'s> {
+        // self.parent.build()
+        todo!()
+    }
+    fn bind(&self, stmt: &mut sqlite::Statement, index: &mut usize) {
+        self.parent.bind(stmt, index);
+        todo!()
+    }
+}
+
+/*
+SQL mapping:
+
+MapQueryable<E>::get()
+ -> SELECT * FROM {E::entity_name()}
+
+UniqueComponent<E, MapQueryable<E>>::get()
+ -> SELECT * FROM {E::entity_name()} WHERE `col1` = ? AND `col2` = ? AND ...
+
+WithComponent<E, EP, MapQueryable<E>>::get()
+ -> SELECT * FROM {E::entity_name()} WHERE `col1` = ?
+
+AssocQueryable<AI>::get()
+ -> SELECT
+        `{AI::RemoteEntity::entity_name()}`.*
+    FROM `{assoc_table_name}`
+    LEFT JOIN `{AI::RemoteEntity::entity_name()}`.id = `{assoc_table_name}`.`{remote_field}`
+    WHERE `{assoc_table_name}`.`{local_field}` = ?
+
+UniqueComponent<E, AssocQueryable<AI>>::get()
+ -> SELECT
+        `{AI::RemoteEntity::entity_name()}`.*
+    FROM `{assoc_table_name}`
+    LEFT JOIN `{AI::RemoteEntity::entity_name()}`.id = `{assoc_table_name}`.`{remote_field}`
+    WHERE
+        `{assoc_table_name}`.`{local_field}` = ?
+        AND `{AI::RemoteEntity::entity_name()}`.`col1` = ?
+        AND `{AI::RemoteEntity::entity_name()}`.`col2` = ?
+        ...
+
+JoinComponent<R, E, EP, MapQueryable<E>>::get()
+ -> SELECT DISTINCT
+        `{R::entity_name()}`.*
+    FROM
+        `{E::entity_name()}`
+    LEFT JOIN `{assoc_table_name}` ON `{E::entity_name()}`.`id` = `{assoc_table_name}`.`{local_field}`
+    LEFT JOIN `{R::entity_name()}` ON `{assoc_table_name}`.`{remote_field}` = `{R::entity_name()}`.`id`
+
+*/

+ 83 - 77
microrm/src/schema.rs

@@ -6,6 +6,8 @@
 //! - local: the current side of an association
 //! - remote: the opposite side of an association
 
+use query::Queryable;
+
 use crate::{
     db::{Connection, DBConnection},
     query::{self, AssocInterface},
@@ -107,7 +109,7 @@ pub trait Relation: 'static {
     const NAME: &'static str;
 }
 
-pub(crate) enum LocalSide {
+pub enum LocalSide {
     Domain,
     Range,
 }
@@ -170,38 +172,15 @@ impl<T: Entity> AssocMap<T> {
             _ghost: Default::default(),
         }
     }
-
-    pub fn get_all(&self) -> DBResult<Vec<IDWrap<T>>> {
-        <Self as AssocInterface>::get_all(self)
-    }
-
-    pub fn lookup_unique(
-        &self,
-        values: &<T::Uniques as EntityPartList>::DatumList,
-    ) -> DBResult<Option<IDWrap<T>>> {
-        <Self as AssocInterface>::lookup_by::<T::Uniques>(self, values).map(|mut v| v.pop())
-    }
-
-    pub fn associate_with(&self, remote_id: T::ID) -> DBResult<()> {
-        <Self as AssocInterface>::associate_with(self, remote_id)
-    }
-
-    pub fn dissociate_with(&self, remote_id: T::ID) -> DBResult<()> {
-        <Self as AssocInterface>::dissociate_with(self, remote_id)
-    }
-
-    pub fn insert(&self, value: T) -> DBResult<T::ID> {
-        <Self as AssocInterface>::insert(self, value)
-    }
 }
 
-impl<T: Entity> EntityMap for AssocMap<T> {
+/*impl<T: Entity> EntityMap for AssocMap<T> {
     type ContainedEntity = T;
 
     fn conn(&self) -> &DBConnection {
         &self.data.as_ref().unwrap().conn
     }
-}
+}*/
 
 impl<T: Entity> Datum for AssocMap<T> {
     fn sql_type() -> &'static str {
@@ -269,22 +248,6 @@ impl<R: Relation> AssocDomain<R> {
             _ghost: Default::default(),
         }
     }
-
-    pub fn get_all(&self) -> DBResult<Vec<IDWrap<R::Range>>> {
-        <Self as AssocInterface>::get_all(self)
-    }
-
-    pub fn associate_with(&self, remote_id: <R::Range as Entity>::ID) -> DBResult<()> {
-        <Self as AssocInterface>::associate_with(self, remote_id)
-    }
-
-    pub fn dissociate_with(&self, remote_id: <R::Range as Entity>::ID) -> DBResult<()> {
-        <Self as AssocInterface>::dissociate_with(self, remote_id)
-    }
-
-    pub fn insert(&self, value: R::Range) -> DBResult<<R::Range as Entity>::ID> {
-        <Self as AssocInterface>::insert(self, value)
-    }
 }
 
 impl<R: Relation> AssocInterface for AssocDomain<R> {
@@ -302,16 +265,6 @@ impl<R: Relation> AssocInterface for AssocDomain<R> {
     }
 }
 
-/*
-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!()
@@ -378,22 +331,6 @@ impl<R: Relation> AssocRange<R> {
             _ghost: Default::default(),
         }
     }
-
-    pub fn get_all(&self) -> DBResult<Vec<IDWrap<R::Domain>>> {
-        <Self as AssocInterface>::get_all(self)
-    }
-
-    pub fn associate_with(&self, remote_id: <R::Domain as Entity>::ID) -> DBResult<()> {
-        <Self as AssocInterface>::associate_with(self, remote_id)
-    }
-
-    pub fn dissociate_with(&self, remote_id: <R::Domain as Entity>::ID) -> DBResult<()> {
-        <Self as AssocInterface>::dissociate_with(self, remote_id)
-    }
-
-    pub fn insert(&self, value: R::Domain) -> DBResult<<R::Domain as Entity>::ID> {
-        <Self as AssocInterface>::insert(self, value)
-    }
 }
 
 impl<R: Relation> AssocInterface for AssocRange<R> {
@@ -411,13 +348,13 @@ impl<R: Relation> AssocInterface for AssocRange<R> {
     }
 }
 
-impl<R: Relation> EntityMap for AssocRange<R> {
+/*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 {
@@ -523,12 +460,14 @@ 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> {
@@ -536,12 +475,12 @@ pub struct IDMap<T: Entity> {
     _ghost: std::marker::PhantomData<T>,
 }
 
-impl<T: Entity> EntityMap for IDMap<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 {
@@ -551,17 +490,22 @@ impl<T: Entity> IDMap<T> {
         }
     }
 
-    /// Retrieve all entities in this map.
-
-    pub fn get_all<E: Entity>(&self) -> DBResult<Vec<IDWrap<E>>> {
-        query::get_all(&self.conn)
+    pub(crate) fn conn(&self) -> &DBConnection {
+        &self.conn
     }
 
     /// Look up an Entity in this map by ID.
     pub fn by_id(&self, id: T::ID) -> DBResult<Option<IDWrap<T>>> {
-        query::by_id(&self.conn, id)
+        self.with(id, &id).first().get()
     }
 
+    /*
+    /// Retrieve all entities in this map.
+    pub fn get_all<E: Entity>(&self) -> DBResult<Vec<IDWrap<E>>> {
+        query::get_all(&self.conn)
+    }
+
+
     /// 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.
@@ -584,6 +528,7 @@ impl<T: Entity> IDMap<T> {
     ) -> DBResult<()> {
         query::delete_by::<T, T::Uniques>(&self.conn, uniques)
     }
+    */
 
     /// Insert a new Entity into this map, and return its new ID.
     pub fn insert(&self, value: T) -> DBResult<T::ID> {
@@ -591,6 +536,65 @@ impl<T: Entity> IDMap<T> {
     }
 }
 
+impl<'a, T: Entity> Queryable for &'a IDMap<T> {
+    type EntityOutput = T;
+    type OutputContainer = Vec<IDWrap<T>>;
+    type StaticVersion = &'static IDMap<T>;
+
+    fn build<'s, 'q: 's>(&'s self) -> query::Query<'s> {
+        unreachable!()
+    }
+    fn bind(&self, _stmt: &mut sqlite::Statement, _index: &mut usize) {
+        unreachable!()
+    }
+
+    fn count(self) -> DBResult<usize> {
+        query::components::MapQueryable::new(self).count()
+    }
+
+    fn get(self) -> DBResult<Self::OutputContainer> {
+        query::components::MapQueryable::new(self).get()
+    }
+
+    fn delete(self) -> DBResult<usize> {
+        query::components::MapQueryable::new(self).delete()
+    }
+
+    fn unique(
+        self,
+        values: <<<Self::EntityOutput as Entity>::Uniques as EntityPartList>::DatumList as datum::DatumList>::Ref<'_>,
+    ) -> impl Queryable<
+        EntityOutput = Self::EntityOutput,
+        OutputContainer = Option<IDWrap<Self::EntityOutput>>,
+    >
+    where
+        Self: Sized,
+    {
+        query::components::MapQueryable::new(self).unique(values)
+    }
+
+    fn with<EP: entity::EntityPart<Entity = Self::EntityOutput>>(
+        self,
+        part: EP,
+        value: &EP::Datum,
+    ) -> impl Queryable<EntityOutput = Self::EntityOutput, OutputContainer = Self::OutputContainer>
+    where
+        Self: Sized,
+    {
+        query::components::MapQueryable::new(self).with(part, value)
+    }
+
+    fn join<AD: AssocInterface, EP: entity::EntityPart<Entity = Self::EntityOutput, Datum = AD>>(
+        self,
+        part: EP,
+    ) -> impl Queryable<EntityOutput = AD::RemoteEntity>
+    where
+        Self: Sized,
+    {
+        query::components::MapQueryable::new(self).join(part)
+    }
+}
+
 pub struct Index<T: Entity, Key: Datum> {
     _conn: DBConnection,
     _ghost: std::marker::PhantomData<(T, Key)>,
@@ -649,6 +653,8 @@ pub trait Database {
             }
         }
 
+        conn.execute_raw("PRAGMA foreign_keys = ON")?;
+
         Ok(Self::build(conn))
     }
 

+ 3 - 1
microrm/src/schema/build.rs

@@ -1,4 +1,5 @@
 use crate::{
+    query::Queryable,
     schema::{
         collect::{EntityStateContainer, PartType},
         meta, DBConnection, Database, DatabaseItem, DatabaseItemVisitor,
@@ -75,7 +76,8 @@ impl DatabaseSchema {
         // check to see if the signature exists and matches
         metadb
             .metastore
-            .lookup_unique(&Self::SCHEMA_SIGNATURE_KEY.to_string())
+            .unique(&Self::SCHEMA_SIGNATURE_KEY.to_string())
+            .get()
             .ok()
             .flatten()
             .map(|kv| kv.value.parse::<u64>().unwrap_or(0) == self.signature)

+ 9 - 1
microrm/src/schema/datum.rs

@@ -13,7 +13,7 @@ mod datum_list;
 // ----------------------------------------------------------------------
 
 /// Represents a data field in an Entity.
-pub trait Datum: 'static {
+pub trait Datum {
     fn sql_type() -> &'static str;
 
     fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, index: usize);
@@ -36,6 +36,14 @@ pub trait Datum: 'static {
 
 /// A fixed-length list of EntityDatums, usually a tuple.
 pub trait DatumList {
+    type Ref<'a>: DatumListRef
+    where
+        Self: 'a;
+    fn accept(&self, visitor: &mut impl DatumVisitor);
+}
+
+/// A DatumList that is passed by reference instead of by value.
+pub trait DatumListRef {
     fn accept(&self, visitor: &mut impl DatumVisitor);
 }
 

+ 32 - 0
microrm/src/schema/datum/datum_common.rs

@@ -201,3 +201,35 @@ impl Datum for Vec<u8> {
         Ok((stmt.read::<Vec<u8>, _>(index)?, index + 1))
     }
 }
+
+impl<'l, T: Datum> Datum for &'l T {
+    fn sql_type() -> &'static str {
+        T::sql_type()
+    }
+
+    fn bind_to<'a>(&self, stmt: &mut sqlite::Statement<'a>, index: usize) {
+        T::bind_to(self, stmt, index)
+    }
+
+    fn build_from<'a>(
+        adata: AssocData,
+        stmt: &mut sqlite::Statement<'a>,
+        index: usize,
+    ) -> DBResult<(Self, usize)>
+    where
+        Self: Sized,
+    {
+        unreachable!()
+    }
+
+    fn accept_discriminator(d: &mut impl crate::schema::DatumDiscriminator)
+    where
+        Self: Sized,
+    {
+        T::accept_discriminator(d)
+    }
+
+    fn accept_entity_visitor(v: &mut impl crate::schema::entity::EntityVisitor) {
+        T::accept_entity_visitor(v)
+    }
+}

+ 70 - 1
microrm/src/schema/datum/datum_list.rs

@@ -1,22 +1,78 @@
-use super::{Datum, DatumList, DatumVisitor};
+use super::{Datum, DatumList, DatumListRef, DatumVisitor};
 
 impl DatumList for () {
+    type Ref<'a> = &'a ();
+    fn accept(&self, _: &mut impl DatumVisitor) {}
+}
+
+impl<'l> DatumListRef for &'l () {
     fn accept(&self, _: &mut impl DatumVisitor) {}
 }
 
 impl<T: Datum> DatumList for T {
+    type Ref<'a> = &'a T where Self: 'a;
+
+    fn accept(&self, visitor: &mut impl DatumVisitor) {
+        visitor.visit(self);
+    }
+}
+
+impl<'l, T: Datum> DatumListRef for &'l T {
     fn accept(&self, visitor: &mut impl DatumVisitor) {
         visitor.visit(self);
     }
 }
 
 impl<T0: Datum> DatumList for (T0,) {
+    type Ref<'a> = (&'a T0,) where Self: 'a;
+
+    fn accept(&self, visitor: &mut impl DatumVisitor) {
+        visitor.visit(&self.0);
+    }
+}
+
+impl<'a, T0: Datum> DatumListRef for (&'a T0,) {
     fn accept(&self, visitor: &mut impl DatumVisitor) {
         visitor.visit(&self.0);
     }
 }
 
+macro_rules! datum_list {
+    ($($ty:ident : $n:tt),+) => {
+        impl<$($ty: Datum),*> DatumList for ($($ty),*) {
+            type Ref<'a> = ($(&'a $ty),*) where Self: 'a;
+            fn accept(&self, visitor: &mut impl DatumVisitor) {
+                $(visitor.visit(&self. $n));*
+            }
+        }
+
+        impl<'l, $($ty: Datum),*> DatumListRef for ($(&'l $ty),*) {
+            fn accept(&self, visitor: &mut impl DatumVisitor) {
+                $(visitor.visit(&self. $n));*
+            }
+        }
+    }
+}
+
+datum_list!(T0: 0, T1: 1);
+datum_list!(T0: 0, T1: 1, T2: 2);
+datum_list!(T0: 0, T1: 1, T2: 2, T3: 3);
+datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4);
+datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5);
+datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6);
+datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7);
+datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8);
+
+/*
 impl<T0: Datum, T1: Datum> DatumList for (T0, T1) {
+    type Ref<'a> = (&'a T0, &'a T1) where Self: 'a;
+    fn accept(&self, visitor: &mut impl DatumVisitor) {
+        visitor.visit(&self.0);
+        visitor.visit(&self.1);
+    }
+}
+
+impl<'a, T0: Datum, T1: Datum> DatumListRef for (&'a T0, &'a T1) {
     fn accept(&self, visitor: &mut impl DatumVisitor) {
         visitor.visit(&self.0);
         visitor.visit(&self.1);
@@ -24,6 +80,15 @@ impl<T0: Datum, T1: Datum> DatumList for (T0, T1) {
 }
 
 impl<T0: Datum, T1: Datum, T2: Datum> DatumList for (T0, T1, T2) {
+    type Ref<'a> = (&'a T0, &'a T1, &'a T2) where Self: 'a;
+    fn accept(&self, visitor: &mut impl DatumVisitor) {
+        visitor.visit(&self.0);
+        visitor.visit(&self.1);
+        visitor.visit(&self.2);
+    }
+}
+
+impl<'a, T0: Datum, T1: Datum, T2: Datum> DatumListRef for (&'a T0, &'a T1, &'a T2) {
     fn accept(&self, visitor: &mut impl DatumVisitor) {
         visitor.visit(&self.0);
         visitor.visit(&self.1);
@@ -32,6 +97,7 @@ impl<T0: Datum, T1: Datum, T2: Datum> DatumList for (T0, T1, T2) {
 }
 
 impl<T0: Datum, T1: Datum, T2: Datum, T3: Datum> DatumList for (T0, T1, T2, T3) {
+    type Ref<'a> = (&'a T0, &'a T1, &'a T2, &'a T3) where Self: 'a;
     fn accept(&self, visitor: &mut impl DatumVisitor) {
         visitor.visit(&self.0);
         visitor.visit(&self.1);
@@ -41,6 +107,7 @@ impl<T0: Datum, T1: Datum, T2: Datum, T3: Datum> DatumList for (T0, T1, T2, T3)
 }
 
 impl<T0: Datum, T1: Datum, T2: Datum, T3: Datum, T4: Datum> DatumList for (T0, T1, T2, T3, T4) {
+    type Ref<'a> = (&'a T0, &'a T1, &'a T2, &'a T3, &'a T4);
     fn accept(&self, visitor: &mut impl DatumVisitor) {
         visitor.visit(&self.0);
         visitor.visit(&self.1);
@@ -53,6 +120,7 @@ impl<T0: Datum, T1: Datum, T2: Datum, T3: Datum, T4: Datum> DatumList for (T0, T
 impl<T0: Datum, T1: Datum, T2: Datum, T3: Datum, T4: Datum, T5: Datum> DatumList
     for (T0, T1, T2, T3, T4, T5)
 {
+    type Ref<'a> = (&'a T0, &'a T1, &'a T2, &'a T3, &'a T4, &'a T5);
     fn accept(&self, visitor: &mut impl DatumVisitor) {
         visitor.visit(&self.0);
         visitor.visit(&self.1);
@@ -62,3 +130,4 @@ impl<T0: Datum, T1: Datum, T2: Datum, T3: Datum, T4: Datum, T5: Datum> DatumList
         visitor.visit(&self.5);
     }
 }
+*/

+ 2 - 1
microrm/src/schema/entity.rs

@@ -29,6 +29,7 @@ pub trait EntityPart: 'static {
     type Entity: Entity;
 
     fn part_name() -> &'static str;
+    fn placeholder() -> &'static str;
     fn unique() -> bool;
 }
 
@@ -62,7 +63,7 @@ mod part_list;
 pub trait Entity: 'static {
     type Parts: EntityPartList;
     type Uniques: EntityPartList;
-    type ID: EntityID<Entity = Self>;
+    type ID: EntityID<Entity = Self> + EntityPart<Datum = Self::ID, Entity = Self>;
 
     fn build(values: <Self::Parts as EntityPartList>::DatumList) -> Self;
 

+ 37 - 11
microrm/src/schema/tests.rs

@@ -61,6 +61,21 @@ mod manual_test_db {
         }
     }
 
+    impl EntityPart for SimpleEntityID {
+        type Datum = Self;
+        type Entity = SimpleEntity;
+
+        fn unique() -> bool {
+            true
+        }
+        fn part_name() -> &'static str {
+            "id"
+        }
+        fn placeholder() -> &'static str {
+            "simple_entity_id"
+        }
+    }
+
     struct SimpleEntityName;
     impl EntityPart for SimpleEntityName {
         type Datum = String;
@@ -68,6 +83,9 @@ mod manual_test_db {
         fn part_name() -> &'static str {
             "name"
         }
+        fn placeholder() -> &'static str {
+            "simple_entity_name"
+        }
         fn unique() -> bool {
             true
         }
@@ -152,7 +170,7 @@ mod manual_test_db {
 mod derive_tests {
     #![allow(unused)]
 
-    use crate::query::AssocInterface;
+    use crate::query::{AssocInterface, Queryable};
     use crate::schema::{AssocMap, Database, IDMap};
     use microrm_macros::{Database, Entity};
 
@@ -193,7 +211,8 @@ mod derive_tests {
         // check that it isn't in the database before we insert it
         assert!(db
             .people
-            .lookup_unique(&name_string)
+            .unique(&name_string)
+            .get()
             .ok()
             .flatten()
             .is_none());
@@ -208,7 +227,8 @@ mod derive_tests {
         // check that it is in the database after we insert it
         assert!(db
             .people
-            .lookup_unique(&name_string)
+            .unique(&name_string)
+            .get()
             .ok()
             .flatten()
             .is_some());
@@ -228,12 +248,16 @@ mod derive_tests {
 
         let person = db
             .people
-            .lookup_unique(&name_string)
+            .unique(&name_string)
+            .get()
             .ok()
             .flatten()
             .expect("couldn't re-get test person entity");
 
-        person.roles.get_all();
+        person
+            .roles
+            .get()
+            .expect("couldn't get associated role entity");
     }
 
     #[test]
@@ -252,7 +276,8 @@ mod derive_tests {
 
         let person = db
             .people
-            .lookup_unique(&name_string)
+            .unique(&name_string)
+            .get()
             .ok()
             .flatten()
             .expect("couldn't re-get test person entity");
@@ -266,7 +291,7 @@ mod derive_tests {
 
 mod mutual_relationship {
     use super::open_test_db;
-    use crate::query::AssocInterface;
+    use crate::query::{AssocInterface, Queryable};
     use crate::schema::{AssocDomain, AssocMap, AssocRange, Database, IDMap};
     use microrm_macros::{Database, Entity};
 
@@ -334,18 +359,19 @@ mod mutual_relationship {
             .by_id(ca)
             .expect("couldn't retrieve customer record")
             .expect("no customer record");
+
         e_ca.receipts
-            .associate_with(ra)
+            .connect_to(ra)
             .expect("couldn't associate customer with receipt");
         e_ca.receipts
-            .associate_with(rb)
+            .connect_to(rb)
             .expect("couldn't associate customer with receipt");
 
         // technically this can fail if sqlite gives ra and rb back in the opposite order, which is
         // valid behaviour
         assert_eq!(
             e_ca.receipts
-                .get_all()
+                .get()
                 .expect("couldn't get receipts associated with customer")
                 .into_iter()
                 .map(|x| x.id())
@@ -362,7 +388,7 @@ mod mutual_relationship {
 
         assert_eq!(
             e_ra.customers
-                .get_all()
+                .get()
                 .expect("couldn't get associated customers")
                 .into_iter()
                 .map(|x| x.id())