riffdb_types/
projection_source.rs1use std::num::NonZeroU64;
4
5use crate::DatabaseId;
6
7pub const MAX_PROJECTION_SOURCE_CONTROL_BYTES: usize = 4 * 1024;
9
10#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct ProjectionCheckpointOrdinal(NonZeroU64);
13
14impl ProjectionCheckpointOrdinal {
15 #[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 #[must_use]
26 pub const fn first() -> Self {
27 Self(NonZeroU64::MIN)
28 }
29
30 #[must_use]
32 pub const fn get(self) -> u64 {
33 self.0.get()
34 }
35
36 #[must_use]
38 pub const fn to_be_bytes(self) -> [u8; 8] {
39 self.get().to_be_bytes()
40 }
41
42 #[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#[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 #[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 #[must_use]
85 pub const fn database_id(self) -> DatabaseId {
86 self.database_id
87 }
88
89 #[must_use]
91 pub const fn allocation_history_incarnation(self) -> u64 {
92 self.allocation_history_incarnation.get()
93 }
94
95 #[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 #[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 #[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 #[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}