From 60753a8f70a29769835b6bbd97a2418eb8134593 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Fri, 24 May 2024 19:49:58 +0200 Subject: [PATCH] TASK: Cosmetic cleanup of `DoctrineDbalContentGraphProjection` Sorts `when`-matchers and -handlers alphabetically and moves other private methods to the bottom of the class --- .../DoctrineDbalContentGraphProjection.php | 1146 +++++++++-------- 1 file changed, 574 insertions(+), 572 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 3d2338a61f9..d2b530d7333 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -101,19 +101,6 @@ public function setUp(): void $this->checkpointStorage->setUp(); } - /** - * @return array - */ - private function determineRequiredSqlStatements(): array - { - $schemaManager = $this->dbal->getSchemaManager(); - if (!$schemaManager instanceof AbstractSchemaManager) { - throw new \RuntimeException('Failed to retrieve Schema Manager', 1625653914); - } - $schema = (new DoctrineDbalContentGraphSchemaBuilder($this->tableNames))->buildSchema($schemaManager); - return DbalSchemaDiff::determineRequiredSqlStatements($this->dbal, $schema); - } - public function status(): ProjectionStatus { $checkpointStorageStatus = $this->checkpointStorage->status(); @@ -153,37 +140,35 @@ public function markStale(): void $this->getState()->forgetInstances(); } - private function truncateDatabaseTables(): void + public function getCheckpointStorage(): DbalCheckpointStorage { - try { - $this->dbal->executeQuery('TRUNCATE table ' . $this->tableNames->node()); - $this->dbal->executeQuery('TRUNCATE table ' . $this->tableNames->hierarchyRelation()); - $this->dbal->executeQuery('TRUNCATE table ' . $this->tableNames->referenceRelation()); - $this->dbal->executeQuery('TRUNCATE table ' . $this->tableNames->dimensionSpacePoints()); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to truncate database tables for projection %s: %s', self::class, $e->getMessage()), 1716478318, $e); - } + return $this->checkpointStorage; + } + + public function getState(): ContentGraphFinder + { + return $this->contentGraphFinder; } public function canHandle(EventInterface $event): bool { return in_array($event::class, [ - RootNodeAggregateWithNodeWasCreated::class, - RootNodeAggregateDimensionsWereUpdated::class, - NodeAggregateWithNodeWasCreated::class, - NodeAggregateNameWasChanged::class, ContentStreamWasForked::class, ContentStreamWasRemoved::class, - NodePropertiesWereSet::class, - NodeReferencesWereSet::class, - NodeAggregateTypeWasChanged::class, - DimensionSpacePointWasMoved::class, DimensionShineThroughWasAdded::class, - NodeAggregateWasRemoved::class, + DimensionSpacePointWasMoved::class, + NodeAggregateNameWasChanged::class, + NodeAggregateTypeWasChanged::class, NodeAggregateWasMoved::class, - NodeSpecializationVariantWasCreated::class, + NodeAggregateWasRemoved::class, + NodeAggregateWithNodeWasCreated::class, NodeGeneralizationVariantWasCreated::class, NodePeerVariantWasCreated::class, + NodePropertiesWereSet::class, + NodeReferencesWereSet::class, + NodeSpecializationVariantWasCreated::class, + RootNodeAggregateDimensionsWereUpdated::class, + RootNodeAggregateWithNodeWasCreated::class, SubtreeWasTagged::class, SubtreeWasUntagged::class, ]); @@ -192,140 +177,598 @@ public function canHandle(EventInterface $event): bool public function apply(EventInterface $event, EventEnvelope $eventEnvelope): void { match ($event::class) { - RootNodeAggregateWithNodeWasCreated::class => $this->whenRootNodeAggregateWithNodeWasCreated($event, $eventEnvelope), - RootNodeAggregateDimensionsWereUpdated::class => $this->whenRootNodeAggregateDimensionsWereUpdated($event), - NodeAggregateWithNodeWasCreated::class => $this->whenNodeAggregateWithNodeWasCreated($event, $eventEnvelope), - NodeAggregateNameWasChanged::class => $this->whenNodeAggregateNameWasChanged($event, $eventEnvelope), ContentStreamWasForked::class => $this->whenContentStreamWasForked($event), ContentStreamWasRemoved::class => $this->whenContentStreamWasRemoved($event), - NodePropertiesWereSet::class => $this->whenNodePropertiesWereSet($event, $eventEnvelope), - NodeReferencesWereSet::class => $this->whenNodeReferencesWereSet($event, $eventEnvelope), - NodeAggregateTypeWasChanged::class => $this->whenNodeAggregateTypeWasChanged($event, $eventEnvelope), - DimensionSpacePointWasMoved::class => $this->whenDimensionSpacePointWasMoved($event), DimensionShineThroughWasAdded::class => $this->whenDimensionShineThroughWasAdded($event), - NodeAggregateWasRemoved::class => $this->whenNodeAggregateWasRemoved($event), + DimensionSpacePointWasMoved::class => $this->whenDimensionSpacePointWasMoved($event), + NodeAggregateNameWasChanged::class => $this->whenNodeAggregateNameWasChanged($event, $eventEnvelope), + NodeAggregateTypeWasChanged::class => $this->whenNodeAggregateTypeWasChanged($event, $eventEnvelope), NodeAggregateWasMoved::class => $this->whenNodeAggregateWasMoved($event), - NodeSpecializationVariantWasCreated::class => $this->whenNodeSpecializationVariantWasCreated($event, $eventEnvelope), + NodeAggregateWasRemoved::class => $this->whenNodeAggregateWasRemoved($event), + NodeAggregateWithNodeWasCreated::class => $this->whenNodeAggregateWithNodeWasCreated($event, $eventEnvelope), NodeGeneralizationVariantWasCreated::class => $this->whenNodeGeneralizationVariantWasCreated($event, $eventEnvelope), NodePeerVariantWasCreated::class => $this->whenNodePeerVariantWasCreated($event, $eventEnvelope), + NodePropertiesWereSet::class => $this->whenNodePropertiesWereSet($event, $eventEnvelope), + NodeReferencesWereSet::class => $this->whenNodeReferencesWereSet($event, $eventEnvelope), + NodeSpecializationVariantWasCreated::class => $this->whenNodeSpecializationVariantWasCreated($event, $eventEnvelope), + RootNodeAggregateDimensionsWereUpdated::class => $this->whenRootNodeAggregateDimensionsWereUpdated($event), + RootNodeAggregateWithNodeWasCreated::class => $this->whenRootNodeAggregateWithNodeWasCreated($event, $eventEnvelope), SubtreeWasTagged::class => $this->whenSubtreeWasTagged($event), SubtreeWasUntagged::class => $this->whenSubtreeWasUntagged($event), default => throw new \InvalidArgumentException(sprintf('Unsupported event %s', get_debug_type($event))), }; } - public function getCheckpointStorage(): DbalCheckpointStorage + private function whenContentStreamWasForked(ContentStreamWasForked $event): void { - return $this->checkpointStorage; - } + // + // 1) Copy HIERARCHY RELATIONS (this is the MAIN OPERATION here) + // + $insertRelationStatement = <<tableNames->hierarchyRelation()} ( + parentnodeanchor, + childnodeanchor, + position, + dimensionspacepointhash, + subtreetags, + contentstreamid + ) + SELECT + h.parentnodeanchor, + h.childnodeanchor, + h.position, + h.dimensionspacepointhash, + h.subtreetags, + "{$event->newContentStreamId->value}" AS contentstreamid + FROM + {$this->tableNames->hierarchyRelation()} h + WHERE h.contentstreamid = :sourceContentStreamId + SQL; + try { + $this->dbal->executeStatement($insertRelationStatement, [ + 'sourceContentStreamId' => $event->sourceContentStreamId->value + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to insert hierarchy relation: %s', $e->getMessage()), 1716489211, $e); + } - public function getState(): ContentGraphFinder - { - return $this->contentGraphFinder; + // NOTE: as reference edges are attached to Relation Anchor Points (and they are lazily copy-on-written), + // we do not need to copy reference edges here (but we need to do it during copy on write). } - private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNodeWasCreated $event, EventEnvelope $eventEnvelope): void + private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): void { - $originDimensionSpacePoint = OriginDimensionSpacePoint::createWithoutDimensions(); - $node = NodeRecord::createNewInDatabase( - $this->dbal, - $this->tableNames, - $event->nodeAggregateId, - $originDimensionSpacePoint->coordinates, - $originDimensionSpacePoint->hash, - SerializedPropertyValues::createEmpty(), - $event->nodeTypeName, - $event->nodeAggregateClassification, - null, - Timestamps::create($eventEnvelope->recordedAt, self::initiatingDateTime($eventEnvelope), null, null), - ); + // Drop hierarchy relations + $deleteHierarchyRelationStatement = <<tableNames->hierarchyRelation()} WHERE contentstreamid = :contentStreamId + SQL; + try { + $this->dbal->executeStatement($deleteHierarchyRelationStatement, [ + 'contentStreamId' => $event->contentStreamId->value + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to delete hierarchy relations: %s', $e->getMessage()), 1716489265, $e); + } - $this->connectHierarchy( - $event->contentStreamId, - NodeRelationAnchorPoint::forRootEdge(), - $node->relationAnchorPoint, - $event->coveredDimensionSpacePoints, - null - ); + // Drop non-referenced nodes (which do not have a hierarchy relation anymore) + $deleteNodesStatement = <<tableNames->node()} + WHERE NOT EXISTS ( + SELECT 1 FROM {$this->tableNames->hierarchyRelation()} + WHERE {$this->tableNames->hierarchyRelation()}.childnodeanchor = {$this->tableNames->node()}.relationanchorpoint + ) + SQL; + try { + $this->dbal->executeStatement($deleteNodesStatement); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to delete non-referenced nodes: %s', $e->getMessage()), 1716489294, $e); + } + + // Drop non-referenced reference relations (i.e. because the referenced nodes are gone by now) + $deleteReferenceRelationsStatement = <<tableNames->referenceRelation()} + WHERE NOT EXISTS ( + SELECT 1 FROM {$this->tableNames->node()} + WHERE {$this->tableNames->node()}.relationanchorpoint = {$this->tableNames->referenceRelation()}.nodeanchorpoint + ) + SQL; + try { + $this->dbal->executeStatement($deleteReferenceRelationsStatement); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to delete non-referenced reference relations: %s', $e->getMessage()), 1716489328, $e); + } } - private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDimensionsWereUpdated $event): void + private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded $event): void { - $rootNodeAnchorPoint = $this->projectionContentGraph - ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( - $event->nodeAggregateId, - /** the origin DSP of the root node is always the empty dimension ({@see whenRootNodeAggregateWithNodeWasCreated}) */ - OriginDimensionSpacePoint::createWithoutDimensions(), - $event->contentStreamId - ); - if ($rootNodeAnchorPoint === null) { - // should never happen. - return; - } + $this->dimensionSpacePointsRepository->insertDimensionSpacePoint($event->target); - // delete all hierarchy edges of the root node - $deleteHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} - WHERE - parentnodeanchor = :parentNodeAnchor - AND childnodeanchor = :childNodeAnchor - AND contentstreamid = :contentStreamId + // 1) hierarchy relations + $insertHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} ( + parentnodeanchor, + childnodeanchor, + position, + subtreetags, + dimensionspacepointhash, + contentstreamid + ) + SELECT + h.parentnodeanchor, + h.childnodeanchor, + h.position, + h.subtreetags, + :newDimensionSpacePointHash AS dimensionspacepointhash, + h.contentstreamid + FROM + {$this->tableNames->hierarchyRelation()} h + WHERE h.contentstreamid = :contentStreamId + AND h.dimensionspacepointhash = :sourceDimensionSpacePointHash SQL; try { - $this->dbal->executeStatement($deleteHierarchyRelationsStatement, [ - 'parentNodeAnchor' => NodeRelationAnchorPoint::forRootEdge()->value, - 'childNodeAnchor' => $rootNodeAnchorPoint->value, + $this->dbal->executeStatement($insertHierarchyRelationsStatement, [ 'contentStreamId' => $event->contentStreamId->value, + 'sourceDimensionSpacePointHash' => $event->source->hash, + 'newDimensionSpacePointHash' => $event->target->hash, ]); } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to delete hierarchy relation: %s', $e->getMessage()), 1716488943, $e); + throw new \RuntimeException(sprintf('Failed to insert hierarchy relations: %s', $e->getMessage()), 1716490758, $e); } - // recreate hierarchy edges for the root node - $this->connectHierarchy( - $event->contentStreamId, - NodeRelationAnchorPoint::forRootEdge(), - $rootNodeAnchorPoint, - $event->coveredDimensionSpacePoints, - null - ); } - private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCreated $event, EventEnvelope $eventEnvelope): void + private function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $event): void { - $this->createNodeWithHierarchy( - $event->contentStreamId, - $event->nodeAggregateId, - $event->nodeTypeName, - $event->parentNodeAggregateId, - $event->originDimensionSpacePoint, - $event->succeedingSiblingsForCoverage, - $event->initialPropertyValues, - $event->nodeAggregateClassification, - $event->nodeName, - $eventEnvelope, - ); - } + $this->dimensionSpacePointsRepository->insertDimensionSpacePoint($event->target); - private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $event, EventEnvelope $eventEnvelope): void - { - foreach ( - $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream( - $event->nodeAggregateId, - $event->contentStreamId, - ) as $anchorPoint - ) { - $this->updateNodeRecordWithCopyOnWrite( - $event->contentStreamId, - $anchorPoint, - function (NodeRecord $node) use ($event, $eventEnvelope) { - $node->nodeName = $event->newNodeName; - $node->timestamps = $node->timestamps->with( + // the ordering is important - we first update the OriginDimensionSpacePoints, as we need the + // hierarchy relations for this query. Then, we update the Hierarchy Relations. + + // 1) originDimensionSpacePoint on Node + $selectRelationsStatement = <<tableNames->node()} n + INNER JOIN {$this->tableNames->hierarchyRelation()} h + ON h.childnodeanchor = n.relationanchorpoint + AND h.contentstreamid = :contentStreamId + AND h.dimensionspacepointhash = :dimensionSpacePointHash + -- find only nodes which have their ORIGIN at the source DimensionSpacePoint, + -- as we need to rewrite these origins (using copy on write) + AND n.origindimensionspacepointhash = :dimensionSpacePointHash + SQL; + try { + $relationAnchorPoints = $this->dbal->fetchFirstColumn($selectRelationsStatement, [ + 'dimensionSpacePointHash' => $event->source->hash, + 'contentStreamId' => $event->contentStreamId->value + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to load relation anchor points: %s', $e->getMessage()), 1716489628, $e); + } + foreach ($relationAnchorPoints as $relationAnchorPoint) { + $this->updateNodeRecordWithCopyOnWrite( + $event->contentStreamId, + NodeRelationAnchorPoint::fromInteger($relationAnchorPoint), + function (NodeRecord $nodeRecord) use ($event) { + $nodeRecord->originDimensionSpacePoint = $event->target->coordinates; + $nodeRecord->originDimensionSpacePointHash = $event->target->hash; + } + ); + } + + // 2) hierarchy relations + $updateHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} h + SET + h.dimensionspacepointhash = :newDimensionSpacePointHash + WHERE + h.dimensionspacepointhash = :originalDimensionSpacePointHash + AND h.contentstreamid = :contentStreamId + SQL; + try { + $this->dbal->executeStatement($updateHierarchyRelationsStatement, [ + 'originalDimensionSpacePointHash' => $event->source->hash, + 'newDimensionSpacePointHash' => $event->target->hash, + 'contentStreamId' => $event->contentStreamId->value, + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to update hierarchy relations: %s', $e->getMessage()), 1716489951, $e); + } + } + + private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $event, EventEnvelope $eventEnvelope): void + { + foreach ( + $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream( + $event->nodeAggregateId, + $event->contentStreamId, + ) as $anchorPoint + ) { + $this->updateNodeRecordWithCopyOnWrite( + $event->contentStreamId, + $anchorPoint, + function (NodeRecord $node) use ($event, $eventEnvelope) { + $node->nodeName = $event->newNodeName; + $node->timestamps = $node->timestamps->with( + lastModified: $eventEnvelope->recordedAt, + originalLastModified: self::initiatingDateTime($eventEnvelope) + ); + } + ); + } + } + + private function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $event, EventEnvelope $eventEnvelope): void + { + $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream($event->nodeAggregateId, $event->contentStreamId); + foreach ($anchorPoints as $anchorPoint) { + $this->updateNodeRecordWithCopyOnWrite( + $event->contentStreamId, + $anchorPoint, + function (NodeRecord $node) use ($event, $eventEnvelope) { + $node->nodeTypeName = $event->newNodeTypeName; + $node->timestamps = $node->timestamps->with( + lastModified: $eventEnvelope->recordedAt, + originalLastModified: self::initiatingDateTime($eventEnvelope) + ); + } + ); + } + } + + private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void + { + $this->moveNodeAggregate($event->contentStreamId, $event->nodeAggregateId, $event->newParentNodeAggregateId, $event->succeedingSiblingsForCoverage); + } + + private function whenNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): void + { + $this->removeNodeAggregate($event->contentStreamId, $event->nodeAggregateId, $event->affectedCoveredDimensionSpacePoints); + } + + private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCreated $event, EventEnvelope $eventEnvelope): void + { + $this->createNodeWithHierarchy( + $event->contentStreamId, + $event->nodeAggregateId, + $event->nodeTypeName, + $event->parentNodeAggregateId, + $event->originDimensionSpacePoint, + $event->succeedingSiblingsForCoverage, + $event->initialPropertyValues, + $event->nodeAggregateClassification, + $event->nodeName, + $eventEnvelope, + ); + } + + private function whenNodeGeneralizationVariantWasCreated(NodeGeneralizationVariantWasCreated $event, EventEnvelope $eventEnvelope): void + { + $this->createNodeGeneralizationVariant($event->contentStreamId, $event->nodeAggregateId, $event->sourceOrigin, $event->generalizationOrigin, $event->variantSucceedingSiblings, $eventEnvelope); + } + + private function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event, EventEnvelope $eventEnvelope): void + { + $this->createNodePeerVariant($event->contentStreamId, $event->nodeAggregateId, $event->sourceOrigin, $event->peerOrigin, $event->peerSucceedingSiblings, $eventEnvelope); + } + + private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEnvelope $eventEnvelope): void + { + $anchorPoint = $this->projectionContentGraph + ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( + $event->getNodeAggregateId(), + $event->getOriginDimensionSpacePoint(), + $event->getContentStreamId() + ); + if (is_null($anchorPoint)) { + throw new \InvalidArgumentException( + 'Cannot update node with copy on write since no anchor point could be resolved for node ' + . $event->getNodeAggregateId()->value . ' in content stream ' + . $event->getContentStreamId()->value, + 1645303332 + ); + } + $this->updateNodeRecordWithCopyOnWrite( + $event->getContentStreamId(), + $anchorPoint, + function (NodeRecord $node) use ($event, $eventEnvelope) { + $node->properties = $node->properties + ->merge($event->propertyValues) + ->unsetProperties($event->propertiesToUnset); + $node->timestamps = $node->timestamps->with( + lastModified: $eventEnvelope->recordedAt, + originalLastModified: self::initiatingDateTime($eventEnvelope) + ); + } + ); + } + + private function whenNodeReferencesWereSet(NodeReferencesWereSet $event, EventEnvelope $eventEnvelope): void + { + foreach ($event->affectedSourceOriginDimensionSpacePoints as $originDimensionSpacePoint) { + $nodeAnchorPoint = $this->projectionContentGraph + ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( + $event->sourceNodeAggregateId, + $originDimensionSpacePoint, + $event->contentStreamId + ); + + if (is_null($nodeAnchorPoint)) { + throw new \InvalidArgumentException( + 'Could not apply event of type "' . get_class($event) + . '" since no anchor point could be resolved for node ' + . $event->getNodeAggregateId()->value . ' in content stream ' + . $event->getContentStreamId()->value, + 1658580583 + ); + } + + $this->updateNodeRecordWithCopyOnWrite( + $event->contentStreamId, + $nodeAnchorPoint, + function (NodeRecord $node) use ($eventEnvelope) { + $node->timestamps = $node->timestamps->with( lastModified: $eventEnvelope->recordedAt, originalLastModified: self::initiatingDateTime($eventEnvelope) ); } ); + + $nodeAnchorPoint = $this->projectionContentGraph + ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( + $event->sourceNodeAggregateId, + $originDimensionSpacePoint, + $event->contentStreamId + ); + + // remove old + try { + $this->dbal->delete($this->tableNames->referenceRelation(), [ + 'nodeanchorpoint' => $nodeAnchorPoint?->value, + 'name' => $event->referenceName->value + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to remove reference relation: %s', $e->getMessage()), 1716486309, $e); + } + + // set new + $position = 0; + /** @var SerializedNodeReference $reference */ + foreach ($event->references as $reference) { + $referencePropertiesJson = null; + if ($reference->properties !== null) { + try { + $referencePropertiesJson = \json_encode($reference->properties, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT); + } catch (\JsonException $e) { + throw new \RuntimeException(sprintf('Failed to JSON-encode reference properties: %s', $e->getMessage()), 1716486271, $e); + } + } + try { + $this->dbal->insert($this->tableNames->referenceRelation(), [ + 'name' => $event->referenceName->value, + 'position' => $position, + 'nodeanchorpoint' => $nodeAnchorPoint?->value, + 'destinationnodeaggregateid' => $reference->targetNodeAggregateId->value, + 'properties' => $referencePropertiesJson, + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to insert reference relation: %s', $e->getMessage()), 1716486309, $e); + } + $position++; + } + } + } + + private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVariantWasCreated $event, EventEnvelope $eventEnvelope): void + { + $this->createNodeSpecializationVariant($event->contentStreamId, $event->nodeAggregateId, $event->sourceOrigin, $event->specializationOrigin, $event->specializationSiblings, $eventEnvelope); + } + + private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDimensionsWereUpdated $event): void + { + $rootNodeAnchorPoint = $this->projectionContentGraph + ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( + $event->nodeAggregateId, + /** the origin DSP of the root node is always the empty dimension ({@see whenRootNodeAggregateWithNodeWasCreated}) */ + OriginDimensionSpacePoint::createWithoutDimensions(), + $event->contentStreamId + ); + if ($rootNodeAnchorPoint === null) { + // should never happen. + return; + } + + // delete all hierarchy edges of the root node + $deleteHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} + WHERE + parentnodeanchor = :parentNodeAnchor + AND childnodeanchor = :childNodeAnchor + AND contentstreamid = :contentStreamId + SQL; + try { + $this->dbal->executeStatement($deleteHierarchyRelationsStatement, [ + 'parentNodeAnchor' => NodeRelationAnchorPoint::forRootEdge()->value, + 'childNodeAnchor' => $rootNodeAnchorPoint->value, + 'contentStreamId' => $event->contentStreamId->value, + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to delete hierarchy relation: %s', $e->getMessage()), 1716488943, $e); + } + // recreate hierarchy edges for the root node + $this->connectHierarchy( + $event->contentStreamId, + NodeRelationAnchorPoint::forRootEdge(), + $rootNodeAnchorPoint, + $event->coveredDimensionSpacePoints, + null + ); + } + + private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNodeWasCreated $event, EventEnvelope $eventEnvelope): void + { + $originDimensionSpacePoint = OriginDimensionSpacePoint::createWithoutDimensions(); + $node = NodeRecord::createNewInDatabase( + $this->dbal, + $this->tableNames, + $event->nodeAggregateId, + $originDimensionSpacePoint->coordinates, + $originDimensionSpacePoint->hash, + SerializedPropertyValues::createEmpty(), + $event->nodeTypeName, + $event->nodeAggregateClassification, + null, + Timestamps::create($eventEnvelope->recordedAt, self::initiatingDateTime($eventEnvelope), null, null), + ); + + $this->connectHierarchy( + $event->contentStreamId, + NodeRelationAnchorPoint::forRootEdge(), + $node->relationAnchorPoint, + $event->coveredDimensionSpacePoints, + null + ); + } + + private function whenSubtreeWasTagged(SubtreeWasTagged $event): void + { + $this->addSubtreeTag($event->contentStreamId, $event->nodeAggregateId, $event->affectedDimensionSpacePoints, $event->tag); + } + + private function whenSubtreeWasUntagged(SubtreeWasUntagged $event): void + { + $this->removeSubtreeTag($event->contentStreamId, $event->nodeAggregateId, $event->affectedDimensionSpacePoints, $event->tag); + } + + + /** --------------------------------- */ + + /** + * @return array + */ + private function determineRequiredSqlStatements(): array + { + $schemaManager = $this->dbal->getSchemaManager(); + if (!$schemaManager instanceof AbstractSchemaManager) { + throw new \RuntimeException('Failed to retrieve Schema Manager', 1625653914); + } + $schema = (new DoctrineDbalContentGraphSchemaBuilder($this->tableNames))->buildSchema($schemaManager); + return DbalSchemaDiff::determineRequiredSqlStatements($this->dbal, $schema); + } + + private function truncateDatabaseTables(): void + { + try { + $this->dbal->executeQuery('TRUNCATE table ' . $this->tableNames->node()); + $this->dbal->executeQuery('TRUNCATE table ' . $this->tableNames->hierarchyRelation()); + $this->dbal->executeQuery('TRUNCATE table ' . $this->tableNames->referenceRelation()); + $this->dbal->executeQuery('TRUNCATE table ' . $this->tableNames->dimensionSpacePoints()); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to truncate database tables for projection %s: %s', self::class, $e->getMessage()), 1716478318, $e); + } + } + + /** + * @param callable(NodeRecord): T $operations + * @return T + * @template T + */ + private function updateNodeRecordWithCopyOnWrite( + ContentStreamId $contentStreamIdWhereWriteOccurs, + NodeRelationAnchorPoint $anchorPoint, + callable $operations + ): mixed { + $contentStreamIds = $this->projectionContentGraph->getAllContentStreamIdsAnchorPointIsContainedIn($anchorPoint); + if (count($contentStreamIds) > 1) { + // Copy on Write needed! + // Copy on Write is a purely "Content Stream" related concept; + // thus we do not care about different DimensionSpacePoints here (but we copy all edges) + + // 1) fetch node, adjust properties, assign new Relation Anchor Point + /** @var NodeRecord $originalNode The anchor point appears in a content stream, so there must be a node */ + $originalNode = $this->projectionContentGraph->getNodeByAnchorPoint($anchorPoint); + $copiedNode = NodeRecord::createCopyFromNodeRecord($this->dbal, $this->tableNames, $originalNode); + $result = $operations($copiedNode); + $copiedNode->updateToDatabase($this->dbal, $this->tableNames); + + // 2) reconnect all edges belonging to this content stream to the new "copied node". + // IMPORTANT: We need to reconnect BOTH the incoming and outgoing edges. + $updateHierarchyRelationStatement = <<tableNames->hierarchyRelation()} h + SET + -- if our (copied) node is the child, we update h.childNodeAnchor + h.childnodeanchor = IF(h.childnodeanchor = :originalNodeAnchor, :newNodeAnchor, h.childnodeanchor), + + -- if our (copied) node is the parent, we update h.parentNodeAnchor + h.parentnodeanchor = IF(h.parentnodeanchor = :originalNodeAnchor, :newNodeAnchor, h.parentnodeanchor) + WHERE + :originalNodeAnchor IN (h.childnodeanchor, h.parentnodeanchor) + AND h.contentstreamid = :contentStreamId + SQL; + try { + $this->dbal->executeStatement($updateHierarchyRelationStatement, [ + 'newNodeAnchor' => $copiedNode->relationAnchorPoint->value, + 'originalNodeAnchor' => $anchorPoint->value, + 'contentStreamId' => $contentStreamIdWhereWriteOccurs->value, + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to update hierarchy relation: %s', $e->getMessage()), 1716486444, $e); + } + // reference relation rows need to be copied as well! + $this->copyReferenceRelations( + $anchorPoint, + $copiedNode->relationAnchorPoint + ); + return $result; + } + + // else: No copy on write needed :) + + $node = $this->projectionContentGraph->getNodeByAnchorPoint($anchorPoint); + if (!$node) { + throw new \RuntimeException(sprintf('Failed to find node for anchor point %s. This is probably a bug in the %s', $anchorPoint->value, self::class), 1716488997); + } + $result = $operations($node); + $node->updateToDatabase($this->dbal, $this->tableNames); + return $result; + } + + private function copyReferenceRelations( + NodeRelationAnchorPoint $sourceRelationAnchorPoint, + NodeRelationAnchorPoint $destinationRelationAnchorPoint + ): void { + $copyReferenceRelationStatement = <<tableNames->referenceRelation()} ( + nodeanchorpoint, + name, + position, + destinationnodeaggregateid + ) + SELECT + :destinationRelationAnchorPoint AS nodeanchorpoint, + ref.name, + ref.position, + ref.destinationnodeaggregateid + FROM + {$this->tableNames->referenceRelation()} ref + WHERE ref.nodeanchorpoint = :sourceNodeAnchorPoint + SQL; + try { + $this->dbal->executeStatement($copyReferenceRelationStatement, [ + 'sourceNodeAnchorPoint' => $sourceRelationAnchorPoint->value, + 'destinationRelationAnchorPoint' => $destinationRelationAnchorPoint->value + ]); + } catch (DbalException $e) { + throw new \RuntimeException(sprintf('Failed to copy reference relations: %s', $e->getMessage()), 1716489394, $e); + } + } + + private static function initiatingDateTime(EventEnvelope $eventEnvelope): \DateTimeImmutable + { + $initiatingTimestamp = $eventEnvelope->event->metadata?->get('initiatingTimestamp'); + $result = $initiatingTimestamp !== null ? \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $initiatingTimestamp) : $eventEnvelope->recordedAt; + if (!$result instanceof \DateTimeImmutable) { + throw new \RuntimeException(sprintf('Failed to extract initiating timestamp from event "%s"', $eventEnvelope->event->id->value), 1678902291); } + return $result; } private function createNodeWithHierarchy( @@ -485,7 +928,7 @@ private function getRelationPositionAfterRecalculation( usort( $hierarchyRelations, static fn (HierarchyRelation $relationA, HierarchyRelation $relationB): int - => $relationA->position <=> $relationB->position + => $relationA->position <=> $relationB->position ); foreach ($hierarchyRelations as $relation) { @@ -499,195 +942,8 @@ private function getRelationPositionAfterRecalculation( } $relation->assignNewPosition($offset, $this->dbal, $this->tableNames); } - - return $position; - } - - private function whenContentStreamWasForked(ContentStreamWasForked $event): void - { - // - // 1) Copy HIERARCHY RELATIONS (this is the MAIN OPERATION here) - // - $insertRelationStatement = <<tableNames->hierarchyRelation()} ( - parentnodeanchor, - childnodeanchor, - position, - dimensionspacepointhash, - subtreetags, - contentstreamid - ) - SELECT - h.parentnodeanchor, - h.childnodeanchor, - h.position, - h.dimensionspacepointhash, - h.subtreetags, - "{$event->newContentStreamId->value}" AS contentstreamid - FROM - {$this->tableNames->hierarchyRelation()} h - WHERE h.contentstreamid = :sourceContentStreamId - SQL; - try { - $this->dbal->executeStatement($insertRelationStatement, [ - 'sourceContentStreamId' => $event->sourceContentStreamId->value - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to insert hierarchy relation: %s', $e->getMessage()), 1716489211, $e); - } - - // NOTE: as reference edges are attached to Relation Anchor Points (and they are lazily copy-on-written), - // we do not need to copy reference edges here (but we need to do it during copy on write). - } - - private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): void - { - // Drop hierarchy relations - $deleteHierarchyRelationStatement = <<tableNames->hierarchyRelation()} WHERE contentstreamid = :contentStreamId - SQL; - try { - $this->dbal->executeStatement($deleteHierarchyRelationStatement, [ - 'contentStreamId' => $event->contentStreamId->value - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to delete hierarchy relations: %s', $e->getMessage()), 1716489265, $e); - } - - // Drop non-referenced nodes (which do not have a hierarchy relation anymore) - $deleteNodesStatement = <<tableNames->node()} - WHERE NOT EXISTS ( - SELECT 1 FROM {$this->tableNames->hierarchyRelation()} - WHERE {$this->tableNames->hierarchyRelation()}.childnodeanchor = {$this->tableNames->node()}.relationanchorpoint - ) - SQL; - try { - $this->dbal->executeStatement($deleteNodesStatement); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to delete non-referenced nodes: %s', $e->getMessage()), 1716489294, $e); - } - - // Drop non-referenced reference relations (i.e. because the referenced nodes are gone by now) - $deleteReferenceRelationsStatement = <<tableNames->referenceRelation()} - WHERE NOT EXISTS ( - SELECT 1 FROM {$this->tableNames->node()} - WHERE {$this->tableNames->node()}.relationanchorpoint = {$this->tableNames->referenceRelation()}.nodeanchorpoint - ) - SQL; - try { - $this->dbal->executeStatement($deleteReferenceRelationsStatement); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to delete non-referenced reference relations: %s', $e->getMessage()), 1716489328, $e); - } - } - - private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEnvelope $eventEnvelope): void - { - $anchorPoint = $this->projectionContentGraph - ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( - $event->getNodeAggregateId(), - $event->getOriginDimensionSpacePoint(), - $event->getContentStreamId() - ); - if (is_null($anchorPoint)) { - throw new \InvalidArgumentException( - 'Cannot update node with copy on write since no anchor point could be resolved for node ' - . $event->getNodeAggregateId()->value . ' in content stream ' - . $event->getContentStreamId()->value, - 1645303332 - ); - } - $this->updateNodeRecordWithCopyOnWrite( - $event->getContentStreamId(), - $anchorPoint, - function (NodeRecord $node) use ($event, $eventEnvelope) { - $node->properties = $node->properties - ->merge($event->propertyValues) - ->unsetProperties($event->propertiesToUnset); - $node->timestamps = $node->timestamps->with( - lastModified: $eventEnvelope->recordedAt, - originalLastModified: self::initiatingDateTime($eventEnvelope) - ); - } - ); - } - - private function whenNodeReferencesWereSet(NodeReferencesWereSet $event, EventEnvelope $eventEnvelope): void - { - foreach ($event->affectedSourceOriginDimensionSpacePoints as $originDimensionSpacePoint) { - $nodeAnchorPoint = $this->projectionContentGraph - ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( - $event->sourceNodeAggregateId, - $originDimensionSpacePoint, - $event->contentStreamId - ); - - if (is_null($nodeAnchorPoint)) { - throw new \InvalidArgumentException( - 'Could not apply event of type "' . get_class($event) - . '" since no anchor point could be resolved for node ' - . $event->getNodeAggregateId()->value . ' in content stream ' - . $event->getContentStreamId()->value, - 1658580583 - ); - } - - $this->updateNodeRecordWithCopyOnWrite( - $event->contentStreamId, - $nodeAnchorPoint, - function (NodeRecord $node) use ($eventEnvelope) { - $node->timestamps = $node->timestamps->with( - lastModified: $eventEnvelope->recordedAt, - originalLastModified: self::initiatingDateTime($eventEnvelope) - ); - } - ); - - $nodeAnchorPoint = $this->projectionContentGraph - ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( - $event->sourceNodeAggregateId, - $originDimensionSpacePoint, - $event->contentStreamId - ); - - // remove old - try { - $this->dbal->delete($this->tableNames->referenceRelation(), [ - 'nodeanchorpoint' => $nodeAnchorPoint?->value, - 'name' => $event->referenceName->value - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to remove reference relation: %s', $e->getMessage()), 1716486309, $e); - } - - // set new - $position = 0; - /** @var SerializedNodeReference $reference */ - foreach ($event->references as $reference) { - $referencePropertiesJson = null; - if ($reference->properties !== null) { - try { - $referencePropertiesJson = \json_encode($reference->properties, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT); - } catch (\JsonException $e) { - throw new \RuntimeException(sprintf('Failed to JSON-encode reference properties: %s', $e->getMessage()), 1716486271, $e); - } - } - try { - $this->dbal->insert($this->tableNames->referenceRelation(), [ - 'name' => $event->referenceName->value, - 'position' => $position, - 'nodeanchorpoint' => $nodeAnchorPoint?->value, - 'destinationnodeaggregateid' => $reference->targetNodeAggregateId->value, - 'properties' => $referencePropertiesJson, - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to insert reference relation: %s', $e->getMessage()), 1716486309, $e); - } - $position++; - } - } + + return $position; } private function copyHierarchyRelationToDimensionSpacePoint( @@ -743,258 +999,4 @@ private function copyNodeToDimensionSpacePoint( ), ); } - - private function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $event, EventEnvelope $eventEnvelope): void - { - $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream($event->nodeAggregateId, $event->contentStreamId); - foreach ($anchorPoints as $anchorPoint) { - $this->updateNodeRecordWithCopyOnWrite( - $event->contentStreamId, - $anchorPoint, - function (NodeRecord $node) use ($event, $eventEnvelope) { - $node->nodeTypeName = $event->newNodeTypeName; - $node->timestamps = $node->timestamps->with( - lastModified: $eventEnvelope->recordedAt, - originalLastModified: self::initiatingDateTime($eventEnvelope) - ); - } - ); - } - } - - /** - * @param callable(NodeRecord): T $operations - * @return T - * @template T - */ - private function updateNodeRecordWithCopyOnWrite( - ContentStreamId $contentStreamIdWhereWriteOccurs, - NodeRelationAnchorPoint $anchorPoint, - callable $operations - ): mixed { - $contentStreamIds = $this->projectionContentGraph->getAllContentStreamIdsAnchorPointIsContainedIn($anchorPoint); - if (count($contentStreamIds) > 1) { - // Copy on Write needed! - // Copy on Write is a purely "Content Stream" related concept; - // thus we do not care about different DimensionSpacePoints here (but we copy all edges) - - // 1) fetch node, adjust properties, assign new Relation Anchor Point - /** @var NodeRecord $originalNode The anchor point appears in a content stream, so there must be a node */ - $originalNode = $this->projectionContentGraph->getNodeByAnchorPoint($anchorPoint); - $copiedNode = NodeRecord::createCopyFromNodeRecord($this->dbal, $this->tableNames, $originalNode); - $result = $operations($copiedNode); - $copiedNode->updateToDatabase($this->dbal, $this->tableNames); - - // 2) reconnect all edges belonging to this content stream to the new "copied node". - // IMPORTANT: We need to reconnect BOTH the incoming and outgoing edges. - $updateHierarchyRelationStatement = <<tableNames->hierarchyRelation()} h - SET - -- if our (copied) node is the child, we update h.childNodeAnchor - h.childnodeanchor = IF(h.childnodeanchor = :originalNodeAnchor, :newNodeAnchor, h.childnodeanchor), - - -- if our (copied) node is the parent, we update h.parentNodeAnchor - h.parentnodeanchor = IF(h.parentnodeanchor = :originalNodeAnchor, :newNodeAnchor, h.parentnodeanchor) - WHERE - :originalNodeAnchor IN (h.childnodeanchor, h.parentnodeanchor) - AND h.contentstreamid = :contentStreamId - SQL; - try { - $this->dbal->executeStatement($updateHierarchyRelationStatement, [ - 'newNodeAnchor' => $copiedNode->relationAnchorPoint->value, - 'originalNodeAnchor' => $anchorPoint->value, - 'contentStreamId' => $contentStreamIdWhereWriteOccurs->value, - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to update hierarchy relation: %s', $e->getMessage()), 1716486444, $e); - } - // reference relation rows need to be copied as well! - $this->copyReferenceRelations( - $anchorPoint, - $copiedNode->relationAnchorPoint - ); - return $result; - } - - // else: No copy on write needed :) - - $node = $this->projectionContentGraph->getNodeByAnchorPoint($anchorPoint); - if (!$node) { - throw new \RuntimeException(sprintf('Failed to find node for anchor point %s. This is probably a bug in the %s', $anchorPoint->value, self::class), 1716488997); - } - $result = $operations($node); - $node->updateToDatabase($this->dbal, $this->tableNames); - return $result; - } - - - private function copyReferenceRelations( - NodeRelationAnchorPoint $sourceRelationAnchorPoint, - NodeRelationAnchorPoint $destinationRelationAnchorPoint - ): void { - $copyReferenceRelationStatement = <<tableNames->referenceRelation()} ( - nodeanchorpoint, - name, - position, - destinationnodeaggregateid - ) - SELECT - :destinationRelationAnchorPoint AS nodeanchorpoint, - ref.name, - ref.position, - ref.destinationnodeaggregateid - FROM - {$this->tableNames->referenceRelation()} ref - WHERE ref.nodeanchorpoint = :sourceNodeAnchorPoint - SQL; - try { - $this->dbal->executeStatement($copyReferenceRelationStatement, [ - 'sourceNodeAnchorPoint' => $sourceRelationAnchorPoint->value, - 'destinationRelationAnchorPoint' => $destinationRelationAnchorPoint->value - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to copy reference relations: %s', $e->getMessage()), 1716489394, $e); - } - } - - private function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $event): void - { - $this->dimensionSpacePointsRepository->insertDimensionSpacePoint($event->target); - - // the ordering is important - we first update the OriginDimensionSpacePoints, as we need the - // hierarchy relations for this query. Then, we update the Hierarchy Relations. - - // 1) originDimensionSpacePoint on Node - $selectRelationsStatement = <<tableNames->node()} n - INNER JOIN {$this->tableNames->hierarchyRelation()} h - ON h.childnodeanchor = n.relationanchorpoint - AND h.contentstreamid = :contentStreamId - AND h.dimensionspacepointhash = :dimensionSpacePointHash - -- find only nodes which have their ORIGIN at the source DimensionSpacePoint, - -- as we need to rewrite these origins (using copy on write) - AND n.origindimensionspacepointhash = :dimensionSpacePointHash - SQL; - try { - $relationAnchorPoints = $this->dbal->fetchFirstColumn($selectRelationsStatement, [ - 'dimensionSpacePointHash' => $event->source->hash, - 'contentStreamId' => $event->contentStreamId->value - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to load relation anchor points: %s', $e->getMessage()), 1716489628, $e); - } - foreach ($relationAnchorPoints as $relationAnchorPoint) { - $this->updateNodeRecordWithCopyOnWrite( - $event->contentStreamId, - NodeRelationAnchorPoint::fromInteger($relationAnchorPoint), - function (NodeRecord $nodeRecord) use ($event) { - $nodeRecord->originDimensionSpacePoint = $event->target->coordinates; - $nodeRecord->originDimensionSpacePointHash = $event->target->hash; - } - ); - } - - // 2) hierarchy relations - $updateHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} h - SET - h.dimensionspacepointhash = :newDimensionSpacePointHash - WHERE - h.dimensionspacepointhash = :originalDimensionSpacePointHash - AND h.contentstreamid = :contentStreamId - SQL; - try { - $this->dbal->executeStatement($updateHierarchyRelationsStatement, [ - 'originalDimensionSpacePointHash' => $event->source->hash, - 'newDimensionSpacePointHash' => $event->target->hash, - 'contentStreamId' => $event->contentStreamId->value, - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to update hierarchy relations: %s', $e->getMessage()), 1716489951, $e); - } - } - - private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded $event): void - { - $this->dimensionSpacePointsRepository->insertDimensionSpacePoint($event->target); - - // 1) hierarchy relations - $insertHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} ( - parentnodeanchor, - childnodeanchor, - position, - subtreetags, - dimensionspacepointhash, - contentstreamid - ) - SELECT - h.parentnodeanchor, - h.childnodeanchor, - h.position, - h.subtreetags, - :newDimensionSpacePointHash AS dimensionspacepointhash, - h.contentstreamid - FROM - {$this->tableNames->hierarchyRelation()} h - WHERE h.contentstreamid = :contentStreamId - AND h.dimensionspacepointhash = :sourceDimensionSpacePointHash - SQL; - try { - $this->dbal->executeStatement($insertHierarchyRelationsStatement, [ - 'contentStreamId' => $event->contentStreamId->value, - 'sourceDimensionSpacePointHash' => $event->source->hash, - 'newDimensionSpacePointHash' => $event->target->hash, - ]); - } catch (DbalException $e) { - throw new \RuntimeException(sprintf('Failed to insert hierarchy relations: %s', $e->getMessage()), 1716490758, $e); - } - } - - private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void - { - $this->moveNodeAggregate($event->contentStreamId, $event->nodeAggregateId, $event->newParentNodeAggregateId, $event->succeedingSiblingsForCoverage); - } - - private function whenSubtreeWasTagged(SubtreeWasTagged $event): void - { - $this->addSubtreeTag($event->contentStreamId, $event->nodeAggregateId, $event->affectedDimensionSpacePoints, $event->tag); - } - - private function whenSubtreeWasUntagged(SubtreeWasUntagged $event): void - { - $this->removeSubtreeTag($event->contentStreamId, $event->nodeAggregateId, $event->affectedDimensionSpacePoints, $event->tag); - } - - private function whenNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): void - { - $this->removeNodeAggregate($event->contentStreamId, $event->nodeAggregateId, $event->affectedCoveredDimensionSpacePoints); - } - - private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVariantWasCreated $event, EventEnvelope $eventEnvelope): void - { - $this->createNodeSpecializationVariant($event->contentStreamId, $event->nodeAggregateId, $event->sourceOrigin, $event->specializationOrigin, $event->specializationSiblings, $eventEnvelope); - } - - private function whenNodeGeneralizationVariantWasCreated(NodeGeneralizationVariantWasCreated $event, EventEnvelope $eventEnvelope): void - { - $this->createNodeGeneralizationVariant($event->contentStreamId, $event->nodeAggregateId, $event->sourceOrigin, $event->generalizationOrigin, $event->variantSucceedingSiblings, $eventEnvelope); - } - - private function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event, EventEnvelope $eventEnvelope): void - { - $this->createNodePeerVariant($event->contentStreamId, $event->nodeAggregateId, $event->sourceOrigin, $event->peerOrigin, $event->peerSucceedingSiblings, $eventEnvelope); - } - - private static function initiatingDateTime(EventEnvelope $eventEnvelope): \DateTimeImmutable - { - $initiatingTimestamp = $eventEnvelope->event->metadata?->get('initiatingTimestamp'); - $result = $initiatingTimestamp !== null ? \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $initiatingTimestamp) : $eventEnvelope->recordedAt; - if (!$result instanceof \DateTimeImmutable) { - throw new \RuntimeException(sprintf('Failed to extract initiating timestamp from event "%s"', $eventEnvelope->event->id->value), 1678902291); - } - return $result; - } }