get_charset_collate()` (per §17.2) and relies on * `InnoDB` as the storage engine (§17.1). * * Scope explicitly NOT covered here: * * - `CHECK (level IN (...))` constraint. §9.2 enumerates the allowed * PSR-3 level values (`debug`, `info`, `notice`, `warning`, * `error`, `critical`; ADR-0008 / ADR-0019), but `dbDelta()` does * not reliably round-trip `CHECK` constraints across MySQL / * MariaDB versions (per §13.4). Application-layer validation on * the log-write path is the canonical enforcement point. * * - `CHECK (channel IN (...))` constraint. §9.2 enumerates the * initial-version logical channel enum (`provider.amazon`, * `provider.rakuten`, `provider.yahoo_shopping`, `sync`, `render`, * `cache`, `rest`, `block`, `audit`, `security`, `cron`, * `migration`, with `other` as the catch-all). Enforcement is * likewise left to the application layer so unknown channels fall * back to `other` rather than being silently dropped. * * - JSON-extraction function indexes on `context_json` * (`idx_event_type_created`, `idx_request_id`, * `idx_correlation_id`). §9.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 to `affilun_products.id` / `affilun_sync_jobs.id` for the * `related_*` columns. Per §11.2 and §13.4, AFFILUN does not declare * foreign keys at the DB level; the cross-references are advisory * for the diagnostics surface rather than referentially-enforced * links. * * - `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. * * - Log retention / purge logic (§9.4). This migration only creates * the table that makes retention possible; the level-aware * retention windows and scheduled purge job belong in the runtime * cleanup path (`cleanup_logs` in `affilun_sync_jobs`, §8.4). * * - 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_logs` table at schema version 6. * * 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 CreateLogsTable 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_logs'; /** * Target schema version installed by this migration. * * Returns version 6 — the revision that adds the operational-log * table on top of `affilun_products` (v1), `affilun_product_meta` (v2), * `affilun_embeds` (v3), `affilun_embed_items` (v4), and `affilun_sync_jobs` * (v5). See `database-schema.md` §13.2 for the canonical version * numbering. */ public function targetVersion(): SchemaVersion { return SchemaVersion::of( 6 ); } /** * 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_logs 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_logs`, in the canonical order * defined by `database-schema.md` §9.2. * * The table is append-only: there is no `updated_at` column * because log entries are never mutated in place. Retention is * handled by the scheduled `cleanup_logs` job (§8.4 / §9.4), not * by row-level updates. * * `channel` is sized `varchar(64)` per §9.2 to accommodate the * dotted logical channel names such as `provider.yahoo_shopping` * with comfortable headroom; `level` is sized `varchar(16)` for * the six PSR-3 level strings, of which `critical` (8 characters) * is the longest. * * 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', 'channel varchar(64) NOT NULL', 'level varchar(16) NOT NULL', 'message text NOT NULL', 'context_json longtext NULL', 'related_product_id bigint unsigned NULL', 'related_job_id bigint unsigned NULL', 'created_at datetime NOT NULL', ); } /** * Index definitions for `affilun_logs`, in the canonical order * defined by `database-schema.md` §9.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_channel_level_created` index supports the * common diagnostics query pattern "show me errors and above on * channel X in the last N hours" without forcing a full table * scan; the single-column indexes on `channel`, `level`, and * `created_at` support the simpler drill-down cases. The * `idx_related_*` indexes accelerate cross-references back to a * specific product or job from the diagnostics surface. * * Optional JSON-extraction function indexes on `context_json` * (`idx_event_type_created`, `idx_request_id`, * `idx_correlation_id`) listed in §9.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_channel (channel)', 'KEY idx_level (level)', 'KEY idx_created_at (created_at)', 'KEY idx_related_product_id (related_product_id)', 'KEY idx_related_job_id (related_job_id)', 'KEY idx_channel_level_created (channel, level, created_at)', ); } }