From 07955192d4ac899bfdbebaa6527fd4045d281786 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sun, 27 Sep 2026 09:06:19 +0800 Subject: [PATCH] chore(2meet-data-optimizer): clean HivePress transients left in wp_options - `wp tmdo cleanup-hp-transients --options [--dry-run|--confirm] [--batch=]` removes the rows the HivePress AddOn wrote to wp_options (`_transient_wpdo_hp_pm_*` and their timeouts) before AddOn 0.3.0 kept them in the object cache, deleting in batches (default 5000). It works without the AddOn active; the namespace is repeated in core for that. - Verify: CleanupHpTransientsTest (RED before; only the AddOn's rows go, look-alike names stay); dev36 --dry-run counts 2,314,750 rows in 3.2 s. The --confirm run belongs after the AddOn change is deployed. - PR: PR-080 --- cli/class-tmdo-cli-post.php | 49 ++++++++++++++++ tests/integration/CleanupHpTransientsTest.php | 58 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 tests/integration/CleanupHpTransientsTest.php diff --git a/cli/class-tmdo-cli-post.php b/cli/class-tmdo-cli-post.php index 1678bd2..9dc287b 100644 --- a/cli/class-tmdo-cli-post.php +++ b/cli/class-tmdo-cli-post.php @@ -430,6 +430,10 @@ class TMDO_CLI_Post { * Safe to run with the filter enabled; the filter prevents new postmeta * writes from re-bloating the table. * + * With --options it removes instead the rows the filter wrote to wp_options + * (`_transient_wpdo_hp_pm_*`) before 1.2.0 / AddOn 0.3.0 kept them in the + * object cache; works without the AddOn active. + * * ## OPTIONS * * [--dry-run] @@ -438,10 +442,17 @@ class TMDO_CLI_Post { * [--confirm] * : Required to actually DELETE rows. * + * [--options] + * : Clean the AddOn's rows in wp_options instead of wp_postmeta. + * + * [--batch=] + * : Rows per DELETE with --options. Default 5000. + * * ## EXAMPLES * * wp wpdo cleanup-hp-transients --dry-run * wp wpdo cleanup-hp-transients --confirm + * wp wpdo cleanup-hp-transients --options --dry-run * * @param array $args Positional arguments (unused). * @param array $assoc_args Named arguments. @@ -461,6 +472,11 @@ class TMDO_CLI_Post { ); } + if ( isset( $assoc_args['options'] ) ) { + self::cleanup_hp_transient_options( $dry_run, max( 1, (int) ( $assoc_args['batch'] ?? 5000 ) ) ); + return; + } + if ( ! class_exists( 'TMDO_Hivepress_Transient_Filter' ) ) { WP_CLI::error( 'TMDO_Hivepress_Transient_Filter not loaded.' ); } @@ -494,6 +510,39 @@ class TMDO_CLI_Post { ); } + /** + * Count or delete the HivePress AddOn's transient rows in wp_options, in + * batches (dev36 held 1.15 M of them). + * + * @param bool $dry_run Count only. + * @param int $batch Rows per DELETE. + * @return void + */ + private static function cleanup_hp_transient_options( bool $dry_run, int $batch ): void { + global $wpdb; + // TMDO_Hivepress_Transient_Filter::TRANSLATED_NAMESPACE (AddOn), repeated + // here so the leftovers can be removed with the AddOn gone. + $ns = $wpdb->esc_like( 'wpdo_hp_pm_' ); + $where = $wpdb->prepare( 'option_name LIKE %s OR option_name LIKE %s', '\_transient\_' . $ns . '%', '\_transient\_timeout\_' . $ns . '%' ); + + if ( $dry_run ) { + $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->options} WHERE {$where}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared + WP_CLI::success( sprintf( '[dry-run] %d HivePress transient row(s) in wp_options would be deleted. Re-run with --confirm.', $count ) ); + return; + } + + $deleted = 0; + do { + $rows = (int) $wpdb->query( "DELETE FROM {$wpdb->options} WHERE {$where} LIMIT {$batch}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $deleted += $rows; + } while ( $rows === $batch ); + + if ( class_exists( 'TMDO_Logger' ) ) { + TMDO_Logger::info( 'cleanup_hp_transient_options', array( 'deleted' => $deleted ) ); + } + WP_CLI::success( sprintf( 'Deleted %d HivePress transient row(s) from wp_options.', $deleted ) ); + } + // ── post-cutover-legacy (v2.9.5) ────────────────────────────────────────── /** diff --git a/tests/integration/CleanupHpTransientsTest.php b/tests/integration/CleanupHpTransientsTest.php new file mode 100644 index 0000000..ed190f7 --- /dev/null +++ b/tests/integration/CleanupHpTransientsTest.php @@ -0,0 +1,58 @@ +query( 'DROP TABLE IF EXISTS `' . self::OPTIONS . '`' ); + $wpdb->query( 'CREATE TABLE `' . self::OPTIONS . "` ( option_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, option_name VARCHAR(191) NOT NULL DEFAULT '', option_value LONGTEXT NOT NULL, autoload VARCHAR(20) NOT NULL DEFAULT 'yes', PRIMARY KEY (option_id), UNIQUE KEY option_name (option_name) ) ENGINE=InnoDB" ); + for ( $i = 1; $i <= 5; $i++ ) { + $wpdb->insert( self::OPTIONS, array( 'option_name' => "_transient_wpdo_hp_pm_{$i}_abc", 'option_value' => 'v', 'autoload' => 'no' ) ); + $wpdb->insert( self::OPTIONS, array( 'option_name' => "_transient_timeout_wpdo_hp_pm_{$i}_abc", 'option_value' => '1', 'autoload' => 'no' ) ); + } + foreach ( array( 'siteurl', '_transient_other_plugin', '_transient_wpdo_hp_pmx', 'wpdo_hp_pm_1_abc' ) as $name ) { + $wpdb->insert( self::OPTIONS, array( 'option_name' => $name, 'option_value' => 'keep' ) ); + } + $GLOBALS['_wp_cli_output'] = array(); + } + + protected function tearDown(): void { + global $wpdb; + $wpdb->query( 'DROP TABLE IF EXISTS `' . self::OPTIONS . '`' ); + } + + private function remaining(): array { + global $wpdb; + return $wpdb->get_col( 'SELECT option_name FROM `' . self::OPTIONS . '` ORDER BY option_name' ); + } + + public function test_dry_run_counts_the_rows_and_deletes_nothing(): void { + ( new TMDO_CLI_Post() )->cleanup_hp_transients( array(), array( 'options' => true, 'dry-run' => true ) ); + + $this->assertCount( 14, $this->remaining() ); + $this->assertStringContainsString( '10 HivePress transient row(s) in wp_options', implode( "\n", $GLOBALS['_wp_cli_output'] ) ); + } + + public function test_confirm_deletes_only_the_hivepress_rows_in_batches(): void { + ( new TMDO_CLI_Post() )->cleanup_hp_transients( array(), array( 'options' => true, 'confirm' => true, 'batch' => '3' ) ); + + $this->assertSame( array( '_transient_other_plugin', '_transient_wpdo_hp_pmx', 'siteurl', 'wpdo_hp_pm_1_abc' ), $this->remaining() ); + $this->assertStringContainsString( 'Deleted 10 HivePress transient row(s) from wp_options', implode( "\n", $GLOBALS['_wp_cli_output'] ) ); + } +}