1use std::collections::HashMap;
2
3use chrono::Utc;
4use sea_orm::{
5 ActiveModelBehavior, ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait,
6 IntoActiveModel, Iterable, QueryFilter, sea_query::Expr,
7};
8use serde::{Serialize, de::DeserializeOwned};
9use serde_json::Value;
10
11use crate::dirty::{self, ChangeMap};
12use crate::querying::AsyncQuerying;
13use crate::{
14 Record, RecordError, RecordState, relation::json_to_sea_value, relation::resolve_column,
15};
16
17#[allow(async_fn_in_trait)]
19pub(crate) trait AsyncPersistence: Record + AsyncQuerying {
20 async fn save(&mut self, db: &DatabaseConnection) -> Result<(), RecordError>
22 where
23 Self: Serialize,
24 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
25 <Self::Entity as EntityTrait>::Model:
26 IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
27 <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
28 {
29 if self.new_record() && !dirty::has_snapshot(self) {
30 dirty::clear_state(self);
31 }
32 if dirty::readonly(self) {
33 return Err(RecordError::ReadOnlyRecord);
34 }
35 if self.destroyed() {
36 return Err(RecordError::NotSaved);
37 }
38
39 let pending_changes = if self.persisted() {
40 ensure_persisted_snapshot(self, db).await?;
41 dirty::current_changes(self)?
42 } else {
43 ChangeMap::new()
44 };
45
46 if self.persisted() && pending_changes.is_empty() {
47 return Ok(());
48 }
49
50 let model = match self.record_state() {
51 RecordState::New => self.to_active_model().insert(db).await?,
52 RecordState::Persisted => self.to_active_model().update(db).await?,
53 RecordState::Destroyed => return Err(RecordError::NotSaved),
54 };
55
56 let mut record = Self::from_sea_model(model);
57 record.set_record_state(RecordState::Persisted);
58 *self = record;
59 dirty::commit_saved_changes(self, pending_changes)?;
60 Ok(())
61 }
62
63 async fn save_bang(&mut self, db: &DatabaseConnection) -> Result<(), RecordError>
65 where
66 Self: Serialize,
67 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
68 <Self::Entity as EntityTrait>::Model:
69 IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
70 <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
71 {
72 self.save(db).await
73 }
74
75 #[allow(dead_code)]
77 fn readonly_bang(&mut self)
78 where
79 Self: Serialize,
80 {
81 dirty::mark_readonly(self).expect("readonly records should serialize");
82 }
83
84 fn readonly(&self) -> bool {
86 dirty::readonly(self)
87 }
88
89 #[allow(dead_code)]
91 async fn touch(&mut self, fields: &[&str], db: &DatabaseConnection) -> Result<(), RecordError>
92 where
93 Self: Serialize + DeserializeOwned,
94 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
95 {
96 if self.readonly() {
97 return Err(RecordError::ReadOnlyRecord);
98 }
99 if !self.persisted() || self.destroyed() {
100 return Err(RecordError::NotSaved);
101 }
102
103 ensure_persisted_snapshot(self, db).await?;
104 let id = self.id().ok_or(RecordError::NotSaved)?;
105 let now = Value::String(Utc::now().to_rfc3339());
106 let mut updates = HashMap::from([("updated_at".to_owned(), now.clone())]);
107 for field in fields {
108 updates.insert((*field).to_owned(), now.clone());
109 }
110
111 let applied_changes = touched_changes(self, &updates);
112 if applied_changes.is_empty() {
113 return Ok(());
114 }
115
116 let affected = Self::update_all(
117 HashMap::from([(Self::primary_key_name().to_owned(), Value::from(id))]),
118 updates.clone(),
119 db,
120 )
121 .await?;
122 if affected == 0 {
123 return Err(RecordError::NotFound);
124 }
125
126 let state = self.record_state();
127 let mut record = merge_attributes(self, updates)?;
128 record.set_record_state(state);
129 *self = record;
130 dirty::apply_persisted_changes(self, &applied_changes)?;
131 Ok(())
132 }
133
134 #[allow(dead_code)]
136 fn becomes<T>(&self) -> Result<T, RecordError>
137 where
138 Self: Serialize,
139 T: Record + Default + Serialize + DeserializeOwned,
140 {
141 let projected = project_attributes::<T, _>(self)?;
142 let mut record: T = serde_json::from_value(projected)
143 .map_err(|error| RecordError::Invalid(error.to_string()))?;
144 record.set_record_state(self.record_state());
145 dirty::copy_tracking_state(self, &record)?;
146 Ok(record)
147 }
148
149 async fn create(
151 attrs: HashMap<String, Value>,
152 db: &DatabaseConnection,
153 ) -> Result<Self, RecordError>
154 where
155 Self: Default + Serialize + DeserializeOwned,
156 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
157 <Self::Entity as EntityTrait>::Model:
158 IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
159 <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
160 {
161 let mut record = merge_attributes(&Self::default(), attrs)?;
162 record.set_record_state(RecordState::New);
163 record.save(db).await?;
164 Ok(record)
165 }
166
167 async fn insert_all(
169 records: Vec<HashMap<String, Value>>,
170 db: &DatabaseConnection,
171 ) -> Result<Vec<Self>, RecordError>
172 where
173 Self: Default + Serialize + DeserializeOwned,
174 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
175 <Self::Entity as EntityTrait>::Model:
176 IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
177 <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
178 {
179 let mut inserted = Vec::with_capacity(records.len());
180 for attrs in records {
181 inserted.push(Self::create(attrs, db).await?);
182 }
183 Ok(inserted)
184 }
185
186 async fn upsert(
188 attrs: HashMap<String, Value>,
189 unique_by: &[&str],
190 db: &DatabaseConnection,
191 ) -> Result<Self, RecordError>
192 where
193 Self: Default + Serialize + DeserializeOwned,
194 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
195 <Self::Entity as EntityTrait>::Model:
196 IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
197 <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
198 {
199 if let Some(conditions) = unique_conditions_from_attrs(unique_by, &attrs)?
200 && let Some(mut existing) = Self::find_by(conditions, db).await?
201 {
202 existing.update_attributes(attrs, db).await?;
203 return Ok(existing);
204 }
205
206 Self::create(attrs, db).await
207 }
208
209 async fn upsert_all(
211 records: Vec<HashMap<String, Value>>,
212 unique_by: &[&str],
213 db: &DatabaseConnection,
214 ) -> Result<Vec<Self>, RecordError>
215 where
216 Self: Default + Serialize + DeserializeOwned,
217 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
218 <Self::Entity as EntityTrait>::Model:
219 IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
220 <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
221 {
222 let mut upserted = Vec::with_capacity(records.len());
223 for attrs in records {
224 upserted.push(Self::upsert(attrs, unique_by, db).await?);
225 }
226 Ok(upserted)
227 }
228
229 async fn update_attributes(
231 &mut self,
232 attrs: HashMap<String, Value>,
233 db: &DatabaseConnection,
234 ) -> Result<(), RecordError>
235 where
236 Self: Serialize + DeserializeOwned,
237 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
238 <Self::Entity as EntityTrait>::Model:
239 IntoActiveModel<<Self::Entity as EntityTrait>::ActiveModel>,
240 <Self::Entity as EntityTrait>::ActiveModel: ActiveModelBehavior + Send,
241 {
242 if self.new_record() && !dirty::has_snapshot(self) {
243 dirty::clear_state(self);
244 }
245 if self.readonly() {
246 return Err(RecordError::ReadOnlyRecord);
247 }
248 if self.destroyed() {
249 return Err(RecordError::NotSaved);
250 }
251
252 let state = self.record_state();
253 let mut record = merge_attributes(self, attrs)?;
254 record.set_record_state(state);
255 *self = record;
256 self.save(db).await
257 }
258
259 async fn destroy(&mut self, db: &DatabaseConnection) -> Result<(), RecordError>
261 where
262 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
263 {
264 if self.readonly() {
265 return Err(RecordError::ReadOnlyRecord);
266 }
267 let id = self.id().ok_or(RecordError::NotSaved)?;
268 Self::delete(id, db).await?;
269 self.set_record_state(RecordState::Destroyed);
270 Ok(())
271 }
272
273 async fn reload(&mut self, db: &DatabaseConnection) -> Result<(), RecordError>
275 where
276 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
277 {
278 let id = self.id().ok_or(RecordError::NotSaved)?;
279 let fresh = Self::find(id, db).await?;
280 *self = fresh;
281 Ok(())
282 }
283
284 async fn delete(id: i64, db: &DatabaseConnection) -> Result<(), RecordError>
286 where
287 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
288 {
289 let mut conditions = HashMap::new();
290 conditions.insert(Self::primary_key_name().to_owned(), Value::from(id));
291 let deleted = Self::destroy_all(conditions, db).await?;
292 if deleted == 0 {
293 return Err(RecordError::NotFound);
294 }
295 Ok(())
296 }
297
298 async fn update_all(
300 conditions: HashMap<String, Value>,
301 updates: HashMap<String, Value>,
302 db: &DatabaseConnection,
303 ) -> Result<u64, RecordError>
304 where
305 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
306 {
307 let relation = Self::r#where(conditions);
308 let mut query = Self::Entity::update_many().filter(relation.condition()?);
309 for (column, value) in updates {
310 let column = resolve_column::<Self>(&column)?;
311 query = query.col_expr(column, Expr::value(json_to_sea_value(&value)?));
312 }
313 let result = query.exec(db).await?;
314 Ok(result.rows_affected)
315 }
316
317 async fn destroy_all(
319 conditions: HashMap<String, Value>,
320 db: &DatabaseConnection,
321 ) -> Result<u64, RecordError>
322 where
323 <Self::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
324 {
325 let result = Self::Entity::delete_many()
326 .filter(Self::r#where(conditions).condition()?)
327 .exec(db)
328 .await?;
329 Ok(result.rows_affected)
330 }
331}
332
333fn merge_attributes<T>(record: &T, attrs: HashMap<String, Value>) -> Result<T, RecordError>
334where
335 T: Serialize + DeserializeOwned,
336{
337 let mut json =
338 serde_json::to_value(record).map_err(|error| RecordError::Invalid(error.to_string()))?;
339 let object = json
340 .as_object_mut()
341 .ok_or_else(|| RecordError::Invalid("record must serialize to a JSON object".to_owned()))?;
342 for (key, value) in attrs {
343 object.insert(key, value);
344 }
345 serde_json::from_value(json).map_err(|error| RecordError::Invalid(error.to_string()))
346}
347
348fn unique_conditions_from_attrs(
349 unique_by: &[&str],
350 attrs: &HashMap<String, Value>,
351) -> Result<Option<HashMap<String, Value>>, RecordError> {
352 if unique_by.is_empty() {
353 return Err(RecordError::Invalid(
354 "unique_by must contain at least one column".to_owned(),
355 ));
356 }
357
358 let mut conditions = HashMap::with_capacity(unique_by.len());
359 for column in unique_by {
360 if column.is_empty() {
361 return Err(RecordError::Invalid(
362 "unique_by columns must be non-empty".to_owned(),
363 ));
364 }
365
366 let Some(value) = attrs.get(*column).cloned() else {
367 return Ok(None);
368 };
369 conditions.insert((*column).to_owned(), value);
370 }
371
372 Ok(Some(conditions))
373}
374
375fn serialize_record<T: Serialize + ?Sized>(record: &T) -> Result<Value, RecordError> {
376 serde_json::to_value(record).map_err(|error| RecordError::Invalid(error.to_string()))
377}
378
379async fn ensure_persisted_snapshot<T>(
380 record: &T,
381 db: &DatabaseConnection,
382) -> Result<(), RecordError>
383where
384 T: Record + AsyncQuerying + Serialize,
385 <T::Entity as EntityTrait>::Column: ColumnTrait + Iterable,
386{
387 if dirty::has_snapshot(record) {
388 return Ok(());
389 }
390
391 let id = record.id().ok_or(RecordError::NotSaved)?;
392 let fresh = T::find(id, db).await?;
393 dirty::initialize_snapshot_value(record, serialize_record(&fresh)?);
394 Ok(())
395}
396
397#[allow(dead_code)]
398fn touched_changes<T>(record: &T, updates: &HashMap<String, Value>) -> ChangeMap
399where
400 T: Record + Serialize,
401{
402 updates
403 .iter()
404 .filter_map(|(field, new)| {
405 let old = dirty::attribute_from_snapshot(record, field).unwrap_or(Value::Null);
406 (old != *new).then(|| (field.clone(), (old, new.clone())))
407 })
408 .collect()
409}
410
411#[allow(dead_code)]
412fn project_attributes<T, U>(source: &U) -> Result<Value, RecordError>
413where
414 T: Default + Serialize,
415 U: Serialize + ?Sized,
416{
417 let mut projected = serialize_record(&T::default())?;
418 let target = projected
419 .as_object_mut()
420 .ok_or_else(|| RecordError::Invalid("record must serialize to a JSON object".to_owned()))?;
421 let source = serialize_record(source)?;
422 let source = source
423 .as_object()
424 .ok_or_else(|| RecordError::Invalid("record must serialize to a JSON object".to_owned()))?;
425
426 for key in target.keys().cloned().collect::<Vec<_>>() {
427 if key == "type" {
428 continue;
429 }
430 if let Some(value) = source.get(&key).cloned() {
431 target.insert(key, value);
432 }
433 }
434
435 Ok(projected)
436}
437
438#[cfg(test)]
439mod tests {
440 use std::collections::HashMap;
441
442 use serde_json::json;
443
444 use super::AsyncPersistence;
445 use crate::{
446 RecordError, RecordState,
447 base::{
448 Record,
449 test_support::{TestUser, seed_users, setup_db},
450 },
451 querying::AsyncQuerying,
452 };
453
454 #[tokio::test]
455 async fn create_inserts_row_and_returns_id() {
456 let db = setup_db().await;
457
458 let user = TestUser::create(
459 HashMap::from([
460 ("name".to_owned(), json!("Alice")),
461 ("email".to_owned(), json!("alice@example.com")),
462 ]),
463 &db,
464 )
465 .await
466 .expect("create should succeed");
467
468 assert!(user.id().is_some());
469 assert!(user.persisted());
470 assert_eq!(TestUser::count(&db).await.expect("count should succeed"), 1);
471 }
472
473 #[tokio::test]
474 async fn save_new_record_inserts_row() {
475 let db = setup_db().await;
476 let mut user = TestUser {
477 name: "Alice".to_owned(),
478 email: "alice@example.com".to_owned(),
479 ..Default::default()
480 };
481
482 user.save(&db).await.expect("save should insert");
483
484 assert!(user.id().is_some());
485 assert!(user.persisted());
486 assert_eq!(TestUser::count(&db).await.expect("count should succeed"), 1);
487 }
488
489 #[tokio::test]
490 async fn save_existing_record_updates_row() {
491 let db = setup_db().await;
492 let mut user = TestUser::create(
493 HashMap::from([
494 ("name".to_owned(), json!("Alice")),
495 ("email".to_owned(), json!("alice@example.com")),
496 ]),
497 &db,
498 )
499 .await
500 .expect("create should succeed");
501
502 user.name = "Alicia".to_owned();
503 user.save(&db).await.expect("save should update");
504
505 let reloaded = TestUser::find(user.id().expect("saved user should have id"), &db)
506 .await
507 .expect("find should succeed");
508 assert_eq!(reloaded.name, "Alicia");
509 }
510
511 #[tokio::test]
512 async fn save_bang_behaves_like_save() {
513 let db = setup_db().await;
514 let mut user = TestUser {
515 name: "Alice".to_owned(),
516 email: "alice@example.com".to_owned(),
517 ..Default::default()
518 };
519
520 user.save_bang(&db).await.expect("save! should succeed");
521
522 assert!(user.persisted());
523 }
524
525 #[tokio::test]
526 async fn update_attributes_merges_and_persists_changes() {
527 let db = setup_db().await;
528 let mut user = TestUser::create(
529 HashMap::from([
530 ("name".to_owned(), json!("Alice")),
531 ("email".to_owned(), json!("alice@example.com")),
532 ]),
533 &db,
534 )
535 .await
536 .expect("create should succeed");
537
538 user.update_attributes(HashMap::from([("name".to_owned(), json!("Alicia"))]), &db)
539 .await
540 .expect("update should succeed");
541
542 assert_eq!(user.name, "Alicia");
543 let from_db = TestUser::find(user.id().expect("user should have id"), &db)
544 .await
545 .expect("find should succeed");
546 assert_eq!(from_db.name, "Alicia");
547 }
548
549 #[tokio::test]
550 async fn destroy_deletes_row_and_marks_destroyed() {
551 let db = setup_db().await;
552 let mut user = TestUser::create(
553 HashMap::from([
554 ("name".to_owned(), json!("Alice")),
555 ("email".to_owned(), json!("alice@example.com")),
556 ]),
557 &db,
558 )
559 .await
560 .expect("create should succeed");
561 let id = user.id().expect("created user should have id");
562
563 user.destroy(&db).await.expect("destroy should succeed");
564
565 assert_eq!(user.record_state(), RecordState::Destroyed);
566 assert!(matches!(
567 TestUser::find(id, &db).await,
568 Err(RecordError::NotFound)
569 ));
570 }
571
572 #[tokio::test]
573 async fn reload_refreshes_from_database() {
574 let db = setup_db().await;
575 let mut user = TestUser::create(
576 HashMap::from([
577 ("name".to_owned(), json!("Alice")),
578 ("email".to_owned(), json!("alice@example.com")),
579 ]),
580 &db,
581 )
582 .await
583 .expect("create should succeed");
584 let id = user.id().expect("created user should have id");
585
586 TestUser::update_all(
587 HashMap::from([("id".to_owned(), json!(id))]),
588 HashMap::from([("name".to_owned(), json!("Alicia"))]),
589 &db,
590 )
591 .await
592 .expect("update_all should succeed");
593 user.reload(&db).await.expect("reload should succeed");
594
595 assert_eq!(user.name, "Alicia");
596 }
597
598 #[tokio::test]
599 async fn create_with_invalid_attributes_fails() {
600 let db = setup_db().await;
601
602 let error = TestUser::create(
603 HashMap::from([
604 ("name".to_owned(), json!(123)),
605 ("email".to_owned(), json!("alice@example.com")),
606 ]),
607 &db,
608 )
609 .await
610 .expect_err("invalid attributes should fail");
611
612 assert!(matches!(error, RecordError::Invalid(_)));
613 }
614
615 #[tokio::test]
616 async fn delete_by_id_removes_row() {
617 let db = setup_db().await;
618 let user = TestUser::create(
619 HashMap::from([
620 ("name".to_owned(), json!("Alice")),
621 ("email".to_owned(), json!("alice@example.com")),
622 ]),
623 &db,
624 )
625 .await
626 .expect("create should succeed");
627 let id = user.id().expect("created user should have id");
628
629 TestUser::delete(id, &db)
630 .await
631 .expect("delete should succeed");
632
633 assert!(matches!(
634 TestUser::find(id, &db).await,
635 Err(RecordError::NotFound)
636 ));
637 }
638
639 #[tokio::test]
640 async fn delete_missing_id_returns_not_found() {
641 let db = setup_db().await;
642
643 let error = TestUser::delete(999, &db)
644 .await
645 .expect_err("missing delete should fail");
646
647 assert!(matches!(error, RecordError::NotFound));
648 }
649
650 #[tokio::test]
651 async fn update_all_returns_affected_row_count() {
652 let db = setup_db().await;
653 seed_users(&db).await;
654
655 let affected = TestUser::update_all(
656 HashMap::from([("name".to_owned(), json!("Bob"))]),
657 HashMap::from([("email".to_owned(), json!("bobby@example.com"))]),
658 &db,
659 )
660 .await
661 .expect("update_all should succeed");
662
663 assert_eq!(affected, 1);
664 let updated = TestUser::find_by(
665 HashMap::from([("email".to_owned(), json!("bobby@example.com"))]),
666 &db,
667 )
668 .await
669 .expect("query should succeed");
670 assert!(updated.is_some());
671 }
672
673 #[tokio::test]
674 async fn destroy_all_returns_deleted_row_count() {
675 let db = setup_db().await;
676 seed_users(&db).await;
677
678 let deleted =
679 TestUser::destroy_all(HashMap::from([("name".to_owned(), json!("Carol"))]), &db)
680 .await
681 .expect("destroy_all should succeed");
682
683 assert_eq!(deleted, 1);
684 assert_eq!(TestUser::count(&db).await.expect("count should succeed"), 2);
685 }
686
687 #[tokio::test]
688 async fn destroyed_record_cannot_be_saved_again() {
689 let db = setup_db().await;
690 let mut user = TestUser::create(
691 HashMap::from([
692 ("name".to_owned(), json!("Alice")),
693 ("email".to_owned(), json!("alice@example.com")),
694 ]),
695 &db,
696 )
697 .await
698 .expect("create should succeed");
699
700 user.destroy(&db).await.expect("destroy should succeed");
701 let error = user
702 .save(&db)
703 .await
704 .expect_err("saving destroyed record should fail");
705
706 assert!(matches!(error, RecordError::NotSaved));
707 }
708
709 #[tokio::test]
710 async fn update_attributes_rejects_unknown_fields() {
711 let db = setup_db().await;
712 let mut user = TestUser::create(
713 HashMap::from([
714 ("name".to_owned(), json!("Alice")),
715 ("email".to_owned(), json!("alice@example.com")),
716 ]),
717 &db,
718 )
719 .await
720 .expect("create should succeed");
721
722 let error = user
723 .update_attributes(HashMap::from([("missing".to_owned(), json!(true))]), &db)
724 .await
725 .expect_err("unknown field should fail");
726
727 assert!(matches!(error, RecordError::Invalid(_)));
728 }
729 #[tokio::test]
730 async fn create_preserves_supplied_attributes() {
731 let db = setup_db().await;
732
733 let user = TestUser::create(
734 HashMap::from([
735 ("name".to_owned(), json!("Dana")),
736 ("email".to_owned(), json!("dana@example.com")),
737 ]),
738 &db,
739 )
740 .await
741 .expect("create should succeed");
742
743 assert_eq!(user.name, "Dana");
744 assert_eq!(user.email, "dana@example.com");
745 }
746
747 #[tokio::test]
748 async fn create_allows_explicit_primary_key() {
749 let db = setup_db().await;
750
751 let user = TestUser::create(
752 HashMap::from([
753 ("id".to_owned(), json!(42)),
754 ("name".to_owned(), json!("Dana")),
755 ("email".to_owned(), json!("dana@example.com")),
756 ]),
757 &db,
758 )
759 .await
760 .expect("create should succeed");
761
762 assert_eq!(user.id(), Some(42));
763 }
764
765 #[tokio::test]
766 async fn create_rejects_unknown_attributes() {
767 let db = setup_db().await;
768
769 let error = TestUser::create(
770 HashMap::from([
771 ("name".to_owned(), json!("Dana")),
772 ("email".to_owned(), json!("dana@example.com")),
773 ("role".to_owned(), json!("admin")),
774 ]),
775 &db,
776 )
777 .await
778 .expect_err("unknown attributes should fail");
779
780 assert!(matches!(error, RecordError::Invalid(_)));
781 }
782
783 #[tokio::test]
784 async fn create_with_duplicate_primary_key_returns_database_error() {
785 let db = setup_db().await;
786 seed_users(&db).await;
787
788 let error = TestUser::create(
789 HashMap::from([
790 ("id".to_owned(), json!(1)),
791 ("name".to_owned(), json!("Dana")),
792 ("email".to_owned(), json!("dana@example.com")),
793 ]),
794 &db,
795 )
796 .await
797 .expect_err("duplicate ids should fail");
798
799 assert!(matches!(error, RecordError::Database(_)));
800 }
801
802 #[tokio::test]
803 async fn save_preserves_existing_primary_key_when_updating() {
804 let db = setup_db().await;
805 let mut user = TestUser::create(
806 HashMap::from([
807 ("name".to_owned(), json!("Dana")),
808 ("email".to_owned(), json!("dana@example.com")),
809 ]),
810 &db,
811 )
812 .await
813 .expect("create should succeed");
814 let id = user.id().expect("created user should have id");
815
816 user.email = "updated@example.com".to_owned();
817 user.save(&db).await.expect("save should update");
818
819 assert_eq!(user.id(), Some(id));
820 }
821
822 #[tokio::test]
823 async fn save_on_destroyed_state_returns_not_saved() {
824 let db = setup_db().await;
825 let mut user = TestUser::persisted(1, "Dana", "dana@example.com");
826 user.set_record_state(RecordState::Destroyed);
827
828 let error = user
829 .save(&db)
830 .await
831 .expect_err("destroyed records cannot be saved");
832
833 assert!(matches!(error, RecordError::NotSaved));
834 }
835
836 #[tokio::test]
837 async fn save_with_duplicate_primary_key_returns_database_error() {
838 let db = setup_db().await;
839 seed_users(&db).await;
840 let mut user = TestUser {
841 id: Some(1),
842 name: "Dana".to_owned(),
843 email: "dana@example.com".to_owned(),
844 ..Default::default()
845 };
846
847 let error = user.save(&db).await.expect_err("duplicate ids should fail");
848
849 assert!(matches!(error, RecordError::Database(_)));
850 }
851
852 #[tokio::test]
853 async fn save_bang_on_destroyed_state_returns_not_saved() {
854 let db = setup_db().await;
855 let mut user = TestUser::persisted(1, "Dana", "dana@example.com");
856 user.set_record_state(RecordState::Destroyed);
857
858 let error = user
859 .save_bang(&db)
860 .await
861 .expect_err("destroyed records cannot be saved");
862
863 assert!(matches!(error, RecordError::NotSaved));
864 }
865
866 #[tokio::test]
867 async fn save_bang_propagates_duplicate_primary_key_errors() {
868 let db = setup_db().await;
869 seed_users(&db).await;
870 let mut user = TestUser {
871 id: Some(1),
872 name: "Dana".to_owned(),
873 email: "dana@example.com".to_owned(),
874 ..Default::default()
875 };
876
877 let error = user
878 .save_bang(&db)
879 .await
880 .expect_err("duplicate ids should fail");
881
882 assert!(matches!(error, RecordError::Database(_)));
883 }
884
885 #[tokio::test]
886 async fn update_attributes_preserves_unspecified_fields() {
887 let db = setup_db().await;
888 let mut user = TestUser::create(
889 HashMap::from([
890 ("name".to_owned(), json!("Dana")),
891 ("email".to_owned(), json!("dana@example.com")),
892 ]),
893 &db,
894 )
895 .await
896 .expect("create should succeed");
897
898 user.update_attributes(HashMap::from([("name".to_owned(), json!("Dani"))]), &db)
899 .await
900 .expect("update should succeed");
901
902 assert_eq!(user.email, "dana@example.com");
903 }
904
905 #[tokio::test]
906 async fn update_attributes_keeps_record_persisted() {
907 let db = setup_db().await;
908 let mut user = TestUser::create(
909 HashMap::from([
910 ("name".to_owned(), json!("Dana")),
911 ("email".to_owned(), json!("dana@example.com")),
912 ]),
913 &db,
914 )
915 .await
916 .expect("create should succeed");
917
918 user.update_attributes(HashMap::from([("name".to_owned(), json!("Dani"))]), &db)
919 .await
920 .expect("update should succeed");
921
922 assert_eq!(user.record_state(), RecordState::Persisted);
923 }
924
925 #[tokio::test]
926 async fn update_attributes_on_new_record_inserts_and_persists() {
927 let db = setup_db().await;
928 let mut user = TestUser::default();
929
930 user.update_attributes(
931 HashMap::from([
932 ("name".to_owned(), json!("Dana")),
933 ("email".to_owned(), json!("dana@example.com")),
934 ]),
935 &db,
936 )
937 .await
938 .expect("update should insert new records");
939
940 assert!(user.id().is_some());
941 assert!(user.persisted());
942 }
943
944 #[tokio::test]
945 async fn update_attributes_on_destroyed_record_returns_not_saved() {
946 let db = setup_db().await;
947 let mut user = TestUser::persisted(1, "Dana", "dana@example.com");
948 user.set_record_state(RecordState::Destroyed);
949
950 let error = user
951 .update_attributes(HashMap::from([("name".to_owned(), json!("Dani"))]), &db)
952 .await
953 .expect_err("destroyed records cannot be updated");
954
955 assert!(matches!(error, RecordError::NotSaved));
956 }
957
958 #[tokio::test]
959 async fn update_attributes_with_type_mismatch_returns_invalid() {
960 let db = setup_db().await;
961 let mut user = TestUser::create(
962 HashMap::from([
963 ("name".to_owned(), json!("Dana")),
964 ("email".to_owned(), json!("dana@example.com")),
965 ]),
966 &db,
967 )
968 .await
969 .expect("create should succeed");
970
971 let error = user
972 .update_attributes(HashMap::from([("id".to_owned(), json!("oops"))]), &db)
973 .await
974 .expect_err("type mismatches should fail");
975
976 assert!(matches!(error, RecordError::Invalid(_)));
977 }
978
979 #[tokio::test]
980 async fn destroy_without_id_returns_not_saved() {
981 let db = setup_db().await;
982 let mut user = TestUser::default();
983
984 let error = user
985 .destroy(&db)
986 .await
987 .expect_err("unsaved records cannot be destroyed");
988
989 assert!(matches!(error, RecordError::NotSaved));
990 }
991
992 #[tokio::test]
993 async fn destroy_after_row_is_missing_returns_not_found() {
994 let db = setup_db().await;
995 let mut user = TestUser::create(
996 HashMap::from([
997 ("name".to_owned(), json!("Dana")),
998 ("email".to_owned(), json!("dana@example.com")),
999 ]),
1000 &db,
1001 )
1002 .await
1003 .expect("create should succeed");
1004 let id = user.id().expect("created user should have id");
1005 TestUser::delete(id, &db)
1006 .await
1007 .expect("delete should remove row");
1008
1009 let error = user
1010 .destroy(&db)
1011 .await
1012 .expect_err("missing rows should fail destroy");
1013
1014 assert!(matches!(error, RecordError::NotFound));
1015 assert_eq!(user.record_state(), RecordState::Persisted);
1016 }
1017
1018 #[tokio::test]
1019 async fn reload_without_id_returns_not_saved() {
1020 let db = setup_db().await;
1021 let mut user = TestUser::default();
1022
1023 let error = user
1024 .reload(&db)
1025 .await
1026 .expect_err("unsaved records cannot reload");
1027
1028 assert!(matches!(error, RecordError::NotSaved));
1029 }
1030
1031 #[tokio::test]
1032 async fn reload_after_row_is_deleted_returns_not_found() {
1033 let db = setup_db().await;
1034 let mut user = TestUser::create(
1035 HashMap::from([
1036 ("name".to_owned(), json!("Dana")),
1037 ("email".to_owned(), json!("dana@example.com")),
1038 ]),
1039 &db,
1040 )
1041 .await
1042 .expect("create should succeed");
1043 let id = user.id().expect("created user should have id");
1044 TestUser::delete(id, &db)
1045 .await
1046 .expect("delete should remove row");
1047
1048 let error = user
1049 .reload(&db)
1050 .await
1051 .expect_err("missing rows should fail reload");
1052
1053 assert!(matches!(error, RecordError::NotFound));
1054 }
1055
1056 #[tokio::test]
1057 async fn reload_restores_persisted_state_from_database() {
1058 let db = setup_db().await;
1059 let mut user = TestUser::create(
1060 HashMap::from([
1061 ("name".to_owned(), json!("Dana")),
1062 ("email".to_owned(), json!("dana@example.com")),
1063 ]),
1064 &db,
1065 )
1066 .await
1067 .expect("create should succeed");
1068 user.set_record_state(RecordState::Destroyed);
1069
1070 user.reload(&db).await.expect("reload should succeed");
1071
1072 assert_eq!(user.record_state(), RecordState::Persisted);
1073 }
1074
1075 #[tokio::test]
1076 async fn update_all_with_unknown_update_column_returns_invalid() {
1077 let db = setup_db().await;
1078 seed_users(&db).await;
1079
1080 let error = TestUser::update_all(
1081 HashMap::from([("name".to_owned(), json!("Alice"))]),
1082 HashMap::from([("missing".to_owned(), json!(true))]),
1083 &db,
1084 )
1085 .await
1086 .expect_err("unknown update columns should fail");
1087
1088 assert!(matches!(error, RecordError::Invalid(_)));
1089 }
1090
1091 #[tokio::test]
1092 async fn update_all_with_unknown_condition_column_returns_invalid() {
1093 let db = setup_db().await;
1094 seed_users(&db).await;
1095
1096 let error = TestUser::update_all(
1097 HashMap::from([("missing".to_owned(), json!(true))]),
1098 HashMap::from([("email".to_owned(), json!("updated@example.com"))]),
1099 &db,
1100 )
1101 .await
1102 .expect_err("unknown condition columns should fail");
1103
1104 assert!(matches!(error, RecordError::Invalid(_)));
1105 }
1106
1107 #[tokio::test]
1108 async fn update_all_with_null_value_returns_invalid() {
1109 let db = setup_db().await;
1110 seed_users(&db).await;
1111
1112 let error = TestUser::update_all(
1113 HashMap::from([("name".to_owned(), json!("Alice"))]),
1114 HashMap::from([("email".to_owned(), serde_json::Value::Null)]),
1115 &db,
1116 )
1117 .await
1118 .expect_err("null updates should fail");
1119
1120 assert!(matches!(error, RecordError::Invalid(_)));
1121 }
1122
1123 #[tokio::test]
1124 async fn update_all_with_empty_conditions_updates_all_rows() {
1125 let db = setup_db().await;
1126 seed_users(&db).await;
1127
1128 let affected = TestUser::update_all(
1129 HashMap::new(),
1130 HashMap::from([("email".to_owned(), json!("updated@example.com"))]),
1131 &db,
1132 )
1133 .await
1134 .expect("update_all should succeed");
1135
1136 assert_eq!(affected, 3);
1137 let users = TestUser::all(&db).await.expect("all should succeed");
1138 assert!(users.iter().all(|user| user.email == "updated@example.com"));
1139 }
1140
1141 #[tokio::test]
1142 async fn destroy_all_with_unknown_condition_column_returns_invalid() {
1143 let db = setup_db().await;
1144 seed_users(&db).await;
1145
1146 let error =
1147 TestUser::destroy_all(HashMap::from([("missing".to_owned(), json!(true))]), &db)
1148 .await
1149 .expect_err("unknown condition columns should fail");
1150
1151 assert!(matches!(error, RecordError::Invalid(_)));
1152 }
1153
1154 #[tokio::test]
1155 async fn destroy_all_with_empty_conditions_deletes_all_rows() {
1156 let db = setup_db().await;
1157 seed_users(&db).await;
1158
1159 let deleted = TestUser::destroy_all(HashMap::new(), &db)
1160 .await
1161 .expect("destroy_all should succeed");
1162
1163 assert_eq!(deleted, 3);
1164 assert_eq!(TestUser::count(&db).await.expect("count should succeed"), 0);
1165 }
1166
1167 #[tokio::test]
1168 async fn destroy_all_with_no_matches_returns_zero() {
1169 let db = setup_db().await;
1170 seed_users(&db).await;
1171
1172 let deleted =
1173 TestUser::destroy_all(HashMap::from([("name".to_owned(), json!("Nobody"))]), &db)
1174 .await
1175 .expect("destroy_all should succeed");
1176
1177 assert_eq!(deleted, 0);
1178 }
1179
1180 #[tokio::test]
1181 async fn upsert_creates_new_record_when_none_exists() {
1182 let db = setup_db().await;
1183
1184 let user = TestUser::upsert(
1185 HashMap::from([
1186 ("name".to_owned(), json!("Alice")),
1187 ("email".to_owned(), json!("alice@example.com")),
1188 ]),
1189 &["email"],
1190 &db,
1191 )
1192 .await
1193 .expect("upsert should create a missing row");
1194
1195 assert!(user.id().is_some());
1196 assert!(user.persisted());
1197 assert_eq!(TestUser::count(&db).await.expect("count should succeed"), 1);
1198 }
1199
1200 #[tokio::test]
1201 async fn upsert_updates_existing_record_when_match_exists() {
1202 let db = setup_db().await;
1203 let existing = TestUser::create(
1204 HashMap::from([
1205 ("name".to_owned(), json!("Alice")),
1206 ("email".to_owned(), json!("alice@example.com")),
1207 ]),
1208 &db,
1209 )
1210 .await
1211 .expect("seed create should succeed");
1212
1213 let user = TestUser::upsert(
1214 HashMap::from([
1215 ("name".to_owned(), json!("Updated Alice")),
1216 ("email".to_owned(), json!("alice@example.com")),
1217 ]),
1218 &["email"],
1219 &db,
1220 )
1221 .await
1222 .expect("upsert should update the matching row");
1223
1224 assert_eq!(user.id(), existing.id());
1225 assert_eq!(TestUser::count(&db).await.expect("count should succeed"), 1);
1226
1227 let reloaded = TestUser::find(existing.id().expect("existing record should have id"), &db)
1228 .await
1229 .expect("find should reload the updated row");
1230 assert_eq!(reloaded.name, "Updated Alice");
1231 assert_eq!(reloaded.email, "alice@example.com");
1232 }
1233
1234 #[tokio::test]
1235 async fn upsert_all_handles_mixed_new_and_existing_records() {
1236 let db = setup_db().await;
1237 let existing = TestUser::create(
1238 HashMap::from([
1239 ("name".to_owned(), json!("Alice")),
1240 ("email".to_owned(), json!("alice@example.com")),
1241 ]),
1242 &db,
1243 )
1244 .await
1245 .expect("seed create should succeed");
1246
1247 let users = TestUser::upsert_all(
1248 vec![
1249 HashMap::from([
1250 ("name".to_owned(), json!("Updated Alice")),
1251 ("email".to_owned(), json!("alice@example.com")),
1252 ]),
1253 HashMap::from([
1254 ("name".to_owned(), json!("Bob")),
1255 ("email".to_owned(), json!("bob@example.com")),
1256 ]),
1257 ],
1258 &["email"],
1259 &db,
1260 )
1261 .await
1262 .expect("upsert_all should handle mixed rows");
1263
1264 assert_eq!(users.len(), 2);
1265 assert_eq!(TestUser::count(&db).await.expect("count should succeed"), 2);
1266
1267 let updated = TestUser::find(existing.id().expect("existing record should have id"), &db)
1268 .await
1269 .expect("find should reload the updated row");
1270 assert_eq!(updated.name, "Updated Alice");
1271
1272 let inserted = TestUser::find_by(
1273 HashMap::from([("email".to_owned(), json!("bob@example.com"))]),
1274 &db,
1275 )
1276 .await
1277 .expect("find_by should succeed")
1278 .expect("new row should exist");
1279 assert_eq!(inserted.name, "Bob");
1280 }
1281
1282 #[test]
1283 fn merge_attributes_overwrites_existing_fields() {
1284 let user = TestUser::persisted(7, "Dana", "dana@example.com");
1285
1286 let merged =
1287 super::merge_attributes(&user, HashMap::from([("name".to_owned(), json!("Dani"))]))
1288 .expect("merge should succeed");
1289
1290 assert_eq!(merged.name, "Dani");
1291 }
1292
1293 #[test]
1294 fn merge_attributes_preserves_unmentioned_fields() {
1295 let user = TestUser::persisted(7, "Dana", "dana@example.com");
1296
1297 let merged =
1298 super::merge_attributes(&user, HashMap::from([("name".to_owned(), json!("Dani"))]))
1299 .expect("merge should succeed");
1300
1301 assert_eq!(merged.email, "dana@example.com");
1302 assert_eq!(merged.id(), Some(7));
1303 }
1304
1305 #[test]
1306 fn merge_attributes_rejects_unknown_fields_directly() {
1307 let user = TestUser::persisted(7, "Dana", "dana@example.com");
1308
1309 let error =
1310 super::merge_attributes(&user, HashMap::from([("role".to_owned(), json!("admin"))]))
1311 .expect_err("unknown fields should fail");
1312
1313 assert!(matches!(error, RecordError::Invalid(_)));
1314 }
1315
1316 #[test]
1317 fn merge_attributes_rejects_type_mismatches_directly() {
1318 let user = TestUser::persisted(7, "Dana", "dana@example.com");
1319
1320 let error =
1321 super::merge_attributes(&user, HashMap::from([("id".to_owned(), json!("oops"))]))
1322 .expect_err("type mismatches should fail");
1323
1324 assert!(matches!(error, RecordError::Invalid(_)));
1325 }
1326
1327 mod dirty_tracking_and_touch_tests {
1328 use std::collections::HashMap;
1329 use std::sync::atomic::{AtomicI64, Ordering};
1330
1331 use sea_orm::{
1332 ActiveModelTrait, ActiveValue::NotSet, ActiveValue::Set, ConnectionTrait, Database,
1333 DatabaseConnection, EntityTrait, Schema,
1334 };
1335 use serde::{Deserialize, Serialize};
1336 use serde_json::Value;
1337
1338 use crate::{
1339 Record, RecordError, RecordState,
1340 dirty::{self, DirtyTracking},
1341 querying::AsyncQuerying,
1342 };
1343
1344 use super::AsyncPersistence;
1345
1346 fn persisted_state() -> RecordState {
1347 RecordState::Persisted
1348 }
1349
1350 pub mod tracked_user {
1351 use sea_orm::entity::prelude::*;
1352
1353 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
1354 #[sea_orm(table_name = "tracked_users")]
1355 pub struct Model {
1356 #[sea_orm(primary_key)]
1357 pub id: i32,
1358 pub name: String,
1359 pub email: String,
1360 pub updated_at: String,
1361 pub touched_at: String,
1362 pub save_marker: String,
1363 #[sea_orm(column_name = "type")]
1364 pub record_type: String,
1365 }
1366
1367 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
1368 pub enum Relation {}
1369
1370 impl ActiveModelBehavior for ActiveModel {}
1371 }
1372
1373 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1374 struct TrackedUser {
1375 id: Option<i64>,
1376 name: String,
1377 email: String,
1378 updated_at: String,
1379 touched_at: String,
1380 save_marker: String,
1381 #[serde(rename = "type")]
1382 record_type: String,
1383 #[serde(skip, default = "persisted_state")]
1384 state: RecordState,
1385 }
1386
1387 impl Default for TrackedUser {
1388 fn default() -> Self {
1389 Self {
1390 id: None,
1391 name: String::new(),
1392 email: String::new(),
1393 updated_at: "original-updated".to_owned(),
1394 touched_at: "original-touched".to_owned(),
1395 save_marker: "seed".to_owned(),
1396 record_type: "TrackedUser".to_owned(),
1397 state: RecordState::New,
1398 }
1399 }
1400 }
1401
1402 impl Record for TrackedUser {
1403 type Entity = tracked_user::Entity;
1404
1405 fn table_name() -> &'static str {
1406 "tracked_users"
1407 }
1408
1409 fn id(&self) -> Option<i64> {
1410 self.id
1411 }
1412
1413 fn record_state(&self) -> RecordState {
1414 self.state
1415 }
1416
1417 fn set_record_state(&mut self, state: RecordState) {
1418 self.state = state;
1419 }
1420
1421 fn from_sea_model(model: <Self::Entity as EntityTrait>::Model) -> Self {
1422 Self {
1423 id: Some(i64::from(model.id)),
1424 name: model.name,
1425 email: model.email,
1426 updated_at: model.updated_at,
1427 touched_at: model.touched_at,
1428 save_marker: model.save_marker,
1429 record_type: model.record_type,
1430 state: RecordState::Persisted,
1431 }
1432 }
1433
1434 fn to_active_model(&self) -> <Self::Entity as EntityTrait>::ActiveModel
1435 where
1436 <Self::Entity as EntityTrait>::ActiveModel: ActiveModelTrait,
1437 {
1438 tracked_user::ActiveModel {
1439 id: match self.id.and_then(|value| i32::try_from(value).ok()) {
1440 Some(value) => Set(value),
1441 None => NotSet,
1442 },
1443 name: Set(self.name.clone()),
1444 email: Set(self.email.clone()),
1445 updated_at: Set(self.updated_at.clone()),
1446 touched_at: Set(self.touched_at.clone()),
1447 save_marker: Set(self.save_marker.clone()),
1448 record_type: Set(self.record_type.clone()),
1449 }
1450 }
1451 }
1452
1453 impl AsyncQuerying for TrackedUser {}
1454 impl AsyncPersistence for TrackedUser {}
1455
1456 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1457 struct PromotedTrackedUser {
1458 id: Option<i64>,
1459 name: String,
1460 email: String,
1461 updated_at: String,
1462 touched_at: String,
1463 save_marker: String,
1464 nickname: String,
1465 #[serde(rename = "type")]
1466 record_type: String,
1467 #[serde(skip, default = "persisted_state")]
1468 state: RecordState,
1469 }
1470
1471 impl Default for PromotedTrackedUser {
1472 fn default() -> Self {
1473 Self {
1474 id: None,
1475 name: String::new(),
1476 email: String::new(),
1477 updated_at: String::new(),
1478 touched_at: String::new(),
1479 save_marker: String::new(),
1480 nickname: "guest".to_owned(),
1481 record_type: "PromotedTrackedUser".to_owned(),
1482 state: RecordState::New,
1483 }
1484 }
1485 }
1486
1487 impl Record for PromotedTrackedUser {
1488 type Entity = tracked_user::Entity;
1489
1490 fn table_name() -> &'static str {
1491 "tracked_users"
1492 }
1493
1494 fn id(&self) -> Option<i64> {
1495 self.id
1496 }
1497
1498 fn record_state(&self) -> RecordState {
1499 self.state
1500 }
1501
1502 fn set_record_state(&mut self, state: RecordState) {
1503 self.state = state;
1504 }
1505
1506 fn from_sea_model(model: <Self::Entity as EntityTrait>::Model) -> Self {
1507 Self {
1508 id: Some(i64::from(model.id)),
1509 name: model.name,
1510 email: model.email,
1511 updated_at: model.updated_at,
1512 touched_at: model.touched_at,
1513 save_marker: model.save_marker,
1514 record_type: model.record_type,
1515 ..Self::default()
1516 }
1517 }
1518
1519 fn to_active_model(&self) -> <Self::Entity as EntityTrait>::ActiveModel
1520 where
1521 <Self::Entity as EntityTrait>::ActiveModel: ActiveModelTrait,
1522 {
1523 tracked_user::ActiveModel {
1524 id: match self.id.and_then(|value| i32::try_from(value).ok()) {
1525 Some(value) => Set(value),
1526 None => NotSet,
1527 },
1528 name: Set(self.name.clone()),
1529 email: Set(self.email.clone()),
1530 updated_at: Set(self.updated_at.clone()),
1531 touched_at: Set(self.touched_at.clone()),
1532 save_marker: Set(self.save_marker.clone()),
1533 record_type: Set(self.record_type.clone()),
1534 }
1535 }
1536 }
1537
1538 impl AsyncQuerying for PromotedTrackedUser {}
1539 impl AsyncPersistence for PromotedTrackedUser {}
1540
1541 async fn setup_tracked_db() -> DatabaseConnection {
1542 let db = Database::connect("sqlite::memory:")
1543 .await
1544 .expect("sqlite in-memory connection should succeed");
1545 let schema = Schema::new(db.get_database_backend());
1546 db.execute(&schema.create_table_from_entity(tracked_user::Entity))
1547 .await
1548 .expect("tracked_users table should be created");
1549 db.execute_unprepared(
1550 "CREATE TRIGGER tracked_users_update_marker AFTER UPDATE ON tracked_users BEGIN UPDATE tracked_users SET save_marker = save_marker || ':updated' WHERE id = NEW.id; END;",
1551 )
1552 .await
1553 .expect("update trigger should be created");
1554 db
1555 }
1556
1557 static NEXT_TRACKED_ID: AtomicI64 = AtomicI64::new(1);
1558
1559 fn tracked_attrs(name: &str, email: &str) -> HashMap<String, Value> {
1560 let id = NEXT_TRACKED_ID.fetch_add(1, Ordering::Relaxed);
1561 HashMap::from([
1562 ("id".to_owned(), Value::from(id)),
1563 ("name".to_owned(), Value::String(name.to_owned())),
1564 ("email".to_owned(), Value::String(email.to_owned())),
1565 ])
1566 }
1567
1568 #[tokio::test]
1569 async fn save_returns_readonly_error_when_record_is_marked_readonly() {
1570 let db = super::setup_db().await;
1571 let mut user =
1572 super::TestUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1573 .await
1574 .expect("create should succeed");
1575 user.readonly_bang();
1576 user.name = "Alicia".to_owned();
1577
1578 let error = user.save(&db).await.expect_err("readonly save should fail");
1579
1580 assert!(matches!(error, RecordError::ReadOnlyRecord));
1581 }
1582
1583 #[tokio::test]
1584 async fn update_attributes_returns_readonly_error_when_record_is_marked_readonly() {
1585 let db = super::setup_db().await;
1586 let mut user =
1587 super::TestUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1588 .await
1589 .expect("create should succeed");
1590 user.readonly_bang();
1591
1592 let error = user
1593 .update_attributes(
1594 HashMap::from([("name".to_owned(), Value::String("Alicia".to_owned()))]),
1595 &db,
1596 )
1597 .await
1598 .expect_err("readonly update should fail");
1599
1600 assert!(matches!(error, RecordError::ReadOnlyRecord));
1601 }
1602
1603 #[tokio::test]
1604 async fn destroy_returns_readonly_error_when_record_is_marked_readonly() {
1605 let db = super::setup_db().await;
1606 let mut user =
1607 super::TestUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1608 .await
1609 .expect("create should succeed");
1610 user.readonly_bang();
1611
1612 let error = user
1613 .destroy(&db)
1614 .await
1615 .expect_err("readonly destroy should fail");
1616
1617 assert!(matches!(error, RecordError::ReadOnlyRecord));
1618 }
1619
1620 #[tokio::test]
1621 async fn failed_save_does_not_clear_dirty_changes() {
1622 let db = super::setup_db().await;
1623 let mut user =
1624 super::TestUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1625 .await
1626 .expect("create should succeed");
1627 user.name = "Alicia".to_owned();
1628 user.readonly_bang();
1629
1630 let error = user.save(&db).await.expect_err("readonly save should fail");
1631
1632 assert!(matches!(error, RecordError::ReadOnlyRecord));
1633 assert!(user.changed());
1634 assert_eq!(
1635 user.attribute_was("name"),
1636 Some(Value::String("Alice".to_owned()))
1637 );
1638 }
1639
1640 #[tokio::test]
1641 async fn save_skips_database_update_when_record_is_unchanged() {
1642 let db = setup_tracked_db().await;
1643 let mut user = TrackedUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1644 .await
1645 .expect("create should succeed");
1646 let id = user.id().expect("persisted record should have id");
1647
1648 user.save(&db)
1649 .await
1650 .expect("unchanged save should short-circuit");
1651
1652 let reloaded = TrackedUser::find(id, &db)
1653 .await
1654 .expect("find should succeed");
1655 assert_eq!(reloaded.save_marker, "seed");
1656 }
1657
1658 #[tokio::test]
1659 async fn save_clears_dirty_changes_after_successful_update() {
1660 let db = setup_tracked_db().await;
1661 let mut user = TrackedUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1662 .await
1663 .expect("create should succeed");
1664 user.name = "Alicia".to_owned();
1665
1666 user.save(&db).await.expect("save should persist changes");
1667
1668 assert!(!user.changed());
1669 assert_eq!(
1670 user.previous_changes().get("name"),
1671 Some(&(
1672 Value::String("Alice".to_owned()),
1673 Value::String("Alicia".to_owned())
1674 ))
1675 );
1676 }
1677
1678 #[tokio::test]
1679 async fn touch_rejects_new_records() {
1680 let db = setup_tracked_db().await;
1681 let mut user = TrackedUser::default();
1682
1683 let error = user
1684 .touch(&[], &db)
1685 .await
1686 .expect_err("touch should reject new records");
1687
1688 assert!(matches!(error, RecordError::NotSaved));
1689 }
1690
1691 #[tokio::test]
1692 async fn touch_rejects_destroyed_records() {
1693 let db = setup_tracked_db().await;
1694 let mut user = TrackedUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1695 .await
1696 .expect("create should succeed");
1697 user.set_record_state(RecordState::Destroyed);
1698
1699 let error = user
1700 .touch(&[], &db)
1701 .await
1702 .expect_err("touch should reject destroyed records");
1703
1704 assert!(matches!(error, RecordError::NotSaved));
1705 }
1706
1707 #[tokio::test]
1708 async fn touch_rejects_readonly_records() {
1709 let db = setup_tracked_db().await;
1710 let mut user = TrackedUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1711 .await
1712 .expect("create should succeed");
1713 user.readonly_bang();
1714
1715 let error = user
1716 .touch(&[], &db)
1717 .await
1718 .expect_err("touch should reject readonly records");
1719
1720 assert!(matches!(error, RecordError::ReadOnlyRecord));
1721 }
1722
1723 #[tokio::test]
1724 async fn touch_updates_only_updated_at_by_default() {
1725 let db = setup_tracked_db().await;
1726 let mut user = TrackedUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1727 .await
1728 .expect("create should succeed");
1729 let id = user.id().expect("persisted record should have id");
1730 let touched_before = user.touched_at.clone();
1731 let updated_before = user.updated_at.clone();
1732
1733 user.touch(&[], &db).await.expect("touch should succeed");
1734
1735 assert_ne!(user.updated_at, updated_before);
1736 assert_eq!(user.touched_at, touched_before);
1737 let reloaded = TrackedUser::find(id, &db)
1738 .await
1739 .expect("find should succeed");
1740 assert_eq!(reloaded.touched_at, touched_before);
1741 }
1742
1743 #[tokio::test]
1744 async fn touch_updates_requested_timestamp_columns() {
1745 let db = setup_tracked_db().await;
1746 let mut user = TrackedUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1747 .await
1748 .expect("create should succeed");
1749 let updated_before = user.updated_at.clone();
1750 let touched_before = user.touched_at.clone();
1751
1752 user.touch(&["touched_at"], &db)
1753 .await
1754 .expect("touch should update both timestamp columns");
1755
1756 assert_ne!(user.updated_at, updated_before);
1757 assert_ne!(user.touched_at, touched_before);
1758 assert_eq!(
1759 user.previous_changes()
1760 .get("touched_at")
1761 .map(|(_, new)| new),
1762 Some(&Value::String(user.touched_at.clone()))
1763 );
1764 }
1765
1766 #[tokio::test]
1767 async fn touch_preserves_other_unsaved_dirty_changes() {
1768 let db = setup_tracked_db().await;
1769 let mut user = TrackedUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1770 .await
1771 .expect("create should succeed");
1772 user.name = "Alicia".to_owned();
1773
1774 user.touch(&["touched_at"], &db)
1775 .await
1776 .expect("touch should persist only timestamps");
1777
1778 assert!(user.attribute_changed("name"));
1779 assert!(!user.attribute_changed("updated_at"));
1780 assert!(!user.attribute_changed("touched_at"));
1781 assert_eq!(
1782 user.attribute_was("name"),
1783 Some(Value::String("Alice".to_owned()))
1784 );
1785 }
1786
1787 #[tokio::test]
1788 async fn becomes_projects_shared_attributes_and_preserves_record_state() {
1789 let db = setup_tracked_db().await;
1790 let mut user = TrackedUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1791 .await
1792 .expect("create should succeed");
1793 user.set_record_state(RecordState::Destroyed);
1794
1795 let promoted = user
1796 .becomes::<PromotedTrackedUser>()
1797 .expect("becomes should succeed");
1798
1799 assert_eq!(promoted.id, user.id);
1800 assert_eq!(promoted.name, user.name);
1801 assert_eq!(promoted.email, user.email);
1802 assert_eq!(promoted.record_state(), RecordState::Destroyed);
1803 assert_eq!(promoted.nickname, "guest");
1804 assert_eq!(promoted.record_type, "PromotedTrackedUser");
1805 }
1806
1807 #[tokio::test]
1808 async fn becomes_preserves_dirty_tracking_state_for_shared_fields() {
1809 let db = setup_tracked_db().await;
1810 let mut user = TrackedUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1811 .await
1812 .expect("create should succeed");
1813 user.name = "Alicia".to_owned();
1814 user.readonly_bang();
1815
1816 let promoted = user
1817 .becomes::<PromotedTrackedUser>()
1818 .expect("becomes should succeed");
1819
1820 assert!(promoted.attribute_changed("name"));
1821 assert_eq!(
1822 promoted.attribute_was("name"),
1823 Some(Value::String("Alice".to_owned()))
1824 );
1825 assert!(dirty::readonly(&promoted));
1826 }
1827
1828 #[tokio::test]
1829 async fn becomes_uses_target_defaults_for_missing_columns() {
1830 let db = setup_tracked_db().await;
1831 let user = TrackedUser::create(tracked_attrs("Alice", "alice@example.com"), &db)
1832 .await
1833 .expect("create should succeed");
1834
1835 let promoted = user
1836 .becomes::<PromotedTrackedUser>()
1837 .expect("becomes should succeed");
1838
1839 assert_eq!(promoted.nickname, "guest");
1840 assert_eq!(promoted.record_type, "PromotedTrackedUser");
1841 }
1842 }
1843}