Merge pull request #32817 from woocommerce/cot/effectively_sync_posts_to_cot

[COT] Effectively synchronize orders from the posts table to the custom orders table
This commit is contained in:
Vedanshu Jain 2022-05-09 20:32:38 +05:30 committed by GitHub
commit 53a6202d68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 309 additions and 287 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Effectively synchronize orders from the posts table to the custom orders table.

View File

@ -8,7 +8,8 @@ namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
use Automattic\WooCommerce\Database\Migrations\MigrationHelper;
/**
* Class MetaToCustomTableMigrator.
* Base class for implementing migrations from the standard WordPress meta table
* to custom structured tables.
*
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
*/
@ -78,7 +79,7 @@ abstract class MetaToCustomTableMigrator {
'primary_key_type' => $type bool|int|string|decimal
)
*/
abstract public function get_schema_config();
abstract public function get_schema_config(): array;
/**
* Specify column config from the source table.
@ -95,7 +96,7 @@ abstract class MetaToCustomTableMigrator {
* ....
* ).
*/
abstract public function get_core_column_mapping();
abstract public function get_core_column_mapping(): array;
/**
* Specify meta keys config from source meta table.
@ -112,7 +113,7 @@ abstract class MetaToCustomTableMigrator {
* ....
* ).
*/
abstract public function get_meta_column_config();
abstract public function get_meta_column_config(): array;
/**
* Generate SQL for data insertion.
@ -125,7 +126,7 @@ abstract class MetaToCustomTableMigrator {
* ($value for row 2)
* ...
*/
public function generate_insert_sql_for_batch( $batch ) {
public 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 );
@ -150,7 +151,7 @@ abstract class MetaToCustomTableMigrator {
* $column2 = VALUES($column2)
* ...
*/
public function generate_update_sql_for_batch( $batch, $entity_row_mapping ) {
public 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();
@ -173,7 +174,7 @@ abstract class MetaToCustomTableMigrator {
*
* @return array[] Schema for primary ID column.
*/
protected function get_destination_table_primary_id_schema() {
protected function get_destination_table_primary_id_schema(): array {
return array(
'destination_primary_key' => array(
'destination' => $this->schema_config['destination']['primary_key'],
@ -190,7 +191,7 @@ abstract class MetaToCustomTableMigrator {
*
* @return array SQL clause for values, columns placeholders, and columns.
*/
protected function generate_column_clauses( $columns_schema, $batch ) {
protected function generate_column_clauses( array $columns_schema, array $batch ): array {
global $wpdb;
$columns = array();
@ -229,7 +230,7 @@ abstract class MetaToCustomTableMigrator {
*
* @return array List of errors happened during migration.
*/
public function process_migration_batch_for_ids( $entity_ids ) {
public function process_migration_batch_for_ids( array $entity_ids ): array {
$data = $this->fetch_data_for_migration_for_ids( $entity_ids );
foreach ( $data['errors'] as $entity_id => $error ) {
@ -259,7 +260,7 @@ abstract class MetaToCustomTableMigrator {
*
* @param array $batch Data to insert, will be of the form as returned by `data` in `fetch_data_for_migration_for_ids`.
*/
protected function process_insert_batch( $batch ) {
protected function process_insert_batch( array $batch ): void {
global $wpdb;
if ( 0 === count( $batch ) ) {
return;
@ -279,7 +280,7 @@ abstract class MetaToCustomTableMigrator {
* @param array $batch Data to insert, will be of the form as returned by `data` in `fetch_data_for_migration_for_ids`.
* @param array $already_migrated Maps rows to update data with their original IDs.
*/
protected function process_update_batch( $batch, $already_migrated ) {
protected function process_update_batch( array $batch, array $already_migrated ): void {
global $wpdb;
if ( 0 === count( $batch ) ) {
return;
@ -310,7 +311,7 @@ abstract class MetaToCustomTableMigrator {
* ...,
* )
*/
public function fetch_data_for_migration_for_ids( $entity_ids ) {
public function fetch_data_for_migration_for_ids( array $entity_ids ): array {
global $wpdb;
if ( empty( $entity_ids ) ) {
@ -352,7 +353,7 @@ abstract class MetaToCustomTableMigrator {
* ...
* )
*/
public function get_already_migrated_records( $entity_ids ) {
public function get_already_migrated_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'];
@ -389,7 +390,7 @@ WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder )
*
* @return string Query that can be used to fetch data.
*/
protected function build_entity_table_query( $entity_ids ) {
protected 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']}`";
@ -430,7 +431,7 @@ WHERE $where_clause;
*
* @return string Query for fetching meta data.
*/
protected function build_meta_data_query( $entity_ids ) {
protected 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 );
@ -469,7 +470,7 @@ WHERE
*
* @return array[] Validated and combined data with errors.
*/
private function process_and_sanitize_data( $entity_data, $meta_data ) {
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 );
@ -488,7 +489,7 @@ WHERE
* @param array $error_records Error records.
* @param array $entity_data Original source data.
*/
private function process_and_sanitize_entity_data( &$sanitized_entity_data, &$error_records, $entity_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 ) {
@ -512,7 +513,7 @@ WHERE
* @param array $error_records Error records.
* @param array $meta_data Original source data.
*/
private function processs_and_sanitize_meta_data( &$sanitized_entity_data, &$error_records, $meta_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 ];
$value = $this->validate_data( $datum->meta_value, $column_schema['type'] );
@ -532,7 +533,7 @@ WHERE
*
* @return float|int|mixed|string|\WP_Error
*/
private function validate_data( $value, $type ) {
private function validate_data( $value, string $type ) {
switch ( $type ) {
case 'decimal':
$value = (float) $value;

View File

@ -8,9 +8,8 @@ namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
use Automattic\WooCommerce\Database\Migrations\MigrationHelper;
/**
* Class MetaToMetaTableMigrator.
*
* Generic class for powering migrations from one meta table to another table.
* Base class for implementing migrations from the standard WordPress meta table
* to custom meta (key-value pairs) tables.
*
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
*/
@ -61,7 +60,7 @@ abstract class MetaToMetaTableMigrator {
* ),
* )
*/
abstract public function get_meta_config();
abstract public function get_meta_config(): array;
/**
* MetaToMetaTableMigrator constructor.
@ -76,7 +75,7 @@ abstract class MetaToMetaTableMigrator {
*
* @param array $entity_ids Entity IDs to process migration for.
*/
public function process_migration_batch_for_ids( $entity_ids ) {
public function process_migration_batch_for_ids( array $entity_ids ): void {
global $wpdb;
$to_migrate = $this->fetch_data_for_migration_for_ids( $entity_ids );
@ -111,7 +110,7 @@ abstract class MetaToMetaTableMigrator {
*
* @return string Query to update batch records.
*/
public function generate_update_sql_for_batch( $batch ) {
public function generate_update_sql_for_batch( array $batch ): string {
global $wpdb;
$table = $this->schema_config['destination']['meta']['table_name'];
@ -149,7 +148,7 @@ abstract class MetaToMetaTableMigrator {
*
* @return string Insert SQL query.
*/
public function generate_insert_sql_for_batch( $batch ) {
public function generate_insert_sql_for_batch( array $batch ): string {
global $wpdb;
$table = $this->schema_config['destination']['meta']['table_name'];
@ -196,7 +195,7 @@ abstract class MetaToMetaTableMigrator {
* ...,
* )
*/
public function fetch_data_for_migration_for_ids( $entity_ids ) {
public function fetch_data_for_migration_for_ids( array $entity_ids ): array {
global $wpdb;
if ( empty( $entity_ids ) ) {
return array(
@ -241,7 +240,7 @@ abstract class MetaToMetaTableMigrator {
*
* @return array Already migrated records.
*/
private function get_already_migrated_records( $entity_ids ) {
private function get_already_migrated_records( array $entity_ids ): array {
global $wpdb;
$destination_table_name = $this->schema_config['destination']['meta']['table_name'];
@ -298,7 +297,7 @@ WHERE destination.$destination_entity_id_column in ( $entity_ids_placeholder ) O
*
* @return array[] Returns two arrays, first for records to migrate, and second for records to upgrade.
*/
private function classify_update_insert_records( $to_migrate, $already_migrated ) {
private function classify_update_insert_records( array $to_migrate, array $already_migrated ): array {
$to_update = array();
$to_insert = array();
@ -347,7 +346,7 @@ WHERE destination.$destination_entity_id_column in ( $entity_ids_placeholder ) O
*
* @return string Query that can be used to fetch data.
*/
private function build_meta_table_query( $entity_ids ) {
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'];

View File

@ -6,11 +6,12 @@
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
/**
* Class WPPostMetaToOrderMetaMigrator.
* Helper class to migrate records from the WordPress post meta table
* to the custom orders meta table.
*
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
*/
class WPPostMetaToOrderMetaMigrator extends MetaToMetaTableMigrator {
class PostMetaToOrderMetaMigrator extends MetaToMetaTableMigrator {
/**
* List of meta keys to exclude from migration.
@ -20,7 +21,7 @@ class WPPostMetaToOrderMetaMigrator extends MetaToMetaTableMigrator {
private $excluded_columns;
/**
* WPPostMetaToOrderMetaMigrator constructor.
* PostMetaToOrderMetaMigrator constructor.
*
* @param array $excluded_columns List of meta keys to exclude from migration.
*/
@ -34,7 +35,7 @@ class WPPostMetaToOrderMetaMigrator extends MetaToMetaTableMigrator {
*
* @return array Meta data migration config.
*/
public function get_meta_config() {
public function get_meta_config(): array {
global $wpdb;
// TODO: Remove hardcoding.
$this->table_names = array(

View File

@ -6,11 +6,12 @@
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
/**
* Class WPPostToOrderAddressTableMigrator
* Helper class to migrate records from the WordPress post table
* to the custom order addresses table.
*
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
*/
class WPPostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
class PostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
/**
* Type of addresses being migrated, could be billing|shipping.
*
@ -19,7 +20,7 @@ class WPPostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
protected $type;
/**
* WPPostToOrderAddressTableMigrator constructor.
* PostToOrderAddressTableMigrator constructor.
*
* @param string $type Type of addresses being migrated, could be billing|shipping.
*/
@ -33,7 +34,7 @@ class WPPostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
*
* @return array Config.
*/
public function get_schema_config() {
public function get_schema_config(): array {
global $wpdb;
// TODO: Remove hardcoding.
$this->table_names = array(
@ -72,7 +73,7 @@ class WPPostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
*
* @return \string[][] Config.
*/
public function get_core_column_mapping() {
public function get_core_column_mapping(): array {
$type = $this->type;
return array(
@ -93,7 +94,7 @@ class WPPostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
*
* @return \string[][] Config.
*/
public function get_meta_column_config() {
public function get_meta_column_config(): array {
$type = $this->type;
return array(
@ -158,7 +159,7 @@ class WPPostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
* ...
* )
*/
public function get_already_migrated_records( $entity_ids ) {
public function get_already_migrated_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'];

View File

@ -6,18 +6,19 @@
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
/**
* Class WPPostToOrderOpTableMigrator
* Helper class to migrate records from the WordPress post table
* to the custom order operations table.
*
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
*/
class WPPostToOrderOpTableMigrator extends MetaToCustomTableMigrator {
class PostToOrderOpTableMigrator extends MetaToCustomTableMigrator {
/**
* Get schema config for wp_posts and wc_order_operational_detail table.
*
* @return array Config.
*/
public function get_schema_config() {
public function get_schema_config(): array {
global $wpdb;
// TODO: Remove hardcoding.
$this->table_names = array(
@ -57,7 +58,7 @@ class WPPostToOrderOpTableMigrator extends MetaToCustomTableMigrator {
*
* @return \string[][] Config.
*/
public function get_core_column_mapping() {
public function get_core_column_mapping(): array {
return array(
'id' => array(
'type' => 'int',
@ -72,7 +73,7 @@ class WPPostToOrderOpTableMigrator extends MetaToCustomTableMigrator {
*
* @return \string[][] Config.
*/
public function get_meta_column_config() {
public function get_meta_column_config(): array {
return array(
'_created_via' => array(
'type' => 'string',

View File

@ -6,16 +6,18 @@
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
/**
* Class WPPostToOrderTableMigrator.
* 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 WPPostToOrderTableMigrator extends MetaToCustomTableMigrator {
class PostToOrderTableMigrator extends MetaToCustomTableMigrator {
/**
* Get schema config for wp_posts and wc_order table.
*
* @return array Config.
*/
public function get_schema_config() {
public function get_schema_config(): array {
global $wpdb;
// TODO: Remove hardcoding.
@ -55,7 +57,7 @@ class WPPostToOrderTableMigrator extends MetaToCustomTableMigrator {
*
* @return \string[][] Config.
*/
public function get_core_column_mapping() {
public function get_core_column_mapping(): array {
return array(
'ID' => array(
'type' => 'int',
@ -85,7 +87,7 @@ class WPPostToOrderTableMigrator extends MetaToCustomTableMigrator {
*
* @return \string[][] Config.
*/
public function get_meta_column_config() {
public function get_meta_column_config(): array {
return array(
'_order_currency' => array(
'type' => 'string',

View File

@ -0,0 +1,102 @@
<?php
/**
* Class for implementing migration from wp_posts and wp_postmeta to custom order tables.
*/
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
use Automattic\WooCommerce\Database\Migrations\MigrationErrorLogger;
/**
* 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 MigrationErrorLogger $error_logger
*/
private $error_logger;
/**
* Migrator instance to migrate data into wc_order table.
*
* @var PostToOrderTableMigrator
*/
private $order_table_migrator;
/**
* Migrator instance to migrate billing data into address table.
*
* @var PostToOrderAddressTableMigrator
*/
private $billing_address_table_migrator;
/**
* Migrator instance to migrate shipping data into address table.
*
* @var PostToOrderAddressTableMigrator
*/
private $shipping_address_table_migrator;
/**
* Migrator instance to migrate operational data.
*
* @var PostToOrderOpTableMigrator
*/
private $operation_data_table_migrator;
/**
* Migrator instance to migrate meta data.
*
* @var MetaToMetaTableMigrator
*/
private $meta_table_migrator;
/**
* PostsToOrdersMigrationController constructor.
*/
public function __construct() {
$this->order_table_migrator = new PostToOrderTableMigrator();
$this->billing_address_table_migrator = new PostToOrderAddressTableMigrator( 'billing' );
$this->shipping_address_table_migrator = new PostToOrderAddressTableMigrator( 'shipping' );
$this->operation_data_table_migrator = new PostToOrderOpTableMigrator();
$excluded_columns = array_keys( $this->order_table_migrator->get_meta_column_config() );
$excluded_columns = array_merge( $excluded_columns, array_keys( $this->billing_address_table_migrator->get_meta_column_config() ) );
$excluded_columns = array_merge( $excluded_columns, array_keys( $this->shipping_address_table_migrator->get_meta_column_config() ) );
$excluded_columns = array_merge( $excluded_columns, array_keys( $this->operation_data_table_migrator->get_meta_column_config() ) );
$this->meta_table_migrator = new PostMetaToOrderMetaMigrator( $excluded_columns );
$this->error_logger = new MigrationErrorLogger();
}
/**
* 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->order_table_migrator->process_migration_batch_for_ids( $order_post_ids );
$this->billing_address_table_migrator->process_migration_batch_for_ids( $order_post_ids );
$this->shipping_address_table_migrator->process_migration_batch_for_ids( $order_post_ids );
$this->operation_data_table_migrator->process_migration_batch_for_ids( $order_post_ids );
$this->meta_table_migrator->process_migration_batch_for_ids( $order_post_ids );
// TODO: Return merged error array.
}
/**
* 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 ) );
// TODO: Return error.
}
}

View File

@ -1,170 +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\Database\Migrations\MigrationErrorLogger;
/**
* Class WPPostToCOTMigrator
*
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
*/
class WPPostToCOTMigrator {
/**
* Error logger for migration errors.
*
* @var MigrationErrorLogger $error_logger
*/
private $error_logger;
/**
* Migrator instance to migrate data into wc_order table.
*
* @var WPPostToOrderTableMigrator
*/
private $order_table_migrator;
/**
* Migrator instance to migrate billing data into address table.
*
* @var WPPostToOrderAddressTableMigrator
*/
private $billing_address_table_migrator;
/**
* Migrator instance to migrate shipping data into address table.
*
* @var WPPostToOrderAddressTableMigrator
*/
private $shipping_address_table_migrator;
/**
* Migrator instance to migrate operational data.
*
* @var WPPostToOrderOpTableMigrator
*/
private $operation_data_table_migrator;
/**
* Migrator instance to migrate meta data.
*
* @var MetaToMetaTableMigrator
*/
private $meta_table_migrator;
/**
* WPPostToCOTMigrator constructor.
*/
public function __construct() {
$this->order_table_migrator = new WPPostToOrderTableMigrator();
$this->billing_address_table_migrator = new WPPostToOrderAddressTableMigrator( 'billing' );
$this->shipping_address_table_migrator = new WPPostToOrderAddressTableMigrator( 'shipping' );
$this->operation_data_table_migrator = new WPPostToOrderOpTableMigrator();
$excluded_columns = array_keys( $this->order_table_migrator->get_meta_column_config() );
$excluded_columns = array_merge( $excluded_columns, array_keys( $this->billing_address_table_migrator->get_meta_column_config() ) );
$excluded_columns = array_merge( $excluded_columns, array_keys( $this->shipping_address_table_migrator->get_meta_column_config() ) );
$excluded_columns = array_merge( $excluded_columns, array_keys( $this->operation_data_table_migrator->get_meta_column_config() ) );
$this->meta_table_migrator = new WPPostMetaToOrderMetaMigrator( $excluded_columns );
$this->error_logger = new MigrationErrorLogger();
}
/**
* Process next migration batch, uses option `wc_cot_migration` to checkpoints of what have been processed so far.
*
* @param int $batch_size Batch size of records to migrate.
*
* @return bool True if migration is completed, false if there are still records to process.
*/
public function process_next_migration_batch( $batch_size = 100 ) {
$order_post_ids = $this->get_next_batch_ids( $batch_size );
if ( 0 === count( $order_post_ids ) ) {
return true;
}
$this->process_migration_for_ids( $order_post_ids );
$last_post_migrated = max( $order_post_ids );
$this->update_checkpoint( $last_post_migrated );
return false;
}
/**
* Process migration for specific order post IDs.
*
* @param array $order_post_ids List of post IDs to migrate.
*/
public function process_migration_for_ids( $order_post_ids ) {
$this->order_table_migrator->process_migration_batch_for_ids( $order_post_ids );
$this->billing_address_table_migrator->process_migration_batch_for_ids( $order_post_ids );
$this->shipping_address_table_migrator->process_migration_batch_for_ids( $order_post_ids );
$this->operation_data_table_migrator->process_migration_batch_for_ids( $order_post_ids );
$this->meta_table_migrator->process_migration_batch_for_ids( $order_post_ids );
// TODO: Return merged error array.
}
/**
* Method to migrate single record.
*
* @param int $post_id Post ID of record to migrate.
*/
public function process_single( $post_id ) {
$this->process_migration_for_ids( array( $post_id ) );
// TODO: Return error.
}
/**
* Helper function to get where clause to send to MetaToCustomTableMigrator instance.
*
* @param int $batch_size Number of orders in batch.
*
* @return array List of IDs in the current patch.
*/
private function get_next_batch_ids( $batch_size ) {
global $wpdb;
$checkpoint = $this->get_checkpoint();
$post_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT ID FROM $wpdb->posts WHERE ID > %d AND post_type = %s ORDER BY ID ASC LIMIT %d ",
$checkpoint['id'],
'shop_order',
$batch_size
)
);
return $post_ids;
}
/**
* Current checkpoint status.
*
* @return false|mixed|void
*/
private function get_checkpoint() {
return get_option( 'wc_cot_migration', array( 'id' => 0 ) );
}
/**
* Updates current checkpoint
*
* @param int $id Order ID.
*/
public function update_checkpoint( $id ) {
return update_option( 'wc_cot_migration', array( 'id' => $id ), false );
}
/**
* Remove checkpoint.
*
* @return bool Whether checkpoint was removed.
*/
public function delete_checkpoint() {
return delete_option( 'wp_cot_migration' );
}
}

View File

@ -6,8 +6,6 @@
namespace Automattic\WooCommerce\Database\Migrations;
/**
* Class MigrationErrorLogger.
*
* Error logging for custom table migrations.
*
* @package Automattic\WooCommerce\Database\Migrations

View File

@ -6,8 +6,6 @@
namespace Automattic\WooCommerce\Database\Migrations;
/**
* Class MigrationHelper.
*
* Helper class to assist with migration related operations.
*/
class MigrationHelper {
@ -33,7 +31,7 @@ class MigrationHelper {
*
* @return string Insert clause.
*/
public static function get_insert_switch( $switch ) {
public static function get_insert_switch( string $switch ): string {
switch ( $switch ) {
case 'insert_ignore':
$insert_query = 'INSERT IGNORE';
@ -59,7 +57,7 @@ class MigrationHelper {
*
* @return array Schema config escaped for backtick.
*/
public static function escape_schema_for_backtick( $schema_config ) {
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' ) );
@ -85,7 +83,7 @@ class MigrationHelper {
*
* @return string $wpdb placeholder.
*/
public static function get_wpdb_placeholder_for_type( $type ) {
public static function get_wpdb_placeholder_for_type( string $type ): string {
return self::$wpdb_placeholder_for_type[ $type ];
}
@ -96,7 +94,7 @@ class MigrationHelper {
*
* @return string SQL clause for INSERT...ON DUPLICATE KEY UPDATE
*/
public static function generate_on_duplicate_statement_clause( $columns ) {
public static function generate_on_duplicate_statement_clause( array $columns ): string {
$update_value_statements = array();
foreach ( $columns as $column ) {
$update_value_statements[] = "$column = VALUES( $column )";

View File

@ -23,6 +23,11 @@ class CustomOrdersTableController {
*/
public const CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION = 'woocommerce_custom_orders_table_enabled';
/**
* The name of the option that tells that the authoritative table must be flipped once sync finishes.
*/
private const AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION = 'woocommerce_auto_flip_authoritative_table_roles';
/**
* The data store object to use.
*
@ -371,7 +376,7 @@ class CustomOrdersTableController {
__( 'Switch to using the orders table as the authoritative data store for orders when sync finishes', 'woocommerce' );
$settings[] = array(
'desc' => $message,
'id' => DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION,
'id' => self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION,
'type' => 'checkbox',
);
}
@ -422,10 +427,14 @@ class CustomOrdersTableController {
return $value;
}
// TODO: Re-enable the following code once the COT to posts table sync is implemented (it's currently disabled to ease testing).
/*
$sync_is_pending = 0 !== $this->data_synchronizer->get_current_orders_pending_sync_count();
if ( $sync_is_pending ) {
throw new \Exception( "The authoritative table for orders storage can't be changed while there are orders out of sync" );
}
*/
return $value;
}
@ -435,11 +444,11 @@ class CustomOrdersTableController {
* Here we switch the authoritative table if needed.
*/
private function process_sync_finished() {
if ( $this->auto_flip_authoritative_table_enabled() ) {
if ( ! $this->auto_flip_authoritative_table_enabled() ) {
return;
}
update_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' );
update_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' );
if ( $this->custom_orders_table_usage_is_enabled() ) {
update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
@ -454,7 +463,7 @@ class CustomOrdersTableController {
* @return bool
*/
private function auto_flip_authoritative_table_enabled(): bool {
return 'yes' === get_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION );
return 'yes' === get_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION );
}
/**
@ -465,16 +474,14 @@ class CustomOrdersTableController {
// Disabling the sync implies disabling the automatic authoritative table switch too.
if ( ! $data_sync_is_enabled && $this->auto_flip_authoritative_table_enabled() ) {
update_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' );
update_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' );
}
// Enabling the sync implies starting it too, if needed.
// We do this check here, and not in process_pre_update_option, so that if for some reason
// the setting is enabled but no sync is in process, sync will start by just saving the
// settings even without modifying them.
if ( $data_sync_is_enabled && ! $this->data_synchronizer->pending_data_sync_is_in_progress() ) {
$this->data_synchronizer->start_synchronizing_pending_orders();
}
$this->data_synchronizer->maybe_start_synchronizing_pending_orders( true );
}
/**

View File

@ -5,6 +5,7 @@
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
defined( 'ABSPATH' ) || exit;
@ -17,13 +18,15 @@ defined( 'ABSPATH' ) || exit;
*/
class DataSynchronizer {
public const ORDERS_DATA_SYNC_ENABLED_OPTION = 'woocommerce_custom_orders_table_data_sync_enabled';
private const INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_initial_orders_pending_sync_count';
public const AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION = 'woocommerce_auto_flip_authoritative_table_roles';
private const PENDING_SYNC_IS_IN_PROGRESS_OPTION = 'woocommerce_custom_orders_table_pending_sync_in_progress';
private const ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK = 'woocommerce_run_orders_sync_callback';
public const PENDING_SYNCHRONIZATION_FINISHED_ACTION = 'woocommerce_orders_sync_finished';
public const PLACEHOLDER_ORDER_POST_TYPE = 'shop_order_placehold';
public const ORDERS_DATA_SYNC_ENABLED_OPTION = 'woocommerce_custom_orders_table_data_sync_enabled';
private const INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_initial_orders_pending_sync_count';
private const PENDING_SYNC_IS_IN_PROGRESS_OPTION = 'woocommerce_custom_orders_table_pending_sync_in_progress';
private const ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK = 'woocommerce_run_orders_sync_callback';
public const PENDING_SYNCHRONIZATION_FINISHED_ACTION = 'woocommerce_orders_sync_finished';
public const PLACEHOLDER_ORDER_POST_TYPE = 'shop_order_placehold';
private const ORDERS_SYNC_BATCH_SIZE = 250;
private const SECONDS_BETWEEN_BATCH_SYNCS = 5;
// Allowed values for $type in get_ids_of_orders_pending_sync method.
public const ID_TYPE_MISSING_IN_ORDERS_TABLE = 0;
@ -47,6 +50,13 @@ class DataSynchronizer {
*/
private $database_util;
/**
* The posts to COT migrator to use.
*
* @var DatabaseUtil
*/
private $posts_to_cot_migrator;
/**
* Class constructor.
*/
@ -57,18 +67,27 @@ class DataSynchronizer {
$this->do_pending_orders_synchronization();
}
);
add_action(
'woocommerce_after_order_object_save',
function() {
$this->maybe_start_synchronizing_pending_orders();
}
);
}
/**
* Class initialization, invoked by the DI container.
*
* @internal
* @param OrdersTableDataStore $data_store The data store to use.
* @param DatabaseUtil $database_util The database util class to use.
* @param OrdersTableDataStore $data_store The data store to use.
* @param DatabaseUtil $database_util The database util class to use.
* @param PostsToOrdersMigrationController $posts_to_cot_migrator The posts to COT migration class to use.
*@internal
*/
final public function init( OrdersTableDataStore $data_store, DatabaseUtil $database_util ) {
$this->data_store = $data_store;
$this->database_util = $database_util;
final public function init( OrdersTableDataStore $data_store, DatabaseUtil $database_util, PostsToOrdersMigrationController $posts_to_cot_migrator ) {
$this->data_store = $data_store;
$this->database_util = $database_util;
$this->posts_to_cot_migrator = $posts_to_cot_migrator;
}
/**
@ -128,7 +147,6 @@ class DataSynchronizer {
return array(
'initial_pending_count' => (int) get_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION, 0 ),
'current_pending_count' => $this->get_current_orders_pending_sync_count(),
'auto_flip' => 'yes' === get_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ),
'sync_in_progress' => $this->pending_data_sync_is_in_progress(),
);
}
@ -156,7 +174,7 @@ class DataSynchronizer {
$orders_table = $wpdb->prefix . 'wc_orders';
if ( 'yes' === get_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) ) {
if ( $this->custom_orders_table_is_authoritative() ) {
$missing_orders_count_sql = "
SELECT COUNT(1) FROM $wpdb->posts posts
INNER JOIN $orders_table orders ON posts.id=orders.id
@ -188,6 +206,15 @@ SELECT(
return (int) $wpdb->get_var( $sql );
}
/**
* Is the custom orders table the authoritative data source for orders currently?
*
* @return bool Whether the custom orders table the authoritative data source for orders currently.
*/
private function custom_orders_table_is_authoritative(): bool {
return 'yes' === get_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION );
}
/**
* Get a list of ids of orders than are out of sync.
*
@ -244,10 +271,21 @@ WHERE
}
/**
* Start an orders synchronization process.
* This will setup the appropriate status information and schedule the first synchronization batch.
* Start an orders synchronization process if all the following is true:
*
* 1. Data synchronization is enabled.
* 2. Data synchronization isn't already in progress ($force can be used to bypass this).
* 3. There's at least one out of sync order.
*
* This will set up the appropriate status information and schedule the first synchronization batch.
*
* @param bool $force If true, (re)start the sync process even if it's already in progress.
*/
public function start_synchronizing_pending_orders() {
public function maybe_start_synchronizing_pending_orders( bool $force = false ) {
if ( ! $this->data_sync_is_enabled() || ( $this->pending_data_sync_is_in_progress() && ! $force ) ) {
return;
}
$initial_pending_count = $this->get_current_orders_pending_sync_count();
if ( 0 === $initial_pending_count ) {
return;
@ -268,7 +306,7 @@ WHERE
private function schedule_pending_orders_synchronization() {
$queue = WC()->get_instance_of( \WC_Queue::class );
$queue->schedule_single(
WC()->call_function( 'time' ) + 1,
WC()->call_function( 'time' ) + self::SECONDS_BETWEEN_BATCH_SYNCS,
self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK,
array(),
'woocommerce-db-updates'
@ -287,9 +325,8 @@ WHERE
$fake_count = get_option( self::FAKE_ORDERS_PENDING_SYNC_COUNT_OPTION );
if ( false !== $fake_count ) {
update_option( 'woocommerce_fake_orders_pending_sync_count', (int) $fake_count - 1 );
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElse
} else {
// TODO: Use get_ids_of_orders_pending_sync to get a batch of order ids and syncrhonize them.
$this->sync_next_batch();
}
if ( 0 === $this->get_current_orders_pending_sync_count() ) {
@ -304,6 +341,49 @@ WHERE
}
}
/**
* Processes a batch of out of sync orders.
* First it synchronizes orders that don't exist in the backup table, and after that,
* it synchronizes orders that exist in both tables but have a different last update date.
*
* @return void
*/
private function sync_next_batch(): void {
/**
* Filter to customize the count of orders that will be synchronized in each step of the custom orders table to/from posts table synchronization process.
*
* @since 6.6.0
*
* @param int Default value for the count.
*/
$batch_size = apply_filters( 'woocommerce_orders_cot_and_posts_sync_step_size', self::ORDERS_SYNC_BATCH_SIZE );
if ( $this->custom_orders_table_is_authoritative() ) {
$order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_MISSING_IN_POSTS_TABLE, $batch_size );
// TODO: Load $order_ids orders from the orders table and create them (by updating the corresponding placeholder record) in the posts table.
} else {
$order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_MISSING_IN_ORDERS_TABLE, $batch_size );
$this->posts_to_cot_migrator->migrate_orders( $order_ids );
}
$batch_size -= count( $order_ids );
if ( 0 === $batch_size ) {
return;
}
$order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_DIFFERENT_UPDATE_DATE, $batch_size );
if ( 0 === count( $order_ids ) ) {
return;
}
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
if ( $this->custom_orders_table_is_authoritative() ) {
// TODO: Load $order_ids orders from the orders table and update them in the posts table.
} else {
$this->posts_to_cot_migrator->migrate_orders( $order_ids );
}
}
/**
* Cleanup all the synchronization status information,
* because the process has been disabled by the user via settings,
@ -312,6 +392,5 @@ WHERE
public function cleanup_synchronization_state() {
delete_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION );
delete_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION );
delete_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION );
}
}

View File

@ -5,7 +5,7 @@
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\WPPostToCOTMigrator;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
/**
@ -21,7 +21,7 @@ class COTMigrationServiceProvider extends AbstractServiceProvider {
* @var string[]
*/
protected $provides = array(
WPPostToCOTMigrator::class,
PostsToOrdersMigrationController::class,
);
/**
@ -32,6 +32,6 @@ class COTMigrationServiceProvider extends AbstractServiceProvider {
* @return void
*/
public function register() {
$this->share( WPPostToCOTMigrator::class );
$this->share( PostsToOrdersMigrationController::class );
}
}

View File

@ -5,6 +5,7 @@
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
@ -31,7 +32,7 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
* Register the classes.
*/
public function register() {
$this->share( DataSynchronizer::class )->addArguments( array( OrdersTableDataStore::class, DatabaseUtil::class ) );
$this->share( DataSynchronizer::class )->addArguments( array( OrdersTableDataStore::class, DatabaseUtil::class, PostsToOrdersMigrationController::class ) );
$this->share( CustomOrdersTableController::class )->addArguments( array( OrdersTableDataStore::class, DataSynchronizer::class ) );
$this->share( OrdersTableDataStore::class );
}

View File

@ -1,9 +1,9 @@
<?php
/**
* Tests for WPPostToCOTMigrator class.
* Tests for PostsToOrdersMigrationController class.
*/
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\WPPostToCOTMigrator;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper;
@ -11,9 +11,9 @@ use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ShippingHelper;
/**
* Class WPPostToCOTMigratorTest.
* Class PostsToOrdersMigrationControllerTest.
*/
class WPPostToCOTMigratorTest extends WC_Unit_Test_Case {
class PostsToOrdersMigrationControllerTest extends WC_Unit_Test_Case {
/**
* @var DataSynchronizer
@ -21,7 +21,7 @@ class WPPostToCOTMigratorTest extends WC_Unit_Test_Case {
private $synchronizer;
/**
* @var WPPostToCOTMigrator
* @var PostsToOrdersMigrationController
*/
private $sut;
@ -37,16 +37,16 @@ class WPPostToCOTMigratorTest extends WC_Unit_Test_Case {
parent::setUp();
OrderHelper::create_order_custom_table_if_not_exist();
$this->data_store = wc_get_container()->get( OrdersTableDataStore::class );
$this->sut = wc_get_container()->get( WPPostToCOTMigrator::class );
$this->sut = wc_get_container()->get( PostsToOrdersMigrationController::class );
}
/**
* Test that migration for a normal order happens as expected.
*/
public function test_process_next_migration_batch_normal_order() {
public function test_migration_for_normal_order() {
$order = wc_get_order( OrderHelper::create_complex_wp_post_order() );
$this->clear_all_orders_and_reset_checkpoint();
$this->sut->process_next_migration_batch( 100 );
$this->clear_all_orders();
$this->sut->migrate_order( $order->get_id() );
$this->assert_core_data_is_migrated( $order );
$this->assert_order_addresses_are_migrated( $order );
@ -57,17 +57,16 @@ class WPPostToCOTMigratorTest extends WC_Unit_Test_Case {
/**
* Test that already migrated order isn't migrated twice.
*/
public function test_process_next_migration_batch_already_migrated_order() {
public function test_migration_for_already_migrated_order() {
global $wpdb;
$order = wc_get_order( OrderHelper::create_complex_wp_post_order() );
$this->clear_all_orders_and_reset_checkpoint();
$this->clear_all_orders();
// Run the migration once.
$this->sut->process_next_migration_batch( 100 );
$this->sut->migrate_order( $order->get_id() );
// Delete checkpoint and run migration again, assert there are still no duplicates.
$this->sut->update_checkpoint( 0 );
$this->sut->process_next_migration_batch( 100 );
// Run the migration again, assert there are still no duplicates.
$this->sut->migrate_order( $order->get_id() );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->assertEquals(
@ -132,21 +131,21 @@ WHERE order_id = {$order_id} AND meta_key = 'non_unique_key_1' AND meta_value in
/**
* Test that when an order is partially migrated, it can still be resumed as expected.
*/
public function test_process_next_migration_batch_interrupted_migrating_order() {
public function test_interrupted_migration() {
$this->markTestSkipped();
}
/**
* Test that invalid order data is not migrated but logged.
*/
public function test_process_next_migration_batch_invalid_order_data() {
public function test_migrating_invalid_order_data() {
$this->markTestSkipped();
}
/**
* Test when one order is invalid but other one is valid in a migration batch.
*/
public function test_process_next_migration_batch_invalid_valid_order_combo() {
public function test_migrating_invalid_valid_order_combo() {
$this->markTestSkipped();
}
@ -352,13 +351,12 @@ WHERE order_id = {$order_id} AND meta_key = 'non_unique_key_1' AND meta_value in
/**
* Helper method to clear checkout and truncate order tables.
*/
private function clear_all_orders_and_reset_checkpoint() {
private function clear_all_orders() {
global $wpdb;
$order_tables = $this->data_store->get_all_table_names();
foreach ( $order_tables as $table ) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query( "TRUNCATE table $table;" );
}
$this->sut->delete_checkpoint();
}
}

View File

@ -1,6 +1,6 @@
<?php
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\WPPostToCOTMigrator;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
@ -12,7 +12,7 @@ use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
/**
* @var WPPostToCOTMigrator
* @var PostsToOrdersMigrationController
*/
private $migrator;
@ -36,7 +36,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
OrderHelper::create_order_custom_table_if_not_exist();
$this->sut = wc_get_container()->get( OrdersTableDataStore::class );
$this->migrator = wc_get_container()->get( WPPostToCOTMigrator::class );
$this->migrator = wc_get_container()->get( PostsToOrdersMigrationController::class );
$this->cpt_data_store = new WC_Order_Data_Store_CPT();
// Add back removed filter.
add_filter( 'query', array( $this, '_create_temporary_tables' ) );
@ -48,7 +48,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
*/
public function test_read_from_migrated_order() {
$post_order_id = OrderHelper::create_complex_wp_post_order();
$this->migrator->process_migration_for_ids( array( $post_order_id ) );
$this->migrator->migrate_orders( array( $post_order_id ) );
$cot_order = new WC_Order();
$cot_order->set_id( $post_order_id );