diff --git a/database/migrations/2026_08_17_000000_add_task_pagination_indexes.php b/database/migrations/2026_08_17_000000_add_task_pagination_indexes.php new file mode 100644 index 0000000000..8a5042068e --- /dev/null +++ b/database/migrations/2026_08_17_000000_add_task_pagination_indexes.php @@ -0,0 +1,78 @@ +alterTable($clauses); + $this->refreshStatistics(); + } + + /** + * Remove completed-task pagination indexes. + */ + public function down(): void + { + $clauses = []; + + if (Schema::hasIndex(self::TABLE, self::COUNT_INDEX)) { + $clauses[] = sprintf('DROP INDEX `%s`', self::COUNT_INDEX); + } + + if (Schema::hasIndex(self::TABLE, self::PAGE_INDEX)) { + $clauses[] = sprintf('DROP INDEX `%s`', self::PAGE_INDEX); + } + + $this->alterTable($clauses); + $this->refreshStatistics(); + } + + private function alterTable(array $clauses): void + { + if ($clauses === []) { + return; + } + + DB::statement(sprintf( + 'ALTER TABLE `%s` %s, ALGORITHM=INPLACE, LOCK=NONE', + self::TABLE, + implode(', ', $clauses) + )); + } + + private function refreshStatistics(): void + { + DB::statement(sprintf('ANALYZE TABLE `%s`', self::TABLE)); + } +}; diff --git a/tests/Feature/Migrations/AddTaskPaginationIndexesTest.php b/tests/Feature/Migrations/AddTaskPaginationIndexesTest.php new file mode 100644 index 0000000000..b67d9ea811 --- /dev/null +++ b/tests/Feature/Migrations/AddTaskPaginationIndexesTest.php @@ -0,0 +1,89 @@ +migration(); + $migration->down(); + + try { + $queries = $this->captureQueries(fn () => $migration->up()); + + $this->assertTrue(Schema::hasIndex('process_request_tokens', self::COUNT_INDEX)); + $this->assertTrue(Schema::hasIndex('process_request_tokens', self::PAGE_INDEX)); + $this->assertStatisticsWereRefreshed($queries); + } finally { + $migration->up(); + } + } + + public function testUpIsIdempotentAndStillRefreshesStatistics(): void + { + $migration = $this->migration(); + $migration->up(); + + $queries = $this->captureQueries(fn () => $migration->up()); + + $this->assertTrue(Schema::hasIndex('process_request_tokens', self::COUNT_INDEX)); + $this->assertTrue(Schema::hasIndex('process_request_tokens', self::PAGE_INDEX)); + $this->assertStatisticsWereRefreshed($queries); + $this->assertFalse(collect($queries)->contains( + fn (string $query) => str_starts_with($query, 'ALTER TABLE `process_request_tokens`') + )); + } + + public function testDownRemovesBothIndexesAndRefreshesStatistics(): void + { + $migration = $this->migration(); + $migration->up(); + + try { + $queries = $this->captureQueries(fn () => $migration->down()); + + $this->assertFalse(Schema::hasIndex('process_request_tokens', self::COUNT_INDEX)); + $this->assertFalse(Schema::hasIndex('process_request_tokens', self::PAGE_INDEX)); + $this->assertStatisticsWereRefreshed($queries); + } finally { + $migration->up(); + } + } + + private function migration() + { + return include base_path(self::MIGRATION_PATH); + } + + private function captureQueries(callable $callback): array + { + $queries = []; + + DB::listen(function (QueryExecuted $query) use (&$queries) { + $queries[] = $query->sql; + }); + + $callback(); + + return $queries; + } + + private function assertStatisticsWereRefreshed(array $queries): void + { + $this->assertContains('ANALYZE TABLE `process_request_tokens`', $queries); + } +}