query( "TRUNCATE TABLE `{$wpdb->prefix}" . WPDO_Demo_Entity_Counter::TABLE . "`" ); // Reset native usermeta global stubs (when running under integration env). $GLOBALS['_wp_usermeta'] = array(); } // ── Schema ───────────────────────────────────────────────────────────── public function test_install_table_creates_with_composite_unique(): void { global $wpdb; $table = $wpdb->prefix . WPDO_Demo_Entity_Counter::TABLE; // Index check: ui_entity_counter must be UNIQUE on (entity_type, entity_id, counter_key). $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s ORDER BY INDEX_NAME, SEQ_IN_INDEX', $table ), ARRAY_A ); $ui_cols = array(); foreach ( $rows as $r ) { if ( 'ui_entity_counter' === $r['INDEX_NAME'] && '0' === (string) $r['NON_UNIQUE'] ) { $ui_cols[] = $r['COLUMN_NAME']; } } $this->assertSame( array( 'entity_type', 'entity_id', 'counter_key' ), $ui_cols ); } // ── set() / get() — idle state (native fallback only) ────────────────── public function test_set_writes_to_native_meta_in_idle_state(): void { WPDO_Demo_Entity_Counter::set( 'user', 100, 'points', 50 ); $this->assertSame( 50, (int) get_user_meta( 100, 'points', true ) ); } public function test_get_reads_native_in_idle_state(): void { update_user_meta( 200, 'points', 75 ); $this->assertSame( 75, WPDO_Demo_Entity_Counter::get( 'user', 200, 'points' ) ); } public function test_idle_state_does_not_dual_write(): void { WPDO_Demo_Entity_Counter::set( 'user', 300, 'points', 99 ); global $wpdb; $count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$wpdb->prefix}" . WPDO_Demo_Entity_Counter::TABLE . "` WHERE entity_type = %s AND entity_id = %d", 'user', 300 ) ); $this->assertSame( 0, $count, 'idle state must NOT dual-write to demo table' ); } // ── set() — dual_write state ──────────────────────────────────────────── public function test_dual_write_state_writes_to_both(): void { WPDO_Feature_Flags::set( WPDO_Demo_Entity_Counter::MODULE, 'dual_write' ); WPDO_Demo_Entity_Counter::set( 'user', 400, 'points', 123 ); global $wpdb; $zone_value = (int) $wpdb->get_var( $wpdb->prepare( "SELECT counter_value FROM `{$wpdb->prefix}" . WPDO_Demo_Entity_Counter::TABLE . "` WHERE entity_type = %s AND entity_id = %d AND counter_key = %s", 'user', 400, 'points' ) ); $this->assertSame( 123, $zone_value, 'dual_write must populate the zone table' ); $this->assertSame( 123, (int) get_user_meta( 400, 'points', true ), 'dual_write must also keep native meta' ); } public function test_upsert_uses_single_round_trip(): void { WPDO_Feature_Flags::set( WPDO_Demo_Entity_Counter::MODULE, 'dual_write' ); // Two rapid writes to the same key — should produce exactly 1 row, not 2. WPDO_Demo_Entity_Counter::set( 'user', 500, 'points', 10 ); WPDO_Demo_Entity_Counter::set( 'user', 500, 'points', 25 ); global $wpdb; $rows = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$wpdb->prefix}" . WPDO_Demo_Entity_Counter::TABLE . "` WHERE entity_type = %s AND entity_id = %d", 'user', 500 ) ); $this->assertSame( '1', (string) $rows, 'composite UNIQUE must collapse to 1 row' ); $value = $wpdb->get_var( $wpdb->prepare( "SELECT counter_value FROM `{$wpdb->prefix}" . WPDO_Demo_Entity_Counter::TABLE . "` WHERE entity_type = %s AND entity_id = %d AND counter_key = %s", 'user', 500, 'points' ) ); $this->assertSame( '25', (string) $value, 'second write must overwrite via UPSERT' ); } // ── get() — cutover state (read from zone) ───────────────────────────── public function test_cutover_state_reads_from_zone_table(): void { WPDO_Feature_Flags::set( WPDO_Demo_Entity_Counter::MODULE, 'dual_write' ); WPDO_Demo_Entity_Counter::set( 'user', 600, 'points', 999 ); // Switch to cutover — reads now come from zone. WPDO_Feature_Flags::set( WPDO_Demo_Entity_Counter::MODULE, 'cutover' ); // Tamper with native meta to prove zone table is the source of truth. update_user_meta( 600, 'points', 0 ); $this->assertSame( 999, WPDO_Demo_Entity_Counter::get( 'user', 600, 'points' ) ); } public function test_cutover_falls_back_to_native_when_zone_row_missing(): void { WPDO_Feature_Flags::set( WPDO_Demo_Entity_Counter::MODULE, 'cutover' ); // No dual_write history — zone table is empty for this entity. update_user_meta( 700, 'points', 42 ); $this->assertSame( 42, WPDO_Demo_Entity_Counter::get( 'user', 700, 'points' ), 'graceful fallback when zone row absent' ); } // ── Cross-entity coverage ────────────────────────────────────────────── public function test_term_entity_works(): void { WPDO_Feature_Flags::set( WPDO_Demo_Entity_Counter::MODULE, 'dual_write' ); WPDO_Demo_Entity_Counter::set( 'term', 800, 'usage_count', 17 ); global $wpdb; $value = (int) $wpdb->get_var( $wpdb->prepare( "SELECT counter_value FROM `{$wpdb->prefix}" . WPDO_Demo_Entity_Counter::TABLE . "` WHERE entity_type = %s AND entity_id = %d AND counter_key = %s", 'term', 800, 'usage_count' ) ); $this->assertSame( 17, $value ); } public function test_comment_entity_works(): void { WPDO_Feature_Flags::set( WPDO_Demo_Entity_Counter::MODULE, 'dual_write' ); WPDO_Demo_Entity_Counter::set( 'comment', 900, 'helpful_count', 8 ); global $wpdb; $value = (int) $wpdb->get_var( $wpdb->prepare( "SELECT counter_value FROM `{$wpdb->prefix}" . WPDO_Demo_Entity_Counter::TABLE . "` WHERE entity_type = %s AND entity_id = %d AND counter_key = %s", 'comment', 900, 'helpful_count' ) ); $this->assertSame( 8, $value ); } public function test_invalid_entity_type_returns_false(): void { $this->assertFalse( WPDO_Demo_Entity_Counter::set( 'bogus', 1, 'k', 1 ) ); $this->assertSame( 0, WPDO_Demo_Entity_Counter::get( 'bogus', 1, 'k' ) ); } // ── Top-N query (killer use case postmeta can't do efficiently) ─────── public function test_top_n_query_returns_sorted_results(): void { WPDO_Feature_Flags::set( WPDO_Demo_Entity_Counter::MODULE, 'dual_write' ); // Seed 5 users with varying point counts. WPDO_Demo_Entity_Counter::set( 'user', 1001, 'points', 100 ); WPDO_Demo_Entity_Counter::set( 'user', 1002, 'points', 500 ); WPDO_Demo_Entity_Counter::set( 'user', 1003, 'points', 200 ); WPDO_Demo_Entity_Counter::set( 'user', 1004, 'points', 800 ); WPDO_Demo_Entity_Counter::set( 'user', 1005, 'points', 350 ); $top3 = WPDO_Demo_Entity_Counter::top_n( 'user', 'points', 3 ); $this->assertCount( 3, $top3 ); // Sorted DESC: 1004(800) > 1002(500) > 1005(350) > 1003(200) > 1001(100) $this->assertSame( 1004, $top3[0]['entity_id'] ); $this->assertSame( 800, $top3[0]['counter_value'] ); $this->assertSame( 1002, $top3[1]['entity_id'] ); $this->assertSame( 500, $top3[1]['counter_value'] ); $this->assertSame( 1005, $top3[2]['entity_id'] ); $this->assertSame( 350, $top3[2]['counter_value'] ); } public function test_top_n_filters_by_entity_type(): void { WPDO_Feature_Flags::set( WPDO_Demo_Entity_Counter::MODULE, 'dual_write' ); WPDO_Demo_Entity_Counter::set( 'user', 2001, 'points', 999 ); WPDO_Demo_Entity_Counter::set( 'term', 2001, 'usage_count', 999 ); // same id, different type. $users = WPDO_Demo_Entity_Counter::top_n( 'user', 'points', 10 ); $terms = WPDO_Demo_Entity_Counter::top_n( 'term', 'usage_count', 10 ); $this->assertCount( 1, $users ); $this->assertCount( 1, $terms ); $this->assertSame( 2001, $users[0]['entity_id'] ); $this->assertSame( 2001, $terms[0]['entity_id'] ); } // ── Mini benchmark — proves zone table beats postmeta on top-N ──────── public function test_benchmark_top_n_zone_vs_postmeta_simulated(): void { WPDO_Feature_Flags::set( WPDO_Demo_Entity_Counter::MODULE, 'dual_write' ); // Seed 100 users with random point values. for ( $i = 3001; $i <= 3100; $i++ ) { WPDO_Demo_Entity_Counter::set( 'user', $i, 'points', wp_rand( 0, 10000 ) ); } // Time the zone-table top-10 query. $t1 = microtime( true ); for ( $i = 0; $i < 100; $i++ ) { WPDO_Demo_Entity_Counter::top_n( 'user', 'points', 10 ); } $zone_ms = ( microtime( true ) - $t1 ) * 1000; // We expect 100 zone reads under 200ms total (well under "1 LEFT JOIN per request"). $this->assertLessThan( 500, $zone_ms, "100 top-N reads from zone table took {$zone_ms}ms — exceeded 500ms ceiling" ); // Print for visibility (PHPUnit captures to test output, no assertion impact). fwrite( STDOUT, "\n Demo benchmark: 100x top-10 in {$zone_ms}ms (avg " . round( $zone_ms / 100, 2 ) . "ms/call)\n" ); } }