1use std::collections::BTreeMap;
23use std::fs::{File, OpenOptions};
24use std::io::{self, Read, Write};
25use std::path::Path;
26
27use crate::epoch::{Epoch, Snapshot};
28use crate::manifest::RunRef;
29use crate::rowid::RowId;
30use crate::{Result, Table};
31use crc::{Crc, CRC_32_ISCSI};
32use mongreldb_types::hlc::HlcTimestamp;
33
34pub const DIRECTORY_FILENAME: &str = "directory.bin";
37pub const DIRECTORY_SHARD_PREFIX: &str = "directory.shard-";
39pub const DIRECTORY_SHARD_SUFFIX: &str = ".bin";
40pub const DEFAULT_SHARD_BYTES: u64 = 16 * 1024 * 1024;
44
45const LOOKUP_MAGIC: [u8; 4] = *b"MLKP";
47const CRC32C: Crc<u32> = Crc::<u32>::new(&CRC_32_ISCSI);
49const FOOTER_LEN: usize = 4;
50const HLC_BYTES: usize = 16;
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct RunLocator {
57 pub run_id: u128,
59 pub min_epoch: Epoch,
60 pub max_epoch: Epoch,
61 pub min_hlc: Option<HlcTimestamp>,
63 pub max_hlc: Option<HlcTimestamp>,
64 pub contains_unstamped_versions: bool,
66}
67
68#[derive(Debug, Clone, Default)]
71pub struct RunLocatorList {
72 pub locators: Vec<RunLocator>,
73}
74
75impl RunLocatorList {
76 pub fn is_empty(&self) -> bool {
77 self.locators.is_empty()
78 }
79
80 pub fn len(&self) -> usize {
81 self.locators.len()
82 }
83}
84
85#[derive(Debug, Clone)]
101pub(crate) enum DirectoryLookupDecision {
102 CompleteMiss,
103 Candidates(Vec<RunLocator>),
104 UnavailableOrStale,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub(crate) struct VersionStamp {
113 pub epoch: Epoch,
114 pub hlc: Option<HlcTimestamp>,
115}
116
117#[derive(Debug, Default, Clone)]
120pub struct RunLookupDirectory {
121 postings: BTreeMap<RowId, RunLocatorList>,
123 fingerprint: Option<u64>,
127}
128
129impl RunLookupDirectory {
130 pub fn empty() -> Self {
132 Self::default()
133 }
134
135 pub fn locate(&self, row_id: RowId) -> &RunLocatorList {
138 static EMPTY: RunLocatorList = RunLocatorList {
139 locators: Vec::new(),
140 };
141 self.postings.get(&row_id).unwrap_or(&EMPTY)
142 }
143
144 pub(crate) fn decide(&self, row_id: RowId) -> DirectoryLookupDecision {
149 match self.postings.get(&row_id) {
150 None => DirectoryLookupDecision::CompleteMiss,
151 Some(list) if list.is_empty() => DirectoryLookupDecision::CompleteMiss,
152 Some(list) => DirectoryLookupDecision::Candidates(list.locators.clone()),
153 }
154 }
155
156 pub fn insert(&mut self, row_id: RowId, locator: RunLocator) {
159 let entry = self.postings.entry(row_id).or_default();
160 let pos = entry
164 .locators
165 .iter()
166 .position(|existing| locator_is_newer(locator, *existing));
167 match pos {
168 Some(idx) => entry.locators.insert(idx, locator),
169 None => entry.locators.push(locator),
170 }
171 }
172
173 pub fn retain_active_runs(&mut self, active_runs: &[u128]) -> usize {
176 let mut removed = 0;
177 for list in self.postings.values_mut() {
178 let before = list.locators.len();
179 list.locators.retain(|l| active_runs.contains(&l.run_id));
180 removed += before - list.locators.len();
181 }
182 removed
183 }
184
185 pub fn set_fingerprint(&mut self, fp: u64) {
187 self.fingerprint = Some(fp);
188 }
189
190 pub fn fingerprint(&self) -> Option<u64> {
191 self.fingerprint
192 }
193
194 pub fn total_locators(&self) -> usize {
196 self.postings.values().map(|l| l.locators.len()).sum()
197 }
198
199 pub fn key_count(&self) -> usize {
201 self.postings.len()
202 }
203
204 pub fn max_locators_per_row(&self) -> usize {
206 self.postings
207 .values()
208 .map(|l| l.locators.len())
209 .max()
210 .unwrap_or(0)
211 }
212
213 pub fn avg_locators_per_row(&self) -> f64 {
215 if self.postings.is_empty() {
216 return 0.0;
217 }
218 self.total_locators() as f64 / self.postings.len() as f64
219 }
220
221 pub fn write_checkpoint(&self, path: &Path) -> io::Result<()> {
227 let fp = self.fingerprint.ok_or_else(|| {
228 io::Error::new(
229 io::ErrorKind::InvalidData,
230 "run-lookup directory has no fingerprint; refusing to checkpoint",
231 )
232 })?;
233 let mut body: Vec<u8> = Vec::new();
234 body.extend_from_slice(&LOOKUP_MAGIC);
235 body.extend_from_slice(&fp.to_le_bytes());
236 let key_count = self.postings.len() as u32;
237 body.extend_from_slice(&key_count.to_le_bytes());
238 let mut prev: u64 = 0;
239 for (row_id, list) in &self.postings {
240 let cur = row_id.0;
241 let delta = cur.checked_sub(prev).ok_or_else(|| {
242 io::Error::new(
243 io::ErrorKind::InvalidData,
244 "run-lookup row_id sequence is not non-decreasing",
245 )
246 })?;
247 encode_varint(delta, &mut body);
248 let locator_count = list.locators.len() as u32;
249 body.extend_from_slice(&locator_count.to_le_bytes());
250 for loc in &list.locators {
251 body.extend_from_slice(&loc.run_id.to_le_bytes());
252 body.extend_from_slice(&loc.min_epoch.0.to_le_bytes());
253 body.extend_from_slice(&loc.max_epoch.0.to_le_bytes());
254 write_optional_hlc(loc.min_hlc, &mut body);
255 write_optional_hlc(loc.max_hlc, &mut body);
256 body.push(if loc.contains_unstamped_versions {
257 1
258 } else {
259 0
260 });
261 }
262 prev = cur;
263 }
264 let crc = CRC32C.checksum(&body);
265 body.extend_from_slice(&crc.to_le_bytes());
266
267 let parent = path.parent().unwrap_or_else(|| Path::new("."));
268 let staging = parent.join(format!(
269 ".{}.{}.{}.staging",
270 path.file_name()
271 .and_then(|name| name.to_str())
272 .unwrap_or("checkpoint"),
273 std::process::id(),
274 std::time::SystemTime::now()
275 .duration_since(std::time::UNIX_EPOCH)
276 .map(|d| d.as_nanos())
277 .unwrap_or(0),
278 ));
279 {
280 let _ = std::fs::remove_file(&staging);
285 let mut file = OpenOptions::new()
286 .create(true)
287 .write(true)
288 .truncate(true)
289 .open(&staging)?;
290 file.write_all(&body)?;
291 file.sync_all()?;
292 }
293 match std::fs::rename(&staging, path) {
294 Ok(()) => {}
295 Err(error) => {
296 let _ = std::fs::remove_file(&staging);
297 return Err(error);
298 }
299 }
300 if let Ok(dir) = OpenOptions::new().read(true).open(parent) {
301 let _ = dir.sync_all();
302 }
303 Ok(())
304 }
305
306 pub fn read_checkpoint(path: &Path, fingerprint: u64) -> io::Result<Self> {
310 let mut file = File::open(path)?;
311 let len = file.metadata()?.len();
312 const MAX_CHECKPOINT_BYTES: u64 = 64 * 1024 * 1024;
313 if len < 4 + 8 + 4 + FOOTER_LEN as u64 || len > MAX_CHECKPOINT_BYTES {
314 return Err(io::Error::new(
315 io::ErrorKind::InvalidData,
316 "run-lookup checkpoint length is out of range",
317 ));
318 }
319 let mut bytes = Vec::with_capacity(len as usize);
320 file.read_to_end(&mut bytes)?;
321 let split = bytes.len() - FOOTER_LEN;
322 let (body, footer) = bytes.split_at(split);
323 let expected = u32::from_le_bytes(footer.try_into().unwrap());
324 let actual = CRC32C.checksum(body);
325 if expected != actual {
326 return Err(io::Error::new(
327 io::ErrorKind::InvalidData,
328 "run-lookup checkpoint CRC mismatch",
329 ));
330 }
331 if body.len() < 4 + 8 + 4 {
332 return Err(io::Error::new(
333 io::ErrorKind::InvalidData,
334 "run-lookup checkpoint body too short",
335 ));
336 }
337 if body[..4] != LOOKUP_MAGIC {
338 return Err(io::Error::new(
339 io::ErrorKind::InvalidData,
340 "run-lookup checkpoint magic mismatch",
341 ));
342 }
343 let stored_fp = u64::from_le_bytes(body[4..12].try_into().unwrap());
344 if stored_fp != fingerprint {
345 return Err(io::Error::new(
346 io::ErrorKind::InvalidData,
347 "run-lookup checkpoint fingerprint does not match active run set",
348 ));
349 }
350 let key_count = u32::from_le_bytes(body[12..16].try_into().unwrap());
351 let mut cursor = &body[16..];
352 let mut postings: BTreeMap<RowId, RunLocatorList> = BTreeMap::new();
353 let mut prev: u64 = 0;
354 for _ in 0..key_count {
355 let (delta, consumed) = decode_varint(cursor)?;
356 cursor = &cursor[consumed..];
357 let cur = prev.checked_add(delta).ok_or_else(|| {
358 io::Error::new(
359 io::ErrorKind::InvalidData,
360 "run-lookup checkpoint varint overflows u64",
361 )
362 })?;
363 if cur < prev {
364 return Err(io::Error::new(
365 io::ErrorKind::InvalidData,
366 "run-lookup checkpoint row_id sequence is not monotonic",
367 ));
368 }
369 if cursor.len() < 4 {
370 return Err(io::Error::new(
371 io::ErrorKind::UnexpectedEof,
372 "run-lookup checkpoint truncates locator count",
373 ));
374 }
375 let locator_count = u32::from_le_bytes(cursor[..4].try_into().unwrap()) as usize;
376 cursor = &cursor[4..];
377 let mut list = RunLocatorList {
378 locators: Vec::with_capacity(locator_count),
379 };
380 for _ in 0..locator_count {
381 let need = 16 + 8 + 8 + 1 + 1 + 1;
386 if cursor.len() < need {
387 return Err(io::Error::new(
388 io::ErrorKind::UnexpectedEof,
389 "run-lookup checkpoint truncates locator fields",
390 ));
391 }
392 let run_id_bytes: [u8; 16] = cursor[..16].try_into().unwrap();
393 let run_id = u128::from_le_bytes(run_id_bytes);
394 cursor = &cursor[16..];
395 let min_epoch = u64::from_le_bytes(cursor[..8].try_into().unwrap());
396 cursor = &cursor[8..];
397 let max_epoch = u64::from_le_bytes(cursor[..8].try_into().unwrap());
398 cursor = &cursor[8..];
399 let (min_hlc, used) = read_optional_hlc(cursor)?;
400 cursor = &cursor[used..];
401 let (max_hlc, used) = read_optional_hlc(cursor)?;
402 cursor = &cursor[used..];
403 if cursor.is_empty() {
404 return Err(io::Error::new(
405 io::ErrorKind::UnexpectedEof,
406 "run-lookup checkpoint truncates unstamped flag",
407 ));
408 }
409 let contains_unstamped = cursor[0] != 0;
410 cursor = &cursor[1..];
411 list.locators.push(RunLocator {
412 run_id,
413 min_epoch: Epoch(min_epoch),
414 max_epoch: Epoch(max_epoch),
415 min_hlc,
416 max_hlc,
417 contains_unstamped_versions: contains_unstamped,
418 });
419 }
420 postings.insert(RowId(cur), list);
421 prev = cur;
422 }
423 if !cursor.is_empty() {
424 return Err(io::Error::new(
425 io::ErrorKind::InvalidData,
426 "run-lookup checkpoint has trailing bytes after declared key count",
427 ));
428 }
429 Ok(Self {
430 postings,
431 fingerprint: Some(fingerprint),
432 })
433 }
434
435 pub fn rebuild_from_runs(runs: &[RunRef], table: &Table) -> Result<Self> {
445 rebuild_from_runs_system_only(runs, table)
446 }
447
448 pub fn write_checkpoint_sharded(
454 &self,
455 base_dir: &Path,
456 max_shard_bytes: u64,
457 ) -> io::Result<()> {
458 let _ = self.fingerprint.ok_or_else(|| {
459 io::Error::new(
460 io::ErrorKind::InvalidData,
461 "run-lookup directory has no fingerprint; refusing to checkpoint",
462 )
463 })?;
464 clear_shards(base_dir)?;
466 let max = max_shard_bytes.max(1024 * 1024); let mut iter = self.postings.iter().peekable();
468 let mut shard_no: usize = 0;
469 while let Some((first_row_id, _)) = iter.peek() {
470 shard_no += 1;
471 let shard_start = first_row_id.0;
472 let mut shard_entries: Vec<(RowId, RunLocatorList)> = Vec::new();
476 let mut body_len: usize = 4 + 8 + 4; while let Some((_row_id, list)) = iter.peek() {
478 let entry_cost = 1 + 8 + 4 + list.locators.len()
481 * (16 + 8 + 8 + 1 + 1 + 16 + 16 + 1); if !shard_entries.is_empty() && body_len + entry_cost > max as usize {
483 break;
484 }
485 body_len += entry_cost;
486 let (rid, list) = iter.next().unwrap();
487 shard_entries.push((*rid, list.clone()));
488 }
489 let last = shard_entries
490 .last()
491 .map(|(rid, _)| rid.0)
492 .unwrap_or(shard_start);
493 let path = if iter.peek().is_some() {
494 directory_shard_path(base_dir, shard_start, Some(last))
495 } else {
496 directory_shard_path(base_dir, shard_start, None)
497 };
498 write_single_shard(&path, &shard_entries, self.fingerprint.unwrap())?;
499 }
500 if shard_no == 0 {
501 let path = directory_shard_path(base_dir, 0, None);
504 write_single_shard(&path, &[], self.fingerprint.unwrap())?;
505 }
506 if let Ok(dir) = OpenOptions::new().read(true).open(base_dir) {
508 let _ = dir.sync_all();
509 }
510 Ok(())
511 }
512
513 pub fn read_checkpoint_sharded(base_dir: &Path, fingerprint: u64) -> io::Result<Self> {
518 let mut paths: Vec<PathBuf> = Vec::new();
519 for entry in std::fs::read_dir(base_dir)? {
520 let entry = entry?;
521 let name = entry.file_name();
522 let Some(name) = name.to_str() else {
523 continue;
524 };
525 if name.starts_with(DIRECTORY_SHARD_PREFIX)
530 && (name.ends_with(DIRECTORY_SHARD_SUFFIX) || name.ends_with("-open"))
531 {
532 paths.push(entry.path());
533 }
534 }
535 paths.sort();
536 if paths.is_empty() {
537 return Err(io::Error::new(
538 io::ErrorKind::NotFound,
539 "no run-lookup directory shards present",
540 ));
541 }
542 let mut merged: BTreeMap<RowId, RunLocatorList> = BTreeMap::new();
543 for path in paths {
544 let shard = read_single_shard(&path, fingerprint)?;
545 for (rid, list) in shard {
546 merged.insert(rid, list);
547 }
548 }
549 Ok(Self {
550 postings: merged,
551 fingerprint: Some(fingerprint),
552 })
553 }
554}
555
556fn clear_shards(base_dir: &Path) -> io::Result<()> {
557 if !base_dir.exists() {
558 return Ok(());
559 }
560 for entry in std::fs::read_dir(base_dir)? {
561 let entry = entry?;
562 let name = entry.file_name();
563 let Some(name) = name.to_str() else {
564 continue;
565 };
566 if name.starts_with(DIRECTORY_SHARD_PREFIX)
567 && (name.ends_with(DIRECTORY_SHARD_SUFFIX) || name.ends_with("-open"))
568 {
569 let _ = std::fs::remove_file(entry.path());
570 }
571 }
572 Ok(())
573}
574
575fn write_single_shard(
576 path: &Path,
577 entries: &[(RowId, RunLocatorList)],
578 fingerprint: u64,
579) -> io::Result<()> {
580 let mut body: Vec<u8> = Vec::new();
581 body.extend_from_slice(&LOOKUP_MAGIC);
582 body.extend_from_slice(&fingerprint.to_le_bytes());
583 let key_count = entries.len() as u32;
584 body.extend_from_slice(&key_count.to_le_bytes());
585 let mut prev: u64 = 0;
586 for (row_id, list) in entries {
587 let cur = row_id.0;
588 let delta = cur.checked_sub(prev).ok_or_else(|| {
589 io::Error::new(
590 io::ErrorKind::InvalidData,
591 "run-lookup row_id sequence is not non-decreasing",
592 )
593 })?;
594 encode_varint(delta, &mut body);
595 let locator_count = list.locators.len() as u32;
596 body.extend_from_slice(&locator_count.to_le_bytes());
597 for loc in &list.locators {
598 body.extend_from_slice(&loc.run_id.to_le_bytes());
599 body.extend_from_slice(&loc.min_epoch.0.to_le_bytes());
600 body.extend_from_slice(&loc.max_epoch.0.to_le_bytes());
601 write_optional_hlc(loc.min_hlc, &mut body);
602 write_optional_hlc(loc.max_hlc, &mut body);
603 body.push(if loc.contains_unstamped_versions {
604 1
605 } else {
606 0
607 });
608 }
609 prev = cur;
610 }
611 let crc = CRC32C.checksum(&body);
612 body.extend_from_slice(&crc.to_le_bytes());
613
614 let parent = path.parent().unwrap_or_else(|| Path::new("."));
615 let staging = parent.join(format!(
616 ".{}.{}.{}.staging",
617 path.file_name()
618 .and_then(|name| name.to_str())
619 .unwrap_or("shard"),
620 std::process::id(),
621 std::time::SystemTime::now()
622 .duration_since(std::time::UNIX_EPOCH)
623 .map(|d| d.as_nanos())
624 .unwrap_or(0),
625 ));
626 {
627 let _ = std::fs::remove_file(&staging);
628 let mut file = OpenOptions::new()
629 .create(true)
630 .write(true)
631 .truncate(true)
632 .open(&staging)?;
633 file.write_all(&body)?;
634 file.sync_all()?;
635 }
636 match std::fs::rename(&staging, path) {
637 Ok(()) => {}
638 Err(error) => {
639 let _ = std::fs::remove_file(&staging);
640 return Err(error);
641 }
642 }
643 Ok(())
644}
645
646fn read_single_shard(path: &Path, fingerprint: u64) -> io::Result<BTreeMap<RowId, RunLocatorList>> {
647 let mut file = File::open(path)?;
648 let len = file.metadata()?.len();
649 if len < 4 + 8 + 4 + FOOTER_LEN as u64 || len > MAX_SHARD_BYTES * 4 {
652 return Err(io::Error::new(
653 io::ErrorKind::InvalidData,
654 "run-lookup shard length is out of range",
655 ));
656 }
657 let mut bytes = Vec::with_capacity(len as usize);
658 file.read_to_end(&mut bytes)?;
659 let split = bytes.len() - FOOTER_LEN;
660 let (body, footer) = bytes.split_at(split);
661 let expected = u32::from_le_bytes(footer.try_into().unwrap());
662 let actual = CRC32C.checksum(body);
663 if expected != actual {
664 return Err(io::Error::new(
665 io::ErrorKind::InvalidData,
666 "run-lookup shard CRC mismatch",
667 ));
668 }
669 if body.len() < 4 + 8 + 4 {
670 return Err(io::Error::new(
671 io::ErrorKind::InvalidData,
672 "run-lookup shard body too short",
673 ));
674 }
675 if body[..4] != LOOKUP_MAGIC {
676 return Err(io::Error::new(
677 io::ErrorKind::InvalidData,
678 "run-lookup shard magic mismatch",
679 ));
680 }
681 let stored_fp = u64::from_le_bytes(body[4..12].try_into().unwrap());
682 if stored_fp != fingerprint {
683 return Err(io::Error::new(
684 io::ErrorKind::InvalidData,
685 "run-lookup shard fingerprint does not match active run set",
686 ));
687 }
688 let key_count = u32::from_le_bytes(body[12..16].try_into().unwrap());
689 let mut cursor = &body[16..];
690 let mut postings: BTreeMap<RowId, RunLocatorList> = BTreeMap::new();
691 let mut prev: u64 = 0;
692 for _ in 0..key_count {
693 let (delta, consumed) = decode_varint(cursor)?;
694 cursor = &cursor[consumed..];
695 let cur = prev.checked_add(delta).ok_or_else(|| {
696 io::Error::new(
697 io::ErrorKind::InvalidData,
698 "run-lookup shard varint overflows u64",
699 )
700 })?;
701 if cur < prev {
702 return Err(io::Error::new(
703 io::ErrorKind::InvalidData,
704 "run-lookup shard row_id sequence is not monotonic",
705 ));
706 }
707 if cursor.len() < 4 {
708 return Err(io::Error::new(
709 io::ErrorKind::UnexpectedEof,
710 "run-lookup shard truncates locator count",
711 ));
712 }
713 let locator_count = u32::from_le_bytes(cursor[..4].try_into().unwrap()) as usize;
714 cursor = &cursor[4..];
715 let mut list = RunLocatorList {
716 locators: Vec::with_capacity(locator_count),
717 };
718 for _ in 0..locator_count {
719 let need = 16 + 8 + 8 + 1 + 1 + 1;
720 if cursor.len() < need {
721 return Err(io::Error::new(
722 io::ErrorKind::UnexpectedEof,
723 "run-lookup shard truncates locator fields",
724 ));
725 }
726 let run_id_bytes: [u8; 16] = cursor[..16].try_into().unwrap();
727 let run_id = u128::from_le_bytes(run_id_bytes);
728 cursor = &cursor[16..];
729 let min_epoch = u64::from_le_bytes(cursor[..8].try_into().unwrap());
730 cursor = &cursor[8..];
731 let max_epoch = u64::from_le_bytes(cursor[..8].try_into().unwrap());
732 cursor = &cursor[8..];
733 let (min_hlc, used) = read_optional_hlc(cursor)?;
734 cursor = &cursor[used..];
735 let (max_hlc, used) = read_optional_hlc(cursor)?;
736 cursor = &cursor[used..];
737 if cursor.is_empty() {
738 return Err(io::Error::new(
739 io::ErrorKind::UnexpectedEof,
740 "run-lookup shard truncates unstamped flag",
741 ));
742 }
743 let contains_unstamped = cursor[0] != 0;
744 cursor = &cursor[1..];
745 list.locators.push(RunLocator {
746 run_id,
747 min_epoch: Epoch(min_epoch),
748 max_epoch: Epoch(max_epoch),
749 min_hlc,
750 max_hlc,
751 contains_unstamped_versions: contains_unstamped,
752 });
753 }
754 postings.insert(RowId(cur), list);
755 prev = cur;
756 }
757 if !cursor.is_empty() {
758 return Err(io::Error::new(
759 io::ErrorKind::InvalidData,
760 "run-lookup shard has trailing bytes after declared key count",
761 ));
762 }
763 Ok(postings)
764}
765
766use std::path::PathBuf;
768
769fn encode_varint(mut value: u64, out: &mut Vec<u8>) {
770 loop {
771 let mut byte = (value & 0x7F) as u8;
772 value >>= 7;
773 if value != 0 {
774 byte |= 0x80;
775 }
776 out.push(byte);
777 if value == 0 {
778 break;
779 }
780 }
781}
782
783fn decode_varint(input: &[u8]) -> io::Result<(u64, usize)> {
784 let mut value: u64 = 0;
785 let mut shift: u32 = 0;
786 let mut consumed = 0;
787 for byte in input.iter().take(10) {
788 consumed += 1;
789 let low = (byte & 0x7F) as u64;
790 value |= low.checked_shl(shift).ok_or_else(|| {
791 io::Error::new(
792 io::ErrorKind::InvalidData,
793 "run-lookup varint overflows u64",
794 )
795 })?;
796 shift += 7;
797 if byte & 0x80 == 0 {
798 return Ok((value, consumed));
799 }
800 }
801 Err(io::Error::new(
802 io::ErrorKind::InvalidData,
803 "run-lookup varint is truncated or too long",
804 ))
805}
806
807fn write_optional_hlc(ts: Option<HlcTimestamp>, out: &mut Vec<u8>) {
808 match ts {
809 None => out.push(0),
810 Some(ts) => {
811 out.push(1);
812 out.extend_from_slice(&ts.physical_micros.to_le_bytes());
813 out.extend_from_slice(&ts.logical.to_le_bytes());
814 out.extend_from_slice(&ts.node_tiebreaker.to_le_bytes());
815 }
816 }
817}
818
819fn read_optional_hlc(input: &[u8]) -> io::Result<(Option<HlcTimestamp>, usize)> {
820 if input.is_empty() {
821 return Err(io::Error::new(
822 io::ErrorKind::UnexpectedEof,
823 "run-lookup checkpoint truncates hlc presence flag",
824 ));
825 }
826 let present = input[0] != 0;
827 if !present {
828 return Ok((None, 1));
829 }
830 if input.len() < 1 + HLC_BYTES {
831 return Err(io::Error::new(
832 io::ErrorKind::UnexpectedEof,
833 "run-lookup checkpoint truncates hlc body",
834 ));
835 }
836 let physical_micros = u64::from_le_bytes(input[1..9].try_into().unwrap());
837 let logical = u32::from_le_bytes(input[9..13].try_into().unwrap());
838 let node_tiebreaker = u32::from_le_bytes(input[13..17].try_into().unwrap());
839 Ok((
840 Some(HlcTimestamp {
841 physical_micros,
842 logical,
843 node_tiebreaker,
844 }),
845 1 + HLC_BYTES,
846 ))
847}
848
849fn locator_is_newer(newer: RunLocator, older: RunLocator) -> bool {
852 if newer.max_epoch > older.max_epoch {
853 return true;
854 }
855 if newer.max_epoch < older.max_epoch {
856 return false;
857 }
858 match (newer.max_hlc, older.max_hlc) {
859 (Some(a), Some(b)) => a > b,
860 (Some(_), None) => true,
861 (None, Some(_)) => false,
862 (None, None) => false,
863 }
864}
865
866impl RunLocator {
867 pub fn is_impossible_for(&self, snapshot: Snapshot) -> bool {
878 let stamped_possible = self.may_have_visible_stamped_version(snapshot);
879 let unstamped_possible = self.may_have_visible_unstamped_version(snapshot);
880
881 !stamped_possible && !unstamped_possible
882 }
883
884 fn may_have_visible_stamped_version(&self, snapshot: Snapshot) -> bool {
892 let Some(min_hlc) = self.min_hlc else {
893 return false;
894 };
895
896 if snapshot.uses_hlc_authority() {
897 min_hlc <= snapshot.commit_ts
898 } else {
899 self.min_epoch <= snapshot.epoch
900 }
901 }
902
903 fn may_have_visible_unstamped_version(&self, snapshot: Snapshot) -> bool {
907 self.contains_unstamped_versions && self.min_epoch <= snapshot.epoch
908 }
909
910 pub(crate) fn can_contain_version_newer_than(
927 &self,
928 best: VersionStamp,
929 snapshot: Snapshot,
930 ) -> bool {
931 if (self.min_hlc.is_none() || self.max_hlc.is_none()) && !self.contains_unstamped_versions {
936 return true;
937 }
938 self.stamped_version_may_beat(best, snapshot)
939 || self.unstamped_version_may_beat(best, snapshot)
940 }
941
942 fn stamped_version_may_beat(&self, best: VersionStamp, snapshot: Snapshot) -> bool {
958 let (Some(min_hlc), Some(max_hlc)) = (self.min_hlc, self.max_hlc) else {
959 return false;
960 };
961
962 match best.hlc {
963 Some(best_hlc) => {
964 if snapshot.uses_hlc_authority() {
965 max_hlc.min(snapshot.commit_ts) > best_hlc
966 } else {
967 self.min_epoch <= snapshot.epoch && max_hlc > best_hlc
968 }
969 }
970 None => {
971 if snapshot.uses_hlc_authority() {
972 min_hlc <= snapshot.commit_ts && self.max_epoch > best.epoch
973 } else {
974 self.max_epoch.min(snapshot.epoch) > best.epoch
975 }
976 }
977 }
978 }
979
980 fn unstamped_version_may_beat(&self, best: VersionStamp, snapshot: Snapshot) -> bool {
987 self.contains_unstamped_versions && self.max_epoch.min(snapshot.epoch) > best.epoch
988 }
989}
990
991pub fn compute_fingerprint(
997 run_ids_ordered: &[u128],
998 schema_id: u64,
999 index_generation: u64,
1000 run_generation: u64,
1001 format_version: u32,
1002) -> u64 {
1003 let mut buf: Vec<u8> = Vec::with_capacity(8 + 8 + 8 + 4 + run_ids_ordered.len() * (16 + 2));
1004 buf.extend_from_slice(&schema_id.to_le_bytes());
1005 buf.extend_from_slice(&index_generation.to_le_bytes());
1006 buf.extend_from_slice(&run_generation.to_le_bytes());
1007 buf.extend_from_slice(&format_version.to_le_bytes());
1008 for run in run_ids_ordered {
1012 let run_id_bytes = run.to_le_bytes();
1013 buf.extend_from_slice(&run_id_bytes);
1014 }
1015 xxhash_rust::xxh3::xxh3_64_with_seed(&buf, 0x9E37_79B1_854A_0001)
1017}
1018
1019pub fn directory_shard_path(
1023 base_dir: &Path,
1024 start: u64,
1025 end_inclusive: Option<u64>,
1026) -> std::path::PathBuf {
1027 match end_inclusive {
1028 Some(end) => base_dir.join(format!(
1029 "{DIRECTORY_SHARD_PREFIX}{start:020}-{end:020}{DIRECTORY_SHARD_SUFFIX}"
1030 )),
1031 None => base_dir.join(format!("{DIRECTORY_SHARD_PREFIX}{start:020}-open")),
1032 }
1033}
1034
1035pub fn rebuild_from_runs_system_only(runs: &[RunRef], table: &Table) -> Result<RunLookupDirectory> {
1042 let mut dir = RunLookupDirectory::empty();
1043 for run_ref in runs {
1044 let mut reader = table.open_reader(run_ref.run_id)?;
1045 type RunRowAgg = (
1048 Epoch,
1049 Epoch,
1050 Option<HlcTimestamp>,
1051 Option<HlcTimestamp>,
1052 bool,
1053 );
1054 let mut per_row: BTreeMap<RowId, RunRowAgg> = BTreeMap::new();
1055 reader.for_each_system(|row_id, committed_epoch, commit_ts, deleted| {
1056 let entry = per_row.entry(row_id).or_insert((
1057 committed_epoch,
1058 committed_epoch,
1059 commit_ts,
1060 commit_ts,
1061 commit_ts.is_none(),
1062 ));
1063 if committed_epoch < entry.0 {
1064 entry.0 = committed_epoch;
1065 }
1066 if committed_epoch > entry.1 {
1067 entry.1 = committed_epoch;
1068 }
1069 match (commit_ts, entry.2) {
1070 (Some(ts), Some(prev)) if ts < prev => entry.2 = Some(ts),
1071 (Some(_), None) => entry.2 = commit_ts,
1072 _ => {}
1073 }
1074 match (commit_ts, entry.3) {
1075 (Some(ts), Some(prev)) if ts > prev => entry.3 = Some(ts),
1076 (Some(_), None) => entry.3 = commit_ts,
1077 _ => {}
1078 }
1079 if commit_ts.is_none() {
1080 entry.4 = true;
1081 }
1082 let _ = deleted;
1088 Ok(())
1089 })?;
1090 for (row_id, (min_epoch, max_epoch, min_hlc, max_hlc, contains_unstamped)) in per_row {
1091 dir.insert(
1092 row_id,
1093 RunLocator {
1094 run_id: run_ref.run_id,
1095 min_epoch,
1096 max_epoch,
1097 min_hlc,
1098 max_hlc,
1099 contains_unstamped_versions: contains_unstamped,
1100 },
1101 );
1102 }
1103 }
1104 Ok(dir)
1105}
1106
1107pub const DIRECTORY_FORMAT_VERSION: u32 = 1;
1110pub const MAX_SHARD_BYTES: u64 = 16 * 1024 * 1024;
1114
1115#[cfg(test)]
1116mod tests {
1117 use super::*;
1118 use tempfile::tempdir;
1119
1120 fn hlc_from_phys(physical_micros: u64) -> HlcTimestamp {
1121 HlcTimestamp {
1122 physical_micros,
1123 logical: 0,
1124 node_tiebreaker: 0,
1125 }
1126 }
1127
1128 fn loc(run_id: u128, epoch: u64, hlc: Option<u64>) -> RunLocator {
1129 RunLocator {
1130 run_id,
1131 min_epoch: Epoch(epoch),
1132 max_epoch: Epoch(epoch),
1133 min_hlc: hlc.map(hlc_from_phys),
1134 max_hlc: hlc.map(hlc_from_phys),
1135 contains_unstamped_versions: hlc.is_none(),
1136 }
1137 }
1138
1139 #[test]
1140 fn empty_directory_returns_empty_list() {
1141 let dir = RunLookupDirectory::empty();
1142 assert!(dir.locate(RowId(42)).is_empty());
1143 assert_eq!(dir.key_count(), 0);
1144 }
1145
1146 #[test]
1147 fn insert_orders_locators_newest_first() {
1148 let mut dir = RunLookupDirectory::empty();
1149 dir.insert(RowId(1), loc(0xA, 1, None));
1150 dir.insert(RowId(1), loc(0xB, 3, None));
1151 dir.insert(RowId(1), loc(0xC, 2, None));
1152 let list = dir.locate(RowId(1));
1153 let ids: Vec<u128> = list.locators.iter().map(|l| l.run_id).collect();
1154 assert_eq!(ids, vec![0xB, 0xC, 0xA]);
1155 }
1156
1157 #[test]
1158 fn retain_active_runs_drops_stale_locators() {
1159 let mut dir = RunLookupDirectory::empty();
1160 dir.insert(RowId(1), loc(10, 1, None));
1161 dir.insert(RowId(1), loc(20, 2, None));
1162 dir.insert(RowId(2), loc(20, 1, None));
1163 let removed = dir.retain_active_runs(&[10]);
1164 assert_eq!(removed, 2);
1165 assert!(dir.locate(RowId(1)).locators.iter().all(|l| l.run_id == 10));
1166 assert!(dir.locate(RowId(2)).is_empty());
1167 }
1168
1169 #[test]
1170 fn hlc_tie_break_resolves_above_epoch_only() {
1171 let mut dir = RunLookupDirectory::empty();
1172 dir.insert(RowId(1), loc(0xA, 5, None));
1173 dir.insert(RowId(1), loc(0xB, 5, Some(100)));
1174 let list = dir.locate(RowId(1));
1175 assert_eq!(list.locators[0].run_id, 0xB, "HLC newer wins on tie");
1176 }
1177
1178 #[test]
1179 fn memory_metrics_report_postings() {
1180 let mut dir = RunLookupDirectory::empty();
1181 dir.insert(RowId(1), loc(1, 1, None));
1182 dir.insert(RowId(1), loc(2, 2, None));
1183 dir.insert(RowId(2), loc(1, 1, None));
1184 assert_eq!(dir.total_locators(), 3);
1185 assert_eq!(dir.key_count(), 2);
1186 assert_eq!(dir.max_locators_per_row(), 2);
1187 assert!((dir.avg_locators_per_row() - 1.5).abs() < 1e-9);
1188 }
1189
1190 #[test]
1191 fn checkpoint_roundtrip_preserves_all_locators() {
1192 let dir = tempdir().unwrap();
1193 let mut src = RunLookupDirectory::empty();
1194 src.set_fingerprint(0x0102_0304_0506_0708);
1195 src.insert(RowId(7), loc(0x11, 1, None));
1196 src.insert(RowId(7), loc(0x22, 5, Some(100)));
1197 src.insert(RowId(8), loc(0x33, 3, Some(50)));
1198 src.insert(RowId(1_000_000), loc(0x44, 9, None));
1199 let path = dir.path().join("lookup.ckpt");
1200 src.write_checkpoint(&path).unwrap();
1201
1202 let restored = RunLookupDirectory::read_checkpoint(&path, 0x0102_0304_0506_0708).unwrap();
1203 assert_eq!(restored.fingerprint(), Some(0x0102_0304_0506_0708));
1204 assert_eq!(restored.key_count(), 3);
1205 assert_eq!(restored.total_locators(), 4);
1206 for (row_id, expected) in [
1207 (RowId(7), vec![0x22, 0x11]),
1208 (RowId(8), vec![0x33]),
1209 (RowId(1_000_000), vec![0x44]),
1210 ] {
1211 let actual: Vec<u128> = restored
1212 .locate(row_id)
1213 .locators
1214 .iter()
1215 .map(|l| l.run_id)
1216 .collect();
1217 assert_eq!(actual, expected, "row_id {row_id} locator order");
1218 }
1219 let list7 = restored.locate(RowId(7));
1220 assert_eq!(
1221 list7.locators[0].min_hlc.map(|t| t.physical_micros),
1222 Some(100)
1223 );
1224 assert!(list7.locators[1].contains_unstamped_versions);
1225 }
1226
1227 #[test]
1228 fn read_checkpoint_rejects_mismatched_fingerprint() {
1229 let dir = tempdir().unwrap();
1230 let mut src = RunLookupDirectory::empty();
1231 src.set_fingerprint(42);
1232 src.insert(RowId(1), loc(7, 1, None));
1233 let path = dir.path().join("lookup.ckpt");
1234 src.write_checkpoint(&path).unwrap();
1235 let err = RunLookupDirectory::read_checkpoint(&path, 43).unwrap_err();
1236 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
1237 assert!(err.to_string().contains("fingerprint"));
1238 }
1239
1240 fn stamped_loc(run_id: u128, epoch: u64, hlc: u64) -> RunLocator {
1241 RunLocator {
1242 run_id,
1243 min_epoch: Epoch(epoch),
1244 max_epoch: Epoch(epoch),
1245 min_hlc: Some(hlc_from_phys(hlc)),
1246 max_hlc: Some(hlc_from_phys(hlc)),
1247 contains_unstamped_versions: false,
1248 }
1249 }
1250
1251 fn mixed_loc(run_id: u128, epoch: u64, hlc: u64) -> RunLocator {
1252 RunLocator {
1253 run_id,
1254 min_epoch: Epoch(epoch),
1255 max_epoch: Epoch(epoch),
1256 min_hlc: Some(hlc_from_phys(hlc)),
1257 max_hlc: Some(hlc_from_phys(hlc)),
1258 contains_unstamped_versions: true,
1259 }
1260 }
1261
1262 #[test]
1263 fn decide_distinguishes_complete_miss_from_candidates() {
1264 let mut dir = RunLookupDirectory::empty();
1265 match dir.decide(RowId(99)) {
1267 DirectoryLookupDecision::CompleteMiss => {}
1268 other => panic!("expected CompleteMiss, got {other:?}"),
1269 }
1270 dir.insert(RowId(7), stamped_loc(1, 1, 100));
1273 dir.insert(RowId(7), stamped_loc(2, 2, 200));
1274 match dir.decide(RowId(7)) {
1275 DirectoryLookupDecision::Candidates(list) => assert_eq!(list.len(), 2),
1276 other => panic!("expected Candidates, got {other:?}"),
1277 }
1278 }
1279
1280 #[test]
1281 fn early_stop_epoch_only_proves_no_beat() {
1282 let l = loc(1, 5, None);
1284 let snap = Snapshot::at(Epoch(20));
1285 let best = VersionStamp {
1286 epoch: Epoch(10),
1287 hlc: None,
1288 };
1289 assert!(
1290 !l.can_contain_version_newer_than(best, snap),
1291 "max_epoch <= best.epoch must early-stop"
1292 );
1293 }
1294
1295 #[test]
1296 fn early_stop_epoch_only_proves_beat() {
1297 let l = loc(1, 15, None);
1299 let snap = Snapshot::at(Epoch(20));
1300 let best = VersionStamp {
1301 epoch: Epoch(10),
1302 hlc: None,
1303 };
1304 assert!(
1305 l.can_contain_version_newer_than(best, snap),
1306 "max_epoch > best.epoch must not early-stop"
1307 );
1308 }
1309
1310 #[test]
1311 fn early_stop_pure_hlc_proves_no_beat() {
1312 let l = stamped_loc(1, 5, 100);
1315 let snap = Snapshot::at_hlc(Epoch(20), hlc_from_phys(1000));
1316 let best = VersionStamp {
1317 epoch: Epoch(5),
1318 hlc: Some(hlc_from_phys(200)),
1319 };
1320 assert!(
1321 !l.can_contain_version_newer_than(best, snap),
1322 "pure HLC: max visible HLC <= best HLC must early-stop"
1323 );
1324 }
1325
1326 #[test]
1327 fn early_stop_pure_hlc_proves_beat() {
1328 let l = stamped_loc(1, 5, 500);
1331 let snap = Snapshot::at_hlc(Epoch(20), hlc_from_phys(1000));
1332 let best = VersionStamp {
1333 epoch: Epoch(5),
1334 hlc: Some(hlc_from_phys(200)),
1335 };
1336 assert!(
1337 l.can_contain_version_newer_than(best, snap),
1338 "pure HLC: max visible HLC > best HLC must not early-stop"
1339 );
1340 }
1341
1342 #[test]
1343 fn early_stop_pure_hlc_respects_snapshot_cap() {
1344 let l = stamped_loc(1, 5, 5000);
1350 let snap = Snapshot::at_hlc(Epoch(20), hlc_from_phys(100));
1351 let best = VersionStamp {
1352 epoch: Epoch(5),
1353 hlc: Some(hlc_from_phys(200)),
1354 };
1355 assert!(
1356 !l.can_contain_version_newer_than(best, snap),
1357 "snapshot HLC cap below locator max must early-stop when best > cap"
1358 );
1359 }
1360
1361 #[test]
1362 fn early_stop_mixed_with_unstamped_falls_back_to_epoch() {
1363 let l = mixed_loc(1, 250, 100);
1368 let snap = Snapshot::at_hlc(Epoch(300), hlc_from_phys(1000));
1369 let best = VersionStamp {
1370 epoch: Epoch(200),
1371 hlc: None,
1372 };
1373 assert!(
1374 l.can_contain_version_newer_than(best, snap),
1375 "mixed: unstamped epoch beat path must not early-stop"
1376 );
1377 let l = mixed_loc(1, 100, 5000);
1379 let best = VersionStamp {
1380 epoch: Epoch(200),
1381 hlc: None,
1382 };
1383 assert!(
1384 !l.can_contain_version_newer_than(best, snap),
1385 "mixed: max epoch <= best.epoch must early-stop"
1386 );
1387 }
1388
1389 #[test]
1390 fn early_stop_hlc_snap_with_unstamped_best_uses_epoch_path() {
1391 let l = stamped_loc(1, 250, 999);
1394 let snap = Snapshot::at_hlc(Epoch(300), hlc_from_phys(1000));
1395 let best = VersionStamp {
1396 epoch: Epoch(200),
1397 hlc: None,
1398 };
1399 assert!(
1400 l.can_contain_version_newer_than(best, snap),
1401 "unstamped best: epoch path wins even under HLC-authority snap"
1402 );
1403 }
1404
1405 #[test]
1406 fn early_stop_unknown_metadata_stays_conservative() {
1407 let l = RunLocator {
1413 run_id: 1,
1414 min_epoch: Epoch(10),
1415 max_epoch: Epoch(10),
1416 min_hlc: Some(hlc_from_phys(50)),
1417 max_hlc: Some(hlc_from_phys(50)),
1418 contains_unstamped_versions: false,
1419 };
1420 let snap = Snapshot::at_hlc(Epoch(20), hlc_from_phys(100));
1421 let best = VersionStamp {
1422 epoch: Epoch(10),
1423 hlc: Some(hlc_from_phys(50)),
1424 };
1425 assert!(
1428 !l.can_contain_version_newer_than(best, snap),
1429 "strict tie (equal epoch+HLC) is not a beat; safe to early-stop"
1430 );
1431
1432 let l_unknown = RunLocator {
1437 run_id: 2,
1438 min_epoch: Epoch(10),
1439 max_epoch: Epoch(10),
1440 min_hlc: None,
1441 max_hlc: None,
1442 contains_unstamped_versions: false,
1443 };
1444 let best_lower = VersionStamp {
1445 epoch: Epoch(5),
1446 hlc: Some(hlc_from_phys(50)),
1447 };
1448 assert!(
1449 l_unknown.can_contain_version_newer_than(best_lower, snap),
1450 "unknown metadata with higher possible epoch must stay conservative"
1451 );
1452 }
1453
1454 #[test]
1459 fn locator_spanning_hlc_snapshot_is_not_impossible() {
1460 let locator = RunLocator {
1463 run_id: 1,
1464 min_epoch: Epoch(10),
1465 max_epoch: Epoch(20),
1466 min_hlc: Some(hlc_from_phys(100)),
1467 max_hlc: Some(hlc_from_phys(1_000)),
1468 contains_unstamped_versions: false,
1469 };
1470 let snapshot = Snapshot::at_hlc(Epoch(15), hlc_from_phys(500));
1471 assert!(
1472 !locator.is_impossible_for(snapshot),
1473 "min_hlc 100 <= snapshot 500: the run spans the snapshot and must be opened"
1474 );
1475 }
1476
1477 #[test]
1478 fn locator_entirely_after_hlc_snapshot_is_impossible() {
1479 let locator = RunLocator {
1480 run_id: 1,
1481 min_epoch: Epoch(10),
1482 max_epoch: Epoch(20),
1483 min_hlc: Some(hlc_from_phys(600)),
1484 max_hlc: Some(hlc_from_phys(1_000)),
1485 contains_unstamped_versions: false,
1486 };
1487 let snapshot = Snapshot::at_hlc(Epoch(15), hlc_from_phys(500));
1488 assert!(
1489 locator.is_impossible_for(snapshot),
1490 "min_hlc 600 > snapshot 500: every stamped version is too new"
1491 );
1492 }
1493
1494 #[test]
1495 fn locator_entirely_before_hlc_snapshot_is_possible() {
1496 let locator = RunLocator {
1497 run_id: 1,
1498 min_epoch: Epoch(10),
1499 max_epoch: Epoch(20),
1500 min_hlc: Some(hlc_from_phys(100)),
1501 max_hlc: Some(hlc_from_phys(200)),
1502 contains_unstamped_versions: false,
1503 };
1504 let snapshot = Snapshot::at_hlc(Epoch(15), hlc_from_phys(500));
1505 assert!(
1506 !locator.is_impossible_for(snapshot),
1507 "locator entirely below the snapshot is obviously possible"
1508 );
1509 }
1510
1511 #[test]
1512 fn mixed_locator_is_impossible_only_when_both_paths_are_excluded() {
1513 let stamped_side_open = RunLocator {
1516 run_id: 1,
1517 min_epoch: Epoch(60),
1518 max_epoch: Epoch(70),
1519 min_hlc: Some(hlc_from_phys(100)),
1520 max_hlc: Some(hlc_from_phys(900)),
1521 contains_unstamped_versions: true,
1522 };
1523 let snap = Snapshot::at_hlc(Epoch(50), hlc_from_phys(500));
1524 assert!(
1525 !stamped_side_open.is_impossible_for(snap),
1526 "stamped min_hlc 100 <= 500 keeps the mixed locator possible"
1527 );
1528 let unstamped_side_open = RunLocator {
1531 run_id: 2,
1532 min_epoch: Epoch(40),
1533 max_epoch: Epoch(70),
1534 min_hlc: Some(hlc_from_phys(600)),
1535 max_hlc: Some(hlc_from_phys(900)),
1536 contains_unstamped_versions: true,
1537 };
1538 assert!(
1539 !unstamped_side_open.is_impossible_for(snap),
1540 "unstamped min_epoch 40 <= 50 keeps the mixed locator possible"
1541 );
1542 let fully_after = RunLocator {
1545 run_id: 3,
1546 min_epoch: Epoch(60),
1547 max_epoch: Epoch(70),
1548 min_hlc: Some(hlc_from_phys(600)),
1549 max_hlc: Some(hlc_from_phys(900)),
1550 contains_unstamped_versions: true,
1551 };
1552 assert!(
1553 fully_after.is_impossible_for(snap),
1554 "every stamped version is HLC-invisible and every unstamped version is epoch-invisible"
1555 );
1556 }
1557
1558 #[test]
1567 fn mixed_locator_stamped_candidate_can_beat_stamped_best_by_hlc() {
1568 let locator = RunLocator {
1569 run_id: 1,
1570 min_epoch: Epoch(40),
1571 max_epoch: Epoch(50),
1572 min_hlc: Some(hlc_from_phys(300)),
1573 max_hlc: Some(hlc_from_phys(300)),
1574 contains_unstamped_versions: true,
1575 };
1576 let snapshot = Snapshot::at_hlc(Epoch(200), hlc_from_phys(400));
1577 let best = VersionStamp {
1578 epoch: Epoch(100),
1579 hlc: Some(hlc_from_phys(200)),
1580 };
1581 assert!(
1582 locator.can_contain_version_newer_than(best, snapshot),
1583 "stamped candidate HLC 300 > best HLC 200; the locator must be opened"
1584 );
1585 }
1586
1587 #[test]
1591 fn epoch_snapshot_stamped_candidate_can_beat_stamped_best_by_hlc() {
1592 let locator = stamped_loc(1, 50, 300);
1593 let snapshot = Snapshot::at(Epoch(200));
1594 let best = VersionStamp {
1595 epoch: Epoch(100),
1596 hlc: Some(hlc_from_phys(200)),
1597 };
1598 assert!(
1599 locator.can_contain_version_newer_than(best, snapshot),
1600 "stamped vs stamped recency is HLC-based even under an epoch-only snapshot"
1601 );
1602 let older = stamped_loc(2, 50, 150);
1604 assert!(
1605 !older.can_contain_version_newer_than(best, snapshot),
1606 "max_hlc 150 <= best HLC 200 and no unstamped rows: provably cannot beat"
1607 );
1608 let future = RunLocator {
1610 run_id: 3,
1611 min_epoch: Epoch(300),
1612 max_epoch: Epoch(400),
1613 min_hlc: Some(hlc_from_phys(300)),
1614 max_hlc: Some(hlc_from_phys(300)),
1615 contains_unstamped_versions: false,
1616 };
1617 assert!(
1618 !future.can_contain_version_newer_than(best, snapshot),
1619 "min_epoch 300 > snapshot epoch 200: no stamped row is epoch-visible"
1620 );
1621 }
1622
1623 #[test]
1627 fn hlc_snapshot_stamped_candidate_can_beat_unstamped_best_by_epoch() {
1628 let locator = stamped_loc(1, 500, 200);
1629 let snapshot = Snapshot::at_hlc(Epoch(300), hlc_from_phys(300));
1630 let best = VersionStamp {
1631 epoch: Epoch(400),
1632 hlc: None,
1633 };
1634 assert!(
1635 locator.can_contain_version_newer_than(best, snapshot),
1636 "candidate HLC 200 <= snapshot 300 and epoch 500 > best epoch 400: must open"
1637 );
1638 let invisible = stamped_loc(2, 500, 400);
1640 assert!(
1641 !invisible.can_contain_version_newer_than(best, snapshot),
1642 "min_hlc 400 > snapshot 300: no stamped version is visible"
1643 );
1644 let older = stamped_loc(3, 350, 200);
1646 assert!(
1647 !older.can_contain_version_newer_than(best, snapshot),
1648 "max_epoch 350 <= best epoch 400: provably cannot beat"
1649 );
1650 }
1651
1652 #[test]
1655 fn unstamped_candidate_uses_epoch_visibility_under_hlc_snapshot() {
1656 let locator = loc(1, 250, None); let snapshot = Snapshot::at_hlc(Epoch(300), hlc_from_phys(1_000));
1658 let best = VersionStamp {
1659 epoch: Epoch(200),
1660 hlc: Some(hlc_from_phys(50)),
1661 };
1662 assert!(
1663 locator.can_contain_version_newer_than(best, snapshot),
1664 "unstamped epoch 250 > best epoch 200 and visible at snapshot epoch 300"
1665 );
1666 let tight_snap = Snapshot::at_hlc(Epoch(240), hlc_from_phys(1_000));
1668 let tight_best = VersionStamp {
1669 epoch: Epoch(240),
1670 hlc: Some(hlc_from_phys(50)),
1671 };
1672 assert!(
1673 !locator.can_contain_version_newer_than(tight_best, tight_snap),
1674 "min(250, 240) = 240 <= best epoch 240: no visible unstamped version can beat"
1675 );
1676 let epoch_snap = Snapshot::at(Epoch(300));
1678 assert!(
1679 locator.can_contain_version_newer_than(best, epoch_snap),
1680 "unstamped epoch comparison does not depend on the snapshot mode"
1681 );
1682 }
1683
1684 #[test]
1687 fn unknown_metadata_is_conservative() {
1688 let best = VersionStamp {
1689 epoch: Epoch(5),
1690 hlc: Some(hlc_from_phys(50)),
1691 };
1692 let hlc_snap = Snapshot::at_hlc(Epoch(20), hlc_from_phys(100));
1693 let epoch_snap = Snapshot::at(Epoch(20));
1694 let no_envelope = RunLocator {
1696 run_id: 1,
1697 min_epoch: Epoch(10),
1698 max_epoch: Epoch(10),
1699 min_hlc: None,
1700 max_hlc: None,
1701 contains_unstamped_versions: false,
1702 };
1703 for snap in [hlc_snap, epoch_snap] {
1704 assert!(
1705 no_envelope.can_contain_version_newer_than(best, snap),
1706 "missing HLC envelope without the unstamped flag is unknown: stay conservative"
1707 );
1708 }
1709 let partial = RunLocator {
1711 min_hlc: Some(hlc_from_phys(10)),
1712 ..no_envelope
1713 };
1714 assert!(
1715 partial.can_contain_version_newer_than(best, hlc_snap),
1716 "a partial HLC envelope cannot prove impossibility: stay conservative"
1717 );
1718 }
1719
1720 #[test]
1723 fn equal_authority_does_not_count_as_strictly_newer() {
1724 let hlc_snap = Snapshot::at_hlc(Epoch(20), hlc_from_phys(100));
1725 let stamped = stamped_loc(1, 10, 50);
1727 let stamped_best = VersionStamp {
1728 epoch: Epoch(10),
1729 hlc: Some(hlc_from_phys(50)),
1730 };
1731 assert!(
1732 !stamped.can_contain_version_newer_than(stamped_best, hlc_snap),
1733 "equal HLC is not strictly newer"
1734 );
1735 let unstamped = loc(2, 10, None);
1737 let unstamped_best = VersionStamp {
1738 epoch: Epoch(10),
1739 hlc: None,
1740 };
1741 assert!(
1742 !unstamped.can_contain_version_newer_than(unstamped_best, hlc_snap),
1743 "equal epoch is not strictly newer"
1744 );
1745 let epoch_snap = Snapshot::at(Epoch(20));
1748 assert!(
1749 !stamped.can_contain_version_newer_than(unstamped_best, epoch_snap),
1750 "equal epoch is not strictly newer on the stamped-vs-unstamped path"
1751 );
1752 assert!(
1754 !unstamped.can_contain_version_newer_than(stamped_best, epoch_snap),
1755 "equal epoch is not strictly newer on the unstamped path"
1756 );
1757 }
1758}