query( 'DROP TABLE IF EXISTS `' . self::TABLE . '`' ); $wpdb->query( 'CREATE TABLE `' . self::TABLE . '` ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, sd_a VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_entity (user_id) ) ENGINE=InnoDB' ); $wpdb->delete( $wpdb->prefix . 'wpdo_registry_meta', array( 'entity_type' => 'user', 'group_name' => 'sd_core' ) ); $wpdb->insert( $wpdb->prefix . 'wpdo_registry_meta', array( 'entity_type' => 'user', 'group_name' => 'sd_core', 'schema_hash' => 'old', 'field_definitions' => '[]' ) ); $GLOBALS['_wp_options'] = array(); $GLOBALS['_wp_scheduled_single'] = array(); $GLOBALS['_wp_filter_callbacks']['tmdo_schema_ddl_allowed'] = array(); // A web request. $ref = new ReflectionProperty( WPDO_Schema_Manager::class, 'table_exists_cache' ); $ref->setAccessible( true ); $ref->setValue( null, array() ); self::set_user_mode( 'dual_write' ); WPDO_Entity_Registry::init(); WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() ); WPDO_Entity_Registry::register_group( 'user', 'sd_core', array( array( 'key' => 'sd_a', 'type' => 'text' ), array( 'key' => 'sd_b', 'type' => 'integer' ) ) ); } protected function tearDown(): void { global $wpdb; $wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE . '`' ); $wpdb->delete( $wpdb->prefix . 'wpdo_registry_meta', array( 'entity_type' => 'user', 'group_name' => 'sd_core' ) ); WPDO_Entity_Registry::init(); self::set_user_mode( null ); unset( $GLOBALS['_wp_scheduled_single'] ); } private static function set_user_mode( ?string $mode ): void { $ref = new ReflectionProperty( WPDO_Mode_Manager::class, 'cache' ); $ref->setAccessible( true ); $ref->setValue( null, null === $mode ? null : array( 'post' => 'disabled', 'user' => $mode, 'term' => 'disabled', 'comment' => 'disabled' ) ); } private function upgrade(): bool { return TMDO_Schema_Manager::create_or_upgrade_table( 'user', 'sd_core', WPDO_Entity_Registry::get_group_fields( 'user', 'sd_core' ) ); } public function test_a_web_request_leaves_the_upgrade_to_wp_cron_and_the_group_native(): void { global $wpdb; $this->assertFalse( $this->upgrade() ); $this->assertTrue( TMDO_Schema_Manager::is_deferred( 'user', 'sd_core' ) ); $this->assertContains( TMDO_Schema_Manager::INDEXES_HOOK, $GLOBALS['_wp_scheduled_single'] ); $this->assertSame( array( 'id', 'user_id', 'sd_a' ), $wpdb->get_col( 'SHOW COLUMNS FROM `' . self::TABLE . '`' ), 'no ALTER' ); $this->assertSame( 'old', TMDO_Schema_Manager::get_stored_schema_hash( 'user', 'sd_core' ) ); } public function test_only_existing_tables_outside_aeav_only_and_outside_cli_or_cron_wait(): void { $this->assertTrue( TMDO_Schema_Manager::upgrade_waits( 'user', self::TABLE ) ); $this->assertFalse( TMDO_Schema_Manager::upgrade_waits( 'user', 'wp_itest_wpdo_user_sd_missing' ), 'a new table is created at once' ); self::set_user_mode( 'aeav_only' ); $this->assertFalse( TMDO_Schema_Manager::upgrade_waits( 'user', self::TABLE ), 'aeav_only: the flat table is the only copy' ); self::set_user_mode( 'dual_write' ); add_filter( 'tmdo_schema_ddl_allowed', static fn() => true ); // WP-CLI / WP-Cron. $this->assertFalse( TMDO_Schema_Manager::upgrade_waits( 'user', self::TABLE ) ); $GLOBALS['_wp_filter_callbacks']['tmdo_schema_ddl_allowed'] = array(); } /** * An existing table is altered by one process at a time: anonymous * /wp-cron.php requests pass ddl_allowed() too (security review H1). */ public function test_an_upgrade_is_skipped_while_another_process_holds_the_schema_lock(): void { add_filter( 'tmdo_schema_ddl_allowed', static fn() => true ); $other = new mysqli( getenv( 'TMDO_TEST_DB_HOST' ) ?: '127.0.0.1', getenv( 'TMDO_TEST_DB_USER' ) ?: 'dbo', (string) getenv( 'TMDO_TEST_DB_PASS' ), getenv( 'TMDO_TEST_DB_NAME' ) ?: 'wp_wpdo_test' ); $name = $other->real_escape_string( TMDO_Schema_Manager::index_lock_name() ); $other->query( "SELECT GET_LOCK(MD5(CONCAT(DATABASE(), '{$name}')), 0)" ); try { $this->assertFalse( $this->upgrade(), 'returns before dbDelta (unavailable here)' ); } finally { $other->query( "SELECT RELEASE_LOCK(MD5(CONCAT(DATABASE(), '{$name}')))" ); $other->close(); $GLOBALS['_wp_filter_callbacks']['tmdo_schema_ddl_allowed'] = array(); } $this->assertFalse( TMDO_Schema_Manager::is_deferred( 'user', 'sd_core' ) ); } /** * After the upgrade the group stays native until the drift fix has * rebuilt the rows written meanwhile (security review M1). */ public function test_a_drift_fix_releases_only_upgraded_groups_of_its_entity(): void { global $wpdb; $current = TMDO_Schema_Manager::calculate_schema_hash( WPDO_Entity_Registry::get_group_fields( 'user', 'sd_core' ) ); update_option( TMDO_Schema_Manager::DEFERRED_OPTION, array( 'user/sd_core' => 1, 'user/sd_later' => 1, // Not upgraded yet (no stored definition). 'post/sd_core' => 1, // Another entity. ) ); TMDO_Schema_Manager::resynced( 'user' ); $this->assertTrue( TMDO_Schema_Manager::is_deferred( 'user', 'sd_core' ), 'stored hash still old: not upgraded' ); $wpdb->update( $wpdb->prefix . 'wpdo_registry_meta', array( 'schema_hash' => $current ), array( 'entity_type' => 'user', 'group_name' => 'sd_core' ) ); TMDO_Schema_Manager::resynced( 'user' ); $this->assertFalse( TMDO_Schema_Manager::is_deferred( 'user', 'sd_core' ) ); $this->assertTrue( TMDO_Schema_Manager::is_deferred( 'user', 'sd_later' ) ); $this->assertTrue( TMDO_Schema_Manager::is_deferred( 'post', 'sd_core' ) ); } /** * The definition went back to the stored one (or another process upgraded * the table) while the group was deferred (correctness review F2). */ public function test_a_deferred_group_whose_definition_matches_again_is_resynced_then_released(): void { global $wpdb; $current = TMDO_Schema_Manager::calculate_schema_hash( WPDO_Entity_Registry::get_group_fields( 'user', 'sd_core' ) ); $wpdb->update( $wpdb->prefix . 'wpdo_registry_meta', array( 'schema_hash' => $current ), array( 'entity_type' => 'user', 'group_name' => 'sd_core' ) ); update_option( TMDO_Schema_Manager::DEFERRED_OPTION, array( 'user/sd_core' => 1 ) ); $this->assertTrue( $this->upgrade() ); $this->assertTrue( TMDO_Schema_Manager::is_deferred( 'user', 'sd_core' ), 'dual_write: native until the drift fix' ); $this->assertContains( TMDO_Drift_Check::RESYNC_HOOK, $GLOBALS['_wp_scheduled_single'] ); self::set_user_mode( 'disabled' ); $this->upgrade(); $this->assertFalse( TMDO_Schema_Manager::is_deferred( 'user', 'sd_core' ), 'no flat writes: nothing to rebuild' ); } /** * The cron event upgrades deferred groups itself: with ALTERNATE_WP_CRON * it runs inside a visitor's request, after init (correctness review F3). */ public function test_the_cron_event_handles_deferred_groups(): void { global $wpdb; $current = TMDO_Schema_Manager::calculate_schema_hash( WPDO_Entity_Registry::get_group_fields( 'user', 'sd_core' ) ); $wpdb->update( $wpdb->prefix . 'wpdo_registry_meta', array( 'schema_hash' => $current ), array( 'entity_type' => 'user', 'group_name' => 'sd_core' ) ); update_option( TMDO_Schema_Manager::DEFERRED_OPTION, array( 'user/sd_core' => 1 ) ); TMDO_Schema_Manager::ensure_pending_indexes(); $this->assertContains( TMDO_Drift_Check::RESYNC_HOOK, $GLOBALS['_wp_scheduled_single'] ); } }