use super::*;
pub(super) const INPUT_FINGERPRINT_DOMAIN: &[u8] = b"distributed.projection.input.v1\0";
pub(super) const FAILURE_FINGERPRINT_DOMAIN: &[u8] = b"distributed.projection.failure.v1\0";
pub(super) const MAX_MESSAGE_ID_BYTES: usize = 255;
pub(super) const MAX_CAUSATION_ID_BYTES: usize = 128;
pub(super) const MAX_FAILURE_ID_BYTES: usize = 255;
pub(super) const MAX_FAILURE_CODE_BYTES: usize = 255;
pub(super) const MAX_FAILURE_DETAIL_BYTES: usize = 1024 * 1024;
pub(crate) const MAX_PROJECTION_QUERY_CHECKPOINT_PROBES: usize = 128;
pub(crate) const MAX_PROJECTION_QUERY_BATCH_ROWS: usize = 4_096;
pub(crate) const MAX_PROJECTION_QUERY_BATCH_CHECKPOINT_PROBES: usize = 4_096;
pub(crate) const MAX_PROJECTION_EVIDENCE_BATCH_ITEMS: usize = 128;
pub const DEFAULT_MAX_RETAINED_PROJECTION_CHANGES: u64 = 4_096;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ProjectionChangeRetention(NonZeroU64);
impl ProjectionChangeRetention {
pub fn new(max_retained_changes: u64) -> Result<Self, ProjectionProtocolValidationError> {
if max_retained_changes > MAX_PROJECTION_POSITION {
return Err(ProjectionProtocolValidationError::TooLarge {
field: "projection retained change count",
value: max_retained_changes,
max: MAX_PROJECTION_POSITION,
});
}
NonZeroU64::new(max_retained_changes).map(Self).ok_or(
ProjectionProtocolValidationError::Zero {
field: "projection retained change count",
},
)
}
pub fn max_retained_changes(self) -> u64 {
self.0.get()
}
}
impl Default for ProjectionChangeRetention {
fn default() -> Self {
Self(
NonZeroU64::new(DEFAULT_MAX_RETAINED_PROJECTION_CHANGES)
.expect("the default projection change retention is nonzero"),
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ProjectionGeneration(NonZeroU64);
impl ProjectionGeneration {
pub fn new(value: u64) -> Result<Self, ProjectionProtocolValidationError> {
if value > MAX_PROJECTION_POSITION {
return Err(ProjectionProtocolValidationError::TooLarge {
field: "projection generation",
value,
max: MAX_PROJECTION_POSITION,
});
}
NonZeroU64::new(value)
.map(Self)
.ok_or(ProjectionProtocolValidationError::Zero {
field: "projection generation",
})
}
pub fn initial() -> Self {
Self(NonZeroU64::MIN)
}
pub fn get(self) -> u64 {
self.0.get()
}
pub fn checked_next(self) -> Result<Self, ProjectionProtocolError> {
let next = self
.get()
.checked_add(1)
.ok_or(ProjectionProtocolError::PositionOverflow {
domain: "projection generation",
})?;
Self::new(next).map_err(|_| ProjectionProtocolError::PositionOverflow {
domain: "projection generation",
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ProjectionInputFingerprint([u8; 32]);
impl ProjectionInputFingerprint {
pub fn from_canonical_bytes(bytes: &[u8]) -> Self {
Self(domain_separated_digest(INPUT_FINGERPRINT_DOMAIN, bytes))
}
pub(crate) fn from_digest(digest: [u8; 32]) -> Self {
Self(digest)
}
pub fn digest(self) -> [u8; 32] {
self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct TrustedProjectionInput {
pub(crate) cursor: ProjectionInputCursor,
pub(crate) fingerprint: ProjectionInputFingerprint,
pub(crate) message_id: String,
pub(crate) causation_id: String,
pub(crate) generation: ProjectionGeneration,
pub(crate) gap_free: bool,
}
impl TrustedProjectionInput {
pub(crate) fn mint(
cursor: ProjectionInputCursor,
fingerprint: ProjectionInputFingerprint,
message_id: impl Into<String>,
causation_id: impl Into<String>,
generation: ProjectionGeneration,
gap_free: bool,
) -> Result<Self, ProjectionProtocolError> {
let message_id = bounded_opaque("projection message ID", message_id, MAX_MESSAGE_ID_BYTES)?;
let causation_id = bounded_opaque(
"projection causation ID",
causation_id,
MAX_CAUSATION_ID_BYTES,
)?;
Ok(Self {
cursor,
fingerprint,
message_id,
causation_id,
generation,
gap_free,
})
}
pub(crate) fn inbox_receipt(&self) -> InboxReceipt {
InboxReceipt::new(self.consumer_name(), self.message_id.clone())
}
pub(super) fn consumer_name(&self) -> String {
format!(
"projection:v1:{}:{}:{}",
digest_hex(&self.cursor.topology().digest()),
digest_hex(&self.cursor.projection_partition().digest()),
self.generation.get(),
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ProjectionInputDisposition {
Pending,
Duplicate(ProjectionCheckpoint),
Stale(ProjectionCheckpoint),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ProjectionModelOwnership {
pub(crate) model: String,
pub(crate) table: String,
}
impl ProjectionModelOwnership {
pub(crate) fn new(
model: impl Into<String>,
table: impl Into<String>,
) -> Result<Self, ProjectionProtocolError> {
Ok(Self {
model: bounded_name("projection model", model, 255)?,
table: bounded_name("projection table", table, 255)?,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[expect(
clippy::large_enum_variant,
reason = "bounded staged mutations carry the exact canonical revision inline for atomic validation"
)]
pub(crate) enum ProjectionRecordExpectation {
Missing,
Exact(RecordRevision),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ProjectionMutationKind {
Upsert,
Delete,
Recreate,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ProjectionRecordMutation {
pub(crate) scope: ProjectionRecordScope,
pub(crate) mutation: TableMutation,
pub(crate) expectation: ProjectionRecordExpectation,
pub(crate) kind: ProjectionMutationKind,
}
impl ProjectionRecordMutation {
pub(crate) fn new(
scope: ProjectionRecordScope,
mutation: TableMutation,
expectation: ProjectionRecordExpectation,
kind: ProjectionMutationKind,
) -> Result<Self, ProjectionProtocolError> {
if let ProjectionRecordExpectation::Exact(revision) = &expectation {
if revision.scope() != &scope {
return Err(ProjectionProtocolError::ScopeMismatch {
field: "projection record expectation",
});
}
}
let is_delete = matches!(mutation, TableMutation::DeleteRow(_));
if is_delete != matches!(kind, ProjectionMutationKind::Delete) {
return Err(ProjectionProtocolError::InvalidBatch(
"projection delete kind and table mutation disagree".into(),
));
}
if matches!(
kind,
ProjectionMutationKind::Delete | ProjectionMutationKind::Recreate
) && !matches!(expectation, ProjectionRecordExpectation::Exact(_))
{
return Err(ProjectionProtocolError::InvalidBatch(
"projection delete/recreate requires an exact record revision".into(),
));
}
Ok(Self {
scope,
mutation,
expectation,
kind,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ProjectionObservationKind {
Record,
Dependency,
}
impl ProjectionObservationKind {
pub(crate) fn as_storage_str(self) -> &'static str {
match self {
Self::Record => "record",
Self::Dependency => "dependency",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ProjectionObservationTarget {
StagedRecord(ProjectionRecordScope),
ExistingRecord(RecordRevision),
Dependency(ProjectionRecordScope),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ProjectionObservationRequest {
pub(crate) kind: ProjectionObservationKind,
pub(crate) target: ProjectionObservationTarget,
}
impl ProjectionObservationRequest {
pub(crate) fn scope(&self) -> &ProjectionRecordScope {
match &self.target {
ProjectionObservationTarget::StagedRecord(scope)
| ProjectionObservationTarget::Dependency(scope) => scope,
ProjectionObservationTarget::ExistingRecord(revision) => revision.scope(),
}
}
}