fix(2meet-data-optimizer): archive sweep copies each trashed meta row once

- sweep() skips meta rows already in wpdo_archive (NOT EXISTS on
  original_meta_id): it copied the same trashed meta again every day and
  wpdo_archive has no unique key (dev20: 300 rows for 150 meta rows)
- TMDO_Zone_Archive::remove_duplicates() keeps the first copy per
  original_meta_id (rows without one are left alone); install() runs it,
  so `wp tmdo install` cleans existing sites
- wpdo_archive gets KEY idx_original_meta for the lookup (new installs;
  existing sites via `wp tmdo install`). SCHEMA_VERSION is not bumped, so
  no web request runs dbDelta
- Verify: ZoneArchiveIntegrationTest (2 new); dev20 read-only check: 150 of
  300 rows would be removed; unit 709, integration 608, phpcs 0, stan OK
- PR: PR-136 (manual F-6)
This commit is contained in:
2026-09-28 18:17:47 +08:00
parent 86ac84e07c
commit 6687d54bef
3 changed files with 81 additions and 1 deletions
+11 -1
View File
@@ -217,6 +217,15 @@ class TMDO_Installer {
// Idempotent — only creates tables that don't already exist.
self::install_v2_tables();
// 1.4.2: archive sweeps before this release copied trashed meta daily.
if ( class_exists( 'TMDO_Zone_Archive' ) ) {
global $wpdb;
$archive = TMDO_Zone_Archive::table();
if ( $archive === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $archive ) ) ) ) {
TMDO_Zone_Archive::remove_duplicates();
}
}
update_option( 'wpdo_db_version', self::SCHEMA_VERSION );
// Register wpdo_features with autoload=no so it doesn't inflate every page load.
@@ -1440,7 +1449,8 @@ class TMDO_Installer {
original_meta_id bigint(20) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY idx_post_id (post_id),
KEY idx_post_type_date (post_type, archived_at)
KEY idx_post_type_date (post_type, archived_at),
KEY idx_original_meta (original_meta_id)
) {$charset};";
foreach ( $sqls as $sql ) {
@@ -210,7 +210,11 @@ class TMDO_Zone_Archive {
global $wpdb;
$cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );
// Rows already archived are skipped: the sweep used to copy the same
// trashed meta again every day (2.4.2 PR-136).
$table = self::table();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal self::table()
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT pm.meta_id, pm.post_id, pm.meta_key, pm.meta_value, p.post_type
@@ -218,12 +222,14 @@ class TMDO_Zone_Archive {
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE p.post_status = 'trash'
AND p.post_modified_gmt < %s
AND NOT EXISTS ( SELECT 1 FROM `{$table}` a WHERE a.original_meta_id = pm.meta_id )
LIMIT %d",
$cutoff,
$limit
),
ARRAY_A
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( ! $rows ) {
return 0;
@@ -245,6 +251,21 @@ class TMDO_Zone_Archive {
return count( $entries );
}
/**
* Delete archive rows that copy the same original meta row again, keeping
* the first copy (sweeps before 1.4.2 archived trashed meta every day).
* Rows without an original meta id are left alone.
*
* @return int Rows deleted.
*/
public static function remove_duplicates(): int {
global $wpdb;
$table = self::table();
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal self::table()
$deleted = $wpdb->query( "DELETE FROM `{$table}` WHERE original_meta_id > 0 AND id NOT IN ( SELECT keep_id FROM ( SELECT MIN(id) AS keep_id FROM `{$table}` WHERE original_meta_id > 0 GROUP BY original_meta_id ) AS k )" );
return is_int( $deleted ) ? $deleted : 0;
}
/**
* Get archive statistics.
*
@@ -45,6 +45,55 @@ class ZoneArchiveIntegrationTest extends TestCase {
$GLOBALS['_wp_postmeta'] = [];
}
// ── sweep / duplicates (2.4.2 PR-136, manual F-6) ─────────────────────
/** Posts and postmeta for sweep(): post 10 trashed 200 days ago, post 11 published. */
private function seed_trashed_post(): void {
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->posts}`" );
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->postmeta}`" );
$wpdb->query( "CREATE TABLE `{$wpdb->posts}` ( ID BIGINT UNSIGNED NOT NULL, post_type VARCHAR(20) NOT NULL DEFAULT 'post', post_status VARCHAR(20) NOT NULL DEFAULT 'publish', post_modified_gmt DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', PRIMARY KEY (ID) ) ENGINE=InnoDB" );
$wpdb->query( "CREATE TABLE `{$wpdb->postmeta}` ( meta_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT UNSIGNED NOT NULL DEFAULT 0, meta_key VARCHAR(255) DEFAULT NULL, meta_value LONGTEXT, PRIMARY KEY (meta_id) ) ENGINE=InnoDB" );
$old = gmdate( 'Y-m-d H:i:s', time() - 200 * DAY_IN_SECONDS );
$wpdb->query( "INSERT INTO `{$wpdb->posts}` VALUES (10, 'hp_listing', 'trash', '{$old}'), (11, 'hp_listing', 'publish', '{$old}')" );
$wpdb->query( "INSERT INTO `{$wpdb->postmeta}` (post_id, meta_key, meta_value) VALUES (10, 'hp_price', '5'), (10, 'hp_rating', '4'), (11, 'hp_price', '9')" );
}
private function drop_posts(): void {
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->posts}`" );
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->postmeta}`" );
}
private function archive_rows(): int {
global $wpdb;
return (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE . '`' );
}
public function test_sweep_copies_each_trashed_meta_row_once(): void {
$this->seed_trashed_post();
try {
$this->assertSame( 2, WPDO_Zone_Archive::sweep( 90, false ) );
$this->assertSame( 0, WPDO_Zone_Archive::sweep( 90, false ), 'already archived' );
$this->assertSame( 2, $this->archive_rows() );
} finally {
$this->drop_posts();
}
}
public function test_remove_duplicates_keeps_the_first_copy_of_each_meta_row(): void {
WPDO_Zone_Archive::archive( 10, 'hp_listing', 'hp_price', '5', 101 );
WPDO_Zone_Archive::archive( 10, 'hp_listing', 'hp_price', '5', 101 );
WPDO_Zone_Archive::archive( 10, 'hp_listing', 'hp_rating', '4', 102 );
WPDO_Zone_Archive::archive( 10, 'hp_listing', 'hp_rating', '4', 102 );
WPDO_Zone_Archive::archive( 12, 'hp_listing', 'hp_price', '7' ); // original_meta_id 0: no identity, kept.
WPDO_Zone_Archive::archive( 12, 'hp_listing', 'hp_price', '7' );
$this->assertSame( 2, WPDO_Zone_Archive::remove_duplicates() );
$this->assertSame( 4, $this->archive_rows() );
$this->assertSame( 0, WPDO_Zone_Archive::remove_duplicates() );
}
// ── archive / get ─────────────────────────────────────────────────────
public function test_archive_stores_plain_entry(): void {