From 0c3f48ca8d93733839ce4e2f43b47f6234152f59 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 3 Jun 2026 18:04:31 +0200 Subject: [PATCH] fix: don't needlessly contrain join when the left side is allow to be null Signed-off-by: Robin Appelman --- .../Partitioned/PartitionedQueryBuilder.php | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php b/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php index 8c04cc7929219..7dc490d977866 100644 --- a/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php +++ b/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php @@ -38,7 +38,7 @@ class PartitionedQueryBuilder extends ShardedQueryBuilder { /** @var array $splitQueries */ private array $splitQueries = []; - /** @var list */ + /** @var array */ private array $partitions = []; /** @var array{'select': string|array, 'alias': ?string}[] */ @@ -180,7 +180,7 @@ private function applySelects(): void { } public function addPartition(PartitionSplit $partition): void { - $this->partitions[] = $partition; + $this->partitions[$partition->name] = $partition; } private function getPartition(string $table): ?PartitionSplit { @@ -264,7 +264,7 @@ public function join($fromAlias, $join, $alias, $condition = null, $joinMode = P if (!isset($this->splitQueries[$partitionName])) { $newPartition = new PartitionSplit($partitionName, [$join]); $newPartition->addAlias($join, $alias); - $this->partitions[] = $newPartition; + $this->partitions[$newPartition->name] = $newPartition; $this->splitQueries[$partitionName] = new PartitionQuery( $this->newQuery(), @@ -349,11 +349,14 @@ public function andWhere(...$where) { if ($where) { foreach ($this->splitPredicatesByParts($where) as $alias => $predicates) { if (isset($this->splitQueries[$alias])) { + $mergedPredicate = new CompositeExpression(CompositeExpression::TYPE_AND, $predicates); // when there is a condition on a table being left-joined it starts to behave as if it's an inner join // since any joined column that doesn't have the left part will not match the condition // when there the condition is `$joinToColumn IS NULL` we instead mark the query as excluding the left half if ($this->splitQueries[$alias]->joinMode === PartitionQuery::JOIN_MODE_LEFT) { - $this->splitQueries[$alias]->joinMode = PartitionQuery::JOIN_MODE_INNER; + if ($this->constraintsPartitionNotNull($mergedPredicate, $this->partitions[$alias])) { + $this->splitQueries[$alias]->joinMode = PartitionQuery::JOIN_MODE_INNER; + } $column = $this->quoteHelper->quoteColumnName($this->splitQueries[$alias]->joinToColumn); foreach ($predicates as $predicate) { @@ -374,6 +377,32 @@ public function andWhere(...$where) { return $this; } + /** + * Check if any part of a predicates constraints any part of a partition to be not null + * + * @return bool + */ + private function constraintsPartitionNotNull($predicate, PartitionSplit $partition): bool { + if ($predicate instanceof CompositeExpression) { + if ($predicate->getType() === CompositeExpression::TYPE_OR) { + $all = true; + foreach ($predicate->getParts() as $part) { + $all = $all && $this->constraintsPartitionNotNull($part, $partition); + } + return $all; + } else { + foreach ($predicate->getParts() as $part) { + if ($this->constraintsPartitionNotNull($part, $partition)) { + return true; + } + } + return false; + } + } else { + return $partition->checkPredicateForTable($predicate) && !str_ends_with($predicate, 'IS NULL'); + } + } + private function getPartitionForPredicate(string $predicate): ?PartitionSplit { foreach ($this->partitions as $partition) {