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', '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 public function up(): void
{ {
if (! Schema::hasTable('releases')) { 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 `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'"; $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) { foreach (self::REMOVED_RELEASE_COLUMNS as $column) {
if (Schema::hasColumn('releases', $column)) { if (Schema::hasColumn('releases', $column)) {
$specifications[] = 'DROP COLUMN `'.$column.'`'; $specifications[] = 'DROP COLUMN `'.$column.'`';
@@ -286,7 +304,7 @@ return new class extends Migration
'ADD UNIQUE INDEX `ux_releases_guid` (`guid`)', 'ADD UNIQUE INDEX `ux_releases_guid` (`guid`)',
'ADD INDEX `ix_releases_predb_id` (`predb_id`)', 'ADD INDEX `ix_releases_predb_id` (`predb_id`)',
'ADD INDEX `ix_releases_size` (`size`)', '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_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`)', '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('predb_id', 'ix_releases_predb_id');
$table->index('size', 'ix_releases_size'); $table->index('size', 'ix_releases_size');
$table->index( $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', 'ix_releases_add_pp_claim_queue',
); );
$table->index( $table->index(
@@ -345,6 +363,8 @@ return new class extends Migration
...$specifications, ...$specifications,
'MODIFY `guid` VARCHAR(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL', '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 `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 `updatetime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `adddate`',
'ADD COLUMN `gid` VARCHAR(32) NULL AFTER `updatetime`', 'ADD COLUMN `gid` VARCHAR(32) NULL AFTER `updatetime`',
'ADD COLUMN `source` SMALLINT UNSIGNED NULL', 'ADD COLUMN `source` SMALLINT UNSIGNED NULL',
@@ -9,22 +9,30 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema; 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` * post-processing candidate query filters `size > min AND size < max`
* ({@see AdditionalCandidateQuery::applyPredicates()}), * ({@see AdditionalCandidateQuery::applyPredicates()}), but `size` was not in
* but `size` was not in the index, so each candidate that later failed the * the index, so each candidate that later failed the size filter still cost a
* size filter still cost a primary key lookup. Appending it after the * primary key lookup. Appending it after the existing columns keeps the
* existing columns keeps the equality prefix and the `postdate DESC` * equality prefix and the `postdate DESC` ordering intact while enabling
* ordering intact while enabling index condition pushdown. * index condition pushdown.
* 2. Narrows both claim tokens to `CHAR(32) ascii`. They only ever hold * 2. Both claim tokens narrowed to `CHAR(32) ascii`. They only ever hold
* `bin2hex(random_bytes(16))`, so `VARCHAR(64)` utf8mb4 reserved four bytes * `bin2hex(random_bytes(16))`, so `VARCHAR(64)` utf8mb4 reserved four bytes
* per character for hex digits. * per character for hex digits.
* *
* `releases.imdbid` is deliberately left alone: it joins `movieinfo.imdbid`, * Both now happen inside the normalization rebuild's single `ALTER`, because the
* and changing the charset on one side only would force an implicit conversion * claim token type change is `ALGORITHM=COPY` only and would otherwise force a
* that disables index usage on that join. * 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 return new class extends Migration
{ {
@@ -42,17 +50,24 @@ return new class extends Migration
return; return;
} }
$indexNeedsSize = ! $this->ppClaimIndexCoversSize();
$columnsToNarrow = $this->claimTokenColumnsToNarrow();
if (! $indexNeedsSize && $columnsToNarrow === []) {
return;
}
if ($this->isMariaDbOrMySql()) { if ($this->isMariaDbOrMySql()) {
$specifications = []; $specifications = [];
if ($this->indexExists(self::PP_CLAIM_INDEX)) { if ($indexNeedsSize) {
$specifications[] = 'DROP INDEX `'.self::PP_CLAIM_INDEX.'`'; 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';
} }
$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)); DB::statement('ALTER TABLE '.$this->table('releases').' '.implode(', ', $specifications));
@@ -60,6 +75,10 @@ return new class extends Migration
return; return;
} }
if (! $indexNeedsSize) {
return;
}
Schema::table('releases', function (Blueprint $table): void { Schema::table('releases', function (Blueprint $table): void {
if ($this->indexExists(self::PP_CLAIM_INDEX)) { if ($this->indexExists(self::PP_CLAIM_INDEX)) {
$table->dropIndex(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')) { foreach (Schema::getIndexes('releases') as $index) {
return; if (($index['name'] ?? null) === self::PP_CLAIM_INDEX) {
return in_array('size', $index['columns'] ?? [], true);
}
} }
if ($this->isMariaDbOrMySql()) { return false;
$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';
}
}
DB::statement('ALTER TABLE '.$this->table('releases').' '.implode(', ', $specifications)); /** @return list<string> */
private function claimTokenColumnsToNarrow(): array
return; {
if (! $this->isMariaDbOrMySql()) {
return [];
} }
Schema::table('releases', function (Blueprint $table): void { $types = [];
if ($this->indexExists(self::PP_CLAIM_INDEX)) { foreach (Schema::getColumns('releases') as $column) {
$table->dropIndex(self::PP_CLAIM_INDEX); $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 private function indexExists(string $name): bool
+3 -3
View File
@@ -1324,7 +1324,7 @@ CREATE TABLE `releases` (
`videostatus` tinyint(1) NOT NULL DEFAULT 0, `videostatus` tinyint(1) NOT NULL DEFAULT 0,
`nzbstatus` tinyint(1) NOT NULL DEFAULT 0, `nzbstatus` tinyint(1) NOT NULL DEFAULT 0,
`nzb_creation_claimed_at` timestamp NULL DEFAULT NULL, `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, `iscategorized` tinyint(1) NOT NULL DEFAULT 0,
`isrenamed` tinyint(1) NOT NULL DEFAULT 0, `isrenamed` tinyint(1) NOT NULL DEFAULT 0,
`proc_pp` 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', `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', `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_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`), PRIMARY KEY (`id`),
UNIQUE KEY `ux_releases_guid` (`guid`), UNIQUE KEY `ux_releases_guid` (`guid`),
KEY `ix_releases_groupsid` (`groups_id`,`passwordstatus`), KEY `ix_releases_groupsid` (`groups_id`,`passwordstatus`),
@@ -1362,7 +1362,7 @@ CREATE TABLE `releases` (
KEY `ix_releases_adddate_id` (`adddate`,`id`), KEY `ix_releases_adddate_id` (`adddate`,`id`),
KEY `ix_releases_predb_id` (`predb_id`), KEY `ix_releases_predb_id` (`predb_id`),
KEY `ix_releases_size` (`size`), 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_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`) KEY `ix_releases_nzb_creation_global_queue` (`nzbstatus`,`postdate` DESC,`id`,`nzb_creation_claimed_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) 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('releases', 'proc_sorter'));
$this->assertFalse(Schema::hasColumn('release_comments', 'gid')); $this->assertFalse(Schema::hasColumn('release_comments', 'gid'));
$this->assertSame( $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->indexColumns('ix_releases_add_pp_claim_queue'),
); );
$this->assertSame( $this->assertSame(
@@ -114,13 +114,11 @@ final class ReleaseSchemaOptimizationMigrationMariaDbTest extends TestCase
} }
#[Test] #[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->seedLegacyData();
$this->migration()->up();
$followUp = $this->followUpMigration();
$followUp->up(); $this->migration()->up();
$this->assertSame( $this->assertSame(
['passwordstatus', 'haspreview', 'nzbstatus', 'leftguid', 'postdate', 'id', 'additional_pp_claimed_at', 'size'], ['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]); 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')); $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( $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->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 private function createSchema(): void