query( 'DROP TABLE IF EXISTS `' . self::USERMETA . '`' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT . '`' ); $wpdb->query( 'CREATE TABLE `' . self::USERMETA . '` ( `umeta_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `user_id` bigint(20) unsigned NOT NULL DEFAULT 0, `meta_key` varchar(255) DEFAULT NULL, `meta_value` longtext DEFAULT NULL, PRIMARY KEY (`umeta_id`), KEY `meta_key` (`meta_key`(191)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' ); $wpdb->query( 'CREATE TABLE `' . self::FLAT . '` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `nickname` varchar(255) DEFAULT NULL, `first_name` varchar(255) DEFAULT NULL, `last_name` varchar(255) DEFAULT NULL, `description` text 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_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' ); } public static function tearDownAfterClass(): void { global $wpdb; $wpdb->query( 'DROP TABLE IF EXISTS `' . self::USERMETA . '`' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT . '`' ); } public function setUp(): void { global $wpdb; $wpdb->query( 'TRUNCATE `' . self::USERMETA . '`' ); $wpdb->query( 'TRUNCATE `' . self::FLAT . '`' ); } // ── Tests ──────────────────────────────────────────────────────────────── public function test_bulk_pivot_produces_one_row_per_user(): void { $this->seed_eav( array( array( 'user_id' => 10, 'meta_key' => 'first_name', 'meta_value' => 'Alice' ), array( 'user_id' => 10, 'meta_key' => 'last_name', 'meta_value' => 'Adams' ), array( 'user_id' => 10, 'meta_key' => 'nickname', 'meta_value' => 'al' ), array( 'user_id' => 11, 'meta_key' => 'first_name', 'meta_value' => 'Bob' ), array( 'user_id' => 11, 'meta_key' => 'description', 'meta_value' => 'engineer' ), ) ); $affected = $this->run_pivot(); // MySQL returns 2*N for INSERT...ON DUPLICATE on conflict, N for new inserts. // Two new users → both INSERTs → affected_rows == 2. $this->assertSame( 2, $affected ); global $wpdb; $row10 = $wpdb->get_row( 'SELECT * FROM `' . self::FLAT . '` WHERE user_id=10', ARRAY_A ); $row11 = $wpdb->get_row( 'SELECT * FROM `' . self::FLAT . '` WHERE user_id=11', ARRAY_A ); $this->assertSame( 'Alice', $row10['first_name'] ); $this->assertSame( 'Adams', $row10['last_name'] ); $this->assertSame( 'al', $row10['nickname'] ); $this->assertNull( $row10['description'] ); $this->assertSame( 'Bob', $row11['first_name'] ); $this->assertSame( 'engineer', $row11['description'] ); $this->assertNull( $row11['last_name'] ); } public function test_bulk_pivot_idempotent_re_run_preserves_data(): void { $this->seed_eav( array( array( 'user_id' => 20, 'meta_key' => 'first_name', 'meta_value' => 'Carol' ), ) ); $this->run_pivot(); $this->run_pivot(); // Second run must not lose or double-count data. global $wpdb; $count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::FLAT . '`' ); $this->assertSame( 1, $count, 'Re-run should not duplicate user_id row' ); $first = $wpdb->get_var( 'SELECT first_name FROM `' . self::FLAT . '` WHERE user_id=20' ); $this->assertSame( 'Carol', $first ); } public function test_bulk_pivot_coalesce_preserves_existing_when_new_eav_subset(): void { // Round 1: full data. $this->seed_eav( array( array( 'user_id' => 30, 'meta_key' => 'first_name', 'meta_value' => 'Dora' ), array( 'user_id' => 30, 'meta_key' => 'last_name', 'meta_value' => 'Diaz' ), ) ); $this->run_pivot(); // Round 2: only first_name remains in EAV (last_name was cleaned). global $wpdb; $wpdb->query( "DELETE FROM `" . self::USERMETA . "` WHERE meta_key='last_name'" ); $this->run_pivot(); $row = $wpdb->get_row( 'SELECT * FROM `' . self::FLAT . '` WHERE user_id=30', ARRAY_A ); // COALESCE(VALUES(last_name), last_name) → keeps 'Diaz' even though new VALUES is NULL. $this->assertSame( 'Dora', $row['first_name'] ); $this->assertSame( 'Diaz', $row['last_name'], 'COALESCE should preserve previously-migrated value when EAV is now empty' ); } public function test_bulk_pivot_handles_empty_eav_gracefully(): void { $affected = $this->run_pivot(); $this->assertSame( 0, $affected ); global $wpdb; $count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::FLAT . '`' ); $this->assertSame( 0, $count ); } public function test_bulk_pivot_uses_max_for_duplicate_meta_keys(): void { // HivePress occasionally writes duplicate meta_value rows for the same key. $this->seed_eav( array( array( 'user_id' => 40, 'meta_key' => 'first_name', 'meta_value' => 'older_value' ), array( 'user_id' => 40, 'meta_key' => 'first_name', 'meta_value' => 'newer_value' ), ) ); $this->run_pivot(); global $wpdb; $first = $wpdb->get_var( 'SELECT first_name FROM `' . self::FLAT . '` WHERE user_id=40' ); // MAX() picks lexicographically larger; for our purpose this just guarantees // deterministic behavior — no NULL, no error. $this->assertNotNull( $first ); $this->assertContains( $first, array( 'older_value', 'newer_value' ) ); } // ── Helpers ────────────────────────────────────────────────────────────── private function seed_eav( array $rows ): void { global $wpdb; foreach ( $rows as $row ) { $wpdb->insert( self::USERMETA, $row ); } } /** * Local mirror of WPDO_Migration_Orchestrator::execute_bulk_pivot() against * isolated test tables. Builds the same SQL form but pointing at our test * usermeta and flat tables (the orchestrator targets $wpdb->usermeta). */ private function run_pivot(): int { global $wpdb; $keys = array( 'nickname', 'first_name', 'last_name', 'description' ); $cols = $keys; $ph = implode( ',', array_fill( 0, count( $keys ), '%s' ) ); $cases = array(); $updates = array(); foreach ( $cols as $col ) { $cases[] = "MAX(CASE WHEN um.meta_key = '{$col}' THEN um.meta_value END) AS `{$col}`"; $updates[] = "`{$col}` = COALESCE(VALUES(`{$col}`), `{$col}`)"; } $sql = sprintf( 'INSERT INTO `%s` (`user_id`, %s) SELECT um.user_id, %s FROM `%s` um WHERE um.meta_key IN (%s) GROUP BY um.user_id ON DUPLICATE KEY UPDATE %s', self::FLAT, implode( ', ', array_map( fn( $c ) => "`{$c}`", $cols ) ), implode( ', ', $cases ), self::USERMETA, $ph, implode( ', ', $updates ) ); $result = $wpdb->query( $wpdb->prepare( $sql, ...$keys ) ); return false === $result ? 0 : (int) $result; } }