Skip to main content

riffdb_types/
projection_source.rs

1//! Structural replay-source locators. A matching tuple is never authority.
2
3use std::num::NonZeroU64;
4
5use crate::DatabaseId;
6
7/// Maximum complete ADR-0316 descriptor/root/maintenance/control envelope.
8pub const MAX_PROJECTION_SOURCE_CONTROL_BYTES: usize = 4 * 1024;
9
10/// Coordinator-allocated nonzero checkpoint ordinal within one allocation incarnation.
11#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct ProjectionCheckpointOrdinal(NonZeroU64);
13
14impl ProjectionCheckpointOrdinal {
15    /// Reconstructs a nonzero ordinal; this does not grant allocation authority.
16    #[must_use]
17    pub const fn new(value: u64) -> Option<Self> {
18        match NonZeroU64::new(value) {
19            Some(value) => Some(Self(value)),
20            None => None,
21        }
22    }
23
24    /// Initial position of a checked new-incarnation allocator.
25    #[must_use]
26    pub const fn first() -> Self {
27        Self(NonZeroU64::MIN)
28    }
29
30    /// Numeric ordinal, never a zero sentinel.
31    #[must_use]
32    pub const fn get(self) -> u64 {
33        self.0.get()
34    }
35
36    /// Canonical ordered numeric representation.
37    #[must_use]
38    pub const fn to_be_bytes(self) -> [u8; 8] {
39        self.get().to_be_bytes()
40    }
41
42    /// Checked successor; exhaustion never wraps or reuses an ordinal.
43    #[must_use]
44    pub const fn checked_next(self) -> Option<Self> {
45        match self.get().checked_add(1) {
46            Some(value) => Self::new(value),
47            None => None,
48        }
49    }
50}
51
52/// Full structural locator of one authoritative replay source.
53///
54/// The allocation incarnation belongs to the original allocation, including
55/// after checked restore/promotion/bootstrap. Matching fields do not establish
56/// transition validity or cross-fork uniqueness; references also require the
57/// immutable digest/origin commitment and verified source-transition evidence.
58#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
59pub struct ProjectionSourceLocatorV1 {
60    database_id: DatabaseId,
61    allocation_history_incarnation: NonZeroU64,
62    ordinal: ProjectionCheckpointOrdinal,
63}
64
65impl ProjectionSourceLocatorV1 {
66    /// Reconstructs the locator without changing its inherited origin identity.
67    #[must_use]
68    pub const fn new(
69        database_id: DatabaseId,
70        allocation_history_incarnation: u64,
71        ordinal: ProjectionCheckpointOrdinal,
72    ) -> Option<Self> {
73        match NonZeroU64::new(allocation_history_incarnation) {
74            Some(allocation_history_incarnation) => Some(Self {
75                database_id,
76                allocation_history_incarnation,
77                ordinal,
78            }),
79            None => None,
80        }
81    }
82
83    /// Permanent database identity of the original allocation.
84    #[must_use]
85    pub const fn database_id(self) -> DatabaseId {
86        self.database_id
87    }
88
89    /// Nonzero incarnation in which the coordinator assigned the ordinal.
90    #[must_use]
91    pub const fn allocation_history_incarnation(self) -> u64 {
92        self.allocation_history_incarnation.get()
93    }
94
95    /// Original nonzero allocation ordinal.
96    #[must_use]
97    pub const fn ordinal(self) -> ProjectionCheckpointOrdinal {
98        self.ordinal
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::{ContentDigest, DIGEST_SCHEME_V1, HashDomain, hash};
106
107    fn database_id(seed: u8) -> DatabaseId {
108        DatabaseId::from_unix_milliseconds_and_random(42, [seed; 10]).unwrap()
109    }
110
111    // req: STO-012, REC-001
112    #[test]
113    fn replay_source_locator_preserves_full_original_allocation_identity() {
114        let database = database_id(3);
115        let ordinal = ProjectionCheckpointOrdinal::first();
116        assert!(ProjectionCheckpointOrdinal::new(0).is_none());
117        assert!(ProjectionSourceLocatorV1::new(database, 0, ordinal).is_none());
118        let original = ProjectionSourceLocatorV1::new(database, 4, ordinal).unwrap();
119        let later_allocation = ProjectionSourceLocatorV1::new(database, 5, ordinal).unwrap();
120        let foreign = ProjectionSourceLocatorV1::new(database_id(4), 4, ordinal).unwrap();
121        assert_ne!(original, later_allocation);
122        assert_ne!(original, foreign);
123        let captured_before_restore = original;
124        assert_eq!(captured_before_restore.allocation_history_incarnation(), 4);
125        assert_eq!(captured_before_restore.ordinal(), ordinal);
126        assert_eq!(captured_before_restore.database_id(), database);
127    }
128
129    // req: STO-012
130    #[test]
131    fn replay_checkpoint_ordinal_exhaustion_never_wraps() {
132        let last = ProjectionCheckpointOrdinal::new(u64::MAX).unwrap();
133        assert!(last.checked_next().is_none());
134        let prior = ProjectionCheckpointOrdinal::new(u64::MAX - 1).unwrap();
135        assert_eq!(prior.checked_next(), Some(last));
136        assert!(prior.to_be_bytes() < last.to_be_bytes());
137    }
138
139    // req: PRJ-004, REC-003
140    #[test]
141    fn replay_source_hash_roles_are_separate_and_stored_scheme_is_checked() {
142        let input = b"same bounded canonical preimage";
143        let rows = hash(HashDomain::ProjectionSourceRows, input);
144        let descriptor = hash(HashDomain::ProjectionSourceDescriptor, input);
145        let origin = hash(HashDomain::ProjectionSourceOrigin, input);
146        assert_ne!(rows, descriptor);
147        assert_ne!(descriptor, origin);
148        assert_ne!(rows, origin);
149        assert_eq!(
150            ContentDigest::from_stored_parts(DIGEST_SCHEME_V1, rows.domain(), *rows.as_bytes()),
151            Some(rows)
152        );
153        assert!(ContentDigest::from_stored_parts(0, rows.domain(), *rows.as_bytes()).is_none());
154        assert!(ContentDigest::from_stored_parts(2, rows.domain(), *rows.as_bytes()).is_none());
155    }
156}