fix(2meet-data-optimizer): resync flat rows from native meta after each write
- dual_write / shadow_read no longer write flat in the update_/add_/
delete_{type}_metadata pre-filters, before WP decides whether native
changes at all. added_/updated_/deleted_{type}_meta (priority 1) call
TMDO_Hook_Bus::sync_from_native(), which copies the entity's first native
row (plain SQL, NULL when gone) into flat. Flat now follows native when WP
skips (same value), refuses (non-matching prev_value, add $unique on an
existing key) or deletes (also delete_all: touched IDs are collected from
the meta IDs in delete_{type}_meta), and a flat value left stale by a
direct SQL write heals on the next write.
- update_metadata_by_mid key changes: the old key is remembered in
update_{type}_metadata_by_mid and re-synced too.
- wpdo_after_write / wpdo_after_delete fire from the sync with the same
arguments; aeav_only keeps the pre-filter short-circuit unchanged.
- $internal_ops lock key includes the entity type (user 5 ≠ post 5).
- Live suite resync: 4 entities × 2 modes × 3 scenarios (old code: 16 of
24 FAIL); integration: pre-filter leaves flat to the sync, delete_all
re-syncs every touched user.
- Verify: live resync + shadow_write + serialized + zero_side_effect 33/33
PASS; unit 687 OK, integration 485 OK, phpcs (phpcs.xml) 0, stan OK
- PR: PR-057
This commit is contained in:
@@ -38,6 +38,7 @@ Exit code 0 when every test passed, 1 otherwise. Output per test:
|
||||
|---|---|
|
||||
| `shadow_write` | writes in shadow_read reach native meta (user, term, HivePress listing) |
|
||||
| `serialized` | array meta values are stored serialized in flat tables, never "Array" |
|
||||
| `resync` | dual_write / shadow_read: flat follows native after every write (4 entities × 2 modes: update, same value, delete, refused writes, stale flat, key rename by meta_id) |
|
||||
| `zero_side_effect` | zone tables that were never created are never queried on save / delete |
|
||||
|
||||
## Tools (`tools/`)
|
||||
|
||||
@@ -113,6 +113,29 @@ final class TMDO_Live_Fixtures {
|
||||
return (int) $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a throwaway comment (on a throwaway post of $post_type).
|
||||
*
|
||||
* @param string $post_type Post type of the parent post.
|
||||
* @param string $comment_type Comment type.
|
||||
*/
|
||||
public static function comment( string $post_type, string $comment_type = 'comment' ): int {
|
||||
$post_id = self::post( $post_type );
|
||||
$id = wp_insert_comment(
|
||||
array(
|
||||
'comment_post_ID' => $post_id,
|
||||
'comment_content' => 'tmdo-live',
|
||||
'comment_type' => $comment_type,
|
||||
'comment_approved' => 1,
|
||||
)
|
||||
);
|
||||
if ( ! $id ) {
|
||||
throw new TMDO_Live_Failure( 'fixture comment could not be created' );
|
||||
}
|
||||
self::$created[] = array( 'comment', (int) $id );
|
||||
return (int) $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Native meta values for a key, read with SQL (no filters, no cache).
|
||||
*
|
||||
@@ -191,6 +214,8 @@ final class TMDO_Live_Fixtures {
|
||||
wp_delete_term( $c[1], $c[2] );
|
||||
} elseif ( 'post' === $c[0] ) {
|
||||
wp_delete_post( $c[1], true );
|
||||
} elseif ( 'comment' === $c[0] ) {
|
||||
wp_delete_comment( $c[1], true );
|
||||
}
|
||||
}
|
||||
self::$created = array();
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?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' );
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user