Browse Source

Implemented datum query-equivalence for smoother query construction.

Kestrel 9 months ago
parent
commit
8238ccea5b

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

@@ -145,7 +145,7 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
             #(#part_names)*
         }
 
-        #[derive(Clone, Copy, PartialEq, PartialOrd, Debug, Hash)]
+        #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
         #vis struct #id_ident (i64);
 
         impl ::microrm::schema::entity::EntityID for #id_ident {

+ 65 - 14
microrm/src/db.rs

@@ -3,6 +3,7 @@ use libsqlite3_sys as sq;
 use std::{
     collections::HashMap,
     ffi::{CStr, CString},
+    pin::Pin,
     sync::{Arc, Mutex},
 };
 
@@ -184,7 +185,10 @@ impl Statement {
         unsafe {
             check_rcode(None, sq::sqlite3_reset(self.stmt))?;
         }
-        Ok(StatementContext { stmt: self })
+        Ok(StatementContext {
+            stmt: self,
+            owned_strings: Default::default(),
+        })
     }
 }
 
@@ -252,6 +256,7 @@ mod test {
 
 pub struct StatementRow<'a> {
     stmt: &'a Statement,
+    owned: Option<Vec<Pin<Box<String>>>>,
 }
 
 impl<'a> StatementRow<'a> {
@@ -262,6 +267,7 @@ impl<'a> StatementRow<'a> {
 
 pub struct StatementContext<'a> {
     stmt: &'a Statement,
+    owned_strings: Vec<Pin<Box<String>>>,
 }
 
 impl<'a> StatementContext<'a> {
@@ -269,6 +275,10 @@ impl<'a> StatementContext<'a> {
         bindable.bind_to(self, index)
     }
 
+    pub fn transfer(&mut self, s: Pin<Box<String>>) {
+        self.owned_strings.push(s);
+    }
+
     fn step(&self) -> Option<()> {
         match unsafe { sq::sqlite3_step(self.stmt.stmt) } {
             sq::SQLITE_ROW => Some(()),
@@ -281,9 +291,14 @@ impl<'a> StatementContext<'a> {
         }
     }
 
-    pub fn run(self) -> DBResult<Option<StatementRow<'a>>> {
+    pub fn run(mut self) -> DBResult<Option<StatementRow<'a>>> {
         if self.step().is_some() {
-            Ok(Some(StatementRow { stmt: self.stmt }))
+            let mut owned = vec![];
+            owned.append(&mut self.owned_strings);
+            Ok(Some(StatementRow {
+                owned: Some(owned),
+                stmt: self.stmt,
+            }))
         } else {
             Ok(None)
         }
@@ -296,7 +311,10 @@ impl<'a> StatementContext<'a> {
             type Item = StatementRow<'a>;
 
             fn next(&mut self) -> Option<Self::Item> {
-                self.0.step().map(|_| StatementRow { stmt: self.0.stmt })
+                self.0.step().map(|_| StatementRow {
+                    owned: None,
+                    stmt: self.0.stmt,
+                })
             }
         }
 
@@ -314,41 +332,61 @@ impl<'a> Drop for StatementContext<'a> {
 }
 
 pub trait Bindable {
-    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()>;
+    fn bind_to<'ctx, 'data: 'ctx>(
+        &'data self,
+        ctx: &StatementContext<'ctx>,
+        index: i32,
+    ) -> DBResult<()>;
 }
 
 impl Bindable for () {
-    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+    fn bind_to<'ctx, 'data: 'ctx>(
+        &'data self,
+        ctx: &StatementContext<'ctx>,
+        index: i32,
+    ) -> DBResult<()> {
         unsafe { check_rcode(None, sq::sqlite3_bind_null(ctx.stmt.stmt, index)) }
     }
 }
 
 impl Bindable for i64 {
-    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+    fn bind_to<'ctx, 'data: 'ctx>(
+        &'data self,
+        ctx: &StatementContext<'ctx>,
+        index: i32,
+    ) -> DBResult<()> {
         unsafe { check_rcode(None, sq::sqlite3_bind_int64(ctx.stmt.stmt, index, *self)) }
     }
 }
 
 impl Bindable for usize {
-    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+    fn bind_to<'ctx, 'data: 'ctx>(
+        &'data self,
+        ctx: &StatementContext<'ctx>,
+        index: i32,
+    ) -> DBResult<()> {
         (*self as i64).bind_to(ctx, index)
     }
 }
 
 impl Bindable for f32 {
-    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+    fn bind_to<'ctx, 'data: 'ctx>(&self, ctx: &StatementContext<'ctx>, index: i32) -> DBResult<()> {
         (*self as f64).bind_to(ctx, index)
     }
 }
 
 impl Bindable for f64 {
-    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+    fn bind_to<'ctx, 'data: 'ctx>(&self, ctx: &StatementContext<'ctx>, index: i32) -> DBResult<()> {
         unsafe { check_rcode(None, sq::sqlite3_bind_double(ctx.stmt.stmt, index, *self)) }
     }
 }
 
 impl<'a> Bindable for &'a str {
-    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+    fn bind_to<'ctx, 'data: 'ctx>(
+        &'data self,
+        ctx: &StatementContext<'ctx>,
+        index: i32,
+    ) -> DBResult<()> {
         unsafe {
             check_rcode(
                 None,
@@ -365,19 +403,31 @@ impl<'a> Bindable for &'a str {
 }
 
 impl Bindable for str {
-    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+    fn bind_to<'ctx, 'data: 'ctx>(
+        &'data self,
+        ctx: &StatementContext<'ctx>,
+        index: i32,
+    ) -> DBResult<()> {
         <&'_ str>::bind_to(&self, ctx, index)
     }
 }
 
 impl Bindable for String {
-    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+    fn bind_to<'ctx, 'data: 'ctx>(
+        &'data self,
+        ctx: &StatementContext<'ctx>,
+        index: i32,
+    ) -> DBResult<()> {
         self.as_str().bind_to(ctx, index)
     }
 }
 
 impl<'a> Bindable for &'a [u8] {
-    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+    fn bind_to<'ctx, 'data: 'ctx>(
+        &'data self,
+        ctx: &StatementContext<'ctx>,
+        index: i32,
+    ) -> DBResult<()> {
         unsafe {
             check_rcode(
                 None,
@@ -430,6 +480,7 @@ impl Readable for String {
                     "NULL pointer result from sqlite3_column_text",
                 ))
             } else {
+                let cstr = CStr::from_ptr(text.cast());
                 Ok(CStr::from_ptr(text.cast()).to_str()?.to_string())
             }
         }

+ 51 - 46
microrm/src/query.rs

@@ -1,4 +1,6 @@
 use crate::db::{Connection, StatementContext, StatementRow};
+use crate::prelude::IDMap;
+use crate::schema::datum::{QueryEquivalent, QueryEquivalentList};
 use crate::schema::entity::helpers::check_assoc;
 use crate::schema::{AssocData, IDWrap, LocalSide};
 use crate::{
@@ -453,7 +455,9 @@ pub trait Queryable {
     /// Filter using the unique index on the entity.
     fn unique(
         self,
-        values: <<<Self::EntityOutput as Entity>::Uniques as EntityPartList>::DatumList as DatumList>::Ref<'_>,
+        values: impl QueryEquivalentList<
+            <<Self::EntityOutput as Entity>::Uniques as EntityPartList>::DatumList,
+        >,
     ) -> impl Queryable<
         EntityOutput = Self::EntityOutput,
         OutputContainer = Option<IDWrap<Self::EntityOutput>>,
@@ -467,7 +471,7 @@ pub trait Queryable {
     fn with<EP: EntityPart<Entity = Self::EntityOutput>>(
         self,
         part: EP,
-        value: &EP::Datum,
+        value: impl QueryEquivalent<EP::Datum>,
     ) -> impl Queryable<EntityOutput = Self::EntityOutput, OutputContainer = Self::OutputContainer>
     where
         Self: Sized,
@@ -492,7 +496,7 @@ pub trait Queryable {
     // Association-following and joining methods
     // ----------------------------------------------------------------------
     /// Join based on an existing association
-    fn join<AD: AssocInterface, EP: EntityPart<Entity = Self::EntityOutput, Datum = AD>>(
+    fn join<AD: AssocInterface + Datum, EP: EntityPart<Entity = Self::EntityOutput, Datum = AD>>(
         self,
         part: EP,
     ) -> impl Queryable<EntityOutput = AD::RemoteEntity, OutputContainer = Vec<IDWrap<AD::RemoteEntity>>>
@@ -503,6 +507,25 @@ pub trait Queryable {
     }
 }
 
+// Generic implementation for all IDMaps
+impl<'a, T: Entity> Queryable for &'a IDMap<T> {
+    type EntityOutput = T;
+    type OutputContainer = Vec<IDWrap<T>>;
+    type StaticVersion = &'static IDMap<T>;
+
+    fn build(&self) -> Query {
+        Query::new()
+            .attach(QueryPart::Root, "SELECT DISTINCT".into())
+            .attach(QueryPart::Columns, "*".into())
+            .attach(QueryPart::From, format!("`{}`", T::entity_name()))
+    }
+    fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {}
+
+    fn conn(&self) -> &Connection {
+        &self.conn
+    }
+}
+
 // Generic implementation for all assoc specification types
 impl<'a, AI: AssocInterface> Queryable for &'a AI {
     type EntityOutput = AI::RemoteEntity;
@@ -510,55 +533,37 @@ impl<'a, AI: AssocInterface> Queryable for &'a AI {
     type StaticVersion = &'static AI;
 
     fn build(&self) -> Query {
-        unreachable!()
+        let anames = AssocNames::collect(*self).unwrap();
+        let assoc_name = anames.assoc_name();
+        Query::new()
+            .attach(QueryPart::Root, "SELECT DISTINCT".into())
+            .attach(QueryPart::Columns, format!("`{}`.*", anames.remote_name))
+            .attach(QueryPart::From, format!("`{}`", assoc_name))
+            .attach(
+                QueryPart::Join,
+                format!(
+                    "`{}` ON `{}`.`id` = `{}`.`{}`",
+                    anames.remote_name, anames.remote_name, assoc_name, anames.remote_field
+                ),
+            )
+            .attach(
+                QueryPart::Where,
+                format!("`{}`.`{}` = ?", assoc_name, anames.local_field),
+            )
     }
-
-    fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {
-        unreachable!()
+    fn bind(&self, ctx: &mut StatementContext, index: &mut i32) {
+        let adata = self
+            .get_data()
+            .expect("binding query for assoc with no data");
+
+        ctx.bind(*index, adata.local_id)
+            .expect("couldn't bind assoc id");
+        *index += 1;
     }
 
     fn conn(&self) -> &Connection {
         &self.get_data().unwrap().conn
     }
-
-    fn count(self) -> DBResult<usize> {
-        components::AssocQueryable::new(self).count()
-    }
-
-    fn get(self) -> DBResult<Self::OutputContainer> {
-        components::AssocQueryable::new(self).get()
-    }
-
-    fn delete(self) -> DBResult<()> {
-        components::AssocQueryable::new(self).delete()
-    }
-
-    fn unique(
-        self,
-        values: <<<Self::EntityOutput as Entity>::Uniques as EntityPartList>::DatumList as DatumList>::Ref<'_>,
-    ) -> impl Queryable<
-        EntityOutput = Self::EntityOutput,
-        OutputContainer = Option<IDWrap<Self::EntityOutput>>,
-    > {
-        components::AssocQueryable::new(self).unique(values)
-    }
-
-    fn with<EP: EntityPart<Entity = Self::EntityOutput>>(
-        self,
-        part: EP,
-        value: &EP::Datum,
-    ) -> impl Queryable<EntityOutput = Self::EntityOutput, OutputContainer = Self::OutputContainer>
-    {
-        components::AssocQueryable::new(self).with(part, value)
-    }
-
-    fn join<AD: AssocInterface, EP: EntityPart<Entity = Self::EntityOutput, Datum = AD>>(
-        self,
-        part: EP,
-    ) -> impl Queryable<EntityOutput = AD::RemoteEntity, OutputContainer = Vec<IDWrap<AD::RemoteEntity>>>
-    {
-        components::AssocQueryable::new(self).join(part)
-    }
 }
 
 #[cfg(test)]

+ 106 - 99
microrm/src/query/components.rs

@@ -5,115 +5,68 @@ use crate::{
     prelude::Queryable,
     query::{AssocInterface, QueryPart},
     schema::{
-        datum::{Datum, DatumList, DatumListRef, DatumVisitor},
+        datum::{
+            Datum, DatumList, DatumListRef, DatumVisitor, QueryEquivalent, QueryEquivalentList,
+        },
         entity::{Entity, EntityPart, EntityPartList, EntityPartVisitor},
-        DatumDiscriminator, IDMap, IDWrap,
+        DatumDiscriminator, IDWrap,
     },
 };
 
 use super::Query;
 
-/// Concrete implementation of Queryable for an IDMap
-pub(crate) struct MapQueryable<'a, E: Entity> {
-    map: &'a IDMap<E>,
-}
-
-impl<'a, E: Entity> MapQueryable<'a, E> {
-    pub fn new(map: &'a IDMap<E>) -> Self {
-        Self { map }
-    }
+/// Filter on a Datum
+pub(crate) struct WithComponent<WEP: EntityPart, Parent: Queryable, QE: QueryEquivalent<WEP::Datum>>
+{
+    // datum: &'a WEP::Datum,
+    datum: QE,
+    parent: Parent,
+    _ghost: std::marker::PhantomData<WEP>,
 }
 
-impl<'a, E: Entity> Queryable for MapQueryable<'a, E> {
-    type EntityOutput = E;
-    type OutputContainer = Vec<IDWrap<E>>;
-    type StaticVersion = MapQueryable<'static, E>;
-
-    fn build(&self) -> Query {
-        Query::new()
-            .attach(QueryPart::Root, "SELECT DISTINCT".into())
-            .attach(QueryPart::Columns, "*".into())
-            .attach(QueryPart::From, format!("`{}`", E::entity_name()))
-    }
-    fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {}
-
-    fn conn(&self) -> &Connection {
-        self.map.conn()
+impl<WEP: EntityPart, Parent: Queryable, QE: QueryEquivalent<WEP::Datum>>
+    WithComponent<WEP, Parent, QE>
+{
+    pub fn new(parent: Parent, _part: WEP, datum: QE) -> Self {
+        Self {
+            datum,
+            parent,
+            _ghost: Default::default(),
+        }
     }
 }
 
-/// Concrete implementation of Queryable for an IDMap
-pub(crate) struct AssocQueryable<'a, AI: AssocInterface> {
-    assoc: &'a AI,
-}
-
-impl<'a, AI: AssocInterface> AssocQueryable<'a, AI> {
-    pub fn new(assoc: &'a AI) -> Self {
-        Self { assoc }
-    }
+/// 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.
+pub(crate) struct CanonicalWithComponent<WEP: EntityPart, Parent: Queryable> {
+    _ghost: std::marker::PhantomData<(WEP, Parent)>,
 }
 
-impl<'a, AI: AssocInterface> Queryable for AssocQueryable<'a, AI> {
-    type EntityOutput = AI::RemoteEntity;
-    type OutputContainer = Vec<IDWrap<AI::RemoteEntity>>;
-    type StaticVersion = AssocQueryable<'static, AI>;
+impl<WEP: EntityPart, Parent: Queryable + 'static> Queryable
+    for CanonicalWithComponent<WEP, Parent>
+{
+    type EntityOutput = WEP::Entity;
+    type OutputContainer = Option<IDWrap<WEP::Entity>>;
+    type StaticVersion = Self;
 
     fn build(&self) -> Query {
-        let anames = super::AssocNames::collect(self.assoc).unwrap();
-        let assoc_name = anames.assoc_name();
-        Query::new()
-            .attach(QueryPart::Root, "SELECT DISTINCT".into())
-            .attach(QueryPart::Columns, format!("`{}`.*", anames.remote_name))
-            .attach(QueryPart::From, format!("`{}`", assoc_name))
-            .attach(
-                QueryPart::Join,
-                format!(
-                    "`{}` ON `{}`.`id` = `{}`.`{}`",
-                    anames.remote_name, anames.remote_name, assoc_name, anames.remote_field
-                ),
-            )
-            .attach(
-                QueryPart::Where,
-                format!("`{}`.`{}` = ?", assoc_name, anames.local_field),
-            )
+        unreachable!()
     }
-    fn bind(&self, ctx: &mut StatementContext, index: &mut i32) {
-        let adata = self
-            .assoc
-            .get_data()
-            .expect("binding query for assoc with no data");
-
-        ctx.bind(*index, adata.local_id)
-            .expect("couldn't bind assoc id");
-        *index += 1;
+    fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {
+        unreachable!()
     }
-
     fn conn(&self) -> &Connection {
-        &self.assoc.get_data().unwrap().conn
+        unreachable!()
     }
 }
 
-/// Filter on a Datum
-pub(crate) struct WithComponent<'a, WEP: EntityPart, Parent: Queryable> {
-    datum: &'a WEP::Datum,
-    parent: Parent,
-    _ghost: std::marker::PhantomData<WEP>,
-}
-
-impl<'a, WEP: EntityPart, Parent: Queryable> WithComponent<'a, WEP, Parent> {
-    pub fn new(parent: Parent, _part: WEP, datum: &'a WEP::Datum) -> Self {
-        Self {
-            datum,
-            parent,
-            _ghost: Default::default(),
-        }
-    }
-}
-
-impl<'a, WEP: EntityPart, Parent: Queryable> Queryable for WithComponent<'a, WEP, Parent> {
+impl<WEP: EntityPart, Parent: Queryable, QE: QueryEquivalent<WEP::Datum>> Queryable
+    for WithComponent<WEP, Parent, QE>
+{
     type EntityOutput = WEP::Entity;
     type OutputContainer = Parent::OutputContainer;
-    type StaticVersion = WithComponent<'static, WEP, Parent::StaticVersion>;
+    type StaticVersion = CanonicalWithComponent<WEP, Parent::StaticVersion>;
 
     fn build(&self) -> Query {
         self.parent.build().attach(
@@ -137,24 +90,68 @@ impl<'a, WEP: EntityPart, Parent: Queryable> Queryable for WithComponent<'a, WEP
 }
 
 /// Filter on the unique index
-pub(crate) struct UniqueComponent<'a, E: Entity, Parent: Queryable> {
-    datum: <<E::Uniques as EntityPartList>::DatumList as DatumList>::Ref<'a>,
+pub(crate) struct UniqueComponent<
+    E: Entity,
+    Parent: Queryable,
+    EL: QueryEquivalentList<<E::Uniques as EntityPartList>::DatumList>,
+> {
+    // datum: <<E::Uniques as EntityPartList>::DatumList as DatumList>::Ref<'a>,
+    datum: EL,
     parent: Parent,
+    _ghost: std::marker::PhantomData<E>,
 }
 
-impl<'a, E: Entity, Parent: Queryable> UniqueComponent<'a, E, Parent> {
+impl<
+        E: Entity,
+        Parent: Queryable,
+        EL: QueryEquivalentList<<E::Uniques as EntityPartList>::DatumList>,
+    > UniqueComponent<E, Parent, EL>
+{
     pub fn new(
         parent: Parent,
-        datum: <<E::Uniques as EntityPartList>::DatumList as DatumList>::Ref<'a>,
+        // datum: <<E::Uniques as EntityPartList>::DatumList as DatumList>::Ref<'a>,
+        datum: EL,
     ) -> Self {
-        Self { datum, parent }
+        Self {
+            datum,
+            parent,
+            _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.
+pub(crate) struct CanonicalUniqueComponent<E: Entity, Parent: Queryable> {
+    _ghost: std::marker::PhantomData<(E, Parent)>,
+}
+
+impl<E: Entity, Parent: Queryable + 'static> Queryable for CanonicalUniqueComponent<E, Parent> {
+    type EntityOutput = E;
+    type OutputContainer = Option<IDWrap<E>>;
+    type StaticVersion = Self;
+
+    fn build(&self) -> Query {
+        unreachable!()
+    }
+    fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {
+        unreachable!()
+    }
+    fn conn(&self) -> &Connection {
+        unreachable!()
     }
 }
 
-impl<'a, E: Entity, Parent: Queryable> Queryable for UniqueComponent<'a, E, Parent> {
+impl<
+        E: Entity,
+        Parent: Queryable,
+        EL: QueryEquivalentList<<E::Uniques as EntityPartList>::DatumList>,
+    > Queryable for UniqueComponent<E, Parent, EL>
+{
     type EntityOutput = E;
     type OutputContainer = Option<IDWrap<E>>;
-    type StaticVersion = UniqueComponent<'static, E, Parent::StaticVersion>;
+    type StaticVersion = CanonicalUniqueComponent<E, Parent::StaticVersion>;
 
     fn build(&self) -> Query {
         let mut query = self.parent.build();
@@ -245,8 +242,13 @@ impl<R: Entity, L: Entity, EP: EntityPart<Entity = L>, Parent: Queryable>
     }
 }
 
-impl<R: Entity, L: Entity, EP: EntityPart<Entity = L>, Parent: Queryable> Queryable
-    for JoinComponent<R, L, EP, Parent>
+impl<
+        R: Entity,
+        L: Entity,
+        EP: EntityPart<Entity = L, Datum = AI>,
+        AI: AssocInterface + Datum,
+        Parent: Queryable,
+    > Queryable for JoinComponent<R, L, EP, Parent>
 {
     type EntityOutput = R;
     type OutputContainer = Vec<IDWrap<R>>;
@@ -255,10 +257,8 @@ impl<R: Entity, L: Entity, EP: EntityPart<Entity = L>, Parent: Queryable> Querya
     fn build(&self) -> Query {
         let remote_name = R::entity_name();
         let local_name = L::entity_name();
-        let part_name = EP::part_name();
-        let assoc_name = format!("{local_name}_{remote_name}_assoc_{part_name}");
 
-        struct Discriminator(Option<(&'static str, &'static str)>);
+        struct Discriminator(Option<(&'static str, &'static str)>, Option<&'static str>);
         impl DatumDiscriminator for Discriminator {
             fn visit_entity_id<E: Entity>(&mut self) {
                 unreachable!()
@@ -275,17 +275,24 @@ impl<R: Entity, L: Entity, EP: EntityPart<Entity = L>, Parent: Queryable> Querya
             }
             fn visit_assoc_domain<R: crate::schema::Relation>(&mut self) {
                 self.0 = Some(("domain", "range"));
+                self.1 = Some(R::NAME);
             }
             fn visit_assoc_range<R: crate::schema::Relation>(&mut self) {
                 self.0 = Some(("range", "domain"));
+                self.1 = Some(R::NAME);
             }
         }
 
-        let mut d = Discriminator(None);
+        let mut d = Discriminator(None, None);
         <EP::Datum>::accept_discriminator(&mut d);
 
         let (local_field, remote_field) = d.0.unwrap();
 
+        let assoc_name = format!(
+            "{local_name}_{remote_name}_assoc_{}",
+            d.1.unwrap_or(EP::part_name())
+        );
+
         self.parent
             .build()
             .attach(

+ 45 - 69
microrm/src/schema.rs

@@ -6,6 +6,8 @@
 //! - local: the current side of an association
 //! - remote: the opposite side of an association
 
+use std::cell::RefCell;
+
 use query::Queryable;
 
 use crate::{
@@ -74,6 +76,12 @@ impl<T: Entity> std::ops::DerefMut for IDWrap<T> {
     }
 }
 
+impl<T: Entity> PartialEq for IDWrap<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.id == other.id
+    }
+}
+
 // ----------------------------------------------------------------------
 // Entity field types
 // ----------------------------------------------------------------------
@@ -111,6 +119,7 @@ pub enum LocalSide {
 }
 
 /// Opaque data structure used for constructing `Assoc{Map,Domain,Range}` instances.
+#[derive(Clone)]
 pub struct AssocData {
     pub(crate) conn: Connection,
     pub(crate) local_name: &'static str,
@@ -126,6 +135,15 @@ pub struct AssocMap<T: Entity> {
     _ghost: std::marker::PhantomData<T>,
 }
 
+impl<T: Entity> Clone for AssocMap<T> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+            _ghost: Default::default(),
+        }
+    }
+}
+
 impl<T: Entity> std::fmt::Debug for AssocMap<T> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.write_fmt(format_args!(
@@ -207,6 +225,15 @@ pub struct AssocDomain<R: Relation> {
     _ghost: std::marker::PhantomData<R>,
 }
 
+impl<R: Relation> Clone for AssocDomain<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+            _ghost: Default::default(),
+        }
+    }
+}
+
 impl<R: Relation> Default for AssocDomain<R> {
     fn default() -> Self {
         Self::empty()
@@ -283,6 +310,15 @@ pub struct AssocRange<R: Relation> {
     _ghost: std::marker::PhantomData<R>,
 }
 
+impl<R: Relation> Clone for AssocRange<R> {
+    fn clone(&self) -> Self {
+        Self {
+            data: self.data.clone(),
+            _ghost: Default::default(),
+        }
+    }
+}
+
 impl<R: Relation> Default for AssocRange<R> {
     fn default() -> Self {
         Self::empty()
@@ -392,11 +428,14 @@ impl<T: 'static + serde::Serialize + serde::de::DeserializeOwned> Datum for Seri
     }
 
     fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
-        <String as Datum>::bind_to(
-            &serde_json::to_string(&self.wrapped).expect("couldn't serialize object into JSON"),
-            stmt,
-            index,
-        )
+        let json = Box::pin(
+            serde_json::to_string(&self.wrapped).expect("couldn't serialize object into JSON"),
+        );
+
+        <String as Datum>::bind_to(&*json.as_ref(), stmt, index);
+
+        // transfer ownership so that the data is still around for later
+        stmt.transfer(json);
     }
 
     fn build_from(adata: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
@@ -417,7 +456,7 @@ impl<T: 'static + serde::Serialize + serde::de::DeserializeOwned> Datum for Seri
 
 /// Table with EntityID-based lookup.
 pub struct IDMap<T: Entity> {
-    conn: Connection,
+    pub(crate) conn: Connection,
     _ghost: std::marker::PhantomData<T>,
 }
 
@@ -444,69 +483,6 @@ impl<T: Entity> IDMap<T> {
     }
 }
 
-impl<'a, T: Entity> Queryable for &'a IDMap<T> {
-    type EntityOutput = T;
-    type OutputContainer = Vec<IDWrap<T>>;
-    type StaticVersion = &'static IDMap<T>;
-
-    fn build(&self) -> query::Query {
-        unreachable!()
-    }
-    fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {
-        unreachable!()
-    }
-
-    fn conn(&self) -> &Connection {
-        &self.conn
-    }
-
-    fn count(self) -> DBResult<usize> {
-        query::components::MapQueryable::new(self).count()
-    }
-
-    fn get(self) -> DBResult<Self::OutputContainer> {
-        query::components::MapQueryable::new(self).get()
-    }
-
-    fn delete(self) -> DBResult<()> {
-        query::components::MapQueryable::new(self).delete()
-    }
-
-    fn unique(
-        self,
-        values: <<<Self::EntityOutput as Entity>::Uniques as EntityPartList>::DatumList as datum::DatumList>::Ref<'_>,
-    ) -> impl Queryable<
-        EntityOutput = Self::EntityOutput,
-        OutputContainer = Option<IDWrap<Self::EntityOutput>>,
-    >
-    where
-        Self: Sized,
-    {
-        query::components::MapQueryable::new(self).unique(values)
-    }
-
-    fn with<EP: entity::EntityPart<Entity = Self::EntityOutput>>(
-        self,
-        part: EP,
-        value: &EP::Datum,
-    ) -> impl Queryable<EntityOutput = Self::EntityOutput, OutputContainer = Self::OutputContainer>
-    where
-        Self: Sized,
-    {
-        query::components::MapQueryable::new(self).with(part, value)
-    }
-
-    fn join<AD: AssocInterface, EP: entity::EntityPart<Entity = Self::EntityOutput, Datum = AD>>(
-        self,
-        part: EP,
-    ) -> impl Queryable<EntityOutput = AD::RemoteEntity, OutputContainer = Vec<IDWrap<AD::RemoteEntity>>>
-    where
-        Self: Sized,
-    {
-        query::components::MapQueryable::new(self).join(part)
-    }
-}
-
 pub struct Index<T: Entity, Key: Datum> {
     _conn: Connection,
     _ghost: std::marker::PhantomData<(T, Key)>,

+ 13 - 0
microrm/src/schema/datum.rs

@@ -48,3 +48,16 @@ pub trait DatumListRef {
 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.
+///
+/// This is purely a marker trait, since the equivalence is based on how sqlite handles things.
+pub trait QueryEquivalent<T: Datum>: Datum {}
+
+// Every type is query-equivalent to itself.
+impl<T: Datum> QueryEquivalent<T> for T {}
+// Every reference type is query-equivalent to the non-referenced type.
+impl<'l, T: Datum> QueryEquivalent<T> for &'l T {}
+
+/// A list version of `QueryEquivalent`.
+pub trait QueryEquivalentList<T: DatumList>: DatumList {}

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

@@ -4,6 +4,8 @@ use crate::{
     DBResult, Error,
 };
 
+use super::QueryEquivalent;
+
 impl Datum for time::OffsetDateTime {
     fn sql_type() -> &'static str {
         "text"
@@ -43,6 +45,27 @@ impl Datum for String {
     }
 }
 
+impl<'l> Datum for &'l str {
+    fn sql_type() -> &'static str {
+        "text"
+    }
+
+    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
+        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 Datum for usize {
     fn sql_type() -> &'static str {
         "int"
@@ -170,6 +193,26 @@ impl Datum for Vec<u8> {
     }
 }
 
+impl<'l> Datum for &'l [u8] {
+    fn sql_type() -> &'static str {
+        "blob"
+    }
+
+    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
+impl<'l> QueryEquivalent<Vec<u8>> for &'l [u8] {}
+
 impl<'l, T: Datum> Datum for &'l T {
     fn sql_type() -> &'static str {
         T::sql_type()

+ 26 - 4
microrm/src/schema/datum/datum_list.rs

@@ -1,4 +1,4 @@
-use super::{Datum, DatumList, DatumListRef, DatumVisitor};
+use super::{Datum, DatumList, DatumListRef, DatumVisitor, QueryEquivalent, QueryEquivalentList};
 
 impl DatumList for () {
     type Ref<'a> = &'a ();
@@ -23,6 +23,8 @@ impl<'l, T: Datum> DatumListRef for &'l T {
     }
 }
 
+impl<T: Datum, E: QueryEquivalent<T>> QueryEquivalentList<T> for E {}
+
 impl<T0: Datum> DatumList for (T0,) {
     type Ref<'a> = (&'a T0,) where Self: 'a;
 
@@ -31,6 +33,8 @@ impl<T0: Datum> DatumList for (T0,) {
     }
 }
 
+impl<T0: Datum, E0: QueryEquivalent<T0>> QueryEquivalentList<(T0,)> for (E0,) {}
+
 impl<'a, T0: Datum> DatumListRef for (&'a T0,) {
     fn accept(&self, visitor: &mut impl DatumVisitor) {
         visitor.visit(&self.0);
@@ -38,7 +42,7 @@ impl<'a, T0: Datum> DatumListRef for (&'a T0,) {
 }
 
 macro_rules! datum_list {
-    ($($ty:ident : $n:tt),+) => {
+    ($($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) {
@@ -51,10 +55,28 @@ macro_rules! datum_list {
                 $(visitor.visit(&self. $n));*
             }
         }
+
+        impl<$( $ty: Datum, $e: QueryEquivalent<$ty> ),*> QueryEquivalentList<( $( $ty ),* )> for ( $( $e ),* ) {}
     }
 }
 
-datum_list!(T0: 0, T1: 1);
+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!(T0: 0, T1: 1);
 datum_list!(T0: 0, T1: 1, T2: 2);
 datum_list!(T0: 0, T1: 1, T2: 2, T3: 3);
 datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4);
@@ -68,4 +90,4 @@ datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9
 datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12);
 datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12, T13: 13);
 datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12, T13: 13, T14: 14);
-datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12, T13: 13, T14: 14, T15: 15);
+datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12, T13: 13, T14: 14, T15: 15);*/

+ 83 - 0
microrm/src/schema/tests.rs

@@ -591,3 +591,86 @@ mod join_test {
         assert_eq!(double_join_count, 1);
     }
 }
+
+mod query_equivalence {
+    use crate::prelude::*;
+    #[derive(Entity, Debug)]
+    struct Item {
+        #[unique]
+        s: String,
+        v: Vec<u8>,
+    }
+
+    #[derive(Database)]
+    struct ItemDB {
+        items: IDMap<Item>,
+    }
+
+    #[test]
+    fn unique_test() {
+        let db = ItemDB::open_path(":memory:").expect("couldn't open test db");
+
+        db.items
+            .insert(Item {
+                s: "string 1".into(),
+                v: vec![0u8, 1u8, 2u8],
+            })
+            .expect("couldn't insert test item");
+
+        db.items
+            .insert(Item {
+                s: "string 2".into(),
+                v: vec![0u8, 1u8, 2u8],
+            })
+            .expect("couldn't insert test item");
+
+        assert!(db
+            .items
+            .unique("string 2")
+            .get()
+            .expect("couldn't query db")
+            .is_some());
+        assert!(db
+            .items
+            .unique("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 {
+                s: "string 1".into(),
+                v: vec![0u8, 1u8, 2u8],
+            })
+            .expect("couldn't insert test item");
+
+        db.items
+            .insert(Item {
+                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())
+                .get()
+                .expect("couldn't query db")
+                .len(),
+            0
+        );
+        assert_eq!(
+            db.items
+                .with(Item::V, [0u8, 1, 2].as_slice())
+                .get()
+                .expect("couldn't query db")
+                .len(),
+            2
+        );
+    }
+}