Merge pull request #32938 from woocommerce/cot/32917

Add CLI support to run migrations.
This commit is contained in:
Vedanshu Jain 2022-05-16 12:34:15 +05:30 committed by GitHub
commit b8e2de8520
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 809 additions and 11 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
CLI support for running COT migrations (one way).

View File

@ -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' ) );
}
}

View File

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

View File

@ -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 '';
}
}
}

View File

@ -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 );
}
}

View File

@ -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.
*

View File

@ -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 {

View File

@ -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,
);
/**

View File

@ -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 ) );
}
}
}