Skip to content

Commit

Permalink
Merge pull request #655 from nextras/group-by-reworks
Browse files Browse the repository at this point in the history
GROUP BY reworks
  • Loading branch information
hrach authored Mar 20, 2024
2 parents cc4142f + beaf39b commit 76f7355
Show file tree
Hide file tree
Showing 42 changed files with 295 additions and 177 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"ext-ctype": "*",
"nette/caching": "~3.2 || ~3.1.3",
"nette/utils": "~3.0 || ~4.0",
"nextras/dbal": "~5.0.0-rc2"
"nextras/dbal": "dev-main#bf717b4b02b45f44b7c25b3b1c6a14a19cc59847"
},
"require-dev": {
"nette/bootstrap": "~3.1",
Expand Down
15 changes: 4 additions & 11 deletions src/Collection/Aggregations/AnyAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use Nextras\Orm\Exception\InvalidArgumentException;
use function array_merge;
use function array_pop;
use function count;


/**
Expand Down Expand Up @@ -65,14 +64,9 @@ public function aggregateExpression(
if ($join === null) {
throw new InvalidArgumentException('Any aggregation applied over expression without a relationship.');
}
if (count($join->groupByColumns) === 0) {
if ($join->toPrimaryKey === null) {
throw new InvalidArgumentException(
'Aggregation applied over a table join without specifying a group-by column (primary key).',
);
}
if (count($join->groupByColumns) > 1) {
throw new InvalidArgumentException(
'Aggregation applied over a table join with multiple group-by columns; currently, this is not supported.',
'Aggregation applied over a table-join without specifying a toPrimaryKey.',
);
}

Expand All @@ -82,12 +76,11 @@ public function aggregateExpression(
toAlias: $join->toAlias,
onExpression: "($join->onExpression) AND $expression->expression",
onArgs: array_merge($join->onArgs, $expression->args),
groupByColumns: $join->groupByColumns,
);

return new DbalExpressionResult(
expression: 'COUNT(%table.%column) > 0',
args: [$join->toAlias, $join->groupByColumns[0]],
expression: 'COUNT(%column) > 0',
args: [$join->toPrimaryKey],
joins: $joins,
groupBy: $expression->groupBy,
isHavingClause: true,
Expand Down
28 changes: 9 additions & 19 deletions src/Collection/Aggregations/CountAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,9 @@ public function aggregateExpression(
if ($join === null) {
throw new InvalidArgumentException('Count aggregation applied over expression without a relationship.');
}
if (count($join->groupByColumns) === 0) {
if ($join->toPrimaryKey === null) {
throw new InvalidArgumentException(
'Aggregation applied over a table join without specifying a group-by column (primary key).',
);
}
if (count($join->groupByColumns) > 1) {
throw new InvalidArgumentException(
'Aggregation applied over a table join with multiple group-by columns; currently, this is not supported.',
'Aggregation applied over a table-join without specifying a toPrimaryKey.',
);
}

Expand All @@ -77,18 +72,15 @@ public function aggregateExpression(
toAlias: $join->toAlias,
onExpression: "($join->onExpression) AND $expression->expression",
onArgs: array_merge($join->onArgs, $expression->args),
groupByColumns: $join->groupByColumns,
);

if ($this->atLeast !== null && $this->atMost !== null) {
return new DbalExpressionResult(
expression: 'COUNT(%table.%column) >= %i AND COUNT(%table.%column) <= %i',
expression: 'COUNT(%column) >= %i AND COUNT(%column) <= %i',
args: [
$join->toAlias,
$join->groupByColumns[0],
$join->toPrimaryKey,
$this->atLeast,
$join->toAlias,
$join->groupByColumns[0],
$join->toPrimaryKey,
$this->atMost,
],
joins: $joins,
Expand All @@ -97,10 +89,9 @@ public function aggregateExpression(
);
} elseif ($this->atMost !== null) {
return new DbalExpressionResult(
expression: 'COUNT(%table.%column) <= %i',
expression: 'COUNT(%column) <= %i',
args: [
$join->toAlias,
$join->groupByColumns[0],
$join->toPrimaryKey,
$this->atMost,
],
joins: $joins,
Expand All @@ -109,10 +100,9 @@ public function aggregateExpression(
);
} else {
return new DbalExpressionResult(
expression: 'COUNT(%table.%column) >= %i',
expression: 'COUNT(%column) >= %i',
args: [
$join->toAlias,
$join->groupByColumns[0],
$join->toPrimaryKey,
$this->atLeast,
],
joins: $joins,
Expand Down
15 changes: 4 additions & 11 deletions src/Collection/Aggregations/NoneAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use Nextras\Orm\Exception\InvalidArgumentException;
use function array_merge;
use function array_pop;
use function count;


/**
Expand Down Expand Up @@ -59,14 +58,9 @@ public function aggregateExpression(
if ($join === null) {
throw new InvalidArgumentException('None aggregation applied over expression without a relationship.');
}
if (count($join->groupByColumns) === 0) {
if ($join->toPrimaryKey === null) {
throw new InvalidArgumentException(
'Aggregation applied over a table join without specifying a group-by column (primary key).',
);
}
if (count($join->groupByColumns) > 1) {
throw new InvalidArgumentException(
'Aggregation applied over a table join with multiple group-by columns; currently, this is not supported.',
'Aggregation applied over a table-join without specifying a toPrimaryKey.',
);
}

Expand All @@ -76,12 +70,11 @@ public function aggregateExpression(
toAlias: $join->toAlias,
onExpression: "($join->onExpression) AND $expression->expression",
onArgs: array_merge($join->onArgs, $expression->args),
groupByColumns: $join->groupByColumns,
);

return new DbalExpressionResult(
expression: 'COUNT(%table.%column) = 0',
args: [$join->toAlias, $join->groupByColumns[0]],
expression: 'COUNT(%column) = 0',
args: [$join->toPrimaryKey],
joins: $joins,
groupBy: $expression->groupBy,
isHavingClause: true,
Expand Down
2 changes: 1 addition & 1 deletion src/Collection/ArrayCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ public function fetchChecked(): IEntity
}


public function fetchAll()
public function fetchAll(): array
{
return iterator_to_array($this->getIterator(), preserve_keys: false);
}
Expand Down
26 changes: 15 additions & 11 deletions src/Collection/DbalCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use Nextras\Orm\Mapper\IRelationshipMapper;
use function count;
use function is_array;
use function str_repeat;


/**
Expand Down Expand Up @@ -179,7 +178,7 @@ public function fetchChecked(): IEntity
}


public function fetchAll()
public function fetchAll(): array
{
return iterator_to_array($this->getIterator(), preserve_keys: false);
}
Expand Down Expand Up @@ -314,8 +313,8 @@ public function getQueryBuilder(): QueryBuilder
aggregator: null,
);
$joins = $expression->joins;
$groupBy = $expression->groupBy;
if ($expression->isHavingClause) {
$groupBy = $expression->groupBy;
$this->queryBuilder->andHaving($expression->expression, ...$expression->args);
} else {
$this->queryBuilder->andWhere($expression->expression, ...$expression->args);
Expand All @@ -325,24 +324,29 @@ public function getQueryBuilder(): QueryBuilder

foreach ($this->ordering as [$expression, $direction]) {
$joins = array_merge($joins, $expression->joins);
if ($expression->isHavingClause) {
$groupBy = array_merge($groupBy, $expression->groupBy);
}
$groupBy = array_merge($groupBy, $expression->groupBy);
$orderingExpression = $helper->processOrderDirection($expression, $direction);
$this->queryBuilder->addOrderBy('%ex', $orderingExpression);
}
$this->ordering = [];

$mergedJoins = $helper->mergeJoins('%and', $joins);
foreach ($mergedJoins as $join) {
$join->applyJoin($this->queryBuilder);
}

if (count($groupBy) > 0) {
$this->queryBuilder->groupBy(
'%ex' . str_repeat(', %ex', count($groupBy) - 1),
...$groupBy,
);
foreach ($this->ordering as [$expression]) {
$groupBy = array_merge($groupBy, $expression->columns);
}
}
$this->ordering = [];

if (count($groupBy) > 0) {
$unique = [];
foreach ($groupBy as $groupByFqn) {
$unique[$groupByFqn->getUnescaped()] = $groupByFqn;
}
$this->queryBuilder->groupBy('%column[]', array_values($unique));
}

return $this->queryBuilder;
Expand Down
2 changes: 1 addition & 1 deletion src/Collection/EmptyCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public function fetchChecked(): IEntity
}


public function fetchAll()
public function fetchAll(): array
{
return [];
}
Expand Down
37 changes: 15 additions & 22 deletions src/Collection/Functions/CompareEqualsFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@


use Nextras\Orm\Collection\Functions\Result\DbalExpressionResult;
use Nextras\Orm\Exception\InvalidArgumentException;
use function array_combine;
use function array_map;
use function count;
use function explode;
use function in_array;
Expand Down Expand Up @@ -34,29 +31,25 @@ protected function evaluateInDb(
if (is_array($value)) {
if (count($value) > 0) {
// Multi-column primary key handling
// extract column names for multiOr simplification
// array{%column, array<string>}
// Construct multiOr simplification as array{list<Fqn>, modifiers: list<string>, values: list<list<mixed>>}
$args = $expression->getArgumentsForExpansion();
if (count($args) === 2 && $args[0] === '%column' && is_array($args[1])) {
$modifiers = explode(',', $modifier);
$columns = [];
foreach ($args[1] as $i => $column) {
$columns[] = $column . $modifiers[$i];
}
$value = array_map(function ($value) use ($columns): array {
$combined = array_combine($columns, $value);
if ($combined === false) { // @phpstan-ignore-line
$pn = count($columns);
$vn = count($value);
throw new InvalidArgumentException("Number of values ($vn) does not match number of properties ($pn).");
$columns = $args[1];
$modifiers = array_map(
fn (string $modifier): ?string => strlen($modifier) === 0 ? null : $modifier,
explode(',', $modifier)
);
$data = [];
foreach ($value as $dataSet) {
$set = [];
foreach ($dataSet as $i => $dataSetValue) {
$set[] = [$columns[$i], $dataSetValue, $modifiers[$i] ?? null];
}
return $combined;
}, $value);
return $expression->withArgs('%multiOr', [$value]);
} else {
if ($modifier !== '%any') {
$modifier .= '[]';
$data[] = $set;
}
return $expression->withArgs('%multiOr', [$data]);
} else {
if ($modifier !== '%any') $modifier .= '[]';
return $expression->append("IN $modifier", $value);
}
} else {
Expand Down
34 changes: 15 additions & 19 deletions src/Collection/Functions/CompareNotEqualsFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,25 @@ protected function evaluateInDb(
if (is_array($value)) {
if (count($value) > 0) {
// Multi-column primary key handling
// extract column names for multiOr simplification
// array{%column, array<string>}
// Construct multiOr simplification as array{list<Fqn>, modifiers: list<string>, values: list<list<mixed>>}
$args = $expression->getArgumentsForExpansion();
if (count($args) === 2 && $args[0] === '%column' && is_array($args[1])) {
$modifiers = explode(',', $modifier);
$columns = [];
foreach ($args[1] as $i => $column) {
$columns[] = $column . $modifiers[$i];
}
$value = array_map(function ($value) use ($columns): array {
$combined = array_combine($columns, $value);
if ($combined === false) { // @phpstan-ignore-line
$pn = count($columns);
$vn = count($value);
throw new InvalidArgumentException("Number of values ($vn) does not match number of properties ($pn).");
$columns = $args[1];
$modifiers = array_map(
fn (string $modifier): ?string => strlen($modifier) === 0 ? null : $modifier,
explode(',', $modifier)
);
$data = [];
foreach ($value as $dataSet) {
$set = [];
foreach ($dataSet as $i => $dataSetValue) {
$set[] = [$columns[$i], $dataSetValue, $modifiers[$i] ?? null];
}
return $combined;
}, $value);
return $expression->withArgs('NOT (%multiOr)', [$value]);
} else {
if ($modifier !== '%any') {
$modifier .= '[]';
$data[] = $set;
}
return $expression->withArgs('NOT (%multiOr)', [$data]);
} else {
if ($modifier !== '%any') $modifier .= '[]';
return $expression->append("NOT IN $modifier", $value);
}
} else {
Expand Down
Loading

0 comments on commit 76f7355

Please sign in to comment.