diff --git a/lib/Migration/Version8000Date20260603120000.php b/lib/Migration/Version8000Date20260603120000.php new file mode 100644 index 000000000..b5fc71e65 --- /dev/null +++ b/lib/Migration/Version8000Date20260603120000.php @@ -0,0 +1,61 @@ +hasTable('activity_mq')) { + return null; + } + + $table = $schema->getTable('activity_mq'); + + if (!$table->hasIndex('amp_user_send')) { + $table->addIndex(['amq_affecteduser', 'amq_latest_send'], 'amp_user_send'); + } + + if ($table->hasIndex('amp_user')) { + $table->dropIndex('amp_user'); + } + + return $schema; + } +} diff --git a/tests/Migration/Version8000Date20260603120000Test.php b/tests/Migration/Version8000Date20260603120000Test.php new file mode 100644 index 000000000..969d3449c --- /dev/null +++ b/tests/Migration/Version8000Date20260603120000Test.php @@ -0,0 +1,105 @@ +output = $this->createMock(IOutput::class); + $this->migration = new Version8000Date20260603120000(); + } + + /** + * On a queue that still has the legacy single-column `amp_user` index the + * migration adds the covering composite index and drops the now-redundant one. + */ + public function testAddsCompositeIndexAndDropsRedundantOne(): void { + $table = $this->createMock(Table::class); + $table->method('hasIndex') + ->willReturnMap([ + ['amp_user_send', false], + ['amp_user', true], + ]); + $table->expects($this->once()) + ->method('addIndex') + ->with(['amq_affecteduser', 'amq_latest_send'], 'amp_user_send'); + $table->expects($this->once()) + ->method('dropIndex') + ->with('amp_user'); + + $schema = $this->getSchemaMock($table); + + $result = $this->migration->changeSchema($this->output, fn (): ISchemaWrapper => $schema, []); + $this->assertSame($schema, $result, 'The schema must be returned when it was changed'); + } + + /** + * Running the migration again (composite index present, legacy index gone) + * must be a no-op so re-runs / fresh installs are not touched. + */ + public function testIsIdempotentWhenAlreadyMigrated(): void { + $table = $this->createMock(Table::class); + $table->method('hasIndex') + ->willReturnMap([ + ['amp_user_send', true], + ['amp_user', false], + ]); + $table->expects($this->never()) + ->method('addIndex'); + $table->expects($this->never()) + ->method('dropIndex'); + + $schema = $this->getSchemaMock($table); + + $result = $this->migration->changeSchema($this->output, fn (): ISchemaWrapper => $schema, []); + $this->assertSame($schema, $result, 'Re-running must not add or drop the indexes again'); + } + + /** + * The mail queue table does not exist on every install (e.g. before its + * creation migration ran), so a missing table must be skipped gracefully. + */ + public function testSkipsWhenTableIsMissing(): void { + $schema = $this->createMock(ISchemaWrapper::class); + $schema->method('hasTable') + ->with('activity_mq') + ->willReturn(false); + $schema->expects($this->never()) + ->method('getTable'); + + $result = $this->migration->changeSchema($this->output, fn (): ISchemaWrapper => $schema, []); + $this->assertNull($result); + } + + protected function getSchemaMock(Table&MockObject $table): ISchemaWrapper&MockObject { + $schema = $this->createMock(ISchemaWrapper::class); + $schema->method('hasTable') + ->with('activity_mq') + ->willReturn(true); + $schema->method('getTable') + ->with('activity_mq') + ->willReturn($table); + + return $schema; + } +}