PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.16
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.16
1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 1.9.13 All 162 releases
woocommerce-pos / includes / Services / Meta_Data_Repair.php

Meta_Data_Repair.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.16, at includes/Services/Meta_Data_Repair.php

175 lines 4.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Safe repair helpers for duplicated postmeta rows.
4 *
5 * @package WCPOS\WooCommercePOS
6 */
7
8 namespace WCPOS\WooCommercePOS\Services;
9
10 /**
11 * Metadata repair service.
12 */
13 class Meta_Data_Repair {
14 /**
15 * Remove exact duplicate postmeta rows from POS-touched products/variations.
16 *
17 * This is intentionally conservative:
18 * - scopes to selected post types (defaults: product + variation),
19 * - scopes to POS-touched posts (requires _woocommerce_pos_uuid by default),
20 * - deduplicates only exact (post_id, meta_key, meta_value) matches.
21 *
22 * @param array $args Repair options: dry_run, batch_size, max_batches,
23 * post_types, and require_pos_uuid.
24 *
25 * @return array{
26 * groups_processed:int,
27 * rows_deleted:int,
28 * rows_would_delete:int,
29 * batches_processed:int,
30 * hit_batch_limit:bool,
31 * errors:int
32 * }
33 */
34 public static function remove_exact_duplicate_postmeta_rows( array $args = array() ): array {
35 global $wpdb;
36
37 $args = wp_parse_args(
38 $args,
39 array(
40 'dry_run' => false,
41 'batch_size' => 500,
42 'max_batches' => 200,
43 'post_types' => array( 'product', 'product_variation' ),
44 'require_pos_uuid' => true,
45 )
46 );
47
48 $batch_size = max( 1, (int) $args['batch_size'] );
49 $max_batches = max( 1, (int) $args['max_batches'] );
50 $post_types = array_values(
51 array_filter(
52 (array) $args['post_types'],
53 static function ( $post_type ): bool {
54 return \is_string( $post_type ) && '' !== $post_type;
55 }
56 )
57 );
58
59 $summary = array(
60 'groups_processed' => 0,
61 'rows_deleted' => 0,
62 'rows_would_delete' => 0,
63 'batches_processed' => 0,
64 'hit_batch_limit' => false,
65 'errors' => 0,
66 );
67
68 if ( empty( $post_types ) ) {
69 return $summary;
70 }
71
72 for ( $batch = 0; $batch < $max_batches; $batch++ ) {
73 $summary['batches_processed'] = $batch + 1;
74
75 $groups = self::get_duplicate_groups(
76 $post_types,
77 $batch_size,
78 (bool) $args['require_pos_uuid']
79 );
80
81 if ( empty( $groups ) ) {
82 return $summary;
83 }
84
85 foreach ( $groups as $group ) {
86 $summary['groups_processed']++;
87 $rows_for_group = max( 0, (int) $group->row_count - 1 );
88
89 if ( $args['dry_run'] ) {
90 $summary['rows_would_delete'] += $rows_for_group;
91 continue;
92 }
93
94 $deleted = $wpdb->query(
95 $wpdb->prepare(
96 "DELETE FROM {$wpdb->postmeta}
97 WHERE post_id = %d
98 AND meta_key = %s
99 AND meta_value = %s
100 AND meta_id <> %d",
101 (int) $group->post_id,
102 (string) $group->meta_key,
103 (string) $group->meta_value,
104 (int) $group->keep_id
105 )
106 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- direct repair query.
107
108 if ( false === $deleted ) {
109 $summary['errors']++;
110 } else {
111 $summary['rows_deleted'] += (int) $deleted;
112 }
113 }
114
115 if ( $args['dry_run'] ) {
116 if ( \count( $groups ) >= $batch_size ) {
117 $summary['hit_batch_limit'] = true;
118 }
119
120 return $summary;
121 }
122
123 if ( \count( $groups ) < $batch_size ) {
124 return $summary;
125 }
126 }
127
128 $summary['hit_batch_limit'] = true;
129
130 return $summary;
131 }
132
133 /**
134 * Fetch duplicate groups for the current batch.
135 *
136 * @param array $post_types Post types to process.
137 * @param int $batch_size Max groups to return.
138 * @param bool $require_pos_uuid Require POS UUID marker on post.
139 *
140 * @return array<\stdClass>
141 */
142 private static function get_duplicate_groups( array $post_types, int $batch_size, bool $require_pos_uuid ): array {
143 global $wpdb;
144
145 $post_type_placeholders = implode( ', ', array_fill( 0, \count( $post_types ), '%s' ) );
146
147 $sql = "SELECT pm.post_id, pm.meta_key, pm.meta_value, COUNT(*) AS row_count, MAX(pm.meta_id) AS keep_id
148 FROM {$wpdb->postmeta} pm
149 INNER JOIN {$wpdb->posts} posts ON posts.ID = pm.post_id
150 WHERE posts.post_type IN ({$post_type_placeholders})";
151
152 if ( $require_pos_uuid ) {
153 $sql .= " AND EXISTS (
154 SELECT 1
155 FROM {$wpdb->postmeta} pu
156 WHERE pu.post_id = pm.post_id
157 AND pu.meta_key = '_woocommerce_pos_uuid'
158 )";
159 }
160
161 $sql .= '
162 GROUP BY pm.post_id, pm.meta_key, pm.meta_value
163 HAVING COUNT(*) > 1
164 ORDER BY pm.post_id ASC, pm.meta_key ASC
165 LIMIT %d';
166
167 $params = array_merge( $post_types, array( $batch_size ) );
168 $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- placeholders prepared in the same call; direct repair query.
169 $wpdb->prepare( $sql, $params ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is prepared in this call.
170 );
171
172 return \is_array( $results ) ? $results : array();
173 }
174 }
175