hardening 2.2: 2meet-data-optimizer 1.2.0 (drift detection, scope, CLI and diagnostics) #6
@@ -240,7 +240,7 @@ class TMDO_CLI_V2 {
|
|||||||
} else {
|
} else {
|
||||||
$report = TMDO_Drift_Check::run( $entity, $group, $sample );
|
$report = TMDO_Drift_Check::run( $entity, $group, $sample );
|
||||||
}
|
}
|
||||||
} catch ( InvalidArgumentException $e ) {
|
} catch ( InvalidArgumentException | RuntimeException $e ) {
|
||||||
WP_CLI::error( $e->getMessage() );
|
WP_CLI::error( $e->getMessage() );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,13 +103,15 @@ final class TMDO_Drift_Check {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Daily check (wpdo_daily_health_check): sample every entity type whose
|
* Daily check (wpdo_daily_health_check): sample every entity type written
|
||||||
* flat tables are written (mode dual_write or higher). The outcome replaces
|
* to both native and flat (dual_write, shadow_read). In aeav_only flat is
|
||||||
* the recorded status, so a group found drifting leaves the router at once.
|
* the source of truth and native is stale by design, so drift means nothing
|
||||||
|
* there. The outcome replaces the recorded status, so a group found
|
||||||
|
* drifting leaves the router at once.
|
||||||
*/
|
*/
|
||||||
public static function cron_tick(): void {
|
public static function cron_tick(): void {
|
||||||
foreach ( TMDO_Entity_Registry::VALID_ENTITY_TYPES as $entity_type ) {
|
foreach ( TMDO_Entity_Registry::VALID_ENTITY_TYPES as $entity_type ) {
|
||||||
if ( ! TMDO_Mode_Manager::writes_to_flat( $entity_type ) ) {
|
if ( ! TMDO_Mode_Manager::writes_to_flat( $entity_type ) || ! TMDO_Mode_Manager::writes_to_eav( $entity_type ) ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -132,11 +134,16 @@ final class TMDO_Drift_Check {
|
|||||||
* @param string $entity_type post | user | term | comment.
|
* @param string $entity_type post | user | term | comment.
|
||||||
* @param string $group One group, or '' for every backfillable group.
|
* @param string $group One group, or '' for every backfillable group.
|
||||||
* @return array{fixed:array{rebuilt:int, cleared:int, deleted_rows:int}, after:array<string, mixed>}
|
* @return array{fixed:array{rebuilt:int, cleared:int, deleted_rows:int}, after:array<string, mixed>}
|
||||||
* @throws InvalidArgumentException Unknown entity type or group.
|
* @throws RuntimeException In aeav_only, where flat is the source of truth
|
||||||
|
* (and InvalidArgumentException via resolve() for an
|
||||||
|
* unknown entity type or group).
|
||||||
*/
|
*/
|
||||||
public static function fix( string $entity_type, string $group = '' ): array {
|
public static function fix( string $entity_type, string $group = '' ): array {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
list( $adapter, $groups ) = self::resolve( $entity_type, $group );
|
list( $adapter, $groups ) = self::resolve( $entity_type, $group );
|
||||||
|
if ( TMDO_Mode_Manager::MODE_AEAV_ONLY === TMDO_Mode_Manager::get( $entity_type ) ) {
|
||||||
|
throw new RuntimeException( esc_html( $entity_type ) . ' is in aeav_only: flat is the source of truth and native meta is stale, so it cannot be rebuilt from native. Write flat back first (wp tmdo bridge-restore-native).' );
|
||||||
|
}
|
||||||
|
|
||||||
$id = $adapter->get_entity_id_column();
|
$id = $adapter->get_entity_id_column();
|
||||||
$fixed = array(
|
$fixed = array(
|
||||||
|
|||||||
@@ -525,12 +525,16 @@ class TMDO_Post_Query_Router {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The last `wp tmdo drift-check` found the group out of step with native
|
* The last `wp tmdo drift-check` found the group out of step with native
|
||||||
* meta. A group never checked keeps routing.
|
* meta. A group never checked keeps routing, and so does every group in
|
||||||
|
* aeav_only, where native is stale by design and no fallback.
|
||||||
*
|
*
|
||||||
* @param string $group Entity group name.
|
* @param string $group Entity group name.
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
private static function drifting( string $group ): bool {
|
private static function drifting( string $group ): bool {
|
||||||
|
if ( TMDO_Mode_Manager::MODE_AEAV_ONLY === TMDO_Mode_Manager::get( 'post' ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
$status = get_option( TMDO_Drift_Check::OPTION, array() );
|
$status = get_option( TMDO_Drift_Check::OPTION, array() );
|
||||||
$last = is_array( $status ) ? ( $status['post'][ $group ] ?? null ) : null;
|
$last = is_array( $status ) ? ( $status['post'][ $group ] ?? null ) : null;
|
||||||
return is_array( $last ) && (int) ( $last['miss'] ?? 0 ) + (int) ( $last['diff'] ?? 0 ) + (int) ( $last['extra'] ?? 0 ) > 0;
|
return is_array( $last ) && (int) ( $last['miss'] ?? 0 ) + (int) ( $last['diff'] ?? 0 ) + (int) ( $last['extra'] ?? 0 ) > 0;
|
||||||
|
|||||||
@@ -214,6 +214,33 @@ class DriftCheckTest extends TestCase {
|
|||||||
$this->assertFalse( get_option( TMDO_Drift_Check::OPTION ) );
|
$this->assertFalse( get_option( TMDO_Drift_Check::OPTION ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_the_daily_tick_skips_aeav_only_where_flat_is_the_source_of_truth(): void {
|
||||||
|
self::set_modes( array( 'post' => 'disabled', 'user' => 'aeav_only', 'term' => 'disabled', 'comment' => 'disabled' ) );
|
||||||
|
try {
|
||||||
|
TMDO_Drift_Check::cron_tick();
|
||||||
|
} finally {
|
||||||
|
self::set_modes( null );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertFalse( get_option( TMDO_Drift_Check::OPTION ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fix_refuses_aeav_only_and_leaves_flat_alone(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$flat = md5( wp_json_encode( $wpdb->get_results( 'SELECT * FROM `' . self::TABLE . '` ORDER BY id', ARRAY_A ) ) );
|
||||||
|
self::set_modes( array( 'post' => 'disabled', 'user' => 'aeav_only', 'term' => 'disabled', 'comment' => 'disabled' ) );
|
||||||
|
try {
|
||||||
|
TMDO_Drift_Check::fix( 'user' );
|
||||||
|
$this->fail( 'fix() must refuse aeav_only' );
|
||||||
|
} catch ( RuntimeException $e ) {
|
||||||
|
$this->assertStringContainsString( 'aeav_only', $e->getMessage() );
|
||||||
|
} finally {
|
||||||
|
self::set_modes( null );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertSame( $flat, md5( wp_json_encode( $wpdb->get_results( 'SELECT * FROM `' . self::TABLE . '` ORDER BY id', ARRAY_A ) ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
public function test_the_daily_tick_runs_on_the_daily_health_check_hook(): void {
|
public function test_the_daily_tick_runs_on_the_daily_health_check_hook(): void {
|
||||||
$source = (string) file_get_contents( WPDO_PLUGIN_DIR . 'includes/class-tmdo-core.php' );
|
$source = (string) file_get_contents( WPDO_PLUGIN_DIR . 'includes/class-tmdo-core.php' );
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,23 @@ class PostQueryRouterDriftTest extends TestCase {
|
|||||||
$this->assertNotEmpty( $query->get( 'wpdo_post_clauses' ) );
|
$this->assertNotEmpty( $query->get( 'wpdo_post_clauses' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_aeav_only_ignores_drift_because_native_is_stale_by_design(): void {
|
||||||
|
self::set_modes(
|
||||||
|
array(
|
||||||
|
'post' => 'aeav_only',
|
||||||
|
'user' => 'disabled',
|
||||||
|
'term' => 'disabled',
|
||||||
|
'comment' => 'disabled',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$this->drift( 'wc_product', 3 );
|
||||||
|
$query = $this->price_query();
|
||||||
|
|
||||||
|
( new WPDO_Post_Query_Router() )->pre_get_posts( $query );
|
||||||
|
|
||||||
|
$this->assertNotEmpty( $query->get( 'wpdo_post_clauses' ) );
|
||||||
|
}
|
||||||
|
|
||||||
public function test_sorting_by_a_drifting_key_stays_native(): void {
|
public function test_sorting_by_a_drifting_key_stays_native(): void {
|
||||||
$this->drift( 'wc_product', 1 );
|
$this->drift( 'wc_product', 1 );
|
||||||
$query = new WP_Query();
|
$query = new WP_Query();
|
||||||
|
|||||||
Reference in New Issue
Block a user