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', ) ); } }