Skip to content

Commit

Permalink
Merge pull request #687 from nextras/rewrites-two-phase
Browse files Browse the repository at this point in the history
Two-phase SQL rewrites
  • Loading branch information
hrach authored Oct 31, 2024
2 parents 17f561f + ad6dfe0 commit 43e6322
Show file tree
Hide file tree
Showing 29 changed files with 271 additions and 166 deletions.
1 change: 0 additions & 1 deletion src/Collection/Aggregations/Aggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public function aggregateValues(array $values);


public function aggregateExpression(
QueryBuilder $queryBuilder,
DbalExpressionResult $expression,
ExpressionContext $context,
): DbalExpressionResult;
Expand Down
3 changes: 1 addition & 2 deletions src/Collection/Aggregations/AnyAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,11 @@ public function aggregateValues(array $values): bool


public function aggregateExpression(
QueryBuilder $queryBuilder,
DbalExpressionResult $expression,
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
1 change: 0 additions & 1 deletion src/Collection/Aggregations/CountAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ public function aggregateValues(array $values): bool


public function aggregateExpression(
QueryBuilder $queryBuilder,
DbalExpressionResult $expression,
ExpressionContext $context,
): DbalExpressionResult
Expand Down
1 change: 0 additions & 1 deletion src/Collection/Aggregations/NoneAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ public function aggregateValues(array $values): bool


public function aggregateExpression(
QueryBuilder $queryBuilder,
DbalExpressionResult $expression,
ExpressionContext $context,
): DbalExpressionResult
Expand Down
1 change: 0 additions & 1 deletion src/Collection/Aggregations/NumericAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ public function aggregateValues(array $values): mixed


public function aggregateExpression(
QueryBuilder $queryBuilder,
DbalExpressionResult $expression,
ExpressionContext $context,
): DbalExpressionResult
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
5 changes: 2 additions & 3 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)
->applyAggregator($builder, ExpressionContext::ValueExpression);
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 43e6322

Please sign in to comment.