Files
2meet-data-optimizer/includes/migration/phases/class-tmdo-phase-verify-sample.php
T
wpdev 76c01e44df refactor: 全部 128 個生產檔加入 declare(strict_types=1)(PR-H)
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入
並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。

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

143 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
*/
declare(strict_types=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' );
}
}