Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e57dd5bd15 | |||
| b2585459e3 | |||
| d129139c7d | |||
| f06a80eae5 | |||
| 130678a8e6 | |||
| 788ddccfb3 | |||
| c6faaebbc1 | |||
| ef7e005583 | |||
| 99f4653b25 | |||
| bab91ef6b9 | |||
| b515b8441e | |||
| fda9e43087 |
@@ -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.2.0
|
* Version: 0.4.0
|
||||||
* 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.2.0' );
|
define( 'TMDO_HIVEPRESS_VERSION', '0.4.0' );
|
||||||
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__ );
|
||||||
|
|||||||
@@ -11,6 +11,80 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.4.0] — 2026-09-27
|
||||||
|
|
||||||
|
Released with 2meet-data-optimizer 1.3.0 (hardening 2.3).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `hp_listing_core` declares indexes on `hp_hourly_rate` and `hp_rating_count`
|
||||||
|
(HivePress filters and sorts on both). They are declared through the group's
|
||||||
|
`indexes`, not `searchable`, so the pinned schema hash is unchanged and the
|
||||||
|
table is not rebuilt; core 1.3.0 adds them in place from WP-CLI / WP-Cron.
|
||||||
|
dev36 (902k listings): two indexes in about 4.7 s; routed `rating_count IN`
|
||||||
|
2.1 s → 0.36 s, `rating_count !=` 2.1 s → 0.76 s, hourly rate + price sort
|
||||||
|
2.1 s → 0.65 s. (PR-090)
|
||||||
|
- With a persistent object cache, transient entries now expire: a timeout
|
||||||
|
entry when it records, a value after at most a week (`MAX_CACHE_TTL`;
|
||||||
|
HivePress writes the value before its timeout, and a miss only makes it
|
||||||
|
rebuild). Entries of deleted posts no longer pile up. (PR-099)
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
- Deploy with core 1.3.0: on an older core the AddOn declares no index at all
|
||||||
|
(core 1.2.0 would run the ALTER inside a web request). (PR-090b)
|
||||||
|
- Redis: entries written by 0.3.0 have no TTL (dev36 had 3.0 M). Give them one
|
||||||
|
with non-blocking commands — never `wp cache flush-group`, which the Redis
|
||||||
|
Object Cache drop-in runs as one server-side script and which blocked Redis
|
||||||
|
on a large group. `NX` leaves entries written by 0.4.0 alone:
|
||||||
|
|
||||||
|
```
|
||||||
|
redis-cli --scan --pattern '<WP_CACHE_KEY_SALT>wp:tmdo_hp_transient:*' \
|
||||||
|
| awk '{printf "EXPIRE %s 604800 NX\r\n", $0}' | redis-cli --pipe
|
||||||
|
```
|
||||||
|
|
||||||
|
dev36: 3,013,352 keys in 45 s, 0 errors, Redis latency max 2 ms. The pattern
|
||||||
|
follows the drop-in's key layout (`<salt><blog prefix>:<group>:<key>`);
|
||||||
|
check it with `redis-cli --scan --pattern '<salt>*tmdo_hp_transient*' | head`
|
||||||
|
first. redis-cli 7 has no `--count` with `--scan`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.3.0] — 2026-09-27
|
||||||
|
|
||||||
|
Released with 2meet-data-optimizer 1.2.0 (hardening 2.2).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- With a persistent object cache (`wp_using_ext_object_cache()`), the
|
||||||
|
transient filter keeps HivePress's per-post transient pairs in the
|
||||||
|
`tmdo_hp_transient` cache group instead of `wp_options`, where they piled
|
||||||
|
up (1.15 M pairs on dev36). The cache is then the only store: rows written
|
||||||
|
before are neither read nor deleted (HivePress rebuilds those caches once),
|
||||||
|
because a `get_option()` miss adds the name to WordPress's `notoptions` for
|
||||||
|
good and a hit keeps the old row in the `options` cache group with no expiry.
|
||||||
|
Without an object cache nothing changes. After upgrading, remove the rows
|
||||||
|
left behind with core's `wp tmdo cleanup-hp-transients --options --confirm`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The detected / bound status cells of the HivePress admin table go through
|
||||||
|
`esc_html()` (2.1 audit); output unchanged.
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
|
||||||
|
- `docs/REGISTERED-KEYS.md`: the six registered keys HivePress never writes
|
||||||
|
as meta, because its models alias them to core columns (listing
|
||||||
|
`hp_vendor` → `post_parent`, `hp_status` → `post_status`; request
|
||||||
|
`hp_status`, `hp_user`; vendor `hp_user`; review `hp_rating` →
|
||||||
|
`comment_karma`). The schema stays; their flat columns are always NULL.
|
||||||
|
- `docs/HIVEPRESS-COVERAGE.md`: dev36 inventory of the meta each HivePress
|
||||||
|
post type holds, where the extensions keep their data and the query shapes
|
||||||
|
the front end produces. Attribute auto-registration is not needed now:
|
||||||
|
every meta key HivePress queries is registered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.2.0] — 2026-09-26
|
## [0.2.0] — 2026-09-26
|
||||||
|
|
||||||
Released with 2meet-data-optimizer 1.1.0 (hardening 2.1, search-index mode).
|
Released with 2meet-data-optimizer 1.1.0 (hardening 2.1, search-index mode).
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ reviews / messages / memberships / requests / listing-meta
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
Registered keys HivePress keeps in core columns instead of meta (`hp_vendor`, `hp_status`, …): see [docs/REGISTERED-KEYS.md](docs/REGISTERED-KEYS.md).
|
||||||
|
What the HivePress extensions write and how they query it (dev36 inventory, D04 decision): see [docs/HIVEPRESS-COVERAGE.md](docs/HIVEPRESS-COVERAGE.md).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- WordPress ≥ 6.0
|
- WordPress ≥ 6.0
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ if ( ! class_exists( 'TMDO_Admin_HivePress' ) ) {
|
|||||||
<tr>
|
<tr>
|
||||||
<td><code><?php echo esc_html( $slug ); ?></code></td>
|
<td><code><?php echo esc_html( $slug ); ?></code></td>
|
||||||
<td><?php echo esc_html( $name ); ?></td>
|
<td><?php echo esc_html( $name ); ?></td>
|
||||||
<td><?php echo $is_detected ? '✅' : '⚪'; ?></td>
|
<td><?php echo esc_html( $is_detected ? '✅' : '⚪' ); ?></td>
|
||||||
<td><?php echo esc_html( $detected[ $slug ] ?? '—' ); ?></td>
|
<td><?php echo esc_html( $detected[ $slug ] ?? '—' ); ?></td>
|
||||||
<td><?php echo $is_bound ? '✅' : '—'; ?></td>
|
<td><?php echo esc_html( $is_bound ? '✅' : '—' ); ?></td>
|
||||||
<td><?php echo null === $score ? '—' : esc_html( (string) $score ); ?></td>
|
<td><?php echo null === $score ? '—' : esc_html( (string) $score ); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# HivePress coverage: what the extensions write and how HivePress queries it
|
||||||
|
|
||||||
|
Read-only inventory on **dev36** (2026-09-27): HivePress 1.7 with blocks, bookings,
|
||||||
|
favorites, marketplace, memberships, messages, requests, reviews, seo, social-links and
|
||||||
|
tags; 101,175 listings, 1,499 vendors, 709 requests. The data is seeded, so features the
|
||||||
|
seed never used show no rows — noted per line. Account pages (favorites, messages,
|
||||||
|
memberships) were not crawled (login).
|
||||||
|
|
||||||
|
## Meta keys written (rows, `hp_*`)
|
||||||
|
|
||||||
|
| Post type | Key | Rows | Registered group | Queried? |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| hp_listing | `hp_hourly_rate` | 101,157 | `hp_listing_core` | yes — filter BETWEEN, sort |
|
||||||
|
| hp_listing | `hp_price` | 100,437 | `hp_listing_core` | yes — filter BETWEEN, sort |
|
||||||
|
| hp_listing | `hp_rating`, `hp_rating_count` | 53,299 | `hp_listing_core` | `hp_rating` sort |
|
||||||
|
| hp_listing | `hp_verified` | 31,985 | `hp_listing_core` | no |
|
||||||
|
| hp_listing | `hp_featured` | 5,131 | `hp_listing_core` | yes — featured block (`= CHAR`, `meta_key` + `rand`) |
|
||||||
|
| hp_listing | `hp_warranty` | 100,401 | — | no (seed data: values are not warranty term ids; see below) |
|
||||||
|
| hp_vendor | `hp_rating`, `hp_hourly_rate`, `hp_rating_count`, `hp_verified` | 761–1,499 | `hp_vendor_core` | `hp_hourly_rate` filter, `hp_rating` sort |
|
||||||
|
| hp_vendor | `hp_availability` | 4 | — | no |
|
||||||
|
| hp_request | `hp_budget` | 709 | `hp_request_core` | yes — filter BETWEEN, `meta_value_num` sort |
|
||||||
|
| hp_request | `hp_due_date` | 709 | — | no |
|
||||||
|
| hp_booking | `hp_start_time`, `hp_end_time` | 1 | `hp_booking_core` | no (1 booking) |
|
||||||
|
| `*_attribute` | `hp_edit_field_*`, `hp_filterable`, … | 3 each | — | attribute settings, read by ID |
|
||||||
|
|
||||||
|
User meta and comment meta: no `hp_*` rows. Term meta: `hp_sort_order` 170, `hp_icon`
|
||||||
|
153 (`hp_taxonomy`).
|
||||||
|
|
||||||
|
## Where the extensions keep their data
|
||||||
|
|
||||||
|
- **Reviews** — `hp_review` comments (5,324); rating in `comment_karma` (see
|
||||||
|
[REGISTERED-KEYS.md](REGISTERED-KEYS.md)).
|
||||||
|
- **Requests / offers** — `hp_request` posts; offers are `hp_offer` comments (48), no meta.
|
||||||
|
- **Messages** — `hp_message` comments (0 on dev36).
|
||||||
|
- **Favorites** — 4 user meta rows (no `hp_` prefix); nothing to flatten.
|
||||||
|
- **Bookings** — `hp_booking` posts (1); **memberships** — no plans on dev36.
|
||||||
|
- **Tags / categories / select attributes** — taxonomies (`hp_listing_tags` 2,160,
|
||||||
|
`hp_listing_category` 101,175, `hp_listing_warranty` 720 relationships). A select
|
||||||
|
filter (`warranty=28`) becomes `tax_query hp_listing_warranty IN`, never a meta query.
|
||||||
|
- **SEO, social links** — no rows on dev36.
|
||||||
|
|
||||||
|
## Query shapes seen (front-end crawl, 18 URLs)
|
||||||
|
|
||||||
|
- Search filters on number attributes: `hp_price` / `hp_hourly_rate` / `hp_budget`
|
||||||
|
`BETWEEN DECIMAL` — routed.
|
||||||
|
- Sort: the named shape `(NOT EXISTS OR EXISTS)` on `hp_price`, `hp_hourly_rate`,
|
||||||
|
`hp_rating` with `orderby=<name>__order` — routed, flat-first since core 1.2.0;
|
||||||
|
requests sort by `meta_key=hp_budget` + `meta_value_num` (a secondary query here) —
|
||||||
|
a shape the router handles.
|
||||||
|
- Featured block: `meta_key=hp_featured` + `orderby=rand`, and `hp_featured = CHAR`.
|
||||||
|
- Every front-end search carries WooCommerce's `product_visibility NOT IN` tax query.
|
||||||
|
- `HivePress\Components\Attribute::set_search_query` runs its own `get_posts()` inside
|
||||||
|
`pre_get_posts` (suppress_filters): not routable (~100 ms on dev36).
|
||||||
|
|
||||||
|
## D04 (auto-register HivePress attributes): not now
|
||||||
|
|
||||||
|
Every meta key HivePress queries on dev36 is already registered; select attributes are
|
||||||
|
taxonomies; the unregistered keys that are written (`hp_due_date`, `hp_availability`,
|
||||||
|
`hp_warranty`) are never queried. Auto-registration would only pay off on a site whose
|
||||||
|
admin adds **filterable or sortable number / date attributes** — revisit then (a doctor
|
||||||
|
check listing such attributes without a registered key would flag it).
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Registered keys HivePress never writes
|
||||||
|
|
||||||
|
Some keys the AddOn registers in its flat groups are **aliases of core columns** in the
|
||||||
|
HivePress models: HivePress reads and writes the column, never a meta row. Their flat
|
||||||
|
columns therefore stay `NULL`, and a meta query on them finds nothing — natively and
|
||||||
|
routed alike.
|
||||||
|
|
||||||
|
| Entity / group | Key | HivePress model field | Stored in |
|
||||||
|
|---|---|---|---|
|
||||||
|
| post / `hp_listing_core` | `hp_vendor` | Listing `vendor` | `wp_posts.post_parent` |
|
||||||
|
| post / `hp_listing_core` | `hp_status` | Listing `status` | `wp_posts.post_status` |
|
||||||
|
| post / `hp_request_core` | `hp_status` | Request `status` | `wp_posts.post_status` |
|
||||||
|
| post / `hp_request_core` | `hp_user` | Request `user` | `wp_posts.post_author` |
|
||||||
|
| post / `hp_vendor_core` | `hp_user` | Vendor `user` | `wp_posts.post_author` |
|
||||||
|
| comment / `hp_review` | `hp_rating` | Review `rating` (hivepress-reviews) | `wp_comments.comment_karma` |
|
||||||
|
|
||||||
|
Sources: `'_alias' => …` in `hivepress/includes/models/class-listing.php`,
|
||||||
|
`class-vendor.php`, `hivepress-requests/includes/models/class-request.php` and
|
||||||
|
`hivepress-reviews/includes/models/class-review.php`.
|
||||||
|
|
||||||
|
## Checked on dev36 (2026-09-27, 101,175 listings)
|
||||||
|
|
||||||
|
- Native meta rows: `hp_vendor` 0, `hp_status` 0, `hp_user` 0, comment `hp_rating` 0.
|
||||||
|
- Instead: 101,175 / 101,175 listings have `post_parent > 0`; listing statuses are
|
||||||
|
`publish` 91,804 / `draft` 9,371; 5,244 of 5,324 `hp_review` comments carry
|
||||||
|
`comment_karma`.
|
||||||
|
|
||||||
|
## What this means
|
||||||
|
|
||||||
|
- Filters and sorts HivePress builds on these fields use the core columns, never
|
||||||
|
`meta_query`, so the router has nothing to route for them.
|
||||||
|
- Drift checks report nothing for them: native and flat are both empty.
|
||||||
|
- **No schema change** (plan decision D03, 2.2): the columns cost only NULLs, and
|
||||||
|
dropping them would need a flat-table migration on every site.
|
||||||
|
|
||||||
|
Not in this list: `hp_expired_time` and `hp_featured_time` are real meta fields of the
|
||||||
|
Listing model (no alias); they are empty on dev36 only because its seeded listings
|
||||||
|
never expire or get featured.
|
||||||
@@ -257,7 +257,18 @@ final class TMDO_HivePress_Entity_Groups {
|
|||||||
'label' => 'Hourly rate',
|
'label' => 'Hourly rate',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
array( 'post_types' => array( 'hp_listing' ) )
|
array(
|
||||||
|
'post_types' => array( 'hp_listing' ),
|
||||||
|
// Filtered and sorted on by HivePress search. Declared as indexes
|
||||||
|
// (added in place) rather than `searchable`, which would change
|
||||||
|
// the schema hash and rebuild a table that may hold aeav_only data.
|
||||||
|
// Only with core 1.3.0+ (ddl_allowed()): older cores run the ALTER
|
||||||
|
// inside whatever request comes first, a visitor's included.
|
||||||
|
'indexes' => method_exists( 'TMDO_Schema_Manager', 'ddl_allowed' ) ? array(
|
||||||
|
'hourly_rate' => array( 'hp_hourly_rate' ),
|
||||||
|
'rating_count' => array( 'hp_rating_count' ),
|
||||||
|
) : array(),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ final class TMDO_Hivepress_Transient_Filter {
|
|||||||
/** Namespace prefix for translated wp_options entries. */
|
/** Namespace prefix for translated wp_options entries. */
|
||||||
public const TRANSLATED_NAMESPACE = 'wpdo_hp_pm_';
|
public const TRANSLATED_NAMESPACE = 'wpdo_hp_pm_';
|
||||||
|
|
||||||
|
/** Object cache group used instead of wp_options when a persistent cache is present. */
|
||||||
|
public const CACHE_GROUP = 'tmdo_hp_transient';
|
||||||
|
|
||||||
|
/** Longest an object-cache entry lives: a week (HivePress's default is a day). */
|
||||||
|
public const MAX_CACHE_TTL = 604800;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the four metadata filters on `init`. Idempotent — safe to call
|
* Register the four metadata filters on `init`. Idempotent — safe to call
|
||||||
* multiple times.
|
* multiple times.
|
||||||
@@ -164,7 +170,16 @@ final class TMDO_Hivepress_Transient_Filter {
|
|||||||
// HivePress cache layer reads the timeout *first*, then decides whether
|
// HivePress cache layer reads the timeout *first*, then decides whether
|
||||||
// to read the value. We must preserve raw stored values until HivePress
|
// to read the value. We must preserve raw stored values until HivePress
|
||||||
// itself orders deletion.
|
// itself orders deletion.
|
||||||
|
if ( wp_using_ext_object_cache() ) {
|
||||||
|
// The object cache is the only store. Rows left in wp_options from
|
||||||
|
// before are not read (`wp tmdo cleanup-hp-transients --options`
|
||||||
|
// removes them): a get_option() miss would add the name to
|
||||||
|
// `notoptions` for good, as later writes never reach wp_options.
|
||||||
|
$value = wp_cache_get( $option_name, self::CACHE_GROUP );
|
||||||
|
$value = false === $value ? null : $value;
|
||||||
|
} else {
|
||||||
$value = get_option( $option_name, null );
|
$value = get_option( $option_name, null );
|
||||||
|
}
|
||||||
|
|
||||||
if ( null === $value ) {
|
if ( null === $value ) {
|
||||||
// Cache miss in our store → fall through to normal postmeta path
|
// Cache miss in our store → fall through to normal postmeta path
|
||||||
@@ -239,16 +254,26 @@ final class TMDO_Hivepress_Transient_Filter {
|
|||||||
$translated = self::translate_key( (int) $object_id, (string) $meta_key );
|
$translated = self::translate_key( (int) $object_id, (string) $meta_key );
|
||||||
$option_name = ( $is_timeout ? '_transient_timeout_' : '_transient_' ) . $translated;
|
$option_name = ( $is_timeout ? '_transient_timeout_' : '_transient_' ) . $translated;
|
||||||
|
|
||||||
|
if ( wp_using_ext_object_cache() ) {
|
||||||
|
wp_cache_delete( $option_name, self::CACHE_GROUP );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
delete_option( $option_name );
|
delete_option( $option_name );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal: persist value or timeout to wp_options.
|
* Internal: persist value or timeout to the object cache when a persistent
|
||||||
|
* one is in use (Redis: a TTL cache belongs there, not in wp_options), else
|
||||||
|
* to wp_options.
|
||||||
*
|
*
|
||||||
* Both halves of the transient pair are written with `autoload=no` (they
|
* In wp_options both halves of the transient pair are written with
|
||||||
* are read on-demand only — never via wp_load_alloptions() — so eagerly
|
* `autoload=no` (they are read on-demand only — never via
|
||||||
* loading them on every page would defeat the point of the move).
|
* wp_load_alloptions() — so eagerly loading them on every page would defeat
|
||||||
|
* the point of the move). In the object cache a timeout entry expires when
|
||||||
|
* it says; the value, written before its timeout, after MAX_CACHE_TTL — a
|
||||||
|
* miss only makes HivePress rebuild it — so entries of deleted posts do
|
||||||
|
* not pile up (2.3 PR-099).
|
||||||
*
|
*
|
||||||
* @param int $post_id Post ID.
|
* @param int $post_id Post ID.
|
||||||
* @param string $meta_key Original `_transient_*` meta_key.
|
* @param string $meta_key Original `_transient_*` meta_key.
|
||||||
@@ -260,6 +285,11 @@ final class TMDO_Hivepress_Transient_Filter {
|
|||||||
$translated = self::translate_key( $post_id, $meta_key );
|
$translated = self::translate_key( $post_id, $meta_key );
|
||||||
$option_name = ( $is_timeout ? '_transient_timeout_' : '_transient_' ) . $translated;
|
$option_name = ( $is_timeout ? '_transient_timeout_' : '_transient_' ) . $translated;
|
||||||
|
|
||||||
|
if ( wp_using_ext_object_cache() ) {
|
||||||
|
$ttl = $is_timeout ? min( self::MAX_CACHE_TTL, max( 1, (int) $meta_value - time() ) ) : self::MAX_CACHE_TTL;
|
||||||
|
wp_cache_set( $option_name, $meta_value, self::CACHE_GROUP, $ttl );
|
||||||
|
return;
|
||||||
|
}
|
||||||
update_option( $option_name, $meta_value, false );
|
update_option( $option_name, $meta_value, false );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,10 @@ class HivepressTransientFilterTest extends TestCase {
|
|||||||
global $wpdb;
|
global $wpdb;
|
||||||
$wpdb->query( 'TRUNCATE TABLE `' . self::POSTMETA . '`' );
|
$wpdb->query( 'TRUNCATE TABLE `' . self::POSTMETA . '`' );
|
||||||
|
|
||||||
// Reset wp_options state for filter tests
|
// Reset wp_options / object cache state for filter tests
|
||||||
$GLOBALS['_wp_options'] = array();
|
$GLOBALS['_wp_options'] = array();
|
||||||
|
$GLOBALS['_wp_cache'] = array();
|
||||||
|
$GLOBALS['_wp_using_ext_object_cache'] = false;
|
||||||
|
|
||||||
// Default the toggle to enabled
|
// Default the toggle to enabled
|
||||||
update_option( WPDO_Hivepress_Transient_Filter::OPT_ENABLED, '1' );
|
update_option( WPDO_Hivepress_Transient_Filter::OPT_ENABLED, '1' );
|
||||||
@@ -195,6 +197,66 @@ class HivepressTransientFilterTest extends TestCase {
|
|||||||
$this->assertSame( array( array( 'serialized', 'data' ) ), $value );
|
$this->assertSame( array( array( 'serialized', 'data' ) ), $value );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── External object cache (2.2 PR-081) ──────────────────────────────────
|
||||||
|
|
||||||
|
public function test_with_an_object_cache_writes_skip_wp_options(): void {
|
||||||
|
$GLOBALS['_wp_using_ext_object_cache'] = true;
|
||||||
|
$key = '_transient_hp_models/cat/v1';
|
||||||
|
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
|
||||||
|
|
||||||
|
$this->assertTrue( WPDO_Hivepress_Transient_Filter::on_update( null, 100, $key, 'val', '' ) );
|
||||||
|
WPDO_Hivepress_Transient_Filter::on_update( null, 100, '_transient_timeout_hp_models/cat/v1', '99999999', '' );
|
||||||
|
|
||||||
|
$this->assertFalse( get_option( '_transient_' . $translated ), 'no wp_options row' );
|
||||||
|
$this->assertFalse( get_option( '_transient_timeout_' . $translated ), 'no wp_options row' );
|
||||||
|
$this->assertSame( array( 'val' ), WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) );
|
||||||
|
$this->assertSame( array( '99999999' ), WPDO_Hivepress_Transient_Filter::on_read( null, 100, '_transient_timeout_hp_models/cat/v1', true ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entries expire in the object cache too (2.3 PR-099): the timeout at the
|
||||||
|
* time it records, the value (written first, before its timeout is known)
|
||||||
|
* after at most a week. Orphans of deleted posts no longer pile up.
|
||||||
|
*/
|
||||||
|
public function test_with_an_object_cache_entries_get_a_ttl(): void {
|
||||||
|
$GLOBALS['_wp_using_ext_object_cache'] = true;
|
||||||
|
$key = '_transient_hp_models/cat/v1';
|
||||||
|
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
|
||||||
|
|
||||||
|
WPDO_Hivepress_Transient_Filter::on_update( null, 100, $key, 'val', '' );
|
||||||
|
WPDO_Hivepress_Transient_Filter::on_update( null, 100, '_transient_timeout_hp_models/cat/v1', (string) ( time() + 3600 ), '' );
|
||||||
|
WPDO_Hivepress_Transient_Filter::on_update( null, 101, '_transient_timeout_hp_models/cat/v1', (string) ( time() - 5 ), '' );
|
||||||
|
|
||||||
|
$ttl = $GLOBALS['_wp_cache_ttl'][ WPDO_Hivepress_Transient_Filter::CACHE_GROUP ];
|
||||||
|
$this->assertSame( WPDO_Hivepress_Transient_Filter::MAX_CACHE_TTL, $ttl[ '_transient_' . $translated ] );
|
||||||
|
$this->assertEqualsWithDelta( 3600, $ttl[ '_transient_timeout_' . $translated ], 2 );
|
||||||
|
$this->assertSame( 1, $ttl[ '_transient_timeout_' . WPDO_Hivepress_Transient_Filter::translate_key( 101, $key ) ], 'already expired' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_with_an_object_cache_a_value_left_in_wp_options_is_not_read(): void {
|
||||||
|
// A miss must not fall back to get_option(): the option is absent after
|
||||||
|
// cleanup and WordPress would add its name to `notoptions` for good.
|
||||||
|
$key = '_transient_hp_models/cat/v1';
|
||||||
|
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
|
||||||
|
update_option( '_transient_' . $translated, 'from-before' );
|
||||||
|
$GLOBALS['_wp_using_ext_object_cache'] = true;
|
||||||
|
|
||||||
|
$this->assertNull( WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_with_an_object_cache_delete_leaves_wp_options_alone(): void {
|
||||||
|
$key = '_transient_hp_models/cat/v1';
|
||||||
|
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
|
||||||
|
update_option( '_transient_' . $translated, 'from-before' );
|
||||||
|
$GLOBALS['_wp_using_ext_object_cache'] = true;
|
||||||
|
WPDO_Hivepress_Transient_Filter::on_update( null, 100, $key, 'val', '' );
|
||||||
|
|
||||||
|
WPDO_Hivepress_Transient_Filter::on_delete( null, 100, $key, '', false );
|
||||||
|
|
||||||
|
$this->assertNull( WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) );
|
||||||
|
$this->assertSame( 'from-before', get_option( '_transient_' . $translated ), 'left for cleanup-hp-transients --options' );
|
||||||
|
}
|
||||||
|
|
||||||
// ── count/purge legacy ──────────────────────────────────────────────────
|
// ── count/purge legacy ──────────────────────────────────────────────────
|
||||||
|
|
||||||
public function test_count_legacy_postmeta_rows_returns_zero_when_clean(): void {
|
public function test_count_legacy_postmeta_rows_returns_zero_when_clean(): void {
|
||||||
|
|||||||
@@ -21,8 +21,23 @@ if ( ! file_exists( $tmdo_core_bootstrap ) ) {
|
|||||||
. dirname( __DIR__, 3 ) . '/2meet-data-optimizer'
|
. dirname( __DIR__, 3 ) . '/2meet-data-optimizer'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Defined before the core bootstrap (which only adds it when missing) so tests can read the TTL.
|
||||||
|
if ( ! function_exists( 'wp_cache_set' ) ) {
|
||||||
|
function wp_cache_set( $key, $value, $group = '', $ttl = 0 ): bool {
|
||||||
|
$GLOBALS['_wp_cache'][ $group ][ $key ] = $value;
|
||||||
|
$GLOBALS['_wp_cache_ttl'][ $group ][ $key ] = (int) $ttl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
require_once $tmdo_core_bootstrap;
|
require_once $tmdo_core_bootstrap;
|
||||||
|
|
||||||
|
// External object cache switch for the transient filter tests (Redis on real sites).
|
||||||
|
if ( ! function_exists( 'wp_using_ext_object_cache' ) ) {
|
||||||
|
function wp_using_ext_object_cache( $using = null ): bool { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||||
|
return ! empty( $GLOBALS['_wp_using_ext_object_cache'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
define( 'TMDO_HIVEPRESS_PATH', dirname( __DIR__, 2 ) . '/' );
|
define( 'TMDO_HIVEPRESS_PATH', dirname( __DIR__, 2 ) . '/' );
|
||||||
define( 'TMDO_HIVEPRESS_URL', 'http://localhost/wp-content/plugins/2meet-data-optimizer-hivepress-addon/' );
|
define( 'TMDO_HIVEPRESS_URL', 'http://localhost/wp-content/plugins/2meet-data-optimizer-hivepress-addon/' );
|
||||||
define( 'TMDO_HIVEPRESS_FILE', TMDO_HIVEPRESS_PATH . '2meet-data-optimizer-hivepress-addon.php' );
|
define( 'TMDO_HIVEPRESS_FILE', TMDO_HIVEPRESS_PATH . '2meet-data-optimizer-hivepress-addon.php' );
|
||||||
|
|||||||
@@ -58,6 +58,21 @@ class HivePressEntityGroupsTest extends TestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HivePress filters and sorts listings by these; they are declared as
|
||||||
|
* group indexes (added in place by core) rather than `searchable`, which
|
||||||
|
* would change the pinned hash and rebuild the table.
|
||||||
|
*/
|
||||||
|
public function test_listing_columns_filtered_and_sorted_on_are_indexed(): void {
|
||||||
|
$this->assertSame(
|
||||||
|
array(
|
||||||
|
'hourly_rate' => array( 'hp_hourly_rate' ),
|
||||||
|
'rating_count' => array( 'hp_rating_count' ),
|
||||||
|
),
|
||||||
|
TMDO_Entity_Registry::get_group_indexes( 'post', 'hp_listing_core' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_shared_keys_keep_their_current_owner(): void {
|
public function test_shared_keys_keep_their_current_owner(): void {
|
||||||
foreach ( self::SHARED_KEY_OWNERS as $key => $group ) {
|
foreach ( self::SHARED_KEY_OWNERS as $key => $group ) {
|
||||||
$this->assertSame( $group, TMDO_Entity_Registry::get_field( 'post', $key )['group'], $key );
|
$this->assertSame( $group, TMDO_Entity_Registry::get_field( 'post', $key )['group'], $key );
|
||||||
|
|||||||
Reference in New Issue
Block a user