Database#
Four operations share one exporter and one importer:
| Operation | Admin tab | WP-CLI | Data flow |
|---|---|---|---|
| Export | Export | wp bb-migrate export |
This database → a .sql.gz file |
| Import | Import | wp bb-migrate import <file> |
A .sql or .sql.gz file → this database |
| Find and replace | Find & Replace | wp bb-migrate replace |
In place, in this database |
| Push / pull | Push, Pull | wp bb-migrate push, pull |
This database → remote, or remote → this database |
Export#
An export writes a gzipped SQL file into the storage folder, or to --file=<path>
on the command line (gzipped when the path ends in .gz, plain otherwise).
wp bb-migrate export --exclude-transients --exclude-spam
wp bb-migrate export --file=/tmp/site.sql.gz --replace="https://example.com=>https://staging.example.com"
What the file contains#
- A header comment
-- BB Migrate SQL exportand a-- meta: {…}line with this site's home URL, site URL, install path, uploads path, table prefix, WordPress and plugin versions, the date and any replacement pairs applied. An import reads that line to propose find and replace pairs. - Three session statements: the character set,
FOREIGN_KEY_CHECKS=0andSQL_MODE='NO_AUTO_VALUE_ON_ZERO'. - For each table:
DROP TABLE IF EXISTS, theCREATE TABLEstatement as MySQL reports it, thenINSERTstatements kept under about 1 MB each. NULLstaysNULL, numeric columns are written unquoted, binary and blob columns as hex literals, everything else as an escaped string.
Rows are read in chunks of chunk_rows (500 by default). A table with a single integer primary key is paged by key;
any other table by LIMIT … OFFSET.
Table selection#
| Option | CLI flag | Effect |
|---|---|---|
| Default | none | Every base table whose name starts with this site's prefix |
| Named tables | --tables=a,b |
Only those tables |
| All tables | --all-tables |
Also tables without this site's prefix. Local exports only; a remote site only ever exposes its prefixed tables |
| Exclude transients | --exclude-transients |
Skips _transient_… and _site_transient_… rows in the options table |
| Exclude spam | --exclude-spam |
Skips comments marked as spam |
| Exclude revisions | --exclude-revisions |
Skips posts of type revision |
| Rename prefix | --prefix=<prefix> |
Writes the tables under another prefix |
Always left out, whatever you select:
- Options named
bb_migrate_…: this site's key, switches, saved connections and job records. - The plugin's own
_bbmig_temp tables. - Views (only base tables are listed).
Prefix rename
--prefix renames tables in the output. It does not rewrite option names or user-meta keys that embed the
prefix, such as <prefix>user_roles and <prefix>capabilities. The same applies to a push to a remote with a
different prefix, which exports under the remote's prefix automatically.
Find and replace while exporting#
--replace (or the box on the Export tab) applies the pairs to every string value as it is written, so the file is
ready for its destination. The live database is not changed.
Find and replace#
wp bb-migrate replace --replace="https://example.com=>https://staging.example.com" --dry-run
wp bb-migrate replace --replace="https://example.com=>https://staging.example.com||/var/www/old/=>/var/www/new/"
Pairs are written from=>to, separated by || on the command line or one per line in the admin screen.
Serialized-safe#
WordPress stores arrays and objects as PHP-serialized text, where each string carries its byte length:
s:19:"https://example.com";. Replacing text inside without fixing the length corrupts the value. BB Migrate does
not unserialize anything. It walks the serialized text, replaces inside each s:N:"…"; token and writes the corrected
length back, which means:
- objects of any class survive, whether or not the class exists on this site,
- nested arrays and strings that are themselves serialized (double-serialized values) are handled by recursion,
- class names in object headers are never altered,
- a value that only looks serialized, and does not parse, is treated as plain text.
For each pair you give, two more forms are replaced automatically:
| Form | Example | Added when |
|---|---|---|
| JSON-escaped | https:\/\/example.com |
the search string contains / |
| URL-encoded | https%3A%2F%2Fexample.com |
the search string contains :// |
tests/replace-test.php checks these cases without WordPress: php tests/replace-test.php.
How the in-place replace runs#
- Only text columns are searched. Numeric, binary and blob, date and time,
enumandsetcolumns are skipped. - Rows are pre-filtered with
LIKE, then updated by primary key. A table without a primary key is updated by matching every column of the row. --tableslimits the run; the default is every table with this site's prefix.--dry-runreports the number of rows that would change and writes nothing.--case-insensitivematches without regard to case.- After a real run the object cache is flushed and the
bb_cache_purge_allaction fires.
The number reported is rows changed, not occurrences.
Import#
wp bb-migrate import /tmp/site.sql.gz
wp bb-migrate import /tmp/site.sql.gz --replace="https://example.com=>http://localhost:8080" --skip-backup
An import replaces this site's tables with the file's. In order:
- Backup. The current database is exported to the storage folder as
…-backup-before-import-…sql.gz, unless--skip-backup. - Load. The file is read as a stream and split into statements (quotes, escapes and comments are honoured).
Table names are rewritten to the
_bbmig_prefix, so nothing touches the live tables yet. - Swap. One
RENAME TABLEputs the new tables in place. See Temp tables and the atomic rename. - Find and replace on the swapped tables.
- Housekeeping. Preserved options are written back, the object cache is flushed,
rewrite_rulesis deleted so it is rebuilt on the next request,bb_cache_purge_allandbb_migrate_importedfire.
In the admin screen, upload the file on the Import tab (or place a larger one in the storage folder by SFTP), choose it from the list, and run. Files with a BB Migrate header show where they came from and prefill the pairs.
Replacement pairs on import#
- With
--replace, exactly those pairs are used. - Without it, and when the file has a BB Migrate header, the pairs are derived from the header: the source's home URL, uploads path and install path are replaced by this site's. The pairs are printed before the import starts.
- A file without a header gets no automatic pairs.
What the destination keeps#
After the swap these options are restored to the values the destination had before: active_plugins,
bb_migrate_settings, bb_cache_settings, blog_public, wp_environment_type. --skip-preserve keeps only
bb_migrate_settings. Details and the filter are in
Concepts.
A file you import by hand is not checked
The statement allow-list described below applies to SQL that arrives from another site over the wire. A local
file passed to import is executed as it is, apart from the table renaming. Import only files you trust.
Push and pull#
Push and pull are an export on one site and an import on the other, streamed in chunks.
wp bb-migrate pull "https://staging.example.com/wp-json/bb-migrate/v1 <key>"
wp bb-migrate push "https://staging.example.com/wp-json/bb-migrate/v1 <key>" --exclude-transients
Pull#
- Handshake, then the remote returns its export plan (tables and row counts) and the SQL header.
- The sender requests each table in chunks and appends them to a
.sql.gzfile in its storage folder. The chunk size adapts: it halves after a chunk over about 700 KB and doubles after one under about 200 KB, between 25 and 5000 rows. - The file is imported here like any other, with the backup first, in strict mode.
The remote needs allow_pull.
Push#
- Handshake.
- Pre-push backup. The sender asks the remote to export its current database into its own storage folder and steps that job until it is done.
- The remote captures its preserved options and opens a receiving job. Stale temp tables are dropped.
- The sender exports each table, under the remote's table prefix, and posts the SQL in chunks. The remote
executes each chunk into temp tables in strict mode. The chunk size adapts to the
max_requestvalue the remote reported in the handshake. - Finalize: the remote swaps the tables, runs find and replace, restores its preserved options and flushes caches.
The remote needs allow_push. If anything fails, the sender asks the remote to abort, which drops the temp tables.
Options#
| Option | CLI flag | Applies to | Effect |
|---|---|---|---|
| Replacement pairs | --replace=<pairs> |
pull, push | Replaces the automatic pairs |
| Tables | --tables=<list> |
pull, push | Only these tables. For a pull the names are matched against the remote's tables |
| Exclusions | --exclude-transients, --exclude-spam, --exclude-revisions |
pull, push | As for export |
| Skip backup | --skip-backup |
pull, push | No backup of the destination database |
| Skip preserve | --skip-preserve |
pull, push | The destination takes the source's active plugins and BB settings, within the limits below |
| Confirm | --yes |
pull, push | Skips the prompt; required for a pull into a site that is not local |
Automatic replacement pairs#
When no pairs are given, they are built from the handshake, source to destination:
| Pair | Notes |
|---|---|
| Home URL | For example https://example.com → https://staging.example.com |
| Protocol-relative home URL | //example.com → //staging.example.com |
| Uploads directory | With a trailing slash |
Install path (ABSPATH) |
With a trailing slash |
Path pairs are skipped when the source path is shorter than six characters, and any pair is skipped when both sides are equal. On a push the pairs are sent with the finalize request and applied by the remote after the swap. The pairs in use are written to the job log.
What is preserved on the receiver#
The same five options as for an import. With "skip preserve" on a push, the receiver still keeps:
bb_migrate_settings, always. A push cannot replace the receiver's key or switches.active_plugins, unless the receiver'sallow_code_filesswitch is on.
Because options named bb_migrate_… are never exported, the sender's key, saved connections and job records do not
reach the receiver either.
Strict mode and the SQL allow-list#
SQL from another site is data from another site. In strict mode, every statement is checked before it runs and must be one of the forms this plugin's own exporter writes. In plain terms:
- Drop a temp table:
DROP TABLE IF EXISTSon a_bbmig_table. - Create a temp table:
CREATE TABLEon a_bbmig_table, with a column list. The body may not contain aSELECT, a file function or clause (LOAD_FILE,OUTFILE,DUMPFILE,DIRECTORY), a remote or merged storage definition (FEDERATED,CONNECTION,MERGE,UNION,TABLESPACE,ENCRYPTION), a trigger, procedure or function, a comment, or a variable. - Insert literal rows into a temp table:
INSERT INTO … (columns) VALUES …, where everything afterVALUESis numbers, hex literals,NULL, quoted strings and punctuation. AnINSERT … SELECTis refused. - Session header lines: the character set, foreign key and unique checks, SQL mode and time zone settings a dump starts and ends with.
Anything else is refused and the transfer stops with Refused statement from remote. The check runs on a copy of the
statement in which every quoted string has been blanked, so the content of your posts cannot make a statement look
allowed or forbidden, and a statement may not contain a second statement. Versioned comments (/*!… */) are accepted
only in the two exact header forms the exporter writes.
Strict mode applies to the chunks a receiver gets during a push and, since 0.2.2, to the dump a sender downloads during a pull.
The pre-push backup#
- It is a full export of the receiver's prefixed tables, labelled
backup-before-push, in the receiver's storage folder. It is deleted afterkeep_files_hourslike any other file there. - Its file name is never sent to the pushing site. Find it on the receiver's Export or Import tab, or on disk.
- A receiver starts at most one such backup every five minutes. A second push inside that window is refused with
A backup started here less than five minutes ago; wait before pushing again.Wait, or push with--skip-backup. - To restore, import the backup file on the receiver.
Limits#
- A remote pull sees only the remote's prefixed tables, but all of them, including the users table.
- A pull or push with a different table prefix renames tables only (see the note under Export).
- The importer is written for the dumps this plugin produces. For a file given to
importit also renames the table inREPLACE INTO,ALTER TABLE,LOCK TABLESandTRUNCATE TABLEstatements to the temp prefix; any other statement in the file runs unchanged. - Large tables take as many requests as they need. The limits that matter are per request: see Hosting notes.