Merge pull request #32938 from woocommerce/cot/32917
Add CLI support to run migrations.
This commit is contained in:
commit
b8e2de8520
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
CLI support for running COT migrations (one way).
|
|
@ -6,6 +6,8 @@
|
|||
* @version 3.0.0
|
||||
*/
|
||||
|
||||
use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
|
@ -39,6 +41,8 @@ class WC_CLI {
|
|||
WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Tool_Command::register_commands' );
|
||||
WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Update_Command::register_commands' );
|
||||
WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Tracker_Command::register_commands' );
|
||||
$cli_runner = wc_get_container()->get( CLIRunner::class );
|
||||
WP_CLI::add_hook( 'after_wp_load', array( $cli_runner, 'register_commands' ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,521 @@
|
|||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
use WP_CLI;
|
||||
|
||||
/**
|
||||
* CLI tool for migrating order data to/from custom table.
|
||||
*
|
||||
* Credits https://github.com/liquidweb/woocommerce-custom-orders-table/blob/develop/includes/class-woocommerce-custom-orders-table-cli.php.
|
||||
*
|
||||
* Class CLIRunner
|
||||
*/
|
||||
class CLIRunner {
|
||||
|
||||
/**
|
||||
* CustomOrdersTableController instance.
|
||||
*
|
||||
* @var CustomOrdersTableController
|
||||
*/
|
||||
private $controller;
|
||||
|
||||
/**
|
||||
* DataSynchronizer instance.
|
||||
*
|
||||
* @var DataSynchronizer;
|
||||
*/
|
||||
private $synchronizer;
|
||||
|
||||
/**
|
||||
* PostsToOrdersMigrationController instance.
|
||||
*
|
||||
* @var PostsToOrdersMigrationController
|
||||
*/
|
||||
private $post_to_cot_migrator;
|
||||
|
||||
/**
|
||||
* Init method, invoked by DI container.
|
||||
*
|
||||
* @param CustomOrdersTableController $controller Instance.
|
||||
* @param DataSynchronizer $synchronizer Instance.
|
||||
* @param PostsToOrdersMigrationController $posts_to_orders_migration_controller Instance.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final public function init( CustomOrdersTableController $controller, DataSynchronizer $synchronizer, PostsToOrdersMigrationController $posts_to_orders_migration_controller ) {
|
||||
$this->controller = $controller;
|
||||
$this->synchronizer = $synchronizer;
|
||||
$this->post_to_cot_migrator = $posts_to_orders_migration_controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers commands for CLI.
|
||||
*/
|
||||
public function register_commands() {
|
||||
WP_CLI::add_command( 'wc cot count_unmigrated', array( $this, 'count_unmigrated' ) );
|
||||
WP_CLI::add_command( 'wc cot migrate', array( $this, 'migrate' ) );
|
||||
WP_CLI::add_command( 'wc cot verify_cot_data', array( $this, 'verify_cot_data' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the COT feature is enabled.
|
||||
*
|
||||
* @param bool $log Optionally log a error message.
|
||||
*
|
||||
* @return bool Whether the COT feature is enabled.
|
||||
*/
|
||||
private function is_enabled( $log = true ) : bool {
|
||||
if ( ! $this->controller->is_feature_visible() ) {
|
||||
if ( $log ) {
|
||||
WP_CLI::log(
|
||||
sprintf(
|
||||
// translators: %s - link to testing instructions webpage.
|
||||
__( 'Custom order table usage is not enabled. If you are testing, you can enable it by following the testing instructions in %s', 'woocommerce' ),
|
||||
'https://developer.woocommerce.com/' // TODO: Change the link when testing instructin page is live.
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->controller->is_feature_visible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to log warning that feature is not yet production ready.
|
||||
*/
|
||||
private function log_production_warning() {
|
||||
WP_CLI::log( __( 'This feature is not production ready yet. Make sure you are not running these commands in your production environment.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many orders have yet to be migrated into the custom orders table.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp wc cot count_unmigrated
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
*
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*
|
||||
* @return int The number of orders to be migrated.*
|
||||
*/
|
||||
public function count_unmigrated( $args = array(), $assoc_args = array() ) : int {
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$order_count = $this->synchronizer->get_current_orders_pending_sync_count();
|
||||
|
||||
$assoc_args = wp_parse_args(
|
||||
$assoc_args,
|
||||
array(
|
||||
'log' => true,
|
||||
)
|
||||
);
|
||||
if ( isset( $assoc_args['log'] ) && $assoc_args['log'] ) {
|
||||
WP_CLI::log(
|
||||
sprintf(
|
||||
/* Translators: %1$d is the number of orders to be migrated. */
|
||||
_n( 'There is %1$d order to be migrated.', 'There are %1$d orders to be migrated.', $order_count, 'woocommerce' ),
|
||||
$order_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (int) $order_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate order data to the custom orders table.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--batch-size=<batch-size>]
|
||||
* : The number of orders to process in each batch.
|
||||
* ---
|
||||
* default: 500
|
||||
* ---
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp wc cot migrate --batch-size=500
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function migrate( $args = array(), $assoc_args = array() ) {
|
||||
$this->log_production_warning();
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->synchronizer->custom_orders_table_is_authoritative() ) {
|
||||
return WP_CLI::error( __( 'Migration is not yet supported when custom tables are authoritative. Switch to post tables as authoritative source if you are testing.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$order_count = $this->count_unmigrated();
|
||||
|
||||
// Abort if there are no orders to migrate.
|
||||
if ( ! $order_count ) {
|
||||
return WP_CLI::warning( __( 'There are no orders to migrate, aborting.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$assoc_args = wp_parse_args(
|
||||
$assoc_args,
|
||||
array(
|
||||
'batch-size' => 500,
|
||||
)
|
||||
);
|
||||
$batch_size = ( (int) $assoc_args['batch-size'] ) === 0 ? 500 : (int) $assoc_args['batch-size'];
|
||||
$progress = WP_CLI\Utils\make_progress_bar( 'Order Data Migration', $order_count / $batch_size );
|
||||
$processed = 0;
|
||||
$batch_count = 1;
|
||||
$total_time = 0;
|
||||
|
||||
while ( $order_count > 0 ) {
|
||||
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
/* Translators: %1$d is the batch number, %2$d is the batch size. */
|
||||
__( 'Beginning batch #%1$d (%2$d orders/batch).', 'woocommerce' ),
|
||||
$batch_count,
|
||||
$batch_size
|
||||
)
|
||||
);
|
||||
$batch_start_time = microtime( true );
|
||||
$order_ids = $this->synchronizer->get_ids_of_orders_pending_sync( $this->synchronizer::ID_TYPE_MISSING_IN_ORDERS_TABLE, $batch_size );
|
||||
if ( count( $order_ids ) ) {
|
||||
$this->post_to_cot_migrator->migrate_orders( $order_ids );
|
||||
}
|
||||
$processed += count( $order_ids );
|
||||
$batch_total_time = microtime( true ) - $batch_start_time;
|
||||
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
// Translators: %1$d is the batch number, %2$f is time taken to process batch.
|
||||
__( 'Batch %1$d (%2$d orders) completed in %3$f seconds', 'woocommerce' ),
|
||||
$batch_count,
|
||||
count( $order_ids ),
|
||||
$batch_total_time
|
||||
)
|
||||
);
|
||||
|
||||
$batch_count ++;
|
||||
$total_time += $batch_total_time;
|
||||
|
||||
$progress->tick();
|
||||
|
||||
$remaining_count = $this->count_unmigrated( array(), array( 'log' => false ) );
|
||||
if ( $remaining_count === $order_count ) {
|
||||
return WP_CLI::error( __( 'Infinite loop detected, aborting.', 'woocommerce' ) );
|
||||
}
|
||||
$order_count = $remaining_count;
|
||||
}
|
||||
|
||||
$progress->finish();
|
||||
|
||||
// Issue a warning if no orders were migrated.
|
||||
if ( ! $processed ) {
|
||||
return WP_CLI::warning( __( 'No orders were migrated.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
WP_CLI::log( __( 'Migration completed.', 'woocommerce' ) );
|
||||
|
||||
return WP_CLI::success(
|
||||
sprintf(
|
||||
/* Translators: %1$d is the number of migrated orders. */
|
||||
_n( '%1$d order was migrated, in %2$f seconds', '%1$d orders were migrated in %2$f seconds', $processed, 'woocommerce' ),
|
||||
$processed,
|
||||
$total_time
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy order data into the postmeta table.
|
||||
*
|
||||
* Note that this could dramatically increase the size of your postmeta table, but is recommended
|
||||
* if you wish to stop using the custom orders table plugin.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--batch-size=<batch-size>]
|
||||
* : The number of orders to process in each batch. Passing a value of 0 will disable batching.
|
||||
* ---
|
||||
* default: 500
|
||||
* ---
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Copy all order data into the post meta table, 500 posts at a time.
|
||||
* wp wc cot backfill --batch-size=500
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function backfill( $args = array(), $assoc_args = array() ) {
|
||||
return WP_CLI::error( __( 'Error: Backfill is not implemented yet.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify migrated order data with original posts data.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--batch-size=<batch-size>]
|
||||
* : The number of orders to verify in each batch.
|
||||
* ---
|
||||
* default: 500
|
||||
* ---
|
||||
*
|
||||
* [--start-from=<order_id>]
|
||||
* : Order ID to start from.
|
||||
* ---
|
||||
* default: 0
|
||||
* ---
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Verify migrated order data, 500 orders at a time.
|
||||
* wp wc cot verify_cot_data --batch-size=500 --start-from=0
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function verify_cot_data( $args = array(), $assoc_args = array() ) {
|
||||
global $wpdb;
|
||||
$this->log_production_warning();
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$assoc_args = wp_parse_args(
|
||||
$assoc_args,
|
||||
array(
|
||||
'batch-size' => 500,
|
||||
'start-from' => 0,
|
||||
)
|
||||
);
|
||||
|
||||
$batch_count = 1;
|
||||
$total_time = 0;
|
||||
$failed_ids = array();
|
||||
$processed = 0;
|
||||
$order_id_start = (int) $assoc_args['start-from'];
|
||||
$order_count = $this->get_verify_order_count( $order_id_start );
|
||||
$batch_size = ( (int) $assoc_args['batch-size'] ) === 0 ? 500 : (int) $assoc_args['batch-size'];
|
||||
|
||||
$progress = WP_CLI\Utils\make_progress_bar( 'Order Data Verification', $order_count / $batch_size );
|
||||
|
||||
if ( ! $order_count ) {
|
||||
return WP_CLI::warning( __( 'There are no orders to verify, aborting.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
while ( $order_count > 0 ) {
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
/* Translators: %1$d is the batch number, %2$d is the batch size. */
|
||||
__( 'Beginning verification for batch #%1$d (%2$d orders/batch).', 'woocommerce' ),
|
||||
$batch_count,
|
||||
$batch_size
|
||||
)
|
||||
);
|
||||
|
||||
$order_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order' AND ID > %d ORDER BY ID ASC LIMIT %d",
|
||||
$order_id_start,
|
||||
$batch_size
|
||||
)
|
||||
);
|
||||
$batch_start_time = microtime( true );
|
||||
$failed_ids = $failed_ids + $this->post_to_cot_migrator->verify_migrated_orders( $order_ids );
|
||||
$failed_ids = $this->verify_meta_data( $order_ids, $failed_ids );
|
||||
$processed += count( $order_ids );
|
||||
$batch_total_time = microtime( true ) - $batch_start_time;
|
||||
$batch_count ++;
|
||||
$total_time += $batch_total_time;
|
||||
|
||||
$progress->tick();
|
||||
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
// Translators: %1$d is the batch number, %2$f is time taken to process batch.
|
||||
__( 'Batch %1$d (%2$d orders) completed in %3$f seconds', 'woocommerce' ),
|
||||
$batch_count,
|
||||
count( $order_ids ),
|
||||
$batch_total_time
|
||||
)
|
||||
);
|
||||
|
||||
$order_id_start = max( $order_ids );
|
||||
$remaining_count = $this->get_verify_order_count( $order_id_start, false );
|
||||
if ( $remaining_count === $order_count ) {
|
||||
return WP_CLI::error( __( 'Infinite loop detected, aborting. No errors found.', 'woocommerce' ) );
|
||||
}
|
||||
$order_count = $remaining_count;
|
||||
}
|
||||
|
||||
$progress->finish();
|
||||
WP_CLI::log( __( 'Verification completed.', 'woocommerce' ) );
|
||||
|
||||
if ( 0 === count( $failed_ids ) ) {
|
||||
return WP_CLI::success(
|
||||
sprintf(
|
||||
/* Translators: %1$d is the number of migrated orders and %2$f is time taken */
|
||||
_n( '%1$d order was verified, in %2$f seconds', '%1$d orders were verified in %2$f seconds', $processed, 'woocommerce' ),
|
||||
$processed,
|
||||
$total_time
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$errors = print_r( $failed_ids, true );
|
||||
|
||||
return WP_CLI::error(
|
||||
sprintf(
|
||||
/* Translators: %1$d is the number of migrated orders, %2$f is time taken, %3$d is number of errors and$4%s is formatted array of order ids. */
|
||||
_n( '%1$d order was verified, in %2$f seconds. %3$f error(s) found: %4$s', '%1$d orders were verified in %2$f seconds Please review above errros. %3$d error(s) found %4$s. Please review above errors.', $processed, 'woocommerce' ),
|
||||
$processed,
|
||||
$total_time,
|
||||
count( $failed_ids ),
|
||||
$errors
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get count for orders needing verification.
|
||||
*
|
||||
* @param int $order_id_start Order ID to start from.
|
||||
* @param bool $log Whether to also log an error message.
|
||||
*
|
||||
* @return int Order count.
|
||||
*/
|
||||
private function get_verify_order_count( int $order_id_start, $log = true ) : int {
|
||||
global $wpdb;
|
||||
|
||||
$order_count = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = 'shop_order' AND ID > %d",
|
||||
$order_id_start
|
||||
)
|
||||
);
|
||||
|
||||
if ( $log ) {
|
||||
WP_CLI::log(
|
||||
sprintf(
|
||||
/* Translators: %1$d is the number of orders to be verified. */
|
||||
_n( 'There is %1$d order to be verified.', 'There are %1$d orders to be verified.', $order_count, 'woocommerce' ),
|
||||
$order_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $order_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify meta data as part of verifying the order object.
|
||||
*
|
||||
* @param array $order_ids Order IDs.
|
||||
* @param array $failed_ids Array for storing failed IDs.
|
||||
*
|
||||
* @return array Failed IDs with meta details.
|
||||
*/
|
||||
private function verify_meta_data( array $order_ids, array $failed_ids ) : array {
|
||||
global $wpdb;
|
||||
if ( ! count( $order_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
$excluded_columns = $this->post_to_cot_migrator->get_migrated_meta_keys();
|
||||
$excluded_columns_placeholder = implode( ', ', array_fill( 0, count( $excluded_columns ), '%s' ) );
|
||||
$order_ids_placeholder = implode( ', ', array_fill( 0, count( $order_ids ), '%d' ) );
|
||||
$meta_table = OrdersTableDataStore::get_meta_table_name();
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- table names are hardcoded, orders_ids and excluded_columns are prepared.
|
||||
$query = $wpdb->prepare(
|
||||
"
|
||||
SELECT {$wpdb->postmeta}.post_id as entity_id, {$wpdb->postmeta}.meta_key, {$wpdb->postmeta}.meta_value
|
||||
FROM $wpdb->postmeta
|
||||
WHERE
|
||||
{$wpdb->postmeta}.post_id in ( $order_ids_placeholder ) AND
|
||||
{$wpdb->postmeta}.meta_key not in ( $excluded_columns_placeholder )
|
||||
ORDER BY {$wpdb->postmeta}.post_id ASC, {$wpdb->postmeta}.meta_key ASC;
|
||||
",
|
||||
array_merge(
|
||||
$order_ids,
|
||||
$excluded_columns
|
||||
)
|
||||
);
|
||||
$source_data = $wpdb->get_results( $query, ARRAY_A );
|
||||
// phpcs:enable
|
||||
|
||||
$normalized_source_data = $this->normalize_raw_meta_data( $source_data );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- table names are hardcoded, orders_ids and excluded_columns are prepared.
|
||||
$migrated_query = $wpdb->prepare(
|
||||
"
|
||||
SELECT $meta_table.order_id as entity_id, $meta_table.meta_key, $meta_table.meta_value
|
||||
FROM $meta_table
|
||||
WHERE
|
||||
$meta_table.order_id in ( $order_ids_placeholder )
|
||||
ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
|
||||
",
|
||||
$order_ids
|
||||
);
|
||||
$migrated_data = $wpdb->get_results( $migrated_query, ARRAY_A );
|
||||
// phpcs:enable
|
||||
|
||||
$normalized_migrated_meta_data = $this->normalize_raw_meta_data( $migrated_data );
|
||||
|
||||
foreach ( $normalized_source_data as $order_id => $meta ) {
|
||||
foreach ( $meta as $meta_key => $values ) {
|
||||
$migrated_meta_values = isset( $normalized_migrated_meta_data[ $order_id ][ $meta_key ] ) ? $normalized_migrated_meta_data[ $order_id ][ $meta_key ] : array();
|
||||
$diff = array_diff( $values, $migrated_meta_values );
|
||||
|
||||
if ( count( $diff ) ) {
|
||||
if ( ! isset( $failed_ids[ $order_id ] ) ) {
|
||||
$failed_ids[ $order_id ] = array();
|
||||
}
|
||||
$failed_ids[ $order_id ][] = array(
|
||||
'order_id' => $order_id,
|
||||
'meta_key' => $meta_key,
|
||||
'orig_meta_values' => $values,
|
||||
'new_meta_values' => $migrated_meta_values,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $failed_ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to normalize response from meta queries into order_id > meta_key > meta_values.
|
||||
*
|
||||
* @param array $data Data fetched from meta queries.
|
||||
*
|
||||
* @return array Normalized data.
|
||||
*/
|
||||
private function normalize_raw_meta_data( array $data ) : array {
|
||||
$clubbed_data = array();
|
||||
foreach ( $data as $row ) {
|
||||
if ( ! isset( $clubbed_data[ $row['entity_id'] ] ) ) {
|
||||
$clubbed_data[ $row['entity_id'] ] = array();
|
||||
}
|
||||
if ( ! isset( $clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ] ) ) {
|
||||
$clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ] = array();
|
||||
}
|
||||
$clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ][] = $row['meta_value'];
|
||||
}
|
||||
return $clubbed_data;
|
||||
}
|
||||
|
||||
}
|
|
@ -367,7 +367,7 @@ abstract class MetaToCustomTableMigrator {
|
|||
|
||||
$already_migrated_entity_ids = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded.
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded.
|
||||
"
|
||||
SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id
|
||||
FROM `$destination_table` destination
|
||||
|
@ -376,7 +376,7 @@ WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder )
|
|||
",
|
||||
$entity_ids
|
||||
)
|
||||
// phpcs:enable
|
||||
// phpcs:enable
|
||||
);
|
||||
|
||||
return array_column( $already_migrated_entity_ids, null, 'source_id' );
|
||||
|
@ -570,4 +570,225 @@ WHERE
|
|||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify whether data was migrated properly for given IDs.
|
||||
*
|
||||
* @param array $source_ids List of source IDs.
|
||||
*
|
||||
* @return array List of IDs along with columns that failed to migrate.
|
||||
*/
|
||||
public function verify_migrated_data( array $source_ids ) : array {
|
||||
global $wpdb;
|
||||
$query = $this->build_verification_query( $source_ids );
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query should already be prepared.
|
||||
$results = $wpdb->get_results( $query, ARRAY_A );
|
||||
|
||||
return $this->verify_data( $results );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate query to fetch data from both source and destination tables. Use the results in `verify_data` to verify if data was migrated properly.
|
||||
*
|
||||
* @param array $source_ids Array of IDs in source table.
|
||||
*
|
||||
* @return string SELECT statement.
|
||||
*/
|
||||
protected function build_verification_query( $source_ids ) {
|
||||
$source_table = $this->schema_config['source']['entity']['table_name'];
|
||||
$meta_table = $this->schema_config['source']['meta']['table_name'];
|
||||
$destination_table = $this->schema_config['destination']['table_name'];
|
||||
$meta_entity_id_column = $this->schema_config['source']['meta']['entity_id_column'];
|
||||
$meta_key_column = $this->schema_config['source']['meta']['meta_key_column'];
|
||||
$meta_value_column = $this->schema_config['source']['meta']['meta_value_column'];
|
||||
$destination_source_rel_column = $this->schema_config['destination']['source_rel_column'];
|
||||
$source_destination_rel_column = $this->schema_config['source']['entity']['destination_rel_column'];
|
||||
$source_meta_rel_column = $this->schema_config['source']['entity']['meta_rel_column'];
|
||||
|
||||
$source_destination_join_clause = "$destination_table ON $destination_table.$destination_source_rel_column = $source_table.$source_destination_rel_column";
|
||||
|
||||
$meta_select_clauses = array();
|
||||
$meta_join_clauses = array();
|
||||
$source_select_clauses = array();
|
||||
$destination_select_clauses = array();
|
||||
|
||||
foreach ( $this->core_column_mapping as $column_name => $schema ) {
|
||||
$source_select_column = isset( $schema['select_clause'] ) ? $schema['select_clause'] : "$source_table.$column_name";
|
||||
$source_select_clauses[] = "$source_select_column as {$source_table}_{$column_name}";
|
||||
$destination_select_clauses[] = "$destination_table.{$schema['destination']} as {$destination_table}_{$schema['destination']}";
|
||||
}
|
||||
|
||||
foreach ( $this->meta_column_mapping as $meta_key => $schema ) {
|
||||
$meta_table_alias = "meta_source_{$schema['destination']}";
|
||||
$meta_select_clauses[] = "$meta_table_alias.$meta_value_column AS $meta_table_alias";
|
||||
$meta_join_clauses[] = "
|
||||
$meta_table $meta_table_alias ON
|
||||
$meta_table_alias.$meta_entity_id_column = $source_table.$source_meta_rel_column AND
|
||||
$meta_table_alias.$meta_key_column = '$meta_key'
|
||||
";
|
||||
$destination_select_clauses[] = "$destination_table.{$schema['destination']} as {$destination_table}_{$schema['destination']}";
|
||||
}
|
||||
|
||||
$select_clause = implode( ', ', array_merge( $source_select_clauses, $meta_select_clauses, $destination_select_clauses ) );
|
||||
|
||||
$meta_join_clause = implode( ' LEFT JOIN ', $meta_join_clauses );
|
||||
|
||||
$where_clause = $this->get_where_clause_for_verification( $source_ids );
|
||||
|
||||
return "
|
||||
SELECT $select_clause
|
||||
FROM $source_table
|
||||
LEFT JOIN $source_destination_join_clause
|
||||
LEFT JOIN $meta_join_clause
|
||||
WHERE $where_clause
|
||||
";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to generate where clause for fetching data for verification.
|
||||
*
|
||||
* @param array $source_ids Array of IDs from source table.
|
||||
*
|
||||
* @return string WHERE clause.
|
||||
*/
|
||||
protected function get_where_clause_for_verification( $source_ids ) {
|
||||
global $wpdb;
|
||||
$source_primary_id_column = $this->schema_config['source']['entity']['primary_key'];
|
||||
$source_table = $this->schema_config['source']['entity']['table_name'];
|
||||
$source_ids_placeholder = implode( ', ', array_fill( 0, count( $source_ids ), '%d' ) );
|
||||
|
||||
return $wpdb->prepare(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Both $source_table and $source_primary_id_column is hardcoded.
|
||||
"$source_table.$source_primary_id_column IN ($source_ids_placeholder)",
|
||||
$source_ids
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify data from both source and destination tables and check if they were migrated properly.
|
||||
*
|
||||
* @param array $collected_data Collected data in array format, should be in same structure as returned from query in `$this->build_verification_query`.
|
||||
*
|
||||
* @return array Array of failed IDs if any, along with columns/meta_key names.
|
||||
*/
|
||||
protected function verify_data( $collected_data ) {
|
||||
$failed_ids = array();
|
||||
foreach ( $collected_data as $row ) {
|
||||
$failed_ids = $this->verify_entity_columns( $row, $failed_ids );
|
||||
$failed_ids = $this->verify_meta_columns( $row, $failed_ids );
|
||||
}
|
||||
|
||||
return $failed_ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to verify and compare core columns.
|
||||
*
|
||||
* @param array $row Both migrated and source data for a single row.
|
||||
* @param array $failed_ids Array of failed IDs.
|
||||
*
|
||||
* @return array Array of failed IDs if any, along with columns/meta_key names.
|
||||
*/
|
||||
private function verify_entity_columns( $row, $failed_ids ) {
|
||||
$primary_key_column = "{$this->schema_config['source']['entity']['table_name']}_{$this->schema_config['source']['entity']['primary_key']}";
|
||||
foreach ( $this->core_column_mapping as $column_name => $schema ) {
|
||||
$source_alias = "{$this->schema_config['source']['entity']['table_name']}_$column_name";
|
||||
$destination_alias = "{$this->schema_config['destination']['table_name']}_{$schema['destination']}";
|
||||
$row = $this->pre_process_row( $row, $schema, $source_alias, $destination_alias );
|
||||
if ( $row[ $source_alias ] !== $row[ $destination_alias ] ) {
|
||||
if ( ! isset( $failed_ids[ $row[ $primary_key_column ] ] ) ) {
|
||||
$failed_ids[ $row[ $primary_key_column ] ] = array();
|
||||
}
|
||||
$failed_ids[ $row[ $primary_key_column ] ][] = array(
|
||||
'column' => $column_name,
|
||||
'original_value' => $row[ $source_alias ],
|
||||
'new_value' => $row[ $destination_alias ],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $failed_ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to verify meta columns.
|
||||
*
|
||||
* @param array $row Both migrated and source data for a single row.
|
||||
* @param array $failed_ids Array of failed IDs.
|
||||
*
|
||||
* @return array Array of failed IDs if any, along with columns/meta_key names.
|
||||
*/
|
||||
private function verify_meta_columns( $row, $failed_ids ) {
|
||||
$primary_key_column = "{$this->schema_config['source']['entity']['table_name']}_{$this->schema_config['source']['entity']['primary_key']}";
|
||||
foreach ( $this->meta_column_mapping as $meta_key => $schema ) {
|
||||
$meta_alias = "meta_source_{$schema['destination']}";
|
||||
$destination_alias = "{$this->schema_config['destination']['table_name']}_{$schema['destination']}";
|
||||
$row = $this->pre_process_row( $row, $schema, $meta_alias, $destination_alias );
|
||||
if ( $row[ $meta_alias ] !== $row[ $destination_alias ] ) {
|
||||
if ( ! isset( $failed_ids[ $row[ $primary_key_column ] ] ) ) {
|
||||
$failed_ids[ $row[ $primary_key_column ] ] = array();
|
||||
}
|
||||
$failed_ids[ $row[ $primary_key_column ] ][] = array(
|
||||
'column' => $meta_key,
|
||||
'original_value' => $row[ $meta_alias ],
|
||||
'new_value' => $row[ $destination_alias ],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $failed_ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to pre-process rows to make sure we parse the correct type.
|
||||
*
|
||||
* @param array $row Both migrated and source data for a single row.
|
||||
* @param array $schema Column schema.
|
||||
* @param string $alias Name of source column.
|
||||
* @param string $destination_alias Name of destination column.
|
||||
*
|
||||
* @return array Processed row.
|
||||
*/
|
||||
private function pre_process_row( $row, $schema, $alias, $destination_alias ) {
|
||||
if ( in_array( $schema['type'], array( 'int', 'decimal' ) ) ) {
|
||||
$row[ $alias ] = wc_format_decimal( $row[ $alias ], false, true );
|
||||
$row[ $destination_alias ] = wc_format_decimal( $row[ $destination_alias ], false, true );
|
||||
}
|
||||
if ( 'bool' === $schema['type'] ) {
|
||||
$row[ $alias ] = wc_string_to_bool( $row[ $alias ] );
|
||||
$row[ $destination_alias ] = wc_string_to_bool( $row[ $destination_alias ] );
|
||||
}
|
||||
if ( 'date_epoch' === $schema['type'] ) {
|
||||
if ( '' === $row[ $alias ] || null === $row[ $alias ] ) {
|
||||
$row[ $alias ] = null;
|
||||
} else {
|
||||
$row[ $alias ] = ( new \DateTime( "@{$row[ $alias ]}" ) )->format( 'Y-m-d H:i:s' );
|
||||
}
|
||||
if ( '0000-00-00 00:00:00' === $row[ $destination_alias ] ) {
|
||||
$row[ $destination_alias ] = null;
|
||||
}
|
||||
}
|
||||
if ( is_null( $row[ $alias ] ) ) {
|
||||
$row[ $alias ] = $this->get_type_defaults( $schema['type'] );
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get default value of a type.
|
||||
*
|
||||
* @param string $type Type.
|
||||
*
|
||||
* @return mixed Default value.
|
||||
*/
|
||||
private function get_type_defaults( $type ) {
|
||||
switch ( $type ) {
|
||||
case 'float':
|
||||
case 'int':
|
||||
return 0;
|
||||
case 'string':
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,4 +189,18 @@ WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) AND dest
|
|||
|
||||
return array_column( $already_migrated_entity_ids, null, 'source_id' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to generate where clause for fetching data for verification.
|
||||
*
|
||||
* @param array $source_ids Array of IDs from source table.
|
||||
*
|
||||
* @return string WHERE clause.
|
||||
*/
|
||||
protected function get_where_clause_for_verification( $source_ids ) {
|
||||
global $wpdb;
|
||||
$query = parent::get_where_clause_for_verification( $source_ids );
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $query should already be prepared, $schema_config is hardcoded.
|
||||
return $wpdb->prepare( "$query AND {$this->schema_config['destination']['table_name']}.address_type = %s", $this->type );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,13 +67,23 @@ class PostsToOrdersMigrationController {
|
|||
$this->shipping_address_table_migrator = new PostToOrderAddressTableMigrator( 'shipping' );
|
||||
$this->operation_data_table_migrator = new PostToOrderOpTableMigrator();
|
||||
|
||||
$excluded_columns = array_keys( $this->order_table_migrator->get_meta_column_config() );
|
||||
$excluded_columns = array_merge( $excluded_columns, array_keys( $this->billing_address_table_migrator->get_meta_column_config() ) );
|
||||
$excluded_columns = array_merge( $excluded_columns, array_keys( $this->shipping_address_table_migrator->get_meta_column_config() ) );
|
||||
$excluded_columns = array_merge( $excluded_columns, array_keys( $this->operation_data_table_migrator->get_meta_column_config() ) );
|
||||
$excluded_columns = $this->get_migrated_meta_keys();
|
||||
|
||||
$this->meta_table_migrator = new PostMetaToOrderMetaMigrator( $excluded_columns );
|
||||
$this->error_logger = new MigrationErrorLogger();
|
||||
$this->meta_table_migrator = new PostMetaToOrderMetaMigrator( $excluded_columns );
|
||||
$this->error_logger = new MigrationErrorLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get keys to migrate for migrations.
|
||||
*
|
||||
* @return int[]|string[]
|
||||
*/
|
||||
public function get_migrated_meta_keys() : array {
|
||||
$migrated_keys = array_keys( $this->order_table_migrator->get_meta_column_config() );
|
||||
$migrated_keys = array_merge( $migrated_keys, array_keys( $this->billing_address_table_migrator->get_meta_column_config() ) );
|
||||
$migrated_keys = array_merge( $migrated_keys, array_keys( $this->shipping_address_table_migrator->get_meta_column_config() ) );
|
||||
$migrated_keys = array_merge( $migrated_keys, array_keys( $this->operation_data_table_migrator->get_meta_column_config() ) );
|
||||
return $migrated_keys;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,6 +100,20 @@ class PostsToOrdersMigrationController {
|
|||
// TODO: Return merged error array.
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify whether the given order IDs were migrated properly or not.
|
||||
*
|
||||
* @param array $order_post_ids Order IDs.
|
||||
*
|
||||
* @return array Array of failed IDs along with columns.
|
||||
*/
|
||||
public function verify_migrated_orders( array $order_post_ids ): array {
|
||||
return $this->order_table_migrator->verify_migrated_data( $order_post_ids ) +
|
||||
$this->billing_address_table_migrator->verify_migrated_data( $order_post_ids ) +
|
||||
$this->shipping_address_table_migrator->verify_migrated_data( $order_post_ids ) +
|
||||
$this->operation_data_table_migrator->verify_migrated_data( $order_post_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates an order from the posts table to the custom orders tables.
|
||||
*
|
||||
|
|
|
@ -53,7 +53,7 @@ class DataSynchronizer {
|
|||
/**
|
||||
* The posts to COT migrator to use.
|
||||
*
|
||||
* @var DatabaseUtil
|
||||
* @var PostsToOrdersMigrationController
|
||||
*/
|
||||
private $posts_to_cot_migrator;
|
||||
|
||||
|
@ -211,7 +211,7 @@ SELECT(
|
|||
*
|
||||
* @return bool Whether the custom orders table the authoritative data source for orders currently.
|
||||
*/
|
||||
private function custom_orders_table_is_authoritative(): bool {
|
||||
public function custom_orders_table_is_authoritative(): bool {
|
||||
return 'yes' === get_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION );
|
||||
}
|
||||
|
||||
|
@ -229,7 +229,7 @@ SELECT(
|
|||
* @return array An array of order ids.
|
||||
* @throws \Exception Invalid parameter.
|
||||
*/
|
||||
private function get_ids_of_orders_pending_sync( int $type, int $limit ) {
|
||||
public function get_ids_of_orders_pending_sync( int $type, int $limit ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( $limit < 1 ) {
|
||||
|
@ -334,6 +334,8 @@ WHERE
|
|||
|
||||
/**
|
||||
* Hook to signal that the orders tables synchronization process has finished (nothing left to synchronize).
|
||||
*
|
||||
* @since 6.5.0
|
||||
*/
|
||||
do_action( self::PENDING_SYNCHRONIZATION_FINISHED_ACTION );
|
||||
} else {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
|
||||
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
|
||||
|
@ -22,6 +23,7 @@ class COTMigrationServiceProvider extends AbstractServiceProvider {
|
|||
*/
|
||||
protected $provides = array(
|
||||
PostsToOrdersMigrationController::class,
|
||||
CLIRunner::class,
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
|
||||
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
|
||||
|
@ -26,6 +28,7 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
|
|||
DataSynchronizer::class,
|
||||
CustomOrdersTableController::class,
|
||||
OrdersTableDataStore::class,
|
||||
CLIRunner::class,
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -35,5 +38,8 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
|
|||
$this->share( DataSynchronizer::class )->addArguments( array( OrdersTableDataStore::class, DatabaseUtil::class, PostsToOrdersMigrationController::class ) );
|
||||
$this->share( CustomOrdersTableController::class )->addArguments( array( OrdersTableDataStore::class, DataSynchronizer::class ) );
|
||||
$this->share( OrdersTableDataStore::class );
|
||||
if ( Constants::is_defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
$this->share( CLIRunner::class )->addArguments( array( CustomOrdersTableController::class, DataSynchronizer::class, PostsToOrdersMigrationController::class ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue