Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TASK: Early termination of findDescendantNodes with limit1 #5436

Draft
wants to merge 1 commit into
base: 9.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -474,10 +474,10 @@
$this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager));
}
if ($filter->searchTerm !== null) {
$this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->searchTerm);
$queryBuilder->andWhere($this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->searchTerm));
}
if ($filter->propertyValue !== null) {
$this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->propertyValue);
$queryBuilder->andWhere($this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->propertyValue));
}
$this->addSubtreeTagConstraints($queryBuilder);
return $queryBuilder;
Expand Down Expand Up @@ -505,16 +505,16 @@
$this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), "{$destinationTablePrefix}n");
}
if ($filter->nodeSearchTerm !== null) {
$this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->nodeSearchTerm, "{$destinationTablePrefix}n");
$queryBuilder->andWhere($this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->nodeSearchTerm, "{$destinationTablePrefix}n"));
}
if ($filter->nodePropertyValue !== null) {
$this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->nodePropertyValue, "{$destinationTablePrefix}n");
$queryBuilder->andWhere($this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->nodePropertyValue, "{$destinationTablePrefix}n"));
}
if ($filter->referenceSearchTerm !== null) {
$this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->referenceSearchTerm, 'r');
$queryBuilder->andWhere($this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->referenceSearchTerm, 'r'));
}
if ($filter->referencePropertyValue !== null) {
$this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->referencePropertyValue, 'r');
$queryBuilder->andWhere($this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->referencePropertyValue, 'r'));
}
if ($filter->referenceName !== null) {
$queryBuilder->andWhere('r.name = :referenceName')->setParameter('referenceName', $filter->referenceName->value);
Expand Down Expand Up @@ -543,10 +543,10 @@
$this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilder, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager));
}
if ($filter->searchTerm !== null) {
$this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->searchTerm);
$queryBuilder->andWhere($this->nodeQueryBuilder->addSearchTermConstraints($queryBuilder, $filter->searchTerm));
}
if ($filter->propertyValue !== null) {
$this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->propertyValue);
$queryBuilder->andWhere($this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilder, $filter->propertyValue));
}
if ($filter->pagination !== null) {
$this->applyPagination($queryBuilder, $filter->pagination);
Expand Down Expand Up @@ -595,9 +595,20 @@
*/
private function buildDescendantNodesQueries(NodeAggregateId $entryNodeAggregateId, FindDescendantNodesFilter|CountDescendantNodesFilter $filter): array
{
$queryBuilderInitial = $this->createQueryBuilder()
$queryBuilderInitial = $this->createQueryBuilder();

// todo optimise also if ordering is ASC
$optimize = $filter->pagination !== null && $filter->pagination->limit === 1 && $filter->pagination->offset === 0 && $filter->ordering === null;

Check failure on line 601 in Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test linting-unit-functionaltests-mysql (deps: highest)

Access to an undefined property Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountDescendantNodesFilter|Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter::$ordering.

Check failure on line 601 in Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test linting-unit-functionaltests-mysql (deps: highest)

Access to an undefined property Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountDescendantNodesFilter|Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter::$pagination.

Check failure on line 601 in Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test linting-unit-functionaltests-mysql (deps: highest)

Access to an undefined property Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountDescendantNodesFilter|Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter::$ordering.

Check failure on line 601 in Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test linting-unit-functionaltests-mysql (deps: highest)

Access to an undefined property Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountDescendantNodesFilter|Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter::$pagination.

$nodeMatcher = (string)$queryBuilderInitial->expr()->and(...(array_filter([

Check failure on line 603 in Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test linting-unit-functionaltests-mysql (deps: highest)

Method Doctrine\DBAL\Query\Expression\ExpressionBuilder::and() invoked with 0 parameters, at least 1 required.

Check failure on line 603 in Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test linting-unit-functionaltests-mysql (deps: highest)

Method Doctrine\DBAL\Query\Expression\ExpressionBuilder::and() invoked with 0 parameters, at least 1 required.
$filter->nodeTypes !== null ? $this->nodeQueryBuilder->addNodeTypeCriteria2($queryBuilderInitial, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager)) : '',
$filter->searchTerm !== null ? $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilderInitial, $filter->searchTerm) : '',
$filter->propertyValue !== null ? $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilderInitial, $filter->propertyValue) : '',
]) ?: ['true'])); // todo empty true case

$queryBuilderInitial
// @see https://mariadb.com/kb/en/library/recursive-common-table-expressions-overview/#cast-to-avoid-data-truncation
->select('n.*, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position')
->select('n.*, h.subtreetags, CAST("ROOT" AS CHAR(50)) AS parentNodeAggregateId, 0 AS level, 0 AS position' . ($optimize ? ", CASE WHEN $nodeMatcher THEN 1 ELSE 0 END AS isMatch" : ''))
->from($this->nodeQueryBuilder->tableNames->node(), 'n')
// we need to join with the hierarchy relation, because we need the node name.
->innerJoin('n', $this->nodeQueryBuilder->tableNames->hierarchyRelation(), 'h', 'h.childnodeanchor = n.relationanchorpoint')
Expand All @@ -610,24 +621,40 @@
->andWhere('p.nodeaggregateid = :entryNodeAggregateId');
$this->addSubtreeTagConstraints($queryBuilderInitial);

$nodeMatcher = (string)$queryBuilderInitial->expr()->and(...(array_filter([

Check failure on line 624 in Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test linting-unit-functionaltests-mysql (deps: highest)

Method Doctrine\DBAL\Query\Expression\ExpressionBuilder::and() invoked with 0 parameters, at least 1 required.

Check failure on line 624 in Neos.ContentGraph.DoctrineDbalAdapter/src/Domain/Repository/ContentSubgraph.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test linting-unit-functionaltests-mysql (deps: highest)

Method Doctrine\DBAL\Query\Expression\ExpressionBuilder::and() invoked with 0 parameters, at least 1 required.
$filter->nodeTypes !== null ? $this->nodeQueryBuilder->addNodeTypeCriteria2($queryBuilderInitial, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager), 'cn') : '',
$filter->searchTerm !== null ? $this->nodeQueryBuilder->addSearchTermConstraints($queryBuilderInitial, $filter->searchTerm, 'cn') : '',
$filter->propertyValue !== null ? $this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilderInitial, $filter->propertyValue, 'cn') : '',
]) ?: ['true']));

$queryBuilderRecursive = $this->createQueryBuilder()
->select('cn.*, h.subtreetags, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position')
->select('cn.*, h.subtreetags, pn.nodeaggregateid AS parentNodeAggregateId, pn.level + 1 AS level, h.position' . ($optimize ? ", CASE WHEN $nodeMatcher THEN 1 ELSE 0 END AS isMatch" : ''))
->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')
->andWhere('h.dimensionspacepointhash = :dimensionSpacePointHash');

if ($optimize) {
$queryBuilderRecursive->andWhere('pn.isMatch = 0');
}

$this->addSubtreeTagConstraints($queryBuilderRecursive);

$queryBuilderCte = $this->nodeQueryBuilder->buildBasicNodesCteQuery($entryNodeAggregateId, $this->contentStreamId, $this->dimensionSpacePoint, 'tree', 'n');
if ($filter->nodeTypes !== null) {
$this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager));
}
if ($filter->searchTerm !== null) {
$this->nodeQueryBuilder->addSearchTermConstraints($queryBuilderCte, $filter->searchTerm);
}
if ($filter->propertyValue !== null) {
$this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilderCte, $filter->propertyValue);

if ($optimize === false) {
if ($filter->nodeTypes !== null) {
$this->nodeQueryBuilder->addNodeTypeCriteria($queryBuilderCte, ExpandedNodeTypeCriteria::create($filter->nodeTypes, $this->nodeTypeManager));
}
if ($filter->searchTerm !== null && $filter->searchTerm !== '') {
$queryBuilderCte->andWhere($this->nodeQueryBuilder->addSearchTermConstraints($queryBuilderCte, $filter->searchTerm));
}
if ($filter->propertyValue !== null) {
$queryBuilderCte->andWhere($this->nodeQueryBuilder->addPropertyValueConstraints($queryBuilderCte, $filter->propertyValue));
}
} else {
$queryBuilderCte->where('n.isMatch = 1');
}
return compact('queryBuilderInitial', 'queryBuilderRecursive', 'queryBuilderCte');
}
Expand Down
46 changes: 36 additions & 10 deletions Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,25 +186,51 @@
}
}

public function addSearchTermConstraints(QueryBuilder $queryBuilder, SearchTerm $searchTerm, string $nodeTableAlias = 'n'): void
// todo combine with above
public function addNodeTypeCriteria2(QueryBuilder $queryBuilder, ExpandedNodeTypeCriteria $constraintsWithSubNodeTypes, string $nodeTableAlias = 'n'): string
{
if ($searchTerm->term === '') {
return;
$nodeTablePrefix = $nodeTableAlias === '' ? '' : $nodeTableAlias . '.';
$allowanceQueryPart = '';
if (!$constraintsWithSubNodeTypes->explicitlyAllowedNodeTypeNames->isEmpty()) {
$allowanceQueryPart = $queryBuilder->expr()->in($nodeTablePrefix . 'nodetypename', ':explicitlyAllowedNodeTypeNames');
$queryBuilder->setParameter('explicitlyAllowedNodeTypeNames', $constraintsWithSubNodeTypes->explicitlyAllowedNodeTypeNames->toStringArray(), ArrayParameterType::STRING);
}
$denyQueryPart = '';
if (!$constraintsWithSubNodeTypes->explicitlyDisallowedNodeTypeNames->isEmpty()) {
$denyQueryPart = $queryBuilder->expr()->notIn($nodeTablePrefix . 'nodetypename', ':explicitlyDisallowedNodeTypeNames');
$queryBuilder->setParameter('explicitlyDisallowedNodeTypeNames', $constraintsWithSubNodeTypes->explicitlyDisallowedNodeTypeNames->toStringArray(), ArrayParameterType::STRING);
}
if ($allowanceQueryPart && $denyQueryPart) {
if ($constraintsWithSubNodeTypes->isWildCardAllowed) {
return (string)$queryBuilder->expr()->or($allowanceQueryPart, $denyQueryPart);
} else {
return (string)$queryBuilder->expr()->and($allowanceQueryPart, $denyQueryPart);
}
} elseif ($allowanceQueryPart && !$constraintsWithSubNodeTypes->isWildCardAllowed) {
return $allowanceQueryPart;
} elseif ($denyQueryPart) {
return $denyQueryPart;
}
$queryBuilder->andWhere('JSON_SEARCH(' . $nodeTableAlias . '.properties, "one", :searchTermPattern, NULL, "$.*.value") IS NOT NULL')->setParameter('searchTermPattern', '%' . $searchTerm->term . '%');
throw new \RuntimeException(sprintf('NO'), 1736614392);
return '';

Check failure on line 215 in Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test linting-unit-functionaltests-mysql (deps: highest)

Unreachable statement - code above always terminates.

Check failure on line 215 in Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test linting-unit-functionaltests-mysql (deps: highest)

Unreachable statement - code above always terminates.
}

public function addPropertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias = 'n'): void
public function addSearchTermConstraints(QueryBuilder $queryBuilder, SearchTerm $searchTerm, string $nodeTableAlias = 'n'): string
{
$queryBuilder->andWhere($this->propertyValueConstraints($queryBuilder, $propertyValue, $nodeTableAlias));
if ($searchTerm->term === '') {
throw new \RuntimeException(sprintf('NO'), 1736614392);
return '';

Check failure on line 222 in Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 Test linting-unit-functionaltests-mysql (deps: highest)

Unreachable statement - code above always terminates.

Check failure on line 222 in Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 Test linting-unit-functionaltests-mysql (deps: highest)

Unreachable statement - code above always terminates.
}
$queryBuilder->setParameter('searchTermPattern', '%' . $searchTerm->term . '%');
return 'JSON_SEARCH(' . $nodeTableAlias . '.properties, "one", :searchTermPattern, NULL, "$.*.value") IS NOT NULL';
}

private function propertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias): string
public function addPropertyValueConstraints(QueryBuilder $queryBuilder, PropertyValueCriteriaInterface $propertyValue, string $nodeTableAlias = 'n'): string
{
return match ($propertyValue::class) {
AndCriteria::class => (string)$queryBuilder->expr()->and($this->propertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)),
NegateCriteria::class => 'NOT (' . $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria, $nodeTableAlias) . ')',
OrCriteria::class => (string)$queryBuilder->expr()->or($this->propertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->propertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)),
AndCriteria::class => (string)$queryBuilder->expr()->and($this->addPropertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->addPropertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)),
NegateCriteria::class => 'NOT (' . $this->addPropertyValueConstraints($queryBuilder, $propertyValue->criteria, $nodeTableAlias) . ')',
OrCriteria::class => (string)$queryBuilder->expr()->or($this->addPropertyValueConstraints($queryBuilder, $propertyValue->criteria1, $nodeTableAlias), $this->addPropertyValueConstraints($queryBuilder, $propertyValue->criteria2, $nodeTableAlias)),
PropertyValueContains::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, '%' . $propertyValue->value . '%', $nodeTableAlias, $propertyValue->caseSensitive),
PropertyValueEndsWith::class => $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, '%' . $propertyValue->value, $nodeTableAlias, $propertyValue->caseSensitive),
PropertyValueEquals::class => is_string($propertyValue->value) ? $this->searchPropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, $nodeTableAlias, $propertyValue->caseSensitive) : $this->comparePropertyValueStatement($queryBuilder, $propertyValue->propertyName, $propertyValue->value, '=', $nodeTableAlias),
Expand Down
Loading
Loading