fix(2meet-data-optimizer): 1.4.2 acceptance fixes for archive sweep and uninstall
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Successful in 21s
Tests / PHP Lint (pull_request) Successful in 18s
Tests / Unit Tests (pull_request) Successful in 41s
Tests / Integration Tests (pull_request) Successful in 1m25s
Tests / PHPCS (pull_request) Successful in 44s
Tests / PHPStan (pull_request) Successful in 47s

- Archive sweep waits until idx_original_meta exists (a plugin update does
  not run dbDelta; without the index NOT EXISTS scans the archive per row)
- Duplicate archive cleanup runs from WP-CLI only, never in a web activation
- Uninstall keeps the LatePoint add-on's tmdo_latepoint_db_version
- AJAX save errors stay on screen instead of fading after 2.5 s
- Verify: 2 new integration tests; unit 711, integration 612, phpcs 0, stan OK
- PR: PR-141 (2.4.2 correctness review)
This commit is contained in:
2026-09-28 18:43:50 +08:00
parent c9d5a08a42
commit 6f117f4e9b
7 changed files with 41 additions and 8 deletions
+10 -2
View File
@@ -32,8 +32,16 @@ Versioning follows [Semantic Versioning](https://semver.org/).
### 升級後 ### 升級後
- 每個站台執行一次 `wp tmdo install`:為 archive 表加上 `idx_original_meta` 並刪除重複的 archive - 每個站台執行一次 `wp tmdo install`:為 archive 表加上 `idx_original_meta` 並刪除重複的 archive
列。表很大時請在離峰時段執行(CLI,不在 web 請求中做 DDL)。未執行也不影響功能,只是 sweep 列(刪除重複只在 CLI 執行,web 啟用外掛時不做)。表很大時請在離峰時段執行。
的 NOT EXISTS 檢查少了索引。 - **未執行前 archive sweep 會暫停**(沒有索引時 NOT EXISTS 會對每筆候選掃整張 archive 表)。
sweep 只複製、不刪 postmeta,暫停不會遺失資料。
- 解除安裝的 option 清理保留 LatePoint AddOn 的 `tmdo_latepoint_db_version`;設定頁的 AJAX 錯誤訊息
不再 2.5 秒後自動消失。(PR-141,驗收修正)
### 已知、未修改
- 已歸檔的 meta 以 `meta_id` 判斷:文章還原、修改後再次丟到回收桶,archive 保留的是第一次的值。
- post 壓測工具預設仍是 `product`,但 product 已沒有 flat 表,量測會回報 `flat_table_missing`。
--- ---
+4 -1
View File
@@ -45,7 +45,7 @@
} }
/** /**
* Show a transient status message, then fade it out after 2.5s. * Show a status message; success fades out after 2.5s, errors stay.
* *
* @param {HTMLElement} statusEl * @param {HTMLElement} statusEl
* @param {string} message * @param {string} message
@@ -56,6 +56,9 @@
statusEl.style.color = isError ? 'var(--color-error, #b03d3d)' : 'var(--color-success, #5a8c5a)'; statusEl.style.color = isError ? 'var(--color-error, #b03d3d)' : 'var(--color-success, #5a8c5a)';
statusEl.classList.add( 'visible' ); statusEl.classList.add( 'visible' );
clearTimeout( statusEl._wpdo_timer ); clearTimeout( statusEl._wpdo_timer );
if ( isError ) {
return;
}
statusEl._wpdo_timer = setTimeout( () => { statusEl._wpdo_timer = setTimeout( () => {
statusEl.classList.remove( 'visible' ); statusEl.classList.remove( 'visible' );
}, 2500 ); }, 2500 );
+4 -2
View File
@@ -218,7 +218,8 @@ class TMDO_Installer {
self::install_v2_tables(); self::install_v2_tables();
// 1.4.2: archive sweeps before this release copied trashed meta daily. // 1.4.2: archive sweeps before this release copied trashed meta daily.
if ( class_exists( 'TMDO_Zone_Archive' ) ) { // One unbounded DELETE: CLI only, never in a web activation.
if ( defined( 'WP_CLI' ) && WP_CLI && class_exists( 'TMDO_Zone_Archive' ) ) {
global $wpdb; global $wpdb;
$archive = TMDO_Zone_Archive::table(); $archive = TMDO_Zone_Archive::table();
if ( $archive === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $archive ) ) ) ) { if ( $archive === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $archive ) ) ) ) {
@@ -993,6 +994,7 @@ class TMDO_Installer {
// accidental option removal. // accidental option removal.
// 1.4.2: tmdo_* options and their transients too (tmdo_schema_deferred, // 1.4.2: tmdo_* options and their transients too (tmdo_schema_deferred,
// tmdo_entity_indexes, tmdo_cov_* …) — a reinstall saw stale ones. // tmdo_entity_indexes, tmdo_cov_* …) — a reinstall saw stale ones.
// tmdo_latepoint_* belongs to the LatePoint add-on.
foreach ( array( 'wpdo_', 'tmdo_', '_transient_tmdo_', '_transient_timeout_tmdo_' ) as $prefix ) { foreach ( array( 'wpdo_', 'tmdo_', '_transient_tmdo_', '_transient_timeout_tmdo_' ) as $prefix ) {
$residual = $wpdb->get_col( $residual = $wpdb->get_col(
$wpdb->prepare( $wpdb->prepare(
@@ -1001,7 +1003,7 @@ class TMDO_Installer {
) )
); );
foreach ( (array) $residual as $opt ) { foreach ( (array) $residual as $opt ) {
if ( preg_match( '/^(_transient_(timeout_)?)?(wpdo|tmdo)_[a-zA-Z0-9_]+$/', $opt ) && delete_option( $opt ) ) { if ( preg_match( '/^(_transient_(timeout_)?)?(wpdo|tmdo)_(?!latepoint_)[a-zA-Z0-9_]+$/', $opt ) && delete_option( $opt ) ) {
++$options_deleted; ++$options_deleted;
} }
} }
@@ -213,6 +213,10 @@ class TMDO_Zone_Archive {
// Rows already archived are skipped: the sweep used to copy the same // Rows already archived are skipped: the sweep used to copy the same
// trashed meta again every day (2.4.2 PR-136). // trashed meta again every day (2.4.2 PR-136).
$table = self::table(); $table = self::table();
// Without the index each candidate scans the archive: wait for `wp tmdo install`.
if ( ! TMDO_IS_SQLITE && ! $wpdb->get_var( "SHOW INDEX FROM `{$table}` WHERE Key_name = 'idx_original_meta'" ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- internal table name
return 0;
}
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal self::table() // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal self::table()
$rows = $wpdb->get_results( $rows = $wpdb->get_results(
+1 -1
View File
@@ -109,7 +109,7 @@ The Classifier tab analyzes your existing `wp_postmeta` data (access frequency,
= 1.4.2 = = 1.4.2 =
* The settings page shows why a bridge-mode change was refused (unsafe jump, Promotion Guard) instead of reporting it saved * The settings page shows why a bridge-mode change was refused (unsafe jump, Promotion Guard) instead of reporting it saved
* Archive sweep skips meta already archived; `wp tmdo install` adds an index on original_meta_id and removes duplicate archive rows * Archive sweep skips meta already archived; `wp tmdo install` adds an index on original_meta_id and removes duplicate archive rows (the sweep waits until the index exists)
* Entity health sees a running backfill (batches are scheduled with entity and group args) * Entity health sees a running backfill (batches are scheduled with entity and group args)
* Deactivate and uninstall clear every scheduled event, including ones scheduled with args; uninstall also removes tmdo_* options and transients * Deactivate and uninstall clear every scheduled event, including ones scheduled with args; uninstall also removes tmdo_* options and transients
* The refused-uninstall notice points to `wp tmdo bridge-restore-native`; the post stress tester no longer uses the retired wc_product table * The refused-uninstall notice points to `wp tmdo bridge-restore-native`; the post stress tester no longer uses the retired wc_product table
+2 -1
View File
@@ -98,7 +98,7 @@ class InstallerCleanupTest extends TestCase {
/** tmdo_* options and transients were left behind; a reinstall then saw stale deferrals and indexes (F-5). */ /** tmdo_* options and transients were left behind; a reinstall then saw stale deferrals and indexes (F-5). */
public function test_uninstall_removes_tmdo_options_and_transients(): void { public function test_uninstall_removes_tmdo_options_and_transients(): void {
global $wpdb; global $wpdb;
foreach ( array( 'tmdo_schema_deferred', 'tmdo_entity_indexes', '_transient_tmdo_cov_x', '_transient_timeout_tmdo_cov_x', 'hivepress_settings' ) as $name ) { foreach ( array( 'tmdo_schema_deferred', 'tmdo_entity_indexes', '_transient_tmdo_cov_x', '_transient_timeout_tmdo_cov_x', 'hivepress_settings', 'tmdo_latepoint_db_version' ) as $name ) {
$wpdb->insert( $wpdb->options, array( 'option_name' => $name, 'option_value' => '1' ) ); $wpdb->insert( $wpdb->options, array( 'option_name' => $name, 'option_value' => '1' ) );
$GLOBALS['_wp_options'][ $name ] = '1'; $GLOBALS['_wp_options'][ $name ] = '1';
} }
@@ -109,6 +109,7 @@ class InstallerCleanupTest extends TestCase {
$this->assertArrayNotHasKey( $name, $GLOBALS['_wp_options'], $name ); $this->assertArrayNotHasKey( $name, $GLOBALS['_wp_options'], $name );
} }
$this->assertArrayHasKey( 'hivepress_settings', $GLOBALS['_wp_options'], 'other plugins untouched' ); $this->assertArrayHasKey( 'hivepress_settings', $GLOBALS['_wp_options'], 'other plugins untouched' );
$this->assertArrayHasKey( 'tmdo_latepoint_db_version', $GLOBALS['_wp_options'], 'the LatePoint add-on owns it' );
} }
public function test_drops_static_tables(): void { public function test_drops_static_tables(): void {
@@ -29,7 +29,8 @@ class ZoneArchiveIntegrationTest extends TestCase {
`original_meta_id` bigint(20) unsigned NOT NULL DEFAULT 0, `original_meta_id` bigint(20) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `post_id` (`post_id`), KEY `post_id` (`post_id`),
KEY `archived_at` (`archived_at`) KEY `archived_at` (`archived_at`),
KEY `idx_original_meta` (`original_meta_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
); );
} }
@@ -81,6 +82,20 @@ class ZoneArchiveIntegrationTest extends TestCase {
} }
} }
/** Until `wp tmdo install` adds the index, NOT EXISTS would scan the archive per candidate. */
public function test_sweep_waits_for_the_original_meta_index(): void {
global $wpdb;
$this->seed_trashed_post();
$wpdb->query( 'ALTER TABLE `' . self::TABLE . '` DROP INDEX `idx_original_meta`' );
try {
$this->assertSame( 0, WPDO_Zone_Archive::sweep( 90, false ) );
$this->assertSame( 0, $this->archive_rows() );
} finally {
$wpdb->query( 'ALTER TABLE `' . self::TABLE . '` ADD KEY `idx_original_meta` (`original_meta_id`)' );
$this->drop_posts();
}
}
public function test_remove_duplicates_keeps_the_first_copy_of_each_meta_row(): void { 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_price', '5', 101 ); WPDO_Zone_Archive::archive( 10, 'hp_listing', 'hp_price', '5', 101 );