From 65ebd0e437f30e6226826787f5e91b4840d6f97d Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:04:12 +0100 Subject: [PATCH 1/7] FEATURE: Introduce internal `ContentStreamDbId`(ReadSide) --- .../src/ContentGraphReadModelAdapter.php | 12 ++-- .../Domain/Projection/ContentStreamDbId.php | 25 ++++++++ .../src/Domain/Repository/ContentGraph.php | 52 ++++++++-------- .../src/Domain/Repository/ContentSubgraph.php | 60 +++++++++---------- 4 files changed, 90 insertions(+), 59 deletions(-) create mode 100644 Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ContentStreamDbId.php diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphReadModelAdapter.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphReadModelAdapter.php index 8a0f6de9359..2bef2ecc699 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphReadModelAdapter.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/ContentGraphReadModelAdapter.php @@ -17,6 +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\Repository\ContentGraph; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; @@ -49,11 +50,13 @@ public function getContentGraph(WorkspaceName $workspaceName): ContentGraph { $currentContentStreamIdStatement = <<tableNames->workspace()} + {$this->tableNames->workspace()} ws + JOIN {$this->tableNames->contentStream()} cs ON cs.id = ws.currentContentStreamId WHERE - name = :workspaceName + ws.name = :workspaceName LIMIT 1 SQL; try { @@ -67,7 +70,8 @@ public function getContentGraph(WorkspaceName $workspaceName): ContentGraph throw WorkspaceDoesNotExist::butWasSupposedTo($workspaceName); } $currentContentStreamId = ContentStreamId::fromString($row['currentContentStreamId']); - return new ContentGraph($this->dbal, $this->nodeFactory, $this->contentRepositoryId, $this->nodeTypeManager, $this->tableNames, $workspaceName, $currentContentStreamId); + $contentStreamDbId = ContentStreamDbId::fromInt((int)$row['contentStreamDbId']); + return new ContentGraph($this->dbal, $this->nodeFactory, $this->contentRepositoryId, $this->nodeTypeManager, $this->tableNames, $workspaceName, $currentContentStreamId, $contentStreamDbId); } public function findWorkspaceByName(WorkspaceName $workspaceName): ?Workspace diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ContentStreamDbId.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ContentStreamDbId.php new file mode 100644 index 00000000000..8efc1ef1499 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ContentStreamDbId.php @@ -0,0 +1,25 @@ +nodeQueryBuilder = new NodeQueryBuilder($this->dbal, $this->tableNames); } @@ -96,7 +98,7 @@ public function getSubgraph( return new ContentSubgraph( $this->contentRepositoryId, $this->workspaceName, - $this->contentStreamId, + $this->contentStreamDbId, $dimensionSpacePoint, $visibilityConstraints, $this->dbal, @@ -135,7 +137,7 @@ public function findRootNodeAggregateByType( public function findRootNodeAggregates( FindRootNodeAggregatesFilter $filter, ): NodeAggregates { - $rootNodeAggregateQueryBuilder = $this->nodeQueryBuilder->buildFindRootNodeAggregatesQuery($this->contentStreamId, $filter); + $rootNodeAggregateQueryBuilder = $this->nodeQueryBuilder->buildFindRootNodeAggregatesQuery($this->contentStreamDbId, $filter); return $this->mapQueryBuilderToNodeAggregates($rootNodeAggregateQueryBuilder); } @@ -146,7 +148,7 @@ public function findNodeAggregatesByType( $queryBuilder ->andWhere('n.nodetypename = :nodeTypeName') ->setParameters([ - 'contentStreamId' => $this->contentStreamId->value, + 'contentStreamDbId' => $this->contentStreamDbId->value, 'nodeTypeName' => $nodeTypeName->value, ]); return $this->mapQueryBuilderToNodeAggregates($queryBuilder); @@ -160,7 +162,7 @@ public function findNodeAggregateById( ->orderBy('n.relationanchorpoint', 'DESC') ->setParameters([ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamId' => $this->contentStreamId->value + 'contentStreamDbId' => $this->contentStreamDbId->value ]); return $this->nodeFactory->mapNodeRowsToNodeAggregate( @@ -178,7 +180,7 @@ public function findNodeAggregatesByIds( ->orderBy('n.relationanchorpoint', 'DESC') ->setParameters([ 'nodeAggregateIds' => $nodeAggregateIds->toStringArray(), - 'contentStreamId' => $this->contentStreamId->value + 'contentStreamDbId' => $this->contentStreamDbId->value ], [ 'nodeAggregateIds' => ArrayParameterType::STRING ]); @@ -197,11 +199,11 @@ public function findParentNodeAggregates( $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeAggregateQuery() ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = n.relationanchorpoint') ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') - ->andWhere('ch.contentstreamid = :contentStreamId') + ->andWhere('ch.contentstreamdbid = :contentStreamDbId') ->andWhere('cn.nodeaggregateid = :nodeAggregateId') ->setParameters([ 'nodeAggregateId' => $childNodeAggregateId->value, - 'contentStreamId' => $this->contentStreamId->value + 'contentStreamDbId' => $this->contentStreamDbId->value ]); return $this->mapQueryBuilderToNodeAggregates($queryBuilder); @@ -213,20 +215,20 @@ public function findAncestorNodeAggregateIds(NodeAggregateId $entryNodeAggregate ->select('ch.parentnodeanchor') ->from($this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ch') ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'c', 'c.relationanchorpoint = ch.childnodeanchor') - ->where('ch.contentstreamid = :contentStreamId') + ->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.contentstreamid = :contentStreamId'); + ->where('ph.contentstreamdbid = :contentStreamDbId'); $queryBuilderCte = $this->createQueryBuilder() ->select('n.nodeAggregateId') ->from('ancestry', 'a') ->innerJoin('a', $this->nodeQueryBuilder->tableNames->node(), 'n', 'n.relationanchorpoint = a.parentnodeanchor') - ->setParameter('contentStreamId', $this->contentStreamId->value) + ->setParameter('contentStreamDbId', $this->contentStreamDbId->value) ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); $nodeAggregateIdRows = $this->fetchCteResults( @@ -242,7 +244,7 @@ public function findAncestorNodeAggregateIds(NodeAggregateId $entryNodeAggregate public function findChildNodeAggregates( NodeAggregateId $parentNodeAggregateId ): NodeAggregates { - $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamId); + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamDbId); return $this->mapQueryBuilderToNodeAggregates($queryBuilder); } @@ -253,20 +255,20 @@ public function findParentNodeAggregateByChildOriginDimensionSpacePoint(NodeAggr ->from($this->nodeQueryBuilder->tableNames->node(), 'pn') ->innerJoin('pn', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('ch', $this->nodeQueryBuilder->tableNames->node(), 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') - ->where('ch.contentstreamid = :contentStreamId') + ->where('ch.contentstreamdbid = :contentStreamDbId') ->andWhere('ch.dimensionspacepointhash = :childOriginDimensionSpacePointHash') ->andWhere('cn.nodeaggregateid = :childNodeAggregateId') ->andWhere('cn.origindimensionspacepointhash = :childOriginDimensionSpacePointHash'); $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamdbid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->nodeQueryBuilder->tableNames->node(), 'n') ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint') ->innerJoin('h', $this->nodeQueryBuilder->tableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') ->where('n.nodeaggregateid = (' . $subQueryBuilder->getSQL() . ')') - ->andWhere('h.contentstreamid = :contentStreamId') + ->andWhere('h.contentstreamdbid = :contentStreamDbId') ->setParameters([ - 'contentStreamId' => $this->contentStreamId->value, + 'contentStreamDbId' => $this->contentStreamDbId->value, 'childNodeAggregateId' => $childNodeAggregateId->value, 'childOriginDimensionSpacePointHash' => $childOriginDimensionSpacePoint->hash, ]); @@ -280,7 +282,7 @@ public function findParentNodeAggregateByChildOriginDimensionSpacePoint(NodeAggr public function findTetheredChildNodeAggregates(NodeAggregateId $parentNodeAggregateId): NodeAggregates { - $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamId) + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamDbId) ->andWhere('cn.classification = :tetheredClassification') ->setParameter('tetheredClassification', NodeAggregateClassification::CLASSIFICATION_TETHERED->value); @@ -291,7 +293,7 @@ public function findChildNodeAggregateByName( NodeAggregateId $parentNodeAggregateId, NodeName $name ): ?NodeAggregate { - $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamId) + $queryBuilder = $this->nodeQueryBuilder->buildChildNodeAggregateQuery($parentNodeAggregateId, $this->contentStreamDbId) ->andWhere('cn.name = :relationName') ->setParameter('relationName', $name->value); @@ -308,14 +310,14 @@ public function getDimensionSpacePointsOccupiedByChildNodeName(NodeName $nodeNam ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'ph', 'ph.childnodeanchor = n.relationanchorpoint') ->where('n.nodeaggregateid = :parentNodeAggregateId') ->andWhere('n.origindimensionspacepointhash = :parentNodeOriginDimensionSpacePointHash') - ->andWhere('ph.contentstreamid = :contentStreamId') - ->andWhere('h.contentstreamid = :contentStreamId') + ->andWhere('ph.contentstreamdbid = :contentStreamDbId') + ->andWhere('h.contentstreamdbid = :contentStreamDbId') ->andWhere('h.dimensionspacepointhash IN (:dimensionSpacePointHashes)') ->andWhere('n.name = :nodeName') ->setParameters([ 'parentNodeAggregateId' => $parentNodeAggregateId->value, 'parentNodeOriginDimensionSpacePointHash' => $parentNodeOriginDimensionSpacePoint->hash, - 'contentStreamId' => $this->contentStreamId->value, + 'contentStreamDbId' => $this->contentStreamDbId->value, 'dimensionSpacePointHashes' => $dimensionSpacePointsToCheck->getPointHashes(), 'nodeName' => $nodeName->value ], [ @@ -332,19 +334,19 @@ public function getDimensionSpacePointsOccupiedByChildNodeName(NodeName $nodeNam public function findNodeAggregatesTaggedBy(SubtreeTag $subtreeTag): NodeAggregates { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamdbid, 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') ->innerJoin('h', $this->tableNames->node(), 'n', 'h.childnodeanchor = n.relationanchorpoint') ->innerJoin('h', $this->tableNames->dimensionSpacePoints(), 'dsp', 'dsp.hash = h.dimensionspacepointhash') - ->where('th.contentstreamid = :contentStreamId') + ->where('th.contentstreamdbid = :contentStreamDbId') ->andWhere('JSON_EXTRACT(th.subtreetags, :tagPath) LIKE "true"') - ->andWhere('h.contentstreamid = :contentStreamId') + ->andWhere('h.contentstreamdbid = :contentStreamDbId') ->orderBy('n.relationanchorpoint', 'DESC') ->setParameters([ 'tagPath' => '$."' . $subtreeTag->value . '"', - 'contentStreamId' => $this->contentStreamId->value + 'contentStreamDbId' => $this->contentStreamDbId->value ]); return $this->mapQueryBuilderToNodeAggregates($queryBuilder); diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php index 45cef04e97a..f65c9dd553e 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php @@ -20,6 +20,7 @@ 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\NodeQueryBuilder; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; @@ -58,7 +59,6 @@ use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\PropertyName; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; /** @@ -95,7 +95,7 @@ final class ContentSubgraph implements ContentSubgraphInterface public function __construct( private readonly ContentRepositoryId $contentRepositoryId, private readonly WorkspaceName $workspaceName, - private readonly ContentStreamId $contentStreamId, + private readonly ContentStreamDbId $contentStreamDbId, private readonly DimensionSpacePoint $dimensionSpacePoint, private readonly VisibilityConstraints $visibilityConstraints, private readonly Connection $dbal, @@ -169,7 +169,7 @@ public function countBackReferences(NodeAggregateId $nodeAggregateId, CountBackR public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node { - $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamId, $this->dimensionSpacePoint) + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamDbId, $this->dimensionSpacePoint) ->andWhere('n.nodeaggregateid = :nodeAggregateId') ->setParameter('nodeAggregateId', $nodeAggregateId->value); $this->addSubtreeTagConstraints($queryBuilder); @@ -178,7 +178,7 @@ public function findNodeById(NodeAggregateId $nodeAggregateId): ?Node public function findNodesByIds(NodeAggregateIds $nodeAggregateIds): Nodes { - $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamId, $this->dimensionSpacePoint) + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamDbId, $this->dimensionSpacePoint) ->andWhere('n.nodeaggregateid in (:nodeAggregateIds)') ->setParameter('nodeAggregateIds', $nodeAggregateIds->toStringArray(), ArrayParameterType::STRING); $this->addSubtreeTagConstraints($queryBuilder); @@ -187,7 +187,7 @@ public function findNodesByIds(NodeAggregateIds $nodeAggregateIds): Nodes public function findRootNodeByType(NodeTypeName $nodeTypeName): ?Node { - $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamId, $this->dimensionSpacePoint) + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamDbId, $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 +196,7 @@ public function findRootNodeByType(NodeTypeName $nodeTypeName): ?Node public function findParentNode(NodeAggregateId $childNodeAggregateId): ?Node { - $queryBuilder = $this->nodeQueryBuilder->buildBasicParentNodeQuery($childNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); + $queryBuilder = $this->nodeQueryBuilder->buildBasicParentNodeQuery($childNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint); $this->addSubtreeTagConstraints($queryBuilder, 'ph'); return $this->fetchNode($queryBuilder); } @@ -228,7 +228,7 @@ public function findNodeByAbsolutePath(AbsoluteNodePath $path): ?Node */ private function findChildNodeConnectedThroughEdgeName(NodeAggregateId $parentNodeAggregateId, NodeName $nodeName): ?Node { - $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint) + $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint) ->andWhere('n.name = :edgeName')->setParameter('edgeName', $nodeName->value); $this->addSubtreeTagConstraints($queryBuilder); return $this->fetchNode($queryBuilder); @@ -276,7 +276,7 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi ->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.contentstreamid = :contentStreamId') + ->where('h.contentstreamdbid = :contentStreamDbId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('n.nodeaggregateid = :entryNodeAggregateId'); $this->addSubtreeTagConstraints($queryBuilderInitial); @@ -286,7 +286,7 @@ public function findSubtree(NodeAggregateId $entryNodeAggregateId, FindSubtreeFi ->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.contentstreamid = :contentStreamId') + ->where('h.contentstreamdbid = :contentStreamDbId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); 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('contentStreamId', $this->contentStreamId->value) + ->setParameter('contentStreamDbId', $this->contentStreamDbId->value) ->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); @@ -380,7 +380,7 @@ public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClose ->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.contentstreamid = :contentStreamId') + ->andWhere('ph.contentstreamdbid = :contentStreamDbId') ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('n.nodeaggregateid = :entryNodeAggregateId'); $this->addSubtreeTagConstraints($queryBuilderInitial, 'ph'); @@ -390,11 +390,11 @@ public function findClosestNode(NodeAggregateId $entryNodeAggregateId, FindClose ->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.contentstreamid = :contentStreamId') + ->where('h.contentstreamdbid = :contentStreamDbId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); $this->addSubtreeTagConstraints($queryBuilderRecursive); - $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); + $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint); $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), 'pn'); $nodeRows = $this->fetchCteResults( $queryBuilderInitial, @@ -437,7 +437,7 @@ public function countDescendantNodes(NodeAggregateId $entryNodeAggregateId, Coun public function countNodes(): int { - $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamId, $this->dimensionSpacePoint, 'n', 'COUNT(*)'); + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeQuery($this->contentStreamDbId, $this->dimensionSpacePoint, 'n', 'COUNT(*)'); try { $result = $this->executeQuery($queryBuilder)->fetchOne(); } catch (DBALException $e) { @@ -483,7 +483,7 @@ private function addSubtreeTagConstraints(QueryBuilder $queryBuilder, string $hi private function buildChildNodesQuery(NodeAggregateId $parentNodeAggregateId, FindChildNodesFilter|CountChildNodesFilter $filter): QueryBuilder { - $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); + $queryBuilder = $this->nodeQueryBuilder->buildBasicChildNodesQuery($parentNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint); if ($filter->nodeTypes !== null) { $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); } @@ -501,7 +501,7 @@ private function buildReferencesQuery(NodeAggregateId $nodeAggregateId, FindRefe { $subselectParameters = [ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamId' => $this->contentStreamId->value, + 'contentStreamDbId' => $this->contentStreamDbId->value, 'dimensionSpacePointHash' => $this->dimensionSpacePoint->hash, ]; $subtreeTagConstraints = ''; @@ -521,12 +521,12 @@ private function buildReferencesQuery(NodeAggregateId $nodeAggregateId, FindRefe 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.contentstreamid = :contentStreamId + AND sh.contentstreamdbid = :contentStreamDbId AND sh.dimensionspacepointhash = :dimensionSpacePointHash ' . $subtreeTagConstraints . ' )')->setParameters($subselectParameters) ->andWhere('dh.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->andWhere('dh.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value); + ->andWhere('dh.contentstreamdbid = :contentStreamDbId')->setParameter('contentStreamDbId', $this->contentStreamDbId->value); $this->addSubtreeTagConstraints($queryBuilder, 'dh'); if ($filter->nodeTypes !== null) { $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), "dn"); @@ -565,7 +565,7 @@ private function buildBackreferencesQuery(NodeAggregateId $nodeAggregateId, Find { $subselectParameters = [ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamId' => $this->contentStreamId->value, + 'contentStreamDbId' => $this->contentStreamDbId->value, 'dimensionSpacePointHash' => $this->dimensionSpacePoint->hash, ]; $subtreeTagConstraints = ''; @@ -585,12 +585,12 @@ private function buildBackreferencesQuery(NodeAggregateId $nodeAggregateId, Find 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.contentstreamid = :contentStreamId + AND dh.contentstreamdbid = :contentStreamDbId AND dh.dimensionspacepointhash = :dimensionSpacePointHash ' . $subtreeTagConstraints . ' )')->setParameters($subselectParameters) ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $this->dimensionSpacePoint->hash) - ->andWhere('sh.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $this->contentStreamId->value); + ->andWhere('sh.contentstreamdbid = :contentStreamDbId')->setParameter('contentStreamDbId', $this->contentStreamDbId->value); $this->addSubtreeTagConstraints($queryBuilder, 'sh'); if ($filter->nodeTypes !== null) { $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), "sn"); @@ -627,7 +627,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->contentStreamId, $this->dimensionSpacePoint); + $queryBuilder = $this->nodeQueryBuilder->buildBasicNodeSiblingsQuery($preceding, $siblingNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint); $this->addSubtreeTagConstraints($queryBuilder); if ($filter->nodeTypes !== null) { @@ -657,9 +657,9 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), '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.contentstreamid = :contentStreamId') + ->where('ch.contentstreamdbid = :contentStreamDbId') ->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash') - ->andWhere('ph.contentstreamid = :contentStreamId') + ->andWhere('ph.contentstreamdbid = :contentStreamDbId') ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('c.nodeaggregateid = :entryNodeAggregateId'); $this->addSubtreeTagConstraints($queryBuilderInitial, 'ph'); @@ -670,11 +670,11 @@ private function buildAncestorNodesQueries(NodeAggregateId $entryNodeAggregateId ->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.contentstreamid = :contentStreamId') + ->where('h.contentstreamdbid = :contentStreamDbId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); $this->addSubtreeTagConstraints($queryBuilderRecursive); - $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint); + $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint); if ($filter->nodeTypes !== null) { $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), 'pn'); } @@ -694,9 +694,9 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate ->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), '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.contentstreamid = :contentStreamId') + ->where('h.contentstreamdbid = :contentStreamDbId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash') - ->andWhere('ph.contentstreamid = :contentStreamId') + ->andWhere('ph.contentstreamdbid = :contentStreamDbId') ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash') ->andWhere('p.nodeaggregateid = :entryNodeAggregateId'); $this->addSubtreeTagConstraints($queryBuilderInitial); @@ -706,11 +706,11 @@ private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregate ->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.contentstreamid = :contentStreamId') + ->where('h.contentstreamdbid = :contentStreamDbId') ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash'); $this->addSubtreeTagConstraints($queryBuilderRecursive); - $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint, 'tree', 'n'); + $queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamDbId, $this->dimensionSpacePoint, 'tree', 'n'); if ($filter->nodeTypes !== null) { $this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)); } From aff4d856a46ceda5f1dd19061b461f30845faacc Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:04:58 +0100 Subject: [PATCH 2/7] FEATURE: Introduce internal `ContentStreamDbId`(WriteSide 01-05) Following features are implemented: 01-RootNodeCreation 02-NodeCreation 03-NodeVariation 04-NodeModification 05-NodeReferencing --- .../DoctrineDbalContentGraphProjection.php | 105 +++++++------- ...trineDbalContentGraphProjectionFactory.php | 3 + .../DoctrineDbalContentGraphSchemaBuilder.php | 15 +- .../Projection/Feature/NodeVariation.php | 76 +++++------ .../Projection/Feature/SubtreeTagging.php | 7 +- .../Domain/Projection/HierarchyRelation.php | 11 +- .../Repository/ContentStreamDbIdFinder.php | 76 +++++++++++ .../Repository/ProjectionContentGraph.php | 128 +++++++++--------- .../src/NodeQueryBuilder.php | 42 +++--- 9 files changed, 276 insertions(+), 187 deletions(-) create mode 100644 Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamDbIdFinder.php diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 11db0a28948..900be544806 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -7,6 +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\Feature\ContentStream; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\NodeMove; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\NodeRemoval; @@ -16,6 +17,7 @@ use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation; 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\DimensionSpacePointsRepository; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ProjectionContentGraph; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; @@ -69,7 +71,6 @@ use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Dbal\DbalSchemaDiff; use Neos\EventStore\Model\EventEnvelope; @@ -93,6 +94,7 @@ public function __construct( private readonly ProjectionContentGraph $projectionContentGraph, private readonly ContentGraphTableNames $tableNames, private readonly DimensionSpacePointsRepository $dimensionSpacePointsRepository, + private readonly ContentStreamDbIdFinder $contentStreamDbIdFinder, private readonly ContentGraphReadModelInterface $contentGraphReadModel ) { } @@ -328,7 +330,7 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded SQL; try { $this->dbal->executeStatement($insertHierarchyRelationsStatement, [ - 'contentStreamId' => $event->contentStreamId->value, + 'contentStreamId' => $this->getContentStreamDbId($event)->value, 'sourceDimensionSpacePointHash' => $event->source->hash, 'newDimensionSpacePointHash' => $event->target->hash, ]); @@ -360,14 +362,14 @@ private function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $ev try { $relationAnchorPoints = $this->dbal->fetchFirstColumn($selectRelationsStatement, [ 'dimensionSpacePointHash' => $event->source->hash, - 'contentStreamId' => $event->contentStreamId->value + 'contentStreamId' => $this->getContentStreamDbId($event)->value ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load relation anchor points: %s', $e->getMessage()), 1716489628, $e); } foreach ($relationAnchorPoints as $relationAnchorPoint) { $this->updateNodeRecordWithCopyOnWrite( - $event->contentStreamId, + $this->getContentStreamDbId($event), NodeRelationAnchorPoint::fromInteger($relationAnchorPoint), function (NodeRecord $nodeRecord) use ($event) { $nodeRecord->originDimensionSpacePoint = $event->target->coordinates; @@ -389,7 +391,7 @@ function (NodeRecord $nodeRecord) use ($event) { $this->dbal->executeStatement($updateHierarchyRelationsStatement, [ 'originalDimensionSpacePointHash' => $event->source->hash, 'newDimensionSpacePointHash' => $event->target->hash, - 'contentStreamId' => $event->contentStreamId->value, + 'contentStreamId' => $this->getContentStreamDbId($event)->value, ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to update hierarchy relations: %s', $e->getMessage()), 1716489951, $e); @@ -401,11 +403,11 @@ private function whenNodeAggregateNameWasChanged(NodeAggregateNameWasChanged $ev foreach ( $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream( $event->nodeAggregateId, - $event->contentStreamId, + $this->getContentStreamDbId($event), ) as $anchorPoint ) { $this->updateNodeRecordWithCopyOnWrite( - $event->contentStreamId, + $this->getContentStreamDbId($event), $anchorPoint, function (NodeRecord $node) use ($event, $eventEnvelope) { $node->nodeName = $event->newNodeName; @@ -420,10 +422,10 @@ function (NodeRecord $node) use ($event, $eventEnvelope) { private function whenNodeAggregateTypeWasChanged(NodeAggregateTypeWasChanged $event, EventEnvelope $eventEnvelope): void { - $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream($event->nodeAggregateId, $event->contentStreamId); + $anchorPoints = $this->projectionContentGraph->getAnchorPointsForNodeAggregateInContentStream($event->nodeAggregateId, $this->getContentStreamDbId($event)); foreach ($anchorPoints as $anchorPoint) { $this->updateNodeRecordWithCopyOnWrite( - $event->contentStreamId, + $this->getContentStreamDbId($event), $anchorPoint, function (NodeRecord $node) use ($event, $eventEnvelope) { $node->nodeTypeName = $event->newNodeTypeName; @@ -438,18 +440,18 @@ function (NodeRecord $node) use ($event, $eventEnvelope) { private function whenNodeAggregateWasMoved(NodeAggregateWasMoved $event): void { - $this->moveNodeAggregate($event->contentStreamId, $event->nodeAggregateId, $event->newParentNodeAggregateId, $event->succeedingSiblingsForCoverage); + $this->moveNodeAggregate($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->newParentNodeAggregateId, $event->succeedingSiblingsForCoverage); } private function whenNodeAggregateWasRemoved(NodeAggregateWasRemoved $event): void { - $this->removeNodeAggregate($event->contentStreamId, $event->nodeAggregateId, $event->affectedCoveredDimensionSpacePoints); + $this->removeNodeAggregate($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->affectedCoveredDimensionSpacePoints); } private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCreated $event, EventEnvelope $eventEnvelope): void { $this->createNodeWithHierarchy( - $event->contentStreamId, + $this->getContentStreamDbId($event), $event->nodeAggregateId, $event->nodeTypeName, $event->parentNodeAggregateId, @@ -465,12 +467,12 @@ private function whenNodeAggregateWithNodeWasCreated(NodeAggregateWithNodeWasCre private function whenNodeGeneralizationVariantWasCreated(NodeGeneralizationVariantWasCreated $event, EventEnvelope $eventEnvelope): void { - $this->createNodeGeneralizationVariant($event->contentStreamId, $event->nodeAggregateId, $event->sourceOrigin, $event->generalizationOrigin, $event->variantSucceedingSiblings, $eventEnvelope); + $this->createNodeGeneralizationVariant($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->sourceOrigin, $event->generalizationOrigin, $event->variantSucceedingSiblings, $eventEnvelope); } private function whenNodePeerVariantWasCreated(NodePeerVariantWasCreated $event, EventEnvelope $eventEnvelope): void { - $this->createNodePeerVariant($event->contentStreamId, $event->nodeAggregateId, $event->sourceOrigin, $event->peerOrigin, $event->peerSucceedingSiblings, $eventEnvelope); + $this->createNodePeerVariant($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->sourceOrigin, $event->peerOrigin, $event->peerSucceedingSiblings, $eventEnvelope); } private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEnvelope $eventEnvelope): void @@ -479,7 +481,7 @@ private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEn ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( $event->getNodeAggregateId(), $event->getOriginDimensionSpacePoint(), - $event->getContentStreamId() + $this->getContentStreamDbId($event) ); if (is_null($anchorPoint)) { throw new \InvalidArgumentException( @@ -490,7 +492,7 @@ private function whenNodePropertiesWereSet(NodePropertiesWereSet $event, EventEn ); } $this->updateNodeRecordWithCopyOnWrite( - $event->getContentStreamId(), + $this->getContentStreamDbId($event), $anchorPoint, function (NodeRecord $node) use ($event, $eventEnvelope) { $node->properties = $node->properties @@ -511,7 +513,7 @@ private function whenNodeReferencesWereSet(NodeReferencesWereSet $event, EventEn ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( $event->nodeAggregateId, $originDimensionSpacePoint, - $event->contentStreamId + $this->getContentStreamDbId($event) ); if (is_null($nodeAnchorPoint)) { @@ -525,7 +527,7 @@ private function whenNodeReferencesWereSet(NodeReferencesWereSet $event, EventEn } $this->updateNodeRecordWithCopyOnWrite( - $event->contentStreamId, + $this->getContentStreamDbId($event), $nodeAnchorPoint, function (NodeRecord $node) use ($eventEnvelope) { $node->timestamps = $node->timestamps->with( @@ -539,7 +541,7 @@ function (NodeRecord $node) use ($eventEnvelope) { ->getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( $event->nodeAggregateId, $originDimensionSpacePoint, - $event->contentStreamId + $this->getContentStreamDbId($event) ); @@ -600,7 +602,7 @@ private function writeReferencesForTargetAnchorPoint(SerializedNodeReferences $n private function whenNodeSpecializationVariantWasCreated(NodeSpecializationVariantWasCreated $event, EventEnvelope $eventEnvelope): void { - $this->createNodeSpecializationVariant($event->contentStreamId, $event->nodeAggregateId, $event->sourceOrigin, $event->specializationOrigin, $event->specializationSiblings, $eventEnvelope); + $this->createNodeSpecializationVariant($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->sourceOrigin, $event->specializationOrigin, $event->specializationSiblings, $eventEnvelope); } private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDimensionsWereUpdated $event): void @@ -610,7 +612,7 @@ private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDim $event->nodeAggregateId, /** the origin DSP of the root node is always the empty dimension ({@see whenRootNodeAggregateWithNodeWasCreated}) */ OriginDimensionSpacePoint::createWithoutDimensions(), - $event->contentStreamId + $this->getContentStreamDbId($event) ); if ($rootNodeAnchorPoint === null) { // should never happen. @@ -619,7 +621,7 @@ private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDim $ingoingRelations = $this->projectionContentGraph->findIngoingHierarchyRelationsForNode( $rootNodeAnchorPoint, - $event->contentStreamId + $this->getContentStreamDbId($event) ); $currentlyCoveredDimensionSpacePoints = []; @@ -631,7 +633,7 @@ private function whenRootNodeAggregateDimensionsWereUpdated(RootNodeAggregateDim // add hierarchy edges for newly added dimensions $this->connectHierarchy( - $event->contentStreamId, + $this->getContentStreamDbId($event), NodeRelationAnchorPoint::forRootEdge(), $rootNodeAnchorPoint, $newlyCoveredDimensionSpacePoints, @@ -656,7 +658,7 @@ private function whenRootNodeAggregateWithNodeWasCreated(RootNodeAggregateWithNo ); $this->connectHierarchy( - $event->contentStreamId, + $this->getContentStreamDbId($event), NodeRelationAnchorPoint::forRootEdge(), $node->relationAnchorPoint, $event->coveredDimensionSpacePoints, @@ -671,12 +673,12 @@ private function whenRootWorkspaceWasCreated(RootWorkspaceWasCreated $event): vo private function whenSubtreeWasTagged(SubtreeWasTagged $event): void { - $this->addSubtreeTag($event->contentStreamId, $event->nodeAggregateId, $event->affectedDimensionSpacePoints, $event->tag); + $this->addSubtreeTag($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->affectedDimensionSpacePoints, $event->tag); } private function whenSubtreeWasUntagged(SubtreeWasUntagged $event): void { - $this->removeSubtreeTag($event->contentStreamId, $event->nodeAggregateId, $event->affectedDimensionSpacePoints, $event->tag); + $this->removeSubtreeTag($this->getContentStreamDbId($event), $event->nodeAggregateId, $event->affectedDimensionSpacePoints, $event->tag); } private function whenWorkspaceBaseWorkspaceWasChanged(WorkspaceBaseWorkspaceWasChanged $event): void @@ -720,6 +722,11 @@ private function whenWorkspaceWasRemoved(WorkspaceWasRemoved $event): void /** --------------------------------- */ + public function getContentStreamDbId(EmbedsContentStreamId $event): ContentStreamDbId + { + return $this->contentStreamDbIdFinder->getContentStreamDbId($event->getContentStreamId()); + } + /** * @return array */ @@ -749,12 +756,12 @@ private function truncateDatabaseTables(): void * @template T */ private function updateNodeRecordWithCopyOnWrite( - ContentStreamId $contentStreamIdWhereWriteOccurs, + ContentStreamDbId $contentStreamDbIdWhereWriteOccurs, NodeRelationAnchorPoint $anchorPoint, callable $operations ): mixed { - $contentStreamIds = $this->projectionContentGraph->getAllContentStreamIdsAnchorPointIsContainedIn($anchorPoint); - if (count($contentStreamIds) > 1) { + $contentStreamDbIds = $this->projectionContentGraph->getAllContentStreamDbIdsAnchorPointIsContainedIn($anchorPoint); + if (count($contentStreamDbIds) > 1) { // Copy on Write needed! // Copy on Write is a purely "Content Stream" related concept; // thus we do not care about different DimensionSpacePoints here (but we copy all edges) @@ -778,13 +785,13 @@ private function updateNodeRecordWithCopyOnWrite( h.parentnodeanchor = IF(h.parentnodeanchor = :originalNodeAnchor, :newNodeAnchor, h.parentnodeanchor) WHERE :originalNodeAnchor IN (h.childnodeanchor, h.parentnodeanchor) - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId SQL; try { $this->dbal->executeStatement($updateHierarchyRelationStatement, [ 'newNodeAnchor' => $copiedNode->relationAnchorPoint->value, 'originalNodeAnchor' => $anchorPoint->value, - 'contentStreamId' => $contentStreamIdWhereWriteOccurs->value, + 'contentStreamDbId' => $contentStreamDbIdWhereWriteOccurs->value, ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to update hierarchy relation: %s', $e->getMessage()), 1716486444, $e); @@ -851,7 +858,7 @@ private static function initiatingDateTime(EventEnvelope $eventEnvelope): \DateT } private function createNodeWithHierarchy( - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, NodeTypeName $nodeTypeName, NodeAggregateId $parentNodeAggregateId, @@ -889,7 +896,7 @@ private function createNodeWithHierarchy( foreach ($missingParentRelations as $dimensionSpacePoint) { $parentNode = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamId, + $contentStreamDbId, $parentNodeAggregateId, $dimensionSpacePoint ); @@ -897,7 +904,7 @@ private function createNodeWithHierarchy( $succeedingSiblingNodeAggregateId = $coverageSucceedingSiblings->getSucceedingSiblingIdForDimensionSpacePoint($dimensionSpacePoint); $succeedingSibling = $succeedingSiblingNodeAggregateId ? $this->projectionContentGraph->findNodeInAggregate( - $contentStreamId, + $contentStreamDbId, $succeedingSiblingNodeAggregateId, $dimensionSpacePoint ) @@ -905,7 +912,7 @@ private function createNodeWithHierarchy( if ($parentNode) { $this->connectHierarchy( - $contentStreamId, + $contentStreamDbId, $parentNode->relationAnchorPoint, $node->relationAnchorPoint, new DimensionSpacePointSet([$dimensionSpacePoint]), @@ -919,7 +926,7 @@ private function createNodeWithHierarchy( } private function connectHierarchy( - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, NodeRelationAnchorPoint $parentNodeAnchorPoint, NodeRelationAnchorPoint $childNodeAnchorPoint, DimensionSpacePointSet $dimensionSpacePointSet, @@ -930,17 +937,17 @@ private function connectHierarchy( $parentNodeAnchorPoint, null, $succeedingSiblingNodeAnchorPoint, - $contentStreamId, + $contentStreamDbId, $dimensionSpacePoint ); - $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamId, $parentNodeAnchorPoint, $dimensionSpacePoint); + $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamDbId, $parentNodeAnchorPoint, $dimensionSpacePoint); $inheritedSubtreeTags = NodeTags::create(SubtreeTags::createEmpty(), $parentSubtreeTags->all()); $hierarchyRelation = new HierarchyRelation( $parentNodeAnchorPoint, $childNodeAnchorPoint, - $contentStreamId, + $contentStreamDbId, $dimensionSpacePoint, $dimensionSpacePoint->hash, $position, @@ -955,14 +962,14 @@ private function getRelationPosition( ?NodeRelationAnchorPoint $parentAnchorPoint, ?NodeRelationAnchorPoint $childAnchorPoint, ?NodeRelationAnchorPoint $succeedingSiblingAnchorPoint, - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint ): int { $position = $this->projectionContentGraph->determineHierarchyRelationPosition( $parentAnchorPoint, $childAnchorPoint, $succeedingSiblingAnchorPoint, - $contentStreamId, + $contentStreamDbId, $dimensionSpacePoint ); @@ -971,7 +978,7 @@ private function getRelationPosition( $parentAnchorPoint, $childAnchorPoint, $succeedingSiblingAnchorPoint, - $contentStreamId, + $contentStreamDbId, $dimensionSpacePoint ); } @@ -983,7 +990,7 @@ private function getRelationPositionAfterRecalculation( ?NodeRelationAnchorPoint $parentAnchorPoint, ?NodeRelationAnchorPoint $childAnchorPoint, ?NodeRelationAnchorPoint $succeedingSiblingAnchorPoint, - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint ): int { if (!$childAnchorPoint && !$parentAnchorPoint) { @@ -998,12 +1005,12 @@ private function getRelationPositionAfterRecalculation( $hierarchyRelations = $parentAnchorPoint ? $this->projectionContentGraph->getOutgoingHierarchyRelationsForNodeAndSubgraph( $parentAnchorPoint, - $contentStreamId, + $contentStreamDbId, $dimensionSpacePoint ) : $this->projectionContentGraph->getIngoingHierarchyRelationsForNodeAndSubgraph( $childAnchorPoint, - $contentStreamId, + $contentStreamDbId, $dimensionSpacePoint ); @@ -1029,25 +1036,25 @@ private function getRelationPositionAfterRecalculation( private function copyHierarchyRelationToDimensionSpacePoint( HierarchyRelation $sourceHierarchyRelation, - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint, NodeRelationAnchorPoint $newParent, NodeRelationAnchorPoint $newChild, ?NodeRelationAnchorPoint $newSucceedingSibling = null, ): HierarchyRelation { - $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamId, $newParent, $dimensionSpacePoint); + $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamDbId, $newParent, $dimensionSpacePoint); $inheritedSubtreeTags = NodeTags::create($sourceHierarchyRelation->subtreeTags->withoutInherited()->all(), $parentSubtreeTags->withoutInherited()->all()); $copy = new HierarchyRelation( $newParent, $newChild, - $contentStreamId, + $contentStreamDbId, $dimensionSpacePoint, $dimensionSpacePoint->hash, $this->getRelationPosition( $newParent, $newChild, $newSucceedingSibling, - $contentStreamId, + $contentStreamDbId, $dimensionSpacePoint ), $inheritedSubtreeTags, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php index f63308efc43..7a4a54daf01 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php @@ -5,6 +5,7 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter; use Doctrine\DBAL\Connection; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ContentStreamDbIdFinder; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ProjectionContentGraph; @@ -31,6 +32,7 @@ public function build( ); $dimensionSpacePointsRepository = new DimensionSpacePointsRepository($this->dbal, $tableNames); + $contentStreamDbIdRepository = new ContentStreamDbIdFinder($this->dbal, $tableNames); $nodeFactory = new NodeFactory( $projectionFactoryDependencies->contentRepositoryId, @@ -54,6 +56,7 @@ public function build( ), $tableNames, $dimensionSpacePointsRepository, + $contentStreamDbIdRepository, $contentGraphReadModel ); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php index 0aaa056c601..47832978d56 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php @@ -62,7 +62,7 @@ private function createHierarchyRelationTable(AbstractPlatform $platform): Table { $table = self::createTable($this->tableNames->hierarchyRelation(), [ (new Column('position', self::type(Types::INTEGER)))->setNotnull(true), - DbalSchemaFactory::columnForContentStreamId('contentstreamid', $platform)->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), @@ -71,11 +71,11 @@ private function createHierarchyRelationTable(AbstractPlatform $platform): Table return $table ->addIndex(['childnodeanchor']) - ->addIndex(['contentstreamid']) + ->addIndex(['contentstreamdbid']) ->addIndex(['parentnodeanchor']) - ->addIndex(['childnodeanchor', 'contentstreamid', 'dimensionspacepointhash', 'position']) - ->addIndex(['parentnodeanchor', 'contentstreamid', 'dimensionspacepointhash', 'position']) - ->addIndex(['contentstreamid', 'dimensionspacepointhash']); + ->addIndex(['childnodeanchor', 'contentstreamdbid', 'dimensionspacepointhash', 'position']) + ->addIndex(['parentnodeanchor', 'contentstreamdbid', 'dimensionspacepointhash', 'position']) + ->addIndex(['contentstreamdbid', 'dimensionspacepointhash']); } private function createDimensionSpacePointsTable(AbstractPlatform $platform): Table @@ -120,6 +120,7 @@ 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), @@ -128,7 +129,9 @@ private function createContentStreamTable(AbstractPlatform $platform): Table (new Column('hasChanges', Type::getType(Types::BOOLEAN)))->setNotnull(true), ]); - return $contentStreamTable->setPrimaryKey(['id']); + return $contentStreamTable + ->addUniqueIndex(['id']) + ->setPrimaryKey(['dbId']); } /** diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php index 233309f003c..51c3a9d269e 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeVariation.php @@ -4,6 +4,7 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; @@ -11,7 +12,6 @@ use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTags; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\EventStore\Model\EventEnvelope; /** @@ -21,16 +21,16 @@ */ trait NodeVariation { - private function createNodeSpecializationVariant(ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $specializationOrigin, InterdimensionalSiblings $specializationSiblings, EventEnvelope $eventEnvelope): void + private function createNodeSpecializationVariant(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $specializationOrigin, InterdimensionalSiblings $specializationSiblings, EventEnvelope $eventEnvelope): void { // Do the actual specialization $sourceNode = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value), 1716498651); } $specializedNode = $this->copyNodeToDimensionSpacePoint( @@ -42,7 +42,7 @@ private function createNodeSpecializationVariant(ContentStreamId $contentStreamI $uncoveredDimensionSpacePoints = $specializationSiblings->toDimensionSpacePointSet()->points; foreach ( $this->projectionContentGraph->findIngoingHierarchyRelationsForNodeAggregate( - $contentStreamId, + $contentStreamDbId, $sourceNode->nodeAggregateId, $specializationSiblings->toDimensionSpacePointSet() ) as $hierarchyRelation @@ -56,29 +56,29 @@ private function createNodeSpecializationVariant(ContentStreamId $contentStreamI } if (!empty($uncoveredDimensionSpacePoints)) { $sourceParent = $this->projectionContentGraph->findParentNode( - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value), 1716498695); } foreach ($uncoveredDimensionSpacePoints as $uncoveredDimensionSpacePoint) { $parentNode = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value, $sourceParent->nodeAggregateId->value), 1716498734); } - $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamId, $parentNode->relationAnchorPoint, $uncoveredDimensionSpacePoint); + $parentSubtreeTags = $this->subtreeTagsForHierarchyRelation($contentStreamDbId, $parentNode->relationAnchorPoint, $uncoveredDimensionSpacePoint); $specializationSucceedingSiblingNodeAggregateId = $specializationSiblings ->getSucceedingSiblingIdForDimensionSpacePoint($uncoveredDimensionSpacePoint); $specializationSucceedingSiblingNode = $specializationSucceedingSiblingNodeAggregateId ? $this->projectionContentGraph->findNodeInAggregate( - $contentStreamId, + $contentStreamDbId, $specializationSucceedingSiblingNodeAggregateId, $uncoveredDimensionSpacePoint ) @@ -87,14 +87,14 @@ private function createNodeSpecializationVariant(ContentStreamId $contentStreamI $hierarchyRelation = new HierarchyRelation( $parentNode->relationAnchorPoint, $specializedNode->relationAnchorPoint, - $contentStreamId, + $contentStreamDbId, $uncoveredDimensionSpacePoint, $uncoveredDimensionSpacePoint->hash, $this->projectionContentGraph->determineHierarchyRelationPosition( $parentNode->relationAnchorPoint, $specializedNode->relationAnchorPoint, $specializationSucceedingSiblingNode?->relationAnchorPoint, - $contentStreamId, + $contentStreamDbId, $uncoveredDimensionSpacePoint ), NodeTags::create(SubtreeTags::createEmpty(), $parentSubtreeTags->all()), @@ -105,7 +105,7 @@ private function createNodeSpecializationVariant(ContentStreamId $contentStreamI foreach ( $this->projectionContentGraph->findOutgoingHierarchyRelationsForNodeAggregate( - $contentStreamId, + $contentStreamDbId, $sourceNode->nodeAggregateId, $specializationSiblings->toDimensionSpacePointSet() ) as $hierarchyRelation @@ -125,24 +125,24 @@ private function createNodeSpecializationVariant(ContentStreamId $contentStreamI ); } - public function createNodeGeneralizationVariant(ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $generalizationOrigin, InterdimensionalSiblings $variantSucceedingSiblings, EventEnvelope $eventEnvelope): void + public function createNodeGeneralizationVariant(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $generalizationOrigin, InterdimensionalSiblings $variantSucceedingSiblings, EventEnvelope $eventEnvelope): void { // do the generalization $sourceNode = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value), 1716498802); } $sourceParentNode = $this->projectionContentGraph->findParentNode( - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value), 1716498857); } $generalizedNode = $this->copyNodeToDimensionSpacePoint( $sourceNode, @@ -153,7 +153,7 @@ public function createNodeGeneralizationVariant(ContentStreamId $contentStreamId $unassignedIngoingDimensionSpacePoints = $variantSucceedingSiblings->toDimensionSpacePointSet(); foreach ( $this->projectionContentGraph->findIngoingHierarchyRelationsForNodeAggregate( - $contentStreamId, + $contentStreamDbId, $nodeAggregateId, $variantSucceedingSiblings->toDimensionSpacePointSet() ) as $existingIngoingHierarchyRelation @@ -172,7 +172,7 @@ public function createNodeGeneralizationVariant(ContentStreamId $contentStreamId foreach ( $this->projectionContentGraph->findOutgoingHierarchyRelationsForNodeAggregate( - $contentStreamId, + $contentStreamDbId, $nodeAggregateId, $variantSucceedingSiblings->toDimensionSpacePointSet() ) as $existingOutgoingHierarchyRelation @@ -188,18 +188,18 @@ public function createNodeGeneralizationVariant(ContentStreamId $contentStreamId if (count($unassignedIngoingDimensionSpacePoints) > 0) { $ingoingSourceHierarchyRelation = $this->projectionContentGraph->findIngoingHierarchyRelationsForNode( $sourceNode->relationAnchorPoint, - $contentStreamId, + $contentStreamDbId, 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(), $contentStreamId->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(), $contentStreamDbId->value), 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( - $contentStreamId, + $contentStreamDbId, $sourceParentNode->nodeAggregateId, $unassignedDimensionSpacePoint ); @@ -209,7 +209,7 @@ public function createNodeGeneralizationVariant(ContentStreamId $contentStreamId $nodeAggregateId->value, $sourceOrigin->toJson(), $unassignedDimensionSpacePoint->toJson(), - $contentStreamId->value, + $contentStreamDbId->value, $sourceParentNode->nodeAggregateId->value ), 1716498961); } @@ -218,7 +218,7 @@ public function createNodeGeneralizationVariant(ContentStreamId $contentStreamId ->getSucceedingSiblingIdForDimensionSpacePoint($unassignedDimensionSpacePoint); $generalizationSucceedingSiblingNode = $generalizationSucceedingSiblingNodeAggregateId ? $this->projectionContentGraph->findNodeInAggregate( - $contentStreamId, + $contentStreamDbId, $generalizationSucceedingSiblingNodeAggregateId, $unassignedDimensionSpacePoint ) @@ -226,7 +226,7 @@ public function createNodeGeneralizationVariant(ContentStreamId $contentStreamId $this->copyHierarchyRelationToDimensionSpacePoint( $ingoingSourceHierarchyRelation, - $contentStreamId, + $contentStreamDbId, $unassignedDimensionSpacePoint, $generalizationParentNode->relationAnchorPoint, $generalizedNode->relationAnchorPoint, @@ -242,16 +242,16 @@ public function createNodeGeneralizationVariant(ContentStreamId $contentStreamId ); } - public function createNodePeerVariant(ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $peerOrigin, InterdimensionalSiblings $peerSucceedingSiblings, EventEnvelope $eventEnvelope): void + public function createNodePeerVariant(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $sourceOrigin, OriginDimensionSpacePoint $peerOrigin, InterdimensionalSiblings $peerSucceedingSiblings, EventEnvelope $eventEnvelope): void { // Do the peer variant creation itself $sourceNode = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value), 1716498802); } $peerNode = $this->copyNodeToDimensionSpacePoint( $sourceNode, @@ -262,7 +262,7 @@ public function createNodePeerVariant(ContentStreamId $contentStreamId, NodeAggr $unassignedIngoingDimensionSpacePoints = $peerSucceedingSiblings->toDimensionSpacePointSet(); foreach ( $this->projectionContentGraph->findIngoingHierarchyRelationsForNodeAggregate( - $contentStreamId, + $contentStreamDbId, $nodeAggregateId, $peerSucceedingSiblings->toDimensionSpacePointSet() ) as $existingIngoingHierarchyRelation @@ -281,7 +281,7 @@ public function createNodePeerVariant(ContentStreamId $contentStreamId, NodeAggr foreach ( $this->projectionContentGraph->findOutgoingHierarchyRelationsForNodeAggregate( - $contentStreamId, + $contentStreamDbId, $nodeAggregateId, $peerSucceedingSiblings->toDimensionSpacePointSet() ) as $existingOutgoingHierarchyRelation @@ -295,36 +295,36 @@ public function createNodePeerVariant(ContentStreamId $contentStreamId, NodeAggr } $sourceParentNode = $this->projectionContentGraph->findParentNode( - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value), 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( - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value, $sourceParentNode->nodeAggregateId->value), 1716499016); } $peerSucceedingSiblingNodeAggregateId = $peerSucceedingSiblings ->getSucceedingSiblingIdForDimensionSpacePoint($coveredDimensionSpacePoint); $peerSucceedingSiblingNode = $peerSucceedingSiblingNodeAggregateId ? $this->projectionContentGraph->findNodeInAggregate( - $contentStreamId, + $contentStreamDbId, $peerSucceedingSiblingNodeAggregateId, $coveredDimensionSpacePoint ) : null; $this->connectHierarchy( - $contentStreamId, + $contentStreamDbId, $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 a83199e8ba0..507e22a7734 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Exception as DBALException; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; @@ -212,7 +213,7 @@ private function moveSubtreeTags(ContentStreamId $contentStreamId, NodeAggregate } } - private function subtreeTagsForHierarchyRelation(ContentStreamId $contentStreamId, NodeRelationAnchorPoint $parentNodeAnchorPoint, DimensionSpacePoint $dimensionSpacePoint): NodeTags + private function subtreeTagsForHierarchyRelation(ContentStreamDbId $contentStreamDbId, NodeRelationAnchorPoint $parentNodeAnchorPoint, DimensionSpacePoint $dimensionSpacePoint): NodeTags { if ($parentNodeAnchorPoint->equals(NodeRelationAnchorPoint::forRootEdge())) { return NodeTags::createEmpty(); @@ -222,11 +223,11 @@ private function subtreeTagsForHierarchyRelation(ContentStreamId $contentStreamI SELECT h.subtreetags FROM ' . $this->tableNames->hierarchyRelation() . ' h WHERE h.childnodeanchor = :parentNodeAnchorPoint - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash ', [ 'parentNodeAnchorPoint' => $parentNodeAnchorPoint->value, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'dimensionSpacePointHash' => $dimensionSpacePoint->hash, ]); } catch (DBALException $e) { diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php index 0b34a8249e1..ef288309b79 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php @@ -17,10 +17,9 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception as DBALException; use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; -use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * The active record for reading and writing hierarchy relations from and to the database @@ -32,7 +31,7 @@ public function __construct( public NodeRelationAnchorPoint $parentNodeAnchor, public NodeRelationAnchorPoint $childNodeAnchor, - public ContentStreamId $contentStreamId, + public ContentStreamDbId $contentStreamDbId, public DimensionSpacePoint $dimensionSpacePoint, public string $dimensionSpacePointHash, public int $position, @@ -54,7 +53,7 @@ public function addToDatabase(Connection $databaseConnection, ContentGraphTableN $databaseConnection->insert($tableNames->hierarchyRelation(), [ 'parentnodeanchor' => $this->parentNodeAnchor->value, 'childnodeanchor' => $this->childNodeAnchor->value, - 'contentstreamid' => $this->contentStreamId->value, + 'contentstreamdbid' => $this->contentStreamDbId->value, 'dimensionspacepointhash' => $this->dimensionSpacePointHash, 'position' => $this->position, 'subtreetags' => $subtreeTagsJson, @@ -131,13 +130,13 @@ public function assignNewPosition(int $position, Connection $databaseConnection, /** * @return array - */ + */ // todo rename? because ambiguous: public function getDatabaseId(): array { return [ 'parentnodeanchor' => $this->parentNodeAnchor->value, 'childnodeanchor' => $this->childNodeAnchor->value, - 'contentstreamid' => $this->contentStreamId->value, + 'contentstreamdbid' => $this->contentStreamDbId->value, 'dimensionspacepointhash' => $this->dimensionSpacePointHash ]; } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamDbIdFinder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamDbIdFinder.php new file mode 100644 index 00000000000..b6f25ecc716 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamDbIdFinder.php @@ -0,0 +1,76 @@ + + */ + 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/ProjectionContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php index 01c4df388ad..d02b04ed3be 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php @@ -19,6 +19,7 @@ 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\HierarchyRelation; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRecord; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint; @@ -26,7 +27,6 @@ use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * The read only content graph for use by the {@see DoctrineDbalContentGraphProjection}. This is the class for low-level operations @@ -51,14 +51,14 @@ public function __construct( * correct) */ public function findParentNode( - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, NodeAggregateId $childNodeAggregateId, OriginDimensionSpacePoint $originDimensionSpacePoint, ?DimensionSpacePoint $coveredDimensionSpacePoint = null ): ?NodeRecord { $parentNodeStatement = <<tableNames->node()} p INNER JOIN {$this->tableNames->hierarchyRelation()} ph ON ph.childnodeanchor = p.relationanchorpoint @@ -68,27 +68,27 @@ public function findParentNode( WHERE c.nodeaggregateid = :childNodeAggregateId AND c.origindimensionspacepointhash = :originDimensionSpacePointHash - AND ph.contentstreamid = :contentStreamId - AND ch.contentstreamid = :contentStreamId + AND ph.contentstreamdbid = :contentStreamDbId + AND ch.contentstreamdbid = :contentStreamDbId AND ph.dimensionspacepointhash = :coveredDimensionSpacePointHash AND ch.dimensionspacepointhash = :coveredDimensionSpacePointHash SQL; try { $nodeRow = $this->dbal->fetchAssociative($parentNodeStatement, [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'childNodeAggregateId' => $childNodeAggregateId->value, 'originDimensionSpacePointHash' => $originDimensionSpacePoint->hash, 'coveredDimensionSpacePointHash' => $coveredDimensionSpacePoint->hash ?? $originDimensionSpacePoint->hash ]); } 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', $contentStreamId->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', $contentStreamDbId->value, $childNodeAggregateId->value, $originDimensionSpacePoint->toJson(), $e->getMessage()), 1716475976, $e); } return $nodeRow ? NodeRecord::fromDatabaseRow($nodeRow) : null; } public function findNodeInAggregate( - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, DimensionSpacePoint $coveredDimensionSpacePoint ): ?NodeRecord { @@ -101,17 +101,17 @@ public function findNodeInAggregate( INNER JOIN {$this->tableNames->dimensionSpacePoints()} dsp ON n.origindimensionspacepointhash = dsp.hash WHERE n.nodeaggregateid = :nodeAggregateId - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash SQL; try { $nodeRow = $this->dbal->fetchAssociative($nodeInAggregateStatement, [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'nodeAggregateId' => $nodeAggregateId->value, 'dimensionSpacePointHash' => $coveredDimensionSpacePoint->hash ]); } 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', $contentStreamId->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', $contentStreamDbId->value, $nodeAggregateId->value, $coveredDimensionSpacePoint->toJson(), $e->getMessage()), 1716474165, $e); } return $nodeRow ? NodeRecord::fromDatabaseRow($nodeRow) : null; @@ -120,7 +120,7 @@ public function findNodeInAggregate( public function getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStream( NodeAggregateId $nodeAggregateId, OriginDimensionSpacePoint $originDimensionSpacePoint, - ContentStreamId $contentStreamId + ContentStreamDbId $contentStreamDbId ): ?NodeRelationAnchorPoint { $relationAnchorPointsStatement = <<dbal->fetchFirstColumn($relationAnchorPointsStatement, [ 'nodeAggregateId' => $nodeAggregateId->value, 'originDimensionSpacePointHash' => $originDimensionSpacePoint->hash, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, ]); } 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', $contentStreamId->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', $contentStreamDbId->value, $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!', $contentStreamId->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!', $contentStreamDbId->value, $nodeAggregateId->value, $originDimensionSpacePoint->toJson()), 1716474484); } return $relationAnchorPoints === [] ? null : NodeRelationAnchorPoint::fromInteger($relationAnchorPoints[0]); } @@ -154,7 +154,7 @@ public function getAnchorPointForNodeAndOriginDimensionSpacePointAndContentStrea */ public function getAnchorPointsForNodeAggregateInContentStream( NodeAggregateId $nodeAggregateId, - ContentStreamId $contentStreamId + ContentStreamDbId $contentStreamDbId ): iterable { $relationAnchorPointsStatement = <<tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId SQL; try { $relationAnchorPoints = $this->dbal->fetchFirstColumn($relationAnchorPointsStatement, [ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, ]); } 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', $contentStreamId->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', $contentStreamDbId->value, $nodeAggregateId->value, $e->getMessage()), 1716474706, $e); } return array_map(NodeRelationAnchorPoint::fromInteger(...), $relationAnchorPoints); @@ -204,7 +204,7 @@ public function determineHierarchyRelationPosition( ?NodeRelationAnchorPoint $parentAnchorPoint, ?NodeRelationAnchorPoint $childAnchorPoint, ?NodeRelationAnchorPoint $succeedingSiblingAnchorPoint, - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint ): int { if (!$parentAnchorPoint && !$childAnchorPoint) { @@ -221,18 +221,18 @@ public function determineHierarchyRelationPosition( {$this->tableNames->hierarchyRelation()} h WHERE h.childnodeanchor = :succeedingSiblingAnchorPoint - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash SQL; try { /** @var array $succeedingSiblingRelation */ $succeedingSiblingRelation = $this->dbal->fetchAssociative($succeedingSiblingRelationStatement, [ 'succeedingSiblingAnchorPoint' => $succeedingSiblingAnchorPoint->value, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'dimensionSpacePointHash' => $dimensionSpacePoint->hash ]); } 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', $contentStreamId->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', $contentStreamDbId->value, $succeedingSiblingAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716474854, $e); } if (!$succeedingSiblingRelation) { @@ -252,19 +252,19 @@ public function determineHierarchyRelationPosition( {$this->tableNames->hierarchyRelation()} h WHERE h.parentnodeanchor = :anchorPoint - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash AND h.position < :position SQL; try { $precedingSiblingData = $this->dbal->fetchAssociative($precedingSiblingStatement, [ 'anchorPoint' => $parentAnchorPoint->value, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'dimensionSpacePointHash' => $dimensionSpacePoint->hash, 'position' => $succeedingSiblingPosition ]); } 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', $contentStreamId->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', $contentStreamDbId->value, $parentAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716474957, $e); } $precedingSiblingPosition = $precedingSiblingData ? ($precedingSiblingData['position'] ?? null) : null; if (!is_null($precedingSiblingPosition)) { @@ -285,18 +285,18 @@ public function determineHierarchyRelationPosition( {$this->tableNames->hierarchyRelation()} h WHERE h.childnodeanchor = :childAnchorPoint - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash SQL; try { /** @var array $childHierarchyRelationData */ $childHierarchyRelationData = $this->dbal->fetchAssociative($childHierarchyRelationStatement, [ 'childAnchorPoint' => $childAnchorPoint->value, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'dimensionSpacePointHash' => $dimensionSpacePoint->hash ]); } 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', $contentStreamId->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', $contentStreamDbId->value, $childAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716475001, $e); } $parentAnchorPoint = NodeRelationAnchorPoint::fromInteger( $childHierarchyRelationData['parentnodeanchor'] @@ -309,17 +309,17 @@ public function determineHierarchyRelationPosition( {$this->tableNames->hierarchyRelation()} h WHERE h.parentnodeanchor = :parentAnchorPoint - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash SQL; try { $rightmostSucceedingSiblingRelationData = $this->dbal->fetchAssociative($rightmostSucceedingSiblingRelationStatement, [ 'parentAnchorPoint' => $parentAnchorPoint->value, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'dimensionSpacePointHash' => $dimensionSpacePoint->hash ]); } 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', $contentStreamId->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', $contentStreamDbId->value, $parentAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716475046, $e); } if ($rightmostSucceedingSiblingRelationData) { @@ -338,7 +338,7 @@ public function determineHierarchyRelationPosition( */ public function getOutgoingHierarchyRelationsForNodeAndSubgraph( NodeRelationAnchorPoint $parentAnchorPoint, - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint ): array { $outgoingHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} h WHERE h.parentnodeanchor = :parentAnchorPoint - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash SQL; try { $rows = $this->dbal->fetchAllAssociative($outgoingHierarchyRelationsStatement, [ 'parentAnchorPoint' => $parentAnchorPoint->value, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'dimensionSpacePointHash' => $dimensionSpacePoint->hash ]); } 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', $contentStreamId->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', $contentStreamDbId->value, $parentAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716475151, $e); } return array_map($this->mapRawDataToHierarchyRelation(...), $rows); } @@ -368,7 +368,7 @@ public function getOutgoingHierarchyRelationsForNodeAndSubgraph( */ public function getIngoingHierarchyRelationsForNodeAndSubgraph( NodeRelationAnchorPoint $childAnchorPoint, - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint ): array { $ingoingHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} h WHERE h.childnodeanchor = :childAnchorPoint - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash SQL; try { $rows = $this->dbal->fetchAllAssociative($ingoingHierarchyRelationsStatement, [ 'childAnchorPoint' => $childAnchorPoint->value, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'dimensionSpacePointHash' => $dimensionSpacePoint->hash ]); } 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', $contentStreamId->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', $contentStreamDbId->value, $childAnchorPoint->value, $dimensionSpacePoint->toJson(), $e->getMessage()), 1716475151, $e); } return array_map($this->mapRawDataToHierarchyRelation(...), $rows); } @@ -398,7 +398,7 @@ public function getIngoingHierarchyRelationsForNodeAndSubgraph( */ public function findIngoingHierarchyRelationsForNode( NodeRelationAnchorPoint $childAnchorPoint, - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, ?DimensionSpacePointSet $restrictToSet = null ): array { $ingoingHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} h WHERE h.childnodeanchor = :childAnchorPoint - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId SQL; $parameters = [ 'childAnchorPoint' => $childAnchorPoint->value, - 'contentStreamId' => $contentStreamId->value + 'contentStreamDbId' => $contentStreamDbId->value ]; $types = []; @@ -424,7 +424,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', $contentStreamId->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', $contentStreamDbId->value, $childAnchorPoint->value, $restrictToSet?->toJson() ?? '[any]', $e->getMessage()), 1716476299, $e); } $relations = []; foreach ($rows as $row) { @@ -438,7 +438,7 @@ public function findIngoingHierarchyRelationsForNode( */ public function findOutgoingHierarchyRelationsForNode( NodeRelationAnchorPoint $parentAnchorPoint, - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, ?DimensionSpacePointSet $restrictToSet = null ): array { $outgoingHierarchyRelationsStatement = <<tableNames->hierarchyRelation()} h WHERE h.parentnodeanchor = :parentAnchorPoint - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId SQL; $parameters = [ 'parentAnchorPoint' => $parentAnchorPoint->value, - 'contentStreamId' => $contentStreamId->value + 'contentStreamDbId' => $contentStreamDbId->value ]; $types = []; @@ -464,7 +464,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', $contentStreamId->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', $contentStreamDbId->value, $parentAnchorPoint->value, $restrictToSet?->toJson() ?? '[any]', $e->getMessage()), 1716476573, $e); } $relations = []; foreach ($rows as $row) { @@ -477,7 +477,7 @@ public function findOutgoingHierarchyRelationsForNode( * @return array */ public function findOutgoingHierarchyRelationsForNodeAggregate( - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $dimensionSpacePointSet ): array { @@ -489,19 +489,19 @@ public function findOutgoingHierarchyRelationsForNodeAggregate( INNER JOIN {$this->tableNames->node()} n ON h.parentnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash IN (:dimensionSpacePointHashes) SQL; try { $rows = $this->dbal->fetchAllAssociative($outgoingHierarchyRelationsStatement, [ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'dimensionSpacePointHashes' => $dimensionSpacePointSet->getPointHashes() ], [ 'dimensionSpacePointHashes' => ArrayParameterType::STRING ]); } 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', $contentStreamId->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', $contentStreamDbId->value, $nodeAggregateId->value, $dimensionSpacePointSet->toJson(), $e->getMessage()), 1716476690, $e); } return array_map($this->mapRawDataToHierarchyRelation(...), $rows); } @@ -510,7 +510,7 @@ public function findOutgoingHierarchyRelationsForNodeAggregate( * @return array */ public function findIngoingHierarchyRelationsForNodeAggregate( - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, ?DimensionSpacePointSet $dimensionSpacePointSet = null ): array { @@ -522,11 +522,11 @@ public function findIngoingHierarchyRelationsForNodeAggregate( INNER JOIN {$this->tableNames->node()} n ON h.childnodeanchor = n.relationanchorpoint WHERE n.nodeaggregateid = :nodeAggregateId - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId SQL; $parameters = [ 'nodeAggregateId' => $nodeAggregateId->value, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, ]; $types = []; if ($dimensionSpacePointSet !== null) { @@ -537,33 +537,33 @@ public function findIngoingHierarchyRelationsForNodeAggregate( 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', $contentStreamId->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', $contentStreamDbId->value, $nodeAggregateId->value, $dimensionSpacePointSet?->toJson() ?? '[any]', $e->getMessage()), 1716476743, $e); } return array_map($this->mapRawDataToHierarchyRelation(...), $rows); } /** - * @return array + * @return array */ - public function getAllContentStreamIdsAnchorPointIsContainedIn( + public function getAllContentStreamDbIdsAnchorPointIsContainedIn( NodeRelationAnchorPoint $nodeRelationAnchorPoint ): array { - $contentStreamIdsStatement = <<tableNames->hierarchyRelation()} h WHERE h.childnodeanchor = :nodeRelationAnchorPoint SQL; try { - $contentStreamIds = $this->dbal->fetchFirstColumn($contentStreamIdsStatement, [ + $contentStreamDbIds = $this->dbal->fetchFirstColumn($contentStreamDbIdsStatement, [ '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(ContentStreamId::fromString(...), $contentStreamIds); + return array_map(ContentStreamDbId::fromInt(...), $contentStreamDbIds); } /** @@ -590,7 +590,7 @@ private function mapRawDataToHierarchyRelation(array $rawData): HierarchyRelatio return new HierarchyRelation( NodeRelationAnchorPoint::fromInteger((int)$rawData['parentnodeanchor']), NodeRelationAnchorPoint::fromInteger((int)$rawData['childnodeanchor']), - ContentStreamId::fromString($rawData['contentstreamid']), + ContentStreamDbId::fromInt((int)$rawData['contentstreamdbid']), DimensionSpacePoint::fromJsonString($dimensionSpacePointJson), $rawData['dimensionspacepointhash'], (int)$rawData['position'], diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php index 50353d33d62..5b83602c81e 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php @@ -7,6 +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\NodeRelationAnchorPoint; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindRootNodeAggregatesFilter; @@ -27,7 +28,6 @@ use Neos\ContentRepository\Core\SharedModel\Id\UuidFactory; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\PropertyName; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Dbal\DbalSchemaFactory; /** @@ -44,36 +44,36 @@ public function __construct( public function buildBasicNodeAggregateQuery(): QueryBuilder { return $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamdbid, 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.contentstreamid = :contentStreamId'); + ->where('h.contentstreamdbid = :contentStreamDbId'); } - public function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId): QueryBuilder + public function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamDbId $contentStreamDbId): QueryBuilder { return $this->createQueryBuilder() - ->select('cn.*, ch.contentstreamid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') + ->select('cn.*, ch.contentstreamdbid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNames->node(), 'pn') ->innerJoin('pn', $this->tableNames->hierarchyRelation(), '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.contentstreamid = :contentStreamId') + ->andWhere('ch.contentstreamdbid = :contentStreamDbId') ->orderBy('ch.position') ->setParameters([ 'parentNodeAggregateId' => $parentNodeAggregateId->value, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, ]); } - public function buildFindRootNodeAggregatesQuery(ContentStreamId $contentStreamId, FindRootNodeAggregatesFilter $filter): QueryBuilder + public function buildFindRootNodeAggregatesQuery(ContentStreamDbId $contentStreamDbId, FindRootNodeAggregatesFilter $filter): QueryBuilder { $queryBuilder = $this->buildBasicNodeAggregateQuery() ->andWhere('h.parentnodeanchor = :rootEdgeParentAnchorId') ->setParameters([ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'rootEdgeParentAnchorId' => NodeRelationAnchorPoint::forRootEdge()->value, ]); @@ -84,17 +84,17 @@ public function buildFindRootNodeAggregatesQuery(ContentStreamId $contentStreamI return $queryBuilder; } - public function buildBasicNodeQuery(ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, string $nodeTableAlias = 'n', string $select = 'n.*, h.subtreetags'): QueryBuilder + public function buildBasicNodeQuery(ContentStreamDbId $contentStreamDbId, 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.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $contentStreamId->value) + ->where('h.contentstreamdbid = :contentStreamDbId')->setParameter('contentStreamDbId', $contentStreamDbId->value) ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash); } - public function buildBasicChildNodesQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + public function buildBasicChildNodesQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder { return $this->createQueryBuilder() ->select('n.*, h.subtreetags') @@ -102,11 +102,11 @@ public function buildBasicChildNodesQuery(NodeAggregateId $parentNodeAggregateId ->innerJoin('pn', $this->tableNames->hierarchyRelation(), '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.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $contentStreamId->value) + ->andWhere('h.contentstreamdbid = :contentStreamDbId')->setParameter('contentStreamDbId', $contentStreamDbId->value) ->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash); } - public function buildBasicParentNodeQuery(NodeAggregateId $childNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + public function buildBasicParentNodeQuery(NodeAggregateId $childNodeAggregateId, ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder { return $this->createQueryBuilder() ->select('pn.*, ch.subtreetags') @@ -115,37 +115,37 @@ public function buildBasicParentNodeQuery(NodeAggregateId $childNodeAggregateId, ->innerJoin('pn', $this->tableNames->node(), 'cn', 'cn.relationanchorpoint = ph.childnodeanchor') ->innerJoin('pn', $this->tableNames->hierarchyRelation(), 'ch', 'ch.childnodeanchor = pn.relationanchorpoint') ->where('cn.nodeaggregateid = :childNodeAggregateId')->setParameter('childNodeAggregateId', $childNodeAggregateId->value) - ->andWhere('ph.contentstreamid = :contentStreamId')->setParameter('contentStreamId', $contentStreamId->value) - ->andWhere('ch.contentstreamid = :contentStreamId') + ->andWhere('ph.contentstreamdbid = :contentStreamDbId')->setParameter('contentStreamDbId', $contentStreamDbId->value) + ->andWhere('ch.contentstreamdbid = :contentStreamDbId') ->andWhere('ph.dimensionspacepointhash = :dimensionSpacePointHash')->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash) ->andWhere('ch.dimensionspacepointhash = :dimensionSpacePointHash'); } - public function buildBasicNodeSiblingsQuery(bool $preceding, NodeAggregateId $siblingNodeAggregateId, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder + public function buildBasicNodeSiblingsQuery(bool $preceding, NodeAggregateId $siblingNodeAggregateId, ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint): QueryBuilder { $sharedSubQuery = $this->createQueryBuilder() ->from($this->tableNames->hierarchyRelation(), 'sh') ->innerJoin('sh', $this->tableNames->node(), 'sn', 'sn.relationanchorpoint = sh.childnodeanchor') ->where('sn.nodeaggregateid = :siblingNodeAggregateId') - ->andWhere('sh.contentstreamid = :contentStreamId') + ->andWhere('sh.contentstreamdbid = :contentStreamDbId') ->andWhere('sh.dimensionspacepointhash = :dimensionSpacePointHash'); $parentNodeAnchorSubQuery = (clone $sharedSubQuery)->select('sh.parentnodeanchor'); $siblingPositionSubQuery = (clone $sharedSubQuery)->select('sh.position'); - return $this->buildBasicNodeQuery($contentStreamId, $dimensionSpacePoint) + return $this->buildBasicNodeQuery($contentStreamDbId, $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, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, string $cteName = 'ancestry', string $cteAlias = 'pn'): QueryBuilder + public function buildBasicNodesCteQuery(NodeAggregateId $entryNodeAggregateId, ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint, string $cteName = 'ancestry', string $cteAlias = 'pn'): QueryBuilder { return $this->createQueryBuilder() ->select('*') ->from($cteName, $cteAlias) - ->setParameter('contentStreamId', $contentStreamId->value) + ->setParameter('contentStreamDbId', $contentStreamDbId->value) ->setParameter('dimensionSpacePointHash', $dimensionSpacePoint->hash) ->setParameter('entryNodeAggregateId', $entryNodeAggregateId->value); } From a6aa91a2257f193ab85d16adece0160232a7a5fa Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:27:48 +0000 Subject: [PATCH 3/7] FEATURE: Introduce internal `ContentStreamDbId`(WriteSide 06-11) Following features are implemented: 06-NodeDisabling 07-NodeRemoval 08-NodeMove 09-NodeRenaming 11-NodeTypeChange --- .../DoctrineDbalContentGraphProjection.php | 40 ++++++++------- .../Domain/Projection/Feature/NodeMove.php | 49 +++++++++---------- .../Domain/Projection/Feature/NodeRemoval.php | 10 ++-- .../Projection/Feature/SubtreeTagging.php | 49 +++++++++---------- 4 files changed, 74 insertions(+), 74 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 900be544806..cf76c111067 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -215,6 +215,11 @@ private function whenContentStreamWasCreated(ContentStreamWasCreated $event): vo 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); + // // 1) Copy HIERARCHY RELATIONS (this is the MAIN OPERATION here) // @@ -225,7 +230,7 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void position, dimensionspacepointhash, subtreetags, - contentstreamid + contentstreamdbid ) SELECT h.parentnodeanchor, @@ -233,14 +238,15 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void h.position, h.dimensionspacepointhash, h.subtreetags, - "{$event->newContentStreamId->value}" AS contentstreamid + :newContentStreamDbId AS contentstreamdbid FROM {$this->tableNames->hierarchyRelation()} h - WHERE h.contentstreamid = :sourceContentStreamId + WHERE h.contentstreamdbid = :sourceContentStreamDbId SQL; try { $this->dbal->executeStatement($insertRelationStatement, [ - 'sourceContentStreamId' => $event->sourceContentStreamId->value + 'newContentStreamDbId' => $newContentStreamDbId->value, + 'sourceContentStreamDbId' => $sourceContentStreamDbId->value ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to insert hierarchy relation: %s', $e->getMessage()), 1716489211, $e); @@ -248,19 +254,19 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void // 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->createContentStream($event->newContentStreamId, $event->sourceContentStreamId, $event->versionOfSourceContentStream); } private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): void { + $contentStreamDbId = $this->contentStreamDbIdFinder->getContentStreamDbId($event->contentStreamId); + // Drop hierarchy relations $deleteHierarchyRelationStatement = <<tableNames->hierarchyRelation()} WHERE contentstreamid = :contentStreamId + DELETE FROM {$this->tableNames->hierarchyRelation()} WHERE contentstreamdbid = :contentStreamDbId SQL; try { $this->dbal->executeStatement($deleteHierarchyRelationStatement, [ - 'contentStreamId' => $event->contentStreamId->value + 'contentStreamDbId' => $contentStreamDbId->value ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to delete hierarchy relations: %s', $e->getMessage()), 1716489265, $e); @@ -314,23 +320,23 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded position, subtreetags, dimensionspacepointhash, - contentstreamid + contentstreamdbid ) SELECT h.parentnodeanchor, h.childnodeanchor, h.position, h.subtreetags, - :newDimensionSpacePointHash AS dimensionspacepointhash, - h.contentstreamid + :newDimensionSpacePointHash AS dimensionspacepointhash, + h.contentstreamdbid FROM {$this->tableNames->hierarchyRelation()} h - WHERE h.contentstreamid = :contentStreamId + WHERE h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :sourceDimensionSpacePointHash SQL; try { $this->dbal->executeStatement($insertHierarchyRelationsStatement, [ - 'contentStreamId' => $this->getContentStreamDbId($event)->value, + 'contentStreamDbId' => $this->getContentStreamDbId($event)->value, 'sourceDimensionSpacePointHash' => $event->source->hash, 'newDimensionSpacePointHash' => $event->target->hash, ]); @@ -352,7 +358,7 @@ private function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $ev FROM {$this->tableNames->node()} n INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint - AND h.contentstreamid = :contentStreamId + 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) @@ -362,7 +368,7 @@ private function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $ev try { $relationAnchorPoints = $this->dbal->fetchFirstColumn($selectRelationsStatement, [ 'dimensionSpacePointHash' => $event->source->hash, - 'contentStreamId' => $this->getContentStreamDbId($event)->value + 'contentStreamDbId' => $this->getContentStreamDbId($event)->value ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load relation anchor points: %s', $e->getMessage()), 1716489628, $e); @@ -385,13 +391,13 @@ function (NodeRecord $nodeRecord) use ($event) { h.dimensionspacepointhash = :newDimensionSpacePointHash WHERE h.dimensionspacepointhash = :originalDimensionSpacePointHash - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId SQL; try { $this->dbal->executeStatement($updateHierarchyRelationsStatement, [ 'originalDimensionSpacePointHash' => $event->source->hash, 'newDimensionSpacePointHash' => $event->target->hash, - 'contentStreamId' => $this->getContentStreamDbId($event)->value, + 'contentStreamDbId' => $this->getContentStreamDbId($event)->value, ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to update hierarchy relations: %s', $e->getMessage()), 1716489951, $e); diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php index ac917262aca..ddc8fc73db5 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeMove.php @@ -4,6 +4,7 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\HierarchyRelation; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRecord; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; @@ -11,7 +12,6 @@ use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSibling; use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSiblings; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * The NodeMove projection feature trait @@ -22,34 +22,34 @@ trait NodeMove { use SubtreeTagging; - private function moveNodeAggregate(ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, ?NodeAggregateId $newParentNodeAggregateId, InterdimensionalSiblings $succeedingSiblingsForCoverage): void + private function moveNodeAggregate(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, ?NodeAggregateId $newParentNodeAggregateId, InterdimensionalSiblings $succeedingSiblingsForCoverage): void { foreach ($succeedingSiblingsForCoverage as $succeedingSiblingForCoverage) { $nodeToBeMoved = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value), 1716471638); } if ($newParentNodeAggregateId) { $this->moveNodeBeneathParent( - $contentStreamId, + $contentStreamDbId, $nodeToBeMoved, $newParentNodeAggregateId, $succeedingSiblingForCoverage ); $this->moveSubtreeTags( - $contentStreamId, + $contentStreamDbId, $newParentNodeAggregateId, $succeedingSiblingForCoverage->dimensionSpacePoint ); } else { $this->moveNodeBeforeSucceedingSibling( - $contentStreamId, + $contentStreamDbId, $nodeToBeMoved, $succeedingSiblingForCoverage, ); @@ -66,14 +66,14 @@ private function moveNodeAggregate(ContentStreamId $contentStreamId, NodeAggrega * The move target is given as $succeedingSiblingNodeMoveTarget. This also specifies the new parent node. */ private function moveNodeBeforeSucceedingSibling( - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, NodeRecord $nodeToBeMoved, InterdimensionalSibling $succeedingSiblingForCoverage, ): void { // find the single ingoing hierarchy relation which we want to move $ingoingHierarchyRelation = $this->findIngoingHierarchyRelationToBeMoved( $nodeToBeMoved, - $contentStreamId, + $contentStreamDbId, $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( - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value, $succeedingSiblingForCoverage->nodeAggregateId->value), 1716471881); } } @@ -95,7 +95,7 @@ private function moveNodeBeforeSucceedingSibling( $ingoingHierarchyRelation->parentNodeAnchor, null, $newSucceedingSibling?->relationAnchorPoint, - $contentStreamId, + $contentStreamDbId, $succeedingSiblingForCoverage->dimensionSpacePoint ); @@ -116,7 +116,7 @@ private function moveNodeBeforeSucceedingSibling( * We always move beneath the parent before the succeeding sibling if given (or to the end) */ private function moveNodeBeneathParent( - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, NodeRecord $nodeToBeMoved, NodeAggregateId $parentNodeAggregateId, InterdimensionalSibling $succeedingSiblingForCoverage, @@ -124,30 +124,30 @@ private function moveNodeBeneathParent( // find the single ingoing hierarchy relation which we want to move $ingoingHierarchyRelation = $this->findIngoingHierarchyRelationToBeMoved( $nodeToBeMoved, - $contentStreamId, + $contentStreamDbId, $succeedingSiblingForCoverage->dimensionSpacePoint ); // find the new parent NodeRecord; We need this record because we'll use its RelationAnchorPoints later. $newParent = $this->projectionContentGraph->findNodeInAggregate( - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value), 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( - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value), 1716471995); } } @@ -156,7 +156,7 @@ private function moveNodeBeneathParent( $newParent->relationAnchorPoint, null, $newSucceedingSibling?->relationAnchorPoint, - $contentStreamId, + $contentStreamDbId, $succeedingSiblingForCoverage->dimensionSpacePoint ); @@ -171,27 +171,22 @@ private function moveNodeBeneathParent( /** * Helper for the move methods. - * - * @param NodeRecord $nodeToBeMoved - * @param ContentStreamId $contentStreamId - * @param DimensionSpacePoint $coveredDimensionSpacePointWhereMoveShouldHappen - * @return HierarchyRelation */ private function findIngoingHierarchyRelationToBeMoved( NodeRecord $nodeToBeMoved, - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $coveredDimensionSpacePointWhereMoveShouldHappen ): HierarchyRelation { $restrictToSet = DimensionSpacePointSet::fromArray([$coveredDimensionSpacePointWhereMoveShouldHappen]); $ingoingHierarchyRelations = $this->projectionContentGraph->findIngoingHierarchyRelationsForNode( $nodeToBeMoved->relationAnchorPoint, - $contentStreamId, + $contentStreamDbId, $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(), $contentStreamId->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(), $contentStreamDbId->value), 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 ba50ba2a0ce..6e7b72fc2ca 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/NodeRemoval.php @@ -5,10 +5,10 @@ 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\HierarchyRelation; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePointSet; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * The NodeRemoval projection feature trait @@ -17,12 +17,12 @@ */ trait NodeRemoval { - private function removeNodeAggregate(ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $affectedCoveredDimensionSpacePoints): void + private function removeNodeAggregate(ContentStreamDbId $contentStreamDbId, 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( - $contentStreamId, + $contentStreamDbId, $nodeAggregateId, $affectedCoveredDimensionSpacePoints ); @@ -40,7 +40,7 @@ private function removeRelationRecursivelyFromDatabaseIncludingNonReferencedNode foreach ( $this->projectionContentGraph->findOutgoingHierarchyRelationsForNode( $ingoingRelation->childNodeAnchor, - $ingoingRelation->contentStreamId, + $ingoingRelation->contentStreamDbId, new DimensionSpacePointSet([$ingoingRelation->dimensionSpacePoint]) ) as $outgoingRelation ) { @@ -59,7 +59,7 @@ private function removeRelationRecursivelyFromDatabaseIncludingNonReferencedNode WHERE n.relationanchorpoint = :anchorPointForNode -- the following line means "left join leads to NO MATCHING hierarchyrelation" - AND h.contentstreamid IS NULL + AND h.contentstreamdbid IS NULL SQL; try { $this->dbal->executeStatement($deleteRelationsStatement, [ diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php index 507e22a7734..1ea90e3f401 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/SubtreeTagging.php @@ -14,7 +14,6 @@ use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** * The subtree tagging projection feature trait @@ -23,7 +22,7 @@ */ trait SubtreeTagging { - private function addSubtreeTag(ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $affectedDimensionSpacePoints, SubtreeTag $tag): void + private function addSubtreeTag(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $affectedDimensionSpacePoints, SubtreeTag $tag): void { $addTagToDescendantsStatement = <<tableNames->hierarchyRelation()} h @@ -34,7 +33,7 @@ private function addSubtreeTag(ContentStreamId $contentStreamId, NodeAggregateId INNER JOIN {$this->tableNames->node()} n ON n.relationanchorpoint = ch.parentnodeanchor WHERE n.nodeaggregateid = :nodeAggregateId - AND ch.contentstreamid = :contentStreamId + AND ch.contentstreamdbid = :contentStreamDbId AND ch.dimensionspacepointhash in (:dimensionSpacePointHashes) AND NOT JSON_CONTAINS_PATH(ch.subtreetags, 'one', :tagPath) UNION ALL @@ -44,7 +43,7 @@ private function addSubtreeTag(ContentStreamId $contentStreamId, NodeAggregateId FROM cte JOIN {$this->tableNames->hierarchyRelation()} dh ON dh.parentnodeanchor = cte.id - AND dh.contentstreamid = :contentStreamId + AND dh.contentstreamdbid = :contentStreamDbId AND dh.dimensionspacepointhash = cte.dsp WHERE NOT JSON_CONTAINS_PATH(dh.subtreetags, 'one', :tagPath) @@ -53,12 +52,12 @@ private function addSubtreeTag(ContentStreamId $contentStreamId, NodeAggregateId ) subquery ON h.dimensionspacepointhash = subquery.dsp AND h.childnodeanchor = subquery.id SET h.subtreetags = JSON_INSERT(h.subtreetags, :tagPath, null) - WHERE h.contentstreamid = :contentStreamId + WHERE h.contentstreamdbid = :contentStreamDbId SQL; try { $this->dbal->executeStatement($addTagToDescendantsStatement, [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'nodeAggregateId' => $nodeAggregateId->value, 'dimensionSpacePointHashes' => $affectedDimensionSpacePoints->getPointHashes(), 'tagPath' => '$."' . $tag->value . '"', @@ -66,7 +65,7 @@ private function addSubtreeTag(ContentStreamId $contentStreamId, NodeAggregateId 'dimensionSpacePointHashes' => ArrayParameterType::STRING, ]); } 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, $contentStreamId->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, $contentStreamDbId->value, $nodeAggregateId->value, $affectedDimensionSpacePoints->toJson(), $e->getMessage()), 1716479749, $e); } $addTagToNodeStatement = <<dbal->executeStatement($addTagToNodeStatement, [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'nodeAggregateId' => $nodeAggregateId->value, 'dimensionSpacePointHashes' => $affectedDimensionSpacePoints->getPointHashes(), 'tagPath' => '$."' . $tag->value . '"', @@ -88,11 +87,11 @@ private function addSubtreeTag(ContentStreamId $contentStreamId, NodeAggregateId 'dimensionSpacePointHashes' => ArrayParameterType::STRING, ]); } 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, $contentStreamId->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, $contentStreamDbId->value, $nodeAggregateId->value, $affectedDimensionSpacePoints->toJson(), $e->getMessage()), 1716479840, $e); } } - private function removeSubtreeTag(ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $affectedDimensionSpacePoints, SubtreeTag $tag): void + private function removeSubtreeTag(ContentStreamDbId $contentStreamDbId, NodeAggregateId $nodeAggregateId, DimensionSpacePointSet $affectedDimensionSpacePoints, SubtreeTag $tag): void { $removeTagStatement = <<tableNames->hierarchyRelation()} h @@ -103,7 +102,7 @@ private function removeSubtreeTag(ContentStreamId $contentStreamId, NodeAggregat INNER JOIN {$this->tableNames->node()} n ON n.relationanchorpoint = ph.childnodeanchor WHERE n.nodeaggregateid = :nodeAggregateId - AND ph.contentstreamid = :contentStreamId + AND ph.contentstreamdbid = :contentStreamDbId AND ph.dimensionspacepointhash in (:dimensionSpacePointHashes) UNION ALL SELECT @@ -112,7 +111,7 @@ private function removeSubtreeTag(ContentStreamId $contentStreamId, NodeAggregat FROM cte JOIN {$this->tableNames->hierarchyRelation()} dh ON dh.parentnodeanchor = cte.id - AND dh.contentstreamid = :contentStreamId + AND dh.contentstreamdbid = :contentStreamDbId AND dh.dimensionspacepointhash = cte.dsp WHERE JSON_EXTRACT(dh.subtreetags, :tagPath) != TRUE @@ -131,15 +130,15 @@ private function removeSubtreeTag(ContentStreamId $contentStreamId, NodeAggregat WHERE ph.parentnodeanchor = gph.childnodeanchor AND n.nodeaggregateid = :nodeAggregateId - AND gph.contentstreamid = :contentStreamId + AND gph.contentstreamdbid = :contentStreamDbId LIMIT 1) as containsTagSubQuery ), JSON_SET(subtreetags, :tagPath, null), JSON_REMOVE(subtreetags, :tagPath) ) - WHERE contentstreamid = :contentStreamId + WHERE contentstreamdbid = :contentStreamDbId SQL; try { $this->dbal->executeStatement($removeTagStatement, [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'nodeAggregateId' => $nodeAggregateId->value, 'dimensionSpacePointHashes' => $affectedDimensionSpacePoints->getPointHashes(), 'tagPath' => '$."' . $tag->value . '"', @@ -147,11 +146,11 @@ private function removeSubtreeTag(ContentStreamId $contentStreamId, NodeAggregat 'dimensionSpacePointHashes' => ArrayParameterType::STRING, ]); } 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, $contentStreamId->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, $contentStreamDbId->value, $nodeAggregateId->value, $affectedDimensionSpacePoints->toJson(), $e->getMessage()), 1716482293, $e); } } - private function moveSubtreeTags(ContentStreamId $contentStreamId, NodeAggregateId $newParentNodeAggregateId, DimensionSpacePoint $coveredDimensionSpacePoint): void + private function moveSubtreeTags(ContentStreamDbId $contentStreamDbId, NodeAggregateId $newParentNodeAggregateId, DimensionSpacePoint $coveredDimensionSpacePoint): void { $moveSubtreeTagsStatement = <<tableNames->hierarchyRelation()} h, @@ -164,7 +163,7 @@ private function moveSubtreeTags(ContentStreamId $contentStreamId, NodeAggregate INNER JOIN {$this->tableNames->node()} tn ON tn.relationanchorpoint = th.childnodeanchor WHERE tn.nodeaggregateid = :newParentNodeAggregateId - AND th.contentstreamid = :contentStreamId + AND th.contentstreamdbid = :contentStreamDbId AND th.dimensionspacepointhash = :dimensionSpacePointHash UNION SELECT @@ -181,7 +180,7 @@ private function moveSubtreeTags(ContentStreamId $contentStreamId, NodeAggregate JOIN {$this->tableNames->hierarchyRelation()} dh ON dh.parentnodeanchor = cte.childnodeanchor - AND dh.contentstreamid = :contentStreamId + AND dh.contentstreamdbid = :contentStreamDbId AND dh.dimensionspacepointhash = :dimensionSpacePointHash ) SELECT * FROM cte @@ -197,19 +196,19 @@ private function moveSubtreeTags(ContentStreamId $contentStreamId, NodeAggregate ) WHERE h.childnodeanchor = r.childnodeanchor - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash 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, [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'newParentNodeAggregateId' => $newParentNodeAggregateId->value, 'dimensionSpacePointHash' => $coveredDimensionSpacePoint->hash, ]); } 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', $contentStreamId->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', $contentStreamDbId->value, $newParentNodeAggregateId->value, $coveredDimensionSpacePoint->toJson(), $e->getMessage()), 1716482574, $e); } } @@ -231,10 +230,10 @@ private function subtreeTagsForHierarchyRelation(ContentStreamDbId $contentStrea 'dimensionSpacePointHash' => $dimensionSpacePoint->hash, ]); } 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(), $contentStreamId->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(), $contentStreamDbId->value, $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(), $contentStreamId->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(), $contentStreamDbId->value), 1704199847); } return NodeFactory::extractNodeTagsFromJson($subtreeTagsJson); } From 3ff31b7fae6d26aedbe0955a8fc7728adf17a1c4 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:18:36 +0100 Subject: [PATCH 4/7] FEATURE: Introduce internal `ContentStreamDbId` (`ProjectionIntegrityViolationDetector`) --- .../ProjectionIntegrityViolationDetector.php | 113 +++++++++--------- 1 file changed, 56 insertions(+), 57 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php index fb3cf40baec..968ef36c437 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php @@ -22,7 +22,6 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\ProjectionIntegrityViolationDetectorInterface; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\Error\Messages\Error; use Neos\Error\Messages\Result; @@ -94,14 +93,14 @@ public function hierarchyIntegrityIsProvided(): Result SELECT COUNT(*) as uniquenessCounter, c.nodeaggregateid, - h.dimensionspacepointhash, h.contentstreamid FROM {$this->tableNames->hierarchyRelation()} h + h.dimensionspacepointhash, h.contentstreamdbid FROM {$this->tableNames->hierarchyRelation()} 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.contentstreamid + h.dimensionspacepointhash, h.contentstreamdbid HAVING uniquenessCounter > 1 SQL; try { @@ -128,14 +127,14 @@ public function siblingsAreDistinctlySorted(): Result $ambiguouslySortedHierarchyRelationStatement = <<tableNames->hierarchyRelation()} GROUP BY position, parentnodeanchor, - contentstreamid, + contentstreamdbid, dimensionspacepointhash HAVING COUNT(position) > 1 @@ -169,7 +168,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['contentstreamid'] + . ' are ambiguously sorted in content stream ' . $hierarchyRelationRecord['contentstreamdbid'] . ' and dimension space point ' . $dimensionSpacePoints[$hierarchyRelationRecord['dimensionspacepointhash']]?->toJson(), self::ERROR_CODE_SIBLINGS_ARE_AMBIGUOUSLY_SORTED )); @@ -183,7 +182,7 @@ public function tetheredNodesAreNamed(): Result $result = new Result(); $unnamedTetheredNodesStatement = <<tableNames->node()} n INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint @@ -191,7 +190,7 @@ public function tetheredNodesAreNamed(): Result n.classification = :tethered AND n.name IS NULL GROUP BY - n.nodeaggregateid, h.contentstreamid + n.nodeaggregateid, h.contentstreamdbid SQL; try { $unnamedTetheredNodeRecords = $this->dbal->fetchAllAssociative($unnamedTetheredNodesStatement, [ @@ -204,7 +203,7 @@ public function tetheredNodesAreNamed(): Result foreach ($unnamedTetheredNodeRecords as $unnamedTetheredNodeRecord) { $result->addError(new Error( 'Node aggregate ' . $unnamedTetheredNodeRecord['nodeaggregateid'] - . ' is unnamed in content stream ' . $unnamedTetheredNodeRecord['contentstreamid'] . '.', + . ' is unnamed in content stream ' . $unnamedTetheredNodeRecord['contentstreamdbid'] . '.', self::ERROR_CODE_TETHERED_NODE_IS_UNNAMED )); } @@ -226,7 +225,7 @@ public function subtreeTagsAreInherited(): Result {$this->tableNames->hierarchyRelation()} h INNER JOIN {$this->tableNames->hierarchyRelation()} ph ON ph.childnodeanchor = h.parentnodeanchor - AND ph.contentstreamid = h.contentstreamid + AND ph.contentstreamdbid = h.contentstreamdbid AND ph.dimensionspacepointhash = h.dimensionspacepointhash WHERE EXISTS ( @@ -280,7 +279,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.contentstreamid = dh.contentstreamid + AND sh.contentstreamdbid = dh.contentstreamdbid AND sh.dimensionspacepointhash = dh.dimensionspacepointhash WHERE d.nodeaggregateid IS NULL GROUP BY s.nodeaggregateid, - sh.contentstreamid, + sh.contentstreamdbid, r.destinationnodeaggregateid SQL; try { @@ -310,7 +309,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['contentstreamId'], + . ' does in content stream ' . $record['contentstreamdbid'], self::ERROR_CODE_REFERENCE_INTEGRITY_IS_COMPROMISED )); } @@ -340,7 +339,7 @@ public function allNodesAreConnectedToARootNodePerSubgraph(): Result {$this->tableNames->hierarchyRelation()} h WHERE h.parentnodeanchor = :rootAnchorPoint - AND h.contentstreamid = :contentStreamId + AND h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash UNION -- -------------------------------- @@ -353,24 +352,24 @@ public function allNodesAreConnectedToARootNodePerSubgraph(): Result INNER JOIN {$this->tableNames->hierarchyRelation()} h on h.parentnodeanchor = p.childnodeanchor WHERE - h.contentstreamid = :contentStreamId + h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash ) SELECT nodeaggregateid FROM {$this->tableNames->node()} n INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint WHERE - h.contentstreamid = :contentStreamId + h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash AND relationanchorpoint NOT IN (SELECT * FROM subgraph) SQL; - foreach ($this->findProjectedContentStreamIds() as $contentStreamId) { + foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { foreach ($this->findProjectedDimensionSpacePoints() as $dimensionSpacePoint) { try { $nodeAggregateIdsInCycles = $this->dbal->fetchFirstColumn($nodeAggregateIdsInCyclesStatement, [ 'rootAnchorPoint' => NodeRelationAnchorPoint::forRootEdge()->value, - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'dimensionSpacePointHash' => $dimensionSpacePoint->hash ]); } catch (DBALException $e) { @@ -379,7 +378,7 @@ public function allNodesAreConnectedToARootNodePerSubgraph(): Result if (!empty($nodeAggregateIdsInCycles)) { $result->addError(new Error( - 'Subgraph defined by content stream ' . $contentStreamId->value + 'Subgraph defined by content stream ' . $contentStreamDbId->value . ' and dimension space point ' . $dimensionSpacePoint->toJson() . ' is cyclic for node aggregates ' . implode(',', $nodeAggregateIdsInCycles), @@ -413,7 +412,7 @@ public function nodeAggregateIdsAreUniquePerSubgraph(): Result {$this->tableNames->node()} n INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint WHERE - h.contentstreamid = :contentStreamId + h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash GROUP BY n.nodeaggregateid @@ -421,11 +420,11 @@ public function nodeAggregateIdsAreUniquePerSubgraph(): Result COUNT(DISTINCT(n.relationanchorpoint)) > 1 SQL; - foreach ($this->findProjectedContentStreamIds() as $contentStreamId) { + foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { foreach ($this->findProjectedDimensionSpacePoints() as $dimensionSpacePoint) { try { $ambiguousNodeAggregateRecords = $this->dbal->fetchAllAssociative($ambiguousNodeAggregatesStatement, [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'dimensionSpacePointHash' => $dimensionSpacePoint->hash ]); } catch (DBALException $e) { @@ -434,7 +433,7 @@ public function nodeAggregateIdsAreUniquePerSubgraph(): Result foreach ($ambiguousNodeAggregateRecords as $ambiguousRecord) { $result->addError(new Error( 'Node aggregate ' . $ambiguousRecord['nodeaggregateid'] - . ' is ambiguous in content stream ' . $contentStreamId->value + . ' is ambiguous in content stream ' . $contentStreamDbId->value . ' and dimension space point ' . $dimensionSpacePoint->toJson(), self::ERROR_CODE_AMBIGUOUS_NODE_AGGREGATE_IN_SUBGRAPH )); @@ -455,7 +454,7 @@ public function allNodesHaveAtMostOneParentPerSubgraph(): Result {$this->tableNames->node()} c INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = c.relationanchorpoint WHERE - h.contentstreamid = :contentStreamId + h.contentstreamdbid = :contentStreamDbId AND h.dimensionspacepointhash = :dimensionSpacePointHash GROUP BY c.relationanchorpoint @@ -463,11 +462,11 @@ public function allNodesHaveAtMostOneParentPerSubgraph(): Result COUNT(DISTINCT(h.parentnodeanchor)) > 1 SQL; - foreach ($this->findProjectedContentStreamIds() as $contentStreamId) { + foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { foreach ($this->findProjectedDimensionSpacePoints() as $dimensionSpacePoint) { try { $nodeRecordsWithMultipleParents = $this->dbal->fetchAllAssociative($nodeRecordsWithMultipleParentsStatement, [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'dimensionSpacePointHash' => $dimensionSpacePoint->hash ]); } catch (DBALException $e) { @@ -477,7 +476,7 @@ public function allNodesHaveAtMostOneParentPerSubgraph(): Result foreach ($nodeRecordsWithMultipleParents as $record) { $result->addError(new Error( 'Node aggregate ' . $record['nodeaggregateid'] - . ' has multiple parents in content stream ' . $contentStreamId->value + . ' has multiple parents in content stream ' . $contentStreamDbId->value . ' and dimension space point ' . $dimensionSpacePoint->toJson(), self::ERROR_CODE_NODE_HAS_MULTIPLE_PARENTS )); @@ -498,18 +497,18 @@ public function nodeAggregatesAreConsistentlyTypedPerContentStream(): Result {$this->tableNames->node()} n INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint WHERE - h.contentstreamid = :contentStreamId + h.contentstreamdbid = :contentStreamDbId AND n.nodeaggregateid = :nodeAggregateId SQL; - foreach ($this->findProjectedContentStreamIds() as $contentStreamId) { + foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { foreach ( $this->findProjectedNodeAggregateIdsInContentStream( - $contentStreamId + $contentStreamDbId ) as $nodeAggregateId ) { try { $nodeTypeNames = $this->dbal->fetchFirstColumn($nodeAggregatesStatement, [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'nodeAggregateId' => $nodeAggregateId->value ]); } catch (DBALException $e) { @@ -519,7 +518,7 @@ public function nodeAggregatesAreConsistentlyTypedPerContentStream(): Result if (count($nodeTypeNames) > 1) { $result->addError(new Error( 'Node aggregate ' . $nodeAggregateId->value - . ' in content stream ' . $contentStreamId->value + . ' in content stream ' . $contentStreamDbId->value . ' is of ambiguous type ("' . implode('","', $nodeTypeNames) . '")', self::ERROR_CODE_NODE_AGGREGATE_IS_AMBIGUOUSLY_TYPED )); @@ -540,18 +539,18 @@ public function nodeAggregatesAreConsistentlyClassifiedPerContentStream(): Resul {$this->tableNames->node()} n INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint WHERE - h.contentstreamid = :contentStreamId + h.contentstreamdbid = :contentStreamDbId AND n.nodeaggregateid = :nodeAggregateId SQL; - foreach ($this->findProjectedContentStreamIds() as $contentStreamId) { + foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { foreach ( $this->findProjectedNodeAggregateIdsInContentStream( - $contentStreamId + $contentStreamDbId ) as $nodeAggregateId ) { try { $classifications = $this->dbal->fetchFirstColumn($nodeAggregatesStatement, [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'nodeAggregateId' => $nodeAggregateId->value ]); } catch (DBALException $e) { @@ -561,7 +560,7 @@ public function nodeAggregatesAreConsistentlyClassifiedPerContentStream(): Resul if (count($classifications) > 1) { $result->addError(new Error( 'Node aggregate ' . $nodeAggregateId->value - . ' in content stream ' . $contentStreamId->value + . ' in content stream ' . $contentStreamDbId->value . ' is ambiguously classified ("' . implode('","', $classifications) . '")', self::ERROR_CODE_NODE_AGGREGATE_IS_AMBIGUOUSLY_CLASSIFIED )); @@ -583,15 +582,15 @@ public function childNodeCoverageIsASubsetOfParentNodeCoverage(): Result INNER JOIN {$this->tableNames->node()} n ON c.childnodeanchor = n.relationanchorpoint LEFT JOIN {$this->tableNames->hierarchyRelation()} p ON c.parentnodeanchor = p.childnodeanchor WHERE - c.contentstreamid = :contentStreamId - AND p.contentstreamid = :contentStreamId + c.contentstreamdbid = :contentStreamDbId + AND p.contentstreamdbid = :contentStreamDbId AND c.dimensionspacepointhash = p.dimensionspacepointhash AND p.childnodeanchor IS NULL SQL; - foreach ($this->findProjectedContentStreamIds() as $contentStreamId) { + foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { try { $excessivelyCoveringNodeRecords = $this->dbal->fetchAllAssociative($excessivelyCoveringStatement, [ - 'contentStreamId' => $contentStreamId->value + 'contentStreamDbId' => $contentStreamDbId->value ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load excessively covering nodes: %s', $e->getMessage()), 1716494618, $e); @@ -599,7 +598,7 @@ public function childNodeCoverageIsASubsetOfParentNodeCoverage(): Result foreach ($excessivelyCoveringNodeRecords as $excessivelyCoveringNodeRecord) { $result->addError(new Error( 'Node aggregate ' . $excessivelyCoveringNodeRecord['nodeaggregateid'] - . ' in content stream ' . $contentStreamId->value + . ' in content stream ' . $contentStreamDbId->value . ' 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 @@ -620,7 +619,7 @@ public function allNodesCoverTheirOrigin(): Result {$this->tableNames->node()} n INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint WHERE - h.contentstreamid = :contentStreamId + h.contentstreamdbid = :contentStreamDbId AND nodeaggregateid NOT IN ( -- this query finds all nodes whose origin *IS COVERED* by an incoming hierarchy relation. SELECT @@ -631,14 +630,14 @@ public function allNodesCoverTheirOrigin(): Result p.childnodeanchor = n.relationanchorpoint AND p.dimensionspacepointhash = n.origindimensionspacepointhash WHERE - p.contentstreamid = :contentStreamId + p.contentstreamdbid = :contentStreamDbId ) AND classification != :rootClassification SQL; - foreach ($this->findProjectedContentStreamIds() as $contentStreamId) { + foreach ($this->findProjectedContentStreamDbIds() as $contentStreamDbId) { try { $nodeRecordsWithMissingOriginCoverage = $this->dbal->fetchAllAssociative($nodesWithMissingOriginCoverageStatement, [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'rootClassification' => NodeAggregateClassification::CLASSIFICATION_ROOT->value ]); } catch (DBALException $e) { @@ -648,7 +647,7 @@ public function allNodesCoverTheirOrigin(): Result foreach ($nodeRecordsWithMissingOriginCoverage as $nodeRecord) { $result->addError(new Error( 'Node aggregate ' . $nodeRecord['nodeaggregateid'] - . ' in content stream ' . $contentStreamId->value + . ' in content stream ' . $contentStreamDbId->value . ' does not cover its origin dimension space point hash ' . $nodeRecord['origindimensionspacepointhash'] . '.', self::ERROR_CODE_NODE_DOES_NOT_COVER_ITS_ORIGIN @@ -662,19 +661,19 @@ public function allNodesCoverTheirOrigin(): Result /** * Returns all content stream ids * - * @return iterable + * @return iterable */ - private function findProjectedContentStreamIds(): iterable + private function findProjectedContentStreamDbIds(): iterable { - $contentStreamIdsStatement = <<tableNames->hierarchyRelation()} + $contentStreamDbIdsStatement = <<tableNames->hierarchyRelation()} SQL; try { - $contentStreamIds = $this->dbal->fetchFirstColumn($contentStreamIdsStatement); + $contentStreamDbIds = $this->dbal->fetchFirstColumn($contentStreamDbIdsStatement); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load content stream ids: %s', $e->getMessage()), 1716494814, $e); } - return array_map(ContentStreamId::fromString(...), $contentStreamIds); + return array_map(fn (string $value) => ContentStreamDbId::fromInt((int)$value), $contentStreamDbIds); } /** @@ -697,7 +696,7 @@ private function findProjectedDimensionSpacePoints(): DimensionSpacePointSet * @return array */ protected function findProjectedNodeAggregateIdsInContentStream( - ContentStreamId $contentStreamId + ContentStreamDbId $contentStreamDbId ): array { $nodeAggregateIdsStatement = <<tableNames->node()} n INNER JOIN {$this->tableNames->hierarchyRelation()} h ON h.childnodeanchor = n.relationanchorpoint WHERE - h.contentstreamid = :contentStreamId + h.contentstreamdbid = :contentStreamDbId SQL; try { $nodeAggregateIds = $this->dbal->fetchFirstColumn($nodeAggregateIdsStatement, [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, ]); } catch (DBALException $e) { throw new \RuntimeException(sprintf('Failed to load node aggregate ids for content stream: %s', $e->getMessage()), 1716495988, $e); From 106c7473c6868b277c459b6793ea629cdaa5c547 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:39:30 +0200 Subject: [PATCH 5/7] FEATURE: Introduce internal `ContentStreamDbId` (`ProjectionIntegrityViolationDetectionTrait`) 2 For internal testing --- ...ectionIntegrityViolationDetectionTrait.php | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php index 40f07224e02..eaf9aea1dde 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php @@ -20,6 +20,7 @@ use Doctrine\DBAL\Exception\InvalidArgumentException; use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames; use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalProjectionIntegrityViolationDetectionRunnerFactory; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentGraph\DoctrineDbalAdapter\Tests\Behavior\Features\Bootstrap\Helpers\TestingNodeAggregateId; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; @@ -118,7 +119,9 @@ public function iChangeTheFollowingHierarchyRelationsParent(TableNode $payloadTa unset($record['subtreetags']); $newParentHierarchyRelation = $this->findHierarchyRelationByIds( - ContentStreamId::fromString($dataset['contentStreamId']), + $this->getContentStreamDbId( + ContentStreamId::fromString($dataset['contentStreamId']) + ), DimensionSpacePoint::fromArray($dataset['dimensionSpacePoint']), NodeAggregateId::fromString($dataset['newParentNodeAggregateId']) ); @@ -162,14 +165,18 @@ public function iChangeTheFollowingNodesName(TableNode $payloadTable): void { $dataset = $this->transformPayloadTableToDataset($payloadTable); + $contentStreamDbId = $this->getContentStreamDbId( + ContentStreamId::fromString($dataset['contentStreamId']) + ); + $relationAnchorPoint = $this->dbal->executeQuery( 'SELECT n.relationanchorpoint FROM ' . $this->tableNames()->node() . ' n JOIN ' . $this->tableNames()->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint - WHERE h.contentstreamid = :contentStreamId + WHERE h.contentstreamdbid = :contentStreamDbId AND n.nodeaggregateId = :nodeAggregateId AND n.origindimensionspacepointhash = :originDimensionSpacePointHash', [ - 'contentStreamId' => $dataset['contentStreamId'], + 'contentStreamDbId' => $contentStreamDbId->value, 'nodeAggregateId' => $dataset['nodeAggregateId'], 'originDimensionSpacePointHash' => OriginDimensionSpacePoint::fromArray($dataset['originDimensionSpacePoint'])->hash, ] @@ -195,8 +202,13 @@ public function iSetTheFollowingPosition(TableNode $payloadTable): void { $dataset = $this->transformPayloadTableToDataset($payloadTable); $dimensionSpacePoint = DimensionSpacePoint::fromArray($dataset['dimensionSpacePoint']); + + $contentStreamDbId = $this->getContentStreamDbId( + ContentStreamId::fromString($dataset['contentStreamId']) + ); + $record = [ - 'contentstreamid' => $dataset['contentStreamId'], + 'contentstreamdbid' => $contentStreamDbId->value, 'dimensionspacepointhash' => $dimensionSpacePoint->hash, 'childnodeanchor' => $this->findRelationAnchorPointByDataset($dataset) ]; @@ -251,7 +263,9 @@ private function transformDatasetToReferenceRelationRecord(array $dataset): arra return [ 'name' => $dataset['referenceName'], 'nodeanchorpoint' => $this->findHierarchyRelationByIds( - ContentStreamId::fromString($dataset['contentStreamId']), + $this->getContentStreamDbId( + ContentStreamId::fromString($dataset['contentStreamId']) + ), DimensionSpacePoint::fromArray($dataset['dimensionSpacePoint']), NodeAggregateId::fromString($dataset['sourceNodeAggregateId']) )['childnodeanchor'], @@ -264,11 +278,14 @@ private function transformDatasetToHierarchyRelationRecord(array $dataset): arra $dimensionSpacePoint = DimensionSpacePoint::fromArray($dataset['dimensionSpacePoint']); $parentNodeAggregateId = TestingNodeAggregateId::fromString($dataset['parentNodeAggregateId']); $childAggregateId = TestingNodeAggregateId::fromString($dataset['childNodeAggregateId']); + $contentStreamDbId = $this->getContentStreamDbId( + ContentStreamId::fromString($dataset['contentStreamId']) + ); $parentHierarchyRelation = $parentNodeAggregateId->isNonExistent() ? null : $this->findHierarchyRelationByIds( - ContentStreamId::fromString($dataset['contentStreamId']), + $contentStreamDbId, $dimensionSpacePoint, NodeAggregateId::fromString($dataset['parentNodeAggregateId']) ); @@ -276,13 +293,13 @@ private function transformDatasetToHierarchyRelationRecord(array $dataset): arra $childHierarchyRelation = $childAggregateId->isNonExistent() ? null : $this->findHierarchyRelationByIds( - ContentStreamId::fromString($dataset['contentStreamId']), + $contentStreamDbId, $dimensionSpacePoint, NodeAggregateId::fromString($dataset['childNodeAggregateId']) ); return [ - 'contentstreamid' => $dataset['contentStreamId'], + 'contentstreamdbid' => $contentStreamDbId->value, 'dimensionspacepointhash' => $dimensionSpacePoint->hash, 'parentnodeanchor' => $parentHierarchyRelation !== null ? $parentHierarchyRelation['childnodeanchor'] : 9999999, 'childnodeanchor' => $childHierarchyRelation !== null ? $childHierarchyRelation['childnodeanchor'] : 8888888, @@ -296,14 +313,16 @@ private function findRelationAnchorPointByDataset(array $dataset): int $dimensionSpacePoint = DimensionSpacePoint::fromArray($dataset['originDimensionSpacePoint'] ?? $dataset['dimensionSpacePoint']); return $this->findHierarchyRelationByIds( - ContentStreamId::fromString($dataset['contentStreamId']), + $this->getContentStreamDbId( + ContentStreamId::fromString($dataset['contentStreamId']) + ), $dimensionSpacePoint, NodeAggregateId::fromString($dataset['nodeAggregateId'] ?? $dataset['childNodeAggregateId']) )['childnodeanchor']; } private function findHierarchyRelationByIds( - ContentStreamId $contentStreamId, + ContentStreamDbId $contentStreamDbId, DimensionSpacePoint $dimensionSpacePoint, NodeAggregateId $nodeAggregateId ): array { @@ -312,17 +331,17 @@ private function findHierarchyRelationByIds( FROM ' . $this->tableNames()->node() . ' n INNER JOIN ' . $this->tableNames()->hierarchyRelation() . ' h ON n.relationanchorpoint = h.childnodeanchor - WHERE n.nodeaggregateid = :nodeAggregateId - AND h.contentstreamid = :contentStreamId + WHERE h.contentstreamdbid = :contentStreamDbId + AND n.nodeaggregateid = :nodeAggregateId AND h.dimensionspacepointhash = :dimensionSpacePointHash', [ - 'contentStreamId' => $contentStreamId->value, + 'contentStreamDbId' => $contentStreamDbId->value, 'dimensionSpacePointHash' => $dimensionSpacePoint->hash, 'nodeAggregateId' => $nodeAggregateId->value ] )->fetchAssociative(); if ($nodeRecord === false) { - throw new \InvalidArgumentException(sprintf('Failed to find hierarchy relation for content stream "%s", dimension space point "%s" and node aggregate id "%s"', $contentStreamId->value, $dimensionSpacePoint->hash, $nodeAggregateId->value), 1708617712); + throw new \InvalidArgumentException(sprintf('Failed to find hierarchy relation for content stream "%s", dimension space point "%s" and node aggregate id "%s"', $contentStreamDbId->value, $dimensionSpacePoint->hash, $nodeAggregateId->value), 1708617712); } return $nodeRecord; @@ -338,6 +357,18 @@ private function transformPayloadTableToDataset(TableNode $payloadTable): array return $result; } + private function getContentStreamDbId(ContentStreamId $contentStreamId): ContentStreamDbId + { + return ContentStreamDbId::fromInt( + $this->dbal->executeQuery( + 'SELECT dbId FROM ' . $this->tableNames()->contentStream() . ' WHERE id = :contentStreamId', + [ + 'contentStreamId' => $contentStreamId->value, + ] + )->fetchOne() + ); + } + /** * @When /^I run integrity violation detection$/ */ From d82fd95bd9cb6ec68d61141c3a24575916abdc35 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:02:50 +0200 Subject: [PATCH 6/7] TASK: Assert events emitted during rebasing to document current state --- .../01-BasicFeatures.feature | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/01-BasicFeatures.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/01-BasicFeatures.feature index 0b56e0f12a9..85656761428 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/01-BasicFeatures.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W6-WorkspaceRebasing/01-BasicFeatures.feature @@ -58,12 +58,33 @@ Feature: Rebasing with no conflict When I am in workspace "user-test" and dimension space point {} Then I expect node aggregate identifier "sir-david-nodenborough" to lead to node user-cs-identifier;sir-david-nodenborough;{} - # only if the force flag is used we enforce a fork: + Scenario: Rebase via force creates new content stream even if there are no changes + # force flag used When the command RebaseWorkspace is executed with payload: | Key | Value | | workspaceName | "user-test" | | rebasedContentStreamId | "user-cs-rebased" | | rebaseErrorHandlingStrategy | "force" | + + Then I expect exactly 2 events to be published on stream with prefix "Workspace:user-test" + And event at index 1 is of type "WorkspaceWasRebased" with payload: + | Key | Expected | + | workspaceName | "user-test" | + | newContentStreamId | "user-cs-rebased" | + | previousContentStreamId | "user-cs-identifier" | + | skippedEvents | [] | + + Then I expect exactly 1 events to be published on stream with prefix "ContentStream:user-cs-rebased" + And event at index 0 is of type "ContentStreamWasForked" with payload: + | Key | Expected | + | newContentStreamId | "user-cs-rebased" | + | sourceContentStreamId | "cs-identifier" | + + Then I expect exactly 3 events to be published on stream with prefix "ContentStream:user-cs-identifier" + And event at index 2 is of type "ContentStreamWasRemoved" with payload: + | Key | Expected | + | contentStreamId | "user-cs-identifier" | + Then I expect the content stream "user-cs-identifier" to not exist Then I expect exactly 2 events to be published on stream with prefix "Workspace:user-test" @@ -90,6 +111,26 @@ Feature: Rebasing with no conflict | Key | Value | | workspaceName | "user-test" | | rebasedContentStreamId | "user-cs-rebased" | + + Then I expect exactly 2 events to be published on stream with prefix "Workspace:user-test" + And event at index 1 is of type "WorkspaceWasRebased" with payload: + | Key | Expected | + | workspaceName | "user-test" | + | newContentStreamId | "user-cs-rebased" | + | previousContentStreamId | "user-cs-identifier" | + | skippedEvents | [] | + + Then I expect exactly 1 events to be published on stream with prefix "ContentStream:user-cs-rebased" + And event at index 0 is of type "ContentStreamWasForked" with payload: + | Key | Expected | + | newContentStreamId | "user-cs-rebased" | + | sourceContentStreamId | "cs-identifier" | + + Then I expect exactly 3 events to be published on stream with prefix "ContentStream:user-cs-identifier" + And event at index 2 is of type "ContentStreamWasRemoved" with payload: + | Key | Expected | + | contentStreamId | "user-cs-identifier" | + Then I expect the content stream "user-cs-identifier" to not exist Then workspaces live,user-test have status UP_TO_DATE @@ -119,6 +160,26 @@ Feature: Rebasing with no conflict | Key | Value | | workspaceName | "user-test" | | rebasedContentStreamId | "user-cs-rebased" | + + Then I expect exactly 2 events to be published on stream with prefix "Workspace:user-test" + And event at index 1 is of type "WorkspaceWasRebased" with payload: + | Key | Expected | + | workspaceName | "user-test" | + | newContentStreamId | "user-cs-rebased" | + | previousContentStreamId | "user-cs-identifier" | + | skippedEvents | [] | + + Then I expect exactly 4 events to be published on stream with prefix "ContentStream:user-cs-rebased" + And event at index 0 is of type "ContentStreamWasForked" with payload: + | Key | Expected | + | newContentStreamId | "user-cs-rebased" | + | sourceContentStreamId | "cs-identifier" | + + Then I expect exactly 4 events to be published on stream with prefix "ContentStream:user-cs-identifier" + And event at index 3 is of type "ContentStreamWasRemoved" with payload: + | Key | Expected | + | contentStreamId | "user-cs-identifier" | + Then I expect the content stream "user-cs-identifier" to not exist Then workspaces live,user-test have status UP_TO_DATE From 203bcc4caf4d814e62a48ab3ae63cba942de6cb2 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:20:57 +0200 Subject: [PATCH 7/7] FEATURE: Non-breaking fast-forward content streams https://github.com/neos/neos-development-collection/issues/5264#issuecomment-4182943154 --- .../DoctrineDbalContentGraphProjection.php | 30 ++++++- .../Feature/ContentStreamForking.php | 82 +++++++++++++++++++ .../Repository/ContentStreamDbIdFinder.php | 5 ++ .../01-BasicFeatures.feature | 21 +++-- .../Event/ContentStreamWasForked.php | 3 + .../Classes/Feature/ContentStreamHandling.php | 1 + .../Feature/WorkspaceCommandHandler.php | 23 ++++-- 7 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/Feature/ContentStreamForking.php diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index cf76c111067..06640a4d751 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -9,6 +9,7 @@ use Doctrine\DBAL\Exception as DBALException; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamDbId; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\ContentStream; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\ContentStreamForking; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\NodeMove; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\NodeRemoval; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\Feature\NodeVariation; @@ -80,6 +81,7 @@ final class DoctrineDbalContentGraphProjection implements ContentGraphProjectionInterface { use ContentStream; + use ContentStreamForking; use NodeMove; use NodeRemoval; use NodeVariation; @@ -215,6 +217,27 @@ private function whenContentStreamWasCreated(ContentStreamWasCreated $event): vo private function whenContentStreamWasForked(ContentStreamWasForked $event): void { + if ($event->fastForwardFromContentStreamId) { + $fastForwardFromContentStreamDbId = $this->contentStreamDbIdFinder->getContentStreamDbId($event->fastForwardFromContentStreamId); + $sourceContentStreamDbId = $this->contentStreamDbIdFinder->getContentStreamDbId($event->sourceContentStreamId); + + // todo cleanup dangling old node records like in remove content stream! + $this->fastForwardHierarchyRelations($fastForwardFromContentStreamDbId, $sourceContentStreamDbId); + + // todo this is mean and removes the old content stream!!! + $this->dbal->update($this->tableNames->contentStream(), [ + 'id' => $event->newContentStreamId->value, + 'sourceContentStreamId' => $event->sourceContentStreamId->value, // todo this line needs further testing as we fast forward a content stream where its base stream was removed during its rebase + 'sourceContentStreamVersion' => $event->versionOfSourceContentStream->value, + 'closed' => 0 + ], [ + 'dbId' => $fastForwardFromContentStreamDbId->value + ]); + // todo hack :O + $this->contentStreamDbIdFinder->clearRuntimeCacheEntry($event->fastForwardFromContentStreamId); + return; + } + $this->createContentStream($event->newContentStreamId, $event->sourceContentStreamId, $event->versionOfSourceContentStream); $newContentStreamDbId = $this->contentStreamDbIdFinder->getContentStreamDbId($event->newContentStreamId); @@ -258,7 +281,12 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void private function whenContentStreamWasRemoved(ContentStreamWasRemoved $event): void { - $contentStreamDbId = $this->contentStreamDbIdFinder->getContentStreamDbId($event->contentStreamId); + try { + $contentStreamDbId = $this->contentStreamDbIdFinder->getContentStreamDbId($event->contentStreamId); + } catch (\RuntimeException) { + // not found, todo hacky + return; + } // Drop hierarchy relations $deleteHierarchyRelationStatement = <<tableNames->hierarchyRelation()} + WHERE + (dimensionspacepointhash, parentnodeanchor, childnodeanchor, contentstreamdbid) + IN ( + SELECT + dimensionspacepointhash, parentnodeanchor, childnodeanchor, contentstreamdbid + FROM + {$this->tableNames->hierarchyRelation()} h_source + WHERE + h_source.contentstreamdbid = :fastForwardContentStreamDbId + AND NOT EXISTS ( + SELECT + h_target.* + FROM + {$this->tableNames->hierarchyRelation()} h_target + WHERE + h_target.contentstreamdbid = :sourceContentStreamDbId + AND h_target.dimensionspacepointhash = h_source.dimensionspacepointhash + AND h_target.parentnodeanchor = h_source.parentnodeanchor + AND h_target.childnodeanchor = h_source.childnodeanchor + ) + ); + SQL; + + $copyNewExclusiveEdges = <<tableNames->hierarchyRelation()} (position, dimensionspacepointhash, parentnodeanchor, childnodeanchor, contentstreamdbid, subtreetags) + SELECT + position, dimensionspacepointhash, parentnodeanchor, childnodeanchor, :fastForwardContentStreamDbId AS contentstreamdbid, subtreetags + FROM + {$this->tableNames->hierarchyRelation()} h_target + WHERE + h_target.contentstreamdbid = :sourceContentStreamDbId + AND NOT EXISTS ( + SELECT + h_source.* + FROM + {$this->tableNames->hierarchyRelation()} h_source + WHERE + h_source.contentstreamdbid = :fastForwardContentStreamDbId + AND h_source.dimensionspacepointhash = h_target.dimensionspacepointhash + AND h_source.parentnodeanchor = h_target.parentnodeanchor + AND h_source.childnodeanchor = h_target.childnodeanchor + ); + SQL; + + try { + $this->dbal->executeStatement($removeStaleExclusiveEdges, [ + 'fastForwardContentStreamDbId' => $fastForwardContentStreamDbId->value, + 'sourceContentStreamDbId' => $sourceContentStreamDbId->value, + ]); + } catch (DBALException $e) { + throw new \RuntimeException(sprintf('Todo: %s', $e->getMessage()), 1716489211, $e); + } + try { + $this->dbal->executeStatement($copyNewExclusiveEdges, [ + 'fastForwardContentStreamDbId' => $fastForwardContentStreamDbId->value, + 'sourceContentStreamDbId' => $sourceContentStreamDbId->value, + ]); + } catch (DBALException $e) { + throw new \RuntimeException(sprintf('Todo: %s', $e->getMessage()), 1716489211, $e); + } + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamDbIdFinder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamDbIdFinder.php index b6f25ecc716..c60106170c8 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamDbIdFinder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentStreamDbIdFinder.php @@ -59,6 +59,11 @@ private function getFromRuntimeCache(ContentStreamId $contentStreamId): ?Content return $this->contentStreamIdRuntimeCache[$contentStreamId->value] ?? null; } + public function clearRuntimeCacheEntry(ContentStreamId $contentStreamId): void + { + unset($this->contentStreamIdRuntimeCache[$contentStreamId->value]); + } + private function fillRuntimeCacheFromDatabase(): void { $allContentStreamIdsStatement = << $this->newContentStreamId, 'sourceContentStreamId' => $this->sourceContentStreamId, 'versionOfSourceContentStream' => $this->versionOfSourceContentStream->value, + 'fastForwardFromContentStreamId' => $this->fastForwardFromContentStreamId?->value ]; } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php index 9f4ccdfa71f..0194a14a13d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php @@ -90,6 +90,7 @@ private function forkContentStream( $newContentStreamId, $sourceContentStreamId, $sourceContentStreamVersion, + fastForwardFromContentStreamId: null ), metadata: ['debug_reason' => $debugReason] ) diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 56e6048adc9..f6944abcb43 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -29,6 +29,7 @@ use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Event\ContentStreamWasClosed; use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Event\ContentStreamWasReopened; use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; +use Neos\ContentRepository\Core\Feature\ContentStreamForking\Event\ContentStreamWasForked; use Neos\ContentRepository\Core\Feature\ContentStreamRemoval\Event\ContentStreamWasRemoved; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; @@ -284,11 +285,21 @@ private function rebaseWorkspaceWithoutChanges( Version $baseWorkspaceContentStreamVersion, ContentStreamId $newContentStreamId ): \Generator { - yield $this->forkContentStream( - $newContentStreamId, - $baseWorkspace->currentContentStreamId, - $baseWorkspaceContentStreamVersion, - sprintf('Rebase empty workspace %s and fork base %s', $workspace->workspaceName->value, $baseWorkspace->workspaceName->value) + yield new EventsToPublish( + ContentStreamEventStreamName::fromContentStreamId($newContentStreamId)->getEventStreamName(), + Events::with( + DecoratedEvent::create( + event: new ContentStreamWasForked( + $newContentStreamId, + $baseWorkspace->currentContentStreamId, + $baseWorkspaceContentStreamVersion, + fastForwardFromContentStreamId: $workspace->currentContentStreamId + ), + metadata: ['debug_reason' => sprintf('Rebase empty workspace %s and fast-forward to base %s', $workspace->workspaceName->value, $baseWorkspace->workspaceName->value)] + ) + ), + // NO_STREAM to ensure the "fork" happens as the first event of the new content stream + ExpectedVersion::NO_STREAM() ); yield new EventsToPublish( @@ -304,6 +315,7 @@ private function rebaseWorkspaceWithoutChanges( ExpectedVersion::ANY() ); + // todo technically obsolete as we already fast-forward to a new cs stream yield $this->removeContentStreamWithoutConstraintChecks($workspace->currentContentStreamId); } @@ -356,6 +368,7 @@ private function handleRebaseWorkspace( if (!$workspace->hasPublishableChanges()) { // if we have no changes in the workspace we can fork from the base directly + // todo we close here but the fast-forward must reopen it yield $this->closeContentStream( $workspace->currentContentStreamId, $workspaceContentStreamVersion