get_charset_collate()` (per §17.2) and relies on * `InnoDB` as the storage engine (§17.1). * * Scope explicitly NOT covered here: * * - `CHECK (audit_kind IN (...))` / `CHECK (result IN (...))` * constraints. §10.2 enumerates the initial-version values * (`configuration_change`, `destructive_action`, `security_event`, * `operational_action`, `compliance_relevant_action` for * `audit_kind`; `success`, `failed`, `rejected` for `result`), but * `dbDelta()` does not reliably round-trip `CHECK` constraints * across MySQL / MariaDB versions (per §13.4). Application-layer * validation on the audit-write path is the canonical enforcement * point. * * - JSON-extraction function indexes on `context_json` * (`idx_request_id`, `idx_correlation_id`). §10.3 lists these as * "必要に応じて" (optional) and explicitly permits substituting * alternatives depending on database dialect and operational * constraints, so they are not part of the initial schema. Future * migrations may add them on environments where the `JSON_EXTRACT` * functional index form is supported (MySQL 8.0+ / MariaDB 10.5+). * * - Foreign keys. Per §11.2 and §13.4, AFFILUN does not declare foreign * keys at the DB level. The `actor_*` / `target_*` pair is kept as * free-form string discriminators rather than typed references so * audit rows survive deletion of their original subject (a deleted * product or user MUST still appear in the audit trail that * records its deletion). * * - `DEFAULT CURRENT_TIMESTAMP` on `created_at`. Per §13.4, * environment-sensitive default clauses are avoided; the timestamp * is supplied by the application layer on insert. * * - Audit retention / purge logic (§10.4). This migration only * creates the table that makes long retention possible; the * scheduled purge and the "purge-of-audit is itself auditable" * rule belong in the runtime cleanup path and the audit-write * contract, respectively. * * - Any DML. This migration creates the empty table only; seed data * and backfills belong in later migrations or runtime code paths. * * The shared DDL-assembly and installer-dispatch skeleton lives in * {@see AbstractCreateTableMigration}; this subclass only declares the * bits that actually differ between tables (target version, * description, logical name, column manifest, index manifest). See the * base class's file-level docblock for the idempotency and * failure-propagation guarantees inherited here. * * @package Affilun\Infrastructure\Persistence\Migrations */ declare(strict_types=1); namespace Affilun\Infrastructure\Persistence\Migrations; use Affilun\Infrastructure\Persistence\SchemaVersion; /** * Creates the `affilun_audit_events` table at schema version 7. * * Concrete {@see AbstractCreateTableMigration} subclass. See the * file-level docblock for the scope of this migration and the columns * and indexes it installs; see the base class for the formatting rules * and failure semantics that apply to every AFFILUN table-creation * migration. */ final class CreateAuditEventsTable extends AbstractCreateTableMigration { /** * Logical, unprefixed AFFILUN table name installed by this migration. * * Kept as a class-level constant so tests can reference the exact * name without duplicating the string literal, and so a future * rename has a single location to update. */ public const TABLE_LOGICAL_NAME = 'affilun_audit_events'; /** * Target schema version installed by this migration. * * Returns version 7 — the revision that adds the audit-trail * table on top of `affilun_products` (v1), `affilun_product_meta` (v2), * `affilun_embeds` (v3), `affilun_embed_items` (v4), `affilun_sync_jobs` (v5), * and `affilun_logs` (v6). See `database-schema.md` §13.2 for the * canonical version numbering. */ public function targetVersion(): SchemaVersion { return SchemaVersion::of( 7 ); } /** * Short, human-readable description of this migration. * * Used by the diagnostics surface and migration logs. Kept terse * and operator-friendly per the * {@see \Affilun\Infrastructure\Persistence\Migration::description()} * contract. */ public function description(): string { return 'create affilun_audit_events table'; } /** * Logical, unprefixed AFFILUN table name installed by this migration. * * Consumed by {@see AbstractCreateTableMigration::up()} and * {@see AbstractCreateTableMigration::tableName()} to resolve the * physical, prefix-applied name through the injected * {@see \Affilun\Infrastructure\Persistence\TableNameResolver}. */ protected function logicalTableName(): string { return self::TABLE_LOGICAL_NAME; } /** * Column definitions for `affilun_audit_events`, in the canonical * order defined by `database-schema.md` §10.2. * * The table is append-only: there is no `updated_at` column * because audit rows are never mutated in place. Corrections to a * previously-recorded event are themselves recorded as new audit * rows so the evidence trail remains tamper-evident. * * `event_type` and `action` are sized `varchar(64)` per §10.2 to * accommodate dotted identifiers such as `settings.updated` with * comfortable headroom. `actor_type` / `target_type` and * `audit_kind` / `result` are sized `varchar(32)` to fit the * enumerated initial-version values (`configuration_change`, * `compliance_relevant_action`, etc.) with headroom for future * additions. `actor_id` / `target_id` are sized `varchar(64)` so * numeric IDs, UUIDs, and short stable string identifiers all fit * without per-subject-type special-casing. * * `safe_summary` is `text` (not `longtext`) because §10.2 requires * a human-readable summary rather than full payload capture; the * full structured diff lives in `changed_fields_json`, and * correlation metadata lives in `context_json`. Keeping the * summary shorter also discourages accidentally embedding secrets * in free-form text. * * Each entry is a single column definition without its trailing * comma; the base class adds the indent and inter-column commas. * * @return list */ protected function columnDefinitions(): array { return array( 'id bigint unsigned NOT NULL AUTO_INCREMENT', 'event_type varchar(64) NOT NULL', 'audit_kind varchar(32) NOT NULL', 'actor_type varchar(32) NULL', 'actor_id varchar(64) NULL', 'target_type varchar(32) NULL', 'target_id varchar(64) NULL', 'action varchar(64) NOT NULL', 'result varchar(32) NOT NULL', 'safe_summary text NOT NULL', 'changed_fields_json longtext NULL', 'context_json longtext NULL', 'created_at datetime NOT NULL', ); } /** * Index definitions for `affilun_audit_events`, in the canonical * order defined by `database-schema.md` §10.3. * * The `PRIMARY KEY` entry uses the two-space separator that * `dbDelta()` relies on to detect the primary key line — this is * a WordPress-specific formatting quirk, not a typo. * * The composite `idx_actor` and `idx_target` indexes support the * common "show me every audit event for this user" / "show me * every audit event on this product" drill-down from the admin * diagnostics surface. The composite `idx_event_type_created` * index accelerates release-gate evidence queries of the form * "every `settings.updated` event in the last N days" without * forcing a full table scan of the long-retention audit table. * * Optional JSON-extraction function indexes on `context_json` * (`idx_request_id`, `idx_correlation_id`) listed in §10.3 are * NOT installed here; see the file-level docblock for the * rationale. * * @return list */ protected function indexDefinitions(): array { return array( 'PRIMARY KEY (id)', 'KEY idx_event_type (event_type)', 'KEY idx_audit_kind (audit_kind)', 'KEY idx_actor (actor_type, actor_id)', 'KEY idx_target (target_type, target_id)', 'KEY idx_created_at (created_at)', 'KEY idx_event_type_created (event_type, created_at)', ); } }