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}