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' ); } }