reifydb_core/value/index/
range.rs

1// Copyright (c) reifydb.com 2025
2// This file is licensed under the AGPL-3.0-or-later, see license.md file
3
4use std::collections::Bound;
5
6use super::EncodedIndexKey;
7use crate::value::encoded::{EncodedKey, EncodedKeyRange};
8
9#[derive(Clone, Debug)]
10pub struct EncodedIndexKeyRange {
11	pub start: Bound<EncodedIndexKey>,
12	pub end: Bound<EncodedIndexKey>,
13}
14
15impl EncodedIndexKeyRange {
16	pub fn new(start: Bound<EncodedIndexKey>, end: Bound<EncodedIndexKey>) -> Self {
17		Self {
18			start,
19			end,
20		}
21	}
22
23	/// Constructs a key range from optional start and end index keys.
24	///
25	/// - `start`: If provided, marks the inclusive lower bound of the range. If `None`, the range is unbounded
26	///   below.
27	/// - `end`: If provided, marks the exclusive upper bound of the range. If `None`, the range is unbounded above.
28	///
29	/// This is useful for creating ranges for index scans.
30	pub fn start_end(start: Option<EncodedIndexKey>, end: Option<EncodedIndexKey>) -> Self {
31		let start = match start {
32			Some(s) => Bound::Included(s),
33			None => Bound::Unbounded,
34		};
35
36		let end = match end {
37			Some(e) => Bound::Excluded(e),
38			None => Bound::Unbounded,
39		};
40
41		Self {
42			start,
43			end,
44		}
45	}
46
47	/// Constructs a key range from optional inclusive start and end index
48	/// keys.
49	///
50	/// - `start`: If provided, marks the inclusive lower bound of the range. If `None`, the range is unbounded
51	///   below.
52	/// - `end`: If provided, marks the inclusive upper bound of the range. If `None`, the range is unbounded above.
53	///
54	/// Both bounds are inclusive when provided.
55	pub fn start_end_inclusive(start: Option<EncodedIndexKey>, end: Option<EncodedIndexKey>) -> Self {
56		let start = match start {
57			Some(s) => Bound::Included(s),
58			None => Bound::Unbounded,
59		};
60
61		let end = match end {
62			Some(e) => Bound::Included(e),
63			None => Bound::Unbounded,
64		};
65
66		Self {
67			start,
68			end,
69		}
70	}
71
72	/// Generates a key range for an index key prefix, used for prefix
73	/// scans.
74	///
75	/// The exclusive end bound is generated by adding 1 to the value of the
76	/// last byte. If the last byte(s) is 0xff (so adding 1 would
77	/// overflow), we instead find the latest non-0xff byte, increment
78	/// that, and truncate the rest. If all bytes are 0xff, we scan to the
79	/// end of the range.
80	pub fn prefix(prefix: &[u8]) -> Self {
81		let start = Bound::Included(EncodedIndexKey::from_bytes(prefix));
82		let end = match prefix.iter().rposition(|&b| b != 0xff) {
83			Some(i) => Bound::Excluded(EncodedIndexKey::from_bytes(
84				&prefix.iter()
85					.take(i)
86					.copied()
87					.chain(std::iter::once(prefix[i] + 1))
88					.collect::<Vec<_>>(),
89			)),
90			None => Bound::Unbounded,
91		};
92		Self {
93			start,
94			end,
95		}
96	}
97
98	/// Constructs a key range that fragments the entire keyspace.
99	pub fn all() -> Self {
100		Self {
101			start: Bound::Unbounded,
102			end: Bound::Unbounded,
103		}
104	}
105
106	/// Converts this EncodedIndexKeyRange to an EncodedKeyRange by
107	/// converting the bounds. This is useful when you need to use index
108	/// ranges with storage APIs that expect EncodedKeyRange.
109	pub fn to_encoded_key_range(&self) -> EncodedKeyRange {
110		let start = match &self.start {
111			Bound::Included(key) => Bound::Included(EncodedKey::new(key.as_slice())),
112			Bound::Excluded(key) => Bound::Excluded(EncodedKey::new(key.as_slice())),
113			Bound::Unbounded => Bound::Unbounded,
114		};
115
116		let end = match &self.end {
117			Bound::Included(key) => Bound::Included(EncodedKey::new(key.as_slice())),
118			Bound::Excluded(key) => Bound::Excluded(EncodedKey::new(key.as_slice())),
119			Bound::Unbounded => Bound::Unbounded,
120		};
121
122		EncodedKeyRange::new(start, end)
123	}
124
125	/// Creates a range from an EncodedIndexKey prefix.
126	/// This will match all keys that start with the given prefix.
127	pub fn from_prefix(key: &EncodedIndexKey) -> Self {
128		Self::prefix(key.as_slice())
129	}
130}
131
132impl From<EncodedIndexKeyRange> for EncodedKeyRange {
133	fn from(range: EncodedIndexKeyRange) -> Self {
134		range.to_encoded_key_range()
135	}
136}
137
138#[cfg(test)]
139mod tests {
140	use reifydb_type::Type;
141
142	use super::*;
143	use crate::{SortDirection, value::index::EncodedIndexLayout};
144
145	#[test]
146	fn test_start_end() {
147		let layout = EncodedIndexLayout::new(&[Type::Uint8], &[SortDirection::Asc]).unwrap();
148
149		let mut key1 = layout.allocate_key();
150		layout.set_u64(&mut key1, 0, 100u64);
151
152		let mut key2 = layout.allocate_key();
153		layout.set_u64(&mut key2, 0, 200u64);
154
155		let range = EncodedIndexKeyRange::start_end(Some(key1.clone()), Some(key2.clone()));
156
157		match &range.start {
158			Bound::Included(k) => {
159				assert_eq!(k.as_slice(), key1.as_slice())
160			}
161			_ => panic!("Expected Included start bound"),
162		}
163
164		match &range.end {
165			Bound::Excluded(k) => {
166				assert_eq!(k.as_slice(), key2.as_slice())
167			}
168			_ => panic!("Expected Excluded end bound"),
169		}
170	}
171
172	#[test]
173	fn test_start_end_inclusive() {
174		let layout = EncodedIndexLayout::new(&[Type::Uint8], &[SortDirection::Asc]).unwrap();
175
176		let mut key1 = layout.allocate_key();
177		layout.set_u64(&mut key1, 0, 100u64);
178
179		let mut key2 = layout.allocate_key();
180		layout.set_u64(&mut key2, 0, 200u64);
181
182		let range = EncodedIndexKeyRange::start_end_inclusive(Some(key1.clone()), Some(key2.clone()));
183
184		match &range.start {
185			Bound::Included(k) => {
186				assert_eq!(k.as_slice(), key1.as_slice())
187			}
188			_ => panic!("Expected Included start bound"),
189		}
190
191		match &range.end {
192			Bound::Included(k) => {
193				assert_eq!(k.as_slice(), key2.as_slice())
194			}
195			_ => panic!("Expected Included end bound"),
196		}
197	}
198
199	#[test]
200	fn test_unbounded() {
201		let range = EncodedIndexKeyRange::start_end(None, None);
202		assert!(matches!(range.start, Bound::Unbounded));
203		assert!(matches!(range.end, Bound::Unbounded));
204	}
205
206	#[test]
207	fn test_prefix() {
208		let prefix = &[0x12, 0x34];
209		let range = EncodedIndexKeyRange::prefix(prefix);
210
211		match &range.start {
212			Bound::Included(k) => assert_eq!(k.as_slice(), prefix),
213			_ => panic!("Expected Included start bound"),
214		}
215
216		match &range.end {
217			Bound::Excluded(k) => {
218				assert_eq!(k.as_slice(), &[0x12, 0x35])
219			}
220			_ => panic!("Expected Excluded end bound"),
221		}
222	}
223
224	#[test]
225	fn test_prefix_with_ff() {
226		let prefix = &[0x12, 0xff];
227		let range = EncodedIndexKeyRange::prefix(prefix);
228
229		match &range.start {
230			Bound::Included(k) => assert_eq!(k.as_slice(), prefix),
231			_ => panic!("Expected Included start bound"),
232		}
233
234		match &range.end {
235			Bound::Excluded(k) => assert_eq!(k.as_slice(), &[0x13]),
236			_ => panic!("Expected Excluded end bound"),
237		}
238	}
239
240	#[test]
241	fn test_prefix_all_ff() {
242		let prefix = &[0xff, 0xff];
243		let range = EncodedIndexKeyRange::prefix(prefix);
244
245		match &range.start {
246			Bound::Included(k) => assert_eq!(k.as_slice(), prefix),
247			_ => panic!("Expected Included start bound"),
248		}
249
250		assert!(matches!(range.end, Bound::Unbounded));
251	}
252
253	#[test]
254	fn test_to_encoded_key_range() {
255		let layout = EncodedIndexLayout::new(&[Type::Uint8], &[SortDirection::Asc]).unwrap();
256
257		let mut key = layout.allocate_key();
258		layout.set_u64(&mut key, 0, 100u64);
259
260		let index_range = EncodedIndexKeyRange::start_end(Some(key.clone()), None);
261		let key_range = index_range.to_encoded_key_range();
262
263		match &key_range.start {
264			Bound::Included(k) => {
265				assert_eq!(k.as_slice(), key.as_slice())
266			}
267			_ => panic!("Expected Included start bound"),
268		}
269
270		assert!(matches!(key_range.end, Bound::Unbounded));
271	}
272
273	#[test]
274	fn test_all() {
275		let range = EncodedIndexKeyRange::all();
276		assert!(matches!(range.start, Bound::Unbounded));
277		assert!(matches!(range.end, Bound::Unbounded));
278	}
279}