Skip to main content

reifydb_core/key/
bound.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{cmp::Ordering, ops::Bound};
5
6use reifydb_codec::key::encoded::{EncodedKey, EncodedKeyRange};
7use smallvec::SmallVec;
8
9use crate::{
10	interface::catalog::object::ObjectId,
11	key::{
12		any::{Field, KeyFields, TaggedKey, Width},
13		tag::KeyTag,
14	},
15};
16
17pub type OwnedField = Field<'static>;
18
19pub fn object_fields(object: ObjectId) -> [OwnedField; 2] {
20	[Field::UAsc(Width::U8, object.type_tag() as u128), Field::UDesc(Width::U64, object.as_u64() as u128)]
21}
22
23#[derive(Debug, Clone)]
24pub enum TaggedKeyBound {
25	Kind(KeyTag),
26	KindEnd(KeyTag),
27	Prefix(KeyTag, SmallVec<[OwnedField; 6]>),
28	PrefixEnd(KeyTag, SmallVec<[OwnedField; 6]>),
29	Key(TaggedKey),
30}
31
32impl TaggedKeyBound {
33	pub fn prefix(kind: KeyTag, fields: impl IntoIterator<Item = OwnedField>) -> Self {
34		Self::Prefix(kind, fields.into_iter().collect())
35	}
36
37	pub fn prefix_end(kind: KeyTag, fields: impl IntoIterator<Item = OwnedField>) -> Self {
38		Self::PrefixEnd(kind, fields.into_iter().collect())
39	}
40
41	fn kind_byte(&self) -> u8 {
42		match self {
43			Self::Kind(kind) | Self::Prefix(kind, _) | Self::PrefixEnd(kind, _) => *kind as u8,
44			Self::KindEnd(kind) => (*kind as u8).wrapping_sub(1),
45			Self::Key(key) => key.kind() as u8,
46		}
47	}
48
49	fn sorts_after_its_extensions(&self) -> bool {
50		matches!(self, Self::PrefixEnd(..))
51	}
52
53	pub fn encode(&self) -> EncodedKey {
54		if let Self::Key(key) = self {
55			return key.encode();
56		}
57		let mut out = vec![!self.kind_byte()];
58		for field in self.bound_fields().iter() {
59			field.encode(&mut out);
60		}
61		if self.sorts_after_its_extensions() {
62			match out.iter().rposition(|byte| *byte != 0xff) {
63				Some(last) => {
64					out.truncate(last + 1);
65					out[last] += 1;
66				}
67				None => out.clear(),
68			}
69		}
70		EncodedKey::new(out)
71	}
72
73	fn bound_fields(&self) -> SmallVec<[Field<'_>; 6]> {
74		match self {
75			Self::Kind(_) | Self::KindEnd(_) => SmallVec::new(),
76			Self::Prefix(_, fields) | Self::PrefixEnd(_, fields) => fields.iter().cloned().collect(),
77			Self::Key(key) => key.fields(),
78		}
79	}
80
81	fn compare_fields(&self, other: &Self) -> Ordering {
82		let left = self.bound_fields();
83		let right = other.bound_fields();
84		for (index, (left_field, right_field)) in left.iter().zip(right.iter()).enumerate() {
85			let ordering = left_field.cmp(right_field);
86			if ordering == Ordering::Equal {
87				continue;
88			}
89
90			if index + 1 == left.len()
91				&& self.sorts_after_its_extensions()
92				&& left_field.is_truncation_of(right_field)
93			{
94				return Ordering::Greater;
95			}
96			if index + 1 == right.len()
97				&& other.sorts_after_its_extensions()
98				&& right_field.is_truncation_of(left_field)
99			{
100				return Ordering::Less;
101			}
102			return ordering;
103		}
104		match (
105			left.len().cmp(&right.len()),
106			self.sorts_after_its_extensions(),
107			other.sorts_after_its_extensions(),
108		) {
109			(Ordering::Less, true, _) | (Ordering::Equal, true, false) => Ordering::Greater,
110			(Ordering::Greater, _, true) | (Ordering::Equal, false, true) => Ordering::Less,
111			(ordering, _, _) => ordering,
112		}
113	}
114}
115
116impl Ord for TaggedKeyBound {
117	fn cmp(&self, other: &Self) -> Ordering {
118		other.kind_byte().cmp(&self.kind_byte()).then_with(|| self.compare_fields(other))
119	}
120}
121
122impl PartialOrd for TaggedKeyBound {
123	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
124		Some(self.cmp(other))
125	}
126}
127
128impl PartialEq for TaggedKeyBound {
129	fn eq(&self, other: &Self) -> bool {
130		self.cmp(other) == Ordering::Equal
131	}
132}
133
134impl Eq for TaggedKeyBound {}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct TaggedKeyBoundRange {
138	pub start: Bound<TaggedKeyBound>,
139	pub end: Bound<TaggedKeyBound>,
140}
141
142impl TaggedKeyBoundRange {
143	pub fn start_end(start: TaggedKeyBound, end: TaggedKeyBound) -> Self {
144		Self {
145			start: Bound::Included(start),
146			end: Bound::Included(end),
147		}
148	}
149
150	pub fn prefix(kind: KeyTag, fields: impl IntoIterator<Item = OwnedField> + Clone) -> Self {
151		Self {
152			start: Bound::Included(TaggedKeyBound::prefix(kind, fields.clone())),
153			end: Bound::Excluded(TaggedKeyBound::prefix_end(kind, fields)),
154		}
155	}
156
157	pub fn kind(kind: KeyTag) -> Self {
158		Self::start_end(TaggedKeyBound::Kind(kind), TaggedKeyBound::KindEnd(kind))
159	}
160
161	pub fn resume_after(self, last: Option<&TaggedKey>) -> Self {
162		match last {
163			Some(last) => Self {
164				start: Bound::Excluded(TaggedKeyBound::Key(last.clone())),
165				end: self.end,
166			},
167			None => self,
168		}
169	}
170
171	pub fn resume_before(self, last: Option<&TaggedKey>) -> Self {
172		match last {
173			Some(last) => Self {
174				start: self.start,
175				end: Bound::Excluded(TaggedKeyBound::Key(last.clone())),
176			},
177			None => self,
178		}
179	}
180
181	pub fn empty(kind: KeyTag) -> Self {
182		Self {
183			start: Bound::Excluded(TaggedKeyBound::Kind(kind)),
184			end: Bound::Excluded(TaggedKeyBound::Kind(kind)),
185		}
186	}
187
188	pub fn all() -> Self {
189		Self {
190			start: Bound::Unbounded,
191			end: Bound::Unbounded,
192		}
193	}
194
195	pub fn contains(&self, bound: &TaggedKeyBound) -> bool {
196		let after_start = match &self.start {
197			Bound::Unbounded => true,
198			Bound::Included(start) => bound >= start,
199			Bound::Excluded(start) => bound > start,
200		};
201		let before_end = match &self.end {
202			Bound::Unbounded => true,
203			Bound::Included(end) => bound <= end,
204			Bound::Excluded(end) => bound < end,
205		};
206		after_start && before_end
207	}
208
209	pub fn encode(&self) -> EncodedKeyRange {
210		EncodedKeyRange::new(encode_bound(&self.start), encode_bound(&self.end))
211	}
212}
213
214fn encode_bound(bound: &Bound<TaggedKeyBound>) -> Bound<EncodedKey> {
215	match bound {
216		Bound::Unbounded => Bound::Unbounded,
217		Bound::Included(key) => Bound::Included(key.encode()),
218		Bound::Excluded(key) => {
219			let encoded = key.encode();
220			if encoded.is_empty() {
221				return Bound::Unbounded;
222			}
223			Bound::Excluded(encoded)
224		}
225	}
226}
227
228impl From<TaggedKey> for TaggedKeyBound {
229	fn from(key: TaggedKey) -> Self {
230		Self::Key(key)
231	}
232}
233
234#[cfg(test)]
235mod tests {
236	use std::ops::Bound;
237
238	use reifydb_codec::key::{
239		encoded::{EncodedKey, EncodedKeyRange},
240		serializer::KeySerializer,
241	};
242	use reifydb_value::value::row_number::RowNumber;
243
244	use super::{OwnedField, TaggedKeyBound, TaggedKeyBoundRange, object_fields};
245	use crate::{
246		interface::catalog::{id::TableId, object::ObjectId, storage::StorageId},
247		key::{
248			any::{Field, TaggedKey, Width},
249			catalog::{DictionaryKey, KeySerializerCatalogExt, TableKey},
250			row::RowKey,
251			tag::KeyTag,
252		},
253	};
254
255	fn rows() -> Vec<(TaggedKey, EncodedKey)> {
256		let mut out = Vec::new();
257		for storage in [1u64, 2, 3] {
258			for row in [1u64, 2, u64::MAX] {
259				let key = RowKey {
260					storage: StorageId::table(storage),
261					row: RowNumber(row),
262				};
263				let encoded = key.encode();
264				out.push((TaggedKey::from(key), encoded));
265			}
266		}
267		for table in [1u64, 2] {
268			let key = TableKey {
269				table: TableId(table),
270			};
271			let encoded = key.encode();
272			out.push((TaggedKey::from(key), encoded));
273		}
274		out
275	}
276
277	fn storage_start(storage: StorageId) -> TaggedKeyBound {
278		TaggedKeyBound::prefix(
279			KeyTag::Row,
280			[
281				OwnedField::UAsc(Width::U8, ObjectId::from(storage).type_tag() as u128),
282				Field::UDesc(Width::U64, ObjectId::from(storage).as_u64() as u128),
283			],
284		)
285	}
286
287	fn storage_end(storage: StorageId) -> TaggedKeyBound {
288		let previous = ObjectId::from(storage).prev();
289		TaggedKeyBound::prefix(
290			KeyTag::Row,
291			[
292				OwnedField::UAsc(Width::U8, previous.type_tag() as u128),
293				Field::UDesc(Width::U64, previous.as_u64() as u128),
294			],
295		)
296	}
297
298	#[test]
299	fn a_typed_key_bound_orders_exactly_like_its_encoding() {
300		let probes = rows();
301		for (left, left_bytes) in &probes {
302			for (right, right_bytes) in &probes {
303				assert_eq!(
304					TaggedKeyBound::Key(left.clone()).cmp(&TaggedKeyBound::Key(right.clone())),
305					left_bytes.cmp(right_bytes),
306					"{left:?} vs {right:?}"
307				);
308			}
309		}
310	}
311
312	#[test]
313	fn a_storage_prefix_selects_the_same_rows_as_the_encoded_range() {
314		for storage in [1u64, 2, 3] {
315			let storage = StorageId::table(storage);
316			let byte_start = RowKey::storage_start(storage);
317			let byte_end = RowKey::storage_end(storage);
318			let typed_start = storage_start(storage);
319			let typed_end = storage_end(storage);
320
321			let probes = rows();
322			let by_bytes: Vec<&TaggedKey> = probes
323				.iter()
324				.filter(|(_, bytes)| *bytes >= byte_start && *bytes <= byte_end)
325				.map(|(key, _)| key)
326				.collect();
327			let by_typed: Vec<&TaggedKey> = probes
328				.iter()
329				.filter(|(key, _)| {
330					let bound = TaggedKeyBound::Key((*key).clone());
331					bound >= typed_start && bound <= typed_end
332				})
333				.map(|(key, _)| key)
334				.collect();
335
336			assert!(!by_bytes.is_empty(), "storage {storage:?} selected nothing by bytes");
337			assert_eq!(by_bytes, by_typed, "storage {storage:?}");
338		}
339	}
340
341	#[test]
342	fn a_field_prefix_bound_encodes_to_the_bytes_its_byte_producer_writes() {
343		// the bound has to be substitutable for the encoded range it replaces, and ordering
344		// alone cannot show that: two bounds can bracket the same typed keys while writing
345		// different bytes, which would silently change what the sqlite blob range selects.
346		for storage in [1u64, 2, u64::MAX] {
347			let storage = StorageId::table(storage);
348			assert_eq!(storage_start(storage).encode(), RowKey::storage_start(storage), "{storage:?}");
349			assert_eq!(storage_end(storage).encode(), RowKey::storage_end(storage), "{storage:?}");
350		}
351	}
352
353	#[test]
354	fn a_kind_span_bound_encodes_the_kind_byte_and_its_predecessor() {
355		// built through the codec rather than through DictionaryKey::full_scan, which now
356		// returns this very bound and would make the assertion compare a value with itself.
357		let mut start = KeySerializer::with_capacity(1);
358		start.extend_u8(DictionaryKey::TAG as u8);
359		let mut end = KeySerializer::with_capacity(1);
360		end.extend_u8(DictionaryKey::TAG as u8 - 1);
361
362		assert_eq!(TaggedKeyBound::Kind(KeyTag::Dictionary).encode(), start.to_encoded_key());
363		assert_eq!(TaggedKeyBound::KindEnd(KeyTag::Dictionary).encode(), end.to_encoded_key());
364	}
365
366	fn storage_fields(storage: StorageId) -> Vec<OwnedField> {
367		object_fields(ObjectId::from(storage)).to_vec()
368	}
369
370	#[test]
371	fn the_object_id_field_pair_encodes_to_what_extend_object_id_writes() {
372		// twenty-one producers project an ObjectId through this helper rather than through the
373		// derive, so it is the one field pair with no generated conformance test behind it.
374		for storage in [0u64, 1, 255, u64::MAX] {
375			let object = ObjectId::from(StorageId::table(storage));
376			let mut replayed = Vec::new();
377			for field in object_fields(object) {
378				field.encode(&mut replayed);
379			}
380			let mut expected = KeySerializer::with_capacity(9);
381			expected.extend_object_id(object);
382			assert_eq!(replayed.as_slice(), expected.to_encoded_key().as_slice(), "{object:?}");
383		}
384	}
385
386	#[test]
387	fn a_field_prefix_range_encodes_to_the_span_the_byte_prefix_helper_computes() {
388		// EncodedKeyRange::prefix ends on the byte successor of the prefix, not on a decremented
389		// field, so PrefixEnd has to reproduce that successor exactly or the range either drops
390		// the last keys of the prefix or reaches into the next one.
391		for storage in [1u64, 2, 255, u64::MAX] {
392			let storage = StorageId::table(storage);
393			let typed = TaggedKeyBoundRange::prefix(KeyTag::Row, storage_fields(storage));
394			let bytes = EncodedKeyRange::prefix(RowKey::storage_start(storage).as_slice());
395			let encoded = typed.encode();
396			assert_eq!(encoded.start, bytes.start, "{storage:?} start");
397			assert_eq!(encoded.end, bytes.end, "{storage:?} end");
398		}
399	}
400
401	#[test]
402	fn a_prefix_range_selects_the_same_keys_typed_as_it_does_encoded() {
403		for storage in [1u64, 2, 3] {
404			let storage = StorageId::table(storage);
405			let typed = TaggedKeyBoundRange::prefix(KeyTag::Row, storage_fields(storage));
406			let bytes = EncodedKeyRange::prefix(RowKey::storage_start(storage).as_slice());
407			let (Bound::Included(typed_start), Bound::Excluded(typed_end)) =
408				(typed.start.clone(), typed.end.clone())
409			else {
410				panic!("a field prefix range is expected to be included-excluded");
411			};
412
413			let probes = rows();
414			let by_bytes: Vec<&TaggedKey> = probes
415				.iter()
416				.filter(|(_, encoded)| contains(&bytes, encoded))
417				.map(|(key, _)| key)
418				.collect();
419			let by_typed: Vec<&TaggedKey> = probes
420				.iter()
421				.filter(|(key, _)| {
422					let bound = TaggedKeyBound::Key((*key).clone());
423					bound >= typed_start && bound < typed_end
424				})
425				.map(|(key, _)| key)
426				.collect();
427
428			assert!(!by_bytes.is_empty(), "storage {storage:?} selected nothing by bytes");
429			assert_eq!(by_bytes, by_typed, "storage {storage:?}");
430		}
431	}
432
433	fn contains(range: &EncodedKeyRange, key: &EncodedKey) -> bool {
434		let after_start = match &range.start {
435			Bound::Unbounded => true,
436			Bound::Included(start) => key >= start,
437			Bound::Excluded(start) => key > start,
438		};
439		let before_end = match &range.end {
440			Bound::Unbounded => true,
441			Bound::Included(end) => key <= end,
442			Bound::Excluded(end) => key < end,
443		};
444		after_start && before_end
445	}
446
447	fn mixed_bounds() -> Vec<TaggedKeyBound> {
448		let mut out = vec![
449			TaggedKeyBound::Kind(KeyTag::Row),
450			TaggedKeyBound::KindEnd(KeyTag::Row),
451			TaggedKeyBound::Kind(KeyTag::Table),
452			TaggedKeyBound::KindEnd(KeyTag::Table),
453		];
454		for storage in [1u64, 2, 3] {
455			let storage = StorageId::table(storage);
456			out.push(TaggedKeyBound::prefix(KeyTag::Row, storage_fields(storage)));
457			out.push(TaggedKeyBound::prefix_end(KeyTag::Row, storage_fields(storage)));
458		}
459		out.extend(rows().into_iter().map(|(key, _)| TaggedKeyBound::Key(key)));
460		out
461	}
462
463	#[test]
464	fn ordering_over_every_bound_shape_is_a_total_order() {
465		// BTreeMap compares in both directions, so an asymmetric arm silently corrupts lookup
466		// rather than failing loudly. A prefix end is only reached from one side by the range
467		// tests above, which cannot see that.
468		let bounds = mixed_bounds();
469		for left in &bounds {
470			for right in &bounds {
471				assert_eq!(
472					left.cmp(right),
473					right.cmp(left).reverse(),
474					"antisymmetry broken\n  left  = {left:?}\n  right = {right:?}"
475				);
476			}
477		}
478		for left in &bounds {
479			for middle in &bounds {
480				for right in &bounds {
481					if left <= middle && middle <= right {
482						assert!(
483							left <= right,
484							"transitivity broken\n  {left:?}\n  {middle:?}\n  {right:?}"
485						);
486					}
487				}
488			}
489		}
490	}
491
492	#[test]
493	fn a_prefix_end_sorts_above_every_key_that_extends_its_prefix() {
494		for storage in [1u64, 2, 3] {
495			let storage = StorageId::table(storage);
496			let end = TaggedKeyBound::prefix_end(KeyTag::Row, storage_fields(storage));
497			let start = TaggedKeyBound::prefix(KeyTag::Row, storage_fields(storage));
498			let mut extensions = 0;
499			for (key, _) in rows() {
500				let bound = TaggedKeyBound::Key(key.clone());
501				if bound >= start && bound < end {
502					extensions += 1;
503					assert!(end > bound, "{end:?} must sort above {bound:?}");
504					assert!(bound < end, "{bound:?} must sort below {end:?}");
505				}
506			}
507			assert!(extensions > 0, "storage {storage:?} has no extension to compare against");
508		}
509	}
510
511	#[test]
512	fn a_kind_range_brackets_the_whole_kind_inclusively() {
513		let mut start = KeySerializer::with_capacity(1);
514		start.extend_u8(DictionaryKey::TAG as u8);
515		let mut end = KeySerializer::with_capacity(1);
516		end.extend_u8(DictionaryKey::TAG as u8 - 1);
517
518		let encoded = TaggedKeyBoundRange::kind(KeyTag::Dictionary).encode();
519		assert_eq!(encoded.start, Bound::Included(start.to_encoded_key()));
520		assert_eq!(encoded.end, Bound::Included(end.to_encoded_key()));
521	}
522
523	#[test]
524	fn a_kind_span_brackets_every_key_of_that_kind_and_nothing_else() {
525		let start = TaggedKeyBound::Kind(KeyTag::Row);
526		let end = TaggedKeyBound::KindEnd(KeyTag::Row);
527		assert!(start < end, "the kind span must not be empty");
528		for (key, _) in rows() {
529			let bound = TaggedKeyBound::Key(key.clone());
530			let inside = bound >= start && bound <= end;
531			assert_eq!(inside, key.kind() == KeyTag::Row, "{key:?}");
532		}
533	}
534}
535
536#[cfg(test)]
537mod bound_order_matches_encoded_order {
538	use std::borrow::Cow;
539
540	use reifydb_codec::key::serializer::KeySerializer;
541	use smallvec::smallvec;
542
543	use super::*;
544	use crate::{
545		interface::catalog::{
546			id::{IndexId, TableId},
547			object::ObjectId,
548		},
549		key::{
550			any::{ByteEncoding, RawEncoding},
551			catalog::IndexEntryKey,
552		},
553		value::index::encoded::EncodedIndexKey,
554	};
555
556	fn table() -> ObjectId {
557		ObjectId::Table(TableId(1))
558	}
559
560	fn index() -> IndexId {
561		IndexId::primary(1u64)
562	}
563
564	// Index tails hold whatever the caller encoded, so a string tail arrives inverted and a
565	// prefix of the plaintext is a prefix of the encoded tail only after the same inversion.
566	fn entry(tail: &str) -> TaggedKeyBound {
567		let mut serializer = KeySerializer::new();
568		serializer.extend_str(tail);
569		TaggedKeyBound::Key(
570			IndexEntryKey::new(table(), index(), EncodedIndexKey::new(serializer.finish().as_slice()))
571				.into(),
572		)
573	}
574
575	fn tail_prefix(byte: u8) -> SmallVec<[OwnedField; 6]> {
576		object_fields(table())
577			.into_iter()
578			.chain([
579				Field::UAsc(Width::U8, 1),
580				Field::UDesc(Width::U64, index().as_u64() as u128),
581				Field::RawAsc(RawEncoding::Verbatim, Cow::Owned(vec![!byte])),
582			])
583			.collect()
584	}
585
586	fn probes() -> Vec<TaggedKeyBound> {
587		vec![
588			TaggedKeyBound::Kind(KeyTag::IndexEntry),
589			TaggedKeyBound::Prefix(KeyTag::IndexEntry, tail_prefix(b'a')),
590			TaggedKeyBound::PrefixEnd(KeyTag::IndexEntry, tail_prefix(b'a')),
591			TaggedKeyBound::Prefix(KeyTag::IndexEntry, tail_prefix(b'b')),
592			TaggedKeyBound::PrefixEnd(KeyTag::IndexEntry, tail_prefix(b'b')),
593			entry("a"),
594			entry("a1"),
595			entry("a3"),
596			entry("aa"),
597			entry("az"),
598			entry("b"),
599			entry("b1"),
600			entry("b2"),
601			entry("c1"),
602			TaggedKeyBound::Prefix(
603				KeyTag::IndexEntry,
604				object_fields(table()).into_iter().collect::<SmallVec<[OwnedField; 6]>>(),
605			),
606			TaggedKeyBound::PrefixEnd(
607				KeyTag::IndexEntry,
608				object_fields(table()).into_iter().collect::<SmallVec<[OwnedField; 6]>>(),
609			),
610			TaggedKeyBound::Prefix(
611				KeyTag::IndexEntry,
612				smallvec![Field::BytesDesc(ByteEncoding::Fixed, Cow::Owned(vec![7, 7]))],
613			),
614		]
615	}
616
617	#[test]
618	fn a_bound_orders_against_a_key_the_way_their_bytes_do() {
619		// Keys are what a bound is ultimately compared against: the pending-writes index is keyed
620		// by `Key` bounds and ranged by the others, and the storage engine merges the same span
621		// on bytes. A disagreement here is a range that silently includes or drops a row.
622		//
623		// Two non-key bounds may legitimately encode to the same byte position and still order
624		// strictly against each other (`PrefixEnd` of one group is `Prefix` of the next), which
625		// only makes range merging more conservative, so those pairs are not compared here.
626		let probes = probes();
627		for left in &probes {
628			for right in &probes {
629				if !matches!(left, TaggedKeyBound::Key(_)) && !matches!(right, TaggedKeyBound::Key(_)) {
630					continue;
631				}
632				let left_bytes = left.encode();
633				let right_bytes = right.encode();
634				assert_eq!(
635					left.cmp(right),
636					left_bytes.as_slice().cmp(right_bytes.as_slice()),
637					"bound order disagrees with encoded order\n  a = {left:?}\n  b = \
638					 {right:?}\n  a bytes = {:02x?}\n  b bytes = {:02x?}",
639					left_bytes.as_slice(),
640					right_bytes.as_slice()
641				);
642			}
643		}
644	}
645
646	#[test]
647	fn every_bound_pair_spans_the_same_keys_typed_as_it_does_encoded() {
648		// Ordering between two non-key bounds may differ from their bytes without harm, but the
649		// set of keys a range admits may not: that set is the range's meaning.
650		let probes = probes();
651		let keys: Vec<&TaggedKeyBound> =
652			probes.iter().filter(|bound| matches!(bound, TaggedKeyBound::Key(_))).collect();
653
654		for start in &probes {
655			for end in &probes {
656				let typed_start = Bound::Included(start.clone());
657				let typed_end = Bound::Excluded(end.clone());
658				let raw_start = Bound::Included(start.encode());
659				let raw_end = Bound::Excluded(end.encode());
660
661				for probe in &keys {
662					let bytes = probe.encode();
663					assert_eq!(
664						contains(&typed_start, &typed_end, probe),
665						contains_bytes(&raw_start, &raw_end, &bytes),
666						"typed and encoded spans disagree\n  start = {start:?}\n  end \
667						 = {end:?}\n  key = {probe:?}"
668					);
669				}
670			}
671		}
672	}
673
674	#[test]
675	fn a_prefix_range_contains_exactly_the_keys_its_encoded_form_contains() {
676		// The regression that motivated the truncation rule: `PrefixEnd` over a tail that is a
677		// strict byte prefix of a key's tail used to sort below that key, so a prefix range
678		// excluded every key it was built to cover.
679		let range = IndexEntryKey::key_prefix_range(table(), index(), &[!b'a']);
680		let encoded = range.encode();
681
682		for tail in ["a", "a1", "a3", "aa", "az", "b", "b1", "c1"] {
683			let bound = entry(tail);
684			let TaggedKeyBound::Key(key) = &bound else {
685				unreachable!("entry builds a Key bound");
686			};
687			let bytes = key.encode();
688
689			let typed = contains(&range.start, &range.end, &bound);
690			let raw = contains_bytes(&encoded.start, &encoded.end, &bytes);
691
692			assert_eq!(typed, raw, "typed and encoded containment disagree for tail {tail}");
693			assert_eq!(typed, tail.starts_with('a'), "wrong containment verdict for tail {tail}");
694		}
695	}
696
697	fn contains(start: &Bound<TaggedKeyBound>, end: &Bound<TaggedKeyBound>, probe: &TaggedKeyBound) -> bool {
698		let lower = match start {
699			Bound::Included(bound) => probe >= bound,
700			Bound::Excluded(bound) => probe > bound,
701			Bound::Unbounded => true,
702		};
703		let upper = match end {
704			Bound::Included(bound) => probe <= bound,
705			Bound::Excluded(bound) => probe < bound,
706			Bound::Unbounded => true,
707		};
708		lower && upper
709	}
710
711	fn contains_bytes(start: &Bound<EncodedKey>, end: &Bound<EncodedKey>, probe: &EncodedKey) -> bool {
712		let lower = match start {
713			Bound::Included(key) => probe >= key,
714			Bound::Excluded(key) => probe > key,
715			Bound::Unbounded => true,
716		};
717		let upper = match end {
718			Bound::Included(key) => probe <= key,
719			Bound::Excluded(key) => probe < key,
720			Bound::Unbounded => true,
721		};
722		lower && upper
723	}
724}