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