@@ -779,6 +779,136 @@ public function test_sync_should_compact_is_false_for_non_compactor() {
779779 $ this ->assertFalse ( $ data ['rooms ' ][0 ]['should_compact ' ] );
780780 }
781781
782+ public function test_sync_compaction_does_not_delete_update_inserted_during_delete () {
783+ global $ wpdb ;
784+
785+ wp_set_current_user ( self ::$ editor_id );
786+
787+ $ room = $ this ->get_post_room ();
788+ $ storage = new WP_Sync_Post_Meta_Storage ();
789+
790+ // Seed three updates so there's something to compact.
791+ for ( $ i = 1 ; $ i <= 3 ; $ i ++ ) {
792+ $ this ->assertTrue (
793+ $ storage ->add_update (
794+ $ room ,
795+ array (
796+ 'client_id ' => $ i ,
797+ 'type ' => 'update ' ,
798+ 'data ' => base64_encode ( "seed- $ i " ),
799+ )
800+ )
801+ );
802+ }
803+
804+ // Capture the cursor after all seeds are in place.
805+ $ storage ->get_updates_after_cursor ( $ room , 0 );
806+ $ compaction_cursor = $ storage ->get_cursor ( $ room );
807+ $ this ->assertGreaterThan ( 0 , $ compaction_cursor );
808+
809+ $ storage_posts = get_posts (
810+ array (
811+ 'post_type ' => WP_Sync_Post_Meta_Storage::POST_TYPE ,
812+ 'posts_per_page ' => 1 ,
813+ 'post_status ' => 'publish ' ,
814+ 'name ' => md5 ( $ room ),
815+ 'fields ' => 'ids ' ,
816+ )
817+ );
818+ $ storage_post_id = array_first ( $ storage_posts );
819+ $ this ->assertIsInt ( $ storage_post_id );
820+
821+ $ concurrent_update = array (
822+ 'client_id ' => 9999 ,
823+ 'type ' => 'update ' ,
824+ 'data ' => base64_encode ( 'arrived-during-compaction ' ),
825+ );
826+
827+ $ original_wpdb = $ wpdb ;
828+ $ proxy_wpdb = new class ( $ original_wpdb , $ storage_post_id , $ concurrent_update ) {
829+ private $ wpdb ;
830+ private $ storage_post_id ;
831+ private $ concurrent_update ;
832+ public $ did_inject = false ;
833+
834+ public function __construct ( $ wpdb , int $ storage_post_id , array $ concurrent_update ) {
835+ $ this ->wpdb = $ wpdb ;
836+ $ this ->storage_post_id = $ storage_post_id ;
837+ $ this ->concurrent_update = $ concurrent_update ;
838+ }
839+
840+ // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Proxy forwards fully prepared core queries.
841+ public function prepare ( ...$ args ) {
842+ return $ this ->wpdb ->prepare ( ...$ args );
843+ }
844+
845+ public function query ( $ query ) {
846+ $ result = $ this ->wpdb ->query ( $ query );
847+
848+ // After the DELETE executes, inject a concurrent update via
849+ // raw SQL through the real $wpdb to avoid metadata cache
850+ // interactions while the proxy is active.
851+ if ( ! $ this ->did_inject
852+ && is_string ( $ query )
853+ && 0 === strpos ( $ query , "DELETE FROM {$ this ->wpdb ->postmeta }" )
854+ && false !== strpos ( $ query , "post_id = {$ this ->storage_post_id }" )
855+ ) {
856+ $ this ->did_inject = true ;
857+ $ this ->wpdb ->insert (
858+ $ this ->wpdb ->postmeta ,
859+ array (
860+ 'post_id ' => $ this ->storage_post_id ,
861+ 'meta_key ' => WP_Sync_Post_Meta_Storage::SYNC_UPDATE_META_KEY ,
862+ 'meta_value ' => maybe_serialize ( $ this ->concurrent_update ),
863+ ),
864+ array ( '%d ' , '%s ' , '%s ' )
865+ );
866+ }
867+
868+ return $ result ;
869+ }
870+ // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
871+
872+ public function __call ( $ name , $ arguments ) {
873+ return $ this ->wpdb ->$ name ( ...$ arguments );
874+ }
875+
876+ public function __get ( $ name ) {
877+ return $ this ->wpdb ->$ name ;
878+ }
879+
880+ public function __set ( $ name , $ value ) {
881+ $ this ->wpdb ->$ name = $ value ;
882+ }
883+ };
884+
885+ // Run compaction through the proxy so the concurrent update
886+ // is injected immediately after the DELETE executes.
887+ $ wpdb = $ proxy_wpdb ;
888+ try {
889+ $ result = $ storage ->remove_updates_before_cursor ( $ room , $ compaction_cursor );
890+ } finally {
891+ $ wpdb = $ original_wpdb ;
892+ }
893+
894+ $ this ->assertTrue ( $ result );
895+ $ this ->assertTrue ( $ proxy_wpdb ->did_inject , 'Expected concurrent update injection to occur. ' );
896+
897+ // Clear caches since the injection bypassed add_update().
898+ wp_cache_delete ( $ storage_post_id , 'post_meta ' );
899+ wp_cache_delete ( 'sync_room_state_ ' . md5 ( $ room ), 'sync ' );
900+
901+ // The concurrent update must survive the compaction delete.
902+ $ updates = $ storage ->get_updates_after_cursor ( $ room , 0 );
903+
904+ $ update_data = wp_list_pluck ( $ updates , 'data ' );
905+ $ this ->assertContains (
906+ $ concurrent_update ['data ' ],
907+ $ update_data ,
908+ 'Concurrent update should survive compaction. '
909+ );
910+ }
911+
782912 /*
783913 * Awareness tests.
784914 */
0 commit comments