feat(2meet-data-optimizer): backfill every registered user group with member-backfill --all
- member-backfill / member-audit take their groups from the registry (TMDO_Entity_Migration_Engine::backfillable_groups(), restored orphans excluded) instead of a hard-coded list that still named the never-registered `sso`; `--all` backfills every user group (R-2.1 had to use wp eval). TMDO_Post_Migration::backfillable_groups() now delegates to it. - member-cutover checks the Mode_Manager result with is_wp_error(): a guarded or invalid transition used to crash with a fatal (WP_Error used as an array). - Verify: CliMemberBackfillTest (--all and the orphan filter RED before; the --group case cannot fail under the non-throwing WP_CLI stub); dev36 `member-backfill --all --reset` 9 groups, 0 errors, checker user miss/diff/extra 0; `member-cutover --confirm` exits 1 with the Guard message, mode unchanged. - PR: PR-068
This commit is contained in:
@@ -35,24 +35,6 @@ if ( ! class_exists( 'WP_CLI' ) ) {
|
|||||||
*/
|
*/
|
||||||
class TMDO_CLI_Member {
|
class TMDO_CLI_Member {
|
||||||
|
|
||||||
/**
|
|
||||||
* Valid group names managed by this CLI.
|
|
||||||
*
|
|
||||||
* V2.7.0: Extended with core_profile/social/commerce/hp_user to absorb
|
|
||||||
* the legacy WP/WC/HP wp_usermeta keys.
|
|
||||||
*/
|
|
||||||
private const VALID_GROUPS = array(
|
|
||||||
'membership',
|
|
||||||
'activity',
|
|
||||||
'profile',
|
|
||||||
'sso',
|
|
||||||
'core_profile',
|
|
||||||
'social',
|
|
||||||
'commerce',
|
|
||||||
'hp_user',
|
|
||||||
'admin_prefs',
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── member-audit ─────────────────────────────────────────────────────────
|
// ── member-audit ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,7 +55,7 @@ class TMDO_CLI_Member {
|
|||||||
WP_CLI::log( '' );
|
WP_CLI::log( '' );
|
||||||
|
|
||||||
$items = array();
|
$items = array();
|
||||||
foreach ( self::VALID_GROUPS as $group ) {
|
foreach ( TMDO_Entity_Registry::get_groups_for_type( 'user' ) as $group ) {
|
||||||
$table = $wpdb->prefix . 'wpdo_user_' . $group;
|
$table = $wpdb->prefix . 'wpdo_user_' . $group;
|
||||||
$exists = (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
$exists = (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
$wpdb->prepare( 'SHOW TABLES LIKE %s', $table )
|
$wpdb->prepare( 'SHOW TABLES LIKE %s', $table )
|
||||||
@@ -111,7 +93,7 @@ class TMDO_CLI_Member {
|
|||||||
// ── member-backfill ───────────────────────────────────────────────────────
|
// ── member-backfill ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backfill wp_usermeta rows into the specified user flat-table group.
|
* Backfill wp_usermeta rows into one user flat-table group, or all of them.
|
||||||
*
|
*
|
||||||
* Uses TMDO_Entity_Migration_Engine::migrate_group() — cursor-based,
|
* Uses TMDO_Entity_Migration_Engine::migrate_group() — cursor-based,
|
||||||
* 500 rows/batch, checkpoint stored in wpdo_migration_status.
|
* 500 rows/batch, checkpoint stored in wpdo_migration_status.
|
||||||
@@ -119,7 +101,10 @@ class TMDO_CLI_Member {
|
|||||||
* ## OPTIONS
|
* ## OPTIONS
|
||||||
*
|
*
|
||||||
* [--group=<group>]
|
* [--group=<group>]
|
||||||
* : Which group to backfill: membership, activity, profile, sso. Default: membership.
|
* : Registered user group to backfill (e.g. membership, activity, core_profile). Default: membership.
|
||||||
|
*
|
||||||
|
* [--all]
|
||||||
|
* : Backfill every registered user group (restored orphan groups excluded).
|
||||||
*
|
*
|
||||||
* [--batch-size=<n>]
|
* [--batch-size=<n>]
|
||||||
* : Rows per batch (default 500, max 2000).
|
* : Rows per batch (default 500, max 2000).
|
||||||
@@ -134,21 +119,38 @@ class TMDO_CLI_Member {
|
|||||||
*
|
*
|
||||||
* wp wpdo member-backfill --group=membership --dry-run
|
* wp wpdo member-backfill --group=membership --dry-run
|
||||||
* wp wpdo member-backfill --group=membership
|
* wp wpdo member-backfill --group=membership
|
||||||
* wp wpdo member-backfill --group=sso --reset
|
* wp wpdo member-backfill --all
|
||||||
*
|
*
|
||||||
* @param array $args Positional arguments.
|
* @param array $args Positional arguments.
|
||||||
* @param array $assoc_args Named arguments.
|
* @param array $assoc_args Named arguments.
|
||||||
*/
|
*/
|
||||||
public function member_backfill( $args, $assoc_args ): void {
|
public function member_backfill( $args, $assoc_args ): void {
|
||||||
$group = (string) ( $assoc_args['group'] ?? 'membership' );
|
|
||||||
$batch_size = min( 2000, max( 1, (int) ( $assoc_args['batch-size'] ?? 500 ) ) );
|
$batch_size = min( 2000, max( 1, (int) ( $assoc_args['batch-size'] ?? 500 ) ) );
|
||||||
$dry_run = isset( $assoc_args['dry-run'] );
|
$dry_run = isset( $assoc_args['dry-run'] );
|
||||||
$reset = isset( $assoc_args['reset'] );
|
$reset = isset( $assoc_args['reset'] );
|
||||||
|
$valid = TMDO_Entity_Migration_Engine::backfillable_groups( 'user' );
|
||||||
|
|
||||||
if ( ! in_array( $group, self::VALID_GROUPS, true ) ) {
|
if ( isset( $assoc_args['all'] ) ) {
|
||||||
WP_CLI::error( 'Invalid --group. Choose: ' . implode( ', ', self::VALID_GROUPS ) );
|
if ( isset( $assoc_args['group'] ) ) {
|
||||||
|
WP_CLI::error( 'Use either --group or --all, not both.' );
|
||||||
|
}
|
||||||
|
$groups = $valid;
|
||||||
|
} else {
|
||||||
|
$groups = array( (string) ( $assoc_args['group'] ?? 'membership' ) );
|
||||||
|
if ( ! in_array( $groups[0], $valid, true ) ) {
|
||||||
|
WP_CLI::error( 'Invalid --group. Choose: ' . implode( ', ', $valid ) );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ( $groups as $group ) {
|
||||||
|
$this->backfill_group( $group, $batch_size, $dry_run, $reset );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill one user group and report the result.
|
||||||
|
*/
|
||||||
|
private function backfill_group( string $group, int $batch_size, bool $dry_run, bool $reset ): void {
|
||||||
if ( $reset ) {
|
if ( $reset ) {
|
||||||
TMDO_Entity_Migration_Engine::reset_checkpoint( 'user', $group );
|
TMDO_Entity_Migration_Engine::reset_checkpoint( 'user', $group );
|
||||||
WP_CLI::log( "Checkpoint cleared for user/{$group}." );
|
WP_CLI::log( "Checkpoint cleared for user/{$group}." );
|
||||||
@@ -298,8 +300,8 @@ class TMDO_CLI_Member {
|
|||||||
|
|
||||||
$result = TMDO_Mode_Manager::set( 'user', 'aeav_only' );
|
$result = TMDO_Mode_Manager::set( 'user', 'aeav_only' );
|
||||||
|
|
||||||
if ( isset( $result['error'] ) ) {
|
if ( is_wp_error( $result ) ) {
|
||||||
WP_CLI::error( 'Cutover failed: ' . $result['error'] );
|
WP_CLI::error( 'Cutover failed: ' . $result->get_error_message() );
|
||||||
}
|
}
|
||||||
|
|
||||||
WP_CLI::success( 'User bridge mode set to aeav_only. All user meta reads/writes now go through flat tables.' );
|
WP_CLI::success( 'User bridge mode set to aeav_only. All user meta reads/writes now go through flat tables.' );
|
||||||
|
|||||||
@@ -386,6 +386,23 @@ final class TMDO_Entity_Migration_Engine {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registered groups of an entity type that can be backfilled from native
|
||||||
|
* meta. Orphan groups restored from wpdo_registry_meta are left out: their
|
||||||
|
* values live only in the flat table, a backfill would wipe them.
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function backfillable_groups( string $entity_type ): array {
|
||||||
|
$orphans = array();
|
||||||
|
foreach ( TMDO_Entity_Registry::get_restored_groups() as $restored ) {
|
||||||
|
if ( $entity_type === $restored['entity_type'] ) {
|
||||||
|
$orphans[] = $restored['group'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array_values( array_diff( TMDO_Entity_Registry::get_groups_for_type( $entity_type ), $orphans ) );
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────
|
||||||
// 斷點管理
|
// 斷點管理
|
||||||
// ─────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -561,13 +561,7 @@ final class TMDO_Post_Migration {
|
|||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
public static function backfillable_groups(): array {
|
public static function backfillable_groups(): array {
|
||||||
$orphans = array();
|
return TMDO_Entity_Migration_Engine::backfillable_groups( self::ENTITY_TYPE );
|
||||||
foreach ( TMDO_Entity_Registry::get_restored_groups() as $restored ) {
|
|
||||||
if ( self::ENTITY_TYPE === $restored['entity_type'] ) {
|
|
||||||
$orphans[] = $restored['group'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return array_values( array_diff( TMDO_Entity_Registry::get_groups_for_type( self::ENTITY_TYPE ), $orphans ) );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `wp tmdo member-backfill` takes its groups from the registry (2.2 PR-068).
|
||||||
|
*
|
||||||
|
* Before: a hard-coded VALID_GROUPS list (with the never-registered `sso`)
|
||||||
|
* rejected groups registered by add-ons, and there was no way to backfill
|
||||||
|
* every user group at once — R-2.1 had to fall back to `wp eval`.
|
||||||
|
*
|
||||||
|
* Fixture (user entity): groups cb_one (cb_a) and cb_two (cb_b).
|
||||||
|
*/
|
||||||
|
class CliMemberBackfillTest extends TestCase {
|
||||||
|
|
||||||
|
private const ONE = 'wp_itest_wpdo_user_cb_one';
|
||||||
|
private const TWO = 'wp_itest_wpdo_user_cb_two';
|
||||||
|
private const STATUS = 'wp_itest_wpdo_migration_status';
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void {
|
||||||
|
foreach ( array(
|
||||||
|
'includes/adapters/interface-entity-adapter.php',
|
||||||
|
'includes/adapters/class-tmdo-adapter-user.php',
|
||||||
|
'cli/class-tmdo-cli-member.php',
|
||||||
|
) as $file ) {
|
||||||
|
require_once WPDO_PLUGIN_DIR . $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) ) ENGINE=InnoDB" );
|
||||||
|
$wpdb->query( 'CREATE TABLE `' . self::ONE . '` ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, cb_a VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_entity (user_id) ) ENGINE=InnoDB' );
|
||||||
|
$wpdb->query( 'CREATE TABLE `' . self::TWO . '` ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, cb_b VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_entity (user_id) ) ENGINE=InnoDB' );
|
||||||
|
$wpdb->query( 'CREATE TABLE `' . self::STATUS . "` ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, entity_type VARCHAR(50) NOT NULL, group_name VARCHAR(50) NOT NULL, last_id BIGINT UNSIGNED NOT NULL DEFAULT 0, total_migrated BIGINT UNSIGNED NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'pending', started_at DATETIME DEFAULT NULL, completed_at DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY entity_group (entity_type, group_name) ) ENGINE=InnoDB" );
|
||||||
|
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => 1, 'meta_key' => 'cb_a', 'meta_value' => 'one' ) );
|
||||||
|
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => 1, 'meta_key' => 'cb_b', 'meta_value' => 'two' ) );
|
||||||
|
|
||||||
|
$GLOBALS['_wp_cache'] = array();
|
||||||
|
WPDO_Entity_Registry::init();
|
||||||
|
WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() );
|
||||||
|
WPDO_Entity_Registry::register_group( 'user', 'cb_one', array( array( 'key' => 'cb_a', 'type' => 'text' ) ) );
|
||||||
|
WPDO_Entity_Registry::register_group( 'user', 'cb_two', array( array( 'key' => 'cb_b', 'type' => 'text' ) ) );
|
||||||
|
WPDO_Schema_Manager::flush_table_exists_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void {
|
||||||
|
self::drop();
|
||||||
|
WPDO_Entity_Registry::init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function drop(): void {
|
||||||
|
global $wpdb;
|
||||||
|
foreach ( array( $wpdb->usermeta, self::ONE, self::TWO, self::STATUS ) as $table ) {
|
||||||
|
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function flat( string $table, string $col ): ?string {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->get_var( "SELECT `{$col}` FROM `{$table}` WHERE user_id = 1" );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_all_backfills_every_registered_user_group(): void {
|
||||||
|
( new TMDO_CLI_Member() )->member_backfill( array(), array( 'all' => true ) );
|
||||||
|
|
||||||
|
$this->assertSame( 'one', $this->flat( self::ONE, 'cb_a' ) );
|
||||||
|
$this->assertSame( 'two', $this->flat( self::TWO, 'cb_b' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_group_accepts_any_registered_user_group(): void {
|
||||||
|
( new TMDO_CLI_Member() )->member_backfill( array(), array( 'group' => 'cb_two' ) );
|
||||||
|
|
||||||
|
$this->assertSame( 'two', $this->flat( self::TWO, 'cb_b' ) );
|
||||||
|
$this->assertNull( $this->flat( self::ONE, 'cb_a' ), 'only the requested group' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_backfillable_groups_leave_out_restored_orphans(): void {
|
||||||
|
WPDO_Entity_Registry::restore_group( 'user', 'cb_orphan', array( array( 'key' => 'cb_orphan_key', 'type' => 'text' ) ) );
|
||||||
|
|
||||||
|
$this->assertSame( array( 'cb_one', 'cb_two' ), TMDO_Entity_Migration_Engine::backfillable_groups( 'user' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user