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

@@ -39,6 +39,68 @@ Example using `/home/www/healthcheck`:
> `sudo chmod 755 /home/www` once is usually enough. `/var/www` doesn't have
> this problem (always world-readable by default).
## Database — MySQL or SQLite
The code runs against either MySQL 8.0+ or SQLite 3.24+. The `config.php.example`
defaults to **MySQL** because the `php-sqlite3` extension is unmaintained
on some older distributions (Ubuntu 20.04 + Ondrej Sury's PHP 8.3 build is
the case that bit us — the SQLite extension is no longer packaged for that
combination, and you can't upgrade the OS without disturbing other live
services).
### MySQL setup
As root on the DB server (root via socket auth works on a stock Ubuntu MySQL):
```sql
CREATE DATABASE att_hc DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'att_hc'@'localhost' IDENTIFIED BY 'STRONG_RANDOM_PASSWORD';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, ALTER, REFERENCES
ON att_hc.* TO 'att_hc'@'localhost';
FLUSH PRIVILEGES;
```
The grant is scoped to `att_hc.*` only — the user can't see or touch other
databases on a shared box. Generate the password with:
```sh
php -r 'echo bin2hex(random_bytes(18)), PHP_EOL;'
```
Then in `config.php`:
```php
'db_dsn' => 'mysql:host=localhost;dbname=att_hc;charset=utf8mb4',
'db_user' => 'att_hc',
'db_pass' => 'STRONG_RANDOM_PASSWORD',
```
The schema is created on the first authenticated request (the migration
runner runs CREATE TABLE IF NOT EXISTS for everything). You can pre-trigger
it by hitting any authenticated endpoint, e.g.:
```sh
curl -s -H "Authorization: Bearer <api_key>" \
https://healthcheck.example.com/sites?limit=1
```
After which `SHOW TABLES IN att_hc;` should list `healthchecks`,
`step_updates`, `migrations`.
### SQLite setup (if `pdo_sqlite` is available)
Set in `config.php`:
```php
'db_dsn' => 'sqlite:' . __DIR__ . '/data/att_hc.sqlite',
'db_user' => null,
'db_pass' => null,
```
The DB file is created automatically inside `data/` on first request, so
that directory needs to be writable by the web user. See the install
snippet below for the `mkdir + chmod 770` step.
## First-time install
```sh
@@ -56,12 +118,15 @@ rsync -avz --exclude='data/' --exclude='config.php' \
cd /home/www/healthcheck
cp config.php.example config.php
php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;' # generate api_key
$EDITOR config.php # paste it in
$EDITOR config.php # paste it in + DB creds
# Only needed for the SQLite backend — MySQL skips this:
mkdir -p data
sudo chown -R www-data:www-data data/ config.php
sudo chmod 640 config.php
sudo chown -R www-data:www-data data/
sudo chmod 770 data/
sudo chown www-data:www-data config.php
sudo chmod 640 config.php
```
## Apache vhost (example)