From 71e8a93a71d3b2283fbec1316ade9a9509a930db Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 25 Sep 2026 13:56:55 +0930 Subject: [PATCH] Dedup discover pool with aggregate to satisfy strict group by --- app/Services/DiscoverService.php | 13 ++- .../Feature/DiscoverServiceDailyPoolTest.php | 85 +++++++++++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 tests/Feature/DiscoverServiceDailyPoolTest.php diff --git a/app/Services/DiscoverService.php b/app/Services/DiscoverService.php index c86723335..72d9346be 100644 --- a/app/Services/DiscoverService.php +++ b/app/Services/DiscoverService.php @@ -10,17 +10,22 @@ class DiscoverService public static function getDailyIdPool() { $min_id = SnowflakeService::byDate(now()->subMonths(3)); - $sqld = db_is_mysql_maria(); + // One post per author (the newest, MAX(id)) so the pool is not + // dominated by prolific accounts. Selecting the aggregated MAX(id) with + // GROUP BY profile_id is valid under ONLY_FULL_GROUP_BY and portable + // across every driver — the previous `select id ... group by profile_id` + // threw 1055 on strict MySQL/MariaDB, and was gated off entirely on + // other drivers (no dedup at all). inRandomOrder() still shuffles the + // pool. This mirrors the MAX(id)+GROUP BY dedup used elsewhere. return DB::table('statuses') + ->selectRaw('MAX(id) as id') ->whereNull('uri') ->whereType('photo') ->whereIsNsfw(false) ->whereVisibility('public') - ->when($sqld, function ($q, $sqld) { - return $q->groupBy('profile_id'); - }) ->where('id', '>', $min_id) + ->groupBy('profile_id') ->inRandomOrder() ->take(300) ->pluck('id'); diff --git a/tests/Feature/DiscoverServiceDailyPoolTest.php b/tests/Feature/DiscoverServiceDailyPoolTest.php new file mode 100644 index 000000000..d60e09e5c --- /dev/null +++ b/tests/Feature/DiscoverServiceDailyPoolTest.php @@ -0,0 +1,85 @@ +create([ + 'profile_id' => $profileId, + 'type' => 'photo', + 'scope' => 'public', + 'visibility' => 'public', + 'is_nsfw' => false, + 'uri' => null, + 'local' => true, + ]); +} + +it('returns exactly one id per author', function () { + $author = User::factory()->create(); + $author->refresh(); + + // Several public photos by the same author. + publicPhoto($author->profile_id); + publicPhoto($author->profile_id); + publicPhoto($author->profile_id); + + $pool = DiscoverService::getDailyIdPool(); + + expect($pool)->toHaveCount(1); +}); + +it('keeps the newest post per author', function () { + $author = User::factory()->create(); + $author->refresh(); + + $ids = collect([ + publicPhoto($author->profile_id)->id, + publicPhoto($author->profile_id)->id, + publicPhoto($author->profile_id)->id, + ]); + $newestId = $ids->max(); + + $pool = DiscoverService::getDailyIdPool(); + + expect($pool->map(fn ($id) => (int) $id)->all())->toBe([(int) $newestId]); +}); + +it('includes one id from each of several authors', function () { + $a = User::factory()->create(); + $a->refresh(); + $b = User::factory()->create(); + $b->refresh(); + + publicPhoto($a->profile_id); + publicPhoto($a->profile_id); + publicPhoto($b->profile_id); + + $pool = DiscoverService::getDailyIdPool()->map(fn ($id) => (int) $id); + + expect($pool)->toHaveCount(2); +});