.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; } }