Skip to main content

sofie_db/
unit_of_work.rs

1use std::any::{Any, TypeId};
2use std::sync::Arc;
3
4use sofie_core::{Cast, ColumnReader, Error, Result, RowReader, SqlValue, Uuid};
5
6use crate::aggregate::{Snapshot, Table, table};
7use crate::sql::same_values;
8use crate::type_cache::TypeCache;
9use crate::{Aggregate, Dialect, Outcome, Plan, Reach, Reply, Stamp, Step, Write};
10
11impl<A: Aggregate> Outcome for A {
12    fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
13        plan.raise(self.meta().events());
14        let Some(pending) = pending_write(self, plan.stamp()) else {
15            return Ok(());
16        };
17        #[cfg_attr(not(feature = "children"), expect(unused_variables))]
18        let owned_through_parent = refuse_beyond_reach(self, plan.reach())?;
19        #[cfg(feature = "module-roles")]
20        plan.act_as(A::MODULE);
21        #[cfg(feature = "children")]
22        crate::children::check_loaded_parent(self, plan, owned_through_parent);
23        let meta = self.meta();
24        if let (Some(loaded), false) = (meta.snapshot.as_ref(), pending.changed) {
25            plan.push(update::<A>(meta.id(), loaded, pending.row, plan.dialect()));
26            return Ok(());
27        }
28        let written = pending.row;
29        let write = Write {
30            table: table::<A>(),
31            id: meta.id(),
32            owner: self.owner(),
33            stamp: plan.stamp(),
34            loaded: meta.snapshot.as_ref(),
35            fields: &written.fields,
36            stored: &written.stored,
37        };
38        for field in A::UNIQUE {
39            let index = format!("{}_{field}", write.table.name);
40            plan.unique_fields
41                .push(crate::outcome::UniqueField::column(A::MODULE, index, field));
42        }
43        for (behavior, _) in &write.table.behaviors {
44            behavior.planned(&write, plan)?;
45        }
46        #[cfg(any(feature = "contracts", feature = "publishing"))]
47        crate::change_event::plan_change_event(self, &write, plan)?;
48        let step = match meta.snapshot.as_ref() {
49            None => insert::<A>(meta.id(), written, plan.dialect()),
50            Some(loaded) => {
51                self.refuse_unlisted_moves(&mut RowReader::new(&loaded.fields))?;
52                #[cfg(feature = "children")]
53                crate::children::refuse_unlisted_moves(self)?;
54                update::<A>(meta.id(), loaded, written, plan.dialect())
55            }
56        };
57        plan.push(step);
58        #[cfg(feature = "cache")]
59        plan.announcements.changed(crate::ChangedRecord {
60            module: A::MODULE,
61            aggregate: A::NAME,
62            id: Some(meta.id()),
63        });
64        #[cfg(feature = "live")]
65        plan_live_changes(self, plan);
66        #[cfg(feature = "children")]
67        crate::children::plan_children(self, plan, owned_through_parent);
68        Ok(())
69    }
70
71    fn committed(&mut self, stamp: Stamp) {
72        let written = pending_write(self, stamp).map(|pending| pending.row);
73        self.meta_mut().written(written);
74        #[cfg(feature = "children")]
75        crate::children::committed(self);
76    }
77
78    fn reply(self) -> Result<Reply> {
79        Ok(Reply::Json(serde_json::json!({
80            "id": self.meta().id().to_string(),
81            "version": self.meta().version(),
82        })))
83    }
84}
85
86/// A record handed to another owner leaves the screens of the one it was loaded with too.
87#[cfg(feature = "live")]
88fn plan_live_changes<A: Aggregate>(aggregate: &A, plan: &mut Plan<'_>) {
89    let owner = aggregate.owner();
90    plan.announcements
91        .live_change(crate::live::TableChange::of::<A>(owner, plan.reach()));
92    let loaded = aggregate
93        .meta()
94        .snapshot
95        .as_ref()
96        .and_then(loaded_owner::<A>);
97    if loaded.is_some() && loaded != owner {
98        plan.announcements
99            .live_change(crate::live::TableChange::of::<A>(loaded, plan.reach()));
100    }
101}
102
103/// The row a save writes. Unchanged but raising events, a loaded record still takes the next
104/// version, so its events commit only from the state they were raised in.
105struct Pending {
106    row: Snapshot,
107    changed: bool,
108}
109
110/// The same stamp gives the same answer, so planning and recording agree.
111fn pending_write<A: Aggregate>(aggregate: &A, stamp: Stamp) -> Option<Pending> {
112    let table = table::<A>();
113    let meta = aggregate.meta();
114    let mut fields = Vec::with_capacity(table.columns.len());
115    aggregate.encode(&mut fields);
116    let mut stored = meta.stored.clone();
117    stored.resize(table.stored.len(), SqlValue::Null);
118    let loaded = meta.snapshot.as_ref();
119    let unchanged = loaded.filter(|loaded| {
120        same_values(&loaded.fields, &fields) && same_values(&loaded.stored, &stored)
121    });
122    #[cfg(feature = "children")]
123    let unchanged = unchanged.filter(|_| !crate::children::children_changed(aggregate));
124    if let Some(loaded) = unchanged {
125        return (!meta.events().is_empty()).then(|| Pending {
126            row: next_version(loaded),
127            changed: false,
128        });
129    }
130    let write = Write {
131        table,
132        id: meta.id(),
133        owner: aggregate.owner(),
134        stamp,
135        loaded,
136        fields: &fields,
137        stored: &[],
138    };
139    for (behavior, range) in &table.behaviors {
140        behavior.written(&write, &mut stored[range.clone()]);
141    }
142    Some(Pending {
143        row: Snapshot { stored, fields },
144        changed: true,
145    })
146}
147
148/// `version` is the first stored column.
149fn next_version(loaded: &Snapshot) -> Snapshot {
150    let mut next = loaded.clone();
151    next.stored[0] = SqlValue::Int(crate::behavior::version(loaded.stored.first()) + 1);
152    next
153}
154
155/// A record owned through its parent is checked in the database, as loaded and as written.
156fn refuse_beyond_reach<A: Aggregate>(aggregate: &A, reach: Reach) -> Result<Option<Uuid>> {
157    let Reach::OwnedBy(owner) = reach else {
158        return Ok(None);
159    };
160    let taken_over = aggregate
161        .meta()
162        .snapshot
163        .as_ref()
164        .and_then(loaded_owner::<A>)
165        .is_some_and(|loaded| loaded != owner);
166    match aggregate.owner() {
167        Some(own) if own == owner && !taken_over => Ok(None),
168        #[cfg(feature = "children")]
169        None if A::OWNER.is_some() => Ok(Some(owner)),
170        _ => Err(Error::internal_message(format!(
171            "an owned area wrote a `{}` its principal does not own; it may only read \
172             records of others and records without an owner",
173            A::NAME
174        ))),
175    }
176}
177
178fn loaded_owner<A: Aggregate>(loaded: &Snapshot) -> Option<Uuid> {
179    let index = table::<A>()
180        .columns
181        .iter()
182        .position(|column| Some(column.as_str()) == A::OWNER)?;
183    match loaded.fields.get(index) {
184        Some(SqlValue::Uuid(owner)) => Some(*owner),
185        _ => None,
186    }
187}
188
189static INSERTS: TypeCache<(TypeId, TypeId), Arc<str>> = TypeCache::new();
190
191/// Its text depends only on the type and the dialect, so it is built once for both.
192fn insert<A: Aggregate>(id: Uuid, written: Snapshot, dialect: &dyn Dialect) -> Step {
193    let key = (TypeId::of::<A>(), (dialect as &dyn Any).type_id());
194    let sql = INSERTS.get_or_insert_with(key, || {
195        let table = table::<A>();
196        let columns: Vec<&str> = table.row_columns().collect();
197        crate::sql::insert(dialect, &table.qualified(dialect), &columns).into()
198    });
199    let mut params = vec![SqlValue::Uuid(id)];
200    params.extend(written.stored);
201    params.extend(written.fields);
202    Step::statement(&*sql, params, None)
203}
204
205static UPDATES: TypeCache<(TypeId, TypeId), Arc<str>> = TypeCache::new();
206
207/// Every column, only where the row still has the guarded columns it was loaded with; one text
208/// per type and dialect, as for inserts.
209fn update<A: Aggregate>(
210    id: Uuid,
211    loaded: &Snapshot,
212    written: Snapshot,
213    dialect: &dyn Dialect,
214) -> Step {
215    let table = table::<A>();
216    let key = (TypeId::of::<A>(), (dialect as &dyn Any).type_id());
217    let sql = UPDATES.get_or_insert_with(key, || update_sql(table, loaded, dialect).into());
218    let mut params = written.stored;
219    params.extend(written.fields);
220    params.push(SqlValue::Uuid(id));
221    params.extend(guarded(table, loaded).map(|(_, value)| value.clone()));
222    Step::statement(&*sql, params, Some(1))
223}
224
225fn update_sql(table: &Table, loaded: &Snapshot, dialect: &dyn Dialect) -> String {
226    let mut index = 0;
227    let mut next = |column: &str| {
228        index += 1;
229        format!("{} = {}", dialect.quote(column), dialect.placeholder(index))
230    };
231    let assignments: Vec<String> = table
232        .stored
233        .iter()
234        .chain(&table.columns)
235        .map(String::as_str)
236        .map(&mut next)
237        .collect();
238    let conditions: Vec<String> = std::iter::once("id")
239        .chain(guarded(table, loaded).map(|(column, _)| column))
240        .map(next)
241        .collect();
242    format!(
243        "UPDATE {} SET {} WHERE {}",
244        table.qualified(dialect),
245        assignments.join(", "),
246        conditions.join(" AND "),
247    )
248}
249
250fn guarded<'a>(
251    table: &'a Table,
252    loaded: &'a Snapshot,
253) -> impl Iterator<Item = (&'a str, &'a SqlValue)> {
254    table
255        .behaviors
256        .iter()
257        .filter(|(behavior, _)| behavior.guards())
258        .flat_map(move |(_, range)| {
259            table.stored[range.clone()]
260                .iter()
261                .map(String::as_str)
262                .zip(&loaded.stored[range.clone()])
263        })
264}
265
266pub fn refuse_unlisted_move<T: Cast>(
267    field: &str,
268    current: &T,
269    loaded: &mut dyn ColumnReader,
270) -> Result<()> {
271    if !T::HOLDS_STATES {
272        return skip(loaded, T::COLUMNS.len());
273    }
274    let stored = T::decode(loaded)?;
275    T::refuse_unlisted_move(field, &stored, current)
276}
277
278fn skip(loaded: &mut dyn ColumnReader, columns: usize) -> Result<()> {
279    for _ in 0..columns {
280        loaded.skip()?;
281    }
282    Ok(())
283}
284
285#[cfg(test)]
286mod tests {
287    use sofie_core::moves::{Moves, Targets};
288    use sofie_core::{ColumnSpec, SqlType, Variants, variants};
289
290    use sofie_core::Instant;
291
292    use super::*;
293    use crate::aggregate::tests::{NUMBERED, Note};
294    use crate::{Field, Meta, SoftDeletes, Source};
295
296    #[derive(Clone, Copy, PartialEq)]
297    enum Light {
298        Green,
299        Red,
300    }
301
302    impl Cast for Light {
303        const COLUMNS: &'static [ColumnSpec] = &[ColumnSpec::new("", SqlType::Text)];
304        const MOVES: Option<Moves<Self>> = Some(|light| {
305            Some(match light {
306                Self::Green => &Targets {
307                    of: "Light",
308                    states: &["RED"],
309                },
310                Self::Red => &Targets {
311                    of: "Light",
312                    states: &[],
313                },
314            })
315        });
316
317        fn encode(&self, out: &mut Vec<SqlValue>) {
318            out.push(SqlValue::Text(self.name().to_owned()));
319        }
320
321        fn decode(row: &mut dyn ColumnReader) -> Result<Self> {
322            match row.text()? {
323                "GREEN" => Ok(Self::Green),
324                _ => Ok(Self::Red),
325            }
326        }
327    }
328
329    impl Light {
330        fn name(self) -> &'static str {
331            match self {
332                Self::Green => "GREEN",
333                Self::Red => "RED",
334            }
335        }
336    }
337
338    fn refused_move<T: Cast + Copy>(field: &str, from: T, to: T) -> Result<()> {
339        let mut values = Vec::new();
340        from.encode(&mut values);
341        refuse_unlisted_move(field, &to, &mut RowReader::new(&values))
342    }
343
344    fn refused_stage_move(to: Stage) -> Result<()> {
345        refused_move("stage", Stage::Open, to)
346    }
347
348    #[test]
349    fn a_listed_move_and_no_move_pass() {
350        assert!(refused_move("light", Light::Green, Light::Red).is_ok());
351        assert!(refused_move("light", Light::Red, Light::Red).is_ok());
352    }
353
354    #[test]
355    fn an_unlisted_move_is_refused_by_the_rule() {
356        let refused = refused_move("light", Light::Red, Light::Green);
357
358        let Err(Error::Invalid(fields)) = &refused else {
359            panic!("expected a field error, got {refused:?}");
360        };
361        assert_eq!(
362            (
363                fields[0].field.as_str(),
364                fields[0].key.as_ref(),
365                fields[0].message.as_str()
366            ),
367            ("light", "validation.move", "cannot move from RED to GREEN")
368        );
369        assert_eq!(
370            fields[0].arguments,
371            [
372                ("from".into(), "RED".to_owned()),
373                ("to".into(), "GREEN".to_owned())
374            ]
375        );
376    }
377
378    #[derive(Clone, Copy)]
379    struct Signal {
380        light: Light,
381        count: i64,
382    }
383
384    /// As `#[derive(Value)]` writes it for several fields.
385    impl Cast for Signal {
386        const COLUMNS: &'static [ColumnSpec] = &[
387            ColumnSpec::new("_light", SqlType::Text),
388            ColumnSpec::new("_count", SqlType::Int),
389        ];
390        const HOLDS_STATES: bool = Light::HOLDS_STATES || i64::HOLDS_STATES;
391
392        fn encode(&self, out: &mut Vec<SqlValue>) {
393            self.light.encode(out);
394            self.count.encode(out);
395        }
396
397        fn decode(row: &mut dyn ColumnReader) -> Result<Self> {
398            Ok(Self {
399                light: Cast::decode(row)?,
400                count: Cast::decode(row)?,
401            })
402        }
403
404        fn refuse_unlisted_move(field: &str, stored: &Self, current: &Self) -> Result<()> {
405            Light::refuse_unlisted_move(field, &stored.light, &current.light)?;
406            i64::refuse_unlisted_move(field, &stored.count, &current.count)
407        }
408    }
409
410    #[test]
411    fn a_state_inside_a_value_of_several_fields_moves_only_as_listed() {
412        let signal = |light, count| Signal { light, count };
413
414        assert!(refused_move("signal", signal(Light::Green, 1), signal(Light::Red, 2)).is_ok());
415        assert!(refused_move("signal", signal(Light::Red, 1), signal(Light::Green, 1)).is_err());
416        assert!(
417            refused_move(
418                "signal",
419                Some(signal(Light::Red, 1)),
420                Some(signal(Light::Green, 1))
421            )
422            .is_err()
423        );
424    }
425
426    #[cfg(feature = "children")]
427    struct StreetLamp {
428        id: Uuid,
429        light: Light,
430    }
431
432    #[cfg(feature = "children")]
433    impl crate::Child for StreetLamp {
434        const TABLE: crate::ChildTable = crate::ChildTable {
435            module: "street",
436            name: "lamp",
437            fields: &[Field::new("light", Light::COLUMNS, false)],
438        };
439
440        fn id(&self) -> Uuid {
441            self.id
442        }
443
444        fn encode(&self, out: &mut Vec<SqlValue>) {
445            self.light.encode(out);
446        }
447
448        fn decode(id: Uuid, row: &mut dyn ColumnReader) -> Result<Self> {
449            Ok(Self {
450                id,
451                light: Cast::decode(row)?,
452            })
453        }
454
455        fn refuse_unlisted_moves(&self, loaded: &mut dyn ColumnReader) -> Result<()> {
456            refuse_unlisted_move("light", &self.light, loaded)
457        }
458    }
459
460    #[cfg(feature = "children")]
461    #[test]
462    fn a_changed_child_row_moves_its_state_only_as_listed() {
463        use crate::ChildRows as _;
464
465        let mut lamps = crate::Children::default();
466        lamps.push(StreetLamp {
467            id: Uuid::now_v7().unwrap(),
468            light: Light::Red,
469        });
470        lamps.committed();
471        lamps[0].light = Light::Green;
472
473        assert!(lamps.refuse_unlisted_moves().is_err());
474        lamps.push(StreetLamp {
475            id: Uuid::now_v7().unwrap(),
476            light: Light::Green,
477        });
478        lamps[0].light = Light::Red;
479        assert!(lamps.refuse_unlisted_moves().is_ok());
480    }
481
482    #[test]
483    fn a_value_without_transitions_may_change_freely() {
484        let values = [SqlValue::Int(1)];
485
486        assert!(refuse_unlisted_move("count", &2_i64, &mut RowReader::new(&values)).is_ok());
487    }
488
489    #[derive(Clone, Copy)]
490    enum Stage {
491        Open,
492        Paid,
493        Shipped,
494    }
495
496    const STAGE: &str = "unit_of_work::Stage";
497
498    impl Variants for Stage {
499        const TYPE: &'static str = "Stage";
500        const PATH: &'static str = STAGE;
501        const VARIANTS: &'static [(&'static str, Self)] = &[
502            ("OPEN", Self::Open),
503            ("PAID", Self::Paid),
504            ("SHIPPED", Self::Shipped),
505        ];
506        const TRANSITIONS: &'static [(Self, Self)] =
507            &[(Self::Open, Self::Paid), (Self::Paid, Self::Shipped)];
508    }
509
510    impl Cast for Stage {
511        const COLUMNS: &'static [ColumnSpec] = variants::COLUMNS;
512        const MOVES: Option<Moves<Self>> = Some(|stage| {
513            Some(match stage {
514                Self::Open => &Targets {
515                    of: STAGE,
516                    states: &["PAID"],
517                },
518                Self::Paid => &Targets {
519                    of: STAGE,
520                    states: &["SHIPPED"],
521                },
522                Self::Shipped => &Targets {
523                    of: STAGE,
524                    states: &[],
525                },
526            })
527        });
528
529        fn encode(&self, out: &mut Vec<SqlValue>) {
530            variants::encode(*self, out);
531        }
532
533        fn decode(row: &mut dyn ColumnReader) -> Result<Self> {
534            variants::decode(row)
535        }
536    }
537
538    #[test]
539    fn moves_made_on_two_records_do_not_explain_a_third_s_jump() {
540        let paid = Stage::Open.to(Stage::Paid, "stage").unwrap();
541        let shipped = Stage::Paid.to(Stage::Shipped, "stage").unwrap();
542
543        assert!(refused_move("stage", Stage::Open, paid).is_ok());
544        assert!(refused_move("stage", Stage::Paid, shipped).is_ok());
545        assert!(matches!(
546            refused_stage_move(Stage::Shipped),
547            Err(Error::Invalid(_))
548        ));
549    }
550
551    #[test]
552    fn two_moves_made_through_to_pass_where_the_same_jump_by_assignment_is_refused() {
553        assert!(matches!(
554            refused_stage_move(Stage::Shipped),
555            Err(Error::Invalid(_))
556        ));
557
558        let shipped = Stage::Open
559            .to(Stage::Paid, "stage")
560            .and_then(|paid| paid.to(Stage::Shipped, "stage"));
561
562        assert!(refused_stage_move(shipped.unwrap()).is_ok());
563    }
564
565    fn now() -> Instant {
566        Instant::from(sofie_core::Timestamp::from_second(1_000).unwrap())
567    }
568
569    fn planned(outcome: &impl Outcome, now: Instant) -> Result<Vec<(String, Vec<SqlValue>)>> {
570        let mut plan = Plan::new(&NUMBERED, Reach::All, now, Vec::new(), None);
571        outcome.plan(&mut plan)?;
572        Ok(plan
573            .into_parts()?
574            .steps
575            .into_iter()
576            .map(|step| match step {
577                Step::Statement { sql, params, .. } => (sql, params),
578                Step::Script(sql) => (sql, Vec::new()),
579            })
580            .collect())
581    }
582
583    fn loaded_note(title: &str) -> Note {
584        let created = SqlValue::Timestamp(sofie_core::Timestamp::from_second(10).unwrap());
585        let values = [
586            SqlValue::Uuid(Uuid::from_u128(1)),
587            SqlValue::Int(4),
588            created.clone(),
589            created,
590            SqlValue::Null,
591            SqlValue::Text(title.to_owned()),
592        ];
593        <Note as Source>::decode(&mut RowReader::new(&values)).unwrap()
594    }
595
596    #[test]
597    fn an_insert_writes_the_behaviors_columns_at_the_commits_time() {
598        let note = Note {
599            meta: Meta::new().unwrap(),
600            title: "draft".to_owned(),
601        };
602
603        let steps = planned(&note, now()).unwrap();
604
605        assert_eq!(
606            steps[0].0,
607            "INSERT INTO \"notes\".\"note\" (\"id\", \"version\", \"created_at\", \"updated_at\", \
608             \"deleted_at\", \"title\") VALUES ($1, $2, $3, $4, $5, $6)"
609        );
610        assert_eq!(
611            steps[0].1[1..5],
612            [
613                SqlValue::Int(1),
614                SqlValue::Timestamp(now().timestamp()),
615                SqlValue::Timestamp(now().timestamp()),
616                SqlValue::Null
617            ]
618        );
619    }
620
621    #[test]
622    fn an_update_writes_every_column_and_counts_the_version_up_where_it_is_unchanged() {
623        let mut note = loaded_note("draft");
624        note.title = "final".to_owned();
625
626        let steps = planned(&note, now()).unwrap();
627
628        assert_eq!(
629            steps[0].0,
630            "UPDATE \"notes\".\"note\" SET \"version\" = $1, \"created_at\" = $2, \
631             \"updated_at\" = $3, \"deleted_at\" = $4, \"title\" = $5 \
632             WHERE \"id\" = $6 AND \"version\" = $7"
633        );
634        assert_eq!(steps[0].1[0], SqlValue::Int(5));
635        assert_eq!(steps[0].1[2], SqlValue::Timestamp(now().timestamp()));
636        assert_eq!(steps[0].1[6], SqlValue::Int(4));
637    }
638
639    struct Noted;
640
641    impl crate::Input for Noted {
642        fn from_json(_value: serde_json::Value) -> Result<Self> {
643            Ok(Self)
644        }
645    }
646
647    impl serde::Serialize for Noted {
648        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
649            serializer.serialize_unit_struct("Noted")
650        }
651    }
652
653    impl crate::Event for Noted {
654        const NAME: &'static str = "notes.Noted";
655    }
656
657    #[test]
658    fn an_event_raised_without_a_change_commits_only_from_the_loaded_version() {
659        let mut note = loaded_note("draft");
660        note.meta.raise(Noted).unwrap();
661
662        let mut plan = Plan::new(&NUMBERED, Reach::All, now(), Vec::new(), None);
663        note.plan(&mut plan).unwrap();
664        let steps = plan.into_parts().unwrap().steps;
665        note.committed(Stamp {
666            now: now(),
667            actor: None,
668            action: None,
669        });
670
671        let Step::Statement {
672            sql,
673            params,
674            expect_rows,
675        } = &steps[0]
676        else {
677            panic!("expected the version-checked update first, got {steps:?}");
678        };
679        assert!(sql.starts_with("UPDATE \"notes\".\"note\""), "{sql}");
680        assert_eq!(params[0], SqlValue::Int(5));
681        assert_eq!(params[2], loaded_note("draft").meta.stored[2]);
682        assert_eq!(params[6], SqlValue::Int(4));
683        assert_eq!(*expect_rows, Some(crate::ExpectedRows::or_conflict(1)));
684        assert_eq!(steps.len(), 2, "the update, then the event");
685        assert_eq!(note.meta().version(), 5);
686        assert_eq!(planned(&note, now()).unwrap().len(), 0);
687    }
688
689    #[test]
690    fn an_event_on_a_record_beyond_reach_is_refused_without_a_change() {
691        let mut note = loaded_note("draft");
692        note.meta.raise(Noted).unwrap();
693
694        let mut plan = Plan::new(
695            &NUMBERED,
696            Reach::OwnedBy(Uuid::from_u128(9)),
697            now(),
698            Vec::new(),
699            None,
700        );
701
702        assert!(matches!(note.plan(&mut plan), Err(Error::Internal { .. })));
703    }
704
705    #[test]
706    fn a_float_is_written_when_its_bits_change_and_not_when_they_stay() {
707        assert!(same_values(
708            &[SqlValue::Float(f64::NAN)],
709            &[SqlValue::Float(f64::NAN)]
710        ));
711        assert!(!same_values(
712            &[SqlValue::Float(0.0)],
713            &[SqlValue::Float(-0.0)]
714        ));
715    }
716
717    #[test]
718    fn a_deletion_is_stamped_and_recorded_once_committed() {
719        let mut note = loaded_note("draft");
720        note.delete();
721
722        let steps = planned(&note, now()).unwrap();
723        note.committed(Stamp {
724            now: now(),
725            actor: None,
726            action: None,
727        });
728
729        assert!(steps[0].0.contains("\"deleted_at\" = $4"), "{}", steps[0].0);
730        assert_eq!(steps[0].1[3], SqlValue::Timestamp(now().timestamp()));
731        assert_eq!(note.deleted_at(), Some(now()));
732        assert_eq!(note.meta().version(), 5);
733        assert_eq!(planned(&note, now()).unwrap().len(), 0);
734        note.restore();
735        assert!(!note.is_deleted());
736        assert_eq!(planned(&note, now()).unwrap().len(), 1);
737    }
738
739    struct Lamp {
740        meta: Meta,
741        light: Light,
742    }
743
744    impl Aggregate for Lamp {
745        const NAME: &'static str = "lamp";
746        const MODULE: &'static str = "lamps";
747        const FIELDS: &'static [Field] = &[Field::new("light", Light::COLUMNS, false)];
748
749        fn meta(&self) -> &Meta {
750            &self.meta
751        }
752
753        fn meta_mut(&mut self) -> &mut Meta {
754            &mut self.meta
755        }
756
757        fn encode(&self, out: &mut Vec<SqlValue>) {
758            self.light.encode(out);
759        }
760
761        fn decode(meta: Meta, row: &mut dyn ColumnReader) -> Result<Self> {
762            Ok(Self {
763                meta,
764                light: Light::decode(row)?,
765            })
766        }
767
768        fn refuse_unlisted_moves(&self, loaded: &mut dyn ColumnReader) -> Result<()> {
769            refuse_unlisted_move("light", &self.light, loaded)
770        }
771    }
772
773    #[test]
774    fn a_save_along_an_unlisted_edge_is_refused_whatever_changed_the_field() {
775        let values = [
776            SqlValue::Uuid(Uuid::from_u128(1)),
777            SqlValue::Int(1),
778            SqlValue::Text("RED".to_owned()),
779        ];
780        let mut lamp = <Lamp as Source>::decode(&mut RowReader::new(&values)).unwrap();
781        lamp.light = Light::Green;
782
783        assert!(matches!(planned(&lamp, now()), Err(Error::Invalid(_))));
784        assert_eq!(lamp.meta.version(), 1);
785    }
786
787    #[cfg(feature = "live")]
788    struct Card {
789        meta: Meta,
790        holder: Uuid,
791    }
792
793    #[cfg(feature = "live")]
794    impl Aggregate for Card {
795        const NAME: &'static str = "card";
796        const MODULE: &'static str = "cards";
797        const FIELDS: &'static [Field] = &[Field::new("holder", Uuid::COLUMNS, false)];
798        const OWNER: Option<&'static str> = Some("holder");
799
800        fn meta(&self) -> &Meta {
801            &self.meta
802        }
803
804        fn meta_mut(&mut self) -> &mut Meta {
805            &mut self.meta
806        }
807
808        fn owner(&self) -> Option<Uuid> {
809            Some(self.holder)
810        }
811
812        fn encode(&self, out: &mut Vec<SqlValue>) {
813            self.holder.encode(out);
814        }
815
816        fn decode(meta: Meta, row: &mut dyn ColumnReader) -> Result<Self> {
817            Ok(Self {
818                meta,
819                holder: Uuid::decode(row)?,
820            })
821        }
822    }
823
824    #[cfg(feature = "live")]
825    #[test]
826    fn a_record_handed_to_another_owner_is_announced_to_both() {
827        let (before, after) = (Uuid::from_u128(7), Uuid::from_u128(8));
828        let values = [
829            SqlValue::Uuid(Uuid::from_u128(1)),
830            SqlValue::Int(1),
831            SqlValue::Uuid(before),
832        ];
833        let mut card = <Card as Source>::decode(&mut RowReader::new(&values)).unwrap();
834        card.holder = after;
835
836        let mut plan = Plan::new(&NUMBERED, Reach::All, now(), Vec::new(), None);
837        card.plan(&mut plan).unwrap();
838        let readers: Vec<crate::Readers> = plan
839            .into_parts()
840            .unwrap()
841            .announcements
842            .live
843            .iter()
844            .map(|change| change.readers)
845            .collect();
846
847        assert_eq!(
848            readers,
849            [crate::Readers::Owner(after), crate::Readers::Owner(before)]
850        );
851    }
852
853    #[cfg(feature = "live")]
854    #[test]
855    fn a_raw_statement_touching_a_table_is_announced_to_its_owner() {
856        let holder = Uuid::from_u128(7);
857        let mut plan = Plan::new(&NUMBERED, Reach::All, now(), Vec::new(), None);
858        plan.push(Step::statement(
859            "UPDATE cards.card SET holder = holder",
860            Vec::new(),
861            None,
862        ));
863        plan.touched::<Card>(Some(holder));
864        let parts = plan.into_parts().unwrap();
865
866        assert_eq!(
867            parts.announcements.live,
868            [crate::TableChange {
869                topic: crate::live::Topic::of::<Card>(),
870                readers: crate::Readers::Owner(holder),
871            }]
872        );
873        #[cfg(feature = "cache")]
874        assert!(matches!(
875            parts.announcements.changed[..],
876            [crate::ChangedRecord {
877                module: "cards",
878                aggregate: "card",
879                id: None
880            }]
881        ));
882    }
883}