diff --git a/includes/engine/class-tmdo-post-status-sync.php b/includes/engine/class-tmdo-post-status-sync.php index 5d3cb5b..e3fdb90 100644 --- a/includes/engine/class-tmdo-post-status-sync.php +++ b/includes/engine/class-tmdo-post-status-sync.php @@ -163,6 +163,10 @@ final class TMDO_Post_Status_Sync { } self::set_state( $group, 'ready' ); } + // Routed counts cached while the copies were stale must go. + if ( ! $dry_run && class_exists( 'TMDO_Post_Query_Router' ) ) { + TMDO_Post_Query_Router::bump_count_version(); + } return $report; } diff --git a/includes/query/class-tmdo-post-query-router.php b/includes/query/class-tmdo-post-query-router.php index 9692c72..794363a 100644 --- a/includes/query/class-tmdo-post-query-router.php +++ b/includes/query/class-tmdo-post-query-router.php @@ -125,9 +125,17 @@ class TMDO_Post_Query_Router { global $wpdb; foreach ( TMDO_Post_Fields::groups_for_post_type( $post->post_type ) as $group ) { $table = TMDO_Schema_Manager::get_table_name( 'post', $group ); - if ( TMDO_Schema_Manager::table_exists( $table ) ) { - $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `{$table}` (`post_id`) VALUES (%d)", (int) $post_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared + if ( ! TMDO_Schema_Manager::table_exists( $table ) ) { + continue; } + // With the status / date copies: this runs after transition_post_status, + // and one NULL copy turns the copy route off for the whole group. + $copies = class_exists( 'TMDO_Post_Status_Sync' ) ? TMDO_Post_Status_Sync::insert_columns( (int) $post_id, $group ) : array(); + if ( $copies ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `{$table}` (`post_id`, `post_status`, `post_date`) VALUES (%d, %s, %s)", (int) $post_id, $copies['post_status'], $copies['post_date'] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared + continue; + } + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `{$table}` (`post_id`) VALUES (%d)", (int) $post_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared } } diff --git a/tests/integration/PostQueryRouterCopyTest.php b/tests/integration/PostQueryRouterCopyTest.php index 07b11c6..84f2340 100644 --- a/tests/integration/PostQueryRouterCopyTest.php +++ b/tests/integration/PostQueryRouterCopyTest.php @@ -133,6 +133,22 @@ class PostQueryRouterCopyTest extends TestCase { $this->assertSame( 'wp_posts.post_title ASC', $router->posts_orderby( 'wp_posts.post_title ASC', $query ) ); } + /** + * wp_insert_post runs after transition_post_status: a new post's empty flat + * row must carry the copies itself, or one NULL turns the copy route off for + * the whole group (2.4 correctness review). + */ + public function test_a_new_post_gets_its_flat_row_with_the_copies(): void { + global $wpdb; + $post = new WP_Post( (object) array( 'ID' => 7, 'post_type' => 'rc_item', 'post_status' => 'draft', 'post_date' => '2026-02-02 00:00:00' ) ); + $GLOBALS['_wp_posts'][7] = $post; + + ( new TMDO_Post_Query_Router() )->add_flat_rows( 7, $post, false ); + + $this->assertSame( array( 'post_status' => 'draft', 'post_date' => '2026-02-02 00:00:00' ), $wpdb->get_row( 'SELECT post_status, post_date FROM `' . self::TABLE . '` WHERE post_id = 7', ARRAY_A ) ); + unset( $GLOBALS['_wp_posts'][7] ); + } + public function test_copy_route_keeps_group_by_with_another_join(): void { global $wpdb; $sql = $this->request( " LEFT JOIN wp_term_relationships ON ({$wpdb->posts}.ID = wp_term_relationships.object_id)" ); diff --git a/tests/integration/PostStatusSyncTest.php b/tests/integration/PostStatusSyncTest.php index 3aaf18f..02d65e8 100644 --- a/tests/integration/PostStatusSyncTest.php +++ b/tests/integration/PostStatusSyncTest.php @@ -132,6 +132,16 @@ class PostStatusSyncTest extends TestCase { $this->assertSame( 2, $report['ps_a']['mismatched'] ); } + /** Routed counts cached while copies were stale are dropped by a sync (2.4 correctness review). */ + public function test_a_sync_invalidates_cached_routed_counts_but_a_dry_run_does_not(): void { + $GLOBALS['_wp_last_changed'][ TMDO_Post_Query_Router::COUNT_VERSION_GROUP ] = 'before'; + TMDO_Post_Status_Sync::run( true ); + $this->assertSame( 'before', $GLOBALS['_wp_last_changed'][ TMDO_Post_Query_Router::COUNT_VERSION_GROUP ] ); + + TMDO_Post_Status_Sync::run(); + $this->assertNotSame( 'before', $GLOBALS['_wp_last_changed'][ TMDO_Post_Query_Router::COUNT_VERSION_GROUP ] ); + } + public function test_copies_use_the_posts_collation_so_they_compare_with_posts(): void { global $wpdb; TMDO_Post_Status_Sync::run(); diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index eddae3c..76fcb96 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -354,6 +354,19 @@ if ( ! function_exists( 'get_term_meta' ) ) { function get_term_meta( int $tid, if ( ! function_exists( 'update_term_meta' ) ) { function update_term_meta( int $tid, string $key, $value, $prev = '' ): bool { $GLOBALS['_wp_termmeta'][ $tid ][ $key ] = $value; return true; } } if ( ! function_exists( 'get_comment_meta' ) ) { function get_comment_meta( int $cid, string $key = '', bool $single = false ) { return $GLOBALS['_wp_commentmeta'][ $cid ][ $key ] ?? ( $single ? '' : [] ); } } if ( ! function_exists( 'update_comment_meta' ) ) { function update_comment_meta( int $cid, string $key, $value, $prev = '' ): bool { $GLOBALS['_wp_commentmeta'][ $cid ][ $key ] = $value; return true; } } +if ( ! class_exists( 'WP_Post' ) ) { + final class WP_Post { + public $ID = 0; + public $post_type = 'post'; + public $post_status = 'publish'; + public $post_date = '0000-00-00 00:00:00'; + public function __construct( $post ) { + foreach ( get_object_vars( $post ) as $k => $v ) { + $this->$k = $v; + } + } + } +} if ( ! function_exists( 'get_post' ) ) { function get_post( $post_id ) { return $GLOBALS['_wp_posts'][ (int) $post_id ] ?? null; } } if ( ! function_exists( 'get_post_type' ) ) { function get_post_type( $post_id ) { return $GLOBALS['_wp_post_types'][ (int) $post_id ] ?? false; } } if ( ! function_exists( 'get_post_status' ) ) { function get_post_status( $post_id ) { return $GLOBALS['_wp_post_status'][ (int) $post_id ] ?? 'publish'; } }