Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
private const TABLE = 'process_request_tokens';

private const COUNT_INDEX = 'process_request_tokens_prt_completed_task_count';

private const PAGE_INDEX = 'process_request_tokens_prt_task_created';

/**
* Add indexes used by completed-task pagination.
*
* Run this migration during a low-traffic window. Although MySQL builds the
* indexes online, it can briefly wait for metadata locks on a large table.
*/
public function up(): void
{
$clauses = [];

if (!Schema::hasIndex(self::TABLE, self::COUNT_INDEX)) {
$clauses[] = sprintf(
'ADD INDEX `%s` (`status`, `is_self_service`, `element_type`, `element_name`)',
self::COUNT_INDEX
);
}

if (!Schema::hasIndex(self::TABLE, self::PAGE_INDEX)) {
$clauses[] = sprintf(
'ADD INDEX `%s` (`is_self_service`, `created_at` DESC, `status`, `element_type`, `element_name`)',
self::PAGE_INDEX
);
}

$this->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));
}
};
89 changes: 89 additions & 0 deletions tests/Feature/Migrations/AddTaskPaginationIndexesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace Tests\Feature\Migrations;

use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;

class AddTaskPaginationIndexesTest extends TestCase
{
private const MIGRATION_PATH = 'database/migrations/2026_08_17_000000_add_task_pagination_indexes.php';

private const COUNT_INDEX = 'process_request_tokens_prt_completed_task_count';

private const PAGE_INDEX = 'process_request_tokens_prt_task_created';

protected $connectionsToTransact = [];

public function testUpCreatesBothIndexesAndRefreshesStatistics(): void
{
$migration = $this->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);
}
}
Loading