Browse Source

rustfmt pass and start of new select API.

Kestrel 1 year ago
parent
commit
400e4f0679

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

@@ -60,9 +60,16 @@ fn derive_columns<'a, I: Iterator<Item = &'a syn::Field>>(
                 fn index(&self) -> usize { #index }
                 fn name(&self) -> &'static str { #snake_case }
 
+                fn static_sql_type() -> &'static str { <#ty as #microrm_ref::model::Modelable>::column_type() }
+                fn static_index() -> usize { #index }
+                fn static_name() -> &'static str { #snake_case }
+
                 fn fk_table_name(&self) -> Option<&'static str> { #fk_table_name }
                 fn fk_column_name(&self) -> Option<&'static str> { #fk_column_name }
             }
+            impl #microrm_ref::entity::EntityColumnType for #columns_name::#converted_case {
+                type DataType = #ty;
+            }
         });
         column_consts.push(quote! {
             #[allow(non_upper_case_globals)]
@@ -77,6 +84,8 @@ fn derive_columns<'a, I: Iterator<Item = &'a syn::Field>>(
         struct_name.to_string().to_case(Case::ScreamingSnake)
     );
 
+    let id_name = format_ident!("{}ID", &input.ident);
+
     quote! {
         pub mod #columns_name {
             pub struct ID ();
@@ -85,13 +94,21 @@ fn derive_columns<'a, I: Iterator<Item = &'a syn::Field>>(
 
         impl #microrm_ref::entity::EntityColumn for #columns_name::ID {
             type Entity = #struct_name;
+
             fn sql_type(&self) -> &'static str { "integer" }
             fn index(&self) -> usize { 0 }
             fn name(&self) -> &'static str { "id" }
 
+            fn static_sql_type() -> &'static str { "integer" }
+            fn static_index() -> usize { 0 }
+            fn static_name() -> &'static str { "id" }
+
             fn fk_table_name(&self) -> Option<&'static str> { None }
             fn fk_column_name(&self) -> Option<&'static str> { None }
         }
+        impl #microrm_ref::entity::EntityColumnType for #columns_name::ID {
+            type DataType = #id_name;
+        }
         #(#column_impls)*
 
         impl #struct_name {

+ 1 - 1
microrm/benches/simple_in_memory.rs

@@ -1,6 +1,6 @@
 use criterion::{black_box, criterion_group, criterion_main, Criterion};
 
-use microrm::{make_index, Entity, prelude::*};
+use microrm::{make_index, prelude::*, Entity};
 use rand::prelude::*;
 use serde::{Deserialize, Serialize};
 

+ 9 - 0
microrm/src/entity.rs

@@ -37,10 +37,19 @@ pub trait EntityColumn: 'static + Send + Sync {
     fn index(&self) -> usize;
     fn name(&self) -> &'static str;
 
+    fn static_sql_type() -> &'static str where Self: Sized;
+    fn static_index() -> usize where Self: Sized;
+    fn static_name() -> &'static str where Self: Sized;
+
     fn fk_table_name(&self) -> Option<&'static str>;
     fn fk_column_name(&self) -> Option<&'static str>;
 }
 
+/// Trait representing the type of a column.
+pub trait EntityColumnType: 'static + EntityColumn {
+    type DataType: Modelable;
+}
+
 /// Trait for entity IDs in the database
 pub trait EntityID: 'static + std::fmt::Debug + Copy + Modelable {
     type Entity: Entity;

+ 44 - 20
microrm/src/lib.rs

@@ -292,7 +292,10 @@ mod test_support {
         let dist = rand::distributions::Uniform::new('a', 'z');
         let mut db_filename = std::env::temp_dir();
         let mut rng = rand::thread_rng();
-        db_filename.push(format!("microrm-{}.db", (0..16).map(|_| dist.sample(&mut rng)).collect::<String>()));
+        db_filename.push(format!(
+            "microrm-{}.db",
+            (0..16).map(|_| dist.sample(&mut rng)).collect::<String>()
+        ));
         db_filename
     }
 }
@@ -459,7 +462,7 @@ mod delete_test {
 mod datatypes {
     use crate::prelude::*;
 
-    #[derive(crate::Entity,serde::Serialize,serde::Deserialize,PartialEq,Debug)]
+    #[derive(crate::Entity, serde::Serialize, serde::Deserialize, PartialEq, Debug)]
     #[microrm_internal]
     pub struct ValueStore {
         pub b: bool,
@@ -475,7 +478,6 @@ mod datatypes {
         pub f_64: f64,
     }
 
-
     #[test]
     fn store_load_datatypes() {
         let schema = crate::Schema::new().entity::<ValueStore>();
@@ -493,12 +495,20 @@ mod datatypes {
             u_64: 3u64 << 62,
             s: "this is a test".to_string(),
             // e**pi
-            f_64: 23.140692632779263f64
+            f_64: 23.140692632779263f64,
         };
 
-        let id = db.query_interface().add(&test_values).expect("failed to add ValueStore");
-
-        let all = db.query_interface().get().by_id(&id).all().expect("failed to get by id");
+        let id = db
+            .query_interface()
+            .add(&test_values)
+            .expect("failed to add ValueStore");
+
+        let all = db
+            .query_interface()
+            .get()
+            .by_id(&id)
+            .all()
+            .expect("failed to get by id");
         assert_eq!(all.len(), 1);
         assert_eq!(all[0].as_ref(), &test_values);
     }
@@ -516,19 +526,26 @@ mod disk_tests {
 
         {
             let schema = crate::Schema::new().entity::<crate::test_support::KVStore>();
-            let db = crate::DB::new(schema, &path_str, crate::CreateMode::AllowNewDatabase).unwrap();
-
-            db.query_interface().add(&crate::test_support::KVStore {
-                key: "key".into(),
-                value: "val".into()
-            }).expect("couldn't add");
+            let db =
+                crate::DB::new(schema, &path_str, crate::CreateMode::AllowNewDatabase).unwrap();
+
+            db.query_interface()
+                .add(&crate::test_support::KVStore {
+                    key: "key".into(),
+                    value: "val".into(),
+                })
+                .expect("couldn't add");
         }
 
         {
             let schema = crate::Schema::new().entity::<crate::test_support::KVStore>();
             let db = crate::DB::new(schema, &path_str, crate::CreateMode::MustExist).unwrap();
 
-            let all = db.query_interface().get::<crate::test_support::KVStore>().all().expect("couldn't get all kv");
+            let all = db
+                .query_interface()
+                .get::<crate::test_support::KVStore>()
+                .all()
+                .expect("couldn't get all kv");
 
             assert_eq!(all.len(), 1);
         }
@@ -543,13 +560,16 @@ mod disk_tests {
 
         {
             let schema = crate::Schema::new().entity::<crate::test_support::KVStore>();
-            let db = crate::DB::new(schema, &path_str, crate::CreateMode::AllowNewDatabase).unwrap();
+            let db =
+                crate::DB::new(schema, &path_str, crate::CreateMode::AllowNewDatabase).unwrap();
             let dbp = crate::DBPool::new(&db);
 
-            dbp.query_interface().add(&crate::test_support::KVStore {
-                key: "key".into(),
-                value: "val".into()
-            }).expect("couldn't add");
+            dbp.query_interface()
+                .add(&crate::test_support::KVStore {
+                    key: "key".into(),
+                    value: "val".into(),
+                })
+                .expect("couldn't add");
         }
 
         {
@@ -557,7 +577,11 @@ mod disk_tests {
             let db = crate::DB::new(schema, &path_str, crate::CreateMode::MustExist).unwrap();
             let dbp = crate::DBPool::new(&db);
 
-            let all = dbp.query_interface().get::<crate::test_support::KVStore>().all().expect("couldn't get all kv");
+            let all = dbp
+                .query_interface()
+                .get::<crate::test_support::KVStore>()
+                .all()
+                .expect("couldn't get all kv");
 
             assert_eq!(all.len(), 1);
         }

+ 1 - 1
microrm/src/model.rs

@@ -1,8 +1,8 @@
 pub(crate) mod store;
 
 // Modelable implementations
-mod modelable;
 mod json;
+mod modelable;
 
 pub use json::JsonWrapper;
 

+ 19 - 9
microrm/src/model/json.rs

@@ -1,15 +1,19 @@
 /// Wrapper struct to store a serializable object as JSON transparently in a text column
 pub struct JsonWrapper<T: serde::Serialize + serde::de::DeserializeOwned + 'static> {
-    wrap: T
+    wrap: T,
 }
 
-impl<T: serde::Serialize + serde::de::DeserializeOwned + 'static> serde::Serialize for JsonWrapper<T> {
+impl<T: serde::Serialize + serde::de::DeserializeOwned + 'static> serde::Serialize
+    for JsonWrapper<T>
+{
     fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
         self.wrap.serialize(serializer)
     }
 }
 
-impl<'de, T: serde::Serialize + serde::de::DeserializeOwned + 'static> serde::Deserialize<'de> for JsonWrapper<T> {
+impl<'de, T: serde::Serialize + serde::de::DeserializeOwned + 'static> serde::Deserialize<'de>
+    for JsonWrapper<T>
+{
     fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
         Ok(Self::wrap(T::deserialize(deserializer)?))
     }
@@ -43,9 +47,13 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned + 'static> From<T> for Js
     }
 }
 
-impl<T: serde::Serialize + serde::de::DeserializeOwned + 'static> super::Modelable for JsonWrapper<T> {
+impl<T: serde::Serialize + serde::de::DeserializeOwned + 'static> super::Modelable
+    for JsonWrapper<T>
+{
     fn bind_to(&self, stmt: &mut sqlite::Statement, col: usize) -> sqlite::Result<()> {
-        serde_json::to_string(&self.wrap).unwrap().bind_to(stmt, col)
+        serde_json::to_string(&self.wrap)
+            .unwrap()
+            .bind_to(stmt, col)
     }
     fn build_from(stmt: &sqlite::Statement, col_offset: usize) -> sqlite::Result<(Self, usize)>
     where
@@ -53,10 +61,12 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned + 'static> super::Modelab
     {
         let s = String::build_from(stmt, col_offset)?;
         Ok((
-            Self::wrap(serde_json::from_str::<T>(s.0.as_str()).map_err(|e| sqlite::Error {
-                code: None,
-                message: Some(e.to_string()),
-            })?),
+            Self::wrap(
+                serde_json::from_str::<T>(s.0.as_str()).map_err(|e| sqlite::Error {
+                    code: None,
+                    message: Some(e.to_string()),
+                })?,
+            ),
             1,
         ))
     }

+ 4 - 10
microrm/src/model/modelable.rs

@@ -64,7 +64,7 @@ impl Modelable for bool {
     where
         Self: Sized,
     {
-        stmt.read(col_offset).map(|x: i64| (x != 0,1))
+        stmt.read(col_offset).map(|x: i64| (x != 0, 1))
     }
     fn column_type() -> &'static str
     where
@@ -155,14 +155,9 @@ impl<'a, T: Modelable> Modelable for &'a T {
 
 impl<T: Modelable> Modelable for Option<T> {
     fn bind_to(&self, stmt: &mut sqlite::Statement, col: usize) -> sqlite::Result<()> {
-
         match self.as_ref() {
-            Some(val) => {
-                val.bind_to(stmt, col)
-            }
-            None => {
-                stmt.bind(col, &sqlite::Value::Null)
-            }
+            Some(val) => val.bind_to(stmt, col),
+            None => stmt.bind(col, &sqlite::Value::Null),
         }
     }
     fn build_from(stmt: &sqlite::Statement, col_offset: usize) -> sqlite::Result<(Self, usize)>
@@ -173,8 +168,7 @@ impl<T: Modelable> Modelable for Option<T> {
         let val = stmt.read::<sqlite::Value>(col_offset)?;
         if val.kind() == sqlite::Type::Null {
             Ok((None, 1))
-        }
-        else {
+        } else {
             let (val, size) = T::build_from(stmt, col_offset)?;
             Ok((Some(val), size))
         }

+ 65 - 5
microrm/src/query.rs

@@ -9,10 +9,12 @@ pub mod filter;
 pub mod resolve;
 pub mod select;
 pub mod update;
+pub mod join;
 
 pub use filter::Filterable;
 pub use resolve::Resolvable;
 pub use update::Settable;
+pub use select::SelectSingleTrait;
 
 /// Wraps an entity with its ID, for example as a query result.
 ///
@@ -131,8 +133,7 @@ impl<'l> QueryInterface<'l> {
         hash: u64,
         create: Create,
         mut with: With,
-    ) -> Return
-where {
+    ) -> Return {
         let mut cache = self.cache.borrow_mut();
         let query = cache.entry(hash).or_insert_with(create);
         let res = with(query);
@@ -191,11 +192,19 @@ impl<'l> QueryInterface<'l> {
 // General query interface
 impl<'l> QueryInterface<'l> {
     /// Get an entity from its table, aka perform a `SELECT` query.
-    pub fn get<'a, 'b, T: Entity>(&'a self) -> select::Select<'b, 'l, T>
+    pub fn get<'a, 'b, T: Entity>(&'a self) -> select::SelectEntire<'b, 'l, T>
     where
         'a: 'b,
     {
-        select::Select::new(self)
+        select::SelectEntire::new(self)
+    }
+
+    /// Select single columns from their tables, aka perform a SELECT with INNER JOINs
+    pub fn select_columns<'a, 'b>(&'a self) -> select::SelectSingle<'b, 'l>
+    where
+        'a: 'b
+    {
+        select::SelectSingle::new(self)
     }
 
     /// Update an entity in its table, aka perform an `UPDATE` query, by selecting columns.
@@ -218,7 +227,10 @@ impl<'l> QueryInterface<'l> {
 impl<'l> QueryInterface<'l> {
     /// Update an entity in its table, aka perform an `UPDATE` query, for all columns.
     pub fn put<T: Entity>(&self, what: &WithID<T>) -> Result<(), Error> {
-        self.update().to(what.as_ref()).by(T::id_column(), &what.id()).exec()
+        self.update()
+            .to(what.as_ref())
+            .by(T::id_column(), &what.id())
+            .exec()
     }
 }
 
@@ -242,4 +254,52 @@ mod test_build {
 
         assert!(qi.get().by(KVStore::Key, "abc").result().is_ok());
     }
+
+    #[derive(Entity, Serialize, Deserialize)]
+    #[microrm_internal]
+    pub struct Group {
+        group: String,
+        by: String,
+        order: String,
+        select: String,
+        table: String,
+    }
+
+    #[test]
+    fn reserved_word_test() {
+        use super::*;
+        let db = crate::DB::new_in_memory(crate::Schema::new().entity::<Group>()).unwrap();
+        let qi = db.query_interface();
+
+        assert!(qi.get().by(Group::Group, "abc").result().is_ok());
+        assert!(qi.get().by(Group::By, "abc").result().is_ok());
+        assert!(qi.get().by(Group::Order, "abc").result().is_ok());
+        assert!(qi.get().by(Group::Select, "abc").result().is_ok());
+        assert!(qi.get().by(Group::Table, "abc").result().is_ok());
+    }
+
+    #[derive(Entity, Serialize, Deserialize)]
+    #[microrm_internal]
+    pub struct Left {
+        content: String,
+    }
+
+    #[derive(Entity, Serialize, Deserialize)]
+    #[microrm_internal]
+    pub struct Right {
+        #[microrm_foreign]
+        link: LeftID,
+        content: String,
+    }
+
+    #[test]
+    fn simple_join_test() {
+        use super::*;
+        let db = crate::DB::new_in_memory(crate::Schema::new().entity::<Left>().entity::<Right>()).unwrap();
+        let qi = db.query_interface();
+
+        use super::build::QueryComponent;
+        println!("{:?}", qi.select_columns().col(Left::Content).col(Right::Content).derive());
+        todo!()
+    }
 }

+ 43 - 3
microrm/src/query/build.rs

@@ -12,7 +12,10 @@ use std::{
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub enum QueryPart {
     Root,
+    Columns,
+    From,
     Set,
+    Join,
     Where,
 }
 
@@ -32,6 +35,30 @@ impl DerivedQuery {
     pub(crate) fn assemble(mut self) -> String {
         let root = self.0.remove(&QueryPart::Root).unwrap().remove(0);
 
+        let columns_ = match self.0.remove(&QueryPart::Columns) {
+            None => String::new(),
+            Some(v) => {
+                format!(
+                    "{}",
+                    v.into_iter()
+                        .reduce(|a, b| format!("{}, {}", a, b))
+                        .unwrap()
+                )
+            }
+        };
+
+        let from_ = match self.0.remove(&QueryPart::From) {
+            None => String::new(),
+            Some(v) => {
+                format!(
+                    "FROM {}",
+                    v.into_iter()
+                        .reduce(|a, b| format!("{}, {}", a, b))
+                        .unwrap()
+                )
+            }
+        };
+
         let set_ = match self.0.remove(&QueryPart::Set) {
             None => String::new(),
             Some(v) => {
@@ -44,6 +71,18 @@ impl DerivedQuery {
             }
         };
 
+        let join_ = match self.0.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.0.remove(&QueryPart::Where) {
             None => String::new(),
             Some(v) => {
@@ -56,9 +95,10 @@ impl DerivedQuery {
             }
         };
 
-        log::trace!("built SQL query: {} {} {}", root, set_, where_);
+        println!("built SQL query: {} {} {} {} {} {}", root, columns_, from_, set_, join_, where_);
+        // log::trace!("built SQL query: {} {} {}", root, set_, where_);
 
-        format!("{} {} {}", root, set_, where_)
+        format!("{} {} {} {} {} {}", root, columns_, from_, set_, join_, where_)
     }
 }
 
@@ -89,7 +129,7 @@ pub enum CompareOp {
     Equals,
     AtLeast,
     MoreThan,
-    NotEqual
+    NotEqual,
 }
 
 impl CompareOp {

+ 4 - 1
microrm/src/query/delete.rs

@@ -24,7 +24,10 @@ impl<'r, 'q, T: Entity> StaticVersion for Delete<'r, 'q, 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()))
+        DerivedQuery::new().add(
+            QueryPart::Root,
+            format!("DELETE FROM `{}`", T::table_name()),
+        )
     }
 
     fn contribute<H: Hasher>(&self, hasher: &mut H) {

+ 1 - 1
microrm/src/query/filter.rs

@@ -93,7 +93,7 @@ where
     fn derive(&self) -> DerivedQuery {
         self.wrap.derive().add(
             QueryPart::Where,
-            format!("{} {} ?", self.col.name(), self.op.ch()),
+            format!("`{}` {} ?", self.col.name(), self.op.ch()),
         )
     }
 

+ 0 - 0
microrm/src/query/join.rs


+ 91 - 9
microrm/src/query/select.rs

@@ -1,15 +1,16 @@
 use super::build::{DerivedQuery, QueryComponent, QueryPart, StaticVersion};
 use super::{Filterable, Resolvable};
 use crate::{Entity, Error, QueryInterface};
+use crate::entity::{EntityColumn, EntityColumnType};
 use std::hash::{Hash, Hasher};
 use std::marker::PhantomData;
 
-pub struct Select<'r, 'q, T: Entity> {
+pub struct SelectEntire<'r, 'q, T: Entity> {
     qi: &'r QueryInterface<'q>,
     _ghost: PhantomData<T>,
 }
 
-impl<'r, 'q, T: Entity> Select<'r, 'q, T> {
+impl<'r, 'q, T: Entity> SelectEntire<'r, 'q, T> {
     pub fn new(qi: &'r QueryInterface<'q>) -> Self {
         Self {
             qi,
@@ -18,20 +19,22 @@ impl<'r, 'q, T: Entity> Select<'r, 'q, T> {
     }
 }
 
-impl<'r, 'q, T: Entity> StaticVersion for Select<'r, 'q, T> {
-    type Is = Select<'static, 'static, T>;
+impl<'r, 'q, T: Entity> StaticVersion for SelectEntire<'r, 'q, T> {
+    type Is = SelectEntire<'static, 'static, T>;
 }
 
-impl<'r, 'q, T: Entity> QueryComponent for Select<'r, 'q, T> {
+impl<'r, 'q, T: Entity> QueryComponent for SelectEntire<'r, 'q, T> {
     fn derive(&self) -> DerivedQuery {
         DerivedQuery::new().add(
             QueryPart::Root,
-            format!("SELECT * FROM `{}`", T::table_name()),
+            "SELECT".to_string() // * FROM `{}`", T::table_name()),
         )
+        .add(QueryPart::Columns, "*".to_string())
+        .add(QueryPart::From, format!("`{}`", T::table_name()))
     }
 
     fn contribute<H: Hasher>(&self, hasher: &mut H) {
-        "select".hash(hasher);
+        "select_entire".hash(hasher);
         std::any::TypeId::of::<T>().hash(hasher);
     }
 
@@ -41,12 +44,91 @@ impl<'r, 'q, T: Entity> QueryComponent for Select<'r, 'q, T> {
     }
 }
 
-impl<'r, 'q, T: Entity> Filterable<'r, 'q> for Select<'r, 'q, T> {
+impl<'r, 'q, T: Entity> Filterable<'r, 'q> for SelectEntire<'r, 'q, T> {
     type Table = T;
 }
 
-impl<'r, 'q, T: Entity> Resolvable<'r, 'q, T> for Select<'r, 'q, T> {
+impl<'r, 'q, T: Entity> Resolvable<'r, 'q, T> for SelectEntire<'r, 'q, T> {
     fn qi(&self) -> &'r QueryInterface<'q> {
         self.qi
     }
 }
+
+pub struct SelectSingle<'r, 'q> {
+    qi: &'r QueryInterface<'q>,
+}
+
+impl<'r, 'q> SelectSingle<'r, 'q> {
+    pub fn new(qi: &'r QueryInterface<'q>) -> Self {
+        Self { qi }
+    }
+}
+
+impl<'r, 'q> StaticVersion for SelectSingle<'r, 'q> {
+    type Is = SelectSingle<'static, 'static>;
+}
+
+impl<'r, 'q> QueryComponent for SelectSingle<'r, 'q> {
+    fn derive(&self) -> DerivedQuery {
+        DerivedQuery::new().add(
+            QueryPart::Root,
+            format!("SELECT"),
+        )
+    }
+
+    fn contribute<H: Hasher>(&self, hasher: &mut H) {
+        "select_single".hash(hasher);
+    }
+
+    fn bind(&self, _stmt: &mut sqlite::Statement<'_>) -> Result<usize, Error> {
+        todo!()
+    }
+}
+
+pub trait SelectSingleTrait<'r, 'q>: QueryComponent + StaticVersion {
+    type RType;
+
+    fn col<C: EntityColumn>(self, _col: C) -> SingleColumn<'r, 'q, Self, C> where Self: Sized {
+        SingleColumn { parent: self, _ghost: Default::default() }
+    }
+}
+
+impl<'r, 'q> SelectSingleTrait<'r, 'q> for SelectSingle<'r, 'q> {
+    type RType = ();
+}
+
+pub struct SingleColumn<'r, 'q, P: SelectSingleTrait<'r, 'q>, C: EntityColumn> {
+    parent: P,
+    _ghost: std::marker::PhantomData<(C, &'r (), &'q ())>
+}
+
+impl<'r, 'q, P: SelectSingleTrait<'r, 'q>, C: EntityColumn> StaticVersion for SingleColumn<'r, 'q, P, C>
+    where P::Is: SelectSingleTrait<'static, 'static> {
+    type Is = SingleColumn<'static, 'static, <P as StaticVersion>::Is, C>;
+}
+
+impl<'r, 'q, P: SelectSingleTrait<'r, 'q>, C: EntityColumn + EntityColumnType> SelectSingleTrait<'r, 'q> for SingleColumn<'r, 'q, P, C>
+    where P::Is: SelectSingleTrait<'static, 'static>
+{
+    type RType = (P::RType, <C as EntityColumnType>::DataType);
+}
+
+impl<'r, 'q, P: SelectSingleTrait<'r, 'q> + StaticVersion, C: EntityColumn> QueryComponent for SingleColumn<'r, 'q, P, C>
+    where P::Is: SelectSingleTrait<'static, 'static> {
+
+    fn derive(&self) -> DerivedQuery {
+        self.parent.derive().add(
+            QueryPart::Columns,
+            format!("`{}.{}`", C::Entity::table_name(), C::static_name())
+        )
+    }
+
+    fn contribute<H: Hasher>(&self, hasher: &mut H) {
+        "single_column".hash(hasher);
+        std::any::TypeId::of::<C>().hash(hasher);
+    }
+
+    fn bind(&self, _stmt: &mut sqlite::Statement<'_>) -> Result<usize, Error> {
+        todo!()
+    }
+}

+ 1 - 1
microrm/src/query/update.rs

@@ -41,7 +41,7 @@ impl<'r, 'q, T: Entity> QueryComponent for Update<'r, 'q, T> {
     }
 
     fn contribute<H: Hasher>(&self, hasher: &mut H) {
-        "select".hash(hasher);
+        "update".hash(hasher);
         std::any::TypeId::of::<T>().hash(hasher);
     }