Skip to main content

rustrails_record/
persistence.rs

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/// Async persistence helpers for [`Record`] types. Use `Persistence` for the sync API.
18#[allow(async_fn_in_trait)]
19pub(crate) trait AsyncPersistence: Record + AsyncQuerying {
20    /// Saves the record, inserting or updating based on [`RecordState`].
21    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    /// Saves the record and preserves validation-style error reporting.
64    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    /// Marks the record readonly for the rest of its lifetime.
76    #[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    /// Returns `true` when the record has been marked readonly.
85    fn readonly(&self) -> bool {
86        dirty::readonly(self)
87    }
88
89    /// Touches `updated_at` and any additional timestamp columns without running a full save.
90    #[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    /// Casts the current record into another record type using shared serialized attributes.
135    #[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    /// Creates a new record from a string-keyed attribute map.
150    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    /// Inserts multiple records at once.
168    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    /// Inserts a record, or updates it if a matching row already exists.
187    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    /// Inserts or updates multiple records at once.
210    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    /// Updates the record with the provided attributes and saves the result.
230    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    /// Deletes the record from the database and marks it destroyed.
260    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    /// Reloads the record from the database.
274    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    /// Deletes a row by primary key without instantiating a wrapper value.
285    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    /// Updates all rows matching the provided conditions and returns the affected row count.
299    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    /// Deletes all rows matching the provided conditions and returns the affected row count.
318    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}