Merge pull request #32034 from woocommerce/cot/31766
Add migration to move orders from wp_posts to custom tables
This commit is contained in:
commit
e9e382adf8
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Generic migration support for migration from posts + postsmeta table to any custom table. Additionaly, implement migrations to various COT tables using this generic support.
|
|
@ -640,7 +640,7 @@ abstract class WC_Data {
|
|||
}
|
||||
}
|
||||
if ( ! empty( $this->cache_group ) ) {
|
||||
$cache_key = WC_Cache_Helper::get_cache_prefix( $this->cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $this->get_id() ) . 'object_meta_' . $this->get_id();
|
||||
$cache_key = self::generate_meta_cache_key( $this->get_id(), $this->cache_group );
|
||||
wp_cache_delete( $cache_key, $this->cache_group );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
namespace Automattic\WooCommerce;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\COTMigrationServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\DownloadPermissionsAdjusterServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\AssignDefaultCategoryServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrdersDataStoreServiceProvider;
|
||||
|
@ -47,6 +48,7 @@ final class Container implements \Psr\Container\ContainerInterface {
|
|||
ProxiesServiceProvider::class,
|
||||
RestockRefundedItemsAdjusterServiceProvider::class,
|
||||
UtilsClassesServiceProvider::class,
|
||||
COTMigrationServiceProvider::class,
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,586 @@
|
|||
<?php
|
||||
/**
|
||||
* Generic migration class to move any entity, entity_meta table combination to custom table.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
|
||||
|
||||
use Automattic\WooCommerce\Database\Migrations\MigrationHelper;
|
||||
|
||||
/**
|
||||
* Class MetaToCustomTableMigrator.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
|
||||
*/
|
||||
abstract class MetaToCustomTableMigrator {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Store errors along with entity IDs from migrations.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $errors;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
$this->errors = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 public function get_schema_config();
|
||||
|
||||
/**
|
||||
* 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 public function get_core_column_mapping();
|
||||
|
||||
/**
|
||||
* 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 public function get_meta_column_config();
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* ...
|
||||
*/
|
||||
public function generate_insert_sql_for_batch( $batch ) {
|
||||
$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 IGNORE 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)
|
||||
* ...
|
||||
*/
|
||||
public function generate_update_sql_for_batch( $batch, $entity_row_mapping ) {
|
||||
$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 = $this->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.
|
||||
*/
|
||||
protected function get_destination_table_primary_id_schema() {
|
||||
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.
|
||||
*/
|
||||
protected function generate_column_clauses( $columns_schema, $batch ) {
|
||||
global $wpdb;
|
||||
|
||||
$columns = array();
|
||||
$placeholders = array();
|
||||
foreach ( $columns_schema as $prev_column => $schema ) {
|
||||
$columns[] = $schema['destination'];
|
||||
$placeholders[] = MigrationHelper::get_wpdb_placeholder_for_type( $schema['type'] );
|
||||
}
|
||||
$placeholders = "'" . implode( "', '", $placeholders ) . "'";
|
||||
|
||||
$values = array();
|
||||
foreach ( array_values( $batch ) as $row ) {
|
||||
$query_params = array();
|
||||
foreach ( $columns as $column ) {
|
||||
$query_params[] = $row[ $column ] ?? null;
|
||||
}
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $placeholders can only contain combination of placeholders described in MigrationHelper::get_wpdb_placeholder_for_type
|
||||
$value_string = '(' . $wpdb->prepare( $placeholders, $query_params ) . ')';
|
||||
$values[] = $value_string;
|
||||
}
|
||||
|
||||
$value_sql = implode( ',', $values );
|
||||
|
||||
$column_sql = implode( '`, `', $columns );
|
||||
|
||||
return array( $value_sql, $column_sql, $columns );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private function generate_on_duplicate_statement_clause( $columns ) {
|
||||
$update_value_statements = array();
|
||||
foreach ( $columns as $column ) {
|
||||
$update_value_statements[] = "$column = VALUES( $column )";
|
||||
}
|
||||
$update_value_clause = implode( ', ', $update_value_statements );
|
||||
|
||||
return "ON DUPLICATE KEY UPDATE $update_value_clause";
|
||||
}
|
||||
|
||||
/**
|
||||
* Process next migration batch, uses option `wc_cot_migration` to checkpoints of what have been processed so far.
|
||||
*
|
||||
* @param array $entity_ids List of entity IDs to perform migrations for.
|
||||
*
|
||||
* @return array List of errors happened during migration.
|
||||
*/
|
||||
public function process_migration_batch_for_ids( $entity_ids ) {
|
||||
$data = $this->fetch_data_for_migration_for_ids( $entity_ids );
|
||||
|
||||
foreach ( $data['errors'] as $entity_id => $error ) {
|
||||
$this->errors[ $entity_id ] = "Error in importing post id $entity_id: " . $error->get_message();
|
||||
}
|
||||
|
||||
if ( count( $data['data'] ) === 0 ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$entity_ids = array_keys( $data['data'] );
|
||||
$already_migrated = $this->get_already_migrated_records( $entity_ids );
|
||||
|
||||
$to_insert = array_diff_key( $data['data'], $already_migrated );
|
||||
$this->process_insert_batch( $to_insert );
|
||||
|
||||
$to_update = array_intersect_key( $data['data'], $already_migrated );
|
||||
$this->process_update_batch( $to_update, $already_migrated );
|
||||
|
||||
return array(
|
||||
'errors' => $this->errors,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
*/
|
||||
protected function process_insert_batch( $batch ) {
|
||||
global $wpdb;
|
||||
if ( 0 === count( $batch ) ) {
|
||||
return;
|
||||
}
|
||||
$queries = $this->generate_insert_sql_for_batch( $batch );
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Queries should already be prepared.
|
||||
$result = $wpdb->query( $queries );
|
||||
$wpdb->query( 'COMMIT;' ); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration?
|
||||
if ( count( $batch ) !== $result ) {
|
||||
$this->errors[] = 'Error with batch: ' . $wpdb->last_error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $already_migrated Maps rows to update data with their original IDs.
|
||||
*/
|
||||
protected function process_update_batch( $batch, $already_migrated ) {
|
||||
global $wpdb;
|
||||
if ( 0 === count( $batch ) ) {
|
||||
return;
|
||||
}
|
||||
$queries = $this->generate_update_sql_for_batch( $batch, $already_migrated );
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Queries should already be prepared.
|
||||
$result = $wpdb->query( $queries );
|
||||
$wpdb->query( 'COMMIT;' ); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration?
|
||||
if ( count( $batch ) !== $result ) {
|
||||
$this->errors[] = 'Error with batch: ' . $wpdb->last_error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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, ...),
|
||||
* ...,
|
||||
* )
|
||||
*/
|
||||
public function fetch_data_for_migration_for_ids( $entity_ids ) {
|
||||
global $wpdb;
|
||||
|
||||
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 = $wpdb->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 = $wpdb->get_results( $meta_table_query );
|
||||
|
||||
return $this->process_and_sanitize_data( $entity_data, $meta_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch id mappings for records that are already inserted, or can be considered duplicates.
|
||||
*
|
||||
* @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,
|
||||
* ),
|
||||
* ...
|
||||
* )
|
||||
*/
|
||||
public function get_already_migrated_records( $entity_ids ) {
|
||||
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' ) );
|
||||
|
||||
$already_migrated_entity_ids = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded.
|
||||
"
|
||||
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 )
|
||||
",
|
||||
$entity_ids
|
||||
)
|
||||
// phpcs:enable
|
||||
);
|
||||
|
||||
return array_column( $already_migrated_entity_ids, null, 'source_id' );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
protected function build_entity_table_query( $entity_ids ) {
|
||||
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,
|
||||
$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.
|
||||
*/
|
||||
protected function build_meta_data_query( $entity_ids ) {
|
||||
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( $entity_data, $meta_data ) {
|
||||
$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( &$sanitized_entity_data, &$error_records, $entity_data ) {
|
||||
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_message();
|
||||
} 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( &$sanitized_entity_data, &$error_records, $meta_data ) {
|
||||
foreach ( $meta_data as $datum ) {
|
||||
$column_schema = $this->meta_column_mapping[ $datum->meta_key ];
|
||||
$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, $type ) {
|
||||
switch ( $type ) {
|
||||
case 'decimal':
|
||||
$value = (float) $value;
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
<?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\CustomOrderTable;
|
||||
|
||||
use Automattic\WooCommerce\Database\Migrations\MigrationHelper;
|
||||
|
||||
/**
|
||||
* Class MetaToMetaTableMigrator.
|
||||
*
|
||||
* Generic class for powering migrations from one meta table to another table.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
|
||||
*/
|
||||
class MetaToMetaTableMigrator {
|
||||
|
||||
/**
|
||||
* Schema config, see __construct for more details.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $schema_config;
|
||||
|
||||
/**
|
||||
* MetaToMetaTableMigrator constructor.
|
||||
*
|
||||
* @param array $schema_config This parameters provides general but essential information about tables under migrations. Must be of the form-
|
||||
* TODO: Add structure.
|
||||
*/
|
||||
public function __construct( $schema_config ) {
|
||||
// TODO: Validate params.
|
||||
$this->schema_config = $schema_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate insert sql queries for batches.
|
||||
*
|
||||
* @param array $batch Data to generate queries for.
|
||||
* @param string $insert_switch Insert switch to use.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generate_insert_sql_for_batch( $batch, $insert_switch ) {
|
||||
global $wpdb;
|
||||
|
||||
$insert_query = MigrationHelper::get_insert_switch( $insert_switch );
|
||||
|
||||
$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`)";
|
||||
$table = $this->schema_config['destination']['meta']['table_name'];
|
||||
|
||||
$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 ( array_values( $batch ) as $row ) {
|
||||
$query_params = array(
|
||||
$row->destination_entity_id,
|
||||
$row->meta_key,
|
||||
$row->meta_value,
|
||||
);
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $placeholder_string is hardcoded.
|
||||
$value_sql = $wpdb->prepare( "$placeholder_string", $query_params );
|
||||
$values[] = $value_sql;
|
||||
}
|
||||
|
||||
$values_sql = implode( '), (', $values );
|
||||
|
||||
return "$insert_query INTO $table $column_sql VALUES ($values_sql)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch data for migration.
|
||||
*
|
||||
* @param array $order_post_ids Array of 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, ...),
|
||||
* ...,
|
||||
* )
|
||||
*/
|
||||
public function fetch_data_for_migration_for_ids( $order_post_ids ) {
|
||||
global $wpdb;
|
||||
if ( empty( $order_post_ids ) ) {
|
||||
return array(
|
||||
'data' => array(),
|
||||
'errors' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
$meta_query = $this->build_meta_table_query( $order_post_ids );
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Meta query has interpolated variables, but they should all be escaped for backticks.
|
||||
$meta_data_rows = $wpdb->get_results( $meta_query );
|
||||
if ( empty( $meta_data_rows ) ) {
|
||||
return array(
|
||||
'data' => array(),
|
||||
'errors' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'data' => $meta_data_rows,
|
||||
'errors' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to build query used to fetch data from source meta table.
|
||||
*
|
||||
* @param string $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( $entity_ids ) {
|
||||
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_entity_id_column ASC";
|
||||
|
||||
$where_clause = "$source_entity_id_column IN (" . implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ) . ')';
|
||||
|
||||
$destination_entity_table = $this->schema_config['destination']['entity']['table_name'];
|
||||
$destination_entity_id_column = $this->schema_config['destination']['entity']['id_column'];
|
||||
$destination_source_id_mapping_column = $this->schema_config['destination']['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 escated 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
|
||||
return $wpdb->prepare(
|
||||
"
|
||||
SELECT
|
||||
source.`$source_entity_id_column` as source_entity_id,
|
||||
destination.`$destination_entity_id_column` as destination_entity_id,
|
||||
source.`$source_meta_key_column` as meta_key,
|
||||
source.`$source_meta_value_column` as meta_value
|
||||
FROM `$source_meta_table` source
|
||||
JOIN `$destination_entity_table` destination ON destination.`$destination_source_id_mapping_column` = source.`$source_entity_id_column`
|
||||
WHERE $where_clause ORDER BY $order_by
|
||||
",
|
||||
$entity_ids
|
||||
);
|
||||
// phpcs:enable
|
||||
}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
<?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();
|
||||
|
||||
$meta_data_config = $this->get_config_for_meta_table();
|
||||
|
||||
$this->meta_table_migrator = new MetaToMetaTableMigrator( $meta_data_config );
|
||||
|
||||
$this->error_logger = new MigrationErrorLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate config for meta data migration.
|
||||
*
|
||||
* @return array Meta data migration config.
|
||||
*/
|
||||
private function get_config_for_meta_table() {
|
||||
global $wpdb;
|
||||
// TODO: Remove hardcoding.
|
||||
$this->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',
|
||||
);
|
||||
|
||||
$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() ) );
|
||||
|
||||
return array(
|
||||
'source' => array(
|
||||
'meta' => array(
|
||||
'table_name' => $wpdb->postmeta,
|
||||
'entity_id_column' => 'post_id',
|
||||
'meta_key_column' => 'meta_key',
|
||||
'meta_value_column' => 'meta_value',
|
||||
),
|
||||
'excluded_keys' => $excluded_columns,
|
||||
),
|
||||
'destination' => array(
|
||||
'meta' => array(
|
||||
'table_name' => $this->table_names['meta'],
|
||||
'entity_id_column' => 'order_id',
|
||||
'meta_key_column' => 'meta_key',
|
||||
'meta_value_column' => 'meta_value',
|
||||
'entity_id_type' => 'int',
|
||||
),
|
||||
'entity' => array(
|
||||
'table_name' => $this->table_names['orders'],
|
||||
'source_id_column' => 'post_id',
|
||||
'id_column' => 'id',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 );
|
||||
// TODO: Add resilience for meta migrations.
|
||||
// $this->process_meta_migration( $order_post_ids );
|
||||
// TODO: Return merged error array.
|
||||
}
|
||||
|
||||
/**
|
||||
* Process migration for metadata for given post ids.
|
||||
*
|
||||
* @param array $order_post_ids Post IDs.
|
||||
*/
|
||||
private function process_meta_migration( $order_post_ids ) {
|
||||
global $wpdb;
|
||||
$data_to_migrate = $this->meta_table_migrator->fetch_data_for_migration_for_ids( $order_post_ids );
|
||||
$insert_queries = $this->meta_table_migrator->generate_insert_sql_for_batch( $data_to_migrate['data'], 'insert' );
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $insert_queries should already be escaped in the generating function.
|
||||
$result = $wpdb->query( $insert_queries );
|
||||
if ( count( $data_to_migrate['data'] ) !== $result ) {
|
||||
// TODO: Find and log entity ids that were not inserted.
|
||||
echo 'error';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
<?php
|
||||
/**
|
||||
* Class for WPPost to wc_order_address table migrator.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
|
||||
|
||||
/**
|
||||
* Class WPPostToOrderAddressTableMigrator
|
||||
*
|
||||
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
|
||||
*/
|
||||
class WPPostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
|
||||
/**
|
||||
* Type of addresses being migrated, could be billing|shipping.
|
||||
*
|
||||
* @var $type
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* WPPostToOrderAddressTableMigrator constructor.
|
||||
*
|
||||
* @param string $type Type of addresses being migrated, could be billing|shipping.
|
||||
*/
|
||||
public function __construct( $type ) {
|
||||
$this->type = $type;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schema config for wp_posts and wc_order_address table.
|
||||
*
|
||||
* @return array Config.
|
||||
*/
|
||||
public function get_schema_config() {
|
||||
global $wpdb;
|
||||
// TODO: Remove hardcoding.
|
||||
$this->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' => $this->table_names['orders'],
|
||||
'meta_rel_column' => 'post_id',
|
||||
'destination_rel_column' => 'id',
|
||||
'primary_key' => 'post_id',
|
||||
),
|
||||
'meta' => array(
|
||||
'table_name' => $wpdb->postmeta,
|
||||
'meta_key_column' => 'meta_key',
|
||||
'meta_value_column' => 'meta_value',
|
||||
'entity_id_column' => 'post_id',
|
||||
),
|
||||
),
|
||||
'destination' => array(
|
||||
'table_name' => $this->table_names['addresses'],
|
||||
'source_rel_column' => 'order_id',
|
||||
'primary_key' => 'id',
|
||||
'primary_key_type' => 'int',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get columns config.
|
||||
*
|
||||
* @return \string[][] Config.
|
||||
*/
|
||||
public function get_core_column_mapping() {
|
||||
$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() {
|
||||
$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',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* We overwrite this method to add a subclause to only fetch address of current type.
|
||||
*
|
||||
* @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,
|
||||
* ),
|
||||
* ...
|
||||
* )
|
||||
*/
|
||||
public function get_already_migrated_records( $entity_ids ) {
|
||||
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'];
|
||||
|
||||
$address_type = $this->type;
|
||||
|
||||
$entity_id_placeholder = implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) );
|
||||
|
||||
$already_migrated_entity_ids = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded.
|
||||
"
|
||||
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 ) AND destination.`address_type` = '$address_type'
|
||||
",
|
||||
$entity_ids
|
||||
)
|
||||
// phpcs:enable
|
||||
);
|
||||
|
||||
return array_column( $already_migrated_entity_ids, null, 'source_id' );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
/**
|
||||
* Class for WPPost to wc_order_operational_details migrator.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
|
||||
|
||||
/**
|
||||
* Class WPPostToOrderOpTableMigrator
|
||||
*
|
||||
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
|
||||
*/
|
||||
class WPPostToOrderOpTableMigrator extends MetaToCustomTableMigrator {
|
||||
|
||||
/**
|
||||
* Get schema config for wp_posts and wc_order_operational_detail table.
|
||||
*
|
||||
* @return array Config.
|
||||
*/
|
||||
public function get_schema_config() {
|
||||
global $wpdb;
|
||||
// TODO: Remove hardcoding.
|
||||
$this->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' => $this->table_names['orders'],
|
||||
'meta_rel_column' => 'post_id',
|
||||
'destination_rel_column' => 'id',
|
||||
'primary_key' => 'post_id',
|
||||
),
|
||||
'meta' => array(
|
||||
'table_name' => $wpdb->postmeta,
|
||||
'meta_key_column' => 'meta_key',
|
||||
'meta_value_column' => 'meta_value',
|
||||
'entity_id_column' => 'post_id',
|
||||
),
|
||||
),
|
||||
'destination' => array(
|
||||
'table_name' => $this->table_names['op_data'],
|
||||
'source_rel_column' => 'order_id',
|
||||
'primary_key' => 'id',
|
||||
'primary_key_type' => 'int',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get columns config.
|
||||
*
|
||||
* @return \string[][] Config.
|
||||
*/
|
||||
public function get_core_column_mapping() {
|
||||
return array(
|
||||
'id' => array(
|
||||
'type' => 'int',
|
||||
'destination' => 'order_id',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get meta data config.
|
||||
*
|
||||
* @return \string[][] Config.
|
||||
*/
|
||||
public function get_meta_column_config() {
|
||||
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',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
/**
|
||||
* Class for WPPost To order table migrator.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
|
||||
|
||||
/**
|
||||
* Class WPPostToOrderTableMigrator.
|
||||
*/
|
||||
class WPPostToOrderTableMigrator extends MetaToCustomTableMigrator {
|
||||
|
||||
/**
|
||||
* Get schema config for wp_posts and wc_order table.
|
||||
*
|
||||
* @return array Config.
|
||||
*/
|
||||
public function get_schema_config() {
|
||||
global $wpdb;
|
||||
|
||||
// TODO: Remove hardcoding.
|
||||
$this->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_key_column' => 'meta_key',
|
||||
'meta_value_column' => 'meta_value',
|
||||
'entity_id_column' => 'post_id',
|
||||
),
|
||||
),
|
||||
'destination' => array(
|
||||
'table_name' => $this->table_names['orders'],
|
||||
'source_rel_column' => 'post_id',
|
||||
'primary_key' => 'id',
|
||||
'primary_key_type' => 'int',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get columns config.
|
||||
*
|
||||
* @return \string[][] Config.
|
||||
*/
|
||||
public function get_core_column_mapping() {
|
||||
return array(
|
||||
'ID' => array(
|
||||
'type' => 'int',
|
||||
'destination' => 'post_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',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get meta data config.
|
||||
*
|
||||
* @return \string[][] Config.
|
||||
*/
|
||||
public function get_meta_column_config() {
|
||||
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',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
/**
|
||||
* Error logger for custom table migrations.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations;
|
||||
|
||||
/**
|
||||
* Class MigrationErrorLogger.
|
||||
*
|
||||
* Error logging for custom table migrations.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Database\Migrations
|
||||
*/
|
||||
class MigrationErrorLogger extends \WC_Logger {
|
||||
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
/**
|
||||
* Helper class with utility functions for migrations.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Database\Migrations;
|
||||
|
||||
/**
|
||||
* Class MigrationHelper.
|
||||
*
|
||||
* 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',
|
||||
);
|
||||
|
||||
/**
|
||||
* Get insert clause for appropriate switch.
|
||||
*
|
||||
* @param string $switch Name of the switch to use.
|
||||
*
|
||||
* @return string Insert clause.
|
||||
*/
|
||||
public static function get_insert_switch( $switch ) {
|
||||
switch ( $switch ) {
|
||||
case 'insert_ignore':
|
||||
$insert_query = 'INSERT IGNORE';
|
||||
break;
|
||||
case 'replace': // delete and then insert.
|
||||
$insert_query = 'REPLACE';
|
||||
break;
|
||||
case 'update':
|
||||
$insert_query = 'UPDATE';
|
||||
break;
|
||||
case 'insert':
|
||||
default:
|
||||
$insert_query = 'INSERT';
|
||||
}
|
||||
|
||||
return $insert_query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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( $schema_config ) {
|
||||
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( $type ) {
|
||||
return self::$wpdb_placeholder_for_type[ $type ];
|
||||
}
|
||||
|
||||
}
|
|
@ -17,7 +17,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
|||
*
|
||||
* @return string The custom orders table name.
|
||||
*/
|
||||
public function get_orders_table_name() {
|
||||
public static function get_orders_table_name() {
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . 'wc_orders';
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
|||
*
|
||||
* @return string The order addresses table name.
|
||||
*/
|
||||
public function get_addresses_table_name() {
|
||||
public static function get_addresses_table_name() {
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . 'wc_order_addresses';
|
||||
}
|
||||
|
@ -37,11 +37,21 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
|||
*
|
||||
* @return string The orders operational data table name.
|
||||
*/
|
||||
public function get_operational_data_table_name() {
|
||||
public static function get_operational_data_table_name() {
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . 'wc_order_operational_data';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the orders meta data table name.
|
||||
*
|
||||
* @return string Name of order meta data table.
|
||||
*/
|
||||
public static function get_meta_table_name() {
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . 'wc_orders_meta';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names of all the tables involved in the custom orders table feature.
|
||||
*
|
||||
|
@ -52,6 +62,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
|||
$this->get_orders_table_name(),
|
||||
$this->get_addresses_table_name(),
|
||||
$this->get_operational_data_table_name(),
|
||||
$this->get_meta_table_name(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -185,6 +196,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
|||
$orders_table_name = $this->get_orders_table_name();
|
||||
$addresses_table_name = $this->get_addresses_table_name();
|
||||
$operational_data_table_name = $this->get_operational_data_table_name();
|
||||
$meta_table = $this->get_meta_table_name();
|
||||
|
||||
$sql = "
|
||||
CREATE TABLE $orders_table_name (
|
||||
|
@ -234,7 +246,7 @@ CREATE TABLE $operational_data_table_name (
|
|||
woocommerce_version varchar(20) NULL,
|
||||
prices_include_tax tinyint(1) NULL,
|
||||
coupon_usages_are_counted tinyint(1) NULL,
|
||||
download_permissionis_granted tinyint(1) NULL,
|
||||
download_permission_granted tinyint(1) NULL,
|
||||
cart_hash varchar(100) NULL,
|
||||
new_order_email_sent tinyint(1) NULL,
|
||||
order_key varchar(100) NULL,
|
||||
|
@ -242,12 +254,21 @@ CREATE TABLE $operational_data_table_name (
|
|||
date_paid_gmt datetime NULL,
|
||||
date_completed_gmt datetime NULL,
|
||||
shipping_tax_amount decimal(26, 8) NULL,
|
||||
shopping_total_amount decimal(26, 8) NULL,
|
||||
shipping_total_amount decimal(26, 8) NULL,
|
||||
discount_tax_amount decimal(26, 8) NULL,
|
||||
discount_total_amount decimal(26, 8) NULL,
|
||||
KEY order_id (order_id),
|
||||
KEY order_key (order_key)
|
||||
);";
|
||||
);
|
||||
CREATE TABLE $meta_table (
|
||||
id bigint(20) unsigned auto_increment primary key,
|
||||
order_id bigint(20) unsigned null,
|
||||
meta_key varchar(255),
|
||||
meta_value text null,
|
||||
KEY meta_key_value (meta_key, meta_value(100))
|
||||
);
|
||||
";
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
/**
|
||||
* Service provider for COTMigration.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\WPPostToCOTMigrator;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
|
||||
/**
|
||||
* Class COTMigrationServiceProvider
|
||||
*
|
||||
* @package Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders
|
||||
*/
|
||||
class COTMigrationServiceProvider extends AbstractServiceProvider {
|
||||
|
||||
/**
|
||||
* Services provided by this provider.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $provides = array(
|
||||
WPPostToCOTMigrator::class,
|
||||
);
|
||||
|
||||
/**
|
||||
* Use the register method to register items with the container via the
|
||||
* protected $this->leagueContainer property or the `getLeagueContainer` method
|
||||
* from the ContainerAwareTrait.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register() {
|
||||
$this->share( WPPostToCOTMigrator::class );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,381 @@
|
|||
<?php
|
||||
/**
|
||||
* Tests for WPPostToCOTMigrator class.
|
||||
*/
|
||||
|
||||
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\WPPostToCOTMigrator;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper;
|
||||
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
|
||||
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ShippingHelper;
|
||||
|
||||
/**
|
||||
* Class WPPostToCOTMigratorTest.
|
||||
*/
|
||||
class WPPostToCOTMigratorTest extends WC_Unit_Test_Case {
|
||||
|
||||
/**
|
||||
* @var DataSynchronizer
|
||||
*/
|
||||
private $synchronizer;
|
||||
|
||||
/**
|
||||
* @var WPPostToCOTMigrator
|
||||
*/
|
||||
private $sut;
|
||||
|
||||
/**
|
||||
* @var OrdersTableDataStore;
|
||||
*/
|
||||
private $data_store;
|
||||
|
||||
/**
|
||||
* Setup data_store and sut.
|
||||
*/
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->create_order_custom_table_if_not_exist();
|
||||
$this->data_store = wc_get_container()->get( OrdersTableDataStore::class );
|
||||
$this->sut = wc_get_container()->get( WPPostToCOTMigrator::class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that migration for a normal order happens as expected.
|
||||
*/
|
||||
public function test_process_next_migration_batch_normal_order() {
|
||||
$order = wc_get_order( $this->create_complex_wp_post_order() );
|
||||
$this->clear_all_orders_and_reset_checkpoint();
|
||||
$this->sut->process_next_migration_batch( 100 );
|
||||
|
||||
$this->assert_core_data_is_migrated( $order );
|
||||
$this->assert_order_addresses_are_migrated( $order );
|
||||
$this->assert_order_op_data_is_migrated( $order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that already migrated order isn't migrated twice.
|
||||
*/
|
||||
public function test_process_next_migration_batch_already_migrated_order() {
|
||||
global $wpdb;
|
||||
$order = wc_get_order( $this->create_complex_wp_post_order() );
|
||||
$this->clear_all_orders_and_reset_checkpoint();
|
||||
|
||||
// Run the migration once.
|
||||
$this->sut->process_next_migration_batch( 100 );
|
||||
|
||||
// Delete checkpoint and run migration again, assert there are still no duplicates.
|
||||
$this->sut->update_checkpoint( 0 );
|
||||
$this->sut->process_next_migration_batch( 100 );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$this->assertEquals(
|
||||
1,
|
||||
$wpdb->get_var(
|
||||
"
|
||||
SELECT COUNT(*) FROM {$this->data_store::get_orders_table_name()}
|
||||
WHERE post_id = {$order->get_id()}"
|
||||
),
|
||||
'Order record is duplicated.'
|
||||
);
|
||||
$order_id = $wpdb->get_var( "SELECT id FROM {$this->data_store::get_orders_table_name()} WHERE post_id = {$order->get_id()}" );
|
||||
$this->assertEquals(
|
||||
1,
|
||||
$wpdb->get_var(
|
||||
"
|
||||
SELECT COUNT(*) FROM {$this->data_store::get_addresses_table_name()}
|
||||
WHERE order_id = {$order_id} AND address_type = 'billing'
|
||||
"
|
||||
)
|
||||
);
|
||||
$this->assertEquals(
|
||||
1,
|
||||
$wpdb->get_var(
|
||||
"
|
||||
SELECT COUNT(*) FROM {$this->data_store::get_addresses_table_name()}
|
||||
WHERE order_id = {$order_id} AND address_type = 'shipping'
|
||||
"
|
||||
)
|
||||
);
|
||||
$this->assertEquals(
|
||||
1,
|
||||
$wpdb->get_var(
|
||||
"
|
||||
SELECT COUNT(*) FROM {$this->data_store::get_operational_data_table_name()}
|
||||
WHERE order_id = {$order_id}
|
||||
"
|
||||
)
|
||||
);
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that invalid order data is not migrated but logged.
|
||||
*/
|
||||
public function test_process_next_migration_batch_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() {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get order object from COT.
|
||||
*
|
||||
* @param WP_Post $post_order Post object for order.
|
||||
*
|
||||
* @return array|object|void|null DB object from COT.
|
||||
*/
|
||||
private function get_order_from_cot( $post_order ) {
|
||||
global $wpdb;
|
||||
$order_table = $this->data_store::get_orders_table_name();
|
||||
$query = "SELECT * FROM $order_table WHERE post_id = {$post_order->get_id()};";
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
return $wpdb->get_row( $query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get address details from DB.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @param string $address_type Address Type.
|
||||
*
|
||||
* @return array|object|void|null DB object.
|
||||
*/
|
||||
private function get_address_details_from_cot( $order_id, $address_type ) {
|
||||
global $wpdb;
|
||||
$address_table = $this->data_store::get_addresses_table_name();
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
return $wpdb->get_row( "SELECT * FROM $address_table WHERE order_id = $order_id AND address_type = '$address_type';" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get operational details from COT.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
*
|
||||
* @return array|object|void|null DB Object.
|
||||
*/
|
||||
private function get_order_operational_data_from_cot( $order_id ) {
|
||||
global $wpdb;
|
||||
$operational_data_table = $this->data_store::get_operational_data_table_name();
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
return $wpdb->get_row( "SELECT * FROM $operational_data_table WHERE order_id = $order_id;" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create complex wp_post based order.
|
||||
*
|
||||
* @return int Order ID
|
||||
*/
|
||||
private function create_complex_wp_post_order() {
|
||||
update_option( 'woocommerce_prices_include_tax', 'yes' );
|
||||
update_option( 'woocommerce_calc_taxes', 'yes' );
|
||||
$uniq_cust_id = wp_generate_password( 10, false );
|
||||
$customer = CustomerHelper::create_customer( "user$uniq_cust_id", $uniq_cust_id, "user$uniq_cust_id@example.com" );
|
||||
$tax_rate = array(
|
||||
'tax_rate_country' => '',
|
||||
'tax_rate_state' => '',
|
||||
'tax_rate' => '15.0000',
|
||||
'tax_rate_name' => 'tax',
|
||||
'tax_rate_priority' => '1',
|
||||
'tax_rate_order' => '1',
|
||||
'tax_rate_shipping' => '1',
|
||||
);
|
||||
WC_Tax::_insert_tax_rate( $tax_rate );
|
||||
|
||||
ShippingHelper::create_simple_flat_rate();
|
||||
|
||||
$order = OrderHelper::create_order();
|
||||
// Make sure this is a wp_post order.
|
||||
$post = get_post( $order->get_id() );
|
||||
$this->assertNotNull( $post, 'Order is not created in wp_post table.' );
|
||||
$this->assertEquals( 'shop_order', $post->post_type, 'Order is not created in wp_post table.' );
|
||||
|
||||
$order->save();
|
||||
|
||||
$order->set_status( 'completed' );
|
||||
$order->set_currency( 'INR' );
|
||||
$order->set_customer_id( $customer->get_id() );
|
||||
$order->set_billing_email( $customer->get_billing_email() );
|
||||
|
||||
$payment_gateway = new WC_Mock_Payment_Gateway();
|
||||
$order->set_payment_method( 'mock' );
|
||||
$order->set_transaction_id( 'mock1' );
|
||||
|
||||
$order->set_customer_ip_address( '1.1.1.1' );
|
||||
$order->set_customer_user_agent( 'wc_unit_tests' );
|
||||
|
||||
$order->save();
|
||||
|
||||
$order->set_shipping_first_name( 'Albert' );
|
||||
$order->set_shipping_last_name( 'Einstein' );
|
||||
$order->set_shipping_company( 'The Olympia Academy' );
|
||||
$order->set_shipping_address_1( '112 Mercer Street' );
|
||||
$order->set_shipping_address_2( 'Princeton' );
|
||||
$order->set_shipping_city( 'New Jersey' );
|
||||
$order->set_shipping_postcode( '08544' );
|
||||
$order->set_shipping_phone( '299792458' );
|
||||
$order->set_shipping_country( 'US' );
|
||||
|
||||
$order->set_created_via( 'unit_tests' );
|
||||
$order->set_version( '0.0.2' );
|
||||
$order->set_prices_include_tax( true );
|
||||
wc_update_coupon_usage_counts( $order->get_id() );
|
||||
$order->get_data_store()->set_download_permissions_granted( $order, true );
|
||||
$order->set_cart_hash( '1234' );
|
||||
$order->update_meta_data( '_new_order_email_sent', 'true' );
|
||||
$order->update_meta_data( '_order_stock_reduced', 'true' );
|
||||
$order->set_date_paid( time() );
|
||||
$order->set_date_completed( time() );
|
||||
$order->calculate_shipping();
|
||||
|
||||
$order->save();
|
||||
$order->save_meta_data();
|
||||
|
||||
return $order->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to assert core data is migrated.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
*/
|
||||
private function assert_core_data_is_migrated( $order ) {
|
||||
$db_order = $this->get_order_from_cot( $order );
|
||||
|
||||
// Verify core data.
|
||||
$this->assertEquals( $order->get_id(), $db_order->post_id );
|
||||
$this->assertEquals( 'wc-' . $order->get_status(), $db_order->status );
|
||||
$this->assertEquals( 'INR', $db_order->currency );
|
||||
$this->assertEquals( $order->get_customer_id(), $db_order->customer_id );
|
||||
$this->assertEquals( $order->get_billing_email(), $db_order->billing_email );
|
||||
$this->assertEquals( $order->get_payment_method(), $db_order->payment_method );
|
||||
$this->assertEquals(
|
||||
$order->get_date_created()->date( DATE_ISO8601 ),
|
||||
( new WC_DateTime( $db_order->date_created_gmt ) )->date( DATE_ISO8601 )
|
||||
);
|
||||
$this->assertEquals( $order->get_date_modified()->date( DATE_ISO8601 ), ( new WC_DateTime( $db_order->date_updated_gmt ) )->date( DATE_ISO8601 ) );
|
||||
$this->assertEquals( $order->get_parent_id(), $db_order->parent_order_id );
|
||||
$this->assertEquals( $order->get_payment_method_title(), $db_order->payment_method_title );
|
||||
$this->assertEquals( $order->get_transaction_id(), $db_order->transaction_id );
|
||||
$this->assertEquals( $order->get_customer_ip_address(), $db_order->ip_address );
|
||||
$this->assertEquals( $order->get_customer_user_agent(), $db_order->user_agent );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to assert addresses are migrated.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
*/
|
||||
private function assert_order_addresses_are_migrated( $order ) {
|
||||
$db_order = $this->get_order_from_cot( $order );
|
||||
|
||||
// Verify order billing address.
|
||||
$db_order_address = $this->get_address_details_from_cot( $db_order->id, 'billing' );
|
||||
$this->assertEquals( $order->get_billing_first_name(), $db_order_address->first_name );
|
||||
$this->assertEquals( $order->get_billing_last_name(), $db_order_address->last_name );
|
||||
$this->assertEquals( $order->get_billing_company(), $db_order_address->company );
|
||||
$this->assertEquals( $order->get_billing_address_1(), $db_order_address->address_1 );
|
||||
$this->assertEquals( $order->get_billing_address_2(), $db_order_address->address_2 );
|
||||
$this->assertEquals( $order->get_billing_city(), $db_order_address->city );
|
||||
$this->assertEquals( $order->get_billing_postcode(), $db_order_address->postcode );
|
||||
$this->assertEquals( $order->get_billing_country(), $db_order_address->country );
|
||||
$this->assertEquals( $order->get_billing_email(), $db_order_address->email );
|
||||
$this->assertEquals( $order->get_billing_phone(), $db_order_address->phone );
|
||||
|
||||
// Verify order shipping address.
|
||||
$db_order_address = $this->get_address_details_from_cot( $db_order->id, 'shipping' );
|
||||
$this->assertEquals( $order->get_shipping_first_name(), $db_order_address->first_name );
|
||||
$this->assertEquals( $order->get_shipping_last_name(), $db_order_address->last_name );
|
||||
$this->assertEquals( $order->get_shipping_company(), $db_order_address->company );
|
||||
$this->assertEquals( $order->get_shipping_address_1(), $db_order_address->address_1 );
|
||||
$this->assertEquals( $order->get_shipping_address_2(), $db_order_address->address_2 );
|
||||
$this->assertEquals( $order->get_shipping_city(), $db_order_address->city );
|
||||
$this->assertEquals( $order->get_shipping_postcode(), $db_order_address->postcode );
|
||||
$this->assertEquals( $order->get_shipping_country(), $db_order_address->country );
|
||||
$this->assertEquals( $order->get_shipping_phone(), $db_order_address->phone );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to assert operational data is migrated.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
*/
|
||||
private function assert_order_op_data_is_migrated( $order ) {
|
||||
$db_order = $this->get_order_from_cot( $order );
|
||||
// Verify order operational data.
|
||||
$db_order_op_data = $this->get_order_operational_data_from_cot( $db_order->id );
|
||||
$this->assertEquals( $order->get_created_via(), $db_order_op_data->created_via );
|
||||
$this->assertEquals( $order->get_version(), $db_order_op_data->woocommerce_version );
|
||||
$this->assertEquals( $order->get_prices_include_tax(), $db_order_op_data->prices_include_tax );
|
||||
$this->assertEquals(
|
||||
wc_string_to_bool( $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ),
|
||||
$db_order_op_data->coupon_usages_are_counted
|
||||
);
|
||||
$this->assertEquals(
|
||||
wc_string_to_bool( $order->get_data_store()->get_download_permissions_granted( $order ) ),
|
||||
$db_order_op_data->download_permission_granted
|
||||
);
|
||||
$this->assertEquals( $order->get_cart_hash(), $db_order_op_data->cart_hash );
|
||||
$this->assertEquals(
|
||||
wc_string_to_bool( $order->get_meta( '_new_order_email_sent' ) ),
|
||||
$db_order_op_data->new_order_email_sent
|
||||
);
|
||||
$this->assertEquals( $order->get_order_key(), $db_order_op_data->order_key );
|
||||
$this->assertEquals( $order->get_data_store()->get_stock_reduced( $order ), $db_order_op_data->order_stock_reduced );
|
||||
$this->assertEquals(
|
||||
$order->get_date_paid()->date( DATE_ISO8601 ),
|
||||
( new WC_DateTime( $db_order_op_data->date_paid_gmt ) )->date( DATE_ISO8601 )
|
||||
);
|
||||
$this->assertEquals(
|
||||
$order->get_date_completed()->date( DATE_ISO8601 ),
|
||||
( new WC_DateTime( $db_order_op_data->date_completed_gmt ) )->date( DATE_ISO8601 )
|
||||
);
|
||||
$this->assertEquals( (float) $order->get_shipping_tax(), (float) $db_order_op_data->shipping_tax_amount );
|
||||
$this->assertEquals( (float) $order->get_shipping_total(), (float) $db_order_op_data->shipping_total_amount );
|
||||
$this->assertEquals( (float) $order->get_discount_tax(), (float) $db_order_op_data->discount_tax_amount );
|
||||
$this->assertEquals( (float) $order->get_discount_total(), (float) $db_order_op_data->discount_total_amount );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to clear checkout and truncate order tables.
|
||||
*/
|
||||
private function clear_all_orders_and_reset_checkpoint() {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create custom tables if not present.
|
||||
*/
|
||||
private function create_order_custom_table_if_not_exist() {
|
||||
$order_table_controller = wc_get_container()
|
||||
->get( 'Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController' );
|
||||
$order_table_controller->show_feature();
|
||||
$this->synchronizer = wc_get_container()
|
||||
->get( DataSynchronizer::class );
|
||||
if ( ! $this->synchronizer->check_orders_table_exists() ) {
|
||||
$this->synchronizer->create_database_tables();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue