1d70d13f45
- a deferred group whose stored definition matches again (reverted, or upgraded by another process) goes through end_deferral() instead of staying native for good (review F2) - the tmdo_ensure_group_indexes handler upgrades deferred tables itself: with ALTERNATE_WP_CRON the event runs after init in a visitor's request (F3) - a new group table created while the entity writes to flat stays native until the drift fix has filled it from native meta; an opted-in HivePress attribute no longer routes searches to an empty table (F6) - direct_read() returns native values for a deferred group (F10); the Hook Bus ignores deferral in aeav_only, as the router does - dbDelta failures: the definition is recorded only when every column exists (schema.upgrade_failed otherwise), so a failed ALTER is retried and the group is not released onto missing columns - test bootstrap: get_metadata() reads the real meta table - Verify: SchemaUpgradeDeferralTest (6), Hook Bus direct read, integration 587, unit 703, phpcs, stan - PR: PR-114 (2.3 acceptance correctness review)
178 lines
8.2 KiB
PHP
178 lines
8.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* An existing flat table whose definition changed is upgraded from WP-CLI /
|
|
* WP-Cron only (2.3 PR-107); web requests record the group as deferred.
|
|
*/
|
|
class SchemaUpgradeDeferralTest extends TestCase {
|
|
|
|
private const TABLE = 'wp_itest_wpdo_user_sd_core';
|
|
|
|
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/adapters/class-tmdo-adapter-user.php',
|
|
) as $file ) {
|
|
require_once $base . $file;
|
|
}
|
|
WPDO_Installer::install_v2_tables();
|
|
}
|
|
|
|
protected function setUp(): void {
|
|
global $wpdb;
|
|
$wpdb->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'] );
|
|
}
|
|
}
|