fix(2meet-data-optimizer): skip zone deletes on tables that were never created

- TMDO_Zone_Router::delete_post() deleted from wp_wpdo_hot_/cold_{post_type}
  whenever fields were registered for that zone, so every post delete on a
  HivePress site (cold fields registered, module idle, table never created)
  logged a "table doesn't exist" error. Hot and cold deletes now also need
  the table to exist; module state is not checked, so a table left by a
  rolled-back module still loses the rows of deleted posts.
- Live suite zero_side_effect: saving / updating / deleting an hp_listing
  issues no query on missing zone tables (covers PR-054 and this PR).
- Verify: ZoneRouterTest 22 OK; live zero_side_effect + shadow_write +
  serialized 9/9 PASS (old code: zero_side_effect FAIL); unit 687 OK,
  integration 484 OK, phpcs (phpcs.xml) 0, stan OK
- PR: PR-054b
This commit is contained in:
2026-09-26 19:02:10 +08:00
parent b84252d787
commit 781f171770
4 changed files with 114 additions and 16 deletions
+6 -3
View File
@@ -104,7 +104,10 @@ class TMDO_Zone_Router {
*
* Callers only need post_id and post_type; zone enumeration and per-zone
* error isolation are handled here. Hot and Cold are skipped when the
* post_type has no registered fields in that zone (table may not exist).
* post_type has no registered fields in that zone, or when the zone table
* was never created (fields are registered long before a module runs).
* Module state is not checked: a table left by a rolled-back module still
* gets its rows for deleted posts removed.
*
* @param int $post_id Post ID being deleted.
* @param string $post_type Post type slug.
@@ -113,7 +116,7 @@ class TMDO_Zone_Router {
public static function delete_post( int $post_id, string $post_type ): void {
$registry = TMDO_Schema_Registry::instance();
if ( ! empty( $registry->get_hot_columns( $post_type ) ) ) {
if ( ! empty( $registry->get_hot_columns( $post_type ) ) && TMDO_Schema_Manager::table_exists( TMDO_Zone_Hot::table( $post_type ) ) ) {
try {
TMDO_Zone_Hot::delete( $post_id, $post_type );
} catch ( \Throwable $e ) {
@@ -121,7 +124,7 @@ class TMDO_Zone_Router {
}
}
if ( ! empty( $registry->get_cold_meta_keys( $post_type ) ) ) {
if ( ! empty( $registry->get_cold_meta_keys( $post_type ) ) && TMDO_Schema_Manager::table_exists( TMDO_Zone_Cold::table( $post_type ) ) ) {
try {
TMDO_Zone_Cold::delete( $post_id, $post_type );
} catch ( \Throwable $e ) {
+1
View File
@@ -38,6 +38,7 @@ Exit code 0 when every test passed, 1 otherwise. Output per test:
|---|---|
| `shadow_write` | writes in shadow_read reach native meta (user, term, HivePress listing) |
| `serialized` | array meta values are stored serialized in flat tables, never "Array" |
| `zero_side_effect` | zone tables that were never created are never queried on save / delete |
## Tools (`tools/`)
+46
View File
@@ -0,0 +1,46 @@
<?php
/**
* Suite: zone tables that were never created are never queried.
*
* Bug (dev36, 2026-09-26): the HivePress AddOn registers cold fields, so every
* save_post / loop_start (PR-054) and every post delete (PR-054b) queried
* wp_wpdo_cold_{post_type} although the cold module was idle and the table
* did not exist ("table doesn't exist" DB errors on each write).
*
* @package WP_Data_Optimizer
*/
declare(strict_types=1);
TMDO_Live::test(
'saving and deleting an hp_listing does not touch missing zone tables',
static function () {
global $wpdb;
$tables = array( TMDO_Zone_Cold::table( 'hp_listing' ), TMDO_Zone_Hot::table( 'hp_listing' ) );
foreach ( $tables as $table ) {
if ( TMDO_Schema_Manager::table_exists( $table ) ) {
throw new TMDO_Live_Skip( "{$table} exists on this site" );
}
}
$touched = array();
$spy = static function ( $sql ) use ( $tables, &$touched ) {
foreach ( $tables as $table ) {
if ( false !== strpos( $sql, $table ) && 0 !== stripos( ltrim( $sql ), 'SHOW TABLES' ) ) {
$touched[] = $sql;
}
}
return $sql;
};
add_filter( 'query', $spy );
try {
$id = TMDO_Live_Fixtures::post( 'hp_listing' );
wp_update_post( array( 'ID' => $id, 'post_title' => 'tmdo-live-updated' ) );
wp_delete_post( $id, true );
} finally {
remove_filter( 'query', $spy );
}
tmdo_live_assert_same( array(), $touched, 'queries on missing zone tables' );
}
);
+61 -13
View File
@@ -21,9 +21,69 @@ class ZoneRouterTest extends TestCase {
self::$last_query = '';
self::$last_delete = [];
self::$get_var_return = null;
WPDO_Schema_Manager::flush_table_exists_cache();
$this->setup_wpdb_mock();
}
/**
* $wpdb mock recording delete() tables; SHOW TABLES reports $existing.
*
* @param string[] $existing Tables that exist.
*/
private function delete_logger( array &$captured, array $existing ): void {
global $wpdb;
$wpdb = new class( $captured, $existing ) {
public string $prefix = 'wp_';
public array $log;
public function __construct( array &$ref, private array $existing ) { $this->log = &$ref; }
public function prepare( string $s, mixed ...$a ): string { return str_replace( '%s', (string) ( $a[0] ?? '' ), $s ); }
public function query( string $s ): int { return 1; }
public function get_var( string $s ): mixed {
foreach ( $this->existing as $t ) {
if ( "SHOW TABLES LIKE {$t}" === $s ) {
return $t;
}
}
return null;
}
public function insert( string $t, array $d, mixed $f = null ): int { return 1; }
public function delete( string $table, array $where, array $formats ): void {
$this->log[] = $table;
}
};
}
private function register_hot_and_cold(): void {
$sr = new ReflectionClass( WPDO_Schema_Registry::class );
$sr->getProperty( 'instance' )->setValue( null, null );
WPDO_Schema_Registry::instance()->register( 'test', array( 'post_type' => 'hp_listing', 'meta_key' => 'hp_price', 'zone' => 'hot', 'column' => 'hp_price', 'type' => 'decimal' ) );
WPDO_Schema_Registry::instance()->register( 'test', array( 'post_type' => 'hp_listing', 'meta_key' => 'hp_images', 'zone' => 'cold' ) );
}
/** Registered fields do not mean the zone tables exist (zero side effect). */
public function test_delete_post_skips_hot_and_cold_tables_that_do_not_exist(): void {
$this->register_hot_and_cold();
$captured = array();
$this->delete_logger( $captured, array() );
WPDO_Zone_Router::delete_post( 3, 'hp_listing' );
$this->assertNotContains( 'wp_wpdo_hot_hp_listing', $captured );
$this->assertNotContains( 'wp_wpdo_cold_hp_listing', $captured );
$this->assertContains( 'wp_wpdo_warm', $captured );
}
public function test_delete_post_deletes_cold_rows_when_the_table_exists(): void {
$this->register_hot_and_cold();
$captured = array();
$this->delete_logger( $captured, array( 'wp_wpdo_cold_hp_listing' ) );
WPDO_Zone_Router::delete_post( 4, 'hp_listing' );
$this->assertContains( 'wp_wpdo_cold_hp_listing', $captured );
$this->assertNotContains( 'wp_wpdo_hot_hp_listing', $captured );
}
private function setup_wpdb_mock(): void {
global $wpdb;
$wpdb = new class {
@@ -204,19 +264,7 @@ class ZoneRouterTest extends TestCase {
) );
$captured = array();
global $wpdb;
$wpdb = new class( $captured ) {
public string $prefix = 'wp_';
public array $log;
public function __construct( array &$ref ) { $this->log = &$ref; }
public function prepare( string $s, mixed ...$a ): string { return $s; }
public function query( string $s ): int { return 1; }
public function get_var( string $s ): mixed { return null; }
public function insert( string $t, array $d, mixed $f = null ): int { return 1; }
public function delete( string $table, array $where, array $formats ): void {
$this->log[] = $table;
}
};
$this->delete_logger( $captured, array( 'wp_wpdo_hot_hp_listing' ) );
WPDO_Zone_Router::delete_post( 10, 'hp_listing' );