Skip to content

Why phpMyAdmin migrations break plugin settings — and why wp search-replace does not

After a domain migration or HTTPS switch, “all plugin settings are gone” or “Elementor layouts are broken” is a common outcome. The cause, in most cases, is running a string replacement against the WordPress database without accounting for PHP serialized data.

WordPress stores plugin configurations, custom field values, and widget settings in PHP’s serialized format. Standard SQL replacements — phpMyAdmin’s find-and-replace, raw UPDATE statements, sed on a .sql dump — rewrite the string value without updating the length metadata that serialization embeds alongside it. The result is a database that appears intact but returns false on every read of the affected values.

wp search-replace handles this correctly. Understanding why makes the pre- and post-execution steps more deliberate.

What PHP serialization stores alongside the value

A serialized entry in WordPress looks like this:

a:2:{s:4:"home";s:22:"http://example.com/top";s:5:"title";s:8:"My Site";}

The segment s:22:"http://example.com/top" means “a string of 22 bytes.” The s:N: prefix records the byte length.

When a simple string replacement changes http://example.com to https://example.com:

  • Before: s:22:"http://example.com/top" (22 bytes)
  • After: s:22:"https://example.com/top" (23 bytes)

The s:22 stays unchanged even though the actual string is now 23 bytes. PHP’s unserialize() detects this mismatch and returns false. The plugin reads false instead of its configuration array and behaves as though the settings were never saved.

phpMyAdmin’s find-and-replace executes a SQL UPDATE at the storage layer. No PHP context exists there — it can’t know the column contains serialized data, and it doesn’t adjust the length prefix.

How wp search-replace handles it

wp search-replace operates at the PHP layer, not the SQL layer:

  1. Reads each column value
  2. Checks whether it’s serialized using is_serialized()
  3. If serialized: calls unserialize() to expand it into a PHP array or object
  4. Applies the string replacement to each value within the expanded structure
  5. Calls serialize() on the result, recalculating all length prefixes
  6. Writes the corrected value back

The length prefix is recalculated at step 5, so s:N: always reflects the actual byte count after replacement. The distinction is the same as the difference between overwriting a file directly versus going through an application that understands the file’s format.

Basic usage

# Backup before anything else — no exceptions
wp db export before-migration-$(date +%Y%m%d).sql

# Dry run: count how many rows would change, without writing anything
wp search-replace 'http://example.com' 'https://example.com' --dry-run

# Execute
wp search-replace 'http://example.com' 'https://example.com'

The --dry-run flag runs the full replacement logic internally but rolls it back, printing only the count of affected rows. Always run this first.

Scoping to specific tables

By default, every table in the database is scanned. For targeted replacements:

# Posts and custom fields only
wp search-replace 'http://example.com' 'https://example.com' wp_posts wp_postmeta

# Options table only (plugin settings, site URL, etc.)
wp search-replace 'http://example.com' 'https://example.com' wp_options

Running table by table is also easier to reason about when something goes wrong — you know exactly which scope was in progress.

The --skip-columns=guid case

The guid column in wp_posts is WordPress’s internal unique identifier for each post. It’s used as the item ID in RSS feeds. Changing guid causes RSS readers to re-import previously-seen posts as new entries.

wp search-replace 'http://example.com' 'https://example.com' --skip-columns=guid

For a new site or one that doesn’t publish RSS, this can be omitted. For an existing site switching to HTTPS with RSS subscribers, include it.

Verifying the result

# Confirm the site URL options are updated
wp option get home
wp option get siteurl

# Confirm nothing was missed — should return 0 replacements
wp search-replace 'http://example.com' 'https://example.com' --dry-run

A --dry-run returning 0 confirms the replacement is complete.

Summary

Method Serialization-safe Audit trail
phpMyAdmin find-and-replace ❌ Corrupts data None
Raw SQL UPDATE ❌ Corrupts data None
wp search-replace ✅ Safe SSH log

If plugin settings are already corrupted from a phpMyAdmin replacement, restore the pre-migration backup with wp db import, then re-run the replacement with wp search-replace. There is no reliable way to repair corrupted serialized data in place — the backup taken before execution is the only recovery path.

For WP-CLI startup issues that can block this command from running, the --skip-plugins --skip-themes rescue method covers how to start WP-CLI when a broken plugin prevents normal startup. If the issue is being locked out of wp-admin entirely rather than WP-CLI itself, the lockout recovery guide covers password resets, plugin self-blocks, and missing administrator accounts.