Skip to main content

reifydb_store_commit/
store.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	cmp::Reverse,
6	collections::{BTreeSet, HashMap, HashSet, VecDeque},
7	iter, mem,
8	ops::Bound,
9	sync::{Arc, atomic::Ordering},
10};
11
12use reifydb_codec::key::encoded::EncodedKey;
13use reifydb_core::{
14	common::CommitVersion,
15	interface::store::EntryKind,
16	metrics::{collect::MetricsCollector, sample::MetricsSample},
17};
18use reifydb_value::{Result, byte_size::ByteSize, count::Count, util::cowvec::CowVec};
19use tracing::{Span, field, instrument};
20
21use crate::{
22	HistoricalSweep, MultiVersionScope, RangeBatch, RangeCursor, RawEntry, TierBatch, VersionedGetResult,
23	entry::{Entries, Entry, entry_bytes},
24	rows::{ActiveRows, MergedRows, Removed, RowMap, lookup, newest_across},
25};
26
27type EvictablePersist = Vec<(EncodedKey, CommitVersion, Option<CowVec<u8>>)>;
28type EvictableDrop = Vec<EvictedVersion>;
29
30const CLOSE_BYTE_THRESHOLD: ByteSize = ByteSize::from_mib(16);
31
32#[derive(Clone, Debug)]
33pub struct EvictedVersion {
34	pub key: EncodedKey,
35	pub version: CommitVersion,
36	pub value_bytes: ByteSize,
37	pub current: bool,
38}
39
40fn value_bytes_of(value: &Option<CowVec<u8>>) -> ByteSize {
41	ByteSize::from_bytes(value.as_ref().map(|v| v.len() as u64).unwrap_or(0))
42}
43
44#[derive(Clone)]
45pub struct CommitStore {
46	inner: Arc<CommitStoreInner>,
47}
48
49struct CommitStoreInner {
50	entries: Entries,
51	close_threshold: u64,
52}
53
54impl Default for CommitStore {
55	fn default() -> Self {
56		Self::new()
57	}
58}
59
60impl CommitStore {
61	#[instrument(name = "store::multi::memory::new", level = "debug")]
62	pub fn new() -> Self {
63		Self::with_close_threshold(CLOSE_BYTE_THRESHOLD)
64	}
65
66	pub fn with_close_threshold(threshold: ByteSize) -> Self {
67		Self {
68			inner: Arc::new(CommitStoreInner {
69				entries: Entries::default(),
70				close_threshold: threshold.as_bytes().max(1),
71			}),
72		}
73	}
74
75	pub fn estimated_current_count(&self, table: EntryKind) -> Result<u64> {
76		let Some(entry) = self.inner.entries.data.get(&table) else {
77			return Ok(0);
78		};
79		Ok(entry.key_count.load(Ordering::Relaxed))
80	}
81
82	pub fn list_all_entry_kinds(&self) -> Result<Vec<EntryKind>> {
83		Ok(self.inner.entries.data.keys())
84	}
85
86	fn collect_oldest_pending(&self) -> Vec<(EntryKind, CommitVersion)> {
87		self.inner
88			.entries
89			.data
90			.keys()
91			.into_iter()
92			.filter_map(|kind| Some((kind, self.oldest_pending_for(kind)?)))
93			.collect()
94	}
95
96	pub fn list_entry_kinds_by_oldest_pending(&self) -> Result<Vec<EntryKind>> {
97		let mut pending = self.collect_oldest_pending();
98		pending.sort_by_key(|(_, version)| *version);
99		Ok(pending.into_iter().map(|(kind, _)| kind).collect())
100	}
101
102	pub fn oldest_pending_for(&self, kind: EntryKind) -> Option<CommitVersion> {
103		let entry = self.inner.entries.data.get(&kind)?;
104		let active = entry.active.read().min_version();
105		let closed = entry.closed.read().iter().map(|map| map.min_version()).min();
106		match (active, closed) {
107			(Some(a), Some(c)) => Some(a.min(c)),
108			(Some(a), None) => Some(a),
109			(None, closed) => closed,
110		}
111	}
112
113	fn resident_bytes(&self, current: bool) -> ByteSize {
114		let total = self
115			.inner
116			.entries
117			.data
118			.keys()
119			.into_iter()
120			.filter_map(|kind| self.inner.entries.data.get(&kind))
121			.map(|entry| {
122				let pick = |rows: &RowMap| {
123					if current {
124						rows.current_bytes()
125					} else {
126						rows.historical_bytes()
127					}
128				};
129				let active = pick(entry.active.read().rows());
130				let closed: u64 = entry.closed.read().iter().map(|map| pick(map.rows())).sum();
131				active + closed
132			})
133			.sum();
134		ByteSize::from_bytes(total)
135	}
136
137	pub fn current_resident_bytes(&self) -> ByteSize {
138		self.resident_bytes(true)
139	}
140
141	pub fn historical_resident_bytes(&self) -> ByteSize {
142		self.resident_bytes(false)
143	}
144
145	#[inline]
146	#[instrument(name = "store::multi::memory::get_or_create_table", level = "trace", skip(self), fields(table = ?table))]
147	fn get_or_create_table(&self, table: EntryKind) -> Arc<Entry> {
148		self.inner.entries.data.get_or_insert_with(table, || Arc::new(Entry::new()))
149	}
150
151	fn close_active(entry: &Entry, active: &mut ActiveRows) {
152		if active.is_empty() {
153			return;
154		}
155		let closing = mem::take(active);
156		entry.closed.write().push_back(Arc::new(closing.close()));
157	}
158
159	#[inline]
160	#[instrument(name = "store::multi::memory::set::table", level = "trace", skip(self, entries), fields(
161		table = ?table,
162		entry_count = entries.len(),
163	))]
164	fn process_table(
165		&self,
166		table: EntryKind,
167		version: CommitVersion,
168		entries: Vec<(EncodedKey, Option<CowVec<u8>>)>,
169	) {
170		let table_entry = self.get_or_create_table(table);
171		let mut active = table_entry.active_write();
172		let keys_before = active.rows().key_count();
173		let mut pending = table_entry.pending.lock();
174		for (key, value) in entries {
175			pending.insert(key.clone());
176			active.insert(key, version, value);
177		}
178		drop(pending);
179		let keys_added = active.rows().key_count() - keys_before;
180		table_entry.key_count.fetch_add(keys_added as u64, Ordering::Relaxed);
181		if active.bytes() >= self.inner.close_threshold {
182			Self::close_active(&table_entry, &mut active);
183		}
184	}
185
186	pub fn oldest_pending_version(&self) -> Option<CommitVersion> {
187		self.collect_oldest_pending().into_iter().map(|(_, version)| version).min()
188	}
189
190	fn seal_for_flush(&self, table: EntryKind, cutoff: CommitVersion) {
191		let Some(entry) = self.inner.entries.data.get(&table) else {
192			return;
193		};
194		let mut active = entry.active_write();
195		if active.min_version().is_some_and(|oldest| oldest <= cutoff) {
196			Self::close_active(&entry, &mut active);
197		}
198	}
199
200	#[instrument(name = "store::multi::memory::collect_evictable_below", level = "debug", skip_all, fields(table = ?table, cutoff = cutoff.0))]
201	pub fn collect_evictable_below(
202		&self,
203		table: EntryKind,
204		cutoff: CommitVersion,
205		budget: ByteSize,
206	) -> (EvictablePersist, EvictableDrop, ByteSize, bool) {
207		self.seal_for_flush(table, cutoff);
208
209		let entry = match self.inner.entries.data.get(&table) {
210			Some(e) => e,
211			None => return (Vec::new(), Vec::new(), ByteSize::ZERO, false),
212		};
213		let closed = entry.closed_snapshot();
214
215		let budget = budget.as_bytes();
216		let mut consumed = 0u64;
217		let mut selected = 0usize;
218		let mut latest: HashMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)> = HashMap::new();
219		let mut to_drop: EvictableDrop = Vec::new();
220		let mut more = false;
221
222		'select: for map in closed.iter() {
223			if map.min_version() > cutoff {
224				continue;
225			}
226			for (key, versions) in map.rows().iter() {
227				if selected > 0 && consumed >= budget {
228					more = true;
229					break 'select;
230				}
231				let newest = versions.keys().next().map(|Reverse(v)| *v);
232				let mut touched = false;
233				for (Reverse(version), value) in versions.iter() {
234					if *version > cutoff {
235						continue;
236					}
237					touched = true;
238					consumed += entry_bytes(key, value);
239					to_drop.push(EvictedVersion {
240						key: key.clone(),
241						version: *version,
242						value_bytes: value_bytes_of(value),
243						current: newest == Some(*version),
244					});
245					match latest.get(key) {
246						Some((best, _)) if *best >= *version => {}
247						_ => {
248							latest.insert(key.clone(), (*version, value.clone()));
249						}
250					}
251				}
252				if touched {
253					selected += 1;
254				}
255			}
256		}
257
258		let to_persist = latest.into_iter().map(|(key, (v, val))| (key, v, val)).collect();
259		(to_persist, to_drop, ByteSize::from_bytes(consumed), more)
260	}
261}
262
263impl CommitStore {
264	#[instrument(name = "store::multi::memory::get", level = "trace", skip(self, key), fields(table = ?table, key_len = key.len(), version = version.0))]
265	pub fn get(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<VersionedGetResult> {
266		let entry = match self.inner.entries.data.get(&table) {
267			Some(e) => e,
268			None => return Ok(VersionedGetResult::NotFound),
269		};
270
271		let active = entry.active.read();
272		let closed = entry.closed.read();
273		let found = lookup(
274			iter::once(active.rows()).chain(closed.iter().rev().map(|map| map.rows())),
275			key,
276			version,
277		);
278
279		Ok(match found {
280			Some((found, Some(value))) => VersionedGetResult::Value {
281				value: value.clone(),
282				version: found,
283			},
284			Some((_, None)) => VersionedGetResult::Tombstone,
285			None => VersionedGetResult::NotFound,
286		})
287	}
288
289	#[instrument(name = "store::multi::memory::contains", level = "trace", skip(self, key), fields(table = ?table, key_len = key.len(), version = version.0), ret)]
290	pub fn contains(&self, table: EntryKind, key: &[u8], version: CommitVersion) -> Result<bool> {
291		let entry = match self.inner.entries.data.get(&table) {
292			Some(e) => e,
293			None => return Ok(false),
294		};
295
296		let active = entry.active.read();
297		let closed = entry.closed.read();
298		let found = lookup(
299			iter::once(active.rows()).chain(closed.iter().rev().map(|map| map.rows())),
300			key,
301			version,
302		);
303
304		Ok(found.is_some_and(|(_, value)| value.is_some()))
305	}
306
307	#[instrument(name = "store::multi::memory::set", level = "trace", skip(self, batches), fields(
308		table_count = batches.len(),
309		total_entry_count = field::Empty,
310		version = version.0
311	))]
312	pub fn set(&self, version: CommitVersion, batches: TierBatch) -> Result<()> {
313		let total_entries: usize = batches.values().map(|v| v.len()).sum();
314
315		batches.into_iter().for_each(|(table, entries)| {
316			self.process_table(table, version, entries);
317		});
318
319		Span::current().record("total_entry_count", total_entries);
320		Ok(())
321	}
322
323	#[instrument(name = "store::multi::memory::range_next", level = "trace", skip(self, cursor, start, end), fields(table = ?table, batch_size = batch_size, scope = ?scope))]
324	pub fn range_next(
325		&self,
326		table: EntryKind,
327		cursor: &mut RangeCursor,
328		start: Bound<&[u8]>,
329		end: Bound<&[u8]>,
330		scope: MultiVersionScope,
331		batch_size: usize,
332	) -> Result<RangeBatch> {
333		let mut entries = Vec::with_capacity(batch_size + 1);
334		let has_more = self.range_next_into(table, cursor, start, end, scope, batch_size, &mut entries)?;
335		Ok(RangeBatch {
336			entries,
337			has_more,
338		})
339	}
340
341	#[allow(clippy::too_many_arguments)]
342	pub fn range_next_into(
343		&self,
344		table: EntryKind,
345		cursor: &mut RangeCursor,
346		start: Bound<&[u8]>,
347		end: Bound<&[u8]>,
348		scope: MultiVersionScope,
349		batch_size: usize,
350		entries: &mut Vec<RawEntry>,
351	) -> Result<bool> {
352		entries.clear();
353
354		if cursor.is_exhausted() {
355			return Ok(false);
356		}
357
358		let entry = match self.inner.entries.data.get(&table) {
359			Some(e) => e,
360			None => {
361				cursor.finish();
362				return Ok(false);
363			}
364		};
365
366		let cursor_key = cursor.last_key().cloned();
367		let active = entry.active.read();
368		let closed = entry.closed.read();
369
370		let iter_start: Bound<&[u8]> = match &cursor_key {
371			Some(last) => Bound::Excluded(last.as_slice()),
372			None => start,
373		};
374
375		let mut merged = MergedRows::new(
376			iter::once(active.rows())
377				.chain(closed.iter().map(|map| map.rows()))
378				.map(|rows| rows.range((iter_start, end)))
379				.collect(),
380			false,
381		);
382
383		entries.reserve(batch_size + 1);
384		while entries.len() <= batch_size {
385			let Some((key, group)) = merged.next_group() else {
386				break;
387			};
388			if let Some((version, value)) = newest_across(group.iter().copied(), scope.read())
389				&& scope.contains(version)
390			{
391				entries.push(RawEntry {
392					key: key.clone(),
393					version,
394					value: value.clone(),
395				});
396			}
397		}
398
399		let has_more = entries.len() > batch_size;
400		if has_more {
401			entries.truncate(batch_size);
402		}
403
404		if let Some(last_entry) = entries.last() {
405			cursor.advance(last_entry.key.clone());
406		}
407		if !has_more {
408			cursor.finish();
409		}
410
411		Ok(has_more)
412	}
413
414	#[instrument(name = "store::multi::memory::range_rev_next", level = "trace", skip(self, cursor, start, end), fields(table = ?table, batch_size = batch_size, scope = ?scope))]
415	pub fn range_rev_next(
416		&self,
417		table: EntryKind,
418		cursor: &mut RangeCursor,
419		start: Bound<&[u8]>,
420		end: Bound<&[u8]>,
421		scope: MultiVersionScope,
422		batch_size: usize,
423	) -> Result<RangeBatch> {
424		if cursor.is_exhausted() {
425			return Ok(RangeBatch::empty());
426		}
427
428		let entry = match self.inner.entries.data.get(&table) {
429			Some(e) => e,
430			None => {
431				cursor.finish();
432				return Ok(RangeBatch::empty());
433			}
434		};
435
436		let cursor_key = cursor.last_key().cloned();
437		let active = entry.active.read();
438		let closed = entry.closed.read();
439
440		let iter_end: Bound<&[u8]> = match &cursor_key {
441			Some(last) => Bound::Excluded(last.as_slice()),
442			None => end,
443		};
444
445		let mut merged = MergedRows::new(
446			iter::once(active.rows())
447				.chain(closed.iter().map(|map| map.rows()))
448				.map(|rows| rows.range((start, iter_end)).rev())
449				.collect(),
450			true,
451		);
452
453		let mut entries: Vec<RawEntry> = Vec::with_capacity(batch_size + 1);
454		while entries.len() <= batch_size {
455			let Some((key, group)) = merged.next_group() else {
456				break;
457			};
458			if let Some((version, value)) = newest_across(group.iter().copied(), scope.read())
459				&& scope.contains(version)
460			{
461				entries.push(RawEntry {
462					key: key.clone(),
463					version,
464					value: value.clone(),
465				});
466			}
467		}
468
469		let has_more = entries.len() > batch_size;
470		if has_more {
471			entries.truncate(batch_size);
472		}
473
474		if let Some(last_entry) = entries.last() {
475			cursor.advance(last_entry.key.clone());
476		}
477		if !has_more {
478			cursor.finish();
479		}
480
481		Ok(RangeBatch {
482			entries,
483			has_more,
484		})
485	}
486
487	#[instrument(name = "store::multi::memory::ensure_table", level = "trace", skip(self), fields(table = ?table))]
488	pub fn ensure_table(&self, table: EntryKind) -> Result<()> {
489		let _ = self.get_or_create_table(table);
490		Ok(())
491	}
492
493	#[instrument(name = "store::multi::memory::clear_table", level = "debug", skip(self), fields(table = ?table))]
494	pub fn clear_table(&self, table: EntryKind) -> Result<()> {
495		if let Some(entry) = self.inner.entries.data.get(&table) {
496			let mut active = entry.active_write();
497			*active = ActiveRows::new();
498			*entry.closed.write() = VecDeque::new();
499			entry.key_count.store(0, Ordering::Relaxed);
500		}
501		Ok(())
502	}
503}
504
505impl CommitStore {
506	#[instrument(name = "store::multi::memory::drop", level = "debug", skip(self, batches), fields(
507		table_count = batches.len(),
508		total_entry_count = field::Empty
509	))]
510	pub fn compact(
511		&self,
512		batches: HashMap<EntryKind, Vec<(EncodedKey, CommitVersion)>>,
513	) -> Result<Vec<EvictedVersion>> {
514		let total_entries: usize = batches.values().map(|v| v.len()).sum();
515		let mut removed: Vec<EvictedVersion> = Vec::with_capacity(total_entries);
516
517		for (table, entries) in batches {
518			let table_entry = self.get_or_create_table(table);
519			let mut dropped: HashMap<EncodedKey, HashSet<CommitVersion>> = HashMap::new();
520			let mut below = CommitVersion(0);
521			for (key, version) in entries {
522				below = below.max(version);
523				dropped.entry(key).or_default().insert(version);
524			}
525
526			let mut active = table_entry.active_write();
527			let mut closed = table_entry.closed.write();
528			let keys_before = active.rows().key_count()
529				+ closed.iter().map(|slot| slot.rows().key_count()).sum::<usize>();
530
531			let newest: HashMap<EncodedKey, CommitVersion> = dropped
532				.keys()
533				.filter_map(|key| {
534					lookup(
535						iter::once(active.rows())
536							.chain(closed.iter().rev().map(|slot| slot.rows())),
537						key,
538						CommitVersion(u64::MAX),
539					)
540					.map(|(version, _)| (key.clone(), version))
541				})
542				.collect();
543
544			let mut record = |entry: &Removed| EvictedVersion {
545				key: entry.key.clone(),
546				version: entry.version,
547				value_bytes: value_bytes_of(&entry.value),
548				current: newest.get(&entry.key) == Some(&entry.version),
549			};
550
551			if active.min_version().is_some_and(|min| min <= below) {
552				removed.extend(active.compact(&dropped).iter().map(&mut record));
553			}
554
555			for slot in closed.iter_mut() {
556				if slot.min_version() > below {
557					continue;
558				}
559				removed.extend(Arc::make_mut(slot).compact(&dropped).iter().map(&mut record));
560			}
561			closed.retain(|map| !map.rows().is_empty());
562			let keys_after = active.rows().key_count()
563				+ closed.iter().map(|slot| slot.rows().key_count()).sum::<usize>();
564			table_entry.key_count.fetch_sub((keys_before - keys_after) as u64, Ordering::Relaxed);
565		}
566
567		Span::current().record("total_entry_count", total_entries);
568		Ok(removed)
569	}
570
571	#[instrument(name = "store::multi::memory::get_all_versions", level = "trace", skip(self, key), fields(table = ?table, key_len = key.len()))]
572	pub fn get_all_versions(
573		&self,
574		table: EntryKind,
575		key: &[u8],
576	) -> Result<Vec<(CommitVersion, Option<CowVec<u8>>)>> {
577		let entry = match self.inner.entries.data.get(&table) {
578			Some(e) => e,
579			None => return Ok(Vec::new()),
580		};
581
582		let active = entry.active.read();
583		let closed = entry.closed.read();
584
585		let mut versions: Vec<(CommitVersion, Option<CowVec<u8>>)> = iter::once(active.rows())
586			.chain(closed.iter().map(|map| map.rows()))
587			.filter_map(|rows| rows.versions_for(key))
588			.flat_map(|found| found.iter().map(|(Reverse(v), value)| (*v, value.clone())))
589			.collect();
590
591		versions.sort_by(|a, b| b.0.cmp(&a.0));
592		versions.dedup_by_key(|(version, _)| *version);
593
594		Ok(versions)
595	}
596
597	#[instrument(name = "store::multi::memory::sweep_historical_below", level = "trace", skip(self), fields(table = ?table, cutoff = cutoff.0, batch_size = batch_size))]
598	pub fn sweep_historical_below(
599		&self,
600		table: EntryKind,
601		cutoff: CommitVersion,
602		batch_size: usize,
603	) -> Result<HistoricalSweep> {
604		let Some(entry) = self.inner.entries.data.get(&table) else {
605			return Ok(HistoricalSweep::default());
606		};
607
608		let queued = mem::take(&mut *entry.pending.lock());
609		let queued = if queued.is_empty() {
610			mem::take(&mut *entry.retained.lock())
611		} else {
612			queued
613		};
614		let mut queued = queued.into_iter();
615		let keys: Vec<EncodedKey> = queued.by_ref().take(batch_size).collect();
616		let mut unexamined: BTreeSet<EncodedKey> = queued.collect();
617		let remaining = unexamined.len() as u64;
618
619		let mut entries: Vec<(EncodedKey, CommitVersion)> = Vec::new();
620		let mut retained: Vec<EncodedKey> = Vec::new();
621		let mut versions: Vec<CommitVersion> = Vec::new();
622		for key in keys {
623			versions.clear();
624			{
625				let active = entry.active.read();
626				if let Some(found) = active.rows().versions_for(key.as_slice()) {
627					versions.extend(found.keys().map(|Reverse(version)| *version));
628				}
629			}
630			{
631				let closed = entry.closed.read();
632				for map in closed.iter() {
633					if let Some(found) = map.rows().versions_for(key.as_slice()) {
634						versions.extend(found.keys().map(|Reverse(version)| *version));
635					}
636				}
637			}
638			versions.sort_unstable();
639			versions.dedup();
640			let Some((_, older)) = versions.split_last() else {
641				continue;
642			};
643			let mut above_cutoff = false;
644			for version in older {
645				if *version < cutoff {
646					entries.push((key.clone(), *version));
647				} else {
648					above_cutoff = true;
649				}
650			}
651			if above_cutoff {
652				retained.push(key);
653			}
654		}
655
656		entry.pending.lock().append(&mut unexamined);
657		entry.retained.lock().extend(retained);
658
659		Ok(HistoricalSweep {
660			entries,
661			remaining,
662		})
663	}
664}
665
666#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
667pub struct MultiCommitMetrics {
668	pub current_bytes: ByteSize,
669	pub historical_bytes: ByteSize,
670	pub table_count: Count,
671	pub current_entries: Count,
672}
673
674impl CommitStore {
675	pub fn metrics(&self) -> MultiCommitMetrics {
676		let kinds = self.list_all_entry_kinds().unwrap_or_default();
677		let current_entries: u64 =
678			kinds.iter().map(|kind| self.estimated_current_count(*kind).unwrap_or(0)).sum();
679		MultiCommitMetrics {
680			current_bytes: self.current_resident_bytes(),
681			historical_bytes: self.historical_resident_bytes(),
682			table_count: Count::new(kinds.len() as u64),
683			current_entries: Count::new(current_entries),
684		}
685	}
686}
687
688impl MetricsCollector for CommitStore {
689	fn collect(&self, out: &mut Vec<MetricsSample>) {
690		out.push(MetricsSample::heap("commit_buffer", "current_bytes", self.current_resident_bytes()));
691		out.push(MetricsSample::heap("commit_buffer", "historical_bytes", self.historical_resident_bytes()));
692		let kinds = self.list_all_entry_kinds().unwrap_or_default();
693		out.push(MetricsSample::count("commit_buffer", "table_count", kinds.len() as u64));
694		let current_entries: u64 =
695			kinds.iter().map(|kind| self.estimated_current_count(*kind).unwrap_or(0)).sum();
696		out.push(MetricsSample::count("commit_buffer", "current_entries", current_entries));
697	}
698}
699
700#[cfg(test)]
701pub mod tests {
702	use std::collections::BTreeMap;
703
704	use reifydb_core::interface::{
705		catalog::{id::TableId, storage::StorageId},
706		store::EntryLayout,
707	};
708
709	use super::*;
710
711	const UNBOUNDED: ByteSize = ByteSize::from_bytes(u64::MAX);
712
713	fn budget_of(entries: &[(EncodedKey, Option<CowVec<u8>>)]) -> ByteSize {
714		// The budget is expressed in the same accounting the buffer's residency counters use, so a test
715		// budget must be derived from entry_bytes rather than from the value length alone.
716		ByteSize::from_bytes(entries.iter().map(|(key, value)| entry_bytes(key, value)).sum())
717	}
718
719	fn keyed(name: &str, value: &[u8]) -> (EncodedKey, Option<CowVec<u8>>) {
720		(EncodedKey::new(name.as_bytes().to_vec()), Some(CowVec::new(value.to_vec())))
721	}
722
723	fn seed(storage: &CommitStore, version: u64, entries: &[(EncodedKey, Option<CowVec<u8>>)]) {
724		for (key, value) in entries {
725			storage.set(
726				CommitVersion(version),
727				HashMap::from([(EntryKind::Multi, vec![(key.clone(), value.clone())])]),
728			)
729			.unwrap();
730		}
731	}
732
733	#[test]
734	fn test_basic_operations() {
735		let storage = CommitStore::new();
736
737		let key = EncodedKey::new(b"key1");
738		let version = CommitVersion(1);
739
740		storage.set(
741			version,
742			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"value1".to_vec())))])]),
743		)
744		.unwrap();
745
746		let value = storage.get(EntryKind::Multi, &key, version).unwrap().value();
747		assert_eq!(value.as_deref(), Some(b"value1".as_slice()));
748
749		assert!(storage.contains(EntryKind::Multi, &key, version).unwrap());
750
751		assert!(!storage.contains(EntryKind::Multi, b"nonexistent", version).unwrap());
752
753		let version2 = CommitVersion(2);
754		storage.set(version2, HashMap::from([(EntryKind::Multi, vec![(key.clone(), None)])])).unwrap();
755		assert!(!storage.contains(EntryKind::Multi, &key, version2).unwrap());
756	}
757
758	#[test]
759	fn test_source_tables() {
760		let storage = CommitStore::new();
761
762		let source1 = StorageId::Table(TableId(1));
763		let source2 = StorageId::Table(TableId(2));
764
765		let key = EncodedKey::new(b"key");
766		let version = CommitVersion(1);
767
768		storage.set(
769			version,
770			HashMap::from([(
771				EntryKind::Source(source1, EntryLayout::Row),
772				vec![(key.clone(), Some(CowVec::new(b"table1".to_vec())))],
773			)]),
774		)
775		.unwrap();
776		storage.set(
777			version,
778			HashMap::from([(
779				EntryKind::Source(source2, EntryLayout::Row),
780				vec![(key.clone(), Some(CowVec::new(b"table2".to_vec())))],
781			)]),
782		)
783		.unwrap();
784
785		assert_eq!(
786			storage.get(EntryKind::Source(source1, EntryLayout::Row), &key, version)
787				.unwrap()
788				.value()
789				.as_deref(),
790			Some(b"table1".as_slice())
791		);
792		assert_eq!(
793			storage.get(EntryKind::Source(source2, EntryLayout::Row), &key, version)
794				.unwrap()
795				.value()
796				.as_deref(),
797			Some(b"table2".as_slice())
798		);
799	}
800
801	#[test]
802	fn test_version_promotion_to_historical() {
803		let storage = CommitStore::new();
804
805		let key = EncodedKey::new(b"key1");
806
807		storage.set(
808			CommitVersion(1),
809			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v1".to_vec())))])]),
810		)
811		.unwrap();
812
813		storage.set(
814			CommitVersion(2),
815			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v2".to_vec())))])]),
816		)
817		.unwrap();
818
819		storage.set(
820			CommitVersion(3),
821			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v3".to_vec())))])]),
822		)
823		.unwrap();
824
825		assert_eq!(
826			storage.get(EntryKind::Multi, &key, CommitVersion(3)).unwrap().value().as_deref(),
827			Some(b"v3".as_slice())
828		);
829
830		assert_eq!(
831			storage.get(EntryKind::Multi, &key, CommitVersion(2)).unwrap().value().as_deref(),
832			Some(b"v2".as_slice())
833		);
834
835		assert_eq!(
836			storage.get(EntryKind::Multi, &key, CommitVersion(1)).unwrap().value().as_deref(),
837			Some(b"v1".as_slice())
838		);
839	}
840
841	#[test]
842	fn test_insert_older_version() {
843		// An out-of-order older commit must stay resolvable: a read takes the largest version <= the
844		// snapshot, so the v2 snapshot resolves to v1.
845		let storage = CommitStore::new();
846
847		let key = EncodedKey::new(b"key1");
848
849		storage.set(
850			CommitVersion(3),
851			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v3".to_vec())))])]),
852		)
853		.unwrap();
854
855		storage.set(
856			CommitVersion(1),
857			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v1".to_vec())))])]),
858		)
859		.unwrap();
860
861		assert_eq!(
862			storage.get(EntryKind::Multi, &key, CommitVersion(3)).unwrap().value().as_deref(),
863			Some(b"v3".as_slice())
864		);
865
866		assert_eq!(
867			storage.get(EntryKind::Multi, &key, CommitVersion(1)).unwrap().value().as_deref(),
868			Some(b"v1".as_slice())
869		);
870
871		assert_eq!(
872			storage.get(EntryKind::Multi, &key, CommitVersion(2)).unwrap().value().as_deref(),
873			Some(b"v1".as_slice())
874		);
875	}
876
877	#[test]
878	fn test_range_next() {
879		let storage = CommitStore::new();
880
881		let version = CommitVersion(1);
882		storage.set(
883			version,
884			HashMap::from([(
885				EntryKind::Multi,
886				vec![
887					(EncodedKey::new(b"a"), Some(CowVec::new(b"1".to_vec()))),
888					(EncodedKey::new(b"b"), Some(CowVec::new(b"2".to_vec()))),
889					(EncodedKey::new(b"c"), Some(CowVec::new(b"3".to_vec()))),
890				],
891			)]),
892		)
893		.unwrap();
894
895		let mut cursor = RangeCursor::new();
896		let batch = storage
897			.range_next(
898				EntryKind::Multi,
899				&mut cursor,
900				Bound::Unbounded,
901				Bound::Unbounded,
902				MultiVersionScope::AsOf {
903					read: version,
904				},
905				100,
906			)
907			.unwrap();
908
909		assert_eq!(batch.entries.len(), 3);
910		assert!(!batch.has_more);
911		assert!(cursor.is_exhausted());
912
913		assert_eq!(&*batch.entries[0].key, b"a");
914		assert_eq!(&*batch.entries[1].key, b"b");
915		assert_eq!(&*batch.entries[2].key, b"c");
916	}
917
918	#[test]
919	fn test_range_rev_next() {
920		let storage = CommitStore::new();
921
922		let version = CommitVersion(1);
923		storage.set(
924			version,
925			HashMap::from([(
926				EntryKind::Multi,
927				vec![
928					(EncodedKey::new(b"a"), Some(CowVec::new(b"1".to_vec()))),
929					(EncodedKey::new(b"b"), Some(CowVec::new(b"2".to_vec()))),
930					(EncodedKey::new(b"c"), Some(CowVec::new(b"3".to_vec()))),
931				],
932			)]),
933		)
934		.unwrap();
935
936		let mut cursor = RangeCursor::new();
937		let batch = storage
938			.range_rev_next(
939				EntryKind::Multi,
940				&mut cursor,
941				Bound::Unbounded,
942				Bound::Unbounded,
943				MultiVersionScope::AsOf {
944					read: version,
945				},
946				100,
947			)
948			.unwrap();
949
950		assert_eq!(batch.entries.len(), 3);
951		assert!(!batch.has_more);
952		assert!(cursor.is_exhausted());
953
954		assert_eq!(&*batch.entries[0].key, b"c");
955		assert_eq!(&*batch.entries[1].key, b"b");
956		assert_eq!(&*batch.entries[2].key, b"a");
957	}
958
959	#[test]
960	fn test_range_streaming_pagination() {
961		let storage = CommitStore::new();
962
963		let version = CommitVersion(1);
964
965		let entries: Vec<_> =
966			(0..10u8).map(|i| (EncodedKey::new(vec![i]), Some(CowVec::new(vec![i * 10])))).collect();
967		storage.set(version, HashMap::from([(EntryKind::Multi, entries)])).unwrap();
968
969		let mut cursor = RangeCursor::new();
970
971		let batch1 = storage
972			.range_next(
973				EntryKind::Multi,
974				&mut cursor,
975				Bound::Unbounded,
976				Bound::Unbounded,
977				MultiVersionScope::AsOf {
978					read: version,
979				},
980				3,
981			)
982			.unwrap();
983		assert_eq!(batch1.entries.len(), 3);
984		assert!(batch1.has_more);
985		assert!(!cursor.is_exhausted());
986
987		assert_eq!(&*batch1.entries[0].key, &[0]);
988		assert_eq!(&*batch1.entries[2].key, &[2]);
989
990		let batch2 = storage
991			.range_next(
992				EntryKind::Multi,
993				&mut cursor,
994				Bound::Unbounded,
995				Bound::Unbounded,
996				MultiVersionScope::AsOf {
997					read: version,
998				},
999				3,
1000			)
1001			.unwrap();
1002		assert_eq!(batch2.entries.len(), 3);
1003		assert!(batch2.has_more);
1004		assert!(!cursor.is_exhausted());
1005
1006		assert_eq!(&*batch2.entries[0].key, &[3]);
1007		assert_eq!(&*batch2.entries[2].key, &[5]);
1008
1009		let batch3 = storage
1010			.range_next(
1011				EntryKind::Multi,
1012				&mut cursor,
1013				Bound::Unbounded,
1014				Bound::Unbounded,
1015				MultiVersionScope::AsOf {
1016					read: version,
1017				},
1018				3,
1019			)
1020			.unwrap();
1021		assert_eq!(batch3.entries.len(), 3);
1022		assert!(batch3.has_more);
1023		assert!(!cursor.is_exhausted());
1024
1025		assert_eq!(&*batch3.entries[0].key, &[6]);
1026		assert_eq!(&*batch3.entries[2].key, &[8]);
1027
1028		let batch4 = storage
1029			.range_next(
1030				EntryKind::Multi,
1031				&mut cursor,
1032				Bound::Unbounded,
1033				Bound::Unbounded,
1034				MultiVersionScope::AsOf {
1035					read: version,
1036				},
1037				3,
1038			)
1039			.unwrap();
1040		assert_eq!(batch4.entries.len(), 1);
1041		assert!(!batch4.has_more);
1042		assert!(cursor.is_exhausted());
1043
1044		assert_eq!(&*batch4.entries[0].key, &[9]);
1045
1046		let batch5 = storage
1047			.range_next(
1048				EntryKind::Multi,
1049				&mut cursor,
1050				Bound::Unbounded,
1051				Bound::Unbounded,
1052				MultiVersionScope::AsOf {
1053					read: version,
1054				},
1055				3,
1056			)
1057			.unwrap();
1058		assert!(batch5.entries.is_empty());
1059	}
1060
1061	#[test]
1062	fn test_range_reving_pagination() {
1063		let storage = CommitStore::new();
1064
1065		let version = CommitVersion(1);
1066
1067		let entries: Vec<_> =
1068			(0..10u8).map(|i| (EncodedKey::new(vec![i]), Some(CowVec::new(vec![i * 10])))).collect();
1069		storage.set(version, HashMap::from([(EntryKind::Multi, entries)])).unwrap();
1070
1071		let mut cursor = RangeCursor::new();
1072
1073		let batch1 = storage
1074			.range_rev_next(
1075				EntryKind::Multi,
1076				&mut cursor,
1077				Bound::Unbounded,
1078				Bound::Unbounded,
1079				MultiVersionScope::AsOf {
1080					read: version,
1081				},
1082				3,
1083			)
1084			.unwrap();
1085		assert_eq!(batch1.entries.len(), 3);
1086		assert!(batch1.has_more);
1087		assert!(!cursor.is_exhausted());
1088
1089		assert_eq!(&*batch1.entries[0].key, &[9]);
1090		assert_eq!(&*batch1.entries[2].key, &[7]);
1091
1092		let batch2 = storage
1093			.range_rev_next(
1094				EntryKind::Multi,
1095				&mut cursor,
1096				Bound::Unbounded,
1097				Bound::Unbounded,
1098				MultiVersionScope::AsOf {
1099					read: version,
1100				},
1101				3,
1102			)
1103			.unwrap();
1104		assert_eq!(batch2.entries.len(), 3);
1105		assert!(batch2.has_more);
1106		assert!(!cursor.is_exhausted());
1107
1108		assert_eq!(&*batch2.entries[0].key, &[6]);
1109		assert_eq!(&*batch2.entries[2].key, &[4]);
1110	}
1111
1112	fn sweep_all(storage: &CommitStore, cutoff: u64, batch: usize) -> Vec<(EncodedKey, CommitVersion)> {
1113		// Drives one full pass the way the actor does: keep sweeping while unexamined keys remain, compacting
1114		// each batch, so a bug in batching or requeueing shows up as missing or duplicated rows.
1115		let mut all = Vec::new();
1116		let mut calls = 0;
1117		loop {
1118			let sweep =
1119				storage.sweep_historical_below(EntryKind::Multi, CommitVersion(cutoff), batch).unwrap();
1120			calls += 1;
1121			assert!(calls <= 64, "a pass must terminate even when every key is retained");
1122			all.extend(sweep.entries.iter().cloned());
1123			if !sweep.entries.is_empty() {
1124				storage.compact(HashMap::from([(EntryKind::Multi, sweep.entries)])).unwrap();
1125			}
1126			if sweep.remaining == 0 {
1127				return all;
1128			}
1129		}
1130	}
1131
1132	#[test]
1133	fn sweep_drops_versions_below_cutoff_across_closed_maps_and_forgets_finished_keys() {
1134		// Every set closes the active map, so each (key, version) sits in its own closed map and a key's
1135		// versions must be gathered across maps; a per-map view would see nothing to reclaim. The newest
1136		// version survives even when it is below the cutoff, and a key with nothing left to reclaim must
1137		// not be examined again on the next pass.
1138		let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
1139		for (name, version) in [
1140			("a", 1),
1141			("a", 2),
1142			("a", 3),
1143			("b", 1),
1144			("b", 2),
1145			("c", 1),
1146			("d", 1),
1147			("d", 2),
1148			("d", 4),
1149			("e", 3),
1150			("e", 2),
1151			("e", 1),
1152		] {
1153			seed(&storage, version, &[keyed(name, b"v")]);
1154		}
1155		let row =
1156			|name: &str, version: u64| (EncodedKey::new(name.as_bytes().to_vec()), CommitVersion(version));
1157		let expected =
1158			vec![row("a", 1), row("a", 2), row("b", 1), row("d", 1), row("d", 2), row("e", 1), row("e", 2)];
1159
1160		assert_eq!(sweep_all(&storage, 4, 100), expected);
1161		for (name, survivors) in
1162			[("a", vec![3]), ("b", vec![2]), ("c", vec![1]), ("d", vec![4]), ("e", vec![3])]
1163		{
1164			let left: Vec<u64> = storage
1165				.get_all_versions(EntryKind::Multi, name.as_bytes())
1166				.unwrap()
1167				.into_iter()
1168				.map(|(v, _)| v.0)
1169				.collect();
1170			assert_eq!(left, survivors, "key {name}");
1171		}
1172
1173		let again = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(100), 100).unwrap();
1174		assert!(again.entries.is_empty());
1175		assert_eq!(again.remaining, 0);
1176	}
1177
1178	#[test]
1179	fn sweep_requeues_keys_whose_older_versions_are_still_above_cutoff() {
1180		// A version that is historical but not yet below the cutoff must be reclaimed by a later pass without
1181		// a new write to the key, so the key has to stay queued; but it must not count as backlog, or the
1182		// actor would spin re-examining it before the cutoff moves.
1183		let storage = CommitStore::new();
1184		let key = EncodedKey::new(b"k".to_vec());
1185		for version in [1, 2, 3] {
1186			seed(&storage, version, &[keyed("k", b"v")]);
1187		}
1188
1189		let first = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(2), 100).unwrap();
1190		assert_eq!(first.entries, vec![(key.clone(), CommitVersion(1))]);
1191		assert_eq!(first.remaining, 0, "a retained key is not backlog");
1192		storage.compact(HashMap::from([(EntryKind::Multi, first.entries)])).unwrap();
1193
1194		let second = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(3), 100).unwrap();
1195		assert_eq!(
1196			second.entries,
1197			vec![(key.clone(), CommitVersion(2))],
1198			"requeued key reclaimed once the cutoff passed"
1199		);
1200		storage.compact(HashMap::from([(EntryKind::Multi, second.entries)])).unwrap();
1201
1202		let third = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(100), 100).unwrap();
1203		assert!(third.entries.is_empty(), "nothing historical is left for the key");
1204	}
1205
1206	#[test]
1207	fn a_retained_key_waits_for_the_next_pass_instead_of_displacing_unexamined_ones() {
1208		// Keys kept back because a version is still above the cutoff must not be handed to the next batch
1209		// ahead of keys nobody has looked at yet: they sort first, so re-queueing them straight away makes
1210		// every batch re-examine the same keys, drop nothing, and report the same backlog forever.
1211		let storage = CommitStore::new();
1212		let names = ["a", "b", "c", "d", "e"];
1213		for name in names {
1214			seed(&storage, 1, &[keyed(name, b"v1")]);
1215			seed(&storage, 2, &[keyed(name, b"v2")]);
1216			seed(&storage, 3, &[keyed(name, b"v3")]);
1217		}
1218
1219		let dropped = |sweep: &HistoricalSweep| -> Vec<(String, u64)> {
1220			sweep.entries
1221				.iter()
1222				.map(|(key, version)| (String::from_utf8(key.as_slice().to_vec()).unwrap(), version.0))
1223				.collect()
1224		};
1225
1226		let first = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(2), 2).unwrap();
1227		assert_eq!(dropped(&first), vec![("a".into(), 1), ("b".into(), 1)]);
1228		assert_eq!(first.remaining, 3);
1229		storage.compact(HashMap::from([(EntryKind::Multi, first.entries)])).unwrap();
1230
1231		let second = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(2), 2).unwrap();
1232		assert_eq!(
1233			dropped(&second),
1234			vec![("c".into(), 1), ("d".into(), 1)],
1235			"a and b were retained, not re-batched"
1236		);
1237		assert_eq!(second.remaining, 1);
1238		storage.compact(HashMap::from([(EntryKind::Multi, second.entries)])).unwrap();
1239
1240		let third = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(2), 2).unwrap();
1241		assert_eq!(dropped(&third), vec![("e".into(), 1)]);
1242		assert_eq!(third.remaining, 0, "the pass ends once every fresh key was examined");
1243		storage.compact(HashMap::from([(EntryKind::Multi, third.entries)])).unwrap();
1244
1245		let next_pass = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(3), 2).unwrap();
1246		assert_eq!(
1247			dropped(&next_pass),
1248			vec![("a".into(), 2), ("b".into(), 2)],
1249			"retained keys come back once the queue is empty"
1250		);
1251		assert_eq!(next_pass.remaining, 3, "the rest of the retained keys are the new backlog");
1252	}
1253
1254	#[test]
1255	fn sweep_examines_at_most_batch_keys_and_reports_the_rest_as_backlog() {
1256		// The batch bounds how many keys one call looks up, and the unexamined keys must be reported so the
1257		// actor keeps going instead of waiting a full interval; across the pass every key is examined
1258		// exactly once and no row is dropped twice.
1259		let storage = CommitStore::new();
1260		let names = ["k1", "k2", "k3", "k4", "k5"];
1261		for name in names {
1262			seed(&storage, 1, &[keyed(name, b"old")]);
1263			seed(&storage, 2, &[keyed(name, b"new")]);
1264		}
1265		seed(&storage, 2, &[keyed("z-single", b"single")]);
1266
1267		let first = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(10), 2).unwrap();
1268		assert_eq!(first.entries.len(), 2);
1269		assert_eq!(first.remaining, 4, "six keys queued, two examined");
1270		storage.compact(HashMap::from([(EntryKind::Multi, first.entries.clone())])).unwrap();
1271
1272		let mut all = first.entries;
1273		all.extend(sweep_all(&storage, 10, 2));
1274		let mut expected: Vec<(EncodedKey, CommitVersion)> = names
1275			.iter()
1276			.map(|name| (EncodedKey::new(name.as_bytes().to_vec()), CommitVersion(1)))
1277			.collect();
1278		expected.sort();
1279		all.sort();
1280		assert_eq!(all, expected);
1281
1282		let after = storage.sweep_historical_below(EntryKind::Multi, CommitVersion(10), 100).unwrap();
1283		assert!(after.entries.is_empty());
1284		assert_eq!(after.remaining, 0, "a single-version key does not linger in the queue");
1285	}
1286
1287	#[test]
1288	fn test_drop_from_historical() {
1289		let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
1290
1291		let key = EncodedKey::new(b"key1");
1292
1293		for v in 1..=3u64 {
1294			storage.set(
1295				CommitVersion(v),
1296				HashMap::from([(
1297					EntryKind::Multi,
1298					vec![(key.clone(), Some(CowVec::new(format!("v{}", v).into_bytes())))],
1299				)]),
1300			)
1301			.unwrap();
1302		}
1303
1304		storage.compact(HashMap::from([(EntryKind::Multi, vec![(key.clone(), CommitVersion(1))])])).unwrap();
1305
1306		assert!(storage.get(EntryKind::Multi, &key, CommitVersion(1)).unwrap().value().is_none());
1307
1308		assert_eq!(
1309			storage.get(EntryKind::Multi, &key, CommitVersion(2)).unwrap().value().as_deref(),
1310			Some(b"v2".as_slice())
1311		);
1312		assert_eq!(
1313			storage.get(EntryKind::Multi, &key, CommitVersion(3)).unwrap().value().as_deref(),
1314			Some(b"v3".as_slice())
1315		);
1316	}
1317	#[test]
1318	fn compact_returns_each_removed_historical_version_flagged_as_not_current() {
1319		// The storage metric only ever decrements from what compact reports back, so a version removed
1320		// physically but omitted from the return value inflates historical_count forever.
1321		let storage = CommitStore::new();
1322
1323		let key = EncodedKey::new(b"key1");
1324
1325		for v in 1..=3u64 {
1326			storage.set(
1327				CommitVersion(v),
1328				HashMap::from([(
1329					EntryKind::Multi,
1330					vec![(key.clone(), Some(CowVec::new(format!("v{}", v).into_bytes())))],
1331				)]),
1332			)
1333			.unwrap();
1334		}
1335		storage.seal_for_flush(EntryKind::Multi, CommitVersion(u64::MAX));
1336
1337		let removed = storage
1338			.compact(HashMap::from([(
1339				EntryKind::Multi,
1340				vec![(key.clone(), CommitVersion(1)), (key.clone(), CommitVersion(2))],
1341			)]))
1342			.unwrap();
1343
1344		let mut versions: Vec<u64> = removed.iter().map(|entry| entry.version.0).collect();
1345		versions.sort_unstable();
1346		assert_eq!(versions, vec![1, 2]);
1347		assert!(removed.iter().all(|entry| !entry.current));
1348		assert!(removed.iter().all(|entry| entry.value_bytes == ByteSize::from_bytes(2)));
1349	}
1350
1351	#[test]
1352	fn compact_reports_the_live_version_and_leaves_surviving_history_in_place() {
1353		// Dropping the live version does not promote the newest survivor, so the removal is only visible
1354		// to the metric through the returned record; the survivors must stay in historical, unreported
1355		// and still readable, which is what makes skipping the promotion safe.
1356		let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
1357
1358		let key = EncodedKey::new(b"key1");
1359
1360		for v in 1..=3u64 {
1361			storage.set(
1362				CommitVersion(v),
1363				HashMap::from([(
1364					EntryKind::Multi,
1365					vec![(key.clone(), Some(CowVec::new(format!("v{}", v).into_bytes())))],
1366				)]),
1367			)
1368			.unwrap();
1369		}
1370
1371		let removed = storage
1372			.compact(HashMap::from([(EntryKind::Multi, vec![(key.clone(), CommitVersion(3))])]))
1373			.unwrap();
1374
1375		assert_eq!(removed.len(), 1, "only the live version was dropped");
1376		assert_eq!(removed[0].version, CommitVersion(3));
1377		assert!(removed[0].current, "the dropped version was the live one");
1378		assert_eq!(removed[0].value_bytes, ByteSize::from_bytes(2));
1379
1380		assert_eq!(
1381			storage.get(EntryKind::Multi, &key, CommitVersion(3)).unwrap().value().as_deref(),
1382			Some(b"v2".as_slice()),
1383			"the newest survivor is still readable from historical without being promoted"
1384		);
1385	}
1386
1387	#[test]
1388	fn compact_reports_the_live_entry_when_every_version_of_a_key_is_removed() {
1389		// The metric routes a current removal to the current counters and a historical one to the
1390		// historical counters, so mislabelling the live entry moves rows between the two columns
1391		// instead of clearing them.
1392		let storage = CommitStore::new();
1393
1394		let key = EncodedKey::new(b"key1");
1395
1396		for v in 1..=3u64 {
1397			storage.set(
1398				CommitVersion(v),
1399				HashMap::from([(
1400					EntryKind::Multi,
1401					vec![(key.clone(), Some(CowVec::new(format!("v{}", v).into_bytes())))],
1402				)]),
1403			)
1404			.unwrap();
1405		}
1406		storage.seal_for_flush(EntryKind::Multi, CommitVersion(u64::MAX));
1407
1408		let removed = storage
1409			.compact(HashMap::from([(
1410				EntryKind::Multi,
1411				vec![
1412					(key.clone(), CommitVersion(1)),
1413					(key.clone(), CommitVersion(2)),
1414					(key.clone(), CommitVersion(3)),
1415				],
1416			)]))
1417			.unwrap();
1418
1419		assert_eq!(removed.len(), 3);
1420
1421		let live: Vec<u64> =
1422			removed.iter().filter(|entry| entry.current).map(|entry| entry.version.0).collect();
1423		assert_eq!(live, vec![3]);
1424
1425		let mut historical: Vec<u64> =
1426			removed.iter().filter(|entry| !entry.current).map(|entry| entry.version.0).collect();
1427		historical.sort_unstable();
1428		assert_eq!(historical, vec![1, 2]);
1429	}
1430
1431	#[test]
1432	fn test_tombstones() {
1433		let storage = CommitStore::new();
1434
1435		let key = EncodedKey::new(b"key1");
1436
1437		storage.set(
1438			CommitVersion(1),
1439			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"value".to_vec())))])]),
1440		)
1441		.unwrap();
1442
1443		storage.set(CommitVersion(2), HashMap::from([(EntryKind::Multi, vec![(key.clone(), None)])])).unwrap();
1444
1445		assert!(storage.get(EntryKind::Multi, &key, CommitVersion(2)).unwrap().value().is_none());
1446		assert!(!storage.contains(EntryKind::Multi, &key, CommitVersion(2)).unwrap());
1447
1448		assert_eq!(
1449			storage.get(EntryKind::Multi, &key, CommitVersion(1)).unwrap().value().as_deref(),
1450			Some(b"value".as_slice())
1451		);
1452	}
1453
1454	#[test]
1455	fn test_collect_evictable_below_keeps_versions_above_cutoff() {
1456		let storage = CommitStore::new();
1457		let key = EncodedKey::new(b"k");
1458		for v in 1..=3u64 {
1459			storage.set(
1460				CommitVersion(v),
1461				HashMap::from([(
1462					EntryKind::Multi,
1463					vec![(key.clone(), Some(CowVec::new(format!("v{v}").into_bytes())))],
1464				)]),
1465			)
1466			.unwrap();
1467		}
1468
1469		// v2 is what a reader in [2, 3) resolves to, so it is the value that must be persisted.
1470		let (to_persist, to_drop, _consumed, _more) =
1471			storage.collect_evictable_below(EntryKind::Multi, CommitVersion(2), UNBOUNDED);
1472		assert_eq!(to_persist.len(), 1);
1473		assert_eq!(to_persist[0].0, key);
1474		assert_eq!(to_persist[0].1, CommitVersion(2));
1475		assert_eq!(to_persist[0].2.as_deref(), Some(b"v2".as_slice()));
1476		let dropped: HashSet<CommitVersion> = to_drop.iter().map(|e| e.version).collect();
1477		assert_eq!(dropped, HashSet::from([CommitVersion(1), CommitVersion(2)]));
1478
1479		storage.compact(HashMap::from([(
1480			EntryKind::Multi,
1481			to_drop.into_iter().map(|e| (e.key, e.version)).collect(),
1482		)]))
1483		.unwrap();
1484		assert_eq!(
1485			storage.get(EntryKind::Multi, &key, CommitVersion(3)).unwrap().value().as_deref(),
1486			Some(b"v3".as_slice())
1487		);
1488		assert!(storage.get(EntryKind::Multi, &key, CommitVersion(2)).unwrap().value().is_none());
1489		assert!(storage.get(EntryKind::Multi, &key, CommitVersion(1)).unwrap().value().is_none());
1490	}
1491
1492	#[test]
1493	fn test_collect_evictable_below_empty_when_all_above_cutoff() {
1494		let storage = CommitStore::new();
1495		let key = EncodedKey::new(b"k");
1496		storage.set(
1497			CommitVersion(5),
1498			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v".to_vec())))])]),
1499		)
1500		.unwrap();
1501		let (to_persist, to_drop, _consumed, _more) =
1502			storage.collect_evictable_below(EntryKind::Multi, CommitVersion(3), UNBOUNDED);
1503		assert!(to_persist.is_empty());
1504		assert!(to_drop.is_empty());
1505	}
1506
1507	#[test]
1508	fn test_collect_evictable_below_persists_exactly_one_value_per_key() {
1509		// Only the latest-<=cutoff value may be persisted: it is the single value a reader at the cutoff
1510		// snapshot resolves to, so persisting an older one corrupts that resolution.
1511		let storage = CommitStore::new();
1512		let key = EncodedKey::new(b"k");
1513		for v in 1..=5u64 {
1514			storage.set(
1515				CommitVersion(v),
1516				HashMap::from([(
1517					EntryKind::Multi,
1518					vec![(key.clone(), Some(CowVec::new(format!("v{v}").into_bytes())))],
1519				)]),
1520			)
1521			.unwrap();
1522		}
1523
1524		let (to_persist, to_drop, _consumed, _more) =
1525			storage.collect_evictable_below(EntryKind::Multi, CommitVersion(4), UNBOUNDED);
1526		assert_eq!(to_persist.len(), 1, "exactly one value persisted per key");
1527		assert_eq!(to_persist[0].1, CommitVersion(4), "the latest version <= cutoff");
1528		assert_eq!(to_persist[0].2.as_deref(), Some(b"v4".as_slice()));
1529
1530		let dropped: HashSet<CommitVersion> = to_drop.iter().map(|e| e.version).collect();
1531		assert_eq!(
1532			dropped,
1533			HashSet::from([CommitVersion(1), CommitVersion(2), CommitVersion(3), CommitVersion(4)])
1534		);
1535	}
1536
1537	#[test]
1538	fn test_collect_evictable_below_persists_tombstone_when_it_is_the_latest() {
1539		// A tombstone that is the latest-<=cutoff version must be carried to the persistent tier; dropping
1540		// it lets a later read resurrect the pre-delete value.
1541		let storage = CommitStore::new();
1542		let key = EncodedKey::new(b"k");
1543		storage.set(
1544			CommitVersion(1),
1545			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v1".to_vec())))])]),
1546		)
1547		.unwrap();
1548		storage.set(CommitVersion(2), HashMap::from([(EntryKind::Multi, vec![(key.clone(), None)])])).unwrap();
1549
1550		let (to_persist, to_drop, _consumed, _more) =
1551			storage.collect_evictable_below(EntryKind::Multi, CommitVersion(2), UNBOUNDED);
1552		assert_eq!(to_persist.len(), 1);
1553		assert_eq!(to_persist[0].1, CommitVersion(2), "the tombstone is the latest version");
1554		assert!(to_persist[0].2.is_none(), "the persisted latest value must be the tombstone, not v1");
1555		assert_eq!(to_drop.len(), 2, "both v1 and the tombstone are dropped from the buffer");
1556	}
1557
1558	#[test]
1559	fn test_collect_evictable_below_only_drops_historical_when_current_is_above_cutoff() {
1560		// A key that is actively written while old snapshots age out: only the historical version may be
1561		// evicted, the current one is still hot and must not be persisted.
1562		let storage = CommitStore::new();
1563		let key = EncodedKey::new(b"k");
1564		storage.set(
1565			CommitVersion(2),
1566			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v2".to_vec())))])]),
1567		)
1568		.unwrap();
1569		storage.set(
1570			CommitVersion(5),
1571			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v5".to_vec())))])]),
1572		)
1573		.unwrap();
1574
1575		let (to_persist, to_drop, _consumed, _more) =
1576			storage.collect_evictable_below(EntryKind::Multi, CommitVersion(3), UNBOUNDED);
1577		assert_eq!(to_persist.len(), 1);
1578		assert_eq!(to_persist[0].1, CommitVersion(2), "only the aged-out historical version is persisted");
1579		assert_eq!(to_persist[0].2.as_deref(), Some(b"v2".as_slice()));
1580		let dropped: HashSet<CommitVersion> = to_drop.iter().map(|e| e.version).collect();
1581		assert_eq!(dropped, HashSet::from([CommitVersion(2)]), "v5 (current, > cutoff) is never dropped");
1582
1583		storage.compact(HashMap::from([(
1584			EntryKind::Multi,
1585			to_drop.into_iter().map(|e| (e.key, e.version)).collect(),
1586		)]))
1587		.unwrap();
1588		assert_eq!(
1589			storage.get(EntryKind::Multi, &key, CommitVersion(5)).unwrap().value().as_deref(),
1590			Some(b"v5".as_slice())
1591		);
1592		assert!(
1593			storage.get(EntryKind::Multi, &key, CommitVersion(3)).unwrap().value().is_none(),
1594			"the v2 a reader at snapshot 3 used to see is gone from the buffer after eviction"
1595		);
1596	}
1597
1598	#[test]
1599	fn test_collect_evictable_below_handles_multiple_keys_independently() {
1600		// The cutoff applies per version, not per key: a key whose only version is above it must stay
1601		// fully resident even while a sibling key is evicted.
1602		let storage = CommitStore::new();
1603		let cold = EncodedKey::new(b"cold");
1604		let hot = EncodedKey::new(b"hot");
1605		storage.set(
1606			CommitVersion(1),
1607			HashMap::from([(EntryKind::Multi, vec![(cold.clone(), Some(CowVec::new(b"cold1".to_vec())))])]),
1608		)
1609		.unwrap();
1610		storage.set(
1611			CommitVersion(9),
1612			HashMap::from([(EntryKind::Multi, vec![(hot.clone(), Some(CowVec::new(b"hot9".to_vec())))])]),
1613		)
1614		.unwrap();
1615
1616		let (to_persist, to_drop, _consumed, _more) =
1617			storage.collect_evictable_below(EntryKind::Multi, CommitVersion(5), UNBOUNDED);
1618		assert_eq!(to_persist.len(), 1, "only the cold key is evictable below the cutoff");
1619		assert_eq!(to_persist[0].0, cold);
1620		assert!(to_drop.iter().all(|e| e.key == cold), "the hot key must not be scheduled for drop");
1621	}
1622
1623	#[test]
1624	fn test_collect_evictable_below_bounds_to_budget_and_drains_across_calls() {
1625		// The budget bounds a flush slice so one transaction never persists the whole evictable set;
1626		// looping bounded calls must still drain exactly the below-cutoff set, no more, no less.
1627		let storage = CommitStore::new();
1628		let entries: Vec<(EncodedKey, Option<CowVec<u8>>)> =
1629			(0..5u8).map(|i| keyed(&format!("k{i}"), &[i])).collect();
1630		seed(&storage, 1, &entries);
1631		let budget = budget_of(&entries[..2]);
1632
1633		let (to_persist, to_drop, consumed, more) =
1634			storage.collect_evictable_below(EntryKind::Multi, CommitVersion(1), budget);
1635		assert_eq!(to_persist.len(), 2, "the budget only covers two entries' worth of bytes");
1636		assert_eq!(to_drop.len(), 2);
1637		assert_eq!(consumed, budget, "the reported spend must be the bytes actually selected");
1638		assert!(more, "three keys remain below the cutoff");
1639
1640		let mut drained = to_persist.len();
1641		let mut compaction_batch: HashMap<EntryKind, Vec<(EncodedKey, CommitVersion)>> = HashMap::new();
1642		compaction_batch.insert(EntryKind::Multi, to_drop.into_iter().map(|e| (e.key, e.version)).collect());
1643		storage.compact(compaction_batch).unwrap();
1644		loop {
1645			let (p, d, _consumed, more) =
1646				storage.collect_evictable_below(EntryKind::Multi, CommitVersion(1), budget);
1647			if p.is_empty() {
1648				assert!(!more, "an empty collect must not claim more remains");
1649				break;
1650			}
1651			drained += p.len();
1652			let mut batch: HashMap<EntryKind, Vec<(EncodedKey, CommitVersion)>> = HashMap::new();
1653			batch.insert(EntryKind::Multi, d.into_iter().map(|e| (e.key, e.version)).collect());
1654			storage.compact(batch).unwrap();
1655			if !more {
1656				break;
1657			}
1658		}
1659		assert_eq!(drained, 5, "every below-cutoff key is drained exactly once");
1660	}
1661
1662	#[test]
1663	fn collect_evictable_below_bounds_the_slice_by_bytes_not_by_key_count() {
1664		// A key-count budget lets one slice pull an unbounded number of bytes: N fat rows cost the same
1665		// as N thin ones. The same byte budget must therefore admit strictly fewer fat keys than thin
1666		// ones, which is exactly what a count-based cap cannot do.
1667		let thin: Vec<(EncodedKey, Option<CowVec<u8>>)> =
1668			(0..8u8).map(|i| keyed(&format!("k{i}"), &[i])).collect();
1669		let fat: Vec<(EncodedKey, Option<CowVec<u8>>)> =
1670			(0..8u8).map(|i| keyed(&format!("k{i}"), &vec![i; 4096])).collect();
1671
1672		let budget = budget_of(&thin[..4]);
1673
1674		let thin_storage = CommitStore::new();
1675		seed(&thin_storage, 1, &thin);
1676		let (thin_persist, _, _, thin_more) =
1677			thin_storage.collect_evictable_below(EntryKind::Multi, CommitVersion(1), budget);
1678
1679		let fat_storage = CommitStore::new();
1680		seed(&fat_storage, 1, &fat);
1681		let (fat_persist, _, fat_consumed, fat_more) =
1682			fat_storage.collect_evictable_below(EntryKind::Multi, CommitVersion(1), budget);
1683
1684		assert_eq!(thin_persist.len(), 4, "four thin entries is exactly what the budget buys");
1685		assert!(thin_more, "four of the eight thin keys are still pending");
1686		assert_eq!(fat_persist.len(), 1, "a single fat entry already exceeds the same byte budget");
1687		assert!(fat_more, "the remaining fat keys are still pending");
1688		assert!(
1689			fat_consumed.as_bytes() > budget.as_bytes(),
1690			"the one admitted fat entry is what pushed the slice over the budget"
1691		);
1692	}
1693
1694	#[test]
1695	fn collect_evictable_below_always_admits_one_entry_even_when_the_budget_cannot_cover_it() {
1696		// A slice must never collect nothing while work is pending: an entry no budget can cover would
1697		// otherwise be skipped every slice forever, and the key it pins holds the durable frontier at its
1698		// commit version, which clamps the tombstone reap cutoff to zero.
1699		let storage = CommitStore::new();
1700		let entries = vec![keyed("huge-a", &vec![7u8; 65536]), keyed("huge-b", &vec![9u8; 65536])];
1701		seed(&storage, 1, &entries);
1702
1703		let (to_persist, to_drop, consumed, more) =
1704			storage.collect_evictable_below(EntryKind::Multi, CommitVersion(1), ByteSize::from_bytes(1));
1705		assert_eq!(to_persist.len(), 1, "the oversized entry must be admitted, not skipped");
1706		assert_eq!(to_drop.len(), 1);
1707		assert!(consumed.as_bytes() > 65536, "the whole oversized entry counts against the slice");
1708		assert!(more, "the second oversized entry is still pending");
1709
1710		let (to_persist, _, consumed, more) =
1711			storage.collect_evictable_below(EntryKind::Multi, CommitVersion(1), ByteSize::ZERO);
1712		assert_eq!(to_persist.len(), 1, "even a zero budget must still release exactly one entry");
1713		assert!(consumed.as_bytes() > 65536);
1714		assert!(more, "a zero budget yields after the one entry it was forced to take");
1715	}
1716
1717	#[test]
1718	fn collect_evictable_below_counts_every_evicted_version_of_a_key_against_the_budget() {
1719		// A key's superseded versions leave the buffer in the same slice as its live one, so a budget
1720		// that only charged for the live version would let a deep version chain blow the slice's byte
1721		// ceiling without ever reporting it.
1722		let storage = CommitStore::new();
1723		let key = EncodedKey::new(b"k".to_vec());
1724		for v in 1..=4u64 {
1725			storage.set(
1726				CommitVersion(v),
1727				HashMap::from([(
1728					EntryKind::Multi,
1729					vec![(key.clone(), Some(CowVec::new(vec![v as u8; 512])))],
1730				)]),
1731			)
1732			.unwrap();
1733		}
1734
1735		let (_, to_drop, consumed, _) =
1736			storage.collect_evictable_below(EntryKind::Multi, CommitVersion(4), UNBOUNDED);
1737
1738		assert_eq!(to_drop.len(), 4, "all four versions leave the buffer");
1739		let expected: u64 = (1..=4u64).map(|v| entry_bytes(&key, &Some(CowVec::new(vec![v as u8; 512])))).sum();
1740		assert_eq!(
1741			consumed,
1742			ByteSize::from_bytes(expected),
1743			"the slice must charge itself for every version it evicted, not just the live one"
1744		);
1745	}
1746
1747	fn walk_versions(storage: &CommitStore, table: EntryKind) -> BTreeMap<EncodedKey, Vec<CommitVersion>> {
1748		// Every version the buffer physically holds, gathered across the active map and every closed one,
1749		// so an assertion can be made against what is stored rather than what a counter claims.
1750		let entry = storage.inner.entries.data.get(&table).expect("table exists");
1751		let active = entry.active.read();
1752		let closed = entry.closed_snapshot();
1753		let mut walked: BTreeMap<EncodedKey, Vec<CommitVersion>> = BTreeMap::new();
1754		for rows in iter::once(active.rows()).chain(closed.iter().map(|map| map.rows())) {
1755			for (key, versions) in rows.iter() {
1756				walked.entry(key.clone())
1757					.or_default()
1758					.extend(versions.keys().map(|Reverse(version)| *version));
1759			}
1760		}
1761		walked
1762	}
1763
1764	fn oldest_of(storage: &CommitStore, table: EntryKind, key: &EncodedKey) -> Option<CommitVersion> {
1765		walk_versions(storage, table).get(key).and_then(|versions| versions.iter().min().copied())
1766	}
1767
1768	fn assert_oldest_pending_tracks_the_maps(storage: &CommitStore, table: EntryKind) {
1769		// A reported floor above the smallest stored version lets the reaper cut a version the buffer has
1770		// not flushed; one below it freezes the floor on a version nobody holds and the sweep never ends.
1771		let walked = walk_versions(storage, table);
1772		let smallest = walked.values().flat_map(|versions| versions.iter().copied()).min();
1773		assert_eq!(
1774			storage.oldest_pending_for(table),
1775			smallest,
1776			"the reported oldest pending version must equal the smallest version the maps still hold"
1777		);
1778
1779		for (key, versions) in walked.iter() {
1780			let reported = storage
1781				.get_all_versions(table, key.as_ref())
1782				.unwrap()
1783				.last()
1784				.map(|(version, _)| *version);
1785			assert_eq!(
1786				reported,
1787				versions.iter().min().copied(),
1788				"a resident key must report its smallest stored version, not a newer one"
1789			);
1790		}
1791	}
1792
1793	#[test]
1794	fn index_stays_consistent_across_new_monotonic_out_of_order_and_drops() {
1795		// The eviction index is what keeps collect_evictable_below O(evictable) instead of O(table);
1796		// drift from the maps either strands a key forever or churns a ghost, so every maintenance path
1797		// is cross-checked against a full walk of both maps.
1798		let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
1799		let kind = EntryKind::Multi;
1800		let a = EncodedKey::new(b"a");
1801		let b = EncodedKey::new(b"b");
1802		let c = EncodedKey::new(b"c");
1803
1804		let set = |v: u64, key: &EncodedKey, val: &str| {
1805			storage.set(
1806				CommitVersion(v),
1807				HashMap::from([(
1808					kind,
1809					vec![(key.clone(), Some(CowVec::new(val.as_bytes().to_vec())))],
1810				)]),
1811			)
1812			.unwrap();
1813		};
1814
1815		set(10, &a, "a10");
1816		set(20, &a, "a20");
1817		set(3, &a, "a3");
1818		set(5, &b, "b5");
1819		set(7, &c, "c7");
1820		assert_eq!(
1821			oldest_of(&storage, kind, &a),
1822			Some(CommitVersion(3)),
1823			"an out-of-order write below the current version must lower a's bucket to 3"
1824		);
1825		assert_oldest_pending_tracks_the_maps(&storage, kind);
1826
1827		storage.compact(HashMap::from([(kind, vec![(a.clone(), CommitVersion(3))])])).unwrap();
1828		assert_eq!(
1829			oldest_of(&storage, kind, &a),
1830			Some(CommitVersion(10)),
1831			"dropping the oldest version must raise the bucket to the next-smallest stored version"
1832		);
1833		assert_oldest_pending_tracks_the_maps(&storage, kind);
1834
1835		storage.compact(HashMap::from([(kind, vec![(b.clone(), CommitVersion(5))])])).unwrap();
1836		assert_eq!(oldest_of(&storage, kind, &b), None, "a fully dropped key must leave the index entirely");
1837		assert_oldest_pending_tracks_the_maps(&storage, kind);
1838	}
1839
1840	#[test]
1841	fn out_of_order_landing_is_selected_for_eviction() {
1842		// A late or replayed commit landing below the current version becomes the key's oldest; an index
1843		// that tracked only first-seen versions would strand it in the buffer forever.
1844		let storage = CommitStore::new();
1845		let key = EncodedKey::new(b"k");
1846		storage.set(
1847			CommitVersion(20),
1848			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v20".to_vec())))])]),
1849		)
1850		.unwrap();
1851		storage.set(
1852			CommitVersion(3),
1853			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v3".to_vec())))])]),
1854		)
1855		.unwrap();
1856
1857		let (to_persist, to_drop, _consumed, _more) =
1858			storage.collect_evictable_below(EntryKind::Multi, CommitVersion(5), UNBOUNDED);
1859		let dropped: HashSet<CommitVersion> = to_drop.iter().map(|e| e.version).collect();
1860		assert_eq!(
1861			dropped,
1862			HashSet::from([CommitVersion(3)]),
1863			"the out-of-order v3 must be selected; v20 stays resident"
1864		);
1865		assert_eq!(to_persist.len(), 1);
1866		assert_eq!(to_persist[0].1, CommitVersion(3), "the aged-out v3 is the value persisted");
1867	}
1868
1869	#[test]
1870	fn byte_tally_matches_a_full_walk_across_mixed_mutations() {
1871		// The tally is incremental, so any drift from the true map contents misreports memory forever
1872		// after; every mutation shape is exercised then compared against an exhaustive walk.
1873		let storage = CommitStore::new();
1874		let k1 = EncodedKey::new(b"key-one");
1875		let k2 = EncodedKey::new(b"key-two");
1876
1877		for v in 1..=3u64 {
1878			storage.set(
1879				CommitVersion(v),
1880				HashMap::from([(
1881					EntryKind::Multi,
1882					vec![(k1.clone(), Some(CowVec::new(format!("value-{v}").into_bytes())))],
1883				)]),
1884			)
1885			.unwrap();
1886		}
1887		storage.set(
1888			CommitVersion(5),
1889			HashMap::from([(EntryKind::Multi, vec![(k2.clone(), Some(CowVec::new(b"x".to_vec())))])]),
1890		)
1891		.unwrap();
1892		storage.set(
1893			CommitVersion(2),
1894			HashMap::from([(EntryKind::Multi, vec![(k2.clone(), Some(CowVec::new(b"older".to_vec())))])]),
1895		)
1896		.unwrap();
1897		storage.set(CommitVersion(6), HashMap::from([(EntryKind::Multi, vec![(k2.clone(), None)])])).unwrap();
1898
1899		storage.compact(HashMap::from([(EntryKind::Multi, vec![(k1.clone(), CommitVersion(3))])])).unwrap();
1900		storage.compact(HashMap::from([(EntryKind::Multi, vec![(k2.clone(), CommitVersion(2))])])).unwrap();
1901
1902		let entry = storage.inner.entries.data.get(&EntryKind::Multi).unwrap();
1903		let active = entry.active.read();
1904		let closed = entry.closed_snapshot();
1905		let mut walked_current = 0u64;
1906		let mut walked_historical = 0u64;
1907		for rows in iter::once(active.rows()).chain(closed.iter().map(|map| map.rows())) {
1908			for (key, versions) in rows.iter() {
1909				let newest = versions.keys().next().map(|Reverse(version)| *version);
1910				for (Reverse(version), value) in versions.iter() {
1911					if newest == Some(*version) {
1912						walked_current += entry_bytes(key, value);
1913					} else {
1914						walked_historical += entry_bytes(key, value);
1915					}
1916				}
1917			}
1918		}
1919		drop(active);
1920
1921		assert!(walked_current > 0, "precondition: the scenario must leave current entries behind");
1922		assert!(walked_historical > 0, "precondition: the scenario must leave historical entries behind");
1923		assert_eq!(
1924			storage.current_resident_bytes().as_bytes(),
1925			walked_current,
1926			"the incremental current tally must equal an exhaustive walk of the newest version of every key"
1927		);
1928		assert_eq!(
1929			storage.historical_resident_bytes().as_bytes(),
1930			walked_historical,
1931			"the incremental historical tally must equal an exhaustive walk of every superseded version"
1932		);
1933	}
1934
1935	#[test]
1936	fn byte_tally_nets_to_zero_when_the_buffer_is_fully_drained() {
1937		// Eviction drains the buffer continuously, so a leak in any release path accumulates into a
1938		// permanently inflated memory report; the live-version drop is included in the sequence.
1939		let storage = CommitStore::new();
1940		let key = EncodedKey::new(b"k");
1941		for v in 1..=4u64 {
1942			storage.set(
1943				CommitVersion(v),
1944				HashMap::from([(
1945					EntryKind::Multi,
1946					vec![(key.clone(), Some(CowVec::new(format!("v{v}").into_bytes())))],
1947				)]),
1948			)
1949			.unwrap();
1950		}
1951		storage.seal_for_flush(EntryKind::Multi, CommitVersion(u64::MAX));
1952		assert!(storage.current_resident_bytes().as_bytes() > 0);
1953		assert!(storage.historical_resident_bytes().as_bytes() > 0);
1954
1955		storage.compact(HashMap::from([(EntryKind::Multi, vec![(key.clone(), CommitVersion(4))])])).unwrap();
1956		storage.compact(HashMap::from([(
1957			EntryKind::Multi,
1958			vec![
1959				(key.clone(), CommitVersion(1)),
1960				(key.clone(), CommitVersion(2)),
1961				(key.clone(), CommitVersion(3)),
1962			],
1963		)]))
1964		.unwrap();
1965
1966		assert_eq!(
1967			storage.current_resident_bytes(),
1968			ByteSize::ZERO,
1969			"draining every entry must return the current tally to zero"
1970		);
1971		assert_eq!(
1972			storage.historical_resident_bytes(),
1973			ByteSize::ZERO,
1974			"draining every entry must return the historical tally to zero"
1975		);
1976	}
1977
1978	#[test]
1979	fn clear_table_resets_the_byte_tally() {
1980		let storage = CommitStore::new();
1981		let key = EncodedKey::new(b"k");
1982		storage.set(
1983			CommitVersion(1),
1984			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v".to_vec())))])]),
1985		)
1986		.unwrap();
1987		storage.set(
1988			CommitVersion(2),
1989			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"w".to_vec())))])]),
1990		)
1991		.unwrap();
1992		assert!(storage.current_resident_bytes().as_bytes() > 0);
1993		assert!(storage.historical_resident_bytes().as_bytes() > 0);
1994
1995		storage.clear_table(EntryKind::Multi).unwrap();
1996		assert_eq!(
1997			storage.current_resident_bytes(),
1998			ByteSize::ZERO,
1999			"clearing a table must zero its byte tally, not leak it"
2000		);
2001		assert_eq!(storage.historical_resident_bytes(), ByteSize::ZERO);
2002	}
2003
2004	#[test]
2005	fn an_empty_buffer_has_no_oldest_pending_version() {
2006		// None means "nothing is waiting to be flushed", which is what lets a retention floor sit at
2007		// the permitted watermark. Reporting a version here would peg the floor to a write that
2008		// does not exist.
2009		let storage = CommitStore::new();
2010
2011		assert_eq!(storage.oldest_pending_version(), None);
2012	}
2013
2014	#[test]
2015	fn oldest_pending_version_is_the_minimum_across_every_entry_kind() {
2016		// The retention floor is global, so a single lagging keyspace has to hold it down. Taking a
2017		// per-kind minimum, or the newest version instead of the oldest, is what lets the tombstone
2018		// reaper delete rows an un-flushed write is about to rewrite.
2019		let storage = CommitStore::new();
2020		let key = EncodedKey::new(b"k");
2021
2022		storage.set(
2023			CommitVersion(40),
2024			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"late".to_vec())))])]),
2025		)
2026		.unwrap();
2027		storage.set(
2028			CommitVersion(7),
2029			HashMap::from([(
2030				EntryKind::Source(StorageId::Table(TableId(1)), EntryLayout::Row),
2031				vec![(key.clone(), Some(CowVec::new(b"early".to_vec())))],
2032			)]),
2033		)
2034		.unwrap();
2035		storage.set(
2036			CommitVersion(19),
2037			HashMap::from([(
2038				EntryKind::Source(StorageId::Table(TableId(2)), EntryLayout::Row),
2039				vec![(key.clone(), Some(CowVec::new(b"mid".to_vec())))],
2040			)]),
2041		)
2042		.unwrap();
2043
2044		assert_eq!(
2045			storage.oldest_pending_version(),
2046			Some(CommitVersion(7)),
2047			"the oldest un-flushed write in any keyspace is what bounds the durable frontier"
2048		);
2049	}
2050
2051	#[test]
2052	fn the_oldest_bucket_in_a_keyspace_wins_over_its_newer_ones() {
2053		// One keyspace holds many keys, each indexed under its own oldest resident version. Reading
2054		// the newest bucket instead of the oldest reports a frontier above writes that are still
2055		// buffered - and the tombstone reaper deletes under exactly that frontier.
2056		let storage = CommitStore::new();
2057
2058		storage.set(
2059			CommitVersion(11),
2060			HashMap::from([(
2061				EntryKind::Multi,
2062				vec![(EncodedKey::new(b"late"), Some(CowVec::new(b"v".to_vec())))],
2063			)]),
2064		)
2065		.unwrap();
2066		storage.set(
2067			CommitVersion(4),
2068			HashMap::from([(
2069				EntryKind::Multi,
2070				vec![(EncodedKey::new(b"early"), Some(CowVec::new(b"v".to_vec())))],
2071			)]),
2072		)
2073		.unwrap();
2074
2075		assert_eq!(storage.oldest_pending_version(), Some(CommitVersion(4)));
2076	}
2077
2078	#[test]
2079	fn a_superseded_version_still_counts_as_pending_until_it_is_compacted_away() {
2080		// Overwriting a key moves the old version into the historical map; it is still resident and
2081		// still un-flushed. If the index followed the current version instead, the frontier would
2082		// jump past a version the flusher has not written yet.
2083		let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
2084		let key = EncodedKey::new(b"k");
2085
2086		storage.set(
2087			CommitVersion(3),
2088			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v3".to_vec())))])]),
2089		)
2090		.unwrap();
2091		storage.set(
2092			CommitVersion(9),
2093			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v9".to_vec())))])]),
2094		)
2095		.unwrap();
2096
2097		assert_eq!(storage.oldest_pending_version(), Some(CommitVersion(3)));
2098
2099		storage.compact(HashMap::from([(EntryKind::Multi, vec![(key.clone(), CommitVersion(3))])])).unwrap();
2100
2101		assert_eq!(
2102			storage.oldest_pending_version(),
2103			Some(CommitVersion(9)),
2104			"once the sweep drains v3 the frontier may advance to the next un-flushed write"
2105		);
2106	}
2107
2108	#[test]
2109	fn draining_the_buffer_clears_the_oldest_pending_version() {
2110		// A sweep that empties a keyspace has to retire its bucket from the index. A stale bucket
2111		// pins the retention floor at a version that is already durable, and reclamation stalls
2112		// forever with no failing symptom.
2113		let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
2114		let key = EncodedKey::new(b"k");
2115
2116		storage.set(
2117			CommitVersion(5),
2118			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"v".to_vec())))])]),
2119		)
2120		.unwrap();
2121		storage.compact(HashMap::from([(EntryKind::Multi, vec![(key.clone(), CommitVersion(5))])])).unwrap();
2122
2123		assert_eq!(storage.oldest_pending_version(), None);
2124	}
2125
2126	#[test]
2127	fn a_default_buffer_round_trips_a_write() {
2128		let storage = CommitStore::new();
2129
2130		let key = EncodedKey::new(b"key");
2131		let version = CommitVersion(1);
2132
2133		storage.set(
2134			version,
2135			HashMap::from([(EntryKind::Multi, vec![(key.clone(), Some(CowVec::new(b"value".to_vec())))])]),
2136		)
2137		.unwrap();
2138		assert_eq!(
2139			storage.get(EntryKind::Multi, &key, version).unwrap().value().as_deref(),
2140			Some(b"value".as_slice())
2141		);
2142	}
2143
2144	#[test]
2145	fn a_default_buffer_scans_every_key_at_the_written_version() {
2146		let storage = CommitStore::new();
2147
2148		let version = CommitVersion(1);
2149		storage.set(
2150			version,
2151			HashMap::from([(
2152				EntryKind::Multi,
2153				vec![
2154					(EncodedKey::new(b"a"), Some(CowVec::new(b"1".to_vec()))),
2155					(EncodedKey::new(b"b"), Some(CowVec::new(b"2".to_vec()))),
2156					(EncodedKey::new(b"c"), Some(CowVec::new(b"3".to_vec()))),
2157				],
2158			)]),
2159		)
2160		.unwrap();
2161
2162		let mut cursor = RangeCursor::new();
2163		let batch = storage
2164			.range_next(
2165				EntryKind::Multi,
2166				&mut cursor,
2167				Bound::Unbounded,
2168				Bound::Unbounded,
2169				MultiVersionScope::AsOf {
2170					read: version,
2171				},
2172				100,
2173			)
2174			.unwrap();
2175
2176		assert_eq!(batch.entries.len(), 3);
2177		assert!(!batch.has_more);
2178		assert!(cursor.is_exhausted());
2179	}
2180
2181	#[test]
2182	fn a_point_read_finds_the_newest_version_at_or_below_the_asked_one_across_closed_maps() {
2183		// Point reads skip maps whose version range cannot beat the best hit; a skip that fires one
2184		// version too early hands back an older row, and one that fires too late is only slow.
2185		let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
2186		let k = EncodedKey::new(b"k");
2187		for (version, value) in [(5u64, b"v5".as_slice()), (1, b"v1"), (9, b"v9")] {
2188			storage.set(
2189				CommitVersion(version),
2190				HashMap::from([(
2191					EntryKind::Multi,
2192					vec![(k.clone(), Some(CowVec::new(value.to_vec())))],
2193				)]),
2194			)
2195			.unwrap();
2196		}
2197		let entry = storage.inner.entries.data.get(&EntryKind::Multi).unwrap();
2198		assert_eq!(entry.closed.read().len(), 3, "precondition: one closed map per write");
2199
2200		let read = |version: u64| match storage.get(EntryKind::Multi, b"k", CommitVersion(version)).unwrap() {
2201			VersionedGetResult::Value {
2202				value,
2203				version,
2204			} => Some((version.0, value.to_vec())),
2205			VersionedGetResult::Tombstone => panic!("no tombstone was written"),
2206			VersionedGetResult::NotFound => None,
2207		};
2208		assert_eq!(read(7), Some((5, b"v5".to_vec())));
2209		assert_eq!(
2210			read(5),
2211			Some((5, b"v5".to_vec())),
2212			"a read at exactly a map's oldest version keeps that map"
2213		);
2214		assert_eq!(read(3), Some((1, b"v1".to_vec())));
2215		assert_eq!(read(100), Some((9, b"v9".to_vec())));
2216		assert_eq!(read(0), None);
2217		assert!(storage.contains(EntryKind::Multi, b"k", CommitVersion(7)).unwrap());
2218		assert!(!storage.contains(EntryKind::Multi, b"k", CommitVersion(0)).unwrap());
2219	}
2220
2221	#[test]
2222	fn the_key_count_follows_writes_closes_compaction_and_clearing() {
2223		// The flush sizes its backlog from this count without walking the maps; a count that drifts from
2224		// the stored keys makes the flush report work that is not there or hide work that is.
2225		let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
2226		let walked = |storage: &CommitStore| -> u64 {
2227			let entry = storage.inner.entries.data.get(&EntryKind::Multi).unwrap();
2228			let active = entry.active.read().rows().key_count();
2229			let closed: usize = entry.closed.read().iter().map(|map| map.rows().key_count()).sum();
2230			(active + closed) as u64
2231		};
2232		seed(&storage, 1, &[keyed("a", b"a1"), keyed("b", b"b1")]);
2233		assert_eq!(storage.estimated_current_count(EntryKind::Multi).unwrap(), 2);
2234		seed(&storage, 2, &[keyed("a", b"a2")]);
2235		assert_eq!(
2236			storage.estimated_current_count(EntryKind::Multi).unwrap(),
2237			3,
2238			"a key rewritten into a new map counts once per map"
2239		);
2240		assert_eq!(storage.estimated_current_count(EntryKind::Multi).unwrap(), walked(&storage));
2241
2242		storage.compact(HashMap::from([(EntryKind::Multi, vec![(EncodedKey::new(b"a"), CommitVersion(1))])]))
2243			.unwrap();
2244		assert_eq!(storage.estimated_current_count(EntryKind::Multi).unwrap(), 2);
2245		assert_eq!(storage.estimated_current_count(EntryKind::Multi).unwrap(), walked(&storage));
2246
2247		storage.clear_table(EntryKind::Multi).unwrap();
2248		assert_eq!(storage.estimated_current_count(EntryKind::Multi).unwrap(), 0);
2249
2250		let same_map = CommitStore::new();
2251		seed(&same_map, 1, &[keyed("a", b"a1")]);
2252		seed(&same_map, 2, &[keyed("a", b"a2")]);
2253		assert_eq!(
2254			same_map.estimated_current_count(EntryKind::Multi).unwrap(),
2255			1,
2256			"a rewrite inside the active map adds no key"
2257		);
2258	}
2259
2260	#[test]
2261	fn compaction_never_alters_a_map_a_reader_still_holds() {
2262		// The flush walks a snapshot of the closed maps while the store keeps compacting; a drop that
2263		// mutated the shared map underneath would let the walk skip or double-read rows.
2264		let storage = CommitStore::with_close_threshold(ByteSize::from_bytes(1));
2265		seed(&storage, 1, &[keyed("a", b"a1")]);
2266		seed(&storage, 2, &[keyed("a", b"a2")]);
2267		let entry = storage.inner.entries.data.get(&EntryKind::Multi).unwrap();
2268		let held = entry.closed_snapshot();
2269
2270		storage.compact(HashMap::from([(EntryKind::Multi, vec![(EncodedKey::new(b"a"), CommitVersion(1))])]))
2271			.unwrap();
2272
2273		let held_versions: usize = held
2274			.iter()
2275			.map(|map| map.rows().iter().map(|(_, versions)| versions.len()).sum::<usize>())
2276			.sum();
2277		assert_eq!(held_versions, 2, "the held snapshot still sees v1");
2278		assert_eq!(storage.get_all_versions(EntryKind::Multi, b"a").unwrap().len(), 1, "the store dropped v1");
2279	}
2280
2281	#[test]
2282	fn the_oldest_pending_version_rises_when_the_oldest_row_leaves_a_map_that_keeps_others() {
2283		// Dropping a map's oldest row in place must move the map's oldest version to the next one it
2284		// still holds; a version range that stays put freezes the retention floor on a version nothing
2285		// holds any more.
2286		let storage = CommitStore::new();
2287		seed(&storage, 3, &[keyed("a", b"a3")]);
2288		seed(&storage, 5, &[keyed("b", b"b5")]);
2289		assert_eq!(storage.oldest_pending_version(), Some(CommitVersion(3)));
2290
2291		storage.compact(HashMap::from([(EntryKind::Multi, vec![(EncodedKey::new(b"a"), CommitVersion(3))])]))
2292			.unwrap();
2293
2294		assert_eq!(storage.oldest_pending_version(), Some(CommitVersion(5)));
2295	}
2296}