Skip to content

Commit

Permalink
rework collection functions to utilize two-phase expression processing
Browse files Browse the repository at this point in the history
To provide more smarted SQL rewrites, we need to know if the expression
itself is in AND/OR junction and if other parts of the junction
require a HAVING clause. This is possible only after getting the full
expression tree. Then we collect the actual expressions.

[closes #690]
[closes #686]
  • Loading branch information
hrach committed Oct 31, 2024
1 parent 67e14dc commit ad6dfe0
Show file tree
Hide file tree
Showing 24 changed files with 265 additions and 157 deletions.
2 changes: 1 addition & 1 deletion src/Collection/Aggregations/AnyAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function aggregateExpression(
ExpressionContext $context,
): DbalExpressionResult
{
if ($context !== ExpressionContext::FilterOr) {
if ($context !== ExpressionContext::FilterOrWithHavingClause) {
// 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;
Expand Down
13 changes: 8 additions & 5 deletions src/Collection/DbalCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,13 @@ public function orderBy($expression, string $direction = ICollection::ASC): ICol

foreach ($expression as $subExpression => $subDirection) {
$collection->ordering[] = [
$helper->processExpression($collection->queryBuilder, $subExpression, ExpressionContext::FilterAnd, null),
$helper->processExpression($collection->queryBuilder, $subExpression, null),
$subDirection,
];
}
} else {
$collection->ordering[] = [
$helper->processExpression($collection->queryBuilder, $expression, ExpressionContext::ValueExpression, null),
$helper->processExpression($collection->queryBuilder, $expression, null),
$direction,
];
}
Expand Down Expand Up @@ -294,15 +294,18 @@ public function getQueryBuilder(): QueryBuilder
$expression = $helper->processExpression(
builder: $this->queryBuilder,
expression: $args,
context: ExpressionContext::FilterAnd,
aggregator: null,
);
$finalContext = $expression->havingExpression === null
? ExpressionContext::FilterAnd
: ExpressionContext::FilterAndWithHavingClause;
$expression = $expression->collect($finalContext);
$joins = $expression->joins;
$groupBy = $expression->groupBy;
if ($expression->expression !== null) {
if ($expression->expression !== null && $expression->args !== []) {
$this->queryBuilder->andWhere($expression->expression, ...$expression->args);
}
if ($expression->havingExpression !== null) {
if ($expression->havingExpression !== null && $expression->havingArgs !== []) {
$this->queryBuilder->andHaving($expression->havingExpression, ...$expression->havingArgs);
}
if ($this->mapper->getDatabasePlatform()->getName() === MySqlPlatform::NAME) {
Expand Down
4 changes: 4 additions & 0 deletions src/Collection/Expression/ExpressionContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@


/**
* @internal
*
* Determines if the expression is processed for AND subtree, OR subtree or as a pure expression,
* e.g., a sorting expression.
*
Expand All @@ -13,5 +15,7 @@ enum ExpressionContext
{
case FilterAnd;
case FilterOr;
case FilterAndWithHavingClause;
case FilterOrWithHavingClause;
case ValueExpression;
}
27 changes: 18 additions & 9 deletions src/Collection/Functions/BaseCompareFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

use Nextras\Dbal\QueryBuilder\QueryBuilder;
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;
use Nextras\Orm\Collection\Helpers\ArrayCollectionHelper;
use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper;
use Nextras\Orm\Entity\IEntity;
use function array_map;
use function assert;
use function count;

Expand All @@ -34,12 +34,7 @@ public function processArrayExpression(
}

if ($valueReference->aggregator !== null) {
$values = array_map(
function ($value) use ($targetValue): bool {
return $this->evaluateInPhp($value, $targetValue);
},
$valueReference->value,
);
$values = $this->multiEvaluateInPhp($valueReference->value, $targetValue);
return new ArrayExpressionResult(
value: $values,
aggregator: $valueReference->aggregator,
Expand All @@ -59,13 +54,12 @@ public function processDbalExpression(
DbalQueryBuilderHelper $helper,
QueryBuilder $builder,
array $args,
ExpressionContext $context,
?Aggregator $aggregator = null,
): DbalExpressionResult
{
assert(count($args) === 2);

$expression = $helper->processExpression($builder, $args[0], $context, $aggregator);
$expression = $helper->processExpression($builder, $args[0], $aggregator);

if ($expression->valueNormalizer !== null) {
$cb = $expression->valueNormalizer;
Expand All @@ -81,6 +75,21 @@ public function processDbalExpression(
abstract protected function evaluateInPhp(mixed $sourceValue, mixed $targetValue): bool;


/**
* @param array<mixed> $values
* @return array<mixed>
*/
protected function multiEvaluateInPhp(array $values, mixed $targetValue): array
{
return array_map(
function ($value) use ($targetValue): bool {
return $this->evaluateInPhp($value, $targetValue);
},
$values,
);
}


/**
* @param literal-string|array<literal-string|null>|null $modifier
*/
Expand Down
3 changes: 1 addition & 2 deletions src/Collection/Functions/BaseNumericAggregateFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ public function processDbalExpression(
DbalQueryBuilderHelper $helper,
QueryBuilder $builder,
array $args,
ExpressionContext $context,
?Aggregator $aggregator = null,
): DbalExpressionResult
{
Expand All @@ -64,7 +63,7 @@ public function processDbalExpression(
throw new InvalidStateException("Cannot apply two aggregations simultaneously.");
}

return $helper->processExpression($builder, $args[0], $context, $this->aggregator)
return $helper->processExpression($builder, $args[0], $this->aggregator)
->applyAggregator(ExpressionContext::ValueExpression);
}
}
1 change: 0 additions & 1 deletion src/Collection/Functions/CollectionFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ public function processDbalExpression(
DbalQueryBuilderHelper $helper,
QueryBuilder $builder,
array $args,
ExpressionContext $context,
?Aggregator $aggregator = null,
): DbalExpressionResult;
}
9 changes: 9 additions & 0 deletions src/Collection/Functions/CompareEqualsFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ protected function evaluateInPhp(mixed $sourceValue, mixed $targetValue): bool
}


protected function multiEvaluateInPhp(array $values, mixed $targetValue): array
{
if ($targetValue === null && $values === []) {
return [true];
}
return parent::multiEvaluateInPhp($values, $targetValue);
}


protected function evaluateInDb(
DbalExpressionResult $expression,
mixed $value,
Expand Down
3 changes: 1 addition & 2 deletions src/Collection/Functions/CompareLikeFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,12 @@ public function processDbalExpression(
DbalQueryBuilderHelper $helper,
QueryBuilder $builder,
array $args,
ExpressionContext $context,
?Aggregator $aggregator = null,
): DbalExpressionResult
{
assert(count($args) === 2);

$expression = $helper->processExpression($builder, $args[0], $context, $aggregator);
$expression = $helper->processExpression($builder, $args[0], $aggregator);

$likeExpression = $args[1];
assert($likeExpression instanceof LikeExpression);
Expand Down
2 changes: 0 additions & 2 deletions src/Collection/Functions/ConjunctionOperatorFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ public function processDbalExpression(
DbalQueryBuilderHelper $helper,
QueryBuilder $builder,
array $args,
ExpressionContext $context,
?Aggregator $aggregator = null,
): DbalExpressionResult
{
Expand All @@ -112,7 +111,6 @@ public function processDbalExpression(
helper: $helper,
builder: $builder,
args: $args,
context: $context,
aggregator: $aggregator,
);
}
Expand Down
2 changes: 0 additions & 2 deletions src/Collection/Functions/DisjunctionOperatorFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ public function processDbalExpression(
DbalQueryBuilderHelper $helper,
QueryBuilder $builder,
array $args,
ExpressionContext $context,
?Aggregator $aggregator = null,
): DbalExpressionResult
{
Expand All @@ -105,7 +104,6 @@ public function processDbalExpression(
helper: $helper,
builder: $builder,
args: $args,
context: ExpressionContext::FilterOr,
aggregator: $aggregator,
);
}
Expand Down
1 change: 0 additions & 1 deletion src/Collection/Functions/FetchPropertyFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ public function processDbalExpression(
DbalQueryBuilderHelper $helper,
QueryBuilder $builder,
array $args,
ExpressionContext $context,
?Aggregator $aggregator = null,
): DbalExpressionResult
{
Expand Down
99 changes: 58 additions & 41 deletions src/Collection/Functions/JunctionFunctionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,59 +61,76 @@ protected function processQueryBuilderExpressionWithModifier(
DbalQueryBuilderHelper $helper,
QueryBuilder $builder,
array $args,
ExpressionContext $context,
?Aggregator $aggregator,
): DbalExpressionResult
{
$processedArgs = [];
$processedHavingArgs = [];
$joins = [];
$groupBy = [];
$columns = [];

[$normalized, $newAggregator] = $this->normalizeFunctions($args);
if ($newAggregator !== null) {
if ($aggregator !== null) throw new InvalidStateException("Cannot apply two aggregations simultaneously.");
$aggregator = $newAggregator;
}

$requiresHaving = false;
$expressions = [];
foreach ($normalized as $collectionFunctionArgs) {
$expression = $helper->processExpression($builder, $collectionFunctionArgs, $context, $aggregator);
$expression = $expression->applyAggregator($builder, $context);
$whereArgs = $expression->getArgsForExpansion();
if ($whereArgs !== []) {
$processedArgs[] = $whereArgs;
}
$havingArgs = $expression->getHavingArgsForExpansion();
if ($havingArgs !== []) {
$processedHavingArgs[] = $havingArgs;
$expressions[] = $expression = $helper->processExpression($builder, $collectionFunctionArgs, $aggregator);
if ($expression->havingExpression !== null) {
$requiresHaving = true;
}
$joins = array_merge($joins, $expression->joins);
$groupBy = array_merge($groupBy, $expression->groupBy);
$columns = array_merge($columns, $expression->columns);
}

if ($context === ExpressionContext::FilterOr && $processedHavingArgs !== []) {
// move all where expressions to HAVING clause
return new DbalExpressionResult(
expression: null,
args: [],
joins: $helper->mergeJoins($dbalModifier, $joins),
groupBy: array_merge($groupBy, $columns),
havingExpression: $dbalModifier,
havingArgs: [array_merge($processedArgs, $processedHavingArgs)],
columns: [],
);
} else {
return new DbalExpressionResult(
expression: $processedArgs === [] ? null : $dbalModifier,
args: $processedArgs === [] ? [] : [$processedArgs],
joins: $helper->mergeJoins($dbalModifier, $joins),
groupBy: $groupBy,
havingExpression: $processedHavingArgs === [] ? null : $dbalModifier,
havingArgs: $processedHavingArgs === [] ? [] : [$processedHavingArgs],
columns: $columns,
);
}
return new DbalExpressionResult(
expression: $dbalModifier,
args: $expressions,
havingExpression: $requiresHaving ? $dbalModifier : null,
collectCallback: function (ExpressionContext $context) use ($helper, $dbalModifier) {
/** @var DbalExpressionResult $this */

$processedArgs = [];
$processedHavingArgs = [];
$joins = [];
$groupBy = [];
$columns = [];

if ($dbalModifier === '%or') {
if ($context === ExpressionContext::FilterAnd) {
$finalContext = ExpressionContext::FilterOr;
} elseif ($context === ExpressionContext::FilterAndWithHavingClause) {
$finalContext = ExpressionContext::FilterOrWithHavingClause;
} else {
$finalContext = $context;
}
} else {
$finalContext = $context;
}

foreach ($this->args as $arg) {
assert($arg instanceof DbalExpressionResult);
$expression = $arg->collect($finalContext)->applyAggregator($finalContext);

$whereArgs = $expression->getArgsForExpansion();
if ($whereArgs !== []) {
$processedArgs[] = $whereArgs;
}
$havingArgs = $expression->getHavingArgsForExpansion();
if ($havingArgs !== []) {
$processedHavingArgs[] = $havingArgs;
}
$joins = array_merge($joins, $expression->joins);
$groupBy = array_merge($groupBy, $expression->groupBy);
$columns = array_merge($columns, $expression->columns);
}

return new DbalExpressionResult(
expression: $processedArgs === [] ? null : $dbalModifier,
args: $processedArgs === [] ? [] : [$processedArgs],
joins: $helper->mergeJoins($dbalModifier, $joins),
groupBy: $groupBy,
havingExpression: $processedHavingArgs === [] ? null : $dbalModifier,
havingArgs: $processedHavingArgs === [] ? [] : [$processedHavingArgs],
columns: $columns,
);
},
);
}
}
Loading

0 comments on commit ad6dfe0

Please sign in to comment.