Explorar o código

Added insert_and_return and entity updating functionality.

Kestrel hai 9 meses
pai
achega
1d7ea8bf22

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

@@ -101,6 +101,14 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
         }
     });
 
+    let part_mut_visit = parts.iter().map(|part| {
+        let part_combined_name = make_combined_name(part);
+        let field = &part.0;
+        quote! {
+            v.visit_datum_mut::<#part_combined_name>(&mut self.#field);
+        }
+    });
+
     let part_names = parts.iter().map(|part| {
         let part_combined_name = make_combined_name(part);
         let part_camel_name = format_ident!("{}", part.0.to_string().to_case(Case::UpperCamel));
@@ -212,6 +220,12 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
                     #part_ref_visit
                 );*
             }
+
+            fn accept_part_visitor_mut(&mut self, v: &mut impl ::microrm::schema::entity::EntityPartVisitor) {
+                #(
+                    #part_mut_visit
+                );*
+            }
         }
     }
     .into()

+ 8 - 0
microrm/src/lib.rs

@@ -33,6 +33,12 @@ pub enum Error {
     LockError(String),
 }
 
+impl std::fmt::Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        <Self as std::fmt::Debug>::fmt(self, f)
+    }
+}
+
 impl<T> From<std::sync::PoisonError<T>> for Error {
     fn from(value: std::sync::PoisonError<T>) -> Self {
         Self::LockError(value.to_string())
@@ -51,4 +57,6 @@ impl From<std::str::Utf8Error> for Error {
     }
 }
 
+impl std::error::Error for Error {}
+
 pub type DBResult<T> = Result<T, Error>;

+ 93 - 12
microrm/src/query.rs

@@ -2,7 +2,8 @@ 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::schema::entity::EntityVisitor;
+use crate::schema::{AssocData, DatumDiscriminator, LocalSide, Stored};
 use crate::{
     schema::datum::{Datum, DatumList},
     schema::entity::{Entity, EntityID, EntityPart, EntityPartList, EntityPartVisitor},
@@ -13,7 +14,7 @@ use std::hash::{Hash, Hasher};
 
 pub(crate) mod components;
 
-pub(crate) fn insert<E: Entity>(conn: &Connection, value: E) -> DBResult<E::ID> {
+pub(crate) fn insert<E: Entity>(conn: &Connection, value: &E) -> DBResult<E::ID> {
     struct InsertQuery<E: Entity>(std::marker::PhantomData<E>);
 
     conn.with_prepared(
@@ -73,6 +74,86 @@ pub(crate) fn insert<E: Entity>(conn: &Connection, value: E) -> DBResult<E::ID>
     )
 }
 
+pub(crate) fn insert_and_return<E: Entity>(conn: &Connection, mut value: E) -> DBResult<Stored<E>> {
+    let id = insert(conn, &value)?;
+
+    // update assoc data in all fields
+    struct DatumWalker<'l>(&'l Connection, i64);
+    impl<'l> EntityPartVisitor for DatumWalker<'l> {
+        fn visit_datum_mut<EP: EntityPart>(&mut self, datum: &mut EP::Datum) {
+            datum.update_adata(AssocData {
+                conn: self.0.clone(),
+                part_name: EP::part_name(),
+                local_name: <EP::Entity as Entity>::entity_name(),
+                local_id: self.1,
+            });
+        }
+    }
+
+    value.accept_part_visitor_mut(&mut DatumWalker(conn, id.into_raw()));
+
+    Ok(Stored::new(conn.clone(), id, value))
+}
+
+pub(crate) fn update_entity<E: Entity>(conn: &Connection, value: &Stored<E>) -> DBResult<()> {
+    struct UpdateQuery<E: Entity>(std::marker::PhantomData<E>);
+
+    conn.with_prepared(
+        std::any::TypeId::of::<UpdateQuery<E>>(),
+        || {
+            let mut set_columns = String::new();
+            struct PartNameVisitor<'a>(&'a mut String);
+            impl<'a> EntityPartVisitor for PartNameVisitor<'a> {
+                fn visit<EP: EntityPart>(&mut self) {
+                    // if this is a set-association, then we don't actually want to do anything
+                    // with it here; it doesn't have a column
+                    if check_assoc::<EP>() {
+                        return;
+                    }
+
+                    if !self.0.is_empty() {
+                        self.0.push_str(", ");
+                    }
+                    self.0.push('`');
+                    self.0.push_str(EP::part_name());
+                    self.0.push_str("` = ?");
+                }
+            }
+
+            E::accept_part_visitor(&mut PartNameVisitor(&mut set_columns));
+            format!(
+                "UPDATE {entity_name} SET {set_columns} WHERE `id` = ?",
+                entity_name = E::entity_name()
+            )
+        },
+        |mut ctx| {
+            struct PartBinder<'a, 'b>(&'a mut StatementContext<'b>, &'a mut i32);
+            impl<'a, 'b> EntityPartVisitor for PartBinder<'a, 'b> {
+                fn visit_datum<EP: EntityPart>(&mut self, datum: &EP::Datum) {
+                    // skip associations, as with the query preparation above
+                    if check_assoc::<EP>() {
+                        return;
+                    }
+
+                    datum.bind_to(self.0, *self.1);
+                    *self.1 += 1;
+                }
+            }
+
+            // first bind all the updating clauses
+            let mut index = 1;
+            value.accept_part_visitor_ref(&mut PartBinder(&mut ctx, &mut index));
+
+            // then bind the id
+            value.id().bind_to(&mut ctx, index);
+
+            ctx.run()?;
+
+            Ok(())
+        },
+    )
+}
+
 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
 pub(crate) enum QueryPart {
     Root,
@@ -310,7 +391,7 @@ pub trait AssocInterface: 'static {
         let adata = self.get_data()?;
 
         // so first, the remote table
-        let remote_id = insert(&adata.conn, value)?;
+        let remote_id = insert(&adata.conn, &value)?;
         // then the association
         self.connect_to(remote_id)?;
         // TODO: handle error case of associate_with() fails but insert() succeeds
@@ -328,13 +409,13 @@ pub trait OutputContainer: 'static {
         Self: Sized;
 }
 
-fn assemble_single<T: Entity>(conn: &Connection, row: &mut StatementRow) -> IDWrap<T> {
+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");
-    IDWrap::new(T::ID::from_raw(id), T::build(datum_list))
+    Stored::new(conn.clone(), T::ID::from_raw(id), T::build(datum_list))
 }
 
-impl<T: Entity> OutputContainer for Option<IDWrap<T>> {
+impl<T: Entity> OutputContainer for Option<Stored<T>> {
     fn assemble_from(conn: &Connection, ctx: StatementContext<'_>) -> DBResult<Self>
     where
         Self: Sized,
@@ -343,7 +424,7 @@ impl<T: Entity> OutputContainer for Option<IDWrap<T>> {
     }
 }
 
-impl<T: Entity> OutputContainer for Vec<IDWrap<T>> {
+impl<T: Entity> OutputContainer for Vec<Stored<T>> {
     fn assemble_from(conn: &Connection, ctx: StatementContext<'_>) -> DBResult<Self>
     where
         Self: Sized,
@@ -460,7 +541,7 @@ pub trait Queryable {
         >,
     ) -> impl Queryable<
         EntityOutput = Self::EntityOutput,
-        OutputContainer = Option<IDWrap<Self::EntityOutput>>,
+        OutputContainer = Option<Stored<Self::EntityOutput>>,
     >
     where
         Self: Sized,
@@ -484,7 +565,7 @@ pub trait Queryable {
         self,
     ) -> impl Queryable<
         EntityOutput = Self::EntityOutput,
-        OutputContainer = Option<IDWrap<Self::EntityOutput>>,
+        OutputContainer = Option<Stored<Self::EntityOutput>>,
     >
     where
         Self: Sized,
@@ -499,7 +580,7 @@ pub trait Queryable {
     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>>>
+    ) -> impl Queryable<EntityOutput = AD::RemoteEntity, OutputContainer = Vec<Stored<AD::RemoteEntity>>>
     where
         Self: Sized,
     {
@@ -510,7 +591,7 @@ 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 OutputContainer = Vec<Stored<T>>;
     type StaticVersion = &'static IDMap<T>;
 
     fn build(&self) -> Query {
@@ -529,7 +610,7 @@ impl<'a, T: Entity> Queryable for &'a IDMap<T> {
 // Generic implementation for all assoc specification types
 impl<'a, AI: AssocInterface> Queryable for &'a AI {
     type EntityOutput = AI::RemoteEntity;
-    type OutputContainer = Vec<IDWrap<AI::RemoteEntity>>;
+    type OutputContainer = Vec<Stored<AI::RemoteEntity>>;
     type StaticVersion = &'static AI;
 
     fn build(&self) -> Query {

+ 8 - 16
microrm/src/query/components.rs

@@ -5,11 +5,9 @@ use crate::{
     prelude::Queryable,
     query::{AssocInterface, QueryPart},
     schema::{
-        datum::{
-            Datum, DatumList, DatumListRef, DatumVisitor, QueryEquivalent, QueryEquivalentList,
-        },
+        datum::{Datum, DatumVisitor, QueryEquivalent, QueryEquivalentList},
         entity::{Entity, EntityPart, EntityPartList, EntityPartVisitor},
-        DatumDiscriminator, IDWrap,
+        DatumDiscriminator, Stored,
     },
 };
 
@@ -18,7 +16,6 @@ use super::Query;
 /// 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>,
@@ -47,7 +44,7 @@ impl<WEP: EntityPart, Parent: Queryable + 'static> Queryable
     for CanonicalWithComponent<WEP, Parent>
 {
     type EntityOutput = WEP::Entity;
-    type OutputContainer = Option<IDWrap<WEP::Entity>>;
+    type OutputContainer = Option<Stored<WEP::Entity>>;
     type StaticVersion = Self;
 
     fn build(&self) -> Query {
@@ -95,7 +92,6 @@ pub(crate) struct UniqueComponent<
     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>,
@@ -107,11 +103,7 @@ impl<
         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: EL,
-    ) -> Self {
+    pub fn new(parent: Parent, datum: EL) -> Self {
         Self {
             datum,
             parent,
@@ -129,7 +121,7 @@ pub(crate) struct CanonicalUniqueComponent<E: Entity, Parent: Queryable> {
 
 impl<E: Entity, Parent: Queryable + 'static> Queryable for CanonicalUniqueComponent<E, Parent> {
     type EntityOutput = E;
-    type OutputContainer = Option<IDWrap<E>>;
+    type OutputContainer = Option<Stored<E>>;
     type StaticVersion = Self;
 
     fn build(&self) -> Query {
@@ -150,7 +142,7 @@ impl<
     > Queryable for UniqueComponent<E, Parent, EL>
 {
     type EntityOutput = E;
-    type OutputContainer = Option<IDWrap<E>>;
+    type OutputContainer = Option<Stored<E>>;
     type StaticVersion = CanonicalUniqueComponent<E, Parent::StaticVersion>;
 
     fn build(&self) -> Query {
@@ -206,7 +198,7 @@ impl<Parent: Queryable> SingleComponent<Parent> {
 
 impl<Parent: Queryable> Queryable for SingleComponent<Parent> {
     type EntityOutput = Parent::EntityOutput;
-    type OutputContainer = Option<IDWrap<Self::EntityOutput>>;
+    type OutputContainer = Option<Stored<Self::EntityOutput>>;
     type StaticVersion = SingleComponent<Parent::StaticVersion>;
 
     fn build(&self) -> Query {
@@ -251,7 +243,7 @@ impl<
     > Queryable for JoinComponent<R, L, EP, Parent>
 {
     type EntityOutput = R;
-    type OutputContainer = Vec<IDWrap<R>>;
+    type OutputContainer = Vec<Stored<R>>;
     type StaticVersion = JoinComponent<R, L, EP, Parent::StaticVersion>;
 
     fn build(&self) -> Query {

+ 43 - 14
microrm/src/schema.rs

@@ -6,15 +6,13 @@
 //! - local: the current side of an association
 //! - remote: the opposite side of an association
 
-use std::cell::RefCell;
-
 use query::Queryable;
 
 use crate::{
     db::{Connection, StatementContext, StatementRow},
     query::{self, AssocInterface},
     schema::datum::Datum,
-    schema::entity::{Entity, EntityPartList, EntityVisitor},
+    schema::entity::{Entity, EntityVisitor},
 };
 use crate::{DBResult, Error};
 
@@ -32,14 +30,19 @@ mod tests;
 // ----------------------------------------------------------------------
 
 /// Wrapper struct that holds both an EntityID and an Entity itself.
-pub struct IDWrap<T: Entity> {
+pub struct Stored<T: Entity> {
+    db: Connection,
     id: T::ID,
     wrap: T,
 }
 
-impl<T: Entity> IDWrap<T> {
-    pub fn new(id: T::ID, value: T) -> Self {
-        Self { id, wrap: value }
+impl<T: Entity> Stored<T> {
+    pub(crate) fn new(db: Connection, id: T::ID, value: T) -> Self {
+        Self {
+            db,
+            id,
+            wrap: value,
+        }
     }
 
     pub fn id(&self) -> T::ID {
@@ -49,34 +52,38 @@ impl<T: Entity> IDWrap<T> {
     pub fn wrapped(self) -> T {
         self.wrap
     }
+
+    pub fn sync(&mut self) -> DBResult<()> {
+        query::update_entity(&self.db, self)
+    }
 }
 
-impl<T: Entity> AsRef<T> for IDWrap<T> {
+impl<T: Entity> AsRef<T> for Stored<T> {
     fn as_ref(&self) -> &T {
         &self.wrap
     }
 }
 
-impl<T: Entity> AsMut<T> for IDWrap<T> {
+impl<T: Entity> AsMut<T> for Stored<T> {
     fn as_mut(&mut self) -> &mut T {
         &mut self.wrap
     }
 }
 
-impl<T: Entity> std::ops::Deref for IDWrap<T> {
+impl<T: Entity> std::ops::Deref for Stored<T> {
     type Target = T;
     fn deref(&self) -> &Self::Target {
         &self.wrap
     }
 }
 
-impl<T: Entity> std::ops::DerefMut for IDWrap<T> {
+impl<T: Entity> std::ops::DerefMut for Stored<T> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.wrap
     }
 }
 
-impl<T: Entity> PartialEq for IDWrap<T> {
+impl<T: Entity> PartialEq for Stored<T> {
     fn eq(&self, other: &Self) -> bool {
         self.id == other.id
     }
@@ -217,6 +224,10 @@ impl<T: Entity> Datum for AssocMap<T> {
             _ghost: Default::default(),
         })
     }
+
+    fn update_adata(&mut self, adata: AssocData) {
+        self.data = Some(adata);
+    }
 }
 
 /// The domain part of a full Relation definition.
@@ -302,6 +313,10 @@ impl<R: Relation> Datum for AssocDomain<R> {
             _ghost: Default::default(),
         })
     }
+
+    fn update_adata(&mut self, adata: AssocData) {
+        self.data = Some(adata);
+    }
 }
 
 /// The range part of a full Relation definition.
@@ -387,6 +402,10 @@ impl<R: Relation> Datum for AssocRange<R> {
             _ghost: Default::default(),
         })
     }
+
+    fn update_adata(&mut self, adata: AssocData) {
+        self.data = Some(adata);
+    }
 }
 
 /// Stores an arbitrary Rust data type as serialized JSON in a string field.
@@ -422,6 +441,12 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> AsRef<T> for Serialized<
     }
 }
 
+impl<T: serde::Serialize + serde::de::DeserializeOwned> AsMut<T> for Serialized<T> {
+    fn as_mut(&mut self) -> &mut T {
+        &mut self.wrapped
+    }
+}
+
 impl<T: 'static + serde::Serialize + serde::de::DeserializeOwned> Datum for Serialized<T> {
     fn sql_type() -> &'static str {
         "text"
@@ -473,13 +498,17 @@ impl<T: Entity> IDMap<T> {
     }
 
     /// Look up an Entity in this map by ID.
-    pub fn by_id(&self, id: T::ID) -> DBResult<Option<IDWrap<T>>> {
+    pub fn by_id(&self, id: T::ID) -> DBResult<Option<Stored<T>>> {
         self.with(id, &id).first().get()
     }
 
     /// Insert a new Entity into this map, and return its new ID.
     pub fn insert(&self, value: T) -> DBResult<T::ID> {
-        query::insert(self.conn(), value)
+        query::insert(self.conn(), &value)
+    }
+
+    pub fn insert_and_return(&self, value: T) -> DBResult<Stored<T>> {
+        query::insert_and_return(self.conn(), value)
     }
 }
 

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

@@ -21,6 +21,7 @@ pub trait Datum {
     fn build_from(adata: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized;
+    fn update_adata(&mut self, _adata: AssocData) {}
 
     fn accept_entity_visitor(_: &mut impl EntityVisitor) {}
     fn accept_discriminator(d: &mut impl DatumDiscriminator)

+ 0 - 16
microrm/src/schema/datum/datum_list.rs

@@ -75,19 +75,3 @@ 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
 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);
-datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5);
-datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6);
-datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7);
-datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8);
-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);
-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);
-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);*/

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

@@ -37,6 +37,7 @@ pub trait EntityPart: 'static {
 pub trait EntityPartVisitor {
     fn visit<EP: EntityPart>(&mut self) {}
     fn visit_datum<EP: EntityPart>(&mut self, _datum: &EP::Datum) {}
+    fn visit_datum_mut<EP: EntityPart>(&mut self, _datum: &mut EP::Datum) {}
 }
 
 /// List of EntityParts.
@@ -67,6 +68,7 @@ pub trait Entity: 'static {
     fn entity_name() -> &'static str;
     fn accept_part_visitor(visitor: &mut impl EntityPartVisitor);
     fn accept_part_visitor_ref(&self, visitor: &mut impl EntityPartVisitor);
+    fn accept_part_visitor_mut(&mut self, _: &mut impl EntityPartVisitor);
 }
 
 /// Visitor for traversing all `Entity`s in a container.

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

@@ -109,6 +109,9 @@ mod manual_test_db {
         fn accept_part_visitor_ref(&self, visitor: &mut impl EntityPartVisitor) {
             visitor.visit_datum::<SimpleEntityName>(&self.name);
         }
+        fn accept_part_visitor_mut(&mut self, visitor: &mut impl EntityPartVisitor) {
+            visitor.visit_datum_mut::<SimpleEntityName>(&mut self.name);
+        }
     }
 
     struct SimpleDatabase {
@@ -305,6 +308,38 @@ mod derive_tests {
 
         assert!(db.people.by_id(id).expect("couldn't query db").is_none());
     }
+
+    #[test]
+    fn update_test() {
+        let db = PeopleDB::open_path(":memory:").expect("couldn't open test db");
+
+        let mut stored = db
+            .people
+            .insert_and_return(Person {
+                name: "person_name".to_string(),
+                roles: Default::default(),
+            })
+            .expect("couldn't insert test person");
+
+        assert_eq!(
+            db.people
+                .with(Person::Name, "person_name")
+                .count()
+                .expect("couldn't execute count query"),
+            1
+        );
+
+        stored.name = "another_person_name".into();
+        stored.sync();
+
+        assert_eq!(
+            db.people
+                .with(Person::Name, "person_name")
+                .count()
+                .expect("couldn't execute count query"),
+            0
+        );
+    }
 }
 
 mod mutual_relationship {