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#[allow(private_bounds)]
12pub trait Persistence: AsyncPersistence {
13 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 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 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 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 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 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 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 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 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 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 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 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}