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