From 884a1ef8382755502170f30d80e8a2cf8a5886d3 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Tue, 20 Feb 2024 16:45:04 +0100 Subject: [PATCH 1/3] rename DbalTableJoin::$primaryKeys to $groupByColumns --- src/Collection/Aggregations/AnyAggregator.php | 18 +++++++------ .../Aggregations/CountAggregator.php | 27 +++++++++++++------ .../Aggregations/NoneAggregator.php | 18 +++++++------ .../Functions/FetchPropertyFunction.php | 4 +-- .../Functions/Result/DbalTableJoin.php | 8 +++--- .../Helpers/DbalQueryBuilderHelper.php | 2 +- 6 files changed, 46 insertions(+), 31 deletions(-) diff --git a/src/Collection/Aggregations/AnyAggregator.php b/src/Collection/Aggregations/AnyAggregator.php index d3a8b692..e4cd5f8c 100644 --- a/src/Collection/Aggregations/AnyAggregator.php +++ b/src/Collection/Aggregations/AnyAggregator.php @@ -7,9 +7,9 @@ use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalTableJoin; use Nextras\Orm\Exception\InvalidArgumentException; -use Nextras\Orm\Exception\InvalidStateException; use function array_merge; use function array_pop; +use function count; /** @@ -55,14 +55,16 @@ public function aggregateExpression( $joins = $expression->joins; $join = array_pop($joins); if ($join === null) { - throw new InvalidStateException('Aggregation applied over expression without a relationship'); + throw new InvalidArgumentException('Any aggregation applied over expression without a relationship.'); } - if (count($join->primaryKeys) === 0) { - throw new InvalidArgumentException('Aggregation applied over a table join without specifying a primary key.'); + if (count($join->groupByColumns) === 0) { + throw new InvalidArgumentException( + 'Aggregation applied over a table join without specifying a group-by column (primary key).', + ); } - if (count($join->primaryKeys) > 1) { + if (count($join->groupByColumns) > 1) { throw new InvalidArgumentException( - 'Aggregation applied over a table join with multi column primary key; currently, this is not supported.', + 'Aggregation applied over a table join with multiple group-by columns; currently, this is not supported.', ); } @@ -72,12 +74,12 @@ public function aggregateExpression( toAlias: $join->toAlias, onExpression: "($join->onExpression) AND $expression->expression", onArgs: array_merge($join->onArgs, $expression->args), - primaryKeys: $join->primaryKeys, + groupByColumns: $join->groupByColumns, ); return new DbalExpressionResult( expression: 'COUNT(%table.%column) > 0', - args: [$join->toAlias, $join->primaryKeys[0]], + args: [$join->toAlias, $join->groupByColumns[0]], joins: $joins, groupBy: $expression->groupBy, isHavingClause: true, diff --git a/src/Collection/Aggregations/CountAggregator.php b/src/Collection/Aggregations/CountAggregator.php index a011114b..dd9815b2 100644 --- a/src/Collection/Aggregations/CountAggregator.php +++ b/src/Collection/Aggregations/CountAggregator.php @@ -7,7 +7,9 @@ use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalTableJoin; use Nextras\Orm\Exception\InvalidArgumentException; -use Nextras\Orm\Exception\InvalidStateException; +use function array_filter; +use function array_pop; +use function count; /** @@ -59,14 +61,16 @@ public function aggregateExpression( $joins = $expression->joins; $join = array_pop($joins); if ($join === null) { - throw new InvalidStateException('Aggregation applied over expression without a relationship'); + throw new InvalidArgumentException('Count aggregation applied over expression without a relationship.'); } - if (count($join->primaryKeys) === 0) { - throw new InvalidArgumentException('Aggregation applied over a table join without specifying a primary key.'); + if (count($join->groupByColumns) === 0) { + throw new InvalidArgumentException( + 'Aggregation applied over a table join without specifying a group-by column (primary key).', + ); } - if (count($join->primaryKeys) > 1) { + if (count($join->groupByColumns) > 1) { throw new InvalidArgumentException( - 'Aggregation applied over a table join with multi column primary key; currently, this is not supported.', + 'Aggregation applied over a table join with multiple group-by columns; currently, this is not supported.', ); } @@ -76,12 +80,19 @@ public function aggregateExpression( toAlias: $join->toAlias, onExpression: "($join->onExpression) AND $expression->expression", onArgs: array_merge($join->onArgs, $expression->args), - primaryKeys: $join->primaryKeys, + groupByColumns: $join->groupByColumns, ); return new DbalExpressionResult( expression: 'COUNT(%table.%column) >= %i AND COUNT(%table.%column) <= %i', - args: [$join->toAlias, $join->primaryKeys[0], $this->atLeast, $join->toAlias, $join->primaryKeys[0], $this->atMost], + args: [ + $join->toAlias, + $join->groupByColumns[0], + $this->atLeast, + $join->toAlias, + $join->groupByColumns[0], + $this->atMost, + ], joins: $joins, groupBy: $expression->groupBy, isHavingClause: true, diff --git a/src/Collection/Aggregations/NoneAggregator.php b/src/Collection/Aggregations/NoneAggregator.php index c02c851e..b1f5ec6a 100644 --- a/src/Collection/Aggregations/NoneAggregator.php +++ b/src/Collection/Aggregations/NoneAggregator.php @@ -7,9 +7,9 @@ use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalTableJoin; use Nextras\Orm\Exception\InvalidArgumentException; -use Nextras\Orm\Exception\InvalidStateException; use function array_merge; use function array_pop; +use function count; /** @@ -55,14 +55,16 @@ public function aggregateExpression( $joins = $expression->joins; $join = array_pop($joins); if ($join === null) { - throw new InvalidStateException('Aggregation applied over expression without a relationship'); + throw new InvalidArgumentException('None aggregation applied over expression without a relationship.'); } - if (count($join->primaryKeys) === 0) { - throw new InvalidArgumentException('Aggregation applied over a table join without specifying a primary key.'); + if (count($join->groupByColumns) === 0) { + throw new InvalidArgumentException( + 'Aggregation applied over a table join without specifying a group-by column (primary key).', + ); } - if (count($join->primaryKeys) > 1) { + if (count($join->groupByColumns) > 1) { throw new InvalidArgumentException( - 'Aggregation applied over a table join with multi column primary key; currently, this is not supported.', + 'Aggregation applied over a table join with multiple group-by columns; currently, this is not supported.', ); } @@ -72,12 +74,12 @@ public function aggregateExpression( toAlias: $join->toAlias, onExpression: "($join->onExpression) AND $expression->expression", onArgs: array_merge($join->onArgs, $expression->args), - primaryKeys: $join->primaryKeys, + groupByColumns: $join->groupByColumns, ); return new DbalExpressionResult( expression: 'COUNT(%table.%column) = 0', - args: [$join->toAlias, $join->primaryKeys[0]], + args: [$join->toAlias, $join->groupByColumns[0]], joins: $joins, groupBy: $expression->groupBy, isHavingClause: true, diff --git a/src/Collection/Functions/FetchPropertyFunction.php b/src/Collection/Functions/FetchPropertyFunction.php index b81baf28..03946abe 100644 --- a/src/Collection/Functions/FetchPropertyFunction.php +++ b/src/Collection/Functions/FetchPropertyFunction.php @@ -335,7 +335,7 @@ private function processRelationship( toAlias: $joinAlias, onExpression: "%table.%column = %table.%column", onArgs: [$currentAlias, $fromColumn, $joinAlias, $inColumn], - primaryKeys: [$currentConventions->getStoragePrimaryKey()[0]], + groupByColumns: [$currentConventions->getStoragePrimaryKey()[0]], ); $currentAlias = $joinAlias; @@ -358,7 +358,7 @@ private function processRelationship( toAlias: $targetAlias, onExpression: "%table.%column = %table.%column", onArgs: [$currentAlias, $fromColumn, $targetAlias, $toColumn], - primaryKeys: [$targetConventions->getStoragePrimaryKey()[0]], + groupByColumns: [$targetConventions->getStoragePrimaryKey()[0]], ); return [$targetAlias, $targetConventions, $targetEntityMetadata, $targetMapper]; diff --git a/src/Collection/Functions/Result/DbalTableJoin.php b/src/Collection/Functions/Result/DbalTableJoin.php index 5dacb111..75fbe2ba 100644 --- a/src/Collection/Functions/Result/DbalTableJoin.php +++ b/src/Collection/Functions/Result/DbalTableJoin.php @@ -11,8 +11,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::$primaryKeys}, - * if not needed or possible, pass jum an empty array. + * 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. * * @experimental */ @@ -24,7 +24,7 @@ class DbalTableJoin * @param literal-string $toAlias * @param literal-string $onExpression * @param array $onArgs - * @param list $primaryKeys + * @param list $groupByColumns */ public function __construct( public readonly string $toExpression, @@ -32,7 +32,7 @@ public function __construct( public readonly string $toAlias, public readonly string $onExpression, public readonly array $onArgs, - public readonly array $primaryKeys = [], + public readonly array $groupByColumns = [], ) { } diff --git a/src/Collection/Helpers/DbalQueryBuilderHelper.php b/src/Collection/Helpers/DbalQueryBuilderHelper.php index c512dc84..2b587890 100644 --- a/src/Collection/Helpers/DbalQueryBuilderHelper.php +++ b/src/Collection/Helpers/DbalQueryBuilderHelper.php @@ -169,7 +169,7 @@ public function mergeJoins(string $dbalModifier, array $joins): array toAlias: $first->toAlias, onExpression: $dbalModifier, onArgs: [$args], - primaryKeys: $first->primaryKeys, + groupByColumns: $first->groupByColumns, ); } } From f5994baff352b6387b30cdb5638c9c7a08e2c4ae Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Wed, 21 Feb 2024 12:11:59 +0100 Subject: [PATCH 2/3] use simple filtering joinst for simple ANY relationship condition --- src/Collection/Aggregations/AnyAggregator.php | 8 ++++++ .../Aggregations/CountAggregator.php | 2 ++ .../Aggregations/IDbalAggregator.php | 2 ++ .../Aggregations/NoneAggregator.php | 2 ++ .../Aggregations/NumericAggregator.php | 2 ++ src/Collection/DbalCollection.php | 12 +++++---- .../Expression/ExpressionContext.php | 17 +++++++++++++ .../Functions/BaseCompareFunction.php | 4 ++- .../BaseNumericAggregateFunction.php | 5 +++- .../Functions/CollectionFunction.php | 2 ++ .../Functions/CompareLikeFunction.php | 4 ++- .../Functions/ConjunctionOperatorFunction.php | 3 +++ .../Functions/DisjunctionOperatorFunction.php | 3 +++ .../Functions/FetchPropertyFunction.php | 3 ++- .../Functions/JunctionFunctionTrait.php | 6 +++-- .../Functions/Result/DbalExpressionResult.php | 25 ++++++++----------- .../Helpers/DbalQueryBuilderHelper.php | 7 +++--- .../Dbal/RelationshipMapperManyHasMany.php | 20 ++++++++++++++- .../relationships.manyHasMany.phpt | 3 +++ .../Dbal/DbalValueOperatorFunctionTest.phpt | 3 ++- .../CollectionAggregationJoinTest_testAny.sql | 8 +++--- ...onAggregationJoinTest_testAnyDependent.sql | 4 +-- ...egationJoinTest_testIndependentSelects.sql | 4 +-- .../CollectionTest_testDistinct.sql | 2 +- ...oredOnManyHasManyRelationshipCondition.sql | 6 ++--- ...t_testCountStoredOnManyToManyCondition.sql | 2 +- ...sManyTest_testJoinAcrossDifferentPaths.sql | 2 +- ...ipManyHasOneTest_testProperAggregation.sql | 2 +- ...toredOnOneHasManyRelationshipCondition.sql | 2 +- 29 files changed, 119 insertions(+), 46 deletions(-) create mode 100644 src/Collection/Expression/ExpressionContext.php diff --git a/src/Collection/Aggregations/AnyAggregator.php b/src/Collection/Aggregations/AnyAggregator.php index e4cd5f8c..69fdf7bc 100644 --- a/src/Collection/Aggregations/AnyAggregator.php +++ b/src/Collection/Aggregations/AnyAggregator.php @@ -4,6 +4,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalTableJoin; use Nextras\Orm\Exception\InvalidArgumentException; @@ -50,8 +51,15 @@ public function aggregateValues(array $values): bool public function aggregateExpression( QueryBuilder $queryBuilder, DbalExpressionResult $expression, + ExpressionContext $context, ): DbalExpressionResult { + if ($context !== ExpressionContext::FilterOr) { + // When we are not in OR expression, we may simply filter the joined table by the condition. + // Otherwise, we have to employ a HAVING clause with aggregation function. + return $expression; + } + $joins = $expression->joins; $join = array_pop($joins); if ($join === null) { diff --git a/src/Collection/Aggregations/CountAggregator.php b/src/Collection/Aggregations/CountAggregator.php index dd9815b2..97d8abac 100644 --- a/src/Collection/Aggregations/CountAggregator.php +++ b/src/Collection/Aggregations/CountAggregator.php @@ -4,6 +4,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalTableJoin; use Nextras\Orm\Exception\InvalidArgumentException; @@ -56,6 +57,7 @@ public function aggregateValues(array $values): bool public function aggregateExpression( QueryBuilder $queryBuilder, DbalExpressionResult $expression, + ExpressionContext $context, ): DbalExpressionResult { $joins = $expression->joins; diff --git a/src/Collection/Aggregations/IDbalAggregator.php b/src/Collection/Aggregations/IDbalAggregator.php index 9a0e306c..d72284b8 100644 --- a/src/Collection/Aggregations/IDbalAggregator.php +++ b/src/Collection/Aggregations/IDbalAggregator.php @@ -4,6 +4,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; @@ -12,5 +13,6 @@ interface IDbalAggregator extends IAggregator public function aggregateExpression( QueryBuilder $queryBuilder, DbalExpressionResult $expression, + ExpressionContext $context, ): DbalExpressionResult; } diff --git a/src/Collection/Aggregations/NoneAggregator.php b/src/Collection/Aggregations/NoneAggregator.php index b1f5ec6a..cced4de8 100644 --- a/src/Collection/Aggregations/NoneAggregator.php +++ b/src/Collection/Aggregations/NoneAggregator.php @@ -4,6 +4,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalTableJoin; use Nextras\Orm\Exception\InvalidArgumentException; @@ -50,6 +51,7 @@ public function aggregateValues(array $values): bool public function aggregateExpression( QueryBuilder $queryBuilder, DbalExpressionResult $expression, + ExpressionContext $context, ): DbalExpressionResult { $joins = $expression->joins; diff --git a/src/Collection/Aggregations/NumericAggregator.php b/src/Collection/Aggregations/NumericAggregator.php index 5c3c09d6..b0b8bf93 100644 --- a/src/Collection/Aggregations/NumericAggregator.php +++ b/src/Collection/Aggregations/NumericAggregator.php @@ -4,6 +4,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; @@ -41,6 +42,7 @@ public function aggregateValues(array $values): mixed public function aggregateExpression( QueryBuilder $queryBuilder, DbalExpressionResult $expression, + ExpressionContext $context, ): DbalExpressionResult { return new DbalExpressionResult( diff --git a/src/Collection/DbalCollection.php b/src/Collection/DbalCollection.php index 213cf0c8..6a3b6d88 100644 --- a/src/Collection/DbalCollection.php +++ b/src/Collection/DbalCollection.php @@ -6,6 +6,7 @@ use Iterator; use Nextras\Dbal\IConnection; use Nextras\Dbal\QueryBuilder\QueryBuilder; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper; use Nextras\Orm\Collection\Helpers\FetchPairsHelper; @@ -122,13 +123,13 @@ public function orderBy($expression, string $direction = ICollection::ASC): ICol foreach ($expression as $subExpression => $subDirection) { $collection->ordering[] = [ - $helper->processExpression($collection->queryBuilder, $subExpression, null), + $helper->processExpression($collection->queryBuilder, $subExpression, ExpressionContext::FilterAnd, null), $subDirection, ]; } } else { $collection->ordering[] = [ - $helper->processExpression($collection->queryBuilder, $expression, null), + $helper->processExpression($collection->queryBuilder, $expression, ExpressionContext::ValueExpression, null), $direction, ]; } @@ -307,9 +308,10 @@ public function getQueryBuilder(): QueryBuilder if (count($args) > 0) { array_unshift($args, ICollection::AND); $expression = $helper->processExpression( - $this->queryBuilder, - $args, - null, + builder: $this->queryBuilder, + expression: $args, + context: ExpressionContext::FilterAnd, + aggregator: null, ); $joins = $expression->joins; if ($expression->isHavingClause) { diff --git a/src/Collection/Expression/ExpressionContext.php b/src/Collection/Expression/ExpressionContext.php new file mode 100644 index 00000000..5e1c0368 --- /dev/null +++ b/src/Collection/Expression/ExpressionContext.php @@ -0,0 +1,17 @@ +processExpression($builder, $args[0], $aggregator); + $expression = $helper->processExpression($builder, $args[0], $context, $aggregator); if ($expression->valueNormalizer !== null) { $cb = $expression->valueNormalizer; diff --git a/src/Collection/Functions/BaseNumericAggregateFunction.php b/src/Collection/Functions/BaseNumericAggregateFunction.php index d47290bc..242d0efd 100644 --- a/src/Collection/Functions/BaseNumericAggregateFunction.php +++ b/src/Collection/Functions/BaseNumericAggregateFunction.php @@ -7,6 +7,7 @@ use Nextras\Orm\Collection\Aggregations\IArrayAggregator; use Nextras\Orm\Collection\Aggregations\IDbalAggregator; use Nextras\Orm\Collection\Aggregations\NumericAggregator; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Helpers\ArrayCollectionHelper; @@ -54,6 +55,7 @@ public function processDbalExpression( DbalQueryBuilderHelper $helper, QueryBuilder $builder, array $args, + ExpressionContext $context, ?IDbalAggregator $aggregator = null, ): DbalExpressionResult { @@ -63,6 +65,7 @@ public function processDbalExpression( throw new InvalidStateException("Cannot apply two aggregations simultaneously."); } - return $helper->processExpression($builder, $args[0], $this->aggregator)->applyAggregator($builder); + return $helper->processExpression($builder, $args[0], $context, $this->aggregator) + ->applyAggregator($builder, ExpressionContext::ValueExpression); } } diff --git a/src/Collection/Functions/CollectionFunction.php b/src/Collection/Functions/CollectionFunction.php index 62af6fef..1bee8e4c 100644 --- a/src/Collection/Functions/CollectionFunction.php +++ b/src/Collection/Functions/CollectionFunction.php @@ -6,6 +6,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; use Nextras\Orm\Collection\Aggregations\IArrayAggregator; use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Helpers\ArrayCollectionHelper; @@ -45,6 +46,7 @@ public function processDbalExpression( DbalQueryBuilderHelper $helper, QueryBuilder $builder, array $args, + ExpressionContext $context, ?IDbalAggregator $aggregator = null, ): DbalExpressionResult; } diff --git a/src/Collection/Functions/CompareLikeFunction.php b/src/Collection/Functions/CompareLikeFunction.php index e5a6948a..0a25f429 100644 --- a/src/Collection/Functions/CompareLikeFunction.php +++ b/src/Collection/Functions/CompareLikeFunction.php @@ -7,6 +7,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; use Nextras\Orm\Collection\Aggregations\IArrayAggregator; use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Expression\LikeExpression; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; @@ -64,12 +65,13 @@ public function processDbalExpression( DbalQueryBuilderHelper $helper, QueryBuilder $builder, array $args, + ExpressionContext $context, ?IDbalAggregator $aggregator = null, ): DbalExpressionResult { assert(count($args) === 2); - $expression = $helper->processExpression($builder, $args[0], $aggregator); + $expression = $helper->processExpression($builder, $args[0], $context, $aggregator); $likeExpression = $args[1]; assert($likeExpression instanceof LikeExpression); diff --git a/src/Collection/Functions/ConjunctionOperatorFunction.php b/src/Collection/Functions/ConjunctionOperatorFunction.php index 13f34f9d..28f687b8 100644 --- a/src/Collection/Functions/ConjunctionOperatorFunction.php +++ b/src/Collection/Functions/ConjunctionOperatorFunction.php @@ -6,6 +6,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; use Nextras\Orm\Collection\Aggregations\IArrayAggregator; use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Helpers\ArrayCollectionHelper; @@ -105,6 +106,7 @@ public function processDbalExpression( DbalQueryBuilderHelper $helper, QueryBuilder $builder, array $args, + ExpressionContext $context, ?IDbalAggregator $aggregator = null, ): DbalExpressionResult { @@ -113,6 +115,7 @@ public function processDbalExpression( helper: $helper, builder: $builder, args: $args, + context: $context, aggregator: $aggregator, ); } diff --git a/src/Collection/Functions/DisjunctionOperatorFunction.php b/src/Collection/Functions/DisjunctionOperatorFunction.php index 675484fe..6086a168 100644 --- a/src/Collection/Functions/DisjunctionOperatorFunction.php +++ b/src/Collection/Functions/DisjunctionOperatorFunction.php @@ -6,6 +6,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; use Nextras\Orm\Collection\Aggregations\IArrayAggregator; use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Helpers\ArrayCollectionHelper; @@ -98,6 +99,7 @@ public function processDbalExpression( DbalQueryBuilderHelper $helper, QueryBuilder $builder, array $args, + ExpressionContext $context, ?IDbalAggregator $aggregator = null, ): DbalExpressionResult { @@ -106,6 +108,7 @@ public function processDbalExpression( helper: $helper, builder: $builder, args: $args, + context: ExpressionContext::FilterOr, aggregator: $aggregator, ); } diff --git a/src/Collection/Functions/FetchPropertyFunction.php b/src/Collection/Functions/FetchPropertyFunction.php index 03946abe..9e6ae521 100644 --- a/src/Collection/Functions/FetchPropertyFunction.php +++ b/src/Collection/Functions/FetchPropertyFunction.php @@ -10,6 +10,7 @@ use Nextras\Orm\Collection\Aggregations\IAggregator; use Nextras\Orm\Collection\Aggregations\IArrayAggregator; use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalTableJoin; @@ -154,6 +155,7 @@ public function processDbalExpression( DbalQueryBuilderHelper $helper, QueryBuilder $builder, array $args, + ExpressionContext $context, ?IDbalAggregator $aggregator = null, ): DbalExpressionResult { @@ -257,7 +259,6 @@ private function processTokens( joins: $joins, groupBy: $groupBy, aggregator: $makeDistinct ? ($aggregator ?? new AnyAggregator()) : null, - isHavingClause: $makeDistinct, propertyMetadata: $propertyMetadata, valueNormalizer: function ($value) use ($propertyMetadata, $currentConventions) { return $this->normalizeValue($value, $propertyMetadata, $currentConventions); diff --git a/src/Collection/Functions/JunctionFunctionTrait.php b/src/Collection/Functions/JunctionFunctionTrait.php index 4c13c4aa..f497cffb 100644 --- a/src/Collection/Functions/JunctionFunctionTrait.php +++ b/src/Collection/Functions/JunctionFunctionTrait.php @@ -6,6 +6,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; use Nextras\Orm\Collection\Aggregations\IAggregator; use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper; use Nextras\Orm\Exception\InvalidArgumentException; @@ -61,6 +62,7 @@ protected function processQueryBuilderExpressionWithModifier( DbalQueryBuilderHelper $helper, QueryBuilder $builder, array $args, + ExpressionContext $context, ?IDbalAggregator $aggregator, ): DbalExpressionResult { @@ -77,8 +79,8 @@ protected function processQueryBuilderExpressionWithModifier( } foreach ($normalized as $collectionFunctionArgs) { - $expression = $helper->processExpression($builder, $collectionFunctionArgs, $aggregator); - $expression = $expression->applyAggregator($builder); + $expression = $helper->processExpression($builder, $collectionFunctionArgs, $context, $aggregator); + $expression = $expression->applyAggregator($builder, $context); $processedArgs[] = $expression->getArgumentsForExpansion(); $joins = array_merge($joins, $expression->joins); $groupBy = array_merge($groupBy, $expression->groupBy); diff --git a/src/Collection/Functions/Result/DbalExpressionResult.php b/src/Collection/Functions/Result/DbalExpressionResult.php index 20fc1edb..0724c4af 100644 --- a/src/Collection/Functions/Result/DbalExpressionResult.php +++ b/src/Collection/Functions/Result/DbalExpressionResult.php @@ -5,16 +5,20 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Entity\Reflection\PropertyMetadata; -use Nextras\Orm\Exception\InvalidArgumentException; use function array_unshift; use function array_values; /** - * Represents an SQL expression. - * This class hold the main expression and its attributes. - * If possible, also holds a reference to a backing property of the expression. + * Represents an SQL expression. This class hold the main expression and its attributes. + * + * The class is used either in WHERE clause or in HAVING clause, it is decided from the outside of this class, + * yet this expression may force its using in HAVING clause by setting {@see $isHavingClause}. + * + * If possible, the expression holds a reference to a backing property of the expression {@see $propertyMetadata}; + * this is utilized to provide a value normalization. */ class DbalExpressionResult { @@ -64,6 +68,7 @@ class DbalExpressionResult */ public readonly ?string $dbalModifier; + /** * @param literal-string $expression * @param list $args @@ -93,10 +98,6 @@ public function __construct( $this->propertyMetadata = $propertyMetadata; $this->valueNormalizer = $valueNormalizer; $this->dbalModifier = $dbalModifier; - - if ($aggregator !== null && !$isHavingClause) { - throw new InvalidArgumentException('Dbal expression with aggregator is expected to be defined as HAVING clause.'); - } } @@ -155,12 +156,8 @@ public function withArgs(string $expression, array $args): DbalExpressionResult /** * Applies the aggregator and returns modified expression result. */ - public function applyAggregator(QueryBuilder $queryBuilder): DbalExpressionResult + public function applyAggregator(QueryBuilder $queryBuilder, ExpressionContext $context): DbalExpressionResult { - if ($this->aggregator === null) { - return $this; - } - - return $this->aggregator->aggregateExpression($queryBuilder, $this); + return $this->aggregator?->aggregateExpression($queryBuilder, $this, $context) ?? $this; } } diff --git a/src/Collection/Helpers/DbalQueryBuilderHelper.php b/src/Collection/Helpers/DbalQueryBuilderHelper.php index 2b587890..0f24d778 100644 --- a/src/Collection/Helpers/DbalQueryBuilderHelper.php +++ b/src/Collection/Helpers/DbalQueryBuilderHelper.php @@ -8,6 +8,7 @@ use Nextras\Dbal\Platforms\Data\Fqn; use Nextras\Dbal\QueryBuilder\QueryBuilder; use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\ConjunctionOperatorFunction; use Nextras\Orm\Collection\Functions\FetchPropertyFunction; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; @@ -73,19 +74,19 @@ public function __construct( public function processExpression( QueryBuilder $builder, array|string $expression, + ExpressionContext $context, ?IDbalAggregator $aggregator, ): DbalExpressionResult { if (is_string($expression)) { $function = FetchPropertyFunction::class; - $collectionFunction = $this->repository->getCollectionFunction($function); $expression = [$expression]; } else { $function = isset($expression[0]) ? array_shift($expression) : ICollection::AND; - $collectionFunction = $this->repository->getCollectionFunction($function); } - return $collectionFunction->processDbalExpression($this, $builder, $expression, $aggregator); + $collectionFunction = $this->repository->getCollectionFunction($function); + return $collectionFunction->processDbalExpression($this, $builder, $expression, $context, $aggregator); } diff --git a/src/Mapper/Dbal/RelationshipMapperManyHasMany.php b/src/Mapper/Dbal/RelationshipMapperManyHasMany.php index 1fa33e7b..191d9a0a 100644 --- a/src/Mapper/Dbal/RelationshipMapperManyHasMany.php +++ b/src/Mapper/Dbal/RelationshipMapperManyHasMany.php @@ -16,6 +16,7 @@ use Nextras\Orm\Entity\IEntityHasPreloadContainer; use Nextras\Orm\Entity\Reflection\PropertyMetadata; use Nextras\Orm\Exception\LogicException; +use Nextras\Orm\Exception\NotSupportedException; use Nextras\Orm\Mapper\IRelationshipMapperManyHasMany; use function array_keys; use function array_merge; @@ -138,6 +139,9 @@ private function fetchByTwoPassStrategy(QueryBuilder $builder, array $values): M /** @var literal-string $targetTable */ $targetTable = DbalQueryBuilderHelper::getAlias($this->joinTable); + $hasJoins = $builder->getClause('join')[0] !== null; + $hasOrderBy = $builder->getClause('order')[0] !== null; + $builder = clone $builder; $builder->joinLeft( "%table AS %table", @@ -148,18 +152,32 @@ private function fetchByTwoPassStrategy(QueryBuilder $builder, array $values): M "$targetTable.{$this->primaryKeyTo}", "{$sourceTable}." . $this->targetMapper->getConventions()->getStoragePrimaryKey()[0], ); + $builder->select('%column', "$targetTable.$this->primaryKeyTo"); $builder->addSelect('%column', "$targetTable.$this->primaryKeyFrom"); - if ($builder->getClause('having')[0] !== null) { + + if ($hasJoins && !$hasOrderBy) { $builder->addGroupBy('%column', "$targetTable.$this->primaryKeyTo"); $builder->addGroupBy('%column', "$targetTable.$this->primaryKeyFrom"); } if ($builder->hasLimitOffsetClause()) { + if ($hasJoins && $hasOrderBy) { + throw new NotSupportedException( + "Relationship cannot be fetched as it combines has-many joins, ORDER BY and LIMIT clause.", + ); + } $result = $this->processMultiResult($builder, $values, $targetTable); } else { $builder->andWhere('%column IN %any', "$targetTable.$this->primaryKeyFrom", $values); + if ($hasJoins && $hasOrderBy) { + /** @var literal-string $sql */ + $sql = $builder->getQuerySql(); + $builder = $this->connection->createQueryBuilder() + ->select('DISTINCT *') + ->from("($sql)", '__tmp', ...$builder->getQueryParameters()); + } $result = $this->connection->queryByQueryBuilder($builder); } diff --git a/tests/cases/integration/Relationships/relationships.manyHasMany.phpt b/tests/cases/integration/Relationships/relationships.manyHasMany.phpt index ac704d30..baa8034b 100644 --- a/tests/cases/integration/Relationships/relationships.manyHasMany.phpt +++ b/tests/cases/integration/Relationships/relationships.manyHasMany.phpt @@ -315,6 +315,9 @@ class RelationshipManyHasManyTest extends DataTestCase $books = $tag->books->findBy([ 'author->tagFollowers->author->id' => 1, ]); + if ($this->section !== Helper::SECTION_MSSQL) { + $books = $books->orderBy('title'); + } Assert::same(1, $books->count()); Assert::same(1, $books->countStored()); } diff --git a/tests/cases/unit/Mapper/Dbal/DbalValueOperatorFunctionTest.phpt b/tests/cases/unit/Mapper/Dbal/DbalValueOperatorFunctionTest.phpt index 4ed47497..925f7388 100644 --- a/tests/cases/unit/Mapper/Dbal/DbalValueOperatorFunctionTest.phpt +++ b/tests/cases/unit/Mapper/Dbal/DbalValueOperatorFunctionTest.phpt @@ -9,6 +9,7 @@ namespace NextrasTests\Orm\Mapper\Dbal; use Mockery; use Nextras\Dbal\QueryBuilder\QueryBuilder; +use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\BaseCompareFunction; use Nextras\Orm\Collection\Functions\CompareEqualsFunction; use Nextras\Orm\Collection\Functions\CompareNotEqualsFunction; @@ -40,7 +41,7 @@ class DbalValueOperatorFunctionTest extends TestCase Assert::same( $expected, - $function->processDbalExpression($helper, $builder, $expr)->getArgumentsForExpansion() + $function->processDbalExpression($helper, $builder, $expr, ExpressionContext::ValueExpression)->getArgumentsForExpansion() ); } diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testAny.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testAny.sql index cd964766..5f49e6d7 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") AND "books_any"."title" = 'Book 1') GROUP BY "authors"."id" HAVING (((COUNT("books_any"."id") > 0))); -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') GROUP BY "authors"."id" HAVING (((COUNT("books_any"."id") > 0)))) 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') GROUP BY "authors"."id" HAVING (((COUNT("books_any"."id") > 0))); -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') GROUP BY "authors"."id" HAVING (((COUNT("books_any"."id") > 0)))) 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'))); +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; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testAnyDependent.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testAnyDependent.sql index 257610ff..c7ce560f 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") AND "books_any"."title" = 'Book 1') AND ("authors"."id" = "books_any"."author_id")) LEFT JOIN "public"."authors" AS "books_translator_any" ON (("books_any"."translator_id" = "books_translator_any"."id") AND "books_translator_any"."id" IS NULL) GROUP BY "authors"."id", "authors"."id" HAVING ((COUNT("books_any"."id") > 0) AND (COUNT("books_translator_any"."id") > 0)); -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') AND ("authors"."id" = "books_any"."author_id")) LEFT JOIN "public"."authors" AS "books_translator_any" ON (("books_any"."translator_id" = "books_translator_any"."id") AND "books_translator_any"."id" IS NULL) GROUP BY "authors"."id", "authors"."id" HAVING ((COUNT("books_any"."id") > 0) AND (COUNT("books_translator_any"."id") > 0))) 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)); +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; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testIndependentSelects.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionAggregationJoinTest_testIndependentSelects.sql index fe16f7df..30598ac9 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") AND "books_any1"."title" = 'Book 1') AND (("authors"."id" = "books_any1"."author_id") AND "books_any1"."price" = 50)) LEFT JOIN "books" AS "books_any2" ON ((("authors"."id" = "books_any2"."author_id") AND "books_any2"."title" = 'Book 2') AND (("authors"."id" = "books_any2"."author_id") AND "books_any2"."price" = 150)) GROUP BY "authors"."id", "authors"."id", "authors"."id", "authors"."id" HAVING (((COUNT("books_any1"."id") > 0) AND (COUNT("books_any1"."id") > 0)) AND ((COUNT("books_any2"."id") > 0) AND (COUNT("books_any2"."id") > 0))); -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") AND "books_any1"."title" = 'Book 1') AND (("authors"."id" = "books_any1"."author_id") AND "books_any1"."price" = 50)) LEFT JOIN "books" AS "books_any2" ON ((("authors"."id" = "books_any2"."author_id") AND "books_any2"."title" = 'Book 2') AND (("authors"."id" = "books_any2"."author_id") AND "books_any2"."price" = 150)) GROUP BY "authors"."id", "authors"."id", "authors"."id", "authors"."id" HAVING (((COUNT("books_any1"."id") > 0) AND (COUNT("books_any1"."id") > 0)) AND ((COUNT("books_any2"."id") > 0) AND (COUNT("books_any2"."id") > 0)))) 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))); +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; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionTest_testDistinct.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionTest_testDistinct.sql index 45913d00..bd5bfab2 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") AND "tag_books_any"."id" = 1) GROUP BY "tag_followers"."tag_id", "tag_followers"."author_id" HAVING ((COUNT("tag_books_any"."id") > 0)); +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)); diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testCountStoredOnManyHasManyRelationshipCondition.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testCountStoredOnManyHasManyRelationshipCondition.sql index 088f7119..91c5b9d5 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)); +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".* 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 "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") AND "author_tagFollowers_author_any"."id" = 1) LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books_x_tags"."book_id" = "books"."id") WHERE "books_x_tags"."tag_id" IN (1) GROUP BY "books"."id", "books_x_tags"."book_id", "books_x_tags"."tag_id" HAVING ((COUNT("author_tagFollowers_author_any"."id") > 0)); +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 "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") AND "author_tagFollowers_author_any"."id" = 1) LEFT JOIN "books_x_tags" AS "books_x_tags" ON ("books_x_tags"."book_id" = "books"."id") WHERE "books_x_tags"."tag_id" IN (1) GROUP BY "books"."id", "books_x_tags"."tag_id" HAVING ((COUNT("author_tagFollowers_author_any"."id") > 0)); +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"; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testCountStoredOnManyToManyCondition.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testCountStoredOnManyToManyCondition.sql index 0c2da4ab..744dbc29 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") AND "tags_any"."name" = 'Tag 2') GROUP BY "books"."id" HAVING ((COUNT("tags_any"."id") > 0))) 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'))) temp; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testJoinAcrossDifferentPaths.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testJoinAcrossDifferentPaths.sql index 74f70256..4ae58394 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testJoinAcrossDifferentPaths.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasManyTest_testJoinAcrossDifferentPaths.sql @@ -4,4 +4,4 @@ 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") AND "tags_any"."name" = 'Tag 5') 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) AND (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") 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; diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasOneTest_testProperAggregation.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasOneTest_testProperAggregation.sql index 45c92340..3d66f677 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasOneTest_testProperAggregation.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipManyHasOneTest_testProperAggregation.sql @@ -1 +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") AND "tags_any"."id" = 1) LEFT JOIN "publishers" AS "publisher" ON ("books"."publisher_id" = "publisher"."publisher_id") GROUP BY "books"."id", "publisher"."name" HAVING ((COUNT("tags_any"."id") > 0) 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')); diff --git a/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testCountStoredOnOneHasManyRelationshipCondition.sql b/tests/sqls/NextrasTests/Orm/Integration/Relationships/RelationshipOneHasManyTest_testCountStoredOnOneHasManyRelationshipCondition.sql index 00be82c5..71c88b50 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") AND "tags_any"."id" = 1) WHERE "books"."publisher_id" IN (1) GROUP BY "books"."id" HAVING ((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))) 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"; From 3f4bdbdde93979ca17d33014eb92b868ed049601 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Mon, 11 Mar 2024 10:42:20 +0100 Subject: [PATCH 3/3] unify Aggregator interface to a single one --- .../{IDbalAggregator.php => Aggregator.php} | 22 ++++++++++++++++++- src/Collection/Aggregations/AnyAggregator.php | 4 ++-- .../Aggregations/CountAggregator.php | 4 ++-- src/Collection/Aggregations/IAggregator.php | 12 ---------- .../Aggregations/IArrayAggregator.php | 16 -------------- .../Aggregations/NoneAggregator.php | 4 ++-- .../Aggregations/NumericAggregator.php | 4 ++-- .../Functions/BaseCompareFunction.php | 7 +++--- .../BaseNumericAggregateFunction.php | 7 +++--- .../Functions/CollectionFunction.php | 10 ++++----- .../Functions/CompareLikeFunction.php | 7 +++--- .../Functions/ConjunctionOperatorFunction.php | 11 ++++------ .../Functions/DisjunctionOperatorFunction.php | 11 ++++------ .../Functions/FetchPropertyFunction.php | 18 +++++++-------- .../Functions/JunctionFunctionTrait.php | 16 ++++++-------- .../Result/ArrayExpressionResult.php | 10 ++++----- .../Functions/Result/DbalExpressionResult.php | 8 ++++--- .../Helpers/ArrayCollectionHelper.php | 10 ++++----- .../Helpers/DbalQueryBuilderHelper.php | 7 +++--- 19 files changed, 86 insertions(+), 102 deletions(-) rename src/Collection/Aggregations/{IDbalAggregator.php => Aggregator.php} (50%) delete mode 100644 src/Collection/Aggregations/IAggregator.php delete mode 100644 src/Collection/Aggregations/IArrayAggregator.php diff --git a/src/Collection/Aggregations/IDbalAggregator.php b/src/Collection/Aggregations/Aggregator.php similarity index 50% rename from src/Collection/Aggregations/IDbalAggregator.php rename to src/Collection/Aggregations/Aggregator.php index d72284b8..5ef07767 100644 --- a/src/Collection/Aggregations/IDbalAggregator.php +++ b/src/Collection/Aggregations/Aggregator.php @@ -8,8 +8,28 @@ use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; -interface IDbalAggregator extends IAggregator +/** + * @template T The type of the aggregation result value. + */ +interface Aggregator { + /** + * Returns a grouping "key" used to join multiple conditions/joins together. + * + * In SQL, it is used as table alias suffix. + * + * @return literal-string + */ + public function getAggregateKey(): string; + + + /** + * @param array $values + * @return T|null + */ + public function aggregateValues(array $values); + + public function aggregateExpression( QueryBuilder $queryBuilder, DbalExpressionResult $expression, diff --git a/src/Collection/Aggregations/AnyAggregator.php b/src/Collection/Aggregations/AnyAggregator.php index 69fdf7bc..09f8cb76 100644 --- a/src/Collection/Aggregations/AnyAggregator.php +++ b/src/Collection/Aggregations/AnyAggregator.php @@ -14,9 +14,9 @@ /** - * @implements IArrayAggregator + * @implements Aggregator */ -class AnyAggregator implements IDbalAggregator, IArrayAggregator +class AnyAggregator implements Aggregator { /** @var literal-string */ private string $aggregateKey; diff --git a/src/Collection/Aggregations/CountAggregator.php b/src/Collection/Aggregations/CountAggregator.php index 97d8abac..2190b4fe 100644 --- a/src/Collection/Aggregations/CountAggregator.php +++ b/src/Collection/Aggregations/CountAggregator.php @@ -14,9 +14,9 @@ /** - * @implements IArrayAggregator + * @implements Aggregator */ -class CountAggregator implements IDbalAggregator, IArrayAggregator +class CountAggregator implements Aggregator { private int $atLeast; diff --git a/src/Collection/Aggregations/IAggregator.php b/src/Collection/Aggregations/IAggregator.php deleted file mode 100644 index 0e53c836..00000000 --- a/src/Collection/Aggregations/IAggregator.php +++ /dev/null @@ -1,12 +0,0 @@ - $values - * @return T|null - */ - function aggregateValues(array $values); -} diff --git a/src/Collection/Aggregations/NoneAggregator.php b/src/Collection/Aggregations/NoneAggregator.php index cced4de8..ba047174 100644 --- a/src/Collection/Aggregations/NoneAggregator.php +++ b/src/Collection/Aggregations/NoneAggregator.php @@ -14,9 +14,9 @@ /** - * @implements IArrayAggregator + * @implements Aggregator */ -class NoneAggregator implements IDbalAggregator, IArrayAggregator +class NoneAggregator implements Aggregator { /** @var literal-string */ private string $aggregateKey; diff --git a/src/Collection/Aggregations/NumericAggregator.php b/src/Collection/Aggregations/NumericAggregator.php index b0b8bf93..78e2cd55 100644 --- a/src/Collection/Aggregations/NumericAggregator.php +++ b/src/Collection/Aggregations/NumericAggregator.php @@ -10,9 +10,9 @@ /** * @internal - * @implements IArrayAggregator + * @implements Aggregator */ -class NumericAggregator implements IDbalAggregator, IArrayAggregator +class NumericAggregator implements Aggregator { /** * @param callable(array): (number|null) $arrayAggregation diff --git a/src/Collection/Functions/BaseCompareFunction.php b/src/Collection/Functions/BaseCompareFunction.php index fc33c9e1..01154467 100644 --- a/src/Collection/Functions/BaseCompareFunction.php +++ b/src/Collection/Functions/BaseCompareFunction.php @@ -4,8 +4,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; -use Nextras\Orm\Collection\Aggregations\IArrayAggregator; -use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; @@ -22,7 +21,7 @@ public function processArrayExpression( ArrayCollectionHelper $helper, IEntity $entity, array $args, - ?IArrayAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): ArrayExpressionResult { assert(count($args) === 2); @@ -61,7 +60,7 @@ public function processDbalExpression( QueryBuilder $builder, array $args, ExpressionContext $context, - ?IDbalAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): DbalExpressionResult { assert(count($args) === 2); diff --git a/src/Collection/Functions/BaseNumericAggregateFunction.php b/src/Collection/Functions/BaseNumericAggregateFunction.php index 242d0efd..335baa4f 100644 --- a/src/Collection/Functions/BaseNumericAggregateFunction.php +++ b/src/Collection/Functions/BaseNumericAggregateFunction.php @@ -4,8 +4,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; -use Nextras\Orm\Collection\Aggregations\IArrayAggregator; -use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Collection\Aggregations\NumericAggregator; use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; @@ -34,7 +33,7 @@ public function processArrayExpression( ArrayCollectionHelper $helper, IEntity $entity, array $args, - ?IArrayAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): ArrayExpressionResult { assert(count($args) === 1 && is_string($args[0])); @@ -56,7 +55,7 @@ public function processDbalExpression( QueryBuilder $builder, array $args, ExpressionContext $context, - ?IDbalAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): DbalExpressionResult { assert(count($args) === 1 && is_string($args[0])); diff --git a/src/Collection/Functions/CollectionFunction.php b/src/Collection/Functions/CollectionFunction.php index 1bee8e4c..cad2b1b1 100644 --- a/src/Collection/Functions/CollectionFunction.php +++ b/src/Collection/Functions/CollectionFunction.php @@ -4,8 +4,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; -use Nextras\Orm\Collection\Aggregations\IArrayAggregator; -use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; @@ -27,13 +26,13 @@ interface CollectionFunction * execution. * Usually returns a boolean for filtering evaluation. * @param array $args - * @param IArrayAggregator|null $aggregator + * @param Aggregator|null $aggregator */ public function processArrayExpression( ArrayCollectionHelper $helper, IEntity $entity, array $args, - ?IArrayAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): ArrayExpressionResult; @@ -41,12 +40,13 @@ public function processArrayExpression( * Returns true if entity should stay in the result collection; the condition is evaluated in database and this * method just returns appropriate Nextras Dbal's filtering expression for passed args. * @param array $args + * @param Aggregator|null $aggregator */ public function processDbalExpression( DbalQueryBuilderHelper $helper, QueryBuilder $builder, array $args, ExpressionContext $context, - ?IDbalAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): DbalExpressionResult; } diff --git a/src/Collection/Functions/CompareLikeFunction.php b/src/Collection/Functions/CompareLikeFunction.php index 0a25f429..03b8adc5 100644 --- a/src/Collection/Functions/CompareLikeFunction.php +++ b/src/Collection/Functions/CompareLikeFunction.php @@ -5,8 +5,7 @@ use Nette\Utils\Strings; use Nextras\Dbal\QueryBuilder\QueryBuilder; -use Nextras\Orm\Collection\Aggregations\IArrayAggregator; -use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Expression\LikeExpression; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; @@ -25,7 +24,7 @@ public function processArrayExpression( ArrayCollectionHelper $helper, IEntity $entity, array $args, - ?IArrayAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): ArrayExpressionResult { assert(count($args) === 2); @@ -66,7 +65,7 @@ public function processDbalExpression( QueryBuilder $builder, array $args, ExpressionContext $context, - ?IDbalAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): DbalExpressionResult { assert(count($args) === 2); diff --git a/src/Collection/Functions/ConjunctionOperatorFunction.php b/src/Collection/Functions/ConjunctionOperatorFunction.php index 28f687b8..425b0c56 100644 --- a/src/Collection/Functions/ConjunctionOperatorFunction.php +++ b/src/Collection/Functions/ConjunctionOperatorFunction.php @@ -4,8 +4,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; -use Nextras\Orm\Collection\Aggregations\IArrayAggregator; -use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; @@ -13,7 +12,6 @@ use Nextras\Orm\Collection\Helpers\ConditionParser; use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper; use Nextras\Orm\Entity\IEntity; -use Nextras\Orm\Exception\InvalidArgumentException; use Nextras\Orm\Exception\InvalidStateException; @@ -33,13 +31,12 @@ public function processArrayExpression( ArrayCollectionHelper $helper, IEntity $entity, array $args, - ?IArrayAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): ArrayExpressionResult { [$normalized, $newAggregator] = $this->normalizeFunctions($args); if ($newAggregator !== null) { if ($aggregator !== null) throw new InvalidStateException("Cannot apply two aggregations simultaneously."); - if (!$newAggregator instanceof IArrayAggregator) throw new InvalidArgumentException('Array requires aggregation instance of IArrayAggregator.'); $aggregator = $newAggregator; } @@ -50,7 +47,7 @@ public function processArrayExpression( * aggregation. */ - /** @var array> $aggregators */ + /** @var array> $aggregators */ $aggregators = []; $values = []; $sizes = []; @@ -107,7 +104,7 @@ public function processDbalExpression( QueryBuilder $builder, array $args, ExpressionContext $context, - ?IDbalAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): DbalExpressionResult { return $this->processQueryBuilderExpressionWithModifier( diff --git a/src/Collection/Functions/DisjunctionOperatorFunction.php b/src/Collection/Functions/DisjunctionOperatorFunction.php index 6086a168..fbaaeaef 100644 --- a/src/Collection/Functions/DisjunctionOperatorFunction.php +++ b/src/Collection/Functions/DisjunctionOperatorFunction.php @@ -4,8 +4,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; -use Nextras\Orm\Collection\Aggregations\IArrayAggregator; -use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; @@ -13,7 +12,6 @@ use Nextras\Orm\Collection\Helpers\ConditionParser; use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper; use Nextras\Orm\Entity\IEntity; -use Nextras\Orm\Exception\InvalidArgumentException; use Nextras\Orm\Exception\InvalidStateException; @@ -33,17 +31,16 @@ public function processArrayExpression( ArrayCollectionHelper $helper, IEntity $entity, array $args, - ?IArrayAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): ArrayExpressionResult { [$normalized, $newAggregator] = $this->normalizeFunctions($args); if ($newAggregator !== null) { if ($aggregator !== null) throw new InvalidStateException("Cannot apply two aggregations simultaneously."); - if (!$newAggregator instanceof IArrayAggregator) throw new InvalidArgumentException('Array requires aggregation instance of IArrayAggregator.'); $aggregator = $newAggregator; } - /** @var array> $aggregators */ + /** @var array> $aggregators */ $aggregators = []; $values = []; $sizes = []; @@ -100,7 +97,7 @@ public function processDbalExpression( QueryBuilder $builder, array $args, ExpressionContext $context, - ?IDbalAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): DbalExpressionResult { return $this->processQueryBuilderExpressionWithModifier( diff --git a/src/Collection/Functions/FetchPropertyFunction.php b/src/Collection/Functions/FetchPropertyFunction.php index 9e6ae521..6b96ee83 100644 --- a/src/Collection/Functions/FetchPropertyFunction.php +++ b/src/Collection/Functions/FetchPropertyFunction.php @@ -7,9 +7,7 @@ use Nextras\Dbal\Platforms\Data\Column; use Nextras\Dbal\QueryBuilder\QueryBuilder; use Nextras\Orm\Collection\Aggregations\AnyAggregator; -use Nextras\Orm\Collection\Aggregations\IAggregator; -use Nextras\Orm\Collection\Aggregations\IArrayAggregator; -use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; @@ -53,7 +51,7 @@ public function processArrayExpression( ArrayCollectionHelper $helper, IEntity $entity, array $args, - ?IArrayAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): ArrayExpressionResult { $argsCount = count($args); @@ -74,14 +72,14 @@ public function processArrayExpression( /** * @param string[] $expressionTokens - * @param IArrayAggregator|null $aggregator + * @param Aggregator|null $aggregator */ private function getValueByTokens( ArrayCollectionHelper $helper, IEntity $entity, array $expressionTokens, EntityMetadata $sourceEntityMeta, - ?IArrayAggregator $aggregator, + ?Aggregator $aggregator, ): ArrayExpressionResult { if (!$entity instanceof $sourceEntityMeta->className) { @@ -156,7 +154,7 @@ public function processDbalExpression( QueryBuilder $builder, array $args, ExpressionContext $context, - ?IDbalAggregator $aggregator = null, + ?Aggregator $aggregator = null, ): DbalExpressionResult { $argsCount = count($args); @@ -177,12 +175,13 @@ public function processDbalExpression( /** * @param array $tokens * @param class-string|null $sourceEntity + * @param Aggregator|null $aggregator */ private function processTokens( array $tokens, ?string $sourceEntity, QueryBuilder $builder, - ?IDbalAggregator $aggregator, + ?Aggregator $aggregator, ): DbalExpressionResult { $lastToken = array_pop($tokens); @@ -271,6 +270,7 @@ private function processTokens( /** * @param array $tokens * @param DbalTableJoin[] $joins + * @param Aggregator|null $aggregator * @param DbalMapper $currentMapper * @return array{string, IConventions, EntityMetadata, DbalMapper} */ @@ -278,7 +278,7 @@ private function processRelationship( array $tokens, array &$joins, PropertyMetadata $property, - ?IAggregator $aggregator, + ?Aggregator $aggregator, IConventions $currentConventions, DbalMapper $currentMapper, string $currentAlias, diff --git a/src/Collection/Functions/JunctionFunctionTrait.php b/src/Collection/Functions/JunctionFunctionTrait.php index f497cffb..3c1093a4 100644 --- a/src/Collection/Functions/JunctionFunctionTrait.php +++ b/src/Collection/Functions/JunctionFunctionTrait.php @@ -4,12 +4,10 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; -use Nextras\Orm\Collection\Aggregations\IAggregator; -use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult; use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper; -use Nextras\Orm\Exception\InvalidArgumentException; use Nextras\Orm\Exception\InvalidStateException; use function array_shift; @@ -20,14 +18,14 @@ trait JunctionFunctionTrait { /** - * Normalizes directly entered column => value expression to expression array. + * Normalizes directly entered column => value expression to an expression array. * @param array $args - * @return array{list, IAggregator|null} + * @return array{list, Aggregator|null} */ protected function normalizeFunctions(array $args): array { $aggregator = null; - if (($args[0] ?? null) instanceof IAggregator) { + if (($args[0] ?? null) instanceof Aggregator) { $aggregator = array_shift($args); } @@ -54,8 +52,9 @@ protected function normalizeFunctions(array $args): array /** - * @param literal-string $dbalModifier either %or or %and dbal modifier + * @param literal-string $dbalModifier either `%or` or `%and` dbal modifier * @param array $args + * @param Aggregator|null $aggregator */ protected function processQueryBuilderExpressionWithModifier( string $dbalModifier, @@ -63,7 +62,7 @@ protected function processQueryBuilderExpressionWithModifier( QueryBuilder $builder, array $args, ExpressionContext $context, - ?IDbalAggregator $aggregator, + ?Aggregator $aggregator, ): DbalExpressionResult { $isHavingClause = false; @@ -74,7 +73,6 @@ protected function processQueryBuilderExpressionWithModifier( [$normalized, $newAggregator] = $this->normalizeFunctions($args); if ($newAggregator !== null) { if ($aggregator !== null) throw new InvalidStateException("Cannot apply two aggregations simultaneously."); - if (!$newAggregator instanceof IDbalAggregator) throw new InvalidArgumentException('Dbal requires aggregation instance of IDbalAggregator.'); $aggregator = $newAggregator; } diff --git a/src/Collection/Functions/Result/ArrayExpressionResult.php b/src/Collection/Functions/Result/ArrayExpressionResult.php index 645a7938..6341b8bb 100644 --- a/src/Collection/Functions/Result/ArrayExpressionResult.php +++ b/src/Collection/Functions/Result/ArrayExpressionResult.php @@ -3,7 +3,7 @@ namespace Nextras\Orm\Collection\Functions\Result; -use Nextras\Orm\Collection\Aggregations\IArrayAggregator; +use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Entity\Reflection\PropertyMetadata; @@ -25,18 +25,18 @@ class ArrayExpressionResult public readonly ?PropertyMetadata $propertyMetadata; /** - * @var IArrayAggregator|null + * @var Aggregator|null */ - public readonly ?IArrayAggregator $aggregator; + public readonly ?Aggregator $aggregator; /** * @param mixed $value - * @param IArrayAggregator|null $aggregator + * @param Aggregator|null $aggregator */ public function __construct( $value, - ?IArrayAggregator $aggregator = null, + ?Aggregator $aggregator = null, ?PropertyMetadata $propertyMetadata = null, ) { diff --git a/src/Collection/Functions/Result/DbalExpressionResult.php b/src/Collection/Functions/Result/DbalExpressionResult.php index 0724c4af..0f525710 100644 --- a/src/Collection/Functions/Result/DbalExpressionResult.php +++ b/src/Collection/Functions/Result/DbalExpressionResult.php @@ -4,7 +4,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; -use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Entity\Reflection\PropertyMetadata; use function array_unshift; @@ -48,8 +48,9 @@ class DbalExpressionResult /** * Result aggregator. + * @var Aggregator|null */ - public readonly ?IDbalAggregator $aggregator; + public readonly ?Aggregator $aggregator; /** * Bool if the expression will be incorporated into WHERE or HAVING clause. @@ -74,6 +75,7 @@ class DbalExpressionResult * @param list $args * @param DbalTableJoin[] $joins * @param array> $groupBy + * @param Aggregator|null $aggregator * @param bool $isHavingClause * @param literal-string $dbalModifier */ @@ -82,7 +84,7 @@ public function __construct( array $args, array $joins = [], array $groupBy = [], - ?IDbalAggregator $aggregator = null, + ?Aggregator $aggregator = null, bool $isHavingClause = false, ?PropertyMetadata $propertyMetadata = null, ?callable $valueNormalizer = null, diff --git a/src/Collection/Helpers/ArrayCollectionHelper.php b/src/Collection/Helpers/ArrayCollectionHelper.php index 1cb00a65..0f0ccca1 100644 --- a/src/Collection/Helpers/ArrayCollectionHelper.php +++ b/src/Collection/Helpers/ArrayCollectionHelper.php @@ -7,7 +7,7 @@ use DateTimeImmutable; use DateTimeInterface; use Nette\Utils\Arrays; -use Nextras\Orm\Collection\Aggregations\IArrayAggregator; +use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Collection\Functions\CollectionFunction; use Nextras\Orm\Collection\Functions\FetchPropertyFunction; use Nextras\Orm\Collection\Functions\Result\ArrayExpressionResult; @@ -35,10 +35,10 @@ public function __construct( /** * @param array $expr - * @param IArrayAggregator|null $aggregator + * @param Aggregator|null $aggregator * @return Closure(IEntity): ArrayExpressionResult */ - public function createFilter(array $expr, ?IArrayAggregator $aggregator): Closure + public function createFilter(array $expr, ?Aggregator $aggregator): Closure { $function = isset($expr[0]) ? array_shift($expr) : ICollection::AND; $customFunction = $this->repository->getCollectionFunction($function); @@ -99,12 +99,12 @@ public function createSorter(array $expressions): Closure /** * @param string|array|list $expression - * @param IArrayAggregator|null $aggregator + * @param Aggregator|null $aggregator */ public function getValue( IEntity $entity, array|string $expression, - ?IArrayAggregator $aggregator, + ?Aggregator $aggregator, ): ArrayExpressionResult { if (is_string($expression)) { diff --git a/src/Collection/Helpers/DbalQueryBuilderHelper.php b/src/Collection/Helpers/DbalQueryBuilderHelper.php index 0f24d778..521fe2bf 100644 --- a/src/Collection/Helpers/DbalQueryBuilderHelper.php +++ b/src/Collection/Helpers/DbalQueryBuilderHelper.php @@ -7,7 +7,7 @@ use Nette\Utils\Strings; use Nextras\Dbal\Platforms\Data\Fqn; use Nextras\Dbal\QueryBuilder\QueryBuilder; -use Nextras\Orm\Collection\Aggregations\IDbalAggregator; +use Nextras\Orm\Collection\Aggregations\Aggregator; use Nextras\Orm\Collection\Expression\ExpressionContext; use Nextras\Orm\Collection\Functions\ConjunctionOperatorFunction; use Nextras\Orm\Collection\Functions\FetchPropertyFunction; @@ -39,7 +39,7 @@ class DbalQueryBuilderHelper public static function getAlias(string|Fqn $name, array $tokens = []): string { $name = $name instanceof Fqn ? $name->name : $name; - $name = Strings::replace($name, '#[^a-z0-9_]#i', ''); + $name = Strings::replace($name, '#[^a-z0-9_]#i', replacement: ''); if (count($tokens) === 0) { return $name; } else { @@ -70,12 +70,13 @@ public function __construct( * {@link ConjunctionOperatorFunction} is used. * * @param array|array|list|string $expression + * @param Aggregator|null $aggregator */ public function processExpression( QueryBuilder $builder, array|string $expression, ExpressionContext $context, - ?IDbalAggregator $aggregator, + ?Aggregator $aggregator, ): DbalExpressionResult { if (is_string($expression)) {