use std::num::NonZeroU64;
use crate::DatabaseId;
pub const MAX_PROJECTION_SOURCE_CONTROL_BYTES: usize = 4 * 1024;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ProjectionCheckpointOrdinal(NonZeroU64);
impl ProjectionCheckpointOrdinal {
#[must_use]
pub const fn new(value: u64) -> Option<Self> {
match NonZeroU64::new(value) {
Some(value) => Some(Self(value)),
None => None,
}
}
#[must_use]
pub const fn first() -> Self {
Self(NonZeroU64::MIN)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0.get()
}
#[must_use]
pub const fn to_be_bytes(self) -> [u8; 8] {
self.get().to_be_bytes()
}
#[must_use]
pub const fn checked_next(self) -> Option<Self> {
match self.get().checked_add(1) {
Some(value) => Self::new(value),
None => None,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ProjectionSourceLocatorV1 {
database_id: DatabaseId,
allocation_history_incarnation: NonZeroU64,
ordinal: ProjectionCheckpointOrdinal,
}
impl ProjectionSourceLocatorV1 {
#[must_use]
pub const fn new(
database_id: DatabaseId,
allocation_history_incarnation: u64,
ordinal: ProjectionCheckpointOrdinal,
) -> Option<Self> {
match NonZeroU64::new(allocation_history_incarnation) {
Some(allocation_history_incarnation) => Some(Self {
database_id,
allocation_history_incarnation,
ordinal,
}),
None => None,
}
}
#[must_use]
pub const fn database_id(self) -> DatabaseId {
self.database_id
}
#[must_use]
pub const fn allocation_history_incarnation(self) -> u64 {
self.allocation_history_incarnation.get()
}
#[must_use]
pub const fn ordinal(self) -> ProjectionCheckpointOrdinal {
self.ordinal
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ContentDigest, DIGEST_SCHEME_V1, HashDomain, hash};
fn database_id(seed: u8) -> DatabaseId {
DatabaseId::from_unix_milliseconds_and_random(42, [seed; 10]).unwrap()
}
#[test]
fn replay_source_locator_preserves_full_original_allocation_identity() {
let database = database_id(3);
let ordinal = ProjectionCheckpointOrdinal::first();
assert!(ProjectionCheckpointOrdinal::new(0).is_none());
assert!(ProjectionSourceLocatorV1::new(database, 0, ordinal).is_none());
let original = ProjectionSourceLocatorV1::new(database, 4, ordinal).unwrap();
let later_allocation = ProjectionSourceLocatorV1::new(database, 5, ordinal).unwrap();
let foreign = ProjectionSourceLocatorV1::new(database_id(4), 4, ordinal).unwrap();
assert_ne!(original, later_allocation);
assert_ne!(original, foreign);
let captured_before_restore = original;
assert_eq!(captured_before_restore.allocation_history_incarnation(), 4);
assert_eq!(captured_before_restore.ordinal(), ordinal);
assert_eq!(captured_before_restore.database_id(), database);
}
#[test]
fn replay_checkpoint_ordinal_exhaustion_never_wraps() {
let last = ProjectionCheckpointOrdinal::new(u64::MAX).unwrap();
assert!(last.checked_next().is_none());
let prior = ProjectionCheckpointOrdinal::new(u64::MAX - 1).unwrap();
assert_eq!(prior.checked_next(), Some(last));
assert!(prior.to_be_bytes() < last.to_be_bytes());
}
#[test]
fn replay_source_hash_roles_are_separate_and_stored_scheme_is_checked() {
let input = b"same bounded canonical preimage";
let rows = hash(HashDomain::ProjectionSourceRows, input);
let descriptor = hash(HashDomain::ProjectionSourceDescriptor, input);
let origin = hash(HashDomain::ProjectionSourceOrigin, input);
assert_ne!(rows, descriptor);
assert_ne!(descriptor, origin);
assert_ne!(rows, origin);
assert_eq!(
ContentDigest::from_stored_parts(DIGEST_SCHEME_V1, rows.domain(), *rows.as_bytes()),
Some(rows)
);
assert!(ContentDigest::from_stored_parts(0, rows.domain(), *rows.as_bytes()).is_none());
assert!(ContentDigest::from_stored_parts(2, rows.domain(), *rows.as_bytes()).is_none());
}
}