feat(2meet-data-optimizer): report flat-vs-native drift with wp tmdo drift-check
- New TMDO_Drift_Check compares every backfillable group of post / user / term / comment with native meta by value (first native row → safe unserialize → Type_Caster::to_db): miss / diff / extra per key, set-based SQL with BINARY exact matches, PHP comparison only for the remaining candidates. Serialized-vs-JSON and 50 vs 50.000000 are no longer drift; over-long text stored as NULL is not a miss; extra covers rows of other post types. Outcome per group goes to the wpdo_drift_status option; nothing else is written. - `wp tmdo drift-check <entity> [--group] [--sample=<n>] [--format]`. A new class instead of extending the post-only shadow verifier, which queries row by row and writes shadow_diffs. - Verify: DriftCheckTest; live suite drift; dev36 post miss=533 diff=2 extra=1 in 23 s (checker's 45 json false diffs gone), user/term/comment 0. - PR: PR-070
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* TMDO_Drift_Check (2.2 PR-070): read-only flat-vs-native comparison.
|
||||
*
|
||||
* Fixture (user entity, group dc_grp):
|
||||
* user 1 every key equal in value, differently formatted
|
||||
* (50 vs 50.000000, serialized array vs JSON, required flag holding
|
||||
* its default with no native row) → clean
|
||||
* user 2 name 'b' vs 'B', cnt 5 vs 6 → diff ×2
|
||||
* user 3 native name, flat row with name NULL → miss
|
||||
* user 4 no native rows, flat cnt 9 → extra
|
||||
* user 5 native name '', flat NULL → clean
|
||||
* user 6 native name longer than a text column, flat NULL → clean
|
||||
* user 7 native cnt, no flat row → miss
|
||||
*/
|
||||
class DriftCheckTest extends TestCase {
|
||||
|
||||
private const TABLE = 'wp_itest_wpdo_user_dc_grp';
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
foreach ( array(
|
||||
'includes/adapters/interface-entity-adapter.php',
|
||||
'includes/adapters/class-tmdo-adapter-user.php',
|
||||
'includes/diagnostic/class-tmdo-drift-check.php',
|
||||
) as $file ) {
|
||||
require_once WPDO_PLUGIN_DIR . $file;
|
||||
}
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
global $wpdb;
|
||||
self::drop();
|
||||
$wpdb->query( "CREATE TABLE `{$wpdb->users}` ( ID BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_login VARCHAR(60) NOT NULL DEFAULT '', PRIMARY KEY (ID) ) ENGINE=InnoDB" );
|
||||
$wpdb->query( "CREATE TABLE `{$wpdb->usermeta}` ( umeta_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, meta_key VARCHAR(255) DEFAULT NULL, meta_value LONGTEXT, PRIMARY KEY (umeta_id), KEY user_id (user_id) ) ENGINE=InnoDB" );
|
||||
$wpdb->query( 'CREATE TABLE `' . self::TABLE . '` ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, num DECIMAL(18,6) DEFAULT NULL, cnt BIGINT(20) DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, prefs TEXT DEFAULT NULL, meta_json LONGTEXT DEFAULT NULL, flag BIGINT(20) NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_entity (user_id) ) ENGINE=InnoDB' );
|
||||
|
||||
for ( $id = 1; $id <= 7; $id++ ) {
|
||||
$wpdb->insert( $wpdb->users, array( 'ID' => $id, 'user_login' => "u{$id}" ) );
|
||||
}
|
||||
$prefs = serialize( array( 'core' => array( 'welcomeGuide' => false ) ) );
|
||||
$this->native( 1, array( 'num' => '50', 'cnt' => '3', 'name' => 'a', 'prefs' => $prefs, 'meta_json' => serialize( array( 'w' => 640, 'tags' => array( 'x' ) ) ) ) );
|
||||
$this->flat( array( 'user_id' => 1, 'num' => '50.000000', 'cnt' => 3, 'name' => 'a', 'prefs' => $prefs, 'meta_json' => '{"w":640,"tags":["x"]}' ) );
|
||||
$this->native( 2, array( 'name' => 'b', 'cnt' => '5' ) );
|
||||
$this->flat( array( 'user_id' => 2, 'name' => 'B', 'cnt' => 6 ) );
|
||||
$this->native( 3, array( 'name' => 'c' ) );
|
||||
$this->flat( array( 'user_id' => 3, 'cnt' => null ) );
|
||||
$this->flat( array( 'user_id' => 4, 'cnt' => 9 ) );
|
||||
$this->native( 5, array( 'name' => '' ) );
|
||||
$this->flat( array( 'user_id' => 5 ) );
|
||||
$this->native( 6, array( 'name' => str_repeat( 'x', 300 ) ) );
|
||||
$this->flat( array( 'user_id' => 6 ) );
|
||||
$this->native( 7, array( 'cnt' => '7' ) );
|
||||
|
||||
$GLOBALS['_wp_options'] = array();
|
||||
$GLOBALS['_wp_cache'] = array();
|
||||
WPDO_Entity_Registry::init();
|
||||
WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() );
|
||||
WPDO_Entity_Registry::register_group(
|
||||
'user',
|
||||
'dc_grp',
|
||||
array(
|
||||
array( 'key' => 'num', 'type' => 'decimal' ),
|
||||
array( 'key' => 'cnt', 'type' => 'integer' ),
|
||||
array( 'key' => 'name', 'type' => 'text' ),
|
||||
array( 'key' => 'prefs', 'type' => 'textarea' ),
|
||||
array( 'key' => 'meta_json', 'type' => 'json' ),
|
||||
array( 'key' => 'flag', 'type' => 'integer', 'default' => 0, 'required' => true ),
|
||||
)
|
||||
);
|
||||
WPDO_Schema_Manager::flush_table_exists_cache();
|
||||
}
|
||||
|
||||
protected function tearDown(): void {
|
||||
self::drop();
|
||||
WPDO_Entity_Registry::init();
|
||||
}
|
||||
|
||||
private static function drop(): void {
|
||||
global $wpdb;
|
||||
foreach ( array( $wpdb->users, $wpdb->usermeta, self::TABLE ) as $table ) {
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
|
||||
}
|
||||
}
|
||||
|
||||
private function native( int $user_id, array $values ): void {
|
||||
global $wpdb;
|
||||
foreach ( $values as $key => $value ) {
|
||||
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => $user_id, 'meta_key' => $key, 'meta_value' => $value ) );
|
||||
}
|
||||
}
|
||||
|
||||
private function flat( array $row ): void {
|
||||
global $wpdb;
|
||||
$wpdb->insert( self::TABLE, $row );
|
||||
}
|
||||
|
||||
private function snapshot(): string {
|
||||
global $wpdb;
|
||||
return md5( wp_json_encode( array( $wpdb->get_results( "SELECT * FROM `{$wpdb->usermeta}` ORDER BY umeta_id", ARRAY_A ), $wpdb->get_results( 'SELECT * FROM `' . self::TABLE . '` ORDER BY id', ARRAY_A ) ) ) );
|
||||
}
|
||||
|
||||
public function test_counts_miss_diff_and_extra_per_key(): void {
|
||||
$report = TMDO_Drift_Check::run( 'user' );
|
||||
|
||||
$this->assertSame( array( 'miss' => 2, 'diff' => 2, 'extra' => 1 ), $report['total'] );
|
||||
$keys = $report['groups']['dc_grp']['keys'];
|
||||
$this->assertSame( array( 'miss' => 1, 'diff' => 1, 'extra' => 0 ), $keys['name'] );
|
||||
$this->assertSame( array( 'miss' => 1, 'diff' => 1, 'extra' => 1 ), $keys['cnt'] );
|
||||
foreach ( array( 'num', 'prefs', 'meta_json', 'flag' ) as $key ) {
|
||||
$this->assertSame( array( 'miss' => 0, 'diff' => 0, 'extra' => 0 ), $keys[ $key ], "{$key}: equal by value" );
|
||||
}
|
||||
}
|
||||
|
||||
public function test_records_the_outcome_per_group(): void {
|
||||
TMDO_Drift_Check::run( 'user', 'dc_grp' );
|
||||
|
||||
$status = get_option( TMDO_Drift_Check::OPTION )['user']['dc_grp'];
|
||||
$this->assertSame( 2, $status['miss'] );
|
||||
$this->assertSame( 2, $status['diff'] );
|
||||
$this->assertSame( 1, $status['extra'] );
|
||||
$this->assertSame( 0, $status['sample'] );
|
||||
$this->assertNotEmpty( $status['checked_at'] );
|
||||
}
|
||||
|
||||
public function test_a_sample_covering_every_user_matches_the_full_check(): void {
|
||||
$report = TMDO_Drift_Check::run( 'user', '', 100 );
|
||||
|
||||
$this->assertSame( array( 'miss' => 2, 'diff' => 2, 'extra' => 1 ), $report['total'] );
|
||||
$this->assertSame( 100, get_option( TMDO_Drift_Check::OPTION )['user']['dc_grp']['sample'] );
|
||||
}
|
||||
|
||||
public function test_never_writes_native_or_flat(): void {
|
||||
$before = $this->snapshot();
|
||||
|
||||
TMDO_Drift_Check::run( 'user' );
|
||||
|
||||
$this->assertSame( $before, $this->snapshot() );
|
||||
}
|
||||
|
||||
public function test_rejects_an_unknown_group(): void {
|
||||
$this->expectException( InvalidArgumentException::class );
|
||||
TMDO_Drift_Check::run( 'user', 'no_such_group' );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user