fix(2meet-data-optimizer): serialize arrays in text fields instead of storing "Array"
- TMDO_Type_Caster: text / textarea values that are arrays or objects are stored with maybe_serialize() (was (string) → the literal "Array", which bridge-restore-native then wrote back over native meta); from_db() unserializes them with TMDO_Safe_Unserialize. text columns are VARCHAR(255): longer values are stored NULL (native keeps them) and logged as text_overflow. - Add `wp tmdo repair-array-literals [--entity] [--confirm]` (TMDO_Array_Literal_Repair): rewrites flat "Array" values from native meta and lists native rows that are "Array" themselves (backup needed). - Verify: TypeCasterSerializeTest 13 OK, ArrayLiteralRepairTest 3 OK; unit 685 OK, integration 483 OK, phpcs (phpcs.xml) 0, stan OK; live `serialized` 3 PASS; dev36 repair dry-run lists the value corrupted by the 2026-09-26 round trip (user #1 community-events-location) - PR: PR-055
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Repair of text / textarea flat columns holding the literal "Array" (stored
|
||||
* by the pre-1.1.0 type caster when the meta value was an array).
|
||||
*
|
||||
* Fixture (user entity, group al_prefs):
|
||||
* user 1 prefs 'Array' native prefs serialized array → repairable
|
||||
* user 2 loc 'Array' native loc 'Array' too → unrepairable, native polluted
|
||||
* user 3 name 'Array' no native row → unrepairable
|
||||
* user 4 prefs serialized array, loc 'Taipei' → untouched
|
||||
*/
|
||||
class ArrayLiteralRepairTest extends TestCase {
|
||||
|
||||
private const TABLE = 'wp_itest_wpdo_user_al_prefs';
|
||||
|
||||
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-cache-orchestrator.php',
|
||||
'includes/adapters/class-tmdo-adapter-user.php',
|
||||
'includes/migration/class-tmdo-array-literal-repair.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, prefs TEXT DEFAULT NULL, loc VARCHAR(255) DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_entity (user_id) ) ENGINE=InnoDB' );
|
||||
|
||||
$prefs = serialize( array( 'core' => array( 'welcomeGuide' => false ) ) );
|
||||
$wpdb->insert( self::TABLE, array( 'user_id' => 1, 'prefs' => 'Array' ) );
|
||||
$wpdb->insert( self::TABLE, array( 'user_id' => 2, 'loc' => 'Array' ) );
|
||||
$wpdb->insert( self::TABLE, array( 'user_id' => 3, 'name' => 'Array' ) );
|
||||
$wpdb->insert( self::TABLE, array( 'user_id' => 4, 'prefs' => $prefs, 'loc' => 'Taipei' ) );
|
||||
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => 1, 'meta_key' => 'prefs', 'meta_value' => $prefs ) );
|
||||
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => 2, 'meta_key' => 'loc', 'meta_value' => 'Array' ) );
|
||||
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => 4, 'meta_key' => 'prefs', 'meta_value' => $prefs ) );
|
||||
|
||||
$GLOBALS['_wp_cache'] = array();
|
||||
WPDO_Entity_Registry::init();
|
||||
WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() );
|
||||
WPDO_Entity_Registry::register_group(
|
||||
'user',
|
||||
'al_prefs',
|
||||
array(
|
||||
array( 'key' => 'prefs', 'type' => 'textarea' ),
|
||||
array( 'key' => 'loc', 'type' => 'text' ),
|
||||
array( 'key' => 'name', 'type' => 'text' ),
|
||||
)
|
||||
);
|
||||
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 ) as $table ) {
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
|
||||
}
|
||||
}
|
||||
|
||||
private function flat( int $user_id, string $col ): ?string {
|
||||
global $wpdb;
|
||||
return $wpdb->get_var( $wpdb->prepare( "SELECT `{$col}` FROM `" . self::TABLE . '` WHERE user_id = %d', $user_id ) );
|
||||
}
|
||||
|
||||
public function test_dry_run_reports_without_writing(): void {
|
||||
$report = TMDO_Array_Literal_Repair::run( 'user', false );
|
||||
|
||||
$this->assertSame( 3, $report['found'] );
|
||||
$this->assertSame( 1, $report['repairable'] );
|
||||
$this->assertSame( 0, $report['repaired'] );
|
||||
$this->assertSame( 'Array', $this->flat( 1, 'prefs' ) );
|
||||
}
|
||||
|
||||
public function test_apply_repairs_from_native_and_lists_the_rest(): void {
|
||||
$report = TMDO_Array_Literal_Repair::run( 'user', true );
|
||||
|
||||
$this->assertSame( 1, $report['repaired'] );
|
||||
$this->assertSame( serialize( array( 'core' => array( 'welcomeGuide' => false ) ) ), $this->flat( 1, 'prefs' ) );
|
||||
$this->assertSame( 'Array', $this->flat( 2, 'loc' ), 'native is "Array" too — nothing to repair from' );
|
||||
$this->assertSame( 'Array', $this->flat( 3, 'name' ), 'no native row' );
|
||||
$this->assertSame( 'Taipei', $this->flat( 4, 'loc' ) );
|
||||
|
||||
$unrepairable = array_map(
|
||||
static function ( $r ) {
|
||||
return $r['id'] . ':' . $r['key'] . ':' . $r['reason'];
|
||||
},
|
||||
$report['unrepairable']
|
||||
);
|
||||
sort( $unrepairable );
|
||||
$this->assertSame( array( '2:loc:native_is_array_literal', '3:name:no_native_row' ), $unrepairable );
|
||||
|
||||
$this->assertSame( array( array( 'group' => 'al_prefs', 'key' => 'loc', 'id' => 2 ) ), $report['native_polluted'] );
|
||||
}
|
||||
|
||||
public function test_second_run_finds_only_the_unrepairable_rows(): void {
|
||||
TMDO_Array_Literal_Repair::run( 'user', true );
|
||||
|
||||
$report = TMDO_Array_Literal_Repair::run( 'user', true );
|
||||
|
||||
$this->assertSame( 2, $report['found'] );
|
||||
$this->assertSame( 0, $report['repaired'] );
|
||||
}
|
||||
}
|
||||
@@ -611,6 +611,7 @@ require_once TMDO_PATH . 'includes/migration/class-tmdo-migration-orchestrator.p
|
||||
require_once TMDO_PATH . 'includes/migration/class-tmdo-post-migration.php';
|
||||
require_once TMDO_PATH . 'includes/migration/class-tmdo-post-group-reconciler.php';
|
||||
require_once TMDO_PATH . 'includes/migration/class-tmdo-native-restorer.php';
|
||||
require_once TMDO_PATH . 'includes/migration/class-tmdo-array-literal-repair.php';
|
||||
require_once TMDO_PATH . 'modules/options/class-tmdo-options-manager.php';
|
||||
require_once TMDO_PATH . 'includes/class-tmdo-postmeta-cleaner.php';
|
||||
require_once TMDO_PATH . 'includes/class-tmdo-termmeta-cleaner.php';
|
||||
|
||||
Reference in New Issue
Block a user