Write a safe plugin activation hook that handles a database migration
A custom-table migration scenario reasoning through dbDelta quirks, multisite network activation, and version-gated upgrades so activation never re-runs destructively.
Works with Claude / GPT1,100 uses★ 4.3
The prompt
code-plugin-activation-hook-db-migration
My plugin needs a custom table `wp_myplugin_bookings` to store appointment data. It's
about to go from v1.3 (no custom table) to v2.0 (introduces the table), and I need the
activation hook to create the table on new installs and migrate existing sites cleanly.
ENVIRONMENT: [this plugin is used on both single-site installs and a couple of
multisite networks with 15-30 subsites each, and it's activated via WP-CLI in bulk on
some client staging environments]
Handle this like a senior dev who has been burned by a bad migration before, in order:
1. Show the `register_activation_hook()` callback calling a `create_table()` method
that uses `dbDelta()`, and list the exact formatting rules `dbDelta()` silently
requires (two spaces after `PRIMARY KEY`, each field on its own line, no backticks
around index names) since getting these wrong makes `dbDelta()` fail silently
without creating or altering anything.
2. Make the migration idempotent and version-gated: store a `myplugin_db_version`
option, and run the table creation/alteration logic on `plugins_loaded` (checking
the stored version against the current one) in addition to activation, since
activation alone won't run again for sites that update the plugin without
deactivating first.
3. Handle multisite correctly: if `is_multisite()` and the plugin is being
network-activated, loop over `get_sites()` and run the migration per-blog with
`switch_to_blog()` / `restore_current_blog()`, rather than only creating the table
on the main site, which is the most common multisite activation bug.
4. Flag the edge case with bulk WP-CLI activation: `register_activation_hook()` fires
once per plugin activation call, so if a script activates the plugin across 20
subsites in a loop without properly switching blog context first, the table can end
up created only on whichever site was active when PHP initialized, not on each
target site. Show the correct WP-CLI-safe pattern.
5. Add a rollback-safe approach: don't drop or alter data destructively in the
migration itself. If v2.0 needs to backfill data from an old postmeta-based
structure into the new table, do it in a batched background process (WP-Cron or
`wp_schedule_single_event`) rather than inline during activation, since activation
hooks that run heavy queries can time out and leave a site in a half-migrated state.
Replace the table schema and version numbers with your actual migration.
The dbDelta() formatting rules in step 1 are the classic silent failure: the function returns no error, so a malformed CREATE TABLE string just does nothing, and you find out days later when a support ticket says a feature “isn’t saving anything.”