Bläddra i källkod

Change query interface significantly.

Now instead of a number of ad-hoc functions, there is instead a smaller
API based around composition.
Kestrel 2 år sedan
förälder
incheckning
0673699fa5
9 ändrade filer med 486 tillägg och 325 borttagningar
  1. 1 0
      .gitignore
  2. 21 0
      Cargo.lock
  3. 2 1
      README.md
  4. 1 1
      microrm-macros/src/entity.rs
  5. 2 0
      microrm/Cargo.toml
  6. 1 0
      microrm/src/error.rs
  7. 45 32
      microrm/src/lib.rs
  8. 83 291
      microrm/src/query.rs
  9. 330 0
      microrm/src/query/build.rs

+ 1 - 0
.gitignore

@@ -1,3 +1,4 @@
+core
 .*.sw?
 /target
 /archive

+ 21 - 0
Cargo.lock

@@ -90,6 +90,26 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "const-str"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b60160dfb442e53d068d55ec16675a7d9df6cacd072d3540742a1e7bcdb6150"
+dependencies = [
+ "const-str-proc-macro",
+]
+
+[[package]]
+name = "const-str-proc-macro"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c047fef6b3d672b32b454079702745d0fc64a69a2c7933caf75684ba41d5df2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "convert_case"
 version = "0.5.0"
@@ -341,6 +361,7 @@ name = "microrm"
 version = "0.2.4"
 dependencies = [
  "base64",
+ "const-str",
  "criterion",
  "lazy_static",
  "microrm-macros",

+ 2 - 1
README.md

@@ -16,6 +16,7 @@ Querying supports a small subset of SQL expressed as type composition.
 A simple example using an SQLite table as an (indexed) key/value store
 might look something like this:
 ```rust
+use microrm::prelude::*;
 use microrm::{Entity,make_index};
 #[derive(Debug,Entity,serde::Serialize,serde::Deserialize)]
 pub struct KVStore {
@@ -44,7 +45,7 @@ qi.add(&KVStore {
 });
 
 // because KVStoreIndex indexes key, this is a logarithmic lookup
-let qr = qi.get_one_by(KVStore::Key, "a_key");
+let qr = qi.get().by(KVStore::Key, "a_key").one().expect("No errors encountered");
 
 assert_eq!(qr.is_some(), true);
 assert_eq!(qr.as_ref().unwrap().key, "a_key");

+ 1 - 1
microrm-macros/src/entity.rs

@@ -34,7 +34,7 @@ fn derive_columns<'a, I: Iterator<Item = &'a syn::Field>>(
         let original_case = name.ident.as_ref().unwrap().clone();
         let snake_case = original_case.to_string();
         if snake_case != snake_case.to_case(Case::Snake) {
-            return quote::quote_spanned!(original_case.span() => compile_error!("Names must be in snake_case"))
+            return quote::quote_spanned!(original_case.span() => compile_error!("Names must be in snake_case"));
         }
 
         let converted_case =

+ 2 - 0
microrm/Cargo.toml

@@ -17,6 +17,8 @@ serde_bytes = { version = "0.11.6" }
 serde_json = { version = "1.0" }
 lazy_static = { version = "1.4.0" }
 
+const-str = { version = "0.3.1" }
+
 microrm-macros = { path = "../microrm-macros", version = "0.2.1" }
 
 [dev-dependencies]

+ 1 - 0
microrm/src/error.rs

@@ -5,6 +5,7 @@ pub enum Error {
     StoreError(String),
     EmptyStoreError,
     CreateError,
+    ExecFailure
 }
 
 impl From<sqlite::Error> for Error {

+ 45 - 32
microrm/src/lib.rs

@@ -16,6 +16,12 @@ pub use error::Error;
 pub use query::{QueryInterface, WithID};
 pub use schema::Schema;
 
+pub mod prelude {
+    pub use crate::query::build::{Filterable,Resolvable};
+}
+
+use prelude::*;
+
 #[macro_export]
 macro_rules! value_list {
     ( $( $element:expr ),* ) => {
@@ -41,6 +47,13 @@ pub enum DBError {
     DropFailure,
     CreateFailure,
     SanityCheckFailure,
+    InternalFailure(crate::Error)
+}
+
+impl From<crate::Error> for DBError {
+    fn from(err: crate::Error) -> Self {
+        Self::InternalFailure(err)
+    }
 }
 
 #[derive(PartialEq, Debug)]
@@ -150,7 +163,7 @@ impl DB {
         }
 
         let qi = query::QueryInterface::new(self);
-        let hash: Option<WithID<Metaschema>> = qi.get_one_by(meta::Metaschema::Key, "schema_hash");
+        let hash: Option<WithID<Metaschema>> = qi.get().by(meta::Metaschema::Key, "schema_hash").one()?;
 
         if hash.is_none() {
             if mode == CreateMode::MustExist {
@@ -187,11 +200,11 @@ impl DB {
             value: self.schema_hash.clone(),
         });
 
-        assert!(add_result.is_some());
+        assert!(add_result.is_ok());
 
-        let sanity_check = qi.get_one_by(meta::Metaschema::Key, "schema_hash");
-        assert!(sanity_check.is_some());
-        assert_eq!(sanity_check.unwrap().value, self.schema_hash);
+        let sanity_check = qi.get().by(meta::Metaschema::Key, "schema_hash").one();
+        assert!(sanity_check.is_ok() && sanity_check.as_ref().unwrap().is_some());
+        assert_eq!(sanity_check.unwrap().unwrap().value, self.schema_hash);
 
         Ok(())
     }
@@ -262,6 +275,8 @@ mod pool_test {
 
 #[cfg(test)]
 mod test {
+    use crate::prelude::*;
+
     use super::DB;
 
     #[derive(serde::Serialize, serde::Deserialize, crate::Entity)]
@@ -296,7 +311,7 @@ mod test {
         let id = qi.add(&S1 { an_id: -1 }).expect("Can't add S1");
         let child_id = qi.add(&S2 { parent_id: id }).expect("Can't add S2");
 
-        qi.get_one_by_id(child_id).expect("Can't get S2 instance");
+        qi.get().by(S2::ID, &child_id).one().expect("Can't get S2 instance");
     }
 
     microrm_macros::make_index_internal!(S2ParentIndex, S2::ParentId);
@@ -304,6 +319,8 @@ mod test {
 
 #[cfg(test)]
 mod test2 {
+    use crate::prelude::*;
+
     #[derive(Debug, crate::Entity, serde::Serialize, serde::Deserialize)]
     #[microrm_internal]
     pub struct KVStore {
@@ -331,19 +348,22 @@ mod test2 {
         qi.add(&KVStore {
             key: "a_key".to_string(),
             value: "a_value".to_string(),
-        });
+        }).unwrap();
 
         // because KVStoreIndex indexes key, this is a logarithmic lookup
-        let qr = qi.get_one_by(KVStore::Key, "a_key");
+        let qr = qi.get().by(KVStore::Key, "a_key").one();
 
-        assert_eq!(qr.is_some(), true);
-        assert_eq!(qr.as_ref().unwrap().key, "a_key");
-        assert_eq!(qr.as_ref().unwrap().value, "a_value");
+        assert_eq!(qr.is_ok(), true);
+        assert_eq!(qr.as_ref().unwrap().is_some(), true);
+        assert_eq!(qr.as_ref().unwrap().as_ref().unwrap().key, "a_key");
+        assert_eq!(qr.as_ref().unwrap().as_ref().unwrap().value, "a_value");
     }
 }
 
 #[cfg(test)]
 mod delete_test {
+    use crate::prelude::*;
+
     #[derive(Debug, crate::Entity, serde::Serialize, serde::Deserialize)]
     #[microrm_internal]
     pub struct KVStore {
@@ -361,46 +381,39 @@ mod delete_test {
         qi.add(&KVStore {
             key: "a".to_string(),
             value: "a_value".to_string(),
-        });
+        }).unwrap();
 
         let insert_two = || {
             qi.add(&KVStore {
                 key: "a".to_string(),
                 value: "a_value".to_string(),
-            });
+            }).unwrap();
 
             qi.add(&KVStore {
                 key: "a".to_string(),
                 value: "another_value".to_string(),
-            });
+            }).unwrap();
         };
 
-        assert!(qi.get_one_by(KVStore::Key, "a").is_some());
-        // is_some() implies no errors were encountered
-        assert!(qi.delete_by(KVStore::Key, "a").is_some());
-        assert!(qi.get_one_by(KVStore::Key, "a").is_none());
+        assert!(qi.get().by(KVStore::Key, "a").one().is_ok());
+        assert!(qi.delete().by(KVStore::Key, "a").exec().is_ok());
+        assert!(qi.get().by(KVStore::Key, "a").one().unwrap().is_none());
 
         insert_two();
 
-        // this should fail as there is more than one thing matching key='a'
-        assert!(qi.get_one_by(KVStore::Key, "a").is_none());
-        let all = qi.get_all_by(KVStore::Key, "a");
-        assert!(all.is_some());
+        let all = qi.get().by(KVStore::Key, "a").all();
+        assert!(all.is_ok());
         assert_eq!(all.unwrap().len(), 2);
 
-        assert!(qi.delete_by(KVStore::Key, "b").is_some());
+        assert!(qi.delete().by(KVStore::Key, "b").exec().is_ok());
 
-        let all = qi.get_all_by(KVStore::Key, "a");
-        assert!(all.is_some());
+        let all = qi.get().by(KVStore::Key, "a").all();
+        assert!(all.is_ok());
         assert_eq!(all.unwrap().len(), 2);
 
-        assert!(qi
-            .delete_by_multi(
-                &[&KVStore::Key, &KVStore::Value],
-                &crate::value_list![&"a", &"another_value"]
-            )
-            .is_some());
-        let one = qi.get_one_by(KVStore::Key, "a");
+        assert!(qi.delete().by(KVStore::Key, &"a").by(KVStore::Value, &"another_value").exec().is_ok());
+
+        let one = qi.get().by(KVStore::Key, "a").one().unwrap();
         assert!(one.is_some());
         assert_eq!(one.unwrap().value, "a_value");
     }

+ 83 - 291
microrm/src/query.rs

@@ -1,11 +1,13 @@
-use std::hash::Hash;
+use crate::prelude::*;
+use std::hash::{Hash,Hasher};
 
 use crate::entity::{Entity, EntityColumn, EntityID};
 use crate::model::Modelable;
 
-// pub mod expr;
-// pub mod condition;
-pub mod builder;
+
+pub mod build;
+
+pub use build::{Filterable, Resolvable};
 
 /// Wraps an entity with its ID, for example as a query result.
 ///
@@ -59,7 +61,7 @@ impl<T: Entity> std::ops::DerefMut for WithID<T> {
     }
 }
 
-type CacheIndex = (&'static str, std::any::TypeId, u64);
+type CacheIndex = u64;
 
 /// The query interface for a database.
 ///
@@ -76,8 +78,6 @@ pub struct QueryInterface<'l> {
     prevent_send: std::marker::PhantomData<*mut ()>,
 }
 
-const NO_HASH: u64 = 0;
-
 impl<'l> QueryInterface<'l> {
     pub fn new(db: &'l crate::DB) -> Self {
         Self {
@@ -87,336 +87,128 @@ impl<'l> QueryInterface<'l> {
         }
     }
 
-    /// Helper function to process an expected single result
-    /// Note that this errors out if there is more than a single result
+    /// Helper function to process an expected single result, discarding the rest
     fn expect_one_result<T>(
         &self,
         stmt: &mut sqlite::Statement,
-        with_result: &mut dyn FnMut(&mut sqlite::Statement) -> Option<T>,
-    ) -> Option<T> {
-        let state = stmt.next().ok()?;
+        with_result: &mut dyn FnMut(&mut sqlite::Statement) -> Result<T, crate::Error>,
+    ) -> Result<Option<T>, crate::Error> {
+        let state = stmt.next()?;
         if state != sqlite::State::Row {
-            return None;
-        }
-
-        let res = with_result(stmt);
-
-        let state = stmt.next().ok()?;
-        if state != sqlite::State::Done {
-            return None;
+            return Ok(None)
+            // return Err(crate::Error::ExecFailure)
         }
 
-        res
+        Ok(Some(with_result(stmt)?))
     }
 
     /// Helper function to process an expected zero results
     /// Note that this errors out if there is any result
-    fn expect_no_result(&self, stmt: &mut sqlite::Statement) -> Option<()> {
-        let state = stmt.next().ok()?;
+    fn expect_no_result(&self, stmt: &mut sqlite::Statement) -> Result<(), crate::Error> {
+        let state = stmt.next()?;
         if state != sqlite::State::Done {
-            return None;
+            return Err(crate::Error::ExecFailure);
         }
 
-        Some(())
+        Ok(())
     }
 }
 
 impl<'l> QueryInterface<'l> {
-    fn cached_query<Return>(
+    fn with_cache<
+        Return,
+        Create: Fn() -> sqlite::Statement<'l>,
+        With: FnMut(&mut sqlite::Statement<'l>) -> Return,
+    >(
         &self,
-        context: &'static str,
-        ty: std::any::TypeId,
-        create: &dyn Fn() -> sqlite::Statement<'l>,
-        with: &mut dyn FnMut(&mut sqlite::Statement<'l>) -> Return,
-    ) -> Return {
+        hash: u64,
+        create: Create,
+        mut with: With,
+    ) -> Return
+    where {
         let mut cache = self.cache.lock().expect("Couldn't acquire cache?");
-        let key = (context, ty, NO_HASH);
-        let query = cache.entry(key).or_insert_with(create);
-
+        let query = cache.entry(hash).or_insert_with(create);
         query.reset().expect("Couldn't reset query");
         with(query)
     }
+}
 
-    fn cached_query_column<T: Entity, Return>(
-        &self,
-        context: &'static str,
-        ty: std::any::TypeId,
-        variant: &[&dyn EntityColumn<Entity = T>],
-        create: &dyn Fn() -> sqlite::Statement<'l>,
-        with: &mut dyn FnMut(&mut sqlite::Statement<'l>) -> Return,
-    ) -> Return {
-        use std::hash::Hasher;
-
-        let mut hasher = std::collections::hash_map::DefaultHasher::new();
-        for v in variant {
-            v.column_typeid().hash(&mut hasher);
-        }
-        let hash = hasher.finish();
-
-        let mut cache = self.cache.lock().expect("Couldn't acquire cache?");
-        let key = (context, ty, hash);
-        let query = cache.entry(key).or_insert_with(create);
-
-        query.reset().expect("Couldn't reset query");
-        with(query)
+impl<'l> QueryInterface<'l> {
+    pub fn insert<T: Entity + serde::Serialize>(&self, m: &T) -> Result<<T as Entity>::ID, crate::Error> {
+        self.add(m)
     }
 
-    /// Search for an entity by a property
-    pub fn get_one_by<C: EntityColumn, V: Modelable>(
-        &self,
-        col: C,
-        val: V,
-    ) -> Option<WithID<C::Entity>> {
-        let table_name = <C::Entity>::table_name();
-        let column_name = col.name();
-
-        self.cached_query_column(
-            "get_one_by",
-            std::any::TypeId::of::<C::Entity>(),
-            &[&col],
+    /// Add an entity to its table
+    pub fn add<T: Entity + serde::Serialize>(&self, m: &T) -> Result<<T as Entity>::ID, crate::Error> {
+        let mut hasher = std::collections::hash_map::DefaultHasher::new();
+        "add".hash(&mut hasher);
+        std::any::TypeId::of::<T>().hash(&mut hasher);
+        self.with_cache(
+            hasher.finish(),
             &|| {
+                let placeholders = (0..(<T as Entity>::column_count() - 1))
+                    .map(|_| "?".to_string())
+                    .collect::<Vec<_>>()
+                    .join(",");
+
                 self.db
                     .conn
                     .prepare(&format!(
-                        "SELECT * FROM \"{}\" WHERE \"{}\" = ?",
-                        table_name, column_name
+                        "INSERT INTO \"{}\" VALUES (NULL, {}) RETURNING \"id\"",
+                        <T as Entity>::table_name(),
+                        placeholders
                     ))
                     .expect("")
             },
-            &mut |stmt| {
-                val.bind_to(stmt, 1).ok()?;
-
-                self.expect_one_result(stmt, &mut |stmt| {
-                    let id: i64 = stmt.read(0).ok()?;
-                    Some(WithID::wrap(
-                        <<C as EntityColumn>::Entity>::build_from(stmt).ok()?,
-                        id,
-                    ))
-                })
-            },
-        )
-    }
-
-    /// Search for an entity by multiple properties
-    pub fn get_one_by_multi<T: Entity>(
-        &self,
-        c: &[&dyn EntityColumn<Entity = T>],
-        val: &[&dyn crate::model::Modelable],
-    ) -> Option<WithID<T>> {
-        let table_name = T::table_name();
-
-        assert_eq!(c.len(), val.len());
-
-        self.cached_query_column(
-            "get_one_by_multi",
-            std::any::TypeId::of::<T>(),
-            c,
-            &|| {
-                let query = format!(
-                    "SELECT * FROM \"{}\" WHERE {}",
-                    table_name,
-                    c.iter()
-                        .map(|col| format!("\"{}\" = ?", col.name()))
-                        .collect::<Vec<_>>()
-                        .join(" AND ")
-                );
-                self.db
-                    .conn
-                    .prepare(&query)
-                    .expect(format!("Failed to prepare SQL query: {}", query).as_str())
-            },
-            &mut |stmt| {
-                for index in 0..val.len() {
-                    val[index].bind_to(stmt, index + 1).ok()?;
-                }
-
-                self.expect_one_result(stmt, &mut |stmt| {
-                    let id: i64 = stmt.read(0).ok()?;
-                    Some(WithID::wrap(T::build_from(stmt).ok()?, id))
-                })
-            },
-        )
-    }
+            &mut |mut stmt: &mut sqlite::Statement<'_>| {
+                crate::model::store::serialize_into(&mut stmt, m)?;
 
-    /// Delete entities by searching with a single property
-    pub fn delete_by<C: EntityColumn, V: Modelable>(&self, c: C, val: V) -> Option<()> {
-        let table_name = <C::Entity>::table_name();
-        let column_name = c.name();
-
-        self.cached_query_column(
-            "delete_by",
-            std::any::TypeId::of::<C::Entity>(),
-            &[&c],
-            &|| {
-                let query = format!("DELETE FROM \"{}\" WHERE {} = ?", table_name, column_name);
-                self.db
-                    .conn
-                    .prepare(&query)
-                    .expect(format!("Failed to prepare SQL query: {}", query).as_str())
-            },
-            &mut |stmt| {
-                val.bind_to(stmt, 1).ok()?;
+                let rowid = self.expect_one_result(&mut stmt, &mut |stmt| {
+                    stmt.read::<i64>(0).map_err(|x| crate::Error::DBError(x))
+                })?;
 
-                self.expect_no_result(stmt)
+                Ok(<T as Entity>::ID::from_raw_id(rowid.unwrap()))
             },
         )
     }
+}
 
-    /// Delete entities by searching with a single property
-    pub fn delete_by_id<I: EntityID<Entity = T>, T: Entity<ID = I>>(
-        &self,
-        id: <T as Entity>::ID,
-    ) -> Option<()> {
-        let table_name = <T as Entity>::table_name();
-
-        self.cached_query(
-            "delete_by_id",
-            std::any::TypeId::of::<T>(),
-            &|| {
-                let query = format!("DELETE FROM \"{}\" WHERE id = ?", table_name);
-                self.db
-                    .conn
-                    .prepare(&query)
-                    .expect(format!("Failed to prepare SQL query: {}", query).as_str())
-            },
-            &mut |stmt| {
-                id.bind_to(stmt, 1).ok()?;
-
-                self.expect_no_result(stmt)
-            },
-        )
-    }
-
-    /// Delete entities by searching with multiple properties
-    pub fn delete_by_multi<T: Entity>(
-        &self,
-        c: &[&dyn EntityColumn<Entity = T>],
-        val: &[&dyn crate::model::Modelable],
-    ) -> Option<()> {
-        let table_name = <T>::table_name();
-
-        assert_eq!(c.len(), val.len());
-
-        self.cached_query_column(
-            "delete_by_multi",
-            std::any::TypeId::of::<T>(),
-            c,
-            &|| {
-                let query = format!(
-                    "DELETE FROM \"{}\" WHERE {}",
-                    table_name,
-                    c.iter()
-                        .map(|col| format!("\"{}\" = ?", col.name()))
-                        .collect::<Vec<_>>()
-                        .join(" AND ")
-                );
-                self.db
-                    .conn
-                    .prepare(&query)
-                    .expect(format!("Failed to prepare SQL query: {}", query).as_str())
-            },
-            &mut |stmt| {
-                for index in 0..val.len() {
-                    val[index].bind_to(stmt, index + 1).ok()?;
-                }
-
-                self.expect_no_result(stmt)
-            },
-        )
+impl<'l> QueryInterface<'l> {
+    pub fn get<'a, 'b, T: Entity>(&'a self) -> build::Select<'b, 'l, T>
+    where
+        'a: 'b,
+    {
+        build::Select::new(self)
     }
 
-    /// Search for an entity by ID
-    pub fn get_one_by_id<I: EntityID<Entity = T>, T: Entity>(&self, id: I) -> Option<WithID<T>> {
-        let table_name = <T as Entity>::table_name();
-
-        self.cached_query(
-            "get_one_by_id",
-            std::any::TypeId::of::<T>(),
-            &|| {
-                self.db
-                    .conn
-                    .prepare(&format!("SELECT * FROM \"{}\" WHERE id = ?", table_name))
-                    .expect("")
-            },
-            &mut |stmt| {
-                id.bind_to(stmt, 1).ok()?;
-
-                self.expect_one_result(stmt, &mut |stmt| {
-                    let id: i64 = stmt.read(0).ok()?;
-                    Some(WithID::wrap(T::build_from(stmt).ok()?, id))
-                })
-            },
-        )
+    pub fn delete<'a, 'b, T: Entity>(&'a self) -> build::Delete<'b, 'l, T>
+    where
+        'a: 'b,
+    {
+        build::Delete::new(self)
     }
+}
 
-    /// Search for all entities matching a property
-    pub fn get_all_by<C: EntityColumn, V: Modelable>(
-        &self,
-        c: C,
-        val: V,
-    ) -> Option<Vec<WithID<C::Entity>>> {
-        let table_name = <C::Entity>::table_name();
-        let column_name = c.name();
-
-        self.cached_query_column(
-            "get_all_by",
-            std::any::TypeId::of::<C::Entity>(),
-            &[&c],
-            &|| {
-                self.db
-                    .conn
-                    .prepare(&format!(
-                        "SELECT * FROM \"{}\" WHERE {} = ?",
-                        table_name, column_name
-                    ))
-                    .expect("")
-            },
-            &mut |stmt| {
-                val.bind_to(stmt, 1).ok()?;
-
-                let mut res = Vec::new();
-                loop {
-                    let state = stmt.next().ok()?;
-                    if state == sqlite::State::Done {
-                        break;
-                    }
+#[cfg(test)]
+mod test_build {
+    use microrm_macros::Entity;
+    use serde::{Deserialize, Serialize};
 
-                    let id: i64 = stmt.read(0).ok()?;
-                    res.push(WithID::wrap(C::Entity::build_from(stmt).ok()?, id));
-                }
+    use crate::QueryInterface;
 
-                Some(res)
-            },
-        )
+    #[derive(Entity, Serialize, Deserialize)]
+    #[microrm_internal]
+    pub struct KVStore {
+        key: String,
+        value: String,
     }
 
-    /// Add an entity to its table
-    pub fn add<T: Entity + serde::Serialize>(&self, m: &T) -> Option<<T as Entity>::ID> {
-        self.cached_query(
-            "add",
-            std::any::TypeId::of::<T>(),
-            &|| {
-                let placeholders = (0..(<T as Entity>::column_count() - 1))
-                    .map(|_| "?".to_string())
-                    .collect::<Vec<_>>()
-                    .join(",");
-
-                self.db
-                    .conn
-                    .prepare(&format!(
-                        "INSERT INTO \"{}\" VALUES (NULL, {}) RETURNING \"id\"",
-                        <T as Entity>::table_name(),
-                        placeholders
-                    ))
-                    .expect("")
-            },
-            &mut |stmt| {
-                crate::model::store::serialize_into(stmt, m).ok()?;
-
-                let rowid = self.expect_one_result(stmt, &mut |stmt| stmt.read::<i64>(0).ok())?;
+    #[test]
+    fn simple_get() {
+        use super::*;
+        let db = crate::DB::new_in_memory(crate::Schema::new().entity::<KVStore>()).unwrap();
+        let qi = db.query_interface();
 
-                Some(<T as Entity>::ID::from_raw_id(rowid))
-            },
-        )
+        assert!(qi.get().by(KVStore::Key, "abc").result().is_ok());
     }
 }

+ 330 - 0
microrm/src/query/build.rs

@@ -0,0 +1,330 @@
+//! Static in-place query construction.
+//!
+//! Interface:
+//! - `qi.get().by(KVStore::Key, &key).result()`
+
+use crate::{entity::EntityColumn, model::Modelable, Entity, Error};
+use std::{
+    collections::HashMap,
+    hash::{Hash, Hasher},
+    marker::PhantomData,
+};
+
+use super::{QueryInterface, WithID};
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum QueryPart {
+    Root,
+    Where,
+}
+
+#[derive(Debug)]
+pub struct DerivedQuery(HashMap<QueryPart, Vec<String>>);
+
+impl DerivedQuery {
+    fn new() -> Self {
+        Self { 0: HashMap::new() }
+    }
+
+    fn add(mut self, to: QueryPart, what: String) -> Self {
+        self.0.entry(to).or_insert_with(|| Vec::new()).push(what);
+        self
+    }
+
+    fn assemble(mut self) -> String {
+        let root = self.0.remove(&QueryPart::Root).unwrap().remove(0);
+        let where_ = match self.0.remove(&QueryPart::Where) {
+            None => String::new(),
+            Some(v) => {
+                format!(
+                    "WHERE {}",
+                    v.into_iter()
+                        .reduce(|a, b| format!("{} AND {}", a, b))
+                        .unwrap()
+                )
+            }
+        };
+
+        format!("{} {}", root, where_)
+    }
+}
+
+pub trait StaticVersion {
+    type Is: 'static;
+
+    fn type_id() -> std::any::TypeId {
+        std::any::TypeId::of::<Self::Is>()
+    }
+}
+
+/// Any query component
+pub trait QueryComponent: StaticVersion {
+    fn derive(&self) -> DerivedQuery {
+        DerivedQuery::new()
+    }
+
+    fn contribute<H: Hasher>(&self, hasher: &mut H);
+
+    /// returns the next index to use for binding
+    fn bind(&self, stmt: &mut sqlite::Statement<'_>) -> Result<usize, Error>;
+}
+
+/// Any query that can be completed/executed
+pub trait Resolvable<'r, 'q, T: Entity>: QueryComponent
+where
+    'q: 'r,
+{
+    fn qi(&self) -> &'r QueryInterface<'q>;
+
+    fn exec(self) -> Result<(), crate::Error> where Self: Sized { self.no_result() }
+    fn one(self) -> Result<Option<WithID<T>>, crate::Error> where Self: Sized { self.result() }
+    fn all(self) -> Result<Vec<WithID<T>>, crate::Error> where Self: Sized { self.results() }
+
+    fn no_result(self) -> Result<(), crate::Error>
+    where
+        Self: Sized,
+    {
+        let mut hasher = std::collections::hash_map::DefaultHasher::new();
+        self.contribute(&mut hasher);
+
+        self.qi().with_cache(
+            hasher.finish(),
+            || self.qi().db.conn.prepare(self.derive().assemble()).unwrap(),
+            |stmt| {
+                self.bind(stmt)?;
+
+                self.qi().expect_no_result(stmt)
+            },
+        )
+    }
+    fn result(self) -> Result<Option<WithID<T>>, crate::Error>
+    where
+        Self: Sized,
+    {
+        let mut hasher = std::collections::hash_map::DefaultHasher::new();
+        self.contribute(&mut hasher);
+        self.qi().with_cache(
+            hasher.finish(),
+            || {
+                let query = self.derive().assemble();
+                self.qi().db.conn.prepare(query).unwrap()
+            },
+            |stmt| {
+                self.bind(stmt)?;
+
+                self.qi().expect_one_result(stmt, &mut |stmt| {
+                    let id: i64 = stmt.read(0)?;
+                    Ok(WithID::wrap(T::build_from(stmt)?, id))
+                })
+            },
+        )
+    }
+    fn results(self) -> Result<Vec<WithID<T>>, crate::Error>
+    where
+        Self: Sized,
+    {
+        let mut hasher = std::collections::hash_map::DefaultHasher::new();
+        self.contribute(&mut hasher);
+        self.qi().with_cache(
+            hasher.finish(),
+            || self.qi().db.conn.prepare(self.derive().assemble()).unwrap(),
+            |stmt| {
+                self.bind(stmt)?;
+
+                let mut res = Vec::new();
+                loop {
+                    let state = stmt.next()?;
+                    if state == sqlite::State::Done {
+                        break;
+                    }
+
+                    let id: i64 = stmt.read(0)?;
+                    res.push(WithID::wrap(T::build_from(stmt)?, id));
+                }
+
+                Ok(res)
+            },
+        )
+    }
+}
+
+/// Any query that can have a WHERE clause attached to it
+pub trait Filterable<'r, 'q>: Resolvable<'r, 'q, Self::Table>
+where
+    'q: 'r,
+{
+    type Table: Entity;
+
+    fn by<C: EntityColumn<Entity = Self::Table>, G: Modelable + ?Sized>(
+        self,
+        col: C,
+        given: &'r G,
+    ) -> Filter<'r, 'q, Self, C, G>
+    where
+        Self: Sized,
+    {
+        Filter {
+            wrap: self,
+            col,
+            given,
+            _ghost: PhantomData,
+        }
+    }
+}
+
+pub struct Select<'r, 'q, T: Entity> {
+    qi: &'r QueryInterface<'q>,
+    _ghost: PhantomData<T>,
+}
+
+impl<'r, 'q, T: Entity> Select<'r, 'q, T> {
+    pub fn new(qi: &'r QueryInterface<'q>) -> Self {
+        Self {
+            qi,
+            _ghost: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<'r, 'q, T: Entity> StaticVersion for Select<'r, 'q, T> {
+    type Is = Select<'static, 'static, T>;
+}
+
+impl<'r, 'q, T: Entity> QueryComponent for Select<'r, 'q, T> {
+    fn derive(&self) -> DerivedQuery {
+        DerivedQuery::new().add(
+            QueryPart::Root,
+            format!("SELECT * FROM {}", T::table_name()),
+        )
+    }
+
+    fn contribute<H: Hasher>(&self, hasher: &mut H) {
+        "select".hash(hasher);
+        std::any::TypeId::of::<T>().hash(hasher);
+    }
+
+    // next binding point is the first
+    fn bind(&self, _stmt: &mut sqlite::Statement<'_>) -> Result<usize, Error> {
+        Ok(1)
+    }
+}
+
+impl<'r, 'q, T: Entity> Filterable<'r, 'q> for Select<'r, 'q, T> {
+    type Table = T;
+}
+
+impl<'r, 'q, T: Entity> Resolvable<'r, 'q, T> for Select<'r, 'q, T> {
+    fn qi(&self) -> &'r QueryInterface<'q> {
+        self.qi
+    }
+}
+
+pub struct Delete<'r, 'q, T: Entity> {
+    qi: &'r QueryInterface<'q>,
+    _ghost: PhantomData<T>,
+}
+
+impl<'r, 'q, T: Entity> Delete<'r, 'q, T> {
+    pub fn new(qi: &'r QueryInterface<'q>) -> Self {
+        Self {
+            qi,
+            _ghost: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<'r, 'q, T: Entity> StaticVersion for Delete<'r, 'q, T> {
+    type Is = Select<'static, 'static, T>;
+}
+
+impl<'r, 'q, T: Entity> QueryComponent for Delete<'r, 'q, T> {
+    fn derive(&self) -> DerivedQuery {
+        DerivedQuery::new().add(QueryPart::Root, format!("DELETE FROM {}", T::table_name()))
+    }
+
+    fn contribute<H: Hasher>(&self, hasher: &mut H) {
+        "delete".hash(hasher);
+        std::any::TypeId::of::<T>().hash(hasher);
+    }
+
+    // next binding point is the first
+    fn bind(&self, _stmt: &mut sqlite::Statement<'_>) -> Result<usize, Error> {
+        Ok(1)
+    }
+}
+
+impl<'r, 'q, T: Entity> Filterable<'r, 'q> for Delete<'r, 'q, T> {
+    type Table = T;
+}
+
+impl<'r, 'q, T: Entity> Resolvable<'r, 'q, T> for Delete<'r, 'q, T> {
+    fn qi(&self) -> &'r QueryInterface<'q> {
+        self.qi
+    }
+}
+
+/// A concrete WHERE clause
+pub struct Filter<'r, 'q, F: Filterable<'r, 'q>, C: EntityColumn, G: Modelable + ?Sized>
+where
+    'q: 'r,
+{
+    wrap: F,
+    col: C,
+    given: &'r G,
+    _ghost: PhantomData<&'q ()>,
+}
+
+impl<'r, 'q, F: Filterable<'r, 'q>, C: EntityColumn, G: Modelable + ?Sized> StaticVersion
+    for Filter<'r, 'q, F, C, G>
+where
+    <F as StaticVersion>::Is: Filterable<'static, 'static>,
+{
+    type Is = Filter<'static, 'static, <F as StaticVersion>::Is, C, u64>;
+}
+
+impl<'r, 'q, F: Filterable<'r, 'q>, C: EntityColumn, G: Modelable + ?Sized> QueryComponent
+    for Filter<'r, 'q, F, C, G>
+where
+    <F as StaticVersion>::Is: Filterable<'static, 'static>,
+    'q: 'r,
+{
+    fn derive(&self) -> DerivedQuery {
+        self.wrap
+            .derive()
+            .add(QueryPart::Where, format!("{} = ?", self.col.name()))
+    }
+
+    fn contribute<H: Hasher>(&self, hasher: &mut H) {
+        self.wrap.contribute(hasher);
+        std::any::TypeId::of::<Self::Is>().hash(hasher);
+        std::any::TypeId::of::<C>().hash(hasher);
+    }
+
+    fn bind(&self, stmt: &mut sqlite::Statement<'_>) -> Result<usize, crate::Error> {
+        let next_index = self.wrap.bind(stmt)?;
+
+        self.given.bind_to(stmt, next_index)?;
+
+        Ok(next_index + 1)
+    }
+}
+
+impl<'r, 'q, F: Filterable<'r, 'q>, C: EntityColumn, G: Modelable + ?Sized>
+    Resolvable<'r, 'q, C::Entity> for Filter<'r, 'q, F, C, G>
+where
+    <F as StaticVersion>::Is: Filterable<'static, 'static>,
+    'q: 'r,
+{
+    fn qi(&self) -> &'r QueryInterface<'q> {
+        self.wrap.qi()
+    }
+}
+
+impl<'r, 'q, F: Filterable<'r, 'q>, C: EntityColumn, G: Modelable + ?Sized> Filterable<'r, 'q>
+    for Filter<'r, 'q, F, C, G>
+where
+    <F as StaticVersion>::Is: Filterable<'static, 'static>,
+    'q: 'r,
+{
+    type Table = C::Entity;
+}