Bläddra i källkod

Added IDWrap<> and Serialized<> types.

Kestrel 1 år sedan
förälder
incheckning
96f9ed0aa9

+ 34 - 4
microrm-macros/src/entity.rs

@@ -109,10 +109,18 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
         .map(|(i, part)| {
             let ident = &part.0;
 
-            let idx = syn::Index::from(i);
-
-            quote! {
-                #ident: values. #idx
+            match parts.len() {
+                1 => {
+                    quote! {
+                        #ident: values
+                    }
+                }
+                _ => {
+                    let idx = syn::Index::from(i);
+                    quote! {
+                        #ident: values. #idx
+                    }
+                }
             }
         })
         .collect::<Vec<_>>();
@@ -141,6 +149,28 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
             fn into_raw(self) -> i64 { self.0 }
         }
 
+        impl ::microrm::entity::EntityDatum for #id_ident {
+            fn sql_type() -> &'static str {
+                <i64 as ::microrm::entity::EntityDatum>::sql_type()
+            }
+
+            fn bind_to<'a>(&self, stmt: &mut ::microrm::sqlite::Statement<'a>, index: usize) {
+                <i64 as ::microrm::entity::EntityDatum>::bind_to(&self.0, stmt, index)
+            }
+
+            fn build_from<'a>(
+                adata: ::microrm::schema::AssocData,
+                stmt: &mut ::microrm::sqlite::Statement<'a>,
+                index: usize,
+            ) -> ::microrm::DBResult<(Self, usize)>
+            where
+                Self: Sized,
+            {
+                let raw = <i64 as ::microrm::entity::EntityDatum>::build_from(adata, stmt, index)?;
+                Ok((Self(raw.0), raw.1))
+            }
+        }
+
         impl ::microrm::entity::Entity for #entity_ident {
             type Parts = #parts_list;
             type Uniques = #uniques_list;

+ 1 - 1
microrm/src/db.rs

@@ -5,7 +5,7 @@ use std::{
     sync::{Arc, Mutex},
 };
 
-pub(crate) type DBConnection = std::sync::Arc<Connection>;
+pub type DBConnection = std::sync::Arc<Connection>;
 
 pub struct Connection {
     // we leak the ConnectionThreadSafe and make sure that the only references to it are stored in

+ 45 - 1
microrm/src/entity/datum.rs

@@ -1,4 +1,4 @@
-use crate::{db::DBConnection, schema::AssocData, DBResult};
+use crate::{schema::AssocData, DBResult};
 
 use super::EntityDatum;
 
@@ -130,3 +130,47 @@ impl<T: EntityDatum> EntityDatum for Option<T> {
         todo!()
     }
 }
+
+impl EntityDatum for bool {
+    fn sql_type() -> &'static str {
+        "int"
+    }
+
+    fn bind_to<'a>(&self, stmt: &mut sqlite::Statement<'a>, index: usize) {
+        stmt.bind((index, if *self { 1 } else { 0 }))
+            .expect("couldn't bind bool");
+    }
+
+    fn build_from<'a>(
+        _: AssocData,
+        stmt: &mut sqlite::Statement<'a>,
+        index: usize,
+    ) -> DBResult<(Self, usize)>
+    where
+        Self: Sized,
+    {
+        Ok((stmt.read::<i64, _>(index)? == 1, index + 1))
+    }
+}
+
+impl EntityDatum for Vec<u8> {
+    fn sql_type() -> &'static str {
+        "blob"
+    }
+
+    fn bind_to<'a>(&self, stmt: &mut sqlite::Statement<'a>, index: usize) {
+        stmt.bind((index, self.as_slice()))
+            .expect("couldn't bind Vec<u8>");
+    }
+
+    fn build_from<'a>(
+        _: AssocData,
+        stmt: &mut sqlite::Statement<'a>,
+        index: usize,
+    ) -> DBResult<(Self, usize)>
+    where
+        Self: Sized,
+    {
+        Ok((stmt.read::<Vec<u8>, _>(index)?, index + 1))
+    }
+}

+ 25 - 0
microrm/src/entity/datum_list.rs

@@ -41,3 +41,28 @@ impl<T0: EntityDatum, T1: EntityDatum, T2: EntityDatum, T3: EntityDatum> EntityD
         visitor.visit(&self.3);
     }
 }
+
+impl<T0: EntityDatum, T1: EntityDatum, T2: EntityDatum, T3: EntityDatum, T4: EntityDatum> EntityDatumList
+    for (T0, T1, T2, T3, T4)
+{
+    fn accept(&self, visitor: &mut impl EntityDatumVisitor) {
+        visitor.visit(&self.0);
+        visitor.visit(&self.1);
+        visitor.visit(&self.2);
+        visitor.visit(&self.3);
+        visitor.visit(&self.4);
+    }
+}
+
+impl<T0: EntityDatum, T1: EntityDatum, T2: EntityDatum, T3: EntityDatum, T4: EntityDatum, T5: EntityDatum> EntityDatumList
+    for (T0, T1, T2, T3, T4, T5)
+{
+    fn accept(&self, visitor: &mut impl EntityDatumVisitor) {
+        visitor.visit(&self.0);
+        visitor.visit(&self.1);
+        visitor.visit(&self.2);
+        visitor.visit(&self.3);
+        visitor.visit(&self.4);
+        visitor.visit(&self.5);
+    }
+}

+ 83 - 0
microrm/src/entity/part_list.rs

@@ -45,6 +45,7 @@ impl<P0: EntityPart> EntityPartList for P0 {
         let idx = 1; // starting index is 1 since index 0 is the ID
         build_datum!(conn, ctx, base_rowid, stmt, idx, d0, P0);
 
+        let _ = idx;
         Ok(d0)
     }
 
@@ -88,6 +89,7 @@ impl<P0: EntityPart, P1: EntityPart> EntityPartList for (P0, P1) {
         build_datum!(conn, ctx, base_rowid, stmt, idx, d0, P0);
         build_datum!(conn, ctx, base_rowid, stmt, idx, d1, P1);
 
+        let _ = idx;
         Ok((d0, d1))
     }
 
@@ -115,6 +117,7 @@ impl<P0: EntityPart, P1: EntityPart, P2: EntityPart> EntityPartList for (P0, P1,
         build_datum!(conn, ctx, base_rowid, stmt, idx, d1, P1);
         build_datum!(conn, ctx, base_rowid, stmt, idx, d2, P2);
 
+        let _ = idx;
         Ok((d0, d1, d2))
     }
 
@@ -147,6 +150,7 @@ impl<P0: EntityPart, P1: EntityPart, P2: EntityPart, P3: EntityPart> EntityPartL
         build_datum!(conn, ctx, base_rowid, stmt, idx, d2, P2);
         build_datum!(conn, ctx, base_rowid, stmt, idx, d3, P3);
 
+        let _ = idx;
         Ok((d0, d1, d2, d3))
     }
 
@@ -163,3 +167,82 @@ impl<P0: EntityPart, P1: EntityPart, P2: EntityPart, P3: EntityPart> EntityPartL
         v.visit_datum::<P3>(&datum_list.3);
     }
 }
+
+impl<P0: EntityPart, P1: EntityPart, P2: EntityPart, P3: EntityPart, P4: EntityPart> EntityPartList
+    for (P0, P1, P2, P3, P4)
+{
+    type DatumList = (P0::Datum, P1::Datum, P2::Datum, P3::Datum, P4::Datum);
+
+    fn build_datum_list(
+        conn: &DBConnection,
+        ctx: &'static str,
+        stmt: &mut sqlite::Statement<'static>,
+    ) -> DBResult<Self::DatumList> {
+        let base_rowid: i64 = stmt.read(0)?;
+        let idx = 1; // starting index is 1 since index 0 is the ID
+        build_datum!(conn, ctx, base_rowid, stmt, idx, d0, P0);
+        build_datum!(conn, ctx, base_rowid, stmt, idx, d1, P1);
+        build_datum!(conn, ctx, base_rowid, stmt, idx, d2, P2);
+        build_datum!(conn, ctx, base_rowid, stmt, idx, d3, P3);
+        build_datum!(conn, ctx, base_rowid, stmt, idx, d4, P4);
+
+        let _ = idx;
+        Ok((d0, d1, d2, d3, d4))
+    }
+
+    fn accept_part_visitor(v: &mut impl EntityPartVisitor) {
+        v.visit::<P0>();
+        v.visit::<P1>();
+        v.visit::<P2>();
+        v.visit::<P3>();
+        v.visit::<P4>();
+    }
+    fn accept_part_visitor_ref(datum_list: &Self::DatumList, v: &mut impl EntityPartVisitor) {
+        v.visit_datum::<P0>(&datum_list.0);
+        v.visit_datum::<P1>(&datum_list.1);
+        v.visit_datum::<P2>(&datum_list.2);
+        v.visit_datum::<P3>(&datum_list.3);
+        v.visit_datum::<P4>(&datum_list.4);
+    }
+}
+
+impl<P0: EntityPart, P1: EntityPart, P2: EntityPart, P3: EntityPart, P4: EntityPart, P5: EntityPart> EntityPartList
+    for (P0, P1, P2, P3, P4, P5)
+{
+    type DatumList = (P0::Datum, P1::Datum, P2::Datum, P3::Datum, P4::Datum, P5::Datum);
+
+    fn build_datum_list(
+        conn: &DBConnection,
+        ctx: &'static str,
+        stmt: &mut sqlite::Statement<'static>,
+    ) -> DBResult<Self::DatumList> {
+        let base_rowid: i64 = stmt.read(0)?;
+        let idx = 1; // starting index is 1 since index 0 is the ID
+        build_datum!(conn, ctx, base_rowid, stmt, idx, d0, P0);
+        build_datum!(conn, ctx, base_rowid, stmt, idx, d1, P1);
+        build_datum!(conn, ctx, base_rowid, stmt, idx, d2, P2);
+        build_datum!(conn, ctx, base_rowid, stmt, idx, d3, P3);
+        build_datum!(conn, ctx, base_rowid, stmt, idx, d4, P4);
+        build_datum!(conn, ctx, base_rowid, stmt, idx, d5, P5);
+
+        let _ = idx;
+        Ok((d0, d1, d2, d3, d4, d5))
+    }
+
+    fn accept_part_visitor(v: &mut impl EntityPartVisitor) {
+        v.visit::<P0>();
+        v.visit::<P1>();
+        v.visit::<P2>();
+        v.visit::<P3>();
+        v.visit::<P4>();
+        v.visit::<P5>();
+    }
+    fn accept_part_visitor_ref(datum_list: &Self::DatumList, v: &mut impl EntityPartVisitor) {
+        v.visit_datum::<P0>(&datum_list.0);
+        v.visit_datum::<P1>(&datum_list.1);
+        v.visit_datum::<P2>(&datum_list.2);
+        v.visit_datum::<P3>(&datum_list.3);
+        v.visit_datum::<P4>(&datum_list.4);
+        v.visit_datum::<P5>(&datum_list.5);
+    }
+}

+ 7 - 1
microrm/src/lib.rs

@@ -6,9 +6,13 @@ pub mod entity;
 mod query;
 pub mod schema;
 
+#[doc(hidden)]
+pub use sqlite;
+
 pub mod prelude {
     pub use microrm_macros::{Database, Entity};
-    // pub use crate::entity::
+    pub use crate::entity::Entity;
+    pub use crate::schema::Database;
 }
 
 // ----------------------------------------------------------------------
@@ -18,8 +22,10 @@ pub mod prelude {
 #[derive(Debug)]
 pub enum DBError {
     EmptyResult,
+    IncompatibleSchema,
     LogicError(&'static str),
     Sqlite(sqlite::Error),
+    JSON(serde_json::Error),
     LockError(String),
 }
 

+ 5 - 5
microrm/src/query.rs

@@ -1,5 +1,5 @@
 use crate::entity::helpers::check_assoc;
-use crate::schema::AssocMap;
+use crate::schema::{AssocMap, IDWrap};
 use crate::{
     entity::{
         Entity, EntityDatum, EntityDatumList, EntityDatumVisitor, EntityID, EntityPart,
@@ -72,7 +72,7 @@ pub(crate) fn select_assoc<E: Entity>(map: &AssocMap<E>) -> DBResult<Vec<E>> {
     )
 }
 
-pub(crate) fn insert_assoc<E: Entity>(map: &AssocMap<E>, value: &E) -> DBResult<()> {
+pub(crate) fn insert_assoc<E: Entity>(map: &AssocMap<E>, value: E) -> DBResult<()> {
     // we're doing two things:
     // - inserting the entity into the target table
     // - adding thw association row into the assoc table
@@ -111,7 +111,7 @@ pub(crate) fn insert_assoc<E: Entity>(map: &AssocMap<E>, value: &E) -> DBResult<
 pub(crate) fn select_by<E: Entity, PL: EntityPartList>(
     map: &IDMap<E>,
     by: &PL::DatumList,
-) -> DBResult<Vec<E>> {
+) -> DBResult<Vec<IDWrap<E>>> {
     struct HashDatumListTypes(std::collections::hash_map::DefaultHasher);
     impl EntityDatumVisitor for HashDatumListTypes {
         fn visit<ED: EntityDatum>(&mut self, _: &ED) {
@@ -157,7 +157,7 @@ pub(crate) fn select_by<E: Entity, PL: EntityPartList>(
             let mut rows = vec![];
             while stmt.next()? == sqlite::State::Row {
                 let datum_list = <E::Parts>::build_datum_list(&map.conn(), map.ctx(), stmt)?;
-                rows.push(E::build(datum_list));
+                rows.push(IDWrap::new(<E::ID>::from_raw(stmt.read::<i64, _>(0)?), E::build(datum_list)));
             }
 
             stmt.reset()?;
@@ -169,7 +169,7 @@ pub(crate) fn select_by<E: Entity, PL: EntityPartList>(
 
 pub(crate) fn insert<E: Entity>(
     map: &impl EntityMap<ContainedEntity = E>,
-    value: &E,
+    value: E,
 ) -> DBResult<E::ID> {
     map.conn().with_prepared(
         query_hash::<E>(map.ctx(), QueryType::Insert),

+ 119 - 9
microrm/src/schema.rs

@@ -14,6 +14,7 @@ mod tests;
 // Specification types
 // ----------------------------------------------------------------------
 
+/// Trait for a type that represents a sqlite table that contains entities
 pub(crate) trait EntityMap {
     type ContainedEntity: Entity;
 
@@ -21,6 +22,51 @@ pub(crate) trait EntityMap {
     fn ctx(&self) -> &'static str;
 }
 
+pub struct IDWrap<T: Entity> {
+    id: T::ID,
+    wrap: T,
+}
+
+impl<T: Entity> IDWrap<T> {
+    pub fn new(id: T::ID, value: T) -> Self {
+        Self { id, wrap: value }
+    }
+
+    pub fn id(&self) -> T::ID {
+        self.id
+    }
+
+    pub fn as_ref(&self) -> &T {
+        &self.wrap
+    }
+}
+
+impl<T: Entity> AsRef<T> for IDWrap<T> {
+    fn as_ref(&self) -> &T {
+        &self.wrap
+    }
+}
+
+impl<T: Entity> AsMut<T> for IDWrap<T> {
+    fn as_mut(&mut self) -> &mut T {
+        &mut self.wrap
+    }
+}
+
+impl<T: Entity> std::ops::Deref for IDWrap<T> {
+    type Target = T;
+    fn deref(&self) -> &Self::Target {
+        &self.wrap
+    }
+}
+
+impl<T: Entity> std::ops::DerefMut for IDWrap<T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.wrap
+    }
+}
+
+/// Table with EntityID-based lookup.
 pub struct IDMap<T: Entity> {
     conn: DBConnection,
     ctx: &'static str,
@@ -55,7 +101,7 @@ impl<T: Entity> IDMap<T> {
     pub fn lookup_unique(
         &self,
         uniques: &<<T as Entity>::Uniques as EntityPartList>::DatumList,
-    ) -> DBResult<Option<T>> {
+    ) -> DBResult<Option<IDWrap<T>>> {
         query::select_by::<T, T::Uniques>(self, uniques).map(|mut v| {
             if v.len() > 0 {
                 Some(v.remove(0))
@@ -65,7 +111,7 @@ impl<T: Entity> IDMap<T> {
         })
     }
 
-    pub fn insert(&self, value: &T) -> DBResult<T::ID> {
+    pub fn insert(&self, value: T) -> DBResult<T::ID> {
         query::insert(self, value)
     }
 }
@@ -109,7 +155,7 @@ impl<T: Entity> AssocMap<T> {
         query::select_assoc(self)
     }
 
-    pub fn insert(&self, value: &T) -> DBResult<()> {
+    pub fn insert(&self, value: T) -> DBResult<()> {
         query::insert_assoc(self, value)
     }
 }
@@ -141,7 +187,7 @@ impl<T: Entity> EntityDatum for AssocMap<T> {
 
     fn build_from<'a>(
         adata: AssocData,
-        stmt: &mut sqlite::Statement<'a>,
+        _stmt: &mut sqlite::Statement<'a>,
         index: usize,
     ) -> DBResult<(Self, usize)>
     where
@@ -158,6 +204,65 @@ impl<T: Entity> EntityDatum for AssocMap<T> {
     }
 }
 
+pub struct Serialized<T: serde::Serialize + serde::de::DeserializeOwned> {
+    wrapped: T,
+}
+
+impl<T: serde::Serialize + serde::de::DeserializeOwned + Default> Default for Serialized<T> {
+    fn default() -> Self {
+        Self {
+            wrapped: T::default(),
+        }
+    }
+}
+
+impl<T: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug> std::fmt::Debug for Serialized<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        <T as std::fmt::Debug>::fmt(&self.wrapped, f)
+    }
+}
+
+impl<T: serde::Serialize + serde::de::DeserializeOwned> From<T> for Serialized<T> {
+    fn from(value: T) -> Self {
+        Self { wrapped: value }
+    }
+}
+
+impl<T: serde::Serialize + serde::de::DeserializeOwned> AsRef<T> for Serialized<T> {
+    fn as_ref(&self) -> &T {
+        &self.wrapped
+    }
+}
+
+impl<T: 'static + serde::Serialize + serde::de::DeserializeOwned> EntityDatum for Serialized<T> {
+    fn sql_type() -> &'static str {
+        "text"
+    }
+
+    fn bind_to<'a>(&self, stmt: &mut sqlite::Statement<'a>, index: usize) {
+        <String as EntityDatum>::bind_to(
+            &serde_json::to_string(&self.wrapped).expect("couldn't serialize object into JSON"),
+            stmt,
+            index
+        )
+    }
+
+    fn build_from<'a>(
+        adata: AssocData,
+        stmt: &mut sqlite::Statement<'a>,
+        index: usize,
+    ) -> DBResult<(Self, usize)>
+    where
+        Self: Sized {
+
+        let (s, idx) = <String as EntityDatum>::build_from(adata, stmt, index)?;
+
+        let d = serde_json::from_str::<T>(s.as_str()).map_err(|e| DBError::JSON(e))?;
+
+        Ok((Self { wrapped: d }, idx))
+    }
+}
+
 pub trait DatabaseItem {
     fn item_key() -> &'static str;
     fn dependency_keys() -> &'static [&'static str];
@@ -198,13 +303,18 @@ pub trait Database {
     {
         let conn = Connection::open(uri)?;
         let schema = build::collect_from_database::<Self>();
-        if !schema.check(conn.clone()) {
-            schema.create(conn.clone())
+        match schema.check(conn.clone()) {
+            // schema checks out
+            Some(true) => {},
+            // schema doesn't match
+            Some(false) => Err(DBError::IncompatibleSchema)?,
+            // no schema found
+            None => {
+                schema.create(conn.clone())?;
+            }
         }
 
-        let db = Self::build(conn);
-
-        Ok(db)
+        Ok(Self::build(conn))
     }
 
     #[doc(hidden)]

+ 14 - 11
microrm/src/schema/build.rs

@@ -1,7 +1,7 @@
-use crate::schema::{
+use crate::{schema::{
     entity::{EntityStateContainer, PartType},
     meta, DBConnection, Database, DatabaseItem, DatabaseItemVisitor,
-};
+}, DBResult};
 
 #[derive(Debug)]
 struct ColumnInfo {
@@ -60,7 +60,12 @@ pub(crate) struct DatabaseSchema {
 
 impl DatabaseSchema {
     const SCHEMA_SIGNATURE_KEY: &'static str = "schema_signature";
-    pub fn check(&self, db: DBConnection) -> bool {
+
+    /// Three possible results:
+    /// - yes, this is a schema match (true)
+    /// - no, this is not a schema match (false)
+    /// - there is no schema that we know of (None)
+    pub fn check(&self, db: DBConnection) -> Option<bool> {
         // attempt to use connection as a MetadataDB database
         let metadb = meta::MetadataDB::build(db);
 
@@ -71,30 +76,28 @@ impl DatabaseSchema {
             .ok()
             .flatten()
             .map(|kv| kv.value.parse::<u64>().unwrap_or(0) == self.signature)
-            .unwrap_or(false)
     }
 
-    pub fn create(&self, db: DBConnection) {
+    pub fn create(&self, db: DBConnection) -> DBResult<()> {
         for query in self.queries.iter() {
-            db.execute_raw(query).expect("Couldn't run creation query!");
+            db.execute_raw(query)?;
         }
 
         // attempt to use connection as a MetadataDB database
         let metadb = meta::MetadataDB::build(db.clone());
 
         for query in collect_from_database::<meta::MetadataDB>().queries.iter() {
-            db.execute_raw(query)
-                .expect("Couldn't run MetadataDB creation query!");
+            db.execute_raw(query)?;
         }
 
         // store signature
         metadb
             .kv_metastore
-            .insert(&meta::KV {
+            .insert(meta::KV {
                 key: Self::SCHEMA_SIGNATURE_KEY.into(),
                 value: format!("{}", self.signature),
-            })
-            .expect("couldn't set schema signature");
+            })?;
+        Ok(())
     }
 }
 

+ 4 - 6
microrm/src/schema/tests.rs

@@ -163,7 +163,7 @@ mod derive_tests {
             .is_none());
 
         db.people
-            .insert(&Person {
+            .insert(Person {
                 name: name_string.clone(),
                 roles: AssocMap::empty(),
             })
@@ -184,7 +184,7 @@ mod derive_tests {
 
         let name_string = "name_here".to_string();
         db.people
-            .insert(&Person {
+            .insert(Person {
                 name: name_string.clone(),
                 roles: AssocMap::empty(),
             })
@@ -208,7 +208,7 @@ mod derive_tests {
 
         let name_string = "name_here".to_string();
         db.people
-            .insert(&Person {
+            .insert(Person {
                 name: name_string.clone(),
                 roles: AssocMap::empty(),
             })
@@ -221,11 +221,9 @@ mod derive_tests {
             .flatten()
             .expect("couldn't re-get test person entity");
 
-        person.roles.insert(&Role {
+        person.roles.insert(Role {
             title: "title A".to_string(),
             permissions: "permissions A".to_string(),
         });
-
-        println!("roles: {:?}", person.roles.get_all());
     }
 }