Skip to main content

reifydb_core/interface/
store.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use reifydb_codec::{
5	key::encoded::{EncodedKey, EncodedKeyRange},
6	row::bytes::EncodedBytes,
7};
8use reifydb_value::{Result, util::cowvec::CowVec};
9
10use crate::{
11	common::CommitVersion,
12	delta::Delta,
13	interface::catalog::storage::StorageId,
14	key::{
15		KeyRangeCodec,
16		any::TaggedKey,
17		row::{
18			PartitionedRowKey, PartitionedRowKeyRange, PartitionedSortedViewRowKey, RowKey, RowKeyRange,
19			SortedViewRowKey, StoragePartitionedRowKey, StorageRowKey,
20		},
21		series::{
22			PartitionedSeriesRowKey, PartitionedSeriesRowKeyRange, SeriesRowKey, SeriesRowKeyRange,
23			StoragePartitionedSeriesKey, StorageSeriesKey,
24		},
25		tag::KeyTag,
26		typed::BoundedKey,
27	},
28};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum Tier {
32	Buffer,
33	Persistent,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum EntryLayout {
38	Row,
39	Series,
40	SortedView,
41}
42
43impl EntryLayout {
44	pub fn type_tag(&self) -> u8 {
45		match self {
46			Self::Row => 1,
47			Self::Series => 2,
48			Self::SortedView => 3,
49		}
50	}
51
52	pub fn from_type_tag(tag: u8) -> Option<Self> {
53		match tag {
54			1 => Some(Self::Row),
55			2 => Some(Self::Series),
56			3 => Some(Self::SortedView),
57			_ => None,
58		}
59	}
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub enum EntryKind {
64	Multi,
65
66	Source(StorageId, EntryLayout),
67
68	PartitionedSource(StorageId, EntryLayout),
69}
70
71impl EntryKind {
72	pub fn caches_ranges(&self) -> bool {
73		matches!(self, Self::Source(_, EntryLayout::Row | EntryLayout::Series))
74	}
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78pub enum StorageKey {
79	Table(StorageRowKey),
80	RingBuffer(StorageRowKey),
81	Queue(StorageRowKey),
82	View(StorageRowKey),
83
84	PartitionedTable(StoragePartitionedRowKey),
85	PartitionedRingBuffer(StoragePartitionedRowKey),
86	PartitionedQueue(StoragePartitionedRowKey),
87	PartitionedView(StoragePartitionedRowKey),
88
89	Series(StorageSeriesKey),
90	SeriesView(StorageSeriesKey),
91
92	PartitionedSeries(StoragePartitionedSeriesKey),
93	PartitionedSeriesView(StoragePartitionedSeriesKey),
94}
95
96fn row_storage_key(storage: StorageId, row: StorageRowKey) -> Option<StorageKey> {
97	match storage {
98		StorageId::Table(_) => Some(StorageKey::Table(row)),
99		StorageId::RingBuffer(_) => Some(StorageKey::RingBuffer(row)),
100		StorageId::Queue(_) => Some(StorageKey::Queue(row)),
101		StorageId::View(_) => Some(StorageKey::View(row)),
102		StorageId::Series(_) => None,
103	}
104}
105
106fn partitioned_row_storage_key(storage: StorageId, row: StoragePartitionedRowKey) -> Option<StorageKey> {
107	match storage {
108		StorageId::Table(_) => Some(StorageKey::PartitionedTable(row)),
109		StorageId::RingBuffer(_) => Some(StorageKey::PartitionedRingBuffer(row)),
110		StorageId::Queue(_) => Some(StorageKey::PartitionedQueue(row)),
111		StorageId::View(_) => Some(StorageKey::PartitionedView(row)),
112		StorageId::Series(_) => None,
113	}
114}
115
116fn series_storage_key(storage: StorageId, series: StorageSeriesKey) -> Option<StorageKey> {
117	match storage {
118		StorageId::Series(_) => Some(StorageKey::Series(series)),
119		StorageId::View(_) => Some(StorageKey::SeriesView(series)),
120		StorageId::Table(_) | StorageId::RingBuffer(_) | StorageId::Queue(_) => None,
121	}
122}
123
124fn partitioned_series_storage_key(storage: StorageId, series: StoragePartitionedSeriesKey) -> Option<StorageKey> {
125	match storage {
126		StorageId::Series(_) => Some(StorageKey::PartitionedSeries(series)),
127		StorageId::View(_) => Some(StorageKey::PartitionedSeriesView(series)),
128		StorageId::Table(_) | StorageId::RingBuffer(_) | StorageId::Queue(_) => None,
129	}
130}
131
132fn source_entry(storage: StorageId, layout: EntryLayout, key: Option<StorageKey>) -> (EntryKind, Option<StorageKey>) {
133	match key {
134		Some(key) => (EntryKind::Source(storage, layout), Some(key)),
135		None => (EntryKind::Multi, None),
136	}
137}
138
139fn partitioned_source_entry(
140	storage: StorageId,
141	layout: EntryLayout,
142	key: Option<StorageKey>,
143) -> (EntryKind, Option<StorageKey>) {
144	match key {
145		Some(key) => (EntryKind::PartitionedSource(storage, layout), Some(key)),
146		None => (EntryKind::Multi, None),
147	}
148}
149
150pub fn storage_key(key: &EncodedKey) -> (EntryKind, Option<StorageKey>) {
151	match KeyTag::of(key) {
152		Some(KeyTag::Row) => match RowKey::decode(key) {
153			Some(row_key) => source_entry(
154				row_key.storage,
155				EntryLayout::Row,
156				row_storage_key(row_key.storage, StorageRowKey::new(row_key.row)),
157			),
158			None => (EntryKind::Multi, None),
159		},
160		Some(KeyTag::SeriesRow) => match SeriesRowKey::decode(key) {
161			Some(series_key) => source_entry(
162				series_key.storage,
163				EntryLayout::Series,
164				series_storage_key(series_key.storage, StorageSeriesKey::from(series_key)),
165			),
166			None => (EntryKind::Multi, None),
167		},
168		Some(KeyTag::PartitionedRow) => match PartitionedRowKey::decode(key) {
169			Some(partitioned_key) => partitioned_source_entry(
170				partitioned_key.storage,
171				EntryLayout::Row,
172				partitioned_row_storage_key(
173					partitioned_key.storage,
174					StoragePartitionedRowKey::new(partitioned_key.partition, partitioned_key.row),
175				),
176			),
177			None => (EntryKind::Multi, None),
178		},
179		Some(KeyTag::PartitionedSeriesRow) => match PartitionedSeriesRowKey::decode(key) {
180			Some(partitioned_key) => partitioned_source_entry(
181				partitioned_key.storage,
182				EntryLayout::Series,
183				partitioned_series_storage_key(
184					partitioned_key.storage,
185					StoragePartitionedSeriesKey::from(partitioned_key),
186				),
187			),
188			None => (EntryKind::Multi, None),
189		},
190		Some(KeyTag::SortedViewRow) => match SortedViewRowKey::storage_of(key) {
191			Some(storage) => (EntryKind::Source(storage, EntryLayout::SortedView), None),
192			None => (EntryKind::Multi, None),
193		},
194		Some(KeyTag::PartitionedSortedViewRow) => match PartitionedSortedViewRowKey::storage_of(key) {
195			Some(storage) => (EntryKind::PartitionedSource(storage, EntryLayout::SortedView), None),
196			None => (EntryKind::Multi, None),
197		},
198		_ => (EntryKind::Multi, None),
199	}
200}
201
202pub fn storage_key_of(key: &TaggedKey) -> (EntryKind, Option<StorageKey>) {
203	match key {
204		TaggedKey::Row(row_key) => source_entry(
205			row_key.storage,
206			EntryLayout::Row,
207			row_storage_key(row_key.storage, StorageRowKey::new(row_key.row)),
208		),
209		TaggedKey::SeriesRow(series_key) => source_entry(
210			series_key.storage,
211			EntryLayout::Series,
212			series_storage_key(series_key.storage, StorageSeriesKey::from(series_key.clone())),
213		),
214		TaggedKey::PartitionedRow(partitioned_key) => partitioned_source_entry(
215			partitioned_key.storage,
216			EntryLayout::Row,
217			partitioned_row_storage_key(
218				partitioned_key.storage,
219				StoragePartitionedRowKey::new(partitioned_key.partition, partitioned_key.row),
220			),
221		),
222		TaggedKey::PartitionedSeriesRow(partitioned_key) => partitioned_source_entry(
223			partitioned_key.storage,
224			EntryLayout::Series,
225			partitioned_series_storage_key(
226				partitioned_key.storage,
227				StoragePartitionedSeriesKey::from(partitioned_key.clone()),
228			),
229		),
230		TaggedKey::SortedViewRow(sorted) => (EntryKind::Source(sorted.storage, EntryLayout::SortedView), None),
231		TaggedKey::PartitionedSortedViewRow(sorted) => {
232			(EntryKind::PartitionedSource(sorted.storage, EntryLayout::SortedView), None)
233		}
234		_ => (EntryKind::Multi, None),
235	}
236}
237
238pub fn classify_key_of(key: &TaggedKey) -> EntryKind {
239	storage_key_of(key).0
240}
241
242pub fn classify_key(key: &EncodedKey) -> EntryKind {
243	storage_key(key).0
244}
245
246pub fn classify_range(range: &EncodedKeyRange) -> Option<EntryKind> {
247	if let (Some(start), Some(_end)) = RowKeyRange::decode(range) {
248		return row_storage_key(start.storage, <StorageRowKey as BoundedKey>::low())
249			.map(|_| EntryKind::Source(start.storage, EntryLayout::Row));
250	}
251
252	if let (Some(start), Some(_end)) = SeriesRowKeyRange::decode(range) {
253		return series_storage_key(start, <StorageSeriesKey as BoundedKey>::low())
254			.map(|_| EntryKind::Source(start, EntryLayout::Series));
255	}
256
257	if let (Some(start), Some(_end)) = PartitionedRowKeyRange::decode(range) {
258		return partitioned_row_storage_key(start.storage, <StoragePartitionedRowKey as BoundedKey>::low())
259			.map(|_| EntryKind::PartitionedSource(start.storage, EntryLayout::Row));
260	}
261
262	if let (Some(start), Some(_end)) = PartitionedSeriesRowKeyRange::decode(range) {
263		return partitioned_series_storage_key(start, <StoragePartitionedSeriesKey as BoundedKey>::low())
264			.map(|_| EntryKind::PartitionedSource(start, EntryLayout::Series));
265	}
266
267	if let Some(storage) = SortedViewRowKey::range_storage_of(range) {
268		return Some(EntryKind::Source(storage, EntryLayout::SortedView));
269	}
270
271	if let Some(storage) = PartitionedSortedViewRowKey::range_storage_of(range) {
272		return Some(EntryKind::PartitionedSource(storage, EntryLayout::SortedView));
273	}
274
275	None
276}
277
278#[derive(Debug, Clone)]
279pub struct MultiVersionRow<K = EncodedKey> {
280	pub key: K,
281	pub bytes: EncodedBytes,
282	pub version: CommitVersion,
283}
284
285#[derive(Debug, Clone)]
286pub struct SingleVersionRow {
287	pub key: EncodedKey,
288	pub bytes: EncodedBytes,
289}
290
291#[derive(Debug, Clone)]
292pub struct MultiVersionBatch<K = EncodedKey> {
293	pub items: Vec<MultiVersionRow<K>>,
294
295	pub has_more: bool,
296}
297
298impl<K> MultiVersionBatch<K> {
299	pub fn empty() -> Self {
300		Self {
301			items: Vec::new(),
302			has_more: false,
303		}
304	}
305
306	pub fn is_empty(&self) -> bool {
307		self.items.is_empty()
308	}
309}
310
311pub trait MultiVersionCommit: Send + Sync {
312	fn commit(&self, deltas: CowVec<Delta>, version: CommitVersion) -> Result<()>;
313}
314
315pub trait MultiVersionGet: Send + Sync {
316	fn get(&self, key: &TaggedKey, version: CommitVersion) -> Result<Option<MultiVersionRow<TaggedKey>>>;
317}
318
319pub trait MultiVersionContains: Send + Sync {
320	fn contains(&self, key: &TaggedKey, version: CommitVersion) -> Result<bool>;
321}
322
323pub trait MultiVersionGetPrevious: Send + Sync {
324	fn get_previous_version(
325		&self,
326		key: &TaggedKey,
327		before_version: CommitVersion,
328	) -> Result<Option<MultiVersionRow<TaggedKey>>>;
329}
330
331pub trait MultiVersionStore:
332	Send + Sync + Clone + MultiVersionCommit + MultiVersionGet + MultiVersionGetPrevious + MultiVersionContains + 'static
333{
334}
335
336#[derive(Debug, Clone)]
337pub struct SingleVersionBatch {
338	pub items: Vec<SingleVersionRow>,
339
340	pub has_more: bool,
341}
342
343impl SingleVersionBatch {
344	pub fn empty() -> Self {
345		Self {
346			items: Vec::new(),
347			has_more: false,
348		}
349	}
350
351	pub fn is_empty(&self) -> bool {
352		self.items.is_empty()
353	}
354}
355
356pub trait SingleVersionCommit: Send + Sync {
357	fn commit(&mut self, deltas: CowVec<Delta>) -> Result<()>;
358}
359
360pub trait SingleVersionGet: Send + Sync {
361	fn get(&self, key: &EncodedKey) -> Result<Option<SingleVersionRow>>;
362}
363
364pub trait SingleVersionContains: Send + Sync {
365	fn contains(&self, key: &EncodedKey) -> Result<bool>;
366}
367
368pub trait SingleVersionRange: Send + Sync {
369	fn range_batch(&self, range: EncodedKeyRange, batch_size: u64) -> Result<SingleVersionBatch>;
370
371	fn range(&self, range: EncodedKeyRange) -> Result<SingleVersionBatch> {
372		self.range_batch(range, 1024)
373	}
374
375	fn prefix(&self, prefix: &EncodedKey) -> Result<SingleVersionBatch> {
376		self.range(EncodedKeyRange::prefix(prefix))
377	}
378}
379
380pub trait SingleVersionRangeRev: Send + Sync {
381	fn range_rev_batch(&self, range: EncodedKeyRange, batch_size: u64) -> Result<SingleVersionBatch>;
382
383	fn range_rev(&self, range: EncodedKeyRange) -> Result<SingleVersionBatch> {
384		self.range_rev_batch(range, 1024)
385	}
386
387	fn prefix_rev(&self, prefix: &EncodedKey) -> Result<SingleVersionBatch> {
388		self.range_rev(EncodedKeyRange::prefix(prefix))
389	}
390}
391
392pub trait SingleVersionStore:
393	Send
394	+ Sync
395	+ Clone
396	+ SingleVersionCommit
397	+ SingleVersionGet
398	+ SingleVersionContains
399	+ SingleVersionRange
400	+ SingleVersionRangeRev
401	+ 'static
402{
403}
404
405#[cfg(test)]
406mod tests {
407	use reifydb_codec::key::encoded::EncodedKey;
408	use reifydb_value::value::{Value, partition::Partition, row_number::RowNumber};
409
410	use super::{EntryKind, EntryLayout, StorageKey, classify_key, classify_range, storage_key};
411	use crate::{
412		interface::catalog::{
413			id::{SeriesId, TableId, ViewId},
414			storage::StorageId,
415		},
416		key::{
417			any::TaggedKey,
418			row::{
419				PartitionedRowKey, PartitionedSortedViewRowKey, RowKey, RowSequenceKey,
420				SortedViewRowKey, StoragePartitionedRowKey, StorageRowKey,
421			},
422			series::{
423				PartitionedSeriesRowKey, PartitionedSeriesRowKeyRange, SeriesRowKey, SeriesRowKeyRange,
424				StoragePartitionedSeriesKey, StorageSeriesKey,
425			},
426		},
427	};
428
429	fn part(v: &str) -> Partition {
430		Partition::of(&[Value::Utf8(v.to_string())])
431	}
432
433	#[test]
434	fn storage_key_hands_back_the_identity_it_decoded() {
435		// the store classifies a key and the tier below then decodes the same bytes again for the row
436		// number; the route must carry that identity so the second decode has nothing left to do
437		let storage = StorageId::Table(TableId(7));
438
439		let row = RowKey::encoded(storage, RowNumber(5));
440		assert_eq!(
441			storage_key(&row),
442			(
443				EntryKind::Source(storage, EntryLayout::Row),
444				Some(StorageKey::Table(StorageRowKey::new(RowNumber(5))))
445			)
446		);
447
448		let partitioned = PartitionedRowKey::encoded(storage, part("us"), RowNumber(5));
449		assert_eq!(
450			storage_key(&partitioned),
451			(
452				EntryKind::PartitionedSource(storage, EntryLayout::Row),
453				Some(StorageKey::PartitionedTable(StoragePartitionedRowKey::new(
454					part("us"),
455					RowNumber(5)
456				)))
457			)
458		);
459	}
460
461	#[test]
462	fn storage_key_names_the_storage_a_row_belongs_to() {
463		// the variant is what keeps a queue row out of a ringbuffer's cache drawer, so every storage kind
464		// that writes plain rows must get its own one rather than a shared Row
465		let row = StorageRowKey::new(RowNumber(5));
466		for (storage, expected) in [
467			(StorageId::table(7), StorageKey::Table(row)),
468			(StorageId::ringbuffer(7), StorageKey::RingBuffer(row)),
469			(StorageId::queue(7), StorageKey::Queue(row)),
470			(StorageId::view(7), StorageKey::View(row)),
471		] {
472			assert_eq!(storage_key(&RowKey::encoded(storage, RowNumber(5))).1, Some(expected));
473		}
474	}
475
476	#[test]
477	fn a_series_key_carries_its_own_identity_on_a_series_and_on_a_view() {
478		// a view's series rows and its plain rows both classify to Source(view); the storage key is what
479		// keeps them in separate cache drawers now that neither is stored by its whole encoded key
480		let series = StorageSeriesKey::new(None, 5, 1);
481		for (storage, expected) in [
482			(StorageId::series(7), StorageKey::Series(series)),
483			(StorageId::view(7), StorageKey::SeriesView(series)),
484		] {
485			let series = SeriesRowKey {
486				storage,
487				variant_tag: None,
488				key: 5,
489				sequence: 1,
490			}
491			.encode();
492			assert_eq!(
493				storage_key(&series),
494				(EntryKind::Source(storage, EntryLayout::Series), Some(expected))
495			);
496		}
497
498		let partitioned = StoragePartitionedSeriesKey::new(part("us"), None, 5, 1);
499		for (storage, expected) in [
500			(StorageId::series(7), StorageKey::PartitionedSeries(partitioned)),
501			(StorageId::view(7), StorageKey::PartitionedSeriesView(partitioned)),
502		] {
503			let partitioned = PartitionedSeriesRowKey::encoded(storage, part("us"), None, 5, 1);
504			assert_eq!(
505				storage_key(&partitioned),
506				(EntryKind::PartitionedSource(storage, EntryLayout::Series), Some(expected))
507			);
508		}
509	}
510
511	#[test]
512	fn a_view_row_and_a_view_series_row_do_not_share_a_storage_key() {
513		// a view is one storage kind holding one layout, so the two must land on distinct entries and
514		// distinct identities, or the point tier hands a series row back for a plain row lookup
515		let storage = StorageId::view(7);
516		let row = storage_key(&RowKey::encoded(storage, RowNumber(5))).1.unwrap();
517		let series = storage_key(
518			&SeriesRowKey {
519				storage,
520				variant_tag: None,
521				key: 5,
522				sequence: 5,
523			}
524			.encode(),
525		)
526		.1
527		.unwrap();
528		assert_ne!(row, series);
529	}
530
531	#[test]
532	fn a_layout_its_storage_cannot_hold_claims_no_entry_at_all() {
533		// a plain row key naming a series storage is nonsense; letting it claim a source entry would put a
534		// key with no identity in a bucket every other member of has one
535		let storage = StorageId::series(7);
536		assert_eq!(storage_key(&RowKey::encoded(storage, RowNumber(5))), (EntryKind::Multi, None));
537
538		let series_on_a_table = SeriesRowKey {
539			storage: StorageId::table(7),
540			variant_tag: None,
541			key: 5,
542			sequence: 1,
543		}
544		.encode();
545		assert_eq!(storage_key(&series_on_a_table), (EntryKind::Multi, None));
546	}
547
548	#[test]
549	fn a_source_entry_never_holds_a_key_without_an_identity() {
550		// every tier below reads the identity, not the bytes, once a key is in a source bucket; a member
551		// with none would have to be found by a scan the bucket has no index for
552		let series = |storage| {
553			SeriesRowKey {
554				storage,
555				variant_tag: None,
556				key: 5,
557				sequence: 1,
558			}
559			.encode()
560		};
561
562		for storage in [
563			StorageId::table(7),
564			StorageId::ringbuffer(7),
565			StorageId::queue(7),
566			StorageId::view(7),
567			StorageId::series(7),
568		] {
569			for key in [
570				RowKey::encoded(storage, RowNumber(5)),
571				PartitionedRowKey::encoded(storage, part("us"), RowNumber(5)),
572				series(storage),
573				PartitionedSeriesRowKey::encoded(storage, part("us"), None, 5, 1),
574				RowSequenceKey::encoded(storage),
575			] {
576				let (entry, ident) = storage_key(&key);
577				match entry {
578					EntryKind::Multi => assert_eq!(ident, None),
579					EntryKind::Source(_, _) | EntryKind::PartitionedSource(_, _) => {
580						assert!(ident.is_some(), "{entry:?} took a key with no identity")
581					}
582				}
583			}
584		}
585	}
586
587	#[test]
588	fn an_entry_never_mixes_the_two_layouts() {
589		// the narrow sqlite schema and the point tier drawers are both picked from the entry alone, so an
590		// entry that admitted both layouts would store a series sequence in a column that has no room
591		let storage = StorageId::view(7);
592		let row = storage_key(&RowKey::encoded(storage, RowNumber(5))).0;
593		let series = storage_key(
594			&SeriesRowKey {
595				storage,
596				variant_tag: None,
597				key: 5,
598				sequence: 1,
599			}
600			.encode(),
601		)
602		.0;
603
604		assert_eq!(row, EntryKind::Source(storage, EntryLayout::Row));
605		assert_eq!(series, EntryKind::Source(storage, EntryLayout::Series));
606
607		let partitioned_row = storage_key(&PartitionedRowKey::encoded(storage, part("us"), RowNumber(5))).0;
608		let partitioned_series =
609			storage_key(&PartitionedSeriesRowKey::encoded(storage, part("us"), None, 5, 1)).0;
610
611		assert_eq!(partitioned_row, EntryKind::PartitionedSource(storage, EntryLayout::Row));
612		assert_eq!(partitioned_series, EntryKind::PartitionedSource(storage, EntryLayout::Series));
613	}
614
615	#[test]
616	fn storage_key_agrees_with_classify_key_on_the_entry() {
617		let storage = StorageId::Table(TableId(7));
618		for key in [
619			RowKey::encoded(storage, RowNumber(5)),
620			PartitionedRowKey::encoded(storage, part("us"), RowNumber(5)),
621			RowSequenceKey::encoded(storage),
622		] {
623			assert_eq!(storage_key(&key).0, classify_key(&key));
624		}
625	}
626
627	#[test]
628	fn an_over_long_row_kind_key_never_passes_as_a_row() {
629		// A key carrying KeyTag::Row but running past the fixed row layout must land in Multi: the
630		// source bucket keys on StorageRowKey alone, so two such keys differing only in their trailing
631		// bytes would alias onto one identity and a get could return another row's bytes.
632		let storage = StorageId::view(3);
633		let mut over_long = RowKey::encoded(storage, RowNumber(1)).as_slice().to_vec();
634		over_long.extend_from_slice(&[0xAA; 8]);
635		over_long.extend_from_slice(&99u64.to_be_bytes());
636
637		assert_eq!(storage_key(&EncodedKey::new(over_long)), (EntryKind::Multi, None));
638	}
639
640	#[test]
641	fn a_sorted_view_key_and_a_sorted_view_scan_range_classify_the_same_way() {
642		// The sink classifies the key it writes; the scan classifies a range over the same keyspace.
643		// When the two disagree the rows are written to one bucket and read from another, and the
644		// view comes back empty with no error raised anywhere along the way.
645		let storage = StorageId::view(3);
646
647		let mut sorted_view = SortedViewRowKey::storage_start(storage).as_slice().to_vec();
648		sorted_view.extend_from_slice(&[0xAA; 8]);
649		sorted_view.extend_from_slice(&99u64.to_be_bytes());
650		let sorted_view = EncodedKey::new(sorted_view);
651		let range = SortedViewRowKey::scan_range(storage, None).encode();
652		assert_eq!(classify_key(&sorted_view), EntryKind::Source(storage, EntryLayout::SortedView));
653		assert_eq!(classify_range(&range).unwrap_or(EntryKind::Multi), classify_key(&sorted_view));
654
655		let mut partitioned = PartitionedSortedViewRowKey::storage_start(storage).as_slice().to_vec();
656		partitioned.extend_from_slice(&[0xBB; 16]);
657		partitioned.extend_from_slice(&[0xAA; 8]);
658		partitioned.extend_from_slice(&99u64.to_be_bytes());
659		let partitioned = EncodedKey::new(partitioned);
660		let partitioned_range = PartitionedSortedViewRowKey::scan_range(storage, None).encode();
661		assert_eq!(classify_key(&partitioned), EntryKind::PartitionedSource(storage, EntryLayout::SortedView));
662		assert_eq!(classify_range(&partitioned_range).unwrap_or(EntryKind::Multi), classify_key(&partitioned));
663	}
664
665	#[test]
666	fn a_sorted_view_scan_range_never_reaches_the_narrowed_row_bucket() {
667		// The narrowed bucket keys on StorageRowKey alone, which cannot carry the sort prefix that
668		// orders these rows, so routing a sorted view range there loses the view's order outright.
669		let storage = StorageId::view(3);
670		assert_ne!(
671			classify_range(&SortedViewRowKey::scan_range(storage, None).encode()),
672			Some(EntryKind::Source(storage, EntryLayout::Row))
673		);
674		assert_ne!(
675			classify_range(&PartitionedSortedViewRowKey::scan_range(storage, None).encode()),
676			Some(EntryKind::PartitionedSource(storage, EntryLayout::Row))
677		);
678	}
679
680	#[test]
681	fn storage_key_leaves_a_key_it_does_not_own_without_an_identity() {
682		assert_eq!(
683			storage_key(&RowSequenceKey::encoded(StorageId::Table(TableId(7)))),
684			(EntryKind::Multi, None)
685		);
686	}
687
688	#[test]
689	fn classify_key_partitioned_row_is_partitioned_source() {
690		let storage = StorageId::Table(TableId(7));
691		let key = PartitionedRowKey::encoded(storage, part("us"), RowNumber(1));
692		assert_eq!(classify_key(&key), EntryKind::PartitionedSource(storage, EntryLayout::Row));
693	}
694
695	#[test]
696	fn classify_key_partitioned_view_row_is_partitioned_source() {
697		// A view that owns its rows must classify to its own id, not fall through to EntryKind::Multi.
698		let storage = StorageId::view(7);
699		let key = PartitionedRowKey::encoded(storage, part("us"), RowNumber(1));
700		assert_eq!(classify_key(&key), EntryKind::PartitionedSource(storage, EntryLayout::Row));
701		assert_ne!(
702			classify_key(&key),
703			EntryKind::PartitionedSource(StorageId::table(7), EntryLayout::Row),
704			"a view and a table sharing id 7 must not classify to the same entry"
705		);
706	}
707
708	#[test]
709	fn classify_key_row_is_still_source() {
710		let storage = StorageId::Table(TableId(7));
711		let key = RowKey::encoded(storage, RowNumber(1));
712		assert_eq!(classify_key(&key), EntryKind::Source(storage, EntryLayout::Row));
713	}
714
715	#[test]
716	fn classify_range_all_partition_forms_are_partitioned_source_for_table_and_view() {
717		// Every range form must carry the owning variant, or a view's sweep hits the table of the same id.
718		for storage in [StorageId::Table(TableId(9)), StorageId::view(9)] {
719			let p = part("us");
720			let last = TaggedKey::from(PartitionedRowKey::new(storage, p, RowNumber(5)));
721			assert_eq!(
722				classify_range(&PartitionedRowKey::partition_range(storage, p).encode()),
723				Some(EntryKind::PartitionedSource(storage, EntryLayout::Row))
724			);
725			assert_eq!(
726				classify_range(
727					&PartitionedRowKey::partition_scan_range(storage, p, Some(&last)).encode()
728				),
729				Some(EntryKind::PartitionedSource(storage, EntryLayout::Row))
730			);
731			assert_eq!(
732				classify_range(&PartitionedRowKey::scan_range(storage, None).encode()),
733				Some(EntryKind::PartitionedSource(storage, EntryLayout::Row))
734			);
735			assert_eq!(
736				classify_range(&PartitionedRowKey::full_scan(storage).encode()),
737				Some(EntryKind::PartitionedSource(storage, EntryLayout::Row))
738			);
739		}
740	}
741
742	#[test]
743	fn classify_range_row_range_is_still_source() {
744		let storage = StorageId::Table(TableId(9));
745		assert_eq!(
746			classify_range(&RowKey::full_scan(storage).encode()),
747			Some(EntryKind::Source(storage, EntryLayout::Row))
748		);
749	}
750
751	#[test]
752	fn classify_key_and_classify_range_agree_for_the_series_kinds() {
753		// expired_batch picks its indexed scan by classify_range while the tier stores entries by classify_key;
754		// if the two disagree the evictor scans an entry that holds none of the rows it is trying to expire.
755		let series = StorageId::series(SeriesId(11));
756		let view = StorageId::View(ViewId(11));
757
758		for storage in [series, view] {
759			let key = SeriesRowKey {
760				storage,
761				variant_tag: None,
762				key: 5,
763				sequence: 1,
764			}
765			.encode();
766			assert_eq!(classify_key(&key), EntryKind::Source(storage, EntryLayout::Series));
767			assert_eq!(
768				classify_range(&SeriesRowKeyRange::full_scan(storage, None).encode()),
769				Some(EntryKind::Source(storage, EntryLayout::Series))
770			);
771
772			let partitioned = PartitionedSeriesRowKey::encoded(
773				storage,
774				Partition::of(&[Value::Utf8("us".to_string())]),
775				None,
776				5,
777				1,
778			);
779			assert_eq!(
780				classify_key(&partitioned),
781				EntryKind::PartitionedSource(storage, EntryLayout::Series)
782			);
783			assert_eq!(
784				classify_range(&PartitionedSeriesRowKeyRange::full_scan(storage).encode()),
785				Some(EntryKind::PartitionedSource(storage, EntryLayout::Series))
786			);
787		}
788	}
789}