This commit is contained in:
DariusIII
2026-08-14 13:00:25 +02:00
parent 931fc55d8a
commit a18b0ef88a
4 changed files with 142 additions and 58 deletions
@@ -50,6 +50,19 @@ return new class extends Migration
'ix_releases_nzb_creation_queue',
];
/**
* Narrowed to `CHAR(32) ascii` as part of this rebuild rather than in a
* follow-up migration: the `guid` charset change already forces
* `ALGORITHM=COPY`, so folding these in costs nothing, whereas a separate
* migration would force a second full table rebuild.
*
* @var list<string>
*/
private const array CLAIM_TOKEN_COLUMNS = [
'nzb_creation_claim_token',
'additional_pp_claim_token',
];
public function up(): void
{
if (! Schema::hasTable('releases')) {
@@ -275,6 +288,11 @@ return new class extends Migration
$specifications[] = 'MODIFY `guid` CHAR(36) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL';
$specifications[] = "MODIFY `leftguid` CHAR(1) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL COMMENT 'The first letter of the release guid'";
foreach (self::CLAIM_TOKEN_COLUMNS as $column) {
if (Schema::hasColumn('releases', $column)) {
$specifications[] = 'MODIFY `'.$column.'` CHAR(32) CHARACTER SET ascii COLLATE ascii_general_ci NULL';
}
}
foreach (self::REMOVED_RELEASE_COLUMNS as $column) {
if (Schema::hasColumn('releases', $column)) {
$specifications[] = 'DROP COLUMN `'.$column.'`';
@@ -286,7 +304,7 @@ return new class extends Migration
'ADD UNIQUE INDEX `ux_releases_guid` (`guid`)',
'ADD INDEX `ix_releases_predb_id` (`predb_id`)',
'ADD INDEX `ix_releases_size` (`size`)',
'ADD INDEX `ix_releases_add_pp_claim_queue` (`passwordstatus`, `haspreview`, `nzbstatus`, `leftguid`, `postdate` DESC, `id`, `additional_pp_claimed_at`)',
'ADD INDEX `ix_releases_add_pp_claim_queue` (`passwordstatus`, `haspreview`, `nzbstatus`, `leftguid`, `postdate` DESC, `id`, `additional_pp_claimed_at`, `size`)',
'ADD INDEX `ix_releases_nzb_creation_group_queue` (`nzbstatus`, `groups_id`, `postdate` DESC, `id`, `nzb_creation_claimed_at`)',
'ADD INDEX `ix_releases_nzb_creation_global_queue` (`nzbstatus`, `postdate` DESC, `id`, `nzb_creation_claimed_at`)',
];
@@ -311,7 +329,7 @@ return new class extends Migration
$table->index('predb_id', 'ix_releases_predb_id');
$table->index('size', 'ix_releases_size');
$table->index(
['passwordstatus', 'haspreview', 'nzbstatus', 'leftguid', 'postdate', 'id', 'additional_pp_claimed_at'],
['passwordstatus', 'haspreview', 'nzbstatus', 'leftguid', 'postdate', 'id', 'additional_pp_claimed_at', 'size'],
'ix_releases_add_pp_claim_queue',
);
$table->index(
@@ -345,6 +363,8 @@ return new class extends Migration
...$specifications,
'MODIFY `guid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL',
"MODIFY `leftguid` CHAR(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The first letter of the release guid'",
'MODIFY `nzb_creation_claim_token` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL',
'MODIFY `additional_pp_claim_token` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL',
'ADD COLUMN `updatetime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `adddate`',
'ADD COLUMN `gid` VARCHAR(32) NULL AFTER `updatetime`',
'ADD COLUMN `source` SMALLINT UNSIGNED NULL',
@@ -9,22 +9,30 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Follow-up to the releases normalization rebuild.
* Catch-up migration for installs that already ran
* `2026_08_13_001652_normalize_and_optimize_releases_table` before it was
* extended to cover these two changes:
*
* 1. Appends `size` to `ix_releases_add_pp_claim_queue`. Every additional
* 1. `size` appended to `ix_releases_add_pp_claim_queue`. Every additional
* post-processing candidate query filters `size > min AND size < max`
* ({@see AdditionalCandidateQuery::applyPredicates()}),
* but `size` was not in the index, so each candidate that later failed the
* size filter still cost a primary key lookup. Appending it after the
* existing columns keeps the equality prefix and the `postdate DESC`
* ordering intact while enabling index condition pushdown.
* 2. Narrows both claim tokens to `CHAR(32) ascii`. They only ever hold
* ({@see AdditionalCandidateQuery::applyPredicates()}), but `size` was not in
* the index, so each candidate that later failed the size filter still cost a
* primary key lookup. Appending it after the existing columns keeps the
* equality prefix and the `postdate DESC` ordering intact while enabling
* index condition pushdown.
* 2. Both claim tokens narrowed to `CHAR(32) ascii`. They only ever hold
* `bin2hex(random_bytes(16))`, so `VARCHAR(64)` utf8mb4 reserved four bytes
* per character for hex digits.
*
* `releases.imdbid` is deliberately left alone: it joins `movieinfo.imdbid`,
* and changing the charset on one side only would force an implicit conversion
* that disables index usage on that join.
* Both now happen inside the normalization rebuild's single `ALTER`, because the
* claim token type change is `ALGORITHM=COPY` only and would otherwise force a
* second full table rebuild. This migration therefore no-ops on any install that
* ran the merged version, and `down()` is intentionally empty: the normalization
* migration's own `down()` restores the legacy column types and index shape.
*
* `releases.imdbid` is deliberately left alone: it joins `movieinfo.imdbid`, and
* changing the charset on one side only would force an implicit conversion that
* disables index usage on that join.
*/
return new class extends Migration
{
@@ -42,17 +50,24 @@ return new class extends Migration
return;
}
$indexNeedsSize = ! $this->ppClaimIndexCoversSize();
$columnsToNarrow = $this->claimTokenColumnsToNarrow();
if (! $indexNeedsSize && $columnsToNarrow === []) {
return;
}
if ($this->isMariaDbOrMySql()) {
$specifications = [];
if ($this->indexExists(self::PP_CLAIM_INDEX)) {
$specifications[] = 'DROP INDEX `'.self::PP_CLAIM_INDEX.'`';
}
$specifications[] = 'ADD INDEX `'.self::PP_CLAIM_INDEX.'` (`passwordstatus`, `haspreview`, `nzbstatus`,'
.' `leftguid`, `postdate` DESC, `id`, `additional_pp_claimed_at`, `size`)';
foreach (self::CLAIM_TOKEN_COLUMNS as $column) {
if (Schema::hasColumn('releases', $column)) {
$specifications[] = 'MODIFY `'.$column.'` CHAR(32) CHARACTER SET ascii COLLATE ascii_general_ci NULL';
if ($indexNeedsSize) {
if ($this->indexExists(self::PP_CLAIM_INDEX)) {
$specifications[] = 'DROP INDEX `'.self::PP_CLAIM_INDEX.'`';
}
$specifications[] = 'ADD INDEX `'.self::PP_CLAIM_INDEX.'` (`passwordstatus`, `haspreview`, `nzbstatus`,'
.' `leftguid`, `postdate` DESC, `id`, `additional_pp_claimed_at`, `size`)';
}
foreach ($columnsToNarrow as $column) {
$specifications[] = 'MODIFY `'.$column.'` CHAR(32) CHARACTER SET ascii COLLATE ascii_general_ci NULL';
}
DB::statement('ALTER TABLE '.$this->table('releases').' '.implode(', ', $specifications));
@@ -60,6 +75,10 @@ return new class extends Migration
return;
}
if (! $indexNeedsSize) {
return;
}
Schema::table('releases', function (Blueprint $table): void {
if ($this->indexExists(self::PP_CLAIM_INDEX)) {
$table->dropIndex(self::PP_CLAIM_INDEX);
@@ -71,39 +90,44 @@ return new class extends Migration
});
}
public function down(): void
/**
* Intentionally empty. The normalization migration owns both changes now, and
* its `down()` restores the legacy column types and index definition. Undoing
* them here as well would trigger a redundant full table rebuild.
*/
public function down(): void {}
private function ppClaimIndexCoversSize(): bool
{
if (! Schema::hasTable('releases')) {
return;
foreach (Schema::getIndexes('releases') as $index) {
if (($index['name'] ?? null) === self::PP_CLAIM_INDEX) {
return in_array('size', $index['columns'] ?? [], true);
}
}
if ($this->isMariaDbOrMySql()) {
$specifications = [];
if ($this->indexExists(self::PP_CLAIM_INDEX)) {
$specifications[] = 'DROP INDEX `'.self::PP_CLAIM_INDEX.'`';
}
$specifications[] = 'ADD INDEX `'.self::PP_CLAIM_INDEX.'` (`passwordstatus`, `haspreview`, `nzbstatus`,'
.' `leftguid`, `postdate` DESC, `id`, `additional_pp_claimed_at`)';
foreach (self::CLAIM_TOKEN_COLUMNS as $column) {
if (Schema::hasColumn('releases', $column)) {
$specifications[] = 'MODIFY `'.$column.'` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL';
}
}
return false;
}
DB::statement('ALTER TABLE '.$this->table('releases').' '.implode(', ', $specifications));
return;
/** @return list<string> */
private function claimTokenColumnsToNarrow(): array
{
if (! $this->isMariaDbOrMySql()) {
return [];
}
Schema::table('releases', function (Blueprint $table): void {
if ($this->indexExists(self::PP_CLAIM_INDEX)) {
$table->dropIndex(self::PP_CLAIM_INDEX);
$types = [];
foreach (Schema::getColumns('releases') as $column) {
$types[$column['name']] = strtolower((string) ($column['type'] ?? ''));
}
$pending = [];
foreach (self::CLAIM_TOKEN_COLUMNS as $column) {
if (isset($types[$column]) && ! str_starts_with($types[$column], 'char(32)')) {
$pending[] = $column;
}
$table->index(
['passwordstatus', 'haspreview', 'nzbstatus', 'leftguid', 'postdate', 'id', 'additional_pp_claimed_at'],
self::PP_CLAIM_INDEX,
);
});
}
return $pending;
}
private function indexExists(string $name): bool
+3 -3
View File
@@ -1324,7 +1324,7 @@ CREATE TABLE `releases` (
`videostatus` tinyint(1) NOT NULL DEFAULT 0,
`nzbstatus` tinyint(1) NOT NULL DEFAULT 0,
`nzb_creation_claimed_at` timestamp NULL DEFAULT NULL,
`nzb_creation_claim_token` varchar(64) DEFAULT NULL,
`nzb_creation_claim_token` char(32) CHARACTER SET ascii COLLATE ascii_general_ci DEFAULT NULL,
`iscategorized` tinyint(1) NOT NULL DEFAULT 0,
`isrenamed` tinyint(1) NOT NULL DEFAULT 0,
`proc_pp` tinyint(1) NOT NULL DEFAULT 0,
@@ -1337,7 +1337,7 @@ CREATE TABLE `releases` (
`proc_crc32` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Has the release been crc32 processed',
`pp_timeout_count` tinyint(4) NOT NULL DEFAULT 0 COMMENT 'Number of times this release timed out during additional post-processing',
`additional_pp_claimed_at` timestamp NULL DEFAULT NULL,
`additional_pp_claim_token` varchar(64) DEFAULT NULL,
`additional_pp_claim_token` char(32) CHARACTER SET ascii COLLATE ascii_general_ci DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_releases_guid` (`guid`),
KEY `ix_releases_groupsid` (`groups_id`,`passwordstatus`),
@@ -1362,7 +1362,7 @@ CREATE TABLE `releases` (
KEY `ix_releases_adddate_id` (`adddate`,`id`),
KEY `ix_releases_predb_id` (`predb_id`),
KEY `ix_releases_size` (`size`),
KEY `ix_releases_add_pp_claim_queue` (`passwordstatus`,`haspreview`,`nzbstatus`,`leftguid`,`postdate` DESC,`id`,`additional_pp_claimed_at`),
KEY `ix_releases_add_pp_claim_queue` (`passwordstatus`,`haspreview`,`nzbstatus`,`leftguid`,`postdate` DESC,`id`,`additional_pp_claimed_at`,`size`),
KEY `ix_releases_nzb_creation_group_queue` (`nzbstatus`,`groups_id`,`postdate` DESC,`id`,`nzb_creation_claimed_at`),
KEY `ix_releases_nzb_creation_global_queue` (`nzbstatus`,`postdate` DESC,`id`,`nzb_creation_claimed_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -76,7 +76,7 @@ final class ReleaseSchemaOptimizationMigrationMariaDbTest extends TestCase
$this->assertFalse(Schema::hasColumn('releases', 'proc_sorter'));
$this->assertFalse(Schema::hasColumn('release_comments', 'gid'));
$this->assertSame(
['passwordstatus', 'haspreview', 'nzbstatus', 'leftguid', 'postdate', 'id', 'additional_pp_claimed_at'],
['passwordstatus', 'haspreview', 'nzbstatus', 'leftguid', 'postdate', 'id', 'additional_pp_claimed_at', 'size'],
$this->indexColumns('ix_releases_add_pp_claim_queue'),
);
$this->assertSame(
@@ -114,13 +114,11 @@ final class ReleaseSchemaOptimizationMigrationMariaDbTest extends TestCase
}
#[Test]
public function claim_token_and_pp_index_follow_up_narrows_columns_and_adds_size(): void
public function rebuild_narrows_claim_tokens_and_covers_size_in_one_pass(): void
{
$this->seedLegacyData();
$this->migration()->up();
$followUp = $this->followUpMigration();
$followUp->up();
$this->migration()->up();
$this->assertSame(
['passwordstatus', 'haspreview', 'nzbstatus', 'leftguid', 'postdate', 'id', 'additional_pp_claimed_at', 'size'],
@@ -135,13 +133,55 @@ final class ReleaseSchemaOptimizationMigrationMariaDbTest extends TestCase
DB::table('releases')->where('id', 1)->update(['additional_pp_claim_token' => $token]);
$this->assertSame($token, DB::table('releases')->where('id', 1)->value('additional_pp_claim_token'));
$followUp->down();
$this->migration()->down();
$this->assertSame('varchar(64)', strtolower($this->columnType('additional_pp_claim_token')));
$this->assertSame('varchar(64)', strtolower($this->columnType('nzb_creation_claim_token')));
}
#[Test]
public function follow_up_migration_is_a_no_op_once_the_rebuild_covers_it(): void
{
$this->seedLegacyData();
$this->migration()->up();
$before = $this->releasesCreateStatement();
$this->followUpMigration()->up();
$this->assertSame($before, $this->releasesCreateStatement());
}
#[Test]
public function follow_up_migration_upgrades_an_install_left_on_the_old_shape(): void
{
$this->seedLegacyData();
$this->migration()->up();
DB::statement(
'ALTER TABLE `'.$this->table('releases').'` '
.'DROP INDEX `ix_releases_add_pp_claim_queue`, '
.'ADD INDEX `ix_releases_add_pp_claim_queue` (`passwordstatus`, `haspreview`, `nzbstatus`, `leftguid`,'
.' `postdate` DESC, `id`, `additional_pp_claimed_at`), '
.'MODIFY `nzb_creation_claim_token` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, '
.'MODIFY `additional_pp_claim_token` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL'
);
$this->followUpMigration()->up();
$this->assertSame(
['passwordstatus', 'haspreview', 'nzbstatus', 'leftguid', 'postdate', 'id', 'additional_pp_claimed_at'],
['passwordstatus', 'haspreview', 'nzbstatus', 'leftguid', 'postdate', 'id', 'additional_pp_claimed_at', 'size'],
$this->indexColumns('ix_releases_add_pp_claim_queue'),
);
$this->assertSame('varchar(64)', strtolower($this->columnType('additional_pp_claim_token')));
foreach (['nzb_creation_claim_token', 'additional_pp_claim_token'] as $column) {
$this->assertSame('char(32)', strtolower($this->columnType($column)));
}
}
private function releasesCreateStatement(): string
{
$row = DB::selectOne('SHOW CREATE TABLE `'.$this->table('releases').'`');
return (string) ($row->{'Create Table'} ?? '');
}
private function createSchema(): void