fix(2meet-data-optimizer): restore native meta without overwriting newer values or inventing defaults

- TMDO_Native_Restorer::run() gains a policy: overwrite (flat wins; the
  default in aeav_only, where writes only reached flat and native is
  stale) or only_missing (native wins, flat only fills gaps; the default in
  every other mode, where native is the source of truth and may be newer —
  on dev36 an aeav_only round trip overwrote WooCommerce stock 42 with 0).
  CLI: --overwrite / --only-missing.
- Native values are read first and each owned key is classified as
  missing / differs / same / default_skipped; unchanged values are no
  longer rewritten. A flat value equal to its field default is not
  written for an entity without a native row (dev36: 1,368 invented
  session_count / account_flags rows). --dry-run reports the counts.
- Verify: NativeRestorer* 17 OK (5 new: policy by mode, forced
  overwrite, only-missing in aeav_only, dry-run counts, no invented
  defaults); unit 687 OK, integration 490 OK, phpcs (phpcs.xml) 0,
  stan OK; dev36 `bridge-restore-native user --dry-run`: policy
  only_missing, activity 120 defaults skipped
- PR: PR-058
This commit is contained in:
2026-09-26 19:22:34 +08:00
parent 77e338f9c3
commit 8c049f0cf2
3 changed files with 278 additions and 28 deletions
+73
View File
@@ -139,4 +139,77 @@ class NativeRestorerTest extends TestCase {
$this->assertSame( array( '9' ), $this->native( 1, 'level' ) );
}
private function set_user_mode( string $mode ): void {
$GLOBALS['_wp_options']['wpdo_bridge_modes'] = array( 'user' => $mode );
TMDO_Mode_Manager::reset_cache();
}
/**
* Native is the source of truth outside aeav_only (e.g. WooCommerce writes
* stock with direct SQL): the default policy there only fills gaps.
*/
public function test_native_authoritative_modes_only_fill_missing_rows(): void {
$this->set_user_mode( 'dual_write' );
$report = TMDO_Native_Restorer::run( 'user', false );
$this->assertSame( array( '1' ), $this->native( 1, 'level' ), 'differing native value kept' );
$this->assertSame( array( serialize( array( 'a', 'b' ) ) ), $this->native( 1, 'tags' ), 'missing native row filled' );
$this->assertSame( array( '5' ), $this->native( 2, 'level' ) );
$this->assertSame( array( 'stale' ), $this->native( 2, 'tags' ), 'NULL flat value does not delete native here' );
$this->assertSame( 'only_missing', $report['policy'] );
$this->assertSame( 2, $report['groups']['rn_profile']['missing'] );
$this->assertSame( 2, $report['groups']['rn_profile']['differs'] );
}
public function test_overwrite_policy_replaces_native_in_any_mode(): void {
$this->set_user_mode( 'dual_write' );
TMDO_Native_Restorer::run( 'user', false, 500, false, '', 'overwrite' );
$this->assertSame( array( '3' ), $this->native( 1, 'level' ) );
$this->assertSame( array(), $this->native( 2, 'tags' ) );
}
public function test_only_missing_policy_keeps_native_in_aeav_only(): void {
TMDO_Native_Restorer::run( 'user', false, 500, false, '', 'only_missing' );
$this->assertSame( array( '1' ), $this->native( 1, 'level' ) );
$this->assertSame( array( serialize( array( 'a', 'b' ) ) ), $this->native( 1, 'tags' ) );
}
public function test_dry_run_classifies_without_writing(): void {
$report = TMDO_Native_Restorer::run( 'user', true );
$this->assertSame( 'overwrite', $report['policy'], 'aeav_only: flat is the source of truth' );
$this->assertSame( 2, $report['groups']['rn_profile']['missing'] );
$this->assertSame( 2, $report['groups']['rn_profile']['differs'] );
$this->assertSame( 0, $report['groups']['rn_profile']['same'] );
$this->assertSame( array( '1' ), $this->native( 1, 'level' ) );
}
/**
* A flat column holding its field default for an entity that never had
* the key (e.g. activity.session_count = 0) is not written back.
*/
public function test_defaults_are_not_invented_as_native_rows(): void {
global $wpdb;
$table = 'wp_itest_wpdo_user_rn_counts';
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
$wpdb->query( "CREATE TABLE `{$table}` ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, visits BIGINT(20) DEFAULT 0, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_entity (user_id) ) ENGINE=InnoDB" );
$wpdb->query( "INSERT INTO `{$table}` (user_id, visits, updated_at) VALUES (1, 0, '2020-01-01'), (2, 7, '2020-01-01')" );
WPDO_Entity_Registry::register_group( 'user', 'rn_counts', array( array( 'key' => 'visits', 'type' => 'integer', 'default' => 0 ) ) );
WPDO_Schema_Manager::flush_table_exists_cache();
try {
$report = TMDO_Native_Restorer::run( 'user', false, 500, false, 'rn_counts' );
$this->assertSame( array(), $this->native( 1, 'visits' ), 'default 0 not invented' );
$this->assertSame( array( '7' ), $this->native( 2, 'visits' ) );
$this->assertSame( 1, $report['groups']['rn_counts']['default_skipped'] );
} finally {
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
}
}
}