Skip to main content

lock_db/
range.rs

1//! Key ranges for range locking.
2//!
3//! A point lock protects one resource; a *range* lock protects a contiguous
4//! span of keys. Range locks exist to stop phantoms: if a transaction reads
5//! "every order with id between 100 and 200", locking only the rows that exist
6//! today does not stop another transaction from inserting id 150 underneath it.
7//! Locking the whole range `[100, 200]` does.
8//!
9//! [`KeyRange`] is the span itself — an inclusive `[start, end]` interval over
10//! `u64` keys. It carries no lock state; it is the key a range lock is taken on.
11//! Inclusive bounds avoid the overflow corner that half-open intervals hit at
12//! `u64::MAX` and make a single-key lock simply `KeyRange::point(k)`.
13
14/// An inclusive range of `u64` keys, `[start, end]`.
15///
16/// Construct one with [`new`](KeyRange::new) (which rejects `start > end`) or
17/// [`point`](KeyRange::point) for a single key. Two ranges held by different
18/// transactions conflict only when they [overlap](KeyRange::overlaps) *and*
19/// their lock modes are incompatible.
20///
21/// # Examples
22///
23/// ```
24/// use lock_db::KeyRange;
25///
26/// let r = KeyRange::new(100, 200).unwrap();
27/// assert!(r.contains(150));
28/// assert!(!r.contains(201));
29/// assert!(r.overlaps(KeyRange::point(200)));
30/// assert!(!r.overlaps(KeyRange::new(201, 300).unwrap()));
31///
32/// // An inverted range is rejected.
33/// assert!(KeyRange::new(5, 4).is_none());
34/// ```
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct KeyRange {
38    start: u64,
39    end: u64,
40}
41
42impl KeyRange {
43    /// Creates the inclusive range `[start, end]`, or `None` if `start > end`.
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use lock_db::KeyRange;
49    ///
50    /// assert!(KeyRange::new(0, 0).is_some()); // a single key
51    /// assert!(KeyRange::new(1, 9).is_some());
52    /// assert!(KeyRange::new(9, 1).is_none());
53    /// ```
54    #[inline]
55    #[must_use]
56    pub const fn new(start: u64, end: u64) -> Option<Self> {
57        if start <= end {
58            Some(Self { start, end })
59        } else {
60            None
61        }
62    }
63
64    /// Creates a range covering the single key `key`.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use lock_db::KeyRange;
70    ///
71    /// let k = KeyRange::point(42);
72    /// assert_eq!(k.start(), 42);
73    /// assert_eq!(k.end(), 42);
74    /// ```
75    #[inline]
76    #[must_use]
77    pub const fn point(key: u64) -> Self {
78        Self {
79            start: key,
80            end: key,
81        }
82    }
83
84    /// Returns the inclusive lower bound.
85    #[inline]
86    #[must_use]
87    pub const fn start(self) -> u64 {
88        self.start
89    }
90
91    /// Returns the inclusive upper bound.
92    #[inline]
93    #[must_use]
94    pub const fn end(self) -> u64 {
95        self.end
96    }
97
98    /// Returns `true` if `key` falls within the range.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use lock_db::KeyRange;
104    ///
105    /// let r = KeyRange::new(10, 20).unwrap();
106    /// assert!(r.contains(10) && r.contains(20));
107    /// assert!(!r.contains(9) && !r.contains(21));
108    /// ```
109    #[inline]
110    #[must_use]
111    pub const fn contains(self, key: u64) -> bool {
112        self.start <= key && key <= self.end
113    }
114
115    /// Returns `true` if the two ranges share at least one key.
116    ///
117    /// Overlap is symmetric: `a.overlaps(b) == b.overlaps(a)`.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use lock_db::KeyRange;
123    ///
124    /// let a = KeyRange::new(10, 20).unwrap();
125    /// assert!(a.overlaps(KeyRange::new(20, 30).unwrap())); // touch at 20
126    /// assert!(!a.overlaps(KeyRange::new(21, 30).unwrap()));
127    /// ```
128    #[inline]
129    #[must_use]
130    pub const fn overlaps(self, other: KeyRange) -> bool {
131        self.start <= other.end && other.start <= self.end
132    }
133}
134
135#[cfg(test)]
136#[allow(clippy::unwrap_used)]
137mod tests {
138    use super::KeyRange;
139
140    #[test]
141    fn test_new_rejects_inverted_range() {
142        assert!(KeyRange::new(5, 4).is_none());
143        assert!(KeyRange::new(4, 4).is_some());
144        assert!(KeyRange::new(4, 5).is_some());
145    }
146
147    #[test]
148    fn test_point_is_single_key() {
149        let p = KeyRange::point(7);
150        assert_eq!((p.start(), p.end()), (7, 7));
151        assert!(p.contains(7));
152        assert!(!p.contains(6));
153        assert!(!p.contains(8));
154    }
155
156    #[test]
157    fn test_contains_bounds_inclusive() {
158        let r = KeyRange::new(10, 20).unwrap();
159        assert!(r.contains(10));
160        assert!(r.contains(15));
161        assert!(r.contains(20));
162        assert!(!r.contains(9));
163        assert!(!r.contains(21));
164    }
165
166    #[test]
167    fn test_overlap_is_symmetric() {
168        let a = KeyRange::new(10, 20).unwrap();
169        let cases = [
170            KeyRange::new(0, 9).unwrap(),
171            KeyRange::new(0, 10).unwrap(),
172            KeyRange::new(15, 25).unwrap(),
173            KeyRange::new(20, 30).unwrap(),
174            KeyRange::new(21, 30).unwrap(),
175            KeyRange::new(5, 25).unwrap(),
176        ];
177        for b in cases {
178            assert_eq!(a.overlaps(b), b.overlaps(a), "{a:?} vs {b:?}");
179        }
180    }
181
182    #[test]
183    fn test_adjacent_ranges_overlap_at_shared_bound() {
184        // Inclusive bounds: [10,20] and [20,30] share key 20.
185        assert!(
186            KeyRange::new(10, 20)
187                .unwrap()
188                .overlaps(KeyRange::new(20, 30).unwrap())
189        );
190        // [10,20] and [21,30] do not.
191        assert!(
192            !KeyRange::new(10, 20)
193                .unwrap()
194                .overlaps(KeyRange::new(21, 30).unwrap())
195        );
196    }
197
198    #[test]
199    fn test_contained_range_overlaps() {
200        let outer = KeyRange::new(0, 100).unwrap();
201        let inner = KeyRange::new(40, 60).unwrap();
202        assert!(outer.overlaps(inner));
203        assert!(inner.overlaps(outer));
204    }
205
206    #[test]
207    fn test_max_key_has_no_overflow() {
208        let r = KeyRange::point(u64::MAX);
209        assert!(r.contains(u64::MAX));
210        assert!(r.overlaps(KeyRange::new(0, u64::MAX).unwrap()));
211    }
212}