Skip to content

fix(updater): portable information_schema guard for lockout columns (#1498) - #1499

Merged
rumblefrog merged 1 commit into
mainfrom
fix/1498-updater-portable-lockout-columns
Jun 7, 2026
Merged

rumblefrog merged 1 commit into
mainfrom
fix/1498-updater-portable-lockout-columns

Conversation

@rumblefrog

Copy link
Copy Markdown
Member

Summary

Fixes #1498. Migration web/updater/data/801.php added the admin lockout columns with ALTER TABLE ... ADD IF NOT EXISTS, a MariaDB-only DDL extension. Stock MySQL (8.x included) and MySQL-compatible engines such as Percona Server reject it with SQLSTATE[42000] 1064 mid-upgrade, so panels on those engines could not upgrade past 801 (the blocker on every supported path: v2 rc5 -> rc6 and v1.8.x -> v2 both start at config.version = 705).

Root cause

ADD IF NOT EXISTS was introduced in #1473 to make #1472's fix idempotent (panels that already carry the columns must not fatal with SQLSTATE[42S21] "Duplicate column name"). It does that on MariaDB, but the syntax is non-portable. Because the dev stack and CI both run MariaDB (which accepts it), the runtime idempotency test (Updater801LockoutColumnsTest) and PHPStan's dba gate (which also introspects MariaDB) both passed it through. The break only surfaced on self-hosters running MySQL / Percona, both first-class supported engines per the docs (prerequisites.mdx: "MySQL >= 5.6 ... 8.0+ is fine and recommended").

Fix

  • Rewrite 801.php to guard each ADD with a portable information_schema.COLUMNS existence probe + a plain ADD COLUMN. Same idempotent contract, runs on both engines, converges to the same schema.
  • Edited 801.php in place rather than shipping a new migration: the effect is unchanged. The Updater only runs versions above config.version, so MariaDB installs that already ran 801 never re-run it; MySQL/Percona installs that fataled never advanced past it and pick up the fixed version on the next pass. Fresh installs get the columns from struc.sql and never run the updater. (Per the AGENTS.md "Updater migrations" rule, in-place edits are correct when the script's effect doesn't change.)
  • The two $this->dbs reads (supplied by Updater::update()'s instance scope) are suppressed inline with @phpstan-ignore variable.undefined; the stale phpstan-baseline.neon entry for 801.php is removed.

Regression guard

Adds web/tests/integration/UpdaterMigrationPortableSqlTest.php — a static source-scan guard (mirrors DeadJsCallSitesTest's pure-file-scan shape) that fails if any v2-era migration (version >= 800) reuses the MariaDB-only ADD ... IF NOT EXISTS form. A runtime test can't catch this class of bug because the suite runs against MariaDB. CREATE TABLE IF NOT EXISTS (valid on every engine) is intentionally not matched. Comments are stripped via php_strip_whitespace() so 801's own explanatory docblock doesn't false-fire.

Out of scope / follow-up

Ten pre-800 migrations carry the same MariaDB-only syntax (1, 112, 150, 153, 160, 241, 291, 295, 351, 355). They only run on ancient (pre-356, SB 1.5.x-era) install -> v2 paths, a real but rarer scenario, and a couple have quirks (295 is a compound ADD ..., ADD ...; 355 hardcodes an sb_mods prefix). Deliberately left out so this fix stays small and low-risk; tracked as a follow-up. The new test's >= 800 floor guards every migration added going forward.

Test plan

  • ./sbpp.sh test --filter='Updater801LockoutColumns|UpdaterMigrationPortableSql' — 3 tests, 23 assertions, green.
  • ./sbpp.sh phpstan — no errors (validates the inline ignores, the baseline removal, the new test file, and that the information_schema SELECT passes the dba gate).
  • Manual verification on a stock MySQL 8 / Percona Server instance (the engine the dev stack can't reproduce): upgrade from config.version = 705 completes past 801 without the 1064 fatal.

…1498)

Migration 801.php added the admin lockout columns with
`ALTER TABLE ... ADD IF NOT EXISTS`, a MariaDB-only DDL extension.
Stock MySQL (8.x included) and MySQL-compatible engines like Percona
Server reject it with SQLSTATE[42000] 1064 mid-upgrade, so panels on
those engines could not upgrade past 801 (v2 rc5 -> rc6, v1.8.x -> v2).

The dev stack and CI both run MariaDB, which accepts the syntax, so the
runtime idempotency test and PHPStan's dba gate both passed it through;
the break only surfaced on self-hosters running MySQL / Percona, both
first-class supported engines per the docs.

Guard each ADD with a portable information_schema existence probe + a
plain `ADD COLUMN`. Same idempotent contract, runs on both engines,
converges to the same schema. Edited 801.php in place rather than
shipping a new migration because the effect is unchanged (the Updater
only runs versions above config.version, so MariaDB installs that
already ran 801 never re-run it; MySQL installs that fataled never
advanced past it and pick up the fixed version on the next pass).

Add UpdaterMigrationPortableSqlTest, a static source-scan guard that
fails if any v2-era migration (version >= 800) reuses the MariaDB-only
`ADD ... IF NOT EXISTS` form. A runtime test can't catch this (the
suite runs against MariaDB). Ten pre-800 migrations carry the same
syntax but only run on ancient-install -> v2 paths; they're a tracked
follow-up, deliberately out of scope here.
@rumblefrog
rumblefrog added this pull request to the merge queue Jun 7, 2026
Merged via the queue into main with commit 97d5804 Jun 7, 2026
5 checks passed
@rumblefrog
rumblefrog deleted the fix/1498-updater-portable-lockout-columns branch June 7, 2026 00:16
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1.8.1 and v2 rc5 ->v2 rc6 Updater gives SQL error

1 participant