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
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user