query( "CREATE TABLE `{$wpdb->posts}` ( ID BIGINT UNSIGNED NOT NULL, post_type VARCHAR(20) NOT NULL DEFAULT 'post', post_status VARCHAR(20) NOT NULL DEFAULT 'publish', PRIMARY KEY (ID) ) ENGINE=InnoDB" ); foreach ( array( 101 => 'hp_listing', 102 => 'hp_listing', 103 => 'hp_listing', 104 => 'hp_listing', 202 => 'hp_vendor' ) as $id => $type ) { $wpdb->insert( $wpdb->posts, array( 'ID' => $id, 'post_type' => $type ) ); } $wpdb->query( 'CREATE TABLE `' . self::LISTING . '` ( id BIGINT NOT NULL AUTO_INCREMENT, post_id BIGINT NOT NULL, shared_key VARCHAR(255) DEFAULT NULL, price DECIMAL(18,6) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_entity (post_id) ) ENGINE=InnoDB' ); $wpdb->query( 'CREATE TABLE `' . self::VENDOR . '` ( id BIGINT NOT NULL AUTO_INCREMENT, post_id BIGINT NOT NULL, shared_key VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_entity (post_id) ) ENGINE=InnoDB' ); $wpdb->insert( self::VENDOR, array( 'post_id' => 101, 'shared_key' => 'live' ) ); $wpdb->insert( self::VENDOR, array( 'post_id' => 102, 'shared_key' => 'only-in-vendor' ) ); $wpdb->query( 'INSERT INTO `' . self::VENDOR . "` (post_id, shared_key) VALUES (104, NULL)" ); $wpdb->insert( self::VENDOR, array( 'post_id' => 202, 'shared_key' => 'vendor own value' ) ); $wpdb->insert( self::LISTING, array( 'post_id' => 101, 'shared_key' => 'backfill', 'price' => 5 ) ); $wpdb->insert( self::LISTING, array( 'post_id' => 103, 'shared_key' => 'keep' ) ); $wpdb->insert( self::LISTING, array( 'post_id' => 104, 'shared_key' => 'backfill-104' ) ); // Historical rows: older than any run, so the catch-up pass leaves them alone. $wpdb->query( 'UPDATE `' . self::VENDOR . "` SET updated_at = '2020-01-01 00:00:00'" ); $wpdb->query( 'UPDATE `' . self::LISTING . "` SET updated_at = '2020-01-01 00:00:00'" ); $GLOBALS['_wp_options'] = array( WPDO_Entity_Registry::SCOPED_ROUTING_OPTION => '0' ); $GLOBALS['_wp_cache'] = array(); WPDO_Entity_Registry::init(); WPDO_Entity_Registry::register_adapter( 'post', new WPDO_Adapter_Post() ); WPDO_Entity_Registry::register_group( 'post', 'rc_listing', array( array( 'key' => 'shared_key', 'type' => 'text' ), array( 'key' => 'price', 'type' => 'decimal' ) ), array( 'post_types' => array( 'hp_listing' ) ) ); WPDO_Entity_Registry::register_group( 'post', 'rc_vendor', array( array( 'key' => 'shared_key', 'type' => 'text' ) ), array( 'post_types' => array( 'hp_vendor' ) ) ); } protected function tearDown(): void { self::drop(); WPDO_Entity_Registry::init(); } private static function drop(): void { global $wpdb; foreach ( array( $wpdb->posts, self::LISTING, self::VENDOR ) as $table ) { $wpdb->query( "DROP TABLE IF EXISTS `{$table}`" ); } } private function listing_value( int $post_id ): ?string { global $wpdb; return $wpdb->get_var( $wpdb->prepare( 'SELECT shared_key FROM `' . self::LISTING . '` WHERE post_id = %d', $post_id ) ); } public function test_dry_run_reports_without_writing(): void { $report = TMDO_Post_Group_Reconciler::run( true ); $this->assertSame( 2, $report['moves'][0]['copied'], '101 and 102 hold non-NULL legacy values' ); $this->assertSame( 1, $report['moves'][0]['overwritten'], '101 has a different listing value' ); $this->assertFalse( $report['enabled'] ); $this->assertSame( 'backfill', $this->listing_value( 101 ) ); $this->assertNull( $this->listing_value( 102 ) ); $this->assertSame( '0', get_option( WPDO_Entity_Registry::SCOPED_ROUTING_OPTION ) ); } public function test_run_moves_the_values_reads_returned_before_the_switch(): void { $report = TMDO_Post_Group_Reconciler::run( false, 1 ); $this->assertSame( 'live', $this->listing_value( 101 ) ); $this->assertSame( 'only-in-vendor', $this->listing_value( 102 ) ); $this->assertSame( 'keep', $this->listing_value( 103 ) ); $this->assertSame( 'backfill-104', $this->listing_value( 104 ), 'NULL legacy values do not erase the listing value' ); $this->assertNull( $this->listing_value( 202 ), 'vendor posts are not copied into the listing table' ); $this->assertSame( 2, $report['moves'][0]['copied'] ); } public function test_other_columns_of_the_target_row_are_untouched(): void { global $wpdb; TMDO_Post_Group_Reconciler::run( false ); $this->assertSame( 5.0, (float) $wpdb->get_var( 'SELECT price FROM `' . self::LISTING . '` WHERE post_id = 101' ) ); } public function test_completion_turns_scoped_routing_on_and_clears_the_cursor(): void { $report = TMDO_Post_Group_Reconciler::run( false ); $this->assertTrue( $report['enabled'] ); $this->assertSame( '1', get_option( WPDO_Entity_Registry::SCOPED_ROUTING_OPTION ) ); $this->assertFalse( get_option( TMDO_Post_Group_Reconciler::CURSOR_OPTION ) ); } public function test_catch_up_copies_rows_written_during_the_run(): void { global $wpdb; // Simulates a legacy-routed write that lands after the batch passed post 101. $wpdb->query( 'UPDATE `' . self::VENDOR . "` SET shared_key = 'written during run', updated_at = NOW() + INTERVAL 1 HOUR WHERE post_id = 101" ); update_option( TMDO_Post_Group_Reconciler::CURSOR_OPTION, array( 'rc_vendor>rc_listing:shared_key' => 101 ) ); TMDO_Post_Group_Reconciler::run( false ); $this->assertSame( 'written during run', $this->listing_value( 101 ) ); } public function test_resumes_after_the_stored_cursor(): void { update_option( TMDO_Post_Group_Reconciler::CURSOR_OPTION, array( 'rc_vendor>rc_listing:shared_key' => 101 ) ); TMDO_Post_Group_Reconciler::run( false ); $this->assertSame( 'backfill', $this->listing_value( 101 ), 'rows up to the cursor were already done' ); $this->assertSame( 'only-in-vendor', $this->listing_value( 102 ) ); } }