ifc_model/mutation/conflict.rs
1//! What a transaction refuses to do, and why.
2//!
3//! # Conflicts are found BEFORE anything is written
4//!
5//! Every variant here is produced during preflight, against a projected view
6//! of what the model would look like if the whole transaction applied. None
7//! of them can be raised mid-apply, because apply runs only once preflight
8//! returned clean -- which is what makes the commit atomic without needing an
9//! undo log.
10
11use crate::value::EntityId;
12
13/// A reason a transaction cannot be committed.
14#[derive(Debug, Clone, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum Conflict {
17 /// The model changed since the transaction was opened.
18 ///
19 /// Optimistic concurrency: two editors read the same model, both plan
20 /// edits, and the second to commit is working from a view that no longer
21 /// exists. Rejecting is the only safe answer -- the second editor's
22 /// preflight was computed against state that has since moved.
23 StaleRevision {
24 /// The revision the transaction was opened against.
25 expected: u64,
26 /// The revision the model is actually at.
27 found: u64,
28 },
29 /// An edit names an entity that does not exist and is not being created.
30 MissingTarget {
31 /// Position of the offending edit within the transaction.
32 edit: usize,
33 /// The id it named.
34 id: EntityId,
35 },
36 /// An insert would overwrite an entity that already exists.
37 ///
38 /// [`crate::Model::insert`] deliberately replaces, because a codec
39 /// re-reading a file must be able to. A transaction refuses instead: an
40 /// author who did not mean to destroy an entity gets told, rather than
41 /// discovering it later in a diff.
42 IdAlreadyExists {
43 /// Position of the offending edit.
44 edit: usize,
45 /// The id it tried to occupy.
46 id: EntityId,
47 },
48 /// An edit writes a reference to an entity that will not exist.
49 ///
50 /// Checked against the PROJECTED model, so referencing an entity the same
51 /// transaction creates is fine, and referencing one it removes is not.
52 DanglingReference {
53 /// Position of the offending edit.
54 edit: usize,
55 /// The entity that would hold the bad reference.
56 from: EntityId,
57 /// The attribute slot it would sit in.
58 slot: usize,
59 /// The target that would not exist.
60 target: EntityId,
61 },
62 /// A removal would leave a surviving entity pointing at nothing.
63 ///
64 /// [`crate::Model::remove`] permits this and documents it; a transaction
65 /// does not. Deleting a storey that walls still reference produces a file
66 /// that parses and is wrong, which is worse than a refused edit.
67 ///
68 /// The fix is to include the referrers' updates in the same transaction,
69 /// which is exactly what the projected check allows.
70 RemovalWouldDangle {
71 /// Position of the offending removal.
72 edit: usize,
73 /// The entity being removed.
74 removed: EntityId,
75 /// A surviving entity that still references it.
76 referrer: EntityId,
77 /// The slot the surviving reference sits in.
78 slot: usize,
79 },
80}
81
82impl std::fmt::Display for Conflict {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 match self {
85 Self::StaleRevision { expected, found } => write!(
86 f,
87 "model moved from revision {expected} to {found} since the transaction opened"
88 ),
89 Self::MissingTarget { edit, id } => {
90 write!(f, "edit {edit} names #{} which does not exist", id.0)
91 }
92 Self::IdAlreadyExists { edit, id } => {
93 write!(f, "edit {edit} would overwrite existing #{}", id.0)
94 }
95 Self::DanglingReference {
96 edit,
97 from,
98 slot,
99 target,
100 } => write!(
101 f,
102 "edit {edit}: #{}[{slot}] would reference #{} which will not exist",
103 from.0, target.0
104 ),
105 Self::RemovalWouldDangle {
106 edit,
107 removed,
108 referrer,
109 slot,
110 } => write!(
111 f,
112 "edit {edit}: removing #{} leaves #{}[{slot}] dangling",
113 removed.0, referrer.0
114 ),
115 }
116 }
117}
118
119impl std::error::Error for Conflict {}