*/ function tmdo_live_resync_cases(): array { return array( 'user first_name' => array( 'user', 'first_name', array( 'TMDO_Live_Fixtures', 'user' ), '' ), 'hp_listing price' => array( 'post', 'hp_price', static fn() => TMDO_Live_Fixtures::post( 'hp_listing' ), 'hp_listing' ), 'hp term icon' => array( 'term', 'hp_icon', static fn() => TMDO_Live_Fixtures::term( 'hp_listing_category' ), '' ), 'hp review rating' => array( 'comment', 'hp_rating', static fn() => TMDO_Live_Fixtures::comment( 'hp_listing', 'hp_review' ), '' ), ); } /** * Assert flat equals the first native value (numeric-aware; missing native = NULL flat). */ function tmdo_live_assert_in_step( string $type, int $id, string $key, string $post_type, string $context ): void { $native = TMDO_Live_Fixtures::native( $type, $id, $key ); $flat = TMDO_Live_Fixtures::flat( $type, $id, $key, $post_type ); $expect = $native ? $native[0] : null; if ( 'NO-ROW' === $flat ) { $flat = null; } $same = ( null === $expect || null === $flat ) ? $expect === $flat : ( is_numeric( $expect ) && is_numeric( $flat ) ? (float) $expect === (float) $flat : $expect === $flat ); tmdo_live_assert_true( $same, sprintf( '%s — native %s, flat %s', $context, var_export( $expect, true ), var_export( $flat, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export } foreach ( array( 'dual_write', 'shadow_read' ) as $tmdo_live_mode ) { foreach ( tmdo_live_resync_cases() as $tmdo_live_label => $tmdo_live_case ) { list( $type, $key, $make, $pt ) = $tmdo_live_case; TMDO_Live::test( "{$tmdo_live_mode} {$tmdo_live_label}: update, same value, delete", static function () use ( $tmdo_live_mode, $type, $key, $make, $pt ) { TMDO_Live_Fixtures::field( $type, $key, $pt ); TMDO_Live_Fixtures::use_modes( array( $type => $tmdo_live_mode ) ); $id = $make(); update_metadata( $type, $id, $key, '11' ); tmdo_live_assert_in_step( $type, $id, $key, $pt, 'new key' ); update_metadata( $type, $id, $key, '12' ); tmdo_live_assert_in_step( $type, $id, $key, $pt, 'update' ); update_metadata( $type, $id, $key, '12' ); tmdo_live_assert_in_step( $type, $id, $key, $pt, 'same value' ); delete_metadata( $type, $id, $key, '12' ); tmdo_live_assert_in_step( $type, $id, $key, $pt, 'delete with value' ); } ); TMDO_Live::test( "{$tmdo_live_mode} {$tmdo_live_label}: writes WP refuses leave flat alone", static function () use ( $tmdo_live_mode, $type, $key, $make, $pt ) { TMDO_Live_Fixtures::field( $type, $key, $pt ); TMDO_Live_Fixtures::use_modes( array( $type => $tmdo_live_mode ) ); $id = $make(); update_metadata( $type, $id, $key, '21' ); update_metadata( $type, $id, $key, '22', 'no-such-previous-value' ); tmdo_live_assert_in_step( $type, $id, $key, $pt, 'non-matching prev_value' ); add_metadata( $type, $id, $key, '23', true ); tmdo_live_assert_in_step( $type, $id, $key, $pt, 'add unique on existing key' ); update_metadata( $type, $id, $key, '24', '21' ); tmdo_live_assert_in_step( $type, $id, $key, $pt, 'matching prev_value' ); } ); TMDO_Live::test( "{$tmdo_live_mode} {$tmdo_live_label}: stale flat heals on the next write, key change by meta_id", static function () use ( $tmdo_live_mode, $type, $key, $make, $pt ) { global $wpdb; TMDO_Live_Fixtures::field( $type, $key, $pt ); TMDO_Live_Fixtures::use_modes( array( $type => $tmdo_live_mode ) ); $id = $make(); update_metadata( $type, $id, $key, '31' ); $table = _get_meta_table( $type ); $id_col = $type . '_id'; $pk = 'user' === $type ? 'umeta_id' : 'meta_id'; // A direct SQL write (as WooCommerce does for stock) leaves flat stale. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WP meta table names. $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET meta_value = '32' WHERE {$id_col} = %d AND meta_key = %s", $id, $key ) ); wp_cache_delete( $id, $type . '_meta' ); update_metadata( $type, $id, $key, '33' ); tmdo_live_assert_in_step( $type, $id, $key, $pt, 'next write after a direct SQL change' ); // Renaming the key by meta_id removes it from native: flat must follow. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WP meta table names. $mid = (int) $wpdb->get_var( $wpdb->prepare( "SELECT {$pk} FROM {$table} WHERE {$id_col} = %d AND meta_key = %s", $id, $key ) ); update_metadata_by_mid( $type, $mid, '34', 'tmdo_live_renamed' ); tmdo_live_assert_in_step( $type, $id, $key, $pt, 'key renamed by meta_id' ); } ); } } TMDO_Live::test( 'deleting an entity leaves no flat row behind (dual_write and shadow_read)', static function () { require_once ABSPATH . 'wp-admin/includes/user.php'; $deleters = array( 'user' => static fn( int $id ) => wp_delete_user( $id ), 'post' => static fn( int $id ) => wp_delete_post( $id, true ), 'term' => static fn( int $id ) => wp_delete_term( $id, 'hp_listing_category' ), 'comment' => static fn( int $id ) => wp_delete_comment( $id, true ), ); foreach ( array( 'dual_write', 'shadow_read' ) as $mode ) { foreach ( tmdo_live_resync_cases() as $label => $case ) { list( $type, $key, $make, $pt ) = $case; TMDO_Live_Fixtures::field( $type, $key, $pt ); TMDO_Live_Fixtures::use_modes( array( $type => $mode ) ); $id = $make(); update_metadata( $type, $id, $key, '41' ); $deleters[ $type ]( $id ); tmdo_live_assert_same( 'NO-ROW', TMDO_Live_Fixtures::flat( $type, $id, $key, $pt ), "{$mode} {$label}: flat row after delete" ); } } } );