Ver código fonte

Added get_ids() and foreign() methods to Queryable.

Kestrel 7 meses atrás
pai
commit
4c9915dfb0
3 arquivos alterados com 248 adições e 41 exclusões
  1. 100 0
      microrm/src/query.rs
  2. 89 41
      microrm/src/query/components.rs
  3. 59 0
      microrm/src/schema/tests.rs

+ 100 - 0
microrm/src/query.rs

@@ -496,19 +496,45 @@ impl<AI: RelationInterface> Insertable<AI::RemoteEntity> for AI {
     }
 }
 
+pub trait IDContainer<T: Entity>: 'static + IntoIterator<Item = T::ID> {
+    fn assemble_from(ctx: StatementContext<'_>) -> DBResult<Self>
+    where
+        Self: Sized;
+}
+
 pub trait OutputContainer<T: Entity>: 'static + IntoIterator<Item = Stored<T>> {
+    type IDContainer: IDContainer<T>;
     fn assemble_from(conn: &Connection, stmt: StatementContext<'_>) -> DBResult<Self>
     where
         Self: Sized;
 }
 
+pub trait ContainerEntityChange<F: Entity, T: Entity>: OutputContainer<F> {
+    type Container: OutputContainer<T>;
+}
+
+fn assemble_id<T: Entity>(row: StatementRow) -> T::ID {
+    <T::ID>::from_raw(row.read::<i64>(0).expect("couldn't read ID"))
+}
+
 fn assemble_single<T: Entity>(conn: &Connection, row: &mut StatementRow) -> Stored<T> {
     let id = row.read::<i64>(0).expect("couldn't read ID");
     let datum_list = <T::Parts>::build_datum_list(conn, row).expect("couldn't build datum list");
     Stored::new(conn.clone(), T::ID::from_raw(id), T::build(datum_list))
 }
 
+impl<T: Entity> IDContainer<T> for Option<T::ID> {
+    fn assemble_from(ctx: StatementContext<'_>) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        Ok(ctx.run()?.map(assemble_id::<T>))
+    }
+}
+
 impl<T: Entity> OutputContainer<T> for Option<Stored<T>> {
+    type IDContainer = Option<T::ID>;
+
     fn assemble_from(conn: &Connection, ctx: StatementContext<'_>) -> DBResult<Self>
     where
         Self: Sized,
@@ -517,7 +543,24 @@ impl<T: Entity> OutputContainer<T> for Option<Stored<T>> {
     }
 }
 
+impl<F: Entity, T: Entity> ContainerEntityChange<F, T> for Option<Stored<F>> {
+    type Container = Option<Stored<T>>;
+}
+
+impl<T: Entity> IDContainer<T> for Vec<T::ID> {
+    fn assemble_from(ctx: StatementContext<'_>) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        ctx.iter()
+            .map(|r| r.map(assemble_id::<T>))
+            .collect::<Result<Vec<_>, Error>>()
+    }
+}
+
 impl<T: Entity> OutputContainer<T> for Vec<Stored<T>> {
+    type IDContainer = Vec<T::ID>;
+
     fn assemble_from(conn: &Connection, ctx: StatementContext<'_>) -> DBResult<Self>
     where
         Self: Sized,
@@ -528,6 +571,10 @@ impl<T: Entity> OutputContainer<T> for Vec<Stored<T>> {
     }
 }
 
+impl<F: Entity, T: Entity> ContainerEntityChange<F, T> for Vec<Stored<F>> {
+    type Container = Vec<Stored<T>>;
+}
+
 /// Represents a searchable context of a given entity.
 pub trait Queryable: Clone {
     /// The entity that results from a search in this context.
@@ -606,6 +653,39 @@ pub trait Queryable: Clone {
         txn.commit()?;
         Ok(out)
     }
+    /// Get IDs of all entities in the current context.
+    fn get_ids(
+        self,
+    ) -> DBResult<<Self::OutputContainer as OutputContainer<Self::EntityOutput>>::IDContainer>
+    where
+        Self: Sized,
+    {
+        let txn = Transaction::new(self.conn())?;
+        struct GetIDTag;
+        let out = self.conn().with_prepared(
+            std::any::TypeId::of::<(Self::StaticVersion, GetIDTag)>(),
+            || {
+                self.build()
+                    .replace(
+                        QueryPart::Columns,
+                        format!(
+                            "`{}`.`id`",
+                            Self::EntityOutput::entity_name()
+                        ),
+                    )
+                    .assemble()
+            },
+            |mut ctx| {
+                // starting index is 1
+                let mut index = 1;
+                self.bind(&mut ctx, &mut index);
+
+                <<Self::OutputContainer as OutputContainer<Self::EntityOutput>>::IDContainer>::assemble_from(ctx)
+            },
+        )?;
+        txn.commit()?;
+        Ok(out)
+    }
     /// Delete all entities in the current context.
     fn delete(self) -> DBResult<()>
     where
@@ -716,6 +796,26 @@ pub trait Queryable: Clone {
     {
         components::JoinComponent::<AD::RemoteEntity, Self::EntityOutput, _, Self>::new(self, part)
     }
+
+    /// Follow a foreign key.
+    fn foreign<EP: EntityPart<Entity = Self::EntityOutput>>(
+        self,
+        part: EP,
+    ) -> impl Queryable<
+        EntityOutput = <EP::Datum as EntityID>::Entity,
+        OutputContainer = <Self::OutputContainer as ContainerEntityChange<
+            Self::EntityOutput,
+            <EP::Datum as EntityID>::Entity,
+        >>::Container,
+    >
+    where
+        Self: Sized,
+        EP::Datum: EntityID,
+        Self::OutputContainer:
+            ContainerEntityChange<Self::EntityOutput, <EP::Datum as EntityID>::Entity>,
+    {
+        components::ForeignComponent::<_, EP, Self>::new(self, part)
+    }
 }
 
 // Generic implementations for all IDMaps

+ 89 - 41
microrm/src/query/components.rs

@@ -12,7 +12,7 @@ use crate::{
     },
 };
 
-use super::Query;
+use super::{ContainerEntityChange, Query};
 
 /// Allow manipulation of an entire table.
 pub(crate) struct TableComponent<E: Entity> {
@@ -76,7 +76,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
+/// this workaround is needed because we very explicitly would like QE to not be a
 /// 'static-restricted type, and it doesn't matter for the purposes of actually distinguishing
 /// between queries.
 #[derive(Clone)]
@@ -416,42 +416,90 @@ impl<
     }
 }
 
-/*
-SQL mapping:
-
-MapQueryable<E>::get()
- -> SELECT * FROM {E::entity_name()}
-
-UniqueComponent<E, MapQueryable<E>>::get()
- -> SELECT * FROM {E::entity_name()} WHERE `col1` = ? AND `col2` = ? AND ...
-
-WithComponent<E, EP, MapQueryable<E>>::get()
- -> SELECT * FROM {E::entity_name()} WHERE `col1` = ?
-
-RelationQueryable<AI>::get()
- -> SELECT
-        `{AI::RemoteEntity::entity_name()}`.*
-    FROM `{relation_table_name}`
-    LEFT JOIN `{AI::RemoteEntity::entity_name()}`.id = `{relation_table_name}`.`{remote_field}`
-    WHERE `{relation_table_name}`.`{local_field}` = ?
-
-UniqueComponent<E, RelationQueryable<AI>>::get()
- -> SELECT
-        `{AI::RemoteEntity::entity_name()}`.*
-    FROM `{relation_table_name}`
-    LEFT JOIN `{AI::RemoteEntity::entity_name()}`.id = `{relation_table_name}`.`{remote_field}`
-    WHERE
-        `{relation_table_name}`.`{local_field}` = ?
-        AND `{AI::RemoteEntity::entity_name()}`.`col1` = ?
-        AND `{AI::RemoteEntity::entity_name()}`.`col2` = ?
-        ...
-
-JoinComponent<R, E, EP, MapQueryable<E>>::get()
- -> SELECT DISTINCT
-        `{R::entity_name()}`.*
-    FROM
-        `{E::entity_name()}`
-    LEFT JOIN `{relation_table_name}` ON `{E::entity_name()}`.`id` = `{relation_table_name}`.`{local_field}`
-    LEFT JOIN `{R::entity_name()}` ON `{relation_table_name}`.`{remote_field}` = `{R::entity_name()}`.`id`
-
-*/
+/// Get an entity via a foreign key
+pub(crate) struct ForeignComponent<FE: Entity, EP: EntityPart, Parent: Queryable> {
+    parent: Parent,
+    _ghost: std::marker::PhantomData<(FE, EP)>,
+}
+
+impl<FE: Entity, EP: EntityPart, Parent: Queryable> ForeignComponent<FE, EP, Parent> {
+    pub fn new(parent: Parent, _part: EP) -> Self {
+        Self {
+            parent,
+            _ghost: Default::default(),
+        }
+    }
+}
+
+impl<FE: Entity, EP: EntityPart, Parent: Queryable> Clone for ForeignComponent<FE, EP, Parent> {
+    fn clone(&self) -> Self {
+        Self {
+            parent: self.parent.clone(),
+            _ghost: Default::default(),
+        }
+    }
+}
+
+pub(crate) struct CanonicalForeignComponent<FE: Entity, EP: EntityPart, Parent: Queryable> {
+    _ghost: std::marker::PhantomData<(FE, EP, Parent)>,
+}
+
+impl<FE: Entity, EP: EntityPart, Parent: Queryable> Clone
+    for CanonicalForeignComponent<FE, EP, Parent>
+{
+    fn clone(&self) -> Self {
+        Self {
+            _ghost: Default::default(),
+        }
+    }
+}
+
+impl<FE: Entity, EP: EntityPart, Parent: Queryable> Queryable
+    for CanonicalForeignComponent<FE, EP, Parent>
+{
+    type EntityOutput = FE;
+    type OutputContainer = Option<Stored<FE>>;
+    type StaticVersion = CanonicalForeignComponent<FE, EP, Parent::StaticVersion>;
+
+    fn build(&self) -> Query {
+        unreachable!()
+    }
+    fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {
+        unreachable!()
+    }
+    fn conn(&self) -> &Connection {
+        unreachable!()
+    }
+}
+
+impl<FE: Entity, EP: EntityPart, Parent: Queryable> Queryable for ForeignComponent<FE, EP, Parent>
+where
+    Parent::OutputContainer: ContainerEntityChange<Parent::EntityOutput, FE>,
+{
+    type EntityOutput = FE;
+    type StaticVersion = CanonicalForeignComponent<FE, EP, Parent::StaticVersion>;
+    type OutputContainer =
+        <Parent::OutputContainer as ContainerEntityChange<Parent::EntityOutput, FE>>::Container;
+
+    fn build(&self) -> Query {
+        let subquery = self.parent.build().replace(
+            QueryPart::Columns,
+            format!("`{}`.`id`", EP::Entity::entity_name()),
+        );
+
+        Query::new()
+            .attach(QueryPart::Root, "SELECT DISTINCT".into())
+            .attach(QueryPart::Columns, "*".into())
+            .attach(QueryPart::From, FE::entity_name().into())
+            .attach(
+                QueryPart::Where,
+                format!("`{}`.`id` = ({})", FE::entity_name(), subquery.assemble()),
+            )
+    }
+    fn bind(&self, stmt: &mut StatementContext, index: &mut i32) {
+        self.parent.bind(stmt, index)
+    }
+    fn conn(&self) -> &Connection {
+        self.parent.conn()
+    }
+}

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

@@ -987,3 +987,62 @@ mod index_test {
         );
     }
 }
+
+mod foreign_key {
+    use microrm::prelude::*;
+    use test_log::test;
+
+    #[derive(Entity)]
+    struct Child {
+        #[key]
+        name: String,
+        parent: ParentID,
+    }
+
+    #[derive(Entity)]
+    struct Parent {
+        name: String,
+    }
+
+    #[derive(Database)]
+    struct PeopleDB {
+        children: microrm::IDMap<Child>,
+        parents: microrm::IDMap<Parent>,
+    }
+
+    #[test]
+    fn simple_fk_query() {
+        let db = PeopleDB::open_path(":memory:").expect("couldn't open DB");
+
+        let pid = db
+            .parents
+            .insert(Parent {
+                name: "parent1".to_string(),
+            })
+            .unwrap();
+        let c1id = db
+            .children
+            .insert(Child {
+                name: "child1".to_string(),
+                parent: pid,
+            })
+            .unwrap();
+
+        assert_eq!(
+            db.children
+                .foreign(Child::Parent)
+                .get_ids()
+                .expect("couldn't run query"),
+            vec![pid]
+        );
+
+        assert_eq!(
+            db.children
+                .keyed("child1")
+                .foreign(Child::Parent)
+                .get_ids()
+                .expect("couldn't run query"),
+            Some(pid)
+        );
+    }
+}