Files
2meet-data-optimizer/includes/migration/phases/class-tmdo-phase-verify-sample.php
T
wpdev c46814ce83 refactor(migration): 回填 Migration Phase Strategy 體系(PR-D)
A v3.0.1 把 orchestrator 的 11 個 phase 抽成可注入的 Phase 物件,B 仍是
1107 行單體、以 'phase_' . $current 字串魔法分派、completed 甚至 inline
在 tick() 裡。本 commit 對齊:

新增 13 檔
- includes/migration/interface-migration-phase.php
- includes/migration/class-tmdo-migration-phase-base.php(log/get_managed_keys/
  execute_bulk_pivot/values_loose_equal 等共用 helper)
- includes/migration/phases/ 11 個 phase 類別

orchestrator 1107 → 640 行
- tick() 改 make_phase() 工廠 + $phase->execute($job)
- 移除 final、self::ENTITY_TYPE → static::(A v3.3.0 late static binding)
- 保留 B 原有的 '✓ %s (%.2fs)' 耗時 log(改為 tick 自行量測,A 版已簡化掉)
- 公開介面(preflight/start/tick/get_status/cancel/resume/needs_attention/
  cron_tick)經 diff 確認與 A 完全一致,呼叫端零影響

連帶
- Schema_Manager 補 table_exists 的 request-scoped cache 與
  flush_table_exists_cache()(A v3.1.6 + v3.4.6),Phase 測試需要它
- back-compat 補 12 個 Phase 類別的 WPDO_ alias
- 移植 MigrationPhaseTest + MigrationPhaseRemainingTest(527 行)

測試隔離差異(B 的 unit bootstrap 會載入 Member_Fields / Post_Fields,
A 的不會):MigrationPhaseRemainingTest 的 setUp 需額外清空 Entity_Registry
與 wpdo_register_entity_fields listener,否則 install_schema 會真的走進
dbDelta。MigrationPhaseTest 的 interface 斷言改用 TMDO_ 正式名稱
(PHP 無法 class_alias 介面,且該契約是核心內部擴充點)。

unit 451 / integration 398 GREEN

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 05:54:49 +08:00

141 lines
3.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Migration phase: sample-verify flat table vs EAV for data integrity.
*
* @package TMDO
* @since 3.0.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Samples a subset of entities and compares flat vs EAV values.
* Aborts with RuntimeException if any divergences are found.
*/
class TMDO_Phase_Verify_Sample extends TMDO_Migration_Phase_Base {
/**
* Minimum sample size regardless of total entity count.
*
* @var int
*/
const VERIFY_SAMPLE_MIN = 500;
/**
* Fraction of total entities to sample in strict mode.
*
* @var float
*/
const VERIFY_SAMPLE_RATIO = 0.10;
/**
* Returns the phase slug.
*
* @return string
*/
public function name(): string {
return 'verify_sample';
}
/**
* Executes the phase.
*
* @param array $job Job array (by reference).
* @return array{status: string}
* @throws \RuntimeException If verification divergences are found.
*/
public function execute( array &$job ): array {
global $wpdb;
if ( ! empty( $job['options']['verify_24h'] ) ) {
$elapsed = time() - ( $job['phase_started_at'] ?? time() );
if ( ! isset( $job['phase_started_at'] ) ) {
$job['phase_started_at'] = time();
$this->log( $job, '⏸ 24h shadow_read window started — wizard will resume after window elapses.' );
return array( 'status' => 'in_progress' );
}
if ( $elapsed < DAY_IN_SECONDS ) {
return array( 'status' => 'in_progress' );
}
$this->log( $job, '⏳ 24h shadow_read window complete; running sample compare' );
}
$strict = ! empty( $job['options']['verify_strict'] );
$users_total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->users}" );
$sample_size = $strict
? (int) max( self::VERIFY_SAMPLE_MIN, ceil( $users_total * self::VERIFY_SAMPLE_RATIO ) )
: 100;
$sample_size = min( $sample_size, $users_total );
$sample_ids = $wpdb->get_col(
$wpdb->prepare( "SELECT ID FROM {$wpdb->users} ORDER BY RAND() LIMIT %d", $sample_size )
);
$diffs = 0;
$compared = 0;
$managed_keys = $this->get_managed_keys();
foreach ( $sample_ids as $uid ) {
foreach ( $managed_keys as $key ) {
$eav = $wpdb->get_var(
$wpdb->prepare(
"SELECT meta_value FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key = %s LIMIT 1", // phpcs:ignore WPDO.AntiEAV.no-direct-usermeta-select -- verify phase: must compare EAV source against flat table to confirm migration correctness
$uid,
$key
)
);
// Skip keys with no EAV source-of-truth — only flag when EAV has data
// but flat doesn't match (real backfill error).
if ( null === $eav || '' === $eav ) {
continue;
}
++$compared;
$flat = TMDO_Hook_Bus::direct_read( $this->entity_type, (int) $uid, $key );
if ( ! $this->values_loose_equal( $flat, $eav ) ) {
++$diffs;
if ( $diffs <= 3 ) {
$this->log(
$job,
sprintf(
' ! diff uid=%d key=%s flat=%s eav=%s',
$uid,
$key,
is_scalar( $flat ) ? (string) $flat : gettype( $flat ),
is_scalar( $eav ) ? (string) $eav : gettype( $eav )
)
);
}
}
}
}
$this->log(
$job,
sprintf(
'Verify: sampled %d users × %d keys = %d compares, %d diff(s)',
count( $sample_ids ),
count( $managed_keys ),
$compared,
$diffs
)
);
if ( $diffs > 0 ) {
throw new \RuntimeException(
sprintf(
'Verification found %d divergence(s) across %d compares — aborting before destructive cleanup.',
$diffs,
$compared
)
);
}
return array( 'status' => 'ok' );
}
}