feat(2meet-data-optimizer): write flat-table data back to native meta
- TMDO_Native_Restorer copies an entity's flat-table values into its native meta table so the value each key reads today is what native returns afterwards: the owning group (by post type for posts) wins, values are stored as WP stores them, NULL removes the native row - Resumable per-group id cursor, catch-up pass for rows updated during the run, optional demote to dual_write; CLI wp tmdo bridge-restore-native <entity> [--dry-run] [--batch=<n>] [--demote] - Verify: NativeRestorerTest (5, user) + NativeRestorerPostTest (2, scoped vs legacy routing); core unit 652 / integration 456, HP 148, WC 10 OK; scale estimate dev21 user ~5.1M / post ~4.0M flat rows - PR: PR-031
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Post write-back follows the same routing reads use.
|
||||
*
|
||||
* Listing 101 has `shared_key` in both group tables: 'L' in its own
|
||||
* (hp_listing-scoped) group, a stray 'V' in the vendor group — the legacy
|
||||
* owner, registered last. Whichever table reads use must be the one copied.
|
||||
*/
|
||||
class NativeRestorerPostTest extends TestCase {
|
||||
|
||||
private const LISTING = 'wp_itest_wpdo_post_nr_listing';
|
||||
private const VENDOR = 'wp_itest_wpdo_post_nr_vendor';
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
$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/engine/class-tmdo-mode-manager.php',
|
||||
'includes/engine/class-tmdo-cache-orchestrator.php',
|
||||
'includes/adapters/class-tmdo-adapter-post.php',
|
||||
'includes/migration/class-tmdo-native-restorer.php',
|
||||
) as $file ) {
|
||||
require_once $base . $file;
|
||||
}
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
global $wpdb;
|
||||
self::drop();
|
||||
$wpdb->query( "CREATE TABLE `{$wpdb->posts}` ( ID BIGINT UNSIGNED NOT NULL, post_type VARCHAR(20) NOT NULL DEFAULT 'post', PRIMARY KEY (ID) ) ENGINE=InnoDB" );
|
||||
$wpdb->query( "CREATE TABLE `{$wpdb->postmeta}` ( meta_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT 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)) ) ENGINE=InnoDB" );
|
||||
foreach ( array( self::LISTING, self::VENDOR ) as $table ) {
|
||||
$wpdb->query( "CREATE TABLE `{$table}` ( id BIGINT NOT NULL AUTO_INCREMENT, post_id BIGINT NOT NULL, shared_key VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT '2020-01-01 00:00:00', PRIMARY KEY (id), UNIQUE KEY uk_entity (post_id) ) ENGINE=InnoDB" );
|
||||
}
|
||||
$wpdb->insert( $wpdb->posts, array( 'ID' => 101, 'post_type' => 'hp_listing' ) );
|
||||
$wpdb->insert( self::LISTING, array( 'post_id' => 101, 'shared_key' => 'L' ) );
|
||||
$wpdb->insert( self::VENDOR, array( 'post_id' => 101, 'shared_key' => 'V' ) );
|
||||
|
||||
$GLOBALS['_wp_cache'] = array();
|
||||
$GLOBALS['_wp_transients'] = array();
|
||||
|
||||
WPDO_Entity_Registry::init();
|
||||
WPDO_Entity_Registry::register_adapter( 'post', new WPDO_Adapter_Post() );
|
||||
WPDO_Entity_Registry::register_group( 'post', 'nr_listing', array( array( 'key' => 'shared_key', 'type' => 'text' ) ), array( 'post_types' => array( 'hp_listing' ) ) );
|
||||
WPDO_Entity_Registry::register_group( 'post', 'nr_vendor', array( array( 'key' => 'shared_key', 'type' => 'text' ) ), array( 'post_types' => array( 'hp_vendor' ) ) );
|
||||
|
||||
$ref = new ReflectionProperty( WPDO_Schema_Manager::class, 'table_exists_cache' );
|
||||
$ref->setAccessible( true );
|
||||
$ref->setValue( null, array() );
|
||||
}
|
||||
|
||||
protected function tearDown(): void {
|
||||
self::drop();
|
||||
WPDO_Entity_Registry::init();
|
||||
}
|
||||
|
||||
private static function drop(): void {
|
||||
global $wpdb;
|
||||
foreach ( array( $wpdb->posts, $wpdb->postmeta, self::LISTING, self::VENDOR ) as $table ) {
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
|
||||
}
|
||||
}
|
||||
|
||||
private function native_value(): array {
|
||||
global $wpdb;
|
||||
return $wpdb->get_col( "SELECT meta_value FROM `{$wpdb->postmeta}` WHERE post_id = 101 AND meta_key = 'shared_key'" );
|
||||
}
|
||||
|
||||
public function test_scoped_routing_writes_back_the_post_types_own_group(): void {
|
||||
$GLOBALS['_wp_options'] = array( WPDO_Entity_Registry::SCOPED_ROUTING_OPTION => '1' );
|
||||
|
||||
TMDO_Native_Restorer::run( 'post' );
|
||||
|
||||
$this->assertSame( array( 'L' ), $this->native_value() );
|
||||
}
|
||||
|
||||
public function test_legacy_routing_writes_back_the_legacy_owner(): void {
|
||||
$GLOBALS['_wp_options'] = array( WPDO_Entity_Registry::SCOPED_ROUTING_OPTION => '0' );
|
||||
|
||||
TMDO_Native_Restorer::run( 'post' );
|
||||
|
||||
$this->assertSame( array( 'V' ), $this->native_value() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Writing flat-table values back to native meta so an aeav_only entity can
|
||||
* leave aeav_only.
|
||||
*
|
||||
* Rule: after the write-back, native meta returns what reads return today.
|
||||
* Fixture (user entity, both groups hold `nickname`; the legacy owner is
|
||||
* rn_extra, registered last):
|
||||
* rn_profile user 1: nickname 'old', level 3, tags ["a","b"]
|
||||
* user 2: nickname NULL, level 5, tags NULL
|
||||
* rn_extra user 1: nickname 'live'
|
||||
* usermeta user 1 level '1' (stale), user 2 tags 'stale'
|
||||
*/
|
||||
class NativeRestorerTest extends TestCase {
|
||||
|
||||
private const PROFILE = 'wp_itest_wpdo_user_rn_profile';
|
||||
private const EXTRA = 'wp_itest_wpdo_user_rn_extra';
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
$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/engine/class-tmdo-mode-manager.php',
|
||||
'includes/engine/class-tmdo-cache-orchestrator.php',
|
||||
'includes/adapters/class-tmdo-adapter-user.php',
|
||||
'includes/migration/class-tmdo-native-restorer.php',
|
||||
) as $file ) {
|
||||
require_once $base . $file;
|
||||
}
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
global $wpdb;
|
||||
self::drop();
|
||||
$wpdb->query( "CREATE TABLE `{$wpdb->usermeta}` ( umeta_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, meta_key VARCHAR(255) DEFAULT NULL, meta_value LONGTEXT, PRIMARY KEY (umeta_id), KEY user_id (user_id), KEY meta_key (meta_key(191)) ) ENGINE=InnoDB" );
|
||||
$wpdb->query( 'CREATE TABLE `' . self::PROFILE . '` ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, nickname VARCHAR(255) DEFAULT NULL, level BIGINT(20) DEFAULT NULL, tags LONGTEXT 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_entity (user_id) ) ENGINE=InnoDB' );
|
||||
$wpdb->query( 'CREATE TABLE `' . self::EXTRA . '` ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, nickname 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_entity (user_id) ) ENGINE=InnoDB' );
|
||||
$wpdb->query( 'INSERT INTO `' . self::PROFILE . "` (user_id, nickname, level, tags) VALUES (1, 'old', 3, '[\"a\",\"b\"]'), (2, NULL, 5, NULL)" );
|
||||
$wpdb->insert( self::EXTRA, array( 'user_id' => 1, 'nickname' => 'live' ) );
|
||||
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => 1, 'meta_key' => 'level', 'meta_value' => '1' ) );
|
||||
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => 2, 'meta_key' => 'tags', 'meta_value' => 'stale' ) );
|
||||
// Historical rows: older than any run, so the catch-up pass leaves them alone.
|
||||
$wpdb->query( 'UPDATE `' . self::PROFILE . "` SET updated_at = '2020-01-01 00:00:00'" );
|
||||
$wpdb->query( 'UPDATE `' . self::EXTRA . "` SET updated_at = '2020-01-01 00:00:00'" );
|
||||
|
||||
$GLOBALS['_wp_options'] = array( 'wpdo_bridge_modes' => array( 'user' => 'aeav_only' ) );
|
||||
$GLOBALS['_wp_cache'] = array();
|
||||
$GLOBALS['_wp_transients'] = array();
|
||||
TMDO_Mode_Manager::reset_cache();
|
||||
|
||||
WPDO_Entity_Registry::init();
|
||||
WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() );
|
||||
WPDO_Entity_Registry::register_group(
|
||||
'user',
|
||||
'rn_profile',
|
||||
array(
|
||||
array( 'key' => 'nickname', 'type' => 'text' ),
|
||||
array( 'key' => 'level', 'type' => 'integer' ),
|
||||
array( 'key' => 'tags', 'type' => 'json' ),
|
||||
)
|
||||
);
|
||||
WPDO_Entity_Registry::register_group( 'user', 'rn_extra', array( array( 'key' => 'nickname', 'type' => 'text' ) ) );
|
||||
|
||||
$ref = new ReflectionProperty( WPDO_Schema_Manager::class, 'table_exists_cache' );
|
||||
$ref->setAccessible( true );
|
||||
$ref->setValue( null, array() );
|
||||
}
|
||||
|
||||
protected function tearDown(): void {
|
||||
self::drop();
|
||||
WPDO_Entity_Registry::init();
|
||||
TMDO_Mode_Manager::reset_cache();
|
||||
}
|
||||
|
||||
private static function drop(): void {
|
||||
global $wpdb;
|
||||
foreach ( array( $wpdb->usermeta, self::PROFILE, self::EXTRA ) as $table ) {
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
|
||||
}
|
||||
}
|
||||
|
||||
/** @return string[] meta_value rows for (user, key). */
|
||||
private function native( int $user_id, string $key ): array {
|
||||
global $wpdb;
|
||||
return $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM `{$wpdb->usermeta}` WHERE user_id = %d AND meta_key = %s", $user_id, $key ) );
|
||||
}
|
||||
|
||||
public function test_dry_run_counts_without_writing(): void {
|
||||
$report = TMDO_Native_Restorer::run( 'user', true );
|
||||
|
||||
$this->assertSame( 2, $report['groups']['rn_profile']['rows'] );
|
||||
$this->assertSame( 1, $report['groups']['rn_extra']['rows'] );
|
||||
$this->assertSame( array( '1' ), $this->native( 1, 'level' ) );
|
||||
$this->assertFalse( $report['demoted'] );
|
||||
}
|
||||
|
||||
public function test_native_meta_ends_up_matching_current_reads(): void {
|
||||
TMDO_Native_Restorer::run( 'user', false, 1 );
|
||||
|
||||
$this->assertSame( array( 'live' ), $this->native( 1, 'nickname' ), 'the group reads use (rn_extra) wins' );
|
||||
$this->assertSame( array( '3' ), $this->native( 1, 'level' ), 'stale native value replaced' );
|
||||
$this->assertSame( array( serialize( array( 'a', 'b' ) ) ), $this->native( 1, 'tags' ), 'json stored the way WP stores arrays' );
|
||||
$this->assertSame( array( '5' ), $this->native( 2, 'level' ) );
|
||||
$this->assertSame( array(), $this->native( 2, 'tags' ), 'NULL flat value removes the stale native row' );
|
||||
$this->assertSame( array(), $this->native( 2, 'nickname' ) );
|
||||
}
|
||||
|
||||
public function test_completion_clears_progress_and_demote_leaves_aeav_only(): void {
|
||||
$report = TMDO_Native_Restorer::run( 'user', false, 500, true );
|
||||
|
||||
$this->assertTrue( $report['demoted'] );
|
||||
$this->assertSame( 'dual_write', TMDO_Mode_Manager::get( 'user' ) );
|
||||
$this->assertFalse( get_option( TMDO_Native_Restorer::CURSOR_OPTION ) );
|
||||
$this->assertFalse( get_option( TMDO_Native_Restorer::STARTED_OPTION ) );
|
||||
}
|
||||
|
||||
public function test_resumes_after_the_stored_cursor(): void {
|
||||
update_option( TMDO_Native_Restorer::CURSOR_OPTION, array( 'user/rn_profile' => 1 ) );
|
||||
|
||||
TMDO_Native_Restorer::run( 'user', false );
|
||||
|
||||
$this->assertSame( array( '1' ), $this->native( 1, 'level' ), 'user 1 was done before the interruption' );
|
||||
$this->assertSame( array( '5' ), $this->native( 2, 'level' ) );
|
||||
}
|
||||
|
||||
public function test_catch_up_copies_rows_written_during_the_run(): void {
|
||||
global $wpdb;
|
||||
$wpdb->query( 'UPDATE `' . self::PROFILE . "` SET level = 9, updated_at = NOW() + INTERVAL 1 HOUR WHERE user_id = 1" );
|
||||
update_option( TMDO_Native_Restorer::CURSOR_OPTION, array( 'user/rn_profile' => 1 ) );
|
||||
|
||||
TMDO_Native_Restorer::run( 'user', false );
|
||||
|
||||
$this->assertSame( array( '9' ), $this->native( 1, 'level' ) );
|
||||
}
|
||||
}
|
||||
@@ -603,6 +603,7 @@ require_once TMDO_PATH . 'includes/migration/phases/class-tmdo-phase-completed.p
|
||||
require_once TMDO_PATH . 'includes/migration/class-tmdo-migration-orchestrator.php';
|
||||
require_once TMDO_PATH . 'includes/migration/class-tmdo-post-migration.php';
|
||||
require_once TMDO_PATH . 'includes/migration/class-tmdo-post-group-reconciler.php';
|
||||
require_once TMDO_PATH . 'includes/migration/class-tmdo-native-restorer.php';
|
||||
require_once TMDO_PATH . 'modules/options/class-tmdo-options-manager.php';
|
||||
require_once TMDO_PATH . 'includes/class-tmdo-postmeta-cleaner.php';
|
||||
require_once TMDO_PATH . 'includes/class-tmdo-termmeta-cleaner.php';
|
||||
|
||||
Reference in New Issue
Block a user