test: ActivityLogIndexesTest regression guard for D-06

PR-2 verified that Spatie's activitylog default migration creates the
composite indexes RFC-WS-7 §3.14 / addendum D-06 require — via
nullableMorphs('subject') and nullableMorphs('causer'), which emit
indexes named `subject` on (subject_type, subject_id) and `causer` on
(causer_type, causer_id).

This test queries information_schema.STATISTICS and fails if either
composite is missing, regardless of the index name. It guards against
silent regression when:
  - A future Spatie major release changes nullableMorphs semantics.
  - A developer rewrites the activity_log migration without preserving
    the morph indexes.
  - A schema-dump regeneration drops them.

Test count 1539 to 1541. Larastan clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 13:00:07 +02:00
parent 49cece3784
commit eb8202584c

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Database;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
/**
* Regression guard for RFC-WS-7 §3.14 / addendum D-06 composite indexes
* on the activity_log table.
*
* Spatie's activitylog default migration calls nullableMorphs('subject')
* and nullableMorphs('causer'), which create composite indexes named
* `subject` on (subject_type, subject_id) and `causer` on
* (causer_type, causer_id). RFC-WS-7 §3.14 / addendum D-06 require
* exactly these indexes for query planner support on activity_log
* lookups by morph subject/causer; PR-2 verified they already exist
* via information_schema.
*
* This test fails when:
* - A future Spatie major release changes nullableMorphs() semantics.
* - A developer rewrites the activity_log migration without keeping
* the morph indexes.
* - A new schema-dump regeneration silently drops them.
*/
final class ActivityLogIndexesTest extends TestCase
{
use RefreshDatabase;
public function test_subject_composite_index_exists(): void
{
$this->assertCompositeIndexExists(
table: 'activity_log',
columns: ['subject_type', 'subject_id'],
description: 'subject composite index (RFC-WS-7 §3.14 / D-06)',
);
}
public function test_causer_composite_index_exists(): void
{
$this->assertCompositeIndexExists(
table: 'activity_log',
columns: ['causer_type', 'causer_id'],
description: 'causer composite index (RFC-WS-7 §3.14 / D-06)',
);
}
/**
* @param list<string> $columns
*/
private function assertCompositeIndexExists(string $table, array $columns, string $description): void
{
$database = config('database.connections.mysql.database');
$rows = DB::select(
'SELECT INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = ?
AND TABLE_NAME = ?
ORDER BY INDEX_NAME, SEQ_IN_INDEX',
[$database, $table],
);
$indexColumns = [];
foreach ($rows as $row) {
$indexColumns[$row->INDEX_NAME][(int) $row->SEQ_IN_INDEX] = $row->COLUMN_NAME;
}
$found = false;
foreach ($indexColumns as $sequence) {
ksort($sequence);
if (array_values($sequence) === $columns) {
$found = true;
break;
}
}
$this->assertTrue(
$found,
sprintf(
'Expected composite index on %s(%s) — %s. Found indexes: %s',
$table,
implode(', ', $columns),
$description,
json_encode($indexColumns, JSON_PRETTY_PRINT),
),
);
}
}