feat(2meet-data-optimizer): reconcile post rows written to the wrong group table

- TMDO_Post_Group_Reconciler copies non-NULL values that legacy routing stored in another group's table into the post type's own group (NULL/missing legacy values keep the scoped value), resumable via a cursor, with a catch-up pass for rows updated during the run, then switches on tmdo_post_scoped_routing
- CLI: wp tmdo post-reconcile-groups [--dry-run] [--batch=<n>]; registry exposes get_group_post_types()
- Verify: PostGroupReconcilerPlanTest (4), PostGroupReconcilerTest (6); core unit 635 / integration 449, HP 145/49, WC 10/22 OK; read-only dry-run: dev20 320 values / 0 overwrites, dev21/22 none
- PR: PR-034
This commit is contained in:
2026-09-26 00:04:38 +08:00
parent 3f4fe52e1b
commit e0683a19b1
8 changed files with 506 additions and 0 deletions
+1
View File
@@ -258,6 +258,7 @@ require_once TMDO_PATH . 'includes/migration/phases/class-tmdo-phase-cleanup.php
require_once TMDO_PATH . 'includes/migration/phases/class-tmdo-phase-completed.php';
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';
add_action( TMDO_Migration_Orchestrator::CRON_HOOK, array( 'TMDO_Migration_Orchestrator', 'cron_tick' ) );
add_action( 'wpdo_bridge_mode_changed', array( 'TMDO_Migration_Orchestrator', 'bust_attention_cache' ) );
add_action( 'tmdo_bridge_mode_changed', array( 'TMDO_Migration_Orchestrator', 'bust_attention_cache' ) );
+46
View File
@@ -259,6 +259,50 @@ class TMDO_CLI_Post {
);
}
// ── post-reconcile-groups ─────────────────────────────────────────────────
/**
* Move post values written under legacy routing into their post type's
* group table, then switch on post_type-scoped routing.
*
* Needed once on sites that already hold post flat rows (the installer
* leaves tmdo_post_scoped_routing off there). Resumable: an interrupted
* run continues from its cursor.
*
* ## OPTIONS
*
* [--dry-run]
* : Count the values that would be copied / overwritten; write nothing.
*
* [--batch=<n>]
* : Rows per batch. Default 500.
*
* ## EXAMPLES
*
* wp tmdo post-reconcile-groups --dry-run
* wp tmdo post-reconcile-groups
*
* @param array $args Positional arguments (unused).
* @param array $assoc_args Named arguments.
*/
public function post_reconcile_groups( $args, $assoc_args ): void {
$dry_run = isset( $assoc_args['dry-run'] );
$report = TMDO_Post_Group_Reconciler::run( $dry_run, (int) ( $assoc_args['batch'] ?? 500 ) );
if ( empty( $report['moves'] ) ) {
WP_CLI::log( 'No shared post keys to move.' );
}
foreach ( $report['moves'] as $m ) {
WP_CLI::log( sprintf( '%s → %s %s: %d value(s)%s', $m['source'], $m['target'], $m['key'], $m['copied'], $dry_run ? sprintf( ', %d would overwrite a different value', $m['overwritten'] ) : '' ) );
}
if ( $dry_run ) {
WP_CLI::success( '[dry-run] Nothing written. Re-run without --dry-run to copy and switch on scoped routing.' );
return;
}
WP_CLI::success( 'Post groups reconciled; tmdo_post_scoped_routing is on.' );
}
// ── post-cleanup (v2.9.3) ─────────────────────────────────────────────────
/**
@@ -853,3 +897,5 @@ WP_CLI::add_command( 'wpdo post-benchmark', array( 'TMDO_CLI_Post', 'post_benchm
WP_CLI::add_command( 'tmdo post-benchmark', array( 'TMDO_CLI_Post', 'post_benchmark' ) );
WP_CLI::add_command( 'wpdo post-shadow-report', array( 'TMDO_CLI_Post', 'post_shadow_report' ) );
WP_CLI::add_command( 'tmdo post-shadow-report', array( 'TMDO_CLI_Post', 'post_shadow_report' ) );
WP_CLI::add_command( 'wpdo post-reconcile-groups', array( 'TMDO_CLI_Post', 'post_reconcile_groups' ) );
WP_CLI::add_command( 'tmdo post-reconcile-groups', array( 'TMDO_CLI_Post', 'post_reconcile_groups' ) );
@@ -37,6 +37,9 @@ final class TMDO_Entity_Registry {
/** @var array<string, array<string, array<string, array>>> 實體類型 → post_type → meta_key → 欄位定義 */
private static array $scoped_index = array();
/** @var array<string, array<string, string[]>> 實體類型 → 群組名 → 限定的 post_type */
private static array $scopes = array();
/** @var array<string, array> "entity/group" → 同名群組以不同定義重複註冊的紀錄 */
private static array $collisions = array();
@@ -70,6 +73,7 @@ final class TMDO_Entity_Registry {
self::$field_index = array();
self::$unscoped_index = array();
self::$scoped_index = array();
self::$scopes = array();
self::$pending_schemas = array();
self::$collisions = array();
self::$restored = array();
@@ -144,6 +148,9 @@ final class TMDO_Entity_Registry {
// 建立索引以供快速查詢
$post_types = array_values( array_filter( array_map( 'strval', (array) ( $args['post_types'] ?? array() ) ) ) );
if ( $post_types ) {
self::$scopes[ $entity_type ][ $group_name ] = $post_types;
}
foreach ( $normalized as $normalized_field ) {
self::index_field( $entity_type, $normalized_field, $post_types );
}
@@ -365,6 +372,15 @@ final class TMDO_Entity_Registry {
return array_keys( self::$groups[ $entity_type ] ?? array() );
}
/**
* 群組限定的 post_type(未限定時為空陣列)。
*
* @return string[]
*/
public static function get_group_post_types( string $entity_type, string $group_name ): array {
return self::$scopes[ $entity_type ][ $group_name ] ?? array();
}
/**
* 取得特定群組的完整欄位定義
*/
@@ -0,0 +1,221 @@
<?php
/**
* TMDO_Post_Group_Reconciler — move post values written under legacy routing.
*
* Before post_type-scoped routing (tmdo_post_scoped_routing), a meta key
* shared by several post groups was read and written through the last group
* registered, e.g. a listing's hp_rating lived in wp_wpdo_post_hp_vendor_core.
* Scoped lookups read it from the listing's own group instead, so those
* values have to be copied there first.
*
* Rule: after the switch every read must return what it returned before.
* - A non-NULL value in the legacy owner's table is what reads served, so
* it overwrites the scoped group's column.
* - NULL / missing legacy values leave the scoped column alone (it holds
* the backfill copy, the only data there is).
* - Source rows are left in place; scoped lookups never read them again.
*
* Batched by entity id with a cursor option so an interrupted run resumes; a
* final pass re-copies legacy rows updated since the first run started, then
* scoped routing is switched on.
*
* @package WP_Data_Optimizer
*/
declare(strict_types=1);
defined( 'ABSPATH' ) || exit;
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- placeholders are built for spread params; table/column names come from the entity registry via Schema_Manager and are sanitized there.
/**
* Copies legacy-routed post values into their post type's group table.
*/
final class TMDO_Post_Group_Reconciler {
/** Option: move id → last entity id copied. */
public const CURSOR_OPTION = 'tmdo_post_reconcile_cursor';
/** Option: DB time the first (possibly interrupted) run started. */
public const STARTED_OPTION = 'tmdo_post_reconcile_started';
/**
* Keys of scoped post groups whose legacy owner is another group.
*
* @return array<int, array{target:string, source:string, key:string, post_types:string[]}>
*/
public static function plan(): array {
$moves = array();
foreach ( TMDO_Entity_Registry::get_groups_for_type( 'post' ) as $group ) {
$post_types = TMDO_Entity_Registry::get_group_post_types( 'post', $group );
if ( empty( $post_types ) ) {
continue;
}
foreach ( TMDO_Entity_Registry::get_group_keys( 'post', $group ) as $key ) {
$legacy = TMDO_Entity_Registry::get_field( 'post', $key );
if ( null === $legacy || $legacy['group'] === $group ) {
continue;
}
$moves[] = array(
'target' => $group,
'source' => $legacy['group'],
'key' => $key,
'post_types' => $post_types,
);
}
}
return $moves;
}
/**
* Copy the values (or, with $dry_run, count what would be copied).
*
* @param bool $dry_run Count only; write nothing.
* @param int $batch Rows per batch.
* @return array{moves: array<int, array>, enabled: bool}
*/
public static function run( bool $dry_run = false, int $batch = 500 ): array {
global $wpdb;
$report = array(
'moves' => array(),
'enabled' => false,
);
$adapter = TMDO_Entity_Registry::get_adapter( 'post' );
if ( ! $adapter ) {
return $report;
}
$id_col = $adapter->get_entity_id_column();
$batch = max( 1, $batch );
if ( ! $dry_run && false === get_option( self::STARTED_OPTION ) ) {
update_option( self::STARTED_OPTION, (string) $wpdb->get_var( 'SELECT NOW()' ), false );
}
$cursor = get_option( self::CURSOR_OPTION, array() );
$cursor = is_array( $cursor ) ? $cursor : array();
$moves = self::plan();
foreach ( $moves as $move ) {
list( $from, $params ) = self::source_clause( $move, $id_col );
$entry = array(
'target' => $move['target'],
'source' => $move['source'],
'key' => $move['key'],
'copied' => 0,
'overwritten' => 0,
);
if ( $dry_run ) {
$target = TMDO_Schema_Manager::get_table_name( 'post', $move['target'] );
$col = TMDO_Schema_Manager::sanitize_column_name( $move['key'] );
$entry['copied'] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) {$from}", ...$params ) );
$entry['overwritten'] = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) {$from} AND EXISTS ( SELECT 1 FROM `{$target}` t WHERE t.`{$id_col}` = s.`{$id_col}` AND t.`{$col}` IS NOT NULL AND t.`{$col}` <> s.`{$col}` )",
...$params
)
);
$report['moves'][] = $entry;
continue;
}
$move_id = "{$move['source']}>{$move['target']}:{$move['key']}";
$last = (int) ( $cursor[ $move_id ] ?? 0 );
do {
$rows = (array) $wpdb->get_results(
$wpdb->prepare( "SELECT s.`{$id_col}` AS id, s.`" . TMDO_Schema_Manager::sanitize_column_name( $move['key'] ) . "` AS v {$from} AND s.`{$id_col}` > %d ORDER BY s.`{$id_col}` LIMIT %d", ...array_merge( $params, array( $last, $batch ) ) ),
ARRAY_A
);
$entry['copied'] += self::copy_rows( $move, $id_col, $rows );
$fetched = count( $rows );
if ( $fetched ) {
$last = (int) end( $rows )['id'];
$cursor[ $move_id ] = $last;
update_option( self::CURSOR_OPTION, $cursor, false );
}
} while ( $fetched === $batch );
$report['moves'][] = $entry;
}
if ( $dry_run ) {
return $report;
}
// Catch-up: legacy-routed writes that landed after a batch passed their row.
$started = (string) get_option( self::STARTED_OPTION );
foreach ( $moves as $move ) {
list( $from, $params ) = self::source_clause( $move, $id_col );
$rows = (array) $wpdb->get_results(
$wpdb->prepare( "SELECT s.`{$id_col}` AS id, s.`" . TMDO_Schema_Manager::sanitize_column_name( $move['key'] ) . "` AS v {$from} AND s.updated_at >= %s", ...array_merge( $params, array( $started ) ) ),
ARRAY_A
);
self::copy_rows( $move, $id_col, $rows );
}
update_option( TMDO_Entity_Registry::SCOPED_ROUTING_OPTION, '1', true );
delete_option( self::CURSOR_OPTION );
delete_option( self::STARTED_OPTION );
$report['enabled'] = true;
return $report;
}
/**
* FROM/WHERE for non-NULL legacy values of the move's post types.
*
* @param array $move One entry of plan().
* @param string $id_col Entity id column of the flat tables.
* @return array{0:string, 1:array}
*/
private static function source_clause( array $move, string $id_col ): array {
global $wpdb;
$source = TMDO_Schema_Manager::get_table_name( 'post', $move['source'] );
$col = TMDO_Schema_Manager::sanitize_column_name( $move['key'] );
$types = implode( ',', array_fill( 0, count( $move['post_types'] ), '%s' ) );
return array(
"FROM `{$source}` s INNER JOIN `{$wpdb->posts}` p ON p.ID = s.`{$id_col}` AND p.post_type IN ({$types}) WHERE s.`{$col}` IS NOT NULL",
$move['post_types'],
);
}
/**
* Upsert one column of the target group rows and drop their cached copies.
*
* @param array $move One entry of plan().
* @param string $id_col Entity id column of the flat tables.
* @param array $rows Source rows: list of { id, v }.
* @return int Rows written.
*/
private static function copy_rows( array $move, string $id_col, array $rows ): int {
global $wpdb;
if ( empty( $rows ) ) {
return 0;
}
$target = TMDO_Schema_Manager::get_table_name( 'post', $move['target'] );
$col = TMDO_Schema_Manager::sanitize_column_name( $move['key'] );
$values = array();
$params = array();
foreach ( $rows as $row ) {
$values[] = '(%d, %s)';
$params[] = (int) $row['id'];
$params[] = (string) $row['v'];
}
$wpdb->query(
$wpdb->prepare(
"INSERT INTO `{$target}` (`{$id_col}`, `{$col}`) VALUES " . implode( ', ', $values ) . " ON DUPLICATE KEY UPDATE `{$col}` = VALUES(`{$col}`)",
...$params
)
);
if ( class_exists( 'TMDO_Cache_Orchestrator' ) ) {
foreach ( $rows as $row ) {
TMDO_Cache_Orchestrator::invalidate( 'post', (int) $row['id'], $move['target'] );
}
}
return count( $rows );
}
}
+1
View File
@@ -642,6 +642,7 @@ require_once TMDO_PATH . 'includes/migration/phases/class-tmdo-phase-cleanup.php
require_once TMDO_PATH . 'includes/migration/phases/class-tmdo-phase-completed.php';
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';
// Options Manager.
require_once TMDO_PATH . 'modules/options/class-tmdo-options-manager.php';
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Moving values written under legacy routing into their post type's group.
*
* Fixture (legacy owner of `shared_key` = rc_vendor, the last group registered):
* listing 101: vendor table 'live' | listing table 'backfill' (+ price 5)
* listing 102: vendor table 'only-in-vendor' | no listing row
* listing 103: no vendor row | listing table 'keep'
* listing 104: vendor row with NULL | listing table 'backfill-104'
* vendor 202: vendor table 'vendor own value'
*
* Reads before the switch returned the vendor-table value for 101/102, so
* those must win; 103/104 were never written after the backfill, so the
* listing value stays.
*/
class PostGroupReconcilerTest extends TestCase {
private const LISTING = 'wp_itest_wpdo_post_rc_listing';
private const VENDOR = 'wp_itest_wpdo_post_rc_vendor';
public static function setUpBeforeClass(): void {
$base = WPDO_PLUGIN_DIR;
foreach ( array(
'includes/adapters/interface-entity-adapter.php',
'includes/engine/class-tmdo-schema-manager.php',
'includes/engine/class-tmdo-entity-registry.php',
'includes/engine/class-tmdo-cache-orchestrator.php',
'includes/adapters/class-tmdo-adapter-post.php',
'includes/migration/class-tmdo-post-group-reconciler.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', post_status VARCHAR(20) NOT NULL DEFAULT 'publish', PRIMARY KEY (ID) ) ENGINE=InnoDB" );
foreach ( array( 101 => 'hp_listing', 102 => 'hp_listing', 103 => 'hp_listing', 104 => 'hp_listing', 202 => 'hp_vendor' ) as $id => $type ) {
$wpdb->insert( $wpdb->posts, array( 'ID' => $id, 'post_type' => $type ) );
}
$wpdb->query( 'CREATE TABLE `' . self::LISTING . '` ( id BIGINT NOT NULL AUTO_INCREMENT, post_id BIGINT NOT NULL, shared_key VARCHAR(255) DEFAULT NULL, price DECIMAL(18,6) 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 (post_id) ) ENGINE=InnoDB' );
$wpdb->query( 'CREATE TABLE `' . self::VENDOR . '` ( 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 CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_entity (post_id) ) ENGINE=InnoDB' );
$wpdb->insert( self::VENDOR, array( 'post_id' => 101, 'shared_key' => 'live' ) );
$wpdb->insert( self::VENDOR, array( 'post_id' => 102, 'shared_key' => 'only-in-vendor' ) );
$wpdb->query( 'INSERT INTO `' . self::VENDOR . "` (post_id, shared_key) VALUES (104, NULL)" );
$wpdb->insert( self::VENDOR, array( 'post_id' => 202, 'shared_key' => 'vendor own value' ) );
$wpdb->insert( self::LISTING, array( 'post_id' => 101, 'shared_key' => 'backfill', 'price' => 5 ) );
$wpdb->insert( self::LISTING, array( 'post_id' => 103, 'shared_key' => 'keep' ) );
$wpdb->insert( self::LISTING, array( 'post_id' => 104, 'shared_key' => 'backfill-104' ) );
// Historical rows: older than any run, so the catch-up pass leaves them alone.
$wpdb->query( 'UPDATE `' . self::VENDOR . "` SET updated_at = '2020-01-01 00:00:00'" );
$wpdb->query( 'UPDATE `' . self::LISTING . "` SET updated_at = '2020-01-01 00:00:00'" );
$GLOBALS['_wp_options'] = array( WPDO_Entity_Registry::SCOPED_ROUTING_OPTION => '0' );
$GLOBALS['_wp_cache'] = array();
WPDO_Entity_Registry::init();
WPDO_Entity_Registry::register_adapter( 'post', new WPDO_Adapter_Post() );
WPDO_Entity_Registry::register_group( 'post', 'rc_listing', array( array( 'key' => 'shared_key', 'type' => 'text' ), array( 'key' => 'price', 'type' => 'decimal' ) ), array( 'post_types' => array( 'hp_listing' ) ) );
WPDO_Entity_Registry::register_group( 'post', 'rc_vendor', array( array( 'key' => 'shared_key', 'type' => 'text' ) ), array( 'post_types' => array( 'hp_vendor' ) ) );
}
protected function tearDown(): void {
self::drop();
WPDO_Entity_Registry::init();
}
private static function drop(): void {
global $wpdb;
foreach ( array( $wpdb->posts, self::LISTING, self::VENDOR ) as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
}
}
private function listing_value( int $post_id ): ?string {
global $wpdb;
return $wpdb->get_var( $wpdb->prepare( 'SELECT shared_key FROM `' . self::LISTING . '` WHERE post_id = %d', $post_id ) );
}
public function test_dry_run_reports_without_writing(): void {
$report = TMDO_Post_Group_Reconciler::run( true );
$this->assertSame( 2, $report['moves'][0]['copied'], '101 and 102 hold non-NULL legacy values' );
$this->assertSame( 1, $report['moves'][0]['overwritten'], '101 has a different listing value' );
$this->assertFalse( $report['enabled'] );
$this->assertSame( 'backfill', $this->listing_value( 101 ) );
$this->assertNull( $this->listing_value( 102 ) );
$this->assertSame( '0', get_option( WPDO_Entity_Registry::SCOPED_ROUTING_OPTION ) );
}
public function test_run_moves_the_values_reads_returned_before_the_switch(): void {
$report = TMDO_Post_Group_Reconciler::run( false, 1 );
$this->assertSame( 'live', $this->listing_value( 101 ) );
$this->assertSame( 'only-in-vendor', $this->listing_value( 102 ) );
$this->assertSame( 'keep', $this->listing_value( 103 ) );
$this->assertSame( 'backfill-104', $this->listing_value( 104 ), 'NULL legacy values do not erase the listing value' );
$this->assertNull( $this->listing_value( 202 ), 'vendor posts are not copied into the listing table' );
$this->assertSame( 2, $report['moves'][0]['copied'] );
}
public function test_other_columns_of_the_target_row_are_untouched(): void {
global $wpdb;
TMDO_Post_Group_Reconciler::run( false );
$this->assertSame( 5.0, (float) $wpdb->get_var( 'SELECT price FROM `' . self::LISTING . '` WHERE post_id = 101' ) );
}
public function test_completion_turns_scoped_routing_on_and_clears_the_cursor(): void {
$report = TMDO_Post_Group_Reconciler::run( false );
$this->assertTrue( $report['enabled'] );
$this->assertSame( '1', get_option( WPDO_Entity_Registry::SCOPED_ROUTING_OPTION ) );
$this->assertFalse( get_option( TMDO_Post_Group_Reconciler::CURSOR_OPTION ) );
}
public function test_catch_up_copies_rows_written_during_the_run(): void {
global $wpdb;
// Simulates a legacy-routed write that lands after the batch passed post 101.
$wpdb->query( 'UPDATE `' . self::VENDOR . "` SET shared_key = 'written during run', updated_at = NOW() + INTERVAL 1 HOUR WHERE post_id = 101" );
update_option( TMDO_Post_Group_Reconciler::CURSOR_OPTION, array( 'rc_vendor>rc_listing:shared_key' => 101 ) );
TMDO_Post_Group_Reconciler::run( false );
$this->assertSame( 'written during run', $this->listing_value( 101 ) );
}
public function test_resumes_after_the_stored_cursor(): void {
update_option( TMDO_Post_Group_Reconciler::CURSOR_OPTION, array( 'rc_vendor>rc_listing:shared_key' => 101 ) );
TMDO_Post_Group_Reconciler::run( false );
$this->assertSame( 'backfill', $this->listing_value( 101 ), 'rows up to the cursor were already done' );
$this->assertSame( 'only-in-vendor', $this->listing_value( 102 ) );
}
}
+1
View File
@@ -602,6 +602,7 @@ require_once TMDO_PATH . 'includes/migration/phases/class-tmdo-phase-cleanup.php
require_once TMDO_PATH . 'includes/migration/phases/class-tmdo-phase-completed.php';
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 . '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';
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Which values the reconciler has to move.
*
* Under legacy routing a key shared by several post groups was read and
* written through the last group registered. Once lookups follow the post
* type, each scoped group must first receive the values its posts stored in
* that legacy owner's table.
*/
class PostGroupReconcilerPlanTest extends TestCase {
protected function setUp(): void {
TMDO_Entity_Registry::init();
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
}
public function test_plan_lists_shared_keys_whose_legacy_owner_is_another_group(): void {
TMDO_Entity_Registry::register_group( 'post', 'listing', array( array( 'key' => 'shared', 'type' => 'text' ), array( 'key' => 'price', 'type' => 'decimal' ) ), array( 'post_types' => array( 'hp_listing' ) ) );
TMDO_Entity_Registry::register_group( 'post', 'vendor', array( array( 'key' => 'shared', 'type' => 'text' ) ), array( 'post_types' => array( 'hp_vendor' ) ) );
$this->assertSame(
array(
array(
'target' => 'listing',
'source' => 'vendor',
'key' => 'shared',
'post_types' => array( 'hp_listing' ),
),
),
TMDO_Post_Group_Reconciler::plan()
);
}
public function test_unscoped_groups_and_unshared_keys_need_no_move(): void {
TMDO_Entity_Registry::register_group( 'post', 'site', array( array( 'key' => '_edit_lock', 'type' => 'text' ) ) );
TMDO_Entity_Registry::register_group( 'post', 'listing', array( array( 'key' => 'price', 'type' => 'decimal' ) ), array( 'post_types' => array( 'hp_listing' ) ) );
$this->assertSame( array(), TMDO_Post_Group_Reconciler::plan() );
}
public function test_registry_exposes_group_post_types(): void {
TMDO_Entity_Registry::register_group( 'post', 'listing', array( array( 'key' => 'price', 'type' => 'decimal' ) ), array( 'post_types' => array( 'hp_listing' ) ) );
TMDO_Entity_Registry::register_group( 'post', 'site', array( array( 'key' => '_edit_lock', 'type' => 'text' ) ) );
$this->assertSame( array( 'hp_listing' ), TMDO_Entity_Registry::get_group_post_types( 'post', 'listing' ) );
$this->assertSame( array(), TMDO_Entity_Registry::get_group_post_types( 'post', 'site' ) );
TMDO_Entity_Registry::init();
$this->assertSame( array(), TMDO_Entity_Registry::get_group_post_types( 'post', 'listing' ) );
}
public function test_core_hivepress_groups_plan_the_eight_misrouted_keys(): void {
TMDO_Post_Fields::register_entity_fields();
$moves = array();
foreach ( TMDO_Post_Group_Reconciler::plan() as $m ) {
$moves[] = "{$m['source']}>{$m['target']}:{$m['key']}";
}
sort( $moves );
$this->assertSame(
array(
'hp_request_core>hp_listing_core:hp_expired_time',
'hp_request_core>hp_listing_core:hp_status',
'hp_request_core>hp_listing_core:hp_view_count',
'hp_vendor_core>hp_listing_core:hp_hourly_rate',
'hp_vendor_core>hp_listing_core:hp_rating',
'hp_vendor_core>hp_listing_core:hp_rating_count',
'hp_vendor_core>hp_listing_core:hp_verified',
'hp_vendor_core>hp_request_core:hp_user',
),
$moves
);
}
}