Skip to main content

distributed/projection/
plan.rs

1use std::collections::BTreeMap;
2
3use serde::Serialize;
4use serde_json::Value;
5
6use crate::projection_protocol::{MAX_PROJECTION_PARTITION_BYTES, MAX_PROJECTION_RECORD_KEY_BYTES};
7use crate::DomainEventOccurrence;
8
9use super::canonical::{bounded_key_bytes, bounded_partition_bytes, canonical_json_bytes};
10use super::{
11    ProjectionArm, ProjectionInvalidation, ProjectionKeyField, ProjectionMutationKind,
12    ProjectionOperation, ProjectionPartition, ProjectionProgram, ProjectionProgramError,
13    ProjectionProgramId, ProjectionRelationship, ProjectionRelationshipEffectKind,
14    ProjectionTarget, ProjectionValue, ResolvedProjectionValue,
15};
16
17/// Resolved logical partition and its bounded canonical encoding.
18#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
19pub struct ResolvedProjectionPartition {
20    partition: ResolvedProjectionPartitionValue,
21    canonical_bytes: Vec<u8>,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
25#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
26enum ResolvedProjectionPartitionValue {
27    Unit,
28    Value(ProjectionValue),
29}
30
31/// Borrowed view of a resolved unit or value partition.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum ResolvedProjectionPartitionRef<'a> {
34    /// The declaration uses the distinguished unit partition.
35    Unit,
36    /// The declaration resolved an explicit partition value.
37    Value(&'a ProjectionValue),
38}
39
40impl ResolvedProjectionPartition {
41    /// Return the distinguished unit tag or concrete logical value.
42    pub fn as_ref(&self) -> ResolvedProjectionPartitionRef<'_> {
43        match &self.partition {
44            ResolvedProjectionPartitionValue::Unit => ResolvedProjectionPartitionRef::Unit,
45            ResolvedProjectionPartitionValue::Value(value) => {
46                ResolvedProjectionPartitionRef::Value(value)
47            }
48        }
49    }
50
51    /// Return the bounded domain-separated canonical partition encoding.
52    ///
53    /// This is a portable logical encoding. A physical adapter must lower the
54    /// logical value through its registered model codec; these bytes are not a
55    /// relational table key.
56    pub fn canonical_bytes(&self) -> &[u8] {
57        &self.canonical_bytes
58    }
59}
60
61/// One concrete component of a composite logical key.
62#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
63pub struct ResolvedProjectionKeyField {
64    ordinal: u32,
65    name: String,
66    value: ProjectionValue,
67}
68
69impl ResolvedProjectionKeyField {
70    /// Return the declared component ordinal.
71    pub fn ordinal(&self) -> u32 {
72        self.ordinal
73    }
74
75    /// Return the declared component name.
76    pub fn name(&self) -> &str {
77        &self.name
78    }
79
80    /// Return the resolved scalar or typed-enum value.
81    pub fn value(&self) -> &ProjectionValue {
82        &self.value
83    }
84}
85
86/// A concrete complete key with its bounded canonical encoding.
87#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
88pub struct ResolvedProjectionKey {
89    fields: Vec<ResolvedProjectionKeyField>,
90    canonical_bytes: Vec<u8>,
91}
92
93impl ResolvedProjectionKey {
94    /// Return components in explicit ordinal order.
95    pub fn fields(&self) -> &[ResolvedProjectionKeyField] {
96        &self.fields
97    }
98
99    /// Return the bounded domain-separated canonical key encoding.
100    ///
101    /// This is a portable logical encoding, not a physical ORM key encoding.
102    pub fn canonical_bytes(&self) -> &[u8] {
103        &self.canonical_bytes
104    }
105}
106
107/// Complete portable scope of one final logical record mutation.
108#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
109pub struct ResolvedProjectionMutationScope {
110    partition: ResolvedProjectionPartition,
111    model: String,
112    storage: String,
113    key: ResolvedProjectionKey,
114}
115
116impl ResolvedProjectionMutationScope {
117    /// Return the logical projection partition.
118    pub fn partition(&self) -> &ResolvedProjectionPartition {
119        &self.partition
120    }
121
122    /// Return the logical model name.
123    pub fn model(&self) -> &str {
124        &self.model
125    }
126
127    /// Return the opaque registered storage identity.
128    pub fn storage(&self) -> &str {
129        &self.storage
130    }
131
132    /// Return the complete portable logical key.
133    pub fn key(&self) -> &ResolvedProjectionKey {
134        &self.key
135    }
136}
137
138/// One concrete projected field, retaining source ordinals and presence state.
139#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
140pub struct ResolvedProjectionField {
141    operation_staging_ordinal: u32,
142    field_ordinal: u32,
143    name: String,
144    value: ResolvedProjectionValue,
145}
146
147impl ResolvedProjectionField {
148    /// Return the operation staging ordinal that first supplied this field.
149    pub fn operation_staging_ordinal(&self) -> u32 {
150        self.operation_staging_ordinal
151    }
152
153    /// Return the field ordinal within its source operation.
154    pub fn field_ordinal(&self) -> u32 {
155        self.field_ordinal
156    }
157
158    /// Return the projected field name.
159    pub fn name(&self) -> &str {
160        &self.name
161    }
162
163    /// Return concrete, absent, or explicit-unset state.
164    pub fn value(&self) -> &ResolvedProjectionValue {
165        &self.value
166    }
167}
168
169/// Exact semantic origins retained for one final logical mutation.
170#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
171pub struct ProjectionMutationProvenance {
172    occurrence: ProjectionOccurrenceProvenance,
173    program_id: ProjectionProgramId,
174    arm_id: String,
175    operation_ids: Vec<String>,
176    staging_ordinals: Vec<u32>,
177    relationship_effects: Vec<ResolvedProjectionRelationshipEffect>,
178    invalidations: Vec<ProjectionInvalidation>,
179}
180
181/// One resolved link, unlink, or conservative relationship invalidation.
182#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
183pub struct ResolvedProjectionRelationshipEffect {
184    ordinal: u32,
185    kind: ProjectionRelationshipEffectKind,
186    relationship: ProjectionRelationship,
187    source_key: Option<ResolvedProjectionKey>,
188    target_key: Option<ResolvedProjectionKey>,
189}
190
191impl ResolvedProjectionRelationshipEffect {
192    /// Return the explicit effect ordinal.
193    pub fn ordinal(&self) -> u32 {
194        self.ordinal
195    }
196
197    /// Return link, unlink, or invalidation.
198    pub fn kind(&self) -> ProjectionRelationshipEffectKind {
199        self.kind
200    }
201
202    /// Return the stable relationship descriptor.
203    pub fn relationship(&self) -> &ProjectionRelationship {
204        &self.relationship
205    }
206
207    /// Return the complete source endpoint key for link or unlink.
208    pub fn source_key(&self) -> Option<&ResolvedProjectionKey> {
209        self.source_key.as_ref()
210    }
211
212    /// Return the complete target endpoint key for link or unlink.
213    pub fn target_key(&self) -> Option<&ResolvedProjectionKey> {
214        self.target_key.as_ref()
215    }
216}
217
218/// Retry-stable semantic identity of the occurrence behind one mutation.
219///
220/// Volatile timestamps, tracing, workflow metadata, and delivery state are
221/// excluded. The parent plan retains the complete immutable occurrence.
222#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
223pub struct ProjectionOccurrenceProvenance {
224    occurrence_version: u16,
225    occurrence_id: String,
226    event_name: String,
227    event_version: u64,
228    body_fingerprint: String,
229    aggregate_type: String,
230    aggregate_id: String,
231    aggregate_sequence: u64,
232    publication_ordinal: u32,
233}
234
235impl ProjectionOccurrenceProvenance {
236    /// Return the canonical occurrence-envelope version.
237    pub fn occurrence_version(&self) -> u16 {
238        self.occurrence_version
239    }
240
241    /// Return the retry-stable occurrence ID.
242    pub fn occurrence_id(&self) -> &str {
243        &self.occurrence_id
244    }
245
246    /// Return the semantic event name.
247    pub fn event_name(&self) -> &str {
248        &self.event_name
249    }
250
251    /// Return the semantic event version.
252    pub fn event_version(&self) -> u64 {
253        self.event_version
254    }
255
256    /// Return the canonical event-body schema fingerprint.
257    pub fn body_fingerprint(&self) -> &str {
258        &self.body_fingerprint
259    }
260
261    /// Return the stable aggregate type.
262    pub fn aggregate_type(&self) -> &str {
263        &self.aggregate_type
264    }
265
266    /// Return the stable aggregate stream ID.
267    pub fn aggregate_id(&self) -> &str {
268        &self.aggregate_id
269    }
270
271    /// Return the causing aggregate sequence.
272    pub fn aggregate_sequence(&self) -> u64 {
273        self.aggregate_sequence
274    }
275
276    /// Return the event publication ordinal within that aggregate sequence.
277    pub fn publication_ordinal(&self) -> u32 {
278        self.publication_ordinal
279    }
280}
281
282impl ProjectionMutationProvenance {
283    /// Return stable occurrence identity retained directly by the mutation.
284    pub fn occurrence(&self) -> &ProjectionOccurrenceProvenance {
285        &self.occurrence
286    }
287
288    /// Return the program digest that defined this mutation.
289    pub fn program_id(&self) -> ProjectionProgramId {
290        self.program_id
291    }
292
293    /// Return the selected arm identifier.
294    pub fn arm_id(&self) -> &str {
295        &self.arm_id
296    }
297
298    /// Return contributing operation IDs in staging order.
299    pub fn operation_ids(&self) -> &[String] {
300        &self.operation_ids
301    }
302
303    /// Return contributing source staging ordinals.
304    pub fn staging_ordinals(&self) -> &[u32] {
305        &self.staging_ordinals
306    }
307
308    /// Return resolved relationship consequences in explicit ordinal order.
309    pub fn relationship_effects(&self) -> &[ResolvedProjectionRelationshipEffect] {
310        &self.relationship_effects
311    }
312
313    /// Return canonical affected model and relationship inventory.
314    pub fn invalidations(&self) -> &[ProjectionInvalidation] {
315        &self.invalidations
316    }
317}
318
319/// One final authoritative, adapter-neutral record mutation.
320#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
321pub struct ResolvedProjectionMutation {
322    kind: ProjectionMutationKind,
323    target: ProjectionTarget,
324    scope: ResolvedProjectionMutationScope,
325    fields: Vec<ResolvedProjectionField>,
326    provenance: ProjectionMutationProvenance,
327}
328
329impl ResolvedProjectionMutation {
330    /// Return the authoritative mutation kind.
331    pub fn kind(&self) -> ProjectionMutationKind {
332        self.kind
333    }
334
335    /// Return the portable logical target.
336    pub fn target(&self) -> &ProjectionTarget {
337        &self.target
338    }
339
340    /// Return the complete logical key.
341    pub fn key(&self) -> &ResolvedProjectionKey {
342        &self.scope.key
343    }
344
345    /// Return the complete partition/model/storage/key scope.
346    pub fn scope(&self) -> &ResolvedProjectionMutationScope {
347        &self.scope
348    }
349
350    /// Return fields in stable source-ordinal order.
351    pub fn fields(&self) -> &[ResolvedProjectionField] {
352        &self.fields
353    }
354
355    /// Return exact event-program-operation provenance.
356    pub fn provenance(&self) -> &ProjectionMutationProvenance {
357        &self.provenance
358    }
359}
360
361/// One event occurrence evaluated through one canonical projection program.
362///
363/// This is a logical semantic plan. It deliberately exposes no constructor
364/// from a physical `TableWritePlan`; adapters may only lower from this type.
365#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
366pub struct ResolvedProjectionPlan {
367    program_id: ProjectionProgramId,
368    occurrence: DomainEventOccurrence,
369    arm_id: String,
370    partition: ResolvedProjectionPartition,
371    mutations: Vec<ResolvedProjectionMutation>,
372}
373
374impl ResolvedProjectionPlan {
375    /// Resolve one occurrence against a validated projection program.
376    ///
377    /// Public so mutation-backed descriptors share the same resolution entry
378    /// point as generated `projection!` programs.
379    ///
380    /// # Errors
381    ///
382    /// Rejects selector mismatches, invalid body values, and bounded-value
383    /// violations.
384    pub fn resolve(
385        program: &ProjectionProgram,
386        occurrence: &DomainEventOccurrence,
387    ) -> Result<Self, ProjectionProgramError> {
388        let matches = program
389            .arms()
390            .iter()
391            .filter(|arm| arm.selector().matches(occurrence))
392            .collect::<Vec<_>>();
393        let arm = match matches.as_slice() {
394            [] => return Err(ProjectionProgramError::NoMatchingArm),
395            [arm] => *arm,
396            _ => return Err(ProjectionProgramError::MultipleMatchingArms),
397        };
398        let body: Value = occurrence
399            .decode_body()
400            .map_err(|error| ProjectionProgramError::CanonicalJson(error.to_string()))?;
401        let program_id = program.id()?;
402        let partition = resolve_partition(program.partition(), occurrence, &body)?;
403        let mut mutations = arm
404            .operations()
405            .iter()
406            .map(|operation| {
407                resolve_operation(program_id, arm, operation, occurrence, &body, &partition)
408            })
409            .collect::<Result<Vec<_>, _>>()?;
410        mutations = coalesce_mutations(mutations)?;
411        mutations.sort_by(|left, right| {
412            left.kind
413                .cmp(&right.kind)
414                .then_with(|| left.target.model().cmp(right.target.model()))
415                .then_with(|| left.target.storage().cmp(right.target.storage()))
416                .then_with(|| {
417                    left.scope
418                        .key
419                        .canonical_bytes
420                        .cmp(&right.scope.key.canonical_bytes)
421                })
422                .then_with(|| {
423                    left.provenance.staging_ordinals[0].cmp(&right.provenance.staging_ordinals[0])
424                })
425        });
426        Ok(Self {
427            program_id,
428            occurrence: occurrence.clone(),
429            arm_id: arm.arm_id().to_owned(),
430            partition,
431            mutations,
432        })
433    }
434
435    /// Return the canonical program identity.
436    pub fn program_id(&self) -> ProjectionProgramId {
437        self.program_id
438    }
439
440    /// Return the exact immutable occurrence that was evaluated.
441    pub fn occurrence(&self) -> &DomainEventOccurrence {
442        &self.occurrence
443    }
444
445    /// Return the selected arm identifier.
446    pub fn arm_id(&self) -> &str {
447        &self.arm_id
448    }
449
450    /// Return the concrete bounded logical partition.
451    pub fn partition(&self) -> &ResolvedProjectionPartition {
452        &self.partition
453    }
454
455    /// Return at most one final mutation for each partition/model/key scope.
456    pub fn mutations(&self) -> &[ResolvedProjectionMutation] {
457        &self.mutations
458    }
459
460    /// Encode canonical JSON for deterministic conformance and handoff.
461    ///
462    /// # Errors
463    ///
464    /// Returns a typed error if canonical serialization fails.
465    pub fn canonical_bytes(&self) -> Result<Vec<u8>, ProjectionProgramError> {
466        canonical_json_bytes(self)
467    }
468}
469
470fn resolve_partition(
471    partition: &ProjectionPartition,
472    occurrence: &DomainEventOccurrence,
473    body: &Value,
474) -> Result<ResolvedProjectionPartition, ProjectionProgramError> {
475    let partition = match partition {
476        ProjectionPartition::Unit => ResolvedProjectionPartitionValue::Unit,
477        ProjectionPartition::Expression(expression) => {
478            let value = match expression.resolve(occurrence, body)? {
479                ResolvedProjectionValue::Value(value) => value,
480                ResolvedProjectionValue::Absent => {
481                    return Err(ProjectionProgramError::RequiredValueAbsent {
482                        path: "projection partition".to_owned(),
483                    });
484                }
485                ResolvedProjectionValue::Unset => {
486                    return Err(ProjectionProgramError::UnsetNotAllowed {
487                        field: "projection partition".to_owned(),
488                    });
489                }
490            };
491            ResolvedProjectionPartitionValue::Value(value)
492        }
493    };
494    let canonical_bytes = bounded_partition_bytes(&partition, MAX_PROJECTION_PARTITION_BYTES)?;
495    Ok(ResolvedProjectionPartition {
496        partition,
497        canonical_bytes,
498    })
499}
500
501fn resolve_operation(
502    program_id: ProjectionProgramId,
503    arm: &ProjectionArm,
504    operation: &ProjectionOperation,
505    occurrence: &DomainEventOccurrence,
506    body: &Value,
507    partition: &ResolvedProjectionPartition,
508) -> Result<ResolvedProjectionMutation, ProjectionProgramError> {
509    let key = resolve_key(operation.key(), occurrence, body)?;
510    let mut fields = Vec::with_capacity(operation.fields().len());
511    for field in operation.fields() {
512        let value = field.assignment().resolve(occurrence, body)?;
513        if operation.kind().is_complete_write() {
514            match &value {
515                ResolvedProjectionValue::Absent => {
516                    return Err(ProjectionProgramError::RequiredValueAbsent {
517                        path: field.name().to_owned(),
518                    });
519                }
520                ResolvedProjectionValue::Unset => {
521                    return Err(ProjectionProgramError::UnsetNotAllowed {
522                        field: field.name().to_owned(),
523                    });
524                }
525                ResolvedProjectionValue::Value(_) => {}
526            }
527        }
528        fields.push(ResolvedProjectionField {
529            operation_staging_ordinal: operation.staging_ordinal(),
530            field_ordinal: field.ordinal(),
531            name: field.name().to_owned(),
532            value,
533        });
534    }
535    let relationship_effects = operation
536        .relationship_effects()
537        .iter()
538        .map(|effect| {
539            let (source_key, target_key) = match effect.kind() {
540                ProjectionRelationshipEffectKind::Link
541                | ProjectionRelationshipEffectKind::Unlink => (
542                    Some(resolve_key(effect.source_key(), occurrence, body)?),
543                    Some(resolve_key(effect.target_key(), occurrence, body)?),
544                ),
545                ProjectionRelationshipEffectKind::Invalidate => (
546                    Some(resolve_key(effect.source_key(), occurrence, body)?),
547                    None,
548                ),
549            };
550            Ok(ResolvedProjectionRelationshipEffect {
551                ordinal: effect.ordinal(),
552                kind: effect.kind(),
553                relationship: effect.relationship().clone(),
554                source_key,
555                target_key,
556            })
557        })
558        .collect::<Result<Vec<_>, ProjectionProgramError>>()?;
559    Ok(ResolvedProjectionMutation {
560        kind: operation.kind(),
561        target: operation.target().clone(),
562        scope: ResolvedProjectionMutationScope {
563            partition: partition.clone(),
564            model: operation.target().model().to_owned(),
565            storage: operation.target().storage().to_owned(),
566            key,
567        },
568        fields,
569        provenance: ProjectionMutationProvenance {
570            occurrence: ProjectionOccurrenceProvenance {
571                occurrence_version: occurrence.occurrence_version(),
572                occurrence_id: occurrence.id().to_owned(),
573                event_name: occurrence.descriptor().name.to_string(),
574                event_version: occurrence.descriptor().version,
575                body_fingerprint: occurrence.descriptor().body.fingerprint.to_string(),
576                aggregate_type: occurrence.aggregate_type().to_owned(),
577                aggregate_id: occurrence.aggregate_id().to_owned(),
578                aggregate_sequence: occurrence.aggregate_sequence(),
579                publication_ordinal: occurrence.publication_ordinal(),
580            },
581            program_id,
582            arm_id: arm.arm_id().to_owned(),
583            operation_ids: vec![operation.operation_id().to_owned()],
584            staging_ordinals: vec![operation.staging_ordinal()],
585            relationship_effects,
586            invalidations: operation.invalidations().to_vec(),
587        },
588    })
589}
590
591fn resolve_key(
592    key: &[ProjectionKeyField],
593    occurrence: &DomainEventOccurrence,
594    body: &Value,
595) -> Result<ResolvedProjectionKey, ProjectionProgramError> {
596    let mut key_fields = Vec::with_capacity(key.len());
597    for field in key {
598        let value = match field.expression().resolve(occurrence, body)? {
599            ResolvedProjectionValue::Value(value) if value.valid_key_component() => value,
600            _ => {
601                return Err(ProjectionProgramError::InvalidKeyValue {
602                    field: field.name().to_owned(),
603                });
604            }
605        };
606        key_fields.push(ResolvedProjectionKeyField {
607            ordinal: field.ordinal(),
608            name: field.name().to_owned(),
609            value,
610        });
611    }
612    let canonical_bytes = bounded_key_bytes(&key_fields, MAX_PROJECTION_RECORD_KEY_BYTES)?;
613    Ok(ResolvedProjectionKey {
614        fields: key_fields,
615        canonical_bytes,
616    })
617}
618
619fn coalesce_mutations(
620    mutations: Vec<ResolvedProjectionMutation>,
621) -> Result<Vec<ResolvedProjectionMutation>, ProjectionProgramError> {
622    let mut by_scope: BTreeMap<(String, String, Vec<u8>), ResolvedProjectionMutation> =
623        BTreeMap::new();
624    for mutation in mutations {
625        let scope = (
626            mutation.target.model().to_owned(),
627            mutation.target.storage().to_owned(),
628            mutation.scope.key.canonical_bytes.clone(),
629        );
630        if let Some(existing) = by_scope.get_mut(&scope) {
631            merge_mutation(existing, mutation)?;
632        } else {
633            by_scope.insert(scope, mutation);
634        }
635    }
636    Ok(by_scope.into_values().collect())
637}
638
639fn merge_mutation(
640    existing: &mut ResolvedProjectionMutation,
641    incoming: ResolvedProjectionMutation,
642) -> Result<(), ProjectionProgramError> {
643    if existing.kind != incoming.kind
644        || existing.target != incoming.target
645        || existing.scope != incoming.scope
646        || existing.provenance.relationship_effects != incoming.provenance.relationship_effects
647    {
648        return Err(ProjectionProgramError::AmbiguousMutation {
649            model: existing.target.model().to_owned(),
650            reason: "same record scope resolves to incompatible logical mutations".to_owned(),
651        });
652    }
653
654    if existing.kind.is_patch() {
655        for field in incoming.fields {
656            if let Some(prior) = existing
657                .fields
658                .iter()
659                .find(|candidate| candidate.name == field.name)
660            {
661                if prior.value != field.value {
662                    return Err(ProjectionProgramError::AmbiguousMutation {
663                        model: existing.target.model().to_owned(),
664                        reason: format!(
665                            "field `{}` receives conflicting values in one occurrence",
666                            field.name
667                        ),
668                    });
669                }
670            } else {
671                existing.fields.push(field);
672            }
673        }
674        existing.fields.sort_by(|left, right| {
675            left.operation_staging_ordinal
676                .cmp(&right.operation_staging_ordinal)
677                .then_with(|| left.field_ordinal.cmp(&right.field_ordinal))
678                .then_with(|| left.name.cmp(&right.name))
679        });
680    } else if !same_resolved_fields_ignoring_staging(&existing.fields, &incoming.fields) {
681        return Err(ProjectionProgramError::AmbiguousMutation {
682            model: existing.target.model().to_owned(),
683            reason: "duplicate complete mutations are not byte-identical".to_owned(),
684        });
685    }
686
687    existing
688        .provenance
689        .operation_ids
690        .extend(incoming.provenance.operation_ids);
691    existing
692        .provenance
693        .staging_ordinals
694        .extend(incoming.provenance.staging_ordinals);
695    existing
696        .provenance
697        .invalidations
698        .extend(incoming.provenance.invalidations);
699    existing.provenance.invalidations.sort();
700    existing.provenance.invalidations.dedup();
701    Ok(())
702}
703
704fn same_resolved_fields_ignoring_staging(
705    left: &[ResolvedProjectionField],
706    right: &[ResolvedProjectionField],
707) -> bool {
708    left.len() == right.len()
709        && left.iter().zip(right).all(|(left, right)| {
710            left.field_ordinal == right.field_ordinal
711                && left.name == right.name
712                && left.value == right.value
713        })
714}