Skip to content

Commit 694e76d

Browse files
committed
Support counting distinct values with aggregate by group
1 parent 3692165 commit 694e76d

File tree

2 files changed

+38
-6
lines changed

2 files changed

+38
-6
lines changed

src/Query/Builder.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ public function toMql(): array
315315
if ($this->groups || $this->aggregate) {
316316
$group = [];
317317
$unwinds = [];
318+
$set = [];
318319

319320
// Add grouping columns to the $group part of the aggregation pipeline.
320321
if ($this->groups) {
@@ -352,15 +353,22 @@ public function toMql(): array
352353

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

355-
if (in_array('*', $aggregations) && $function === 'count' && empty($group['_id'])) {
356+
if ($column === '*' && $function === 'count' && ! $this->groups) {
356357
$options = $this->inheritConnectionOptions($this->options);
357358

358359
return ['countDocuments' => [$wheres, $options]];
359360
}
360361

362+
// "aggregate" is the name of the field that will hold the aggregated value.
361363
if ($function === 'count') {
362-
// Translate count into sum.
363-
$group['aggregate'] = ['$sum' => 1];
364+
if ($column === '*' || $aggregations === []) {
365+
// Translate count into sum.
366+
$group['aggregate'] = ['$sum' => 1];
367+
} else {
368+
// Count the number of distinct values.
369+
$group['aggregate'] = ['$addToSet' => '$' . $column];
370+
$set['aggregate'] = ['$size' => '$aggregate'];
371+
}
364372
} else {
365373
$group['aggregate'] = ['$' . $function => '$' . $column];
366374
}
@@ -387,6 +395,10 @@ public function toMql(): array
387395
$pipeline[] = ['$group' => $group];
388396
}
389397

398+
if ($set) {
399+
$pipeline[] = ['$set' => $set];
400+
}
401+
390402
// Apply order and limit
391403
if ($this->orders) {
392404
$pipeline[] = ['$sort' => $this->aliasIdForQuery($this->orders)];
@@ -627,6 +639,10 @@ public function aggregate($function = null, $columns = ['*'])
627639
*/
628640
public function aggregateByGroup(string $function, array $columns = ['*'])
629641
{
642+
if (count($columns) > 1) {
643+
throw new InvalidArgumentException('Aggregating by group requires zero or one columns.');
644+
}
645+
630646
return $this->aggregate($function, $columns);
631647
}
632648

tests/QueryBuilderTest.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -622,17 +622,25 @@ public function testSubdocumentArrayAggregate()
622622
public function testAggregateGroupBy()
623623
{
624624
DB::table('users')->insert([
625-
['name' => 'John Doe', 'role' => 'admin', 'score' => 1],
626-
['name' => 'Jane Doe', 'role' => 'admin', 'score' => 2],
625+
['name' => 'John Doe', 'role' => 'admin', 'score' => 1, 'active' => true],
626+
['name' => 'Jane Doe', 'role' => 'admin', 'score' => 2, 'active' => true],
627627
['name' => 'Robert Roe', 'role' => 'user', 'score' => 4],
628628
]);
629629

630630
$results = DB::table('users')->groupBy('role')->orderBy('role')->aggregateByGroup('count');
631631
$this->assertInstanceOf(LaravelCollection::class, $results);
632632
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 2], (object) ['role' => 'user', 'aggregate' => 1]], $results->toArray());
633633

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+
634642
if (! method_exists(Builder::class, 'countByGroup')) {
635-
$this->markTestSkipped('countBy* function require Laravel v11.38+');
643+
$this->markTestSkipped('*byGroup functions require Laravel v11.38+');
636644
}
637645

638646
$results = DB::table('users')->groupBy('role')->orderBy('role')->countByGroup();
@@ -656,6 +664,14 @@ public function testAggregateGroupBy()
656664
$this->assertEquals([(object) ['role' => 'admin', 'aggregate' => 1.5], (object) ['role' => 'user', 'aggregate' => 4]], $results->toArray());
657665
}
658666

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+
659675
public function testUpdateWithUpsert()
660676
{
661677
DB::table('items')->where('name', 'knife')

0 commit comments

Comments
 (0)