From ee2be8bbde89989d0e4d95c31f57e9f040f77b1d Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Tue, 19 Mar 2024 13:40:54 +0100 Subject: [PATCH] fix missing GROUP BY, rework its handling & internal representation [closes #518][refs #548] --- src/Collection/Aggregations/AnyAggregator.php | 15 +-- .../Aggregations/CountAggregator.php | 28 ++--- .../Aggregations/NoneAggregator.php | 15 +-- src/Collection/DbalCollection.php | 24 ++-- .../Functions/CompareEqualsFunction.php | 37 +++--- .../Functions/CompareNotEqualsFunction.php | 34 +++--- .../Functions/FetchPropertyFunction.php | 52 ++++---- .../Functions/JunctionFunctionTrait.php | 5 +- .../Functions/Result/DbalExpressionResult.php | 18 ++- .../Functions/Result/DbalTableJoin.php | 9 +- .../Helpers/DbalQueryBuilderHelper.php | 2 +- .../Dbal/RelationshipMapperManyHasMany.php | 8 +- .../Functions/FetchPropertyFunctionTest.php | 112 ++++++++++++++++++ .../relationships.manyHasOne.phpt | 12 ++ .../relationships.oneHasMany.phpt | 10 ++ .../CollectionAggregationJoinTest_testAny.sql | 8 +- ...onAggregationJoinTest_testAnyDependent.sql | 8 +- ...nTest_testHasValueOrEmptyWithFunctions.sql | 4 +- ...egationJoinTest_testIndependentSelects.sql | 4 +- ...estAggregationWithNoAggregateCondition.sql | 4 +- .../CollectionTest_testDistinct.sql | 2 +- ...ropertyFunctionTest_testManyHasOneJoin.sql | 0 ...ropertyFunctionTest_testOneHasManyJoin.sql | 0 ...PropertyFunctionTest_testOneHasOneJoin.sql | 0 ...ropertyFunctionTest_testManyHasOneJoin.sql | 0 ...ropertyFunctionTest_testOneHasManyJoin.sql | 0 ...oredOnManyHasManyRelationshipCondition.sql | 6 +- ...t_testCountStoredOnManyToManyCondition.sql | 2 +- ...sManyTest_testJoinAcrossDifferentPaths.sql | 4 +- ...ipManyHasOneTest_testProperAggregation.sql | 3 +- ...toredOnOneHasManyRelationshipCondition.sql | 4 +- ...leColumnOnHasManyRelationshipCondition.sql | 2 + ...stSameTableJoinWithImplicitAggregation.sql | 4 +- 33 files changed, 270 insertions(+), 166 deletions(-) create mode 100644 tests/cases/integration/Collection/Functions/FetchPropertyFunctionTest.php create mode 100644 tests/sqls/NextrasTests/Orm/Integration/Collection/Functions/FetchPropertyFunctionTest_testManyHasOneJoin.sql create mode 100644 tests/sqls/NextrasTests/Orm/Integration/Collection/Functions/FetchPropertyFunctionTest_testOneHasManyJoin.sql create mode 100644 tests/sqls/NextrasTests/Orm/Integration/Collection/Functions/FetchPropertyFunctionTest_testOneHasOneJoin.sql create mode 100644 tests/sqls/NextrasTests/Orm/Integration/Relationships/FetchPropertyFunctionTest_testManyHasOneJoin.sql create mode 100644 tests/sqls/NextrasTests/Orm/Integration/Relationships/FetchPropertyFunctionTest_testOneHasManyJoin.sql create mode 100644 tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testOrderByDifferentTableColumnOnHasManyRelationshipCondition.sql diff --git a/src/Collection/Aggregations/AnyAggregator.php b/src/Collection/Aggregations/AnyAggregator.php index 09f8cb76..7ee3ba19 100644 --- a/src/Collection/Aggregations/AnyAggregator.php +++ b/src/Collection/Aggregations/AnyAggregator.php @@ -10,7 +10,6 @@ use Nextras\Orm\Exception\InvalidArgumentException; use function array_merge; use function array_pop; -use function count; /** @@ -65,14 +64,9 @@ public function aggregateExpression( if ($join === null) { throw new InvalidArgumentException('Any aggregation applied over expression without a relationship.'); } - if (count($join->groupByColumns) === 0) { + if ($join->toPrimaryKey === null) { throw new InvalidArgumentException( - 'Aggregation applied over a table join without specifying a group-by column (primary key).', - ); - } - if (count($join->groupByColumns) > 1) { - throw new InvalidArgumentException( - 'Aggregation applied over a table join with multiple group-by columns; currently, this is not supported.', + 'Aggregation applied over a table-join without specifying a toPrimaryKey.', ); } @@ -82,12 +76,11 @@ public function aggregateExpression( toAlias: $join->toAlias, onExpression: "($join->onExpression) AND $expression->expression", onArgs: array_merge($join->onArgs, $expression->args), - groupByColumns: $join->groupByColumns, ); return new DbalExpressionResult( - expression: 'COUNT(%table.%column) > 0', - args: [$join->toAlias, $join->groupByColumns[0]], + expression: 'COUNT(%column) > 0', + args: [$join->toPrimaryKey], joins: $joins, groupBy: $expression->groupBy, isHavingClause: true, diff --git a/src/Collection/Aggregations/CountAggregator.php b/src/Collection/Aggregations/CountAggregator.php index e2603fa3..2b6043f4 100644 --- a/src/Collection/Aggregations/CountAggregator.php +++ b/src/Collection/Aggregations/CountAggregator.php @@ -60,14 +60,9 @@ public function aggregateExpression( if ($join === null) { throw new InvalidArgumentException('Count aggregation applied over expression without a relationship.'); } - if (count($join->groupByColumns) === 0) { + if ($join->toPrimaryKey === null) { throw new InvalidArgumentException( - 'Aggregation applied over a table join without specifying a group-by column (primary key).', - ); - } - if (count($join->groupByColumns) > 1) { - throw new InvalidArgumentException( - 'Aggregation applied over a table join with multiple group-by columns; currently, this is not supported.', + 'Aggregation applied over a table-join without specifying a toPrimaryKey.', ); } @@ -77,18 +72,15 @@ public function aggregateExpression( toAlias: $join->toAlias, onExpression: "($join->onExpression) AND $expression->expression", onArgs: array_merge($join->onArgs, $expression->args), - groupByColumns: $join->groupByColumns, ); if ($this->atLeast !== null && $this->atMost !== null) { return new DbalExpressionResult( - expression: 'COUNT(%table.%column) >= %i AND COUNT(%table.%column) <= %i', + expression: 'COUNT(%column) >= %i AND COUNT(%column) <= %i', args: [ - $join->toAlias, - $join->groupByColumns[0], + $join->toPrimaryKey, $this->atLeast, - $join->toAlias, - $join->groupByColumns[0], + $join->toPrimaryKey, $this->atMost, ], joins: $joins, @@ -97,10 +89,9 @@ public function aggregateExpression( ); } elseif ($this->atMost !== null) { return new DbalExpressionResult( - expression: 'COUNT(%table.%column) <= %i', + expression: 'COUNT(%column) <= %i', args: [ - $join->toAlias, - $join->groupByColumns[0], + $join->toPrimaryKey, $this->atMost, ], joins: $joins, @@ -109,10 +100,9 @@ public function aggregateExpression( ); } else { return new DbalExpressionResult( - expression: 'COUNT(%table.%column) >= %i', + expression: 'COUNT(%column) >= %i', args: [ - $join->toAlias, - $join->groupByColumns[0], + $join->toPrimaryKey, $this->atLeast, ], joins: $joins, diff --git a/src/Collection/Aggregations/NoneAggregator.php b/src/Collection/Aggregations/NoneAggregator.php index ba047174..62e0530d 100644 --- a/src/Collection/Aggregations/NoneAggregator.php +++ b/src/Collection/Aggregations/NoneAggregator.php @@ -10,7 +10,6 @@ use Nextras\Orm\Exception\InvalidArgumentException; use function array_merge; use function array_pop; -use function count; /** @@ -59,14 +58,9 @@ public function aggregateExpression( if ($join === null) { throw new InvalidArgumentException('None aggregation applied over expression without a relationship.'); } - if (count($join->groupByColumns) === 0) { + if ($join->toPrimaryKey === null) { throw new InvalidArgumentException( - 'Aggregation applied over a table join without specifying a group-by column (primary key).', - ); - } - if (count($join->groupByColumns) > 1) { - throw new InvalidArgumentException( - 'Aggregation applied over a table join with multiple group-by columns; currently, this is not supported.', + 'Aggregation applied over a table-join without specifying a toPrimaryKey.', ); } @@ -76,12 +70,11 @@ public function aggregateExpression( toAlias: $join->toAlias, onExpression: "($join->onExpression) AND $expression->expression", onArgs: array_merge($join->onArgs, $expression->args), - groupByColumns: $join->groupByColumns, ); return new DbalExpressionResult( - expression: 'COUNT(%table.%column) = 0', - args: [$join->toAlias, $join->groupByColumns[0]], + expression: 'COUNT(%column) = 0', + args: [$join->toPrimaryKey], joins: $joins, groupBy: $expression->groupBy, isHavingClause: true, diff --git a/src/Collection/DbalCollection.php b/src/Collection/DbalCollection.php index ca41af86..f98b1a1a 100644 --- a/src/Collection/DbalCollection.php +++ b/src/Collection/DbalCollection.php @@ -17,7 +17,6 @@ use Nextras\Orm\Mapper\IRelationshipMapper; use function count; use function is_array; -use function str_repeat; /** @@ -314,8 +313,8 @@ public function getQueryBuilder(): QueryBuilder aggregator: null, ); $joins = $expression->joins; + $groupBy = $expression->groupBy; if ($expression->isHavingClause) { - $groupBy = $expression->groupBy; $this->queryBuilder->andHaving($expression->expression, ...$expression->args); } else { $this->queryBuilder->andWhere($expression->expression, ...$expression->args); @@ -325,13 +324,10 @@ public function getQueryBuilder(): QueryBuilder foreach ($this->ordering as [$expression, $direction]) { $joins = array_merge($joins, $expression->joins); - if ($expression->isHavingClause) { - $groupBy = array_merge($groupBy, $expression->groupBy); - } + $groupBy = array_merge($groupBy, $expression->groupBy); $orderingExpression = $helper->processOrderDirection($expression, $direction); $this->queryBuilder->addOrderBy('%ex', $orderingExpression); } - $this->ordering = []; $mergedJoins = $helper->mergeJoins('%and', $joins); foreach ($mergedJoins as $join) { @@ -339,10 +335,18 @@ public function getQueryBuilder(): QueryBuilder } if (count($groupBy) > 0) { - $this->queryBuilder->groupBy( - '%ex' . str_repeat(', %ex', count($groupBy) - 1), - ...$groupBy, - ); + foreach ($this->ordering as [$expression]) { + $groupBy = array_merge($groupBy, $expression->columns); + } + } + $this->ordering = []; + + if (count($groupBy) > 0) { + $unique = []; + foreach ($groupBy as $groupByFqn) { + $unique[$groupByFqn->getUnescaped()] = $groupByFqn; + } + $this->queryBuilder->groupBy('%column[]', array_values($unique)); } return $this->queryBuilder; diff --git a/src/Collection/Functions/CompareEqualsFunction.php b/src/Collection/Functions/CompareEqualsFunction.php index edc36c18..b57b0fd7 100644 --- a/src/Collection/Functions/CompareEqualsFunction.php +++ b/src/Collection/Functions/CompareEqualsFunction.php @@ -4,9 +4,6 @@ use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; -use Nextras\Orm\Exception\InvalidArgumentException; -use function array_combine; -use function array_map; use function count; use function explode; use function in_array; @@ -34,29 +31,25 @@ protected function evaluateInDb( if (is_array($value)) { if (count($value) > 0) { // Multi-column primary key handling - // extract column names for multiOr simplification - // array{%column, array} + // Construct multiOr simplification as array{list, modifiers: list, values: list>} $args = $expression->getArgumentsForExpansion(); if (count($args) === 2 && $args[0] === '%column' && is_array($args[1])) { - $modifiers = explode(',', $modifier); - $columns = []; - foreach ($args[1] as $i => $column) { - $columns[] = $column . $modifiers[$i]; - } - $value = array_map(function ($value) use ($columns): array { - $combined = array_combine($columns, $value); - if ($combined === false) { // @phpstan-ignore-line - $pn = count($columns); - $vn = count($value); - throw new InvalidArgumentException("Number of values ($vn) does not match number of properties ($pn)."); + $columns = $args[1]; + $modifiers = array_map( + fn (string $modifier): ?string => strlen($modifier) === 0 ? null : $modifier, + explode(',', $modifier) + ); + $data = []; + foreach ($value as $dataSet) { + $set = []; + foreach ($dataSet as $i => $dataSetValue) { + $set[] = [$columns[$i], $dataSetValue, $modifiers[$i] ?? null]; } - return $combined; - }, $value); - return $expression->withArgs('%multiOr', [$value]); - } else { - if ($modifier !== '%any') { - $modifier .= '[]'; + $data[] = $set; } + return $expression->withArgs('%multiOr', [$data]); + } else { + if ($modifier !== '%any') $modifier .= '[]'; return $expression->append("IN $modifier", $value); } } else { diff --git a/src/Collection/Functions/CompareNotEqualsFunction.php b/src/Collection/Functions/CompareNotEqualsFunction.php index 236af54b..87f32b05 100644 --- a/src/Collection/Functions/CompareNotEqualsFunction.php +++ b/src/Collection/Functions/CompareNotEqualsFunction.php @@ -34,29 +34,25 @@ protected function evaluateInDb( if (is_array($value)) { if (count($value) > 0) { // Multi-column primary key handling - // extract column names for multiOr simplification - // array{%column, array} + // Construct multiOr simplification as array{list, modifiers: list, values: list>} $args = $expression->getArgumentsForExpansion(); if (count($args) === 2 && $args[0] === '%column' && is_array($args[1])) { - $modifiers = explode(',', $modifier); - $columns = []; - foreach ($args[1] as $i => $column) { - $columns[] = $column . $modifiers[$i]; - } - $value = array_map(function ($value) use ($columns): array { - $combined = array_combine($columns, $value); - if ($combined === false) { // @phpstan-ignore-line - $pn = count($columns); - $vn = count($value); - throw new InvalidArgumentException("Number of values ($vn) does not match number of properties ($pn)."); + $columns = $args[1]; + $modifiers = array_map( + fn (string $modifier): ?string => strlen($modifier) === 0 ? null : $modifier, + explode(',', $modifier) + ); + $data = []; + foreach ($value as $dataSet) { + $set = []; + foreach ($dataSet as $i => $dataSetValue) { + $set[] = [$columns[$i], $dataSetValue, $modifiers[$i] ?? null]; } - return $combined; - }, $value); - return $expression->withArgs('NOT (%multiOr)', [$value]); - } else { - if ($modifier !== '%any') { - $modifier .= '[]'; + $data[] = $set; } + return $expression->withArgs('NOT (%multiOr)', [$data]); + } else { + if ($modifier !== '%any') $modifier .= '[]'; return $expression->append("NOT IN $modifier", $value); } } else { diff --git a/src/Collection/Functions/FetchPropertyFunction.php b/src/Collection/Functions/FetchPropertyFunction.php index 6b96ee83..99551833 100644 --- a/src/Collection/Functions/FetchPropertyFunction.php +++ b/src/Collection/Functions/FetchPropertyFunction.php @@ -5,9 +5,10 @@ use Nette\Utils\Arrays; use Nextras\Dbal\Platforms\Data\Column; +use Nextras\Dbal\Platforms\Data\Fqn; use Nextras\Dbal\QueryBuilder\QueryBuilder; -use Nextras\Orm\Collection\Aggregations\AnyAggregator; use Nextras\Orm\Collection\Aggregations\Aggregator; +use Nextras\Orm\Collection\Aggregations\AnyAggregator; use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; @@ -27,8 +28,11 @@ use Nextras\Orm\Mapper\IMapper; use Nextras\Orm\Model\IModel; use Nextras\Orm\Repository\IRepository; +use function array_values; +use function assert; use function count; use function get_class; +use function is_array; class FetchPropertyFunction implements CollectionFunction @@ -109,7 +113,7 @@ public function __toString() $propertyName = array_shift($tokens); assert($propertyName !== null); $propertyMeta = $entityMeta->getProperty($propertyName); // check if property exists - // We allow to cycle-through even if $value is null to properly detect $isMultiValue + // We allow cycling-through even if $value is null to properly detect $isMultiValue // to return related aggregator. $value = $value !== null && $value->hasValue($propertyName) ? $value->getValue($propertyName) : null; @@ -246,17 +250,12 @@ private function processTokens( $modifier, ); - if ($makeDistinct) { - $groupBy = $this->makeDistinct($builder, $this->getDbalMapper()); - } else { - $groupBy = [['%column', $column]]; - } - return new DbalExpressionResult( expression: '%column', args: [$column], joins: $joins, - groupBy: $groupBy, + groupBy: $makeDistinct ? $this->createBaseGroupBy($builder, $this->getDbalMapper()) : [], + columns: is_array($column) ? $column : [$column], aggregator: $makeDistinct ? ($aggregator ?? new AnyAggregator()) : null, propertyMetadata: $propertyMetadata, valueNormalizer: function ($value) use ($propertyMetadata, $currentConventions) { @@ -336,7 +335,7 @@ private function processRelationship( toAlias: $joinAlias, onExpression: "%table.%column = %table.%column", onArgs: [$currentAlias, $fromColumn, $joinAlias, $inColumn], - groupByColumns: [$currentConventions->getStoragePrimaryKey()[0]], + toPrimaryKey: new Fqn(schema: $currentAlias, name: $currentConventions->getStoragePrimaryKey()[0]), ); $currentAlias = $joinAlias; @@ -359,7 +358,7 @@ private function processRelationship( toAlias: $targetAlias, onExpression: "%table.%column = %table.%column", onArgs: [$currentAlias, $fromColumn, $targetAlias, $toColumn], - groupByColumns: [$targetConventions->getStoragePrimaryKey()[0]], + toPrimaryKey: new Fqn(schema: $targetAlias, name: $targetConventions->getStoragePrimaryKey()[0]), ); return [$targetAlias, $targetConventions, $targetEntityMetadata, $targetMapper]; @@ -367,7 +366,7 @@ private function processRelationship( /** - * @return string|array + * @return Fqn|list */ private function toColumnExpr( EntityMetadata $entityMetadata, @@ -376,7 +375,7 @@ private function toColumnExpr( string $alias, string $propertyPrefixTokens, string &$modifier, - ): array|string + ): Fqn|array { if ($propertyMetadata->isPrimary && $propertyMetadata->isVirtual) { // primary-proxy $primaryKey = $entityMetadata->getPrimaryKey(); @@ -385,7 +384,7 @@ private function toColumnExpr( $modifiers = []; foreach ($primaryKey as $columnName) { $columnName = $conventions->convertEntityToStorageKey($propertyPrefixTokens . $columnName); - $pair[] = "$alias.$columnName"; + $pair[] = new Fqn(schema: $alias, name: $columnName); $modifiers[] = $conventions->getModifier($columnName); } $modifier = implode(',', $modifiers); @@ -399,38 +398,37 @@ private function toColumnExpr( $columnName = $conventions->convertEntityToStorageKey($propertyPrefixTokens . $propertyName); $modifier = $conventions->getModifier($columnName); - $columnExpr = "$alias.$columnName"; - return $columnExpr; + return new Fqn(schema: $alias, name: $columnName); } /** * @param DbalMapper $mapper - * @return array> + * @return list */ - private function makeDistinct(QueryBuilder $builder, DbalMapper $mapper): array + private function createBaseGroupBy(QueryBuilder $builder, DbalMapper $mapper): array { $baseTable = $builder->getFromAlias(); + assert($baseTable !== null); + if ($mapper->getDatabasePlatform()->getName() === 'mssql') { $tableName = $mapper->getConventions()->getStorageTable(); $columns = $mapper->getDatabasePlatform()->getColumns( table: $tableName->fqnName->name, schema: $tableName->fqnName->schema, ); - $columnNames = array_map(function (Column $column) use ($baseTable): string { - return $baseTable . '.' . $column->name; - }, $columns); - return [['%column[]', $columnNames]]; - + $groupBy = array_values(array_map( + fn (Column $column): Fqn => new Fqn(schema: $baseTable, name: $column->name), + $columns, + )); + return $groupBy; } else { $primaryKey = $this->getDbalMapper()->getConventions()->getStoragePrimaryKey(); - $groupBy = []; foreach ($primaryKey as $column) { - $groupBy[] = "$baseTable.$column"; + $groupBy[] = new Fqn(schema: $baseTable, name: $column); } - - return [['%column[]', $groupBy]]; + return $groupBy; } } diff --git a/src/Collection/Functions/JunctionFunctionTrait.php b/src/Collection/Functions/JunctionFunctionTrait.php index 3c1093a4..a63d0fc3 100644 --- a/src/Collection/Functions/JunctionFunctionTrait.php +++ b/src/Collection/Functions/JunctionFunctionTrait.php @@ -69,6 +69,7 @@ protected function processQueryBuilderExpressionWithModifier( $processedArgs = []; $joins = []; $groupBy = []; + $columns = []; [$normalized, $newAggregator] = $this->normalizeFunctions($args); if ($newAggregator !== null) { @@ -82,6 +83,7 @@ protected function processQueryBuilderExpressionWithModifier( $processedArgs[] = $expression->getArgumentsForExpansion(); $joins = array_merge($joins, $expression->joins); $groupBy = array_merge($groupBy, $expression->groupBy); + $columns = array_merge($columns, $expression->columns); $isHavingClause = $isHavingClause || $expression->isHavingClause; } @@ -89,7 +91,8 @@ protected function processQueryBuilderExpressionWithModifier( expression: $dbalModifier, args: [$processedArgs], joins: $helper->mergeJoins($dbalModifier, $joins), - groupBy: $groupBy, + groupBy: $isHavingClause ? array_merge($groupBy, $columns) : $groupBy, + columns: $isHavingClause ? [] : $columns, isHavingClause: $isHavingClause, ); } diff --git a/src/Collection/Functions/Result/DbalExpressionResult.php b/src/Collection/Functions/Result/DbalExpressionResult.php index 0f525710..0f7287af 100644 --- a/src/Collection/Functions/Result/DbalExpressionResult.php +++ b/src/Collection/Functions/Result/DbalExpressionResult.php @@ -3,6 +3,7 @@ namespace Nextras\Orm\Collection\Functions\Result; +use Nextras\Dbal\Platforms\Data\Fqn; use Nextras\Dbal\QueryBuilder\QueryBuilder; use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Collection\Expression\ExpressionContext; @@ -39,13 +40,6 @@ class DbalExpressionResult */ public readonly array $joins; - /** - * List of arguments possible to pass to %ex modifier. - * Those grouping expressions are applied iff the $isHavingClause is true. - * @var array> - */ - public readonly array $groupBy; - /** * Result aggregator. * @var Aggregator|null @@ -74,16 +68,18 @@ class DbalExpressionResult * @param literal-string $expression * @param list $args * @param DbalTableJoin[] $joins - * @param array> $groupBy + * @param list $groupBy List of columns used for grouping. + * @param list $columns List of columns used in the expression. If needed, this is later used to properly reference in GROUP BY clause. * @param Aggregator|null $aggregator * @param bool $isHavingClause - * @param literal-string $dbalModifier + * @param literal-string|null $dbalModifier */ public function __construct( string $expression, array $args, array $joins = [], - array $groupBy = [], + public readonly array $groupBy = [], + public readonly array $columns = [], ?Aggregator $aggregator = null, bool $isHavingClause = false, ?PropertyMetadata $propertyMetadata = null, @@ -95,7 +91,6 @@ public function __construct( $this->args = $args; $this->aggregator = $aggregator; $this->joins = $joins; - $this->groupBy = $groupBy; $this->isHavingClause = $isHavingClause; $this->propertyMetadata = $propertyMetadata; $this->valueNormalizer = $valueNormalizer; @@ -149,6 +144,7 @@ public function withArgs(string $expression, array $args): DbalExpressionResult args: $args, joins: $this->joins, groupBy: $this->groupBy, + columns: $this->columns, aggregator: $this->aggregator, isHavingClause: $this->isHavingClause, ); diff --git a/src/Collection/Functions/Result/DbalTableJoin.php b/src/Collection/Functions/Result/DbalTableJoin.php index 75fbe2ba..416cb6bb 100644 --- a/src/Collection/Functions/Result/DbalTableJoin.php +++ b/src/Collection/Functions/Result/DbalTableJoin.php @@ -3,6 +3,7 @@ namespace Nextras\Orm\Collection\Functions\Result; +use Nextras\Dbal\Platforms\Data\Fqn; use Nextras\Dbal\QueryBuilder\QueryBuilder; @@ -11,8 +12,8 @@ * * The joins are created lazily and this class holds data for it. * - * If there is an aggregation, the joined table needs to be grouped by {@see DbalTableJoin::$groupByColumns}, - * if not needed or possible, pass just an empty array. + * Later, to construct an aggregation, the aggregation condition is created over {@see DbalTableJoin::$toPrimaryKey} column. + * If not needed or possible, pass a null. * * @experimental */ @@ -24,7 +25,7 @@ class DbalTableJoin * @param literal-string $toAlias * @param literal-string $onExpression * @param array $onArgs - * @param list $groupByColumns + * @param Fqn|null $toPrimaryKey */ public function __construct( public readonly string $toExpression, @@ -32,7 +33,7 @@ public function __construct( public readonly string $toAlias, public readonly string $onExpression, public readonly array $onArgs, - public readonly array $groupByColumns = [], + public readonly Fqn|null $toPrimaryKey = null, ) { } diff --git a/src/Collection/Helpers/DbalQueryBuilderHelper.php b/src/Collection/Helpers/DbalQueryBuilderHelper.php index 521fe2bf..94bd89c9 100644 --- a/src/Collection/Helpers/DbalQueryBuilderHelper.php +++ b/src/Collection/Helpers/DbalQueryBuilderHelper.php @@ -171,7 +171,7 @@ public function mergeJoins(string $dbalModifier, array $joins): array toAlias: $first->toAlias, onExpression: $dbalModifier, onArgs: [$args], - groupByColumns: $first->groupByColumns, + toPrimaryKey: $first->toPrimaryKey, ); } } diff --git a/src/Mapper/Dbal/RelationshipMapperManyHasMany.php b/src/Mapper/Dbal/RelationshipMapperManyHasMany.php index 191d9a0a..b52d29f2 100644 --- a/src/Mapper/Dbal/RelationshipMapperManyHasMany.php +++ b/src/Mapper/Dbal/RelationshipMapperManyHasMany.php @@ -139,7 +139,7 @@ private function fetchByTwoPassStrategy(QueryBuilder $builder, array $values): M /** @var literal-string $targetTable */ $targetTable = DbalQueryBuilderHelper::getAlias($this->joinTable); - $hasJoins = $builder->getClause('join')[0] !== null; + $hasGroupBy = $builder->getClause('group')[0] !== null; $hasOrderBy = $builder->getClause('order')[0] !== null; $builder = clone $builder; @@ -156,13 +156,13 @@ private function fetchByTwoPassStrategy(QueryBuilder $builder, array $values): M $builder->select('%column', "$targetTable.$this->primaryKeyTo"); $builder->addSelect('%column', "$targetTable.$this->primaryKeyFrom"); - if ($hasJoins && !$hasOrderBy) { + if ($hasGroupBy) { $builder->addGroupBy('%column', "$targetTable.$this->primaryKeyTo"); $builder->addGroupBy('%column', "$targetTable.$this->primaryKeyFrom"); } if ($builder->hasLimitOffsetClause()) { - if ($hasJoins && $hasOrderBy) { + if ($hasGroupBy && $hasOrderBy) { throw new NotSupportedException( "Relationship cannot be fetched as it combines has-many joins, ORDER BY and LIMIT clause.", ); @@ -171,7 +171,7 @@ private function fetchByTwoPassStrategy(QueryBuilder $builder, array $values): M } else { $builder->andWhere('%column IN %any', "$targetTable.$this->primaryKeyFrom", $values); - if ($hasJoins && $hasOrderBy) { + if ($hasGroupBy && $hasOrderBy) { /** @var literal-string $sql */ $sql = $builder->getQuerySql(); $builder = $this->connection->createQueryBuilder() diff --git a/tests/cases/integration/Collection/Functions/FetchPropertyFunctionTest.php b/tests/cases/integration/Collection/Functions/FetchPropertyFunctionTest.php new file mode 100644 index 00000000..1a4ae322 --- /dev/null +++ b/tests/cases/integration/Collection/Functions/FetchPropertyFunctionTest.php @@ -0,0 +1,112 @@ +section === Helper::SECTION_ARRAY) $this->skip(); + /** @var TestMapper $mapper */ + $mapper = $this->orm->books->getMapper(); + $builder = $mapper->builder(); + $helper = new DbalQueryBuilderHelper($this->orm->books); + $function = new FetchPropertyFunction($this->orm->books, $this->orm->books->getMapper(), $this->orm); + + $expression = $function->processDbalExpression( + $helper, + $builder, + ['author->name'], + ExpressionContext::FilterAnd, + ); + Assert::count(0, $expression->groupBy); + Assert::count(1, $expression->joins); + Assert::equal(new Fqn('author', 'id'), $expression->joins[0]->toPrimaryKey); + Assert::count(1, $expression->columns); + Assert::equal(new Fqn('author', 'name'), $expression->columns[0]); + } + + + public function testOneHasManyJoin(): void + { + if ($this->section === Helper::SECTION_ARRAY) $this->skip(); + /** @var TestMapper $mapper */ + $mapper = $this->orm->authors->getMapper(); + $builder = $mapper->builder(); + $helper = new DbalQueryBuilderHelper($this->orm->authors); + $function = new FetchPropertyFunction($this->orm->authors, $this->orm->authors->getMapper(), $this->orm); + + $expression = $function->processDbalExpression( + $helper, + $builder, + ['books->title'], + ExpressionContext::FilterAnd, + ); + Assert::count(1, $expression->groupBy); + Assert::equal(new Fqn('authors', 'id'), $expression->groupBy[0]); + Assert::count(1, $expression->joins); + Assert::equal(new Fqn('books_any', 'id'), $expression->joins[0]->toPrimaryKey); + Assert::count(1, $expression->columns); + Assert::equal(new Fqn('books_any', 'title'), $expression->columns[0]); + + $expression = $function->processDbalExpression( + $helper, + $builder, + ['books->title'], + ExpressionContext::FilterAnd, + new AnyAggregator('any2'), + ); + Assert::count(1, $expression->groupBy); + Assert::equal(new Fqn('authors', 'id'), $expression->groupBy[0]); + Assert::count(1, $expression->joins); + Assert::equal(new Fqn('books_any2', 'id'), $expression->joins[0]->toPrimaryKey); + Assert::count(1, $expression->columns); + Assert::equal(new Fqn('books_any2', 'title'), $expression->columns[0]); + } + + + public function testOneHasOneJoin(): void + { + if ($this->section === Helper::SECTION_ARRAY) $this->skip(); + /** @var TestMapper $mapper */ + $mapper = $this->orm->eans->getMapper(); + $builder = $mapper->builder(); + $helper = new DbalQueryBuilderHelper($this->orm->eans); + $function = new FetchPropertyFunction($this->orm->eans, $this->orm->eans->getMapper(), $this->orm); + + $expression = $function->processDbalExpression( + $helper, + $builder, + ['book->title'], + ExpressionContext::FilterAnd, + ); + Assert::count(0, $expression->groupBy); + Assert::count(1, $expression->joins); + Assert::equal(new Fqn('book', 'id'), $expression->joins[0]->toPrimaryKey); + Assert::count(1, $expression->columns); + Assert::equal(new Fqn('book', 'title'), $expression->columns[0]); + } +} + + +(new FetchPropertyFunctionTest())->run(); diff --git a/tests/cases/integration/Relationships/relationships.manyHasOne.phpt b/tests/cases/integration/Relationships/relationships.manyHasOne.phpt index 5bf9458d..cf7d38cd 100644 --- a/tests/cases/integration/Relationships/relationships.manyHasOne.phpt +++ b/tests/cases/integration/Relationships/relationships.manyHasOne.phpt @@ -8,6 +8,8 @@ namespace NextrasTests\Orm\Integration\Relationships; +use Nextras\Orm\Collection\Aggregations\NoneAggregator; +use Nextras\Orm\Collection\ICollection; use NextrasTests\Orm\Author; use NextrasTests\Orm\Book; use NextrasTests\Orm\DataTestCase; @@ -150,6 +152,16 @@ class RelationshipManyHasOneTest extends DataTestCase 'publisher->name' => 'Nextras publisher A', ]); Assert::same($books->count(), 1); + + // the publisher.name reference in HAVING needs to be in GROUP BY + // finds books published by Nextras published A and with none Tag 1 ~ matches book#4 + $books = $this->orm->books->findBy([ + ICollection::AND, + new NoneAggregator(), + 'tags->id' => 1, + 'publisher->name' => 'Nextras publisher A', + ]); + Assert::same($books->count(), 1); } } diff --git a/tests/cases/integration/Relationships/relationships.oneHasMany.phpt b/tests/cases/integration/Relationships/relationships.oneHasMany.phpt index 31b86b81..be5936ea 100644 --- a/tests/cases/integration/Relationships/relationships.oneHasMany.phpt +++ b/tests/cases/integration/Relationships/relationships.oneHasMany.phpt @@ -349,6 +349,16 @@ class RelationshipOneHasManyTest extends DataTestCase Assert::same(1, $books->countStored()); Assert::same(1, $books->count()); } + + + public function testOrderByDifferentTableColumnOnHasManyRelationshipCondition(): void + { + $publisher = $this->orm->publishers->getByIdChecked(1); + $books = $publisher->books->toCollection()->findBy([ + 'tags->id' => 1, + ])->orderBy('author->name'); + Assert::same(1, $books->count()); + } } diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testAny.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testAny.sql index 5f49e6d7..0b4694c5 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testAny.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testAny.sql @@ -1,4 +1,4 @@ -SELECT "authors".* FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON ("authors"."id" = "books_any"."author_id") WHERE ((("books_any"."title" = 'Book 1'))); -SELECT COUNT(*) AS count FROM (SELECT "authors"."id" FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON ("authors"."id" = "books_any"."author_id") WHERE ((("books_any"."title" = 'Book 1')))) temp; -SELECT "authors".* FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON ("authors"."id" = "books_any"."author_id") WHERE ((("books_any"."title" = 'Book 1'))); -SELECT COUNT(*) AS count FROM (SELECT "authors"."id" FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON ("authors"."id" = "books_any"."author_id") WHERE ((("books_any"."title" = 'Book 1')))) temp; +SELECT "authors".* FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON ("authors"."id" = "books_any"."author_id") WHERE ((("books_any"."title" = 'Book 1'))) GROUP BY "authors"."id"; +SELECT COUNT(*) AS count FROM (SELECT "authors"."id" FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON ("authors"."id" = "books_any"."author_id") WHERE ((("books_any"."title" = 'Book 1'))) GROUP BY "authors"."id") temp; +SELECT "authors".* FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON ("authors"."id" = "books_any"."author_id") WHERE ((("books_any"."title" = 'Book 1'))) GROUP BY "authors"."id"; +SELECT COUNT(*) AS count FROM (SELECT "authors"."id" FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON ("authors"."id" = "books_any"."author_id") WHERE ((("books_any"."title" = 'Book 1'))) GROUP BY "authors"."id") temp; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testAnyDependent.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testAnyDependent.sql index c7ce560f..bcac78cf 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testAnyDependent.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testAnyDependent.sql @@ -1,4 +1,4 @@ -SELECT "authors".* FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON ("authors"."id" = "books_any"."author_id") LEFT JOIN "public"."authors" AS "books_translator_any" ON ("books_any"."translator_id" = "books_translator_any"."id") WHERE (("books_any"."title" = 'Book 1') AND ("books_translator_any"."id" IS NULL)); -SELECT COUNT(*) AS count FROM (SELECT "authors"."id" FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON ("authors"."id" = "books_any"."author_id") LEFT JOIN "public"."authors" AS "books_translator_any" ON ("books_any"."translator_id" = "books_translator_any"."id") WHERE (("books_any"."title" = 'Book 1') AND ("books_translator_any"."id" IS NULL))) temp; -SELECT "authors".* FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_count" ON (("authors"."id" = "books_count"."author_id") OR (("authors"."id" = "books_count"."author_id") AND "books_count"."price" < 100)) LEFT JOIN "public"."authors" AS "books_translator_count" ON (("books_count"."translator_id" = "books_translator_count"."id") AND "books_translator_count"."id" IS NOT NULL) GROUP BY "authors"."id", "authors"."id" HAVING ((COUNT("books_translator_count"."id") >= 1 AND COUNT("books_translator_count"."id") <= 1) OR (COUNT("books_count"."id") >= 1 AND COUNT("books_count"."id") <= 1)); -SELECT COUNT(*) AS count FROM (SELECT "authors"."id" FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_count" ON (("authors"."id" = "books_count"."author_id") OR (("authors"."id" = "books_count"."author_id") AND "books_count"."price" < 100)) LEFT JOIN "public"."authors" AS "books_translator_count" ON (("books_count"."translator_id" = "books_translator_count"."id") AND "books_translator_count"."id" IS NOT NULL) GROUP BY "authors"."id", "authors"."id" HAVING ((COUNT("books_translator_count"."id") >= 1 AND COUNT("books_translator_count"."id") <= 1) OR (COUNT("books_count"."id") >= 1 AND COUNT("books_count"."id") <= 1))) temp; +SELECT "authors".* FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON ("authors"."id" = "books_any"."author_id") LEFT JOIN "public"."authors" AS "books_translator_any" ON ("books_any"."translator_id" = "books_translator_any"."id") WHERE (("books_any"."title" = 'Book 1') AND ("books_translator_any"."id" IS NULL)) GROUP BY "authors"."id"; +SELECT COUNT(*) AS count FROM (SELECT "authors"."id" FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON ("authors"."id" = "books_any"."author_id") LEFT JOIN "public"."authors" AS "books_translator_any" ON ("books_any"."translator_id" = "books_translator_any"."id") WHERE (("books_any"."title" = 'Book 1') AND ("books_translator_any"."id" IS NULL)) GROUP BY "authors"."id") temp; +SELECT "authors".* FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_count" ON (("authors"."id" = "books_count"."author_id") OR (("authors"."id" = "books_count"."author_id") AND "books_count"."price" < 100)) LEFT JOIN "public"."authors" AS "books_translator_count" ON (("books_count"."translator_id" = "books_translator_count"."id") AND "books_translator_count"."id" IS NOT NULL) GROUP BY "authors"."id" HAVING ((COUNT("books_translator_count"."id") >= 1 AND COUNT("books_translator_count"."id") <= 1) OR (COUNT("books_count"."id") >= 1 AND COUNT("books_count"."id") <= 1)); +SELECT COUNT(*) AS count FROM (SELECT "authors"."id" FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_count" ON (("authors"."id" = "books_count"."author_id") OR (("authors"."id" = "books_count"."author_id") AND "books_count"."price" < 100)) LEFT JOIN "public"."authors" AS "books_translator_count" ON (("books_count"."translator_id" = "books_translator_count"."id") AND "books_translator_count"."id" IS NOT NULL) GROUP BY "authors"."id" HAVING ((COUNT("books_translator_count"."id") >= 1 AND COUNT("books_translator_count"."id") <= 1) OR (COUNT("books_count"."id") >= 1 AND COUNT("books_count"."id") <= 1))) temp; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testHasValueOrEmptyWithFunctions.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testHasValueOrEmptyWithFunctions.sql index 44ce3782..8c13a7c2 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testHasValueOrEmptyWithFunctions.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testHasValueOrEmptyWithFunctions.sql @@ -1,2 +1,2 @@ -SELECT "books".* FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON (("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" IN (1)) LEFT JOIN "tags" AS "tags__COUNT" ON ("books_x_tags"."tag_id" = "tags__COUNT"."id") GROUP BY "books"."id", "books"."id" HAVING (((COUNT("tags_any"."id") > 0)) OR (COUNT("tags__COUNT"."id") = 0)); -SELECT COUNT(*) AS count FROM (SELECT "books"."id" FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON (("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" IN (1)) LEFT JOIN "tags" AS "tags__COUNT" ON ("books_x_tags"."tag_id" = "tags__COUNT"."id") GROUP BY "books"."id", "books"."id" HAVING (((COUNT("tags_any"."id") > 0)) OR (COUNT("tags__COUNT"."id") = 0))) temp; +SELECT "books".* FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON (("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" IN (1)) LEFT JOIN "tags" AS "tags__COUNT" ON ("books_x_tags"."tag_id" = "tags__COUNT"."id") GROUP BY "books"."id" HAVING (((COUNT("tags_any"."id") > 0)) OR (COUNT("tags__COUNT"."id") = 0)); +SELECT COUNT(*) AS count FROM (SELECT "books"."id" FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON (("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" IN (1)) LEFT JOIN "tags" AS "tags__COUNT" ON ("books_x_tags"."tag_id" = "tags__COUNT"."id") GROUP BY "books"."id" HAVING (((COUNT("tags_any"."id") > 0)) OR (COUNT("tags__COUNT"."id") = 0))) temp; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testIndependentSelects.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testIndependentSelects.sql index 30598ac9..33290007 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testIndependentSelects.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testIndependentSelects.sql @@ -1,2 +1,2 @@ -SELECT "authors".* FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any1" ON ("authors"."id" = "books_any1"."author_id") LEFT JOIN "books" AS "books_any2" ON ("authors"."id" = "books_any2"."author_id") WHERE ((("books_any1"."title" = 'Book 1') AND ("books_any1"."price" = 50)) AND (("books_any2"."title" = 'Book 2') AND ("books_any2"."price" = 150))); -SELECT COUNT(*) AS count FROM (SELECT "authors"."id" FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any1" ON ("authors"."id" = "books_any1"."author_id") LEFT JOIN "books" AS "books_any2" ON ("authors"."id" = "books_any2"."author_id") WHERE ((("books_any1"."title" = 'Book 1') AND ("books_any1"."price" = 50)) AND (("books_any2"."title" = 'Book 2') AND ("books_any2"."price" = 150)))) temp; +SELECT "authors".* FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any1" ON ("authors"."id" = "books_any1"."author_id") LEFT JOIN "books" AS "books_any2" ON ("authors"."id" = "books_any2"."author_id") WHERE ((("books_any1"."title" = 'Book 1') AND ("books_any1"."price" = 50)) AND (("books_any2"."title" = 'Book 2') AND ("books_any2"."price" = 150))) GROUP BY "authors"."id"; +SELECT COUNT(*) AS count FROM (SELECT "authors"."id" FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any1" ON ("authors"."id" = "books_any1"."author_id") LEFT JOIN "books" AS "books_any2" ON ("authors"."id" = "books_any2"."author_id") WHERE ((("books_any1"."title" = 'Book 1') AND ("books_any1"."price" = 50)) AND (("books_any2"."title" = 'Book 2') AND ("books_any2"."price" = 150))) GROUP BY "authors"."id") temp; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationTest_testAggregationWithNoAggregateCondition.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationTest_testAggregationWithNoAggregateCondition.sql index 59f66eed..c197f74f 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationTest_testAggregationWithNoAggregateCondition.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationTest_testAggregationWithNoAggregateCondition.sql @@ -1,2 +1,2 @@ -SELECT "authors".* FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON (("authors"."id" = "books_any"."author_id") AND "books_any"."title" = 'Book 1') LEFT JOIN "tag_followers" AS "tagFollowers__COUNT" ON ("authors"."id" = "tagFollowers__COUNT"."author_id") GROUP BY "authors"."id", "authors"."id" HAVING (((COUNT("books_any"."id") > 0)) OR (COUNT("tagFollowers__COUNT"."tag_id") <= 2)); -SELECT COUNT(*) AS count FROM (SELECT "authors"."id" FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON (("authors"."id" = "books_any"."author_id") AND "books_any"."title" = 'Book 1') LEFT JOIN "tag_followers" AS "tagFollowers__COUNT" ON ("authors"."id" = "tagFollowers__COUNT"."author_id") GROUP BY "authors"."id", "authors"."id" HAVING (((COUNT("books_any"."id") > 0)) OR (COUNT("tagFollowers__COUNT"."tag_id") <= 2))) temp; +SELECT "authors".* FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON (("authors"."id" = "books_any"."author_id") AND "books_any"."title" = 'Book 1') LEFT JOIN "tag_followers" AS "tagFollowers__COUNT" ON ("authors"."id" = "tagFollowers__COUNT"."author_id") GROUP BY "authors"."id" HAVING (((COUNT("books_any"."id") > 0)) OR (COUNT("tagFollowers__COUNT"."tag_id") <= 2)); +SELECT COUNT(*) AS count FROM (SELECT "authors"."id" FROM "public"."authors" AS "authors" LEFT JOIN "books" AS "books_any" ON (("authors"."id" = "books_any"."author_id") AND "books_any"."title" = 'Book 1') LEFT JOIN "tag_followers" AS "tagFollowers__COUNT" ON ("authors"."id" = "tagFollowers__COUNT"."author_id") GROUP BY "authors"."id" HAVING (((COUNT("books_any"."id") > 0)) OR (COUNT("tagFollowers__COUNT"."tag_id") <= 2))) temp; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionTest_testDistinct.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionTest_testDistinct.sql index bd5bfab2..759f6569 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionTest_testDistinct.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionTest_testDistinct.sql @@ -1 +1 @@ -SELECT "tag_followers".* FROM "tag_followers" AS "tag_followers" LEFT JOIN "tags" AS "tag" ON ("tag_followers"."tag_id" = "tag"."id") LEFT JOIN "books_x_tags" AS "tag_books_x_tags" ON ("tag"."id" = "tag_books_x_tags"."tag_id") LEFT JOIN "books" AS "tag_books_any" ON ("tag_books_x_tags"."book_id" = "tag_books_any"."id") WHERE (("tag_books_any"."id" = 1)); +SELECT "tag_followers".* FROM "tag_followers" AS "tag_followers" LEFT JOIN "tags" AS "tag" ON ("tag_followers"."tag_id" = "tag"."id") LEFT JOIN "books_x_tags" AS "tag_books_x_tags" ON ("tag"."id" = "tag_books_x_tags"."tag_id") LEFT JOIN "books" AS "tag_books_any" ON ("tag_books_x_tags"."book_id" = "tag_books_any"."id") WHERE (("tag_books_any"."id" = 1)) GROUP BY "tag_followers"."tag_id", "tag_followers"."author_id"; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/Functions/FetchPropertyFunctionTest_testManyHasOneJoin.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/Functions/FetchPropertyFunctionTest_testManyHasOneJoin.sql new file mode 100644 index 00000000..e69de29b diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/Functions/FetchPropertyFunctionTest_testOneHasManyJoin.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/Functions/FetchPropertyFunctionTest_testOneHasManyJoin.sql new file mode 100644 index 00000000..e69de29b diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/Functions/FetchPropertyFunctionTest_testOneHasOneJoin.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/Functions/FetchPropertyFunctionTest_testOneHasOneJoin.sql new file mode 100644 index 00000000..e69de29b diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/FetchPropertyFunctionTest_testManyHasOneJoin.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/FetchPropertyFunctionTest_testManyHasOneJoin.sql new file mode 100644 index 00000000..e69de29b diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/FetchPropertyFunctionTest_testOneHasManyJoin.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/FetchPropertyFunctionTest_testOneHasManyJoin.sql new file mode 100644 index 00000000..e69de29b diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testCountStoredOnManyHasManyRelationshipCondition.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testCountStoredOnManyHasManyRelationshipCondition.sql index 91c5b9d5..07e2cca3 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testCountStoredOnManyHasManyRelationshipCondition.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testCountStoredOnManyHasManyRelationshipCondition.sql @@ -1,7 +1,7 @@ SELECT "tags".* FROM "tags" AS "tags" WHERE (("tags"."id" = 1)); -SELECT "books_x_tags"."book_id", "books_x_tags"."tag_id" FROM "books" AS "books" LEFT JOIN "public"."authors" AS "author" ON ("books"."author_id" = "author"."id") LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books_x_tags"."book_id" = "books"."id") WHERE ((("author"."id" = 1))) AND ("books_x_tags"."tag_id" IN (1)) GROUP BY "books_x_tags"."book_id", "books_x_tags"."tag_id"; +SELECT "books_x_tags"."book_id", "books_x_tags"."tag_id" FROM "books" AS "books" LEFT JOIN "public"."authors" AS "author" ON ("books"."author_id" = "author"."id") LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books_x_tags"."book_id" = "books"."id") WHERE ((("author"."id" = 1))) AND ("books_x_tags"."tag_id" IN (1)); SELECT "books".* FROM "books" AS "books" WHERE (("books"."id" IN (1))); SELECT "books_x_tags"."tag_id", COUNT(DISTINCT "books_x_tags"."book_id") AS "count" FROM "books" AS "books" LEFT JOIN "public"."authors" AS "author" ON ("books"."author_id" = "author"."id") LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books_x_tags"."book_id" = "books"."id") WHERE ((("author"."id" = 1))) AND ("books_x_tags"."tag_id" IN (1)) GROUP BY "books_x_tags"."tag_id"; -SELECT DISTINCT * FROM (SELECT "books_x_tags"."book_id", "books_x_tags"."tag_id" FROM "books" AS "books" LEFT JOIN "public"."authors" AS "author" ON ("books"."author_id" = "author"."id") LEFT JOIN "tag_followers" AS "author_tagFollowers_any" ON ("author"."id" = "author_tagFollowers_any"."author_id") LEFT JOIN "public"."authors" AS "author_tagFollowers_author_any" ON ("author_tagFollowers_any"."author_id" = "author_tagFollowers_author_any"."id") LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books_x_tags"."book_id" = "books"."id") WHERE ((("author_tagFollowers_author_any"."id" = 1))) AND ("books_x_tags"."tag_id" IN (1)) ORDER BY "books"."title" ASC) AS "__tmp"; +SELECT DISTINCT * FROM (SELECT "books_x_tags"."book_id", "books_x_tags"."tag_id" FROM "books" AS "books" LEFT JOIN "public"."authors" AS "author" ON ("books"."author_id" = "author"."id") LEFT JOIN "tag_followers" AS "author_tagFollowers_any" ON ("author"."id" = "author_tagFollowers_any"."author_id") LEFT JOIN "public"."authors" AS "author_tagFollowers_author_any" ON ("author_tagFollowers_any"."author_id" = "author_tagFollowers_author_any"."id") LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books_x_tags"."book_id" = "books"."id") WHERE ((("author_tagFollowers_author_any"."id" = 1))) AND ("books_x_tags"."tag_id" IN (1)) GROUP BY "books"."id", "books"."title", "books_x_tags"."book_id", "books_x_tags"."tag_id" ORDER BY "books"."title" ASC) AS "__tmp"; SELECT "books".* FROM "books" AS "books" WHERE (("books"."id" IN (1))); -SELECT "books_x_tags"."tag_id", COUNT(DISTINCT "books_x_tags"."book_id") AS "count" FROM "books" AS "books" LEFT JOIN "public"."authors" AS "author" ON ("books"."author_id" = "author"."id") LEFT JOIN "tag_followers" AS "author_tagFollowers_any" ON ("author"."id" = "author_tagFollowers_any"."author_id") LEFT JOIN "public"."authors" AS "author_tagFollowers_author_any" ON ("author_tagFollowers_any"."author_id" = "author_tagFollowers_author_any"."id") LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books_x_tags"."book_id" = "books"."id") WHERE ((("author_tagFollowers_author_any"."id" = 1))) AND ("books_x_tags"."tag_id" IN (1)) GROUP BY "books_x_tags"."tag_id"; +SELECT "books_x_tags"."tag_id", COUNT(DISTINCT "books_x_tags"."book_id") AS "count" FROM "books" AS "books" LEFT JOIN "public"."authors" AS "author" ON ("books"."author_id" = "author"."id") LEFT JOIN "tag_followers" AS "author_tagFollowers_any" ON ("author"."id" = "author_tagFollowers_any"."author_id") LEFT JOIN "public"."authors" AS "author_tagFollowers_author_any" ON ("author_tagFollowers_any"."author_id" = "author_tagFollowers_author_any"."id") LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books_x_tags"."book_id" = "books"."id") WHERE ((("author_tagFollowers_author_any"."id" = 1))) AND ("books_x_tags"."tag_id" IN (1)) GROUP BY "books"."id", "books"."title", "books_x_tags"."tag_id"; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testCountStoredOnManyToManyCondition.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testCountStoredOnManyToManyCondition.sql index 744dbc29..0d94db00 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testCountStoredOnManyToManyCondition.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testCountStoredOnManyToManyCondition.sql @@ -1 +1 @@ -SELECT COUNT(*) AS count FROM (SELECT "books"."id" FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ("books_x_tags"."tag_id" = "tags_any"."id") WHERE (("tags_any"."name" = 'Tag 2'))) temp; +SELECT COUNT(*) AS count FROM (SELECT "books"."id" FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ("books_x_tags"."tag_id" = "tags_any"."id") WHERE (("tags_any"."name" = 'Tag 2')) GROUP BY "books"."id") temp; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testJoinAcrossDifferentPaths.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testJoinAcrossDifferentPaths.sql index 4ae58394..e9bf2f75 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testJoinAcrossDifferentPaths.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testJoinAcrossDifferentPaths.sql @@ -1,7 +1,7 @@ -SELECT "books".* FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON (("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."name" = 'Tag 1') LEFT JOIN "books" AS "nextPart" ON ("books"."next_part" = "nextPart"."id") LEFT JOIN "books_x_tags" AS "nextPart_books_x_tags" ON ("nextPart"."id" = "nextPart_books_x_tags"."book_id") LEFT JOIN "tags" AS "nextPart_tags_any" ON (("nextPart_books_x_tags"."tag_id" = "nextPart_tags_any"."id") AND "nextPart_tags_any"."name" = 'Tag 3') GROUP BY "books"."id", "books"."id" HAVING ((COUNT("tags_any"."id") > 0) OR (COUNT("nextPart_tags_any"."id") > 0)) ORDER BY "books"."id" ASC; +SELECT "books".* FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON (("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."name" = 'Tag 1') LEFT JOIN "books" AS "nextPart" ON ("books"."next_part" = "nextPart"."id") LEFT JOIN "books_x_tags" AS "nextPart_books_x_tags" ON ("nextPart"."id" = "nextPart_books_x_tags"."book_id") LEFT JOIN "tags" AS "nextPart_tags_any" ON (("nextPart_books_x_tags"."tag_id" = "nextPart_tags_any"."id") AND "nextPart_tags_any"."name" = 'Tag 3') GROUP BY "books"."id" HAVING ((COUNT("tags_any"."id") > 0) OR (COUNT("nextPart_tags_any"."id") > 0)) ORDER BY "books"."id" ASC; START TRANSACTION; INSERT INTO "tags" ("name", "is_global") VALUES ('Tag 5', 'y'); SELECT CURRVAL('"tags_id_seq"'); INSERT INTO "books_x_tags" ("book_id", "tag_id") VALUES (4, 4); COMMIT; -SELECT "books".* FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ("books_x_tags"."tag_id" = "tags_any"."id") LEFT JOIN "books" AS "nextPart" ON ("books"."next_part" = "nextPart"."id") LEFT JOIN "books_x_tags" AS "nextPart_books_x_tags" ON ("nextPart"."id" = "nextPart_books_x_tags"."book_id") LEFT JOIN "tags" AS "nextPart_tags_any" ON ("nextPart_books_x_tags"."tag_id" = "nextPart_tags_any"."id") WHERE (("tags_any"."name" = 'Tag 5') AND ("nextPart_tags_any"."name" = 'Tag 3')) ORDER BY "books"."id" ASC; +SELECT "books".* FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ("books_x_tags"."tag_id" = "tags_any"."id") LEFT JOIN "books" AS "nextPart" ON ("books"."next_part" = "nextPart"."id") LEFT JOIN "books_x_tags" AS "nextPart_books_x_tags" ON ("nextPart"."id" = "nextPart_books_x_tags"."book_id") LEFT JOIN "tags" AS "nextPart_tags_any" ON ("nextPart_books_x_tags"."tag_id" = "nextPart_tags_any"."id") WHERE (("tags_any"."name" = 'Tag 5') AND ("nextPart_tags_any"."name" = 'Tag 3')) GROUP BY "books"."id" ORDER BY "books"."id" ASC; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasOneTest_testProperAggregation.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasOneTest_testProperAggregation.sql index 3d66f677..a35a1b94 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasOneTest_testProperAggregation.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasOneTest_testProperAggregation.sql @@ -1 +1,2 @@ -SELECT "books".* FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ("books_x_tags"."tag_id" = "tags_any"."id") LEFT JOIN "publishers" AS "publisher" ON ("books"."publisher_id" = "publisher"."publisher_id") WHERE (("tags_any"."id" = 1) AND ("publisher"."name" = 'Nextras publisher A')); +SELECT "books".* FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ("books_x_tags"."tag_id" = "tags_any"."id") LEFT JOIN "publishers" AS "publisher" ON ("books"."publisher_id" = "publisher"."publisher_id") WHERE (("tags_any"."id" = 1) AND ("publisher"."name" = 'Nextras publisher A')) GROUP BY "books"."id"; +SELECT "books".* FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_none" ON (("books_x_tags"."tag_id" = "tags_none"."id") AND "tags_none"."id" = 1) LEFT JOIN "publishers" AS "publisher" ON ("books"."publisher_id" = "publisher"."publisher_id") GROUP BY "books"."id", "publisher"."name" HAVING ((COUNT("tags_none"."id") = 0) AND ("publisher"."name" = 'Nextras publisher A')); diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testCountStoredOnOneHasManyRelationshipCondition.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testCountStoredOnOneHasManyRelationshipCondition.sql index 71c88b50..048372c9 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testCountStoredOnOneHasManyRelationshipCondition.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testCountStoredOnOneHasManyRelationshipCondition.sql @@ -1,3 +1,3 @@ SELECT "publishers".* FROM "publishers" AS "publishers" WHERE (("publishers"."publisher_id" = 1)); -SELECT "publisher_id", COUNT(DISTINCT "count") as "count" FROM (SELECT "books".*, "books"."id" AS "count" FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ("books_x_tags"."tag_id" = "tags_any"."id") WHERE ((("tags_any"."id" = 1))) AND ("books"."publisher_id" IN (1))) AS "temp" GROUP BY "publisher_id"; -SELECT "publisher_id", COUNT(DISTINCT "count") as "count" FROM (SELECT "books".*, "books"."id" AS "count" FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON (("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" = 1) WHERE "books"."publisher_id" IN (1) GROUP BY "books"."title", "books"."id" HAVING (("books"."title" = 'Book 1') OR (COUNT("tags_any"."id") > 0))) AS "temp" GROUP BY "publisher_id"; +SELECT "publisher_id", COUNT(DISTINCT "count") as "count" FROM (SELECT "books".*, "books"."id" AS "count" FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ("books_x_tags"."tag_id" = "tags_any"."id") WHERE ((("tags_any"."id" = 1))) AND ("books"."publisher_id" IN (1)) GROUP BY "books"."id") AS "temp" GROUP BY "publisher_id"; +SELECT "publisher_id", COUNT(DISTINCT "count") as "count" FROM (SELECT "books".*, "books"."id" AS "count" FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON (("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" = 1) WHERE "books"."publisher_id" IN (1) GROUP BY "books"."id", "books"."title" HAVING (("books"."title" = 'Book 1') OR (COUNT("tags_any"."id") > 0))) AS "temp" GROUP BY "publisher_id"; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testOrderByDifferentTableColumnOnHasManyRelationshipCondition.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testOrderByDifferentTableColumnOnHasManyRelationshipCondition.sql new file mode 100644 index 00000000..95adf688 --- /dev/null +++ b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testOrderByDifferentTableColumnOnHasManyRelationshipCondition.sql @@ -0,0 +1,2 @@ +SELECT "publishers".* FROM "publishers" AS "publishers" WHERE (("publishers"."publisher_id" = 1)); +SELECT "books".* FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ("books_x_tags"."tag_id" = "tags_any"."id") LEFT JOIN "public"."authors" AS "author" ON ("books"."author_id" = "author"."id") WHERE ((("tags_any"."id" = 1))) AND ("books"."publisher_id" IN (1)) GROUP BY "books"."id", "author"."name" ORDER BY "author"."name" ASC; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testSameTableJoinWithImplicitAggregation.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testSameTableJoinWithImplicitAggregation.sql index 38918f42..bef4622c 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testSameTableJoinWithImplicitAggregation.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testSameTableJoinWithImplicitAggregation.sql @@ -1,2 +1,2 @@ -SELECT COUNT(*) AS count FROM (SELECT "books"."id" FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ((("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" IN (1)) OR (("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" IS NULL)) GROUP BY "books"."id", "books"."id" HAVING (((COUNT("tags_any"."id") > 0)) OR ((COUNT("tags_any"."id") > 0)))) temp; -SELECT "books".* FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ((("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" IN (1)) OR (("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" IS NULL)) GROUP BY "books"."id", "books"."id" HAVING (((COUNT("tags_any"."id") > 0)) OR ((COUNT("tags_any"."id") > 0))); +SELECT COUNT(*) AS count FROM (SELECT "books"."id" FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ((("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" IN (1)) OR (("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" IS NULL)) GROUP BY "books"."id" HAVING (((COUNT("tags_any"."id") > 0)) OR ((COUNT("tags_any"."id") > 0)))) temp; +SELECT "books".* FROM "books" AS "books" LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books"."id" = "books_x_tags"."book_id") LEFT JOIN "tags" AS "tags_any" ON ((("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" IN (1)) OR (("books_x_tags"."tag_id" = "tags_any"."id") AND "tags_any"."id" IS NULL)) GROUP BY "books"."id" HAVING (((COUNT("tags_any"."id") > 0)) OR ((COUNT("tags_any"."id") > 0)));