diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php index 06f352beda3..cb43d749e47 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/Features/Bootstrap/ProjectionIntegrityViolationDetectionTrait.php @@ -239,7 +239,6 @@ private function transformDatasetToHierarchyRelationRecord(array $dataset): arra return [ 'contentstreamid' => $dataset['contentStreamId'], - 'dimensionspacepoint' => $dimensionSpacePoint->toJson(), 'dimensionspacepointhash' => $dimensionSpacePoint->hash, 'parentnodeanchor' => $parentHierarchyRelation !== null ? $parentHierarchyRelation['childnodeanchor'] : 9999999, 'childnodeanchor' => $childHierarchyRelation !== null ? $childHierarchyRelation['childnodeanchor'] : 8888888, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 89b2bbf5067..54e6296c3b3 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -15,6 +15,7 @@ use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRecord; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\NodeRelationAnchorPoint; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ContentGraph; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ProjectionContentGraph; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; @@ -90,6 +91,7 @@ public function __construct( private readonly NodeTypeManager $nodeTypeManager, private readonly ProjectionContentGraph $projectionContentGraph, private readonly string $tableNamePrefix, + private readonly DimensionSpacePointsRepository $dimensionSpacePointsRepository ) { $this->checkpointStorage = new DbalCheckpointStorage( $this->dbalClient->getConnection(), @@ -174,6 +176,7 @@ private function truncateDatabaseTables(): void $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_node'); $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_hierarchyrelation'); $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_referencerelation'); + $connection->executeQuery('TRUNCATE table ' . $this->tableNamePrefix . '_dimensionspacepoints'); } public function canHandle(EventInterface $event): bool @@ -599,7 +602,6 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void childnodeanchor, `name`, position, - dimensionspacepoint, dimensionspacepointhash, subtreetags, contentstreamid @@ -609,7 +611,6 @@ private function whenContentStreamWasForked(ContentStreamWasForked $event): void h.childnodeanchor, h.name, h.position, - h.dimensionspacepoint, h.dimensionspacepointhash, h.subtreetags, "' . $event->newContentStreamId->value . '" AS contentstreamid @@ -947,6 +948,8 @@ private function copyReferenceRelations( private function whenDimensionSpacePointWasMoved(DimensionSpacePointWasMoved $event): void { $this->transactional(function () use ($event) { + $this->dimensionSpacePointsRepository->insertDimensionSpacePoint($event->target); + // the ordering is important - we first update the OriginDimensionSpacePoints, as we need the // hierarchy relations for this query. Then, we update the Hierarchy Relations. @@ -985,7 +988,6 @@ function (NodeRecord $nodeRecord) use ($event) { ' UPDATE ' . $this->tableNamePrefix . '_hierarchyrelation h SET - h.dimensionspacepoint = :newDimensionSpacePoint, h.dimensionspacepointhash = :newDimensionSpacePointHash WHERE h.dimensionspacepointhash = :originalDimensionSpacePointHash @@ -994,7 +996,6 @@ function (NodeRecord $nodeRecord) use ($event) { [ 'originalDimensionSpacePointHash' => $event->source->hash, 'newDimensionSpacePointHash' => $event->target->hash, - 'newDimensionSpacePoint' => $event->target->toJson(), 'contentStreamId' => $event->contentStreamId->value ] ); @@ -1004,6 +1005,8 @@ function (NodeRecord $nodeRecord) use ($event) { private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded $event): void { $this->transactional(function () use ($event) { + $this->dimensionSpacePointsRepository->insertDimensionSpacePoint($event->target); + // 1) hierarchy relations $this->getDatabaseConnection()->executeStatement( ' @@ -1013,7 +1016,6 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded `name`, position, subtreetags, - dimensionspacepoint, dimensionspacepointhash, contentstreamid ) @@ -1023,7 +1025,6 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded h.name, h.position, h.subtreetags, - :newDimensionSpacePoint AS dimensionspacepoint, :newDimensionSpacePointHash AS dimensionspacepointhash, h.contentstreamid FROM @@ -1034,7 +1035,6 @@ private function whenDimensionShineThroughWasAdded(DimensionShineThroughWasAdded 'contentStreamId' => $event->contentStreamId->value, 'sourceDimensionSpacePointHash' => $event->source->hash, 'newDimensionSpacePointHash' => $event->target->hash, - 'newDimensionSpacePoint' => $event->target->toJson(), ] ); }); diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php index 546e574d4e2..64891e2cc33 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjectionFactory.php @@ -4,6 +4,7 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory; use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ProjectionContentGraph; use Neos\ContentRepository\Core\Factory\ProjectionFactoryDependencies; @@ -40,13 +41,16 @@ public function build( $projectionFactoryDependencies->contentRepositoryId ); + $dimensionSpacePointsRepository = new DimensionSpacePointsRepository($this->dbalClient->getConnection(), $tableNamePrefix); + return new ContentGraphProjection( new DoctrineDbalContentGraphProjection( $this->dbalClient, new NodeFactory( $projectionFactoryDependencies->contentRepositoryId, $projectionFactoryDependencies->nodeTypeManager, - $projectionFactoryDependencies->propertyConverter + $projectionFactoryDependencies->propertyConverter, + $dimensionSpacePointsRepository ), $projectionFactoryDependencies->contentRepositoryId, $projectionFactoryDependencies->nodeTypeManager, @@ -54,7 +58,8 @@ public function build( $this->dbalClient, $tableNamePrefix ), - $tableNamePrefix + $tableNamePrefix, + $dimensionSpacePointsRepository ) ); } diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php index b260a7ad155..06cc35ff431 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphSchemaBuilder.php @@ -28,6 +28,7 @@ public function buildSchema(AbstractSchemaManager $schemaManager): Schema $this->createNodeTable(), $this->createHierarchyRelationTable(), $this->createReferenceRelationTable(), + $this->createDimensionSpacePointsTable() ]); } @@ -36,7 +37,6 @@ private function createNodeTable(): Table $table = new Table($this->tableNamePrefix . '_node', [ DbalSchemaFactory::columnForNodeAnchorPoint('relationanchorpoint')->setAutoincrement(true), DbalSchemaFactory::columnForNodeAggregateId('nodeaggregateid')->setNotnull(false), - DbalSchemaFactory::columnForDimensionSpacePoint('origindimensionspacepoint')->setNotnull(false), DbalSchemaFactory::columnForDimensionSpacePointHash('origindimensionspacepointhash')->setNotnull(false), DbalSchemaFactory::columnForNodeTypeName('nodetypename'), (new Column('properties', Type::getType(Types::TEXT)))->setNotnull(true)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION), @@ -56,10 +56,9 @@ private function createNodeTable(): Table private function createHierarchyRelationTable(): Table { $table = new Table($this->tableNamePrefix . '_hierarchyrelation', [ - (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(false)->setCustomSchemaOption('collation', self::DEFAULT_TEXT_COLLATION), + (new Column('name', Type::getType(Types::STRING)))->setLength(255)->setNotnull(false)->setCustomSchemaOption('charset', 'ascii')->setCustomSchemaOption('collation', 'ascii_general_ci'), (new Column('position', Type::getType(Types::INTEGER)))->setNotnull(true), DbalSchemaFactory::columnForContentStreamId('contentstreamid')->setNotnull(true), - DbalSchemaFactory::columnForDimensionSpacePoint('dimensionspacepoint')->setNotnull(true), DbalSchemaFactory::columnForDimensionSpacePointHash('dimensionspacepointhash')->setNotnull(true), DbalSchemaFactory::columnForNodeAnchorPoint('parentnodeanchor'), DbalSchemaFactory::columnForNodeAnchorPoint('childnodeanchor'), @@ -67,13 +66,21 @@ private function createHierarchyRelationTable(): Table ]); return $table - ->addIndex(['childnodeanchor']) - ->addIndex(['contentstreamid']) - ->addIndex(['parentnodeanchor']) - ->addIndex(['contentstreamid', 'childnodeanchor', 'dimensionspacepointhash']) + ->setPrimaryKey(['childnodeanchor', 'contentstreamid', 'dimensionspacepointhash', 'parentnodeanchor']) ->addIndex(['contentstreamid', 'dimensionspacepointhash']); } + private function createDimensionSpacePointsTable(): Table + { + $table = new Table($this->tableNamePrefix . '_dimensionspacepoints', [ + DbalSchemaFactory::columnForDimensionSpacePointHash('hash')->setNotnull(true), + DbalSchemaFactory::columnForDimensionSpacePoint('dimensionspacepoint')->setNotnull(true) + ]); + + return $table + ->setPrimaryKey(['hash']); + } + private function createReferenceRelationTable(): Table { $table = new Table($this->tableNamePrefix . '_referencerelation', [ diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php index b5e6a3c38eb..8bc02f1e7d3 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/HierarchyRelation.php @@ -16,6 +16,7 @@ use Doctrine\DBAL\Connection; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -44,16 +45,22 @@ public function __construct( */ public function addToDatabase(Connection $databaseConnection, string $tableNamePrefix): void { - $databaseConnection->insert($tableNamePrefix . '_hierarchyrelation', [ - 'parentnodeanchor' => $this->parentNodeAnchor->value, - 'childnodeanchor' => $this->childNodeAnchor->value, - 'name' => $this->name?->value, - 'contentstreamid' => $this->contentStreamId->value, - 'dimensionspacepoint' => $this->dimensionSpacePoint->toJson(), - 'dimensionspacepointhash' => $this->dimensionSpacePointHash, - 'position' => $this->position, - 'subtreetags' => json_encode($this->subtreeTags, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT), - ]); + $databaseConnection->transactional(function ($databaseConnection) use ( + $tableNamePrefix + ) { + $dimensionSpacePoints = new DimensionSpacePointsRepository($databaseConnection, $tableNamePrefix); + $dimensionSpacePoints->insertDimensionSpacePoint($this->dimensionSpacePoint); + + $databaseConnection->insert($tableNamePrefix . '_hierarchyrelation', [ + 'parentnodeanchor' => $this->parentNodeAnchor->value, + 'childnodeanchor' => $this->childNodeAnchor->value, + 'name' => $this->name?->value, + 'contentstreamid' => $this->contentStreamId->value, + 'dimensionspacepointhash' => $this->dimensionSpacePointHash, + 'position' => $this->position, + 'subtreetags' => json_encode($this->subtreeTags, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT), + ]); + }); } /** diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php index d8c3087e7cf..fbc75314807 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/NodeRecord.php @@ -15,7 +15,9 @@ namespace Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\DBAL\Types\Types; +use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\DimensionSpacePointsRepository; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; @@ -55,7 +57,6 @@ public function updateToDatabase(Connection $databaseConnection, string $tableNa $tableNamePrefix . '_node', [ 'nodeaggregateid' => $this->nodeAggregateId->value, - 'origindimensionspacepoint' => json_encode($this->originDimensionSpacePoint), 'origindimensionspacepointhash' => $this->originDimensionSpacePointHash, 'properties' => json_encode($this->properties), 'nodetypename' => $this->nodeTypeName->value, @@ -138,25 +139,38 @@ public static function createNewInDatabase( ?NodeName $nodeName, Timestamps $timestamps, ): self { - $databaseConnection->insert($tableNamePrefix . '_node', [ - 'nodeaggregateid' => $nodeAggregateId->value, - 'origindimensionspacepoint' => json_encode($originDimensionSpacePoint), - 'origindimensionspacepointhash' => $originDimensionSpacePointHash, - 'properties' => json_encode($properties), - 'nodetypename' => $nodeTypeName->value, - 'classification' => $classification->value, - 'created' => $timestamps->created, - 'originalcreated' => $timestamps->originalCreated, - 'lastmodified' => $timestamps->lastModified, - 'originallastmodified' => $timestamps->originalLastModified, - ], [ - 'created' => Types::DATETIME_IMMUTABLE, - 'originalcreated' => Types::DATETIME_IMMUTABLE, - 'lastmodified' => Types::DATETIME_IMMUTABLE, - 'originallastmodified' => Types::DATETIME_IMMUTABLE, - ]); + $relationAnchorPoint = $databaseConnection->transactional(function ($databaseConnection) use ( + $tableNamePrefix, + $nodeAggregateId, + $originDimensionSpacePoint, + $originDimensionSpacePointHash, + $properties, + $nodeTypeName, + $classification, + $timestamps + ) { + $dimensionSpacePoints = new DimensionSpacePointsRepository($databaseConnection, $tableNamePrefix); + $dimensionSpacePoints->insertDimensionSpacePointByHashAndCoordinates($originDimensionSpacePointHash, $originDimensionSpacePoint); + + $databaseConnection->insert($tableNamePrefix . '_node', [ + 'nodeaggregateid' => $nodeAggregateId->value, + 'origindimensionspacepointhash' => $originDimensionSpacePointHash, + 'properties' => json_encode($properties), + 'nodetypename' => $nodeTypeName->value, + 'classification' => $classification->value, + 'created' => $timestamps->created, + 'originalcreated' => $timestamps->originalCreated, + 'lastmodified' => $timestamps->lastModified, + 'originallastmodified' => $timestamps->originalLastModified, + ], [ + 'created' => Types::DATETIME_IMMUTABLE, + 'originalcreated' => Types::DATETIME_IMMUTABLE, + 'lastmodified' => Types::DATETIME_IMMUTABLE, + 'originallastmodified' => Types::DATETIME_IMMUTABLE, + ]); - $relationAnchorPoint = NodeRelationAnchorPoint::fromInteger((int)$databaseConnection->lastInsertId()); + return NodeRelationAnchorPoint::fromInteger((int)$databaseConnection->lastInsertId()); + }); return new self( $relationAnchorPoint, diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php index 46b9d8a5be4..adb394fb6cd 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Projection/ProjectionIntegrityViolationDetector.php @@ -67,8 +67,8 @@ public function hierarchyIntegrityIsProvided(): Result } $invalidlyHashedHierarchyRelationRecords = $this->client->getConnection()->executeQuery( - 'SELECT * FROM ' . $this->tableNamePrefix . '_hierarchyrelation - WHERE dimensionspacepointhash != MD5(dimensionspacepoint)' + 'SELECT * FROM ' . $this->tableNamePrefix . '_hierarchyrelation h LEFT JOIN ' . $this->tableNamePrefix . '_dimensionspacepoints dsp ON dsp.hash = h.dimensionspacepointhash + HAVING dsp.dimensionspacepoint IS NULL' )->fetchAllAssociative(); foreach ($invalidlyHashedHierarchyRelationRecords as $record) { @@ -118,6 +118,12 @@ public function siblingsAreDistinctlySorted(): Result HAVING COUNT(position) > 1' ); + if ($ambiguouslySortedHierarchyRelationRecords->columnCount() === 0) { + return $result; + } + + $dimensionSpacePoints = $this->findProjectedDimensionSpacePoints(); + foreach ($ambiguouslySortedHierarchyRelationRecords as $hierarchyRelationRecord) { $ambiguouslySortedNodeRecords = $this->client->getConnection()->executeQuery( 'SELECT nodeaggregateid @@ -133,7 +139,7 @@ public function siblingsAreDistinctlySorted(): Result return $record['nodeaggregateid']; }, $ambiguouslySortedNodeRecords)) . ' are ambiguously sorted in content stream ' . $hierarchyRelationRecord['contentstreamid'] - . ' and dimension space point ' . $hierarchyRelationRecord['dimensionspacepoint'], + . ' and dimension space point ' . $dimensionSpacePoints[$hierarchyRelationRecord['dimensionspacepointhash']]?->toJson(), self::ERROR_CODE_SIBLINGS_ARE_AMBIGUOUSLY_SORTED )); } @@ -516,7 +522,7 @@ public function childNodeCoverageIsASubsetOfParentNodeCoverage(): Result $result = new Result(); foreach ($this->findProjectedContentStreamIds() as $contentStreamId) { $excessivelyCoveringNodeRecords = $this->client->getConnection()->executeQuery( - 'SELECT n.nodeaggregateid, c.dimensionspacepoint + 'SELECT n.nodeaggregateid, c.dimensionspacepointhash FROM ' . $this->tableNamePrefix . '_hierarchyrelation c INNER JOIN ' . $this->tableNamePrefix . '_node n ON c.childnodeanchor = n.relationanchorpoint @@ -535,7 +541,7 @@ public function childNodeCoverageIsASubsetOfParentNodeCoverage(): Result $result->addError(new Error( 'Node aggregate ' . $excessivelyCoveringNodeRecord['nodeaggregateid'] . ' in content stream ' . $contentStreamId->value - . ' covers dimension space point ' . $excessivelyCoveringNodeRecord['dimensionspacepoint'] + . ' 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 )); @@ -553,7 +559,7 @@ public function allNodesCoverTheirOrigin(): Result $result = new Result(); foreach ($this->findProjectedContentStreamIds() as $contentStreamId) { $nodeRecordsWithMissingOriginCoverage = $this->client->getConnection()->executeQuery( - 'SELECT nodeaggregateid, origindimensionspacepoint + 'SELECT nodeaggregateid, origindimensionspacepointhash FROM ' . $this->tableNamePrefix . '_node n INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint @@ -579,7 +585,7 @@ public function allNodesCoverTheirOrigin(): Result $result->addError(new Error( 'Node aggregate ' . $nodeRecord['nodeaggregateid'] . ' in content stream ' . $contentStreamId->value - . ' does not cover its origin dimension space point ' . $nodeRecord['origindimensionspacepoint'] + . ' does not cover its origin dimension space point hash ' . $nodeRecord['origindimensionspacepointhash'] . '.', self::ERROR_CODE_NODE_DOES_NOT_COVER_ITS_ORIGIN )); @@ -615,7 +621,7 @@ protected function findProjectedContentStreamIds(): iterable protected function findProjectedDimensionSpacePoints(): DimensionSpacePointSet { $records = $this->client->getConnection()->executeQuery( - 'SELECT DISTINCT dimensionspacepoint FROM ' . $this->tableNamePrefix . '_hierarchyrelation' + 'SELECT dimensionspacepoint FROM ' . $this->tableNamePrefix . '_dimensionspacepoints' )->fetchAllAssociative(); $records = array_map(function (array $record) { diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php index 93381ed2fe5..1df35d06fa3 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentGraph.php @@ -56,6 +56,9 @@ * - cn -> child node * - h -> the hierarchy edge connecting parent and child * - ph -> the hierarchy edge incoming to the parent (sometimes relevant) + * - dsp -> dimension space point, resolves hashes to full dimension coordinates + * - cdsp -> child dimension space point, same as dsp for child queries + * - pdsp -> parent dimension space point, same as dsp for parent queries * * @internal the parent interface {@see ContentGraphInterface} is API */ @@ -137,9 +140,10 @@ public function findRootNodeAggregates( FindRootNodeAggregatesFilter $filter, ): NodeAggregates { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.name, h.subtreetags, h.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamid, h.name, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') ->where('h.contentstreamid = :contentStreamId') ->andWhere('h.parentnodeanchor = :rootEdgeParentAnchorId') ->setParameters([ @@ -160,9 +164,10 @@ public function findNodeAggregatesByType( NodeTypeName $nodeTypeName ): iterable { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.contentstreamid, h.name, h.subtreetags, h.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.contentstreamid, h.name, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') ->where('h.contentstreamid = :contentStreamId') ->andWhere('n.nodetypename = :nodeTypeName') ->setParameters([ @@ -177,9 +182,10 @@ public function findNodeAggregateById( NodeAggregateId $nodeAggregateId ): ?NodeAggregate { $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid, h.subtreetags, h.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.name, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_hierarchyrelation', 'h') ->innerJoin('h', $this->tableNamePrefix . '_node', 'n', 'n.relationanchorpoint = h.childnodeanchor') + ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') ->where('n.nodeaggregateid = :nodeAggregateId') ->andWhere('h.contentstreamid = :contentStreamId') ->setParameters([ @@ -202,11 +208,12 @@ public function findParentNodeAggregates( NodeAggregateId $childNodeAggregateId ): iterable { $queryBuilder = $this->createQueryBuilder() - ->select('pn.*, ph.name, ph.contentstreamid, ph.subtreetags, ph.dimensionspacepoint AS covereddimensionspacepoint') + ->select('pn.*, ph.name, ph.contentstreamid, ph.subtreetags, pdsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') + ->innerJoin('ph', $this->tableNamePrefix . '_dimensionspacepoints', 'pdsp', 'pdsp.hash = ph.dimensionspacepointhash') ->where('cn.nodeaggregateid = :nodeAggregateId') ->andWhere('ph.contentstreamid = :contentStreamId') ->andWhere('ch.contentstreamid = :contentStreamId') @@ -234,9 +241,10 @@ public function findParentNodeAggregateByChildOriginDimensionSpacePoint( ->andWhere('cn.origindimensionspacepointhash = :childOriginDimensionSpacePointHash'); $queryBuilder = $this->createQueryBuilder() - ->select('n.*, h.name, h.contentstreamid, h.subtreetags, h.dimensionspacepoint AS covereddimensionspacepoint') + ->select('n.*, h.name, h.contentstreamid, h.subtreetags, dsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'n') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'h', 'h.childnodeanchor = n.relationanchorpoint') + ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') ->where('n.nodeaggregateid = (' . $subQueryBuilder->getSQL() . ')') ->andWhere('h.contentstreamid = :contentStreamId') ->setParameters([ @@ -306,9 +314,10 @@ public function getDimensionSpacePointsOccupiedByChildNodeName( DimensionSpacePointSet $dimensionSpacePointsToCheck ): DimensionSpacePointSet { $queryBuilder = $this->createQueryBuilder() - ->select('h.dimensionspacepoint, h.dimensionspacepointhash') + ->select('dsp.dimensionspacepoint, h.dimensionspacepointhash') ->from($this->tableNamePrefix . '_hierarchyrelation', 'h') ->innerJoin('h', $this->tableNamePrefix . '_node', 'n', 'n.relationanchorpoint = h.parentnodeanchor') + ->innerJoin('h', $this->tableNamePrefix . '_dimensionspacepoints', 'dsp', 'dsp.hash = h.dimensionspacepointhash') ->innerJoin('n', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = n.relationanchorpoint') ->where('n.nodeaggregateid = :parentNodeAggregateId') ->andWhere('n.origindimensionspacepointhash = :parentNodeOriginDimensionSpacePointHash') @@ -368,10 +377,11 @@ public function getSubgraphs(): array private function buildChildNodeAggregateQuery(NodeAggregateId $parentNodeAggregateId, ContentStreamId $contentStreamId): QueryBuilder { return $this->createQueryBuilder() - ->select('cn.*, ch.name, ch.contentstreamid, ch.subtreetags, ch.dimensionspacepoint AS covereddimensionspacepoint') + ->select('cn.*, ch.name, ch.contentstreamid, ch.subtreetags, cdsp.dimensionspacepoint AS covereddimensionspacepoint') ->from($this->tableNamePrefix . '_node', 'pn') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ph', 'ph.childnodeanchor = pn.relationanchorpoint') ->innerJoin('pn', $this->tableNamePrefix . '_hierarchyrelation', 'ch', 'ch.parentnodeanchor = pn.relationanchorpoint') + ->innerJoin('ch', $this->tableNamePrefix . '_dimensionspacepoints', 'cdsp', 'cdsp.hash = ch.dimensionspacepointhash') ->innerJoin('ch', $this->tableNamePrefix . '_node', 'cn', 'cn.relationanchorpoint = ch.childnodeanchor') ->where('pn.nodeaggregateid = :parentNodeAggregateId') ->andWhere('ph.contentstreamid = :contentStreamId') diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/DimensionSpacePointsRepository.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/DimensionSpacePointsRepository.php new file mode 100644 index 00000000000..1a409b5abc6 --- /dev/null +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/DimensionSpacePointsRepository.php @@ -0,0 +1,104 @@ + + */ + private array $dimensionspacePointsRuntimeCache = []; + + public function __construct( + private readonly Connection $databaseConnection, + private readonly string $tableNamePrefix + ) { + } + + public function insertDimensionSpacePoint(AbstractDimensionSpacePoint $dimensionSpacePoint): void + { + if ($this->getCoordinatesByHashFromRuntimeCache($dimensionSpacePoint->hash) !== null) { + return; + } + + $this->dimensionspacePointsRuntimeCache[$dimensionSpacePoint->hash] = $dimensionSpacePoint->toJson(); + $this->writeDimensionSpacePoint($dimensionSpacePoint->hash, $dimensionSpacePoint->toJson()); + } + + /** + * @param string $hash + * @param array $dimensionSpacePointCoordinates + * @return void + * @throws \Doctrine\DBAL\Exception + */ + public function insertDimensionSpacePointByHashAndCoordinates(string $hash, array $dimensionSpacePointCoordinates): void + { + if ($this->getCoordinatesByHashFromRuntimeCache($hash) !== null) { + return; + } + + $dimensionSpacePointCoordinatesJson = json_encode($dimensionSpacePointCoordinates, JSON_THROW_ON_ERROR); + $this->dimensionspacePointsRuntimeCache[$hash] = $dimensionSpacePointCoordinatesJson; + $this->writeDimensionSpacePoint($hash, $dimensionSpacePointCoordinatesJson); + } + + public function getOriginDimensionSpacePointByHash(string $hash): OriginDimensionSpacePoint + { + $coordinates = $this->getCoordinatesByHashFromRuntimeCache($hash); + if ($coordinates === null) { + $this->fillRuntimeCacheFromDatabase(); + $coordinates = $this->getCoordinatesByHashFromRuntimeCache($hash); + } + + if ($coordinates === null) { + throw new \RuntimeException(sprintf('A DimensionSpacePoint record with the given hash "%s" was not found in the projection, cannot determine coordinates.', $hash), 1710335509); + } + + return OriginDimensionSpacePoint::fromJsonString($coordinates); + } + + private function writeDimensionSpacePoint(string $hash, string $dimensionSpacePointCoordinatesJson): void + { + $this->databaseConnection->executeStatement( + 'INSERT IGNORE INTO ' . $this->tableNamePrefix . '_dimensionspacepoints (hash, dimensionspacepoint) VALUES (:dimensionspacepointhash, :dimensionspacepoint)', + [ + 'dimensionspacepointhash' => $hash, + 'dimensionspacepoint' => $dimensionSpacePointCoordinatesJson + ] + ); + } + + private function getCoordinatesByHashFromRuntimeCache(string $hash): ?string + { + return $this->dimensionspacePointsRuntimeCache[$hash] ?? null; + } + + private function fillRuntimeCacheFromDatabase(): void + { + $allDimensionSpacePoints = $this->databaseConnection->fetchAllAssociative('SELECT hash, dimensionspacepoint FROM ' . $this->tableNamePrefix . '_dimensionspacepoints'); + foreach ($allDimensionSpacePoints as $dimensionSpacePointRow) { + $this->dimensionspacePointsRuntimeCache[(string)$dimensionSpacePointRow['hash']] = (string)$dimensionSpacePointRow['dimensionspacepoint']; + } + } +} diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php index 837b7767eda..88887b67d9a 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/NodeFactory.php @@ -54,7 +54,8 @@ final class NodeFactory public function __construct( private readonly ContentRepositoryId $contentRepositoryId, private readonly NodeTypeManager $nodeTypeManager, - private readonly PropertyConverter $propertyConverter + private readonly PropertyConverter $propertyConverter, + private readonly DimensionSpacePointsRepository $dimensionSpacePointRepository ) { } @@ -79,7 +80,7 @@ public function mapNodeRowToNode( $visibilityConstraints ), NodeAggregateId::fromString($nodeRow['nodeaggregateid']), - OriginDimensionSpacePoint::fromJsonString($nodeRow['origindimensionspacepoint']), + $this->dimensionSpacePointRepository->getOriginDimensionSpacePointByHash($nodeRow['origindimensionspacepointhash']), NodeAggregateClassification::from($nodeRow['classification']), NodeTypeName::fromString($nodeRow['nodetypename']), $nodeType, @@ -169,9 +170,7 @@ public function mapNodeRowsToNodeAggregate( foreach ($nodeRows as $nodeRow) { // A node can occupy exactly one DSP and cover multiple ones... - $occupiedDimensionSpacePoint = OriginDimensionSpacePoint::fromJsonString( - $nodeRow['origindimensionspacepoint'] - ); + $occupiedDimensionSpacePoint = $this->dimensionSpacePointRepository->getOriginDimensionSpacePointByHash($nodeRow['origindimensionspacepointhash']); if (!isset($nodesByOccupiedDimensionSpacePoints[$occupiedDimensionSpacePoint->hash])) { // ... so we handle occupation exactly once ... $nodesByOccupiedDimensionSpacePoints[$occupiedDimensionSpacePoint->hash] = $this->mapNodeRowToNode( @@ -248,9 +247,7 @@ public function mapNodeRowsToNodeAggregates( foreach ($nodeRows as $nodeRow) { // A node can occupy exactly one DSP and cover multiple ones... $rawNodeAggregateId = $nodeRow['nodeaggregateid']; - $occupiedDimensionSpacePoint = OriginDimensionSpacePoint::fromJsonString( - $nodeRow['origindimensionspacepoint'] - ); + $occupiedDimensionSpacePoint = $this->dimensionSpacePointRepository->getOriginDimensionSpacePointByHash($nodeRow['origindimensionspacepointhash']); if ( !isset($nodesByOccupiedDimensionSpacePointsByNodeAggregate [$rawNodeAggregateId][$occupiedDimensionSpacePoint->hash]) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php index ce2552fd2fc..02e6b63e5b9 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ProjectionContentGraph.php @@ -71,10 +71,11 @@ public function findParentNode( : $originDimensionSpacePoint->hash ]; $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT p.*, ph.contentstreamid, ph.name, ph.subtreetags FROM ' . $this->tableNamePrefix . '_node p + 'SELECT p.*, ph.contentstreamid, ph.name, ph.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node p INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ph ON ph.childnodeanchor = p.relationanchorpoint INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation ch ON ch.parentnodeanchor = p.relationanchorpoint INNER JOIN ' . $this->tableNamePrefix . '_node c ON ch.childnodeanchor = c.relationanchorpoint + INNER JOIN ' . $this->tableNamePrefix . '_dimensionspacepoints dsp ON p.origindimensionspacepointhash = dsp.hash WHERE c.nodeaggregateid = :childNodeAggregateId AND c.origindimensionspacepointhash = :originDimensionSpacePointHash AND ph.contentstreamid = :contentStreamId @@ -101,8 +102,9 @@ public function findNodeInAggregate( DimensionSpacePoint $coveredDimensionSpacePoint ): ?NodeRecord { $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT n.*, h.name, h.subtreetags FROM ' . $this->tableNamePrefix . '_node n + 'SELECT n.*, h.name, h.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node n INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint + INNER JOIN ' . $this->tableNamePrefix . '_dimensionspacepoints dsp ON n.origindimensionspacepointhash = dsp.hash WHERE n.nodeaggregateid = :nodeAggregateId AND h.contentstreamid = :contentStreamId AND h.dimensionspacepointhash = :dimensionSpacePointHash', @@ -129,8 +131,9 @@ public function findNodeByIds( OriginDimensionSpacePoint $originDimensionSpacePoint ): ?NodeRecord { $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT n.*, h.name, h.subtreetags FROM ' . $this->tableNamePrefix . '_node n + 'SELECT n.*, h.name, h.subtreetags, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node n INNER JOIN ' . $this->tableNamePrefix . '_hierarchyrelation h ON h.childnodeanchor = n.relationanchorpoint + INNER JOIN ' . $this->tableNamePrefix . '_dimensionspacepoints dsp ON n.origindimensionspacepointhash = dsp.hash WHERE n.nodeaggregateid = :nodeAggregateId AND n.origindimensionspacepointhash = :originDimensionSpacePointHash AND h.contentstreamid = :contentStreamId', @@ -216,7 +219,8 @@ public function getAnchorPointsForNodeAggregateInContentStream( public function getNodeByAnchorPoint(NodeRelationAnchorPoint $nodeRelationAnchorPoint): ?NodeRecord { $nodeRow = $this->getDatabaseConnection()->executeQuery( - 'SELECT n.* FROM ' . $this->tableNamePrefix . '_node n + 'SELECT n.*, dsp.dimensionspacepoint AS origindimensionspacepoint FROM ' . $this->tableNamePrefix . '_node n + INNER JOIN ' . $this->tableNamePrefix . '_dimensionspacepoints dsp ON n.origindimensionspacepointhash = dsp.hash WHERE n.relationanchorpoint = :relationAnchorPoint', [ 'relationAnchorPoint' => $nodeRelationAnchorPoint->value, @@ -648,12 +652,14 @@ public function findDescendantNodeAggregateIds( */ protected function mapRawDataToHierarchyRelation(array $rawData): HierarchyRelation { + $dimensionspacepointRaw = $this->client->getConnection()->fetchOne('SELECT dimensionspacepoint FROM ' . $this->tableNamePrefix . '_dimensionspacepoints WHERE hash = :hash', ['hash' => $rawData['dimensionspacepointhash']]); + return new HierarchyRelation( NodeRelationAnchorPoint::fromInteger((int)$rawData['parentnodeanchor']), NodeRelationAnchorPoint::fromInteger((int)$rawData['childnodeanchor']), $rawData['name'] ? NodeName::fromString($rawData['name']) : null, ContentStreamId::fromString($rawData['contentstreamid']), - DimensionSpacePoint::fromJsonString($rawData['dimensionspacepoint']), + DimensionSpacePoint::fromJsonString($dimensionspacepointRaw), $rawData['dimensionspacepointhash'], (int)$rawData['position'], NodeFactory::extractNodeTagsFromJson($rawData['subtreetags']),