fix(2meet-data-optimizer): add declared flat-table indexes from WP-CLI or WP-Cron only, one process at a time
- ensure_group_indexes() runs the ALTER only under WP-CLI or WP-Cron (filter tmdo_schema_ddl_allowed); web requests, including /wp-admin/ before auth_redirect() and admin-post.php, schedule tmdo_ensure_group_indexes instead - MySQL GET_LOCK keeps it to one process (add_option() is INSERT ... ON DUPLICATE KEY UPDATE and cannot serve as a lock); released by MySQL if the process dies - the applied signature is recorded only when every ALTER succeeds; failures log schema.index_failed and stay in pending_indexes(), which doctor reports - new tables get their declared cidx_ indexes in CREATE TABLE - test stub wpdb::query()/insert() return false on SQL errors like wpdb - Verify: GroupCompositeIndexTest 7/7, unit 698, integration 576, phpcs, stan - PR: PR-090a
This commit is contained in:
@@ -31,6 +31,10 @@ class GroupCompositeIndexTest extends TestCase {
|
||||
$wpdb->query( 'CREATE TABLE `' . self::TABLE . '` ( id BIGINT NOT NULL AUTO_INCREMENT, post_id BIGINT NOT NULL, ci_flag BIGINT(20) DEFAULT NULL, ci_price DECIMAL(18,6) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_entity (post_id) ) ENGINE=InnoDB' );
|
||||
$wpdb->query( 'INSERT INTO `' . self::TABLE . '` (post_id, ci_flag, ci_price) VALUES (1, 1, 10), (2, 0, 20)' );
|
||||
$GLOBALS['_wp_options'] = array();
|
||||
// Index DDL runs from WP-CLI / WP-Cron only (2.3 PR-090a); these tests act as one.
|
||||
$GLOBALS['_wp_filter_callbacks']['tmdo_schema_ddl_allowed'] = array();
|
||||
add_filter( 'tmdo_schema_ddl_allowed', static fn() => true );
|
||||
$GLOBALS['_wp_scheduled_single'] = array();
|
||||
|
||||
$ref = new ReflectionProperty( WPDO_Schema_Manager::class, 'table_exists_cache' );
|
||||
$ref->setAccessible( true );
|
||||
@@ -53,6 +57,81 @@ class GroupCompositeIndexTest extends TestCase {
|
||||
global $wpdb;
|
||||
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE . '`' );
|
||||
WPDO_Entity_Registry::init();
|
||||
$GLOBALS['_wp_filter_callbacks']['tmdo_schema_ddl_allowed'] = array();
|
||||
unset( $GLOBALS['_wp_is_admin'], $GLOBALS['_wp_scheduled_single'] );
|
||||
}
|
||||
|
||||
public function test_a_web_request_adds_no_index_and_leaves_it_to_wp_cron(): void {
|
||||
$GLOBALS['_wp_filter_callbacks']['tmdo_schema_ddl_allowed'] = array();
|
||||
foreach ( array( false, true ) as $is_admin ) { // /wp-admin/ runs init before auth_redirect().
|
||||
$GLOBALS['_wp_is_admin'] = $is_admin;
|
||||
$this->assertFalse( TMDO_Schema_Manager::ddl_allowed() );
|
||||
|
||||
TMDO_Schema_Manager::ensure_group_indexes( 'post', 'ci_core' );
|
||||
|
||||
$this->assertArrayNotHasKey( 'cidx_flag_price', $this->indexes(), 'no ALTER in a web request' );
|
||||
}
|
||||
$this->assertSame( array( 'post/ci_core' ), TMDO_Schema_Manager::pending_indexes() );
|
||||
$this->assertContains( TMDO_Schema_Manager::INDEXES_HOOK, $GLOBALS['_wp_scheduled_single'] );
|
||||
|
||||
add_filter( 'tmdo_schema_ddl_allowed', static fn() => true ); // The cron run.
|
||||
TMDO_Schema_Manager::ensure_pending_indexes();
|
||||
|
||||
$this->assertArrayHasKey( 'cidx_flag_price', $this->indexes() );
|
||||
$this->assertSame( array(), TMDO_Schema_Manager::pending_indexes() );
|
||||
}
|
||||
|
||||
public function test_the_ddl_decision_can_be_overridden(): void {
|
||||
add_filter( 'tmdo_schema_ddl_allowed', static fn() => false );
|
||||
|
||||
TMDO_Schema_Manager::ensure_group_indexes( 'post', 'ci_core' );
|
||||
|
||||
$this->assertArrayNotHasKey( 'cidx_flag_price', $this->indexes() );
|
||||
}
|
||||
|
||||
public function test_a_failed_alter_is_not_recorded_as_applied(): void {
|
||||
global $wpdb;
|
||||
$wpdb->query( 'ALTER TABLE `' . self::TABLE . '` ADD COLUMN ci_note TEXT DEFAULT NULL' );
|
||||
WPDO_Entity_Registry::init();
|
||||
WPDO_Entity_Registry::register_adapter( 'post', new WPDO_Adapter_Post() );
|
||||
WPDO_Entity_Registry::register_group(
|
||||
'post',
|
||||
'ci_core',
|
||||
array(
|
||||
array( 'key' => 'ci_flag', 'type' => 'integer' ),
|
||||
array( 'key' => 'ci_note', 'type' => 'textarea' ),
|
||||
),
|
||||
array( 'indexes' => array( 'flag_note' => array( 'ci_flag', 'ci_note' ) ) ) // TEXT without a prefix length: MySQL 1170.
|
||||
);
|
||||
|
||||
TMDO_Schema_Manager::ensure_group_indexes( 'post', 'ci_core' );
|
||||
|
||||
$this->assertArrayNotHasKey( 'cidx_flag_note', $this->indexes() );
|
||||
$this->assertFalse( get_option( TMDO_Schema_Manager::INDEXES_OPTION ), 'not recorded, so it is retried' );
|
||||
$this->assertSame( array( 'post/ci_core' ), TMDO_Schema_Manager::pending_indexes() );
|
||||
}
|
||||
|
||||
public function test_a_lock_held_by_another_process_skips_the_ddl_until_it_is_released(): void {
|
||||
$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() );
|
||||
$this->assertSame( '1', (string) $other->query( "SELECT GET_LOCK(MD5(CONCAT(DATABASE(), '{$name}')), 0)" )->fetch_row()[0] );
|
||||
|
||||
TMDO_Schema_Manager::ensure_group_indexes( 'post', 'ci_core' );
|
||||
$this->assertArrayNotHasKey( 'cidx_flag_price', $this->indexes(), 'another process holds the lock' );
|
||||
|
||||
$other->query( "SELECT RELEASE_LOCK(MD5(CONCAT(DATABASE(), '{$name}')))" );
|
||||
$other->close();
|
||||
TMDO_Schema_Manager::ensure_group_indexes( 'post', 'ci_core' );
|
||||
|
||||
$this->assertArrayHasKey( 'cidx_flag_price', $this->indexes() );
|
||||
$this->assertSame( array(), TMDO_Schema_Manager::pending_indexes() );
|
||||
global $wpdb;
|
||||
$this->assertSame( '1', (string) $wpdb->get_var( $wpdb->prepare( 'SELECT IS_FREE_LOCK(MD5(CONCAT(DATABASE(), %s)))', TMDO_Schema_Manager::index_lock_name() ) ), 'lock released' );
|
||||
}
|
||||
|
||||
/** @return array<string, string[]> index name → ordered columns */
|
||||
|
||||
@@ -168,7 +168,12 @@ $wpdb = new class( $_mysqli ) {
|
||||
fn( $v ) => $v === null ? 'NULL' : "'" . $this->db->real_escape_string( (string) $v ) . "'",
|
||||
array_values( $data )
|
||||
) );
|
||||
$ok = $this->db->query( "INSERT INTO `{$table}` ({$cols}) VALUES ({$vals})" );
|
||||
try {
|
||||
$ok = $this->db->query( "INSERT INTO `{$table}` ({$cols}) VALUES ({$vals})" );
|
||||
} catch ( mysqli_sql_exception $e ) { // Like wpdb::insert(): an SQL error returns false.
|
||||
$this->last_error = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
if ( $ok ) { $this->insert_id = (int) $this->db->insert_id; return $this->insert_id; }
|
||||
$this->last_error = (string) $this->db->error;
|
||||
return false;
|
||||
@@ -200,7 +205,12 @@ $wpdb = new class( $_mysqli ) {
|
||||
public function flush(): void {}
|
||||
|
||||
public function query( string $sql ): int|bool {
|
||||
$result = $this->db->query( $sql );
|
||||
try {
|
||||
$result = $this->db->query( $sql );
|
||||
} catch ( mysqli_sql_exception $e ) { // Like wpdb::query(): an SQL error returns false.
|
||||
$this->last_error = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
if ( $result instanceof mysqli_result ) { $result->free(); return true; }
|
||||
if ( false === $result ) { return false; }
|
||||
return $this->db->affected_rows;
|
||||
@@ -291,7 +301,7 @@ if ( ! function_exists( 'is_singular' ) ) { function is_singular( $t = '' ):
|
||||
if ( ! function_exists( 'wp_doing_ajax' ) ) { function wp_doing_ajax(): bool { return false; } }
|
||||
if ( ! function_exists( 'wp_next_scheduled' ) ) { function wp_next_scheduled( string $hook ): int|false { return false; } }
|
||||
if ( ! function_exists( 'wp_schedule_event' ) ) { function wp_schedule_event( int $t, string $r, string $h ): bool { return true; } }
|
||||
if ( ! function_exists( 'wp_schedule_single_event' ) ) { function wp_schedule_single_event( int $ts, string $hook ): bool { return true; } }
|
||||
if ( ! function_exists( 'wp_schedule_single_event' ) ) { function wp_schedule_single_event( int $ts, string $hook ): bool { $GLOBALS['_wp_scheduled_single'][] = $hook; return true; } }
|
||||
if ( ! function_exists( 'wp_clear_scheduled_hook' ) ) { function wp_clear_scheduled_hook( string $hook ): int|false { return 0; } }
|
||||
if ( ! function_exists( 'sanitize_text_field' ) ) { function sanitize_text_field( string $s ): string { return trim( strip_tags( $s ) ); } }
|
||||
if ( ! function_exists( '__' ) ) { function __( string $text, string $domain = 'default' ): string { return $text; } }
|
||||
|
||||
Reference in New Issue
Block a user