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' ) ); } }