Files
2meet-data-optimizer/tests/integration/MigrationOrchestratorTest.php
wpdev d36bb954d1 chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 05:06:36 +08:00

220 lines
8.2 KiB
PHP

<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Integration test: WPDO_Migration_Orchestrator core SQL paths.
*
* Focuses on the parts that cannot be mocked at unit-test level:
* - Bulk SQL pivot (INSERT...SELECT...GROUP BY...ON DUPLICATE KEY UPDATE)
* - Idempotency (re-running pivot must not lose data, must not double-count)
* - Lock acquisition
* - Managed-key list correctness
*
* The orchestrator's full state-machine flow is exercised live on the dev
* environment (see PLAN.md / W-6 smoke-test); this test covers the
* deterministic SQL transforms that are easiest to regress.
*
* Requires real MariaDB (WPDO_TEST_DB_PASS env var must be set).
*/
final class MigrationOrchestratorTest extends TestCase {
private const USERMETA = 'wp_itest_usermeta';
private const FLAT = 'wp_itest_wpdo_user_core_profile';
public static function setUpBeforeClass(): void {
global $wpdb;
$base = WPDO_PLUGIN_DIR;
foreach ( array(
'includes/adapters/interface-entity-adapter.php',
'includes/engine/class-tmdo-type-caster.php',
'includes/engine/class-tmdo-schema-manager.php',
'includes/engine/class-tmdo-entity-registry.php',
'includes/adapters/class-tmdo-adapter-user.php',
'includes/engine/class-tmdo-entity-migration-engine.php',
) as $f ) {
require_once $base . $f;
}
// Drop + recreate to guarantee clean schema.
$wpdb->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;
}
}