Browse Source

Many minor changes to better support autogenerating interfaces based on microrm schemata.

Kestrel 7 months ago
parent
commit
60ab001dc1

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

@@ -1,6 +1,30 @@
 use convert_case::{Case, Casing};
 use quote::{format_ident, quote};
 
+fn extract_doc_comment(attrs: &Vec<syn::Attribute>) -> proc_macro2::TokenStream {
+    attrs
+        .iter()
+        .map(|a| match a.parse_meta() {
+            Ok(syn::Meta::NameValue(mnv)) => {
+                if mnv.path.is_ident("doc") {
+                    if let syn::Lit::Str(ls) = mnv.lit {
+                        let lsv = ls.value();
+                        return Some(quote! { Some(#lsv) });
+                    }
+                }
+                None
+            }
+            _ => None,
+        })
+        .flatten()
+        .next()
+        .unwrap_or(quote! { None })
+}
+
+fn is_elided(attrs: &Vec<syn::Attribute>) -> bool {
+    attrs.iter().filter(|a| a.path.is_ident("elide")).count() > 0
+}
+
 pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let input: syn::DeriveInput = syn::parse_macro_input!(tokens);
 
@@ -67,8 +91,10 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
 
         let unique = unique_parts.iter().any(|p| p.0 == part.0);
 
+        let doc = extract_doc_comment(&part.2);
+
         quote! {
-            #[derive(Clone, Copy)]
+            #[derive(Clone, Copy, Default)]
             #vis struct #part_combined_name;
             impl ::microrm::schema::entity::EntityPart for #part_combined_name {
                 type Datum = #part_type;
@@ -82,6 +108,9 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
                 fn unique() -> bool {
                     #unique
                 }
+                fn desc() -> Option<&'static str> {
+                    #doc
+                }
             }
         }
     });
@@ -141,6 +170,7 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
 
     let debug_fields = parts
         .iter()
+        .filter(|part| !is_elided(&part.2))
         .map(|part| {
             let ident = &part.0;
             let field = ident.to_string();
@@ -153,6 +183,7 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let parts_list = make_part_list(&parts);
     let uniques_list = make_part_list(&unique_parts);
 
+    let entity_ident_str = entity_ident.to_string();
     let entity_name = entity_ident.to_string().to_case(Case::Snake);
 
     let id_ident = format_ident!("{}ID", entity_ident);
@@ -164,7 +195,7 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
             #(#part_names)*
         }
 
-        #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
+        #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
         #vis struct #id_ident (i64);
 
         impl ::microrm::schema::entity::EntityID for #id_ident {
@@ -181,6 +212,8 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
             fn unique() -> bool { false }
             fn part_name() -> &'static str { "id" }
             fn placeholder() -> &'static str { "TODO" }
+
+            fn desc() -> Option<&'static str> { None }
         }
 
         impl ::microrm::schema::datum::Datum for #id_ident {
@@ -203,15 +236,21 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
                 Ok(Self(<i64 as ::microrm::schema::datum::Datum>::build_from(adata, stmt, index)?))
             }
 
-            fn accept_discriminator(d: &mut impl ::microrm::schema::DatumDiscriminator) where Self: Sized {
+            fn accept_discriminator(d: &mut impl ::microrm::schema::datum::DatumDiscriminator) where Self: Sized {
                 d.visit_entity_id::<#entity_ident>();
             }
+
+            fn accept_discriminator_ref(&self, d: &mut impl ::microrm::schema::datum::DatumDiscriminatorRef) where Self: Sized {
+                d.visit_entity_id::<#entity_ident>(self);
+            }
         }
 
+        impl ::microrm::schema::datum::ConcreteDatum for #id_ident {}
+
         impl ::std::fmt::Debug for #entity_ident {
             fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
                 use ::microrm::schema::datum::Datum;
-                let mut ds = f.debug_struct(#entity_name);
+                let mut ds = f.debug_struct(#entity_ident_str);
                 #(#debug_fields)*
                 ds.finish()
             }

+ 1 - 90
microrm-macros/src/lib.rs

@@ -3,7 +3,7 @@ use proc_macro::TokenStream;
 mod database;
 mod entity;
 
-#[proc_macro_derive(Entity, attributes(unique))]
+#[proc_macro_derive(Entity, attributes(unique, elide))]
 pub fn derive_entity(tokens: TokenStream) -> TokenStream {
     entity::derive(tokens)
 }
@@ -12,92 +12,3 @@ pub fn derive_entity(tokens: TokenStream) -> TokenStream {
 pub fn derive_database(tokens: TokenStream) -> TokenStream {
     database::derive(tokens)
 }
-
-/*
-
-mod entity;
-mod index;
-mod modelable;
-
-fn parse_microrm_ref(attrs: &[syn::Attribute]) -> proc_macro2::TokenStream {
-    for attr in attrs {
-        if attr.path.segments.is_empty() {
-            continue;
-        }
-
-        if attr.tokens.is_empty() && attr.path.segments.last().unwrap().ident == "microrm_internal"
-        {
-            return quote! { crate };
-        }
-    }
-
-    quote! { ::microrm }
-}
-
-/// Turns a serializable/deserializable struct into a microrm entity model.
-///
-/// There are two important visible effects:
-/// - Provides an implementation of `microrm::entity::Entity`
-/// - Defines a <struct-name>Columns enum
-///
-/// Note that names are converted from CamelCase to snake_case and vice versa
-/// where applicable, so a struct named `TestModel` is given a table name `test_model`
-/// and a struct field named `field_name` is given a variant name of `FieldName`.
-///
-/// The `#[microrm...]` attributes can be used to control the derivation somewhat.
-/// The following are understood for the Entity struct:
-/// - `#[microrm_internal]`: this is internal to the microrm crate (of extremely limited usefulness
-/// outside the microrm library)
-///
-/// The following are understood on individual fields:
-/// - `#[microrm_foreign]`: this is a foreign key (and the field must be of a type implementing `EntityID`)
-#[proc_macro_derive(Entity, attributes(microrm_internal, microrm_foreign))]
-pub fn derive_entity(tokens: TokenStream) -> TokenStream {
-    entity::derive(tokens)
-}
-
-/// Marks a struct or enum as able to be directly used in an Entity to correspond to a single database column.
-#[proc_macro_derive(Modelable, attributes(microrm_internal))]
-pub fn derive_modelable(tokens: TokenStream) -> TokenStream {
-    modelable::derive(tokens)
-}
-
-/// Defines a struct to represent a optionally-unique index on a table.
-///
-/// Suppose the following `Entity` definition is used:
-///
-/// ```ignore
-/// #[derive(Entity,Serialize,Deserialize)]
-/// struct SystemUser {
-///     username: String,
-///     hashed_password: String
-/// }
-/// ```
-///
-/// We can now use `make_index!` to define an index on the username field:
-/// ```ignore
-/// make_index!(SystemUsernameIndex, SystemUserColumns::Username)
-/// ```
-///
-/// This index can be made unique by adding a `!` prior to the type name, as:
-/// ```ignore
-/// make_index!(!SystemUsernameUniqueIndex, SystemUserColumns::Username)
-/// ```
-#[proc_macro]
-pub fn make_index(tokens: TokenStream) -> TokenStream {
-    index::do_make_index(tokens, quote! { ::microrm })
-}
-
-/// For internal use inside the microrm library. See `make_index`.
-#[proc_macro]
-pub fn make_index_internal(tokens: TokenStream) -> TokenStream {
-    index::do_make_index(tokens, quote! { crate })
-}
-
-/*#[proc_macro]
-pub fn query(tokens: TokenStream) -> TokenStream {
-    query::do_query(tokens)
-}
-*/
-
-*/

+ 29 - 29
microrm/src/db.rs

@@ -99,7 +99,7 @@ impl Connection {
     pub fn execute_raw_sql(&self, sql: impl AsRef<str>) -> DBResult<()> {
         let data = self.0.lock()?;
 
-        println!("executing: {sql}", sql = sql.as_ref());
+        log::trace!("executing raw sql: {sql}", sql = sql.as_ref());
 
         unsafe {
             let c_sql = CString::new(sql.as_ref())?;
@@ -188,12 +188,11 @@ pub(crate) struct Transaction<'l> {
 
 impl<'l> Transaction<'l> {
     pub fn new(db: &'l Connection) -> DBResult<Self> {
-        println!("backtrace: {}", std::backtrace::Backtrace::force_capture());
         db.execute_raw_sql("BEGIN TRANSACTION")?;
         /*struct BeginQuery;
         db.with_prepared(
             std::any::TypeId::of::<BeginQuery>(),
-            || "BEGIN".to_string(),
+            || "BEGIN TRANSACTION".to_string(),
             |ctx| {
                 ctx.run().map(|_| ())
             })?; */
@@ -247,8 +246,6 @@ impl Statement {
             check_rcode(|| None, sq::sqlite3_reset(self.stmt))?;
         }
 
-        let v = unsafe { CStr::from_ptr(sq::sqlite3_sql(self.stmt)).to_str().unwrap() };
-        println!("making Statement context for SQL: {}", v);
         Ok(StatementContext {
             stmt: self,
             owned_strings: Default::default(),
@@ -302,6 +299,7 @@ mod test {
                 let count = ctx
                     .iter()
                     .map(|row| {
+                        let row = row.unwrap();
                         assert_eq!(row.read::<i64>(0).expect("couldn't read row ID"), 1);
                         assert_eq!(
                             row.read::<String>(1).expect("couldn't read row value"),
@@ -343,25 +341,22 @@ impl<'a> StatementContext<'a> {
         self.owned_strings.push(s);
     }
 
-    fn step(&self) -> Option<()> {
+    fn step(&self) -> DBResult<bool> {
         match unsafe { sq::sqlite3_step(self.stmt.stmt) } {
-            sq::SQLITE_ROW => {
-                println!("sqlite3_step: row");
-                Some(())
-            }
-            sq::SQLITE_DONE => {
-                println!("sqlite3_step: done");
-                None
-            }
+            sq::SQLITE_ROW => Ok(true),
+            sq::SQLITE_DONE => Ok(false),
             sq::SQLITE_BUSY => {
-                println!("Concurrent database access!");
-                None
+                log::trace!("Concurrent database access!");
+                todo!()
+            }
+            sq::SQLITE_CONSTRAINT => {
+                log::trace!("SQLite constraint violation");
+                return Err(Error::LogicError("constraint violation"));
             }
             err => {
-                println!("unexpected error during sqlite3_step: {:?}", err);
-                // let _ = check_rcode(|| None, err);
-                // Ok(false)
-                None
+                log::trace!("unexpected error during sqlite3_step: {:?}", err);
+                check_rcode(|| None, err)?;
+                unreachable!()
             }
         }
     }
@@ -369,7 +364,7 @@ impl<'a> StatementContext<'a> {
     // this needs to be replaced with a "single" version that keeps the StatementContext alive, or
     // StatementRow needs an optional StatementContext to keep alive
     pub fn run(self) -> DBResult<Option<StatementRow<'a>>> {
-        if self.step().is_some() {
+        if self.step()? {
             Ok(Some(StatementRow {
                 stmt: self.stmt,
                 _ctx: Some(self),
@@ -379,17 +374,23 @@ impl<'a> StatementContext<'a> {
         }
     }
 
-    pub fn iter(self) -> impl Iterator<Item = StatementRow<'a>> {
+    pub fn iter(self) -> impl Iterator<Item = DBResult<StatementRow<'a>>> {
         struct I<'a>(StatementContext<'a>);
 
         impl<'a> Iterator for I<'a> {
-            type Item = StatementRow<'a>;
+            type Item = DBResult<StatementRow<'a>>;
 
             fn next(&mut self) -> Option<Self::Item> {
-                self.0.step().map(|_| StatementRow {
-                    _ctx: None,
-                    stmt: self.0.stmt,
-                })
+                // XXX: unwrap
+                match self.0.step() {
+                    Ok(true) => Some(Ok(StatementRow {
+                        _ctx: None,
+                        stmt: self.0.stmt,
+                    })),
+                    Ok(false) => None,
+                    Err(e) => Some(Err(e)),
+                }
+                /*map(|_| )*/
             }
         }
 
@@ -401,9 +402,8 @@ impl<'a> Drop for StatementContext<'a> {
     fn drop(&mut self) {
         // attempt to bind NULLs into each parameter
         unsafe {
-            println!("clearing bindings...");
             // clear out the rest of the rows
-            while self.step().is_some() {}
+            while self.step().is_ok_and(|v| v) {}
             sq::sqlite3_clear_bindings(self.stmt.stmt);
         }
     }

+ 1 - 1
microrm/src/lib.rs

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

+ 23 - 17
microrm/src/query.rs

@@ -387,8 +387,19 @@ pub trait AssocInterface: 'static {
 
         txn.commit()
     }
+}
+
+// ----------------------------------------------------------------------
+// New query interface
+// ----------------------------------------------------------------------
+
+pub trait Insertable<E: Entity> {
+    fn insert(&self, value: E) -> DBResult<E::ID>;
+    fn insert_and_return(&self, value: E) -> DBResult<Stored<E>>;
+}
 
-    fn insert(&self, value: Self::RemoteEntity) -> DBResult<<Self::RemoteEntity as Entity>::ID>
+impl<AI: AssocInterface> Insertable<AI::RemoteEntity> for AI {
+    fn insert(&self, value: AI::RemoteEntity) -> DBResult<<AI::RemoteEntity as Entity>::ID>
     where
         Self: Sized,
     {
@@ -404,14 +415,14 @@ pub trait AssocInterface: 'static {
         // so first, into the remote table
         let remote_id = insert(&adata.conn, &value)?;
         // then the association
-        do_connect::<Self::RemoteEntity>(adata, an, remote_id)?;
+        do_connect::<AI::RemoteEntity>(adata, an, remote_id)?;
 
         txn.commit()?;
 
         Ok(remote_id)
     }
 
-    fn insert_and_return(&self, value: Self::RemoteEntity) -> DBResult<Stored<Self::RemoteEntity>>
+    fn insert_and_return(&self, value: AI::RemoteEntity) -> DBResult<Stored<AI::RemoteEntity>>
     where
         Self: Sized,
     {
@@ -427,7 +438,7 @@ pub trait AssocInterface: 'static {
         // so first, into the remote table
         let remote = insert_and_return(&adata.conn, value)?;
         // then the association
-        do_connect::<Self::RemoteEntity>(adata, an, remote.id())?;
+        do_connect::<AI::RemoteEntity>(adata, an, remote.id())?;
 
         txn.commit()?;
 
@@ -435,11 +446,7 @@ pub trait AssocInterface: 'static {
     }
 }
 
-// ----------------------------------------------------------------------
-// New query interface
-// ----------------------------------------------------------------------
-
-pub trait OutputContainer: 'static {
+pub trait OutputContainer<T: Entity>: 'static + IntoIterator<Item = Stored<T>> {
     fn assemble_from(conn: &Connection, stmt: StatementContext<'_>) -> DBResult<Self>
     where
         Self: Sized;
@@ -451,7 +458,7 @@ fn assemble_single<T: Entity>(conn: &Connection, row: &mut StatementRow) -> Stor
     Stored::new(conn.clone(), T::ID::from_raw(id), T::build(datum_list))
 }
 
-impl<T: Entity> OutputContainer for Option<Stored<T>> {
+impl<T: Entity> OutputContainer<T> for Option<Stored<T>> {
     fn assemble_from(conn: &Connection, ctx: StatementContext<'_>) -> DBResult<Self>
     where
         Self: Sized,
@@ -460,21 +467,20 @@ impl<T: Entity> OutputContainer for Option<Stored<T>> {
     }
 }
 
-impl<T: Entity> OutputContainer for Vec<Stored<T>> {
+impl<T: Entity> OutputContainer<T> for Vec<Stored<T>> {
     fn assemble_from(conn: &Connection, ctx: StatementContext<'_>) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok(ctx
-            .iter()
-            .map(|mut r| assemble_single(conn, &mut r))
-            .collect())
+        ctx.iter()
+            .map(|r| r.map(|mut s| assemble_single(conn, &mut s)))
+            .collect::<Result<Vec<_>, Error>>()
     }
 }
 
-pub trait Queryable {
+pub trait Queryable: Clone {
     type EntityOutput: Entity;
-    type OutputContainer: OutputContainer;
+    type OutputContainer: OutputContainer<Self::EntityOutput>;
     type StaticVersion: Queryable + 'static;
 
     fn build(&self) -> Query;

+ 44 - 4
microrm/src/query/components.rs

@@ -5,15 +5,16 @@ use crate::{
     prelude::Queryable,
     query::{AssocInterface, QueryPart},
     schema::{
-        datum::{Datum, DatumVisitor, QueryEquivalent, QueryEquivalentList},
+        datum::{Datum, DatumDiscriminator, DatumVisitor, QueryEquivalent, QueryEquivalentList},
         entity::{Entity, EntityPart, EntityPartList, EntityPartVisitor},
-        DatumDiscriminator, Stored,
+        Stored,
     },
 };
 
 use super::Query;
 
 /// Filter on a Datum
+#[derive(Clone)]
 pub(crate) struct WithComponent<WEP: EntityPart, Parent: Queryable, QE: QueryEquivalent<WEP::Datum>>
 {
     datum: QE,
@@ -36,6 +37,7 @@ impl<WEP: EntityPart, Parent: Queryable, QE: QueryEquivalent<WEP::Datum>>
 /// this workaround is needed because we very explicitly would like EL to not be a
 /// 'static-restricted type, and it doesn't matter for the purposes of actually distinguishing
 /// between queries.
+#[derive(Clone)]
 pub(crate) struct CanonicalWithComponent<WEP: EntityPart, Parent: Queryable> {
     _ghost: std::marker::PhantomData<(WEP, Parent)>,
 }
@@ -58,8 +60,11 @@ impl<WEP: EntityPart, Parent: Queryable + 'static> Queryable
     }
 }
 
-impl<WEP: EntityPart, Parent: Queryable, QE: QueryEquivalent<WEP::Datum>> Queryable
-    for WithComponent<WEP, Parent, QE>
+impl<
+        Parent: Queryable,
+        WEP: EntityPart<Entity = Parent::EntityOutput>,
+        QE: QueryEquivalent<WEP::Datum>,
+    > Queryable for WithComponent<WEP, Parent, QE>
 {
     type EntityOutput = WEP::Entity;
     type OutputContainer = Parent::OutputContainer;
@@ -112,6 +117,21 @@ impl<
     }
 }
 
+impl<
+        E: Entity,
+        Parent: Queryable,
+        EL: QueryEquivalentList<<E::Uniques as EntityPartList>::DatumList>,
+    > Clone for UniqueComponent<E, Parent, EL>
+{
+    fn clone(&self) -> Self {
+        Self {
+            datum: self.datum.clone(),
+            parent: self.parent.clone(),
+            _ghost: Default::default(),
+        }
+    }
+}
+
 /// this workaround is needed because we very explicitly would like EL to not be a
 /// 'static-restricted type, and it doesn't matter for the purposes of actually distinguishing
 /// between queries.
@@ -135,6 +155,14 @@ impl<E: Entity, Parent: Queryable + 'static> Queryable for CanonicalUniqueCompon
     }
 }
 
+impl<E: Entity, Parent: Queryable + 'static> Clone for CanonicalUniqueComponent<E, Parent> {
+    fn clone(&self) -> Self {
+        Self {
+            _ghost: Default::default(),
+        }
+    }
+}
+
 impl<
         E: Entity,
         Parent: Queryable,
@@ -186,6 +214,7 @@ impl<
     }
 }
 
+#[derive(Clone)]
 pub(crate) struct SingleComponent<Parent: Queryable> {
     parent: Parent,
 }
@@ -234,6 +263,17 @@ impl<R: Entity, L: Entity, EP: EntityPart<Entity = L>, Parent: Queryable>
     }
 }
 
+impl<R: Entity, L: Entity, EP: EntityPart<Entity = L>, Parent: Queryable> Clone
+    for JoinComponent<R, L, EP, Parent>
+{
+    fn clone(&self) -> Self {
+        Self {
+            parent: self.parent.clone(),
+            _ghost: Default::default(),
+        }
+    }
+}
+
 impl<
         R: Entity,
         L: Entity,

+ 61 - 21
microrm/src/schema.rs

@@ -10,12 +10,14 @@ use query::Queryable;
 
 use crate::{
     db::{Connection, StatementContext, StatementRow, Transaction},
-    query::{self, AssocInterface},
-    schema::datum::Datum,
+    query::{self, AssocInterface, Insertable},
+    schema::datum::{Datum, DatumDiscriminator},
     schema::entity::{Entity, EntityVisitor},
 };
 use crate::{DBResult, Error};
 
+use self::datum::{ConcreteDatum, DatumDiscriminatorRef};
+
 pub mod datum;
 pub mod entity;
 
@@ -104,16 +106,6 @@ impl<T: Entity> PartialEq for Stored<T> {
 // Entity field types
 // ----------------------------------------------------------------------
 
-/// Visitor for allowing for type-specific behaviour
-pub trait DatumDiscriminator {
-    fn visit_entity_id<E: Entity>(&mut self);
-    fn visit_serialized<T: serde::Serialize + serde::de::DeserializeOwned>(&mut self);
-    fn visit_bare_field<T: Datum>(&mut self);
-    fn visit_assoc_map<E: Entity>(&mut self);
-    fn visit_assoc_domain<R: Relation>(&mut self);
-    fn visit_assoc_range<R: Relation>(&mut self);
-}
-
 /// Represents an arbitrary relation between two types of entities.
 ///
 /// In its most generic form, stores an arbitrary set of `(Domain::ID, Range::ID)` pairs used to
@@ -228,6 +220,13 @@ impl<T: Entity> Datum for AssocMap<T> {
         d.visit_assoc_map::<T>();
     }
 
+    fn accept_discriminator_ref(&self, d: &mut impl DatumDiscriminatorRef)
+    where
+        Self: Sized,
+    {
+        d.visit_assoc_map::<T>(self);
+    }
+
     fn bind_to(&self, _stmt: &mut StatementContext, _index: i32) {
         unreachable!()
     }
@@ -246,6 +245,7 @@ impl<T: Entity> Datum for AssocMap<T> {
         self.data = Some(adata);
     }
 }
+impl<T: Entity> ConcreteDatum for AssocMap<T> {}
 
 /// The domain part of a full Relation definition.
 pub struct AssocDomain<R: Relation> {
@@ -323,6 +323,13 @@ impl<R: Relation> Datum for AssocDomain<R> {
         d.visit_assoc_domain::<R>();
     }
 
+    fn accept_discriminator_ref(&self, d: &mut impl DatumDiscriminatorRef)
+    where
+        Self: Sized,
+    {
+        d.visit_assoc_domain(self);
+    }
+
     fn bind_to(&self, _stmt: &mut StatementContext, _index: i32) {
         unreachable!()
     }
@@ -341,6 +348,7 @@ impl<R: Relation> Datum for AssocDomain<R> {
         self.data = Some(adata);
     }
 }
+impl<R: Relation> ConcreteDatum for AssocDomain<R> {}
 
 /// The range part of a full Relation definition.
 pub struct AssocRange<R: Relation> {
@@ -418,6 +426,13 @@ impl<R: Relation> Datum for AssocRange<R> {
         d.visit_assoc_range::<R>();
     }
 
+    fn accept_discriminator_ref(&self, d: &mut impl DatumDiscriminatorRef)
+    where
+        Self: Sized,
+    {
+        d.visit_assoc_range(self);
+    }
+
     fn bind_to(&self, _stmt: &mut StatementContext, _index: i32) {
         unreachable!()
     }
@@ -436,13 +451,17 @@ impl<R: Relation> Datum for AssocRange<R> {
         self.data = Some(adata);
     }
 }
+impl<R: Relation> ConcreteDatum for AssocRange<R> {}
 
 /// Stores an arbitrary Rust data type as serialized JSON in a string field.
-pub struct Serialized<T: serde::Serialize + serde::de::DeserializeOwned> {
+#[derive(Clone)]
+pub struct Serialized<T: serde::Serialize + serde::de::DeserializeOwned + Clone> {
     wrapped: T,
 }
 
-impl<T: serde::Serialize + serde::de::DeserializeOwned + Default> Default for Serialized<T> {
+impl<T: serde::Serialize + serde::de::DeserializeOwned + Default + Clone> Default
+    for Serialized<T>
+{
     fn default() -> Self {
         Self {
             wrapped: T::default(),
@@ -450,7 +469,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned + Default> Default for Se
     }
 }
 
-impl<T: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug> std::fmt::Debug
+impl<T: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug + Clone> std::fmt::Debug
     for Serialized<T>
 {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -458,25 +477,25 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug> std::f
     }
 }
 
-impl<T: serde::Serialize + serde::de::DeserializeOwned> From<T> for Serialized<T> {
+impl<T: serde::Serialize + serde::de::DeserializeOwned + Clone> 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> {
+impl<T: serde::Serialize + serde::de::DeserializeOwned + Clone> AsRef<T> for Serialized<T> {
     fn as_ref(&self) -> &T {
         &self.wrapped
     }
 }
 
-impl<T: serde::Serialize + serde::de::DeserializeOwned> AsMut<T> for Serialized<T> {
+impl<T: serde::Serialize + serde::de::DeserializeOwned + Clone> AsMut<T> for Serialized<T> {
     fn as_mut(&mut self) -> &mut T {
         &mut self.wrapped
     }
 }
 
-impl<T: 'static + serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug> Datum
+impl<T: 'static + serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug + Clone> Datum
     for Serialized<T>
 {
     fn sql_type() -> &'static str {
@@ -504,6 +523,25 @@ impl<T: 'static + serde::Serialize + serde::de::DeserializeOwned + std::fmt::Deb
 
         Ok(Self { wrapped: d })
     }
+
+    fn accept_discriminator(d: &mut impl DatumDiscriminator)
+    where
+        Self: Sized,
+    {
+        d.visit_serialized::<T>();
+    }
+
+    fn accept_discriminator_ref(&self, d: &mut impl DatumDiscriminatorRef)
+    where
+        Self: Sized,
+    {
+        d.visit_serialized::<T>(&self.wrapped);
+    }
+}
+
+impl<T: 'static + serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug + Clone>
+    ConcreteDatum for Serialized<T>
+{
 }
 
 // ----------------------------------------------------------------------
@@ -532,16 +570,18 @@ impl<T: Entity> IDMap<T> {
     pub fn by_id(&self, id: T::ID) -> DBResult<Option<Stored<T>>> {
         self.with(id, &id).first().get()
     }
+}
 
+impl<'l, T: Entity> Insertable<T> for IDMap<T> {
     /// Insert a new Entity into this map, and return its new ID.
-    pub fn insert(&self, value: T) -> DBResult<T::ID> {
+    fn insert(&self, value: T) -> DBResult<T::ID> {
         let txn = Transaction::new(self.conn())?;
         let out = query::insert(self.conn(), &value)?;
         txn.commit()?;
         Ok(out)
     }
 
-    pub fn insert_and_return(&self, value: T) -> DBResult<Stored<T>> {
+    fn insert_and_return(&self, value: T) -> DBResult<Stored<T>> {
         let txn = Transaction::new(self.conn())?;
         let out = query::insert_and_return(self.conn(), value)?;
         txn.commit()?;

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

@@ -1,5 +1,5 @@
 use crate::{
-    query::Queryable,
+    prelude::*,
     schema::{
         collect::{EntityStateContainer, PartType},
         meta, Connection, Database, DatabaseItem, DatabaseItemVisitor,

+ 54 - 15
microrm/src/schema/datum.rs

@@ -1,6 +1,6 @@
 use crate::{
     db::{StatementContext, StatementRow},
-    schema::{AssocData, DatumDiscriminator, EntityVisitor},
+    schema::{AssocData, AssocDomain, AssocMap, AssocRange, Entity, EntityVisitor, Relation},
     DBResult,
 };
 
@@ -14,7 +14,7 @@ mod datum_list;
 // ----------------------------------------------------------------------
 
 /// Represents a data field in an Entity.
-pub trait Datum: std::fmt::Debug {
+pub trait Datum: Clone + std::fmt::Debug {
     fn sql_type() -> &'static str;
 
     fn debug_field(&self, field: &'static str, fmt: &mut std::fmt::DebugStruct)
@@ -25,9 +25,12 @@ pub trait Datum: std::fmt::Debug {
     }
 
     fn bind_to(&self, _stmt: &mut StatementContext, index: i32);
-    fn build_from(adata: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
+    fn build_from(_adata: AssocData, _stmt: &mut StatementRow, _index: &mut i32) -> DBResult<Self>
     where
-        Self: Sized;
+        Self: Sized,
+    {
+        unreachable!()
+    }
     fn update_adata(&mut self, _adata: AssocData) {}
 
     fn accept_entity_visitor(_: &mut impl EntityVisitor) {}
@@ -37,19 +40,47 @@ pub trait Datum: std::fmt::Debug {
     {
         d.visit_bare_field::<Self>();
     }
+
+    fn accept_discriminator_ref(&self, d: &mut impl DatumDiscriminatorRef)
+    where
+        Self: Sized,
+    {
+        d.visit_bare_field::<Self>(self);
+    }
+}
+
+/// marker trait for 'concrete' types, i.e. those with a static lifetime.
+pub trait ConcreteDatum: 'static + Datum {}
+
+/// Visitor for allowing for type-specific behaviour
+pub trait DatumDiscriminator {
+    fn visit_entity_id<E: Entity>(&mut self);
+    fn visit_serialized<T: serde::Serialize + serde::de::DeserializeOwned>(&mut self);
+    fn visit_bare_field<T: Datum>(&mut self);
+    fn visit_assoc_map<E: Entity>(&mut self);
+    fn visit_assoc_domain<R: Relation>(&mut self);
+    fn visit_assoc_range<R: Relation>(&mut self);
+}
+
+pub trait DatumDiscriminatorRef {
+    fn visit_entity_id<E: Entity>(&mut self, _: &E::ID);
+    fn visit_serialized<T: serde::Serialize + serde::de::DeserializeOwned>(&mut self, _: &T);
+    fn visit_bare_field<T: Datum>(&mut self, _: &T);
+    fn visit_assoc_map<E: Entity>(&mut self, _: &AssocMap<E>);
+    fn visit_assoc_domain<R: Relation>(&mut self, _: &AssocDomain<R>);
+    fn visit_assoc_range<R: Relation>(&mut self, _: &AssocRange<R>);
 }
 
 /// A fixed-length list of EntityDatums, usually a tuple.
-pub trait DatumList {
-    type Ref<'a>: DatumListRef
-    where
-        Self: 'a;
+pub trait DatumList: Clone {
     fn accept(&self, visitor: &mut impl DatumVisitor);
+
+    const LEN: usize;
 }
 
-/// A DatumList that is passed by reference instead of by value.
-pub trait DatumListRef {
-    fn accept(&self, visitor: &mut impl DatumVisitor);
+/// A list of concrete datums.
+pub trait ConcreteDatumList: DatumList + Clone {
+    fn build_equivalent<'l>(from: &'l [&'l str]) -> Option<impl QueryEquivalentList<Self> + 'l>;
 }
 
 /// A walker for a DatumList instance.
@@ -57,15 +88,23 @@ pub trait DatumVisitor {
     fn visit<ED: Datum>(&mut self, datum: &ED);
 }
 
-/// A mapping between datums where one can stand in for the other during queries.
+/// A forward-edge mapping between datums where one can stand in for the other during queries.
 ///
 /// This is purely a marker trait, since the equivalence is based on how sqlite handles things.
-pub trait QueryEquivalent<T: Datum>: Datum {}
+/// Because this is a forward-edge mapping, if type X implements QueryEquivalent<Y>, it means X can
+/// stand in for Y. For example, the following should be true:
+/// - for datum T, &T should implement QueryEquivalent<T>
+/// - for datum T, StringQuery should implement QueryEquivalent<T>
+pub trait QueryEquivalent<T: ConcreteDatum>: Datum {}
 
 // Every type is query-equivalent to itself.
-impl<T: Datum> QueryEquivalent<T> for T {}
+impl<T: ConcreteDatum> QueryEquivalent<T> for T {}
 // Every reference type is query-equivalent to the non-referenced type.
-impl<'l, T: Datum> QueryEquivalent<T> for &'l T {}
+impl<'l, T: ConcreteDatum> QueryEquivalent<T> for &'l T {}
+
+/// Used if you wish to use sqlite's built-in string equivalence for a query.
+#[derive(Clone, Debug)]
+pub struct StringQuery<'l>(pub &'l str);
 
 /// A list version of `QueryEquivalent`.
 pub trait QueryEquivalentList<T: DatumList>: DatumList {}

+ 29 - 19
microrm/src/schema/datum/datum_common.rs

@@ -4,8 +4,24 @@ use crate::{
     DBResult, Error,
 };
 
-use super::QueryEquivalent;
+use super::{
+    ConcreteDatum, DatumDiscriminator, DatumDiscriminatorRef, QueryEquivalent, StringQuery,
+};
 
+impl<'l> Datum for StringQuery<'l> {
+    fn sql_type() -> &'static str {
+        "text"
+    }
+    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
+        self.0.bind_to(stmt, index)
+    }
+}
+
+// a StringQuery and any datum are equivalent for the purposes of a query
+// impl<'l, 'b, T: ConcreteDatum> QueryEquivalent<StringQuery<'l>> for &'b T {}
+impl<'l, T: ConcreteDatum> QueryEquivalent<T> for StringQuery<'l> {}
+
+impl ConcreteDatum for time::OffsetDateTime {}
 impl Datum for time::OffsetDateTime {
     fn sql_type() -> &'static str {
         "text"
@@ -25,6 +41,7 @@ impl Datum for time::OffsetDateTime {
     }
 }
 
+impl ConcreteDatum for String {}
 impl Datum for String {
     fn sql_type() -> &'static str {
         "text"
@@ -54,18 +71,12 @@ impl<'l> Datum for &'l str {
         stmt.bind(index, *self)
             .expect("couldn't bind string reference");
     }
-
-    fn build_from(_adata: AssocData, _stmt: &mut StatementRow, _index: &mut i32) -> DBResult<Self>
-    where
-        Self: Sized,
-    {
-        unreachable!()
-    }
 }
 
 // a str reference and an owned string are equivalent for the purposes of a query
 impl<'l> QueryEquivalent<String> for &'l str {}
 
+impl ConcreteDatum for usize {}
 impl Datum for usize {
     fn sql_type() -> &'static str {
         "int"
@@ -84,6 +95,7 @@ impl Datum for usize {
     }
 }
 
+impl ConcreteDatum for isize {}
 impl Datum for isize {
     fn sql_type() -> &'static str {
         "int"
@@ -101,6 +113,7 @@ impl Datum for isize {
     }
 }
 
+impl ConcreteDatum for u64 {}
 impl Datum for u64 {
     fn sql_type() -> &'static str {
         "int"
@@ -118,6 +131,7 @@ impl Datum for u64 {
     }
 }
 
+impl ConcreteDatum for i64 {}
 impl Datum for i64 {
     fn sql_type() -> &'static str {
         "int"
@@ -137,6 +151,7 @@ impl Datum for i64 {
     }
 }
 
+impl<T: 'static + Datum> ConcreteDatum for Option<T> {}
 impl<T: Datum> Datum for Option<T> {
     fn sql_type() -> &'static str {
         T::sql_type()
@@ -166,6 +181,7 @@ impl<T: Datum> Datum for Option<T> {
     }
 }
 
+impl ConcreteDatum for bool {}
 impl Datum for bool {
     fn sql_type() -> &'static str {
         "int"
@@ -184,6 +200,7 @@ impl Datum for bool {
     }
 }
 
+impl ConcreteDatum for Vec<u8> {}
 impl Datum for Vec<u8> {
     fn sql_type() -> &'static str {
         "blob"
@@ -212,13 +229,6 @@ impl<'l> Datum for &'l [u8] {
     fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
         stmt.bind(index, *self).expect("couldn't bind byte slice");
     }
-
-    fn build_from(_adata: AssocData, _stmt: &mut StatementRow, _index: &mut i32) -> DBResult<Self>
-    where
-        Self: Sized,
-    {
-        unreachable!()
-    }
 }
 
 // a byte slice and an owned byte slice are equivalent for the purposes of a query
@@ -233,18 +243,18 @@ impl<'l, T: Datum> Datum for &'l T {
         T::bind_to(self, stmt, index)
     }
 
-    fn build_from(_adata: AssocData, _stmt: &mut StatementRow, _index: &mut i32) -> DBResult<Self>
+    fn accept_discriminator(d: &mut impl DatumDiscriminator)
     where
         Self: Sized,
     {
-        unreachable!()
+        T::accept_discriminator(d)
     }
 
-    fn accept_discriminator(d: &mut impl crate::schema::DatumDiscriminator)
+    fn accept_discriminator_ref(&self, d: &mut impl DatumDiscriminatorRef)
     where
         Self: Sized,
     {
-        T::accept_discriminator(d)
+        T::accept_discriminator_ref(self, d)
     }
 
     fn accept_entity_visitor(v: &mut impl crate::schema::entity::EntityVisitor) {

+ 52 - 40
microrm/src/schema/datum/datum_list.rs

@@ -1,77 +1,89 @@
-use super::{Datum, DatumList, DatumListRef, DatumVisitor, QueryEquivalent, QueryEquivalentList};
+use super::{
+    ConcreteDatum, ConcreteDatumList, Datum, DatumList, DatumVisitor, QueryEquivalent,
+    QueryEquivalentList, StringQuery,
+};
 
 impl DatumList for () {
-    type Ref<'a> = &'a ();
     fn accept(&self, _: &mut impl DatumVisitor) {}
-}
 
-impl<'l> DatumListRef for &'l () {
-    fn accept(&self, _: &mut impl DatumVisitor) {}
+    const LEN: usize = 0;
 }
 
-impl<T: Datum> DatumList for T {
-    type Ref<'a> = &'a T where Self: 'a;
+impl QueryEquivalentList<()> for () {}
 
+impl<T: Datum> DatumList for T {
     fn accept(&self, visitor: &mut impl DatumVisitor) {
         visitor.visit(self);
     }
+
+    const LEN: usize = 1;
 }
 
-impl<'l, T: Datum> DatumListRef for &'l T {
-    fn accept(&self, visitor: &mut impl DatumVisitor) {
-        visitor.visit(self);
+impl<T: ConcreteDatum> ConcreteDatumList for T {
+    fn build_equivalent<'l>(from: &'l [&'l str]) -> Option<impl QueryEquivalentList<Self> + 'l> {
+        if from.len() != 1 {
+            None
+        } else {
+            Some(StringQuery(from[0]))
+        }
     }
 }
 
-impl<T: Datum, E: QueryEquivalent<T>> QueryEquivalentList<T> for E {}
+impl<T: ConcreteDatum, E: QueryEquivalent<T>> QueryEquivalentList<T> for E {}
 
 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<T0: Datum, E0: QueryEquivalent<T0>> QueryEquivalentList<(T0,)> for (E0,) {}
+    const LEN: usize = 1;
+}
 
-impl<'a, T0: Datum> DatumListRef for (&'a T0,) {
-    fn accept(&self, visitor: &mut impl DatumVisitor) {
-        visitor.visit(&self.0);
+impl<T0: ConcreteDatum> ConcreteDatumList for (T0,) {
+    fn build_equivalent<'l>(from: &'l [&'l str]) -> Option<impl QueryEquivalentList<Self> + 'l> {
+        if from.len() != 1 {
+            None
+        } else {
+            Some((StringQuery(from[0]),))
+        }
     }
 }
 
+impl<T0: ConcreteDatum, E0: QueryEquivalent<T0>> QueryEquivalentList<(T0,)> for (E0,) {}
+
 macro_rules! datum_list {
-    ($($ty:ident : $e:ident : $n:tt),+) => {
+    ($len:literal, $($ty:ident : $e: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));*
             }
+            const LEN: usize = $len;
         }
 
-        impl<'l, $($ty: Datum),*> DatumListRef for ($(&'l $ty),*) {
-            fn accept(&self, visitor: &mut impl DatumVisitor) {
-                $(visitor.visit(&self. $n));*
+        impl<$( $ty: ConcreteDatum, $e: QueryEquivalent<$ty> ),*> QueryEquivalentList<( $( $ty ),* )> for ( $( $e ),* ) {}
+
+        impl<$( $ty: ConcreteDatum ),*> ConcreteDatumList for ($($ty),*) {
+            fn build_equivalent<'l>(from: &'l [&'l str]) -> Option<impl QueryEquivalentList<Self> + 'l> {
+                Some((
+                        $( StringQuery( from.get($n)? ) ),*
+                ))
             }
         }
-
-        impl<$( $ty: Datum, $e: QueryEquivalent<$ty> ),*> QueryEquivalentList<( $( $ty ),* )> for ( $( $e ),* ) {}
     }
 }
 
-datum_list!(T0:E0:0, T1:E1:1);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9, T10:E10:10);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9, T10:E10:10, T11:E11:11);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9, T10:E10:10, T11:E11:11, T12:E12:12);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9, T10:E10:10, T11:E11:11, T12:E12:12, T13:E13:13);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9, T10:E10:10, T11:E11:11, T12:E12:12, T13:E13:13, T14:E14:14);
-datum_list!(T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9, T10:E10:10, T11:E11:11, T12:E12:12, T13:E13:13, T14:E14:14, T15:E15:15);
+datum_list!(2, T0:E0:0, T1:E1:1);
+datum_list!(3, T0:E0:0, T1:E1:1, T2:E2:2);
+datum_list!(4, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3);
+datum_list!(5, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4);
+datum_list!(6, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5);
+datum_list!(7, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6);
+datum_list!(8, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7);
+datum_list!(9, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8);
+datum_list!(10, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9);
+datum_list!(11, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9, T10:E10:10);
+datum_list!(12, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9, T10:E10:10, T11:E11:11);
+datum_list!(13, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9, T10:E10:10, T11:E11:11, T12:E12:12);
+datum_list!(14, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9, T10:E10:10, T11:E11:11, T12:E12:12, T13:E13:13);
+datum_list!(15, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9, T10:E10:10, T11:E11:11, T12:E12:12, T13:E13:13, T14:E14:14);
+datum_list!(16, T0:E0:0, T1:E1:1, T2:E2:2, T3:E3:3, T4:E4:4, T5:E5:5, T6:E6:6, T7:E7:7, T8:E8:8, T9:E9:9, T10:E10:10, T11:E11:11, T12:E12:12, T13:E13:13, T14:E14:14, T15:E15:15);

+ 8 - 5
microrm/src/schema/entity.rs

@@ -6,10 +6,12 @@ use crate::{
     DBResult,
 };
 
+use super::datum::{ConcreteDatum, QueryEquivalentList};
+
 pub(crate) mod helpers;
 
 /// Integral identifier for an entity.
-pub trait EntityID: 'static + PartialEq + Hash + PartialOrd + Debug + Copy + Datum {
+pub trait EntityID: 'static + PartialEq + Hash + PartialOrd + Debug + Copy + ConcreteDatum {
     type Entity: Entity<ID = Self>;
 
     /// Construct from a raw integer.
@@ -24,13 +26,14 @@ pub trait EntityID: 'static + PartialEq + Hash + PartialOrd + Debug + Copy + Dat
 // ----------------------------------------------------------------------
 
 /// A single data field in an Entity, automatically declared and derived as part of `#[derive(Entity)]`.
-pub trait EntityPart: 'static {
-    type Datum: Datum;
+pub trait EntityPart: Default + Clone + 'static {
+    type Datum: ConcreteDatum;
     type Entity: Entity;
 
     fn part_name() -> &'static str;
     fn placeholder() -> &'static str;
     fn unique() -> bool;
+    fn desc() -> Option<&'static str>;
 }
 
 /// Visitor for traversing all `EntityPart`s in an `Entity` or `EntityPartList`.
@@ -42,7 +45,7 @@ pub trait EntityPartVisitor {
 
 /// List of EntityParts.
 pub trait EntityPartList: 'static {
-    type DatumList: DatumList;
+    type DatumList: DatumList + QueryEquivalentList<Self::DatumList>;
 
     fn build_datum_list(conn: &Connection, stmt: &mut StatementRow) -> DBResult<Self::DatumList>;
 
@@ -58,7 +61,7 @@ mod part_list;
 // ----------------------------------------------------------------------
 
 /// A single database entity, aka an object type that gets its own table.
-pub trait Entity: 'static {
+pub trait Entity: 'static + std::fmt::Debug {
     type Parts: EntityPartList;
     type Uniques: EntityPartList;
     type ID: EntityID<Entity = Self> + EntityPart<Datum = Self::ID, Entity = Self>;

+ 2 - 0
microrm/src/schema/meta.rs

@@ -2,8 +2,10 @@ use crate::schema::IDMap;
 
 #[derive(microrm_macros::Entity)]
 pub struct Meta {
+    /// Metadata key-value key
     #[unique]
     pub key: String,
+    /// Metadata key-value value
     pub value: String,
 }
 

+ 100 - 21
microrm/src/schema/tests.rs

@@ -11,18 +11,19 @@ fn open_test_db<DB: super::Database>(identifier: &'static str) -> DB {
 mod manual_test_db {
     // simple hand-built database example
     use crate::db::{Connection, StatementContext, StatementRow};
-    use crate::schema::datum::Datum;
+    use crate::schema::datum::{ConcreteDatum, Datum};
     use crate::schema::entity::{
         Entity, EntityID, EntityPart, EntityPartList, EntityPartVisitor, EntityVisitor,
     };
     use crate::schema::{Database, DatabaseItem, DatabaseItemVisitor, IDMap};
     use test_log::test;
 
+    #[derive(Debug)]
     struct SimpleEntity {
         name: String,
     }
 
-    #[derive(Clone, Copy, PartialEq, PartialOrd, Hash, Debug)]
+    #[derive(Clone, Copy, Default, PartialEq, PartialOrd, Hash, Debug)]
     struct SimpleEntityID(i64);
 
     impl Datum for SimpleEntityID {
@@ -51,6 +52,7 @@ mod manual_test_db {
             todo!()
         }
     }
+    impl ConcreteDatum for SimpleEntityID {}
 
     impl EntityID for SimpleEntityID {
         type Entity = SimpleEntity;
@@ -76,8 +78,12 @@ mod manual_test_db {
         fn placeholder() -> &'static str {
             "simple_entity_id"
         }
+        fn desc() -> Option<&'static str> {
+            None
+        }
     }
 
+    #[derive(Clone, Default)]
     struct SimpleEntityName;
     impl EntityPart for SimpleEntityName {
         type Datum = String;
@@ -91,6 +97,9 @@ mod manual_test_db {
         fn unique() -> bool {
             true
         }
+        fn desc() -> Option<&'static str> {
+            None
+        }
     }
 
     impl Entity for SimpleEntity {
@@ -175,7 +184,7 @@ mod manual_test_db {
 mod derive_tests {
     #![allow(unused)]
 
-    use crate::query::{AssocInterface, Queryable};
+    use crate::prelude::*;
     use crate::schema::{AssocMap, Database, IDMap};
     use microrm_macros::{Database, Entity};
     use test_log::test;
@@ -347,7 +356,7 @@ mod derive_tests {
 
 mod mutual_relationship {
     use super::open_test_db;
-    use crate::query::{AssocInterface, Queryable};
+    use crate::prelude::*;
     use crate::schema::{AssocDomain, AssocMap, AssocRange, Database, IDMap};
     use microrm_macros::{Database, Entity};
     use test_log::test;
@@ -637,78 +646,148 @@ mod query_equivalence {
     use crate::prelude::*;
     use test_log::test;
     #[derive(Entity)]
-    struct Item {
+    struct SingleItem {
         #[unique]
         s: String,
         v: Vec<u8>,
     }
 
+    #[derive(Entity)]
+    struct DoubleItem {
+        #[unique]
+        s: String,
+        #[unique]
+        t: String,
+        v: usize,
+    }
+
     #[derive(Database)]
     struct ItemDB {
-        items: IDMap<Item>,
+        single_items: IDMap<SingleItem>,
+        double_items: IDMap<DoubleItem>,
     }
 
     #[test]
-    fn unique_test() {
+    fn single_test() {
         let db = ItemDB::open_path(":memory:").expect("couldn't open test db");
 
-        db.items
-            .insert(Item {
+        db.single_items
+            .insert(SingleItem {
                 s: "string 1".into(),
                 v: vec![0u8, 1u8, 2u8],
             })
             .expect("couldn't insert test item");
 
-        db.items
-            .insert(Item {
+        db.single_items
+            .insert(SingleItem {
                 s: "string 2".into(),
                 v: vec![0u8, 1u8, 2u8],
             })
             .expect("couldn't insert test item");
 
         assert!(db
-            .items
+            .single_items
             .unique("string 2")
             .get()
             .expect("couldn't query db")
             .is_some());
         assert!(db
-            .items
+            .single_items
             .unique("string 3")
             .get()
             .expect("couldn't query db")
             .is_none());
+
+        assert!(db
+            .single_items
+            .unique(String::from("string 2"))
+            .get()
+            .expect("couldn't query db")
+            .is_some());
+        assert!(db
+            .single_items
+            .unique(String::from("string 3"))
+            .get()
+            .expect("couldn't query db")
+            .is_none());
+    }
+
+    #[test]
+    fn double_test() {
+        let db = ItemDB::open_path(":memory:").expect("couldn't open test db");
+
+        db.double_items
+            .insert(DoubleItem {
+                s: "string 1".into(),
+                t: "second string 1".into(),
+                v: 42,
+            })
+            .expect("couldn't insert test item");
+
+        db.double_items
+            .insert(DoubleItem {
+                s: "string 2".into(),
+                t: "second string 2".into(),
+                v: 6,
+            })
+            .expect("couldn't insert test item");
+
+        assert!(db
+            .double_items
+            .unique(("string 2", "second string 2"))
+            .get()
+            .expect("couldn't query db")
+            .is_some());
+        assert!(db
+            .double_items
+            .unique(("string 3", "second string 3"))
+            .get()
+            .expect("couldn't query db")
+            .is_none());
+
+        assert!(db
+            .double_items
+            .unique((String::from("string 2"), String::from("second string 2")))
+            .get()
+            .expect("couldn't query db")
+            .is_some());
+        assert!(db
+            .double_items
+            .unique((String::from("string 3"), String::from("second string 3")))
+            .get()
+            .expect("couldn't query db")
+            .is_none());
     }
 
     #[test]
     fn with_test() {
         let db = ItemDB::open_path(":memory:").expect("couldn't open test db");
 
-        db.items
-            .insert(Item {
+        db.single_items
+            .insert(SingleItem {
                 s: "string 1".into(),
                 v: vec![0u8, 1u8, 2u8],
             })
             .expect("couldn't insert test item");
 
-        db.items
-            .insert(Item {
+        db.single_items
+            .insert(SingleItem {
                 s: "string 2".into(),
                 v: vec![0u8, 1u8, 2u8],
             })
             .expect("couldn't insert test item");
 
         assert_eq!(
-            db.items
-                .with(Item::V, [0u8].as_slice())
+            db.single_items
+                .with(SingleItem::V, [0u8].as_slice())
                 .get()
                 .expect("couldn't query db")
                 .len(),
             0
         );
         assert_eq!(
-            db.items
-                .with(Item::V, [0u8, 1, 2].as_slice())
+            db.single_items
+                .with(SingleItem::V, [0u8, 1, 2].as_slice())
                 .get()
                 .expect("couldn't query db")
                 .len(),