query( 'DROP TABLE IF EXISTS `' . self::POSTS . '`' ); $wpdb->query( 'CREATE TABLE `' . self::POSTS . '` ( ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_type varchar(20) NOT NULL DEFAULT \'post\', PRIMARY KEY (ID), KEY post_type (post_type) ) DEFAULT CHARACTER SET utf8mb4' ); // wp_postmeta — same schema as production WP. $wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTMETA . '`' ); $wpdb->query( 'CREATE TABLE `' . self::POSTMETA . '` ( meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_id bigint(20) unsigned NOT NULL DEFAULT 0, meta_key varchar(255) DEFAULT NULL, meta_value longtext, PRIMARY KEY (meta_id), KEY post_id (post_id), KEY meta_key (meta_key(191)) ) DEFAULT CHARACTER SET utf8mb4' ); // Flat target for wc_product group (subset of full schema for test). $wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT . '`' ); $wpdb->query( 'CREATE TABLE `' . self::FLAT . '` ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_id bigint(20) unsigned NOT NULL, _price decimal(18,6) DEFAULT NULL, _regular_price decimal(18,6) DEFAULT NULL, _stock bigint(20) DEFAULT NULL, _stock_status varchar(100) DEFAULT NULL, _sku varchar(255) DEFAULT NULL, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_post_id (post_id) ) DEFAULT CHARACTER SET utf8mb4' ); // Reset Entity Registry + register post adapter + post fields. WPDO_Entity_Registry::init(); WPDO_Entity_Registry::register_adapter( 'post', new WPDO_Adapter_Post() ); WPDO_Post_Fields::register_entity_fields(); } public static function tearDownAfterClass(): void { global $wpdb; $wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTS . '`' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTMETA . '`' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT . '`' ); // Reset Mode_Manager + Entity_Registry to avoid polluting later tests. $ref = new ReflectionClass( WPDO_Mode_Manager::class ); $cache = $ref->getProperty( 'cache' ); $cache->setAccessible( true ); $cache->setValue( null, null ); WPDO_Entity_Registry::init(); } protected function setUp(): void { global $wpdb; $wpdb->query( 'TRUNCATE TABLE `' . self::POSTS . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::POSTMETA . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::FLAT . '`' ); } private function seed_post( int $id, string $post_type ): void { global $wpdb; $wpdb->insert( self::POSTS, array( 'ID' => $id, 'post_type' => $post_type ) ); } private function seed_meta( int $post_id, string $key, string $value ): void { global $wpdb; $wpdb->insert( self::POSTMETA, array( 'post_id' => $post_id, 'meta_key' => $key, 'meta_value' => $value ) ); } // ── diagnose() ──────────────────────────────────────────────────────────── public function test_diagnose_reports_posts_postmeta_ratio(): void { // 5 posts, 12 postmeta rows → ratio 2.4 for ( $i = 1; $i <= 5; $i++ ) { $this->seed_post( $i, 'post' ); } for ( $i = 0; $i < 12; $i++ ) { $this->seed_meta( ( $i % 5 ) + 1, 'random_key', 'v' ); } $result = WPDO_Post_Migration::diagnose(); $this->assertSame( 5, $result['posts'] ); $this->assertSame( 12, $result['postmeta'] ); $this->assertSame( 2.4, $result['ratio'] ); } public function test_diagnose_reports_eav_rows_per_managed_group(): void { $this->seed_post( 1, 'product' ); $this->seed_meta( 1, '_price', '99.99' ); $this->seed_meta( 1, '_stock', '5' ); $this->seed_meta( 1, 'unrelated_key', 'x' ); // not in any group $result = WPDO_Post_Migration::diagnose(); $this->assertArrayHasKey( 'groups', $result ); $this->assertArrayHasKey( 'wc_product', $result['groups'] ); $this->assertSame( 2, $result['groups']['wc_product']['eav_rows'], 'wc_product group has 2 EAV rows: _price + _stock (unrelated_key excluded).' ); } public function test_diagnose_reports_zero_eav_for_unused_group(): void { $this->seed_post( 1, 'post' ); $this->seed_meta( 1, 'random_key', 'v' ); $result = WPDO_Post_Migration::diagnose(); // nav_menu_item group has no postmeta seeded. $this->assertSame( 0, $result['groups']['nav_menu_item']['eav_rows'] ); } public function test_diagnose_reports_post_mode(): void { $result = WPDO_Post_Migration::diagnose(); $this->assertArrayHasKey( 'mode', $result ); // Default post mode is 'disabled' per Mode_Manager defaults(). $this->assertSame( 'disabled', $result['mode'] ); } // ── backfill_group() — bulk SQL pivot ───────────────────────────────────── public function test_backfill_group_pivots_wc_product_keys(): void { $this->seed_post( 1, 'product' ); $this->seed_meta( 1, '_price', '99.99' ); $this->seed_meta( 1, '_regular_price', '120.00' ); $this->seed_meta( 1, '_stock', '5' ); $this->seed_meta( 1, '_stock_status', 'instock' ); $this->seed_meta( 1, '_sku', 'SKU-001' ); $this->seed_post( 2, 'product' ); $this->seed_meta( 2, '_price', '49.50' ); $this->seed_meta( 2, '_stock_status', 'outofstock' ); $result = WPDO_Post_Migration::backfill_group( 'wc_product' ); $this->assertSame( 2, $result['migrated'], 'Two posts produce two flat rows.' ); global $wpdb; $row1 = $wpdb->get_row( 'SELECT _price, _stock, _sku FROM `' . self::FLAT . '` WHERE post_id = 1', ARRAY_A ); $this->assertSame( '99.990000', $row1['_price'], 'Price decimal stored with full precision.' ); $this->assertSame( '5', $row1['_stock'] ); $this->assertSame( 'SKU-001', $row1['_sku'] ); $row2 = $wpdb->get_row( 'SELECT _price, _stock_status, _sku FROM `' . self::FLAT . '` WHERE post_id = 2', ARRAY_A ); $this->assertSame( '49.500000', $row2['_price'] ); $this->assertSame( 'outofstock', $row2['_stock_status'] ); $this->assertNull( $row2['_sku'], 'Unset key remains NULL in flat row.' ); } public function test_backfill_group_is_idempotent(): void { $this->seed_post( 1, 'product' ); $this->seed_meta( 1, '_price', '50.00' ); WPDO_Post_Migration::backfill_group( 'wc_product' ); $result_second = WPDO_Post_Migration::backfill_group( 'wc_product' ); $this->assertSame( 1, $result_second['migrated'], 'Re-running backfill is idempotent (UPSERT).' ); global $wpdb; $count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::FLAT . '`' ); $this->assertSame( 1, $count, 'No duplicate rows after re-run.' ); } public function test_backfill_group_skips_posts_of_wrong_type(): void { // _price on a non-product post should NOT migrate to wc_product flat. $this->seed_post( 99, 'post' ); $this->seed_meta( 99, '_price', '100.00' ); $result = WPDO_Post_Migration::backfill_group( 'wc_product' ); $this->assertSame( 0, $result['migrated'], 'Posts of wrong type are excluded by post_type filter.' ); } public function test_backfill_group_rejects_invalid_group(): void { $this->expectException( InvalidArgumentException::class ); WPDO_Post_Migration::backfill_group( 'bogus_group' ); } }