terms = self::TERMS; $wpdb->termmeta = self::TERMMETA; $wpdb->comments = self::COMMENTS; $wpdb->commentmeta = self::COMMENTMETA; // Source tables (terms / comments) need term_id / comment_ID columns. $wpdb->query( 'DROP TABLE IF EXISTS `' . self::TERMS . '`' ); $wpdb->query( 'CREATE TABLE `' . self::TERMS . '` ( term_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, name varchar(200) NOT NULL DEFAULT "", PRIMARY KEY (term_id) ) DEFAULT CHARACTER SET utf8mb4' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::COMMENTS . '`' ); $wpdb->query( 'CREATE TABLE `' . self::COMMENTS . '` ( comment_ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, comment_post_ID bigint(20) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (comment_ID) ) DEFAULT CHARACTER SET utf8mb4' ); $wpdb->query( 'CREATE TABLE IF NOT EXISTS `' . self::TERMMETA . '` ( meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, term_id bigint(20) unsigned NOT NULL DEFAULT 0, meta_key varchar(255) DEFAULT NULL, meta_value longtext, PRIMARY KEY (meta_id), KEY term_id (term_id), KEY meta_key (meta_key(191)) ) DEFAULT CHARACTER SET utf8mb4' ); $wpdb->query( 'CREATE TABLE IF NOT EXISTS `' . self::COMMENTMETA . '` ( meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, comment_id bigint(20) unsigned NOT NULL DEFAULT 0, meta_key varchar(255) DEFAULT NULL, meta_value longtext, PRIMARY KEY (meta_id), KEY comment_id (comment_id), KEY meta_key (meta_key(191)) ) DEFAULT CHARACTER SET utf8mb4' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::TERM_FLAT . '`' ); $wpdb->query( 'CREATE TABLE `' . self::TERM_FLAT . '` ( term_id bigint(20) unsigned NOT NULL, hp_sort_order int(11) DEFAULT NULL, hp_default tinyint(1) DEFAULT NULL, hp_icon varchar(64) DEFAULT NULL, PRIMARY KEY (term_id) ) DEFAULT CHARACTER SET utf8mb4' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::COMMENT_FLAT . '`' ); $wpdb->query( 'CREATE TABLE `' . self::COMMENT_FLAT . '` ( comment_id bigint(20) unsigned NOT NULL, hp_rating tinyint(1) DEFAULT NULL, PRIMARY KEY (comment_id) ) DEFAULT CHARACTER SET utf8mb4' ); } public static function tearDownAfterClass(): void { global $wpdb; foreach ( array( self::TERMS, self::COMMENTS, self::TERM_FLAT, self::COMMENT_FLAT ) as $tbl ) { $wpdb->query( 'DROP TABLE IF EXISTS `' . $tbl . '`' ); } } protected function setUp(): void { global $wpdb; $wpdb->query( 'TRUNCATE TABLE `' . self::TERMS . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::COMMENTS . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::TERMMETA . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::COMMENTMETA . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::TERM_FLAT . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::COMMENT_FLAT . '`' ); } // ── sample_compare contract ───────────────────────────────────────────── public function test_sample_compare_invalid_entity_type_throws(): void { $this->expectException( InvalidArgumentException::class ); WPDO_Term_Comment_Shadow_Verifier::sample_compare( 'bogus', 'hp_taxonomy', self::TERM_FLAT, array( 'hp_sort_order' ) ); } public function test_sample_compare_zero_sample_size_throws(): void { $this->expectException( InvalidArgumentException::class ); WPDO_Term_Comment_Shadow_Verifier::sample_compare( 'term', 'hp_taxonomy', self::TERM_FLAT, array( 'hp_sort_order' ), 0 ); } public function test_sample_compare_empty_keys_throws(): void { $this->expectException( InvalidArgumentException::class ); WPDO_Term_Comment_Shadow_Verifier::sample_compare( 'term', 'hp_taxonomy', self::TERM_FLAT, array() ); } public function test_sample_compare_returns_zeros_for_empty_db(): void { $result = WPDO_Term_Comment_Shadow_Verifier::sample_compare( 'term', 'hp_taxonomy', self::TERM_FLAT, array( 'hp_sort_order' ) ); $this->assertSame( 0, $result['sampled'] ); $this->assertSame( 'term', $result['entity_type'] ); $this->assertSame( 'hp_taxonomy', $result['group'] ); } public function test_sample_compare_counts_matches_when_in_sync(): void { global $wpdb; // 3 terms, all in sync between wp_termmeta and flat table. for ( $i = 1; $i <= 3; $i++ ) { $wpdb->insert( self::TERMS, array( 'term_id' => $i, 'name' => 'term_' . $i ) ); $wpdb->insert( self::TERMMETA, array( 'term_id' => $i, 'meta_key' => 'hp_sort_order', 'meta_value' => $i * 10 ) ); $wpdb->insert( self::TERM_FLAT, array( 'term_id' => $i, 'hp_sort_order' => $i * 10 ) ); } $result = WPDO_Term_Comment_Shadow_Verifier::sample_compare( 'term', 'hp_taxonomy', self::TERM_FLAT, array( 'hp_sort_order' ), 10 ); $this->assertSame( 3, $result['sampled'] ); $this->assertSame( 3, $result['matched'] ); $this->assertSame( 0, $result['diffs'] ); $this->assertSame( 0, $result['missing_flat'] ); $this->assertSame( 0, $result['missing_meta'] ); } public function test_sample_compare_detects_missing_flat(): void { global $wpdb; // 2 terms with wp_termmeta but no flat row. $wpdb->insert( self::TERMS, array( 'term_id' => 1, 'name' => 'a' ) ); $wpdb->insert( self::TERMS, array( 'term_id' => 2, 'name' => 'b' ) ); $wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'hp_sort_order', 'meta_value' => 5 ) ); $wpdb->insert( self::TERMMETA, array( 'term_id' => 2, 'meta_key' => 'hp_sort_order', 'meta_value' => 10 ) ); $result = WPDO_Term_Comment_Shadow_Verifier::sample_compare( 'term', 'hp_taxonomy', self::TERM_FLAT, array( 'hp_sort_order' ), 10 ); $this->assertSame( 2, $result['sampled'] ); $this->assertSame( 0, $result['matched'] ); $this->assertSame( 2, $result['missing_flat'] ); } public function test_sample_compare_detects_missing_meta(): void { global $wpdb; // 2 terms with flat rows but no wp_termmeta. $wpdb->insert( self::TERMS, array( 'term_id' => 1, 'name' => 'a' ) ); $wpdb->insert( self::TERMS, array( 'term_id' => 2, 'name' => 'b' ) ); $wpdb->insert( self::TERM_FLAT, array( 'term_id' => 1, 'hp_sort_order' => 5 ) ); $wpdb->insert( self::TERM_FLAT, array( 'term_id' => 2, 'hp_sort_order' => 10 ) ); $result = WPDO_Term_Comment_Shadow_Verifier::sample_compare( 'term', 'hp_taxonomy', self::TERM_FLAT, array( 'hp_sort_order' ), 10 ); $this->assertSame( 2, $result['sampled'] ); $this->assertSame( 2, $result['missing_meta'] ); } public function test_sample_compare_detects_value_diff(): void { global $wpdb; $wpdb->insert( self::TERMS, array( 'term_id' => 1, 'name' => 'a' ) ); $wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'hp_sort_order', 'meta_value' => '5' ) ); $wpdb->insert( self::TERM_FLAT, array( 'term_id' => 1, 'hp_sort_order' => 99 ) ); $result = WPDO_Term_Comment_Shadow_Verifier::sample_compare( 'term', 'hp_taxonomy', self::TERM_FLAT, array( 'hp_sort_order' ), 10 ); $this->assertSame( 1, $result['diffs'] ); $this->assertSame( 0, $result['matched'] ); } public function test_sample_compare_loose_equal_matches_numeric(): void { global $wpdb; $wpdb->insert( self::TERMS, array( 'term_id' => 1, 'name' => 'a' ) ); // wp_termmeta stores '5' as string, flat stores 5 as int — should match $wpdb->insert( self::TERMMETA, array( 'term_id' => 1, 'meta_key' => 'hp_sort_order', 'meta_value' => '5' ) ); $wpdb->insert( self::TERM_FLAT, array( 'term_id' => 1, 'hp_sort_order' => 5 ) ); $result = WPDO_Term_Comment_Shadow_Verifier::sample_compare( 'term', 'hp_taxonomy', self::TERM_FLAT, array( 'hp_sort_order' ), 10 ); $this->assertSame( 1, $result['matched'], 'String "5" must loose-equal int 5' ); $this->assertSame( 0, $result['diffs'] ); } public function test_sample_compare_handles_comment_entity(): void { global $wpdb; $wpdb->insert( self::COMMENTS, array( 'comment_ID' => 1, 'comment_post_ID' => 100 ) ); $wpdb->insert( self::COMMENTMETA, array( 'comment_id' => 1, 'meta_key' => 'hp_rating', 'meta_value' => 5 ) ); $wpdb->insert( self::COMMENT_FLAT, array( 'comment_id' => 1, 'hp_rating' => 5 ) ); $result = WPDO_Term_Comment_Shadow_Verifier::sample_compare( 'comment', 'hp_review', self::COMMENT_FLAT, array( 'hp_rating' ), 10 ); $this->assertSame( 1, $result['sampled'] ); $this->assertSame( 1, $result['matched'] ); $this->assertSame( 'comment', $result['entity_type'] ); } // ── cron_tick mode-gating ─────────────────────────────────────────────── public function test_cron_tick_no_op_when_neither_in_shadow_read(): void { // Set both modes to dual_write so cron should no-op. if ( class_exists( 'WPDO_Mode_Manager' ) ) { WPDO_Mode_Manager::set( 'term', 'dual_write' ); WPDO_Mode_Manager::set( 'comment', 'dual_write' ); } // cron_tick should return without error and not touch the tables. WPDO_Term_Comment_Shadow_Verifier::cron_tick(); $this->assertTrue( true ); // No exception = no-op succeeded } }