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