Code

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.”