From 2022cf1d08285b628f3f4d4340dfd9913fb0ae61 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sat, 26 Sep 2026 23:30:28 +0800 Subject: [PATCH] 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 --- cli/class-tmdo-cli-member.php | 56 ++++++------ .../class-tmdo-entity-migration-engine.php | 17 ++++ .../migration/class-tmdo-post-migration.php | 8 +- tests/integration/CliMemberBackfillTest.php | 85 +++++++++++++++++++ 4 files changed, 132 insertions(+), 34 deletions(-) create mode 100644 tests/integration/CliMemberBackfillTest.php diff --git a/cli/class-tmdo-cli-member.php b/cli/class-tmdo-cli-member.php index 06c296c..66c3e7c 100644 --- a/cli/class-tmdo-cli-member.php +++ b/cli/class-tmdo-cli-member.php @@ -35,24 +35,6 @@ if ( ! class_exists( 'WP_CLI' ) ) { */ 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 ───────────────────────────────────────────────────────── /** @@ -73,7 +55,7 @@ class TMDO_CLI_Member { WP_CLI::log( '' ); $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; $exists = (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) @@ -111,7 +93,7 @@ class TMDO_CLI_Member { // ── 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, * 500 rows/batch, checkpoint stored in wpdo_migration_status. @@ -119,7 +101,10 @@ class TMDO_CLI_Member { * ## OPTIONS * * [--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=] * : 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 - * wp wpdo member-backfill --group=sso --reset + * wp wpdo member-backfill --all * * @param array $args Positional arguments. * @param array $assoc_args Named arguments. */ 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 ) ) ); $dry_run = isset( $assoc_args['dry-run'] ); $reset = isset( $assoc_args['reset'] ); + $valid = TMDO_Entity_Migration_Engine::backfillable_groups( 'user' ); - if ( ! in_array( $group, self::VALID_GROUPS, true ) ) { - WP_CLI::error( 'Invalid --group. Choose: ' . implode( ', ', self::VALID_GROUPS ) ); + if ( isset( $assoc_args['all'] ) ) { + 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 ) { TMDO_Entity_Migration_Engine::reset_checkpoint( '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' ); - if ( isset( $result['error'] ) ) { - WP_CLI::error( 'Cutover failed: ' . $result['error'] ); + if ( is_wp_error( $result ) ) { + 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.' ); diff --git a/includes/engine/class-tmdo-entity-migration-engine.php b/includes/engine/class-tmdo-entity-migration-engine.php index d31231d..85d40e6 100644 --- a/includes/engine/class-tmdo-entity-migration-engine.php +++ b/includes/engine/class-tmdo-entity-migration-engine.php @@ -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 ) ); + } + // ───────────────────────────────────────────────────────── // 斷點管理 // ───────────────────────────────────────────────────────── diff --git a/includes/migration/class-tmdo-post-migration.php b/includes/migration/class-tmdo-post-migration.php index b776a98..4f77abe 100644 --- a/includes/migration/class-tmdo-post-migration.php +++ b/includes/migration/class-tmdo-post-migration.php @@ -561,13 +561,7 @@ final class TMDO_Post_Migration { * @return string[] */ public static function backfillable_groups(): array { - $orphans = array(); - 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 ) ); + return TMDO_Entity_Migration_Engine::backfillable_groups( self::ENTITY_TYPE ); } /** diff --git a/tests/integration/CliMemberBackfillTest.php b/tests/integration/CliMemberBackfillTest.php new file mode 100644 index 0000000..e49030c --- /dev/null +++ b/tests/integration/CliMemberBackfillTest.php @@ -0,0 +1,85 @@ +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' ) ); + } +}