` table * to the new `wp_wpdo_post_` flat table: * * - copies common columns by name intersection * - skips auto_increment id + updated_at columns (let flat manage them) * - idempotent (re-run doesn't duplicate; ON DUPLICATE KEY UPDATE) * - leaves the legacy table untouched (safety net for v3.0.0 DROP) * - verify_legacy_cutover() reports row count + sample mismatches */ class PostLegacyCutoverTest extends TestCase { private const HOT_TABLE = 'wp_itest_wpdo_hot_hp_listing'; private const FLAT_TABLE = 'wp_itest_wpdo_post_hp_listing_core'; public static function setUpBeforeClass(): void { global $wpdb; if ( ! class_exists( 'WPDO_Post_Migration' ) ) { require_once WPDO_PLUGIN_DIR . 'includes/migration/class-tmdo-post-migration.php'; } // Legacy hot table — narrower schema, hp_booking_enabled is hot-only. $wpdb->query( 'DROP TABLE IF EXISTS `' . self::HOT_TABLE . '`' ); $wpdb->query( 'CREATE TABLE `' . self::HOT_TABLE . '` ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_id bigint(20) unsigned NOT NULL DEFAULT 0, hp_price decimal(10,2) NOT NULL DEFAULT 0.00, hp_featured tinyint(1) NOT NULL DEFAULT 0, hp_verified tinyint(1) NOT NULL DEFAULT 0, hp_expired_time bigint(20) NOT NULL DEFAULT 0, hp_featured_time bigint(20) NOT NULL DEFAULT 0, updated_at datetime NOT NULL DEFAULT \'0000-00-00 00:00:00\', hp_booking_enabled tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY post_id (post_id) ) DEFAULT CHARACTER SET utf8mb4' ); // Flat target — wider schema, includes hp_status/hp_vendor not in hot. $wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT_TABLE . '`' ); $wpdb->query( 'CREATE TABLE `' . self::FLAT_TABLE . '` ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_id bigint(20) unsigned NOT NULL, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, hp_price decimal(18,6) DEFAULT NULL, hp_status varchar(100) DEFAULT NULL, hp_featured bigint(20) DEFAULT NULL, hp_verified bigint(20) DEFAULT NULL, hp_vendor bigint(20) DEFAULT NULL, hp_expired_time bigint(20) DEFAULT NULL, hp_featured_time bigint(20) DEFAULT NULL, hp_view_count bigint(20) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_post_id (post_id) ) DEFAULT CHARACTER SET utf8mb4' ); } public static function tearDownAfterClass(): void { global $wpdb; $wpdb->query( 'DROP TABLE IF EXISTS `' . self::HOT_TABLE . '`' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT_TABLE . '`' ); } protected function setUp(): void { global $wpdb; $wpdb->query( 'TRUNCATE TABLE `' . self::HOT_TABLE . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::FLAT_TABLE . '`' ); } private function seed_hot( int $post_id, array $cols ): void { global $wpdb; $wpdb->insert( self::HOT_TABLE, array_merge( array( 'post_id' => $post_id ), $cols ) ); } // ── copy_legacy_hot_table() ────────────────────────────────────────────── public function test_copy_legacy_hot_table_copies_all_rows(): void { // Seed 5 rows. for ( $i = 1; $i <= 5; $i++ ) { $this->seed_hot( 100 + $i, array( 'hp_price' => 50.00 + $i, 'hp_featured' => $i % 2, 'hp_verified' => 1, 'hp_expired_time' => 9999999 + $i, 'hp_featured_time' => 0, ) ); } $result = WPDO_Post_Migration::copy_legacy_hot_table( 'hp_listing', self::HOT_TABLE, self::FLAT_TABLE ); $this->assertSame( 5, $result['copied'] ); global $wpdb; $flat_count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::FLAT_TABLE . '`' ); $this->assertSame( 5, $flat_count ); // Verify a row's data round-trips. $row = $wpdb->get_row( 'SELECT hp_price, hp_featured, hp_verified, hp_expired_time FROM `' . self::FLAT_TABLE . '` WHERE post_id = 103', ARRAY_A ); $this->assertSame( '53.000000', $row['hp_price'] ); $this->assertSame( '1', $row['hp_featured'] ); // 3 % 2 = 1 $this->assertSame( '1', $row['hp_verified'] ); $this->assertSame( '10000002', $row['hp_expired_time'] ); } public function test_copy_skips_id_and_updated_at_columns(): void { $this->seed_hot( 200, array( 'hp_price' => 99.99, 'hp_featured' => 0, 'hp_verified' => 1, 'hp_expired_time' => 0, 'hp_featured_time' => 0, ) ); WPDO_Post_Migration::copy_legacy_hot_table( 'hp_listing', self::HOT_TABLE, self::FLAT_TABLE ); global $wpdb; // Flat row's id should be auto-assigned (not the hot row's id). // Flat row's updated_at should be CURRENT_TIMESTAMP (not 0000-00-00). $row = $wpdb->get_row( 'SELECT id, updated_at FROM `' . self::FLAT_TABLE . '` WHERE post_id = 200', ARRAY_A ); $this->assertNotEmpty( $row['updated_at'] ); $this->assertNotEquals( '0000-00-00 00:00:00', $row['updated_at'] ); } public function test_copy_legacy_hot_table_is_idempotent(): void { $this->seed_hot( 300, array( 'hp_price' => 10.00, 'hp_featured' => 0, 'hp_verified' => 1, 'hp_expired_time' => 0, 'hp_featured_time' => 0, ) ); WPDO_Post_Migration::copy_legacy_hot_table( 'hp_listing', self::HOT_TABLE, self::FLAT_TABLE ); WPDO_Post_Migration::copy_legacy_hot_table( 'hp_listing', self::HOT_TABLE, self::FLAT_TABLE ); global $wpdb; $count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::FLAT_TABLE . '`' ); $this->assertSame( 1, $count, 'Re-running copy is idempotent — UPSERT, no duplicate.' ); } public function test_copy_does_not_modify_legacy_hot_table(): void { $this->seed_hot( 400, array( 'hp_price' => 25.00, 'hp_featured' => 0, 'hp_verified' => 1, 'hp_expired_time' => 0, 'hp_featured_time' => 0, ) ); global $wpdb; $before = $wpdb->get_results( 'SELECT * FROM `' . self::HOT_TABLE . '` ORDER BY id', ARRAY_A ); WPDO_Post_Migration::copy_legacy_hot_table( 'hp_listing', self::HOT_TABLE, self::FLAT_TABLE ); $after = $wpdb->get_results( 'SELECT * FROM `' . self::HOT_TABLE . '` ORDER BY id', ARRAY_A ); $this->assertEquals( $before, $after, 'Legacy hot table must remain untouched (safety net for v3.0.0).' ); } public function test_copy_returns_zero_when_hot_table_empty(): void { $result = WPDO_Post_Migration::copy_legacy_hot_table( 'hp_listing', self::HOT_TABLE, self::FLAT_TABLE ); $this->assertSame( 0, $result['copied'] ); } public function test_copy_throws_when_hot_table_missing(): void { $this->expectException( RuntimeException::class ); WPDO_Post_Migration::copy_legacy_hot_table( 'hp_listing', 'wp_itest_nonexistent_hot', self::FLAT_TABLE ); } // ── verify_legacy_cutover() ────────────────────────────────────────────── public function test_verify_reports_match_when_counts_equal(): void { for ( $i = 1; $i <= 3; $i++ ) { $this->seed_hot( 500 + $i, array( 'hp_price' => $i * 10, 'hp_featured' => 0, 'hp_verified' => 1, 'hp_expired_time' => 0, 'hp_featured_time' => 0, ) ); } WPDO_Post_Migration::copy_legacy_hot_table( 'hp_listing', self::HOT_TABLE, self::FLAT_TABLE ); $verify = WPDO_Post_Migration::verify_legacy_cutover( self::HOT_TABLE, self::FLAT_TABLE ); $this->assertSame( 3, $verify['hot_rows'] ); $this->assertSame( 3, $verify['flat_rows'] ); $this->assertSame( 0, $verify['mismatched_rows'] ); $this->assertTrue( $verify['ok'] ); } public function test_verify_reports_mismatch_when_flat_lags_hot(): void { // Seed hot with 3 rows. for ( $i = 1; $i <= 3; $i++ ) { $this->seed_hot( 600 + $i, array( 'hp_price' => $i * 10, 'hp_featured' => 0, 'hp_verified' => 1, 'hp_expired_time' => 0, 'hp_featured_time' => 0, ) ); } // Don't run copy — flat stays empty. $verify = WPDO_Post_Migration::verify_legacy_cutover( self::HOT_TABLE, self::FLAT_TABLE ); $this->assertSame( 3, $verify['hot_rows'] ); $this->assertSame( 0, $verify['flat_rows'] ); $this->assertFalse( $verify['ok'] ); } }