Files
2meet-data-optimizer/tests/integration/EntityMigrationEngineDefaultsTest.php
T
wpdev 13ea341ef8 fix(2meet-data-optimizer): do not invent default rows when migrating an entity group
- migrate_group() REPLACEs the whole flat row; columns without a native row fell back to their DDL default (session_count / account_flags DEFAULT 0), so every backfilled user got invented zeros. Keys with no native row are now written as NULL; required NOT NULL columns keep their default.
- Covers the cron batch path (migrate_group_batch) and clears stale flat values on re-run.
- Verify: EntityMigrationEngineDefaultsTest (3 RED on main); dev36 user/activity backfill: checker extra 123 → 0 for session_count and account_flags, miss/diff unchanged.
- PR: PR-066
2026-09-26 22:46:01 +08:00

140 lines
5.7 KiB
PHP

<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* TMDO_Entity_Migration_Engine::migrate_group() must not invent values for keys
* the object has no native row for.
*
* Bug (dev36 R-2.1, 2026-09-26): the engine REPLACEs only the columns it found
* native rows for, so every other column fell back to its DDL default —
* user activity.session_count / account_flags (nullable, DEFAULT 0) became 0
* for every backfilled user, and the checker's "extra" count went 60 → 123.
*
* Fixture (user entity, group md_act):
* login_count integer, default 0, nullable
* session_count integer, default 0, nullable
* note text
* req_flag integer, required (NOT NULL DEFAULT 0)
*/
class EntityMigrationEngineDefaultsTest extends TestCase {
private const TABLE = 'wp_itest_wpdo_user_md_act';
private const STATUS = 'wp_itest_wpdo_migration_status';
public static function setUpBeforeClass(): void {
foreach ( array(
'includes/adapters/interface-entity-adapter.php',
'includes/engine/class-tmdo-type-caster.php',
'includes/engine/class-tmdo-schema-manager.php',
'includes/engine/class-tmdo-entity-registry.php',
'includes/engine/class-tmdo-entity-migration-engine.php',
'includes/adapters/class-tmdo-adapter-user.php',
) as $file ) {
require_once WPDO_PLUGIN_DIR . $file;
}
}
protected function setUp(): void {
global $wpdb;
self::drop();
$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, login_count BIGINT(20) DEFAULT 0, session_count BIGINT(20) DEFAULT 0, note VARCHAR(255) DEFAULT NULL, req_flag BIGINT(20) NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_entity (user_id) ) ENGINE=InnoDB' );
$wpdb->query( 'CREATE TABLE `' . self::STATUS . "` ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, entity_type VARCHAR(50) NOT NULL, group_name VARCHAR(50) NOT NULL, last_id BIGINT UNSIGNED NOT NULL DEFAULT 0, total_migrated BIGINT UNSIGNED NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'pending', started_at DATETIME DEFAULT NULL, completed_at DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY entity_group (entity_type, group_name) ) ENGINE=InnoDB" );
$GLOBALS['_wp_cache'] = array();
WPDO_Entity_Registry::init();
WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() );
WPDO_Entity_Registry::register_group(
'user',
'md_act',
array(
array( 'key' => 'login_count', 'type' => 'integer', 'default' => 0 ),
array( 'key' => 'session_count', 'type' => 'integer', 'default' => 0 ),
array( 'key' => 'note', 'type' => 'text' ),
array( 'key' => 'req_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->usermeta, self::TABLE, self::STATUS ) as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
}
}
private function native( int $user_id, string $key, string $value ): void {
global $wpdb;
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => $user_id, 'meta_key' => $key, 'meta_value' => $value ) );
}
/** @return array<string, string|null> */
private function flat_row( int $user_id ): array {
global $wpdb;
return (array) $wpdb->get_row( $wpdb->prepare( 'SELECT login_count, session_count, note, req_flag FROM `' . self::TABLE . '` WHERE user_id = %d', $user_id ), ARRAY_A );
}
private function migrate(): array {
return TMDO_Entity_Migration_Engine::migrate_group( 'user', 'md_act', array( 'resume' => false, 'sleep_ms' => 0 ) );
}
public function test_keys_without_a_native_row_stay_null_instead_of_the_column_default(): void {
$this->native( 1, 'login_count', '7' );
$report = $this->migrate();
$this->assertSame( 1, $report['migrated'] );
$row = $this->flat_row( 1 );
$this->assertSame( '7', $row['login_count'] );
$this->assertNull( $row['session_count'], 'no native session_count — must not become the DEFAULT 0' );
$this->assertNull( $row['note'] );
}
public function test_a_native_value_equal_to_the_default_is_still_written(): void {
$this->native( 1, 'session_count', '0' );
$this->migrate();
$this->assertSame( '0', $this->flat_row( 1 )['session_count'] );
}
public function test_a_stale_flat_value_is_cleared_when_the_native_row_is_gone(): void {
global $wpdb;
$wpdb->insert( self::TABLE, array( 'user_id' => 1, 'login_count' => 3, 'session_count' => 5, 'note' => 'old' ) );
$this->native( 1, 'login_count', '4' );
$this->migrate();
$row = $this->flat_row( 1 );
$this->assertSame( '4', $row['login_count'] );
$this->assertNull( $row['session_count'] );
$this->assertNull( $row['note'] );
}
public function test_a_required_column_without_a_native_row_keeps_its_default(): void {
$this->native( 1, 'login_count', '1' );
$report = $this->migrate();
$this->assertSame( 0, $report['errors'] );
$this->assertSame( '0', $this->flat_row( 1 )['req_flag'], 'NOT NULL column cannot hold NULL — its default is the documented stand-in' );
}
public function test_the_cron_batch_path_does_not_invent_defaults_either(): void {
$this->native( 1, 'login_count', '2' );
$result = TMDO_Entity_Migration_Engine::migrate_group_batch( 'user', 'md_act' );
$this->assertSame( 1, $result['migrated'] );
$this->assertNull( $this->flat_row( 1 )['session_count'] );
}
}