PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.1
WooCommerce v10.9.1
10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Database / Migrations / MetaToCustomTableMigrator.php
woocommerce / src / Database / Migrations Last commit date
CustomOrderTable 4 months ago MetaToCustomTableMigrator.php 1 year ago MetaToMetaTableMigrator.php 7 months ago MigrationHelper.php 3 years ago TableMigrator.php 1 year ago
MetaToCustomTableMigrator.php
894 lines
1 <?php
2 /**
3 * Generic migration class to move any entity, entity_meta table combination to custom table.
4 */
5
6 namespace Automattic\WooCommerce\Database\Migrations;
7
8 /**
9 * Base class for implementing migrations from the standard WordPress meta table
10 * to custom structured tables.
11 *
12 * @package Automattic\WooCommerce\Database\Migrations
13 */
14 abstract class MetaToCustomTableMigrator extends TableMigrator {
15
16 /**
17 * Config for tables being migrated and migrated from. See __construct() for detailed config.
18 *
19 * @var array
20 */
21 protected $schema_config;
22
23 /**
24 * Meta config, see __construct for detailed config.
25 *
26 * @var array
27 */
28 protected $meta_column_mapping;
29
30 /**
31 * Column mapping from source table to destination custom table. See __construct for detailed config.
32 *
33 * @var array
34 */
35 protected $core_column_mapping;
36
37 /**
38 * MetaToCustomTableMigrator constructor.
39 */
40 public function __construct() {
41 $this->schema_config = MigrationHelper::escape_schema_for_backtick( $this->get_schema_config() );
42 $this->meta_column_mapping = $this->get_meta_column_config();
43 $this->core_column_mapping = $this->get_core_column_mapping();
44 }
45
46 /**
47 * Specify schema config the source and destination table.
48 *
49 * @return array Schema, must of the form:
50 * array(
51 'source' => array(
52 'entity' => array(
53 'table_name' => $source_table_name,
54 'meta_rel_column' => $column_meta, Name of column in source table which is referenced by meta table.
55 'destination_rel_column' => $column_dest, Name of column in source table which is refenced by destination table,
56 'primary_key' => $primary_key, Primary key of the source table
57 ),
58 'meta' => array(
59 'table' => $meta_table_name,
60 'meta_key_column' => $meta_key_column_name,
61 'meta_value_column' => $meta_value_column_name,
62 'entity_id_column' => $entity_id_column, Name of the column having entity IDs.
63 ),
64 ),
65 'destination' => array(
66 'table_name' => $table_name, Name of destination table,
67 'source_rel_column' => $column_source_id, Name of the column in destination table which is referenced by source table.
68 'primary_key' => $table_primary_key,
69 'primary_key_type' => $type bool|int|string|decimal
70 )
71 */
72 abstract protected function get_schema_config(): array;
73
74 /**
75 * Specify column config from the source table.
76 *
77 * @return array Config, must be of the form:
78 * array(
79 * '$source_column_name_1' => array( // $source_column_name_1 is column name in source table, or a select statement.
80 * 'type' => 'type of value, could be string/int/date/float.',
81 * 'destination' => 'name of the column in column name where this data should be inserted in.',
82 * ),
83 * '$source_column_name_2' => array(
84 * ......
85 * ),
86 * ....
87 * ).
88 */
89 abstract protected function get_core_column_mapping(): array;
90
91 /**
92 * Specify meta keys config from source meta table.
93 *
94 * @return array Config, must be of the form.
95 * array(
96 * '$meta_key_1' => array( // $meta_key_1 is the name of meta_key in source meta table.
97 * 'type' => 'type of value, could be string/int/date/float',
98 * 'destination' => 'name of the column in column name where this data should be inserted in.',
99 * ),
100 * '$meta_key_2' => array(
101 * ......
102 * ),
103 * ....
104 * ).
105 */
106 abstract protected function get_meta_column_config(): array;
107
108 /**
109 * Generate SQL for data insertion.
110 *
111 * @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration_for_ids()` method.
112 *
113 * @return string Generated queries for insertion for this batch, would be of the form:
114 * INSERT IGNORE INTO $table_name ($columns) values
115 * ($value for row 1)
116 * ($value for row 2)
117 * ...
118 */
119 private function generate_insert_sql_for_batch( array $batch ): string {
120 $table = $this->schema_config['destination']['table_name'];
121
122 list( $value_sql, $column_sql ) = $this->generate_column_clauses( array_merge( $this->core_column_mapping, $this->meta_column_mapping ), $batch );
123
124 return "INSERT INTO $table (`$column_sql`) VALUES $value_sql;"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, -- $insert_query is hardcoded, $value_sql is already escaped.
125 }
126
127 /**
128 * Generate SQL for data updating.
129 *
130 * @param array $batch Data to generate queries for. Will be `data` array returned by fetch_data_for_migration_for_ids() method.
131 *
132 * @param array $entity_row_mapping Maps rows to update data with their original IDs. Will be returned by `generate_update_sql_for_batch`.
133 *
134 * @return string Generated queries for batch update. Would be of the form:
135 * INSERT INTO $table ( $columns ) VALUES
136 * ($value for row 1)
137 * ($value for row 2)
138 * ...
139 * ON DUPLICATE KEY UPDATE
140 * $column1 = VALUES($column1)
141 * $column2 = VALUES($column2)
142 * ...
143 */
144 private function generate_update_sql_for_batch( array $batch, array $entity_row_mapping ): string {
145 $table = $this->schema_config['destination']['table_name'];
146
147 $destination_primary_id_schema = $this->get_destination_table_primary_id_schema();
148 foreach ( $batch as $entity_id => $row ) {
149 $batch[ $entity_id ][ $destination_primary_id_schema['destination_primary_key']['destination'] ] = $entity_row_mapping[ $entity_id ]->destination_id;
150 }
151
152 list( $value_sql, $column_sql, $columns ) = $this->generate_column_clauses(
153 array_merge( $destination_primary_id_schema, $this->core_column_mapping, $this->meta_column_mapping ),
154 $batch
155 );
156
157 $duplicate_update_key_statement = MigrationHelper::generate_on_duplicate_statement_clause( $columns );
158
159 return "INSERT INTO $table (`$column_sql`) VALUES $value_sql $duplicate_update_key_statement;";
160 }
161
162 /**
163 * Generate schema for primary ID column of destination table.
164 *
165 * @return array[] Schema for primary ID column.
166 */
167 private function get_destination_table_primary_id_schema(): array {
168 return array(
169 'destination_primary_key' => array(
170 'destination' => $this->schema_config['destination']['primary_key'],
171 'type' => $this->schema_config['destination']['primary_key_type'],
172 ),
173 );
174 }
175
176 /**
177 * Generate values and columns clauses to be used in INSERT and INSERT..ON DUPLICATE KEY UPDATE statements.
178 *
179 * @param array $columns_schema Columns config for destination table.
180 * @param array $batch Actual data to migrate as returned by `data` in `fetch_data_for_migration_for_ids` method.
181 *
182 * @return array SQL clause for values, columns placeholders, and columns.
183 */
184 private function generate_column_clauses( array $columns_schema, array $batch ): array {
185 global $wpdb;
186
187 $columns = array();
188 $placeholders = array();
189 foreach ( $columns_schema as $prev_column => $schema ) {
190 if ( in_array( $schema['destination'], $columns, true ) ) {
191 continue;
192 }
193 $columns[] = $schema['destination'];
194 $placeholders[] = MigrationHelper::get_wpdb_placeholder_for_type( $schema['type'] );
195 }
196
197 $values = array();
198 foreach ( array_values( $batch ) as $row ) {
199 $row_values = array();
200 foreach ( $columns as $index => $column ) {
201 if ( ! isset( $row[ $column ] ) || is_null( $row[ $column ] ) ) {
202 $row_values[] = 'NULL';
203 } else {
204 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- $placeholders is a placeholder.
205 $row_values[] = $wpdb->prepare( $placeholders[ $index ], $row[ $column ] );
206 }
207 }
208
209 $value_string = '(' . implode( ',', $row_values ) . ')';
210 $values[] = $value_string;
211 }
212
213 $value_sql = implode( ',', $values );
214
215 $column_sql = implode( '`, `', $columns );
216
217 return array( $value_sql, $column_sql, $columns );
218 }
219
220 /**
221 * Return data to be migrated for a batch of entities.
222 *
223 * @param array $entity_ids Ids of entities to migrate.
224 *
225 * @return array[] Data to be migrated. Would be of the form: array( 'data' => array( ... ), 'errors' => array( ... ) ).
226 */
227 public function fetch_sanitized_migration_data( $entity_ids ) {
228 $this->clear_errors();
229 $data = $this->fetch_data_for_migration_for_ids( $entity_ids );
230
231 foreach ( $data['errors'] as $entity_id => $errors ) {
232 foreach ( $errors as $column_name => $error_message ) {
233 $this->add_error( "Error importing data for post with id $entity_id: column $column_name: $error_message" );
234 }
235 }
236 return array(
237 'data' => $data['data'],
238 'errors' => $this->get_errors(),
239 );
240 }
241
242 /**
243 * Migrate a batch of entities from the posts table to the corresponding table.
244 *
245 * @param array $entity_ids Ids of entities to migrate.
246 *
247 * @return void
248 */
249 protected function process_migration_batch_for_ids_core( array $entity_ids ): void {
250 $data = $this->fetch_sanitized_migration_data( $entity_ids );
251 $this->process_migration_data( $data );
252 }
253
254 /**
255 * Process migration data for a batch of entities.
256 *
257 * @param array $data Data to be migrated. Should be of the form: array( 'data' => array( ... ) ) as returned by the `fetch_sanitized_migration_data` method.
258 *
259 * @return array Array of errors and exception if any.
260 */
261 public function process_migration_data( array $data ) {
262 $this->clear_errors();
263 $exception = null;
264
265 if ( ! isset( $data['data'] ) || ! is_array( $data['data'] ) || count( $data['data'] ) === 0 ) {
266 return array(
267 'errors' => $this->get_errors(),
268 'exception' => null,
269 );
270 }
271
272 try {
273 $entity_ids = array_keys( $data['data'] );
274 $existing_records = $this->get_already_existing_records( $entity_ids );
275
276 $to_insert = array_diff_key( $data['data'], $existing_records );
277 $this->process_insert_batch( $to_insert );
278
279 $to_update = array_intersect_key( $data['data'], $existing_records );
280 $this->process_update_batch( $to_update, $existing_records );
281 } catch ( \Exception $e ) {
282 $exception = $e;
283 }
284
285 return array(
286 'errors' => $this->get_errors(),
287 'exception' => $exception,
288 );
289 }
290
291 /**
292 * Process batch for insertion into destination table.
293 *
294 * @param array $batch Data to insert, will be of the form as returned by `data` in `fetch_data_for_migration_for_ids`.
295 */
296 private function process_insert_batch( array $batch ): void {
297 if ( 0 === count( $batch ) ) {
298 return;
299 }
300
301 $queries = $this->generate_insert_sql_for_batch( $batch );
302 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Queries should already be prepared.
303 $processed_rows_count = $this->db_query( $queries );
304 $this->maybe_add_insert_or_update_error( 'insert', $processed_rows_count );
305 }
306
307 /**
308 * Process batch for update into destination table.
309 *
310 * @param array $batch Data to insert, will be of the form as returned by `data` in `fetch_data_for_migration_for_ids`.
311 * @param array $ids_mapping Maps rows to update data with their original IDs.
312 */
313 private function process_update_batch( array $batch, array $ids_mapping ): void {
314 if ( 0 === count( $batch ) ) {
315 return;
316 }
317
318 $queries = $this->generate_update_sql_for_batch( $batch, $ids_mapping );
319 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Queries should already be prepared.
320 $processed_rows_count = $this->db_query( $queries ) / 2;
321 $this->maybe_add_insert_or_update_error( 'update', $processed_rows_count );
322 }
323
324
325 /**
326 * Fetch data for migration.
327 *
328 * @param array $entity_ids Entity IDs to fetch data for.
329 *
330 * @return array[] Data along with errors (if any), will of the form:
331 * array(
332 * 'data' => array(
333 * 'id_1' => array( 'column1' => value1, 'column2' => value2, ...),
334 * ...,
335 * ),
336 * 'errors' => array(
337 * 'id_1' => array( 'column1' => error1, 'column2' => value2, ...),
338 * ...,
339 * )
340 */
341 private function fetch_data_for_migration_for_ids( array $entity_ids ): array {
342 if ( empty( $entity_ids ) ) {
343 return array(
344 'data' => array(),
345 'errors' => array(),
346 );
347 }
348
349 $entity_table_query = $this->build_entity_table_query( $entity_ids );
350 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_entity_table_query is already prepared.
351 $entity_data = $this->db_get_results( $entity_table_query );
352 if ( empty( $entity_data ) ) {
353 return array(
354 'data' => array(),
355 'errors' => array(),
356 );
357 }
358 $entity_meta_rel_ids = array_column( $entity_data, 'entity_meta_rel_id' );
359
360 $meta_table_query = $this->build_meta_data_query( $entity_meta_rel_ids );
361 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_meta_data_query is already prepared.
362 $meta_data = $this->db_get_results( $meta_table_query );
363
364 return $this->process_and_sanitize_data( $entity_data, $meta_data );
365 }
366
367 /**
368 * Fetch id mappings for records that are already inserted in the destination table.
369 *
370 * @param array $entity_ids List of entity IDs to verify.
371 *
372 * @return array Already migrated entities, would be of the form
373 * array(
374 * '$source_id1' => array(
375 * 'source_id' => $source_id1,
376 * 'destination_id' => $destination_id1
377 * 'modified' => 0 if it can be determined that the row doesn't need update, 1 otherwise
378 * ),
379 * ...
380 * )
381 */
382 protected function get_already_existing_records( array $entity_ids ): array {
383 global $wpdb;
384
385 $source_table = $this->schema_config['source']['entity']['table_name'];
386 $source_destination_join_column = $this->schema_config['source']['entity']['destination_rel_column'];
387 $source_primary_key_column = $this->schema_config['source']['entity']['primary_key'];
388
389 $destination_table = $this->schema_config['destination']['table_name'];
390 $destination_source_join_column = $this->schema_config['destination']['source_rel_column'];
391 $destination_primary_key_column = $this->schema_config['destination']['primary_key'];
392
393 $entity_id_placeholder = implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) );
394
395 $additional_where = $this->get_additional_where_clause_for_get_data_to_insert_or_update( $entity_ids );
396
397 $already_migrated_entity_ids = $this->db_get_results(
398 $wpdb->prepare(
399 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded.
400 "
401 SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id
402 FROM `$destination_table` destination
403 JOIN `$source_table` source ON source.`$source_destination_join_column` = destination.`$destination_source_join_column`
404 WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) $additional_where
405 ",
406 $entity_ids
407 )
408 // phpcs:enable
409 );
410
411 return array_column( $already_migrated_entity_ids, null, 'source_id' );
412 }
413
414 /**
415 * Get additional string to be appended to the WHERE clause of the SQL query used by get_data_to_insert_or_update.
416 *
417 * @param array $entity_ids The ids of the entities being inserted or updated.
418 * @return string Additional string for the WHERE clause, must either be empty or start with "AND" or "OR".
419 */
420 protected function get_additional_where_clause_for_get_data_to_insert_or_update( array $entity_ids ): string {
421 return '';
422 }
423
424 /**
425 * Helper method to build query used to fetch data from core source table.
426 *
427 * @param array $entity_ids List of entity IDs to fetch.
428 *
429 * @return string Query that can be used to fetch data.
430 */
431 private function build_entity_table_query( array $entity_ids ): string {
432 global $wpdb;
433
434 $source_entity_table = $this->schema_config['source']['entity']['table_name'];
435 $source_meta_rel_id_column = "`$source_entity_table`.`{$this->schema_config['source']['entity']['meta_rel_column']}`";
436 $source_primary_key_column = "`$source_entity_table`.`{$this->schema_config['source']['entity']['primary_key']}`";
437
438 $where_clause = "$source_primary_key_column IN (" . implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) ) . ')';
439 $entity_keys = array();
440 foreach ( $this->core_column_mapping as $column_name => $column_schema ) {
441 if ( isset( $column_schema['select_clause'] ) ) {
442 $select_clause = $column_schema['select_clause'];
443 $entity_keys[] = "$select_clause AS $column_name";
444 } else {
445 $entity_keys[] = "$source_entity_table.$column_name";
446 }
447 }
448 $entity_column_string = implode( ', ', $entity_keys );
449 // 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.
450 $query = $wpdb->prepare(
451 "
452 SELECT
453 $source_meta_rel_id_column as entity_meta_rel_id,
454 $source_primary_key_column as primary_key_id,
455 $entity_column_string
456 FROM `$source_entity_table`
457 WHERE $where_clause;
458 ",
459 $entity_ids
460 );
461
462 // phpcs:enable
463
464 return $query;
465 }
466
467 /**
468 * Helper method to build query that will be used to fetch data from source meta table.
469 *
470 * @param array $entity_ids List of IDs to fetch metadata for.
471 *
472 * @return string Query for fetching meta data.
473 */
474 private function build_meta_data_query( array $entity_ids ): string {
475 global $wpdb;
476
477 $meta_table = $this->schema_config['source']['meta']['table_name'];
478 $meta_keys = array_keys( $this->meta_column_mapping );
479 $meta_key_column = $this->schema_config['source']['meta']['meta_key_column'];
480 $meta_value_column = $this->schema_config['source']['meta']['meta_value_column'];
481 $meta_table_relational_key = $this->schema_config['source']['meta']['entity_id_column'];
482
483 $meta_column_string = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) );
484 $entity_id_string = implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) );
485
486 // 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.
487 $query = $wpdb->prepare(
488 "
489 SELECT `$meta_table_relational_key` as entity_id, `$meta_key_column` as meta_key, `$meta_value_column` as meta_value
490 FROM `$meta_table`
491 WHERE
492 `$meta_table_relational_key` IN ( $entity_id_string )
493 AND `$meta_key_column` IN ( $meta_column_string );
494 ",
495 array_merge(
496 $entity_ids,
497 $meta_keys
498 )
499 );
500
501 // phpcs:enable
502
503 return $query;
504 }
505
506 /**
507 * Helper function to validate and combine data before we try to insert.
508 *
509 * @param array $entity_data Data from source table.
510 * @param array $meta_data Data from meta table.
511 *
512 * @return array[] Validated and combined data with errors.
513 */
514 private function process_and_sanitize_data( array $entity_data, array $meta_data ): array {
515 $sanitized_entity_data = array();
516 $error_records = array();
517 $this->process_and_sanitize_entity_data( $sanitized_entity_data, $error_records, $entity_data );
518 $this->processs_and_sanitize_meta_data( $sanitized_entity_data, $error_records, $meta_data );
519
520 return array(
521 'data' => $sanitized_entity_data,
522 'errors' => $error_records,
523 );
524 }
525
526 /**
527 * Helper method to sanitize core source table.
528 *
529 * @param array $sanitized_entity_data Array containing sanitized data for insertion.
530 * @param array $error_records Error records.
531 * @param array $entity_data Original source data.
532 */
533 private function process_and_sanitize_entity_data( array &$sanitized_entity_data, array &$error_records, array $entity_data ): void {
534 foreach ( $entity_data as $entity ) {
535 $row_data = array();
536 foreach ( $this->core_column_mapping as $column_name => $schema ) {
537 $custom_table_column_name = $schema['destination'] ?? $column_name;
538 $value = $entity->$column_name;
539 $value = $this->validate_data( $value, $schema['type'] );
540 if ( is_wp_error( $value ) ) {
541 $error_records[ $entity->primary_key_id ][ $custom_table_column_name ] = $value->get_error_code();
542 } else {
543 $row_data[ $custom_table_column_name ] = $value;
544 }
545 }
546 $sanitized_entity_data[ $entity->entity_meta_rel_id ] = $row_data;
547 }
548 }
549
550 /**
551 * Helper method to sanitize soure meta data.
552 *
553 * @param array $sanitized_entity_data Array containing sanitized data for insertion.
554 * @param array $error_records Error records.
555 * @param array $meta_data Original source data.
556 */
557 private function processs_and_sanitize_meta_data( array &$sanitized_entity_data, array &$error_records, array $meta_data ): void {
558 foreach ( $meta_data as $datum ) {
559 $column_schema = $this->meta_column_mapping[ $datum->meta_key ];
560 if ( isset( $sanitized_entity_data[ $datum->entity_id ][ $column_schema['destination'] ] ) ) {
561 // We pick only the first meta if there are duplicates for a flat column, to be consistent with WP core behavior in handing duplicate meta which are marked as unique.
562 continue;
563 }
564 $value = $this->validate_data( $datum->meta_value, $column_schema['type'] );
565 if ( is_wp_error( $value ) ) {
566 $error_records[ $datum->entity_id ][ $column_schema['destination'] ] = "{$value->get_error_code()}: {$value->get_error_message()}";
567 } else {
568 $sanitized_entity_data[ $datum->entity_id ][ $column_schema['destination'] ] = $value;
569 }
570 }
571 }
572
573 /**
574 * Validate and transform data so that we catch as many errors as possible before inserting.
575 *
576 * @param mixed $value Actual data value.
577 * @param string $type Type of data, could be decimal, int, date, string.
578 *
579 * @return float|int|mixed|string|\WP_Error
580 */
581 private function validate_data( $value, string $type ) {
582 switch ( $type ) {
583 case 'decimal':
584 $value = wc_format_decimal( floatval( $value ), false, true );
585 break;
586 case 'int':
587 $value = (int) $value;
588 break;
589 case 'bool':
590 $value = wc_string_to_bool( $value );
591 break;
592 case 'date':
593 try {
594 if ( '' === $value ) {
595 $value = null;
596 } else {
597 $value = ( new \DateTime( $value ) )->format( 'Y-m-d H:i:s' );
598 }
599 } catch ( \Exception $e ) {
600 return new \WP_Error( $e->getMessage() );
601 }
602 break;
603 case 'date_epoch':
604 try {
605 if ( '' === $value ) {
606 $value = null;
607 } else {
608 $value = ( new \DateTime( "@$value" ) )->format( 'Y-m-d H:i:s' );
609 }
610 } catch ( \Exception $e ) {
611 return new \WP_Error( $e->getMessage() );
612 }
613 break;
614 }
615
616 return $value;
617 }
618
619 /**
620 * Verify whether data was migrated properly for given IDs.
621 *
622 * @param array $source_ids List of source IDs.
623 *
624 * @return array List of IDs along with columns that failed to migrate.
625 */
626 public function verify_migrated_data( array $source_ids ) : array {
627 global $wpdb;
628 $query = $this->build_verification_query( $source_ids );
629 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query should already be prepared.
630 $results = $wpdb->get_results( $query, ARRAY_A );
631 $results = $this->fill_source_metadata( $results, $source_ids );
632 return $this->verify_data( $results );
633 }
634
635 /**
636 * Generate query to fetch data from both source and destination tables. Use the results in `verify_data` to verify if data was migrated properly.
637 *
638 * @param array $source_ids Array of IDs in source table.
639 *
640 * @return string SELECT statement.
641 */
642 protected function build_verification_query( $source_ids ) {
643 $source_table = $this->schema_config['source']['entity']['table_name'];
644 $destination_table = $this->schema_config['destination']['table_name'];
645 $destination_source_rel_column = $this->schema_config['destination']['source_rel_column'];
646 $source_destination_rel_column = $this->schema_config['source']['entity']['destination_rel_column'];
647
648 $source_destination_join_clause = "$destination_table ON $destination_table.$destination_source_rel_column = $source_table.$source_destination_rel_column";
649
650 $meta_select_clauses = array();
651 $source_select_clauses = array();
652 $destination_select_clauses = array();
653
654 foreach ( $this->core_column_mapping as $column_name => $schema ) {
655 $source_select_column = isset( $schema['select_clause'] ) ? $schema['select_clause'] : "$source_table.$column_name";
656 $source_select_clauses[] = "$source_select_column as {$source_table}_{$column_name}";
657 $destination_select_clauses[] = "$destination_table.{$schema['destination']} as {$destination_table}_{$schema['destination']}";
658 }
659
660 foreach ( $this->meta_column_mapping as $meta_key => $schema ) {
661 $destination_select_clauses[] = "$destination_table.{$schema['destination']} as {$destination_table}_{$schema['destination']}";
662 }
663
664 $select_clause = implode( ', ', array_merge( $source_select_clauses, $meta_select_clauses, $destination_select_clauses ) );
665
666 $where_clause = $this->get_where_clause_for_verification( $source_ids );
667
668 return "
669 SELECT $select_clause
670 FROM $source_table
671 LEFT JOIN $source_destination_join_clause
672 WHERE $where_clause
673 ";
674 }
675
676 /**
677 * Fill source metadata for given IDs for verification. This will return filled data in following format:
678 * [
679 * {
680 * $source_table_$source_column: $value,
681 * ...,
682 * $destination_table_$destination_column: $value,
683 * ...
684 * meta_source_{$destination_column_name1}: $meta_value,
685 * ...
686 * },
687 * ...
688 * ]
689 *
690 * @param array $results Entity data from both source and destination table.
691 * @param array $source_ids List of source IDs.
692 *
693 * @return array Filled $results param with source metadata.
694 */
695 private function fill_source_metadata( $results, $source_ids ) {
696 global $wpdb;
697 $meta_table = $this->schema_config['source']['meta']['table_name'];
698 $meta_entity_id_column = $this->schema_config['source']['meta']['entity_id_column'];
699 $meta_key_column = $this->schema_config['source']['meta']['meta_key_column'];
700 $meta_value_column = $this->schema_config['source']['meta']['meta_value_column'];
701 $meta_id_column = $this->schema_config['source']['meta']['meta_id_column'];
702 $meta_columns = array_keys( $this->meta_column_mapping );
703
704 $meta_columns_placeholder = implode( ', ', array_fill( 0, count( $meta_columns ), '%s' ) );
705 $source_ids_placeholder = implode( ', ', array_fill( 0, count( $source_ids ), '%d' ) );
706
707 $query = $wpdb->prepare(
708 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
709 "SELECT $meta_entity_id_column as entity_id, $meta_key_column as meta_key, $meta_value_column as meta_value
710 FROM $meta_table
711 WHERE $meta_entity_id_column IN ($source_ids_placeholder)
712 AND $meta_key_column IN ($meta_columns_placeholder)
713 ORDER BY $meta_id_column ASC",
714 array_merge( $source_ids, $meta_columns )
715 );
716 //phpcs:enable
717
718 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
719 $meta_data = $wpdb->get_results( $query, ARRAY_A );
720 $source_metadata_rows = array();
721 foreach ( $meta_data as $meta_datum ) {
722 if ( ! isset( $source_metadata_rows[ $meta_datum['entity_id'] ] ) ) {
723 $source_metadata_rows[ $meta_datum['entity_id'] ] = array();
724 }
725 $destination_column = $this->meta_column_mapping[ $meta_datum['meta_key'] ]['destination'];
726 $alias = "meta_source_{$destination_column}";
727 if ( isset( $source_metadata_rows[ $meta_datum['entity_id'] ][ $alias ] ) ) {
728 // Only process first value, duplicate values mapping to flat columns are ignored to be consistent with WP core.
729 continue;
730 }
731 $source_metadata_rows[ $meta_datum['entity_id'] ][ $alias ] = $meta_datum['meta_value'];
732 }
733 foreach ( $results as $index => $result_row ) {
734 $source_id = $result_row[ $this->schema_config['source']['entity']['table_name'] . '_' . $this->schema_config['source']['entity']['primary_key'] ];
735 $results[ $index ] = array_merge( $result_row, ( $source_metadata_rows[ $source_id ] ?? array() ) );
736 }
737 return $results;
738 }
739
740 /**
741 * Helper function to generate where clause for fetching data for verification.
742 *
743 * @param array $source_ids Array of IDs from source table.
744 *
745 * @return string WHERE clause.
746 */
747 protected function get_where_clause_for_verification( $source_ids ) {
748 global $wpdb;
749 $source_primary_id_column = $this->schema_config['source']['entity']['primary_key'];
750 $source_table = $this->schema_config['source']['entity']['table_name'];
751 $source_ids_placeholder = implode( ', ', array_fill( 0, count( $source_ids ), '%d' ) );
752
753 return $wpdb->prepare(
754 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
755 "$source_table.$source_primary_id_column IN ($source_ids_placeholder)",
756 $source_ids
757 );
758 }
759
760 /**
761 * Verify data from both source and destination tables and check if they were migrated properly.
762 *
763 * @param array $collected_data Collected data in array format, should be in same structure as returned from query in `$this->build_verification_query`.
764 *
765 * @return array Array of failed IDs if any, along with columns/meta_key names.
766 */
767 protected function verify_data( $collected_data ) {
768 $failed_ids = array();
769 foreach ( $collected_data as $row ) {
770 $failed_ids = $this->verify_entity_columns( $row, $failed_ids );
771 $failed_ids = $this->verify_meta_columns( $row, $failed_ids );
772 }
773
774 return $failed_ids;
775 }
776
777 /**
778 * Helper method to verify and compare core columns.
779 *
780 * @param array $row Both migrated and source data for a single row.
781 * @param array $failed_ids Array of failed IDs.
782 *
783 * @return array Array of failed IDs if any, along with columns/meta_key names.
784 */
785 private function verify_entity_columns( $row, $failed_ids ) {
786 $primary_key_column = "{$this->schema_config['source']['entity']['table_name']}_{$this->schema_config['source']['entity']['primary_key']}";
787 foreach ( $this->core_column_mapping as $column_name => $schema ) {
788 $source_alias = "{$this->schema_config['source']['entity']['table_name']}_$column_name";
789 $destination_alias = "{$this->schema_config['destination']['table_name']}_{$schema['destination']}";
790 $row = $this->pre_process_row( $row, $schema, $source_alias, $destination_alias );
791 if ( $row[ $source_alias ] !== $row[ $destination_alias ] ) {
792 if ( ! isset( $failed_ids[ $row[ $primary_key_column ] ] ) ) {
793 $failed_ids[ $row[ $primary_key_column ] ] = array();
794 }
795 $failed_ids[ $row[ $primary_key_column ] ][] = array(
796 'column' => $column_name,
797 'original_value' => $row[ $source_alias ],
798 'new_value' => $row[ $destination_alias ],
799 );
800 }
801 }
802
803 return $failed_ids;
804 }
805
806 /**
807 * Helper method to verify meta columns.
808 *
809 * @param array $row Both migrated and source data for a single row.
810 * @param array $failed_ids Array of failed IDs.
811 *
812 * @return array Array of failed IDs if any, along with columns/meta_key names.
813 */
814 private function verify_meta_columns( $row, $failed_ids ) {
815 $primary_key_column = "{$this->schema_config['source']['entity']['table_name']}_{$this->schema_config['source']['entity']['primary_key']}";
816 foreach ( $this->meta_column_mapping as $meta_key => $schema ) {
817 $meta_alias = "meta_source_{$schema['destination']}";
818 $destination_alias = "{$this->schema_config['destination']['table_name']}_{$schema['destination']}";
819 $row = $this->pre_process_row( $row, $schema, $meta_alias, $destination_alias );
820 if ( $row[ $meta_alias ] !== $row[ $destination_alias ] ) {
821 if ( ! isset( $failed_ids[ $row[ $primary_key_column ] ] ) ) {
822 $failed_ids[ $row[ $primary_key_column ] ] = array();
823 }
824 $failed_ids[ $row[ $primary_key_column ] ][] = array(
825 'column' => $meta_key,
826 'original_value' => $row[ $meta_alias ],
827 'new_value' => $row[ $destination_alias ],
828 );
829 }
830 }
831
832 return $failed_ids;
833 }
834
835 /**
836 * Helper method to pre-process rows to make sure we parse the correct type.
837 *
838 * @param array $row Both migrated and source data for a single row.
839 * @param array $schema Column schema.
840 * @param string $alias Name of source column.
841 * @param string $destination_alias Name of destination column.
842 *
843 * @return array Processed row.
844 */
845 private function pre_process_row( $row, $schema, $alias, $destination_alias ) {
846 if ( ! isset( $row[ $alias ] ) ) {
847 $row[ $alias ] = $this->get_type_defaults( $schema['type'] );
848 }
849 if ( is_null( $row[ $destination_alias ] ) ) {
850 $row[ $destination_alias ] = $this->get_type_defaults( $schema['type'] );
851 }
852 if ( in_array( $schema['type'], array( 'int', 'decimal', 'float' ), true ) ) {
853 if ( '' === $row[ $alias ] || null === $row[ $alias ] ) {
854 $row[ $alias ] = 0; // $wpdb->prepare forces empty values to 0.
855 }
856 $row[ $alias ] = wc_format_decimal( floatval( $row[ $alias ] ), false, true );
857 $row[ $destination_alias ] = wc_format_decimal( floatval( $row[ $destination_alias ] ), false, true );
858 }
859 if ( 'bool' === $schema['type'] ) {
860 $row[ $alias ] = wc_string_to_bool( $row[ $alias ] );
861 $row[ $destination_alias ] = wc_string_to_bool( $row[ $destination_alias ] );
862 }
863 if ( 'date_epoch' === $schema['type'] ) {
864 if ( '' === $row[ $alias ] || null === $row[ $alias ] ) {
865 $row[ $alias ] = null;
866 } else {
867 $row[ $alias ] = ( new \DateTime( "@{$row[ $alias ]}" ) )->format( 'Y-m-d H:i:s' );
868 }
869 if ( '0000-00-00 00:00:00' === $row[ $destination_alias ] ) {
870 $row[ $destination_alias ] = null;
871 }
872 }
873 return $row;
874 }
875
876 /**
877 * Helper method to get default value of a type.
878 *
879 * @param string $type Type.
880 *
881 * @return mixed Default value.
882 */
883 private function get_type_defaults( $type ) {
884 switch ( $type ) {
885 case 'float':
886 case 'int':
887 case 'decimal':
888 return 0;
889 case 'string':
890 return '';
891 }
892 }
893 }
894