Skip to main content

grafeo_common/
mvcc.rs

1//! MVCC (Multi-Version Concurrency Control) primitives.
2//!
3//! This is how Grafeo handles concurrent reads and writes without blocking.
4//! Each entity has a [`VersionChain`] that tracks all versions. Readers see
5//! consistent snapshots, writers create new versions, and old versions get
6//! garbage collected when no one needs them anymore.
7
8use std::collections::VecDeque;
9
10#[cfg(feature = "tiered-storage")]
11use smallvec::SmallVec;
12
13use crate::types::{EpochId, TransactionId};
14
15/// Tracks when a version was created and deleted for visibility checks.
16#[derive(Debug, Clone, Copy)]
17pub struct VersionInfo {
18    /// The epoch this version was created in.
19    pub created_epoch: EpochId,
20    /// The epoch this version was deleted in (if any).
21    pub deleted_epoch: Option<EpochId>,
22    /// The transaction that created this version.
23    pub created_by: TransactionId,
24    /// The transaction that deleted this version (if any).
25    /// Used for rollback: only the deleting transaction's rollback can undelete.
26    pub deleted_by: Option<TransactionId>,
27}
28
29impl VersionInfo {
30    /// Creates a new version info.
31    #[must_use]
32    pub fn new(created_epoch: EpochId, created_by: TransactionId) -> Self {
33        Self {
34            created_epoch,
35            deleted_epoch: None,
36            created_by,
37            deleted_by: None,
38        }
39    }
40
41    /// Marks this version as deleted by a specific transaction.
42    pub fn mark_deleted(&mut self, epoch: EpochId, deleted_by: TransactionId) {
43        self.deleted_epoch = Some(epoch);
44        self.deleted_by = Some(deleted_by);
45    }
46
47    /// Unmarks deletion if deleted by the given transaction. Returns `true` if undeleted.
48    ///
49    /// Used during rollback to restore versions deleted within the rolled-back transaction.
50    pub fn unmark_deleted_by(&mut self, transaction_id: TransactionId) -> bool {
51        if self.deleted_by == Some(transaction_id) {
52            self.deleted_epoch = None;
53            self.deleted_by = None;
54            true
55        } else {
56            false
57        }
58    }
59
60    /// Checks if this version is visible at the given epoch.
61    #[inline]
62    #[must_use]
63    pub fn is_visible_at(&self, epoch: EpochId) -> bool {
64        // Visible if created before or at the viewing epoch
65        // and not deleted before the viewing epoch
66        if !self.created_epoch.is_visible_at(epoch) {
67            return false;
68        }
69
70        if let Some(deleted) = self.deleted_epoch {
71            // Not visible if deleted at or before the viewing epoch
72            deleted.as_u64() > epoch.as_u64()
73        } else {
74            true
75        }
76    }
77
78    /// Checks if this version is visible to a specific transaction.
79    ///
80    /// A version is visible to a transaction if:
81    /// 1. It was created by the same transaction and not deleted by self, OR
82    /// 2. It was created in an epoch before the transaction's start epoch
83    ///    and not deleted before that epoch, and not deleted by this transaction
84    #[inline]
85    #[must_use]
86    pub fn is_visible_to(&self, viewing_epoch: EpochId, viewing_tx: TransactionId) -> bool {
87        // If this version was deleted by the viewing transaction, it is not visible
88        if self.deleted_by == Some(viewing_tx) {
89            return false;
90        }
91
92        // Own modifications are always visible (if not deleted)
93        if self.created_by == viewing_tx {
94            return self.deleted_epoch.is_none();
95        }
96
97        // Otherwise, use epoch-based visibility
98        self.is_visible_at(viewing_epoch)
99    }
100}
101
102/// A single version of data.
103#[derive(Debug, Clone)]
104pub struct Version<T> {
105    /// Visibility metadata.
106    pub info: VersionInfo,
107    /// The actual data.
108    pub data: T,
109}
110
111impl<T> Version<T> {
112    /// Creates a new version.
113    #[must_use]
114    pub fn new(data: T, created_epoch: EpochId, created_by: TransactionId) -> Self {
115        Self {
116            info: VersionInfo::new(created_epoch, created_by),
117            data,
118        }
119    }
120}
121
122/// All versions of a single entity, newest first.
123///
124/// Each node/edge has one of these tracking its version history. Use
125/// [`visible_at()`](Self::visible_at) to get the version at a specific epoch,
126/// or [`visible_to()`](Self::visible_to) for transaction-aware visibility.
127#[derive(Debug, Clone)]
128pub struct VersionChain<T> {
129    /// Versions ordered newest-first.
130    versions: VecDeque<Version<T>>,
131}
132
133impl<T> VersionChain<T> {
134    /// Creates a new empty version chain.
135    #[must_use]
136    pub fn new() -> Self {
137        Self {
138            versions: VecDeque::new(),
139        }
140    }
141
142    /// Creates a version chain with an initial version.
143    #[must_use]
144    pub fn with_initial(data: T, created_epoch: EpochId, created_by: TransactionId) -> Self {
145        let mut chain = Self::new();
146        chain.add_version(data, created_epoch, created_by);
147        chain
148    }
149
150    /// Adds a new version to the chain.
151    ///
152    /// The new version becomes the head of the chain.
153    pub fn add_version(&mut self, data: T, created_epoch: EpochId, created_by: TransactionId) {
154        let version = Version::new(data, created_epoch, created_by);
155        self.versions.push_front(version);
156    }
157
158    /// Finds the version visible at the given epoch.
159    ///
160    /// Returns a reference to the visible version's data, or `None` if no version
161    /// is visible at that epoch.
162    #[inline]
163    #[must_use]
164    pub fn visible_at(&self, epoch: EpochId) -> Option<&T> {
165        self.versions
166            .iter()
167            .find(|v| v.info.is_visible_at(epoch))
168            .map(|v| &v.data)
169    }
170
171    /// Finds the version visible to a specific transaction.
172    ///
173    /// This considers both the transaction's epoch and its own uncommitted changes.
174    #[inline]
175    #[must_use]
176    pub fn visible_to(&self, epoch: EpochId, tx: TransactionId) -> Option<&T> {
177        self.versions
178            .iter()
179            .find(|v| v.info.is_visible_to(epoch, tx))
180            .map(|v| &v.data)
181    }
182
183    /// Marks the current visible version as deleted by a specific transaction.
184    ///
185    /// Returns `true` if a version was marked, `false` if no visible version exists.
186    pub fn mark_deleted(&mut self, delete_epoch: EpochId, deleted_by: TransactionId) -> bool {
187        for version in &mut self.versions {
188            if version.info.deleted_epoch.is_none() {
189                version.info.mark_deleted(delete_epoch, deleted_by);
190                return true;
191            }
192        }
193        false
194    }
195
196    /// Unmarks deletion for versions deleted by the given transaction.
197    ///
198    /// Returns `true` if any version was undeleted. Used during rollback to
199    /// restore versions that were deleted within the rolled-back transaction.
200    pub fn unmark_deleted_by(&mut self, tx: TransactionId) -> bool {
201        let mut any_undeleted = false;
202        for version in &mut self.versions {
203            if version.info.unmark_deleted_by(tx) {
204                any_undeleted = true;
205            }
206        }
207        any_undeleted
208    }
209
210    /// Checks if any version was modified by the given transaction.
211    #[must_use]
212    pub fn modified_by(&self, tx: TransactionId) -> bool {
213        self.versions.iter().any(|v| v.info.created_by == tx)
214    }
215
216    /// Checks if any version was deleted by the given transaction.
217    #[must_use]
218    pub fn deleted_by(&self, tx: TransactionId) -> bool {
219        self.versions.iter().any(|v| v.info.deleted_by == Some(tx))
220    }
221
222    /// Removes all versions created by the given transaction.
223    ///
224    /// Used for rollback to discard uncommitted changes.
225    pub fn remove_versions_by(&mut self, tx: TransactionId) {
226        self.versions.retain(|v| v.info.created_by != tx);
227    }
228
229    /// Finalizes PENDING epochs for versions created by the given transaction.
230    ///
231    /// Called at commit time to make uncommitted versions visible at the
232    /// real commit epoch instead of `EpochId::PENDING`.
233    pub fn finalize_epochs(&mut self, transaction_id: TransactionId, commit_epoch: EpochId) {
234        for version in &mut self.versions {
235            if version.info.created_by == transaction_id
236                && version.info.created_epoch == EpochId::PENDING
237            {
238                version.info.created_epoch = commit_epoch;
239            }
240        }
241    }
242
243    /// Checks if there's a concurrent modification conflict.
244    ///
245    /// A conflict exists if another transaction modified this entity
246    /// after our start epoch.
247    #[must_use]
248    pub fn has_conflict(&self, start_epoch: EpochId, our_tx: TransactionId) -> bool {
249        self.versions.iter().any(|v| {
250            v.info.created_by != our_tx && v.info.created_epoch.as_u64() > start_epoch.as_u64()
251        })
252    }
253
254    /// Returns the number of versions in the chain.
255    #[must_use]
256    pub fn version_count(&self) -> usize {
257        self.versions.len()
258    }
259
260    /// Returns true if the chain has no versions.
261    #[must_use]
262    pub fn is_empty(&self) -> bool {
263        self.versions.is_empty()
264    }
265
266    /// Garbage collects old versions that are no longer visible to any transaction.
267    ///
268    /// Keeps versions that might still be visible to transactions at or after `min_epoch`.
269    pub fn gc(&mut self, min_epoch: EpochId) {
270        if self.versions.is_empty() {
271            return;
272        }
273
274        let mut keep_count = 0;
275        let mut found_old_visible = false;
276
277        for (i, version) in self.versions.iter().enumerate() {
278            if version.info.created_epoch.as_u64() >= min_epoch.as_u64() {
279                keep_count = i + 1;
280            } else if !found_old_visible {
281                // Keep the first (most recent) old version
282                found_old_visible = true;
283                keep_count = i + 1;
284            }
285        }
286
287        self.versions.truncate(keep_count);
288    }
289
290    /// Returns an iterator over all versions with their metadata, newest first.
291    ///
292    /// Each item is `(&VersionInfo, &T)` giving both the visibility metadata
293    /// and a reference to the version data.
294    pub fn history(&self) -> impl Iterator<Item = (&VersionInfo, &T)> {
295        self.versions.iter().map(|v| (&v.info, &v.data))
296    }
297
298    /// Returns a reference to the latest version's data regardless of visibility.
299    #[must_use]
300    pub fn latest(&self) -> Option<&T> {
301        self.versions.front().map(|v| &v.data)
302    }
303
304    /// Returns a mutable reference to the latest version's data.
305    #[must_use]
306    pub fn latest_mut(&mut self) -> Option<&mut T> {
307        self.versions.front_mut().map(|v| &mut v.data)
308    }
309
310    /// Returns estimated heap memory in bytes for this version chain.
311    ///
312    /// Counts the `VecDeque` capacity overhead. Does not include the
313    /// size of `T` payloads (the caller accounts for those).
314    #[must_use]
315    pub fn heap_memory_bytes(&self) -> usize {
316        self.versions.capacity() * std::mem::size_of::<Version<T>>()
317    }
318}
319
320impl<T> Default for VersionChain<T> {
321    fn default() -> Self {
322        Self::new()
323    }
324}
325
326impl<T: Clone> VersionChain<T> {
327    /// Gets a mutable reference to the visible version's data for modification.
328    ///
329    /// If the version is not owned by this transaction, creates a new version
330    /// with a copy of the data.
331    pub fn get_mut(
332        &mut self,
333        epoch: EpochId,
334        tx: TransactionId,
335        modify_epoch: EpochId,
336    ) -> Option<&mut T> {
337        // Find the visible version
338        let visible_idx = self
339            .versions
340            .iter()
341            .position(|v| v.info.is_visible_to(epoch, tx))?;
342
343        let visible = &self.versions[visible_idx];
344
345        if visible.info.created_by == tx {
346            // Already our version, modify in place
347            Some(&mut self.versions[visible_idx].data)
348        } else {
349            // Create a new version with copied data
350            let new_data = visible.data.clone();
351            self.add_version(new_data, modify_epoch, tx);
352            Some(&mut self.versions[0].data)
353        }
354    }
355}
356
357// ============================================================================
358// Tiered Storage Types (Phase 13)
359// ============================================================================
360//
361// These types support the tiered hot/cold storage architecture where version
362// metadata is stored separately from version data. Data lives in arenas (hot)
363// or compressed epoch blocks (cold), while VersionIndex holds lightweight refs.
364
365/// Compact representation of an optional epoch ID.
366///
367/// Uses `u32::MAX` as sentinel for `None`, allowing epochs up to ~4 billion.
368/// This saves 4 bytes compared to `Option<EpochId>` due to niche optimization.
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
370#[repr(transparent)]
371#[cfg(feature = "tiered-storage")]
372pub struct OptionalEpochId(u32);
373
374#[cfg(feature = "tiered-storage")]
375impl OptionalEpochId {
376    /// Represents no epoch (deleted_epoch = None).
377    pub const NONE: Self = Self(u32::MAX);
378
379    /// Creates an `OptionalEpochId` from an epoch.
380    ///
381    /// # Panics
382    /// Panics if epoch exceeds u32::MAX - 1 (4,294,967,294).
383    #[must_use]
384    pub fn some(epoch: EpochId) -> Self {
385        assert!(
386            epoch.as_u64() < u64::from(u32::MAX),
387            "epoch {} exceeds OptionalEpochId capacity (max {})",
388            epoch.as_u64(),
389            u32::MAX as u64 - 1
390        );
391        Self(epoch.as_u64() as u32)
392    }
393
394    /// Returns the contained epoch, or `None` if this is `NONE`.
395    #[inline]
396    #[must_use]
397    pub fn get(self) -> Option<EpochId> {
398        if self.0 == u32::MAX {
399            None
400        } else {
401            Some(EpochId::new(u64::from(self.0)))
402        }
403    }
404
405    /// Returns `true` if this contains an epoch.
406    #[must_use]
407    pub fn is_some(self) -> bool {
408        self.0 != u32::MAX
409    }
410
411    /// Returns `true` if this is `NONE`.
412    #[inline]
413    #[must_use]
414    pub fn is_none(self) -> bool {
415        self.0 == u32::MAX
416    }
417}
418
419/// Reference to a hot (arena-allocated) version.
420///
421/// Hot versions are stored in the epoch's arena and can be accessed directly.
422/// This struct only holds metadata; the actual data lives in the arena.
423///
424/// # Memory Layout
425/// - `epoch`: 8 bytes
426/// - `arena_epoch`: 8 bytes
427/// - `arena_offset`: 4 bytes
428/// - `created_by`: 8 bytes
429/// - `deleted_epoch`: 4 bytes
430/// - Total: 32 bytes
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432#[cfg(feature = "tiered-storage")]
433pub struct HotVersionRef {
434    /// Epoch used for MVCC visibility checks.
435    ///
436    /// Set to `EpochId::PENDING` for uncommitted transactional versions,
437    /// then finalized to the real commit epoch on commit.
438    pub epoch: EpochId,
439    /// Epoch whose arena holds the actual data.
440    ///
441    /// Always the real epoch (never PENDING), used for arena lookups.
442    pub arena_epoch: EpochId,
443    /// Offset within the epoch's arena where the data is stored.
444    pub arena_offset: u32,
445    /// Transaction that created this version.
446    pub created_by: TransactionId,
447    /// Epoch when this version was deleted (NONE if still alive).
448    pub deleted_epoch: OptionalEpochId,
449    /// Transaction that deleted this version (for rollback support).
450    pub deleted_by: Option<TransactionId>,
451}
452
453#[cfg(feature = "tiered-storage")]
454impl HotVersionRef {
455    /// Creates a new hot version reference.
456    ///
457    /// `epoch` is used for MVCC visibility (may be `PENDING` for uncommitted versions).
458    /// `arena_epoch` is the real epoch whose arena holds the data.
459    #[must_use]
460    pub fn new(
461        epoch: EpochId,
462        arena_epoch: EpochId,
463        arena_offset: u32,
464        created_by: TransactionId,
465    ) -> Self {
466        Self {
467            epoch,
468            arena_epoch,
469            arena_offset,
470            created_by,
471            deleted_epoch: OptionalEpochId::NONE,
472            deleted_by: None,
473        }
474    }
475
476    /// Marks this version as deleted by a specific transaction.
477    pub fn mark_deleted(&mut self, epoch: EpochId, deleted_by: TransactionId) {
478        self.deleted_epoch = OptionalEpochId::some(epoch);
479        self.deleted_by = Some(deleted_by);
480    }
481
482    /// Unmarks deletion if deleted by the given transaction. Returns `true` if undeleted.
483    pub fn unmark_deleted_by(&mut self, transaction_id: TransactionId) -> bool {
484        if self.deleted_by == Some(transaction_id) {
485            self.deleted_epoch = OptionalEpochId::NONE;
486            self.deleted_by = None;
487            true
488        } else {
489            false
490        }
491    }
492
493    /// Checks if this version is visible at the given epoch.
494    #[inline]
495    #[must_use]
496    pub fn is_visible_at(&self, viewing_epoch: EpochId) -> bool {
497        // Must be created at or before the viewing epoch
498        if !self.epoch.is_visible_at(viewing_epoch) {
499            return false;
500        }
501        // Must not be deleted at or before the viewing epoch
502        match self.deleted_epoch.get() {
503            Some(deleted) => deleted.as_u64() > viewing_epoch.as_u64(),
504            None => true,
505        }
506    }
507
508    /// Checks if this version is visible to a specific transaction.
509    #[inline]
510    #[must_use]
511    pub fn is_visible_to(&self, viewing_epoch: EpochId, viewing_tx: TransactionId) -> bool {
512        // If deleted by the viewing transaction, not visible
513        if self.deleted_by == Some(viewing_tx) {
514            return false;
515        }
516        // Own modifications are always visible (if not deleted)
517        if self.created_by == viewing_tx {
518            return self.deleted_epoch.is_none();
519        }
520        // Otherwise use epoch-based visibility
521        self.is_visible_at(viewing_epoch)
522    }
523}
524
525/// Reference to a cold (compressed) version.
526///
527/// Cold versions are stored in compressed epoch blocks. This is a placeholder
528/// for Phase 14 - the actual compression logic will be implemented there.
529///
530/// # Memory Layout
531/// - `epoch`: 8 bytes
532/// - `block_offset`: 4 bytes
533/// - `length`: 2 bytes
534/// - `created_by`: 8 bytes
535/// - `deleted_epoch`: 4 bytes
536/// - Total: 26 bytes (+ 6 padding = 32 bytes aligned)
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
538#[cfg(feature = "tiered-storage")]
539pub struct ColdVersionRef {
540    /// Epoch when this version was created.
541    pub epoch: EpochId,
542    /// Offset within the compressed epoch block.
543    pub block_offset: u32,
544    /// Compressed length in bytes.
545    pub length: u16,
546    /// Transaction that created this version.
547    pub created_by: TransactionId,
548    /// Epoch when this version was deleted.
549    pub deleted_epoch: OptionalEpochId,
550    /// Transaction that deleted this version (for rollback support).
551    pub deleted_by: Option<TransactionId>,
552}
553
554#[cfg(feature = "tiered-storage")]
555impl ColdVersionRef {
556    /// Checks if this version is visible at the given epoch.
557    #[inline]
558    #[must_use]
559    pub fn is_visible_at(&self, viewing_epoch: EpochId) -> bool {
560        if !self.epoch.is_visible_at(viewing_epoch) {
561            return false;
562        }
563        match self.deleted_epoch.get() {
564            Some(deleted) => deleted.as_u64() > viewing_epoch.as_u64(),
565            None => true,
566        }
567    }
568
569    /// Checks if this version is visible to a specific transaction.
570    #[inline]
571    #[must_use]
572    pub fn is_visible_to(&self, viewing_epoch: EpochId, viewing_tx: TransactionId) -> bool {
573        // If deleted by the viewing transaction, not visible
574        if self.deleted_by == Some(viewing_tx) {
575            return false;
576        }
577        if self.created_by == viewing_tx {
578            return self.deleted_epoch.is_none();
579        }
580        self.is_visible_at(viewing_epoch)
581    }
582}
583
584/// Unified reference to either a hot or cold version.
585#[derive(Debug, Clone, Copy)]
586#[cfg(feature = "tiered-storage")]
587pub enum VersionRef {
588    /// Version data is in arena (hot tier).
589    Hot(HotVersionRef),
590    /// Version data is in compressed storage (cold tier).
591    Cold(ColdVersionRef),
592}
593
594#[cfg(feature = "tiered-storage")]
595impl VersionRef {
596    /// Returns the epoch when this version was created.
597    #[must_use]
598    pub fn epoch(&self) -> EpochId {
599        match self {
600            Self::Hot(h) => h.epoch,
601            Self::Cold(c) => c.epoch,
602        }
603    }
604
605    /// Returns the transaction that created this version.
606    #[must_use]
607    pub fn created_by(&self) -> TransactionId {
608        match self {
609            Self::Hot(h) => h.created_by,
610            Self::Cold(c) => c.created_by,
611        }
612    }
613
614    /// Returns `true` if this is a hot version.
615    #[must_use]
616    pub fn is_hot(&self) -> bool {
617        matches!(self, Self::Hot(_))
618    }
619
620    /// Returns `true` if this is a cold version.
621    #[must_use]
622    pub fn is_cold(&self) -> bool {
623        matches!(self, Self::Cold(_))
624    }
625
626    /// Returns the epoch when this version was deleted, if any.
627    #[must_use]
628    pub fn deleted_epoch(&self) -> Option<EpochId> {
629        match self {
630            Self::Hot(h) => h.deleted_epoch.get(),
631            Self::Cold(c) => c.deleted_epoch.get(),
632        }
633    }
634}
635
636/// Tiered version index - replaces `VersionChain<T>` for hot/cold storage.
637///
638/// Instead of storing data inline, `VersionIndex` holds lightweight references
639/// to data stored in arenas (hot) or compressed blocks (cold). This enables:
640///
641/// - **No heap allocation** for typical 1-2 version case (SmallVec inline)
642/// - **Separation of metadata and data** for compression
643/// - **Fast visibility checks** via cached `latest_epoch`
644/// - **O(1) epoch drops** instead of per-version deallocation
645///
646/// # Memory Layout
647/// - `hot`: SmallVec<[HotVersionRef; 2]> ≈ 56 bytes inline
648/// - `cold`: SmallVec<[ColdVersionRef; 4]> ≈ 136 bytes inline
649/// - `latest_epoch`: 8 bytes
650/// - Total: ~200 bytes, no heap for typical case
651#[derive(Debug, Clone)]
652#[cfg(feature = "tiered-storage")]
653pub struct VersionIndex {
654    /// Hot versions in arena storage (most recent first).
655    hot: SmallVec<[HotVersionRef; 2]>,
656    /// Cold versions in compressed storage (most recent first).
657    cold: SmallVec<[ColdVersionRef; 4]>,
658    /// Cached epoch of the latest version for fast staleness checks.
659    latest_epoch: EpochId,
660}
661
662#[cfg(feature = "tiered-storage")]
663impl VersionIndex {
664    /// Creates a new empty version index.
665    #[must_use]
666    pub fn new() -> Self {
667        Self {
668            hot: SmallVec::new(),
669            cold: SmallVec::new(),
670            latest_epoch: EpochId::INITIAL,
671        }
672    }
673
674    /// Creates a version index with an initial hot version.
675    #[must_use]
676    pub fn with_initial(hot_ref: HotVersionRef) -> Self {
677        let mut index = Self::new();
678        index.add_hot(hot_ref);
679        index
680    }
681
682    /// Adds a new hot version (becomes the latest).
683    pub fn add_hot(&mut self, hot_ref: HotVersionRef) {
684        // Insert at front (most recent first)
685        self.hot.insert(0, hot_ref);
686        self.latest_epoch = hot_ref.epoch;
687    }
688
689    /// Returns the latest epoch for quick staleness checks.
690    #[must_use]
691    pub fn latest_epoch(&self) -> EpochId {
692        self.latest_epoch
693    }
694
695    /// Returns `true` if this entity has no versions.
696    #[must_use]
697    pub fn is_empty(&self) -> bool {
698        self.hot.is_empty() && self.cold.is_empty()
699    }
700
701    /// Returns the total version count (hot + cold).
702    #[must_use]
703    pub fn version_count(&self) -> usize {
704        self.hot.len() + self.cold.len()
705    }
706
707    /// Returns the number of hot versions.
708    #[must_use]
709    pub fn hot_count(&self) -> usize {
710        self.hot.len()
711    }
712
713    /// Returns the number of cold versions.
714    #[must_use]
715    pub fn cold_count(&self) -> usize {
716        self.cold.len()
717    }
718
719    /// Finds the version visible at the given epoch.
720    #[inline]
721    #[must_use]
722    pub fn visible_at(&self, epoch: EpochId) -> Option<VersionRef> {
723        // Check hot versions first (most recent first, likely case)
724        for v in &self.hot {
725            if v.is_visible_at(epoch) {
726                return Some(VersionRef::Hot(*v));
727            }
728        }
729        // Fall back to cold versions
730        for v in &self.cold {
731            if v.is_visible_at(epoch) {
732                return Some(VersionRef::Cold(*v));
733            }
734        }
735        None
736    }
737
738    /// Finds the version visible to a specific transaction.
739    #[inline]
740    #[must_use]
741    pub fn visible_to(&self, epoch: EpochId, tx: TransactionId) -> Option<VersionRef> {
742        // Check hot versions first
743        for v in &self.hot {
744            if v.is_visible_to(epoch, tx) {
745                return Some(VersionRef::Hot(*v));
746            }
747        }
748        // Fall back to cold versions
749        for v in &self.cold {
750            if v.is_visible_to(epoch, tx) {
751                return Some(VersionRef::Cold(*v));
752            }
753        }
754        None
755    }
756
757    /// Marks the currently visible version as deleted by a specific transaction.
758    ///
759    /// Returns `true` if a version was marked, `false` if no visible version exists.
760    pub fn mark_deleted(&mut self, delete_epoch: EpochId, deleted_by: TransactionId) -> bool {
761        // Find the first non-deleted hot version and mark it
762        for v in &mut self.hot {
763            if v.deleted_epoch.is_none() {
764                v.mark_deleted(delete_epoch, deleted_by);
765                return true;
766            }
767        }
768        // Check cold versions (rare case)
769        for v in &mut self.cold {
770            if v.deleted_epoch.is_none() {
771                v.deleted_epoch = OptionalEpochId::some(delete_epoch);
772                v.deleted_by = Some(deleted_by);
773                return true;
774            }
775        }
776        false
777    }
778
779    /// Unmarks deletion for versions deleted by the given transaction.
780    ///
781    /// Returns `true` if any version was undeleted. Used during rollback.
782    pub fn unmark_deleted_by(&mut self, tx: TransactionId) -> bool {
783        let mut any_undeleted = false;
784        for v in &mut self.hot {
785            if v.unmark_deleted_by(tx) {
786                any_undeleted = true;
787            }
788        }
789        for v in &mut self.cold {
790            if v.deleted_by == Some(tx) {
791                v.deleted_epoch = OptionalEpochId::NONE;
792                v.deleted_by = None;
793                any_undeleted = true;
794            }
795        }
796        any_undeleted
797    }
798
799    /// Checks if any version was created by the given transaction.
800    #[must_use]
801    pub fn modified_by(&self, tx: TransactionId) -> bool {
802        self.hot.iter().any(|v| v.created_by == tx) || self.cold.iter().any(|v| v.created_by == tx)
803    }
804
805    /// Checks if any version was deleted by the given transaction.
806    #[must_use]
807    pub fn deleted_by(&self, tx: TransactionId) -> bool {
808        self.hot.iter().any(|v| v.deleted_by == Some(tx))
809            || self.cold.iter().any(|v| v.deleted_by == Some(tx))
810    }
811
812    /// Removes all versions created by the given transaction (for rollback).
813    pub fn remove_versions_by(&mut self, tx: TransactionId) {
814        self.hot.retain(|v| v.created_by != tx);
815        self.cold.retain(|v| v.created_by != tx);
816        self.recalculate_latest_epoch();
817    }
818
819    /// Finalizes PENDING epochs for hot versions created by the given transaction.
820    ///
821    /// Called at commit time to make uncommitted versions visible at the
822    /// real commit epoch instead of `EpochId::PENDING`.
823    pub fn finalize_epochs(&mut self, transaction_id: TransactionId, commit_epoch: EpochId) {
824        for v in &mut self.hot {
825            if v.created_by == transaction_id && v.epoch == EpochId::PENDING {
826                v.epoch = commit_epoch;
827            }
828        }
829        self.recalculate_latest_epoch();
830    }
831
832    /// Checks for write conflict with concurrent transaction.
833    ///
834    /// A conflict exists if another transaction modified this entity
835    /// after our start epoch.
836    #[must_use]
837    pub fn has_conflict(&self, start_epoch: EpochId, our_tx: TransactionId) -> bool {
838        self.hot
839            .iter()
840            .any(|v| v.created_by != our_tx && v.epoch.as_u64() > start_epoch.as_u64())
841            || self
842                .cold
843                .iter()
844                .any(|v| v.created_by != our_tx && v.epoch.as_u64() > start_epoch.as_u64())
845    }
846
847    /// Garbage collects old versions not needed by any active transaction.
848    ///
849    /// Keeps versions that might still be visible to transactions at or after `min_epoch`.
850    pub fn gc(&mut self, min_epoch: EpochId) {
851        if self.is_empty() {
852            return;
853        }
854
855        // Keep versions that:
856        // 1. Were created at or after min_epoch
857        // 2. The first (most recent) version created before min_epoch
858        let mut found_old_visible = false;
859
860        self.hot.retain(|v| {
861            if v.epoch.as_u64() >= min_epoch.as_u64() {
862                true
863            } else if !found_old_visible {
864                found_old_visible = true;
865                true
866            } else {
867                false
868            }
869        });
870
871        // Same for cold, but only if we haven't found an old visible in hot
872        if !found_old_visible {
873            self.cold.retain(|v| {
874                if v.epoch.as_u64() >= min_epoch.as_u64() {
875                    true
876                } else if !found_old_visible {
877                    found_old_visible = true;
878                    true
879                } else {
880                    false
881                }
882            });
883        } else {
884            // All cold versions are older, only keep those >= min_epoch
885            self.cold.retain(|v| v.epoch.as_u64() >= min_epoch.as_u64());
886        }
887    }
888
889    /// Returns epoch IDs of all versions, newest first.
890    ///
891    /// Combines hot and cold versions, sorted by epoch descending.
892    #[must_use]
893    pub fn version_epochs(&self) -> Vec<EpochId> {
894        let mut epochs: Vec<EpochId> = self
895            .hot
896            .iter()
897            .map(|v| v.epoch)
898            .chain(self.cold.iter().map(|v| v.epoch))
899            .collect();
900        epochs.sort_by_key(|e| std::cmp::Reverse(e.as_u64()));
901        epochs
902    }
903
904    /// Returns all version references with their metadata, newest first.
905    ///
906    /// Each item is `(EpochId, Option<EpochId>, VersionRef)` giving
907    /// the created epoch, deleted epoch, and a reference to read the data.
908    #[must_use]
909    pub fn version_history(&self) -> Vec<(EpochId, Option<EpochId>, VersionRef)> {
910        let mut versions: Vec<(EpochId, Option<EpochId>, VersionRef)> = self
911            .hot
912            .iter()
913            .map(|v| (v.epoch, v.deleted_epoch.get(), VersionRef::Hot(*v)))
914            .chain(
915                self.cold
916                    .iter()
917                    .map(|v| (v.epoch, v.deleted_epoch.get(), VersionRef::Cold(*v))),
918            )
919            .collect();
920        versions.sort_by_key(|v| std::cmp::Reverse(v.0.as_u64()));
921        versions
922    }
923
924    /// Returns a reference to the latest version regardless of visibility.
925    #[must_use]
926    pub fn latest(&self) -> Option<VersionRef> {
927        self.hot
928            .first()
929            .map(|v| VersionRef::Hot(*v))
930            .or_else(|| self.cold.first().map(|v| VersionRef::Cold(*v)))
931    }
932
933    /// Freezes hot versions for a given epoch into cold storage.
934    ///
935    /// This is called when an epoch is no longer needed by any active transaction.
936    /// The actual compression happens in Phase 14; for now this just moves refs.
937    pub fn freeze_epoch(
938        &mut self,
939        epoch: EpochId,
940        cold_refs: impl Iterator<Item = ColdVersionRef>,
941    ) {
942        // Remove hot refs for this epoch
943        self.hot.retain(|v| v.epoch != epoch);
944
945        // Add cold refs
946        self.cold.extend(cold_refs);
947
948        // Keep cold sorted by epoch (descending = most recent first)
949        self.cold
950            .sort_by(|a, b| b.epoch.as_u64().cmp(&a.epoch.as_u64()));
951
952        self.recalculate_latest_epoch();
953    }
954
955    /// Returns hot version refs for a specific epoch (for freeze operation).
956    pub fn hot_refs_for_epoch(&self, epoch: EpochId) -> impl Iterator<Item = &HotVersionRef> {
957        self.hot.iter().filter(move |v| v.epoch == epoch)
958    }
959
960    /// Returns `true` if the hot SmallVec has spilled to the heap.
961    #[must_use]
962    pub fn hot_spilled(&self) -> bool {
963        self.hot.spilled()
964    }
965
966    /// Returns `true` if the cold SmallVec has spilled to the heap.
967    #[must_use]
968    pub fn cold_spilled(&self) -> bool {
969        self.cold.spilled()
970    }
971
972    fn recalculate_latest_epoch(&mut self) {
973        self.latest_epoch = self
974            .hot
975            .first()
976            .map(|v| v.epoch)
977            .or_else(|| self.cold.first().map(|v| v.epoch))
978            .unwrap_or(EpochId::INITIAL);
979    }
980}
981
982#[cfg(feature = "tiered-storage")]
983impl Default for VersionIndex {
984    fn default() -> Self {
985        Self::new()
986    }
987}
988
989#[cfg(test)]
990mod tests {
991    use super::*;
992
993    #[test]
994    fn test_version_visibility() {
995        let v = VersionInfo::new(EpochId::new(5), TransactionId::new(1));
996
997        // Not visible before creation
998        assert!(!v.is_visible_at(EpochId::new(4)));
999
1000        // Visible at creation epoch and after
1001        assert!(v.is_visible_at(EpochId::new(5)));
1002        assert!(v.is_visible_at(EpochId::new(10)));
1003    }
1004
1005    #[test]
1006    fn test_deleted_version_visibility() {
1007        let mut v = VersionInfo::new(EpochId::new(5), TransactionId::new(1));
1008        v.mark_deleted(EpochId::new(10), TransactionId::new(99));
1009
1010        // Visible between creation and deletion
1011        assert!(v.is_visible_at(EpochId::new(5)));
1012        assert!(v.is_visible_at(EpochId::new(9)));
1013
1014        // Not visible at or after deletion
1015        assert!(!v.is_visible_at(EpochId::new(10)));
1016        assert!(!v.is_visible_at(EpochId::new(15)));
1017    }
1018
1019    #[test]
1020    fn test_version_visibility_to_transaction() {
1021        let v = VersionInfo::new(EpochId::new(5), TransactionId::new(1));
1022
1023        // Creator can see it even if viewing at earlier epoch
1024        assert!(v.is_visible_to(EpochId::new(3), TransactionId::new(1)));
1025
1026        // Other transactions can only see it at or after creation epoch
1027        assert!(!v.is_visible_to(EpochId::new(3), TransactionId::new(2)));
1028        assert!(v.is_visible_to(EpochId::new(5), TransactionId::new(2)));
1029    }
1030
1031    #[test]
1032    fn test_version_chain_basic() {
1033        let mut chain = VersionChain::with_initial("v1", EpochId::new(1), TransactionId::new(1));
1034
1035        // Should see v1 at epoch 1+
1036        assert_eq!(chain.visible_at(EpochId::new(1)), Some(&"v1"));
1037        assert_eq!(chain.visible_at(EpochId::new(0)), None);
1038
1039        // Add v2
1040        chain.add_version("v2", EpochId::new(5), TransactionId::new(2));
1041
1042        // Should see v1 at epoch < 5, v2 at epoch >= 5
1043        assert_eq!(chain.visible_at(EpochId::new(1)), Some(&"v1"));
1044        assert_eq!(chain.visible_at(EpochId::new(4)), Some(&"v1"));
1045        assert_eq!(chain.visible_at(EpochId::new(5)), Some(&"v2"));
1046        assert_eq!(chain.visible_at(EpochId::new(10)), Some(&"v2"));
1047    }
1048
1049    #[test]
1050    fn test_version_chain_rollback() {
1051        let mut chain = VersionChain::with_initial("v1", EpochId::new(1), TransactionId::new(1));
1052        chain.add_version("v2", EpochId::new(5), TransactionId::new(2));
1053        chain.add_version("v3", EpochId::new(6), TransactionId::new(2));
1054
1055        assert_eq!(chain.version_count(), 3);
1056
1057        // Rollback tx 2's changes
1058        chain.remove_versions_by(TransactionId::new(2));
1059
1060        assert_eq!(chain.version_count(), 1);
1061        assert_eq!(chain.visible_at(EpochId::new(10)), Some(&"v1"));
1062    }
1063
1064    #[test]
1065    fn test_version_chain_deletion() {
1066        let mut chain = VersionChain::with_initial("v1", EpochId::new(1), TransactionId::new(1));
1067
1068        // Mark as deleted at epoch 5 by transaction 99 (system)
1069        assert!(chain.mark_deleted(EpochId::new(5), TransactionId::new(99)));
1070
1071        // Should see v1 before deletion, nothing after
1072        assert_eq!(chain.visible_at(EpochId::new(4)), Some(&"v1"));
1073        assert_eq!(chain.visible_at(EpochId::new(5)), None);
1074        assert_eq!(chain.visible_at(EpochId::new(10)), None);
1075    }
1076}
1077
1078// ============================================================================
1079// Tiered Storage Tests
1080// ============================================================================
1081
1082#[cfg(all(test, feature = "tiered-storage"))]
1083mod tiered_storage_tests {
1084    use super::*;
1085
1086    #[test]
1087    fn test_optional_epoch_id() {
1088        // Test NONE
1089        let none = OptionalEpochId::NONE;
1090        assert!(none.is_none());
1091        assert!(!none.is_some());
1092        assert_eq!(none.get(), None);
1093
1094        // Test Some
1095        let some = OptionalEpochId::some(EpochId::new(42));
1096        assert!(some.is_some());
1097        assert!(!some.is_none());
1098        assert_eq!(some.get(), Some(EpochId::new(42)));
1099
1100        // Test zero epoch
1101        let zero = OptionalEpochId::some(EpochId::new(0));
1102        assert!(zero.is_some());
1103        assert_eq!(zero.get(), Some(EpochId::new(0)));
1104    }
1105
1106    #[test]
1107    fn test_hot_version_ref_visibility() {
1108        let hot = HotVersionRef::new(EpochId::new(5), EpochId::new(5), 100, TransactionId::new(1));
1109
1110        // Not visible before creation
1111        assert!(!hot.is_visible_at(EpochId::new(4)));
1112
1113        // Visible at creation and after
1114        assert!(hot.is_visible_at(EpochId::new(5)));
1115        assert!(hot.is_visible_at(EpochId::new(10)));
1116    }
1117
1118    #[test]
1119    fn test_hot_version_ref_deleted_visibility() {
1120        let mut hot =
1121            HotVersionRef::new(EpochId::new(5), EpochId::new(5), 100, TransactionId::new(1));
1122        hot.deleted_epoch = OptionalEpochId::some(EpochId::new(10));
1123
1124        // Visible between creation and deletion
1125        assert!(hot.is_visible_at(EpochId::new(5)));
1126        assert!(hot.is_visible_at(EpochId::new(9)));
1127
1128        // Not visible at or after deletion
1129        assert!(!hot.is_visible_at(EpochId::new(10)));
1130        assert!(!hot.is_visible_at(EpochId::new(15)));
1131    }
1132
1133    #[test]
1134    fn test_hot_version_ref_transaction_visibility() {
1135        let hot = HotVersionRef::new(EpochId::new(5), EpochId::new(5), 100, TransactionId::new(1));
1136
1137        // Creator can see it even at earlier epoch
1138        assert!(hot.is_visible_to(EpochId::new(3), TransactionId::new(1)));
1139
1140        // Other transactions can only see it at or after creation
1141        assert!(!hot.is_visible_to(EpochId::new(3), TransactionId::new(2)));
1142        assert!(hot.is_visible_to(EpochId::new(5), TransactionId::new(2)));
1143    }
1144
1145    #[test]
1146    fn test_version_index_basic() {
1147        let hot = HotVersionRef::new(EpochId::new(1), EpochId::new(1), 0, TransactionId::new(1));
1148        let mut index = VersionIndex::with_initial(hot);
1149
1150        // Should see version at epoch 1+
1151        assert!(index.visible_at(EpochId::new(1)).is_some());
1152        assert!(index.visible_at(EpochId::new(0)).is_none());
1153
1154        // Add another version
1155        let hot2 = HotVersionRef::new(EpochId::new(5), EpochId::new(5), 100, TransactionId::new(2));
1156        index.add_hot(hot2);
1157
1158        // Should see v1 at epoch < 5, v2 at epoch >= 5
1159        let v1 = index.visible_at(EpochId::new(4)).unwrap();
1160        assert!(matches!(v1, VersionRef::Hot(h) if h.arena_offset == 0));
1161
1162        let v2 = index.visible_at(EpochId::new(5)).unwrap();
1163        assert!(matches!(v2, VersionRef::Hot(h) if h.arena_offset == 100));
1164    }
1165
1166    #[test]
1167    fn test_version_index_deletion() {
1168        let hot = HotVersionRef::new(EpochId::new(1), EpochId::new(1), 0, TransactionId::new(1));
1169        let mut index = VersionIndex::with_initial(hot);
1170
1171        // Mark as deleted at epoch 5
1172        assert!(index.mark_deleted(EpochId::new(5), TransactionId::new(99)));
1173
1174        // Should see version before deletion, nothing after
1175        assert!(index.visible_at(EpochId::new(4)).is_some());
1176        assert!(index.visible_at(EpochId::new(5)).is_none());
1177        assert!(index.visible_at(EpochId::new(10)).is_none());
1178    }
1179
1180    #[test]
1181    fn test_version_index_transaction_visibility() {
1182        let tx = TransactionId::new(10);
1183        let hot = HotVersionRef::new(EpochId::new(5), EpochId::new(5), 0, tx);
1184        let index = VersionIndex::with_initial(hot);
1185
1186        // Creator can see it even at earlier epoch
1187        assert!(index.visible_to(EpochId::new(3), tx).is_some());
1188
1189        // Other transactions cannot see it before creation
1190        assert!(
1191            index
1192                .visible_to(EpochId::new(3), TransactionId::new(20))
1193                .is_none()
1194        );
1195        assert!(
1196            index
1197                .visible_to(EpochId::new(5), TransactionId::new(20))
1198                .is_some()
1199        );
1200    }
1201
1202    #[test]
1203    fn test_version_index_rollback() {
1204        let tx1 = TransactionId::new(10);
1205        let tx2 = TransactionId::new(20);
1206
1207        let mut index = VersionIndex::new();
1208        index.add_hot(HotVersionRef::new(EpochId::new(1), EpochId::new(1), 0, tx1));
1209        index.add_hot(HotVersionRef::new(
1210            EpochId::new(2),
1211            EpochId::new(2),
1212            100,
1213            tx2,
1214        ));
1215        index.add_hot(HotVersionRef::new(
1216            EpochId::new(3),
1217            EpochId::new(3),
1218            200,
1219            tx2,
1220        ));
1221
1222        assert_eq!(index.version_count(), 3);
1223        assert!(index.modified_by(tx1));
1224        assert!(index.modified_by(tx2));
1225
1226        // Rollback tx2's changes
1227        index.remove_versions_by(tx2);
1228
1229        assert_eq!(index.version_count(), 1);
1230        assert!(index.modified_by(tx1));
1231        assert!(!index.modified_by(tx2));
1232
1233        // Should only see tx1's version
1234        let v = index.visible_at(EpochId::new(10)).unwrap();
1235        assert!(matches!(v, VersionRef::Hot(h) if h.created_by == tx1));
1236    }
1237
1238    #[test]
1239    fn test_version_index_gc() {
1240        let mut index = VersionIndex::new();
1241
1242        // Add versions at epochs 1, 3, 5
1243        for epoch in [1, 3, 5] {
1244            index.add_hot(HotVersionRef::new(
1245                EpochId::new(epoch),
1246                EpochId::new(epoch),
1247                epoch as u32 * 100,
1248                TransactionId::new(epoch),
1249            ));
1250        }
1251
1252        assert_eq!(index.version_count(), 3);
1253
1254        // GC with min_epoch = 4
1255        // Should keep: epoch 5 (>= 4) and epoch 3 (first old visible)
1256        index.gc(EpochId::new(4));
1257
1258        assert_eq!(index.version_count(), 2);
1259
1260        // Verify we kept epochs 5 and 3
1261        assert!(index.visible_at(EpochId::new(5)).is_some());
1262        assert!(index.visible_at(EpochId::new(3)).is_some());
1263    }
1264
1265    #[test]
1266    fn test_version_index_conflict_detection() {
1267        let tx1 = TransactionId::new(10);
1268        let tx2 = TransactionId::new(20);
1269
1270        let mut index = VersionIndex::new();
1271        index.add_hot(HotVersionRef::new(EpochId::new(1), EpochId::new(1), 0, tx1));
1272        index.add_hot(HotVersionRef::new(
1273            EpochId::new(5),
1274            EpochId::new(5),
1275            100,
1276            tx2,
1277        ));
1278
1279        // tx1 started at epoch 0, tx2 modified at epoch 5 -> conflict for tx1
1280        assert!(index.has_conflict(EpochId::new(0), tx1));
1281
1282        // tx2 started at epoch 0, tx1 modified at epoch 1 -> also conflict for tx2
1283        assert!(index.has_conflict(EpochId::new(0), tx2));
1284
1285        // tx1 started after tx2's modification -> no conflict
1286        assert!(!index.has_conflict(EpochId::new(5), tx1));
1287
1288        // tx2 started after tx1's modification -> no conflict
1289        assert!(!index.has_conflict(EpochId::new(1), tx2));
1290
1291        // If only tx1's version exists, tx1 doesn't conflict with itself
1292        let mut index2 = VersionIndex::new();
1293        index2.add_hot(HotVersionRef::new(EpochId::new(5), EpochId::new(5), 0, tx1));
1294        assert!(!index2.has_conflict(EpochId::new(0), tx1));
1295    }
1296
1297    #[test]
1298    fn test_version_index_smallvec_no_heap() {
1299        let mut index = VersionIndex::new();
1300
1301        // Add 2 hot versions (within inline capacity)
1302        for i in 0..2 {
1303            index.add_hot(HotVersionRef::new(
1304                EpochId::new(i),
1305                EpochId::new(i),
1306                i as u32,
1307                TransactionId::new(i),
1308            ));
1309        }
1310
1311        // SmallVec should not have spilled to heap
1312        assert!(!index.hot_spilled());
1313        assert!(!index.cold_spilled());
1314    }
1315
1316    #[test]
1317    fn test_version_index_freeze_epoch() {
1318        let mut index = VersionIndex::new();
1319        index.add_hot(HotVersionRef::new(
1320            EpochId::new(1),
1321            EpochId::new(1),
1322            0,
1323            TransactionId::new(1),
1324        ));
1325        index.add_hot(HotVersionRef::new(
1326            EpochId::new(2),
1327            EpochId::new(2),
1328            100,
1329            TransactionId::new(2),
1330        ));
1331
1332        assert_eq!(index.hot_count(), 2);
1333        assert_eq!(index.cold_count(), 0);
1334
1335        // Freeze epoch 1
1336        let cold_ref = ColdVersionRef {
1337            epoch: EpochId::new(1),
1338            block_offset: 0,
1339            length: 32,
1340            created_by: TransactionId::new(1),
1341            deleted_epoch: OptionalEpochId::NONE,
1342            deleted_by: None,
1343        };
1344        index.freeze_epoch(EpochId::new(1), std::iter::once(cold_ref));
1345
1346        // Hot should have 1, cold should have 1
1347        assert_eq!(index.hot_count(), 1);
1348        assert_eq!(index.cold_count(), 1);
1349
1350        // Visibility should still work
1351        assert!(index.visible_at(EpochId::new(1)).is_some());
1352        assert!(index.visible_at(EpochId::new(2)).is_some());
1353
1354        // Check that cold version is actually cold
1355        let v1 = index.visible_at(EpochId::new(1)).unwrap();
1356        assert!(v1.is_cold());
1357
1358        let v2 = index.visible_at(EpochId::new(2)).unwrap();
1359        assert!(v2.is_hot());
1360    }
1361
1362    #[test]
1363    fn test_version_ref_accessors() {
1364        let hot = HotVersionRef::new(
1365            EpochId::new(5),
1366            EpochId::new(5),
1367            100,
1368            TransactionId::new(10),
1369        );
1370        let vr = VersionRef::Hot(hot);
1371
1372        assert_eq!(vr.epoch(), EpochId::new(5));
1373        assert_eq!(vr.created_by(), TransactionId::new(10));
1374        assert!(vr.is_hot());
1375        assert!(!vr.is_cold());
1376    }
1377
1378    #[test]
1379    fn test_version_index_latest_epoch() {
1380        let mut index = VersionIndex::new();
1381        assert_eq!(index.latest_epoch(), EpochId::INITIAL);
1382
1383        index.add_hot(HotVersionRef::new(
1384            EpochId::new(5),
1385            EpochId::new(5),
1386            0,
1387            TransactionId::new(1),
1388        ));
1389        assert_eq!(index.latest_epoch(), EpochId::new(5));
1390
1391        index.add_hot(HotVersionRef::new(
1392            EpochId::new(10),
1393            EpochId::new(10),
1394            100,
1395            TransactionId::new(2),
1396        ));
1397        assert_eq!(index.latest_epoch(), EpochId::new(10));
1398
1399        // After rollback, should recalculate
1400        index.remove_versions_by(TransactionId::new(2));
1401        assert_eq!(index.latest_epoch(), EpochId::new(5));
1402    }
1403
1404    #[test]
1405    fn test_version_index_default() {
1406        let index = VersionIndex::default();
1407        assert!(index.is_empty());
1408        assert_eq!(index.version_count(), 0);
1409    }
1410
1411    #[test]
1412    fn test_version_index_latest() {
1413        let mut index = VersionIndex::new();
1414        assert!(index.latest().is_none());
1415
1416        index.add_hot(HotVersionRef::new(
1417            EpochId::new(1),
1418            EpochId::new(1),
1419            0,
1420            TransactionId::new(1),
1421        ));
1422        let latest = index.latest().unwrap();
1423        assert!(matches!(latest, VersionRef::Hot(h) if h.epoch == EpochId::new(1)));
1424
1425        index.add_hot(HotVersionRef::new(
1426            EpochId::new(5),
1427            EpochId::new(5),
1428            100,
1429            TransactionId::new(2),
1430        ));
1431        let latest = index.latest().unwrap();
1432        assert!(matches!(latest, VersionRef::Hot(h) if h.epoch == EpochId::new(5)));
1433    }
1434}