diff --git a/admin/class-tmdo-admin.php b/admin/class-tmdo-admin.php index f28da03..9addced 100644 --- a/admin/class-tmdo-admin.php +++ b/admin/class-tmdo-admin.php @@ -556,11 +556,11 @@ class TMDO_Admin { exit; } - // v2.10.0: Post Migration Wizard — backfill all 7 groups. + // v2.10.0: Post Migration Wizard — backfill every registered post group. if ( isset( $_POST['wpdo_post_backfill_all'] ) && check_admin_referer( 'wpdo_post_backfill_all' ) && class_exists( 'TMDO_Post_Migration' ) ) { $total_migrated = 0; $errors = array(); - foreach ( array( 'wp_core', 'attachment', 'wc_product', 'hp_listing_core', 'hp_request_core', 'hp_vendor_core', 'nav_menu_item' ) as $group ) { + foreach ( TMDO_Post_Migration::backfillable_groups() as $group ) { try { $result = TMDO_Post_Migration::backfill_group( $group ); $total_migrated += (int) $result['migrated']; diff --git a/cli/class-tmdo-cli-post.php b/cli/class-tmdo-cli-post.php index ac9382f..54f0e01 100644 --- a/cli/class-tmdo-cli-post.php +++ b/cli/class-tmdo-cli-post.php @@ -198,25 +198,16 @@ class TMDO_CLI_Post { /** * Backfill one post entity group from wp_postmeta into its flat table - * via a single bulk SQL pivot (ON DUPLICATE KEY UPDATE). + * via a single bulk SQL pivot (ON DUPLICATE KEY UPDATE). Groups with + * JSON-typed fields are backfilled row by row instead. * - * Idempotent — safe to re-run. Skips JSON-typed fields (handled by the - * row-by-row backfill phase in v2.9.4+). + * Idempotent — safe to re-run. * * ## OPTIONS * * --group= - * : Entity group name. Required. - * --- - * options: - * - wp_core - * - attachment - * - wc_product - * - hp_listing_core - * - hp_request_core - * - hp_vendor_core - * - nav_menu_item - * --- + * : Registered post entity group name (e.g. wp_core, attachment, + * hp_listing_core). Required. * * ## EXAMPLES * diff --git a/includes/migration/class-tmdo-post-migration.php b/includes/migration/class-tmdo-post-migration.php index 26caf95..b776a98 100644 --- a/includes/migration/class-tmdo-post-migration.php +++ b/includes/migration/class-tmdo-post-migration.php @@ -99,11 +99,12 @@ final class TMDO_Post_Migration { /** * Bulk SQL pivot for one group: select managed keys from wp_postmeta, * filter by post_type, pivot via MAX(CASE) GROUP BY post_id, UPSERT - * into the group's flat table. + * into the group's flat table. Groups with json fields go row by row + * through backfill_group_json() instead. * * @param string $group_name Entity group name (e.g. 'wc_product'). * @return array{migrated:int,group:string,post_type:string|null} - * @throws InvalidArgumentException When group is not registered. + * @throws InvalidArgumentException When group is not registered, or is a restored orphan group. * @throws RuntimeException When the pivot SQL fails. */ public static function backfill_group( string $group_name ): array { @@ -115,8 +116,30 @@ final class TMDO_Post_Migration { 'Unknown post entity group: ' . esc_html( $group_name ) ); } + if ( ! in_array( $group_name, self::backfillable_groups(), true ) ) { + throw new InvalidArgumentException( + 'Orphan post group, its values live only in the flat table: ' . esc_html( $group_name ) + ); + } + + $post_type = self::group_post_type( $group_name ); + + // json values need PHP-level unserialize → json encode, which the bulk + // pivot below cannot do: backfill such groups row by row instead. + if ( in_array( 'json', array_column( $fields, 'type' ), true ) ) { + $result = self::backfill_group_json( $group_name, array( 'resume' => false ) ); + if ( ! empty( $result['error'] ) || ! empty( $result['errors'] ) ) { + throw new RuntimeException( + 'backfill_group failed for ' . esc_html( $group_name ) . ': ' . esc_html( (string) ( $result['error'] ?? $result['errors'] . ' row error(s)' ) ) + ); + } + return array( + 'migrated' => (int) $result['migrated'], + 'group' => $group_name, + 'post_type' => $post_type, + ); + } - $post_type = self::group_post_type( $group_name ); $flat_table = TMDO_Schema_Manager::get_table_name( self::ENTITY_TYPE, $group_name ); // Introspect target table columns so we only pivot fields that actually @@ -131,16 +154,10 @@ final class TMDO_Post_Migration { } // Build column list and CASE expressions. - // Skip json/textarea types from the bulk pivot — those need row-by-row - // unserialize handling (deferred to v2.9.4 backfill_unserialize phase). $columns = array(); $cases = array(); $update_parts = array(); foreach ( $fields as $field ) { - $type = $field['type'] ?? 'text'; - if ( 'json' === $type ) { - continue; - } $col = TMDO_Schema_Manager::sanitize_column_name( $field['key'] ); if ( ! isset( $existing_cols[ $col ] ) ) { continue; // Column not present in this table — skip silently. @@ -536,6 +553,23 @@ final class TMDO_Post_Migration { // Helpers // ───────────────────────────────────────────────────────────────────────── + /** + * Registered post groups that can be backfilled from wp_postmeta. Orphan + * groups restored from wpdo_registry_meta are left out: their values live + * only in the flat table. + * + * @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 ) ); + } + /** * All meta_keys registered for any post entity group. * diff --git a/tests/integration/PostBackfillJsonTest.php b/tests/integration/PostBackfillJsonTest.php index 4a6bf39..d87d099 100644 --- a/tests/integration/PostBackfillJsonTest.php +++ b/tests/integration/PostBackfillJsonTest.php @@ -215,6 +215,38 @@ class PostBackfillJsonTest extends TestCase { $this->assertSame( 0, $result['errors'] ?? -1 ); } + // ── backfill_group() / backfillable_groups() (2.2 PR-067) ─────────────── + + public function test_backfill_group_fills_json_columns_instead_of_skipping_them(): void { + $this->seed_attachment( 1, array( 'width' => 640 ), 'Alt text' ); + + $result = WPDO_Post_Migration::backfill_group( 'attachment' ); + + $this->assertSame( 'attachment', $result['group'] ); + global $wpdb; + $row = $wpdb->get_row( 'SELECT _wp_attachment_metadata, _wp_attachment_image_alt FROM `' . self::FLAT . '` WHERE post_id = 1', ARRAY_A ); + $this->assertSame( array( 'width' => 640 ), json_decode( (string) $row['_wp_attachment_metadata'], true ), 'json field must not be left NULL by the bulk pivot' ); + $this->assertSame( 'Alt text', $row['_wp_attachment_image_alt'] ); + } + + public function test_backfillable_groups_are_the_registered_post_groups(): void { + WPDO_Entity_Registry::register_group( 'post', 'itest_extra', array( array( 'key' => 'itest_extra_key', 'type' => 'text' ) ) ); + + $groups = WPDO_Post_Migration::backfillable_groups(); + + $this->assertSame( WPDO_Entity_Registry::get_groups_for_type( 'post' ), $groups ); + $this->assertContains( 'itest_extra', $groups, 'a group registered by an add-on must be offered for backfill' ); + } + + public function test_restored_orphan_groups_are_never_backfilled(): void { + WPDO_Entity_Registry::restore_group( 'post', 'itest_orphan', array( array( 'key' => 'itest_orphan_key', 'type' => 'text' ) ) ); + $this->assertContains( 'itest_orphan', WPDO_Entity_Registry::get_groups_for_type( 'post' ) ); + + $this->assertNotContains( 'itest_orphan', WPDO_Post_Migration::backfillable_groups(), 'orphan data lives only in the flat table' ); + $this->expectException( InvalidArgumentException::class ); + WPDO_Post_Migration::backfill_group( 'itest_orphan' ); + } + public function test_returns_error_for_unknown_group(): void { $result = WPDO_Post_Migration::backfill_group_json( 'bogus_group' );