Skip to main content

lock_db/
mode.rs

1//! Lock modes and the compatibility matrix.
2//!
3//! The compatibility matrix is the correctness core of a lock manager: it
4//! decides, for any two transactions contending for the same resource, whether
5//! both requests can be held at once. Get this wrong and the manager hands out
6//! conflicting locks; everything above it then corrupts data. For that reason
7//! the matrix lives in one small, `const`, exhaustively tested place rather than
8//! being scattered across the acquire path.
9//!
10//! `lock-db` implements the five standard multi-granularity locking (MGL)
11//! modes. Two are the fundamental modes — shared (read) and exclusive (write).
12//! The other three are *intention* modes that a transaction takes on a coarse
13//! resource (a table, say) to announce that it intends to take a finer lock (a
14//! row) underneath it. Intention locks are what make hierarchical locking
15//! correct without forcing every reader to inspect every individual row lock:
16//! a transaction wanting to lock a whole table exclusively need only check that
17//! no one holds an intention lock on the table, instead of scanning every row.
18//!
19//! See the [crate-level docs](crate) for the granularity protocol that ties the
20//! modes to a database/table/page/row hierarchy.
21
22/// The mode in which a transaction holds, or wants to hold, a lock.
23///
24/// The five modes form a lattice ordered by privilege: holding a stronger mode
25/// grants the capabilities of every weaker mode it [covers](LockMode::covers).
26/// Acquiring a mode while already holding another upgrades to their
27/// [join](LockMode::join) (least upper bound) — for example shared + intention
28/// exclusive becomes [`SharedIntentionExclusive`](LockMode::SharedIntentionExclusive).
29///
30/// # Examples
31///
32/// ```
33/// use lock_db::LockMode;
34///
35/// // Two readers coexist; a writer excludes everyone.
36/// assert!(LockMode::Shared.compatible_with(LockMode::Shared));
37/// assert!(!LockMode::Shared.compatible_with(LockMode::Exclusive));
38///
39/// // Intention-shared coexists with everything except an exclusive lock.
40/// assert!(LockMode::IntentionShared.compatible_with(LockMode::SharedIntentionExclusive));
41/// assert!(!LockMode::IntentionShared.compatible_with(LockMode::Exclusive));
42/// ```
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45pub enum LockMode {
46    /// Intention shared (IS). Announces intent to take shared locks on finer
47    /// resources beneath this one. Compatible with everything except exclusive.
48    IntentionShared,
49
50    /// Intention exclusive (IX). Announces intent to take exclusive (or shared)
51    /// locks on finer resources. Compatible with the intention modes only.
52    IntentionExclusive,
53
54    /// Shared (S). A read lock. Any number of transactions may hold a resource
55    /// `Shared` at once, alongside intention-shared holders.
56    Shared,
57
58    /// Shared and intention exclusive (SIX). The transaction reads the whole
59    /// subtree (S) and intends to write part of it (IX). Compatible only with
60    /// intention-shared.
61    SharedIntentionExclusive,
62
63    /// Exclusive (X). A write lock. Held by at most one transaction, and only
64    /// when no other transaction holds the resource in any mode.
65    Exclusive,
66}
67
68impl LockMode {
69    /// Maps a mode to its row/column in the compatibility and join tables.
70    #[inline]
71    const fn index(self) -> usize {
72        match self {
73            LockMode::IntentionShared => 0,
74            LockMode::IntentionExclusive => 1,
75            LockMode::Shared => 2,
76            LockMode::SharedIntentionExclusive => 3,
77            LockMode::Exclusive => 4,
78        }
79    }
80
81    /// Returns `true` if a lock in `self` mode and a lock in `other` mode may be
82    /// held on the same resource by two different transactions at once.
83    ///
84    /// This is the symmetric compatibility relation of the standard MGL matrix:
85    ///
86    /// |       | IS | IX | S  | SIX | X  |
87    /// |-------|----|----|----|-----|----|
88    /// | **IS**  | ✓ | ✓ | ✓ | ✓  | ✗ |
89    /// | **IX**  | ✓ | ✓ | ✗ | ✗  | ✗ |
90    /// | **S**   | ✓ | ✗ | ✓ | ✗  | ✗ |
91    /// | **SIX** | ✓ | ✗ | ✗ | ✗  | ✗ |
92    /// | **X**   | ✗ | ✗ | ✗ | ✗  | ✗ |
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use lock_db::LockMode;
98    ///
99    /// assert!(LockMode::IntentionShared.compatible_with(LockMode::IntentionExclusive));
100    /// assert!(!LockMode::IntentionExclusive.compatible_with(LockMode::Shared));
101    /// assert!(!LockMode::Exclusive.compatible_with(LockMode::IntentionShared));
102    /// ```
103    #[inline]
104    #[must_use]
105    pub const fn compatible_with(self, other: LockMode) -> bool {
106        // Rows/cols ordered IS, IX, S, SIX, X.
107        const COMPAT: [[bool; 5]; 5] = [
108            [true, true, true, true, false],     // IS
109            [true, true, false, false, false],   // IX
110            [true, false, true, false, false],   // S
111            [true, false, false, false, false],  // SIX
112            [false, false, false, false, false], // X
113        ];
114        COMPAT[self.index()][other.index()]
115    }
116
117    /// Returns the least mode that grants everything both `self` and `other`
118    /// grant — their least upper bound in the privilege lattice.
119    ///
120    /// This is what an upgrade resolves to: a transaction already holding `self`
121    /// that requests `other` ends up holding `self.join(other)`. For example a
122    /// reader (`Shared`) that announces intent to write part of the subtree
123    /// (`IntentionExclusive`) upgrades to `SharedIntentionExclusive`. `join` is
124    /// commutative and associative.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use lock_db::LockMode;
130    ///
131    /// assert_eq!(
132    ///     LockMode::Shared.join(LockMode::IntentionExclusive),
133    ///     LockMode::SharedIntentionExclusive,
134    /// );
135    /// assert_eq!(LockMode::IntentionShared.join(LockMode::Shared), LockMode::Shared);
136    /// assert_eq!(LockMode::Shared.join(LockMode::Exclusive), LockMode::Exclusive);
137    /// // join with itself is itself.
138    /// assert_eq!(LockMode::IntentionExclusive.join(LockMode::IntentionExclusive), LockMode::IntentionExclusive);
139    /// ```
140    #[inline]
141    #[must_use]
142    pub const fn join(self, other: LockMode) -> LockMode {
143        use LockMode::{
144            Exclusive, IntentionExclusive, IntentionShared, Shared, SharedIntentionExclusive,
145        };
146        // Rows/cols ordered IS, IX, S, SIX, X. Symmetric.
147        const JOIN: [[LockMode; 5]; 5] = [
148            // IS
149            [
150                IntentionShared,
151                IntentionExclusive,
152                Shared,
153                SharedIntentionExclusive,
154                Exclusive,
155            ],
156            // IX
157            [
158                IntentionExclusive,
159                IntentionExclusive,
160                SharedIntentionExclusive,
161                SharedIntentionExclusive,
162                Exclusive,
163            ],
164            // S
165            [
166                Shared,
167                SharedIntentionExclusive,
168                Shared,
169                SharedIntentionExclusive,
170                Exclusive,
171            ],
172            // SIX
173            [
174                SharedIntentionExclusive,
175                SharedIntentionExclusive,
176                SharedIntentionExclusive,
177                SharedIntentionExclusive,
178                Exclusive,
179            ],
180            // X
181            [Exclusive, Exclusive, Exclusive, Exclusive, Exclusive],
182        ];
183        JOIN[self.index()][other.index()]
184    }
185
186    /// Returns `true` if holding `self` already grants everything `other` would.
187    ///
188    /// Equivalent to `self.join(other) == self`: `self` sits at or above `other`
189    /// in the lattice. This drives the idempotent path of acquisition — a
190    /// request for a mode you already cover is a no-op.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// use lock_db::LockMode;
196    ///
197    /// assert!(LockMode::Exclusive.covers(LockMode::Shared));
198    /// assert!(LockMode::SharedIntentionExclusive.covers(LockMode::Shared));
199    /// assert!(LockMode::SharedIntentionExclusive.covers(LockMode::IntentionExclusive));
200    /// assert!(!LockMode::Shared.covers(LockMode::IntentionExclusive));
201    /// ```
202    #[inline]
203    #[must_use]
204    pub const fn covers(self, other: LockMode) -> bool {
205        // `PartialEq` is not `const`, so compare discriminants directly.
206        self.join(other) as u8 == self as u8
207    }
208
209    /// Returns `true` for [`LockMode::Exclusive`].
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// use lock_db::LockMode;
215    ///
216    /// assert!(LockMode::Exclusive.is_exclusive());
217    /// assert!(!LockMode::Shared.is_exclusive());
218    /// ```
219    #[inline]
220    #[must_use]
221    pub const fn is_exclusive(self) -> bool {
222        matches!(self, LockMode::Exclusive)
223    }
224
225    /// Returns `true` for the intention modes (IS, IX, SIX).
226    ///
227    /// Intention modes are placeholders taken on a coarse resource to signal
228    /// that a finer lock will be taken beneath it; they are not themselves read
229    /// or write locks on the resource's own data.
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use lock_db::LockMode;
235    ///
236    /// assert!(LockMode::IntentionShared.is_intention());
237    /// assert!(LockMode::SharedIntentionExclusive.is_intention());
238    /// assert!(!LockMode::Shared.is_intention());
239    /// assert!(!LockMode::Exclusive.is_intention());
240    /// ```
241    #[inline]
242    #[must_use]
243    pub const fn is_intention(self) -> bool {
244        matches!(
245            self,
246            LockMode::IntentionShared
247                | LockMode::IntentionExclusive
248                | LockMode::SharedIntentionExclusive
249        )
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::LockMode::{
256        self, Exclusive, IntentionExclusive, IntentionShared, Shared, SharedIntentionExclusive,
257    };
258
259    const ALL: [LockMode; 5] = [
260        IntentionShared,
261        IntentionExclusive,
262        Shared,
263        SharedIntentionExclusive,
264        Exclusive,
265    ];
266
267    #[test]
268    fn test_compatibility_matches_standard_mgl_matrix() {
269        // The canonical Gray/Reuter compatibility matrix, IS IX S SIX X.
270        let expected = [
271            [true, true, true, true, false],
272            [true, true, false, false, false],
273            [true, false, true, false, false],
274            [true, false, false, false, false],
275            [false, false, false, false, false],
276        ];
277        for (i, a) in ALL.iter().enumerate() {
278            for (j, b) in ALL.iter().enumerate() {
279                assert_eq!(
280                    a.compatible_with(*b),
281                    expected[i][j],
282                    "compat({a:?}, {b:?})"
283                );
284            }
285        }
286    }
287
288    #[test]
289    fn test_compatible_is_symmetric() {
290        for a in ALL {
291            for b in ALL {
292                assert_eq!(a.compatible_with(b), b.compatible_with(a), "{a:?} vs {b:?}");
293            }
294        }
295    }
296
297    #[test]
298    fn test_exclusive_incompatible_with_all() {
299        for m in ALL {
300            assert!(!Exclusive.compatible_with(m));
301            assert!(!m.compatible_with(Exclusive));
302        }
303    }
304
305    #[test]
306    fn test_intention_shared_compatible_with_all_but_exclusive() {
307        for m in ALL {
308            assert_eq!(IntentionShared.compatible_with(m), m != Exclusive);
309        }
310    }
311
312    #[test]
313    fn test_join_is_commutative() {
314        for a in ALL {
315            for b in ALL {
316                assert_eq!(a.join(b), b.join(a), "join({a:?}, {b:?})");
317            }
318        }
319    }
320
321    #[test]
322    fn test_join_is_associative() {
323        for a in ALL {
324            for b in ALL {
325                for c in ALL {
326                    assert_eq!(a.join(b).join(c), a.join(b.join(c)));
327                }
328            }
329        }
330    }
331
332    #[test]
333    fn test_join_idempotent() {
334        for m in ALL {
335            assert_eq!(m.join(m), m);
336        }
337    }
338
339    #[test]
340    fn test_join_examples() {
341        assert_eq!(Shared.join(IntentionExclusive), SharedIntentionExclusive);
342        assert_eq!(IntentionShared.join(Shared), Shared);
343        assert_eq!(IntentionShared.join(IntentionExclusive), IntentionExclusive);
344        assert_eq!(Shared.join(Exclusive), Exclusive);
345        assert_eq!(SharedIntentionExclusive.join(Exclusive), Exclusive);
346    }
347
348    #[test]
349    fn test_covers_is_join_equals_self() {
350        for a in ALL {
351            for b in ALL {
352                assert_eq!(a.covers(b), a.join(b) == a, "covers({a:?}, {b:?})");
353            }
354        }
355    }
356
357    #[test]
358    fn test_exclusive_covers_everything() {
359        for m in ALL {
360            assert!(Exclusive.covers(m));
361        }
362    }
363
364    #[test]
365    fn test_six_covers_its_components() {
366        assert!(SharedIntentionExclusive.covers(Shared));
367        assert!(SharedIntentionExclusive.covers(IntentionExclusive));
368        assert!(SharedIntentionExclusive.covers(IntentionShared));
369        assert!(!SharedIntentionExclusive.covers(Exclusive));
370    }
371
372    #[test]
373    fn test_covers_reflexive() {
374        for m in ALL {
375            assert!(m.covers(m));
376        }
377    }
378
379    #[test]
380    fn test_is_exclusive_and_is_intention() {
381        assert!(Exclusive.is_exclusive());
382        assert!(!Shared.is_exclusive());
383        for m in [
384            IntentionShared,
385            IntentionExclusive,
386            SharedIntentionExclusive,
387        ] {
388            assert!(m.is_intention());
389        }
390        assert!(!Shared.is_intention());
391        assert!(!Exclusive.is_intention());
392    }
393
394    #[test]
395    fn test_two_compatible_modes_join_below_any_conflict() {
396        // Sanity: if two modes are compatible, neither is exclusive.
397        for a in ALL {
398            for b in ALL {
399                if a.compatible_with(b) {
400                    assert!(!(a.is_exclusive() && b.is_exclusive()));
401                }
402            }
403        }
404    }
405}