diff --git a/cli/class-tmdo-cli-v2.php b/cli/class-tmdo-cli-v2.php index 5926c70..bdc6730 100644 --- a/cli/class-tmdo-cli-v2.php +++ b/cli/class-tmdo-cli-v2.php @@ -240,7 +240,7 @@ class TMDO_CLI_V2 { } else { $report = TMDO_Drift_Check::run( $entity, $group, $sample ); } - } catch ( InvalidArgumentException $e ) { + } catch ( InvalidArgumentException | RuntimeException $e ) { WP_CLI::error( $e->getMessage() ); return; } diff --git a/includes/diagnostic/class-tmdo-drift-check.php b/includes/diagnostic/class-tmdo-drift-check.php index 35bad01..0ea49d2 100644 --- a/includes/diagnostic/class-tmdo-drift-check.php +++ b/includes/diagnostic/class-tmdo-drift-check.php @@ -103,13 +103,15 @@ final class TMDO_Drift_Check { } /** - * Daily check (wpdo_daily_health_check): sample every entity type whose - * flat tables are written (mode dual_write or higher). The outcome replaces - * the recorded status, so a group found drifting leaves the router at once. + * Daily check (wpdo_daily_health_check): sample every entity type written + * to both native and flat (dual_write, shadow_read). In aeav_only flat is + * 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 { 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; } try { @@ -132,11 +134,16 @@ final class TMDO_Drift_Check { * @param string $entity_type post | user | term | comment. * @param string $group One group, or '' for every backfillable group. * @return array{fixed:array{rebuilt:int, cleared:int, deleted_rows:int}, after:array} - * @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 { global $wpdb; 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(); $fixed = array( diff --git a/includes/query/class-tmdo-post-query-router.php b/includes/query/class-tmdo-post-query-router.php index 0f0210b..2fe44e8 100644 --- a/includes/query/class-tmdo-post-query-router.php +++ b/includes/query/class-tmdo-post-query-router.php @@ -525,12 +525,16 @@ class TMDO_Post_Query_Router { /** * 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. * @return 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() ); $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; diff --git a/tests/integration/DriftCheckTest.php b/tests/integration/DriftCheckTest.php index 76a0ff5..1a66471 100644 --- a/tests/integration/DriftCheckTest.php +++ b/tests/integration/DriftCheckTest.php @@ -214,6 +214,33 @@ class DriftCheckTest extends TestCase { $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 { $source = (string) file_get_contents( WPDO_PLUGIN_DIR . 'includes/class-tmdo-core.php' ); diff --git a/tests/integration/PostQueryRouterDriftTest.php b/tests/integration/PostQueryRouterDriftTest.php index 806ddca..db076e7 100644 --- a/tests/integration/PostQueryRouterDriftTest.php +++ b/tests/integration/PostQueryRouterDriftTest.php @@ -100,6 +100,23 @@ class PostQueryRouterDriftTest extends TestCase { $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 { $this->drift( 'wc_product', 1 ); $query = new WP_Query();