a7134bfb80
- Deleting a post / user / term / comment drops its flat row in the adapter delete hook, then WordPress deletes each meta row; sync_from_native upserted NULL for each and brought the row back (512 orphan rows on dev36). With no native value it now only clears the column on an existing row - Verify: tests/live/run.php resync (25/25, entity deletes in dual_write and shadow_read leave no flat row); mut.php T8 flat_row NO-ROW; integration 516 - PR: PR-057b
139 lines
6.1 KiB
PHP
139 lines
6.1 KiB
PHP
<?php
|
|
/**
|
|
* Suite: in dual_write / shadow_read the flat value always follows native meta.
|
|
*
|
|
* Before PR-057 the Hook Bus wrote flat in the pre-filter, before WP decided
|
|
* whether native changed at all: a non-matching prev_value, add($unique) on an
|
|
* existing key or a key change via update_metadata_by_mid left flat and native
|
|
* apart, and nothing healed a flat value made stale by a direct SQL write.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Entity cases: [entity type, key, fixture factory, post type for group lookup].
|
|
*
|
|
* @return array<string, array{0:string, 1:string, 2:callable, 3:string}>
|
|
*/
|
|
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" );
|
|
}
|
|
}
|
|
}
|
|
);
|