diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphReadModelAdapter.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphReadModelAdapter.php index 2bef2ecc699..c50bc1b87b2 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphReadModelAdapter.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphReadModelAdapter.php @@ -17,7 +17,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Query\QueryBuilder; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamLayers; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ContentGraph; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; @@ -51,27 +51,27 @@ public function getContentGraph(WorkspaceName $workspaceName): ContentGraph $currentContentStreamIdStatement = <<tableNames->workspace()} ws - JOIN {$this->tableNames->contentStream()} cs ON cs.id = ws.currentContentStreamId + JOIN {$this->tableNames->contentStreamLayer()} l ON l.contentStreamId = ws.currentContentStreamId WHERE ws.name = :workspaceName - LIMIT 1 SQL; try { - $row = $this->dbal->fetchAssociative($currentContentStreamIdStatement, [ + $rows = $this->dbal->fetchAllAssociative($currentContentStreamIdStatement, [ 'workspaceName' => $workspaceName->value, ]); } catch (Exception $e) { throw new \RuntimeException(sprintf('Failed to load current content stream id from database: %s', $e->getMessage()), 1716903166, $e); } - if ($row === false) { + if ($rows === []) { throw WorkspaceDoesNotExist::butWasSupposedTo($workspaceName); } - $currentContentStreamId = ContentStreamId::fromString($row['currentContentStreamId']); - $contentStreamDbId = ContentStreamDbId::fromInt((int)$row['contentStreamDbId']); - return new ContentGraph($this->dbal, $this->nodeFactory, $this->contentRepositoryId, $this->nodeTypeManager, $this->tableNames, $workspaceName, $currentContentStreamId, $contentStreamDbId); + $firstRow = reset($rows); + $currentContentStreamId = ContentStreamId::fromString($firstRow['currentContentStreamId']); + $contentStreamLayers = ContentStreamLayers::fromArray(array_column($rows, 'contentStreamLayer')); + return new ContentGraph($this->dbal, $this->nodeFactory, $this->contentRepositoryId, $this->nodeTypeManager, $this->tableNames, $workspaceName, $currentContentStreamId, $contentStreamLayers); } public function findWorkspaceByName(WorkspaceName $workspaceName): ?Workspace diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphTableNames.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphTableNames.php index 51f14b392cc..26644a082d0 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphTableNames.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphTableNames.php @@ -51,4 +51,9 @@ public function contentStream(): string { return $this->tableNamePrefix . '_contentstream'; } + + public function contentStreamLayer(): string + { + return $this->tableNamePrefix . '_contentstreamlayer'; + } } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index cf76c111067..fee6e8dfd2a 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -7,7 +7,7 @@ use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception as DBALException; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamLayers; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\ContentStream; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\NodeMove; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\NodeRemoval; @@ -15,9 +15,10 @@ use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\SubtreeTagging; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\Workspace; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelationId; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRecord; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ContentStreamDbIdFinder; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ContentStreamLayerFinder; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ProjectionContentGraph; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; @@ -86,6 +87,7 @@ final class DoctrineDbalContentGraphProjection implements ContentGraphProjection use SubtreeTagging; use Workspace; + private HierarchyRelationStatement $hierarchyRelationStatement; public const RELATION_DEFAULT_OFFSET = 128; @@ -94,9 +96,10 @@ public function __construct( private readonly ProjectionContentGraph $projectionContentGraph, private readonly ContentGraphTableNames $tableNames, private readonly DimensionSpacePointsRepository $dimensionSpacePointsRepository, - private readonly ContentStreamDbIdFinder $contentStreamDbIdFinder, + private readonly ContentStreamLayerFinder $contentStreamLayerFinder, private readonly ContentGraphReadModelInterface $contentGraphReadModel ) { + $this->hierarchyRelationStatement = HierarchyRelationStatement::for($this->tableNames); } public function setUp(): void @@ -211,96 +214,87 @@ private function whenContentStreamWasClosed(ContentStreamWasClosed $event): void private function whenContentStreamWasCreated(ContentStreamWasCreated $event): void { $this->createContentStream($event->contentStreamId); + $this->dbal->insert($this->tableNames->contentStreamLayer(), [ + 'contentStreamId' => $event->contentStreamId->value, + ]); } private function whenContentStreamWasForked(ContentStreamWasForked $event): void { $this->createContentStream($event->newContentStreamId, $event->sourceContentStreamId, $event->versionOfSourceContentStream); - $newContentStreamDbId = $this->contentStreamDbIdFinder->getContentStreamDbId($event->newContentStreamId); - $sourceContentStreamDbId = $this->contentStreamDbIdFinder->getContentStreamDbId($event->sourceContentStreamId); + $sourceContentStreamLayer = $this->contentStreamLayerFinder->getContentStreamLayers($event->sourceContentStreamId); - // - // 1) Copy HIERARCHY RELATIONS (this is the MAIN OPERATION here) - // - $insertRelationStatement = <<tableNames->hierarchyRelation()} ( - parentnodeanchor, - childnodeanchor, - position, - dimensionspacepointhash, - subtreetags, - contentstreamdbid - ) - SELECT - h.parentnodeanchor, - h.childnodeanchor, - h.position, - h.dimensionspacepointhash, - h.subtreetags, - :newContentStreamDbId AS contentstreamdbid - FROM - {$this->tableNames->hierarchyRelation()} h - WHERE h.contentstreamdbid = :sourceContentStreamDbId - SQL; - try { - $this->dbal->executeStatement($insertRelationStatement, [ - 'newContentStreamDbId' => $newContentStreamDbId->value, - 'sourceContentStreamDbId' => $sourceContentStreamDbId->value + $this->dbal->insert($this->tableNames->contentStreamLayer(), [ + 'contentStreamId' => $event->sourceContentStreamId, + ]); + + foreach ($sourceContentStreamLayer->items as $sourceDbId) { + $this->dbal->insert($this->tableNames->contentStreamLayer(), [ + 'contentStreamId' => $event->newContentStreamId, + 'contentStreamLayer' => $sourceDbId->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). + $this->dbal->insert($this->tableNames->contentStreamLayer(), [ + 'contentStreamId' => $event->newContentStreamId, + ]); } private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): void { - $contentStreamDbId = $this->contentStreamDbIdFinder->getContentStreamDbId($event->contentStreamId); + $contentStreamLayers = $this->contentStreamLayerFinder->getContentStreamLayers($event->contentStreamId); // Drop hierarchy relations - $deleteHierarchyRelationStatement = <<tableNames->hierarchyRelation()} WHERE contentstreamdbid = :contentStreamDbId - SQL; - try { - $this->dbal->executeStatement($deleteHierarchyRelationStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value - ]); - } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to delete hierarchy relations: %s', $e->getMessage()), 1716489265, $e); - } + // TODO reimplement + // $deleteHierarchyRelationStatement = <<tableNames->hierarchyRelation()} WHERE contentstreamlayer IN (:contentStreamLayers) + // SQL; + // try { + // $this->dbal->executeStatement($deleteHierarchyRelationStatement, [ + // 'contentStreamLayers' => $contentStreamLayers->toIntArray() + // ], [ + // 'contentStreamLayers' => ArrayParameterType::INTEGER, + // ]); + // } 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); - } + // TODO reimplement + // $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); - } + // TODO reimplement + // $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); + // } $this->removeContentStream($event->contentStreamId); + + $this->dbal->delete($this->tableNames->contentStreamLayer(), [ + 'contentStreamId' => $event->contentStreamId->value, + ]); } private function whenContentStreamWasReopened(ContentStreamWasReopened $event): void @@ -315,30 +309,31 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded // 1) hierarchy relations $insertHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} ( + contentstreamlayer, parentnodeanchor, childnodeanchor, position, subtreetags, - dimensionspacepointhash, - contentstreamdbid + dimensionspacepointhash ) SELECT + :targetContentStreamLayer as contentstreamlayer, h.parentnodeanchor, h.childnodeanchor, h.position, h.subtreetags, - :newDimensionSpacePointHash AS dimensionspacepointhash, - h.contentstreamdbid + :newDimensionSpacePointHash AS dimensionspacepointhash FROM - {$this->tableNames->hierarchyRelation()} h - WHERE h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :sourceDimensionSpacePointHash + {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :sourceDimensionSpacePointHash')->toSql()} h SQL; try { $this->dbal->executeStatement($insertHierarchyRelationsStatement, [ - 'contentStreamDbId' => $this->getContentStreamDbId($event)->value, + 'contentStreamLayers' => $this->getContentStreamLayers($event)->toIntArray(), 'sourceDimensionSpacePointHash' => $event->source->hash, 'newDimensionSpacePointHash' => $event->target->hash, + 'targetContentStreamLayer' => $this->getContentStreamLayers($event)->getWriteLayer()->value, + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to insert hierarchy relations: %s', $e->getMessage()), 1716490758, $e); @@ -356,10 +351,8 @@ private function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $ev $selectRelationsStatement = <<tableNames->node()} n - INNER JOIN {$this->tableNames->hierarchyRelation()} h + INNER JOIN {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h ON h.childnodeanchor = n.relationanchorpoint - AND h.contentstreamdbid = :contentStreamDbId - 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 @@ -368,14 +361,16 @@ private function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $ev try { $relationAnchorPoints = $this->dbal->fetchFirstColumn($selectRelationsStatement, [ 'dimensionSpacePointHash' => $event->source->hash, - 'contentStreamDbId' => $this->getContentStreamDbId($event)->value + 'contentStreamLayers' => $this->getContentStreamLayers($event)->toIntArray(), + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load relation anchor points: %s', $e->getMessage()), 1716489628, $e); } foreach ($relationAnchorPoints as $relationAnchorPoint) { $this->updateNodeRecordWithCopyOnWrite( - $this->getContentStreamDbId($event), + $this->getContentStreamLayers($event), NodeRelationAnchorPoint::fromInteger($relationAnchorPoint), function (NodeRecord $nodeRecord) use ($event) { $nodeRecord->originDimensionSpacePoint = $event->target->coordinates; @@ -386,18 +381,35 @@ function (NodeRecord $nodeRecord) use ($event) { // 2) hierarchy relations $updateHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} h - SET - h.dimensionspacepointhash = :newDimensionSpacePointHash - WHERE - h.dimensionspacepointhash = :originalDimensionSpacePointHash - AND h.contentstreamdbid = :contentStreamDbId + INSERT INTO {$this->tableNames->hierarchyRelation()} + ( + id, + contentstreamlayer, + parentnodeanchor, + childnodeanchor, + position, + subtreetags, + dimensionspacepointhash + ) + SELECT + h.id, + :targetContentStreamLayer as contentstreamlayer, + h.parentnodeanchor, + h.childnodeanchor, + h.position, + h.subtreetags, + :newDimensionSpacePointHash AS dimensionspacepointhash + FROM {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :originalDimensionSpacePointHash')->toSql()} AS h + ON DUPLICATE KEY UPDATE dimensionspacepointhash = VALUES(dimensionspacepointhash) SQL; try { $this->dbal->executeStatement($updateHierarchyRelationsStatement, [ 'originalDimensionSpacePointHash' => $event->source->hash, 'newDimensionSpacePointHash' => $event->target->hash, - 'contentStreamDbId' => $this->getContentStreamDbId($event)->value, + 'contentStreamLayers' => $this->getContentStreamLayers($event)->toIntArray(), + 'targetContentStreamLayer' => $this->getContentStreamLayers($event)->getWriteLayer()->value, + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to update hierarchy relations: %s', $e->getMessage()), 1716489951, $e); @@ -409,11 +421,11 @@ private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $ev foreach ( $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream( $event->nodeAggregateId, - $this->getContentStreamDbId($event), + $this->getContentStreamLayers($event), ) as $anchorPoint ) { $this->updateNodeRecordWithCopyOnWrite( - $this->getContentStreamDbId($event), + $this->getContentStreamLayers($event), $anchorPoint, function (NodeRecord $node) use ($event, $eventEnvelope) { $node->nodeName = $event->newNodeName; @@ -428,10 +440,10 @@ function (NodeRecord $node) use ($event, $eventEnvelope) { private function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $event, EventEnvelope $eventEnvelope): void { - $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream($event->nodeAggregateId, $this->getContentStreamDbId($event)); + $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream($event->nodeAggregateId, $this->getContentStreamLayers($event)); foreach ($anchorPoints as $anchorPoint) { $this->updateNodeRecordWithCopyOnWrite( - $this->getContentStreamDbId($event), + $this->getContentStreamLayers($event), $anchorPoint, function (NodeRecord $node) use ($event, $eventEnvelope) { $node->nodeTypeName = $event->newNodeTypeName; @@ -446,18 +458,18 @@ function (NodeRecord $node) use ($event, $eventEnvelope) { private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void { - $this->moveNodeAggregate($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->newParentNodeAggregateId, $event->succeedingSiblingsForCoverage); + $this->moveNodeAggregate($this->getContentStreamLayers($event), $event->nodeAggregateId, $event->newParentNodeAggregateId, $event->succeedingSiblingsForCoverage); } private function whenNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): void { - $this->removeNodeAggregate($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->affectedCoveredDimensionSpacePoints); + $this->removeNodeAggregate($this->getContentStreamLayers($event), $event->nodeAggregateId, $event->affectedCoveredDimensionSpacePoints); } private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCreated $event, EventEnvelope $eventEnvelope): void { $this->createNodeWithHierarchy( - $this->getContentStreamDbId($event), + $this->getContentStreamLayers($event), $event->nodeAggregateId, $event->nodeTypeName, $event->parentNodeAggregateId, @@ -473,12 +485,12 @@ private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCre private function whenNodeGeneralizationVariantWasCreated(NodeGeneralizationVariantWasCreated $event, EventEnvelope $eventEnvelope): void { - $this->createNodeGeneralizationVariant($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->sourceOrigin, $event->generalizationOrigin, $event->variantSucceedingSiblings, $eventEnvelope); + $this->createNodeGeneralizationVariant($this->getContentStreamLayers($event), $event->nodeAggregateId, $event->sourceOrigin, $event->generalizationOrigin, $event->variantSucceedingSiblings, $eventEnvelope); } private function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event, EventEnvelope $eventEnvelope): void { - $this->createNodePeerVariant($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->sourceOrigin, $event->peerOrigin, $event->peerSucceedingSiblings, $eventEnvelope); + $this->createNodePeerVariant($this->getContentStreamLayers($event), $event->nodeAggregateId, $event->sourceOrigin, $event->peerOrigin, $event->peerSucceedingSiblings, $eventEnvelope); } private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEnvelope $eventEnvelope): void @@ -487,7 +499,7 @@ private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEn ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( $event->getNodeAggregateId(), $event->getOriginDimensionSpacePoint(), - $this->getContentStreamDbId($event) + $this->getContentStreamLayers($event) ); if (is_null($anchorPoint)) { throw new \InvalidArgumentException( @@ -498,7 +510,7 @@ private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEn ); } $this->updateNodeRecordWithCopyOnWrite( - $this->getContentStreamDbId($event), + $this->getContentStreamLayers($event), $anchorPoint, function (NodeRecord $node) use ($event, $eventEnvelope) { $node->properties = $node->properties @@ -519,7 +531,7 @@ private function whenNodeReferencesWereSet(NodeReferencesWereSet $event, EventEn ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( $event->nodeAggregateId, $originDimensionSpacePoint, - $this->getContentStreamDbId($event) + $this->getContentStreamLayers($event) ); if (is_null($nodeAnchorPoint)) { @@ -533,7 +545,7 @@ private function whenNodeReferencesWereSet(NodeReferencesWereSet $event, EventEn } $this->updateNodeRecordWithCopyOnWrite( - $this->getContentStreamDbId($event), + $this->getContentStreamLayers($event), $nodeAnchorPoint, function (NodeRecord $node) use ($eventEnvelope) { $node->timestamps = $node->timestamps->with( @@ -547,7 +559,7 @@ function (NodeRecord $node) use ($eventEnvelope) { ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( $event->nodeAggregateId, $originDimensionSpacePoint, - $this->getContentStreamDbId($event) + $this->getContentStreamLayers($event) ); @@ -608,7 +620,7 @@ private function writeReferencesForTargetAnchorPoint(SerializedNodeReferences $n private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVariantWasCreated $event, EventEnvelope $eventEnvelope): void { - $this->createNodeSpecializationVariant($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->sourceOrigin, $event->specializationOrigin, $event->specializationSiblings, $eventEnvelope); + $this->createNodeSpecializationVariant($this->getContentStreamLayers($event), $event->nodeAggregateId, $event->sourceOrigin, $event->specializationOrigin, $event->specializationSiblings, $eventEnvelope); } private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDimensionsWereUpdated $event): void @@ -618,7 +630,7 @@ private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDim $event->nodeAggregateId, /** the origin DSP of the root node is always the empty dimension ({@see whenRootNodeAggregateWithNodeWasCreated}) */ OriginDimensionSpacePoint::createWithoutDimensions(), - $this->getContentStreamDbId($event) + $this->getContentStreamLayers($event) ); if ($rootNodeAnchorPoint === null) { // should never happen. @@ -627,7 +639,7 @@ private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDim $ingoingRelations = $this->projectionContentGraph->findIngoingHierarchyRelationsForNode( $rootNodeAnchorPoint, - $this->getContentStreamDbId($event) + $this->getContentStreamLayers($event) ); $currentlyCoveredDimensionSpacePoints = []; @@ -639,7 +651,7 @@ private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDim // add hierarchy edges for newly added dimensions $this->connectHierarchy( - $this->getContentStreamDbId($event), + $this->getContentStreamLayers($event), NodeRelationAnchorPoint::forRootEdge(), $rootNodeAnchorPoint, $newlyCoveredDimensionSpacePoints, @@ -664,7 +676,7 @@ private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNo ); $this->connectHierarchy( - $this->getContentStreamDbId($event), + $this->getContentStreamLayers($event), NodeRelationAnchorPoint::forRootEdge(), $node->relationAnchorPoint, $event->coveredDimensionSpacePoints, @@ -679,12 +691,12 @@ private function whenRootWorkspaceWasCreated(RootWorkspaceWasCreated $event): vo private function whenSubtreeWasTagged(SubtreeWasTagged $event): void { - $this->addSubtreeTag($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->affectedDimensionSpacePoints, $event->tag); + $this->addSubtreeTag($this->getContentStreamLayers($event), $event->nodeAggregateId, $event->affectedDimensionSpacePoints, $event->tag); } private function whenSubtreeWasUntagged(SubtreeWasUntagged $event): void { - $this->removeSubtreeTag($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->affectedDimensionSpacePoints, $event->tag); + $this->removeSubtreeTag($this->getContentStreamLayers($event), $event->nodeAggregateId, $event->affectedDimensionSpacePoints, $event->tag); } private function whenWorkspaceBaseWorkspaceWasChanged(WorkspaceBaseWorkspaceWasChanged $event): void @@ -728,9 +740,9 @@ private function whenWorkspaceWasRemoved(WorkspaceWasRemoved $event): void /** --------------------------------- */ - public function getContentStreamDbId(EmbedsContentStreamId $event): ContentStreamDbId + public function getContentStreamLayers(EmbedsContentStreamId $event): ContentStreamLayers { - return $this->contentStreamDbIdFinder->getContentStreamDbId($event->getContentStreamId()); + return $this->contentStreamLayerFinder->getContentStreamLayers($event->getContentStreamId()); } /** @@ -751,6 +763,7 @@ private function truncateDatabaseTables(): void $this->dbal->executeQuery('TRUNCATE table ' . $this->tableNames->dimensionSpacePoints()); $this->dbal->executeQuery('TRUNCATE table ' . $this->tableNames->workspace()); $this->dbal->executeQuery('TRUNCATE table ' . $this->tableNames->contentStream()); + $this->dbal->executeQuery('TRUNCATE table ' . $this->tableNames->contentStreamLayer()); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to truncate database tables for projection %s: %s', self::class, $e->getMessage()), 1716478318, $e); } @@ -762,12 +775,12 @@ private function truncateDatabaseTables(): void * @template T */ private function updateNodeRecordWithCopyOnWrite( - ContentStreamDbId $contentStreamDbIdWhereWriteOccurs, + ContentStreamLayers $contentStreamLayersWhereWriteOccurs, NodeRelationAnchorPoint $anchorPoint, callable $operations ): mixed { - $contentStreamDbIds = $this->projectionContentGraph->getAllContentStreamDbIdsAnchorPointIsContainedIn($anchorPoint); - if (count($contentStreamDbIds) > 1) { + $contentStreamLayersWithMaterializedNode = $this->projectionContentGraph->getAllContentStreamLayersAnchorPointIsContainedIn($anchorPoint); + if (!$contentStreamLayersWithMaterializedNode->equals($contentStreamLayersWhereWriteOccurs->getWriteLayer())) { // 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) @@ -781,27 +794,47 @@ private function updateNodeRecordWithCopyOnWrite( // 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.contentstreamdbid = :contentStreamDbId + $copyHierarchyRelationStatement = <<tableNames->hierarchyRelation()} ( + id, + parentnodeanchor, + childnodeanchor, + position, + subtreetags, + dimensionspacepointhash, + contentstreamlayer + ) + SELECT + h.id, + -- if our (copied) node is the parent, we update h.parentNodeAnchor + IF(h.parentnodeanchor = :originalNodeAnchor, :newNodeAnchor, h.parentnodeanchor) as parentnodeanchor, + -- if our (copied) node is the child, we update h.childNodeAnchor + IF(h.childnodeanchor = :originalNodeAnchor, :newNodeAnchor, h.childnodeanchor) as childnodeanchor, + h.position, + h.subtreetags, + h.dimensionspacepointhash, + :targetContentStreamLayer as contentstreamlayer + FROM + {$this->tableNames->hierarchyRelation()} h + WHERE + :originalNodeAnchor IN (h.childnodeanchor, h.parentnodeanchor) + AND h.contentstreamlayer IN (:contentStreamLayers) + ON DUPLICATE KEY UPDATE parentnodeanchor = VALUES(parentnodeanchor), childnodeanchor = VALUES(childnodeanchor) SQL; + try { - $this->dbal->executeStatement($updateHierarchyRelationStatement, [ + $this->dbal->executeStatement($copyHierarchyRelationStatement, [ 'newNodeAnchor' => $copiedNode->relationAnchorPoint->value, 'originalNodeAnchor' => $anchorPoint->value, - 'contentStreamDbId' => $contentStreamDbIdWhereWriteOccurs->value, + 'contentStreamLayers' => $contentStreamLayersWhereWriteOccurs->toIntArray(), + 'targetContentStreamLayer' => $contentStreamLayersWhereWriteOccurs->getWriteLayer()->value, + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } 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, @@ -864,7 +897,7 @@ private static function initiatingDateTime(EventEnvelope $eventEnvelope): \DateT } private function createNodeWithHierarchy( - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, NodeAggregateId $nodeAggregateId, NodeTypeName $nodeTypeName, NodeAggregateId $parentNodeAggregateId, @@ -902,7 +935,7 @@ private function createNodeWithHierarchy( foreach ($missingParentRelations as $dimensionSpacePoint) { $parentNode = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $parentNodeAggregateId, $dimensionSpacePoint ); @@ -910,7 +943,7 @@ private function createNodeWithHierarchy( $succeedingSiblingNodeAggregateId = $coverageSucceedingSiblings->getSucceedingSiblingIdForDimensionSpacePoint($dimensionSpacePoint); $succeedingSibling = $succeedingSiblingNodeAggregateId ? $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $succeedingSiblingNodeAggregateId, $dimensionSpacePoint ) @@ -918,7 +951,7 @@ private function createNodeWithHierarchy( if ($parentNode) { $this->connectHierarchy( - $contentStreamDbId, + $contentStreamLayers, $parentNode->relationAnchorPoint, $node->relationAnchorPoint, new DimensionSpacePointSet([$dimensionSpacePoint]), @@ -932,7 +965,7 @@ private function createNodeWithHierarchy( } private function connectHierarchy( - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, NodeRelationAnchorPoint $parentNodeAnchorPoint, NodeRelationAnchorPoint $childNodeAnchorPoint, DimensionSpacePointSet $dimensionSpacePointSet, @@ -943,17 +976,18 @@ private function connectHierarchy( $parentNodeAnchorPoint, null, $succeedingSiblingNodeAnchorPoint, - $contentStreamDbId, + $contentStreamLayers, $dimensionSpacePoint ); - $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamDbId, $parentNodeAnchorPoint, $dimensionSpacePoint); + $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamLayers, $parentNodeAnchorPoint, $dimensionSpacePoint); $inheritedSubtreeTags = NodeTags::create(SubtreeTags::createEmpty(), $parentSubtreeTags->all()); $hierarchyRelation = new HierarchyRelation( + HierarchyRelationId::createAutoIncremented(), + $contentStreamLayers->getWriteLayer(), $parentNodeAnchorPoint, $childNodeAnchorPoint, - $contentStreamDbId, $dimensionSpacePoint, $dimensionSpacePoint->hash, $position, @@ -968,14 +1002,14 @@ private function getRelationPosition( ?NodeRelationAnchorPoint $parentAnchorPoint, ?NodeRelationAnchorPoint $childAnchorPoint, ?NodeRelationAnchorPoint $succeedingSiblingAnchorPoint, - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, DimensionSpacePoint $dimensionSpacePoint ): int { $position = $this->projectionContentGraph->determineHierarchyRelationPosition( $parentAnchorPoint, $childAnchorPoint, $succeedingSiblingAnchorPoint, - $contentStreamDbId, + $contentStreamLayers, $dimensionSpacePoint ); @@ -984,7 +1018,7 @@ private function getRelationPosition( $parentAnchorPoint, $childAnchorPoint, $succeedingSiblingAnchorPoint, - $contentStreamDbId, + $contentStreamLayers, $dimensionSpacePoint ); } @@ -996,7 +1030,7 @@ private function getRelationPositionAfterRecalculation( ?NodeRelationAnchorPoint $parentAnchorPoint, ?NodeRelationAnchorPoint $childAnchorPoint, ?NodeRelationAnchorPoint $succeedingSiblingAnchorPoint, - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, DimensionSpacePoint $dimensionSpacePoint ): int { if (!$childAnchorPoint && !$parentAnchorPoint) { @@ -1011,12 +1045,12 @@ private function getRelationPositionAfterRecalculation( $hierarchyRelations = $parentAnchorPoint ? $this->projectionContentGraph->getOutgoingHierarchyRelationsForNodeAndSubgraph( $parentAnchorPoint, - $contentStreamDbId, + $contentStreamLayers, $dimensionSpacePoint ) : $this->projectionContentGraph->getIngoingHierarchyRelationsForNodeAndSubgraph( $childAnchorPoint, - $contentStreamDbId, + $contentStreamLayers, $dimensionSpacePoint ); @@ -1042,25 +1076,26 @@ private function getRelationPositionAfterRecalculation( private function copyHierarchyRelationToDimensionSpacePoint( HierarchyRelation $sourceHierarchyRelation, - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, DimensionSpacePoint $dimensionSpacePoint, NodeRelationAnchorPoint $newParent, NodeRelationAnchorPoint $newChild, ?NodeRelationAnchorPoint $newSucceedingSibling = null, ): HierarchyRelation { - $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamDbId, $newParent, $dimensionSpacePoint); + $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamLayers, $newParent, $dimensionSpacePoint); $inheritedSubtreeTags = NodeTags::create($sourceHierarchyRelation->subtreeTags->withoutInherited()->all(), $parentSubtreeTags->withoutInherited()->all()); $copy = new HierarchyRelation( + HierarchyRelationId::createAutoIncremented(), + $contentStreamLayers->getWriteLayer(), $newParent, $newChild, - $contentStreamDbId, $dimensionSpacePoint, $dimensionSpacePoint->hash, $this->getRelationPosition( $newParent, $newChild, $newSucceedingSibling, - $contentStreamDbId, + $contentStreamLayers, $dimensionSpacePoint ), $inheritedSubtreeTags, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php index 7a4a54daf01..6d930aa4c02 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php @@ -5,7 +5,7 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter; use Doctrine\DBAL\Connection; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ContentStreamDbIdFinder; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ContentStreamLayerFinder; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ProjectionContentGraph; @@ -32,7 +32,7 @@ public function build( ); $dimensionSpacePointsRepository = new DimensionSpacePointsRepository($this->dbal, $tableNames); - $contentStreamDbIdRepository = new ContentStreamDbIdFinder($this->dbal, $tableNames); + $contentStreamLayerFinder = new ContentStreamLayerFinder($this->dbal, $tableNames); $nodeFactory = new NodeFactory( $projectionFactoryDependencies->contentRepositoryId, @@ -56,7 +56,7 @@ public function build( ), $tableNames, $dimensionSpacePointsRepository, - $contentStreamDbIdRepository, + $contentStreamLayerFinder, $contentGraphReadModel ); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php index 47832978d56..5aa5e799661 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php @@ -33,6 +33,7 @@ public function buildSchema(Connection $connection): Schema $this->createDimensionSpacePointsTable($connection->getDatabasePlatform()), $this->createWorkspaceTable($connection->getDatabasePlatform()), $this->createContentStreamTable($connection->getDatabasePlatform()), + $this->createContentStreamLayerTable($connection->getDatabasePlatform()), ]); } @@ -61,21 +62,25 @@ private function createNodeTable(AbstractPlatform $platform): Table private function createHierarchyRelationTable(AbstractPlatform $platform): Table { $table = self::createTable($this->tableNames->hierarchyRelation(), [ + (new Column('id', Type::getType(Types::INTEGER)))->setAutoincrement(true)->setNotnull(true), + (new Column('contentstreamlayer', self::type(Types::INTEGER)))->setNotnull(true), (new Column('position', self::type(Types::INTEGER)))->setNotnull(true), - (new Column('contentstreamdbid', self::type(Types::INTEGER)))->setNotnull(true), DbalSchemaFactory::columnForDimensionSpacePointHash('dimensionspacepointhash', $platform)->setNotnull(true), - DbalSchemaFactory::columnForNodeAnchorPoint('parentnodeanchor', $platform), - DbalSchemaFactory::columnForNodeAnchorPoint('childnodeanchor', $platform), + // todo nullable? + DbalSchemaFactory::columnForNodeAnchorPoint('parentnodeanchor', $platform)->setNotnull(false), + DbalSchemaFactory::columnForNodeAnchorPoint('childnodeanchor', $platform)->setNotnull(false), (new Column('subtreetags', self::type(Types::JSON))), ]); return $table + ->addIndex(['id']) + ->addUniqueIndex(['id', 'contentstreamlayer']) ->addIndex(['childnodeanchor']) - ->addIndex(['contentstreamdbid']) + ->addIndex(['contentstreamlayer']) ->addIndex(['parentnodeanchor']) - ->addIndex(['childnodeanchor', 'contentstreamdbid', 'dimensionspacepointhash', 'position']) - ->addIndex(['parentnodeanchor', 'contentstreamdbid', 'dimensionspacepointhash', 'position']) - ->addIndex(['contentstreamdbid', 'dimensionspacepointhash']); + ->addIndex(['childnodeanchor', 'contentstreamlayer', 'dimensionspacepointhash', 'position']) + ->addIndex(['parentnodeanchor', 'contentstreamlayer', 'dimensionspacepointhash', 'position']) + ->addIndex(['contentstreamlayer', 'dimensionspacepointhash']); } private function createDimensionSpacePointsTable(AbstractPlatform $platform): Table @@ -120,7 +125,6 @@ private function createWorkspaceTable(AbstractPlatform $platform): Table private function createContentStreamTable(AbstractPlatform $platform): Table { $contentStreamTable = self::createTable($this->tableNames->contentStream(), [ - (new Column('dbId', Type::getType(Types::INTEGER)))->setAutoincrement(true)->setNotnull(true), DbalSchemaFactory::columnForContentStreamId('id', $platform)->setNotnull(true), (new Column('version', Type::getType(Types::INTEGER)))->setNotnull(true), DbalSchemaFactory::columnForContentStreamId('sourceContentStreamId', $platform)->setNotnull(false), @@ -130,8 +134,19 @@ private function createContentStreamTable(AbstractPlatform $platform): Table ]); return $contentStreamTable - ->addUniqueIndex(['id']) - ->setPrimaryKey(['dbId']); + ->setPrimaryKey(['id']); + } + + private function createContentStreamLayerTable(AbstractPlatform $platform): Table + { + $contentStreamLayerTable = self::createTable($this->tableNames->contentStreamLayer(), [ + DbalSchemaFactory::columnForContentStreamId('contentStreamId', $platform)->setNotnull(true), + (new Column('contentStreamLayer', Type::getType(Types::INTEGER)))->setAutoincrement(true)->setNotnull(true), + ]); + + return $contentStreamLayerTable + ->addIndex(['contentStreamLayer']) + ->setPrimaryKey(['contentStreamId', 'contentStreamLayer']); } /** diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ContentStreamDbId.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ContentStreamDbId.php deleted file mode 100644 index 8efc1ef1499..00000000000 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ContentStreamDbId.php +++ /dev/null @@ -1,25 +0,0 @@ -value === $id->value; + } + + public static function fromInt(int $value): self + { + return new self($value); + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ContentStreamLayers.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ContentStreamLayers.php new file mode 100644 index 00000000000..500c1b7dec5 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ContentStreamLayers.php @@ -0,0 +1,71 @@ + $items + */ + private function __construct( + // todo remove all usages + // public int $value, + public array $items + ) { + } + + public static function from(ContentStreamLayer ...$items): self + { + if ($items === []) { + throw new \InvalidArgumentException('Db ids must not be empty', 1775819046); + } + $indexed = []; + foreach ($items as $id) { + $indexed[$id->value] = $id; + } + return new self( + items: $indexed, + ); + } + + /** @param array $array */ + public static function fromArray(array $array): self + { + return self::from( + ...array_map(ContentStreamLayer::fromInt(...), $array), + ); + } + + public function getWriteLayer(): ContentStreamLayer + { + return $this->items[max(array_keys($this->items))]; + } + + public function equals(ContentStreamLayer $id): bool + { + return count($this->items) === 1 && array_key_exists($id->value, $this->items); + } + + public function contain(ContentStreamLayer $id): bool + { + return array_key_exists($id->value, $this->items); + } + + /** + * @return list + */ + public function toIntArray(): array + { + return array_keys($this->items); + } + + public function toDebugString(): string + { + return sprintf('DbIds[%s]', join(',', $this->toIntArray())); + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php index ddc8fc73db5..65cc6f3e597 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php @@ -4,7 +4,7 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamLayers; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRecord; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; @@ -22,34 +22,34 @@ trait NodeMove { use SubtreeTagging; - private function moveNodeAggregate(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, ?NodeAggregateId $newParentNodeAggregateId, InterdimensionalSiblings $succeedingSiblingsForCoverage): void + private function moveNodeAggregate(ContentStreamLayers $contentStreamLayers, NodeAggregateId $nodeAggregateId, ?NodeAggregateId $newParentNodeAggregateId, InterdimensionalSiblings $succeedingSiblingsForCoverage): void { foreach ($succeedingSiblingsForCoverage as $succeedingSiblingForCoverage) { $nodeToBeMoved = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $nodeAggregateId, $succeedingSiblingForCoverage->dimensionSpacePoint ); if (is_null($nodeToBeMoved)) { - throw new \RuntimeException(sprintf('Failed to move node "%s" in sub graph %s@%s because it does not exist', $nodeAggregateId->value, $succeedingSiblingForCoverage->dimensionSpacePoint->toJson(), $contentStreamDbId->value), 1716471638); + throw new \RuntimeException(sprintf('Failed to move node "%s" in sub graph %s@%s because it does not exist', $nodeAggregateId->value, $succeedingSiblingForCoverage->dimensionSpacePoint->toJson(), $contentStreamLayers->toDebugString()), 1716471638); } if ($newParentNodeAggregateId) { $this->moveNodeBeneathParent( - $contentStreamDbId, + $contentStreamLayers, $nodeToBeMoved, $newParentNodeAggregateId, $succeedingSiblingForCoverage ); $this->moveSubtreeTags( - $contentStreamDbId, + $contentStreamLayers, $newParentNodeAggregateId, $succeedingSiblingForCoverage->dimensionSpacePoint ); } else { $this->moveNodeBeforeSucceedingSibling( - $contentStreamDbId, + $contentStreamLayers, $nodeToBeMoved, $succeedingSiblingForCoverage, ); @@ -66,14 +66,14 @@ private function moveNodeAggregate(ContentStreamDbId $contentStreamDbId, NodeAgg * The move target is given as $succeedingSiblingNodeMoveTarget. This also specifies the new parent node. */ private function moveNodeBeforeSucceedingSibling( - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, NodeRecord $nodeToBeMoved, InterdimensionalSibling $succeedingSiblingForCoverage, ): void { // find the single ingoing hierarchy relation which we want to move $ingoingHierarchyRelation = $this->findIngoingHierarchyRelationToBeMoved( $nodeToBeMoved, - $contentStreamDbId, + $contentStreamLayers, $succeedingSiblingForCoverage->dimensionSpacePoint ); @@ -81,12 +81,12 @@ private function moveNodeBeforeSucceedingSibling( if ($succeedingSiblingForCoverage->nodeAggregateId) { // find the new succeeding sibling NodeRecord; We need this record because we'll use its RelationAnchorPoint later. $newSucceedingSibling = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $succeedingSiblingForCoverage->nodeAggregateId, $succeedingSiblingForCoverage->dimensionSpacePoint ); if ($newSucceedingSibling === null) { - throw new \RuntimeException(sprintf('Failed to move node "%s" in sub graph %s@%s because target succeeding sibling node "%s" is missing', $nodeToBeMoved->nodeAggregateId->value, $succeedingSiblingForCoverage->dimensionSpacePoint->toJson(), $contentStreamDbId->value, $succeedingSiblingForCoverage->nodeAggregateId->value), 1716471881); + throw new \RuntimeException(sprintf('Failed to move node "%s" in sub graph %s@%s because target succeeding sibling node "%s" is missing', $nodeToBeMoved->nodeAggregateId->value, $succeedingSiblingForCoverage->dimensionSpacePoint->toJson(), $contentStreamLayers->toDebugString(), $succeedingSiblingForCoverage->nodeAggregateId->value), 1716471881); } } @@ -95,16 +95,27 @@ private function moveNodeBeforeSucceedingSibling( $ingoingHierarchyRelation->parentNodeAnchor, null, $newSucceedingSibling?->relationAnchorPoint, - $contentStreamDbId, + $contentStreamLayers, $succeedingSiblingForCoverage->dimensionSpacePoint ); // ...and assign the new position - $ingoingHierarchyRelation->assignNewPosition( - $newPosition, - $this->dbal, - $this->tableNames - ); + if ($contentStreamLayers->getWriteLayer()->equals($ingoingHierarchyRelation->contentStreamLayer)) { + $ingoingHierarchyRelation->assignNewPosition( + $newPosition, + $this->dbal, + $this->tableNames + ); + } else { + $copiedHierarchyRelation = $ingoingHierarchyRelation->with( + contentStreamLayer: $contentStreamLayers->getWriteLayer(), + position: $newPosition, + ); + $copiedHierarchyRelation->addToDatabase( + $this->dbal, + $this->tableNames + ); + } } /** @@ -116,7 +127,7 @@ private function moveNodeBeforeSucceedingSibling( * We always move beneath the parent before the succeeding sibling if given (or to the end) */ private function moveNodeBeneathParent( - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, NodeRecord $nodeToBeMoved, NodeAggregateId $parentNodeAggregateId, InterdimensionalSibling $succeedingSiblingForCoverage, @@ -124,30 +135,30 @@ private function moveNodeBeneathParent( // find the single ingoing hierarchy relation which we want to move $ingoingHierarchyRelation = $this->findIngoingHierarchyRelationToBeMoved( $nodeToBeMoved, - $contentStreamDbId, + $contentStreamLayers, $succeedingSiblingForCoverage->dimensionSpacePoint ); // find the new parent NodeRecord; We need this record because we'll use its RelationAnchorPoints later. $newParent = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $parentNodeAggregateId, $succeedingSiblingForCoverage->dimensionSpacePoint ); if ($newParent === null) { - throw new \RuntimeException(sprintf('Failed to move node "%s" in sub graph %s@%s because target parent node is missing', $nodeToBeMoved->nodeAggregateId->value, $succeedingSiblingForCoverage->dimensionSpacePoint->toJson(), $contentStreamDbId->value), 1716471955); + throw new \RuntimeException(sprintf('Failed to move node "%s" in sub graph %s@%s because target parent node is missing', $nodeToBeMoved->nodeAggregateId->value, $succeedingSiblingForCoverage->dimensionSpacePoint->toJson(), $contentStreamLayers->toDebugString()), 1716471955); } $newSucceedingSibling = null; if ($succeedingSiblingForCoverage->nodeAggregateId) { // find the new succeeding sibling NodeRecord; We need this record because we'll use its RelationAnchorPoint later. $newSucceedingSibling = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $succeedingSiblingForCoverage->nodeAggregateId, $succeedingSiblingForCoverage->dimensionSpacePoint ); if ($newSucceedingSibling === null) { - throw new \RuntimeException(sprintf('Failed to move node "%s" in sub graph %s@%s because target succeeding sibling node is missing', $nodeToBeMoved->nodeAggregateId->value, $succeedingSiblingForCoverage->dimensionSpacePoint->toJson(), $contentStreamDbId->value), 1716471995); + throw new \RuntimeException(sprintf('Failed to move node "%s" in sub graph %s@%s because target succeeding sibling node is missing', $nodeToBeMoved->nodeAggregateId->value, $succeedingSiblingForCoverage->dimensionSpacePoint->toJson(), $contentStreamLayers->toDebugString()), 1716471995); } } @@ -156,17 +167,29 @@ private function moveNodeBeneathParent( $newParent->relationAnchorPoint, null, $newSucceedingSibling?->relationAnchorPoint, - $contentStreamDbId, + $contentStreamLayers, $succeedingSiblingForCoverage->dimensionSpacePoint ); // this is the actual move - $ingoingHierarchyRelation->assignNewParentNode( - $newParent->relationAnchorPoint, - $newPosition, - $this->dbal, - $this->tableNames - ); + if ($contentStreamLayers->getWriteLayer()->equals($ingoingHierarchyRelation->contentStreamLayer)) { + $ingoingHierarchyRelation->assignNewParentNode( + $newParent->relationAnchorPoint, + $newPosition, + $this->dbal, + $this->tableNames + ); + } else { + $copiedHierarchyRelation = $ingoingHierarchyRelation->with( + parentNodeAnchor: $newParent->relationAnchorPoint, + contentStreamLayer: $contentStreamLayers->getWriteLayer(), + position: $newPosition, + ); + $copiedHierarchyRelation->addToDatabase( + $this->dbal, + $this->tableNames + ); + } } /** @@ -174,19 +197,19 @@ private function moveNodeBeneathParent( */ private function findIngoingHierarchyRelationToBeMoved( NodeRecord $nodeToBeMoved, - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, DimensionSpacePoint $coveredDimensionSpacePointWhereMoveShouldHappen ): HierarchyRelation { $restrictToSet = DimensionSpacePointSet::fromArray([$coveredDimensionSpacePointWhereMoveShouldHappen]); $ingoingHierarchyRelations = $this->projectionContentGraph->findIngoingHierarchyRelationsForNode( $nodeToBeMoved->relationAnchorPoint, - $contentStreamDbId, + $contentStreamLayers, $restrictToSet, ); if (count($ingoingHierarchyRelations) !== 1) { // there should always be exactly one incoming relation in the given DimensionSpacePoint; everything // else would be a totally wrong behavior of findIngoingHierarchyRelationsForNode(). - throw new \RuntimeException(sprintf('Failed move node "%s" in sub graph %s@%s because ingoing source hierarchy relation is missing', $nodeToBeMoved->nodeAggregateId->value, $restrictToSet->toJson(), $contentStreamDbId->value), 1716472138); + throw new \RuntimeException(sprintf('Failed move node "%s" in sub graph %s@%s because ingoing source hierarchy relation is missing', $nodeToBeMoved->nodeAggregateId->value, $restrictToSet->toJson(), $contentStreamLayers->toDebugString()), 1716472138); } return reset($ingoingHierarchyRelations); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php index 6e7b72fc2ca..0906aa665dd 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php @@ -5,7 +5,7 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature; use Doctrine\DBAL\Exception as DBALException; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamLayers; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; @@ -17,56 +17,96 @@ */ trait NodeRemoval { - private function removeNodeAggregate(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $affectedCoveredDimensionSpacePoints): void + private function removeNodeAggregate(ContentStreamLayers $contentStreamLayers, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $affectedCoveredDimensionSpacePoints): void { // the focus here is to be correct; that's why the method is not overly performant (for now at least). We might // lateron find tricks to improve performance $ingoingRelations = $this->projectionContentGraph->findIngoingHierarchyRelationsForNodeAggregate( - $contentStreamDbId, + $contentStreamLayers, $nodeAggregateId, $affectedCoveredDimensionSpacePoints ); foreach ($ingoingRelations as $ingoingRelation) { - $this->removeRelationRecursivelyFromDatabaseIncludingNonReferencedNodes($ingoingRelation); + $this->removeRelationRecursivelyFromDatabaseIncludingNonReferencedNodes($ingoingRelation, $contentStreamLayers); } } private function removeRelationRecursivelyFromDatabaseIncludingNonReferencedNodes( - HierarchyRelation $ingoingRelation + HierarchyRelation $ingoingRelation, + ContentStreamLayers $contentStreamLayers, ): void { - $ingoingRelation->removeFromDatabase($this->dbal, $this->tableNames); + // $ingoingRelation->removeFromDatabase($this->dbal, $this->tableNames); foreach ( $this->projectionContentGraph->findOutgoingHierarchyRelationsForNode( $ingoingRelation->childNodeAnchor, - $ingoingRelation->contentStreamDbId, + $contentStreamLayers, new DimensionSpacePointSet([$ingoingRelation->dimensionSpacePoint]) ) as $outgoingRelation ) { - $this->removeRelationRecursivelyFromDatabaseIncludingNonReferencedNodes($outgoingRelation); + $this->removeRelationRecursivelyFromDatabaseIncludingNonReferencedNodes($outgoingRelation, $contentStreamLayers); } - // remove node itself if it does not have any incoming hierarchy relations anymore - // also remove outbound reference relations - $deleteRelationsStatement = <<tableNames->node()} n - LEFT JOIN {$this->tableNames->referenceRelation()} r ON r.nodeanchorpoint = n.relationanchorpoint - LEFT JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint - WHERE - n.relationanchorpoint = :anchorPointForNode - -- the following line means "left join leads to NO MATCHING hierarchyrelation" - AND h.contentstreamdbid IS NULL - SQL; - try { - $this->dbal->executeStatement($deleteRelationsStatement, [ - 'anchorPointForNode' => $ingoingRelation->childNodeAnchor->value, - ]); - } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to remove relations from database: %s', $e->getMessage()), 1716473385, $e); + if ($contentStreamLayers->getWriteLayer()->equals($ingoingRelation->contentStreamLayer)) { + $ingoingRelation->removeFromDatabase($this->dbal, $this->tableNames); + // remove node itself if it does not have any incoming hierarchy relations anymore + // also remove outbound reference relations + $deleteRelationsStatement = <<tableNames->node()} n + LEFT JOIN {$this->tableNames->referenceRelation()} r ON r.nodeanchorpoint = n.relationanchorpoint + LEFT JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint + WHERE + n.relationanchorpoint = :anchorPointForNode + -- the following line means "left join leads to NO MATCHING hierarchyrelation" + AND h.contentstreamlayer IS NULL + SQL; + try { + $this->dbal->executeStatement($deleteRelationsStatement, [ + 'anchorPointForNode' => $ingoingRelation->childNodeAnchor->value, + ]); + } catch (DBALException $e) { + throw new \RuntimeException(sprintf('Failed to remove relations from database: %s', $e->getMessage()), 1716473385, $e); + } + } else { + $copyRemovedHierarchyRelationStatement = <<tableNames->hierarchyRelation()} ( + id, + parentnodeanchor, + childnodeanchor, + position, + subtreetags, + dimensionspacepointhash, + contentstreamlayer + ) + SELECT + h.id, + NULL as parentnodeanchor, + NULL as childnodeanchor, + -- todo these fieds could be empty as well -- + h.position, + h.subtreetags, + h.dimensionspacepointhash, + :targetContentStreamLayer as contentstreamlayer + FROM + {$this->tableNames->hierarchyRelation()} h + WHERE + id = :id + AND contentstreamlayer = :contentStreamLayer + SQL; + + try { + $this->dbal->executeStatement($copyRemovedHierarchyRelationStatement, [ + 'id' => $ingoingRelation->hierarchyRelationId->value, + 'contentStreamLayer' => $ingoingRelation->contentStreamLayer->value, + 'targetContentStreamLayer' => $contentStreamLayers->getWriteLayer()->value, + ]); + } catch (DBALException $e) { + throw new \RuntimeException(sprintf('Failed to copy hierarchy relation: %s', $e->getMessage()), 1775978611, $e); + } } } } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php index 51c3a9d269e..dbccdba4931 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php @@ -4,8 +4,9 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamLayers; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelationId; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSiblings; @@ -21,16 +22,16 @@ */ trait NodeVariation { - private function createNodeSpecializationVariant(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $specializationOrigin, InterdimensionalSiblings $specializationSiblings, EventEnvelope $eventEnvelope): void + private function createNodeSpecializationVariant(ContentStreamLayers $contentStreamLayers, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $specializationOrigin, InterdimensionalSiblings $specializationSiblings, EventEnvelope $eventEnvelope): void { // Do the actual specialization $sourceNode = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $nodeAggregateId, $sourceOrigin->toDimensionSpacePoint() ); if (is_null($sourceNode)) { - throw new \RuntimeException(sprintf('Failed to create node specialization variant for node "%s" in sub graph %s@%s because the source node is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamDbId->value), 1716498651); + throw new \RuntimeException(sprintf('Failed to create node specialization variant for node "%s" in sub graph %s@%s because the source node is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamLayers->toDebugString()), 1716498651); } $specializedNode = $this->copyNodeToDimensionSpacePoint( @@ -42,59 +43,71 @@ private function createNodeSpecializationVariant(ContentStreamDbId $contentStrea $uncoveredDimensionSpacePoints = $specializationSiblings->toDimensionSpacePointSet()->points; foreach ( $this->projectionContentGraph->findIngoingHierarchyRelationsForNodeAggregate( - $contentStreamDbId, + $contentStreamLayers, $sourceNode->nodeAggregateId, $specializationSiblings->toDimensionSpacePointSet() ) as $hierarchyRelation ) { - $hierarchyRelation->assignNewChildNode( - $specializedNode->relationAnchorPoint, - $this->dbal, - $this->tableNames - ); + if ($contentStreamLayers->getWriteLayer()->equals($hierarchyRelation->contentStreamLayer)) { + $hierarchyRelation->assignNewChildNode( + $specializedNode->relationAnchorPoint, + $this->dbal, + $this->tableNames + ); + } else { + $copiedHierarchyRelation = $hierarchyRelation->with( + childNodeAnchor: $specializedNode->relationAnchorPoint, + contentStreamLayer: $contentStreamLayers->getWriteLayer(), + ); + $copiedHierarchyRelation->addToDatabase( + $this->dbal, + $this->tableNames + ); + } unset($uncoveredDimensionSpacePoints[$hierarchyRelation->dimensionSpacePointHash]); } if (!empty($uncoveredDimensionSpacePoints)) { $sourceParent = $this->projectionContentGraph->findParentNode( - $contentStreamDbId, + $contentStreamLayers, $nodeAggregateId, $sourceOrigin, ); if (is_null($sourceParent)) { - throw new \RuntimeException(sprintf('Failed to create node specialization variant for node "%s" in sub graph %s@%s because the source parent node is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamDbId->value), 1716498695); + throw new \RuntimeException(sprintf('Failed to create node specialization variant for node "%s" in sub graph %s@%s because the source parent node is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamLayers->toDebugString()), 1716498695); } foreach ($uncoveredDimensionSpacePoints as $uncoveredDimensionSpacePoint) { $parentNode = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $sourceParent->nodeAggregateId, $uncoveredDimensionSpacePoint ); if (is_null($parentNode)) { - throw new \RuntimeException(sprintf('Failed to create node specialization variant for node "%s" in sub graph %s@%s because the target parent node "%s" is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamDbId->value, $sourceParent->nodeAggregateId->value), 1716498734); + throw new \RuntimeException(sprintf('Failed to create node specialization variant for node "%s" in sub graph %s@%s because the target parent node "%s" is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamLayers->toDebugString(), $sourceParent->nodeAggregateId->value), 1716498734); } - $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamDbId, $parentNode->relationAnchorPoint, $uncoveredDimensionSpacePoint); + $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamLayers, $parentNode->relationAnchorPoint, $uncoveredDimensionSpacePoint); $specializationSucceedingSiblingNodeAggregateId = $specializationSiblings ->getSucceedingSiblingIdForDimensionSpacePoint($uncoveredDimensionSpacePoint); $specializationSucceedingSiblingNode = $specializationSucceedingSiblingNodeAggregateId ? $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $specializationSucceedingSiblingNodeAggregateId, $uncoveredDimensionSpacePoint ) : null; $hierarchyRelation = new HierarchyRelation( + HierarchyRelationId::createAutoIncremented(), + $contentStreamLayers->getWriteLayer(), $parentNode->relationAnchorPoint, $specializedNode->relationAnchorPoint, - $contentStreamDbId, $uncoveredDimensionSpacePoint, $uncoveredDimensionSpacePoint->hash, $this->projectionContentGraph->determineHierarchyRelationPosition( $parentNode->relationAnchorPoint, $specializedNode->relationAnchorPoint, $specializationSucceedingSiblingNode?->relationAnchorPoint, - $contentStreamDbId, + $contentStreamLayers, $uncoveredDimensionSpacePoint ), NodeTags::create(SubtreeTags::createEmpty(), $parentSubtreeTags->all()), @@ -105,17 +118,28 @@ private function createNodeSpecializationVariant(ContentStreamDbId $contentStrea foreach ( $this->projectionContentGraph->findOutgoingHierarchyRelationsForNodeAggregate( - $contentStreamDbId, + $contentStreamLayers, $sourceNode->nodeAggregateId, $specializationSiblings->toDimensionSpacePointSet() ) as $hierarchyRelation ) { - $hierarchyRelation->assignNewParentNode( - $specializedNode->relationAnchorPoint, - null, - $this->dbal, - $this->tableNames - ); + if ($contentStreamLayers->getWriteLayer()->equals($hierarchyRelation->contentStreamLayer)) { + $hierarchyRelation->assignNewParentNode( + $specializedNode->relationAnchorPoint, + null, + $this->dbal, + $this->tableNames + ); + } else { + $copiedHierarchyRelation = $hierarchyRelation->with( + parentNodeAnchor: $specializedNode->relationAnchorPoint, + contentStreamLayer: $contentStreamLayers->getWriteLayer(), + ); + $copiedHierarchyRelation->addToDatabase( + $this->dbal, + $this->tableNames + ); + } } // Copy Reference Edges @@ -125,24 +149,24 @@ private function createNodeSpecializationVariant(ContentStreamDbId $contentStrea ); } - public function createNodeGeneralizationVariant(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $generalizationOrigin, InterdimensionalSiblings $variantSucceedingSiblings, EventEnvelope $eventEnvelope): void + public function createNodeGeneralizationVariant(ContentStreamLayers $contentStreamLayers, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $generalizationOrigin, InterdimensionalSiblings $variantSucceedingSiblings, EventEnvelope $eventEnvelope): void { // do the generalization $sourceNode = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $nodeAggregateId, $sourceOrigin->toDimensionSpacePoint() ); if (is_null($sourceNode)) { - throw new \RuntimeException(sprintf('Failed to create node generalization variant for node "%s" in sub graph %s@%s because the source node is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamDbId->value), 1716498802); + throw new \RuntimeException(sprintf('Failed to create node generalization variant for node "%s" in sub graph %s@%s because the source node is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamLayers->toDebugString()), 1716498802); } $sourceParentNode = $this->projectionContentGraph->findParentNode( - $contentStreamDbId, + $contentStreamLayers, $nodeAggregateId, $sourceOrigin ); if (is_null($sourceParentNode)) { - throw new \RuntimeException(sprintf('Failed to create node generalization variant for node "%s" in sub graph %s@%s because the source parent node is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamDbId->value), 1716498857); + throw new \RuntimeException(sprintf('Failed to create node generalization variant for node "%s" in sub graph %s@%s because the source parent node is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamLayers->toDebugString()), 1716498857); } $generalizedNode = $this->copyNodeToDimensionSpacePoint( $sourceNode, @@ -153,16 +177,27 @@ public function createNodeGeneralizationVariant(ContentStreamDbId $contentStream $unassignedIngoingDimensionSpacePoints = $variantSucceedingSiblings->toDimensionSpacePointSet(); foreach ( $this->projectionContentGraph->findIngoingHierarchyRelationsForNodeAggregate( - $contentStreamDbId, + $contentStreamLayers, $nodeAggregateId, $variantSucceedingSiblings->toDimensionSpacePointSet() ) as $existingIngoingHierarchyRelation ) { - $existingIngoingHierarchyRelation->assignNewChildNode( - $generalizedNode->relationAnchorPoint, - $this->dbal, - $this->tableNames - ); + if ($contentStreamLayers->getWriteLayer()->equals($existingIngoingHierarchyRelation->contentStreamLayer)) { + $existingIngoingHierarchyRelation->assignNewChildNode( + $generalizedNode->relationAnchorPoint, + $this->dbal, + $this->tableNames + ); + } else { + $copiedHierarchyRelation = $existingIngoingHierarchyRelation->with( + childNodeAnchor: $generalizedNode->relationAnchorPoint, + contentStreamLayer: $contentStreamLayers->getWriteLayer(), + ); + $copiedHierarchyRelation->addToDatabase( + $this->dbal, + $this->tableNames + ); + } $unassignedIngoingDimensionSpacePoints = $unassignedIngoingDimensionSpacePoints->getDifference( new DimensionSpacePointSet([ $existingIngoingHierarchyRelation->dimensionSpacePoint @@ -172,34 +207,45 @@ public function createNodeGeneralizationVariant(ContentStreamDbId $contentStream foreach ( $this->projectionContentGraph->findOutgoingHierarchyRelationsForNodeAggregate( - $contentStreamDbId, + $contentStreamLayers, $nodeAggregateId, $variantSucceedingSiblings->toDimensionSpacePointSet() ) as $existingOutgoingHierarchyRelation ) { - $existingOutgoingHierarchyRelation->assignNewParentNode( - $generalizedNode->relationAnchorPoint, - null, - $this->dbal, - $this->tableNames - ); + if ($contentStreamLayers->getWriteLayer()->equals($existingOutgoingHierarchyRelation->contentStreamLayer)) { + $existingOutgoingHierarchyRelation->assignNewParentNode( + $generalizedNode->relationAnchorPoint, + null, + $this->dbal, + $this->tableNames + ); + } else { + $copiedHierarchyRelation = $existingOutgoingHierarchyRelation->with( + parentNodeAnchor: $generalizedNode->relationAnchorPoint, + contentStreamLayer: $contentStreamLayers->getWriteLayer(), + ); + $copiedHierarchyRelation->addToDatabase( + $this->dbal, + $this->tableNames + ); + } } if (count($unassignedIngoingDimensionSpacePoints) > 0) { $ingoingSourceHierarchyRelation = $this->projectionContentGraph->findIngoingHierarchyRelationsForNode( $sourceNode->relationAnchorPoint, - $contentStreamDbId, + $contentStreamLayers, new DimensionSpacePointSet([$sourceOrigin->toDimensionSpacePoint()]) )[$sourceOrigin->hash] ?? null; if (is_null($ingoingSourceHierarchyRelation)) { - throw new \RuntimeException(sprintf('Failed to create node generalization variant for node "%s" in sub graph %s@%s because the ingoing hierarchy relation is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamDbId->value), 1716498940); + throw new \RuntimeException(sprintf('Failed to create node generalization variant for node "%s" in sub graph %s@%s because the ingoing hierarchy relation is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamLayers->toDebugString()), 1716498940); } // the null case is caught by the NodeAggregate or its command handler foreach ($unassignedIngoingDimensionSpacePoints as $unassignedDimensionSpacePoint) { // The parent node aggregate might be varied as well, // so we need to find a parent node for each covered dimension space point $generalizationParentNode = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $sourceParentNode->nodeAggregateId, $unassignedDimensionSpacePoint ); @@ -209,7 +255,7 @@ public function createNodeGeneralizationVariant(ContentStreamDbId $contentStream $nodeAggregateId->value, $sourceOrigin->toJson(), $unassignedDimensionSpacePoint->toJson(), - $contentStreamDbId->value, + $contentStreamLayers->toDebugString(), $sourceParentNode->nodeAggregateId->value ), 1716498961); } @@ -218,7 +264,7 @@ public function createNodeGeneralizationVariant(ContentStreamDbId $contentStream ->getSucceedingSiblingIdForDimensionSpacePoint($unassignedDimensionSpacePoint); $generalizationSucceedingSiblingNode = $generalizationSucceedingSiblingNodeAggregateId ? $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $generalizationSucceedingSiblingNodeAggregateId, $unassignedDimensionSpacePoint ) @@ -226,7 +272,7 @@ public function createNodeGeneralizationVariant(ContentStreamDbId $contentStream $this->copyHierarchyRelationToDimensionSpacePoint( $ingoingSourceHierarchyRelation, - $contentStreamDbId, + $contentStreamLayers, $unassignedDimensionSpacePoint, $generalizationParentNode->relationAnchorPoint, $generalizedNode->relationAnchorPoint, @@ -242,16 +288,16 @@ public function createNodeGeneralizationVariant(ContentStreamDbId $contentStream ); } - public function createNodePeerVariant(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $peerOrigin, InterdimensionalSiblings $peerSucceedingSiblings, EventEnvelope $eventEnvelope): void + public function createNodePeerVariant(ContentStreamLayers $contentStreamLayers, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $peerOrigin, InterdimensionalSiblings $peerSucceedingSiblings, EventEnvelope $eventEnvelope): void { // Do the peer variant creation itself $sourceNode = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $nodeAggregateId, $sourceOrigin->toDimensionSpacePoint() ); if (is_null($sourceNode)) { - throw new \RuntimeException(sprintf('Failed to create node peer variant for node "%s" in sub graph %s@%s because the source node is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamDbId->value), 1716498802); + throw new \RuntimeException(sprintf('Failed to create node peer variant for node "%s" in sub graph %s@%s because the source node is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamLayers->toDebugString()), 1716498802); } $peerNode = $this->copyNodeToDimensionSpacePoint( $sourceNode, @@ -262,16 +308,27 @@ public function createNodePeerVariant(ContentStreamDbId $contentStreamDbId, Node $unassignedIngoingDimensionSpacePoints = $peerSucceedingSiblings->toDimensionSpacePointSet(); foreach ( $this->projectionContentGraph->findIngoingHierarchyRelationsForNodeAggregate( - $contentStreamDbId, + $contentStreamLayers, $nodeAggregateId, $peerSucceedingSiblings->toDimensionSpacePointSet() ) as $existingIngoingHierarchyRelation ) { - $existingIngoingHierarchyRelation->assignNewChildNode( - $peerNode->relationAnchorPoint, - $this->dbal, - $this->tableNames - ); + if ($contentStreamLayers->getWriteLayer()->equals($existingIngoingHierarchyRelation->contentStreamLayer)) { + $existingIngoingHierarchyRelation->assignNewChildNode( + $peerNode->relationAnchorPoint, + $this->dbal, + $this->tableNames + ); + } else { + $copiedHierarchyRelation = $existingIngoingHierarchyRelation->with( + childNodeAnchor: $peerNode->relationAnchorPoint, + contentStreamLayer: $contentStreamLayers->getWriteLayer(), + ); + $copiedHierarchyRelation->addToDatabase( + $this->dbal, + $this->tableNames + ); + } $unassignedIngoingDimensionSpacePoints = $unassignedIngoingDimensionSpacePoints->getDifference( new DimensionSpacePointSet([ $existingIngoingHierarchyRelation->dimensionSpacePoint @@ -281,50 +338,61 @@ public function createNodePeerVariant(ContentStreamDbId $contentStreamDbId, Node foreach ( $this->projectionContentGraph->findOutgoingHierarchyRelationsForNodeAggregate( - $contentStreamDbId, + $contentStreamLayers, $nodeAggregateId, $peerSucceedingSiblings->toDimensionSpacePointSet() ) as $existingOutgoingHierarchyRelation ) { - $existingOutgoingHierarchyRelation->assignNewParentNode( - $peerNode->relationAnchorPoint, - null, - $this->dbal, - $this->tableNames - ); + if ($contentStreamLayers->getWriteLayer()->equals($existingOutgoingHierarchyRelation->contentStreamLayer)) { + $existingOutgoingHierarchyRelation->assignNewParentNode( + $peerNode->relationAnchorPoint, + null, + $this->dbal, + $this->tableNames + ); + } else { + $copiedHierarchyRelation = $existingOutgoingHierarchyRelation->with( + parentNodeAnchor: $peerNode->relationAnchorPoint, + contentStreamLayer: $contentStreamLayers->getWriteLayer(), + ); + $copiedHierarchyRelation->addToDatabase( + $this->dbal, + $this->tableNames + ); + } } $sourceParentNode = $this->projectionContentGraph->findParentNode( - $contentStreamDbId, + $contentStreamLayers, $nodeAggregateId, $sourceOrigin ); if (is_null($sourceParentNode)) { - throw new \RuntimeException(sprintf('Failed to create node peer variant for node "%s" in sub graph %s@%s because the source parent node is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamDbId->value), 1716498881); + throw new \RuntimeException(sprintf('Failed to create node peer variant for node "%s" in sub graph %s@%s because the source parent node is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamLayers->toDebugString()), 1716498881); } foreach ($unassignedIngoingDimensionSpacePoints as $coveredDimensionSpacePoint) { // The parent node aggregate might be varied as well, // so we need to find a parent node for each covered dimension space point $peerParentNode = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $sourceParentNode->nodeAggregateId, $coveredDimensionSpacePoint ); if (is_null($peerParentNode)) { - throw new \RuntimeException(sprintf('Failed to create node peer variant for node "%s" in sub graph %s@%s because the target parent node "%s" is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamDbId->value, $sourceParentNode->nodeAggregateId->value), 1716499016); + throw new \RuntimeException(sprintf('Failed to create node peer variant for node "%s" in sub graph %s@%s because the target parent node "%s" is missing', $nodeAggregateId->value, $sourceOrigin->toJson(), $contentStreamLayers->toDebugString(), $sourceParentNode->nodeAggregateId->value), 1716499016); } $peerSucceedingSiblingNodeAggregateId = $peerSucceedingSiblings ->getSucceedingSiblingIdForDimensionSpacePoint($coveredDimensionSpacePoint); $peerSucceedingSiblingNode = $peerSucceedingSiblingNodeAggregateId ? $this->projectionContentGraph->findNodeInAggregate( - $contentStreamDbId, + $contentStreamLayers, $peerSucceedingSiblingNodeAggregateId, $coveredDimensionSpacePoint ) : null; $this->connectHierarchy( - $contentStreamDbId, + $contentStreamLayers, $peerParentNode->relationAnchorPoint, $peerNode->relationAnchorPoint, new DimensionSpacePointSet([$coveredDimensionSpacePoint]), diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php index 1ea90e3f401..3a987faa133 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php @@ -6,9 +6,10 @@ use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Exception as DBALException; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamLayers; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; +use Neos\ContentGraph\DoctrineDbalAdapter\HierarchyRelationStatement; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; @@ -22,96 +23,159 @@ */ trait SubtreeTagging { - private function addSubtreeTag(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $affectedDimensionSpacePoints, SubtreeTag $tag): void + private function addSubtreeTag(ContentStreamLayers $contentStreamLayers, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $affectedDimensionSpacePoints, SubtreeTag $tag): void { $addTagToDescendantsStatement = <<tableNames->hierarchyRelation()} h + INSERT INTO {$this->tableNames->hierarchyRelation()} ( + id, + parentnodeanchor, + childnodeanchor, + position, + subtreetags, + dimensionspacepointhash, + contentstreamlayer + ) + SELECT + h.id, + h.parentnodeanchor, + h.childnodeanchor, + h.position, + JSON_INSERT(h.subtreetags, :tagPath, null) as subtreetags, + h.dimensionspacepointhash, + :targetContentStreamLayer as contentstreamlayer + FROM {$this->tableNames->hierarchyRelation()} h JOIN ( + -- todo use new id? WITH RECURSIVE cte (id, dsp) AS ( SELECT ch.childnodeanchor, ch.dimensionspacepointhash - FROM {$this->tableNames->hierarchyRelation()} ch + FROM {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash in (:dimensionSpacePointHashes)')->andWhere('NOT JSON_CONTAINS_PATH(h.subtreetags, \'one\', :tagPath)')->toSql()} ch INNER JOIN {$this->tableNames->node()} n ON n.relationanchorpoint = ch.parentnodeanchor WHERE n.nodeaggregateid = :nodeAggregateId - AND ch.contentstreamdbid = :contentStreamDbId - AND ch.dimensionspacepointhash in (:dimensionSpacePointHashes) - AND NOT JSON_CONTAINS_PATH(ch.subtreetags, 'one', :tagPath) UNION ALL SELECT - dh.childnodeanchor, - dh.dimensionspacepointhash + dh.childnodeanchor, dh.dimensionspacepointhash FROM cte - JOIN {$this->tableNames->hierarchyRelation()} dh ON dh.parentnodeanchor = cte.id - AND dh.contentstreamdbid = :contentStreamDbId - AND dh.dimensionspacepointhash = cte.dsp + JOIN {$this->hierarchyRelationStatement->toSql()} dh ON dh.parentnodeanchor = cte.id + -- todo why not in where???? or why not to dimensionSpacePointHashes + AND dh.dimensionspacepointhash = cte.dsp WHERE NOT JSON_CONTAINS_PATH(dh.subtreetags, 'one', :tagPath) ) SELECT * FROM cte - ) subquery ON h.dimensionspacepointhash = subquery.dsp + ) subquery + ON h.dimensionspacepointhash = subquery.dsp AND h.childnodeanchor = subquery.id - SET h.subtreetags = JSON_INSERT(h.subtreetags, :tagPath, null) - WHERE h.contentstreamdbid = :contentStreamDbId + WHERE h.contentstreamlayer IN (:contentStreamLayers) + ON DUPLICATE KEY UPDATE subtreetags = VALUES(subtreetags) SQL; try { $this->dbal->executeStatement($addTagToDescendantsStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'nodeAggregateId' => $nodeAggregateId->value, 'dimensionSpacePointHashes' => $affectedDimensionSpacePoints->getPointHashes(), 'tagPath' => '$."' . $tag->value . '"', + 'targetContentStreamLayer' => $contentStreamLayers->getWriteLayer()->value, ], [ 'dimensionSpacePointHashes' => ArrayParameterType::STRING, + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('1: Failed to add subtree tag %s for content stream %s, node aggregate id %s and dimension space points %s: %s', $tag->value, $contentStreamDbId->value, $nodeAggregateId->value, $affectedDimensionSpacePoints->toJson(), $e->getMessage()), 1716479749, $e); + throw new \RuntimeException(sprintf('1: Failed to add subtree tag %s for content stream %s, node aggregate id %s and dimension space points %s: %s', $tag->value, $contentStreamLayers->toDebugString(), $nodeAggregateId->value, $affectedDimensionSpacePoints->toJson(), $e->getMessage()), 1716479749, $e); } $addTagToNodeStatement = <<tableNames->hierarchyRelation()} h + INSERT INTO {$this->tableNames->hierarchyRelation()} ( + id, + parentnodeanchor, + childnodeanchor, + position, + subtreetags, + dimensionspacepointhash, + contentstreamlayer + ) + SELECT + h.id, + h.parentnodeanchor, + h.childnodeanchor, + h.position, + JSON_SET(h.subtreetags, :tagPath, true) as subtreetags, + h.dimensionspacepointhash, + :targetContentStreamLayer as contentstreamlayer + FROM {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash in (:dimensionSpacePointHashes)')->toSql()} h INNER JOIN {$this->tableNames->node()} n ON n.relationanchorpoint = h.childnodeanchor - SET h.subtreetags = JSON_SET(h.subtreetags, :tagPath, true) WHERE n.nodeaggregateid = :nodeAggregateId - AND h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash in (:dimensionSpacePointHashes) + ON DUPLICATE KEY UPDATE subtreetags = VALUES(subtreetags) SQL; try { $this->dbal->executeStatement($addTagToNodeStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'nodeAggregateId' => $nodeAggregateId->value, 'dimensionSpacePointHashes' => $affectedDimensionSpacePoints->getPointHashes(), 'tagPath' => '$."' . $tag->value . '"', + 'targetContentStreamLayer' => $contentStreamLayers->getWriteLayer()->value, ], [ 'dimensionSpacePointHashes' => ArrayParameterType::STRING, + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('2: Failed to add subtree tag %s for content stream %s, node aggregate id %s and dimension space points %s: %s', $tag->value, $contentStreamDbId->value, $nodeAggregateId->value, $affectedDimensionSpacePoints->toJson(), $e->getMessage()), 1716479840, $e); + throw new \RuntimeException(sprintf('2: Failed to add subtree tag %s for content stream %s, node aggregate id %s and dimension space points %s: %s', $tag->value, $contentStreamLayers->toDebugString(), $nodeAggregateId->value, $affectedDimensionSpacePoints->toJson(), $e->getMessage()), 1716479840, $e); } } - private function removeSubtreeTag(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $affectedDimensionSpacePoints, SubtreeTag $tag): void + private function removeSubtreeTag(ContentStreamLayers $contentStreamLayers, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $affectedDimensionSpacePoints, SubtreeTag $tag): void { $removeTagStatement = <<tableNames->hierarchyRelation()} h + INSERT INTO {$this->tableNames->hierarchyRelation()} ( + id, + parentnodeanchor, + childnodeanchor, + position, + subtreetags, + dimensionspacepointhash, + contentstreamlayer + ) + SELECT + h.id, + h.parentnodeanchor, + h.childnodeanchor, + h.position, + IF( + ( + SELECT containsTag FROM (SELECT + JSON_CONTAINS_PATH(gph.subtreetags, 'one', :tagPath) as containsTag + FROM + {$this->tableNames->hierarchyRelation()} gph + INNER JOIN {$this->tableNames->hierarchyRelation()} ph ON ph.parentnodeanchor = gph.childnodeanchor + INNER JOIN {$this->tableNames->node()} n ON n.relationanchorpoint = ph.childnodeanchor + WHERE + ph.parentnodeanchor = gph.childnodeanchor + AND n.nodeaggregateid = :nodeAggregateId + AND gph.contentstreamlayer IN (:contentStreamLayers) + LIMIT 1) as containsTagSubQuery + ), JSON_SET(subtreetags, :tagPath, null), JSON_REMOVE(subtreetags, :tagPath) + ) as subtreetags, + h.dimensionspacepointhash, + :targetContentStreamLayer as contentstreamlayer + FROM {$this->tableNames->hierarchyRelation()} h JOIN ( + -- todo use new actual id? WITH RECURSIVE cte (id, dsp) AS ( SELECT ph.childnodeanchor, ph.dimensionspacepointhash - FROM {$this->tableNames->hierarchyRelation()} ph + FROM {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash in (:dimensionSpacePointHashes)')->toSql()} ph INNER JOIN {$this->tableNames->node()} n ON n.relationanchorpoint = ph.childnodeanchor WHERE n.nodeaggregateid = :nodeAggregateId - AND ph.contentstreamdbid = :contentStreamDbId - AND ph.dimensionspacepointhash in (:dimensionSpacePointHashes) UNION ALL SELECT dh.childnodeanchor, dh.dimensionspacepointhash FROM cte - JOIN {$this->tableNames->hierarchyRelation()} dh ON dh.parentnodeanchor = cte.id - AND dh.contentstreamdbid = :contentStreamDbId + JOIN {$this->hierarchyRelationStatement->toSql()} dh ON dh.parentnodeanchor = cte.id AND dh.dimensionspacepointhash = cte.dsp WHERE JSON_EXTRACT(dh.subtreetags, :tagPath) != TRUE @@ -119,42 +183,55 @@ private function removeSubtreeTag(ContentStreamDbId $contentStreamDbId, NodeAggr SELECT * FROM cte ) subquery ON h.dimensionspacepointhash = subquery.dsp AND h.childnodeanchor = subquery.id - SET subtreetags = IF( - ( - SELECT containsTag FROM (SELECT - JSON_CONTAINS_PATH(gph.subtreetags, 'one', :tagPath) as containsTag - FROM - {$this->tableNames->hierarchyRelation()} gph - INNER JOIN {$this->tableNames->hierarchyRelation()} ph ON ph.parentnodeanchor = gph.childnodeanchor - INNER JOIN {$this->tableNames->node()} n ON n.relationanchorpoint = ph.childnodeanchor - WHERE - ph.parentnodeanchor = gph.childnodeanchor - AND n.nodeaggregateid = :nodeAggregateId - AND gph.contentstreamdbid = :contentStreamDbId - LIMIT 1) as containsTagSubQuery - ), JSON_SET(subtreetags, :tagPath, null), JSON_REMOVE(subtreetags, :tagPath) - ) - WHERE contentstreamdbid = :contentStreamDbId + WHERE contentstreamlayer IN (:contentStreamLayers) + ON DUPLICATE KEY UPDATE subtreetags = VALUES(subtreetags) SQL; try { $this->dbal->executeStatement($removeTagStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'nodeAggregateId' => $nodeAggregateId->value, 'dimensionSpacePointHashes' => $affectedDimensionSpacePoints->getPointHashes(), 'tagPath' => '$."' . $tag->value . '"', + 'targetContentStreamLayer' => $contentStreamLayers->getWriteLayer()->value, ], [ 'dimensionSpacePointHashes' => ArrayParameterType::STRING, + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to remove subtree tag %s for content stream %s, node aggregate id %s and dimension space points %s: %s', $tag->value, $contentStreamDbId->value, $nodeAggregateId->value, $affectedDimensionSpacePoints->toJson(), $e->getMessage()), 1716482293, $e); + throw new \RuntimeException(sprintf('Failed to remove subtree tag %s for content stream %s, node aggregate id %s and dimension space points %s: %s', $tag->value, $contentStreamLayers->toDebugString(), $nodeAggregateId->value, $affectedDimensionSpacePoints->toJson(), $e->getMessage()), 1716482293, $e); } } - private function moveSubtreeTags(ContentStreamDbId $contentStreamDbId, NodeAggregateId $newParentNodeAggregateId, DimensionSpacePoint $coveredDimensionSpacePoint): void + private function moveSubtreeTags(ContentStreamLayers $contentStreamLayers, NodeAggregateId $newParentNodeAggregateId, DimensionSpacePoint $coveredDimensionSpacePoint): void { $moveSubtreeTagsStatement = <<tableNames->hierarchyRelation()} h, + INSERT INTO {$this->tableNames->hierarchyRelation()} ( + id, + parentnodeanchor, + childnodeanchor, + position, + subtreetags, + dimensionspacepointhash, + contentstreamlayer + ) + SELECT + h.id, + h.parentnodeanchor, + h.childnodeanchor, + h.position, ( + SELECT + JSON_MERGE_PATCH( + IFNULL(JSON_OBJECTAGG(htk.k, null), '{}'), + JSON_MERGE_PATCH('{}', h.subtreetags) + ) + FROM + JSON_TABLE(r.subtreeTagsToInherit, '\$[*]' COLUMNS (k VARCHAR(36) PATH '\$')) htk + ) as subtreetags, + h.dimensionspacepointhash, + :targetContentStreamLayer as contentstreamlayer + FROM {$this->tableNames->hierarchyRelation()} h + JOIN ( WITH RECURSIVE cte AS ( SELECT JSON_KEYS(th.subtreetags) subtreeTagsToInherit, th.childnodeanchor @@ -163,7 +240,7 @@ private function moveSubtreeTags(ContentStreamDbId $contentStreamDbId, NodeAggre INNER JOIN {$this->tableNames->node()} tn ON tn.relationanchorpoint = th.childnodeanchor WHERE tn.nodeaggregateid = :newParentNodeAggregateId - AND th.contentstreamdbid = :contentStreamDbId + AND th.contentstreamlayer IN (:contentStreamLayers) AND th.dimensionspacepointhash = :dimensionSpacePointHash UNION SELECT @@ -180,60 +257,57 @@ private function moveSubtreeTags(ContentStreamDbId $contentStreamDbId, NodeAggre JOIN {$this->tableNames->hierarchyRelation()} dh ON dh.parentnodeanchor = cte.childnodeanchor - AND dh.contentstreamdbid = :contentStreamDbId + AND dh.contentstreamlayer IN (:contentStreamLayers) AND dh.dimensionspacepointhash = :dimensionSpacePointHash ) SELECT * FROM cte ) AS r - SET h.subtreetags = ( - SELECT - JSON_MERGE_PATCH( - IFNULL(JSON_OBJECTAGG(htk.k, null), '{}'), - JSON_MERGE_PATCH('{}', h.subtreetags) - ) - FROM - JSON_TABLE(r.subtreeTagsToInherit, '\$[*]' COLUMNS (k VARCHAR(36) PATH '\$')) htk - ) WHERE h.childnodeanchor = r.childnodeanchor - AND h.contentstreamdbid = :contentStreamDbId + AND h.contentstreamlayer IN (:contentStreamLayers) AND h.dimensionspacepointhash = :dimensionSpacePointHash + ON DUPLICATE KEY UPDATE subtreetags = VALUES(subtreetags) SQL; try { // Mysql hack, too eager to optimize https://dev.mysql.com/doc/refman/8.4/en/derived-table-optimization.html $this->dbal->executeQuery('set optimizer_switch="derived_merge=off"'); $this->dbal->executeStatement($moveSubtreeTagsStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'newParentNodeAggregateId' => $newParentNodeAggregateId->value, 'dimensionSpacePointHash' => $coveredDimensionSpacePoint->hash, + 'targetContentStreamLayer' => $contentStreamLayers->getWriteLayer()->value, + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to move subtree tags for content stream %s, new parent node aggregate id %s and dimension space point %s: %s', $contentStreamDbId->value, $newParentNodeAggregateId->value, $coveredDimensionSpacePoint->toJson(), $e->getMessage()), 1716482574, $e); + throw new \RuntimeException(sprintf('Failed to move subtree tags for content stream %s, new parent node aggregate id %s and dimension space point %s: %s', $contentStreamLayers->toDebugString(), $newParentNodeAggregateId->value, $coveredDimensionSpacePoint->toJson(), $e->getMessage()), 1716482574, $e); } } - private function subtreeTagsForHierarchyRelation(ContentStreamDbId $contentStreamDbId, NodeRelationAnchorPoint $parentNodeAnchorPoint, DimensionSpacePoint $dimensionSpacePoint): NodeTags + private function subtreeTagsForHierarchyRelation(ContentStreamLayers $contentStreamLayers, NodeRelationAnchorPoint $parentNodeAnchorPoint, DimensionSpacePoint $dimensionSpacePoint): NodeTags { if ($parentNodeAnchorPoint->equals(NodeRelationAnchorPoint::forRootEdge())) { return NodeTags::createEmpty(); } + + $subtreeTagsStatement = <<hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h + WHERE h.childnodeanchor = :parentNodeAnchorPoint + SQL; + try { - $subtreeTagsJson = $this->dbal->fetchOne(' - SELECT h.subtreetags FROM ' . $this->tableNames->hierarchyRelation() . ' h - WHERE - h.childnodeanchor = :parentNodeAnchorPoint - AND h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash - ', [ + $subtreeTagsJson = $this->dbal->fetchOne($subtreeTagsStatement, [ 'parentNodeAnchorPoint' => $parentNodeAnchorPoint->value, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'dimensionSpacePointHash' => $dimensionSpacePoint->hash, + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to fetch subtree tags for hierarchy parent anchor point "%s" in content subgraph "%s@%s": %s', $parentNodeAnchorPoint->value, $dimensionSpacePoint->toJson(), $contentStreamDbId->value, $e->getMessage()), 1716478760, $e); + throw new \RuntimeException(sprintf('Failed to fetch subtree tags for hierarchy parent anchor point "%s" in content subgraph "%s@%s": %s', $parentNodeAnchorPoint->value, $dimensionSpacePoint->toJson(), $contentStreamLayers->toDebugString(), $e->getMessage()), 1716478760, $e); } if (!is_string($subtreeTagsJson)) { - throw new \RuntimeException(sprintf('Failed to fetch subtree tags for hierarchy parent anchor point "%s" in content subgraph "%s@%s"', $parentNodeAnchorPoint->value, $dimensionSpacePoint->toJson(), $contentStreamDbId->value), 1704199847); + throw new \RuntimeException(sprintf('Failed to fetch subtree tags for hierarchy parent anchor point "%s" in content subgraph "%s@%s"', $parentNodeAnchorPoint->value, $dimensionSpacePoint->toJson(), $contentStreamLayers->toDebugString()), 1704199847); } return NodeFactory::extractNodeTagsFromJson($subtreeTagsJson); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php index ef288309b79..be88cd3cc84 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php @@ -29,9 +29,10 @@ final readonly class HierarchyRelation { public function __construct( + public HierarchyRelationId $hierarchyRelationId, + public ContentStreamLayer $contentStreamLayer, public NodeRelationAnchorPoint $parentNodeAnchor, public NodeRelationAnchorPoint $childNodeAnchor, - public ContentStreamDbId $contentStreamDbId, public DimensionSpacePoint $dimensionSpacePoint, public string $dimensionSpacePointHash, public int $position, @@ -39,6 +40,28 @@ public function __construct( ) { } + public function with( + ?HierarchyRelationId $hierarchyRelationDId = null, + ?NodeRelationAnchorPoint $parentNodeAnchor = null, + ?NodeRelationAnchorPoint $childNodeAnchor = null, + ?ContentStreamLayer $contentStreamLayer = null, + ?DimensionSpacePoint $dimensionSpacePoint = null, + ?string $dimensionSpacePointHash = null, + ?int $position = null, + ?NodeTags $subtreeTags = null, + ): self { + return new self( + hierarchyRelationId: $hierarchyRelationDId ?? $this->hierarchyRelationId, + contentStreamLayer: $contentStreamLayer ?? $this->contentStreamLayer, + parentNodeAnchor: $parentNodeAnchor ?? $this->parentNodeAnchor, + childNodeAnchor: $childNodeAnchor ?? $this->childNodeAnchor, + dimensionSpacePoint: $dimensionSpacePoint ?? $this->dimensionSpacePoint, + dimensionSpacePointHash: $dimensionSpacePointHash ?? $this->dimensionSpacePointHash, + position: $position ?? $this->position, + subtreeTags: $subtreeTags ?? $this->subtreeTags, + ); + } + public function addToDatabase(Connection $databaseConnection, ContentGraphTableNames $tableNames): void { $dimensionSpacePoints = new DimensionSpacePointsRepository($databaseConnection, $tableNames); @@ -51,9 +74,10 @@ public function addToDatabase(Connection $databaseConnection, ContentGraphTableN try { $databaseConnection->insert($tableNames->hierarchyRelation(), [ + 'id' => $this->hierarchyRelationId->value, 'parentnodeanchor' => $this->parentNodeAnchor->value, 'childnodeanchor' => $this->childNodeAnchor->value, - 'contentstreamdbid' => $this->contentStreamDbId->value, + 'contentstreamlayer' => $this->contentStreamLayer->value, 'dimensionspacepointhash' => $this->dimensionSpacePointHash, 'position' => $this->position, 'subtreetags' => $subtreeTagsJson, @@ -130,14 +154,20 @@ public function assignNewPosition(int $position, Connection $databaseConnection, /** * @return array - */ // todo rename? because ambiguous: + */ public function getDatabaseId(): array { + if (!$this->hierarchyRelationId->value) { + throw new \RuntimeException(sprintf('Hierarchy relation was not created in the database and does not have an id: %s', json_encode([ + 'parentnodeanchor' => $this->parentNodeAnchor->value, + 'childnodeanchor' => $this->childNodeAnchor->value, + 'contentstreamlayer' => $this->contentStreamLayer->value, + 'dimensionspacepointhash' => $this->dimensionSpacePointHash + ])), 1775979706); + } return [ - 'parentnodeanchor' => $this->parentNodeAnchor->value, - 'childnodeanchor' => $this->childNodeAnchor->value, - 'contentstreamdbid' => $this->contentStreamDbId->value, - 'dimensionspacepointhash' => $this->dimensionSpacePointHash + 'id' => $this->hierarchyRelationId->value, + 'contentstreamlayer' => $this->contentStreamLayer->value, ]; } } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelationId.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelationId.php new file mode 100644 index 00000000000..eb5ed6dc06d --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelationId.php @@ -0,0 +1,34 @@ +value === $id->value; + } + + public static function fromInt(int $value): self + { + return new self($value); + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php index 968ef36c437..4de238945d8 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php @@ -14,9 +14,11 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception as DBALException; use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; +use Neos\ContentGraph\DoctrineDbalAdapter\HierarchyRelationStatement; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\Projection\ContentGraph\ProjectionIntegrityViolationDetectorInterface; @@ -32,10 +34,13 @@ */ final class ProjectionIntegrityViolationDetector implements ProjectionIntegrityViolationDetectorInterface { + private readonly HierarchyRelationStatement $hierarchyRelationStatement; + public function __construct( private readonly Connection $dbal, private readonly ContentGraphTableNames $tableNames, ) { + $this->hierarchyRelationStatement = HierarchyRelationStatement::for($this->tableNames); } public function hierarchyIntegrityIsProvided(): Result @@ -89,18 +94,23 @@ public function hierarchyIntegrityIsProvided(): Result )); } + // FIXME the violation does not consider multiple layers $hierarchyRelationsAppearingMultipleTimesStatement = <<tableNames->hierarchyRelation()} h + h.dimensionspacepointhash, + h.contentstreamlayer + FROM {$this->tableNames->hierarchyRelation()} AS h LEFT JOIN {$this->tableNames->node()} p ON h.parentnodeanchor = p.relationanchorpoint LEFT JOIN {$this->tableNames->node()} c ON h.childnodeanchor = c.relationanchorpoint WHERE h.parentnodeanchor != :rootNodeAnchor GROUP BY - p.nodeaggregateid, c.nodeaggregateid, - h.dimensionspacepointhash, h.contentstreamdbid + p.nodeaggregateid, + c.nodeaggregateid, + h.dimensionspacepointhash, + h.contentstreamlayer HAVING uniquenessCounter > 1 SQL; try { @@ -125,16 +135,19 @@ public function siblingsAreDistinctlySorted(): Result { $result = new Result(); + // FIXME the violation does not consider multiple layers $ambiguouslySortedHierarchyRelationStatement = <<tableNames->hierarchyRelation()} GROUP BY + contentstreamlayer, position, parentnodeanchor, - contentstreamdbid, dimensionspacepointhash HAVING COUNT(position) > 1 @@ -168,7 +181,7 @@ public function siblingsAreDistinctlySorted(): Result $result->addError(new Error( 'Siblings ' . implode(', ', array_map(static fn (array $record) => $record['nodeaggregateid'], $ambiguouslySortedNodeRecords)) - . ' are ambiguously sorted in content stream ' . $hierarchyRelationRecord['contentstreamdbid'] + . ' are ambiguously sorted in content stream ' . $hierarchyRelationRecord['contentstreamlayer'] . ' and dimension space point ' . $dimensionSpacePoints[$hierarchyRelationRecord['dimensionspacepointhash']]?->toJson(), self::ERROR_CODE_SIBLINGS_ARE_AMBIGUOUSLY_SORTED )); @@ -182,7 +195,7 @@ public function tetheredNodesAreNamed(): Result $result = new Result(); $unnamedTetheredNodesStatement = <<tableNames->node()} n INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint @@ -190,7 +203,7 @@ public function tetheredNodesAreNamed(): Result n.classification = :tethered AND n.name IS NULL GROUP BY - n.nodeaggregateid, h.contentstreamdbid + n.nodeaggregateid, h.contentstreamlayer SQL; try { $unnamedTetheredNodeRecords = $this->dbal->fetchAllAssociative($unnamedTetheredNodesStatement, [ @@ -203,7 +216,7 @@ public function tetheredNodesAreNamed(): Result foreach ($unnamedTetheredNodeRecords as $unnamedTetheredNodeRecord) { $result->addError(new Error( 'Node aggregate ' . $unnamedTetheredNodeRecord['nodeaggregateid'] - . ' is unnamed in content stream ' . $unnamedTetheredNodeRecord['contentstreamdbid'] . '.', + . ' is unnamed in content stream ' . $unnamedTetheredNodeRecord['contentstreamlayer'] . '.', self::ERROR_CODE_TETHERED_NODE_IS_UNNAMED )); } @@ -225,7 +238,7 @@ public function subtreeTagsAreInherited(): Result {$this->tableNames->hierarchyRelation()} h INNER JOIN {$this->tableNames->hierarchyRelation()} ph ON ph.childnodeanchor = h.parentnodeanchor - AND ph.contentstreamdbid = h.contentstreamdbid + AND ph.contentstreamlayer = h.contentstreamlayer AND ph.dimensionspacepointhash = h.dimensionspacepointhash WHERE EXISTS ( @@ -279,7 +292,7 @@ public function referenceIntegrityIsProvided(): Result $referenceRelationRecordsWithInvalidTargetStatement = <<tableNames->node()} d INNER JOIN {$this->tableNames->hierarchyRelation()} dh ON d.relationanchorpoint = dh.childnodeanchor ) ON r.destinationnodeaggregateid = d.nodeaggregateid - AND sh.contentstreamdbid = dh.contentstreamdbid + AND sh.contentstreamlayer = dh.contentstreamlayer AND sh.dimensionspacepointhash = dh.dimensionspacepointhash WHERE d.nodeaggregateid IS NULL GROUP BY s.nodeaggregateid, - sh.contentstreamdbid, + sh.contentstreamlayer, r.destinationnodeaggregateid SQL; try { @@ -309,7 +322,7 @@ public function referenceIntegrityIsProvided(): Result $result->addError(new Error( 'Destination node aggregate ' . $record['destinationNodeAggregateId'] . ' does not cover any dimension space points the source ' . $record['sourceNodeAggregateId'] - . ' does in content stream ' . $record['contentstreamdbid'], + . ' does in content stream ' . $record['contentstreamlayer'], self::ERROR_CODE_REFERENCE_INTEGRITY_IS_COMPROMISED )); } @@ -336,11 +349,9 @@ public function allNodesAreConnectedToARootNodePerSubgraph(): Result SELECT h.childnodeanchor FROM - {$this->tableNames->hierarchyRelation()} h + {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h WHERE h.parentnodeanchor = :rootAnchorPoint - AND h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash UNION -- -------------------------------- -- RECURSIVE query: do one "child" query step @@ -349,28 +360,25 @@ public function allNodesAreConnectedToARootNodePerSubgraph(): Result h.childnodeanchor FROM subgraph p - INNER JOIN {$this->tableNames->hierarchyRelation()} h - on h.parentnodeanchor = p.childnodeanchor - WHERE - h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash + INNER JOIN {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h + ON h.parentnodeanchor = p.childnodeanchor ) SELECT nodeaggregateid FROM {$this->tableNames->node()} n - INNER JOIN {$this->tableNames->hierarchyRelation()} h + INNER JOIN {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h ON h.childnodeanchor = n.relationanchorpoint WHERE - h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash - AND relationanchorpoint NOT IN (SELECT * FROM subgraph) + relationanchorpoint NOT IN (SELECT * FROM subgraph) SQL; - foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { + foreach ($this->findProjectedContentStreamLayers() as $contentStreamLayers) { foreach ($this->findProjectedDimensionSpacePoints() as $dimensionSpacePoint) { try { $nodeAggregateIdsInCycles = $this->dbal->fetchFirstColumn($nodeAggregateIdsInCyclesStatement, [ 'rootAnchorPoint' => NodeRelationAnchorPoint::forRootEdge()->value, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'dimensionSpacePointHash' => $dimensionSpacePoint->hash + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load cyclic node relations: %s', $e->getMessage()), 1716493090, $e); @@ -378,7 +386,7 @@ public function allNodesAreConnectedToARootNodePerSubgraph(): Result if (!empty($nodeAggregateIdsInCycles)) { $result->addError(new Error( - 'Subgraph defined by content stream ' . $contentStreamDbId->value + 'Subgraph defined by content stream ' . $contentStreamLayers->toDebugString() . ' and dimension space point ' . $dimensionSpacePoint->toJson() . ' is cyclic for node aggregates ' . implode(',', $nodeAggregateIdsInCycles), @@ -410,22 +418,21 @@ public function nodeAggregateIdsAreUniquePerSubgraph(): Result n.nodeaggregateid, COUNT(n.relationanchorpoint) FROM {$this->tableNames->node()} n - INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint - WHERE - h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash + INNER JOIN {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h ON h.childnodeanchor = n.relationanchorpoint GROUP BY n.nodeaggregateid HAVING COUNT(DISTINCT(n.relationanchorpoint)) > 1 SQL; - foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { + foreach ($this->findProjectedContentStreamLayers() as $contentStreamLayers) { foreach ($this->findProjectedDimensionSpacePoints() as $dimensionSpacePoint) { try { $ambiguousNodeAggregateRecords = $this->dbal->fetchAllAssociative($ambiguousNodeAggregatesStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'dimensionSpacePointHash' => $dimensionSpacePoint->hash + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load ambiguous node aggregates: %s', $e->getMessage()), 1716494110, $e); @@ -433,7 +440,7 @@ public function nodeAggregateIdsAreUniquePerSubgraph(): Result foreach ($ambiguousNodeAggregateRecords as $ambiguousRecord) { $result->addError(new Error( 'Node aggregate ' . $ambiguousRecord['nodeaggregateid'] - . ' is ambiguous in content stream ' . $contentStreamDbId->value + . ' is ambiguous in content stream ' . $contentStreamLayers->toDebugString() . ' and dimension space point ' . $dimensionSpacePoint->toJson(), self::ERROR_CODE_AMBIGUOUS_NODE_AGGREGATE_IN_SUBGRAPH )); @@ -452,22 +459,21 @@ public function allNodesHaveAtMostOneParentPerSubgraph(): Result c.nodeaggregateid FROM {$this->tableNames->node()} c - INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = c.relationanchorpoint - WHERE - h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash + INNER JOIN {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h ON h.childnodeanchor = c.relationanchorpoint GROUP BY c.relationanchorpoint HAVING COUNT(DISTINCT(h.parentnodeanchor)) > 1 SQL; - foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { + foreach ($this->findProjectedContentStreamLayers() as $contentStreamLayers) { foreach ($this->findProjectedDimensionSpacePoints() as $dimensionSpacePoint) { try { $nodeRecordsWithMultipleParents = $this->dbal->fetchAllAssociative($nodeRecordsWithMultipleParentsStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'dimensionSpacePointHash' => $dimensionSpacePoint->hash + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load nodes with multiple parents: %s', $e->getMessage()), 1716494223, $e); @@ -476,7 +482,7 @@ public function allNodesHaveAtMostOneParentPerSubgraph(): Result foreach ($nodeRecordsWithMultipleParents as $record) { $result->addError(new Error( 'Node aggregate ' . $record['nodeaggregateid'] - . ' has multiple parents in content stream ' . $contentStreamDbId->value + . ' has multiple parents in content stream ' . $contentStreamLayers->toDebugString() . ' and dimension space point ' . $dimensionSpacePoint->toJson(), self::ERROR_CODE_NODE_HAS_MULTIPLE_PARENTS )); @@ -495,21 +501,22 @@ public function nodeAggregatesAreConsistentlyTypedPerContentStream(): Result DISTINCT n.nodetypename FROM {$this->tableNames->node()} n - INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint + INNER JOIN {$this->hierarchyRelationStatement->toSql()} h ON h.childnodeanchor = n.relationanchorpoint WHERE - h.contentstreamdbid = :contentStreamDbId - AND n.nodeaggregateid = :nodeAggregateId + n.nodeaggregateid = :nodeAggregateId SQL; - foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { + foreach ($this->findProjectedContentStreamLayers() as $contentStreamLayers) { foreach ( $this->findProjectedNodeAggregateIdsInContentStream( - $contentStreamDbId + $contentStreamLayers ) as $nodeAggregateId ) { try { $nodeTypeNames = $this->dbal->fetchFirstColumn($nodeAggregatesStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'nodeAggregateId' => $nodeAggregateId->value + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load node type names: %s', $e->getMessage()), 1716494446, $e); @@ -518,7 +525,7 @@ public function nodeAggregatesAreConsistentlyTypedPerContentStream(): Result if (count($nodeTypeNames) > 1) { $result->addError(new Error( 'Node aggregate ' . $nodeAggregateId->value - . ' in content stream ' . $contentStreamDbId->value + . ' in content stream ' . $contentStreamLayers->toDebugString() . ' is of ambiguous type ("' . implode('","', $nodeTypeNames) . '")', self::ERROR_CODE_NODE_AGGREGATE_IS_AMBIGUOUSLY_TYPED )); @@ -537,21 +544,23 @@ public function nodeAggregatesAreConsistentlyClassifiedPerContentStream(): Resul DISTINCT n.classification FROM {$this->tableNames->node()} n - INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint + INNER JOIN {$this->hierarchyRelationStatement->toSql()} h ON h.childnodeanchor = n.relationanchorpoint WHERE - h.contentstreamdbid = :contentStreamDbId - AND n.nodeaggregateid = :nodeAggregateId + n.nodeaggregateid = :nodeAggregateId SQL; - foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { + + foreach ($this->findProjectedContentStreamLayers() as $contentStreamLayers) { foreach ( $this->findProjectedNodeAggregateIdsInContentStream( - $contentStreamDbId + $contentStreamLayers ) as $nodeAggregateId ) { try { $classifications = $this->dbal->fetchFirstColumn($nodeAggregatesStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'nodeAggregateId' => $nodeAggregateId->value + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load node classifications: %s', $e->getMessage()), 1716494466, $e); @@ -560,7 +569,7 @@ public function nodeAggregatesAreConsistentlyClassifiedPerContentStream(): Resul if (count($classifications) > 1) { $result->addError(new Error( 'Node aggregate ' . $nodeAggregateId->value - . ' in content stream ' . $contentStreamDbId->value + . ' in content stream ' . $contentStreamLayers->toDebugString() . ' is ambiguously classified ("' . implode('","', $classifications) . '")', self::ERROR_CODE_NODE_AGGREGATE_IS_AMBIGUOUSLY_CLASSIFIED )); @@ -578,19 +587,19 @@ public function childNodeCoverageIsASubsetOfParentNodeCoverage(): Result SELECT n.nodeaggregateid, c.dimensionspacepointhash FROM - {$this->tableNames->hierarchyRelation()} c + {$this->hierarchyRelationStatement->toSql()} c INNER JOIN {$this->tableNames->node()} n ON c.childnodeanchor = n.relationanchorpoint - LEFT JOIN {$this->tableNames->hierarchyRelation()} p ON c.parentnodeanchor = p.childnodeanchor + LEFT JOIN {$this->hierarchyRelationStatement->toSql()} p ON c.parentnodeanchor = p.childnodeanchor WHERE - c.contentstreamdbid = :contentStreamDbId - AND p.contentstreamdbid = :contentStreamDbId - AND c.dimensionspacepointhash = p.dimensionspacepointhash + c.dimensionspacepointhash = p.dimensionspacepointhash AND p.childnodeanchor IS NULL SQL; - foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { + foreach ($this->findProjectedContentStreamLayers() as $contentStreamLayers) { try { $excessivelyCoveringNodeRecords = $this->dbal->fetchAllAssociative($excessivelyCoveringStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value + 'contentStreamLayers' => $contentStreamLayers->toIntArray() + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load excessively covering nodes: %s', $e->getMessage()), 1716494618, $e); @@ -598,7 +607,7 @@ public function childNodeCoverageIsASubsetOfParentNodeCoverage(): Result foreach ($excessivelyCoveringNodeRecords as $excessivelyCoveringNodeRecord) { $result->addError(new Error( 'Node aggregate ' . $excessivelyCoveringNodeRecord['nodeaggregateid'] - . ' in content stream ' . $contentStreamDbId->value + . ' in content stream ' . $contentStreamLayers->toDebugString() . ' covers dimension space point hash ' . $excessivelyCoveringNodeRecord['dimensionspacepointhash'] . ' but its parent does not.', self::ERROR_CODE_CHILD_NODE_COVERAGE_IS_NO_SUBSET_OF_PARENT_NODE_COVERAGE @@ -617,28 +626,27 @@ public function allNodesCoverTheirOrigin(): Result nodeaggregateid, origindimensionspacepointhash FROM {$this->tableNames->node()} n - INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint + INNER JOIN {$this->hierarchyRelationStatement->toSql()} h ON h.childnodeanchor = n.relationanchorpoint WHERE - h.contentstreamdbid = :contentStreamDbId - AND nodeaggregateid NOT IN ( + nodeaggregateid NOT IN ( -- this query finds all nodes whose origin *IS COVERED* by an incoming hierarchy relation. SELECT n.nodeaggregateid FROM {$this->tableNames->node()} n - LEFT JOIN {$this->tableNames->hierarchyRelation()} p ON + LEFT JOIN {$this->hierarchyRelationStatement->toSql()} p ON p.childnodeanchor = n.relationanchorpoint AND p.dimensionspacepointhash = n.origindimensionspacepointhash - WHERE - p.contentstreamdbid = :contentStreamDbId - ) - AND classification != :rootClassification + ) + AND classification != :rootClassification SQL; - foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { + foreach ($this->findProjectedContentStreamLayers() as $contentStreamLayers) { try { $nodeRecordsWithMissingOriginCoverage = $this->dbal->fetchAllAssociative($nodesWithMissingOriginCoverageStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'rootClassification' => NodeAggregateClassification::CLASSIFICATION_ROOT->value + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load nodes with missing origin coverage: %s', $e->getMessage()), 1716494752, $e); @@ -647,7 +655,7 @@ public function allNodesCoverTheirOrigin(): Result foreach ($nodeRecordsWithMissingOriginCoverage as $nodeRecord) { $result->addError(new Error( 'Node aggregate ' . $nodeRecord['nodeaggregateid'] - . ' in content stream ' . $contentStreamDbId->value + . ' in content stream ' . $contentStreamLayers->toDebugString() . ' does not cover its origin dimension space point hash ' . $nodeRecord['origindimensionspacepointhash'] . '.', self::ERROR_CODE_NODE_DOES_NOT_COVER_ITS_ORIGIN @@ -659,21 +667,28 @@ public function allNodesCoverTheirOrigin(): Result } /** - * Returns all content stream ids - * - * @return iterable + * @return iterable */ - private function findProjectedContentStreamDbIds(): iterable + private function findProjectedContentStreamLayers(): iterable { - $contentStreamDbIdsStatement = <<tableNames->hierarchyRelation()} + $contentStreamLayersStatement = <<tableNames->contentStreamLayer()} SQL; try { - $contentStreamDbIds = $this->dbal->fetchFirstColumn($contentStreamDbIdsStatement); + $contentStreamLayers = $this->dbal->fetchAllAssociative($contentStreamLayersStatement); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load content stream ids: %s', $e->getMessage()), 1716494814, $e); } - return array_map(fn (string $value) => ContentStreamDbId::fromInt((int)$value), $contentStreamDbIds); + + foreach (array_unique(array_column($contentStreamLayers, 'contentstreamid')) as $contentStreamId) { + $items = []; + foreach ($contentStreamLayers as $row) { + if ($row['contentstreamid'] === $contentStreamId) { + $items[] = $row['contentstreamlayer']; + } + } + yield ContentStreamLayers::fromArray($items); + } } /** @@ -696,20 +711,20 @@ private function findProjectedDimensionSpacePoints(): DimensionSpacePointSet * @return array */ protected function findProjectedNodeAggregateIdsInContentStream( - ContentStreamDbId $contentStreamDbId + ContentStreamLayers $contentStreamLayers ): array { $nodeAggregateIdsStatement = <<tableNames->node()} n - INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint - WHERE - h.contentstreamdbid = :contentStreamDbId + INNER JOIN {$this->hierarchyRelationStatement->toSql()} h ON h.childnodeanchor = n.relationanchorpoint SQL; try { $nodeAggregateIds = $this->dbal->fetchFirstColumn($nodeAggregateIdsStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load node aggregate ids for content stream: %s', $e->getMessage()), 1716495988, $e); diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php index 20d555ac8dd..cb502aa3412 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php @@ -19,7 +19,8 @@ use Doctrine\DBAL\Exception as DBALException; use Doctrine\DBAL\Query\QueryBuilder; use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamLayers; +use Neos\ContentGraph\DoctrineDbalAdapter\HierarchyRelationStatement; use Neos\ContentGraph\DoctrineDbalAdapter\NodeQueryBuilder; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; @@ -68,6 +69,8 @@ final class ContentGraph implements ContentGraphInterface { private readonly NodeQueryBuilder $nodeQueryBuilder; + private readonly HierarchyRelationStatement $hierarchyRelationStatement; + public function __construct( private readonly Connection $dbal, private readonly NodeFactory $nodeFactory, @@ -76,9 +79,10 @@ public function __construct( private readonly ContentGraphTableNames $tableNames, public readonly WorkspaceName $workspaceName, public readonly ContentStreamId $contentStreamId, - public readonly ContentStreamDbId $contentStreamDbId, + public readonly ContentStreamLayers $contentStreamLayers, ) { $this->nodeQueryBuilder = new NodeQueryBuilder($this->dbal, $this->tableNames); + $this->hierarchyRelationStatement = HierarchyRelationStatement::for($this->tableNames); } public function getContentRepositoryId(): ContentRepositoryId @@ -98,7 +102,7 @@ public function getSubgraph( return new ContentSubgraph( $this->contentRepositoryId, $this->workspaceName, - $this->contentStreamDbId, + $this->contentStreamLayers, $dimensionSpacePoint, $visibilityConstraints, $this->dbal, @@ -137,7 +141,7 @@ public function findRootNodeAggregateByType( public function findRootNodeAggregates( FindRootNodeAggregatesFilter $filter, ): NodeAggregates { - $rootNodeAggregateQueryBuilder = $this->nodeQueryBuilder->buildFindRootNodeAggregatesQuery($this->contentStreamDbId, $filter); + $rootNodeAggregateQueryBuilder = $this->nodeQueryBuilder->buildFindRootNodeAggregatesQuery($this->contentStreamLayers, $filter); return $this->mapQueryBuilderToNodeAggregates($rootNodeAggregateQueryBuilder); } @@ -148,8 +152,10 @@ public function findNodeAggregatesByType( $queryBuilder ->andWhere('n.nodetypename = :nodeTypeName') ->setParameters([ - 'contentStreamDbId' => $this->contentStreamDbId->value, + 'contentStreamLayers' => $this->contentStreamLayers->toIntArray(), 'nodeTypeName' => $nodeTypeName->value, + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); return $this->mapQueryBuilderToNodeAggregates($queryBuilder); } @@ -162,7 +168,9 @@ public function findNodeAggregateById( ->orderBy('n.relationanchorpoint', 'DESC') ->setParameters([ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamDbId' => $this->contentStreamDbId->value + 'contentStreamLayers' => $this->contentStreamLayers->toIntArray() + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); return $this->nodeFactory->mapNodeRowsToNodeAggregate( @@ -180,9 +188,10 @@ public function findNodeAggregatesByIds( ->orderBy('n.relationanchorpoint', 'DESC') ->setParameters([ 'nodeAggregateIds' => $nodeAggregateIds->toStringArray(), - 'contentStreamDbId' => $this->contentStreamDbId->value + 'contentStreamLayers' => $this->contentStreamLayers->toIntArray() ], [ - 'nodeAggregateIds' => ArrayParameterType::STRING + 'nodeAggregateIds' => ArrayParameterType::STRING, + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); return $this->mapQueryBuilderToNodeAggregates($queryBuilder); @@ -197,13 +206,14 @@ public function findParentNodeAggregates( NodeAggregateId $childNodeAggregateId ): NodeAggregates { $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeAggregateQuery() - ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') + ->innerJoin('n', $this->hierarchyRelationStatement->toSql(), 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') - ->andWhere('ch.contentstreamdbid = :contentStreamDbId') ->andWhere('cn.nodeaggregateid = :nodeAggregateId') ->setParameters([ 'nodeAggregateId' => $childNodeAggregateId->value, - 'contentStreamDbId' => $this->contentStreamDbId->value + 'contentStreamLayers' => $this->contentStreamLayers->toIntArray() + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); return $this->mapQueryBuilderToNodeAggregates($queryBuilder); @@ -213,22 +223,20 @@ public function findAncestorNodeAggregateIds(NodeAggregateId $entryNodeAggregate { $queryBuilderInitial = $this->createQueryBuilder() ->select('ch.parentnodeanchor') - ->from($this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ch') + ->from($this->hierarchyRelationStatement->toSql(), 'ch') ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'c', 'c.relationanchorpoint = ch.childnodeanchor') - ->where('ch.contentstreamdbid = :contentStreamDbId') ->andWhere('c.nodeaggregateid = :entryNodeAggregateId'); $queryBuilderRecursive = $this->createQueryBuilder() ->select('ph.parentnodeanchor') ->from('ancestry', 'ch') - ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ph', 'ph.childnodeanchor = ch.parentnodeanchor') - ->where('ph.contentstreamdbid = :contentStreamDbId'); + ->innerJoin('ch', $this->hierarchyRelationStatement->toSql(), 'ph', 'ph.childnodeanchor = ch.parentnodeanchor'); $queryBuilderCte = $this->createQueryBuilder() ->select('n.nodeAggregateId') ->from('ancestry', 'a') ->innerJoin('a', $this->nodeQueryBuilder->tableNames->node(), 'n', 'n.relationanchorpoint = a.parentnodeanchor') - ->setParameter('contentStreamDbId', $this->contentStreamDbId->value) + ->setParameter('contentStreamLayers', $this->contentStreamLayers->toIntArray(), ArrayParameterType::STRING) ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); $nodeAggregateIdRows = $this->fetchCteResults( @@ -244,7 +252,7 @@ public function findAncestorNodeAggregateIds(NodeAggregateId $entryNodeAggregate public function findChildNodeAggregates( NodeAggregateId $parentNodeAggregateId ): NodeAggregates { - $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamDbId); + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamLayers); return $this->mapQueryBuilderToNodeAggregates($queryBuilder); } @@ -253,24 +261,23 @@ public function findParentNodeAggregateByChildOriginDimensionSpacePoint(NodeAggr $subQueryBuilder = $this->createQueryBuilder() ->select('pn.nodeaggregateid') ->from($this->nodeQueryBuilder->tableNames->node(), 'pn') - ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :childOriginDimensionSpacePointHash')->toSql(), 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') - ->where('ch.contentstreamdbid = :contentStreamDbId') - ->andWhere('ch.dimensionspacepointhash = :childOriginDimensionSpacePointHash') - ->andWhere('cn.nodeaggregateid = :childNodeAggregateId') + ->where('cn.nodeaggregateid = :childNodeAggregateId') ->andWhere('cn.origindimensionspacepointhash = :childOriginDimensionSpacePointHash'); $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamdbid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamlayer, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->nodeQueryBuilder->tableNames->node(), 'n') - ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->innerJoin('n', $this->hierarchyRelationStatement->toSql(), 'h', 'h.childnodeanchor = n.relationanchorpoint') ->innerJoin('h', $this->nodeQueryBuilder->tableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') ->where('n.nodeaggregateid = (' . $subQueryBuilder->getSQL() . ')') - ->andWhere('h.contentstreamdbid = :contentStreamDbId') ->setParameters([ - 'contentStreamDbId' => $this->contentStreamDbId->value, + 'contentStreamLayers' => $this->contentStreamLayers->toIntArray(), 'childNodeAggregateId' => $childNodeAggregateId->value, 'childOriginDimensionSpacePointHash' => $childOriginDimensionSpacePoint->hash, + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); return $this->nodeFactory->mapNodeRowsToNodeAggregate( @@ -282,7 +289,7 @@ public function findParentNodeAggregateByChildOriginDimensionSpacePoint(NodeAggr public function findTetheredChildNodeAggregates(NodeAggregateId $parentNodeAggregateId): NodeAggregates { - $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamDbId) + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamLayers) ->andWhere('cn.classification = :tetheredClassification') ->setParameter('tetheredClassification', NodeAggregateClassification::CLASSIFICATION_TETHERED->value); @@ -293,7 +300,7 @@ public function findChildNodeAggregateByName( NodeAggregateId $parentNodeAggregateId, NodeName $name ): ?NodeAggregate { - $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamDbId) + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamLayers) ->andWhere('cn.name = :relationName') ->setParameter('relationName', $name->value); @@ -304,24 +311,22 @@ public function getDimensionSpacePointsOccupiedByChildNodeName(NodeName $nodeNam { $queryBuilder = $this->createQueryBuilder() ->select('dsp.dimensionspacepoint, h.dimensionspacepointhash') - ->from($this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h') + ->from($this->hierarchyRelationStatement->where('h.dimensionspacepointhash IN (:dimensionSpacePointHashes)')->toSql(), 'h') ->innerJoin('h', $this->nodeQueryBuilder->tableNames->node(), 'n', 'n.relationanchorpoint = h.parentnodeanchor') ->innerJoin('h', $this->nodeQueryBuilder->tableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ph', 'ph.childnodeanchor = n.relationanchorpoint') + ->innerJoin('n', $this->hierarchyRelationStatement->toSql(), 'ph', 'ph.childnodeanchor = n.relationanchorpoint') ->where('n.nodeaggregateid = :parentNodeAggregateId') ->andWhere('n.origindimensionspacepointhash = :parentNodeOriginDimensionSpacePointHash') - ->andWhere('ph.contentstreamdbid = :contentStreamDbId') - ->andWhere('h.contentstreamdbid = :contentStreamDbId') - ->andWhere('h.dimensionspacepointhash IN (:dimensionSpacePointHashes)') ->andWhere('n.name = :nodeName') ->setParameters([ 'parentNodeAggregateId' => $parentNodeAggregateId->value, 'parentNodeOriginDimensionSpacePointHash' => $parentNodeOriginDimensionSpacePoint->hash, - 'contentStreamDbId' => $this->contentStreamDbId->value, + 'contentStreamLayers' => $this->contentStreamLayers->toIntArray(), 'dimensionSpacePointHashes' => $dimensionSpacePointsToCheck->getPointHashes(), 'nodeName' => $nodeName->value ], [ 'dimensionSpacePointHashes' => ArrayParameterType::STRING, + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); $dimensionSpacePoints = []; foreach ($this->fetchRows($queryBuilder) as $hierarchyRelationData) { @@ -334,19 +339,18 @@ public function getDimensionSpacePointsOccupiedByChildNodeName(NodeName $nodeNam public function findNodeAggregatesTaggedBy(SubtreeTag $subtreeTag): NodeAggregates { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamdbid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamlayer, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') // select the subtree tags from tagged (t) h and then join h again to fetch all node rows in that aggregate - ->from($this->tableNames->hierarchyRelation(), 'th') - ->innerJoin('th', $this->tableNames->hierarchyRelation(), 'h', 'th.childnodeanchor = h.childnodeanchor') + ->from($this->hierarchyRelationStatement->where('JSON_EXTRACT(h.subtreetags, :tagPath) LIKE "true"')->toSql(), 'th') + ->innerJoin('th', $this->hierarchyRelationStatement->toSql(), 'h', 'th.childnodeanchor = h.childnodeanchor') ->innerJoin('h', $this->tableNames->node(), 'n', 'h.childnodeanchor = n.relationanchorpoint') ->innerJoin('h', $this->tableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->where('th.contentstreamdbid = :contentStreamDbId') - ->andWhere('JSON_EXTRACT(th.subtreetags, :tagPath) LIKE "true"') - ->andWhere('h.contentstreamdbid = :contentStreamDbId') ->orderBy('n.relationanchorpoint', 'DESC') ->setParameters([ 'tagPath' => '$."' . $subtreeTag->value . '"', - 'contentStreamDbId' => $this->contentStreamDbId->value + 'contentStreamLayers' => $this->contentStreamLayers->toIntArray() + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); return $this->mapQueryBuilderToNodeAggregates($queryBuilder); diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamDbIdFinder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamDbIdFinder.php deleted file mode 100644 index b6f25ecc716..00000000000 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamDbIdFinder.php +++ /dev/null @@ -1,76 +0,0 @@ - - */ - private array $contentStreamIdRuntimeCache = []; - - public function __construct( - private readonly Connection $dbal, - private readonly ContentGraphTableNames $tableNames, - ) { - } - - public function getContentStreamDbId(ContentStreamId $contentStreamId): ContentStreamDbId - { - $contentStreamDbId = $this->getFromRuntimeCache($contentStreamId); - if ($contentStreamDbId === null) { - $this->fillRuntimeCacheFromDatabase(); - $contentStreamDbId = $this->getFromRuntimeCache($contentStreamId); - } - - if ($contentStreamDbId === null) { - throw new \RuntimeException(sprintf('A ContentStream with id "%s" was not found in the projection, cannot determine ContentStreamDbId.', $contentStreamId->value), 1769945094); - } - - return $contentStreamDbId; - } - - private function getFromRuntimeCache(ContentStreamId $contentStreamId): ?ContentStreamDbId - { - return $this->contentStreamIdRuntimeCache[$contentStreamId->value] ?? null; - } - - private function fillRuntimeCacheFromDatabase(): void - { - $allContentStreamIdsStatement = <<tableNames->contentStream()} - SQL; - try { - $allContentStreamIds = $this->dbal->fetchAllAssociative($allContentStreamIdsStatement); - } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load content stream ids from database: %s', $e->getMessage()), 1769945050, $e); - } - foreach ($allContentStreamIds as $contentStreamIdRow) { - $this->contentStreamIdRuntimeCache[(string)$contentStreamIdRow['id']] = ContentStreamDbId::fromInt($contentStreamIdRow['dbId']); - } - } -} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamLayerFinder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamLayerFinder.php new file mode 100644 index 00000000000..313cbd6d253 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamLayerFinder.php @@ -0,0 +1,47 @@ +tableNames->contentStreamLayer()} + WHERE contentstreamid = :contentStreamId + SQL; + try { + $contentStreamLayers = $this->dbal->fetchFirstColumn($contentStreamLayersStatement, ['contentStreamId' => $contentStreamId->value]); + } catch (DBALException $e) { + throw new \RuntimeException(sprintf('Failed to load content stream ids from database: %s', $e->getMessage()), 1769945050, $e); + } + return ContentStreamLayers::fromArray($contentStreamLayers); + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index f65c9dd553e..44c66bb63d3 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -20,7 +20,8 @@ use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Result; use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamLayers; +use Neos\ContentGraph\DoctrineDbalAdapter\HierarchyRelationStatement; use Neos\ContentGraph\DoctrineDbalAdapter\NodeQueryBuilder; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; @@ -92,10 +93,12 @@ final class ContentSubgraph implements ContentSubgraphInterface { private readonly NodeQueryBuilder $nodeQueryBuilder; + private readonly HierarchyRelationStatement $hierarchyRelationStatement; + public function __construct( private readonly ContentRepositoryId $contentRepositoryId, private readonly WorkspaceName $workspaceName, - private readonly ContentStreamDbId $contentStreamDbId, + private readonly ContentStreamLayers $contentStreamLayers, private readonly DimensionSpacePoint $dimensionSpacePoint, private readonly VisibilityConstraints $visibilityConstraints, private readonly Connection $dbal, @@ -104,6 +107,7 @@ public function __construct( ContentGraphTableNames $tableNames ) { $this->nodeQueryBuilder = new NodeQueryBuilder($this->dbal, $tableNames); + $this->hierarchyRelationStatement = HierarchyRelationStatement::for($tableNames); } public function getContentRepositoryId(): ContentRepositoryId @@ -169,7 +173,7 @@ public function countBackReferences(NodeAggregateId $nodeAggregateId, CountBackR public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node { - $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamDbId, $this->dimensionSpacePoint) + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamLayers, $this->dimensionSpacePoint) ->andWhere('n.nodeaggregateid = :nodeAggregateId') ->setParameter('nodeAggregateId', $nodeAggregateId->value); $this->addSubtreeTagConstraints($queryBuilder); @@ -178,7 +182,7 @@ public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node public function findNodesByIds(NodeAggregateIds $nodeAggregateIds): Nodes { - $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamDbId, $this->dimensionSpacePoint) + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamLayers, $this->dimensionSpacePoint) ->andWhere('n.nodeaggregateid in (:nodeAggregateIds)') ->setParameter('nodeAggregateIds', $nodeAggregateIds->toStringArray(), ArrayParameterType::STRING); $this->addSubtreeTagConstraints($queryBuilder); @@ -187,7 +191,7 @@ public function findNodesByIds(NodeAggregateIds $nodeAggregateIds): Nodes public function findRootNodeByType(NodeTypeName $nodeTypeName): ?Node { - $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamDbId, $this->dimensionSpacePoint) + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamLayers, $this->dimensionSpacePoint) ->andWhere('n.nodetypename = :nodeTypeName')->setParameter('nodeTypeName', $nodeTypeName->value) ->andWhere('n.classification = :nodeAggregateClassification')->setParameter('nodeAggregateClassification', NodeAggregateClassification::CLASSIFICATION_ROOT->value); $this->addSubtreeTagConstraints($queryBuilder); @@ -196,7 +200,7 @@ public function findRootNodeByType(NodeTypeName $nodeTypeName): ?Node public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node { - $queryBuilder = $this->nodeQueryBuilder->buildBasicParentNodeQuery($childNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint); + $queryBuilder = $this->nodeQueryBuilder->buildBasicParentNodeQuery($childNodeAggregateId, $this->contentStreamLayers, $this->dimensionSpacePoint); $this->addSubtreeTagConstraints($queryBuilder, 'ph'); return $this->fetchNode($queryBuilder); } @@ -228,7 +232,7 @@ public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node */ private function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $nodeName): ?Node { - $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint) + $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamLayers, $this->dimensionSpacePoint) ->andWhere('n.name = :edgeName')->setParameter('edgeName', $nodeName->value); $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); @@ -275,19 +279,15 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi // @see https://mariadb.com/kb/en/library/recursive-common-table-expressions-overview/#cast-to-avoid-data-truncation ->select('n.*, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') ->from($this->nodeQueryBuilder->tableNames->node(), 'n') - ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->where('h.contentstreamdbid = :contentStreamDbId') - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash') - ->andWhere('n.nodeaggregateid = :entryNodeAggregateId'); + ->innerJoin('n', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->where('n.nodeaggregateid = :entryNodeAggregateId'); $this->addSubtreeTagConstraints($queryBuilderInitial); $queryBuilderRecursive = $this->createQueryBuilder() ->select('c.*, h.subtreetags, p.nodeaggregateid AS parentNodeAggregateId, p.level + 1 AS level, h.position') ->from('tree', 'p') - ->innerJoin('p', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.parentnodeanchor = p.relationanchorpoint') - ->innerJoin('p', $this->nodeQueryBuilder->tableNames->node(), 'c', 'c.relationanchorpoint = h.childnodeanchor') - ->where('h.contentstreamdbid = :contentStreamDbId') - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); + ->innerJoin('p', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'h', 'h.parentnodeanchor = p.relationanchorpoint') + ->innerJoin('p', $this->nodeQueryBuilder->tableNames->node(), 'c', 'c.relationanchorpoint = h.childnodeanchor'); if ($filter->maximumLevels !== null) { $queryBuilderRecursive->andWhere('p.level < :maximumLevels')->setParameter('maximumLevels', $filter->maximumLevels); } @@ -301,7 +301,7 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi ->from('tree') ->orderBy('level') ->addOrderBy('position') - ->setParameter('contentStreamDbId', $this->contentStreamDbId->value) + ->setParameter('contentStreamLayers', $this->contentStreamLayers->toIntArray(), ArrayParameterType::INTEGER) ->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); @@ -379,22 +379,18 @@ public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClose ->select('n.*, ph.subtreetags, ph.parentnodeanchor') ->from($this->nodeQueryBuilder->tableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. - ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ph', 'n.relationanchorpoint = ph.childnodeanchor') - ->andWhere('ph.contentstreamdbid = :contentStreamDbId') - ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash') - ->andWhere('n.nodeaggregateid = :entryNodeAggregateId'); + ->innerJoin('n', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'ph', 'n.relationanchorpoint = ph.childnodeanchor') + ->where('n.nodeaggregateid = :entryNodeAggregateId'); $this->addSubtreeTagConstraints($queryBuilderInitial, 'ph'); $queryBuilderRecursive = $this->createQueryBuilder() ->select('pn.*, h.subtreetags, h.parentnodeanchor') ->from('ancestry', 'cn') ->innerJoin('cn', $this->nodeQueryBuilder->tableNames->node(), 'pn', 'pn.relationanchorpoint = cn.parentnodeanchor') - ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = pn.relationanchorpoint') - ->where('h.contentstreamdbid = :contentStreamDbId') - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); + ->innerJoin('pn', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'h', 'h.childnodeanchor = pn.relationanchorpoint'); $this->addSubtreeTagConstraints($queryBuilderRecursive); - $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint); + $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamLayers, $this->dimensionSpacePoint); $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), 'pn'); $nodeRows = $this->fetchCteResults( $queryBuilderInitial, @@ -437,7 +433,7 @@ public function countDescendantNodes(NodeAggregateId $entryNodeAggregateId, Coun public function countNodes(): int { - $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamDbId, $this->dimensionSpacePoint, 'n', 'COUNT(*)'); + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamLayers, $this->dimensionSpacePoint, 'n', 'COUNT(*)'); try { $result = $this->executeQuery($queryBuilder)->fetchOne(); } catch (DBALException $e) { @@ -483,7 +479,7 @@ private function addSubtreeTagConstraints(QueryBuilder $queryBuilder, string $hi private function buildChildNodesQuery(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter|CountChildNodesFilter $filter): QueryBuilder { - $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint); + $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamLayers, $this->dimensionSpacePoint); if ($filter->nodeTypes !== null) { $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); } @@ -501,9 +497,12 @@ private function buildReferencesQuery(NodeAggregateId $nodeAggregateId, FindRefe { $subselectParameters = [ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamDbId' => $this->contentStreamDbId->value, + 'contentStreamLayers' => $this->contentStreamLayers->toIntArray(), 'dimensionSpacePointHash' => $this->dimensionSpacePoint->hash, ]; + $subselectTypes = [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, + ]; $subtreeTagConstraints = ''; $i = 0; foreach ($this->visibilityConstraints->excludedSubtreeTags as $excludedTag) { @@ -514,19 +513,17 @@ private function buildReferencesQuery(NodeAggregateId $nodeAggregateId, FindRefe $queryBuilder = $this->createQueryBuilder() ->select("dn.*, dh.subtreetags, r.name AS referencename, r.properties AS referenceproperties") - ->from($this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'dh') + ->from($this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'dh') ->innerJoin('dh', $this->nodeQueryBuilder->tableNames->node(), 'dn', 'dn.relationanchorpoint = dh.childnodeanchor') ->innerJoin('dn', $this->nodeQueryBuilder->tableNames->referenceRelation(), 'r', 'r.destinationnodeaggregateid = dn.nodeaggregateid') ->where('r.nodeanchorpoint = ( SELECT relationanchorpoint FROM ' . $this->nodeQueryBuilder->tableNames->node() . ' sn - JOIN ' . $this->nodeQueryBuilder->tableNames->hierarchyRelation() . ' sh ON sn.relationanchorpoint = sh.childnodeanchor - WHERE sn.nodeaggregateid = :nodeAggregateId - AND sh.contentstreamdbid = :contentStreamDbId - AND sh.dimensionspacepointhash = :dimensionSpacePointHash ' + JOIN ' . $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql() . ' sh ON sn.relationanchorpoint = sh.childnodeanchor + WHERE sn.nodeaggregateid = :nodeAggregateId ' . $subtreeTagConstraints . ' - )')->setParameters($subselectParameters) - ->andWhere('dh.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->andWhere('dh.contentstreamdbid = :contentStreamDbId')->setParameter('contentStreamDbId', $this->contentStreamDbId->value); + )')->setParameters($subselectParameters, $subselectTypes) + ->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) + ->setParameter('contentStreamLayers', $this->contentStreamLayers->toIntArray(), ArrayParameterType::INTEGER); $this->addSubtreeTagConstraints($queryBuilder, 'dh'); if ($filter->nodeTypes !== null) { $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), "dn"); @@ -565,9 +562,12 @@ private function buildBackreferencesQuery(NodeAggregateId $nodeAggregateId, Find { $subselectParameters = [ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamDbId' => $this->contentStreamDbId->value, + 'contentStreamLayers' => $this->contentStreamLayers->toIntArray(), 'dimensionSpacePointHash' => $this->dimensionSpacePoint->hash, ]; + $subselectTypes = [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, + ]; $subtreeTagConstraints = ''; $i = 0; foreach ($this->visibilityConstraints->excludedSubtreeTags as $excludedTag) { @@ -578,19 +578,18 @@ private function buildBackreferencesQuery(NodeAggregateId $nodeAggregateId, Find $queryBuilder = $this->createQueryBuilder() ->select("sn.*, sh.subtreetags, r.name AS referencename, r.properties AS referenceproperties") - ->from($this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'sh') + ->from($this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'sh') ->innerJoin('sh', $this->nodeQueryBuilder->tableNames->node(), 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') ->innerJoin('sn', $this->nodeQueryBuilder->tableNames->referenceRelation(), 'r', 'r.nodeanchorpoint = sn.relationanchorpoint') + // todo use join to instead of query ->where('r.destinationnodeaggregateid = ( SELECT nodeaggregateid FROM ' . $this->nodeQueryBuilder->tableNames->node() . ' dn - JOIN ' . $this->nodeQueryBuilder->tableNames->hierarchyRelation() . ' dh ON dn.relationanchorpoint = dh.childnodeanchor - WHERE dn.nodeaggregateid = :nodeAggregateId - AND dh.contentstreamdbid = :contentStreamDbId - AND dh.dimensionspacepointhash = :dimensionSpacePointHash ' + JOIN ' . $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql() . ' dh ON dn.relationanchorpoint = dh.childnodeanchor + WHERE dn.nodeaggregateid = :nodeAggregateId ' . $subtreeTagConstraints . ' - )')->setParameters($subselectParameters) - ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->andWhere('sh.contentstreamdbid = :contentStreamDbId')->setParameter('contentStreamDbId', $this->contentStreamDbId->value); + )')->setParameters($subselectParameters, $subselectTypes) + ->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) + ->setParameter('contentStreamLayers', $this->contentStreamLayers->toIntArray(), ArrayParameterType::INTEGER); $this->addSubtreeTagConstraints($queryBuilder, 'sh'); if ($filter->nodeTypes !== null) { $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), "sn"); @@ -627,7 +626,7 @@ private function buildBackreferencesQuery(NodeAggregateId $nodeAggregateId, Find private function buildSiblingsQuery(bool $preceding, NodeAggregateId $siblingNodeAggregateId, FindPrecedingSiblingNodesFilter|FindSucceedingSiblingNodesFilter $filter): QueryBuilder { - $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeSiblingsQuery($preceding, $siblingNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint); + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeSiblingsQuery($preceding, $siblingNodeAggregateId, $this->contentStreamLayers, $this->dimensionSpacePoint); $this->addSubtreeTagConstraints($queryBuilder); if ($filter->nodeTypes !== null) { @@ -654,14 +653,11 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId ->select('n.*, ph.subtreetags, ph.parentnodeanchor, 0 AS level') ->from($this->nodeQueryBuilder->tableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. - ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') + ->innerJoin('n', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'c', 'c.relationanchorpoint = ch.childnodeanchor') - ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ph', 'n.relationanchorpoint = ph.childnodeanchor') - ->where('ch.contentstreamdbid = :contentStreamDbId') - ->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash') - ->andWhere('ph.contentstreamdbid = :contentStreamDbId') - ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash') + ->innerJoin('n', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'ph', 'n.relationanchorpoint = ph.childnodeanchor') ->andWhere('c.nodeaggregateid = :entryNodeAggregateId'); + // TODO subtree tag constraints on hierarchyRelationStatement!! $this->addSubtreeTagConstraints($queryBuilderInitial, 'ph'); $this->addSubtreeTagConstraints($queryBuilderInitial, 'ch'); @@ -669,12 +665,10 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId ->select('pn.*, h.subtreetags, h.parentnodeanchor, ch.level + 1 AS level') ->from('ancestry', 'ch') ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'pn', 'pn.relationanchorpoint = ch.parentnodeanchor') - ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = pn.relationanchorpoint') - ->where('h.contentstreamdbid = :contentStreamDbId') - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); + ->innerJoin('pn', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'h', 'h.childnodeanchor = pn.relationanchorpoint'); $this->addSubtreeTagConstraints($queryBuilderRecursive); - $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint); + $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamLayers, $this->dimensionSpacePoint); if ($filter->nodeTypes !== null) { $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), 'pn'); } @@ -691,26 +685,20 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate ->select('n.*, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position') ->from($this->nodeQueryBuilder->tableNames->node(), 'n') // we need to join with the hierarchy relation, because we need the node name. - ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->innerJoin('n', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'h', 'h.childnodeanchor = n.relationanchorpoint') ->innerJoin('n', $this->nodeQueryBuilder->tableNames->node(), 'p', 'p.relationanchorpoint = h.parentnodeanchor') - ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ph', 'ph.childnodeanchor = p.relationanchorpoint') - ->where('h.contentstreamdbid = :contentStreamDbId') - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash') - ->andWhere('ph.contentstreamdbid = :contentStreamDbId') - ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash') - ->andWhere('p.nodeaggregateid = :entryNodeAggregateId'); + ->innerJoin('n', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'ph', 'ph.childnodeanchor = p.relationanchorpoint') + ->where('p.nodeaggregateid = :entryNodeAggregateId'); $this->addSubtreeTagConstraints($queryBuilderInitial); $queryBuilderRecursive = $this->createQueryBuilder() ->select('cn.*, h.subtreetags, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position') ->from('tree', 'pn') - ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') - ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->node(), 'cn', 'cn.relationanchorpoint = h.childnodeanchor') - ->where('h.contentstreamdbid = :contentStreamDbId') - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); + ->innerJoin('pn', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->node(), 'cn', 'cn.relationanchorpoint = h.childnodeanchor'); $this->addSubtreeTagConstraints($queryBuilderRecursive); - $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint, 'tree', 'n'); + $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamLayers, $this->dimensionSpacePoint, 'tree', 'n'); if ($filter->nodeTypes !== null) { $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php index d02b04ed3be..47eec2334e7 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php @@ -19,10 +19,13 @@ use Doctrine\DBAL\Exception as DBALException; use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjection; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamLayer; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamLayers; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelationId; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRecord; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint; +use Neos\ContentGraph\DoctrineDbalAdapter\HierarchyRelationStatement; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; @@ -38,10 +41,13 @@ */ class ProjectionContentGraph { + private HierarchyRelationStatement $hierarchyRelationStatement; + public function __construct( private readonly Connection $dbal, private readonly ContentGraphTableNames $tableNames, ) { + $this->hierarchyRelationStatement = HierarchyRelationStatement::for($this->tableNames); } /** @@ -51,44 +57,42 @@ public function __construct( * correct) */ public function findParentNode( - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, NodeAggregateId $childNodeAggregateId, OriginDimensionSpacePoint $originDimensionSpacePoint, ?DimensionSpacePoint $coveredDimensionSpacePoint = null ): ?NodeRecord { $parentNodeStatement = <<tableNames->node()} p - INNER JOIN {$this->tableNames->hierarchyRelation()} ph ON ph.childnodeanchor = p.relationanchorpoint - INNER JOIN {$this->tableNames->hierarchyRelation()} ch ON ch.parentnodeanchor = p.relationanchorpoint + INNER JOIN {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :coveredDimensionSpacePointHash')->toSql()} ph ON ph.childnodeanchor = p.relationanchorpoint + INNER JOIN {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :coveredDimensionSpacePointHash')->toSql()} ch ON ch.parentnodeanchor = p.relationanchorpoint INNER JOIN {$this->tableNames->node()} c ON ch.childnodeanchor = c.relationanchorpoint INNER JOIN {$this->tableNames->dimensionSpacePoints()} dsp ON p.origindimensionspacepointhash = dsp.hash WHERE c.nodeaggregateid = :childNodeAggregateId AND c.origindimensionspacepointhash = :originDimensionSpacePointHash - AND ph.contentstreamdbid = :contentStreamDbId - AND ch.contentstreamdbid = :contentStreamDbId - AND ph.dimensionspacepointhash = :coveredDimensionSpacePointHash - AND ch.dimensionspacepointhash = :coveredDimensionSpacePointHash SQL; try { $nodeRow = $this->dbal->fetchAssociative($parentNodeStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'childNodeAggregateId' => $childNodeAggregateId->value, 'originDimensionSpacePointHash' => $originDimensionSpacePoint->hash, 'coveredDimensionSpacePointHash' => $coveredDimensionSpacePoint->hash ?? $originDimensionSpacePoint->hash + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load parent node for content stream %s, child node aggregate id %s, origin dimension space point %s from database: %s', $contentStreamDbId->value, $childNodeAggregateId->value, $originDimensionSpacePoint->toJson(), $e->getMessage()), 1716475976, $e); + throw new \RuntimeException(sprintf('Failed to load parent node for content stream %s, child node aggregate id %s, origin dimension space point %s from database: %s', $contentStreamLayers->toDebugString(), $childNodeAggregateId->value, $originDimensionSpacePoint->toJson(), $e->getMessage()), 1716475976, $e); } return $nodeRow ? NodeRecord::fromDatabaseRow($nodeRow) : null; } public function findNodeInAggregate( - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, NodeAggregateId $nodeAggregateId, DimensionSpacePoint $coveredDimensionSpacePoint ): ?NodeRecord { @@ -97,21 +101,21 @@ public function findNodeInAggregate( n.*, h.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM {$this->tableNames->node()} n - INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint + INNER JOIN {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h ON h.childnodeanchor = n.relationanchorpoint INNER JOIN {$this->tableNames->dimensionSpacePoints()} dsp ON n.origindimensionspacepointhash = dsp.hash WHERE n.nodeaggregateid = :nodeAggregateId - AND h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash SQL; try { $nodeRow = $this->dbal->fetchAssociative($nodeInAggregateStatement, [ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'nodeAggregateId' => $nodeAggregateId->value, 'dimensionSpacePointHash' => $coveredDimensionSpacePoint->hash + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load node for content stream %s, aggregate id %s and covered dimension space point %s from database: %s', $contentStreamDbId->value, $nodeAggregateId->value, $coveredDimensionSpacePoint->toJson(), $e->getMessage()), 1716474165, $e); + throw new \RuntimeException(sprintf('Failed to load node for content stream %s, aggregate id %s and covered dimension space point %s from database: %s', $contentStreamLayers->toDebugString(), $nodeAggregateId->value, $coveredDimensionSpacePoint->toJson(), $e->getMessage()), 1716474165, $e); } return $nodeRow ? NodeRecord::fromDatabaseRow($nodeRow) : null; @@ -120,31 +124,32 @@ public function findNodeInAggregate( public function getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $originDimensionSpacePoint, - ContentStreamDbId $contentStreamDbId + ContentStreamLayers $contentStreamLayers ): ?NodeRelationAnchorPoint { $relationAnchorPointsStatement = <<tableNames->node()} n - INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint + INNER JOIN {$this->hierarchyRelationStatement->toSql()} AS h ON h.childnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId AND n.origindimensionspacepointhash = :originDimensionSpacePointHash - AND h.contentstreamdbid = :contentStreamDbId SQL; try { $relationAnchorPoints = $this->dbal->fetchFirstColumn($relationAnchorPointsStatement, [ 'nodeAggregateId' => $nodeAggregateId->value, 'originDimensionSpacePointHash' => $originDimensionSpacePoint->hash, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load node anchor points for content stream %s, node aggregate %s and origin dimension space point %s from database: %s', $contentStreamDbId->value, $nodeAggregateId->value, $originDimensionSpacePoint->toJson(), $e->getMessage()), 1716474224, $e); + throw new \RuntimeException(sprintf('Failed to load node anchor points for content stream %s, node aggregate %s and origin dimension space point %s from database: %s', $contentStreamLayers->toDebugString(), $nodeAggregateId->value, $originDimensionSpacePoint->toJson(), $e->getMessage()), 1716474224, $e); } if (count($relationAnchorPoints) > 1) { - throw new \RuntimeException(sprintf('More than one node anchor point for content stream: %s, node aggregate id: %s and origin dimension space point: %s – this should not happen and might be a conceptual problem!', $contentStreamDbId->value, $nodeAggregateId->value, $originDimensionSpacePoint->toJson()), 1716474484); + throw new \RuntimeException(sprintf('More than one node anchor point for content stream: %s, node aggregate id: %s and origin dimension space point: %s – this should not happen and might be a conceptual problem!', $contentStreamLayers->toDebugString(), $nodeAggregateId->value, $originDimensionSpacePoint->toJson()), 1716474484); } return $relationAnchorPoints === [] ? null : NodeRelationAnchorPoint::fromInteger($relationAnchorPoints[0]); } @@ -154,25 +159,26 @@ public function getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStrea */ public function getAnchorPointsForNodeAggregateInContentStream( NodeAggregateId $nodeAggregateId, - ContentStreamDbId $contentStreamDbId + ContentStreamLayers $contentStreamLayers ): iterable { $relationAnchorPointsStatement = <<tableNames->node()} n - INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint + INNER JOIN {$this->hierarchyRelationStatement->toSql()} h ON h.childnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId - AND h.contentstreamdbid = :contentStreamDbId SQL; try { $relationAnchorPoints = $this->dbal->fetchFirstColumn($relationAnchorPointsStatement, [ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load node anchor points for content stream %s and node aggregate id %s from database: %s', $contentStreamDbId->value, $nodeAggregateId->value, $e->getMessage()), 1716474706, $e); + throw new \RuntimeException(sprintf('Failed to load node anchor points for content stream %s and node aggregate id %s from database: %s', $contentStreamLayers->toDebugString(), $nodeAggregateId->value, $e->getMessage()), 1716474706, $e); } return array_map(NodeRelationAnchorPoint::fromInteger(...), $relationAnchorPoints); @@ -204,7 +210,7 @@ public function determineHierarchyRelationPosition( ?NodeRelationAnchorPoint $parentAnchorPoint, ?NodeRelationAnchorPoint $childAnchorPoint, ?NodeRelationAnchorPoint $succeedingSiblingAnchorPoint, - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, DimensionSpacePoint $dimensionSpacePoint ): int { if (!$parentAnchorPoint && !$childAnchorPoint) { @@ -218,21 +224,21 @@ public function determineHierarchyRelationPosition( SELECT h.* FROM - {$this->tableNames->hierarchyRelation()} h + {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h WHERE h.childnodeanchor = :succeedingSiblingAnchorPoint - AND h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash SQL; try { /** @var array $succeedingSiblingRelation */ $succeedingSiblingRelation = $this->dbal->fetchAssociative($succeedingSiblingRelationStatement, [ 'succeedingSiblingAnchorPoint' => $succeedingSiblingAnchorPoint->value, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'dimensionSpacePointHash' => $dimensionSpacePoint->hash + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load succeeding sibling relations for content stream %s, anchor point %s and dimension space point %s from database: %s', $contentStreamDbId->value, $succeedingSiblingAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716474854, $e); + throw new \RuntimeException(sprintf('Failed to load succeeding sibling relations for content stream %s, anchor point %s and dimension space point %s from database: %s', $contentStreamLayers->toDebugString(), $succeedingSiblingAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716474854, $e); } if (!$succeedingSiblingRelation) { @@ -249,22 +255,22 @@ public function determineHierarchyRelationPosition( SELECT MAX(h.position) AS position FROM - {$this->tableNames->hierarchyRelation()} h + {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h WHERE h.parentnodeanchor = :anchorPoint - AND h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash AND h.position < :position SQL; try { $precedingSiblingData = $this->dbal->fetchAssociative($precedingSiblingStatement, [ 'anchorPoint' => $parentAnchorPoint->value, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'dimensionSpacePointHash' => $dimensionSpacePoint->hash, 'position' => $succeedingSiblingPosition + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load preceding sibling relations for content stream %s, anchor point %s and dimension space point %s from database: %s', $contentStreamDbId->value, $parentAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716474957, $e); + throw new \RuntimeException(sprintf('Failed to load preceding sibling relations for content stream %s, anchor point %s and dimension space point %s from database: %s', $contentStreamLayers->toDebugString(), $parentAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716474957, $e); } $precedingSiblingPosition = $precedingSiblingData ? ($precedingSiblingData['position'] ?? null) : null; if (!is_null($precedingSiblingPosition)) { @@ -282,21 +288,21 @@ public function determineHierarchyRelationPosition( SELECT h.parentnodeanchor FROM - {$this->tableNames->hierarchyRelation()} h + {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h WHERE h.childnodeanchor = :childAnchorPoint - AND h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash SQL; try { /** @var array $childHierarchyRelationData */ $childHierarchyRelationData = $this->dbal->fetchAssociative($childHierarchyRelationStatement, [ 'childAnchorPoint' => $childAnchorPoint->value, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'dimensionSpacePointHash' => $dimensionSpacePoint->hash + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load child hierarchy relation for content stream %s, anchor point %s and dimension space point %s from database: %s', $contentStreamDbId->value, $childAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716475001, $e); + throw new \RuntimeException(sprintf('Failed to load child hierarchy relation for content stream %s, anchor point %s and dimension space point %s from database: %s', $contentStreamLayers->toDebugString(), $childAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716475001, $e); } $parentAnchorPoint = NodeRelationAnchorPoint::fromInteger( $childHierarchyRelationData['parentnodeanchor'] @@ -306,20 +312,20 @@ public function determineHierarchyRelationPosition( SELECT MAX(h.position) AS position FROM - {$this->tableNames->hierarchyRelation()} h + {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h WHERE h.parentnodeanchor = :parentAnchorPoint - AND h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash SQL; try { $rightmostSucceedingSiblingRelationData = $this->dbal->fetchAssociative($rightmostSucceedingSiblingRelationStatement, [ 'parentAnchorPoint' => $parentAnchorPoint->value, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'dimensionSpacePointHash' => $dimensionSpacePoint->hash + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to right most succeeding relation for content stream %s, anchor point %s and dimension space point %s from database: %s', $contentStreamDbId->value, $parentAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716475046, $e); + throw new \RuntimeException(sprintf('Failed to right most succeeding relation for content stream %s, anchor point %s and dimension space point %s from database: %s', $contentStreamLayers->toDebugString(), $parentAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716475046, $e); } if ($rightmostSucceedingSiblingRelationData) { @@ -338,27 +344,27 @@ public function determineHierarchyRelationPosition( */ public function getOutgoingHierarchyRelationsForNodeAndSubgraph( NodeRelationAnchorPoint $parentAnchorPoint, - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, DimensionSpacePoint $dimensionSpacePoint ): array { $outgoingHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} h + {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h WHERE h.parentnodeanchor = :parentAnchorPoint - AND h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash SQL; try { $rows = $this->dbal->fetchAllAssociative($outgoingHierarchyRelationsStatement, [ 'parentAnchorPoint' => $parentAnchorPoint->value, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'dimensionSpacePointHash' => $dimensionSpacePoint->hash + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load outgoing hierarchy relations for content stream %s, parent anchor point %s and dimension space point %s from database: %s', $contentStreamDbId->value, $parentAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716475151, $e); + throw new \RuntimeException(sprintf('Failed to load outgoing hierarchy relations for content stream %s, parent anchor point %s and dimension space point %s from database: %s', $contentStreamLayers->toDebugString(), $parentAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716475151, $e); } return array_map($this->mapRawDataToHierarchyRelation(...), $rows); } @@ -368,27 +374,27 @@ public function getOutgoingHierarchyRelationsForNodeAndSubgraph( */ public function getIngoingHierarchyRelationsForNodeAndSubgraph( NodeRelationAnchorPoint $childAnchorPoint, - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, DimensionSpacePoint $dimensionSpacePoint ): array { $ingoingHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} h + {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql()} h WHERE h.childnodeanchor = :childAnchorPoint - AND h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash = :dimensionSpacePointHash SQL; try { $rows = $this->dbal->fetchAllAssociative($ingoingHierarchyRelationsStatement, [ 'childAnchorPoint' => $childAnchorPoint->value, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'dimensionSpacePointHash' => $dimensionSpacePoint->hash + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load ingoing hierarchy relations for content stream %s, child anchor point %s and dimension space point %s from database: %s', $contentStreamDbId->value, $childAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716475151, $e); + throw new \RuntimeException(sprintf('Failed to load ingoing hierarchy relations for content stream %s, child anchor point %s and dimension space point %s from database: %s', $contentStreamLayers->toDebugString(), $childAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716475151, $e); } return array_map($this->mapRawDataToHierarchyRelation(...), $rows); } @@ -398,23 +404,24 @@ public function getIngoingHierarchyRelationsForNodeAndSubgraph( */ public function findIngoingHierarchyRelationsForNode( NodeRelationAnchorPoint $childAnchorPoint, - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, ?DimensionSpacePointSet $restrictToSet = null ): array { $ingoingHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} h + {$this->hierarchyRelationStatement->toSql()} h WHERE h.childnodeanchor = :childAnchorPoint - AND h.contentstreamdbid = :contentStreamDbId SQL; $parameters = [ 'childAnchorPoint' => $childAnchorPoint->value, - 'contentStreamDbId' => $contentStreamDbId->value + 'contentStreamLayers' => $contentStreamLayers->toIntArray() + ]; + $types = [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]; - $types = []; if ($restrictToSet) { $ingoingHierarchyRelationsStatement .= ' AND h.dimensionspacepointhash IN (:dimensionSpacePointHashes)'; @@ -424,7 +431,7 @@ public function findIngoingHierarchyRelationsForNode( try { $rows = $this->dbal->fetchAllAssociative($ingoingHierarchyRelationsStatement, $parameters, $types); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load ingoing hierarchy relations for content stream %s, child anchor point %s and dimension space points %s from database: %s', $contentStreamDbId->value, $childAnchorPoint->value, $restrictToSet?->toJson() ?? '[any]', $e->getMessage()), 1716476299, $e); + throw new \RuntimeException(sprintf('Failed to load ingoing hierarchy relations for content stream %s, child anchor point %s and dimension space points %s from database: %s', $contentStreamLayers->toDebugString(), $childAnchorPoint->value, $restrictToSet?->toJson() ?? '[any]', $e->getMessage()), 1716476299, $e); } $relations = []; foreach ($rows as $row) { @@ -438,23 +445,24 @@ public function findIngoingHierarchyRelationsForNode( */ public function findOutgoingHierarchyRelationsForNode( NodeRelationAnchorPoint $parentAnchorPoint, - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, ?DimensionSpacePointSet $restrictToSet = null ): array { $outgoingHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} h + {$this->hierarchyRelationStatement->toSql()} h WHERE h.parentnodeanchor = :parentAnchorPoint - AND h.contentstreamdbid = :contentStreamDbId SQL; $parameters = [ 'parentAnchorPoint' => $parentAnchorPoint->value, - 'contentStreamDbId' => $contentStreamDbId->value + 'contentStreamLayers' => $contentStreamLayers->toIntArray() + ]; + $types = [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]; - $types = []; if ($restrictToSet) { $outgoingHierarchyRelationsStatement .= ' AND h.dimensionspacepointhash IN (:dimensionSpacePointHashes)'; @@ -464,7 +472,7 @@ public function findOutgoingHierarchyRelationsForNode( try { $rows = $this->dbal->fetchAllAssociative($outgoingHierarchyRelationsStatement, $parameters, $types); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load outgoing hierarchy relations for content stream %s, parent anchor point %s and dimension space points %s from database: %s', $contentStreamDbId->value, $parentAnchorPoint->value, $restrictToSet?->toJson() ?? '[any]', $e->getMessage()), 1716476573, $e); + throw new \RuntimeException(sprintf('Failed to load outgoing hierarchy relations for content stream %s, parent anchor point %s and dimension space points %s from database: %s', $contentStreamLayers->toDebugString(), $parentAnchorPoint->value, $restrictToSet?->toJson() ?? '[any]', $e->getMessage()), 1716476573, $e); } $relations = []; foreach ($rows as $row) { @@ -477,7 +485,7 @@ public function findOutgoingHierarchyRelationsForNode( * @return array */ public function findOutgoingHierarchyRelationsForNodeAggregate( - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $dimensionSpacePointSet ): array { @@ -485,23 +493,22 @@ public function findOutgoingHierarchyRelationsForNodeAggregate( SELECT h.* FROM - {$this->tableNames->hierarchyRelation()} h + {$this->hierarchyRelationStatement->where('h.dimensionspacepointhash IN (:dimensionSpacePointHashes)')->toSql()} h INNER JOIN {$this->tableNames->node()} n ON h.parentnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId - AND h.contentstreamdbid = :contentStreamDbId - AND h.dimensionspacepointhash IN (:dimensionSpacePointHashes) SQL; try { $rows = $this->dbal->fetchAllAssociative($outgoingHierarchyRelationsStatement, [ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'dimensionSpacePointHashes' => $dimensionSpacePointSet->getPointHashes() ], [ - 'dimensionSpacePointHashes' => ArrayParameterType::STRING + 'dimensionSpacePointHashes' => ArrayParameterType::STRING, + 'contentStreamLayers' => ArrayParameterType::INTEGER, ]); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load outgoing hierarchy relations for content stream %s, node aggregate id %s and dimension space points %s from database: %s', $contentStreamDbId->value, $nodeAggregateId->value, $dimensionSpacePointSet->toJson(), $e->getMessage()), 1716476690, $e); + throw new \RuntimeException(sprintf('Failed to load outgoing hierarchy relations for content stream %s, node aggregate id %s and dimension space points %s from database: %s', $contentStreamLayers->toDebugString(), $nodeAggregateId->value, $dimensionSpacePointSet->toJson(), $e->getMessage()), 1716476690, $e); } return array_map($this->mapRawDataToHierarchyRelation(...), $rows); } @@ -510,7 +517,7 @@ public function findOutgoingHierarchyRelationsForNodeAggregate( * @return array */ public function findIngoingHierarchyRelationsForNodeAggregate( - ContentStreamDbId $contentStreamDbId, + ContentStreamLayers $contentStreamLayers, NodeAggregateId $nodeAggregateId, ?DimensionSpacePointSet $dimensionSpacePointSet = null ): array { @@ -518,52 +525,47 @@ public function findIngoingHierarchyRelationsForNodeAggregate( SELECT h.* FROM - {$this->tableNames->hierarchyRelation()} h + {$this->hierarchyRelationStatement->where($dimensionSpacePointSet !== null ? 'h.dimensionspacepointhash IN (:dimensionSpacePointHashes)' : '')->toSql()} h INNER JOIN {$this->tableNames->node()} n ON h.childnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId - AND h.contentstreamdbid = :contentStreamDbId SQL; $parameters = [ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), + ...($dimensionSpacePointSet !== null ? ['dimensionSpacePointHashes' => $dimensionSpacePointSet->getPointHashes()] : []), + ]; + $types = [ + 'contentStreamLayers' => ArrayParameterType::INTEGER, + ...($dimensionSpacePointSet !== null ? ['dimensionSpacePointHashes' => ArrayParameterType::STRING] : []), ]; - $types = []; - if ($dimensionSpacePointSet !== null) { - $ingoingHierarchyRelationsStatement .= ' AND h.dimensionspacepointhash IN (:dimensionSpacePointHashes)'; - $parameters['dimensionSpacePointHashes'] = $dimensionSpacePointSet->getPointHashes(); - $types['dimensionSpacePointHashes'] = ArrayParameterType::STRING; - } try { $rows = $this->dbal->fetchAllAssociative($ingoingHierarchyRelationsStatement, $parameters, $types); } catch (DBALException $e) { - throw new \RuntimeException(sprintf('Failed to load ingoing hierarchy relations for content stream %s, node aggregate id %s and dimension space points %s from database: %s', $contentStreamDbId->value, $nodeAggregateId->value, $dimensionSpacePointSet?->toJson() ?? '[any]', $e->getMessage()), 1716476743, $e); + throw new \RuntimeException(sprintf('Failed to load ingoing hierarchy relations for content stream %s, node aggregate id %s and dimension space points %s from database: %s', $contentStreamLayers->toDebugString(), $nodeAggregateId->value, $dimensionSpacePointSet?->toJson() ?? '[any]', $e->getMessage()), 1716476743, $e); } return array_map($this->mapRawDataToHierarchyRelation(...), $rows); } - /** - * @return array - */ - public function getAllContentStreamDbIdsAnchorPointIsContainedIn( + public function getAllContentStreamLayersAnchorPointIsContainedIn( NodeRelationAnchorPoint $nodeRelationAnchorPoint - ): array { - $contentStreamDbIdsStatement = <<tableNames->hierarchyRelation()} h WHERE h.childnodeanchor = :nodeRelationAnchorPoint SQL; try { - $contentStreamDbIds = $this->dbal->fetchFirstColumn($contentStreamDbIdsStatement, [ + $contentStreamLayers = $this->dbal->fetchFirstColumn($contentStreamLayersStatement, [ 'nodeRelationAnchorPoint' => $nodeRelationAnchorPoint->value, ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load content stream ids for relation anchor point %s from database: %s', $nodeRelationAnchorPoint->value, $e->getMessage()), 1716478504, $e); } - return array_map(ContentStreamDbId::fromInt(...), $contentStreamDbIds); + return ContentStreamLayers::fromArray($contentStreamLayers); } /** @@ -588,9 +590,10 @@ private function mapRawDataToHierarchyRelation(array $rawData): HierarchyRelatio } return new HierarchyRelation( + HierarchyRelationId::fromInt((int)$rawData['id']), + ContentStreamLayer::fromInt((int)$rawData['contentstreamlayer']), NodeRelationAnchorPoint::fromInteger((int)$rawData['parentnodeanchor']), NodeRelationAnchorPoint::fromInteger((int)$rawData['childnodeanchor']), - ContentStreamDbId::fromInt((int)$rawData['contentstreamdbid']), DimensionSpacePoint::fromJsonString($dimensionSpacePointJson), $rawData['dimensionspacepointhash'], (int)$rawData['position'], diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/HierarchyRelationStatement.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/HierarchyRelationStatement.php new file mode 100644 index 00000000000..f8e16348584 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/HierarchyRelationStatement.php @@ -0,0 +1,59 @@ + $whereClauses + */ + private function __construct( + private ContentGraphTableNames $tableNames, + private array $whereClauses + ) { + } + + public static function for(ContentGraphTableNames $tableNames): self + { + return new self($tableNames, []); + } + + public function where(string $where): self + { + return new self( + tableNames: $this->tableNames, + whereClauses: $where === '' ? [] : [$where], + ); + } + + public function andWhere(string $where): self + { + return new self( + tableNames: $this->tableNames, + whereClauses: [...$this->whereClauses, ...($where === '' ? [] : [$where])], + ); + } + + public function toSql(): string + { + $additionalWhereClauses = $this->whereClauses === [] ? '' : sprintf(" WHERE %s\n", join("\n AND ", $this->whereClauses)); + + return <<tableNames->hierarchyRelation()} as h + INNER JOIN ( + SELECT id, MAX(contentstreamlayer) as contentstreamlayer + FROM {$this->tableNames->hierarchyRelation()} + WHERE (contentstreamlayer IN (:contentStreamLayers)) + GROUP BY id + ) AS activeLayer + ON h.id = activeLayer.id AND h.contentstreamlayer = activeLayer.contentstreamlayer + {$additionalWhereClauses}) + SQL; + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php index 5b83602c81e..14da8af4223 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php @@ -7,7 +7,7 @@ use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Query\QueryBuilder; -use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamLayers; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindRootNodeAggregatesFilter; @@ -35,46 +35,51 @@ */ final readonly class NodeQueryBuilder { + private HierarchyRelationStatement $hierarchyRelationStatement; + public function __construct( private Connection $connection, public ContentGraphTableNames $tableNames ) { + $this->hierarchyRelationStatement = HierarchyRelationStatement::for($this->tableNames); } public function buildBasicNodeAggregateQuery(): QueryBuilder { return $this->createQueryBuilder() - ->select('n.*, h.contentstreamdbid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamlayer, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNames->node(), 'n') - ->innerJoin('n', $this->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') - ->innerJoin('h', $this->tableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->where('h.contentstreamdbid = :contentStreamDbId'); + ->innerJoin('n', $this->hierarchyRelationStatement->toSql(), 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->innerJoin('h', $this->tableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash'); } - public function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamDbId $contentStreamDbId): QueryBuilder + public function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamLayers $contentStreamLayers): QueryBuilder { return $this->createQueryBuilder() - ->select('cn.*, ch.contentstreamdbid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('cn.*, ch.contentstreamlayer, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNames->node(), 'pn') - ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->hierarchyRelationStatement->toSql(), 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('ch', $this->tableNames->dimensionSpacePoints(), 'cdsp', 'cdsp.hash = ch.dimensionspacepointhash') ->innerJoin('ch', $this->tableNames->node(), 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') ->where('pn.nodeaggregateid = :parentNodeAggregateId') - ->andWhere('ch.contentstreamdbid = :contentStreamDbId') ->orderBy('ch.position') ->setParameters([ 'parentNodeAggregateId' => $parentNodeAggregateId->value, - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); } - public function buildFindRootNodeAggregatesQuery(ContentStreamDbId $contentStreamDbId, FindRootNodeAggregatesFilter $filter): QueryBuilder + public function buildFindRootNodeAggregatesQuery(ContentStreamLayers $contentStreamLayers, FindRootNodeAggregatesFilter $filter): QueryBuilder { $queryBuilder = $this->buildBasicNodeAggregateQuery() ->andWhere('h.parentnodeanchor = :rootEdgeParentAnchorId') ->setParameters([ - 'contentStreamDbId' => $contentStreamDbId->value, + 'contentStreamLayers' => $contentStreamLayers->toIntArray(), 'rootEdgeParentAnchorId' => NodeRelationAnchorPoint::forRootEdge()->value, + ], [ + 'contentStreamLayers' => ArrayParameterType::INTEGER ]); if ($filter->nodeTypeName !== null) { @@ -84,68 +89,65 @@ public function buildFindRootNodeAggregatesQuery(ContentStreamDbId $contentStrea return $queryBuilder; } - public function buildBasicNodeQuery(ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint, string $nodeTableAlias = 'n', string $select = 'n.*, h.subtreetags'): QueryBuilder + public function buildBasicNodeQuery(ContentStreamLayers $contentStreamLayers, DimensionSpacePoint $dimensionSpacePoint, string $nodeTableAlias = 'n', string $select = 'n.*, h.subtreetags'): QueryBuilder { return $this->createQueryBuilder() ->select($select) ->from($this->tableNames->node(), $nodeTableAlias) - ->innerJoin($nodeTableAlias, $this->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = ' . $nodeTableAlias . '.relationanchorpoint') - ->where('h.contentstreamdbid = :contentStreamDbId')->setParameter('contentStreamDbId', $contentStreamDbId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash); + ->innerJoin($nodeTableAlias, $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'h', 'h.childnodeanchor = ' . $nodeTableAlias . '.relationanchorpoint') + ->setParameter('contentStreamLayers', $contentStreamLayers->toIntArray(), ArrayParameterType::INTEGER) + ->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash); } - public function buildBasicChildNodesQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + public function buildBasicChildNodesQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamLayers $contentStreamLayers, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder { return $this->createQueryBuilder() ->select('n.*, h.subtreetags') ->from($this->tableNames->node(), 'pn') - ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'h', 'h.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNames->node(), 'n', 'h.childnodeanchor = n.relationanchorpoint') ->where('pn.nodeaggregateid = :parentNodeAggregateId')->setParameter('parentNodeAggregateId', $parentNodeAggregateId->value) - ->andWhere('h.contentstreamdbid = :contentStreamDbId')->setParameter('contentStreamDbId', $contentStreamDbId->value) - ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash); + ->setParameter('contentStreamLayers', $contentStreamLayers->toIntArray(), ArrayParameterType::INTEGER) + ->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash); } - public function buildBasicParentNodeQuery(NodeAggregateId $childNodeAggregateId, ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + public function buildBasicParentNodeQuery(NodeAggregateId $childNodeAggregateId, ContentStreamLayers $contentStreamLayers, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder { return $this->createQueryBuilder() ->select('pn.*, ch.subtreetags') ->from($this->tableNames->node(), 'pn') - ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ph', 'ph.parentnodeanchor = pn.relationanchorpoint') + // todo we calculate hierarchy rows twice -> optimise + ->innerJoin('pn', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'ph', 'ph.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNames->node(), 'cn', 'cn.relationanchorpoint = ph.childnodeanchor') - ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ch', 'ch.childnodeanchor = pn.relationanchorpoint') + ->innerJoin('pn', $this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'ch', 'ch.childnodeanchor = pn.relationanchorpoint') ->where('cn.nodeaggregateid = :childNodeAggregateId')->setParameter('childNodeAggregateId', $childNodeAggregateId->value) - ->andWhere('ph.contentstreamdbid = :contentStreamDbId')->setParameter('contentStreamDbId', $contentStreamDbId->value) - ->andWhere('ch.contentstreamdbid = :contentStreamDbId') - ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash) - ->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash'); + ->setParameter('contentStreamLayers', $contentStreamLayers->toIntArray(), ArrayParameterType::INTEGER) + ->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash); } - public function buildBasicNodeSiblingsQuery(bool $preceding, NodeAggregateId $siblingNodeAggregateId, ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + public function buildBasicNodeSiblingsQuery(bool $preceding, NodeAggregateId $siblingNodeAggregateId, ContentStreamLayers $contentStreamLayers, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder { $sharedSubQuery = $this->createQueryBuilder() - ->from($this->tableNames->hierarchyRelation(), 'sh') + ->from($this->hierarchyRelationStatement->where('h.dimensionspacepointhash = :dimensionSpacePointHash')->toSql(), 'sh') ->innerJoin('sh', $this->tableNames->node(), 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') - ->where('sn.nodeaggregateid = :siblingNodeAggregateId') - ->andWhere('sh.contentstreamdbid = :contentStreamDbId') - ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash'); + ->where('sn.nodeaggregateid = :siblingNodeAggregateId'); $parentNodeAnchorSubQuery = (clone $sharedSubQuery)->select('sh.parentnodeanchor'); $siblingPositionSubQuery = (clone $sharedSubQuery)->select('sh.position'); - return $this->buildBasicNodeQuery($contentStreamDbId, $dimensionSpacePoint) + return $this->buildBasicNodeQuery($contentStreamLayers, $dimensionSpacePoint) ->andWhere('h.parentnodeanchor = (' . $parentNodeAnchorSubQuery->getSQL() . ')') ->andWhere('n.nodeaggregateid != :siblingNodeAggregateId')->setParameter('siblingNodeAggregateId', $siblingNodeAggregateId->value) ->andWhere('h.position ' . ($preceding ? '<' : '>') . ' (' . $siblingPositionSubQuery->getSQL() . ')') ->orderBy('h.position', $preceding ? 'DESC' : 'ASC'); } - public function buildBasicNodesCteQuery(NodeAggregateId $entryNodeAggregateId, ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint, string $cteName = 'ancestry', string $cteAlias = 'pn'): QueryBuilder + public function buildBasicNodesCteQuery(NodeAggregateId $entryNodeAggregateId, ContentStreamLayers $contentStreamLayers, DimensionSpacePoint $dimensionSpacePoint, string $cteName = 'ancestry', string $cteAlias = 'pn'): QueryBuilder { return $this->createQueryBuilder() ->select('*') ->from($cteName, $cteAlias) - ->setParameter('contentStreamDbId', $contentStreamDbId->value) + ->setParameter('contentStreamLayers', $contentStreamLayers->toIntArray(), ArrayParameterType::INTEGER) ->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash) ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); } diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/NodeReferencesOnForkContentStream.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/NodeReferencesOnForkContentStream.feature index 7049a063589..e8074cb2647 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/NodeReferencesOnForkContentStream.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/NodeReferencesOnForkContentStream.feature @@ -76,7 +76,7 @@ Feature: On forking a content stream, node references should be copied as well. And the command SetNodeProperties is executed with payload: | Key | Value | | nodeAggregateId | "source-nodandaise" | - | propertyValues | {"text": "Modified in live workspace"} | + | propertyValues | {"text": "Modified in user workspace"} | Then I expect node aggregate identifier "source-nodandaise" to lead to node user-cs-identifier;source-nodandaise;{"language": "de"} And I expect this node to have the following references: | Name | Node | Properties |