Skip to main content

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 {}