Skip to main content

reifydb_store_commit/
rows.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	cmp::{Ordering, Reverse},
6	collections::{BTreeMap, BinaryHeap, HashMap, HashSet, btree_map},
7	ops::RangeBounds,
8};
9
10use reifydb_codec::key::encoded::EncodedKey;
11use reifydb_core::{common::CommitVersion, metrics::heap::HeapSize};
12use reifydb_value::reifydb_assertions;
13
14use crate::entry::{Value, entry_bytes_with};
15
16pub(super) type VersionMap = BTreeMap<Reverse<CommitVersion>, Value>;
17
18#[derive(Clone, Default)]
19pub(super) struct RowMap {
20	entries: BTreeMap<EncodedKey, VersionMap>,
21	current_bytes: u64,
22	historical_bytes: u64,
23	versions: BTreeMap<CommitVersion, u32>,
24}
25
26impl RowMap {
27	pub fn insert(&mut self, key: EncodedKey, version: CommitVersion, value: Value) {
28		let key_heap = key.heap_size();
29		let bytes = entry_bytes_with(key_heap, &value);
30
31		let versions = self.entries.entry(key).or_default();
32		let previous_newest = versions.keys().next().map(|Reverse(v)| *v);
33		let replaced = versions.insert(Reverse(version), value);
34		let stays_newest = previous_newest.is_none_or(|newest| version >= newest);
35		let demoted_bytes = previous_newest
36			.filter(|newest| version > *newest)
37			.and_then(|newest| versions.get(&Reverse(newest)))
38			.map(|demoted| entry_bytes_with(key_heap, demoted));
39
40		match replaced {
41			Some(replaced) => {
42				let replaced_bytes = entry_bytes_with(key_heap, &replaced);
43				if previous_newest == Some(version) {
44					self.current_bytes = self.current_bytes.saturating_sub(replaced_bytes);
45				} else {
46					self.historical_bytes = self.historical_bytes.saturating_sub(replaced_bytes);
47				}
48			}
49			None => *self.versions.entry(version).or_insert(0) += 1,
50		}
51
52		if let Some(demoted) = demoted_bytes {
53			self.current_bytes = self.current_bytes.saturating_sub(demoted);
54			self.historical_bytes = self.historical_bytes.saturating_add(demoted);
55		}
56
57		if stays_newest {
58			self.current_bytes = self.current_bytes.saturating_add(bytes);
59		} else {
60			self.historical_bytes = self.historical_bytes.saturating_add(bytes);
61		}
62	}
63
64	pub fn remove(&mut self, dropped: &HashMap<EncodedKey, HashSet<CommitVersion>>) -> Vec<Removed> {
65		let mut removed = Vec::new();
66		for (key, versions) in dropped {
67			let Some(held) = self.entries.get_mut(key) else {
68				continue;
69			};
70			let key_heap = key.heap_size();
71			for version in versions {
72				let was_newest = held.keys().next() == Some(&Reverse(*version));
73				let Some(value) = held.remove(&Reverse(*version)) else {
74					continue;
75				};
76				let bytes = entry_bytes_with(key_heap, &value);
77				if was_newest {
78					self.current_bytes = self.current_bytes.saturating_sub(bytes);
79					if let Some((_, promoted)) = held.iter().next() {
80						let promoted = entry_bytes_with(key_heap, promoted);
81						self.historical_bytes = self.historical_bytes.saturating_sub(promoted);
82						self.current_bytes = self.current_bytes.saturating_add(promoted);
83					}
84				} else {
85					self.historical_bytes = self.historical_bytes.saturating_sub(bytes);
86				}
87				Self::forget(&mut self.versions, *version);
88				removed.push(Removed {
89					key: key.clone(),
90					version: *version,
91					value,
92				});
93			}
94			if held.is_empty() {
95				self.entries.remove(key);
96			}
97		}
98		removed
99	}
100
101	fn forget(versions: &mut BTreeMap<CommitVersion, u32>, version: CommitVersion) {
102		let count = versions.get_mut(&version).expect("every stored version is counted");
103		*count -= 1;
104		if *count == 0 {
105			versions.remove(&version);
106		}
107	}
108
109	pub fn get(&self, key: &[u8], version: CommitVersion) -> Option<(CommitVersion, &Value)> {
110		self.entries
111			.get(key)
112			.and_then(|versions| versions.range(Reverse(version)..).next())
113			.map(|(Reverse(found), value)| (*found, value))
114	}
115
116	pub fn versions_for(&self, key: &[u8]) -> Option<&VersionMap> {
117		self.entries.get(key)
118	}
119
120	pub fn range<R>(&self, bounds: R) -> btree_map::Range<'_, EncodedKey, VersionMap>
121	where
122		R: RangeBounds<[u8]>,
123	{
124		self.entries.range::<[u8], R>(bounds)
125	}
126
127	pub fn iter(&self) -> btree_map::Iter<'_, EncodedKey, VersionMap> {
128		self.entries.iter()
129	}
130
131	pub fn key_count(&self) -> usize {
132		self.entries.len()
133	}
134
135	pub fn current_bytes(&self) -> u64 {
136		self.current_bytes
137	}
138
139	pub fn historical_bytes(&self) -> u64 {
140		self.historical_bytes
141	}
142
143	pub fn bytes(&self) -> u64 {
144		self.current_bytes.saturating_add(self.historical_bytes)
145	}
146
147	pub fn is_empty(&self) -> bool {
148		self.entries.is_empty()
149	}
150
151	pub fn min_version(&self) -> Option<CommitVersion> {
152		self.versions.keys().next().copied()
153	}
154
155	pub fn max_version(&self) -> Option<CommitVersion> {
156		self.versions.keys().next_back().copied()
157	}
158}
159
160pub(super) struct Removed {
161	pub key: EncodedKey,
162	pub version: CommitVersion,
163	pub value: Value,
164}
165
166pub(super) fn lookup<'a>(
167	maps: impl Iterator<Item = &'a RowMap>,
168	key: &[u8],
169	version: CommitVersion,
170) -> Option<(CommitVersion, &'a Value)> {
171	let mut best: Option<(CommitVersion, &'a Value)> = None;
172	for rows in maps {
173		if rows.min_version().is_none_or(|min| min > version) {
174			continue;
175		}
176		if let Some((found, _)) = best
177			&& rows.max_version().is_some_and(|max| max <= found)
178		{
179			continue;
180		}
181		if let Some((found, value)) = rows.get(key, version)
182			&& best.is_none_or(|(best, _)| found > best)
183		{
184			best = Some((found, value));
185		}
186	}
187	best
188}
189
190pub(super) fn newest_across<'a>(
191	maps: impl Iterator<Item = &'a VersionMap>,
192	version: CommitVersion,
193) -> Option<(CommitVersion, &'a Value)> {
194	maps.filter_map(|versions| {
195		versions.range(Reverse(version)..).next().map(|(Reverse(found), value)| (*found, value))
196	})
197	.max_by_key(|(found, _)| *found)
198}
199
200struct Head<'a> {
201	key: &'a EncodedKey,
202	versions: &'a VersionMap,
203	source: usize,
204	reverse: bool,
205}
206
207impl PartialEq for Head<'_> {
208	fn eq(&self, other: &Self) -> bool {
209		self.key == other.key
210	}
211}
212
213impl Eq for Head<'_> {}
214
215impl PartialOrd for Head<'_> {
216	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
217		Some(self.cmp(other))
218	}
219}
220
221impl Ord for Head<'_> {
222	fn cmp(&self, other: &Self) -> Ordering {
223		let order = self.key.cmp(other.key);
224		if self.reverse {
225			order
226		} else {
227			order.reverse()
228		}
229	}
230}
231
232pub(super) struct MergedRows<'a, I>
233where
234	I: Iterator<Item = (&'a EncodedKey, &'a VersionMap)>,
235{
236	iters: Vec<I>,
237	heads: BinaryHeap<Head<'a>>,
238	reverse: bool,
239	group: Vec<&'a VersionMap>,
240}
241
242impl<'a, I> MergedRows<'a, I>
243where
244	I: Iterator<Item = (&'a EncodedKey, &'a VersionMap)>,
245{
246	pub fn new(mut iters: Vec<I>, reverse: bool) -> Self {
247		let mut heads = BinaryHeap::with_capacity(iters.len());
248		for (source, iter) in iters.iter_mut().enumerate() {
249			if let Some((key, versions)) = iter.next() {
250				heads.push(Head {
251					key,
252					versions,
253					source,
254					reverse,
255				});
256			}
257		}
258		Self {
259			iters,
260			heads,
261			reverse,
262			group: Vec::new(),
263		}
264	}
265
266	fn advance(&mut self, source: usize) {
267		if let Some((key, versions)) = self.iters[source].next() {
268			self.heads.push(Head {
269				key,
270				versions,
271				source,
272				reverse: self.reverse,
273			});
274		}
275	}
276
277	pub fn next_group(&mut self) -> Option<(&'a EncodedKey, &[&'a VersionMap])> {
278		let first = self.heads.pop()?;
279		let target = first.key;
280		self.group.clear();
281		self.group.push(first.versions);
282		self.advance(first.source);
283		while self.heads.peek().is_some_and(|head| head.key == target) {
284			let head = self.heads.pop().expect("a peeked heap yields");
285			self.group.push(head.versions);
286			self.advance(head.source);
287		}
288		Some((target, &self.group))
289	}
290}
291
292#[derive(Default)]
293pub(super) struct ActiveRows {
294	rows: RowMap,
295}
296
297impl ActiveRows {
298	pub fn new() -> Self {
299		Self::default()
300	}
301
302	pub fn rows(&self) -> &RowMap {
303		&self.rows
304	}
305
306	pub fn insert(&mut self, key: EncodedKey, version: CommitVersion, value: Value) {
307		self.rows.insert(key, version, value);
308	}
309
310	pub fn min_version(&self) -> Option<CommitVersion> {
311		self.rows.min_version()
312	}
313
314	pub fn compact(&mut self, dropped: &HashMap<EncodedKey, HashSet<CommitVersion>>) -> Vec<Removed> {
315		self.rows.remove(dropped)
316	}
317
318	pub fn bytes(&self) -> u64 {
319		self.rows.bytes()
320	}
321
322	pub fn is_empty(&self) -> bool {
323		self.rows.is_empty()
324	}
325
326	pub fn close(self) -> ClosedRows {
327		reifydb_assertions! {
328			assert!(
329				!self.rows.is_empty(),
330				"closing an empty active map mints a closed map with no version range, and the flush \
331				 orders closed maps by that range"
332			);
333		}
334		ClosedRows {
335			rows: self.rows,
336		}
337	}
338}
339
340#[derive(Clone)]
341pub(super) struct ClosedRows {
342	rows: RowMap,
343}
344
345impl ClosedRows {
346	pub fn rows(&self) -> &RowMap {
347		&self.rows
348	}
349
350	pub fn min_version(&self) -> CommitVersion {
351		self.rows.min_version().expect("a closed map holds at least one row")
352	}
353
354	pub fn compact(&mut self, dropped: &HashMap<EncodedKey, HashSet<CommitVersion>>) -> Vec<Removed> {
355		self.rows.remove(dropped)
356	}
357}
358
359#[cfg(test)]
360mod tests {
361	use std::ops::Bound;
362
363	use reifydb_value::util::cowvec::CowVec;
364
365	use super::*;
366
367	fn key(name: &str) -> EncodedKey {
368		EncodedKey::new(name.as_bytes().to_vec())
369	}
370
371	fn val(bytes: &str) -> Value {
372		Some(CowVec::new(bytes.as_bytes().to_vec()))
373	}
374
375	fn walked_bytes(rows: &RowMap) -> u64 {
376		rows.iter()
377			.map(|(key, versions)| {
378				let heap = key.heap_size();
379				versions.values().map(|value| entry_bytes_with(heap, value)).sum::<u64>()
380			})
381			.sum()
382	}
383
384	fn walked_version_count(rows: &RowMap) -> usize {
385		rows.iter().map(|(_, versions)| versions.len()).sum()
386	}
387
388	fn walked_current_bytes(rows: &RowMap) -> u64 {
389		rows.iter()
390			.filter_map(|(key, versions)| {
391				versions.values().next().map(|value| entry_bytes_with(key.heap_size(), value))
392			})
393			.sum()
394	}
395
396	#[test]
397	fn a_lone_version_of_a_key_is_billed_as_current() {
398		// Every key's newest version is its current one; billing a first write as historical understates
399		// the current residency the flush uses to size a slice.
400		let mut rows = RowMap::default();
401		rows.insert(key("a"), CommitVersion(1), val("one"));
402
403		assert_eq!(rows.key_count(), 1);
404		assert_eq!(walked_version_count(&rows), 1);
405		assert_eq!(rows.historical_bytes(), 0, "a key with a single version has no history");
406		assert_eq!(rows.current_bytes(), walked_current_bytes(&rows));
407	}
408
409	#[test]
410	fn a_newer_version_demotes_the_previous_newest_into_history() {
411		// The newest version must move to historical when it is superseded, otherwise both versions are
412		// billed as current and the current residency double-counts the key.
413		let mut rows = RowMap::default();
414		rows.insert(key("a"), CommitVersion(1), val("one"));
415		let first = rows.current_bytes();
416		rows.insert(key("a"), CommitVersion(2), val("twotwo"));
417
418		assert_eq!(rows.key_count(), 1, "a second version is not a second key");
419		assert_eq!(walked_version_count(&rows), 2);
420		assert_eq!(rows.historical_bytes(), first, "the superseded version carries its own bytes down");
421		assert_eq!(rows.current_bytes(), walked_current_bytes(&rows));
422		assert_eq!(rows.bytes(), walked_bytes(&rows));
423	}
424
425	#[test]
426	fn a_version_landing_below_the_newest_never_disturbs_current() {
427		// Commits can land out of order; an older version must be billed as history without moving the
428		// standing current version, or the current tally drifts on every late arrival.
429		let mut rows = RowMap::default();
430		rows.insert(key("a"), CommitVersion(5), val("five"));
431		let current_after_newest = rows.current_bytes();
432		rows.insert(key("a"), CommitVersion(2), val("two"));
433
434		assert_eq!(rows.current_bytes(), current_after_newest, "a late older version is not current");
435		assert_eq!(walked_version_count(&rows), 2);
436		assert_eq!(rows.bytes(), walked_bytes(&rows));
437	}
438
439	#[test]
440	fn rewriting_a_version_in_place_rebills_it_rather_than_stacking() {
441		// Re-inserting a version already present replaces it; counting the new bytes without retiring the
442		// old ones leaks residency that nothing will ever reclaim.
443		let mut rows = RowMap::default();
444		rows.insert(key("a"), CommitVersion(1), val("short"));
445		rows.insert(key("a"), CommitVersion(1), val("a much longer value"));
446
447		assert_eq!(walked_version_count(&rows), 1, "a rewrite is not a new version");
448		assert_eq!(rows.current_bytes(), walked_current_bytes(&rows));
449		assert_eq!(rows.bytes(), walked_bytes(&rows));
450	}
451
452	#[test]
453	fn rewriting_a_historical_version_in_place_rebills_only_history() {
454		// The same rewrite one version below the newest must settle against the historical tally; billing
455		// it to current would inflate the residency the flush sizes its slice from.
456		let mut rows = RowMap::default();
457		rows.insert(key("a"), CommitVersion(2), val("newest"));
458		let current = rows.current_bytes();
459		rows.insert(key("a"), CommitVersion(1), val("old"));
460		rows.insert(key("a"), CommitVersion(1), val("an old value made longer"));
461
462		assert_eq!(rows.current_bytes(), current, "the newest version was never touched");
463		assert_eq!(walked_version_count(&rows), 2);
464		assert_eq!(rows.bytes(), walked_bytes(&rows));
465	}
466
467	#[test]
468	fn the_tally_matches_a_full_walk_across_mixed_mutations() {
469		// The counters are maintained incrementally, so any path that forgets an adjustment shows up only
470		// as drift against an independent walk of what is actually stored.
471		let mut rows = RowMap::default();
472		rows.insert(key("a"), CommitVersion(1), val("a1"));
473		rows.insert(key("b"), CommitVersion(1), val("b1"));
474		rows.insert(key("a"), CommitVersion(3), val("a3-longer"));
475		rows.insert(key("a"), CommitVersion(2), val("a2"));
476		rows.insert(key("b"), CommitVersion(4), val("b4"));
477		rows.insert(key("c"), CommitVersion(2), None);
478		rows.insert(key("b"), CommitVersion(4), val("b4-rewritten"));
479
480		assert_eq!(rows.bytes(), walked_bytes(&rows));
481		assert_eq!(rows.current_bytes(), walked_current_bytes(&rows));
482		assert_eq!(rows.historical_bytes(), walked_bytes(&rows) - walked_current_bytes(&rows));
483		assert_eq!(rows.key_count(), 3);
484		assert_eq!(walked_version_count(&rows), 6);
485	}
486
487	#[test]
488	fn a_tombstone_is_a_version_like_any_other() {
489		// A delete is stored as a none value and still occupies a version; skipping it would let a deleted
490		// key read through to the value it shadowed.
491		let mut rows = RowMap::default();
492		rows.insert(key("a"), CommitVersion(1), val("live"));
493		rows.insert(key("a"), CommitVersion(2), None);
494
495		assert_eq!(walked_version_count(&rows), 2);
496		assert_eq!(rows.get(b"a", CommitVersion(2)), Some((CommitVersion(2), &None)));
497		assert_eq!(rows.bytes(), walked_bytes(&rows));
498	}
499
500	#[test]
501	fn a_read_sees_the_newest_version_at_or_below_the_asked_one() {
502		// A read at version v must not see writes above v, and must not skip past the newest write at or
503		// below it, or a snapshot read returns a row from the wrong point in time.
504		let mut rows = RowMap::default();
505		rows.insert(key("a"), CommitVersion(1), val("one"));
506		rows.insert(key("a"), CommitVersion(3), val("three"));
507		rows.insert(key("a"), CommitVersion(5), val("five"));
508
509		assert_eq!(rows.get(b"a", CommitVersion(4)), Some((CommitVersion(3), &val("three"))));
510		assert_eq!(rows.get(b"a", CommitVersion(5)), Some((CommitVersion(5), &val("five"))));
511		assert_eq!(rows.get(b"a", CommitVersion(2)), Some((CommitVersion(1), &val("one"))));
512		assert_eq!(rows.get(b"a", CommitVersion(0)), None, "nothing was written at or below version 0");
513		assert_eq!(rows.get(b"missing", CommitVersion(5)), None);
514	}
515
516	#[test]
517	fn the_oldest_version_is_the_smallest_whatever_the_insertion_order() {
518		// The flush gates on this version, so one that tracks insertion order rather than version order
519		// would let a map be skipped while it still holds writes below the cutoff.
520		let mut rows = RowMap::default();
521		rows.insert(key("a"), CommitVersion(7), val("seven"));
522		rows.insert(key("b"), CommitVersion(2), val("two"));
523		rows.insert(key("c"), CommitVersion(9), val("nine"));
524		rows.insert(key("d"), CommitVersion(4), val("four"));
525
526		assert_eq!(rows.min_version(), Some(CommitVersion(2)));
527	}
528
529	#[test]
530	fn an_empty_row_map_has_no_oldest_version() {
531		// A closed map with no oldest version cannot be gated against a cutoff, which is why closing an
532		// empty active map is refused rather than defaulted to zero.
533		let rows = RowMap::default();
534
535		assert_eq!(rows.min_version(), None);
536		assert!(rows.is_empty());
537		assert_eq!(rows.bytes(), 0);
538	}
539
540	#[test]
541	fn closing_records_the_oldest_version_of_the_rows_it_holds() {
542		// Every flush gate skips a closed map by this version, so one above the map's true oldest write
543		// hides that write from the sweep forever.
544		let mut active = ActiveRows::new();
545		active.insert(key("a"), CommitVersion(3), val("three"));
546		active.insert(key("b"), CommitVersion(8), val("eight"));
547
548		let closed = active.close();
549
550		assert_eq!(closed.min_version(), CommitVersion(3));
551	}
552
553	#[test]
554	fn range_walks_keys_in_order_within_the_bounds() {
555		// The commit-buffer scan merges this iterator against the closed maps by key, so it must yield keys
556		// in ascending order and honour the bounds it was given.
557		let mut rows = RowMap::default();
558		for name in ["a", "b", "c", "d"] {
559			rows.insert(key(name), CommitVersion(1), val(name));
560		}
561
562		let seen: Vec<&[u8]> = rows
563			.range::<(Bound<&[u8]>, Bound<&[u8]>)>((
564				Bound::Excluded(b"a".as_slice()),
565				Bound::Included(b"c".as_slice()),
566			))
567			.map(|(key, _)| key.as_slice())
568			.collect();
569
570		assert_eq!(seen, vec![b"b".as_slice(), b"c".as_slice()]);
571	}
572
573	#[test]
574	fn removing_the_newest_version_promotes_the_next_one_into_current() {
575		// The flush drops a key's newest version when it moves to the persistent tier; the version left
576		// behind becomes current, and a tally that leaves it billed as history under-reports the residency
577		// the flush sizes its next slice from.
578		let mut rows = RowMap::default();
579		rows.insert(key("a"), CommitVersion(1), val("one"));
580		rows.insert(key("a"), CommitVersion(2), val("two-two"));
581		rows.insert(key("a"), CommitVersion(3), val("three"));
582
583		let removed = rows.remove(&HashMap::from([(key("a"), HashSet::from([CommitVersion(3)]))]));
584
585		assert_eq!(removed.len(), 1);
586		assert_eq!(removed[0].version, CommitVersion(3));
587		assert_eq!(removed[0].value, val("three"));
588		assert_eq!(walked_version_count(&rows), 2);
589		assert_eq!(rows.current_bytes(), walked_current_bytes(&rows));
590		assert_eq!(rows.bytes(), walked_bytes(&rows));
591		assert_eq!(rows.max_version(), Some(CommitVersion(2)));
592		assert_eq!(rows.get(b"a", CommitVersion(9)), Some((CommitVersion(2), &val("two-two"))));
593	}
594
595	#[test]
596	fn removing_a_historical_version_touches_only_the_historical_tally() {
597		// Historical GC only ever drops superseded versions; if that reached into the current tally the
598		// flush would size slices from a residency the buffer no longer holds.
599		let mut rows = RowMap::default();
600		rows.insert(key("a"), CommitVersion(1), val("one"));
601		rows.insert(key("a"), CommitVersion(2), val("two"));
602		let current = rows.current_bytes();
603
604		rows.remove(&HashMap::from([(key("a"), HashSet::from([CommitVersion(1)]))]));
605
606		assert_eq!(rows.current_bytes(), current);
607		assert_eq!(rows.historical_bytes(), 0);
608		assert_eq!(rows.min_version(), Some(CommitVersion(2)));
609		assert_eq!(walked_version_count(&rows), 1);
610	}
611
612	#[test]
613	fn removing_every_version_of_a_key_forgets_the_key_and_its_versions() {
614		// A key with no versions left must not linger as an empty entry: it would count as a stored key
615		// and keep the map's version range alive for a flush gate that has nothing left to flush.
616		let mut rows = RowMap::default();
617		rows.insert(key("a"), CommitVersion(4), val("four"));
618		rows.insert(key("b"), CommitVersion(6), val("six"));
619
620		let removed = rows.remove(&HashMap::from([(key("a"), HashSet::from([CommitVersion(4)]))]));
621
622		assert_eq!(removed.len(), 1);
623		assert_eq!(rows.key_count(), 1);
624		assert_eq!(rows.min_version(), Some(CommitVersion(6)));
625		assert_eq!(rows.max_version(), Some(CommitVersion(6)));
626		assert_eq!(rows.bytes(), walked_bytes(&rows));
627
628		rows.remove(&HashMap::from([(key("b"), HashSet::from([CommitVersion(6)]))]));
629
630		assert!(rows.is_empty());
631		assert_eq!(rows.min_version(), None);
632		assert_eq!(rows.bytes(), 0);
633	}
634
635	#[test]
636	fn removing_a_version_the_map_does_not_hold_changes_nothing() {
637		// A drop batch names versions across every map of a kind; a map that holds the key at other
638		// versions only must leave them and its tallies alone.
639		let mut rows = RowMap::default();
640		rows.insert(key("a"), CommitVersion(2), val("two"));
641		let bytes = rows.bytes();
642
643		let removed = rows.remove(&HashMap::from([
644			(key("a"), HashSet::from([CommitVersion(1)])),
645			(key("zz"), HashSet::from([CommitVersion(2)])),
646		]));
647
648		assert!(removed.is_empty());
649		assert_eq!(rows.bytes(), bytes);
650		assert_eq!(walked_version_count(&rows), 1);
651		assert_eq!(rows.min_version(), Some(CommitVersion(2)));
652	}
653
654	#[test]
655	fn a_version_shared_by_two_keys_stays_in_range_until_both_are_gone() {
656		// One commit writes many keys at the same version; forgetting the version when the first key
657		// leaves would lift the map's oldest version above rows it still holds.
658		let mut rows = RowMap::default();
659		rows.insert(key("a"), CommitVersion(1), val("a"));
660		rows.insert(key("b"), CommitVersion(1), val("b"));
661		rows.insert(key("c"), CommitVersion(2), val("c"));
662
663		rows.remove(&HashMap::from([(key("a"), HashSet::from([CommitVersion(1)]))]));
664		assert_eq!(rows.min_version(), Some(CommitVersion(1)));
665
666		rows.remove(&HashMap::from([(key("b"), HashSet::from([CommitVersion(1)]))]));
667		assert_eq!(rows.min_version(), Some(CommitVersion(2)));
668	}
669
670	#[test]
671	fn merging_yields_each_key_once_in_order_with_every_map_that_holds_it() {
672		// The range scan resolves a key's version from the group it is handed; a key split into two
673		// groups would surface twice, and a group missing a map would read a stale version.
674		let mut a = RowMap::default();
675		let mut b = RowMap::default();
676		let mut c = RowMap::default();
677		for name in ["a", "c", "e"] {
678			a.insert(key(name), CommitVersion(1), val(name));
679		}
680		for name in ["b", "c"] {
681			b.insert(key(name), CommitVersion(2), val(name));
682		}
683		for name in ["c", "d"] {
684			c.insert(key(name), CommitVersion(3), val(name));
685		}
686		let maps = [&a, &b, &c];
687		let unbounded = || (Bound::<&[u8]>::Unbounded, Bound::<&[u8]>::Unbounded);
688
689		let mut forward = MergedRows::new(maps.iter().map(|rows| rows.range(unbounded())).collect(), false);
690		let mut seen = Vec::new();
691		while let Some((key, group)) = forward.next_group() {
692			seen.push((key.as_slice().to_vec(), group.len()));
693		}
694		assert_eq!(
695			seen,
696			vec![
697				(b"a".to_vec(), 1),
698				(b"b".to_vec(), 1),
699				(b"c".to_vec(), 3),
700				(b"d".to_vec(), 1),
701				(b"e".to_vec(), 1)
702			]
703		);
704
705		let mut backward =
706			MergedRows::new(maps.iter().map(|rows| rows.range(unbounded()).rev()).collect(), true);
707		let mut seen = Vec::new();
708		while let Some((key, group)) = backward.next_group() {
709			seen.push((key.as_slice().to_vec(), group.len()));
710		}
711		assert_eq!(
712			seen,
713			vec![
714				(b"e".to_vec(), 1),
715				(b"d".to_vec(), 1),
716				(b"c".to_vec(), 3),
717				(b"b".to_vec(), 1),
718				(b"a".to_vec(), 1)
719			]
720		);
721	}
722}