use std::collections::BTreeSet;
use std::fmt;
use std::marker::PhantomData;
use serde::{Serialize, Serializer};
use crate::projection_protocol::{MAX_PROJECTION_PARTITION_BYTES, MAX_PROJECTION_RECORD_KEY_BYTES};
use crate::DomainEventOccurrence;
use super::canonical::{canonical_json_bytes, digest_program};
use super::expression::{
expressions_statically_distinct, non_empty, validate_named_ordinals, validate_ordinals,
};
use super::{
ProjectionAssignment, ProjectionEventSelector, ProjectionExpression, ProjectionInvalidation,
ProjectionProgramError, ProjectionRelationship, ProjectionTarget, ResolvedProjectionPlan,
};
use super::{MAX_PROJECTION_EXPRESSION_DEPTH, MAX_PROJECTION_PATH_SEGMENTS};
pub const PROJECTION_PROGRAM_IR_VERSION: u16 = 1;
pub const PROJECTION_OPERATION_SEMANTICS_VERSION: u16 = 1;
pub const MAX_PROJECTION_OPERATIONS_PER_OCCURRENCE: usize = 128;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub struct ProjectionProgramLimits {
expression_value_levels: u16,
path_segments: u16,
operations_per_occurrence: u16,
key_bytes: u32,
partition_bytes: u32,
}
impl ProjectionProgramLimits {
fn version_one() -> Self {
Self {
expression_value_levels: MAX_PROJECTION_EXPRESSION_DEPTH as u16,
path_segments: MAX_PROJECTION_PATH_SEGMENTS as u16,
operations_per_occurrence: MAX_PROJECTION_OPERATIONS_PER_OCCURRENCE as u16,
key_bytes: MAX_PROJECTION_RECORD_KEY_BYTES as u32,
partition_bytes: MAX_PROJECTION_PARTITION_BYTES as u32,
}
}
pub fn expression_value_levels(&self) -> u16 {
self.expression_value_levels
}
pub fn path_segments(&self) -> u16 {
self.path_segments
}
pub fn operations_per_occurrence(&self) -> u16 {
self.operations_per_occurrence
}
pub fn key_bytes(&self) -> u32 {
self.key_bytes
}
pub fn partition_bytes(&self) -> u32 {
self.partition_bytes
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(tag = "kind", content = "expression", rename_all = "snake_case")]
pub enum ProjectionPartition {
Unit,
Expression(ProjectionExpression),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ProjectionKeyField {
ordinal: u32,
name: String,
expression: ProjectionExpression,
}
impl ProjectionKeyField {
pub fn try_new(
ordinal: u32,
name: impl Into<String>,
expression: ProjectionExpression,
) -> Result<Self, ProjectionProgramError> {
Ok(Self {
ordinal,
name: non_empty(name.into(), "projection key field")?,
expression,
})
}
pub fn ordinal(&self) -> u32 {
self.ordinal
}
pub fn name(&self) -> &str {
&self.name
}
pub fn expression(&self) -> &ProjectionExpression {
&self.expression
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ProjectionField {
ordinal: u32,
name: String,
assignment: ProjectionAssignment,
}
impl ProjectionField {
pub fn try_new(
ordinal: u32,
name: impl Into<String>,
assignment: ProjectionAssignment,
) -> Result<Self, ProjectionProgramError> {
Ok(Self {
ordinal,
name: non_empty(name.into(), "projection field")?,
assignment,
})
}
pub fn ordinal(&self) -> u32 {
self.ordinal
}
pub fn name(&self) -> &str {
&self.name
}
pub fn assignment(&self) -> &ProjectionAssignment {
&self.assignment
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProjectionMutationKind {
Insert,
Upsert,
Patch,
UpsertPatch,
Delete,
Recreate,
InsertRelated,
UpsertRelated,
}
impl ProjectionMutationKind {
pub(crate) fn is_complete_write(self) -> bool {
matches!(
self,
Self::Insert
| Self::Upsert
| Self::Recreate
| Self::InsertRelated
| Self::UpsertRelated
)
}
pub(crate) fn is_patch(self) -> bool {
matches!(self, Self::Patch | Self::UpsertPatch)
}
fn is_related(self) -> bool {
matches!(self, Self::InsertRelated | Self::UpsertRelated)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProjectionRelationshipEffectKind {
Link,
Unlink,
Invalidate,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ProjectionRelationshipEffect {
ordinal: u32,
kind: ProjectionRelationshipEffectKind,
relationship: ProjectionRelationship,
source_key: Vec<ProjectionKeyField>,
target_key: Vec<ProjectionKeyField>,
}
impl ProjectionRelationshipEffect {
pub fn link(
ordinal: u32,
relationship: ProjectionRelationship,
source_key: Vec<ProjectionKeyField>,
target_key: Vec<ProjectionKeyField>,
) -> Result<Self, ProjectionProgramError> {
Self::with_keys(
ordinal,
ProjectionRelationshipEffectKind::Link,
relationship,
source_key,
target_key,
)
}
pub fn unlink(
ordinal: u32,
relationship: ProjectionRelationship,
source_key: Vec<ProjectionKeyField>,
target_key: Vec<ProjectionKeyField>,
) -> Result<Self, ProjectionProgramError> {
Self::with_keys(
ordinal,
ProjectionRelationshipEffectKind::Unlink,
relationship,
source_key,
target_key,
)
}
pub fn invalidate(
ordinal: u32,
relationship: ProjectionRelationship,
mut source_key: Vec<ProjectionKeyField>,
) -> Result<Self, ProjectionProgramError> {
source_key.sort_by_key(ProjectionKeyField::ordinal);
validate_named_ordinals(
&source_key,
"relationship invalidation source key",
ProjectionKeyField::ordinal,
ProjectionKeyField::name,
)?;
if source_key.is_empty() {
return Err(ProjectionProgramError::InvalidOperation {
operation: "relationship invalidation".to_owned(),
reason: "relationship invalidation requires a complete source key".to_owned(),
});
}
Ok(Self {
ordinal,
kind: ProjectionRelationshipEffectKind::Invalidate,
relationship,
source_key,
target_key: Vec::new(),
})
}
fn with_keys(
ordinal: u32,
kind: ProjectionRelationshipEffectKind,
relationship: ProjectionRelationship,
mut source_key: Vec<ProjectionKeyField>,
mut target_key: Vec<ProjectionKeyField>,
) -> Result<Self, ProjectionProgramError> {
source_key.sort_by_key(ProjectionKeyField::ordinal);
target_key.sort_by_key(ProjectionKeyField::ordinal);
validate_named_ordinals(
&source_key,
"relationship source key",
ProjectionKeyField::ordinal,
ProjectionKeyField::name,
)?;
validate_named_ordinals(
&target_key,
"relationship target key",
ProjectionKeyField::ordinal,
ProjectionKeyField::name,
)?;
if source_key.is_empty() || target_key.is_empty() {
return Err(ProjectionProgramError::InvalidOperation {
operation: "relationship effect".to_owned(),
reason: "link and unlink require complete source and target keys".to_owned(),
});
}
Ok(Self {
ordinal,
kind,
relationship,
source_key,
target_key,
})
}
pub fn ordinal(&self) -> u32 {
self.ordinal
}
pub fn kind(&self) -> ProjectionRelationshipEffectKind {
self.kind
}
pub fn relationship(&self) -> &ProjectionRelationship {
&self.relationship
}
pub fn source_key(&self) -> &[ProjectionKeyField] {
&self.source_key
}
pub fn target_key(&self) -> &[ProjectionKeyField] {
&self.target_key
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ProjectionOperation {
operation_id: String,
staging_ordinal: u32,
kind: ProjectionMutationKind,
target: ProjectionTarget,
key: Vec<ProjectionKeyField>,
fields: Vec<ProjectionField>,
relationship_effects: Vec<ProjectionRelationshipEffect>,
invalidations: Vec<ProjectionInvalidation>,
}
impl ProjectionOperation {
#[allow(clippy::too_many_arguments)]
pub fn try_new(
operation_id: impl Into<String>,
staging_ordinal: u32,
kind: ProjectionMutationKind,
target: ProjectionTarget,
mut key: Vec<ProjectionKeyField>,
mut fields: Vec<ProjectionField>,
mut relationship_effects: Vec<ProjectionRelationshipEffect>,
mut invalidations: Vec<ProjectionInvalidation>,
) -> Result<Self, ProjectionProgramError> {
let operation_id = non_empty(operation_id.into(), "projection operation ID")?;
key.sort_by_key(ProjectionKeyField::ordinal);
fields.sort_by_key(ProjectionField::ordinal);
validate_named_ordinals(
&key,
"projection key field",
ProjectionKeyField::ordinal,
ProjectionKeyField::name,
)?;
validate_named_ordinals(
&fields,
"projection field",
ProjectionField::ordinal,
ProjectionField::name,
)?;
if key.is_empty() {
return Err(ProjectionProgramError::InvalidOperation {
operation: operation_id,
reason: "a complete logical key is required".to_owned(),
});
}
if kind == ProjectionMutationKind::Delete && !fields.is_empty() {
return Err(ProjectionProgramError::InvalidOperation {
operation: operation_id,
reason: "delete cannot declare fields".to_owned(),
});
}
if kind != ProjectionMutationKind::Delete && fields.is_empty() {
return Err(ProjectionProgramError::InvalidOperation {
operation: operation_id,
reason: "a write operation requires at least one field".to_owned(),
});
}
if kind.is_complete_write()
&& fields
.iter()
.any(|field| field.assignment == ProjectionAssignment::Unset)
{
return Err(ProjectionProgramError::InvalidOperation {
operation: operation_id,
reason: "complete-row writes cannot unset fields".to_owned(),
});
}
if kind.is_complete_write() {
for key_field in &key {
let Some(field) = fields.iter().find(|field| field.name() == key_field.name())
else {
continue;
};
if !matches!(
field.assignment(),
ProjectionAssignment::Set(expression)
if expression == key_field.expression()
) {
return Err(ProjectionProgramError::InvalidOperation {
operation: operation_id,
reason: format!(
"complete-row key field `{}` must use the exact key expression",
key_field.name()
),
});
}
}
}
relationship_effects.sort_by_key(ProjectionRelationshipEffect::ordinal);
validate_ordinals(
&relationship_effects,
"relationship effect",
ProjectionRelationshipEffect::ordinal,
)?;
if kind.is_related() && relationship_effects.is_empty() {
return Err(ProjectionProgramError::InvalidOperation {
operation: operation_id,
reason: "related-row writes require relationship provenance".to_owned(),
});
}
invalidations.sort();
invalidations.dedup();
validate_relationship_invalidations(&operation_id, &relationship_effects, &invalidations)?;
Ok(Self {
operation_id,
staging_ordinal,
kind,
target,
key,
fields,
relationship_effects,
invalidations,
})
}
pub fn operation_id(&self) -> &str {
&self.operation_id
}
pub fn staging_ordinal(&self) -> u32 {
self.staging_ordinal
}
pub fn kind(&self) -> ProjectionMutationKind {
self.kind
}
pub fn target(&self) -> &ProjectionTarget {
&self.target
}
pub fn key(&self) -> &[ProjectionKeyField] {
&self.key
}
pub fn fields(&self) -> &[ProjectionField] {
&self.fields
}
pub fn relationship_effects(&self) -> &[ProjectionRelationshipEffect] {
&self.relationship_effects
}
pub fn invalidations(&self) -> &[ProjectionInvalidation] {
&self.invalidations
}
}
fn validate_relationship_invalidations(
operation_id: &str,
effects: &[ProjectionRelationshipEffect],
invalidations: &[ProjectionInvalidation],
) -> Result<(), ProjectionProgramError> {
let declared = invalidations
.iter()
.filter_map(|invalidation| match invalidation {
ProjectionInvalidation::Relationship {
source_model,
relationship,
target_model,
} => Some((
source_model.as_str(),
relationship.as_str(),
target_model.as_str(),
)),
ProjectionInvalidation::Model { .. } => None,
})
.collect::<BTreeSet<_>>();
let proven = effects
.iter()
.filter(|effect| effect.kind == ProjectionRelationshipEffectKind::Invalidate)
.map(|effect| {
(
effect.relationship.source_model(),
effect.relationship.relationship(),
effect.relationship.target_model(),
)
})
.collect::<BTreeSet<_>>();
if declared != proven {
return Err(ProjectionProgramError::InvalidOperation {
operation: operation_id.to_owned(),
reason: "relationship invalidation inventory must exactly match keyed \
relationship invalidation effects"
.to_owned(),
});
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ProjectionArm {
arm_id: String,
selector: ProjectionEventSelector,
operations: Vec<ProjectionOperation>,
}
impl ProjectionArm {
pub fn try_new(
arm_id: impl Into<String>,
selector: ProjectionEventSelector,
mut operations: Vec<ProjectionOperation>,
) -> Result<Self, ProjectionProgramError> {
let arm_id = non_empty(arm_id.into(), "projection arm ID")?;
if operations.len() > MAX_PROJECTION_OPERATIONS_PER_OCCURRENCE {
return Err(ProjectionProgramError::TooManyOperations {
count: operations.len(),
max: MAX_PROJECTION_OPERATIONS_PER_OCCURRENCE,
});
}
operations.sort_by_key(ProjectionOperation::staging_ordinal);
validate_named_ordinals(
&operations,
"projection operation",
ProjectionOperation::staging_ordinal,
ProjectionOperation::operation_id,
)?;
validate_static_ambiguity(&operations)?;
Ok(Self {
arm_id,
selector,
operations,
})
}
pub fn arm_id(&self) -> &str {
&self.arm_id
}
pub fn selector(&self) -> &ProjectionEventSelector {
&self.selector
}
pub fn operations(&self) -> &[ProjectionOperation] {
&self.operations
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ProjectionProgram {
ir_version: u16,
operation_semantics_version: u16,
limits: ProjectionProgramLimits,
name: String,
version: u64,
partition: ProjectionPartition,
arms: Vec<ProjectionArm>,
}
impl ProjectionProgram {
pub fn try_new(
name: impl Into<String>,
version: u64,
partition: ProjectionPartition,
mut arms: Vec<ProjectionArm>,
) -> Result<Self, ProjectionProgramError> {
let name = non_empty(name.into(), "projection program name")?;
if version == 0 {
return Err(ProjectionProgramError::ZeroVersion(
"projection program version",
));
}
if arms.is_empty() {
return Err(ProjectionProgramError::InvalidOperation {
operation: name,
reason: "a program requires at least one event arm".to_owned(),
});
}
arms.sort_by(|left, right| {
left.selector
.canonical_cmp(&right.selector)
.then_with(|| left.arm_id.cmp(&right.arm_id))
});
for pair in arms.windows(2) {
if pair[0].selector == pair[1].selector {
return Err(ProjectionProgramError::DuplicateSelector);
}
}
Ok(Self {
ir_version: PROJECTION_PROGRAM_IR_VERSION,
operation_semantics_version: PROJECTION_OPERATION_SEMANTICS_VERSION,
limits: ProjectionProgramLimits::version_one(),
name,
version,
partition,
arms,
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn ir_version(&self) -> u16 {
self.ir_version
}
pub fn operation_semantics_version(&self) -> u16 {
self.operation_semantics_version
}
pub fn limits(&self) -> ProjectionProgramLimits {
self.limits
}
pub fn version(&self) -> u64 {
self.version
}
pub fn partition(&self) -> &ProjectionPartition {
&self.partition
}
pub fn arms(&self) -> &[ProjectionArm] {
&self.arms
}
pub fn canonical_bytes(&self) -> Result<Vec<u8>, ProjectionProgramError> {
canonical_json_bytes(self)
}
pub fn id(&self) -> Result<ProjectionProgramId, ProjectionProgramError> {
Ok(ProjectionProgramId(digest_program(
&self.canonical_bytes()?,
)))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProjectionProgramId([u8; 32]);
impl ProjectionProgramId {
pub fn parse(value: &str) -> Result<Self, ProjectionProgramError> {
let Some(hex) = value.strip_prefix("pp1:sha256:") else {
return Err(ProjectionProgramError::InvalidProgramId);
};
if hex.len() != 64
|| !hex
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return Err(ProjectionProgramError::InvalidProgramId);
}
let mut bytes = [0_u8; 32];
for (index, pair) in hex.as_bytes().chunks_exact(2).enumerate() {
let high = hex_nibble(pair[0]).ok_or(ProjectionProgramError::InvalidProgramId)?;
let low = hex_nibble(pair[1]).ok_or(ProjectionProgramError::InvalidProgramId)?;
bytes[index] = (high << 4) | low;
}
Ok(Self(bytes))
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Display for ProjectionProgramId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("pp1:sha256:")?;
for byte in self.0 {
write!(formatter, "{byte:02x}")?;
}
Ok(())
}
}
impl Serialize for ProjectionProgramId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[derive(Clone, Debug)]
pub struct ProjectionPlanTemplate<E: ProjectionEventSet> {
program: ProjectionProgram,
marker: PhantomData<fn() -> E>,
}
pub trait ProjectionEventSet {
fn projection_event_selectors() -> Result<Vec<ProjectionEventSelector>, ProjectionProgramError>;
}
impl<E: ProjectionEventSet> ProjectionPlanTemplate<E> {
pub fn try_new(program: ProjectionProgram) -> Result<Self, ProjectionProgramError> {
program.canonical_bytes()?;
let mut expected = E::projection_event_selectors()?;
expected.sort_by(ProjectionEventSelector::canonical_cmp);
let actual = program
.arms()
.iter()
.map(|arm| arm.selector().clone())
.collect::<Vec<_>>();
if expected != actual {
return Err(ProjectionProgramError::EventSetMismatch);
}
Ok(Self {
program,
marker: PhantomData,
})
}
pub fn program(&self) -> &ProjectionProgram {
&self.program
}
pub fn resolve(
&self,
occurrence: &DomainEventOccurrence,
) -> Result<ResolvedProjectionPlan, ProjectionProgramError> {
ResolvedProjectionPlan::resolve(&self.program, occurrence)
}
}
fn validate_static_ambiguity(
operations: &[ProjectionOperation],
) -> Result<(), ProjectionProgramError> {
for (index, left) in operations.iter().enumerate() {
for right in &operations[index + 1..] {
if left.target != right.target {
continue;
}
if left.key == right.key {
let compatible = left.kind == right.kind
&& left.relationship_effects == right.relationship_effects
&& if left.kind.is_patch() {
left.fields.iter().all(|left_field| {
right
.fields
.iter()
.find(|right_field| right_field.name == left_field.name)
.is_none_or(|right_field| {
right_field.assignment == left_field.assignment
})
})
} else {
same_fields_ignoring_operation(&left.fields, &right.fields)
};
if !compatible {
return Err(ProjectionProgramError::AmbiguousMutation {
model: left.target.model().to_owned(),
reason: format!(
"operations `{}` and `{}` statically target the same key \
with order-dependent semantics",
left.operation_id, right.operation_id
),
});
}
} else if !keys_statically_disjoint(&left.key, &right.key) {
return Err(ProjectionProgramError::AmbiguousMutation {
model: left.target.model().to_owned(),
reason: format!(
"operations `{}` and `{}` have dynamic keys whose overlap \
cannot be disproved at registration",
left.operation_id, right.operation_id
),
});
}
}
}
Ok(())
}
fn keys_statically_disjoint(left: &[ProjectionKeyField], right: &[ProjectionKeyField]) -> bool {
left.len() == right.len()
&& left.iter().zip(right).any(|(left, right)| {
left.ordinal == right.ordinal
&& left.name == right.name
&& expressions_statically_distinct(&left.expression, &right.expression)
})
}
fn same_fields_ignoring_operation(left: &[ProjectionField], right: &[ProjectionField]) -> bool {
left.len() == right.len()
&& left.iter().zip(right).all(|(left, right)| {
left.ordinal == right.ordinal
&& left.name == right.name
&& left.assignment == right.assignment
})
}
fn hex_nibble(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
_ => None,
}
}