Perf: customer press-releases-Liste ueber UNION statt OR-Paginate

Die Uebersicht /admin/me/press-releases war auch fuer leere Konten ~2s: der OR-Filter in scopeAccessibleBy zwang die Paginator-count(*) zum Full-Scan (EXPLAIN type=ALL, ~174k Zeilen) und den Zeilen-Fetch bei 0 Treffern zum vollen created_at-Index-Scan (~1s). Neue PressRelease::accessiblePageFor() nutzt dieselbe index-freundliche UNION wie accessibleStatusCountsFor(): schmale UNION liefert die Seiten-IDs ueber user_id-/company_id-Index, danach nur 25 Modelle hydratisiert; Total via schnellem UNION-count. Messung: leer ~2000ms -> 167ms, groesstes Konto (20.830 PMs) ~740ms -> 468ms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-02 10:55:45 +00:00
parent a25355ede3
commit 17b586f136
3 changed files with 164 additions and 11 deletions

View file

@ -0,0 +1,79 @@
<?php
use App\Enums\PressReleaseStatus;
use App\Models\Company;
use App\Models\PressRelease;
use App\Models\User;
test('includes authored and company-linked releases and dedupes overlap', function () {
$user = User::factory()->create();
$company = Company::factory()->create();
$user->companies()->attach($company->id, ['role' => 'member']);
$authored = PressRelease::factory()->create(['user_id' => $user->id, 'company_id' => null]);
$viaCompany = PressRelease::factory()->create(['company_id' => $company->id]);
// erfüllt beide Zweige – darf nur einmal gezählt werden
$both = PressRelease::factory()->create(['user_id' => $user->id, 'company_id' => $company->id]);
$page = PressRelease::accessiblePageFor($user);
expect($page->total())->toBe(3)
->and($page->pluck('id')->sort()->values()->all())
->toBe(collect([$authored->id, $viaCompany->id, $both->id])->sort()->values()->all());
});
test('excludes releases the user cannot access', function () {
$user = User::factory()->create();
$stranger = PressRelease::factory()->create();
$page = PressRelease::accessiblePageFor($user);
expect($page->total())->toBe(0)
->and($page->contains($stranger))->toBeFalse();
});
test('applies the constrain closure to both union branches', function () {
$user = User::factory()->create();
PressRelease::factory()->create(['user_id' => $user->id, 'status' => PressReleaseStatus::Published->value]);
PressRelease::factory()->create(['user_id' => $user->id, 'status' => PressReleaseStatus::Draft->value]);
$page = PressRelease::accessiblePageFor(
$user,
fn ($query) => $query->where('status', PressReleaseStatus::Published->value),
);
expect($page->total())->toBe(1)
->and($page->first()->status)->toBe(PressReleaseStatus::Published);
});
test('respects sort column and direction', function () {
$user = User::factory()->create();
$a = PressRelease::factory()->create(['user_id' => $user->id, 'title' => 'Alpha']);
$z = PressRelease::factory()->create(['user_id' => $user->id, 'title' => 'Zulu']);
$asc = PressRelease::accessiblePageFor($user, null, 'title', 'asc');
$desc = PressRelease::accessiblePageFor($user, null, 'title', 'desc');
expect($asc->first()->id)->toBe($a->id)
->and($desc->first()->id)->toBe($z->id);
});
test('paginates with the given page size', function () {
$user = User::factory()->create();
PressRelease::factory()->count(7)->create(['user_id' => $user->id]);
$page = PressRelease::accessiblePageFor($user, null, 'created_at', 'desc', 5);
expect($page->total())->toBe(7)
->and($page->perPage())->toBe(5)
->and($page->count())->toBe(5);
});
test('falls back to created_at for an invalid sort column', function () {
$user = User::factory()->create();
PressRelease::factory()->create(['user_id' => $user->id]);
$page = PressRelease::accessiblePageFor($user, null, 'title; drop table', 'sideways');
expect($page->total())->toBe(1);
});