1use aequora_protocol::{ChangeKind, OperationEnvelope, RejectionCode, SessionMetadata};
4use aequora_types::{
5 ActorId, DeviceId, EventId, JobId, LineageContext, LineageRef, OperationId, SchemaVersion,
6 TenantId,
7};
8use async_trait::async_trait;
9use serde::de::DeserializeOwned;
10use std::{
11 cmp::Reverse,
12 collections::{BinaryHeap, HashMap},
13 sync::Arc,
14};
15use thiserror::Error;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub struct AuthContext {
20 pub actor_id: ActorId,
22 pub tenant_id: TenantId,
24 pub device_id: DeviceId,
26}
27
28impl AuthContext {
29 pub fn from_validated_security(
39 context: &aequora_security::ValidatedAuthContext,
40 ) -> Result<Self, aequora_security::SecurityError> {
41 let device_id = context
42 .device_id()
43 .ok_or(aequora_security::SecurityError::DeviceBindingRequired)?;
44 Ok(Self {
45 actor_id: context.principal_id(),
46 tenant_id: context.tenant_id(),
47 device_id,
48 })
49 }
50}
51
52#[derive(Clone, Copy, Debug)]
54pub struct IncomingOperation<'a>(&'a OperationEnvelope);
55
56impl<'a> IncomingOperation<'a> {
57 #[must_use]
59 pub const fn new(operation: &'a OperationEnvelope) -> Self {
60 Self(operation)
61 }
62
63 pub fn authenticate(
69 self,
70 auth: &AuthContext,
71 ) -> Result<AuthenticatedOperation<'a>, ExecutionError> {
72 if self.0.tenant_id != auth.tenant_id
73 || self.0.actor_id != auth.actor_id
74 || self.0.device_id != auth.device_id
75 {
76 return Err(ExecutionError::identity_mismatch(
77 "operation identity does not match the authenticated context",
78 ));
79 }
80 Ok(AuthenticatedOperation(self.0))
81 }
82}
83
84#[derive(Clone, Copy, Debug)]
86pub struct AuthenticatedOperation<'a>(&'a OperationEnvelope);
87
88impl<'a> AuthenticatedOperation<'a> {
89 #[must_use]
91 pub const fn envelope(self) -> &'a OperationEnvelope {
92 self.0
93 }
94
95 #[must_use]
97 pub fn provenance(self, auth: &AuthContext) -> TrustedProvenance {
98 TrustedProvenance {
99 actor_id: auth.actor_id,
100 tenant_id: auth.tenant_id,
101 device_id: auth.device_id,
102 operation_id: self.0.operation_id,
103 operation_lineage: self
104 .0
105 .metadata
106 .lineage
107 .resolved_for_operation(self.0.operation_id),
108 }
109 }
110
111 #[must_use]
114 pub const fn authorize(self) -> AuthorizedOperation<'a> {
115 AuthorizedOperation(self.0)
116 }
117}
118
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub struct TrustedProvenance {
122 actor_id: ActorId,
123 tenant_id: TenantId,
124 device_id: DeviceId,
125 operation_id: OperationId,
126 operation_lineage: LineageContext,
127}
128
129impl TrustedProvenance {
130 #[must_use]
132 pub const fn actor_id(self) -> ActorId {
133 self.actor_id
134 }
135
136 #[must_use]
138 pub const fn tenant_id(self) -> TenantId {
139 self.tenant_id
140 }
141
142 #[must_use]
144 pub const fn device_id(self) -> DeviceId {
145 self.device_id
146 }
147
148 #[must_use]
150 pub const fn operation_id(self) -> OperationId {
151 self.operation_id
152 }
153
154 #[must_use]
156 pub const fn operation_lineage(self) -> LineageContext {
157 self.operation_lineage
158 }
159
160 #[must_use]
162 pub const fn primary_event(self, event_id: EventId) -> DerivedEventProvenance {
163 DerivedEventProvenance {
164 event_id,
165 lineage: self
166 .operation_lineage
167 .derived(LineageRef::Operation(self.operation_id)),
168 }
169 }
170
171 #[must_use]
173 pub const fn job(self, job_id: JobId, caused_by: LineageRef) -> JobProvenance {
174 JobProvenance {
175 job_id,
176 lineage: self.operation_lineage.derived(caused_by),
177 }
178 }
179}
180
181#[derive(Clone, Copy, Debug, Eq, PartialEq)]
183pub struct DerivedEventProvenance {
184 pub event_id: EventId,
186 pub lineage: LineageContext,
188}
189
190impl DerivedEventProvenance {
191 #[must_use]
193 pub const fn derive(self, event_id: EventId) -> Self {
194 Self {
195 event_id,
196 lineage: self.lineage.derived(LineageRef::Event(self.event_id)),
197 }
198 }
199}
200
201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203pub struct JobProvenance {
204 pub job_id: JobId,
206 pub lineage: LineageContext,
208}
209
210#[derive(Clone, Copy, Debug)]
212pub struct AuthorizedOperation<'a>(&'a OperationEnvelope);
213
214impl<'a> AuthorizedOperation<'a> {
215 #[must_use]
217 pub const fn envelope(self) -> &'a OperationEnvelope {
218 self.0
219 }
220
221 #[must_use]
223 pub const fn validate(self) -> ValidatedOperation<'a> {
224 ValidatedOperation(self.0)
225 }
226}
227
228#[derive(Clone, Copy, Debug)]
230pub struct ValidatedOperation<'a>(&'a OperationEnvelope);
231
232impl<'a> ValidatedOperation<'a> {
233 #[must_use]
235 pub const fn executable(self) -> ExecutableOperation<'a> {
236 ExecutableOperation(self.0)
237 }
238}
239
240#[derive(Clone, Copy, Debug)]
242pub struct ExecutableOperation<'a>(&'a OperationEnvelope);
243
244impl<'a> ExecutableOperation<'a> {
245 #[must_use]
247 pub const fn envelope(self) -> &'a OperationEnvelope {
248 self.0
249 }
250}
251
252#[derive(Clone, Debug, Eq, PartialEq)]
254pub struct CurrentEntity {
255 pub version: aequora_types::EntityVersion,
257 pub payload: Vec<u8>,
259 pub tombstone: bool,
261}
262
263#[derive(Clone, Debug, Eq, PartialEq)]
265pub struct AuthoritativeMutation {
266 pub payload: Vec<u8>,
268 pub change_kind: ChangeKind,
270}
271
272#[derive(Clone, Debug, Error, Eq, PartialEq)]
274#[error("{message}")]
275pub struct ExecutionError {
276 pub code: RejectionCode,
278 pub message: String,
280}
281
282impl ExecutionError {
283 #[must_use]
285 pub fn identity_mismatch(message: impl Into<String>) -> Self {
286 Self {
287 code: RejectionCode::IdentityMismatch,
288 message: message.into(),
289 }
290 }
291
292 #[must_use]
294 pub fn unauthorized(message: impl Into<String>) -> Self {
295 Self {
296 code: RejectionCode::Unauthorized,
297 message: message.into(),
298 }
299 }
300
301 #[must_use]
303 pub fn business_rule(message: impl Into<String>) -> Self {
304 Self {
305 code: RejectionCode::BusinessRule,
306 message: message.into(),
307 }
308 }
309
310 #[must_use]
312 pub fn invalid_operation(message: impl Into<String>) -> Self {
313 Self {
314 code: RejectionCode::InvalidOperation,
315 message: message.into(),
316 }
317 }
318
319 #[must_use]
321 pub fn schema_incompatible(message: impl Into<String>) -> Self {
322 Self {
323 code: RejectionCode::SchemaIncompatible,
324 message: message.into(),
325 }
326 }
327}
328
329#[async_trait]
334pub trait OperationExecutor: Send + Sync {
335 async fn authorize_scope(
337 &self,
338 auth: &AuthContext,
339 session: &SessionMetadata,
340 ) -> Result<(), ExecutionError>;
341
342 async fn authorize<'a>(
344 &self,
345 auth: &AuthContext,
346 operation: AuthenticatedOperation<'a>,
347 ) -> Result<AuthorizedOperation<'a>, ExecutionError>;
348
349 async fn execute(
352 &self,
353 auth: &AuthContext,
354 operation: ExecutableOperation<'_>,
355 current: Option<&CurrentEntity>,
356 ) -> Result<AuthoritativeMutation, ExecutionError>;
357}
358
359pub trait DomainOperation: DeserializeOwned + Send + Sync + 'static {
361 const KIND: u16;
363 const CURRENT_SCHEMA: u16;
365}
366
367#[async_trait]
374pub trait OperationHandler<O>: Send + Sync
375where
376 O: DomainOperation,
377{
378 async fn authorize(
380 &self,
381 auth: &AuthContext,
382 operation: &O,
383 envelope: &OperationEnvelope,
384 ) -> Result<(), ExecutionError>;
385
386 async fn execute(
388 &self,
389 auth: &AuthContext,
390 operation: &O,
391 envelope: &OperationEnvelope,
392 current: Option<&CurrentEntity>,
393 ) -> Result<AuthoritativeMutation, ExecutionError>;
394}
395
396#[async_trait]
398pub trait ScopeAuthorizer: Send + Sync {
399 async fn authorize_scope(
401 &self,
402 auth: &AuthContext,
403 session: &SessionMetadata,
404 ) -> Result<(), ExecutionError>;
405}
406
407pub trait PayloadMigrator: Send + Sync {
410 fn migrate(&self, from: SchemaVersion, payload: &[u8]) -> Result<Vec<u8>, ExecutionError>;
416}
417
418#[async_trait]
419trait ErasedOperationHandler: Send + Sync {
420 async fn authorize(
421 &self,
422 auth: &AuthContext,
423 envelope: &OperationEnvelope,
424 ) -> Result<(), ExecutionError>;
425
426 async fn execute(
427 &self,
428 auth: &AuthContext,
429 envelope: &OperationEnvelope,
430 current: Option<&CurrentEntity>,
431 ) -> Result<AuthoritativeMutation, ExecutionError>;
432}
433
434struct TypedHandler<O, H> {
435 handler: H,
436 minimum_schema: u16,
437 migrator: Option<Arc<dyn PayloadMigrator>>,
438 marker: std::marker::PhantomData<fn() -> O>,
439}
440
441impl<O, H> TypedHandler<O, H>
442where
443 O: DomainOperation,
444{
445 fn decode(&self, envelope: &OperationEnvelope) -> Result<O, ExecutionError> {
446 let schema = envelope.schema_version.0;
447 if schema < self.minimum_schema || schema > O::CURRENT_SCHEMA {
448 return Err(ExecutionError::schema_incompatible(format!(
449 "operation schema {schema} is outside supported range {}..={}",
450 self.minimum_schema,
451 O::CURRENT_SCHEMA
452 )));
453 }
454 if schema == O::CURRENT_SCHEMA {
455 return postcard::from_bytes(&envelope.payload).map_err(|_| {
456 ExecutionError::invalid_operation("current operation payload is malformed")
457 });
458 }
459 let migrator = self.migrator.as_ref().ok_or_else(|| {
460 ExecutionError::schema_incompatible("historical operation requires a migration adapter")
461 })?;
462 let current = migrator.migrate(envelope.schema_version, &envelope.payload)?;
463 postcard::from_bytes(¤t).map_err(|_| {
464 ExecutionError::invalid_operation("migrated operation payload is malformed")
465 })
466 }
467}
468
469#[async_trait]
470impl<O, H> ErasedOperationHandler for TypedHandler<O, H>
471where
472 O: DomainOperation,
473 H: OperationHandler<O>,
474{
475 async fn authorize(
476 &self,
477 auth: &AuthContext,
478 envelope: &OperationEnvelope,
479 ) -> Result<(), ExecutionError> {
480 let operation = self.decode(envelope)?;
481 self.handler.authorize(auth, &operation, envelope).await
482 }
483
484 async fn execute(
485 &self,
486 auth: &AuthContext,
487 envelope: &OperationEnvelope,
488 current: Option<&CurrentEntity>,
489 ) -> Result<AuthoritativeMutation, ExecutionError> {
490 let operation = self.decode(envelope)?;
491 self.handler
492 .execute(auth, &operation, envelope, current)
493 .await
494 }
495}
496
497#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
499pub enum RegistrationError {
500 #[error("operation kind {0} is already registered")]
502 Duplicate(u16),
503 #[error("operation schema window is invalid")]
505 SchemaWindow,
506}
507
508pub struct OperationRegistry {
510 scope_authorizer: Arc<dyn ScopeAuthorizer>,
511 handlers: HashMap<u16, Arc<dyn ErasedOperationHandler>>,
512}
513
514impl OperationRegistry {
515 #[must_use]
517 pub fn new<A>(scope_authorizer: A) -> Self
518 where
519 A: ScopeAuthorizer + 'static,
520 {
521 Self {
522 scope_authorizer: Arc::new(scope_authorizer),
523 handlers: HashMap::new(),
524 }
525 }
526
527 pub fn register<O, H>(&mut self, handler: H) -> Result<&mut Self, RegistrationError>
533 where
534 O: DomainOperation,
535 H: OperationHandler<O> + 'static,
536 {
537 self.register_inner::<O, H>(handler, O::CURRENT_SCHEMA, None)
538 }
539
540 pub fn register_with_migration<O, H, M>(
546 &mut self,
547 minimum_schema: u16,
548 handler: H,
549 migrator: M,
550 ) -> Result<&mut Self, RegistrationError>
551 where
552 O: DomainOperation,
553 H: OperationHandler<O> + 'static,
554 M: PayloadMigrator + 'static,
555 {
556 self.register_inner::<O, H>(handler, minimum_schema, Some(Arc::new(migrator)))
557 }
558
559 fn register_inner<O, H>(
560 &mut self,
561 handler: H,
562 minimum_schema: u16,
563 migrator: Option<Arc<dyn PayloadMigrator>>,
564 ) -> Result<&mut Self, RegistrationError>
565 where
566 O: DomainOperation,
567 H: OperationHandler<O> + 'static,
568 {
569 if minimum_schema == 0 || minimum_schema > O::CURRENT_SCHEMA {
570 return Err(RegistrationError::SchemaWindow);
571 }
572 if self.handlers.contains_key(&O::KIND) {
573 return Err(RegistrationError::Duplicate(O::KIND));
574 }
575 self.handlers.insert(
576 O::KIND,
577 Arc::new(TypedHandler::<O, H> {
578 handler,
579 minimum_schema,
580 migrator,
581 marker: std::marker::PhantomData,
582 }),
583 );
584 Ok(self)
585 }
586
587 fn handler(
588 &self,
589 operation_kind: u16,
590 ) -> Result<&Arc<dyn ErasedOperationHandler>, ExecutionError> {
591 self.handlers.get(&operation_kind).ok_or_else(|| {
592 ExecutionError::invalid_operation(format!(
593 "operation kind {operation_kind} is not registered"
594 ))
595 })
596 }
597}
598
599#[async_trait]
600impl OperationExecutor for OperationRegistry {
601 async fn authorize_scope(
602 &self,
603 auth: &AuthContext,
604 session: &SessionMetadata,
605 ) -> Result<(), ExecutionError> {
606 self.scope_authorizer.authorize_scope(auth, session).await
607 }
608
609 async fn authorize<'a>(
610 &self,
611 auth: &AuthContext,
612 operation: AuthenticatedOperation<'a>,
613 ) -> Result<AuthorizedOperation<'a>, ExecutionError> {
614 let envelope = operation.envelope();
615 self.handler(envelope.operation_kind.0)?
616 .authorize(auth, envelope)
617 .await?;
618 Ok(operation.authorize())
619 }
620
621 async fn execute(
622 &self,
623 auth: &AuthContext,
624 operation: ExecutableOperation<'_>,
625 current: Option<&CurrentEntity>,
626 ) -> Result<AuthoritativeMutation, ExecutionError> {
627 let envelope = operation.envelope();
628 self.handler(envelope.operation_kind.0)?
629 .execute(auth, envelope, current)
630 .await
631 }
632}
633
634#[derive(Clone, Debug, Error, Eq, PartialEq)]
636pub enum DependencyError {
637 #[error("operation {0} appears more than once in the dependency graph")]
639 Duplicate(aequora_types::OperationId),
640 #[error("operation dependency graph contains a cycle")]
642 Cycle,
643}
644
645#[derive(Clone, Debug, Eq, PartialEq)]
647pub struct DependencyPlan {
648 ordered_indices: Vec<usize>,
649 groups: Vec<Vec<usize>>,
650}
651
652impl DependencyPlan {
653 #[must_use]
655 pub fn ordered_indices(&self) -> &[usize] {
656 &self.ordered_indices
657 }
658
659 #[must_use]
661 pub fn groups(&self) -> &[Vec<usize>] {
662 &self.groups
663 }
664}
665
666pub fn plan_dependencies(
674 operations: &[OperationEnvelope],
675) -> Result<DependencyPlan, DependencyError> {
676 let mut positions = HashMap::with_capacity(operations.len());
677 for (index, operation) in operations.iter().enumerate() {
678 if positions.insert(operation.operation_id, index).is_some() {
679 return Err(DependencyError::Duplicate(operation.operation_id));
680 }
681 }
682
683 let mut indegree = vec![0_usize; operations.len()];
684 let mut outgoing = vec![Vec::new(); operations.len()];
685 for (index, operation) in operations.iter().enumerate() {
686 for dependency in &operation.metadata.dependencies {
687 if let Some(&dependency_index) = positions.get(dependency) {
688 indegree[index] = indegree[index].saturating_add(1);
689 outgoing[dependency_index].push(index);
690 }
691 }
692 }
693
694 let mut ready: BinaryHeap<Reverse<usize>> = indegree
695 .iter()
696 .enumerate()
697 .filter_map(|(index, degree)| (*degree == 0).then_some(Reverse(index)))
698 .collect();
699 let mut ordered_indices = Vec::with_capacity(operations.len());
700 let mut groups = Vec::new();
701 while !ready.is_empty() {
702 let mut group = Vec::with_capacity(ready.len());
703 while let Some(Reverse(index)) = ready.pop() {
704 group.push(index);
705 }
706 for &index in &group {
707 ordered_indices.push(index);
708 for &dependent in &outgoing[index] {
709 indegree[dependent] = indegree[dependent].saturating_sub(1);
710 if indegree[dependent] == 0 {
711 ready.push(Reverse(dependent));
712 }
713 }
714 }
715 groups.push(group);
716 }
717 if ordered_indices.len() != operations.len() {
718 return Err(DependencyError::Cycle);
719 }
720 Ok(DependencyPlan {
721 ordered_indices,
722 groups,
723 })
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use aequora_protocol::{OperationKind, OperationMetadata};
730 use aequora_types::{
731 EntityId, EntityRef, EntityType, HybridTimestamp, NodeId, OperationId, ProtocolVersion,
732 };
733 use serde::{Deserialize, Serialize};
734
735 #[derive(Deserialize, Serialize)]
736 struct CurrentCommand {
737 value: u16,
738 }
739
740 impl DomainOperation for CurrentCommand {
741 const KIND: u16 = 9;
742 const CURRENT_SCHEMA: u16 = 2;
743 }
744
745 #[derive(Deserialize, Serialize)]
746 struct LegacyCommand {
747 value: u8,
748 }
749
750 struct CommandMigration;
751
752 impl PayloadMigrator for CommandMigration {
753 fn migrate(&self, from: SchemaVersion, payload: &[u8]) -> Result<Vec<u8>, ExecutionError> {
754 if from != SchemaVersion(1) {
755 return Err(ExecutionError::schema_incompatible(
756 "only schema one can be migrated",
757 ));
758 }
759 let legacy: LegacyCommand = postcard::from_bytes(payload)
760 .map_err(|_| ExecutionError::invalid_operation("legacy payload is malformed"))?;
761 postcard::to_stdvec(&CurrentCommand {
762 value: u16::from(legacy.value),
763 })
764 .map_err(|_| ExecutionError::invalid_operation("migration encoding failed"))
765 }
766 }
767
768 struct AllowScope;
769
770 #[async_trait]
771 impl ScopeAuthorizer for AllowScope {
772 async fn authorize_scope(
773 &self,
774 _auth: &AuthContext,
775 _session: &SessionMetadata,
776 ) -> Result<(), ExecutionError> {
777 Ok(())
778 }
779 }
780
781 struct CurrentHandler;
782
783 #[async_trait]
784 impl OperationHandler<CurrentCommand> for CurrentHandler {
785 async fn authorize(
786 &self,
787 _auth: &AuthContext,
788 _operation: &CurrentCommand,
789 _envelope: &OperationEnvelope,
790 ) -> Result<(), ExecutionError> {
791 Ok(())
792 }
793
794 async fn execute(
795 &self,
796 _auth: &AuthContext,
797 operation: &CurrentCommand,
798 _envelope: &OperationEnvelope,
799 _current: Option<&CurrentEntity>,
800 ) -> Result<AuthoritativeMutation, ExecutionError> {
801 Ok(AuthoritativeMutation {
802 payload: operation.value.to_be_bytes().to_vec(),
803 change_kind: ChangeKind::Upsert,
804 })
805 }
806 }
807
808 fn envelope(payload: Vec<u8>, schema_version: SchemaVersion) -> OperationEnvelope {
809 OperationEnvelope {
810 protocol_version: ProtocolVersion::V1,
811 operation_id: OperationId::new(),
812 tenant_id: TenantId::new(),
813 actor_id: ActorId::new(),
814 device_id: DeviceId::new(),
815 entity: EntityRef {
816 entity_type: EntityType::new(1).unwrap_or_else(|error| panic!("{error}")),
817 entity_id: EntityId::new(),
818 },
819 base_version: None,
820 created_at: HybridTimestamp {
821 physical_ms: 1,
822 logical: 0,
823 node: NodeId::new(),
824 },
825 schema_version,
826 operation_kind: OperationKind(CurrentCommand::KIND),
827 payload,
828 metadata: OperationMetadata::default(),
829 }
830 }
831
832 #[tokio::test]
833 async fn registry_migrates_only_the_explicit_schema_window() {
834 let mut registry = OperationRegistry::new(AllowScope);
835 registry
836 .register_with_migration::<CurrentCommand, _, _>(1, CurrentHandler, CommandMigration)
837 .unwrap_or_else(|error| panic!("{error}"));
838 let legacy = LegacyCommand { value: 42 };
839 let operation = envelope(
840 postcard::to_stdvec(&legacy).unwrap_or_else(|error| panic!("{error}")),
841 SchemaVersion(1),
842 );
843 let auth = AuthContext {
844 actor_id: operation.actor_id,
845 tenant_id: operation.tenant_id,
846 device_id: operation.device_id,
847 };
848
849 let authenticated = IncomingOperation::new(&operation)
850 .authenticate(&auth)
851 .unwrap_or_else(|error| panic!("{error}"));
852 let authorized = registry
853 .authorize(&auth, authenticated)
854 .await
855 .unwrap_or_else(|error| panic!("{error}"));
856 let mutation = registry
857 .execute(&auth, authorized.validate().executable(), None)
858 .await
859 .unwrap_or_else(|error| panic!("{error}"));
860 assert_eq!(mutation.payload, 42_u16.to_be_bytes());
861
862 let unsupported = envelope(Vec::new(), SchemaVersion(3));
863 let unsupported_auth = AuthContext {
864 actor_id: unsupported.actor_id,
865 tenant_id: unsupported.tenant_id,
866 device_id: unsupported.device_id,
867 };
868 let unsupported = IncomingOperation::new(&unsupported)
869 .authenticate(&unsupported_auth)
870 .unwrap_or_else(|error| panic!("{error}"));
871 assert!(matches!(
872 registry.authorize(&unsupported_auth, unsupported).await,
873 Err(ExecutionError {
874 code: RejectionCode::SchemaIncompatible,
875 ..
876 })
877 ));
878 }
879
880 #[test]
881 fn authenticated_provenance_preserves_correlation_for_events_and_jobs() {
882 let operation = envelope(Vec::new(), SchemaVersion(1));
883 let auth = AuthContext {
884 actor_id: operation.actor_id,
885 tenant_id: operation.tenant_id,
886 device_id: operation.device_id,
887 };
888 let authenticated = IncomingOperation::new(&operation)
889 .authenticate(&auth)
890 .unwrap_or_else(|error| panic!("{error}"));
891 let provenance = authenticated.provenance(&auth);
892 let event = provenance.primary_event(EventId::new());
893 let derived = event.derive(EventId::new());
894 let job = provenance.job(JobId::new(), LineageRef::Event(derived.event_id));
895
896 assert_eq!(provenance.tenant_id(), auth.tenant_id);
897 assert_eq!(
898 event.lineage.correlation_id,
899 operation.metadata.lineage.correlation_id
900 );
901 assert_eq!(
902 event.lineage.caused_by,
903 Some(LineageRef::Operation(operation.operation_id))
904 );
905 assert_eq!(derived.lineage.correlation_id, event.lineage.correlation_id);
906 assert_eq!(
907 derived.lineage.caused_by,
908 Some(LineageRef::Event(event.event_id))
909 );
910 assert_eq!(job.lineage.correlation_id, event.lineage.correlation_id);
911 assert_eq!(
912 job.lineage.caused_by,
913 Some(LineageRef::Event(derived.event_id))
914 );
915 }
916}