Skip to content

Commit

Permalink
Merge pull request #652 from nextras/join-aggregations-any
Browse files Browse the repository at this point in the history
Fallback to a simple WHERE filter in simple ANY aggregations
  • Loading branch information
hrach authored Mar 11, 2024
2 parents ba30f9a + 3f4bdbd commit 86aba3e
Show file tree
Hide file tree
Showing 35 changed files with 266 additions and 194 deletions.
38 changes: 38 additions & 0 deletions src/Collection/Aggregations/Aggregator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php declare(strict_types = 1);

namespace Nextras\Orm\Collection\Aggregations;


use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\Expression\ExpressionContext;
use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult;


/**
* @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<T> $values
* @return T|null
*/
public function aggregateValues(array $values);


public function aggregateExpression(
QueryBuilder $queryBuilder,
DbalExpressionResult $expression,
ExpressionContext $context,
): DbalExpressionResult;
}
30 changes: 20 additions & 10 deletions src/Collection/Aggregations/AnyAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@


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;
use Nextras\Orm\Exception\InvalidStateException;
use function array_merge;
use function array_pop;
use function count;


/**
* @implements IArrayAggregator<bool>
* @implements Aggregator<bool>
*/
class AnyAggregator implements IDbalAggregator, IArrayAggregator
class AnyAggregator implements Aggregator
{
/** @var literal-string */
private string $aggregateKey;
Expand Down Expand Up @@ -50,19 +51,28 @@ 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) {
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.',
);
}

Expand All @@ -72,12 +82,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,
Expand Down
33 changes: 23 additions & 10 deletions src/Collection/Aggregations/CountAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@


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;
use Nextras\Orm\Exception\InvalidStateException;
use function array_filter;
use function array_pop;
use function count;


/**
* @implements IArrayAggregator<bool>
* @implements Aggregator<bool>
*/
class CountAggregator implements IDbalAggregator, IArrayAggregator
class CountAggregator implements Aggregator
{
private int $atLeast;

Expand Down Expand Up @@ -54,19 +57,22 @@ public function aggregateValues(array $values): bool
public function aggregateExpression(
QueryBuilder $queryBuilder,
DbalExpressionResult $expression,
ExpressionContext $context,
): DbalExpressionResult
{
$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.',
);
}

Expand All @@ -76,12 +82,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,
Expand Down
12 changes: 0 additions & 12 deletions src/Collection/Aggregations/IAggregator.php

This file was deleted.

16 changes: 0 additions & 16 deletions src/Collection/Aggregations/IArrayAggregator.php

This file was deleted.

16 changes: 0 additions & 16 deletions src/Collection/Aggregations/IDbalAggregator.php

This file was deleted.

24 changes: 14 additions & 10 deletions src/Collection/Aggregations/NoneAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@


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;
use Nextras\Orm\Exception\InvalidStateException;
use function array_merge;
use function array_pop;
use function count;


/**
* @implements IArrayAggregator<bool>
* @implements Aggregator<bool>
*/
class NoneAggregator implements IDbalAggregator, IArrayAggregator
class NoneAggregator implements Aggregator
{
/** @var literal-string */
private string $aggregateKey;
Expand Down Expand Up @@ -50,19 +51,22 @@ public function aggregateValues(array $values): bool
public function aggregateExpression(
QueryBuilder $queryBuilder,
DbalExpressionResult $expression,
ExpressionContext $context,
): DbalExpressionResult
{
$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.',
);
}

Expand All @@ -72,12 +76,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,
Expand Down
6 changes: 4 additions & 2 deletions src/Collection/Aggregations/NumericAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@


use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\Expression\ExpressionContext;
use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult;


/**
* @internal
* @implements IArrayAggregator<number>
* @implements Aggregator<number>
*/
class NumericAggregator implements IDbalAggregator, IArrayAggregator
class NumericAggregator implements Aggregator
{
/**
* @param callable(array<number>): (number|null) $arrayAggregation
Expand Down Expand Up @@ -41,6 +42,7 @@ public function aggregateValues(array $values): mixed
public function aggregateExpression(
QueryBuilder $queryBuilder,
DbalExpressionResult $expression,
ExpressionContext $context,
): DbalExpressionResult
{
return new DbalExpressionResult(
Expand Down
12 changes: 7 additions & 5 deletions src/Collection/DbalCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
];
}
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions src/Collection/Expression/ExpressionContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);

namespace Nextras\Orm\Collection\Expression;


/**
* @internal
*
* Determines if the expression is processed for AND subtree, OR subtree or pure expression, e.g., a sorting expression.
* OR subtree requires more complex SQL construction, in other modes we may optimize the resulting SQL.
*/
enum ExpressionContext
{
case FilterAnd;
case FilterOr;
case ValueExpression;
}
Loading

0 comments on commit 86aba3e

Please sign in to comment.