10 Commits

Author SHA1 Message Date
wpdev d6fd831416 Merge pull request 'release 0.5.3: 2meet-data-optimizer-hivepress-addon' (#7) from release/0.5.3 into main
Tests / PHP Lint (push) Successful in 6s
Tests / Unit Tests (push) Successful in 1m6s
Tests / Integration Tests (push) Successful in 1m6s
2026-10-07 14:02:51 +08:00
wpdev 763f705ac1 fix(2meet-data-optimizer-hivepress-addon): 0.5.3 view-count flush keeps flat row and meta cache in sync
Tests / PHP Lint (pull_request) Successful in 6s
Tests / Unit Tests (pull_request) Successful in 53s
Tests / Integration Tests (pull_request) Successful in 1m13s
flush_views_to_postmeta() adds warm-zone views back with a direct UPDATE,
which the Hook Bus never sees: under dual_write/shadow_read the
hp_listing_core.hp_view_count column drifted after every hourly batch and
the (Redis) object cache kept the old count. After the UPDATE, drop the
post's meta cache and, while native is still the source of truth, rebuild
the entity's flat row with migrate_entity().
2026-10-07 10:37:57 +08:00
wpdev 5953a5ed3b fix(2meet-data-optimizer-hivepress-addon): 0.5.2 attribute bridge uses hp_{slug} meta keys
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:30:09 +08:00
wpdev 6909bf6a99 Merge pull request 'release 0.5.1: 2meet-data-optimizer-hivepress-addon' (#6) from release/0.5.1 into main
Tests / PHP Lint (push) Successful in 9s
Tests / Unit Tests (push) Successful in 1m1s
Tests / Integration Tests (push) Successful in 1m47s
2026-10-02 20:46:33 +08:00
wpdev 9803f8379d chore(2meet-data-optimizer-hivepress-addon): 0.5.1 repackage (no functional change)
Tests / PHP Lint (pull_request) Successful in 7s
Tests / Unit Tests (pull_request) Successful in 56s
Tests / Integration Tests (pull_request) Successful in 1m22s
Version bump for a re-release packaged alongside the WP-MVB v8 hub (dev39)
acceptance; code is identical to the previous release.
2026-10-02 17:23:21 +08:00
wpdev f7f9bc5cc4 Merge pull request 'hardening 2.4: 2meet-data-optimizer-hivepress-addon 0.5.0 (sort indexes, a day's transient TTL)' (#5) from hardening/2.4 into main
Tests / PHP Lint (push) Successful in 7s
Tests / Unit Tests (push) Successful in 1m24s
Tests / Integration Tests (push) Successful in 1m24s
2026-09-27 23:57:57 +08:00
wpdev bfe8912600 chore(2meet-data-optimizer-hivepress-addon): 0.5.0 release notes and version
Tests / PHP Lint (pull_request) Successful in 7s
Tests / Unit Tests (pull_request) Successful in 1m23s
Tests / Integration Tests (pull_request) Successful in 1m16s
- CHANGELOG: PR-119 (sort indexes ending with post_id), PR-120 (a day's TTL)
- Version 0.4.0 -> 0.5.0 (header, TMDO_HIVEPRESS_VERSION)
- PR: PR-125
2026-09-27 23:29:33 +08:00
wpdev 15bbf335f4 perf(2meet-data-optimizer-hivepress-addon): keep HivePress transients a day in the object cache
- MAX_CACHE_TTL: a week -> a day (HivePress's own default); a timeout still
  outlives its value by a minute
- dev36 (1.3M listings): 4.7M entries / 1 GB in the shared Redis at a week,
  evicting other sites' keys
- Verify: composer test (148), integration (53), phpcs clean on the file
- PR: PR-120
2026-09-27 23:26:08 +08:00
wpdev 0cd2a9db5b perf(2meet-data-optimizer-hivepress-addon): (column, post_id) indexes for the listing sorts HivePress offers
- hp_listing_core declares (hp_price, post_id), (hp_rating, post_id) and
  (hp_hourly_rate, post_id): with core 1.4.0 a flat-first price, rating or
  hourly-rate sort reads them in order instead of sorting every match
  (dev36 stage 3: the price-range + price-sort search was the slowest routed
  page, 2.9 s at 1.3M listings); core 1.3.0 ignores indexes ending with post_id
- the pinned schema hash is unchanged (indexes are not part of it)
- Verify: HivePressEntityGroupsTest, unit 148, integration 53, phpcs;
  dev36 front-end sorts
- PR: PR-119
2026-09-27 22:41:46 +08:00
wpdev d802bf0145 Merge pull request 'hardening 2.3: 2meet-data-optimizer-hivepress-addon 0.4.0 (listing indexes, transient TTLs, site attributes)' (#4) from hardening/2.3 into main
Tests / PHP Lint (push) Successful in 24s
Tests / Unit Tests (push) Successful in 1m15s
Tests / Integration Tests (push) Successful in 1m32s
2026-09-27 18:37:50 +08:00
9 changed files with 121 additions and 15 deletions
+2 -2
View File
@@ -3,7 +3,7 @@
* Plugin Name: 2meet Data Optimizer — Hivepress AddOn * Plugin Name: 2meet Data Optimizer — Hivepress AddOn
* Plugin URI: https://2meet.io/2meet-data-optimizer/hivepress * Plugin URI: https://2meet.io/2meet-data-optimizer/hivepress
* Description: HivePress + 12 擴充整合:13 adapter (core/reviews/bookings/messages/memberships/requests/favorites/statistics/tags/seo/social-links/blocks/marketplace) + 7 HPCT interceptor + 5 query interceptor + admin tab + CLI namespace * Description: HivePress + 12 擴充整合:13 adapter (core/reviews/bookings/messages/memberships/requests/favorites/statistics/tags/seo/social-links/blocks/marketplace) + 7 HPCT interceptor + 5 query interceptor + admin tab + CLI namespace
* Version: 0.4.0 * Version: 0.5.3
* Requires at least: 6.0 * Requires at least: 6.0
* Tested up to: 6.9.4 * Tested up to: 6.9.4
* Requires PHP: 8.1 * Requires PHP: 8.1
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
} }
define( 'TMDO_HIVEPRESS_VERSION', '0.4.0' ); define( 'TMDO_HIVEPRESS_VERSION', '0.5.3' );
define( 'TMDO_HIVEPRESS_PATH', plugin_dir_path( __FILE__ ) ); define( 'TMDO_HIVEPRESS_PATH', plugin_dir_path( __FILE__ ) );
define( 'TMDO_HIVEPRESS_URL', plugin_dir_url( __FILE__ ) ); define( 'TMDO_HIVEPRESS_URL', plugin_dir_url( __FILE__ ) );
define( 'TMDO_HIVEPRESS_FILE', __FILE__ ); define( 'TMDO_HIVEPRESS_FILE', __FILE__ );
+51
View File
@@ -11,6 +11,57 @@ Versioning follows [Semantic Versioning](https://semver.org/).
--- ---
## [0.5.3] — 2026-10-07 — 瀏覽數 flush 不再造成漂移
### Fixed
- **`TMDO_Listing_Stats::flush_views_to_postmeta()` 寫完後同步扁平表與快取**:每小時把 warm zone 的瀏覽數
用原子 `UPDATE … meta_value + N` 加回 `hp_view_count`,繞過了 meta API:Hook Bus 看不到這次寫入,
dual_write/shadow_read 下 `hp_listing_core.hp_view_count` 每批落後一次(夜間 drift-check 每晚 diff),
物件快取(Redis 下跨請求)也一直是舊值。現在 UPDATE 之後清掉該篇的 `post_meta` 快取,
並在原生仍是真相時(同時寫原生與扁平)用 `TMDO_Entity_Migration_Engine::migrate_entity()` 重建那一列。
`hp_view_count` 同時在 `hp_listing_core` 與 `hp_request_core`:只重建含這個 key、而且適用該篇 post type 的群組
(不帶 post type 的 `get_field()` 只回最後登記的那組)。aeav_only 不動(扁平才是真相)。
---
## [0.5.2] — 2026-10-05 — attribute bridge 的 meta key
### Fixed
- **`TMDO_HivePress_Attribute_Bridge` 組出正確的 meta key**:HivePress 把每個模型的屬性存成
`hp_{slug}`(例如 `hp_warranty`),不是 `hp_{model}_{slug}`。舊的 key 在資料庫裡不存在,
照它建議的欄位登記後永遠是空的。
---
## [0.5.1] — 2026-10-02 — 重新打包
### Changed
- 重新打包(無功能變更):與 WP-MVB v8 樞紐站(dev39)一同驗收後發版;程式碼與前一版相同。
---
## [0.5.0] — 2026-09-28
Released with 2meet-data-optimizer 1.4.0 (hardening 2.4).
### Changed
- `hp_listing_core` declares `(hp_price, post_id)`, `(hp_rating, post_id)` and
`(hp_hourly_rate, post_id)` indexes for the listing sorts HivePress offers:
with core 1.4.0 reading the ID tie-break from the flat `post_id`, a sort walks
the index and stops at the page instead of sorting every match (dev36, 1.3M
listings: price sort 14 s native → 0.37 s routed, hourly rate 11.5 s → 0.35 s,
rating 43 s → 0.51 s). Core 1.3.0 ignores indexes ending with `post_id`, so
nothing changes there. The pinned schema hash is unchanged. (PR-119)
- Transient entries in a persistent object cache live a day (`MAX_CACHE_TTL`,
HivePress's own default) instead of a week; a timeout still outlives its value
by a minute. At a week dev36 kept 4.7M entries (1 GB) in the shared Redis and
pushed other sites' keys out. Entries already cached keep their week. (PR-120)
---
## [0.4.0] — 2026-09-27 ## [0.4.0] — 2026-09-27
Released with 2meet-data-optimizer 1.3.0 (hardening 2.3). Released with 2meet-data-optimizer 1.3.0 (hardening 2.3).
@@ -266,8 +266,13 @@ final class TMDO_HivePress_Entity_Groups {
// Only with core 1.3.0+ (ddl_allowed()): older cores run the ALTER // Only with core 1.3.0+ (ddl_allowed()): older cores run the ALTER
// inside whatever request comes first, a visitor's included. // inside whatever request comes first, a visitor's included.
'indexes' => method_exists( 'TMDO_Schema_Manager', 'ddl_allowed' ) ? array( 'indexes' => method_exists( 'TMDO_Schema_Manager', 'ddl_allowed' ) ? array(
'hourly_rate' => array( 'hp_hourly_rate' ), 'hourly_rate' => array( 'hp_hourly_rate' ),
'rating_count' => array( 'hp_rating_count' ), 'rating_count' => array( 'hp_rating_count' ),
// HivePress sorts: the value then the ID tie-break, which core 1.4.0
// reads in order (flat-first) instead of sorting every match.
'price_id' => array( 'hp_price', 'post_id' ),
'rating_id' => array( 'hp_rating', 'post_id' ),
'hourly_rate_id' => array( 'hp_hourly_rate', 'post_id' ),
) : array(), ) : array(),
) )
); );
@@ -80,8 +80,8 @@ final class TMDO_Hivepress_Transient_Filter {
/** Object cache group used instead of wp_options when a persistent cache is present. */ /** Object cache group used instead of wp_options when a persistent cache is present. */
public const CACHE_GROUP = 'tmdo_hp_transient'; public const CACHE_GROUP = 'tmdo_hp_transient';
/** Longest an object-cache entry lives: a week (HivePress's default is a day). */ /** Longest an object-cache entry lives: a day, HivePress's own default (2.4 PR-120). */
public const MAX_CACHE_TTL = 604800; public const MAX_CACHE_TTL = DAY_IN_SECONDS;
/** /**
* Register the four metadata filters on `init`. Idempotent — safe to call * Register the four metadata filters on `init`. Idempotent — safe to call
+36
View File
@@ -154,6 +154,8 @@ class TMDO_Listing_Stats {
// Seed the key when it does not exist yet. // Seed the key when it does not exist yet.
if ( ! $updated ) { if ( ! $updated ) {
add_post_meta( $post_id, 'hp_view_count', $new_views, true ); add_post_meta( $post_id, 'hp_view_count', $new_views, true );
} else {
self::resync_view_count( $post_id );
} }
// Remove the warm entry after flush. // Remove the warm entry after flush.
@@ -164,6 +166,40 @@ class TMDO_Listing_Stats {
return $flushed; return $flushed;
} }
/**
* The atomic UPDATE above bypasses the meta API: the object cache (persistent
* under Redis) keeps the old count, and the Hook Bus never sees the write, so in
* dual_write / shadow_read the flat column drifts by every flushed batch
* (drift-check diff on hp_view_count each night). Drop the cached meta and
* rebuild the entity's flat row from native — only while native is the source
* of truth (aeav_only keeps flat authoritative).
*
* @param int $post_id Listing post ID.
*/
private static function resync_view_count( int $post_id ): void {
wp_cache_delete( $post_id, 'post_meta' );
if ( ! class_exists( 'TMDO_Mode_Manager' ) || ! class_exists( 'TMDO_Entity_Registry' ) || ! class_exists( 'TMDO_Entity_Migration_Engine' ) ) {
return;
}
if ( ! TMDO_Mode_Manager::writes_to_flat( 'post' ) || ! TMDO_Mode_Manager::writes_to_eav( 'post' ) ) {
return;
}
// hp_view_count lives in more than one group (hp_listing_core, hp_request_core):
// get_field() without a post type returns whichever registered last. Rebuild only
// the groups that hold the key and apply to this post's type.
$type = (string) get_post_type( $post_id );
foreach ( TMDO_Entity_Registry::get_groups_for_type( 'post' ) as $group ) {
$types = TMDO_Entity_Registry::get_group_post_types( 'post', $group );
if ( ! in_array( 'hp_view_count', TMDO_Entity_Registry::get_group_keys( 'post', $group ), true ) || ( $types && ! in_array( $type, $types, true ) ) ) {
continue;
}
TMDO_Entity_Migration_Engine::migrate_entity( 'post', $group, $post_id );
if ( class_exists( 'TMDO_Cache_Orchestrator' ) ) {
TMDO_Cache_Orchestrator::invalidate( 'post', $post_id, $group );
}
}
}
// ── Zone D: Expired Listing Archival ───────────────────────────────────── // ── Zone D: Expired Listing Archival ─────────────────────────────────────
/** /**
@@ -20,7 +20,7 @@
* Usage: * Usage:
* $bridge = new TMDO_HivePress_Attribute_Bridge(); * $bridge = new TMDO_HivePress_Attribute_Bridge();
* $candidates = $bridge->discover_attributes(); * $candidates = $bridge->discover_attributes();
* // returns: [['slug' => 'beds', 'type' => 'integer', 'filterable' => true, 'searchable' => true, 'meta_key' => 'hp_listing_beds'], ...] * // returns: [['slug' => 'beds', 'type' => 'integer', 'filterable' => true, 'searchable' => true, 'meta_key' => 'hp_beds'], ...]
* *
* @package WP_Data_Optimizer * @package WP_Data_Optimizer
* @since 3.0.0 * @since 3.0.0
@@ -123,7 +123,8 @@ if ( ! class_exists( 'TMDO_HivePress_Attribute_Bridge' ) ) {
return array( return array(
'model' => $model, 'model' => $model,
'slug' => $slug, 'slug' => $slug,
'meta_key' => sprintf( 'hp_%s_%s', $model, $slug ), // HivePress stores every model's attribute as hp_{slug} (e.g. hp_warranty), not hp_{model}_{slug}.
'meta_key' => 'hp_' . $slug,
'type' => (string) ( $cfg['edit_field']['type'] ?? $cfg['search_field']['type'] ?? 'text' ), 'type' => (string) ( $cfg['edit_field']['type'] ?? $cfg['search_field']['type'] ?? 'text' ),
'filterable' => (bool) ( $cfg['filterable'] ?? false ), 'filterable' => (bool) ( $cfg['filterable'] ?? false ),
'sortable' => (bool) ( $cfg['sortable'] ?? false ), 'sortable' => (bool) ( $cfg['sortable'] ?? false ),
@@ -215,11 +215,14 @@ class HivepressTransientFilterTest extends TestCase {
/** /**
* Entries expire in the object cache too (2.3 PR-099): the value (written * Entries expire in the object cache too (2.3 PR-099): the value (written
* first, before its timeout is known) after a week, the timeout after the * first, before its timeout is known) after a day, the timeout after the
* later of that and its own time, plus a minute. Orphans of deleted posts * later of that and its own time, plus a minute. Orphans of deleted posts
* no longer pile up. * no longer pile up.
*/ */
public function test_with_an_object_cache_entries_get_a_ttl(): void { public function test_with_an_object_cache_entries_get_a_ttl(): void {
// A day, HivePress's own default: at a week, 1.3M listings kept 4.7M
// entries (1 GB) in a shared Redis and pushed other sites' keys out (2.4 PR-120).
$this->assertSame( 86400, WPDO_Hivepress_Transient_Filter::MAX_CACHE_TTL );
$GLOBALS['_wp_using_ext_object_cache'] = true; $GLOBALS['_wp_using_ext_object_cache'] = true;
$key = '_transient_hp_models/cat/v1'; $key = '_transient_hp_models/cat/v1';
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key ); $translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
+1 -1
View File
@@ -62,7 +62,7 @@ class HivePressAttributeBridgeTest extends TestCase {
$this->assertSame( 'listing', $record['model'] ); $this->assertSame( 'listing', $record['model'] );
$this->assertSame( 'beds', $record['slug'] ); $this->assertSame( 'beds', $record['slug'] );
$this->assertSame( 'hp_listing_beds', $record['meta_key'] ); $this->assertSame( 'hp_beds', $record['meta_key'] );
$this->assertSame( 'integer', $record['type'] ); $this->assertSame( 'integer', $record['type'] );
$this->assertTrue( $record['filterable'] ); $this->assertTrue( $record['filterable'] );
$this->assertFalse( $record['sortable'] ); $this->assertFalse( $record['sortable'] );
+15 -5
View File
@@ -37,9 +37,15 @@ class HivePressEntityGroupsTest extends TestCase {
'hp_user' => 'hp_vendor_core', 'hp_user' => 'hp_vendor_core',
); );
private function post_adapter(): TMDO_Entity_Adapter_Interface {
$adapter = $this->createMock( TMDO_Entity_Adapter_Interface::class );
$adapter->method( 'get_entity_id_column' )->willReturn( 'post_id' );
return $adapter;
}
protected function setUp(): void { protected function setUp(): void {
TMDO_Entity_Registry::init(); TMDO_Entity_Registry::init();
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) ); TMDO_Entity_Registry::register_adapter( 'post', $this->post_adapter() );
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) ); TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
TMDO_HivePress_Entity_Groups::register_entity_fields(); TMDO_HivePress_Entity_Groups::register_entity_fields();
} }
@@ -66,8 +72,12 @@ class HivePressEntityGroupsTest extends TestCase {
public function test_listing_columns_filtered_and_sorted_on_are_indexed(): void { public function test_listing_columns_filtered_and_sorted_on_are_indexed(): void {
$this->assertSame( $this->assertSame(
array( array(
'hourly_rate' => array( 'hp_hourly_rate' ), 'hourly_rate' => array( 'hp_hourly_rate' ),
'rating_count' => array( 'hp_rating_count' ), 'rating_count' => array( 'hp_rating_count' ),
// Sorts: the value then the ID tie-break, read in order (2.4 PR-119).
'price_id' => array( 'hp_price', 'post_id' ),
'rating_id' => array( 'hp_rating', 'post_id' ),
'hourly_rate_id' => array( 'hp_hourly_rate', 'post_id' ),
), ),
TMDO_Entity_Registry::get_group_indexes( 'post', 'hp_listing_core' ) TMDO_Entity_Registry::get_group_indexes( 'post', 'hp_listing_core' )
); );
@@ -94,7 +104,7 @@ class HivePressEntityGroupsTest extends TestCase {
$GLOBALS['_wp_did_action']['after_setup_theme'] = 1; $GLOBALS['_wp_did_action']['after_setup_theme'] = 1;
try { try {
TMDO_Entity_Registry::init(); TMDO_Entity_Registry::init();
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) ); TMDO_Entity_Registry::register_adapter( 'post', $this->post_adapter() );
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) ); TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
TMDO_HivePress_Entity_Groups::register_entity_fields(); TMDO_HivePress_Entity_Groups::register_entity_fields();
} finally { } finally {
@@ -118,7 +128,7 @@ class HivePressEntityGroupsTest extends TestCase {
add_filter( 'tmdo_hp_listing_attribute_fields', static fn() => array( 'hp_warranty' => 'integer' ) ); add_filter( 'tmdo_hp_listing_attribute_fields', static fn() => array( 'hp_warranty' => 'integer' ) );
try { try {
TMDO_Entity_Registry::init(); TMDO_Entity_Registry::init();
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) ); TMDO_Entity_Registry::register_adapter( 'post', $this->post_adapter() );
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) ); TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
TMDO_HivePress_Entity_Groups::register_entity_fields(); TMDO_HivePress_Entity_Groups::register_entity_fields();
$this->assertSame( array(), TMDO_Entity_Registry::get_group_keys( 'post', 'hp_listing_attrs' ), 'plugins_loaded run: not yet' ); $this->assertSame( array(), TMDO_Entity_Registry::get_group_keys( 'post', 'hp_listing_attrs' ), 'plugins_loaded run: not yet' );