Skip to content

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 export and 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=0 and SQL_MODE='NO_AUTO_VALUE_ON_ZERO'.
  • For each table: DROP TABLE IF EXISTS, the CREATE TABLE statement as MySQL reports it, then INSERT statements kept under about 1 MB each.
  • NULL stays NULL, 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, enum and set columns 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.
  • --tables limits the run; the default is every table with this site's prefix.
  • --dry-run reports the number of rows that would change and writes nothing.
  • --case-insensitive matches without regard to case.
  • After a real run the object cache is flushed and the bb_cache_purge_all action 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:

  1. Backup. The current database is exported to the storage folder as …-backup-before-import-…sql.gz, unless --skip-backup.
  2. 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.
  3. Swap. One RENAME TABLE puts the new tables in place. See Temp tables and the atomic rename.
  4. Find and replace on the swapped tables.
  5. Housekeeping. Preserved options are written back, the object cache is flushed, rewrite_rules is deleted so it is rebuilt on the next request, bb_cache_purge_all and bb_migrate_imported fire.

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#

  1. Handshake, then the remote returns its export plan (tables and row counts) and the SQL header.
  2. The sender requests each table in chunks and appends them to a .sql.gz file 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.
  3. The file is imported here like any other, with the backup first, in strict mode.

The remote needs allow_pull.

Push#

  1. Handshake.
  2. 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.
  3. The remote captures its preserved options and opens a receiving job. Stale temp tables are dropped.
  4. 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_request value the remote reported in the handshake.
  5. 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's allow_code_files switch 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 EXISTS on a _bbmig_ table.
  • Create a temp table: CREATE TABLE on a _bbmig_ table, with a column list. The body may not contain a SELECT, 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 after VALUES is numbers, hex literals, NULL, quoted strings and punctuation. An INSERT … SELECT is 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 after keep_files_hours like 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 import it also renames the table in REPLACE INTO, ALTER TABLE, LOCK TABLES and TRUNCATE TABLE statements 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.