Skip to content

fix: bump the database version only after the migration succeeded - #844

Open
2ndkauboy wants to merge 1 commit into
v3from
fix/798-db-version-after-migration
Open

fix: bump the database version only after the migration succeeded#844
2ndkauboy wants to merge 1 commit into
v3from
fix/798-db-version-after-migration

Conversation

@2ndkauboy

Copy link
Copy Markdown
Member

Fixes #798.

Handlers\PluginUpdate::maybe_update_database() wrote the new database version before it migrated anything, so any error inside the steps spent the one chance a site gets. db_version_is_current() then reported the database as up-to-date on every later request and the migration never ran again.

What the user sees: the legacy antispam_bee option is still in the database, antispam_bee_options was never written, and the site falls back to Settings::$defaults. Their entire configuration is gone, silently, with no way to retrigger the migration short of editing antispambee_db_version by hand.

#793 removed the most likely trigger — the Undefined array key warnings — but not the failure mode. A fatal, a DB error, a timeout, or anything a future step adds still burns the one attempt.

The change

The update_option() call moves after the steps. Deferring it cannot make the migration run twice inside one request: self::$db_update_triggered is set before any of it, which is what the issue already noted.

The steps move into run_migration_steps(), reached through static:: rather than self::. That is what lets a test supply a failing migration — the suite cannot provoke a real fatal on purpose, and defining a constant to force one leaks into every later test in the process.

One thing worth reviewing

The null === $version_from_db check became is_scalar( $version_from_db ).

A fresh install has nothing to migrate, which null already covered. But a recorded revision that is not a scalar cannot be compared against at all, and with the version write no longer happening first, such a value would now fatal on every request rather than once. Both cases have no migration to run, so both fall through to the version write. Only this class ever writes that option, so this is defence against a corrupted row rather than a case anyone should hit.

Tests

Two integration tests, in tests/Integration/Handlers/PluginUpdateTest.php:

  • a failing migration leaves the database version at 1.02 and writes no v3 option
  • the next request retries it and migrates from the legacy option, not from defaults

Both fail against the old code — verified by reinstating the early write:

...S.........FF                                                   15 / 15 (100%)
Tests: 15, Assertions: 29, Failures: 2.

The failure is driven through FailingPluginUpdate, a subclass whose run_migration_steps() throws, standing in for a fatal, a DB error, a timeout or a promoted warning.

Suite Result
unit 160 tests, 325 assertions
integration, single site 15 tests, 29 assertions, 1 skipped
integration, multisite 15 tests, 31 assertions

PHPStan clean, phpcs clean.

@2ndkauboy 2ndkauboy added this to the 3.0.0-beta.3 milestone Aug 21, 2026
@2ndkauboy 2ndkauboy added the v3 This issue is for the new version (v3) of the plugin label Aug 21, 2026
@github-actions

Copy link
Copy Markdown

🔍 WordPress Plugin Check Report

❌ Status: Failed

📊 Report

🎯 Total Issues ❌ Errors ⚠️ Warnings
1 1 0

❌ Errors (1)

📁 readme.txt (1 error)
📍 Line 🔖 Check 💬 Message
0 outdated_tested_upto_header Tested up to: 7.0 < 7.1. The "Tested up to" value in your plugin is not set to the current version of WordPress. This means your plugin will not show up in searches, as we require plugins to be compatible and documented as tested up to the most recent version of WordPress.

🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

`Handlers\PluginUpdate::maybe_update_database()` wrote the new database version
before it migrated anything, so a single failure spent the one chance a site
gets. Any error inside the steps — a fatal, a DB error, a timeout, a warning
promoted to an exception — left the version already raised, `db_version_is_current()`
reported the database as up-to-date on every later request, and the migration
never ran again.

The consequence is silent and unrecoverable: the legacy `antispam_bee` option is
still there, `antispam_bee_options` was never written, and the site falls back to
`Settings::$defaults`. The user's entire configuration is gone with no way to
retrigger the migration short of editing `antispambee_db_version` by hand.

The version write now happens after the steps have completed. Deferring it cannot
make the migration run twice inside one request, because `self::$db_update_triggered`
is set before any of it.

The steps move into `run_migration_steps()`, reached through `static::` so a test
can supply a failing migration without needing an error the suite cannot provoke
on purpose. The `null` check becomes `is_scalar()` on the way: a fresh install has
nothing to migrate, and a recorded revision that is not a scalar cannot be
compared against — which used to fatal once and would now fatal on every request,
since nothing raises the version past it.

Fixes #798
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v3 This issue is for the new version (v3) of the plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant