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#[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
100struct Pending {
103 row: Snapshot,
104 changed: bool,
105}
106
107fn 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
145fn 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
152fn 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
188fn 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
204fn 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 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, ¤t.light)?;
403 i64::refuse_unlisted_move(field, &stored.count, ¤t.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(¬e, 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(¬e, 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(¬e, 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(¬e, 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(¬e, now()).unwrap().len(), 0);
731 note.restore();
732 assert!(!note.is_deleted());
733 assert_eq!(planned(¬e, 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}