Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 8829052

Browse files
authored
PHPORM-286 Add Query::countByGroup() and other aggregateByGroup() functions (#3243)
* PHPORM-286 Add Query::countByGroup and other aggregateByGroup functions * Support counting distinct values with aggregate by group * Disable fail-fast due to Atlas issues
1 parent 35f4699 commit 8829052

File tree

3 files changed

+101
-4
lines changed

3 files changed

+101
-4
lines changed

‎.github/workflows/build-ci.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ jobs:
1111
name: "PHP ${{ matrix.php }} Laravel ${{ matrix.laravel }} MongoDB ${{ matrix.mongodb }} ${{ matrix.mode }}"
1212

1313
strategy:
14+
# Tests with Atlas fail randomly
15+
fail-fast: false
1416
matrix:
1517
os:
1618
- "ubuntu-latest"

‎src/Query/Builder.php‎

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use Override;
3232
use RuntimeException;
3333
use stdClass;
34+
use TypeError;
3435

3536
use function array_fill_keys;
3637
use function array_filter;
@@ -315,6 +316,7 @@ public function toMql(): array
315316
if ($this->groups || $this->aggregate) {
316317
$group = [];
317318
$unwinds = [];
319+
$set = [];
318320

319321
// Add grouping columns to the $group part of the aggregation pipeline.
320322
if ($this->groups) {
@@ -325,8 +327,10 @@ public function toMql(): array
325327
// this mimics SQL's behaviour a bit.
326328
$group[$column] = ['$last' => '$' . $column];
327329
}
330+
}
328331

329-
// Do the same for other columns that are selected.
332+
// Add the last value of each column when there is no aggregate function.
333+
if ($this->groups && ! $this->aggregate) {
330334
foreach ($columns as $column) {
331335
$key = str_replace('.', '_', $column);
332336

@@ -350,15 +354,22 @@ public function toMql(): array
350354

351355
$aggregations = blank($this->aggregate['columns']) ? [] : $this->aggregate['columns'];
352356

353-
if (in_array('*', $aggregations) && $function === 'count') {
357+
if ($column === '*'&& $function === 'count' && ! $this->groups) {
354358
$options = $this->inheritConnectionOptions($this->options);
355359

356360
return ['countDocuments' => [$wheres, $options]];
357361
}
358362

363+
// "aggregate" is the name of the field that will hold the aggregated value.
359364
if ($function === 'count') {
360-
// Translate count into sum.
361-
$group['aggregate'] = ['$sum' => 1];
365+
if ($column === '*' || $aggregations === []) {
366+
// Translate count into sum.
367+
$group['aggregate'] = ['$sum' => 1];
368+
} else {
369+
// Count the number of distinct values.
370+
$group['aggregate'] = ['$addToSet' => '$' . $column];
371+
$set['aggregate'] = ['$size' => '$aggregate'];
372+
}
362373
} else {
363374
$group['aggregate'] = ['$' . $function => '$' . $column];
364375
}
@@ -385,6 +396,10 @@ public function toMql(): array
385396
$pipeline[] = ['$group' => $group];
386397
}
387398

399+
if ($set) {
400+
$pipeline[] = ['$set' => $set];
401+
}
402+
388403
// Apply order and limit
389404
if ($this->orders) {
390405
$pipeline[] = ['$sort' => $this->aliasIdForQuery($this->orders)];
@@ -560,6 +575,8 @@ public function generateCacheKey()
560575
/** @return ($function is null ? AggregationBuilder : mixed) */
561576
public function aggregate($function = null, $columns = ['*'])
562577
{
578+
assert(is_array($columns), new TypeError(sprintf('Argument #2 ($columns) must be of type array, %s given', get_debug_type($columns))));
579+
563580
if ($function === null) {
564581
if (! trait_exists(FluentFactoryTrait::class)) {
565582
// This error will be unreachable when the mongodb/builder package will be merged into mongodb/mongodb
@@ -600,13 +617,36 @@ public function aggregate($function = null, $columns = ['*'])
600617
$this->columns = $previousColumns;
601618
$this->bindings['select'] = $previousSelectBindings;
602619

620+
// When the aggregation is per group, we return the results as is.
621+
if ($this->groups) {
622+
return $results->map(function (object $result) {
623+
unset($result->id);
624+
625+
return $result;
626+
});
627+
}
628+
603629
if (isset($results[0])) {
604630
$result = (array) $results[0];
605631

606632
return $result['aggregate'];
607633
}
608634
}
609635

636+
/**
637+
* {@inheritDoc}
638+
*
639+
* @see \Illuminate\Database\Query\Builder::aggregateByGroup()
640+
*/
641+
public function aggregateByGroup(string $function, array $columns = ['*'])
642+
{
643+
if (count($columns) > 1) {
644+
throw new InvalidArgumentException('Aggregating by group requires zero or one columns.');
645+
}
646+
647+
return $this->aggregate($function, $columns);
648+
}
649+
610650
/** @inheritdoc */
611651
public function exists()
612652
{

‎tests/QueryBuilderTest.php‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Carbon\Carbon;
88
use DateTime;
99
use DateTimeImmutable;
10+
use Illuminate\Support\Collection as LaravelCollection;
1011
use Illuminate\Support\Facades\Date;
1112
use Illuminate\Support\Facades\DB;
1213
use Illuminate\Support\LazyCollection;
@@ -32,6 +33,7 @@
3233
use function count;
3334
use function key;
3435
use function md5;
36+
use function method_exists;
3537
use function sort;
3638
use function strlen;
3739

@@ -617,6 +619,59 @@ public function testSubdocumentArrayAggregate()
617619
$this->assertEquals(12, DB::table('items')->avg('amount.*.hidden'));
618620
}
619621

622+
public function testAggregateGroupBy()
623+
{
624+
DB::table('users')->insert([
625+
['name' => 'John Doe', 'role' => 'admin', 'score' => 1, 'active' => true],
626+
['name' => 'Jane Doe', 'role' => 'admin', 'score' => 2, 'active' => true],
627+
['name' => 'Robert Roe', 'role' => 'user', 'score' => 4],
628+
]);
629+
630+
$results = DB::table('users')->groupBy('role')->orderBy('role')->aggregateByGroup('count');
631+
$this->assertInstanceOf(LaravelCollection::class, $results);
632+
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 2], (object) ['role' => 'user', 'aggregate' => 1]], $results->toArray());
633+
634+
$results = DB::table('users')->groupBy('role')->orderBy('role')->aggregateByGroup('count', ['active']);
635+
$this->assertInstanceOf(LaravelCollection::class, $results);
636+
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 1], (object) ['role' => 'user', 'aggregate' => 0]], $results->toArray());
637+
638+
$results = DB::table('users')->groupBy('role')->orderBy('role')->aggregateByGroup('max', ['score']);
639+
$this->assertInstanceOf(LaravelCollection::class, $results);
640+
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 2], (object) ['role' => 'user', 'aggregate' => 4]], $results->toArray());
641+
642+
if (! method_exists(Builder::class, 'countByGroup')) {
643+
$this->markTestSkipped('*byGroup functions require Laravel v11.38+');
644+
}
645+
646+
$results = DB::table('users')->groupBy('role')->orderBy('role')->countByGroup();
647+
$this->assertInstanceOf(LaravelCollection::class, $results);
648+
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 2], (object) ['role' => 'user', 'aggregate' => 1]], $results->toArray());
649+
650+
$results = DB::table('users')->groupBy('role')->orderBy('role')->maxByGroup('score');
651+
$this->assertInstanceOf(LaravelCollection::class, $results);
652+
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 2], (object) ['role' => 'user', 'aggregate' => 4]], $results->toArray());
653+
654+
$results = DB::table('users')->groupBy('role')->orderBy('role')->minByGroup('score');
655+
$this->assertInstanceOf(LaravelCollection::class, $results);
656+
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 1], (object) ['role' => 'user', 'aggregate' => 4]], $results->toArray());
657+
658+
$results = DB::table('users')->groupBy('role')->orderBy('role')->sumByGroup('score');
659+
$this->assertInstanceOf(LaravelCollection::class, $results);
660+
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 3], (object) ['role' => 'user', 'aggregate' => 4]], $results->toArray());
661+
662+
$results = DB::table('users')->groupBy('role')->orderBy('role')->avgByGroup('score');
663+
$this->assertInstanceOf(LaravelCollection::class, $results);
664+
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 1.5], (object) ['role' => 'user', 'aggregate' => 4]], $results->toArray());
665+
}
666+
667+
public function testAggregateByGroupException(): void
668+
{
669+
$this->expectException(InvalidArgumentException::class);
670+
$this->expectExceptionMessage('Aggregating by group requires zero or one columns.');
671+
672+
DB::table('users')->aggregateByGroup('max', ['foo', 'bar']);
673+
}
674+
620675
public function testUpdateWithUpsert()
621676
{
622677
DB::table('items')->where('name', 'knife')

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /