no wp
This commit is contained in:
@@ -1,865 +0,0 @@
|
||||
<?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 Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
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 sync', array( $this, 'sync' ) );
|
||||
WP_CLI::add_command( 'wc cot verify_cot_data', array( $this, 'verify_cot_data' ) );
|
||||
WP_CLI::add_command( 'wc cot enable', array( $this, 'enable' ) );
|
||||
WP_CLI::add_command( 'wc cot disable', array( $this, 'disable' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->custom_orders_table_usage_is_enabled() ) {
|
||||
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://github.com/woocommerce/woocommerce/wiki/High-Performance-Order-Storage-Upgrade-Recipe-Book'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->controller->custom_orders_table_usage_is_enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// 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 synced. */
|
||||
_n(
|
||||
'There is %1$d order to be synced.',
|
||||
'There are %1$d orders to be synced.',
|
||||
$order_count,
|
||||
'woocommerce'
|
||||
),
|
||||
$order_count
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (int) $order_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync order data between the custom order tables and the core WordPress post tables.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--batch-size=<batch-size>]
|
||||
* : The number of orders to process in each batch.
|
||||
* ---
|
||||
* default: 500
|
||||
* ---
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp wc cot sync --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 sync( $args = array(), $assoc_args = array() ) {
|
||||
if ( ! $this->synchronizer->check_orders_table_exists() ) {
|
||||
WP_CLI::warning( __( 'Custom order tables does not exist, creating...', 'woocommerce' ) );
|
||||
$this->synchronizer->create_database_tables();
|
||||
if ( $this->synchronizer->check_orders_table_exists() ) {
|
||||
WP_CLI::success( __( 'Custom order tables were created successfully.', 'woocommerce' ) );
|
||||
} else {
|
||||
WP_CLI::error( __( 'Custom order tables could not be created.', '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 sync, 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 Sync', $order_count / $batch_size );
|
||||
$processed = 0;
|
||||
$batch_count = 1;
|
||||
$total_time = 0;
|
||||
$orders_remaining = true;
|
||||
|
||||
while ( $order_count > 0 || $orders_remaining ) {
|
||||
$remaining_count = $order_count;
|
||||
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
/* Translators: %1$d is the batch number and %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_next_batch_to_process( $batch_size );
|
||||
if ( count( $order_ids ) ) {
|
||||
$this->synchronizer->process_batch( $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$d is the number of processed orders and %3$d is the execution time in seconds.
|
||||
__( 'Batch %1$d (%2$d orders) completed in %3$d seconds', 'woocommerce' ),
|
||||
$batch_count,
|
||||
count( $order_ids ),
|
||||
$batch_total_time
|
||||
)
|
||||
);
|
||||
|
||||
$batch_count ++;
|
||||
$total_time += $batch_total_time;
|
||||
|
||||
$progress->tick();
|
||||
|
||||
$orders_remaining = count( $this->synchronizer->get_next_batch_to_process( 1 ) ) > 0;
|
||||
$order_count = $remaining_count - $batch_size;
|
||||
}
|
||||
|
||||
$progress->finish();
|
||||
|
||||
// Issue a warning if no orders were migrated.
|
||||
if ( ! $processed ) {
|
||||
return WP_CLI::warning( __( 'No orders were synced.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
WP_CLI::log( __( 'Sync completed.', 'woocommerce' ) );
|
||||
|
||||
return WP_CLI::success(
|
||||
sprintf(
|
||||
/* Translators: %1$d is the number of migrated orders and %2$d is the execution time in seconds. */
|
||||
_n(
|
||||
'%1$d order was synced in %2$d seconds.',
|
||||
'%1$d orders were synced in %2$d seconds.',
|
||||
$processed,
|
||||
'woocommerce'
|
||||
),
|
||||
$processed,
|
||||
$total_time
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* [Deprecated] Use `wp wc cot sync` instead.
|
||||
* 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 migrate( $args = array(), $assoc_args = array() ) {
|
||||
WP_CLI::log( __( 'Migrate command is deprecated. Please use `sync` instead.', '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
|
||||
* ---
|
||||
*
|
||||
* [--end-at=<order_id>]
|
||||
* : Order ID to end at.
|
||||
* ---
|
||||
* default: -1
|
||||
* ---
|
||||
*
|
||||
* [--verbose]
|
||||
* : Whether to output errors as they happen in batch, or output them all together at the end.
|
||||
* ---
|
||||
* default: false
|
||||
* ---
|
||||
*
|
||||
* [--order-types]
|
||||
* : Comma seperated list of order types that needs to be verified. For example, --order-types=shop_order,shop_order_refund
|
||||
* ---
|
||||
* default: Output of function `wc_get_order_types( 'cot-migration' )`
|
||||
*
|
||||
* [--re-migrate]
|
||||
* : Attempt to re-migrate orders that failed verification. You should only use this option when you have never run the site with HPOS as authoritative source of order data yet, or you have manually checked the reported errors, otherwise, you risk stale data overwriting the more recent data.
|
||||
* This option can only be enabled when --verbose flag is also set.
|
||||
* default: false
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Verify migrated order data, 500 orders at a time.
|
||||
* wp wc cot verify_cot_data --batch-size=500 --start-from=0 --end-at=10000
|
||||
*
|
||||
* @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;
|
||||
|
||||
if ( ! $this->synchronizer->check_orders_table_exists() ) {
|
||||
WP_CLI::error( __( 'Orders table does not exist.', 'woocommerce' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
$assoc_args = wp_parse_args(
|
||||
$assoc_args,
|
||||
array(
|
||||
'batch-size' => 500,
|
||||
'start-from' => 0,
|
||||
'end-at' => - 1,
|
||||
'verbose' => false,
|
||||
'order-types' => '',
|
||||
're-migrate' => false,
|
||||
)
|
||||
);
|
||||
|
||||
$batch_count = 1;
|
||||
$total_time = 0;
|
||||
$failed_ids = array();
|
||||
$processed = 0;
|
||||
$order_id_start = (int) $assoc_args['start-from'];
|
||||
$order_id_end = (int) $assoc_args['end-at'];
|
||||
$order_id_end = -1 === $order_id_end ? PHP_INT_MAX : $order_id_end;
|
||||
$batch_size = ( (int) $assoc_args['batch-size'] ) === 0 ? 500 : (int) $assoc_args['batch-size'];
|
||||
$verbose = (bool) $assoc_args['verbose'];
|
||||
$order_types = wc_get_order_types( 'cot-migration' );
|
||||
$remigrate = (bool) $assoc_args['re-migrate'];
|
||||
if ( ! empty( $assoc_args['order-types'] ) ) {
|
||||
$passed_order_types = array_map( 'trim', explode( ',', $assoc_args['order-types'] ) );
|
||||
$order_types = array_intersect( $order_types, $passed_order_types );
|
||||
}
|
||||
|
||||
if ( 0 === count( $order_types ) ) {
|
||||
return WP_CLI::error(
|
||||
sprintf(
|
||||
/* Translators: %s is the comma seperated list of order types. */
|
||||
__( 'Passed order type does not match any registered order types. Following order types are registered: %s', 'woocommerce' ),
|
||||
implode( ',', wc_get_order_types( 'cot-migration' ) )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$order_types_pl = implode( ',', array_fill( 0, count( $order_types ), '%s' ) );
|
||||
|
||||
$order_count = $this->get_verify_order_count( $order_id_start, $order_id_end, $order_types, false );
|
||||
|
||||
$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
|
||||
)
|
||||
);
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Inputs are prepared.
|
||||
$order_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT ID FROM $wpdb->posts WHERE post_type in ( $order_types_pl ) AND ID >= %d AND ID <= %d ORDER BY ID ASC LIMIT %d",
|
||||
array_merge(
|
||||
$order_types,
|
||||
array(
|
||||
$order_id_start,
|
||||
$order_id_end,
|
||||
$batch_size,
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
// phpcs:enable
|
||||
$batch_start_time = microtime( true );
|
||||
$failed_ids_in_current_batch = $this->post_to_cot_migrator->verify_migrated_orders( $order_ids );
|
||||
$failed_ids_in_current_batch = $this->verify_meta_data( $order_ids, $failed_ids_in_current_batch );
|
||||
$failed_ids = $verbose ? array() : $failed_ids + $failed_ids_in_current_batch;
|
||||
$processed += count( $order_ids );
|
||||
$batch_total_time = microtime( true ) - $batch_start_time;
|
||||
$batch_count ++;
|
||||
$total_time += $batch_total_time;
|
||||
|
||||
if ( $verbose && count( $failed_ids_in_current_batch ) > 0 ) {
|
||||
$errors = wp_json_encode( $failed_ids_in_current_batch, JSON_PRETTY_PRINT );
|
||||
WP_CLI::warning(
|
||||
sprintf(
|
||||
/* Translators: %1$d is number of errors and %2$s is the formatted array of order IDs. */
|
||||
_n(
|
||||
'%1$d error found: %2$s. Please review the error above.',
|
||||
'%1$d errors found: %2$s. Please review the errors above.',
|
||||
count( $failed_ids_in_current_batch ),
|
||||
'woocommerce'
|
||||
),
|
||||
count( $failed_ids_in_current_batch ),
|
||||
$errors
|
||||
)
|
||||
);
|
||||
if ( $remigrate ) {
|
||||
WP_CLI::warning(
|
||||
sprintf(
|
||||
__( 'Attempting to remigrate...', 'woocommerce' )
|
||||
)
|
||||
);
|
||||
$failed_ids = array_keys( $failed_ids_in_current_batch );
|
||||
$this->synchronizer->process_batch( $failed_ids );
|
||||
$errors_in_remigrate_batch = $this->post_to_cot_migrator->verify_migrated_orders( $failed_ids );
|
||||
$errors_in_remigrate_batch = $this->verify_meta_data( $failed_ids, $errors_in_remigrate_batch );
|
||||
if ( count( $errors_in_remigrate_batch ) > 0 ) {
|
||||
$formatted_errors = wp_json_encode( $errors_in_remigrate_batch, JSON_PRETTY_PRINT );
|
||||
WP_CLI::warning(
|
||||
sprintf(
|
||||
/* Translators: %1$d is number of errors and %2$s is the formatted array of order IDs. */
|
||||
_n(
|
||||
'%1$d error found: %2$s when re-migrating order. Please review the error above.',
|
||||
'%1$d errors found: %2$s when re-migrating orders. Please review the errors above.',
|
||||
count( $errors_in_remigrate_batch ),
|
||||
'woocommerce'
|
||||
),
|
||||
count( $errors_in_remigrate_batch ),
|
||||
$formatted_errors
|
||||
)
|
||||
);
|
||||
} else {
|
||||
WP_CLI::warning( 'Re-migration successful.', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$progress->tick();
|
||||
|
||||
WP_CLI::debug(
|
||||
sprintf(
|
||||
/* Translators: %1$d is the batch number, %2$d is time taken to process batch. */
|
||||
__( 'Batch %1$d (%2$d orders) completed in %3$d seconds.', 'woocommerce' ),
|
||||
$batch_count,
|
||||
count( $order_ids ),
|
||||
$batch_total_time
|
||||
)
|
||||
);
|
||||
|
||||
$order_id_start = max( $order_ids ) + 1;
|
||||
$remaining_count = $this->get_verify_order_count( $order_id_start, $order_id_end, $order_types, 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 ( $verbose ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 0 === count( $failed_ids ) ) {
|
||||
return WP_CLI::success(
|
||||
sprintf(
|
||||
/* Translators: %1$d is the number of migrated orders and %2$d is time taken. */
|
||||
_n(
|
||||
'%1$d order was verified in %2$d seconds.',
|
||||
'%1$d orders were verified in %2$d seconds.',
|
||||
$processed,
|
||||
'woocommerce'
|
||||
),
|
||||
$processed,
|
||||
$total_time
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$errors = wp_json_encode( $failed_ids, JSON_PRETTY_PRINT );
|
||||
|
||||
return WP_CLI::error(
|
||||
sprintf(
|
||||
'%1$s %2$s',
|
||||
sprintf(
|
||||
/* Translators: %1$d is the number of migrated orders and %2$d is the execution time in seconds. */
|
||||
_n(
|
||||
'%1$d order was verified in %2$d seconds.',
|
||||
'%1$d orders were verified in %2$d seconds.',
|
||||
$processed,
|
||||
'woocommerce'
|
||||
),
|
||||
$processed,
|
||||
$total_time
|
||||
),
|
||||
sprintf(
|
||||
/* Translators: %1$d is number of errors and %2$s is the formatted array of order IDs. */
|
||||
_n(
|
||||
'%1$d error found: %2$s. Please review the error above.',
|
||||
'%1$d errors found: %2$s. Please review the errors above.',
|
||||
count( $failed_ids ),
|
||||
'woocommerce'
|
||||
),
|
||||
count( $failed_ids ),
|
||||
$errors
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get count for orders needing verification.
|
||||
*
|
||||
* @param int $order_id_start Order ID to start from.
|
||||
* @param int $order_id_end Order ID to end at.
|
||||
* @param array $order_types List of order types to verify.
|
||||
* @param bool $log Whether to also log an error message.
|
||||
*
|
||||
* @return int Order count.
|
||||
*/
|
||||
private function get_verify_order_count( int $order_id_start, int $order_id_end, array $order_types, bool $log = true ) : int {
|
||||
global $wpdb;
|
||||
|
||||
$order_types_placeholder = implode( ',', array_fill( 0, count( $order_types ), '%s' ) );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Inputs are prepared.
|
||||
$order_count = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $wpdb->posts WHERE post_type in ($order_types_placeholder) AND ID >= %d AND ID <= %d",
|
||||
array_merge(
|
||||
$order_types,
|
||||
array(
|
||||
$order_id_start,
|
||||
$order_id_end,
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
// phpcs:enable
|
||||
|
||||
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 {
|
||||
$meta_keys_to_ignore = array(
|
||||
'_paid_date', // This has been deprecated and replaced by '_date_paid' in the CPT datastore.
|
||||
'_completed_date', // This has been deprecated and replaced by '_date_completed' in the CPT datastore.
|
||||
'_edit_lock',
|
||||
);
|
||||
|
||||
global $wpdb;
|
||||
if ( ! count( $order_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
$excluded_columns = array_merge(
|
||||
$this->post_to_cot_migrator->get_migrated_meta_keys(),
|
||||
$meta_keys_to_ignore
|
||||
);
|
||||
$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, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- 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, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- 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, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Not a meta query.
|
||||
'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(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Not a meta query.
|
||||
}
|
||||
$clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ][] = $row['meta_value'];
|
||||
}
|
||||
return $clubbed_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom order tables (HPOS) to authoritative if: 1). HPOS and posts tables are in sync, or, 2). This is a new shop (in this case also create tables). Additionally, all installed WC plugins should be compatible.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--for-new-shop]
|
||||
* : Enable only if this is a new shop, irrespective of whether tables are in sync.
|
||||
* ---
|
||||
* default: false
|
||||
* ---
|
||||
*
|
||||
* [--with-sync]
|
||||
* : Also enables sync (if it's currently not enabled).
|
||||
* ---
|
||||
* default: false
|
||||
* ---
|
||||
*
|
||||
* ### EXAMPLES
|
||||
*
|
||||
* # Enable HPOS on new shops.
|
||||
* wp wc cot enable --for-new-shop
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enable( array $args = array(), array $assoc_args = array() ) {
|
||||
$assoc_args = wp_parse_args(
|
||||
$assoc_args,
|
||||
array(
|
||||
'for-new-shop' => false,
|
||||
'with-sync' => false,
|
||||
)
|
||||
);
|
||||
|
||||
$enable_hpos = true;
|
||||
WP_CLI::log( __( 'Running pre-enable checks...', 'woocommerce' ) );
|
||||
|
||||
$is_new_shop = \WC_Install::is_new_install();
|
||||
if ( $assoc_args['for-new-shop'] && ! $is_new_shop ) {
|
||||
WP_CLI::error( __( '[Failed] This is not a new shop, but --for-new-shop flag was passed.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
/** Feature controller instance @var FeaturesController $feature_controller */
|
||||
$feature_controller = wc_get_container()->get( FeaturesController::class );
|
||||
$plugin_info = $feature_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
|
||||
if ( count( array_merge( $plugin_info['uncertain'], $plugin_info['incompatible'] ) ) > 0 ) {
|
||||
WP_CLI::warning( __( '[Failed] Some installed plugins are incompatible. Please review the plugins by going to WooCommerce > Settings > Advanced > Features and see the "Order data storage" section.', 'woocommerce' ) );
|
||||
$enable_hpos = false;
|
||||
}
|
||||
|
||||
/** DataSynchronizer instance @var DataSynchronizer $data_synchronizer */
|
||||
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
|
||||
$pending_orders = $data_synchronizer->get_total_pending_count();
|
||||
$table_exists = $data_synchronizer->check_orders_table_exists();
|
||||
|
||||
if ( ! $table_exists ) {
|
||||
WP_CLI::warning( __( 'Orders table does not exist. Creating...', 'woocommerce' ) );
|
||||
if ( $is_new_shop || 0 === $pending_orders ) {
|
||||
$data_synchronizer->create_database_tables();
|
||||
if ( $data_synchronizer->check_orders_table_exists() ) {
|
||||
WP_CLI::log( __( 'Orders table created.', 'woocommerce' ) );
|
||||
$table_exists = true;
|
||||
} else {
|
||||
WP_CLI::warning( __( '[Failed] Orders table could not be created.', 'woocommerce' ) );
|
||||
$enable_hpos = false;
|
||||
}
|
||||
} else {
|
||||
WP_CLI::warning( __( '[Failed] The orders table does not exist and this is not a new shop. Please create the table by going to WooCommerce > Settings > Advanced > Features and enabling sync.', 'woocommerce' ) );
|
||||
$enable_hpos = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $pending_orders > 0 ) {
|
||||
WP_CLI::warning(
|
||||
sprintf(
|
||||
// translators: %s is the command to run (wp wc cot sync).
|
||||
__( '[Failed] There are orders pending sync. Please run `%s` to sync pending orders.', 'woocommerce' ),
|
||||
'wp wc cot sync',
|
||||
)
|
||||
);
|
||||
$enable_hpos = false;
|
||||
}
|
||||
|
||||
if ( $assoc_args['with-sync'] && $table_exists ) {
|
||||
if ( $data_synchronizer->data_sync_is_enabled() ) {
|
||||
WP_CLI::warning( __( 'Sync is already enabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
$feature_controller->change_feature_enable( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, true );
|
||||
WP_CLI::success( __( 'Sync enabled.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $enable_hpos ) {
|
||||
WP_CLI::error( __( 'HPOS pre-checks failed, please see the errors above', 'woocommerce' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
/** CustomOrdersTableController instance @var CustomOrdersTableController $cot_status */
|
||||
$cot_status = wc_get_container()->get( CustomOrdersTableController::class );
|
||||
if ( $cot_status->custom_orders_table_usage_is_enabled() ) {
|
||||
WP_CLI::warning( __( 'HPOS is already enabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
$feature_controller->change_feature_enable( 'custom_order_tables', true );
|
||||
if ( $cot_status->custom_orders_table_usage_is_enabled() ) {
|
||||
WP_CLI::success( __( 'HPOS enabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
WP_CLI::error( __( 'HPOS could not be enabled.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables custom order tables (HPOS) and posts to authoritative if HPOS and post tables are in sync.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--with-sync]
|
||||
* : Also disables sync (if it's currently enabled).
|
||||
* ---
|
||||
* default: false
|
||||
* ---
|
||||
*
|
||||
* ### EXAMPLES
|
||||
*
|
||||
* # Disable HPOS.
|
||||
* wp wc cot disable
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function disable( $args, $assoc_args ) {
|
||||
$assoc_args = wp_parse_args(
|
||||
$assoc_args,
|
||||
array(
|
||||
'with-sync' => false,
|
||||
)
|
||||
);
|
||||
|
||||
WP_CLI::log( __( 'Running pre-disable checks...', 'woocommerce' ) );
|
||||
|
||||
/** DataSynchronizer instance @var DataSynchronizer $data_synchronizer */
|
||||
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
|
||||
$pending_orders = $data_synchronizer->get_total_pending_count();
|
||||
if ( $pending_orders > 0 ) {
|
||||
return WP_CLI::error(
|
||||
sprintf(
|
||||
// translators: %s is the command to run (wp wc cot sync).
|
||||
__( '[Failed] There are orders pending sync. Please run `%s` to sync pending orders.', 'woocommerce' ),
|
||||
'wp wc cot sync',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** FeaturesController instance @var FeaturesController $feature_controller */
|
||||
$feature_controller = wc_get_container()->get( FeaturesController::class );
|
||||
|
||||
/** CustomOrdersTableController instance @var CustomOrdersTableController $cot_status */
|
||||
$cot_status = wc_get_container()->get( CustomOrdersTableController::class );
|
||||
if ( ! $cot_status->custom_orders_table_usage_is_enabled() ) {
|
||||
WP_CLI::warning( __( 'HPOS is already disabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
$feature_controller->change_feature_enable( 'custom_order_tables', false );
|
||||
if ( $cot_status->custom_orders_table_usage_is_enabled() ) {
|
||||
return WP_CLI::warning( __( 'HPOS could not be disabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
WP_CLI::success( __( 'HPOS disabled.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $assoc_args['with-sync'] ) {
|
||||
if ( ! $data_synchronizer->data_sync_is_enabled() ) {
|
||||
return WP_CLI::warning( __( 'Sync is already disabled.', 'woocommerce' ) );
|
||||
}
|
||||
$feature_controller->change_feature_enable( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, false );
|
||||
if ( $data_synchronizer->data_sync_is_enabled() ) {
|
||||
return WP_CLI::warning( __( 'Sync could not be disabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
WP_CLI::success( __( 'Sync disabled.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Migration class for migrating from WPPostMeta to OrderMeta table.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
|
||||
|
||||
use Automattic\WooCommerce\Database\Migrations\MetaToMetaTableMigrator;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
|
||||
/**
|
||||
* Helper class to migrate records from the WordPress post meta table
|
||||
* to the custom orders meta table.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
|
||||
*/
|
||||
class PostMetaToOrderMetaMigrator extends MetaToMetaTableMigrator {
|
||||
|
||||
/**
|
||||
* List of meta keys to exclude from migration.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $excluded_columns;
|
||||
|
||||
/**
|
||||
* PostMetaToOrderMetaMigrator constructor.
|
||||
*
|
||||
* @param array $excluded_columns List of meta keys to exclude from migration.
|
||||
*/
|
||||
public function __construct( $excluded_columns ) {
|
||||
$this->excluded_columns = $excluded_columns;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate config for meta data migration.
|
||||
*
|
||||
* @return array Meta data migration config.
|
||||
*/
|
||||
protected function get_meta_config(): array {
|
||||
global $wpdb;
|
||||
|
||||
return array(
|
||||
'source' => array(
|
||||
'meta' => array(
|
||||
'table_name' => $wpdb->postmeta,
|
||||
'entity_id_column' => 'post_id',
|
||||
'meta_id_column' => 'meta_id',
|
||||
'meta_key_column' => 'meta_key',
|
||||
'meta_value_column' => 'meta_value',
|
||||
),
|
||||
'entity' => array(
|
||||
'table_name' => $wpdb->posts,
|
||||
'source_id_column' => 'ID',
|
||||
'id_column' => 'ID',
|
||||
),
|
||||
'excluded_keys' => $this->excluded_columns,
|
||||
),
|
||||
'destination' => array(
|
||||
'meta' => array(
|
||||
'table_name' => OrdersTableDataStore::get_meta_table_name(),
|
||||
'entity_id_column' => 'order_id',
|
||||
'meta_key_column' => 'meta_key',
|
||||
'meta_value_column' => 'meta_value',
|
||||
'entity_id_type' => 'int',
|
||||
'meta_id_column' => 'id',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for WPPost to wc_order_address table migrator.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
|
||||
|
||||
use Automattic\WooCommerce\Database\Migrations\MetaToCustomTableMigrator;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
|
||||
/**
|
||||
* Helper class to migrate records from the WordPress post table
|
||||
* to the custom order addresses table.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
|
||||
*/
|
||||
class PostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
|
||||
/**
|
||||
* Type of addresses being migrated; 'billing' or 'shipping'.
|
||||
*
|
||||
* @var $type
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* PostToOrderAddressTableMigrator constructor.
|
||||
*
|
||||
* @param string $type Type of address being migrated; 'billing' or 'shipping'.
|
||||
*/
|
||||
public function __construct( $type ) {
|
||||
$this->type = $type;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schema config for wp_posts and wc_order_address table.
|
||||
*
|
||||
* @return array Config.
|
||||
*/
|
||||
protected function get_schema_config(): array {
|
||||
global $wpdb;
|
||||
|
||||
return array(
|
||||
'source' => array(
|
||||
'entity' => array(
|
||||
'table_name' => $wpdb->posts,
|
||||
'meta_rel_column' => 'ID',
|
||||
'destination_rel_column' => 'ID',
|
||||
'primary_key' => 'ID',
|
||||
),
|
||||
'meta' => array(
|
||||
'table_name' => $wpdb->postmeta,
|
||||
'meta_id_column' => 'meta_id',
|
||||
'meta_key_column' => 'meta_key',
|
||||
'meta_value_column' => 'meta_value',
|
||||
'entity_id_column' => 'post_id',
|
||||
),
|
||||
),
|
||||
'destination' => array(
|
||||
'table_name' => OrdersTableDataStore::get_addresses_table_name(),
|
||||
'source_rel_column' => 'order_id',
|
||||
'primary_key' => 'id',
|
||||
'primary_key_type' => 'int',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get columns config.
|
||||
*
|
||||
* @return \string[][] Config.
|
||||
*/
|
||||
protected function get_core_column_mapping(): array {
|
||||
$type = $this->type;
|
||||
|
||||
return array(
|
||||
'ID' => array(
|
||||
'type' => 'int',
|
||||
'destination' => 'order_id',
|
||||
),
|
||||
'type' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'address_type',
|
||||
'select_clause' => "'$type'",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get meta data config.
|
||||
*
|
||||
* @return \string[][] Config.
|
||||
*/
|
||||
public function get_meta_column_config(): array {
|
||||
$type = $this->type;
|
||||
|
||||
return array(
|
||||
"_{$type}_first_name" => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'first_name',
|
||||
),
|
||||
"_{$type}_last_name" => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'last_name',
|
||||
),
|
||||
"_{$type}_company" => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'company',
|
||||
),
|
||||
"_{$type}_address_1" => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'address_1',
|
||||
),
|
||||
"_{$type}_address_2" => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'address_2',
|
||||
),
|
||||
"_{$type}_city" => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'city',
|
||||
),
|
||||
"_{$type}_state" => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'state',
|
||||
),
|
||||
"_{$type}_postcode" => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'postcode',
|
||||
),
|
||||
"_{$type}_country" => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'country',
|
||||
),
|
||||
"_{$type}_email" => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'email',
|
||||
),
|
||||
"_{$type}_phone" => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'phone',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional WHERE clause to only fetch the addresses of the current type.
|
||||
*
|
||||
* @param array $entity_ids The ids of the entities being inserted or updated.
|
||||
* @return string The additional string for the WHERE clause.
|
||||
*/
|
||||
protected function get_additional_where_clause_for_get_data_to_insert_or_update( array $entity_ids ): string {
|
||||
return "AND destination.`address_type` = '{$this->type}'";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 );
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for WPPost to wc_order_operational_details migrator.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
|
||||
|
||||
use Automattic\WooCommerce\Database\Migrations\MetaToCustomTableMigrator;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
|
||||
/**
|
||||
* Helper class to migrate records from the WordPress post table
|
||||
* to the custom order operations table.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
|
||||
*/
|
||||
class PostToOrderOpTableMigrator extends MetaToCustomTableMigrator {
|
||||
|
||||
/**
|
||||
* Get schema config for wp_posts and wc_order_operational_detail table.
|
||||
*
|
||||
* @return array Config.
|
||||
*/
|
||||
protected function get_schema_config(): array {
|
||||
global $wpdb;
|
||||
|
||||
return array(
|
||||
'source' => array(
|
||||
'entity' => array(
|
||||
'table_name' => $wpdb->posts,
|
||||
'meta_rel_column' => 'ID',
|
||||
'destination_rel_column' => 'ID',
|
||||
'primary_key' => 'ID',
|
||||
),
|
||||
'meta' => array(
|
||||
'table_name' => $wpdb->postmeta,
|
||||
'meta_id_column' => 'meta_id',
|
||||
'meta_key_column' => 'meta_key',
|
||||
'meta_value_column' => 'meta_value',
|
||||
'entity_id_column' => 'post_id',
|
||||
),
|
||||
),
|
||||
'destination' => array(
|
||||
'table_name' => OrdersTableDataStore::get_operational_data_table_name(),
|
||||
'source_rel_column' => 'order_id',
|
||||
'primary_key' => 'id',
|
||||
'primary_key_type' => 'int',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get columns config.
|
||||
*
|
||||
* @return \string[][] Config.
|
||||
*/
|
||||
protected function get_core_column_mapping(): array {
|
||||
return array(
|
||||
'ID' => array(
|
||||
'type' => 'int',
|
||||
'destination' => 'order_id',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get meta data config.
|
||||
*
|
||||
* @return \string[][] Config.
|
||||
*/
|
||||
public function get_meta_column_config(): array {
|
||||
return array(
|
||||
'_created_via' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'created_via',
|
||||
),
|
||||
'_order_version' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'woocommerce_version',
|
||||
),
|
||||
'_prices_include_tax' => array(
|
||||
'type' => 'bool',
|
||||
'destination' => 'prices_include_tax',
|
||||
),
|
||||
'_recorded_coupon_usage_counts' => array(
|
||||
'type' => 'bool',
|
||||
'destination' => 'coupon_usages_are_counted',
|
||||
),
|
||||
'_download_permissions_granted' => array(
|
||||
'type' => 'bool',
|
||||
'destination' => 'download_permission_granted',
|
||||
),
|
||||
'_cart_hash' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'cart_hash',
|
||||
),
|
||||
'_new_order_email_sent' => array(
|
||||
'type' => 'bool',
|
||||
'destination' => 'new_order_email_sent',
|
||||
),
|
||||
'_order_key' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'order_key',
|
||||
),
|
||||
'_order_stock_reduced' => array(
|
||||
'type' => 'bool',
|
||||
'destination' => 'order_stock_reduced',
|
||||
),
|
||||
'_date_paid' => array(
|
||||
'type' => 'date_epoch',
|
||||
'destination' => 'date_paid_gmt',
|
||||
),
|
||||
'_date_completed' => array(
|
||||
'type' => 'date_epoch',
|
||||
'destination' => 'date_completed_gmt',
|
||||
),
|
||||
'_order_shipping_tax' => array(
|
||||
'type' => 'decimal',
|
||||
'destination' => 'shipping_tax_amount',
|
||||
),
|
||||
'_order_shipping' => array(
|
||||
'type' => 'decimal',
|
||||
'destination' => 'shipping_total_amount',
|
||||
),
|
||||
'_cart_discount_tax' => array(
|
||||
'type' => 'decimal',
|
||||
'destination' => 'discount_tax_amount',
|
||||
),
|
||||
'_cart_discount' => array(
|
||||
'type' => 'decimal',
|
||||
'destination' => 'discount_total_amount',
|
||||
),
|
||||
'_recorded_sales' => array(
|
||||
'type' => 'bool',
|
||||
'destination' => 'recorded_sales',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for WPPost To order table migrator.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
|
||||
|
||||
use Automattic\WooCommerce\Database\Migrations\MetaToCustomTableMigrator;
|
||||
|
||||
/**
|
||||
* Helper class to migrate records from the WordPress post table
|
||||
* to the custom order table (and only that table - PostsToOrdersMigrationController
|
||||
* is used for fully migrating orders).
|
||||
*/
|
||||
class PostToOrderTableMigrator extends MetaToCustomTableMigrator {
|
||||
|
||||
/**
|
||||
* Get schema config for wp_posts and wc_order table.
|
||||
*
|
||||
* @return array Config.
|
||||
*/
|
||||
protected function get_schema_config(): array {
|
||||
global $wpdb;
|
||||
|
||||
$table_names = array(
|
||||
'orders' => $wpdb->prefix . 'wc_orders',
|
||||
'addresses' => $wpdb->prefix . 'wc_order_addresses',
|
||||
'op_data' => $wpdb->prefix . 'wc_order_operational_data',
|
||||
'meta' => $wpdb->prefix . 'wc_orders_meta',
|
||||
);
|
||||
|
||||
return array(
|
||||
'source' => array(
|
||||
'entity' => array(
|
||||
'table_name' => $wpdb->posts,
|
||||
'meta_rel_column' => 'ID',
|
||||
'destination_rel_column' => 'ID',
|
||||
'primary_key' => 'ID',
|
||||
),
|
||||
'meta' => array(
|
||||
'table_name' => $wpdb->postmeta,
|
||||
'meta_id_column' => 'meta_id',
|
||||
'meta_key_column' => 'meta_key',
|
||||
'meta_value_column' => 'meta_value',
|
||||
'entity_id_column' => 'post_id',
|
||||
),
|
||||
),
|
||||
'destination' => array(
|
||||
'table_name' => $table_names['orders'],
|
||||
'source_rel_column' => 'id',
|
||||
'primary_key' => 'id',
|
||||
'primary_key_type' => 'int',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get columns config.
|
||||
*
|
||||
* @return \string[][] Config.
|
||||
*/
|
||||
protected function get_core_column_mapping(): array {
|
||||
return array(
|
||||
'ID' => array(
|
||||
'type' => 'int',
|
||||
'destination' => 'id',
|
||||
),
|
||||
'post_status' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'status',
|
||||
),
|
||||
'post_date_gmt' => array(
|
||||
'type' => 'date',
|
||||
'destination' => 'date_created_gmt',
|
||||
),
|
||||
'post_modified_gmt' => array(
|
||||
'type' => 'date',
|
||||
'destination' => 'date_updated_gmt',
|
||||
),
|
||||
'post_parent' => array(
|
||||
'type' => 'int',
|
||||
'destination' => 'parent_order_id',
|
||||
),
|
||||
'post_type' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'type',
|
||||
),
|
||||
'post_excerpt' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'customer_note',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get meta data config.
|
||||
*
|
||||
* @return \string[][] Config.
|
||||
*/
|
||||
public function get_meta_column_config(): array {
|
||||
return array(
|
||||
'_order_currency' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'currency',
|
||||
),
|
||||
'_order_tax' => array(
|
||||
'type' => 'decimal',
|
||||
'destination' => 'tax_amount',
|
||||
),
|
||||
'_order_total' => array(
|
||||
'type' => 'decimal',
|
||||
'destination' => 'total_amount',
|
||||
),
|
||||
'_customer_user' => array(
|
||||
'type' => 'int',
|
||||
'destination' => 'customer_id',
|
||||
),
|
||||
'_billing_email' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'billing_email',
|
||||
),
|
||||
'_payment_method' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'payment_method',
|
||||
),
|
||||
'_payment_method_title' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'payment_method_title',
|
||||
),
|
||||
'_customer_ip_address' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'ip_address',
|
||||
),
|
||||
'_customer_user_agent' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'user_agent',
|
||||
),
|
||||
'_transaction_id' => array(
|
||||
'type' => 'string',
|
||||
'destination' => 'transaction_id',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for implementing migration from wp_posts and wp_postmeta to custom order tables.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
|
||||
/**
|
||||
* This is the main class used to perform the complete migration of orders
|
||||
* from the posts table to the custom orders table.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
|
||||
*/
|
||||
class PostsToOrdersMigrationController {
|
||||
|
||||
/**
|
||||
* Error logger for migration errors.
|
||||
*
|
||||
* @var \WC_Logger
|
||||
*/
|
||||
private $error_logger;
|
||||
|
||||
/**
|
||||
* Array of objects used to perform the migration.
|
||||
*
|
||||
* @var TableMigrator[]
|
||||
*/
|
||||
private $all_migrators;
|
||||
|
||||
/**
|
||||
* The source name to use for logs.
|
||||
*/
|
||||
public const LOGS_SOURCE_NAME = 'posts-to-orders-migration';
|
||||
|
||||
/**
|
||||
* PostsToOrdersMigrationController constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
$this->all_migrators = array();
|
||||
$this->all_migrators['order'] = new PostToOrderTableMigrator();
|
||||
$this->all_migrators['order_address_billing'] = new PostToOrderAddressTableMigrator( 'billing' );
|
||||
$this->all_migrators['order_address_shipping'] = new PostToOrderAddressTableMigrator( 'shipping' );
|
||||
$this->all_migrators['order_operational_data'] = new PostToOrderOpTableMigrator();
|
||||
$this->all_migrators['order_meta'] = new PostMetaToOrderMetaMigrator( $this->get_migrated_meta_keys() );
|
||||
$this->error_logger = wc_get_logger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get migrated keys for all the tables in this controller.
|
||||
*
|
||||
* @return string[] Array of meta keys.
|
||||
*/
|
||||
public function get_migrated_meta_keys() {
|
||||
$migrated_meta_keys = array();
|
||||
foreach ( $this->all_migrators as $name => $migrator ) {
|
||||
if ( method_exists( $migrator, 'get_meta_column_config' ) ) {
|
||||
$migrated_meta_keys = array_merge( $migrated_meta_keys, $migrator->get_meta_column_config() );
|
||||
}
|
||||
}
|
||||
return array_keys( $migrated_meta_keys );
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a set of orders from the posts table to the custom orders tables.
|
||||
*
|
||||
* @param array $order_post_ids List of post IDs of the orders to migrate.
|
||||
*/
|
||||
public function migrate_orders( array $order_post_ids ): void {
|
||||
$this->error_logger = WC()->call_function( 'wc_get_logger' );
|
||||
|
||||
$data = array();
|
||||
try {
|
||||
foreach ( $this->all_migrators as $name => $migrator ) {
|
||||
$data[ $name ] = $migrator->fetch_sanitized_migration_data( $order_post_ids );
|
||||
if ( ! empty( $data[ $name ]['errors'] ) ) {
|
||||
$this->handle_migration_error( $order_post_ids, $data[ $name ]['errors'], null, null, $name );
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
$this->handle_migration_error( $order_post_ids, $data, $e, null, 'Fetching data' );
|
||||
return;
|
||||
}
|
||||
|
||||
$using_transactions = $this->maybe_start_transaction();
|
||||
|
||||
foreach ( $this->all_migrators as $name => $migrator ) {
|
||||
$results = $migrator->process_migration_data( $data[ $name ] );
|
||||
$errors = array_unique( $results['errors'] );
|
||||
$exception = $results['exception'];
|
||||
|
||||
if ( null === $exception && empty( $errors ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->handle_migration_error( $order_post_ids, $errors, $exception, $using_transactions, $name );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $using_transactions ) {
|
||||
$this->commit_transaction();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Log migration errors if any.
|
||||
*
|
||||
* @param array $order_post_ids List of post IDs of the orders to migrate.
|
||||
* @param array $errors List of errors to log.
|
||||
* @param \Exception|null $exception Exception to log.
|
||||
* @param bool|null $using_transactions Whether transactions were used.
|
||||
* @param string $name Name of the migrator.
|
||||
*/
|
||||
private function handle_migration_error( array $order_post_ids, array $errors, ?\Exception $exception, ?bool $using_transactions, string $name ) {
|
||||
$batch = ArrayUtil::to_ranges_string( $order_post_ids );
|
||||
|
||||
if ( null !== $exception ) {
|
||||
$exception_class = get_class( $exception );
|
||||
$this->error_logger->error(
|
||||
"$name: when processing ids $batch: ($exception_class) {$exception->getMessage()}, {$exception->getTraceAsString()}",
|
||||
array(
|
||||
'source' => self::LOGS_SOURCE_NAME,
|
||||
'ids' => $order_post_ids,
|
||||
'exception' => $exception,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $errors as $error ) {
|
||||
$this->error_logger->error(
|
||||
"$name: when processing ids $batch: $error",
|
||||
array(
|
||||
'source' => self::LOGS_SOURCE_NAME,
|
||||
'ids' => $order_post_ids,
|
||||
'error' => $error,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( $using_transactions ) {
|
||||
$this->rollback_transaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a database transaction if the configuration mandates so.
|
||||
*
|
||||
* @return bool|null True if transaction started, false if transactions won't be used, null if transaction failed to start.
|
||||
*
|
||||
* @throws \Exception If the transaction isolation level is invalid.
|
||||
*/
|
||||
private function maybe_start_transaction(): ?bool {
|
||||
|
||||
$use_transactions = get_option( CustomOrdersTableController::USE_DB_TRANSACTIONS_OPTION, 'yes' );
|
||||
if ( 'yes' !== $use_transactions ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$transaction_isolation_level = get_option( CustomOrdersTableController::DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION, CustomOrdersTableController::DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL );
|
||||
$valid_transaction_isolation_levels = array( 'READ UNCOMMITTED', 'READ COMMITTED', 'REPEATABLE READ', 'SERIALIZABLE' );
|
||||
if ( ! in_array( $transaction_isolation_level, $valid_transaction_isolation_levels, true ) ) {
|
||||
throw new \Exception( "Invalid database transaction isolation level name $transaction_isolation_level" );
|
||||
}
|
||||
|
||||
$set_transaction_isolation_level_command = "SET TRANSACTION ISOLATION LEVEL $transaction_isolation_level";
|
||||
|
||||
// We suppress errors in transaction isolation level setting because it's not supported by all DB engines, additionally, this might be executing in context of another transaction with a different isolation level.
|
||||
if ( ! $this->db_query( $set_transaction_isolation_level_command, true ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->db_query( 'START TRANSACTION' ) ? true : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the current database transaction.
|
||||
*
|
||||
* @return bool True on success, false on error.
|
||||
*/
|
||||
private function commit_transaction(): bool {
|
||||
return $this->db_query( 'COMMIT' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback the current database transaction.
|
||||
*
|
||||
* @return bool True on success, false on error.
|
||||
*/
|
||||
private function rollback_transaction(): bool {
|
||||
return $this->db_query( 'ROLLBACK' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a database query and log any errors.
|
||||
*
|
||||
* @param string $query The SQL query to execute.
|
||||
* @param bool $supress_errors Whether to suppress errors.
|
||||
*
|
||||
* @return bool True if the query succeeded, false if there were errors.
|
||||
*/
|
||||
private function db_query( string $query, bool $supress_errors = false ): bool {
|
||||
$wpdb = WC()->get_global( 'wpdb' );
|
||||
|
||||
try {
|
||||
if ( $supress_errors ) {
|
||||
$suppress = $wpdb->suppress_errors( true );
|
||||
}
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$wpdb->query( $query );
|
||||
if ( $supress_errors ) {
|
||||
$wpdb->suppress_errors( $suppress );
|
||||
}
|
||||
} catch ( \Exception $exception ) {
|
||||
$exception_class = get_class( $exception );
|
||||
$this->error_logger->error(
|
||||
"PostsToOrdersMigrationController: when executing $query: ($exception_class) {$exception->getMessage()}, {$exception->getTraceAsString()}",
|
||||
array(
|
||||
'source' => self::LOGS_SOURCE_NAME,
|
||||
'exception' => $exception,
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$error = $wpdb->last_error;
|
||||
if ( '' !== $error ) {
|
||||
$this->error_logger->error(
|
||||
"PostsToOrdersMigrationController: when executing $query: $error",
|
||||
array(
|
||||
'source' => self::LOGS_SOURCE_NAME,
|
||||
'error' => $error,
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
$errors = array();
|
||||
foreach ( $this->all_migrators as $migrator ) {
|
||||
if ( method_exists( $migrator, 'verify_migrated_data' ) ) {
|
||||
$errors = $errors + $migrator->verify_migrated_data( $order_post_ids );
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates an order from the posts table to the custom orders tables.
|
||||
*
|
||||
* @param int $order_post_id Post ID of the order to migrate.
|
||||
*/
|
||||
public function migrate_order( int $order_post_id ): void {
|
||||
$this->migrate_orders( array( $order_post_id ) );
|
||||
}
|
||||
}
|
||||
@@ -1,893 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Generic migration class to move any entity, entity_meta table combination to custom table.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations;
|
||||
|
||||
/**
|
||||
* Base class for implementing migrations from the standard WordPress meta table
|
||||
* to custom structured tables.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
|
||||
*/
|
||||
abstract class MetaToCustomTableMigrator extends TableMigrator {
|
||||
|
||||
/**
|
||||
* Config for tables being migrated and migrated from. See __construct() for detailed config.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $schema_config;
|
||||
|
||||
/**
|
||||
* Meta config, see __construct for detailed config.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $meta_column_mapping;
|
||||
|
||||
/**
|
||||
* Column mapping from source table to destination custom table. See __construct for detailed config.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $core_column_mapping;
|
||||
|
||||
/**
|
||||
* MetaToCustomTableMigrator constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->schema_config = MigrationHelper::escape_schema_for_backtick( $this->get_schema_config() );
|
||||
$this->meta_column_mapping = $this->get_meta_column_config();
|
||||
$this->core_column_mapping = $this->get_core_column_mapping();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify schema config the source and destination table.
|
||||
*
|
||||
* @return array Schema, must of the form:
|
||||
* array(
|
||||
'source' => array(
|
||||
'entity' => array(
|
||||
'table_name' => $source_table_name,
|
||||
'meta_rel_column' => $column_meta, Name of column in source table which is referenced by meta table.
|
||||
'destination_rel_column' => $column_dest, Name of column in source table which is refenced by destination table,
|
||||
'primary_key' => $primary_key, Primary key of the source table
|
||||
),
|
||||
'meta' => array(
|
||||
'table' => $meta_table_name,
|
||||
'meta_key_column' => $meta_key_column_name,
|
||||
'meta_value_column' => $meta_value_column_name,
|
||||
'entity_id_column' => $entity_id_column, Name of the column having entity IDs.
|
||||
),
|
||||
),
|
||||
'destination' => array(
|
||||
'table_name' => $table_name, Name of destination table,
|
||||
'source_rel_column' => $column_source_id, Name of the column in destination table which is referenced by source table.
|
||||
'primary_key' => $table_primary_key,
|
||||
'primary_key_type' => $type bool|int|string|decimal
|
||||
)
|
||||
*/
|
||||
abstract protected function get_schema_config(): array;
|
||||
|
||||
/**
|
||||
* Specify column config from the source table.
|
||||
*
|
||||
* @return array Config, must be of the form:
|
||||
* array(
|
||||
* '$source_column_name_1' => array( // $source_column_name_1 is column name in source table, or a select statement.
|
||||
* 'type' => 'type of value, could be string/int/date/float.',
|
||||
* 'destination' => 'name of the column in column name where this data should be inserted in.',
|
||||
* ),
|
||||
* '$source_column_name_2' => array(
|
||||
* ......
|
||||
* ),
|
||||
* ....
|
||||
* ).
|
||||
*/
|
||||
abstract protected function get_core_column_mapping(): array;
|
||||
|
||||
/**
|
||||
* Specify meta keys config from source meta table.
|
||||
*
|
||||
* @return array Config, must be of the form.
|
||||
* array(
|
||||
* '$meta_key_1' => array( // $meta_key_1 is the name of meta_key in source meta table.
|
||||
* 'type' => 'type of value, could be string/int/date/float',
|
||||
* 'destination' => 'name of the column in column name where this data should be inserted in.',
|
||||
* ),
|
||||
* '$meta_key_2' => array(
|
||||
* ......
|
||||
* ),
|
||||
* ....
|
||||
* ).
|
||||
*/
|
||||
abstract protected function get_meta_column_config(): array;
|
||||
|
||||
/**
|
||||
* Generate SQL for data insertion.
|
||||
*
|
||||
* @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration_for_ids()` method.
|
||||
*
|
||||
* @return string Generated queries for insertion for this batch, would be of the form:
|
||||
* INSERT IGNORE INTO $table_name ($columns) values
|
||||
* ($value for row 1)
|
||||
* ($value for row 2)
|
||||
* ...
|
||||
*/
|
||||
private function generate_insert_sql_for_batch( array $batch ): string {
|
||||
$table = $this->schema_config['destination']['table_name'];
|
||||
|
||||
list( $value_sql, $column_sql ) = $this->generate_column_clauses( array_merge( $this->core_column_mapping, $this->meta_column_mapping ), $batch );
|
||||
|
||||
return "INSERT INTO $table (`$column_sql`) VALUES $value_sql;"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, -- $insert_query is hardcoded, $value_sql is already escaped.
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SQL for data updating.
|
||||
*
|
||||
* @param array $batch Data to generate queries for. Will be `data` array returned by fetch_data_for_migration_for_ids() method.
|
||||
*
|
||||
* @param array $entity_row_mapping Maps rows to update data with their original IDs. Will be returned by `generate_update_sql_for_batch`.
|
||||
*
|
||||
* @return string Generated queries for batch update. Would be of the form:
|
||||
* INSERT INTO $table ( $columns ) VALUES
|
||||
* ($value for row 1)
|
||||
* ($valye for row 2)
|
||||
* ...
|
||||
* ON DUPLICATE KEY UPDATE
|
||||
* $column1 = VALUES($column1)
|
||||
* $column2 = VALUES($column2)
|
||||
* ...
|
||||
*/
|
||||
private function generate_update_sql_for_batch( array $batch, array $entity_row_mapping ): string {
|
||||
$table = $this->schema_config['destination']['table_name'];
|
||||
|
||||
$destination_primary_id_schema = $this->get_destination_table_primary_id_schema();
|
||||
foreach ( $batch as $entity_id => $row ) {
|
||||
$batch[ $entity_id ][ $destination_primary_id_schema['destination_primary_key']['destination'] ] = $entity_row_mapping[ $entity_id ]->destination_id;
|
||||
}
|
||||
|
||||
list( $value_sql, $column_sql, $columns ) = $this->generate_column_clauses(
|
||||
array_merge( $destination_primary_id_schema, $this->core_column_mapping, $this->meta_column_mapping ),
|
||||
$batch
|
||||
);
|
||||
|
||||
$duplicate_update_key_statement = MigrationHelper::generate_on_duplicate_statement_clause( $columns );
|
||||
|
||||
return "INSERT INTO $table (`$column_sql`) VALUES $value_sql $duplicate_update_key_statement;";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate schema for primary ID column of destination table.
|
||||
*
|
||||
* @return array[] Schema for primary ID column.
|
||||
*/
|
||||
private function get_destination_table_primary_id_schema(): array {
|
||||
return array(
|
||||
'destination_primary_key' => array(
|
||||
'destination' => $this->schema_config['destination']['primary_key'],
|
||||
'type' => $this->schema_config['destination']['primary_key_type'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate values and columns clauses to be used in INSERT and INSERT..ON DUPLICATE KEY UPDATE statements.
|
||||
*
|
||||
* @param array $columns_schema Columns config for destination table.
|
||||
* @param array $batch Actual data to migrate as returned by `data` in `fetch_data_for_migration_for_ids` method.
|
||||
*
|
||||
* @return array SQL clause for values, columns placeholders, and columns.
|
||||
*/
|
||||
private function generate_column_clauses( array $columns_schema, array $batch ): array {
|
||||
global $wpdb;
|
||||
|
||||
$columns = array();
|
||||
$placeholders = array();
|
||||
foreach ( $columns_schema as $prev_column => $schema ) {
|
||||
if ( in_array( $schema['destination'], $columns, true ) ) {
|
||||
continue;
|
||||
}
|
||||
$columns[] = $schema['destination'];
|
||||
$placeholders[] = MigrationHelper::get_wpdb_placeholder_for_type( $schema['type'] );
|
||||
}
|
||||
|
||||
$values = array();
|
||||
foreach ( array_values( $batch ) as $row ) {
|
||||
$row_values = array();
|
||||
foreach ( $columns as $index => $column ) {
|
||||
if ( ! isset( $row[ $column ] ) || is_null( $row[ $column ] ) ) {
|
||||
$row_values[] = 'NULL';
|
||||
} else {
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- $placeholders is a placeholder.
|
||||
$row_values[] = $wpdb->prepare( $placeholders[ $index ], $row[ $column ] );
|
||||
}
|
||||
}
|
||||
|
||||
$value_string = '(' . implode( ',', $row_values ) . ')';
|
||||
$values[] = $value_string;
|
||||
}
|
||||
|
||||
$value_sql = implode( ',', $values );
|
||||
|
||||
$column_sql = implode( '`, `', $columns );
|
||||
|
||||
return array( $value_sql, $column_sql, $columns );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data to be migrated for a batch of entities.
|
||||
*
|
||||
* @param array $entity_ids Ids of entities to migrate.
|
||||
*
|
||||
* @return array[] Data to be migrated. Would be of the form: array( 'data' => array( ... ), 'errors' => array( ... ) ).
|
||||
*/
|
||||
public function fetch_sanitized_migration_data( $entity_ids ) {
|
||||
$this->clear_errors();
|
||||
$data = $this->fetch_data_for_migration_for_ids( $entity_ids );
|
||||
|
||||
foreach ( $data['errors'] as $entity_id => $errors ) {
|
||||
foreach ( $errors as $column_name => $error_message ) {
|
||||
$this->add_error( "Error importing data for post with id $entity_id: column $column_name: $error_message" );
|
||||
}
|
||||
}
|
||||
return array(
|
||||
'data' => $data['data'],
|
||||
'errors' => $this->get_errors(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate a batch of entities from the posts table to the corresponding table.
|
||||
*
|
||||
* @param array $entity_ids Ids of entities to migrate.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function process_migration_batch_for_ids_core( array $entity_ids ): void {
|
||||
$data = $this->fetch_sanitized_migration_data( $entity_ids );
|
||||
$this->process_migration_data( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process migration data for a batch of entities.
|
||||
*
|
||||
* @param array $data Data to be migrated. Should be of the form: array( 'data' => array( ... ) ) as returned by the `fetch_sanitized_migration_data` method.
|
||||
*
|
||||
* @return array Array of errors and exception if any.
|
||||
*/
|
||||
public function process_migration_data( array $data ) {
|
||||
$this->clear_errors();
|
||||
$exception = null;
|
||||
|
||||
if ( count( $data['data'] ) === 0 ) {
|
||||
return array(
|
||||
'errors' => $this->get_errors(),
|
||||
'exception' => null,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$entity_ids = array_keys( $data['data'] );
|
||||
$existing_records = $this->get_already_existing_records( $entity_ids );
|
||||
|
||||
$to_insert = array_diff_key( $data['data'], $existing_records );
|
||||
$this->process_insert_batch( $to_insert );
|
||||
|
||||
$to_update = array_intersect_key( $data['data'], $existing_records );
|
||||
$this->process_update_batch( $to_update, $existing_records );
|
||||
} catch ( \Exception $e ) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
return array(
|
||||
'errors' => $this->get_errors(),
|
||||
'exception' => $exception,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process batch for insertion into destination table.
|
||||
*
|
||||
* @param array $batch Data to insert, will be of the form as returned by `data` in `fetch_data_for_migration_for_ids`.
|
||||
*/
|
||||
private function process_insert_batch( array $batch ): void {
|
||||
if ( 0 === count( $batch ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queries = $this->generate_insert_sql_for_batch( $batch );
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Queries should already be prepared.
|
||||
$processed_rows_count = $this->db_query( $queries );
|
||||
$this->maybe_add_insert_or_update_error( 'insert', $processed_rows_count );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process batch for update into destination table.
|
||||
*
|
||||
* @param array $batch Data to insert, will be of the form as returned by `data` in `fetch_data_for_migration_for_ids`.
|
||||
* @param array $ids_mapping Maps rows to update data with their original IDs.
|
||||
*/
|
||||
private function process_update_batch( array $batch, array $ids_mapping ): void {
|
||||
if ( 0 === count( $batch ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queries = $this->generate_update_sql_for_batch( $batch, $ids_mapping );
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Queries should already be prepared.
|
||||
$processed_rows_count = $this->db_query( $queries ) / 2;
|
||||
$this->maybe_add_insert_or_update_error( 'update', $processed_rows_count );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch data for migration.
|
||||
*
|
||||
* @param array $entity_ids Entity IDs to fetch data for.
|
||||
*
|
||||
* @return array[] Data along with errors (if any), will of the form:
|
||||
* array(
|
||||
* 'data' => array(
|
||||
* 'id_1' => array( 'column1' => value1, 'column2' => value2, ...),
|
||||
* ...,
|
||||
* ),
|
||||
* 'errors' => array(
|
||||
* 'id_1' => array( 'column1' => error1, 'column2' => value2, ...),
|
||||
* ...,
|
||||
* )
|
||||
*/
|
||||
private function fetch_data_for_migration_for_ids( array $entity_ids ): array {
|
||||
if ( empty( $entity_ids ) ) {
|
||||
return array(
|
||||
'data' => array(),
|
||||
'errors' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
$entity_table_query = $this->build_entity_table_query( $entity_ids );
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_entity_table_query is already prepared.
|
||||
$entity_data = $this->db_get_results( $entity_table_query );
|
||||
if ( empty( $entity_data ) ) {
|
||||
return array(
|
||||
'data' => array(),
|
||||
'errors' => array(),
|
||||
);
|
||||
}
|
||||
$entity_meta_rel_ids = array_column( $entity_data, 'entity_meta_rel_id' );
|
||||
|
||||
$meta_table_query = $this->build_meta_data_query( $entity_meta_rel_ids );
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_meta_data_query is already prepared.
|
||||
$meta_data = $this->db_get_results( $meta_table_query );
|
||||
|
||||
return $this->process_and_sanitize_data( $entity_data, $meta_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch id mappings for records that are already inserted in the destination table.
|
||||
*
|
||||
* @param array $entity_ids List of entity IDs to verify.
|
||||
*
|
||||
* @return array Already migrated entities, would be of the form
|
||||
* array(
|
||||
* '$source_id1' => array(
|
||||
* 'source_id' => $source_id1,
|
||||
* 'destination_id' => $destination_id1
|
||||
* 'modified' => 0 if it can be determined that the row doesn't need update, 1 otherwise
|
||||
* ),
|
||||
* ...
|
||||
* )
|
||||
*/
|
||||
protected function get_already_existing_records( array $entity_ids ): array {
|
||||
global $wpdb;
|
||||
|
||||
$source_table = $this->schema_config['source']['entity']['table_name'];
|
||||
$source_destination_join_column = $this->schema_config['source']['entity']['destination_rel_column'];
|
||||
$source_primary_key_column = $this->schema_config['source']['entity']['primary_key'];
|
||||
|
||||
$destination_table = $this->schema_config['destination']['table_name'];
|
||||
$destination_source_join_column = $this->schema_config['destination']['source_rel_column'];
|
||||
$destination_primary_key_column = $this->schema_config['destination']['primary_key'];
|
||||
|
||||
$entity_id_placeholder = implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) );
|
||||
|
||||
$additional_where = $this->get_additional_where_clause_for_get_data_to_insert_or_update( $entity_ids );
|
||||
|
||||
$already_migrated_entity_ids = $this->db_get_results(
|
||||
$wpdb->prepare(
|
||||
// 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
|
||||
JOIN `$source_table` source ON source.`$source_destination_join_column` = destination.`$destination_source_join_column`
|
||||
WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) $additional_where
|
||||
",
|
||||
$entity_ids
|
||||
)
|
||||
// phpcs:enable
|
||||
);
|
||||
|
||||
return array_column( $already_migrated_entity_ids, null, 'source_id' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional string to be appended to the WHERE clause of the SQL query used by get_data_to_insert_or_update.
|
||||
*
|
||||
* @param array $entity_ids The ids of the entities being inserted or updated.
|
||||
* @return string Additional string for the WHERE clause, must either be empty or start with "AND" or "OR".
|
||||
*/
|
||||
protected function get_additional_where_clause_for_get_data_to_insert_or_update( array $entity_ids ): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to build query used to fetch data from core source table.
|
||||
*
|
||||
* @param array $entity_ids List of entity IDs to fetch.
|
||||
*
|
||||
* @return string Query that can be used to fetch data.
|
||||
*/
|
||||
private function build_entity_table_query( array $entity_ids ): string {
|
||||
global $wpdb;
|
||||
|
||||
$source_entity_table = $this->schema_config['source']['entity']['table_name'];
|
||||
$source_meta_rel_id_column = "`$source_entity_table`.`{$this->schema_config['source']['entity']['meta_rel_column']}`";
|
||||
$source_primary_key_column = "`$source_entity_table`.`{$this->schema_config['source']['entity']['primary_key']}`";
|
||||
|
||||
$where_clause = "$source_primary_key_column IN (" . implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) ) . ')';
|
||||
$entity_keys = array();
|
||||
foreach ( $this->core_column_mapping as $column_name => $column_schema ) {
|
||||
if ( isset( $column_schema['select_clause'] ) ) {
|
||||
$select_clause = $column_schema['select_clause'];
|
||||
$entity_keys[] = "$select_clause AS $column_name";
|
||||
} else {
|
||||
$entity_keys[] = "$source_entity_table.$column_name";
|
||||
}
|
||||
}
|
||||
$entity_column_string = implode( ', ', $entity_keys );
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $source_meta_rel_id_column, $source_destination_rel_id_column etc is escaped for backticks. $where clause and $order_by should already be escaped.
|
||||
$query = $wpdb->prepare(
|
||||
"
|
||||
SELECT
|
||||
$source_meta_rel_id_column as entity_meta_rel_id,
|
||||
$source_primary_key_column as primary_key_id,
|
||||
$entity_column_string
|
||||
FROM `$source_entity_table`
|
||||
WHERE $where_clause;
|
||||
",
|
||||
$entity_ids
|
||||
);
|
||||
|
||||
// phpcs:enable
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to build query that will be used to fetch data from source meta table.
|
||||
*
|
||||
* @param array $entity_ids List of IDs to fetch metadata for.
|
||||
*
|
||||
* @return string Query for fetching meta data.
|
||||
*/
|
||||
private function build_meta_data_query( array $entity_ids ): string {
|
||||
global $wpdb;
|
||||
|
||||
$meta_table = $this->schema_config['source']['meta']['table_name'];
|
||||
$meta_keys = array_keys( $this->meta_column_mapping );
|
||||
$meta_key_column = $this->schema_config['source']['meta']['meta_key_column'];
|
||||
$meta_value_column = $this->schema_config['source']['meta']['meta_value_column'];
|
||||
$meta_table_relational_key = $this->schema_config['source']['meta']['entity_id_column'];
|
||||
|
||||
$meta_column_string = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) );
|
||||
$entity_id_string = implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $meta_table_relational_key, $meta_key_column, $meta_value_column and $meta_table is escaped for backticks. $entity_id_string and $meta_column_string are placeholders.
|
||||
$query = $wpdb->prepare(
|
||||
"
|
||||
SELECT `$meta_table_relational_key` as entity_id, `$meta_key_column` as meta_key, `$meta_value_column` as meta_value
|
||||
FROM `$meta_table`
|
||||
WHERE
|
||||
`$meta_table_relational_key` IN ( $entity_id_string )
|
||||
AND `$meta_key_column` IN ( $meta_column_string );
|
||||
",
|
||||
array_merge(
|
||||
$entity_ids,
|
||||
$meta_keys
|
||||
)
|
||||
);
|
||||
|
||||
// phpcs:enable
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to validate and combine data before we try to insert.
|
||||
*
|
||||
* @param array $entity_data Data from source table.
|
||||
* @param array $meta_data Data from meta table.
|
||||
*
|
||||
* @return array[] Validated and combined data with errors.
|
||||
*/
|
||||
private function process_and_sanitize_data( array $entity_data, array $meta_data ): array {
|
||||
$sanitized_entity_data = array();
|
||||
$error_records = array();
|
||||
$this->process_and_sanitize_entity_data( $sanitized_entity_data, $error_records, $entity_data );
|
||||
$this->processs_and_sanitize_meta_data( $sanitized_entity_data, $error_records, $meta_data );
|
||||
|
||||
return array(
|
||||
'data' => $sanitized_entity_data,
|
||||
'errors' => $error_records,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to sanitize core source table.
|
||||
*
|
||||
* @param array $sanitized_entity_data Array containing sanitized data for insertion.
|
||||
* @param array $error_records Error records.
|
||||
* @param array $entity_data Original source data.
|
||||
*/
|
||||
private function process_and_sanitize_entity_data( array &$sanitized_entity_data, array &$error_records, array $entity_data ): void {
|
||||
foreach ( $entity_data as $entity ) {
|
||||
$row_data = array();
|
||||
foreach ( $this->core_column_mapping as $column_name => $schema ) {
|
||||
$custom_table_column_name = $schema['destination'] ?? $column_name;
|
||||
$value = $entity->$column_name;
|
||||
$value = $this->validate_data( $value, $schema['type'] );
|
||||
if ( is_wp_error( $value ) ) {
|
||||
$error_records[ $entity->primary_key_id ][ $custom_table_column_name ] = $value->get_error_code();
|
||||
} else {
|
||||
$row_data[ $custom_table_column_name ] = $value;
|
||||
}
|
||||
}
|
||||
$sanitized_entity_data[ $entity->entity_meta_rel_id ] = $row_data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to sanitize soure meta data.
|
||||
*
|
||||
* @param array $sanitized_entity_data Array containing sanitized data for insertion.
|
||||
* @param array $error_records Error records.
|
||||
* @param array $meta_data Original source data.
|
||||
*/
|
||||
private function processs_and_sanitize_meta_data( array &$sanitized_entity_data, array &$error_records, array $meta_data ): void {
|
||||
foreach ( $meta_data as $datum ) {
|
||||
$column_schema = $this->meta_column_mapping[ $datum->meta_key ];
|
||||
if ( isset( $sanitized_entity_data[ $datum->entity_id ][ $column_schema['destination'] ] ) ) {
|
||||
// We pick only the first meta if there are duplicates for a flat column, to be consistent with WP core behavior in handing duplicate meta which are marked as unique.
|
||||
continue;
|
||||
}
|
||||
$value = $this->validate_data( $datum->meta_value, $column_schema['type'] );
|
||||
if ( is_wp_error( $value ) ) {
|
||||
$error_records[ $datum->entity_id ][ $column_schema['destination'] ] = "{$value->get_error_code()}: {$value->get_error_message()}";
|
||||
} else {
|
||||
$sanitized_entity_data[ $datum->entity_id ][ $column_schema['destination'] ] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and transform data so that we catch as many errors as possible before inserting.
|
||||
*
|
||||
* @param mixed $value Actual data value.
|
||||
* @param string $type Type of data, could be decimal, int, date, string.
|
||||
*
|
||||
* @return float|int|mixed|string|\WP_Error
|
||||
*/
|
||||
private function validate_data( $value, string $type ) {
|
||||
switch ( $type ) {
|
||||
case 'decimal':
|
||||
$value = wc_format_decimal( floatval( $value ), false, true );
|
||||
break;
|
||||
case 'int':
|
||||
$value = (int) $value;
|
||||
break;
|
||||
case 'bool':
|
||||
$value = wc_string_to_bool( $value );
|
||||
break;
|
||||
case 'date':
|
||||
try {
|
||||
if ( '' === $value ) {
|
||||
$value = null;
|
||||
} else {
|
||||
$value = ( new \DateTime( $value ) )->format( 'Y-m-d H:i:s' );
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
return new \WP_Error( $e->getMessage() );
|
||||
}
|
||||
break;
|
||||
case 'date_epoch':
|
||||
try {
|
||||
if ( '' === $value ) {
|
||||
$value = null;
|
||||
} else {
|
||||
$value = ( new \DateTime( "@$value" ) )->format( 'Y-m-d H:i:s' );
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
return new \WP_Error( $e->getMessage() );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
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 );
|
||||
$results = $this->fill_source_metadata( $results, $source_ids );
|
||||
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'];
|
||||
$destination_table = $this->schema_config['destination']['table_name'];
|
||||
$destination_source_rel_column = $this->schema_config['destination']['source_rel_column'];
|
||||
$source_destination_rel_column = $this->schema_config['source']['entity']['destination_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();
|
||||
$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 ) {
|
||||
$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 ) );
|
||||
|
||||
$where_clause = $this->get_where_clause_for_verification( $source_ids );
|
||||
|
||||
return "
|
||||
SELECT $select_clause
|
||||
FROM $source_table
|
||||
LEFT JOIN $source_destination_join_clause
|
||||
WHERE $where_clause
|
||||
";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill source metadata for given IDs for verification. This will return filled data in following format:
|
||||
* [
|
||||
* {
|
||||
* $source_table_$source_column: $value,
|
||||
* ...,
|
||||
* $destination_table_$destination_column: $value,
|
||||
* ...
|
||||
* meta_source_{$destination_column_name1}: $meta_value,
|
||||
* ...
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
*
|
||||
* @param array $results Entity data from both source and destination table.
|
||||
* @param array $source_ids List of source IDs.
|
||||
*
|
||||
* @return array Filled $results param with source metadata.
|
||||
*/
|
||||
private function fill_source_metadata( $results, $source_ids ) {
|
||||
global $wpdb;
|
||||
$meta_table = $this->schema_config['source']['meta']['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'];
|
||||
$meta_id_column = $this->schema_config['source']['meta']['meta_id_column'];
|
||||
$meta_columns = array_keys( $this->meta_column_mapping );
|
||||
|
||||
$meta_columns_placeholder = implode( ', ', array_fill( 0, count( $meta_columns ), '%s' ) );
|
||||
$source_ids_placeholder = implode( ', ', array_fill( 0, count( $source_ids ), '%d' ) );
|
||||
|
||||
$query = $wpdb->prepare(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
"SELECT $meta_entity_id_column as entity_id, $meta_key_column as meta_key, $meta_value_column as meta_value
|
||||
FROM $meta_table
|
||||
WHERE $meta_entity_id_column IN ($source_ids_placeholder)
|
||||
AND $meta_key_column IN ($meta_columns_placeholder)
|
||||
ORDER BY $meta_id_column ASC",
|
||||
array_merge( $source_ids, $meta_columns )
|
||||
);
|
||||
//phpcs:enable
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$meta_data = $wpdb->get_results( $query, ARRAY_A );
|
||||
$source_metadata_rows = array();
|
||||
foreach ( $meta_data as $meta_datum ) {
|
||||
if ( ! isset( $source_metadata_rows[ $meta_datum['entity_id'] ] ) ) {
|
||||
$source_metadata_rows[ $meta_datum['entity_id'] ] = array();
|
||||
}
|
||||
$destination_column = $this->meta_column_mapping[ $meta_datum['meta_key'] ]['destination'];
|
||||
$alias = "meta_source_{$destination_column}";
|
||||
if ( isset( $source_metadata_rows[ $meta_datum['entity_id'] ][ $alias ] ) ) {
|
||||
// Only process first value, duplicate values mapping to flat columns are ignored to be consistent with WP core.
|
||||
continue;
|
||||
}
|
||||
$source_metadata_rows[ $meta_datum['entity_id'] ][ $alias ] = $meta_datum['meta_value'];
|
||||
}
|
||||
foreach ( $results as $index => $result_row ) {
|
||||
$source_id = $result_row[ $this->schema_config['source']['entity']['table_name'] . '_' . $this->schema_config['source']['entity']['primary_key'] ];
|
||||
$results[ $index ] = array_merge( $result_row, ( $source_metadata_rows[ $source_id ] ?? array() ) );
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
"$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 ( ! isset( $row[ $alias ] ) ) {
|
||||
$row[ $alias ] = $this->get_type_defaults( $schema['type'] );
|
||||
}
|
||||
if ( is_null( $row[ $destination_alias ] ) ) {
|
||||
$row[ $destination_alias ] = $this->get_type_defaults( $schema['type'] );
|
||||
}
|
||||
if ( in_array( $schema['type'], array( 'int', 'decimal', 'float' ), true ) ) {
|
||||
if ( '' === $row[ $alias ] || null === $row[ $alias ] ) {
|
||||
$row[ $alias ] = 0; // $wpdb->prepare forces empty values to 0.
|
||||
}
|
||||
$row[ $alias ] = wc_format_decimal( floatval( $row[ $alias ] ), false, true );
|
||||
$row[ $destination_alias ] = wc_format_decimal( floatval( $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;
|
||||
}
|
||||
}
|
||||
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':
|
||||
case 'decimal':
|
||||
return 0;
|
||||
case 'string':
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Generic Migration class to move any meta data associated to an entity, to a different meta table associated with a custom entity table.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations;
|
||||
|
||||
/**
|
||||
* Base class for implementing migrations from the standard WordPress meta table
|
||||
* to custom meta (key-value pairs) tables.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
|
||||
*/
|
||||
abstract class MetaToMetaTableMigrator extends TableMigrator {
|
||||
|
||||
/**
|
||||
* Schema config, see __construct for more details.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $schema_config;
|
||||
|
||||
/**
|
||||
* Returns config for the migration.
|
||||
*
|
||||
* @return array Meta config, must be in following format:
|
||||
* array(
|
||||
* 'source' => array(
|
||||
* 'meta' => array(
|
||||
* 'table_name' => source_meta_table_name,
|
||||
* 'entity_id_column' => entity_id column name in source meta table,
|
||||
* 'meta_key_column' => meta_key column',
|
||||
* 'meta_value_column' => meta_value column',
|
||||
* ),
|
||||
* 'entity' => array(
|
||||
* 'table_name' => entity table name for the meta table,
|
||||
* 'source_id_column' => column name in entity table which maps to meta table,
|
||||
* 'id_column' => id column in entity table,
|
||||
* ),
|
||||
* 'excluded_keys' => array of keys to exclude,
|
||||
* ),
|
||||
* 'destination' => array(
|
||||
* 'meta' => array(
|
||||
* 'table_name' => destination meta table name,
|
||||
* 'entity_id_column' => entity_id column in meta table,
|
||||
* 'meta_key_column' => meta key column,
|
||||
* 'meta_value_column' => meta_value column,
|
||||
* 'entity_id_type' => data type of entity id,
|
||||
* 'meta_id_column' => id column in meta table,
|
||||
* ),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
abstract protected function get_meta_config(): array;
|
||||
|
||||
/**
|
||||
* MetaToMetaTableMigrator constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->schema_config = $this->get_meta_config();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data to be migrated for a batch of entities.
|
||||
*
|
||||
* @param array $entity_ids Ids of entities to migrate.
|
||||
*
|
||||
* @return array[] Data to be migrated. Would be of the form: array( 'data' => array( ... ), 'errors' => array( ... ) ).
|
||||
*/
|
||||
public function fetch_sanitized_migration_data( $entity_ids ) {
|
||||
$this->clear_errors();
|
||||
$to_migrate = $this->fetch_data_for_migration_for_ids( $entity_ids );
|
||||
if ( empty( $to_migrate ) ) {
|
||||
return array(
|
||||
'data' => array(),
|
||||
'errors' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
$already_migrated = $this->get_already_migrated_records( array_keys( $to_migrate ) );
|
||||
|
||||
return array(
|
||||
'data' => $this->classify_update_insert_records( $to_migrate, $already_migrated ),
|
||||
'errors' => $this->get_errors(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate a batch of entities from the posts table to the corresponding table.
|
||||
*
|
||||
* @param array $entity_ids Ids of entities ro migrate.
|
||||
*/
|
||||
protected function process_migration_batch_for_ids_core( array $entity_ids ): void {
|
||||
$sanitized_data = $this->fetch_sanitized_migration_data( $entity_ids );
|
||||
$this->process_migration_data( $sanitized_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process migration data for a batch of entities.
|
||||
*
|
||||
* @param array $data Data to be migrated. Should be of the form: array( 'data' => array( ... ) ) as returned by the `fetch_sanitized_migration_data` method.
|
||||
*
|
||||
* @return array Array of errors and exception if any.
|
||||
*/
|
||||
public function process_migration_data( array $data ) {
|
||||
if ( isset( $data['data'] ) ) {
|
||||
$data = $data['data'];
|
||||
}
|
||||
$this->clear_errors();
|
||||
$exception = null;
|
||||
|
||||
$to_insert = $data[0];
|
||||
$to_update = $data[1];
|
||||
|
||||
try {
|
||||
if ( ! empty( $to_insert ) ) {
|
||||
$insert_queries = $this->generate_insert_sql_for_batch( $to_insert );
|
||||
$processed_rows_count = $this->db_query( $insert_queries );
|
||||
$this->maybe_add_insert_or_update_error( 'insert', $processed_rows_count );
|
||||
}
|
||||
|
||||
if ( ! empty( $to_update ) ) {
|
||||
$update_queries = $this->generate_update_sql_for_batch( $to_update );
|
||||
$processed_rows_count = $this->db_query( $update_queries );
|
||||
$this->maybe_add_insert_or_update_error( 'update', $processed_rows_count );
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
return array(
|
||||
'errors' => $this->get_errors(),
|
||||
'exception' => $exception,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate update SQL for given batch.
|
||||
*
|
||||
* @param array $batch List of data to generate update SQL for. Should be in same format as output of $this->fetch_data_for_migration_for_ids.
|
||||
*
|
||||
* @return string Query to update batch records.
|
||||
*/
|
||||
private function generate_update_sql_for_batch( array $batch ): string {
|
||||
global $wpdb;
|
||||
|
||||
$table = $this->schema_config['destination']['meta']['table_name'];
|
||||
$meta_id_column = $this->schema_config['destination']['meta']['meta_id_column'];
|
||||
$meta_key_column = $this->schema_config['destination']['meta']['meta_key_column'];
|
||||
$meta_value_column = $this->schema_config['destination']['meta']['meta_value_column'];
|
||||
$entity_id_column = $this->schema_config['destination']['meta']['entity_id_column'];
|
||||
$columns = array( $meta_id_column, $entity_id_column, $meta_key_column, $meta_value_column );
|
||||
$columns_sql = implode( '`, `', $columns );
|
||||
|
||||
$entity_id_column_placeholder = MigrationHelper::get_wpdb_placeholder_for_type( $this->schema_config['destination']['meta']['entity_id_type'] );
|
||||
$placeholder_string = "%d, $entity_id_column_placeholder, %s, %s";
|
||||
$values = array();
|
||||
foreach ( $batch as $entity_id => $rows ) {
|
||||
foreach ( $rows as $meta_key => $meta_details ) {
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders
|
||||
$values[] = $wpdb->prepare(
|
||||
"( $placeholder_string )",
|
||||
array( $meta_details['id'], $entity_id, $meta_key, $meta_details['meta_value'] )
|
||||
);
|
||||
// phpcs:enable
|
||||
}
|
||||
}
|
||||
$value_sql = implode( ',', $values );
|
||||
|
||||
$on_duplicate_key_clause = MigrationHelper::generate_on_duplicate_statement_clause( $columns );
|
||||
|
||||
return "INSERT INTO $table ( `$columns_sql` ) VALUES $value_sql $on_duplicate_key_clause";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate insert sql queries for batches.
|
||||
*
|
||||
* @param array $batch Data to generate queries for.
|
||||
*
|
||||
* @return string Insert SQL query.
|
||||
*/
|
||||
private function generate_insert_sql_for_batch( array $batch ): string {
|
||||
global $wpdb;
|
||||
|
||||
$table = $this->schema_config['destination']['meta']['table_name'];
|
||||
$meta_key_column = $this->schema_config['destination']['meta']['meta_key_column'];
|
||||
$meta_value_column = $this->schema_config['destination']['meta']['meta_value_column'];
|
||||
$entity_id_column = $this->schema_config['destination']['meta']['entity_id_column'];
|
||||
$column_sql = "(`$entity_id_column`, `$meta_key_column`, `$meta_value_column`)";
|
||||
|
||||
$entity_id_column_placeholder = MigrationHelper::get_wpdb_placeholder_for_type( $this->schema_config['destination']['meta']['entity_id_type'] );
|
||||
$placeholder_string = "$entity_id_column_placeholder, %s, %s";
|
||||
$values = array();
|
||||
foreach ( $batch as $entity_id => $rows ) {
|
||||
foreach ( $rows as $meta_key => $meta_values ) {
|
||||
foreach ( $meta_values as $meta_value ) {
|
||||
$query_params = array(
|
||||
$entity_id,
|
||||
$meta_key,
|
||||
$meta_value,
|
||||
);
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders
|
||||
$value_sql = $wpdb->prepare( "$placeholder_string", $query_params );
|
||||
$values[] = $value_sql;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$values_sql = implode( '), (', $values );
|
||||
|
||||
return "INSERT IGNORE INTO $table $column_sql VALUES ($values_sql)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch data for migration.
|
||||
*
|
||||
* @param array $entity_ids Array of IDs to fetch data for.
|
||||
*
|
||||
* @return array[] Data, will of the form:
|
||||
* array(
|
||||
* 'id_1' => array( 'column1' => array( value1_1, value1_2...), 'column2' => array(value2_1, value2_2...), ...),
|
||||
* ...,
|
||||
* )
|
||||
*/
|
||||
public function fetch_data_for_migration_for_ids( array $entity_ids ): array {
|
||||
if ( empty( $entity_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$meta_query = $this->build_meta_table_query( $entity_ids );
|
||||
|
||||
$meta_data_rows = $this->db_get_results( $meta_query );
|
||||
if ( empty( $meta_data_rows ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
foreach ( $meta_data_rows as $migrate_row ) {
|
||||
if ( ! isset( $to_migrate[ $migrate_row->entity_id ] ) ) {
|
||||
$to_migrate[ $migrate_row->entity_id ] = array();
|
||||
}
|
||||
|
||||
if ( ! isset( $to_migrate[ $migrate_row->entity_id ][ $migrate_row->meta_key ] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
$to_migrate[ $migrate_row->entity_id ][ $migrate_row->meta_key ] = array();
|
||||
}
|
||||
|
||||
$to_migrate[ $migrate_row->entity_id ][ $migrate_row->meta_key ][] = $migrate_row->meta_value;
|
||||
}
|
||||
|
||||
return $to_migrate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get already migrated records. Will be used to find prevent migration of already migrated records.
|
||||
*
|
||||
* @param array $entity_ids List of entity ids to check for.
|
||||
*
|
||||
* @return array Already migrated records.
|
||||
*/
|
||||
private function get_already_migrated_records( array $entity_ids ): array {
|
||||
global $wpdb;
|
||||
|
||||
$destination_table_name = $this->schema_config['destination']['meta']['table_name'];
|
||||
$destination_id_column = $this->schema_config['destination']['meta']['meta_id_column'];
|
||||
$destination_entity_id_column = $this->schema_config['destination']['meta']['entity_id_column'];
|
||||
$destination_meta_key_column = $this->schema_config['destination']['meta']['meta_key_column'];
|
||||
$destination_meta_value_column = $this->schema_config['destination']['meta']['meta_value_column'];
|
||||
|
||||
$entity_id_type_placeholder = MigrationHelper::get_wpdb_placeholder_for_type( $this->schema_config['destination']['meta']['entity_id_type'] );
|
||||
$entity_ids_placeholder = implode( ',', array_fill( 0, count( $entity_ids ), $entity_id_type_placeholder ) );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
$data_already_migrated = $this->db_get_results(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT
|
||||
$destination_id_column meta_id,
|
||||
$destination_entity_id_column entity_id,
|
||||
$destination_meta_key_column meta_key,
|
||||
$destination_meta_value_column meta_value
|
||||
FROM $destination_table_name destination
|
||||
WHERE destination.$destination_entity_id_column in ( $entity_ids_placeholder ) ORDER BY destination.$destination_entity_id_column
|
||||
",
|
||||
$entity_ids
|
||||
)
|
||||
);
|
||||
// phpcs:enable
|
||||
|
||||
$already_migrated = array();
|
||||
|
||||
foreach ( $data_already_migrated as $migrate_row ) {
|
||||
if ( ! isset( $already_migrated[ $migrate_row->entity_id ] ) ) {
|
||||
$already_migrated[ $migrate_row->entity_id ] = array();
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
if ( ! isset( $already_migrated[ $migrate_row->entity_id ][ $migrate_row->meta_key ] ) ) {
|
||||
$already_migrated[ $migrate_row->entity_id ][ $migrate_row->meta_key ] = array();
|
||||
}
|
||||
|
||||
$already_migrated[ $migrate_row->entity_id ][ $migrate_row->meta_key ][] = array(
|
||||
'id' => $migrate_row->meta_id,
|
||||
'meta_value' => $migrate_row->meta_value,
|
||||
);
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
return $already_migrated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify each record on whether to migrate or update.
|
||||
*
|
||||
* @param array $to_migrate Records to migrate.
|
||||
* @param array $already_migrated Records already migrated.
|
||||
*
|
||||
* @return array[] Returns two arrays, first for records to migrate, and second for records to upgrade.
|
||||
*/
|
||||
private function classify_update_insert_records( array $to_migrate, array $already_migrated ): array {
|
||||
$to_update = array();
|
||||
$to_insert = array();
|
||||
|
||||
foreach ( $to_migrate as $entity_id => $rows ) {
|
||||
foreach ( $rows as $meta_key => $meta_values ) {
|
||||
// If there is no corresponding record in the destination table then insert.
|
||||
// If there is single value in both already migrated and current then update.
|
||||
// If there are multiple values in either already_migrated records or in to_migrate_records, then insert instead of updating.
|
||||
if ( ! isset( $already_migrated[ $entity_id ][ $meta_key ] ) ) {
|
||||
if ( ! isset( $to_insert[ $entity_id ] ) ) {
|
||||
$to_insert[ $entity_id ] = array();
|
||||
}
|
||||
$to_insert[ $entity_id ][ $meta_key ] = $meta_values;
|
||||
} else {
|
||||
if ( 1 === count( $meta_values ) && 1 === count( $already_migrated[ $entity_id ][ $meta_key ] ) ) {
|
||||
if ( $meta_values[0] === $already_migrated[ $entity_id ][ $meta_key ][0]['meta_value'] ) {
|
||||
continue;
|
||||
}
|
||||
if ( ! isset( $to_update[ $entity_id ] ) ) {
|
||||
$to_update[ $entity_id ] = array();
|
||||
}
|
||||
$to_update[ $entity_id ][ $meta_key ] = array(
|
||||
'id' => $already_migrated[ $entity_id ][ $meta_key ][0]['id'],
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
'meta_value' => $meta_values[0],
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// There are multiple meta entries, let's find the unique entries and insert.
|
||||
$unique_meta_values = array_diff( $meta_values, array_column( $already_migrated[ $entity_id ][ $meta_key ], 'meta_value' ) );
|
||||
if ( 0 === count( $unique_meta_values ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( ! isset( $to_insert[ $entity_id ] ) ) {
|
||||
$to_insert[ $entity_id ] = array();
|
||||
}
|
||||
$to_insert[ $entity_id ][ $meta_key ] = $unique_meta_values;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array( $to_insert, $to_update );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to build query used to fetch data from source meta table.
|
||||
*
|
||||
* @param array $entity_ids List of entity IDs to build meta query for.
|
||||
*
|
||||
* @return string Query that can be used to fetch data.
|
||||
*/
|
||||
private function build_meta_table_query( array $entity_ids ): string {
|
||||
global $wpdb;
|
||||
$source_meta_table = $this->schema_config['source']['meta']['table_name'];
|
||||
$source_meta_key_column = $this->schema_config['source']['meta']['meta_key_column'];
|
||||
$source_meta_value_column = $this->schema_config['source']['meta']['meta_value_column'];
|
||||
$source_entity_id_column = $this->schema_config['source']['meta']['entity_id_column'];
|
||||
$order_by = "source.$source_entity_id_column ASC";
|
||||
|
||||
$where_clause = "source.`$source_entity_id_column` IN (" . implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ) . ')';
|
||||
|
||||
$entity_table = $this->schema_config['source']['entity']['table_name'];
|
||||
$entity_id_column = $this->schema_config['source']['entity']['id_column'];
|
||||
$entity_meta_id_mapping_column = $this->schema_config['source']['entity']['source_id_column'];
|
||||
|
||||
if ( $this->schema_config['source']['excluded_keys'] ) {
|
||||
$key_placeholder = implode( ',', array_fill( 0, count( $this->schema_config['source']['excluded_keys'] ), '%s' ) );
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $source_meta_key_column is escaped for backticks, $key_placeholder is hardcoded.
|
||||
$exclude_clause = $wpdb->prepare( "source.$source_meta_key_column NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] );
|
||||
$where_clause = "$where_clause AND $exclude_clause";
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
return $wpdb->prepare(
|
||||
"
|
||||
SELECT
|
||||
source.`$source_entity_id_column` as source_entity_id,
|
||||
entity.`$entity_id_column` as entity_id,
|
||||
source.`$source_meta_key_column` as meta_key,
|
||||
source.`$source_meta_value_column` as meta_value
|
||||
FROM `$source_meta_table` source
|
||||
JOIN `$entity_table` entity ON entity.`$entity_meta_id_mapping_column` = source.`$source_entity_id_column`
|
||||
WHERE $where_clause ORDER BY $order_by
|
||||
",
|
||||
$entity_ids
|
||||
);
|
||||
// phpcs:enable
|
||||
}
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Helper class with utility functions for migrations.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
|
||||
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
use Automattic\WooCommerce\Utilities\StringUtil;
|
||||
|
||||
/**
|
||||
* Helper class to assist with migration related operations.
|
||||
*/
|
||||
class MigrationHelper {
|
||||
|
||||
/**
|
||||
* Placeholders that we will use in building $wpdb queries.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private static $wpdb_placeholder_for_type = array(
|
||||
'int' => '%d',
|
||||
'decimal' => '%f',
|
||||
'string' => '%s',
|
||||
'date' => '%s',
|
||||
'date_epoch' => '%s',
|
||||
'bool' => '%d',
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper method to escape backtick in various schema fields.
|
||||
*
|
||||
* @param array $schema_config Schema config.
|
||||
*
|
||||
* @return array Schema config escaped for backtick.
|
||||
*/
|
||||
public static function escape_schema_for_backtick( array $schema_config ): array {
|
||||
array_walk( $schema_config['source']['entity'], array( self::class, 'escape_and_add_backtick' ) );
|
||||
array_walk( $schema_config['source']['meta'], array( self::class, 'escape_and_add_backtick' ) );
|
||||
array_walk( $schema_config['destination'], array( self::class, 'escape_and_add_backtick' ) );
|
||||
return $schema_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to escape backtick in column and table names.
|
||||
* WP does not provide a method to escape table/columns names yet, but hopefully soon in @link https://core.trac.wordpress.org/ticket/52506
|
||||
*
|
||||
* @param string|array $identifier Column or table name.
|
||||
*
|
||||
* @return array|string|string[] Escaped identifier.
|
||||
*/
|
||||
public static function escape_and_add_backtick( $identifier ) {
|
||||
return '`' . str_replace( '`', '``', $identifier ) . '`';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return $wpdb->prepare placeholder for data type.
|
||||
*
|
||||
* @param string $type Data type.
|
||||
*
|
||||
* @return string $wpdb placeholder.
|
||||
*/
|
||||
public static function get_wpdb_placeholder_for_type( string $type ): string {
|
||||
return self::$wpdb_placeholder_for_type[ $type ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates ON DUPLICATE KEY UPDATE clause to be used in migration.
|
||||
*
|
||||
* @param array $columns List of column names.
|
||||
*
|
||||
* @return string SQL clause for INSERT...ON DUPLICATE KEY UPDATE
|
||||
*/
|
||||
public static function generate_on_duplicate_statement_clause( array $columns ): string {
|
||||
$db_util = wc_get_container()->get( DatabaseUtil::class );
|
||||
return $db_util->generate_on_duplicate_statement_clause( $columns );
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate state codes in all the required places in the database, needed after they change for a given country.
|
||||
*
|
||||
* @param string $country_code The country that has the states for which the migration is needed.
|
||||
* @param array $old_to_new_states_mapping An associative array where keys are the old state codes and values are the new state codes.
|
||||
* @return bool True if there are more records that need to be migrated, false otherwise.
|
||||
*/
|
||||
public static function migrate_country_states( string $country_code, array $old_to_new_states_mapping ): bool {
|
||||
$more_remaining = self::migrate_country_states_for_orders( $country_code, $old_to_new_states_mapping );
|
||||
if ( ! $more_remaining ) {
|
||||
self::migrate_country_states_for_misc_data( $country_code, $old_to_new_states_mapping );
|
||||
}
|
||||
return $more_remaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate state codes in all the required places in the database (except orders).
|
||||
*
|
||||
* @param string $country_code The country that has the states for which the migration is needed.
|
||||
* @param array $old_to_new_states_mapping An associative array where keys are the old state codes and values are the new state codes.
|
||||
* @return void
|
||||
*/
|
||||
private static function migrate_country_states_for_misc_data( string $country_code, array $old_to_new_states_mapping ): void {
|
||||
self::migrate_country_states_for_shipping_locations( $country_code, $old_to_new_states_mapping );
|
||||
self::migrate_country_states_for_tax_rates( $country_code, $old_to_new_states_mapping );
|
||||
self::migrate_country_states_for_store_location( $country_code, $old_to_new_states_mapping );
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate state codes in the shipping locations table.
|
||||
*
|
||||
* @param string $country_code The country that has the states for which the migration is needed.
|
||||
* @param array $old_to_new_states_mapping An associative array where keys are the old state codes and values are the new state codes.
|
||||
* @return void
|
||||
*/
|
||||
private static function migrate_country_states_for_shipping_locations( string $country_code, array $old_to_new_states_mapping ): void {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
$sql = "SELECT location_id, location_code FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE location_code LIKE '{$country_code}:%'";
|
||||
$locations_data = $wpdb->get_results( $sql, ARRAY_A );
|
||||
|
||||
foreach ( $locations_data as $location_data ) {
|
||||
$old_state_code = substr( $location_data['location_code'], 3 );
|
||||
if ( array_key_exists( $old_state_code, $old_to_new_states_mapping ) ) {
|
||||
$new_location_code = "{$country_code}:{$old_to_new_states_mapping[$old_state_code]}";
|
||||
$update_query = $wpdb->prepare(
|
||||
"UPDATE {$wpdb->prefix}woocommerce_shipping_zone_locations SET location_code=%s WHERE location_id=%d",
|
||||
$new_location_code,
|
||||
$location_data['location_id']
|
||||
);
|
||||
$wpdb->query( $update_query );
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the state code for the store location.
|
||||
*
|
||||
* @param string $country_code The country that has the states for which the migration is needed.
|
||||
* @param array $old_to_new_states_mapping An associative array where keys are the old state codes and values are the new state codes.
|
||||
* @return void
|
||||
*/
|
||||
private static function migrate_country_states_for_store_location( string $country_code, array $old_to_new_states_mapping ): void {
|
||||
$store_location = get_option( 'woocommerce_default_country', '' );
|
||||
if ( StringUtil::starts_with( $store_location, "{$country_code}:" ) ) {
|
||||
$old_location_code = substr( $store_location, 3 );
|
||||
if ( array_key_exists( $old_location_code, $old_to_new_states_mapping ) ) {
|
||||
$new_location_code = "{$country_code}:{$old_to_new_states_mapping[$old_location_code]}";
|
||||
update_option( 'woocommerce_default_country', $new_location_code );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate state codes for orders in the orders table and in the posts table.
|
||||
* It will migrate only N*2*(number of states) records, being N equal to 100 by default
|
||||
* but this number can be modified via the woocommerce_migrate_country_states_for_orders_batch_size filter.
|
||||
*
|
||||
* @param string $country_code The country that has the states for which the migration is needed.
|
||||
* @param array $old_to_new_states_mapping An associative array where keys are the old state codes and values are the new state codes.
|
||||
* @return bool True if there are more records that need to be migrated, false otherwise.
|
||||
*/
|
||||
private static function migrate_country_states_for_orders( string $country_code, array $old_to_new_states_mapping ): bool {
|
||||
global $wpdb;
|
||||
|
||||
/**
|
||||
* Filters the value of N, where the maximum count of database records that will be updated in one single run of migrate_country_states_for_orders
|
||||
* is N*2*count($old_to_new_states_mapping) if the woocommerce_orders table exists, or N*count($old_to_new_states_mapping) otherwise.
|
||||
*
|
||||
* @param int $batch_size Default value for the count of records to update.
|
||||
* @param string $country_code Country code for the update.
|
||||
* @param array $old_to_new_states_mapping Associative array of old to new state codes.
|
||||
*
|
||||
* @since 7.2.0
|
||||
*/
|
||||
$limit = apply_filters( 'woocommerce_migrate_country_states_for_orders_batch_size', 100, $country_code, $old_to_new_states_mapping );
|
||||
$cot_exists = wc_get_container()->get( DataSynchronizer::class )->check_orders_table_exists();
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
|
||||
foreach ( $old_to_new_states_mapping as $old_state => $new_state ) {
|
||||
if ( $cot_exists ) {
|
||||
$update_query = $wpdb->prepare(
|
||||
"UPDATE {$wpdb->prefix}wc_order_addresses SET state=%s WHERE country=%s AND state=%s LIMIT %d",
|
||||
$new_state,
|
||||
$country_code,
|
||||
$old_state,
|
||||
$limit
|
||||
);
|
||||
|
||||
$wpdb->query( $update_query );
|
||||
}
|
||||
|
||||
// We need to split the update query for the postmeta table in two, select + update,
|
||||
// because MySQL doesn't support the LIMIT keyword in multi-table UPDATE statements.
|
||||
|
||||
$select_meta_ids_query = $wpdb->prepare(
|
||||
"SELECT meta_id FROM {$wpdb->prefix}postmeta,
|
||||
(SELECT DISTINCT post_id FROM {$wpdb->prefix}postmeta
|
||||
WHERE (meta_key = '_billing_country' OR meta_key='_shipping_country') AND meta_value=%s)
|
||||
AS states_in_country
|
||||
WHERE (meta_key='_billing_state' OR meta_key='_shipping_state')
|
||||
AND meta_value=%s
|
||||
AND {$wpdb->postmeta}.post_id = states_in_country.post_id
|
||||
LIMIT %d",
|
||||
$country_code,
|
||||
$old_state,
|
||||
$limit
|
||||
);
|
||||
|
||||
$meta_ids = $wpdb->get_results( $select_meta_ids_query, ARRAY_A );
|
||||
if ( ! empty( $meta_ids ) ) {
|
||||
$meta_ids = ArrayUtil::select( $meta_ids, 'meta_id' );
|
||||
$meta_ids_as_comma_separated = '(' . join( ',', $meta_ids ) . ')';
|
||||
|
||||
$update_query = $wpdb->prepare(
|
||||
"UPDATE {$wpdb->prefix}postmeta
|
||||
SET meta_value=%s
|
||||
WHERE meta_id IN {$meta_ids_as_comma_separated}",
|
||||
$new_state
|
||||
);
|
||||
|
||||
$wpdb->query( $update_query );
|
||||
}
|
||||
}
|
||||
|
||||
$states_as_comma_separated = "('" . join( "','", array_keys( $old_to_new_states_mapping ) ) . "')";
|
||||
|
||||
$posts_exist_query = $wpdb->prepare(
|
||||
"
|
||||
SELECT 1 FROM {$wpdb->prefix}postmeta
|
||||
WHERE (meta_key='_billing_state' OR meta_key='_shipping_state')
|
||||
AND meta_value IN {$states_as_comma_separated}
|
||||
AND post_id IN (
|
||||
SELECT post_id FROM {$wpdb->prefix}postmeta WHERE
|
||||
(meta_key = '_billing_country' OR meta_key='_shipping_country')
|
||||
AND meta_value=%s
|
||||
)",
|
||||
$country_code
|
||||
);
|
||||
|
||||
if ( $cot_exists ) {
|
||||
$more_exist_query = $wpdb->prepare(
|
||||
"
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM {$wpdb->prefix}wc_order_addresses
|
||||
WHERE country=%s AND state IN {$states_as_comma_separated}
|
||||
)
|
||||
OR EXISTS (
|
||||
{$posts_exist_query}
|
||||
)",
|
||||
$country_code
|
||||
);
|
||||
} else {
|
||||
$more_exist_query = "SELECT EXISTS ({$posts_exist_query})";
|
||||
}
|
||||
|
||||
return (int) ( $wpdb->get_var( $more_exist_query ) ) !== 0;
|
||||
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate state codes in the tax rates table.
|
||||
*
|
||||
* @param string $country_code The country that has the states for which the migration is needed.
|
||||
* @param array $old_to_new_states_mapping An associative array where keys are the old state codes and values are the new state codes.
|
||||
* @return void
|
||||
*/
|
||||
private static function migrate_country_states_for_tax_rates( string $country_code, array $old_to_new_states_mapping ): void {
|
||||
global $wpdb;
|
||||
|
||||
foreach ( $old_to_new_states_mapping as $old_state_code => $new_state_code ) {
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"UPDATE {$wpdb->prefix}woocommerce_tax_rates SET tax_rate_state=%s WHERE tax_rate_country=%s AND tax_rate_state=%s",
|
||||
$new_state_code,
|
||||
$country_code,
|
||||
$old_state_code
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Base class for all the WP posts to order table migrator.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations;
|
||||
|
||||
/**
|
||||
* Base class for implementing WP posts to order tables migrations handlers.
|
||||
* It mainly contains methods to deal with error handling.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
|
||||
*/
|
||||
abstract class TableMigrator {
|
||||
|
||||
/**
|
||||
* An array of cummulated error messages.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $errors;
|
||||
|
||||
/**
|
||||
* Clear the error messages list.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function clear_errors(): void {
|
||||
$this->errors = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an error message to the errors list unless it's there already.
|
||||
*
|
||||
* @param string $error The error message to add.
|
||||
* @return void
|
||||
*/
|
||||
protected function add_error( string $error ): void {
|
||||
if ( is_null( $this->errors ) ) {
|
||||
$this->errors = array();
|
||||
}
|
||||
|
||||
if ( ! in_array( $error, $this->errors, true ) ) {
|
||||
$this->errors[] = $error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of error messages added.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_errors(): array {
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run $wpdb->query and add the error, if any, to the errors list.
|
||||
*
|
||||
* @param string $query The SQL query to run.
|
||||
* @return mixed Whatever $wpdb->query returns.
|
||||
*/
|
||||
protected function db_query( string $query ) {
|
||||
$wpdb = WC()->get_global( 'wpdb' );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$result = $wpdb->query( $query );
|
||||
|
||||
if ( '' !== $wpdb->last_error ) {
|
||||
$this->add_error( $wpdb->last_error );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run $wpdb->get_results and add the error, if any, to the errors list.
|
||||
*
|
||||
* @param string|null $query The SQL query to run.
|
||||
* @param string $output Any of ARRAY_A | ARRAY_N | OBJECT | OBJECT_K constants.
|
||||
* @return mixed Whatever $wpdb->get_results returns.
|
||||
*/
|
||||
protected function db_get_results( string $query = null, string $output = OBJECT ) {
|
||||
$wpdb = WC()->get_global( 'wpdb' );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$result = $wpdb->get_results( $query, $output );
|
||||
|
||||
if ( '' !== $wpdb->last_error ) {
|
||||
$this->add_error( $wpdb->last_error );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate a batch of orders, logging any database error that could arise and the exception thrown if any.
|
||||
*
|
||||
* @param array $entity_ids Order ids to migrate.
|
||||
* @return array An array containing the keys 'errors' (array of strings) and 'exception' (exception object or null).
|
||||
*
|
||||
* @deprecated 8.0.0 Use `fetch_sanitized_migration_data` and `process_migration_data` instead.
|
||||
*/
|
||||
public function process_migration_batch_for_ids( array $entity_ids ): array {
|
||||
$this->clear_errors();
|
||||
$exception = null;
|
||||
|
||||
try {
|
||||
$this->process_migration_batch_for_ids_core( $entity_ids );
|
||||
} catch ( \Exception $ex ) {
|
||||
$exception = $ex;
|
||||
}
|
||||
|
||||
return array(
|
||||
'errors' => $this->get_errors(),
|
||||
'exception' => $exception,
|
||||
);
|
||||
}
|
||||
|
||||
// phpcs:disable Squiz.Commenting.FunctionComment.InvalidNoReturn, Squiz.Commenting.FunctionCommentThrowTag.Missing -- Methods are not marked abstract for back compat.
|
||||
/**
|
||||
* Return data to be migrated for a batch of entities.
|
||||
*
|
||||
* @param array $entity_ids Ids of entities to migrate.
|
||||
*
|
||||
* @return array[] Data to be migrated. Would be of the form: array( 'data' => array( ... ), 'errors' => array( ... ) ).
|
||||
*/
|
||||
public function fetch_sanitized_migration_data( array $entity_ids ) {
|
||||
throw new \Exception( 'Not implemented' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process migration data for a batch of entities.
|
||||
*
|
||||
* @param array $data Data to be migrated. Should be of the form: array( 'data' => array( ... ) ) as returned by the `fetch_sanitized_migration_data` method.
|
||||
*
|
||||
* @return array Array of errors and exception if any.
|
||||
*/
|
||||
public function process_migration_data( array $data ) {
|
||||
throw new \Exception( 'Not implemented' );
|
||||
}
|
||||
// phpcs:enable
|
||||
|
||||
/**
|
||||
* The core method that actually performs the migration for the supplied batch of order ids.
|
||||
* It doesn't need to deal with database errors nor with exceptions.
|
||||
*
|
||||
* @param array $entity_ids Order ids to migrate.
|
||||
* @return void
|
||||
*
|
||||
* @deprecated 8.0.0 Use `fetch_sanitized_migration_data` and `process_migration_data` instead.
|
||||
*/
|
||||
abstract protected function process_migration_batch_for_ids_core( array $entity_ids ): void;
|
||||
|
||||
/**
|
||||
* Check if the amount of processed database rows matches the amount of orders to process, and log an error if not.
|
||||
*
|
||||
* @param string $operation Operation performed, 'insert' or 'update'.
|
||||
* @param array|bool $received_rows_count Value returned by @wpdb after executing the query.
|
||||
* @return void
|
||||
*/
|
||||
protected function maybe_add_insert_or_update_error( string $operation, $received_rows_count ) {
|
||||
if ( false === $received_rows_count ) {
|
||||
$this->add_error( "$operation operation didn't complete, the database query failed" );
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user