From 43ae38aebcac87ae6b508e8fc0945b064c2fb251 Mon Sep 17 00:00:00 2001 From: Rushaway Date: Mon, 26 Oct 2026 08:44:53 +0100 Subject: [PATCH] fix(db): restore missing sb_bans composite indexes Port of upstream sbpp/sourcebans-pp#1605 (584ef2ef). `702.php` adds `type_authid` / `type_ip` on `:prefix_bans`, but some long-lived installs carry a `config.version` past 702 without them, so 702 never runs there. Add a migration that creates both indexes idempotently (portable information_schema probe, no MariaDB-only `ADD INDEX IF NOT EXISTS`). Fork adaptations: - upstream's migration 812 lands as 815 (811-814 are taken here); - the `PruneBans()` half of the upstream fix (dropping the FORCE INDEX hints that fatal with error 1176 without the indexes) is not needed: this fork's PruneBans() never had the hints. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 ++ .../integration/Updater815BansIndexesTest.php | 87 +++++++++++++++++++ .../UpdaterMigrationPortableSqlTest.php | 4 +- web/updater/data/815.php | 40 +++++++++ web/updater/store.json | 3 +- 5 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 web/tests/integration/Updater815BansIndexesTest.php create mode 100644 web/updater/data/815.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 028484ccc..3a4d5fd8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ Legend: ? = Other stuff ``` +(26/10/26): Version 2.2.3-srcdslab (**Run updater required**) +----------------------- +``` +01. ! Upgraded installs missing the type_authid / type_ip indexes on + sb_bans get them back (updater migration 815); the ban check, + banlist and PruneBans() no longer scan the whole table there +``` + (22/10/26): Version 2.1.26-srcdslab ----------------------- ``` diff --git a/web/tests/integration/Updater815BansIndexesTest.php b/web/tests/integration/Updater815BansIndexesTest.php new file mode 100644 index 000000000..09ed29960 --- /dev/null +++ b/web/tests/integration/Updater815BansIndexesTest.php @@ -0,0 +1,87 @@ +indexExists('type_authid')) { + $pdo->exec("ALTER TABLE `{$table}` ADD INDEX `type_authid` (`type`, `authid`)"); + } + if (!$this->indexExists('type_ip')) { + $pdo->exec("ALTER TABLE `{$table}` ADD INDEX `type_ip` (`type`, `ip`)"); + } + + parent::tearDown(); + } + + private function indexExists(string $index): bool + { + $stmt = Fixture::rawPdo()->prepare( + 'SELECT COUNT(*) FROM information_schema.STATISTICS ' + . 'WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME = ?' + ); + $stmt->execute([DB_NAME, DB_PREFIX . '_bans', $index]); + + return (int) $stmt->fetchColumn() > 0; + } + + private function dropIndexes(): void + { + $table = DB_PREFIX . '_bans'; + foreach (self::INDEXES as $index) { + Fixture::rawPdo()->exec("ALTER TABLE `{$table}` DROP INDEX `{$index}`"); + $this->assertFalse($this->indexExists($index), "Pre-condition: {$index} was dropped."); + } + } + + private function runMigration(): bool + { + $ctx = new class($GLOBALS['PDO']) { + public function __construct(public \Database $dbs) {} + + public function run(string $path): mixed + { + return require $path; + } + }; + + return (bool) $ctx->run(ROOT . 'updater/data/815.php'); + } + + public function testPruneBansRunsWithoutCompositeIndexes(): void + { + $this->dropIndexes(); + + \PruneBans(); + + $this->addToAssertionCount(1); + } + + public function testMigrationRestoresMissingIndexesAndIsIdempotent(): void + { + $this->dropIndexes(); + + $this->assertTrue($this->runMigration(), 'First run should succeed.'); + foreach (self::INDEXES as $index) { + $this->assertTrue($this->indexExists($index), "Migration must add {$index}."); + } + + $this->assertTrue($this->runMigration(), 'Second run must not fatal on duplicate index.'); + } +} diff --git a/web/tests/integration/UpdaterMigrationPortableSqlTest.php b/web/tests/integration/UpdaterMigrationPortableSqlTest.php index 7bee7f025..c19452f95 100644 --- a/web/tests/integration/UpdaterMigrationPortableSqlTest.php +++ b/web/tests/integration/UpdaterMigrationPortableSqlTest.php @@ -25,14 +25,14 @@ * * Scope (focused, per #1498) * -------------------------- - * Only migrations with version >= 800 (the 8xx v2 block, 801-810 today) + * Only migrations with version >= 800 (the 8xx v2 block, 801-815 today) * are scanned. Two reasons: * * - Every supported upgrade path the issue names (v2 rc5 -> rc6, * v1.8.x -> v2) starts at `config.version = 705`, so the updater only * runs 801+. 801 is the first migration past 705 and the actual * blocker. Future migrations are always numbered above the current - * max (811+), so a `>= 800` floor guards every migration that will + * max (816+), so a `>= 800` floor guards every migration that will * ever be added going forward. * - Ten pre-800 migrations carry the same MariaDB-only syntax * (1, 112, 150, 153, 160, 241, 291, 295, 351, 355). They only run for diff --git a/web/updater/data/815.php b/web/updater/data/815.php new file mode 100644 index 000000000..ffc77353f --- /dev/null +++ b/web/updater/data/815.php @@ -0,0 +1,40 @@ +dbs` read is suppressed like every sibling migration. + +$ensureBansIndex = static function (\Database $dbs, string $index, string $alterSql): void { + $dbs->query( + 'SELECT COUNT(*) AS c FROM information_schema.STATISTICS ' + . 'WHERE TABLE_SCHEMA = DATABASE() ' + . 'AND TABLE_NAME = :table ' + . 'AND INDEX_NAME = :index' + ); + $dbs->bind(':table', $dbs->getPrefix() . '_bans'); + $dbs->bind(':index', $index); + $row = $dbs->single(); + + if (is_array($row) && (int) ($row['c'] ?? 0) > 0) { + return; + } + + $dbs->query($alterSql); + $dbs->execute(); +}; + +// @phpstan-ignore variable.undefined +$ensureBansIndex($this->dbs, 'type_authid', 'ALTER TABLE `:prefix_bans` ADD INDEX `type_authid` (`type`, `authid`)'); +// @phpstan-ignore variable.undefined +$ensureBansIndex($this->dbs, 'type_ip', 'ALTER TABLE `:prefix_bans` ADD INDEX `type_ip` (`type`, `ip`)'); + +return true; diff --git a/web/updater/store.json b/web/updater/store.json index 611d2f882..0b2598205 100644 --- a/web/updater/store.json +++ b/web/updater/store.json @@ -52,5 +52,6 @@ "811": "811.php", "812": "812.php", "813": "813.php", - "814": "814.php" + "814": "814.php", + "815": "815.php" }