1653011f4f
- On init:1, re-register aeav_only groups whose flat table exists and whose definition is in wpdo_registry_meta but no active plugin registers them, so their data stays readable (routing only, never DDL) - Keys owned by a live group, or with rows in native meta (newer data written while orphaned), are left alone; the native-meta check is cached for a day - Surface restored groups via Site Health (wpdo_orphaned_entity_groups), the admin Doctor tab, and wp wpdo doctor - Verify: EntityRegistryRestoreTest (9), OrphanGroupRestoreTest (7); core unit 622 / integration 439, HP 141/49, WC 10/22 OK; dry-run on dev20–22 restores 16/17/4 groups, keeps hp_taxonomy hp_sort_order/hp_icon on native - PR: PR-018
193 lines
7.4 KiB
PHP
193 lines
7.4 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Orphaned flat-table groups stay readable.
|
|
*
|
|
* Scenario: a plugin registered an entity group, its flat table was created
|
|
* (definition persisted in wpdo_registry_meta), data landed there, and then
|
|
* the plugin went away. In aeav_only mode the values exist only in the flat
|
|
* table, so without the group registration reads come back empty.
|
|
*/
|
|
class OrphanGroupRestoreTest extends TestCase {
|
|
|
|
private const GROUP = 'orphan_itest';
|
|
private const TABLE = 'wp_itest_wpdo_user_orphan_itest';
|
|
|
|
public static function setUpBeforeClass(): void {
|
|
$base = WPDO_PLUGIN_DIR;
|
|
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-mode-manager.php',
|
|
'includes/engine/class-tmdo-cache-orchestrator.php',
|
|
'includes/adapters/class-tmdo-adapter-user.php',
|
|
'includes/engine/class-tmdo-hook-bus.php',
|
|
) as $file ) {
|
|
require_once $base . $file;
|
|
}
|
|
WPDO_Installer::install_v2_tables();
|
|
|
|
global $wpdb;
|
|
$wpdb->query(
|
|
"CREATE TABLE IF NOT EXISTS `{$wpdb->usermeta}` (
|
|
umeta_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
user_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
|
meta_key VARCHAR(255) DEFAULT NULL,
|
|
meta_value LONGTEXT,
|
|
PRIMARY KEY (umeta_id),
|
|
KEY meta_key (meta_key(191))
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
|
);
|
|
}
|
|
|
|
protected function setUp(): void {
|
|
global $wpdb;
|
|
self::drop_fixture();
|
|
$GLOBALS['_wp_options'] = array();
|
|
$GLOBALS['_wp_cache'] = array();
|
|
$GLOBALS['_wp_transients'] = array();
|
|
self::reset_statics();
|
|
|
|
// A provider registered the group earlier: its flat table exists and the
|
|
// normalized definition was persisted to registry_meta (what
|
|
// Schema_Manager::create_or_upgrade_table() leaves behind — built by hand
|
|
// here because that method needs the real wp-admin upgrade.php).
|
|
WPDO_Entity_Registry::init();
|
|
WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() );
|
|
WPDO_Entity_Registry::register_group( 'user', self::GROUP, array( array( 'key' => 'orphan_note', 'type' => 'text' ) ) );
|
|
$wpdb->query(
|
|
'CREATE TABLE `' . self::TABLE . '` (
|
|
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
|
|
`user_id` BIGINT(20) NOT NULL,
|
|
`orphan_note` VARCHAR(255) DEFAULT NULL,
|
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (`id`),
|
|
UNIQUE KEY `uk_entity` (`user_id`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
|
);
|
|
$wpdb->insert(
|
|
$wpdb->prefix . 'wpdo_registry_meta',
|
|
array(
|
|
'entity_type' => 'user',
|
|
'group_name' => self::GROUP,
|
|
'schema_hash' => 'itest',
|
|
'field_definitions' => wp_json_encode( WPDO_Entity_Registry::get_group_fields( 'user', self::GROUP ) ),
|
|
)
|
|
);
|
|
$wpdb->insert( self::TABLE, array( 'user_id' => 77, 'orphan_note' => 'kept' ) );
|
|
|
|
// The provider goes away: next request starts with an empty registry.
|
|
WPDO_Entity_Registry::init();
|
|
WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() );
|
|
self::reset_statics();
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
self::drop_fixture();
|
|
WPDO_Entity_Registry::init();
|
|
self::set_modes( null );
|
|
}
|
|
|
|
public function test_orphaned_group_is_unreadable_before_restore(): void {
|
|
self::set_modes( 'aeav_only' );
|
|
|
|
$this->assertNull( WPDO_Hook_Bus::intercept_get( null, 77, 'orphan_note', true, 'user' ) );
|
|
}
|
|
|
|
public function test_restore_makes_aeav_only_data_readable_again(): void {
|
|
self::set_modes( 'aeav_only' );
|
|
|
|
$this->assertSame( 1, WPDO_Schema_Manager::restore_orphaned_groups() );
|
|
|
|
$this->assertSame( array( 'kept' ), WPDO_Hook_Bus::intercept_get( null, 77, 'orphan_note', true, 'user' ) );
|
|
$this->assertSame( array(), WPDO_Entity_Registry::get_pending_schemas(), 'Restoring must not queue DDL.' );
|
|
}
|
|
|
|
public function test_restore_only_applies_to_aeav_only(): void {
|
|
// dual_write / shadow_read still write native meta, so an orphaned key
|
|
// keeps reading correctly from there — nothing to restore.
|
|
foreach ( array( 'disabled', 'dual_write', 'shadow_read' ) as $mode ) {
|
|
self::set_modes( $mode );
|
|
$this->assertSame( 0, WPDO_Schema_Manager::restore_orphaned_groups(), $mode );
|
|
$this->assertNull( WPDO_Entity_Registry::get_field( 'user', 'orphan_note' ), $mode );
|
|
}
|
|
}
|
|
|
|
public function test_key_with_native_meta_rows_stays_on_native(): void {
|
|
global $wpdb;
|
|
self::set_modes( 'aeav_only' );
|
|
// Written to native meta while the group was unregistered — newer than the flat copy.
|
|
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => 5, 'meta_key' => 'orphan_note', 'meta_value' => 'native' ) );
|
|
|
|
$this->assertSame( 0, WPDO_Schema_Manager::restore_orphaned_groups() );
|
|
|
|
$this->assertNull( WPDO_Hook_Bus::intercept_get( null, 5, 'orphan_note', true, 'user' ), 'Native meta must keep serving this key.' );
|
|
$this->assertSame( array( 'orphan_note' ), WPDO_Entity_Registry::get_restored_groups()[0]['native'] );
|
|
}
|
|
|
|
public function test_native_meta_check_is_cached(): void {
|
|
self::set_modes( 'aeav_only' );
|
|
WPDO_Schema_Manager::restore_orphaned_groups();
|
|
|
|
$this->assertSame( array( 'user:orphan_note' => false ), get_transient( 'tmdo_orphan_native_keys' ) );
|
|
}
|
|
|
|
public function test_restore_skips_groups_whose_table_is_gone(): void {
|
|
global $wpdb;
|
|
self::set_modes( 'aeav_only' );
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE . '`' );
|
|
self::reset_statics();
|
|
|
|
$this->assertSame( 0, WPDO_Schema_Manager::restore_orphaned_groups() );
|
|
}
|
|
|
|
public function test_registered_group_is_left_alone(): void {
|
|
self::set_modes( 'aeav_only' );
|
|
WPDO_Entity_Registry::register_group( 'user', self::GROUP, array( array( 'key' => 'orphan_note', 'type' => 'text' ) ) );
|
|
|
|
$this->assertSame( 0, WPDO_Schema_Manager::restore_orphaned_groups() );
|
|
$this->assertSame( array(), WPDO_Entity_Registry::get_restored_groups() );
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
|
|
private static function drop_fixture(): void {
|
|
global $wpdb;
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE . '`' );
|
|
$wpdb->delete( $wpdb->prefix . 'wpdo_registry_meta', array( 'entity_type' => 'user', 'group_name' => self::GROUP ) );
|
|
$wpdb->delete( $wpdb->usermeta, array( 'meta_key' => 'orphan_note' ) );
|
|
}
|
|
|
|
/** Clear Schema_Manager's table_exists cache and the Cache_Orchestrator L1. */
|
|
private static function reset_statics(): void {
|
|
$ref = new ReflectionProperty( WPDO_Schema_Manager::class, 'table_exists_cache' );
|
|
$ref->setAccessible( true );
|
|
$ref->setValue( null, array() );
|
|
|
|
$l1 = new ReflectionProperty( WPDO_Cache_Orchestrator::class, 'l1_cache' );
|
|
$l1->setAccessible( true );
|
|
$l1->setValue( null, array() );
|
|
}
|
|
|
|
/** Inject the user mode straight into Mode_Manager's cache (null = reset). */
|
|
private static function set_modes( ?string $user_mode ): void {
|
|
$ref = new ReflectionProperty( WPDO_Mode_Manager::class, 'cache' );
|
|
$ref->setAccessible( true );
|
|
$ref->setValue(
|
|
null,
|
|
null === $user_mode ? null : array(
|
|
'user' => $user_mode,
|
|
'post' => 'disabled',
|
|
'term' => 'disabled',
|
|
'comment' => 'disabled',
|
|
)
|
|
);
|
|
}
|
|
}
|