.schema (kind db-schema) and
* db/
.jsonl (kind db-rows). Rows go to a work file in the job folder first, which is then added and deleted.
*
* Crash safety: the writer state and this cursor are saved together. A finished table's work file is deleted only at
* the start of the next slice, once the cursor that records the table as added has been saved; the files finished in
* the last slice are removed by the write-files step.
*
* @package Mench_Migrate
*/
defined( 'ABSPATH' ) || exit;
// phpcs:disable WordPress.WP.AlternativeFunctions -- Streams backups, uploads and job files (often several GB) chunk by chunk inside the plugin's own storage folder and the site being restored; WP_Filesystem has no streaming or locking API.
class Mench_Migrate_Step_Export_Db extends Mench_Migrate_Step_Export_Base {
public function label(): string {
return __( 'Exporting the database', 'mench-migrate' );
}
public function run( Mench_Migrate_Job $job, float $deadline ): Mench_Migrate_Step_Result {
global $wpdb;
if ( empty( $job->data['options']['include']['db'] ) ) {
return Mench_Migrate_Step_Result::done( [ 'db' => [ 'tables' => 0, 'rows' => 0 ] ] );
}
$c = $job->cursor + [ 'tables' => null, 'est' => 0, 'i' => 0, 'phase' => 'rows', 'rows_cursor' => null, 'table_rows' => 0, 'schema' => false, 'add' => [], 'rows' => 0, 'delete' => [] ];
foreach ( (array) $c['delete'] as $f ) {
$path = self::db_dir( $job ) . '/' . basename( (string) $f );
is_file( $path ) && unlink( $path );
}
$c['delete'] = [];
$exporter = new Mench_Migrate_Db_Exporter( $wpdb, $wpdb->prefix );
if ( null === $c['tables'] ) {
$c['tables'] = $exporter->tables();
$c['est'] = self::estimate_rows( $c['tables'] );
foreach ( $exporter->skipped() as $view ) {
$job->log( 'warning', 'Skipped view ' . $view . '.' );
}
}
$dir = self::db_dir( $job );
if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) {
throw new Mench_Migrate_Storage_Exception( 'Cannot create the database work folder.', 'export-db' );
}
$writer = null;
$count = count( $c['tables'] );
$est = (int) ( $c['est'] ?? 0 );
$report = static function ( int $table_rows ) use ( $job, &$c, $count, $est ): void {
$rows = (int) $c['rows'] + $table_rows;
Mench_Migrate_Progress::report( $job, [
'counts' => [ 'tables_done' => (int) $c['i'], 'tables_total' => $count, 'rows_done' => $rows ],
'fraction' => $est > 0 ? min( 0.99, $rows / $est ) : (int) $c['i'] / max( 1, $count ),
] );
};
$report( 'rows' === $c['phase'] ? (int) ( $c['rows_cursor']['rows'] ?? 0 ) : (int) $c['table_rows'] );
while ( $c['i'] < $count ) {
$table = (string) $c['tables'][ $c['i'] ];
$work = sprintf( '%05d.jsonl', $c['i'] ); // Never derived from the table name, so it cannot leave the folder.
$file = $dir . '/' . $work;
if ( 'rows' === $c['phase'] ) {
$res = $exporter->export_rows( $table, $file, $c['rows_cursor'], $deadline, $table === $wpdb->options ? [ self::class, 'keep_option' ] : null, $report );
Mench_Migrate_Job_Runner::heartbeat( $job );
if ( ! $res['done'] ) {
$c['rows_cursor'] = $res['cursor'];
break;
}
$c['table_rows'] = $res['rows'];
$c['phase'] = 'add';
if ( microtime( true ) >= $deadline ) {
break;
}
}
$writer = $writer ?? self::writer( $job );
if ( ! $c['schema'] ) {
$writer->add_string( 'db/' . $table . '.schema', $exporter->schema( $table ), 'db-schema' );
$c['schema'] = true;
}
$res = $writer->add_file( $file, 'db/' . $table . '.jsonl', $c['add'], $deadline, 'db-rows' );
$c['add'] = $res['cursor'];
Mench_Migrate_Job_Runner::heartbeat( $job );
if ( ! $res['done'] ) {
break;
}
$c['rows'] += $c['table_rows'];
$c['delete'][] = $work;
$c['i']++;
$c['phase'] = 'rows';
$c['rows_cursor'] = null;
$c['table_rows'] = 0;
$c['schema'] = false;
$c['add'] = [];
$report( 0 );
if ( microtime( true ) >= $deadline ) {
break;
}
}
if ( null !== $writer ) {
$job->data['writer'] = $writer->state();
unset( $writer );
}
if ( $c['i'] < $count ) {
return Mench_Migrate_Step_Result::more( $c, $est > 0 ? min( 0.99, $c['rows'] / $est ) : $c['i'] / max( 1, $count ) );
}
// The last slice's work files stay until this result is saved; the write-files step removes the folder first.
$job->log( 'info', 'Exported ' . $count . ' tables, ' . $c['rows'] . ' rows.' );
return Mench_Migrate_Step_Result::done( [ 'db' => [ 'tables' => $count, 'rows' => $c['rows'] ] ] );
}
/** The server's row estimate for these tables (information_schema), only to size the progress bar. */
private static function estimate_rows( array $tables ): int {
global $wpdb;
if ( ! $tables ) {
return 0;
}
$in = implode( ',', array_fill( 0, count( $tables ), '%s' ) );
$sum = $wpdb->get_var( $wpdb->prepare( "SELECT SUM(TABLE_ROWS) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ($in)", $tables ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.DirectDatabaseQuery
return max( 0, (int) $sum );
}
/**
* Leaves every Mench Migrate transient (and its timeout) out of the archive: the password stash, and site-specific
* state such as the backup protection probe, which would make the target show the source's result.
*/
public static function keep_option( array $row ): bool {
$name = (string) ( $row['option_name'] ?? '' );
foreach ( [ '_transient_', '_transient_timeout_', '_site_transient_', '_site_transient_timeout_' ] as $prefix ) {
if ( 0 === strpos( $name, $prefix . 'mench_migrate_' ) ) {
return false;
}
}
return true;
}
}