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#[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
103struct Pending {
106 row: Snapshot,
107 changed: bool,
108}
109
110fn 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
148fn 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
155fn 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
191fn 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
207fn 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 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, ¤t.light)?;
406 i64::refuse_unlisted_move(field, &stored.count, ¤t.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(¬e, 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(¬e, 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(¬e, 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(¬e, 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(¬e, now()).unwrap().len(), 0);
734 note.restore();
735 assert!(!note.is_deleted());
736 assert_eq!(planned(¬e, 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}