Server: MySQL support (portable schema + driver-aware upsert)

Some VPS configs can't get pdo_sqlite at all — Ubuntu 20.04 with Ondrej
Sury's PHP 8.3 builds doesn't ship php8.3-sqlite3, and OS upgrade isn't
always an option. Make MySQL the documented default while keeping SQLite
working where it's available.

- Schema (0001_initial.sql): TEXT → VARCHAR(N), INTEGER → BIGINT, keys
  declared with explicit PRIMARY KEY (...) syntax. Drops the previously
  unused request_log table (it had AUTO_INCREMENT, which spells
  differently on each engine and nothing wrote to it anyway). Both
  engines accept the new column types and indexes are IF NOT EXISTS for
  retry-safety.
- Migrations.php: guard commit()/rollBack() with inTransaction(). MySQL
  implicitly commits any open transaction the moment it sees a DDL
  statement, so by the time we explicitly commit() the transaction is
  already gone and PDO throws "There is no active transaction". Same
  schema in PHP CREATE TABLE migrations also moved to VARCHAR/BIGINT.
- Store::upsertStep: driver-detect via PDO::ATTR_DRIVER_NAME and emit
  ON DUPLICATE KEY UPDATE for MySQL, ON CONFLICT (...) DO UPDATE for
  SQLite/PostgreSQL. VALUES(col) (vs new.col aliasing) for MySQL 5.7
  compatibility.
- Db.php: when DSN is mysql:, SET NAMES utf8mb4 + sql_mode strict on
  every session so we get sane behaviour regardless of server defaults.
  SQLite branch (PRAGMA foreign_keys/journal_mode/synchronous) unchanged.
- config.php.example: MySQL DSN is now the default + an inline SQLite
  alternative block.
- DEPLOY.md: new "Database — MySQL or SQLite" section explaining when to
  pick which and showing the CREATE DATABASE / CREATE USER / GRANT
  statements. Install snippet split so SQLite-only steps (mkdir data,
  chmod 770) are clearly optional.

Verified end-to-end on a live MySQL 8.0.34 box: POST creates session
(201), PUT step inserts (200) and updates via the upsert branch (200),
GET returns the round-tripped state, /sites lists distinct site_keys.
SQLite path still re-applies the migration idempotently locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 15:36:45 +01:00
parent 12ab917d04
commit 3c6220e64b
6 changed files with 167 additions and 54 deletions

View File

@@ -1,46 +1,46 @@
-- Portable across SQLite and MySQL 8.0+:
-- - VARCHAR(N) is honoured by MySQL, treated as TEXT by SQLite.
-- - BIGINT covers post-2038 unix seconds; SQLite stores as INTEGER affinity.
-- - No AUTO_INCREMENT in this file — we don't need it for the current tables.
-- (An audit table that needs an autoincrement id can come in a later
-- migration with per-driver dispatch in Migrations.php.)
--
-- Re-running is safe: Migrations.php only applies each .sql file once
-- (tracked in the `migrations` table created in PHP).
CREATE TABLE IF NOT EXISTS healthchecks (
id TEXT PRIMARY KEY,
site_key TEXT NOT NULL,
started_at INTEGER NOT NULL,
finished_at INTEGER NULL,
technician_id INTEGER NULL,
reporting_url TEXT NOT NULL,
wp_version TEXT NULL,
php_version TEXT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
id VARCHAR(64) NOT NULL,
site_key VARCHAR(255) NOT NULL,
started_at BIGINT NOT NULL,
finished_at BIGINT NULL,
technician_id BIGINT NULL,
reporting_url VARCHAR(512) NOT NULL,
wp_version VARCHAR(32) NULL,
php_version VARCHAR(32) NULL,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
PRIMARY KEY (id),
UNIQUE (site_key, started_at)
);
CREATE INDEX IF NOT EXISTS healthchecks_site_started_idx
ON healthchecks (site_key, started_at DESC);
ON healthchecks (site_key, started_at);
CREATE INDEX IF NOT EXISTS healthchecks_site_finished_idx
ON healthchecks (site_key, finished_at);
CREATE TABLE IF NOT EXISTS step_updates (
healthcheck_id TEXT NOT NULL REFERENCES healthchecks(id) ON DELETE CASCADE,
step_id TEXT NOT NULL,
status TEXT NOT NULL,
notes TEXT NOT NULL DEFAULT '',
autocheck_json TEXT NULL,
reporting_url TEXT NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (healthcheck_id, step_id)
healthcheck_id VARCHAR(64) NOT NULL,
step_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
notes TEXT NOT NULL,
autocheck_json TEXT NULL,
reporting_url VARCHAR(512) NOT NULL,
updated_at BIGINT NOT NULL,
PRIMARY KEY (healthcheck_id, step_id),
CONSTRAINT fk_step_updates_hc FOREIGN KEY (healthcheck_id)
REFERENCES healthchecks(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS step_updates_step_updated_idx
ON step_updates (step_id, updated_at DESC);
CREATE TABLE IF NOT EXISTS request_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
method TEXT NOT NULL,
path TEXT NOT NULL,
status INTEGER NOT NULL,
ip TEXT NULL,
bytes_in INTEGER NULL,
bytes_out INTEGER NULL
);
CREATE INDEX IF NOT EXISTS request_log_ts_idx ON request_log (ts DESC);
ON step_updates (step_id, updated_at);