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