Skip to main content

rustrails_record/
sync_persistence.rs

1use std::collections::HashMap;
2
3use sea_orm::{ActiveModelBehavior, ColumnTrait, EntityTrait, IntoActiveModel, Iterable};
4use serde::{Serialize, de::DeserializeOwned};
5use serde_json::Value;
6
7use crate::{RecordError, persistence::AsyncPersistence};
8use rustrails_support::{database, runtime};
9
10/// Synchronous persistence interface for [`Record`] types.
11#[allow(private_bounds)]
12pub trait Persistence: AsyncPersistence {
13    /// Saves the record, inserting or updating based on its current state.
14    fn save_sync(&mut self) -> Result<(), RecordError>
15    where
16        Self: Serialize,
17        <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
18        <Self::Entity as EntityTrait>::Model:
19            IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
20        <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
21    {
22        database::with_db(|db| runtime::block_on(self.save(db)))
23    }
24
25    /// Saves the record and preserves validation-style error reporting.
26    fn save_bang_sync(&mut self) -> Result<(), RecordError>
27    where
28        Self: Serialize,
29        <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
30        <Self::Entity as EntityTrait>::Model:
31            IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
32        <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
33    {
34        database::with_db(|db| runtime::block_on(self.save_bang(db)))
35    }
36
37    /// Creates a new record from a string-keyed attribute map.
38    fn create_sync(attrs: HashMap<String, Value>) -> Result<Self, RecordError>
39    where
40        Self: Default + Serialize + DeserializeOwned,
41        <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
42        <Self::Entity as EntityTrait>::Model:
43            IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
44        <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
45    {
46        database::with_db(|db| runtime::block_on(Self::create(attrs, db)))
47    }
48
49    /// Inserts multiple records at once.
50    fn insert_all_sync(records: Vec<HashMap<String, Value>>) -> Result<Vec<Self>, RecordError>
51    where
52        Self: Default + Serialize + DeserializeOwned,
53        <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
54        <Self::Entity as EntityTrait>::Model:
55            IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
56        <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
57    {
58        database::with_db(|db| runtime::block_on(Self::insert_all(records, db)))
59    }
60
61    /// Inserts a record, or updates it if a matching row already exists.
62    fn upsert_sync(attrs: HashMap<String, Value>, unique_by: &[&str]) -> Result<Self, RecordError>
63    where
64        Self: Default + Serialize + DeserializeOwned,
65        <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
66        <Self::Entity as EntityTrait>::Model:
67            IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
68        <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
69    {
70        database::with_db(|db| runtime::block_on(Self::upsert(attrs, unique_by, db)))
71    }
72
73    /// Inserts or updates multiple records at once.
74    fn upsert_all_sync(
75        records: Vec<HashMap<String, Value>>,
76        unique_by: &[&str],
77    ) -> Result<Vec<Self>, RecordError>
78    where
79        Self: Default + Serialize + DeserializeOwned,
80        <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
81        <Self::Entity as EntityTrait>::Model:
82            IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
83        <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
84    {
85        database::with_db(|db| runtime::block_on(Self::upsert_all(records, unique_by, db)))
86    }
87
88    /// Updates the record with the provided attributes and saves the result.
89    fn update_attributes_sync(&mut self, attrs: HashMap<String, Value>) -> Result<(), RecordError>
90    where
91        Self: Serialize + DeserializeOwned,
92        <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
93        <Self::Entity as EntityTrait>::Model:
94            IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
95        <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
96    {
97        database::with_db(|db| runtime::block_on(self.update_attributes(attrs, db)))
98    }
99
100    /// Deletes the record from the database and marks it destroyed.
101    fn destroy_sync(&mut self) -> Result<(), RecordError>
102    where
103        <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
104    {
105        database::with_db(|db| runtime::block_on(self.destroy(db)))
106    }
107
108    /// Reloads the record from the database.
109    fn reload_sync(&mut self) -> Result<(), RecordError>
110    where
111        <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
112    {
113        database::with_db(|db| runtime::block_on(self.reload(db)))
114    }
115
116    /// Deletes a row by primary key without instantiating a wrapper value.
117    fn delete_sync(id: i64) -> Result<(), RecordError>
118    where
119        <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
120    {
121        database::with_db(|db| runtime::block_on(Self::delete(id, db)))
122    }
123
124    /// Updates all rows matching the provided conditions and returns the affected row count.
125    fn update_all_sync(
126        conditions: HashMap<String, Value>,
127        updates: HashMap<String, Value>,
128    ) -> Result<u64, RecordError>
129    where
130        <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
131    {
132        database::with_db(|db| runtime::block_on(Self::update_all(conditions, updates, db)))
133    }
134
135    /// Deletes all rows matching the provided conditions and returns the affected row count.
136    fn destroy_all_sync(conditions: HashMap<String, Value>) -> Result<u64, RecordError>
137    where
138        <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
139    {
140        database::with_db(|db| runtime::block_on(Self::destroy_all(conditions, db)))
141    }
142}
143
144impl<T: AsyncPersistence> Persistence for T {}
145
146#[cfg(test)]
147mod tests {
148    use std::collections::HashMap;
149
150    use sea_orm::{ConnectionTrait, Schema};
151    use serde_json::{Value, json};
152
153    use super::Persistence;
154    use crate::{
155        Querying, Record, RecordError, RecordState,
156        base::test_support::{TestUser, test_user},
157    };
158    use rustrails_support::{database, runtime};
159
160    fn setup_sync_db() -> tokio::runtime::Runtime {
161        let runtime_handle = runtime::init_runtime();
162        database::establish("sqlite::memory:").expect("sqlite in-memory connection should succeed");
163        runtime::block_on(async {
164            let db = database::db();
165            let schema = Schema::new(db.get_database_backend());
166            db.execute(&schema.create_table_from_entity(test_user::Entity))
167                .await
168                .expect("test_users table should be created");
169        });
170        runtime_handle
171    }
172
173    fn user_attrs(name: &str, email: &str) -> HashMap<String, Value> {
174        HashMap::from([
175            ("name".to_owned(), json!(name)),
176            ("email".to_owned(), json!(email)),
177        ])
178    }
179
180    fn run_sync_test(test: impl FnOnce() + Send + 'static) {
181        let handle = std::thread::spawn(test);
182        if let Err(payload) = handle.join() {
183            std::panic::resume_unwind(payload);
184        }
185    }
186
187    fn with_sync_db(test: impl FnOnce(&tokio::runtime::Runtime) + Send + 'static) {
188        run_sync_test(move || {
189            let runtime_handle = setup_sync_db();
190            test(&runtime_handle);
191        });
192    }
193
194    #[test]
195    fn create_sync_inserts_a_row_and_returns_it() {
196        with_sync_db(|_| {
197            let user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
198                .expect("create_sync should succeed");
199
200            assert!(user.id().is_some());
201            assert!(user.persisted());
202            assert_eq!(
203                TestUser::count_sync().expect("count_sync should succeed"),
204                1
205            );
206        });
207    }
208
209    #[test]
210    fn create_sync_preserves_supplied_attributes() {
211        with_sync_db(|_| {
212            let user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
213                .expect("create_sync should succeed");
214
215            assert_eq!(user.name, "Dana");
216            assert_eq!(user.email, "dana@example.com");
217        });
218    }
219
220    #[test]
221    fn create_sync_allows_explicit_primary_key() {
222        with_sync_db(|_| {
223            let user = TestUser::create_sync(HashMap::from([
224                ("id".to_owned(), json!(42)),
225                ("name".to_owned(), json!("Dana")),
226                ("email".to_owned(), json!("dana@example.com")),
227            ]))
228            .expect("create_sync should succeed");
229
230            assert_eq!(user.id(), Some(42));
231            assert!(user.persisted());
232        });
233    }
234
235    #[test]
236    fn create_sync_with_empty_attributes_uses_defaults_and_persists() {
237        with_sync_db(|_| {
238            let user =
239                TestUser::create_sync(HashMap::new()).expect("empty create_sync should succeed");
240
241            assert!(user.id().is_some());
242            assert_eq!(user.name, "");
243            assert_eq!(user.email, "");
244            assert!(user.persisted());
245        });
246    }
247
248    #[test]
249    fn create_sync_rejects_unknown_attributes() {
250        with_sync_db(|_| {
251            let error = TestUser::create_sync(HashMap::from([
252                ("name".to_owned(), json!("Dana")),
253                ("email".to_owned(), json!("dana@example.com")),
254                ("role".to_owned(), json!("admin")),
255            ]))
256            .expect_err("unknown attributes should fail");
257
258            assert!(matches!(error, RecordError::Invalid(_)));
259        });
260    }
261
262    #[test]
263    fn create_sync_with_invalid_attributes_fails() {
264        with_sync_db(|_| {
265            let error = TestUser::create_sync(HashMap::from([
266                ("name".to_owned(), json!(123)),
267                ("email".to_owned(), json!("alice@example.com")),
268            ]))
269            .expect_err("invalid attributes should fail");
270
271            assert!(matches!(error, RecordError::Invalid(_)));
272        });
273    }
274
275    #[test]
276    fn create_sync_with_duplicate_primary_key_returns_database_error() {
277        with_sync_db(|_| {
278            let duplicate_id = 70_001;
279            let _existing = TestUser::create_sync(HashMap::from([
280                ("id".to_owned(), json!(duplicate_id)),
281                ("name".to_owned(), json!("Alice")),
282                ("email".to_owned(), json!("alice@example.com")),
283            ]))
284            .expect("initial create_sync should succeed");
285
286            let error = TestUser::create_sync(HashMap::from([
287                ("id".to_owned(), json!(duplicate_id)),
288                ("name".to_owned(), json!("Dana")),
289                ("email".to_owned(), json!("dana@example.com")),
290            ]))
291            .expect_err("duplicate ids should fail");
292
293            assert!(matches!(error, RecordError::Database(_)));
294        });
295    }
296
297    #[test]
298    fn save_sync_inserts_new_records() {
299        with_sync_db(|_| {
300            let mut user = TestUser {
301                name: "Alice".to_owned(),
302                email: "alice@example.com".to_owned(),
303                ..Default::default()
304            };
305
306            user.save_sync().expect("save_sync should insert");
307
308            assert!(user.id().is_some());
309            assert!(user.persisted());
310            assert_eq!(
311                TestUser::count_sync().expect("count_sync should succeed"),
312                1
313            );
314        });
315    }
316
317    #[test]
318    fn save_sync_updates_existing_records() {
319        with_sync_db(|_| {
320            let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
321                .expect("create_sync should succeed");
322
323            user.name = "Alicia".to_owned();
324            user.save_sync().expect("save_sync should update");
325
326            let reloaded = TestUser::find_sync(user.id().expect("saved user should have id"))
327                .expect("find_sync should succeed");
328            assert_eq!(reloaded.name, "Alicia");
329        });
330    }
331
332    #[test]
333    fn save_sync_preserves_existing_primary_key_when_updating() {
334        with_sync_db(|_| {
335            let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
336                .expect("create_sync should succeed");
337            let id = user.id().expect("created user should have id");
338
339            user.email = "updated@example.com".to_owned();
340            user.save_sync().expect("save_sync should update");
341
342            assert_eq!(user.id(), Some(id));
343        });
344    }
345
346    #[test]
347    fn save_sync_on_destroyed_state_returns_not_saved() {
348        with_sync_db(|_| {
349            let mut user = TestUser::persisted(1, "Dana", "dana@example.com");
350            user.set_record_state(RecordState::Destroyed);
351
352            let error = user
353                .save_sync()
354                .expect_err("destroyed records cannot be saved");
355
356            assert!(matches!(error, RecordError::NotSaved));
357        });
358    }
359
360    #[test]
361    fn save_sync_with_duplicate_primary_key_returns_database_error() {
362        with_sync_db(|_| {
363            let duplicate_id = 70_002;
364            let _existing = TestUser::create_sync(HashMap::from([
365                ("id".to_owned(), json!(duplicate_id)),
366                ("name".to_owned(), json!("Alice")),
367                ("email".to_owned(), json!("alice@example.com")),
368            ]))
369            .expect("initial create_sync should succeed");
370
371            let mut user = TestUser {
372                id: Some(duplicate_id),
373                name: "Dana".to_owned(),
374                email: "dana@example.com".to_owned(),
375                ..Default::default()
376            };
377
378            let error = user.save_sync().expect_err("duplicate ids should fail");
379
380            assert!(matches!(error, RecordError::Database(_)));
381        });
382    }
383
384    #[test]
385    fn save_bang_sync_behaves_like_save_sync() {
386        with_sync_db(|_| {
387            let mut user = TestUser {
388                name: "Alice".to_owned(),
389                email: "alice@example.com".to_owned(),
390                ..Default::default()
391            };
392
393            user.save_bang_sync()
394                .expect("save_bang_sync should succeed");
395
396            assert!(user.persisted());
397            assert_eq!(
398                TestUser::count_sync().expect("count_sync should succeed"),
399                1
400            );
401        });
402    }
403
404    #[test]
405    fn save_bang_sync_on_destroyed_state_returns_not_saved() {
406        with_sync_db(|_| {
407            let mut user = TestUser::persisted(1, "Dana", "dana@example.com");
408            user.set_record_state(RecordState::Destroyed);
409
410            let error = user
411                .save_bang_sync()
412                .expect_err("destroyed records cannot be saved");
413
414            assert!(matches!(error, RecordError::NotSaved));
415        });
416    }
417
418    #[test]
419    fn save_bang_sync_propagates_duplicate_primary_key_errors() {
420        with_sync_db(|_| {
421            let duplicate_id = 70_003;
422            let _existing = TestUser::create_sync(HashMap::from([
423                ("id".to_owned(), json!(duplicate_id)),
424                ("name".to_owned(), json!("Alice")),
425                ("email".to_owned(), json!("alice@example.com")),
426            ]))
427            .expect("initial create_sync should succeed");
428
429            let mut user = TestUser {
430                id: Some(duplicate_id),
431                name: "Dana".to_owned(),
432                email: "dana@example.com".to_owned(),
433                ..Default::default()
434            };
435
436            let error = user
437                .save_bang_sync()
438                .expect_err("duplicate ids should fail");
439
440            assert!(matches!(error, RecordError::Database(_)));
441        });
442    }
443
444    #[test]
445    fn update_attributes_sync_merges_and_persists_changes() {
446        with_sync_db(|_| {
447            let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
448                .expect("create_sync should succeed");
449
450            user.update_attributes_sync(HashMap::from([("name".to_owned(), json!("Alicia"))]))
451                .expect("update_attributes_sync should succeed");
452
453            assert_eq!(user.name, "Alicia");
454            let from_db = TestUser::find_sync(user.id().expect("user should have id"))
455                .expect("find_sync should succeed");
456            assert_eq!(from_db.name, "Alicia");
457        });
458    }
459
460    #[test]
461    fn update_attributes_sync_preserves_unspecified_fields() {
462        with_sync_db(|_| {
463            let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
464                .expect("create_sync should succeed");
465
466            user.update_attributes_sync(HashMap::from([("name".to_owned(), json!("Dani"))]))
467                .expect("update_attributes_sync should succeed");
468
469            assert_eq!(user.email, "dana@example.com");
470        });
471    }
472
473    #[test]
474    fn update_attributes_sync_keeps_record_persisted() {
475        with_sync_db(|_| {
476            let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
477                .expect("create_sync should succeed");
478
479            user.update_attributes_sync(HashMap::from([("name".to_owned(), json!("Dani"))]))
480                .expect("update_attributes_sync should succeed");
481
482            assert_eq!(user.record_state(), RecordState::Persisted);
483        });
484    }
485
486    #[test]
487    fn update_attributes_sync_on_new_record_inserts_and_persists() {
488        with_sync_db(|_| {
489            let mut user = TestUser::default();
490
491            user.update_attributes_sync(user_attrs("Dana", "dana@example.com"))
492                .expect("update_attributes_sync should insert new records");
493
494            assert!(user.id().is_some());
495            assert!(user.persisted());
496        });
497    }
498
499    #[test]
500    fn update_attributes_sync_rejects_unknown_fields() {
501        with_sync_db(|_| {
502            let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
503                .expect("create_sync should succeed");
504
505            let error = user
506                .update_attributes_sync(HashMap::from([("missing".to_owned(), json!(true))]))
507                .expect_err("unknown field should fail");
508
509            assert!(matches!(error, RecordError::Invalid(_)));
510        });
511    }
512
513    #[test]
514    fn update_attributes_sync_on_destroyed_record_returns_not_saved() {
515        with_sync_db(|_| {
516            let mut user = TestUser::persisted(1, "Dana", "dana@example.com");
517            user.set_record_state(RecordState::Destroyed);
518
519            let error = user
520                .update_attributes_sync(HashMap::from([("name".to_owned(), json!("Dani"))]))
521                .expect_err("destroyed records cannot be updated");
522
523            assert!(matches!(error, RecordError::NotSaved));
524        });
525    }
526
527    #[test]
528    fn update_attributes_sync_with_type_mismatch_returns_invalid() {
529        with_sync_db(|_| {
530            let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
531                .expect("create_sync should succeed");
532
533            let error = user
534                .update_attributes_sync(HashMap::from([("id".to_owned(), json!("oops"))]))
535                .expect_err("type mismatches should fail");
536
537            assert!(matches!(error, RecordError::Invalid(_)));
538        });
539    }
540
541    #[test]
542    fn destroy_sync_deletes_rows_and_marks_records_destroyed() {
543        with_sync_db(|_| {
544            let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
545                .expect("create_sync should succeed");
546            let id = user.id().expect("created user should have id");
547
548            user.destroy_sync().expect("destroy_sync should succeed");
549
550            assert_eq!(user.record_state(), RecordState::Destroyed);
551            assert!(matches!(
552                TestUser::find_sync(id),
553                Err(RecordError::NotFound)
554            ));
555            assert_eq!(
556                TestUser::count_sync().expect("count_sync should succeed"),
557                0
558            );
559        });
560    }
561
562    #[test]
563    fn destroyed_record_cannot_be_saved_again() {
564        with_sync_db(|_| {
565            let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
566                .expect("create_sync should succeed");
567
568            user.destroy_sync().expect("destroy_sync should succeed");
569            let error = user
570                .save_sync()
571                .expect_err("saving destroyed record should fail");
572
573            assert!(matches!(error, RecordError::NotSaved));
574        });
575    }
576
577    #[test]
578    fn destroy_sync_without_id_returns_not_saved() {
579        with_sync_db(|_| {
580            let mut user = TestUser::default();
581
582            let error = user
583                .destroy_sync()
584                .expect_err("unsaved records cannot be destroyed");
585
586            assert!(matches!(error, RecordError::NotSaved));
587        });
588    }
589
590    #[test]
591    fn destroy_sync_after_row_is_missing_returns_not_found() {
592        with_sync_db(|_| {
593            let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
594                .expect("create_sync should succeed");
595            let id = user.id().expect("created user should have id");
596
597            TestUser::delete_sync(id).expect("delete_sync should remove row");
598            let error = user
599                .destroy_sync()
600                .expect_err("missing rows should fail destroy_sync");
601
602            assert!(matches!(error, RecordError::NotFound));
603            assert_eq!(user.record_state(), RecordState::Persisted);
604        });
605    }
606
607    #[test]
608    fn reload_sync_refreshes_from_the_database() {
609        with_sync_db(|_| {
610            let mut user = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
611                .expect("create_sync should succeed");
612            let id = user.id().expect("created user should have id");
613
614            TestUser::update_all_sync(
615                HashMap::from([("id".to_owned(), json!(id))]),
616                HashMap::from([("name".to_owned(), json!("Alicia"))]),
617            )
618            .expect("update_all_sync should succeed");
619            user.reload_sync().expect("reload_sync should succeed");
620
621            assert_eq!(user.name, "Alicia");
622        });
623    }
624
625    #[test]
626    fn reload_sync_without_id_returns_not_saved() {
627        with_sync_db(|_| {
628            let mut user = TestUser::default();
629
630            let error = user
631                .reload_sync()
632                .expect_err("unsaved records cannot reload");
633
634            assert!(matches!(error, RecordError::NotSaved));
635        });
636    }
637
638    #[test]
639    fn reload_sync_after_row_is_deleted_returns_not_found() {
640        with_sync_db(|_| {
641            let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
642                .expect("create_sync should succeed");
643            let id = user.id().expect("created user should have id");
644
645            TestUser::delete_sync(id).expect("delete_sync should remove row");
646            let error = user
647                .reload_sync()
648                .expect_err("missing rows should fail reload_sync");
649
650            assert!(matches!(error, RecordError::NotFound));
651        });
652    }
653
654    #[test]
655    fn reload_sync_restores_persisted_state_from_database() {
656        with_sync_db(|_| {
657            let mut user = TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
658                .expect("create_sync should succeed");
659            user.set_record_state(RecordState::Destroyed);
660
661            user.reload_sync().expect("reload_sync should succeed");
662
663            assert_eq!(user.record_state(), RecordState::Persisted);
664        });
665    }
666
667    #[test]
668    fn delete_sync_removes_rows_by_id_without_loading_record() {
669        with_sync_db(|_| {
670            let id = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
671                .expect("create_sync should succeed")
672                .id()
673                .expect("created user should have id");
674
675            TestUser::delete_sync(id).expect("delete_sync should succeed");
676
677            assert!(matches!(
678                TestUser::find_sync(id),
679                Err(RecordError::NotFound)
680            ));
681            assert_eq!(
682                TestUser::count_sync().expect("count_sync should succeed"),
683                0
684            );
685        });
686    }
687
688    #[test]
689    fn delete_sync_missing_id_returns_not_found() {
690        with_sync_db(|_| {
691            let error = TestUser::delete_sync(999).expect_err("missing delete_sync should fail");
692
693            assert!(matches!(error, RecordError::NotFound));
694        });
695    }
696
697    #[test]
698    fn sync_persistence_helpers_can_run_inside_the_same_async_runtime() {
699        with_sync_db(|runtime_handle| {
700            let user = runtime_handle.block_on(async {
701                TestUser::create_sync(user_attrs("Dana", "dana@example.com"))
702                    .expect("create_sync should work inside async context")
703            });
704
705            assert_eq!(user.name, "Dana");
706        });
707    }
708
709    #[test]
710    fn persistence_trait_is_the_sync_api() {
711        run_sync_test(|| {
712            fn assert_sync_api<T: Persistence>() {}
713            assert_sync_api::<crate::base::test_support::TestUser>();
714        });
715    }
716
717    #[test]
718    fn upsert_sync_creates_a_new_record_when_none_exists() {
719        with_sync_db(|_| {
720            let user = TestUser::upsert_sync(user_attrs("Alice", "alice@example.com"), &["email"])
721                .expect("upsert_sync should create a missing row");
722
723            assert!(user.id().is_some());
724            assert!(user.persisted());
725            assert_eq!(
726                TestUser::count_sync().expect("count_sync should succeed"),
727                1
728            );
729        });
730    }
731
732    #[test]
733    fn upsert_sync_updates_an_existing_record_when_match_exists() {
734        with_sync_db(|_| {
735            let existing = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
736                .expect("seed create_sync should succeed");
737
738            let user =
739                TestUser::upsert_sync(user_attrs("Updated Alice", "alice@example.com"), &["email"])
740                    .expect("upsert_sync should update the matching row");
741
742            assert_eq!(user.id(), existing.id());
743            assert_eq!(
744                TestUser::count_sync().expect("count_sync should succeed"),
745                1
746            );
747
748            let reloaded =
749                TestUser::find_sync(existing.id().expect("existing record should have id"))
750                    .expect("find_sync should reload the updated row");
751            assert_eq!(reloaded.name, "Updated Alice");
752            assert_eq!(reloaded.email, "alice@example.com");
753        });
754    }
755
756    #[test]
757    fn upsert_all_sync_handles_mixed_new_and_existing_records() {
758        with_sync_db(|_| {
759            let existing = TestUser::create_sync(user_attrs("Alice", "alice@example.com"))
760                .expect("seed create_sync should succeed");
761
762            let users = TestUser::upsert_all_sync(
763                vec![
764                    user_attrs("Updated Alice", "alice@example.com"),
765                    user_attrs("Bob", "bob@example.com"),
766                ],
767                &["email"],
768            )
769            .expect("upsert_all_sync should handle mixed rows");
770
771            assert_eq!(users.len(), 2);
772            assert_eq!(
773                TestUser::count_sync().expect("count_sync should succeed"),
774                2
775            );
776
777            let updated =
778                TestUser::find_sync(existing.id().expect("existing record should have id"))
779                    .expect("find_sync should reload the updated row");
780            assert_eq!(updated.name, "Updated Alice");
781
782            let inserted = TestUser::find_by_sync(HashMap::from([(
783                "email".to_owned(),
784                json!("bob@example.com"),
785            )]))
786            .expect("find_by_sync should succeed")
787            .expect("new row should exist");
788            assert_eq!(inserted.name, "Bob");
789        });
790    }
791}