From e443f20edbd7595b49e423d32af4b663ae73d606 Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 25 Sep 2026 22:28:54 +0800 Subject: [PATCH 01/12] fix(2meet-data-optimizer-hivepress-addon): map bookings hot field to hp_quantity - hivepress-bookings has no guests field; the places booked are stored as hp_quantity (only when "Allow multiple places" is enabled) - Verified against hivepress-bookings 1.5.5: start_time / end_time were already correct and stay unchanged Verify: vendor/bin/phpunit --filter test_on_register_fields_uses_real_booking_meta_keys; addon unit 155 OK, integration 57 OK PR: PR-009 --- .../class-tmdo-hivepress-bookings-adapter.php | 7 ++++--- tests/unit/BookingsAdapterTest.php | 12 ++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-bookings-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-bookings-adapter.php index 52e1967..60cfaa1 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-bookings-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-bookings-adapter.php @@ -10,7 +10,8 @@ * * start_time / end_time → unix timestamp range queries (hot) * status → enum filter (hot) - * guests → numeric count (hot for slot-availability) + * quantity → places booked, hp_quantity (hot for slot-availability; + * only written when "Allow multiple places" is on) * listing → post_parent (already a posts column) * * Hot-zone target: `wp_wpdo_hot_hp_booking` with covering indexes on @@ -91,10 +92,10 @@ if ( ! class_exists( 'TMDO_HivePress_Bookings_Adapter' ) ) { ), array( 'post_type' => 'hp_booking', - 'meta_key' => 'hp_guests', + 'meta_key' => 'hp_quantity', 'zone' => 'hot', 'data_type' => 'int(11) NOT NULL DEFAULT 0', - 'column' => 'hp_guests', + 'column' => 'hp_quantity', 'indexed' => false, ), ) diff --git a/tests/unit/BookingsAdapterTest.php b/tests/unit/BookingsAdapterTest.php index 155603d..b0bd684 100644 --- a/tests/unit/BookingsAdapterTest.php +++ b/tests/unit/BookingsAdapterTest.php @@ -46,6 +46,18 @@ class HivePressBookingsAdapterTest extends TestCase { $this->assertTrue( (bool) $end['indexed'] ); } + public function test_on_register_fields_uses_real_booking_meta_keys(): void { + $registry = WPDO_Schema_Registry::instance(); + $this->adapter->on_register_fields( $registry ); + + // hivepress-bookings has no guests field; places are stored as hp_quantity. + $this->assertNull( $registry->get_field( 'hp_booking', 'hp_guests' ) ); + + $quantity = $registry->get_field( 'hp_booking', 'hp_quantity' ); + $this->assertNotNull( $quantity ); + $this->assertSame( 'hot', $quantity['zone'] ); + } + public function test_score_reflects_unverified_perf_dimension(): void { $score = $this->adapter->suitability_score(); // D8 reduced to 0.9 because schema is best-effort until addon installs. From d3ddbb893345643854b8c86fd0cf729e20b233b5 Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 25 Sep 2026 22:35:30 +0800 Subject: [PATCH 02/12] fix(2meet-data-optimizer-hivepress-addon): register enabled social links as hp_{name} - hivepress-social-links stores each link as hp_{name} (hp_facebook, not hp_facebook_url) and lets admins enable links separately for listings and vendors; the hardcoded 7 *_url vendor keys never matched real meta - Read hp_listing_social_links / hp_vendor_social_links and register each enabled link on its own post type, so custom links (e.g. line_oa) work - dev34 check: meta holds hp_instagram / hp_website, and zero *_url keys Verify: vendor/bin/phpunit --filter HivePressSocialLinksAdapterTest; addon unit 156 OK, integration 57 OK PR: PR-010 --- ...ss-tmdo-hivepress-social-links-adapter.php | 69 ++++++++++--------- tests/unit/SocialLinksAdapterTest.php | 38 +++++++--- 2 files changed, 62 insertions(+), 45 deletions(-) diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-social-links-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-social-links-adapter.php index 7636de1..225bef5 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-social-links-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-social-links-adapter.php @@ -2,13 +2,13 @@ /** * Social Links adapter — `hivepress-social-links`. * - * Adds social URL fields to vendor profiles (Facebook, Instagram, Twitter, - * LinkedIn, YouTube, etc.). Stored in postmeta as `hp_social_*` keys. + * The addon adds one attribute per social link the admin enables, separately + * for listings (`hp_listing_social_links`) and vendors (`hp_vendor_social_links`). + * Each attribute is stored in postmeta as `hp_{name}` (e.g. `hp_facebook`). * * Display-only — never used in search/filter — so the right zone is Cold - * (object cache + JSON blob fallback). Listing page reads ~7 social URLs; - * caching them as a single blob keyed on vendor_id eliminates 7 postmeta - * lookups per render. + * (object cache + JSON blob fallback): one blob per post replaces one + * postmeta lookup per link on every render. * * @package WP_Data_Optimizer * @since 3.0.0 @@ -27,25 +27,6 @@ if ( ! class_exists( 'TMDO_HivePress_Social_Links_Adapter' ) ) { use TMDO_HivePress_Adapter_Trait; - /** - * Social URL meta keys owned by this addon. - * - * Sources (HivePress 1.0.3 default field set): - * facebook_url, instagram_url, twitter_url, linkedin_url, - * youtube_url, telegram_url, whatsapp_url - * - * @var array - */ - private const SOCIAL_KEYS = array( - 'hp_facebook_url', - 'hp_instagram_url', - 'hp_twitter_url', - 'hp_linkedin_url', - 'hp_youtube_url', - 'hp_telegram_url', - 'hp_whatsapp_url', - ); - /** * Constructor — bind register hooks via inherited trait. */ @@ -74,24 +55,44 @@ if ( ! class_exists( 'TMDO_HivePress_Social_Links_Adapter' ) ) { } /** - * Register vendor social URL fields to Cold zone. + * Register the enabled social link fields to the Cold zone. * * @param TMDO_Schema_Registry $registry Schema registry singleton. */ public function on_register_fields( TMDO_Schema_Registry $registry ): void { $fields = array(); - foreach ( self::SOCIAL_KEYS as $key ) { - $fields[] = array( - 'post_type' => 'hp_vendor', - 'meta_key' => $key, - 'zone' => 'cold', - 'cache_group' => 'wpdo_cold_hp_vendor', - 'cache_ttl' => HOUR_IN_SECONDS, - ); + foreach ( $this->enabled_meta_keys() as $post_type => $keys ) { + foreach ( $keys as $key ) { + $fields[] = array( + 'post_type' => $post_type, + 'meta_key' => $key, + 'zone' => 'cold', + 'cache_group' => 'wpdo_cold_' . $post_type, + 'cache_ttl' => HOUR_IN_SECONDS, + ); + } } $registry->register_many( $this->plugin_slug(), $fields ); } + /** + * Meta keys of the social links enabled in the addon settings. + * + * @return array> post_type => meta keys. + */ + private function enabled_meta_keys(): array { + $keys = array(); + foreach ( array( 'listing', 'vendor' ) as $model ) { + foreach ( (array) get_option( 'hp_' . $model . '_social_links', array() ) as $name ) { + $name = sanitize_key( (string) $name ); + if ( '' !== $name ) { + $keys[ 'hp_' . $model ][] = 'hp_' . $name; + } + } + } + return $keys; + } + /** * Doctor probe — cold zone uses cache; verify cache layer is reachable. * @@ -100,7 +101,7 @@ if ( ! class_exists( 'TMDO_HivePress_Social_Links_Adapter' ) ) { public function doctor_check(): array { return array( 'ok' => true, - 'message' => sprintf( 'hivepress-social-links: %d cold fields registered', count( self::SOCIAL_KEYS ) ), + 'message' => sprintf( 'hivepress-social-links: %d cold fields registered', array_sum( array_map( 'count', $this->enabled_meta_keys() ) ) ), ); } diff --git a/tests/unit/SocialLinksAdapterTest.php b/tests/unit/SocialLinksAdapterTest.php index b14b543..eb0ce82 100644 --- a/tests/unit/SocialLinksAdapterTest.php +++ b/tests/unit/SocialLinksAdapterTest.php @@ -18,6 +18,8 @@ class HivePressSocialLinksAdapterTest extends TestCase { $prop->setAccessible( true ); $prop->setValue( null, null ); + unset( $GLOBALS['_wp_options']['hp_vendor_social_links'], $GLOBALS['_wp_options']['hp_listing_social_links'] ); + $this->adapter = new WPDO_HivePress_Social_Links_Adapter(); } @@ -30,25 +32,39 @@ class HivePressSocialLinksAdapterTest extends TestCase { $this->assertSame( 'HivePress\\SocialLinks\\Plugin', $this->adapter->detection_class() ); } - public function test_on_register_fields_registers_seven_cold_fields(): void { + public function test_registers_enabled_links_per_model(): void { + // hivepress-social-links stores each enabled link as the attribute hp_{name}, + // with separate admin selections for listings and vendors. + update_option( 'hp_vendor_social_links', array( 'facebook', 'website' ) ); + update_option( 'hp_listing_social_links', array( 'instagram' ) ); + $registry = WPDO_Schema_Registry::instance(); $this->adapter->on_register_fields( $registry ); - // All seven social URL fields should land in cold zone on hp_vendor. - $facebook = $registry->get_field( 'hp_vendor', 'hp_facebook_url' ); - $this->assertSame( 'cold', $facebook['zone'] ); + $this->assertSame( 'cold', $registry->get_field( 'hp_vendor', 'hp_facebook' )['zone'] ); + $this->assertSame( 'cold', $registry->get_field( 'hp_vendor', 'hp_website' )['zone'] ); + $this->assertSame( 'cold', $registry->get_field( 'hp_listing', 'hp_instagram' )['zone'] ); - $instagram = $registry->get_field( 'hp_vendor', 'hp_instagram_url' ); - $this->assertSame( 'cold', $instagram['zone'] ); - - $whatsapp = $registry->get_field( 'hp_vendor', 'hp_whatsapp_url' ); - $this->assertSame( 'cold', $whatsapp['zone'] ); + $this->assertNull( $registry->get_field( 'hp_vendor', 'hp_instagram' ) ); + $this->assertNull( $registry->get_field( 'hp_vendor', 'hp_facebook_url' ) ); } - public function test_doctor_check_reports_field_count(): void { + public function test_registers_nothing_when_no_links_enabled(): void { + $registry = WPDO_Schema_Registry::instance(); + $this->adapter->on_register_fields( $registry ); + + $this->assertNull( $registry->get_field( 'hp_vendor', 'hp_facebook' ) ); + $this->assertNull( $registry->get_field( 'hp_listing', 'hp_facebook' ) ); + } + + public function test_doctor_check_reports_registered_count(): void { + update_option( 'hp_vendor_social_links', array( 'facebook', 'website' ) ); + update_option( 'hp_listing_social_links', array( 'instagram' ) ); + $result = $this->adapter->doctor_check(); + $this->assertTrue( $result['ok'] ); - $this->assertStringContainsString( '7 cold fields', $result['message'] ); + $this->assertStringContainsString( '3 cold fields', $result['message'] ); } public function test_score_aggregates_to_perfect_ten(): void { From f089b0934b24ee3c8f93ce9e4e9a8d710ced9160 Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 25 Sep 2026 22:39:57 +0800 Subject: [PATCH 03/12] fix(2meet-data-optimizer-hivepress-addon): map marketplace hot field to hp_price - hivepress-marketplace stores its filterable, sortable listing price as hp_price; hp_purchase_price never existed (dev34: 588 hp_price rows) - Drop hp_purchase_count: the addon keeps no per-listing sales counter - Remove the stale docblock note about the legacy core registration path Verify: vendor/bin/phpunit --filter test_on_register_fields_registers_hot_hp_price; addon unit 156 OK, integration 57 OK PR: PR-011 --- ...ass-tmdo-hivepress-marketplace-adapter.php | 24 ++++++------------- tests/unit/MarketplaceAdapterTest.php | 10 ++++---- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-marketplace-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-marketplace-adapter.php index 776219f..f5042b2 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-marketplace-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-marketplace-adapter.php @@ -3,16 +3,12 @@ * Marketplace adapter — `hivepress-marketplace` (DETECT-ONLY in v3.0.0). * * The marketplace addon turns listings into purchasable items, integrating - * with WooCommerce. It adds: + * with WooCommerce. Its filterable, sortable listing attribute `price` is + * stored as `hp_price` → decimal(10,2), used in price-range filters (HOT). * - * hp_purchase_price → decimal(10,2), used in price-range filters (HOT) - * hp_purchase_count → counter (WARM zone, low-criticality) - * - * Note: `hp_purchase_price` is also conditionally registered by the existing - * legacy hivepress integration (Sprint 1 left that path) — the dedup in - * Schema_Registry is provider+key based, so registering again under the - * `hivepress-marketplace` provider is safe and lets `wp wpdo doctor` show - * the right ownership. + * Its other listing attributes (price tiers / extras / discounts, quantity, + * subscription period, …) are not search filters, and it keeps no per-listing + * sales counter. * * Detect-only: not installed in dev environment. Schema reserved for the * day the addon ships. @@ -72,18 +68,12 @@ if ( ! class_exists( 'TMDO_HivePress_Marketplace_Adapter' ) ) { array( array( 'post_type' => 'hp_listing', - 'meta_key' => 'hp_purchase_price', + 'meta_key' => 'hp_price', 'zone' => 'hot', 'data_type' => 'decimal(10,2) DEFAULT NULL', - 'column' => 'hp_purchase_price', + 'column' => 'hp_price', 'indexed' => true, ), - array( - 'post_type' => 'hp_listing', - 'meta_key' => 'hp_purchase_count', - 'zone' => 'warm', - 'ttl' => null, - ), ) ); } diff --git a/tests/unit/MarketplaceAdapterTest.php b/tests/unit/MarketplaceAdapterTest.php index e1d592c..55e93de 100644 --- a/tests/unit/MarketplaceAdapterTest.php +++ b/tests/unit/MarketplaceAdapterTest.php @@ -33,17 +33,19 @@ class HivePressMarketplaceAdapterTest extends TestCase { $this->assertSame( 'HivePress\\Marketplace\\Plugin', $this->adapter->detection_class() ); } - public function test_on_register_fields_registers_hot_price_and_warm_counter(): void { + public function test_on_register_fields_registers_hot_hp_price(): void { $registry = WPDO_Schema_Registry::instance(); $this->adapter->on_register_fields( $registry ); - $price = $registry->get_field( 'hp_listing', 'hp_purchase_price' ); + // Marketplace's filterable, sortable listing "price" attribute is stored as hp_price. + $price = $registry->get_field( 'hp_listing', 'hp_price' ); $this->assertSame( 'hot', $price['zone'] ); $this->assertTrue( (bool) $price['indexed'] ); $this->assertStringContainsString( 'decimal', $price['data_type'] ); - $count = $registry->get_field( 'hp_listing', 'hp_purchase_count' ); - $this->assertSame( 'warm', $count['zone'] ); + // Neither key exists in hivepress-marketplace. + $this->assertNull( $registry->get_field( 'hp_listing', 'hp_purchase_price' ) ); + $this->assertNull( $registry->get_field( 'hp_listing', 'hp_purchase_count' ) ); } public function test_score_aggregates_to_nine_eight_eight(): void { From 57772101849961da1f600721a56c7c47cd4634cb Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 25 Sep 2026 22:58:34 +0800 Subject: [PATCH 04/12] refactor(2meet-data-optimizer-hivepress-addon): remove comment mirror writes to never-created tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Favorites/Messages adapters reduced to detection only; Requests keeps hot_hp_request fields, drops hp_offer mirror - Delete TMDO_HivePress_Comment_Router (read path to wpdo_comment_hp_* tables that no installer creates) - Verify: addon unit 141 OK, integration 52 OK; phpcs 41E/57W → 35E/48W - PR: PR-013 --- includes/back-compat-aliases.php | 1 - includes/class-tmdo-hivepress-bootstrap.php | 1 - ...class-tmdo-hivepress-favorites-adapter.php | 181 +-------------- .../class-tmdo-hivepress-messages-adapter.php | 216 +----------------- .../class-tmdo-hivepress-requests-adapter.php | 173 ++------------ .../class-tmdo-hivepress-reviews-adapter.php | 12 +- .../class-tmdo-hivepress-bootstrap.php | 9 +- .../class-tmdo-hivepress-comment-router.php | 166 -------------- .../hivepress/interface-tmdo-hp-adapter.php | 2 +- tests/bootstrap.php | 1 - .../integration/HivepressIntegrationTest.php | 136 ----------- tests/integration/bootstrap.php | 1 - tests/unit/CommentRouterTest.php | 92 -------- tests/unit/FavoritesAdapterTest.php | 71 +----- tests/unit/MessagesAdapterTest.php | 74 ++---- tests/unit/RequestsAdapterTest.php | 47 +--- tests/unit/WpdbMockTrait.php | 2 +- 17 files changed, 77 insertions(+), 1108 deletions(-) delete mode 100644 includes/hivepress/class-tmdo-hivepress-comment-router.php delete mode 100644 tests/unit/CommentRouterTest.php diff --git a/includes/back-compat-aliases.php b/includes/back-compat-aliases.php index 6eb79a0..78f487f 100644 --- a/includes/back-compat-aliases.php +++ b/includes/back-compat-aliases.php @@ -29,7 +29,6 @@ add_action( 'TMDO_HivePress_Blocks_Adapter' => 'WPDO_HivePress_Blocks_Adapter', 'TMDO_HivePress_Bookings_Adapter' => 'WPDO_HivePress_Bookings_Adapter', 'TMDO_HivePress_Bootstrap' => 'WPDO_HivePress_Bootstrap', - 'TMDO_HivePress_Comment_Router' => 'WPDO_HivePress_Comment_Router', 'TMDO_HivePress_Conflict_Guard' => 'WPDO_HivePress_Conflict_Guard', 'TMDO_HivePress_Core_Adapter' => 'WPDO_HivePress_Core_Adapter', 'TMDO_HivePress_Cron_Optimizer' => 'WPDO_HivePress_Cron_Optimizer', diff --git a/includes/class-tmdo-hivepress-bootstrap.php b/includes/class-tmdo-hivepress-bootstrap.php index 4c3ea9c..c5f9c5c 100644 --- a/includes/class-tmdo-hivepress-bootstrap.php +++ b/includes/class-tmdo-hivepress-bootstrap.php @@ -44,7 +44,6 @@ final class TMDO_HP_Bootstrap { require_once $base . 'includes/hivepress/class-tmdo-hivepress-detector.php'; require_once $base . 'includes/hivepress/class-tmdo-hivepress-conflict-guard.php'; require_once $base . 'includes/hivepress/class-tmdo-hivepress-bootstrap.php'; - require_once $base . 'includes/hivepress/class-tmdo-hivepress-comment-router.php'; require_once $base . 'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php'; // 13 adapters diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-favorites-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-favorites-adapter.php index 9086044..d87bc84 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-favorites-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-favorites-adapter.php @@ -8,27 +8,9 @@ * - listing → comment_post_ID * - added_at → comment_date * - * Problem: `wp_comments` has no UNIQUE constraint on (user_id, comment_post_ID, comment_type). - * A double-tap on the favourite button can race and create two duplicate - * favourite rows. HivePress relies on application-level dedup (SELECT before - * INSERT) which is not race-safe. - * - * Fix: shadow table `wp_wpdo_comment_hp_favorite` with UNIQUE(user_id, listing_id). - * Adapter mirrors every favorite write into the shadow table; the comment - * router rewrites "is X favorited by user Y" reads to hit the shadow table - * (1 indexed lookup vs WP_Comment_Query meta scan). - * - * Schema (declared via expected_columns; created by TMDO_Schema_Manager - * when adapter mode flips to dual_write): - * - * CREATE TABLE wp_wpdo_comment_hp_favorite ( - * comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, - * user_id BIGINT UNSIGNED NOT NULL, - * listing_id BIGINT UNSIGNED NOT NULL, - * created_at DATETIME NOT NULL, - * UNIQUE KEY unique_user_listing (user_id, listing_id), - * KEY idx_listing (listing_id) - * ); + * Nothing is stored in comment meta, so there is no EAV surface to flatten: + * the adapter only reports detection and doctor status. (It used to mirror + * favorites into `wpdo_comment_hp_favorite`, a table nothing ever created.) * * @package WP_Data_Optimizer * @since 3.0.0 @@ -41,18 +23,15 @@ if ( ! defined( 'ABSPATH' ) ) { if ( ! class_exists( 'TMDO_HivePress_Favorites_Adapter' ) ) { /** - * Favorites addon adapter. + * Favorites addon adapter (detection and doctor only). */ final class TMDO_HivePress_Favorites_Adapter implements TMDO_HivePress_Adapter { use TMDO_HivePress_Adapter_Trait; - /** Comment_type owned by this adapter. */ + /** Comment_type of the Favorite model. */ public const COMMENT_TYPE = 'hp_favorite'; - /** Shadow table name (without wpdb prefix). */ - public const TABLE = 'wpdo_comment_hp_favorite'; - /** * Constructor — bind register hooks via inherited trait. */ @@ -79,156 +58,6 @@ if ( ! class_exists( 'TMDO_HivePress_Favorites_Adapter' ) ) { public function minimum_addon_version(): string { return '1.2.0'; } - - /** - * Declare the shadow table to the Custom_Table_Registry. - * - * @param TMDO_Custom_Table_Registry $registry Registry singleton. - */ - public function on_register_custom_tables( TMDO_Custom_Table_Registry $registry ): void { - $this->register_custom_table( - $registry, - array( - 'table_name' => self::TABLE, - 'primary_key' => 'comment_id', - 'post_type_link' => null, - 'expected_columns' => array( - 'comment_id' => 'BIGINT UNSIGNED NOT NULL PRIMARY KEY', - 'user_id' => 'BIGINT UNSIGNED NOT NULL', - 'listing_id' => 'BIGINT UNSIGNED NOT NULL', - 'created_at' => 'DATETIME NOT NULL', - ), - 'indexes' => array( - 'unique_user_listing' => 'UNIQUE (user_id, listing_id)', - 'idx_listing' => '(listing_id)', - ), - 'doctor_callback' => array( $this, 'doctor_check' ), - ) - ); - } - - /** - * Sync wp_comments → shadow table on insert. - * - * Bound during `on_register_event_hooks()`. The comment-router uses - * the shadow table for "is favorited" lookups; sync keeps it consistent. - */ - public function on_register_event_hooks(): void { - add_action( 'wp_insert_comment', array( $this, 'mirror_insert' ), 10, 2 ); - add_action( 'deleted_comment', array( $this, 'mirror_delete' ), 10, 2 ); - } - - /** - * Mirror a newly-inserted hp_favorite into the shadow table. - * - * @param int $comment_id Comment ID. - * @param object|\WP_Comment $comment Comment object. - */ - public function mirror_insert( int $comment_id, $comment ): void { - if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { - return; - } - global $wpdb; - $user_id = (int) ( $comment->user_id ?? 0 ); - $listing_id = (int) ( $comment->comment_post_ID ?? 0 ); - if ( $user_id <= 0 || $listing_id <= 0 ) { - return; - } - $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->prepare( - 'INSERT IGNORE INTO `' . $wpdb->prefix . self::TABLE . '` (comment_id, user_id, listing_id, created_at) VALUES (%d, %d, %d, %s)', - $comment_id, - $user_id, - $listing_id, - $comment->comment_date ?? current_time( 'mysql' ) - ) - ); - } - - /** - * Remove the shadow row when a hp_favorite comment is deleted. - * - * @param int $comment_id Comment ID. - * @param object|\WP_Comment $comment Comment object. - */ - public function mirror_delete( int $comment_id, $comment ): void { - if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { - return; - } - global $wpdb; - $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $wpdb->prefix . self::TABLE, - array( 'comment_id' => $comment_id ), - array( '%d' ) - ); - } - - /** - * Doctor probe — verify shadow table exists. - * - * @return array{ok:bool, message:string, details?:array} - */ - public function doctor_check(): array { - global $wpdb; - if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) { - return array( - 'ok' => false, - 'message' => 'wpdb global not available', - ); - } - $table = $wpdb->prefix . self::TABLE; - try { - $exists = (bool) $wpdb->get_var( - $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) - ); - if ( ! $exists ) { - return array( - 'ok' => false, - 'message' => sprintf( 'hivepress-favorites: shadow table %s missing', $table ), - 'details' => array( 'table' => $table ), - ); - } - $rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - return array( - 'ok' => true, - 'message' => sprintf( 'hivepress-favorites: %s rows=%d', $table, $rows ), - 'details' => array( - 'table' => $table, - 'rows' => $rows, - ), - ); - } catch ( \Throwable $e ) { - return array( - 'ok' => false, - 'message' => sprintf( 'hivepress-favorites doctor failed: %s', $e->getMessage() ), - ); - } - } - - /** - * 8-D self-score. - * - * D1: 1.0 — adapter never calls *_meta() - * D2: 1.0 — uses wpdb prefix, no hardcoded table literals - * D3: 1.0 — shadow table covers the model - * D4: 1.0 — expected_columns + indexes registered - * D5: 1.0 — listens to wp_insert_comment for cross-write consistency - * D6: 1.0 — zero options/transients - * D7: 1.0 — only queries own shadow table - * D8: 1.0 — UNIQUE(user_id, listing_id) covering index - */ - public function suitability_score(): array { - return $this->compose_score( array() ); - } - - /** - * FSM modules this adapter contributes. - * - * @return array - */ - public function migrations(): array { - return array( 'comment_hp_favorite' ); - } } } // end if ( ! class_exists ) diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-messages-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-messages-adapter.php index cc044dc..6fc1d29 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-messages-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-messages-adapter.php @@ -3,28 +3,12 @@ * Messages adapter — `hivepress-messages`. * * The Message model extends Comment (`comment_type = hp_message`) and stores - * the recipient_id in `comment_karma` — a creative re-use of an unused column, - * but it leaves the field unindexed for "messages received by user N" - * queries. Result: every dashboard "unread count" badge does an O(N) - * `WHERE comment_type = 'hp_message' AND comment_karma = N` scan over - * the entire wp_comments table. + * the recipient in `comment_karma`, the sender in `user_id` and the read flag + * in `comment_approved`. * - * Fix: shadow table `wp_wpdo_comment_hp_message` with proper recipient_id - * column + index. Adapter mirrors every hp_message write into the shadow, - * and the comment-router rewrites recipient/unread queries to hit the index. - * - * Schema: - * - * CREATE TABLE wp_wpdo_comment_hp_message ( - * comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, - * sender_id BIGINT UNSIGNED NOT NULL, - * recipient_id BIGINT UNSIGNED NOT NULL, - * listing_id BIGINT UNSIGNED NOT NULL, - * is_read TINYINT(1) NOT NULL DEFAULT 0, - * sent_at DATETIME NOT NULL, - * KEY idx_recipient_unread (recipient_id, is_read), - * KEY idx_thread (listing_id, sent_at) - * ); + * Nothing is stored in comment meta, so there is no EAV surface to flatten: + * the adapter only reports detection and doctor status. (It used to mirror + * messages into `wpdo_comment_hp_message`, a table nothing ever created.) * * @package WP_Data_Optimizer * @since 3.0.0 @@ -37,18 +21,15 @@ if ( ! defined( 'ABSPATH' ) ) { if ( ! class_exists( 'TMDO_HivePress_Messages_Adapter' ) ) { /** - * Messages addon adapter. + * Messages addon adapter (detection and doctor only). */ final class TMDO_HivePress_Messages_Adapter implements TMDO_HivePress_Adapter { use TMDO_HivePress_Adapter_Trait; - /** Comment_type owned by this adapter. */ + /** Comment_type of the Message model. */ public const COMMENT_TYPE = 'hp_message'; - /** Shadow table name (without wpdb prefix). */ - public const TABLE = 'wpdo_comment_hp_message'; - /** * Constructor — bind register hooks via inherited trait. */ @@ -75,189 +56,6 @@ if ( ! class_exists( 'TMDO_HivePress_Messages_Adapter' ) ) { public function minimum_addon_version(): string { return '1.4.0'; } - - /** - * Declare the shadow table to the Custom_Table_Registry. - * - * @param TMDO_Custom_Table_Registry $registry Registry singleton. - */ - public function on_register_custom_tables( TMDO_Custom_Table_Registry $registry ): void { - $this->register_custom_table( - $registry, - array( - 'table_name' => self::TABLE, - 'primary_key' => 'comment_id', - 'post_type_link' => null, - 'expected_columns' => array( - 'comment_id' => 'BIGINT UNSIGNED NOT NULL PRIMARY KEY', - 'sender_id' => 'BIGINT UNSIGNED NOT NULL', - 'recipient_id' => 'BIGINT UNSIGNED NOT NULL', - 'listing_id' => 'BIGINT UNSIGNED NOT NULL', - 'is_read' => 'TINYINT(1) NOT NULL DEFAULT 0', - 'sent_at' => 'DATETIME NOT NULL', - ), - 'indexes' => array( - 'idx_recipient_unread' => '(recipient_id, is_read)', - 'idx_thread' => '(listing_id, sent_at)', - ), - 'doctor_callback' => array( $this, 'doctor_check' ), - ) - ); - } - - /** - * Bind sync hooks on hp_message comment insert / update / delete. - * - * The recipient_id is stored in comment_karma by HivePress; we mirror - * it into a properly-named indexed column. is_read tracks comment_approved - * (HP uses 0=unread / 1=read) — same column, mirrored. - */ - public function on_register_event_hooks(): void { - add_action( 'wp_insert_comment', array( $this, 'mirror_insert' ), 10, 2 ); - add_action( 'wp_set_comment_status', array( $this, 'mirror_status_change' ), 10, 2 ); - add_action( 'deleted_comment', array( $this, 'mirror_delete' ), 10, 2 ); - } - - /** - * Mirror a new hp_message into the shadow table. - * - * @param int $comment_id Comment ID. - * @param object|\WP_Comment $comment Comment object. - */ - public function mirror_insert( int $comment_id, $comment ): void { - if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { - return; - } - global $wpdb; - $sender_id = (int) ( $comment->user_id ?? 0 ); - $recipient_id = (int) ( $comment->comment_karma ?? 0 ); - $listing_id = (int) ( $comment->comment_post_ID ?? 0 ); - $is_read = (int) ( $comment->comment_approved ?? 0 ); - if ( $sender_id <= 0 || $recipient_id <= 0 ) { - // Malformed message — nothing useful to mirror. - return; - } - $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->prepare( - 'INSERT INTO `' . $wpdb->prefix . self::TABLE . '` (comment_id, sender_id, recipient_id, listing_id, is_read, sent_at) VALUES (%d, %d, %d, %d, %d, %s) ON DUPLICATE KEY UPDATE is_read = VALUES(is_read)', - $comment_id, - $sender_id, - $recipient_id, - $listing_id, - $is_read, - $comment->comment_date ?? current_time( 'mysql' ) - ) - ); - } - - /** - * Update is_read when a message is marked read (comment_approved flip). - * - * @param int $comment_id Comment ID. - * @param string $status New status ('approve', 'hold', 'spam', 'trash'). - */ - public function mirror_status_change( int $comment_id, string $status ): void { - global $wpdb; - // Look up the comment to confirm it's hp_message before touching. - if ( ! function_exists( 'get_comment' ) ) { - return; - } - $comment = get_comment( $comment_id ); - if ( ! $comment || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { - return; - } - $is_read = ( 'approve' === $status ) ? 1 : 0; - $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $wpdb->prefix . self::TABLE, - array( 'is_read' => $is_read ), - array( 'comment_id' => $comment_id ), - array( '%d' ), - array( '%d' ) - ); - } - - /** - * Remove shadow row on hp_message deletion. - * - * @param int $comment_id Comment ID. - * @param object|\WP_Comment $comment Comment object. - */ - public function mirror_delete( int $comment_id, $comment ): void { - if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { - return; - } - global $wpdb; - $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $wpdb->prefix . self::TABLE, - array( 'comment_id' => $comment_id ), - array( '%d' ) - ); - } - - /** - * Doctor probe — verify shadow table. - * - * @return array{ok:bool, message:string, details?:array} - */ - public function doctor_check(): array { - global $wpdb; - if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) { - return array( - 'ok' => false, - 'message' => 'wpdb global not available', - ); - } - $table = $wpdb->prefix . self::TABLE; - try { - $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - if ( ! $exists ) { - return array( - 'ok' => false, - 'message' => sprintf( 'hivepress-messages: shadow table %s missing', $table ), - 'details' => array( 'table' => $table ), - ); - } - $rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - return array( - 'ok' => true, - 'message' => sprintf( 'hivepress-messages: %s rows=%d', $table, $rows ), - 'details' => array( - 'table' => $table, - 'rows' => $rows, - ), - ); - } catch ( \Throwable $e ) { - return array( - 'ok' => false, - 'message' => sprintf( 'hivepress-messages doctor failed: %s', $e->getMessage() ), - ); - } - } - - /** - * 8-D self-score. - * - * D1: 1.0 — adapter never calls *_meta() - * D2: 1.0 — uses wpdb prefix exclusively - * D3: 1.0 — shadow table covers recipient/unread access - * D4: 1.0 — expected_columns + indexes registered - * D5: 1.0 — listens to wp_insert_comment + status change for sync - * D6: 1.0 — zero options/transients - * D7: 1.0 — only queries own shadow table - * D8: 1.0 — idx_recipient_unread covers the hot dashboard query - */ - public function suitability_score(): array { - return $this->compose_score( array() ); - } - - /** - * FSM modules this adapter contributes. - * - * @return array - */ - public function migrations(): array { - return array( 'comment_hp_message' ); - } } } // end if ( ! class_exists ) diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-requests-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-requests-adapter.php index 934f651..4ef0b7f 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-requests-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-requests-adapter.php @@ -9,21 +9,9 @@ * — same access pattern as Listing (user dashboard + expiry cron) * * - Offer (comment_type=hp_offer, parent post_type=hp_request) - * comment-column based; benefit comes from comment-router using - * (comment_post_ID, comment_type) covering index + dedicated shadow - * table for "approved offers per request" aggregation. - * - * Shadow table for offers (analogous to favorites pattern): - * - * CREATE TABLE wp_wpdo_comment_hp_offer ( - * comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, - * request_id BIGINT UNSIGNED NOT NULL, - * bidder_id BIGINT UNSIGNED NOT NULL, - * approved TINYINT(1) NOT NULL DEFAULT 0, - * created_at DATETIME NOT NULL, - * KEY idx_request_approved (request_id, approved), - * KEY idx_bidder (bidder_id) - * ); + * stored in comment columns only — no comment meta to flatten. + * (The adapter used to mirror offers into `wpdo_comment_hp_offer`, + * a table nothing ever created.) * * @package WP_Data_Optimizer * @since 3.0.0 @@ -45,9 +33,6 @@ if ( ! class_exists( 'TMDO_HivePress_Requests_Adapter' ) ) { /** Comment_type for the Offer side of Requests. */ public const OFFER_COMMENT_TYPE = 'hp_offer'; - /** Shadow table for offers (without wpdb prefix). */ - public const OFFER_TABLE = 'wpdo_comment_hp_offer'; - /** * Constructor — bind register hooks via inherited trait. */ @@ -105,104 +90,7 @@ if ( ! class_exists( 'TMDO_HivePress_Requests_Adapter' ) ) { } /** - * Declare offer shadow table. - * - * @param TMDO_Custom_Table_Registry $registry Registry singleton. - */ - public function on_register_custom_tables( TMDO_Custom_Table_Registry $registry ): void { - $this->register_custom_table( - $registry, - array( - 'table_name' => self::OFFER_TABLE, - 'primary_key' => 'comment_id', - 'post_type_link' => 'hp_request', - 'expected_columns' => array( - 'comment_id' => 'BIGINT UNSIGNED NOT NULL PRIMARY KEY', - 'request_id' => 'BIGINT UNSIGNED NOT NULL', - 'bidder_id' => 'BIGINT UNSIGNED NOT NULL', - 'approved' => 'TINYINT(1) NOT NULL DEFAULT 0', - 'created_at' => 'DATETIME NOT NULL', - ), - 'indexes' => array( - 'idx_request_approved' => '(request_id, approved)', - 'idx_bidder' => '(bidder_id)', - ), - 'doctor_callback' => array( $this, 'doctor_check' ), - ) - ); - } - - /** - * Mirror new hp_offer comments into the shadow table. - */ - public function on_register_event_hooks(): void { - add_action( 'wp_insert_comment', array( $this, 'mirror_offer_insert' ), 10, 2 ); - add_action( 'edit_comment', array( $this, 'mirror_offer_update' ), 10, 2 ); - add_action( 'deleted_comment', array( $this, 'mirror_offer_delete' ), 10, 2 ); - } - - /** - * Insert a hp_offer mirror row. - * - * @param int $comment_id Comment ID. - * @param object|\WP_Comment $comment Comment object. - */ - public function mirror_offer_insert( int $comment_id, $comment ): void { - if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::OFFER_COMMENT_TYPE ) { - return; - } - global $wpdb; - $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->prepare( - 'INSERT INTO `' . $wpdb->prefix . self::OFFER_TABLE . '` (comment_id, request_id, bidder_id, approved, created_at) VALUES (%d, %d, %d, %d, %s) ON DUPLICATE KEY UPDATE approved = VALUES(approved)', - $comment_id, - (int) ( $comment->comment_post_ID ?? 0 ), - (int) ( $comment->user_id ?? 0 ), - (int) ( $comment->comment_approved ?? 0 ), - $comment->comment_date ?? current_time( 'mysql' ) - ) - ); - } - - /** - * Update mirror row when comment is edited (approval flip is the hot path). - * - * @param int $comment_id Comment ID. - * @param array $data Comment data array passed by edit_comment. - */ - public function mirror_offer_update( int $comment_id, $data ): void { - unset( $data ); - global $wpdb; - // Re-fetch comment object for accurate state then re-mirror via insert path. - if ( function_exists( 'get_comment' ) ) { - $comment = get_comment( $comment_id ); - if ( $comment ) { - $this->mirror_offer_insert( $comment_id, $comment ); - } - } - unset( $wpdb ); - } - - /** - * Remove mirror row on hp_offer deletion. - * - * @param int $comment_id Comment ID. - * @param object|\WP_Comment $comment Comment object. - */ - public function mirror_offer_delete( int $comment_id, $comment ): void { - if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::OFFER_COMMENT_TYPE ) { - return; - } - global $wpdb; - $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $wpdb->prefix . self::OFFER_TABLE, - array( 'comment_id' => $comment_id ), - array( '%d' ) - ); - } - - /** - * Doctor probe — request hot table + offer shadow table. + * Doctor probe — request hot table. * * @return array{ok:bool, message:string, details?:array} */ @@ -214,41 +102,21 @@ if ( ! class_exists( 'TMDO_HivePress_Requests_Adapter' ) ) { 'message' => 'wpdb global not available', ); } - $details = array(); - $ok = true; - foreach ( array( - 'hp_request_hot' => $wpdb->prefix . 'wpdo_hot_hp_request', - 'hp_offer_shadow' => $wpdb->prefix . self::OFFER_TABLE, - ) as $key => $table ) { - try { - $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - $details[ $key ] = array( - 'table' => $table, - 'exists' => $exists, - ); - if ( ! $exists ) { - $ok = false; - } - } catch ( \Throwable $e ) { - $details[ $key ] = array( - 'table' => $table, - 'error' => $e->getMessage(), - ); - $ok = false; - } + $table = $wpdb->prefix . 'wpdo_hot_hp_request'; + try { + $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); + return array( + 'ok' => $exists, + 'message' => $exists + ? sprintf( 'hivepress-requests: %s present', $table ) + : sprintf( 'hivepress-requests: %s missing', $table ), + ); + } catch ( \Throwable $e ) { + return array( + 'ok' => false, + 'message' => sprintf( 'hivepress-requests doctor failed: %s', $e->getMessage() ), + ); } - return array( - 'ok' => $ok, - 'message' => $ok ? 'hivepress-requests: tables OK' : 'hivepress-requests: one or more tables missing', - 'details' => $details, - ); - } - - /** - * 8-D self-score. - */ - public function suitability_score(): array { - return $this->compose_score( array() ); } /** @@ -257,10 +125,7 @@ if ( ! class_exists( 'TMDO_HivePress_Requests_Adapter' ) ) { * @return array */ public function migrations(): array { - return array( - 'hot_hp_request', - 'comment_hp_offer', - ); + return array( 'hot_hp_request' ); } } diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-reviews-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-reviews-adapter.php index 8dae0ad..a3ded1c 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-reviews-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-reviews-adapter.php @@ -10,14 +10,12 @@ * - approved → comment_approved * - listing → comment_post_ID * - * No postmeta is used; no entity-bridge group needed. Hot-path optimization - * comes from `TMDO_HivePress_Comment_Router` rewriting comment-query SQL to - * use `comment_post_ID + comment_type + comment_approved` index, and from - * the `wp_wpdo_comment_hp_review` flat table that WPDO Comment entity bridge - * already maintains (since v2.13.x). + * No postmeta is used; no entity-bridge group needed. Queries use the core + * `wp_comments` indexes on `comment_post_ID` / `comment_type`, and the + * `wp_wpdo_comment_hp_review` flat table is maintained by the WPDO Comment + * entity bridge (since v2.13.x). * * This adapter's role: - * - Declare the comment_type to the comment-router * - Doctor probe of the entity-bridge table * - Self-report 8-D suitability score * @@ -124,7 +122,7 @@ if ( ! class_exists( 'TMDO_HivePress_Reviews_Adapter' ) ) { * D5: 1.0 — no cross-adapter writes needed yet * D6: 1.0 — zero options/transients written * D7: 1.0 — only queries own entity-bridge table - * D8: 1.0 — comment-router uses (comment_post_ID, comment_type) covering index + * D8: 1.0 — reads use the core wp_comments (comment_post_ID, comment_type) indexes */ public function suitability_score(): array { return $this->compose_score( array() ); diff --git a/includes/hivepress/class-tmdo-hivepress-bootstrap.php b/includes/hivepress/class-tmdo-hivepress-bootstrap.php index d271b28..5654af6 100644 --- a/includes/hivepress/class-tmdo-hivepress-bootstrap.php +++ b/includes/hivepress/class-tmdo-hivepress-bootstrap.php @@ -118,12 +118,9 @@ if ( ! class_exists( 'TMDO_HivePress_Bootstrap' ) ) { self::bind_adapter( $slug, $version ); } - // Step 5: register cross-cutting components (router + cron optimizer). - // Both are gated by their own opt-in option and only activate when - // the operator flips it; safe to call here unconditionally. - if ( class_exists( 'TMDO_HivePress_Comment_Router' ) ) { - TMDO_HivePress_Comment_Router::register(); - } + // Step 5: register the cron optimizer. It is gated by its own opt-in + // option and only activates when the operator flips it; safe to call + // here unconditionally. if ( class_exists( 'TMDO_HivePress_Cron_Optimizer' ) ) { TMDO_HivePress_Cron_Optimizer::register(); } diff --git a/includes/hivepress/class-tmdo-hivepress-comment-router.php b/includes/hivepress/class-tmdo-hivepress-comment-router.php deleted file mode 100644 index cb37e5a..0000000 --- a/includes/hivepress/class-tmdo-hivepress-comment-router.php +++ /dev/null @@ -1,166 +0,0 @@ - - */ - private const SHADOW_MAP = array( - 'hp_message' => 'wpdo_comment_hp_message', - 'hp_favorite' => 'wpdo_comment_hp_favorite', - 'hp_offer' => 'wpdo_comment_hp_offer', - 'hp_review' => 'wpdo_comment_hp_review', - ); - - /** - * Whether the router is bound this request. - * - * @var bool - */ - private static bool $bound = false; - - /** - * Bind the comments_clauses filter (idempotent). - * - * Bootstrap calls this from each adapter's on_register_query_hooks() - * so router activation follows the same gate as adapter binding. - */ - public static function register(): void { - if ( self::$bound ) { - return; - } - if ( ! self::is_enabled() ) { - return; - } - add_filter( 'comments_clauses', array( __CLASS__, 'rewrite' ), 20, 2 ); - self::$bound = true; - } - - /** - * Reset internal state (test only). - * - * @internal - */ - public static function reset_for_tests(): void { - self::$bound = false; - } - - /** - * Whether router is enabled via the operator option. - */ - public static function is_enabled(): bool { - if ( ! function_exists( 'get_option' ) ) { - return false; - } - return (bool) (int) get_option( self::OPTION_ENABLED, 0 ); - } - - /** - * Filter callback: rewrite comments_clauses for known hp comment types. - * - * @param array $clauses SQL clauses (join, where, fields, ...). - * @param \WP_Comment_Query $query The query object. - * @return array Possibly-modified clauses. - */ - public static function rewrite( array $clauses, $query ): array { - $type = self::query_comment_type( $query ); - if ( '' === $type || ! isset( self::SHADOW_MAP[ $type ] ) ) { - return $clauses; - } - - global $wpdb; - $shadow = $wpdb->prefix . self::SHADOW_MAP[ $type ]; - $alias = 'wpdo_shadow'; - - // LEFT JOIN keeps row visibility when shadow lags (during backfill). - $join = ( $clauses['join'] ?? '' ) . " LEFT JOIN `{$shadow}` AS {$alias} ON {$alias}.comment_id = {$wpdb->comments}.comment_ID"; - - // Filter pushdown: hp_message recipient lookup uses comment_karma in HP. - // We rewrite WHERE clauses that filter by comment_karma (= recipient hack) - // to use the indexed shadow column. - $where = (string) ( $clauses['where'] ?? '' ); - if ( 'hp_message' === $type && '' !== $where ) { - $where = preg_replace( - '/' . preg_quote( $wpdb->comments, '/' ) . '\.comment_karma\s*=\s*(\d+)/', - $alias . '.recipient_id = $1', - $where - ); - } - - $clauses['join'] = $join; - $clauses['where'] = $where; - - /** - * Filter: wpdo_hivepress_comment_router_clauses - * - * Allows fine-grained tweaks per comment_type. Intended for adapter - * tests + future custom-column lookups. - * - * @param array $clauses Modified clauses. - * @param string $type Matched comment_type. - * @param string $alias Shadow table alias. - */ - return apply_filters( 'wpdo_hivepress_comment_router_clauses', $clauses, $type, $alias ); - } - - /** - * Extract the comment_type from a WP_Comment_Query. - * - * @param \WP_Comment_Query $query Query object. - */ - private static function query_comment_type( $query ): string { - if ( ! is_object( $query ) || ! isset( $query->query_vars ) ) { - return ''; - } - $type = $query->query_vars['type'] ?? ''; - if ( is_array( $type ) ) { - $type = reset( $type ); - } - return is_string( $type ) ? $type : ''; - } - } - -} // end if ( ! class_exists ) diff --git a/includes/hivepress/interface-tmdo-hp-adapter.php b/includes/hivepress/interface-tmdo-hp-adapter.php index acc2233..0cd52c4 100644 --- a/includes/hivepress/interface-tmdo-hp-adapter.php +++ b/includes/hivepress/interface-tmdo-hp-adapter.php @@ -119,7 +119,7 @@ if ( ! interface_exists( 'TMDO_HivePress_Adapter' ) ) { * * Adapters with hot zone fields used in search/filter MUST register a * `pre_get_posts` / `pre_get_comments` filter that defers to - * `TMDO_HivePress_Query_Router` / `..._Comment_Router`. + * `TMDO_HivePress_Query_Router`. * * Called on `init:5` after Schema_Registry is fully populated. */ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index d041084..e358c4e 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -38,7 +38,6 @@ $tmdo_hp_files = array( 'includes/hivepress/class-tmdo-hivepress-detector.php', 'includes/hivepress/class-tmdo-hivepress-conflict-guard.php', 'includes/hivepress/class-tmdo-hivepress-bootstrap.php', - 'includes/hivepress/class-tmdo-hivepress-comment-router.php', 'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php', 'includes/hivepress/class-tmdo-hivepress-attribute-bridge.php', 'includes/hivepress/class-tmdo-hivepress-suitability-scorer.php', diff --git a/tests/integration/HivepressIntegrationTest.php b/tests/integration/HivepressIntegrationTest.php index b1f6ce1..bfa2e4b 100644 --- a/tests/integration/HivepressIntegrationTest.php +++ b/tests/integration/HivepressIntegrationTest.php @@ -12,147 +12,17 @@ use PHPUnit\Framework\TestCase; * Exercises against real MariaDB (via tests/integration/bootstrap.php) the * scenarios that unit tests can only stub: * - * - Shadow table CREATE actually works with the declared expected_columns - * - UNIQUE constraint on (user_id, listing_id) really prevents dup favorites - * - mirror_insert() roundtrips through real $wpdb (SQL syntax valid) * - 7-state FSM lifecycle moves WPDO_Feature_Flags correctly * * @group integration */ class HivepressIntegrationTest extends TestCase { - /** Test-scope shadow table for favorites. */ - private const TABLE_FAVORITE = 'wp_itest_wpdo_comment_hp_favorite'; - - /** Test-scope shadow table for messages. */ - private const TABLE_MESSAGE = 'wp_itest_wpdo_comment_hp_message'; - - public static function setUpBeforeClass(): void { - // HivePress integration files are loaded at file-top level so the - // Testable* subclass definitions below can resolve their parents at - // parse time. Schema setup happens here. - global $wpdb; - $wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_FAVORITE . '`' ); - $wpdb->query( "CREATE TABLE `" . self::TABLE_FAVORITE . "` ( - comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, - user_id BIGINT UNSIGNED NOT NULL, - listing_id BIGINT UNSIGNED NOT NULL, - created_at DATETIME NOT NULL, - UNIQUE KEY unique_user_listing (user_id, listing_id), - KEY idx_listing (listing_id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" ); - - $wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_MESSAGE . '`' ); - $wpdb->query( "CREATE TABLE `" . self::TABLE_MESSAGE . "` ( - comment_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, - sender_id BIGINT UNSIGNED NOT NULL, - recipient_id BIGINT UNSIGNED NOT NULL, - listing_id BIGINT UNSIGNED NOT NULL, - is_read TINYINT(1) NOT NULL DEFAULT 0, - sent_at DATETIME NOT NULL, - KEY idx_recipient_unread (recipient_id, is_read), - KEY idx_thread (listing_id, sent_at) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" ); - } - - public static function tearDownAfterClass(): void { - global $wpdb; - $wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_FAVORITE . '`' ); - $wpdb->query( 'DROP TABLE IF EXISTS `' . self::TABLE_MESSAGE . '`' ); - } - protected function setUp(): void { - global $wpdb; - $wpdb->query( 'TRUNCATE TABLE `' . self::TABLE_FAVORITE . '`' ); - $wpdb->query( 'TRUNCATE TABLE `' . self::TABLE_MESSAGE . '`' ); // Clear FSM module state between tests. $GLOBALS['_wp_options']['wpdo_features'] = array(); } - // ── Schema tests ──────────────────────────────────────────────────────── - - public function test_favorite_table_has_unique_user_listing_constraint(): void { - global $wpdb; - $row = $wpdb->get_row( 'SHOW CREATE TABLE `' . self::TABLE_FAVORITE . '`', 'ARRAY_A' ); - $this->assertNotNull( $row ); - $ddl = $row['Create Table'] ?? ''; - $this->assertStringContainsString( 'UNIQUE KEY', $ddl ); - $this->assertStringContainsString( 'unique_user_listing', $ddl ); - } - - public function test_message_table_has_recipient_unread_index(): void { - global $wpdb; - $row = $wpdb->get_row( 'SHOW CREATE TABLE `' . self::TABLE_MESSAGE . '`', 'ARRAY_A' ); - $this->assertNotNull( $row ); - $ddl = $row['Create Table'] ?? ''; - $this->assertStringContainsString( 'idx_recipient_unread', $ddl ); - $this->assertStringContainsString( 'recipient_id', $ddl ); - } - - // ── mirror_insert real roundtrip ──────────────────────────────────────── - - public function test_favorite_mirror_insert_writes_real_row(): void { - global $wpdb; - $adapter = new WPDO_HivePress_Favorites_Adapter(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_favorite'; - $comment->user_id = 11; - $comment->comment_post_ID = 222; - $comment->comment_date = '2026-05-04 12:00:00'; - - $adapter->mirror_insert( 1001, $comment ); - - $rows = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE_FAVORITE . '`' ); - $this->assertSame( 1, $rows ); - - $saved = $wpdb->get_row( 'SELECT * FROM `' . self::TABLE_FAVORITE . '` WHERE comment_id = 1001', 'ARRAY_A' ); - $this->assertSame( '11', (string) $saved['user_id'] ); - $this->assertSame( '222', (string) $saved['listing_id'] ); - } - - public function test_favorite_unique_constraint_blocks_double_tap(): void { - global $wpdb; - $adapter = new WPDO_HivePress_Favorites_Adapter(); - - // Two different comment_ids but same user+listing — UNIQUE should win. - $comment = new stdClass(); - $comment->comment_type = 'hp_favorite'; - $comment->user_id = 7; - $comment->comment_post_ID = 99; - $comment->comment_date = '2026-05-04 12:00:00'; - - $adapter->mirror_insert( 2001, $comment ); - $adapter->mirror_insert( 2002, $comment ); // Race-style duplicate. - - $rows = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE_FAVORITE . '` WHERE user_id = 7 AND listing_id = 99' ); - $this->assertSame( 1, $rows, 'UNIQUE(user_id, listing_id) must reject duplicate.' ); - } - - public function test_message_mirror_insert_promotes_recipient_id(): void { - global $wpdb; - $adapter = new WPDO_HivePress_Messages_Adapter(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_message'; - $comment->user_id = 5; // sender - $comment->comment_karma = 42; // recipient (HP hack) - $comment->comment_post_ID = 99; - $comment->comment_approved = 0; - $comment->comment_date = '2026-05-04 12:00:00'; - - $adapter->mirror_insert( 3001, $comment ); - - $saved = $wpdb->get_row( 'SELECT * FROM `' . self::TABLE_MESSAGE . '` WHERE comment_id = 3001', 'ARRAY_A' ); - $this->assertNotNull( $saved ); - $this->assertSame( '42', (string) $saved['recipient_id'] ); - $this->assertSame( '5', (string) $saved['sender_id'] ); - - // recipient_unread index supports the dashboard query. - $unread_count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::TABLE_MESSAGE . '` WHERE recipient_id = 42 AND is_read = 0' ); - $this->assertSame( 1, $unread_count ); - } - // ── FSM lifecycle ─────────────────────────────────────────────────────── public function test_fsm_lifecycle_progression(): void { @@ -189,9 +59,3 @@ class HivepressIntegrationTest extends TestCase { $this->assertSame( 0, $count, 'Optimizer must short-circuit when module is idle.' ); } } - -// Production adapters use `$wpdb->prefix . self::TABLE` to compose the full -// table name. Integration test bootstrap sets prefix to `wp_itest_`, so the -// production mirror_insert() naturally writes to wp_itest_wpdo_comment_hp_*. -// No subclassing or test-double needed — the production code path is exercised -// directly against real MariaDB with isolated prefix. diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index 2ce8c97..72f5713 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -39,7 +39,6 @@ $tmdo_hp_files = array( 'includes/hivepress/class-tmdo-hivepress-detector.php', 'includes/hivepress/class-tmdo-hivepress-conflict-guard.php', 'includes/hivepress/class-tmdo-hivepress-bootstrap.php', - 'includes/hivepress/class-tmdo-hivepress-comment-router.php', 'includes/hivepress/class-tmdo-hivepress-cron-optimizer.php', 'includes/hivepress/class-tmdo-hivepress-attribute-bridge.php', 'includes/hivepress/class-tmdo-hivepress-suitability-scorer.php', diff --git a/tests/unit/CommentRouterTest.php b/tests/unit/CommentRouterTest.php deleted file mode 100644 index 7ce74f4..0000000 --- a/tests/unit/CommentRouterTest.php +++ /dev/null @@ -1,92 +0,0 @@ -install_wpdb_mock(); - WPDO_HivePress_Comment_Router::reset_for_tests(); - // Default disabled. - $GLOBALS['_wp_options'][ WPDO_HivePress_Comment_Router::OPTION_ENABLED ] = 0; - } - - public function test_disabled_by_default(): void { - $this->assertFalse( WPDO_HivePress_Comment_Router::is_enabled() ); - } - - public function test_enabled_when_option_set(): void { - $GLOBALS['_wp_options'][ WPDO_HivePress_Comment_Router::OPTION_ENABLED ] = 1; - $this->assertTrue( WPDO_HivePress_Comment_Router::is_enabled() ); - } - - public function test_register_is_idempotent(): void { - $GLOBALS['_wp_options'][ WPDO_HivePress_Comment_Router::OPTION_ENABLED ] = 1; - WPDO_HivePress_Comment_Router::register(); - WPDO_HivePress_Comment_Router::register(); // Second call must be no-op. - $this->assertTrue( true ); // No throw = pass - } - - public function test_rewrite_skips_unknown_comment_type(): void { - $query = new stdClass(); - $query->query_vars = array( 'type' => 'comment' ); // not in shadow map - - $clauses = array( 'join' => '', 'where' => '1=1' ); - $out = WPDO_HivePress_Comment_Router::rewrite( $clauses, $query ); - - $this->assertSame( $clauses, $out ); - } - - public function test_rewrite_adds_left_join_for_hp_favorite(): void { - $query = new stdClass(); - $query->query_vars = array( 'type' => 'hp_favorite' ); - - $clauses = array( 'join' => '', 'where' => '1=1' ); - $out = WPDO_HivePress_Comment_Router::rewrite( $clauses, $query ); - - $this->assertStringContainsString( 'LEFT JOIN', $out['join'] ); - $this->assertStringContainsString( 'wpdo_comment_hp_favorite', $out['join'] ); - $this->assertStringContainsString( 'AS wpdo_shadow', $out['join'] ); - } - - public function test_rewrite_promotes_recipient_for_hp_message(): void { - $query = new stdClass(); - $query->query_vars = array( 'type' => 'hp_message' ); - - $clauses = array( - 'join' => '', - 'where' => 'wp_comments.comment_karma = 42 AND comment_approved = 0', - ); - $out = WPDO_HivePress_Comment_Router::rewrite( $clauses, $query ); - - // recipient lookup should be rewritten to use shadow column. - $this->assertStringContainsString( 'wpdo_shadow.recipient_id = 42', $out['where'] ); - // Other predicates preserved. - $this->assertStringContainsString( 'comment_approved = 0', $out['where'] ); - } - - public function test_rewrite_handles_array_type(): void { - $query = new stdClass(); - $query->query_vars = array( 'type' => array( 'hp_review', 'comment' ) ); - - $clauses = array( 'join' => '', 'where' => '' ); - $out = WPDO_HivePress_Comment_Router::rewrite( $clauses, $query ); - - // First element of type array should match (hp_review). - $this->assertStringContainsString( 'wpdo_comment_hp_review', $out['join'] ); - } - - public function test_rewrite_returns_unchanged_when_query_invalid(): void { - $clauses = array( 'join' => '', 'where' => '1=1' ); - $out = WPDO_HivePress_Comment_Router::rewrite( $clauses, 'not an object' ); - $this->assertSame( $clauses, $out ); - } -} diff --git a/tests/unit/FavoritesAdapterTest.php b/tests/unit/FavoritesAdapterTest.php index 0b74b86..b6a3e23 100644 --- a/tests/unit/FavoritesAdapterTest.php +++ b/tests/unit/FavoritesAdapterTest.php @@ -6,6 +6,9 @@ use PHPUnit\Framework\TestCase; /** * Tests for WPDO_HivePress_Favorites_Adapter. * + * The adapter no longer mirrors favorites into a shadow table: that table was + * never created, so every mirror write failed. + * * @covers WPDO_HivePress_Favorites_Adapter */ class HivePressFavoritesAdapterTest extends TestCase { @@ -32,72 +35,22 @@ class HivePressFavoritesAdapterTest extends TestCase { public function test_constants_exposed(): void { $this->assertSame( 'hp_favorite', WPDO_HivePress_Favorites_Adapter::COMMENT_TYPE ); - $this->assertSame( 'wpdo_comment_hp_favorite', WPDO_HivePress_Favorites_Adapter::TABLE ); } - public function test_on_register_custom_tables_declares_shadow_table(): void { + public function test_registers_no_shadow_table(): void { $registry = WPDO_Custom_Table_Registry::instance(); $this->adapter->on_register_custom_tables( $registry ); - $tables = $registry->all(); - $this->assertCount( 1, $tables ); - - $cfg = reset( $tables ); - $this->assertSame( 'hivepress-favorites', $cfg['provider'] ); - $this->assertSame( 'wpdo_comment_hp_favorite', $cfg['table_name'] ); - $this->assertSame( 'comment_id', $cfg['primary_key'] ); - $this->assertArrayHasKey( 'unique_user_listing', $cfg['indexes'] ); - $this->assertStringContainsString( 'UNIQUE', $cfg['indexes']['unique_user_listing'] ); + $this->assertSame( array(), $registry->all() ); } - public function test_mirror_insert_skips_non_favorite_comments(): void { - // Stub $wpdb tracks queries. - global $wpdb; - $wpdb->queries = array(); + public function test_binds_no_comment_hooks(): void { + $GLOBALS['_wp_filter_callbacks'] = array(); - $comment = new stdClass(); - $comment->comment_type = 'comment'; // ← not hp_favorite - $comment->user_id = 1; - $comment->comment_post_ID = 99; + $this->adapter->on_register_event_hooks(); - $this->adapter->mirror_insert( 123, $comment ); - $this->assertSame( array(), $wpdb->queries ); - } - - public function test_mirror_insert_writes_for_hp_favorite(): void { - global $wpdb; - $wpdb->queries = array(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_favorite'; - $comment->user_id = 7; - $comment->comment_post_ID = 42; - $comment->comment_date = '2026-05-04 12:00:00'; - - $this->adapter->mirror_insert( 100, $comment ); - - $this->assertCount( 1, $wpdb->queries ); - $this->assertStringContainsString( 'INSERT IGNORE', $wpdb->queries[0] ); - $this->assertStringContainsString( 'wpdo_comment_hp_favorite', $wpdb->queries[0] ); - } - - public function test_mirror_insert_skips_when_user_or_listing_missing(): void { - global $wpdb; - $wpdb->queries = array(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_favorite'; - $comment->user_id = 0; - $comment->comment_post_ID = 99; - - $this->adapter->mirror_insert( 100, $comment ); - $this->assertSame( array(), $wpdb->queries ); - } - - public function test_doctor_check_reports_missing_table_in_stub_env(): void { - $result = $this->adapter->doctor_check(); - $this->assertFalse( $result['ok'] ); - $this->assertStringContainsString( 'wpdo_comment_hp_favorite', $result['message'] ); + $this->assertArrayNotHasKey( 'wp_insert_comment', $GLOBALS['_wp_filter_callbacks'] ); + $this->assertArrayNotHasKey( 'deleted_comment', $GLOBALS['_wp_filter_callbacks'] ); } public function test_score_aggregates_to_perfect_ten(): void { @@ -105,7 +58,7 @@ class HivePressFavoritesAdapterTest extends TestCase { $this->assertSame( 10.0, $score['aggregate'] ); } - public function test_migrations_lists_comment_module(): void { - $this->assertSame( array( 'comment_hp_favorite' ), $this->adapter->migrations() ); + public function test_migrations_is_empty(): void { + $this->assertSame( array(), $this->adapter->migrations() ); } } diff --git a/tests/unit/MessagesAdapterTest.php b/tests/unit/MessagesAdapterTest.php index ab59f5c..c9d6c33 100644 --- a/tests/unit/MessagesAdapterTest.php +++ b/tests/unit/MessagesAdapterTest.php @@ -6,6 +6,9 @@ use PHPUnit\Framework\TestCase; /** * Tests for WPDO_HivePress_Messages_Adapter. * + * The adapter no longer mirrors messages into a shadow table: that table was + * never created, so every mirror write failed. + * * @covers WPDO_HivePress_Messages_Adapter */ class HivePressMessagesAdapterTest extends TestCase { @@ -28,77 +31,26 @@ class HivePressMessagesAdapterTest extends TestCase { $this->assertSame( 'hivepress-messages', $this->adapter->plugin_slug() ); $this->assertSame( 'HivePress\\Messages\\Plugin', $this->adapter->detection_class() ); $this->assertSame( 'hp_message', WPDO_HivePress_Messages_Adapter::COMMENT_TYPE ); - $this->assertSame( 'wpdo_comment_hp_message', WPDO_HivePress_Messages_Adapter::TABLE ); } - public function test_on_register_custom_tables_declares_indexed_recipient(): void { + public function test_registers_no_shadow_table(): void { $registry = WPDO_Custom_Table_Registry::instance(); $this->adapter->on_register_custom_tables( $registry ); - $tables = $registry->all(); - $cfg = reset( $tables ); - $this->assertSame( 'wpdo_comment_hp_message', $cfg['table_name'] ); - $this->assertArrayHasKey( 'recipient_id', $cfg['expected_columns'] ); - $this->assertArrayHasKey( 'idx_recipient_unread', $cfg['indexes'] ); - $this->assertStringContainsString( 'recipient_id, is_read', $cfg['indexes']['idx_recipient_unread'] ); + $this->assertSame( array(), $registry->all() ); } - public function test_mirror_insert_skips_non_message_comments(): void { - global $wpdb; - $wpdb->queries = array(); + public function test_binds_no_comment_hooks(): void { + $GLOBALS['_wp_filter_callbacks'] = array(); - $comment = new stdClass(); - $comment->comment_type = 'comment'; - $comment->user_id = 1; - $comment->comment_karma = 5; + $this->adapter->on_register_event_hooks(); - $this->adapter->mirror_insert( 1, $comment ); - $this->assertSame( array(), $wpdb->queries ); + $this->assertArrayNotHasKey( 'wp_insert_comment', $GLOBALS['_wp_filter_callbacks'] ); + $this->assertArrayNotHasKey( 'wp_set_comment_status', $GLOBALS['_wp_filter_callbacks'] ); + $this->assertArrayNotHasKey( 'deleted_comment', $GLOBALS['_wp_filter_callbacks'] ); } - public function test_mirror_insert_promotes_recipient_from_comment_karma(): void { - global $wpdb; - $wpdb->queries = array(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_message'; - $comment->user_id = 7; // sender - $comment->comment_karma = 42; // ← recipient hack lives here - $comment->comment_post_ID = 99; - $comment->comment_approved = 0; - $comment->comment_date = '2026-05-04 12:00:00'; - - $this->adapter->mirror_insert( 100, $comment ); - - $this->assertCount( 1, $wpdb->queries ); - $sql = $wpdb->queries[0]; - $this->assertStringContainsString( 'INSERT INTO', $sql ); - $this->assertStringContainsString( 'wpdo_comment_hp_message', $sql ); - // The promoted recipient_id should appear in the bound SQL. - $this->assertStringContainsString( '42', $sql ); - } - - public function test_mirror_insert_skips_when_sender_or_recipient_zero(): void { - global $wpdb; - $wpdb->queries = array(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_message'; - $comment->user_id = 0; // bad sender - $comment->comment_karma = 42; - $comment->comment_post_ID = 99; - - $this->adapter->mirror_insert( 100, $comment ); - $this->assertSame( array(), $wpdb->queries ); - } - - public function test_doctor_check_reports_missing_table_in_stub_env(): void { - $result = $this->adapter->doctor_check(); - $this->assertFalse( $result['ok'] ); - $this->assertStringContainsString( 'wpdo_comment_hp_message', $result['message'] ); - } - - public function test_score_aggregates_to_perfect_ten(): void { - $this->assertSame( 10.0, $this->adapter->suitability_score()['aggregate'] ); + public function test_migrations_is_empty(): void { + $this->assertSame( array(), $this->adapter->migrations() ); } } diff --git a/tests/unit/RequestsAdapterTest.php b/tests/unit/RequestsAdapterTest.php index 276629e..0c850b1 100644 --- a/tests/unit/RequestsAdapterTest.php +++ b/tests/unit/RequestsAdapterTest.php @@ -48,54 +48,29 @@ class HivePressRequestsAdapterTest extends TestCase { $this->assertTrue( (bool) $expired['indexed'] ); } - public function test_on_register_custom_tables_declares_offer_shadow(): void { + public function test_registers_no_offer_shadow_table(): void { + // The offer shadow table was never created, so every mirror write failed. $registry = WPDO_Custom_Table_Registry::instance(); $this->adapter->on_register_custom_tables( $registry ); - $tables = $registry->all(); - $cfg = reset( $tables ); - $this->assertSame( 'wpdo_comment_hp_offer', $cfg['table_name'] ); - $this->assertSame( 'hp_request', $cfg['post_type_link'] ); - $this->assertArrayHasKey( 'idx_request_approved', $cfg['indexes'] ); + $this->assertSame( array(), $registry->all() ); } - public function test_mirror_offer_insert_skips_other_comment_types(): void { - global $wpdb; - $wpdb->queries = array(); + public function test_binds_no_comment_hooks(): void { + $GLOBALS['_wp_filter_callbacks'] = array(); - $comment = new stdClass(); - $comment->comment_type = 'comment'; // ← not hp_offer - $comment->comment_post_ID = 99; - $comment->user_id = 1; + $this->adapter->on_register_event_hooks(); - $this->adapter->mirror_offer_insert( 1, $comment ); - $this->assertSame( array(), $wpdb->queries ); - } - - public function test_mirror_offer_insert_writes_for_hp_offer(): void { - global $wpdb; - $wpdb->queries = array(); - - $comment = new stdClass(); - $comment->comment_type = 'hp_offer'; - $comment->comment_post_ID = 42; - $comment->user_id = 7; - $comment->comment_approved = 0; - $comment->comment_date = '2026-05-04 12:00:00'; - - $this->adapter->mirror_offer_insert( 100, $comment ); - $this->assertCount( 1, $wpdb->queries ); - $this->assertStringContainsString( 'INSERT INTO', $wpdb->queries[0] ); - $this->assertStringContainsString( 'wpdo_comment_hp_offer', $wpdb->queries[0] ); + $this->assertArrayNotHasKey( 'wp_insert_comment', $GLOBALS['_wp_filter_callbacks'] ); + $this->assertArrayNotHasKey( 'edit_comment', $GLOBALS['_wp_filter_callbacks'] ); + $this->assertArrayNotHasKey( 'deleted_comment', $GLOBALS['_wp_filter_callbacks'] ); } public function test_score_aggregates_to_perfect_ten(): void { $this->assertSame( 10.0, $this->adapter->suitability_score()['aggregate'] ); } - public function test_migrations_lists_request_and_offer_modules(): void { - $mods = $this->adapter->migrations(); - $this->assertContains( 'hot_hp_request', $mods ); - $this->assertContains( 'comment_hp_offer', $mods ); + public function test_migrations_lists_only_request_hot_module(): void { + $this->assertSame( array( 'hot_hp_request' ), $this->adapter->migrations() ); } } diff --git a/tests/unit/WpdbMockTrait.php b/tests/unit/WpdbMockTrait.php index e5ec071..c8da95d 100644 --- a/tests/unit/WpdbMockTrait.php +++ b/tests/unit/WpdbMockTrait.php @@ -7,7 +7,7 @@ declare(strict_types=1); * Several existing wpdo tests (ZoneHotTest, SyncBridgeTest, etc.) replace the * global $wpdb with their own anonymous-class mock and never restore it. When * our adapter tests run after those, $wpdb lacks the query/get_col/delete - * methods our mirror_insert / cron-optimizer code paths call, causing + * methods our cron-optimizer code paths call, causing * cross-test pollution failures. * * Trait reinstalls a known $wpdb mock + clears the wpdo_features option so From ff6d6b10e49804b6e1f52940b01c3527c16eba7e Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 25 Sep 2026 23:01:56 +0800 Subject: [PATCH 05/12] refactor(2meet-data-optimizer-hivepress-addon): remove HPCT-era interceptors and query rewriters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete 7 interceptors + 5 query rewriters that read/write hpct_* tables no site has (only an empty hpct_latepoint_idx exists) - Drop their loading/registration in TMDO_HP_Bootstrap, back-compat aliases, integration bootstrap, and ListingMetaInterceptorTest - Verify: unit 141 OK, integration 49 OK, bootstrap smoke OK; phpcs 34E/40W → 25E/28W - PR: PR-014 --- includes/back-compat-aliases.php | 12 - includes/class-tmdo-hivepress-bootstrap.php | 60 +--- .../class-tmdo-favorites-interceptor.php | 117 ------ .../class-tmdo-listing-meta-interceptor.php | 339 ------------------ .../class-tmdo-memberships-interceptor.php | 76 ---- .../class-tmdo-messages-interceptor.php | 78 ---- .../class-tmdo-requests-interceptor.php | 74 ---- .../class-tmdo-reviews-interceptor.php | 116 ------ .../class-tmdo-statistics-interceptor.php | 139 ------- .../query/class-tmdo-listing-meta-query.php | 236 ------------ .../query/class-tmdo-memberships-query.php | 67 ---- includes/query/class-tmdo-messages-query.php | 63 ---- includes/query/class-tmdo-requests-query.php | 61 ---- includes/query/class-tmdo-reviews-query.php | 64 ---- .../ListingMetaInterceptorTest.php | 160 --------- tests/integration/bootstrap.php | 8 - 16 files changed, 1 insertion(+), 1669 deletions(-) delete mode 100644 includes/interceptors/class-tmdo-favorites-interceptor.php delete mode 100644 includes/interceptors/class-tmdo-listing-meta-interceptor.php delete mode 100644 includes/interceptors/class-tmdo-memberships-interceptor.php delete mode 100644 includes/interceptors/class-tmdo-messages-interceptor.php delete mode 100644 includes/interceptors/class-tmdo-requests-interceptor.php delete mode 100644 includes/interceptors/class-tmdo-reviews-interceptor.php delete mode 100644 includes/interceptors/class-tmdo-statistics-interceptor.php delete mode 100644 includes/query/class-tmdo-listing-meta-query.php delete mode 100644 includes/query/class-tmdo-memberships-query.php delete mode 100644 includes/query/class-tmdo-messages-query.php delete mode 100644 includes/query/class-tmdo-requests-query.php delete mode 100644 includes/query/class-tmdo-reviews-query.php delete mode 100644 tests/integration/ListingMetaInterceptorTest.php diff --git a/includes/back-compat-aliases.php b/includes/back-compat-aliases.php index 78f487f..27c5adf 100644 --- a/includes/back-compat-aliases.php +++ b/includes/back-compat-aliases.php @@ -20,7 +20,6 @@ add_action( $aliases = array( 'TMDO_Admin_HivePress' => 'WPDO_Admin_HivePress', 'TMDO_CLI_HivePress' => 'WPDO_CLI_HivePress', - 'TMDO_Favorites_Interceptor' => 'WPDO_Favorites_Interceptor', 'TMDO_HPCT_Import' => 'WPDO_HPCT_Import', 'TMDO_HP_Bootstrap' => 'WPDO_HP_Bootstrap', 'TMDO_HivePress' => 'WPDO_HivePress', @@ -47,18 +46,7 @@ add_action( 'TMDO_HivePress_Tags_Adapter' => 'WPDO_HivePress_Tags_Adapter', 'TMDO_Hivepress_Term_Comment_Fields' => 'WPDO_Hivepress_Term_Comment_Fields', 'TMDO_Hivepress_Transient_Filter' => 'WPDO_Hivepress_Transient_Filter', - 'TMDO_Listing_Meta_Interceptor' => 'WPDO_Listing_Meta_Interceptor', - 'TMDO_Listing_Meta_Query' => 'WPDO_Listing_Meta_Query', 'TMDO_Listing_Stats' => 'WPDO_Listing_Stats', - 'TMDO_Memberships_Interceptor' => 'WPDO_Memberships_Interceptor', - 'TMDO_Memberships_Query' => 'WPDO_Memberships_Query', - 'TMDO_Messages_Interceptor' => 'WPDO_Messages_Interceptor', - 'TMDO_Messages_Query' => 'WPDO_Messages_Query', - 'TMDO_Requests_Interceptor' => 'WPDO_Requests_Interceptor', - 'TMDO_Requests_Query' => 'WPDO_Requests_Query', - 'TMDO_Reviews_Interceptor' => 'WPDO_Reviews_Interceptor', - 'TMDO_Reviews_Query' => 'WPDO_Reviews_Query', - 'TMDO_Statistics_Interceptor' => 'WPDO_Statistics_Interceptor', ); foreach ( $aliases as $tmdo_class => $wpdo_alias ) { if ( class_exists( $tmdo_class, false ) && ! class_exists( $wpdo_alias, false ) ) { diff --git a/includes/class-tmdo-hivepress-bootstrap.php b/includes/class-tmdo-hivepress-bootstrap.php index c5f9c5c..831d33d 100644 --- a/includes/class-tmdo-hivepress-bootstrap.php +++ b/includes/class-tmdo-hivepress-bootstrap.php @@ -2,8 +2,7 @@ /** * Bootstrap for 2meet Data Optimizer HivePress AddOn. * - * 載入 13 adapter + 7 interceptor + 5 query interceptor + 補助組件 + - * admin tab + CLI namespace。 + * 載入 13 adapter + 補助組件 + admin tab + CLI namespace。 * * 注意:HP 的 V3.0.0 family bootstrap 內部也有一個 class TMDO_HivePress_Bootstrap * (在 includes/hivepress/class-tmdo-hivepress-bootstrap.php)。為避免衝突,本 @@ -64,35 +63,6 @@ final class TMDO_HP_Bootstrap { require_once $base . 'includes/hivepress/class-tmdo-hivepress-benchmark.php'; require_once $base . 'includes/hivepress/class-tmdo-hivepress-rest.php'; - // 7 HPCT interceptors. - // reviews / messages / memberships / requests extend the core's - // TMDO_Standard_Post_Interceptor — requiring them against a core older than - // v0.2.0 would fatal at parse time, so gate those four on the base class. - $has_standard_base = class_exists( 'TMDO_Standard_Post_Interceptor' ); - foreach ( array( - 'reviews', 'messages', 'favorites', 'memberships', - 'statistics', 'requests', 'listing-meta', - ) as $intslug ) { - $needs_base = in_array( $intslug, array( 'reviews', 'messages', 'memberships', 'requests' ), true ); - if ( $needs_base && ! $has_standard_base ) { - continue; - } - $file = $base . "includes/interceptors/class-tmdo-{$intslug}-interceptor.php"; - if ( file_exists( $file ) ) { - require_once $file; - } - } - - // 5 query interceptors - foreach ( array( - 'reviews', 'messages', 'memberships', 'requests', 'listing-meta', - ) as $qslug ) { - $file = $base . "includes/query/class-tmdo-{$qslug}-query.php"; - if ( file_exists( $file ) ) { - require_once $file; - } - } - // Misc HP integration files require_once $base . 'includes/class-tmdo-hivepress.php'; require_once $base . 'includes/class-tmdo-hivepress-transient-filter.php'; @@ -110,34 +80,6 @@ final class TMDO_HP_Bootstrap { TMDO_HivePress_Bootstrap::boot(); } - // 7 HPCT interceptors - foreach ( array( - 'TMDO_Reviews_Interceptor', - 'TMDO_Messages_Interceptor', - 'TMDO_Favorites_Interceptor', - 'TMDO_Memberships_Interceptor', - 'TMDO_Statistics_Interceptor', - 'TMDO_Requests_Interceptor', - 'TMDO_Listing_Meta_Interceptor', - ) as $cls ) { - if ( class_exists( $cls ) ) { - ( new $cls() )->register_hooks(); - } - } - - // 5 query interceptors - foreach ( array( - 'TMDO_Reviews_Query', - 'TMDO_Messages_Query', - 'TMDO_Memberships_Query', - 'TMDO_Requests_Query', - 'TMDO_Listing_Meta_Query', - ) as $cls ) { - if ( class_exists( $cls ) ) { - ( new $cls() )->register_hooks(); - } - } - // HP transient filter (init:5) if ( class_exists( 'TMDO_Hivepress_Transient_Filter' ) ) { add_action( 'init', array( 'TMDO_Hivepress_Transient_Filter', 'init' ), 5 ); diff --git a/includes/interceptors/class-tmdo-favorites-interceptor.php b/includes/interceptors/class-tmdo-favorites-interceptor.php deleted file mode 100644 index c24abad..0000000 --- a/includes/interceptors/class-tmdo-favorites-interceptor.php +++ /dev/null @@ -1,117 +0,0 @@ -is_active() ) { - return $check; - } - - try { - $this->sync_favorites( $user_id, $meta_value ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'update_user_metadata', $e->getMessage() ); - } - - return $check; - } - - /** - * Deletes favorites records when a listing post is deleted. - * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @return void - */ - public function action_delete_post( int $post_id, \WP_Post $post ): void { - if ( 'hp_listing' !== $post->post_type || ! $this->is_active() ) { - return; - } - - try { - global $wpdb; - $wpdb->delete( TMDO_DB::table( 'hpct_favorites' ), array( 'listing_id' => $post_id ), array( '%d' ) ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() ); - } - } - - /** - * Syncs user favorites to the hpct_favorites table. - * - * @param int $user_id User ID. - * @param mixed $meta_value Favorites meta value (array or serialized). - * @return void - */ - private function sync_favorites( int $user_id, $meta_value ): void { - global $wpdb; - $table = TMDO_DB::table( 'hpct_favorites' ); - $now = TMDO_DB::now(); - - // v2.13.3: object-injection-safe unserialize (fixes L-DESER-1). - // Input is user-controlled wp_usermeta value via update_user_meta hook. - $listing_ids = is_array( $meta_value ) - ? $meta_value - : (array) TMDO_Safe_Unserialize::run( $meta_value ); - $listing_ids = array_filter( array_map( 'absint', $listing_ids ) ); - - // Delete all existing and re-insert. - $wpdb->delete( $table, array( 'user_id' => $user_id ), array( '%d' ) ); - - $insert_sql = TMDO_IS_SQLITE ? 'INSERT OR IGNORE' : 'INSERT IGNORE'; - - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- insert_sql is 'INSERT IGNORE'/'INSERT OR IGNORE'; table name from TMDO_DB::table(). - foreach ( $listing_ids as $listing_id ) { - $wpdb->query( - $wpdb->prepare( - "{$insert_sql} INTO `{$table}` (user_id, listing_id, created_at) VALUES (%d, %d, %s)", - $user_id, - $listing_id, - $now - ) - ); - } - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - } -} diff --git a/includes/interceptors/class-tmdo-listing-meta-interceptor.php b/includes/interceptors/class-tmdo-listing-meta-interceptor.php deleted file mode 100644 index bd4e9d6..0000000 --- a/includes/interceptors/class-tmdo-listing-meta-interceptor.php +++ /dev/null @@ -1,339 +0,0 @@ -get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - $exists = ( null !== $found && '' !== $found ); - return $exists; - } - - /** - * Reset cached table_exists state — for tests and PR-3 schema changes. - * - * @internal - */ - public static function reset_table_exists_cache(): void { - // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged - // Use reflection to clear the static. Cleanest approach in PHP 8.1+. - ( function () { - static $exists = null; - $exists = null; - } )(); - // The above closure does not actually reset the bound static of table_exists(). - // Instead we expose a flag via a class-level static. - self::$table_exists_cache_invalidated_at = microtime( true ); - } - - /** - * Marker for cache invalidation. Real reset happens by re-calling table_exists() - * in a fresh process; tests should isolate via runInSeparateProcess where needed. - * - * @var float - */ - private static float $table_exists_cache_invalidated_at = 0.0; - - /** - * Records a single "skipped — table missing" entry in wpdo_errors per request. - * - * Direct INSERT (not via TMDO_Logger::error) so we avoid cascading the - * message to PHP's error_log on every page load. Idempotent within a - * single request via static guard. - * - * @return void - */ - private static function log_skip_once(): void { - static $logged = false; - if ( $logged ) { - return; - } - $logged = true; - - // Direct INSERT — wrapped in try/catch because the wpdo_errors table - // might not exist in fresh installs. Skipping the log is acceptable; - // breaking register_hooks() is not. - try { - global $wpdb; - $wpdb->insert( - $wpdb->prefix . 'wpdo_errors', - array( - 'module' => 'listing_meta', - 'zone' => '', - 'hook' => 'register_hooks', - 'message' => 'Skipped: hpct_listing_meta table missing. Run `wp wpdo import-hpct` or migrate first.', - 'context' => '{}', - 'created_at' => current_time( 'mysql' ), - ), - array( '%s', '%s', '%s', '%s', '%s', '%s' ) - ); - } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- intentional: wpdo_errors absence is non-critical. - // Silently swallow — wpdo_errors table absence is non-critical. - } - } - - /** - * Deletes listing meta when a post is deleted. - * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @return void - */ - public function action_delete_post( int $post_id, \WP_Post $post ): void { - if ( 'hp_listing' !== $post->post_type || ! $this->is_active() ) { - return; - } - - try { - global $wpdb; - $wpdb->delete( TMDO_DB::table( 'hpct_listing_meta' ), array( 'listing_id' => $post_id ), array( '%d' ) ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() ); - } - } - - /** - * Filters get_post_metadata for hp_listing posts. - * - * @param mixed $value Current value or null. - * @param int $object_id Post ID. - * @param string $meta_key Meta key. - * @param bool $single Whether to return single value. - * @return mixed Filtered value. - */ - public function filter_get_meta( $value, int $object_id, string $meta_key, bool $single ) { - if ( ! $this->is_enabled() || ! $this->is_hp_listing( $object_id ) || ! $this->is_hp_key( $meta_key ) ) { - return $value; - } - - return $this->intercept( - function () use ( $object_id, $meta_key, $single ) { - global $wpdb; - $table = TMDO_DB::table( 'hpct_listing_meta' ); - - if ( $single ) { - $val = $wpdb->get_var( - $wpdb->prepare( - "SELECT meta_value FROM `{$table}` WHERE listing_id = %d AND meta_key = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name validated by TMDO_DB::table() + sanitize_key(). - $object_id, - $meta_key - ) - ); - return null !== $val ? $val : null; - } - - return $wpdb->get_col( - $wpdb->prepare( - "SELECT meta_value FROM `{$table}` WHERE listing_id = %d AND meta_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name validated by TMDO_DB::table() + sanitize_key(). - $object_id, - $meta_key - ) - ) ?: null; - }, - fn() => $value, - 'get_post_metadata' - ); - } - - /** - * Filters update_post_metadata for hp_listing posts. - * - * @param mixed $check Whether to short-circuit. - * @param int $object_id Post ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value. - * @param mixed $prev_value Previous meta value. - * @return mixed Filtered check value. - */ - public function filter_update_meta( $check, int $object_id, string $meta_key, $meta_value, $prev_value ) { - if ( ! $this->is_hp_listing( $object_id ) || ! $this->is_hp_key( $meta_key ) || ! $this->is_active() ) { - return $check; - } - - try { - $this->upsert_meta( $object_id, $meta_key, $meta_value ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() ); - } - - return $check; - } - - /** - * Filters add_post_metadata for hp_listing posts. - * - * @param mixed $check Whether to short-circuit. - * @param int $object_id Post ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value. - * @param bool $unique Whether meta key should be unique. - * @return mixed Filtered check value. - */ - public function filter_add_meta( $check, int $object_id, string $meta_key, $meta_value, bool $unique ) { - if ( ! $this->is_hp_listing( $object_id ) || ! $this->is_hp_key( $meta_key ) || ! $this->is_active() ) { - return $check; - } - - try { - $this->upsert_meta( $object_id, $meta_key, $meta_value ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'add_post_metadata', $e->getMessage() ); - } - - return $check; - } - - /** - * Filters delete_post_metadata for hp_listing posts. - * - * @param mixed $check Whether to short-circuit. - * @param int $object_id Post ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value to match. - * @param bool $delete_all Whether to delete all matching. - * @return mixed Filtered check value. - */ - public function filter_delete_meta( $check, int $object_id, string $meta_key, $meta_value, bool $delete_all ) { - if ( ! $this->is_hp_listing( $object_id ) || ! $this->is_hp_key( $meta_key ) || ! $this->is_active() ) { - return $check; - } - - try { - global $wpdb; - $wpdb->delete( - TMDO_DB::table( 'hpct_listing_meta' ), - array( - 'listing_id' => $object_id, - 'meta_key' => $meta_key, - ), - array( '%d', '%s' ) - ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'delete_post_metadata', $e->getMessage() ); - } - - return $check; - } - - /** - * Checks whether the post is an hp_listing. - * - * @param int $post_id Post ID. - * @return bool True if hp_listing post type. - */ - private function is_hp_listing( int $post_id ): bool { - return 'hp_listing' === get_post_type( $post_id ); - } - - /** - * Checks whether the meta key is an hp_ or _hp_ key. - * - * @param string $meta_key Meta key. - * @return bool True if key starts with hp_ or _hp_. - */ - private function is_hp_key( string $meta_key ): bool { - return str_starts_with( $meta_key, 'hp_' ) || str_starts_with( $meta_key, '_hp_' ); - } - - /** - * Inserts or updates a listing meta value in the hpct_listing_meta table. - * - * Single round-trip via TMDO_DB::upsert() — uses ON DUPLICATE KEY UPDATE - * (MySQL) or ON CONFLICT(listing_id, meta_key) DO UPDATE (SQLite). - * Solves audit finding P-C1 (the previous SELECT + INSERT/UPDATE pattern - * cost 2 SQL round-trips per write). - * - * Requires UNIQUE KEY (listing_id, meta_key) on hpct_listing_meta — - * present by design from HPCT v1.0+. - * - * @param int $listing_id Listing post ID. - * @param string $meta_key Meta key. - * @param mixed $meta_value Meta value. - * @return void - */ - private function upsert_meta( int $listing_id, string $meta_key, $meta_value ): void { - // maybe_serialize array values to match WordPress's native postmeta semantics. - $serialized = is_array( $meta_value ) || is_object( $meta_value ) - ? maybe_serialize( $meta_value ) - : (string) $meta_value; - - TMDO_DB::upsert( - TMDO_DB::table( 'hpct_listing_meta' ), - array( - 'listing_id' => $listing_id, - 'meta_key' => $meta_key, - 'meta_value' => $serialized, - ), - array( 'meta_value' ), // Update only meta_value on conflict. - array( 'listing_id', 'meta_key' ), // Composite unique key. - array( '%d', '%s', '%s' ) - ); - } -} diff --git a/includes/interceptors/class-tmdo-memberships-interceptor.php b/includes/interceptors/class-tmdo-memberships-interceptor.php deleted file mode 100644 index fad2903..0000000 --- a/includes/interceptors/class-tmdo-memberships-interceptor.php +++ /dev/null @@ -1,76 +0,0 @@ - 'plan_id', - 'hp_user' => 'user_id', - 'hp_price' => 'price', - 'hp_order' => 'order_id', - 'hp_start_date' => 'start_date', - 'hp_end_date' => 'end_date', - ); - - /** - * Returns the WordPress post_type slug handled by this interceptor. - * - * @return string - */ - protected function get_post_type(): string { - return 'hp_membership'; - } - - /** - * Returns the table key passed to TMDO_DB::table(). - * - * @return string - */ - protected function get_table_key(): string { - return 'hpct_memberships'; - } - - /** - * Builds the INSERT row for a newly created membership post. - * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @param string $now Formatted datetime string. - * @return array{values: array, formats: array} - */ - protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array { - return array( - 'values' => array( - 'post_id' => $post_id, - 'user_id' => (int) $post->post_author, - 'status' => $post->post_status, - 'created_at' => $now, - 'updated_at' => $now, - ), - 'formats' => array( '%d', '%d', '%s', '%s', '%s' ), - ); - } -} diff --git a/includes/interceptors/class-tmdo-messages-interceptor.php b/includes/interceptors/class-tmdo-messages-interceptor.php deleted file mode 100644 index 777545c..0000000 --- a/includes/interceptors/class-tmdo-messages-interceptor.php +++ /dev/null @@ -1,78 +0,0 @@ - 'sender_id', - 'hp_recipient' => 'recipient_id', - 'hp_listing' => 'listing_id', - 'hp_read' => 'is_read', - ); - - /** - * Returns the WordPress post_type slug handled by this interceptor. - * - * @return string - */ - protected function get_post_type(): string { - return 'hp_message'; - } - - /** - * Returns the table key passed to TMDO_DB::table(). - * - * @return string - */ - protected function get_table_key(): string { - return 'hpct_messages'; - } - - /** - * Builds the INSERT row for a newly created message post. - * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @param string $now Formatted datetime string. - * @return array{values: array, formats: array} - */ - protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array { - return array( - 'values' => array( - 'post_id' => $post_id, - 'thread_id' => (int) $post->post_parent, - 'sender_id' => (int) $post->post_author, - 'subject' => sanitize_text_field( $post->post_title ), - 'body' => $post->post_content, - 'status' => $post->post_status, - 'sent_at' => $now, - 'created_at' => $now, - 'updated_at' => $now, - ), - 'formats' => array( '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s' ), - ); - } -} diff --git a/includes/interceptors/class-tmdo-requests-interceptor.php b/includes/interceptors/class-tmdo-requests-interceptor.php deleted file mode 100644 index 87cef31..0000000 --- a/includes/interceptors/class-tmdo-requests-interceptor.php +++ /dev/null @@ -1,74 +0,0 @@ - 'vendor_id', - 'hp_listing' => 'listing_id', - 'hp_budget' => 'budget', - ); - - /** - * Returns the WordPress post_type slug handled by this interceptor. - * - * @return string - */ - protected function get_post_type(): string { - return 'hp_request'; - } - - /** - * Returns the table key passed to TMDO_DB::table(). - * - * @return string - */ - protected function get_table_key(): string { - return 'hpct_requests'; - } - - /** - * Builds the INSERT row for a newly created request post. - * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @param string $now Formatted datetime string. - * @return array{values: array, formats: array} - */ - protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array { - return array( - 'values' => array( - 'post_id' => $post_id, - 'user_id' => (int) $post->post_author, - 'status' => $post->post_status, - 'message' => $post->post_content, - 'created_at' => $now, - 'updated_at' => $now, - ), - 'formats' => array( '%d', '%d', '%s', '%s', '%s', '%s' ), - ); - } -} diff --git a/includes/interceptors/class-tmdo-reviews-interceptor.php b/includes/interceptors/class-tmdo-reviews-interceptor.php deleted file mode 100644 index fa66e6e..0000000 --- a/includes/interceptors/class-tmdo-reviews-interceptor.php +++ /dev/null @@ -1,116 +0,0 @@ - 'rating', - 'hp_listing' => 'listing_id', - '_hp_vendor' => 'vendor_id', - ); - - /** - * Returns the WordPress post_type slug handled by this interceptor. - * - * @return string - */ - protected function get_post_type(): string { - return 'hp_review'; - } - - /** - * Returns the table key passed to TMDO_DB::table(). - * - * @return string - */ - protected function get_table_key(): string { - return 'hpct_reviews'; - } - - /** - * Registers WordPress hooks, including the extra get_post_metadata hook. - * - * @return void - */ - public function register_hooks(): void { - parent::register_hooks(); - add_filter( 'get_post_metadata', array( $this, 'filter_get_meta' ), 10, 4 ); - } - - /** - * Builds the INSERT row for a newly created review post. - * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @param string $now Formatted datetime string. - * @return array{values: array, formats: array} - */ - protected function build_insert_data( int $post_id, \WP_Post $post, string $now ): array { - return array( - 'values' => array( - 'post_id' => $post_id, - 'user_id' => (int) $post->post_author, - 'status' => $post->post_status, - 'title' => sanitize_text_field( $post->post_title ), - 'content' => $post->post_content, - 'created_at' => $now, - 'updated_at' => $now, - ), - 'formats' => array( '%d', '%d', '%s', '%s', '%s', '%s', '%s' ), - ); - } - - /** - * Reads hp_rating directly from the reviews flat table. - * - * @param mixed $value Short-circuit value. - * @param int $post_id Post ID. - * @param string $meta_key Meta key. - * @param bool $single Whether to return a single value. - * @return mixed - */ - public function filter_get_meta( $value, int $post_id, string $meta_key, bool $single ) { - if ( ! $this->is_enabled() || 'hp_rating' !== $meta_key || 'hp_review' !== get_post_type( $post_id ) ) { - return $value; - } - - return $this->intercept( - function () use ( $post_id, $single ) { - global $wpdb; - $rating = $wpdb->get_var( - $wpdb->prepare( - 'SELECT rating FROM ' . TMDO_DB::table( 'hpct_reviews' ) . ' WHERE post_id = %d LIMIT 1', - $post_id - ) - ); - return null !== $rating ? ( $single ? $rating : array( $rating ) ) : null; - }, - fn() => $value, - 'get_post_metadata' - ); - } -} diff --git a/includes/interceptors/class-tmdo-statistics-interceptor.php b/includes/interceptors/class-tmdo-statistics-interceptor.php deleted file mode 100644 index 4f160eb..0000000 --- a/includes/interceptors/class-tmdo-statistics-interceptor.php +++ /dev/null @@ -1,139 +0,0 @@ -is_active() ) { - return $check; - } - - try { - $this->record_event( $post_id, 'view' ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'update_post_metadata', $e->getMessage() ); - } - - return $check; - } - - /** - * Records a view event when a HivePress listing is viewed. - * - * @param mixed $listing Listing object or ID. - * @return void - */ - public function action_listing_viewed( $listing ): void { - if ( ! $this->is_active() ) { - return; - } - - $listing_id = is_object( $listing ) && method_exists( $listing, 'get_id' ) - ? $listing->get_id() - : ( is_numeric( $listing ) ? (int) $listing : 0 ); - - if ( ! $listing_id ) { - return; - } - - try { - $this->record_event( $listing_id, 'hp_action_view' ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'listing_viewed', $e->getMessage() ); - } - } - - /** - * Deletes statistics records when a listing post is deleted. - * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @return void - */ - public function action_delete_post( int $post_id, \WP_Post $post ): void { - if ( 'hp_listing' !== $post->post_type || ! $this->is_active() ) { - return; - } - - try { - global $wpdb; - $wpdb->delete( TMDO_DB::table( 'hpct_statistics' ), array( 'listing_id' => $post_id ), array( '%d' ) ); - } catch ( \Throwable $e ) { - TMDO_Logger::error( $this->module, 'before_delete_post', $e->getMessage() ); - } - } - - /** - * Records a statistics event to the hpct_statistics table. - * - * @param int $listing_id Listing post ID. - * @param string $event_type Event type identifier. - * @return void - */ - private function record_event( int $listing_id, string $event_type ): void { - global $wpdb; - - $user_id = get_current_user_id(); - $ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) ); - $ip_hash = $ip ? hash( 'sha256', $ip ) : ''; - - $wpdb->insert( - TMDO_DB::table( 'hpct_statistics' ), - array( - 'listing_id' => $listing_id, - 'event_type' => $event_type, - 'user_id' => $user_id, - 'ip_hash' => $ip_hash, - 'referrer' => substr( esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ), 0, 500 ), - 'created_at' => TMDO_DB::now(), - ), - array( '%d', '%s', '%d', '%s', '%s', '%s' ) - ); - } -} diff --git a/includes/query/class-tmdo-listing-meta-query.php b/includes/query/class-tmdo-listing-meta-query.php deleted file mode 100644 index 5d634f1..0000000 --- a/includes/query/class-tmdo-listing-meta-query.php +++ /dev/null @@ -1,236 +0,0 @@ -= 100 - * - * All hp_* and _hp_* meta keys are intercepted. Any other meta keys are - * left in the meta_query to be handled by wp_postmeta as usual. - */ -class TMDO_Listing_Meta_Query extends TMDO_Query_Interceptor_Base { - - /** - * Returns the module identifier. - * - * @return string Module name. - */ - protected function get_module(): string { - return 'listing_meta'; - } - - /** - * Returns the post types this interceptor handles. - * - * @return string[] Post type slugs. - */ - protected function get_post_types(): array { - return array( 'hp_listing' ); - } - - /** - * Returns the custom table name. - * - * @return string Table name without prefix. - */ - protected function get_table(): string { - return 'hpct_listing_meta'; - } - - /** - * Returns the join column for this KV table. - * - * @return string Join column name. - */ - protected function get_join_column(): string { - return 'listing_id'; - } - - /** - * Not used — this class overrides pre_get_posts directly. - * - * @return array Empty array. - */ - protected function get_meta_key_map(): array { - return array(); - } - - // ── Override pre_get_posts for KV-table pattern ─────────────────────── - - /** - * Intercepts pre_get_posts to handle KV-table meta_query conditions. - * - * @param \WP_Query $query The WP_Query object. - * @return void - */ - public function pre_get_posts( \WP_Query $query ): void { - if ( ! $this->should_intercept( $query ) ) { - return; - } - - $raw_meta_query = (array) $query->get( 'meta_query' ); - if ( empty( $raw_meta_query ) ) { - return; - } - - $our_clauses = array(); - $remaining = array(); - - foreach ( $raw_meta_query as $k => $clause ) { - if ( 'relation' === $k || ! is_array( $clause ) || ! isset( $clause['key'] ) ) { - $remaining[ $k ] = $clause; - continue; - } - - if ( $this->is_hp_key( $clause['key'] ) ) { - $our_clauses[] = array( - 'meta_key' => $clause['key'], - 'value' => $clause['value'] ?? '', - 'compare' => strtoupper( trim( $clause['compare'] ?? '=' ) ), - 'type' => strtoupper( trim( $clause['type'] ?? 'CHAR' ) ), - ); - } else { - $remaining[ $k ] = $clause; - } - } - - if ( empty( $our_clauses ) ) { - return; - } - - if ( isset( $raw_meta_query['relation'] ) && ! isset( $remaining['relation'] ) ) { - $remaining['relation'] = $raw_meta_query['relation']; - } - - $query->set( 'meta_query', $remaining ); - $query->set( $this->query_var(), $our_clauses ); - } - - // ── Override posts_join for KV-table: one JOIN alias per condition ───── - - /** - * Appends LEFT JOINs for each KV-table meta_query condition. - * - * @param string $join Current JOIN SQL. - * @param \WP_Query $query The WP_Query object. - * @return string Modified JOIN SQL. - */ - public function posts_join( ?string $join, \WP_Query $query ): string { - $join = (string) ( $join ?? '' ); - $clauses = $this->get_clauses( $query ); - if ( empty( $clauses ) ) { - return $join; - } - - global $wpdb; - $table = TMDO_DB::table( $this->get_table() ); - $join_col = esc_sql( $this->get_join_column() ); - - foreach ( $clauses as $i => $clause ) { - $alias = $this->alias_for( $i ); - $meta_key = $clause['meta_key']; - - if ( false === strpos( $join, "`{$alias}`" ) ) { - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $table from TMDO_DB::table(); $alias and $join_col are sanitize_key()-validated. - $join .= $wpdb->prepare( - " LEFT JOIN `{$table}` AS `{$alias}`" - . " ON (`{$wpdb->posts}`.`ID` = `{$alias}`.`{$join_col}`" - . " AND `{$alias}`.`meta_key` = %s)", - $meta_key - ); - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - } - } - - return $join; - } - - // ── Override posts_where for KV-table ───────────────────────────────── - - /** - * Appends WHERE conditions for KV-table meta_value comparisons. - * - * @param string $where Current WHERE SQL. - * @param \WP_Query $query The WP_Query object. - * @return string Modified WHERE SQL. - */ - public function posts_where( ?string $where, \WP_Query $query ): string { - $where = (string) ( $where ?? '' ); - $clauses = $this->get_clauses( $query ); - if ( empty( $clauses ) ) { - return $where; - } - - foreach ( $clauses as $i => $clause ) { - $alias = $this->alias_for( $i ); - $col = "`{$alias}`.`meta_value`"; - $compare = $this->sanitize_compare( $clause['compare'] ); - $type = $clause['type']; - $value = $clause['value']; - - $where .= $this->build_condition( $col, $compare, $type, $value ); - } - - return $where; - } - - // ── Override posts_groupby ───────────────────────────────────────────── - - /** - * Ensures GROUP BY is set when KV-table clauses are active. - * - * @param string $groupby Current GROUP BY SQL. - * @param \WP_Query $query The WP_Query object. - * @return string Modified GROUP BY SQL. - */ - public function posts_groupby( ?string $groupby, \WP_Query $query ): string { - $groupby = (string) ( $groupby ?? '' ); - $clauses = $this->get_clauses( $query ); - if ( empty( $clauses ) ) { - return $groupby; - } - - global $wpdb; - if ( '' === trim( $groupby ) ) { - $groupby = "`{$wpdb->posts}`.`ID`"; - } - - return $groupby; - } - - // ── Helpers ─────────────────────────────────────────────────────────── - - /** - * Generates a unique SQL alias for a KV-table JOIN at a given index. - * - * @param int $index Zero-based clause index. - * @return string SQL alias string. - */ - private function alias_for( int $index ): string { - return "wpdo_lm_{$index}"; - } - - /** - * Checks whether a meta key belongs to HivePress (hp_ or _hp_ prefix). - * - * @param string $meta_key Meta key to check. - * @return bool True if the key is an HP key. - */ - private function is_hp_key( string $meta_key ): bool { - return str_starts_with( $meta_key, 'hp_' ) || str_starts_with( $meta_key, '_hp_' ); - } -} diff --git a/includes/query/class-tmdo-memberships-query.php b/includes/query/class-tmdo-memberships-query.php deleted file mode 100644 index 083c8bf..0000000 --- a/includes/query/class-tmdo-memberships-query.php +++ /dev/null @@ -1,67 +0,0 @@ - plan_id (BIGINT) - * hp_user => user_id (BIGINT) - * hp_price => price (VARCHAR) - * hp_order => order_id (BIGINT) - * hp_start_date => start_date (DATETIME) - * hp_end_date => end_date (DATETIME) - */ -class TMDO_Memberships_Query extends TMDO_Query_Interceptor_Base { - - /** - * Returns the module identifier. - * - * @return string Module name. - */ - protected function get_module(): string { - return 'memberships'; - } - - /** - * Returns the post types this interceptor handles. - * - * @return string[] Post type slugs. - */ - protected function get_post_types(): array { - return array( 'hp_membership' ); - } - - /** - * Returns the custom table name. - * - * @return string Table name without prefix. - */ - protected function get_table(): string { - return 'hpct_memberships'; - } - - /** - * Returns the meta_key to column mapping for hpct_memberships. - * - * @return array Meta key to column name map. - */ - protected function get_meta_key_map(): array { - return array( - 'hp_plan' => 'plan_id', - 'hp_user' => 'user_id', - 'hp_price' => 'price', - 'hp_order' => 'order_id', - 'hp_start_date' => 'start_date', - 'hp_end_date' => 'end_date', - ); - } -} diff --git a/includes/query/class-tmdo-messages-query.php b/includes/query/class-tmdo-messages-query.php deleted file mode 100644 index 604806f..0000000 --- a/includes/query/class-tmdo-messages-query.php +++ /dev/null @@ -1,63 +0,0 @@ - sender_id (BIGINT) - * hp_recipient => recipient_id (BIGINT) - * hp_listing => listing_id (BIGINT) - * hp_read => is_read (TINYINT) - */ -class TMDO_Messages_Query extends TMDO_Query_Interceptor_Base { - - /** - * Returns the module identifier. - * - * @return string Module name. - */ - protected function get_module(): string { - return 'messages'; - } - - /** - * Returns the post types this interceptor handles. - * - * @return string[] Post type slugs. - */ - protected function get_post_types(): array { - return array( 'hp_message' ); - } - - /** - * Returns the custom table name. - * - * @return string Table name without prefix. - */ - protected function get_table(): string { - return 'hpct_messages'; - } - - /** - * Returns the meta_key to column mapping for hpct_messages. - * - * @return array Meta key to column name map. - */ - protected function get_meta_key_map(): array { - return array( - 'hp_sender' => 'sender_id', - 'hp_recipient' => 'recipient_id', - 'hp_listing' => 'listing_id', - 'hp_read' => 'is_read', - ); - } -} diff --git a/includes/query/class-tmdo-requests-query.php b/includes/query/class-tmdo-requests-query.php deleted file mode 100644 index bdf5e58..0000000 --- a/includes/query/class-tmdo-requests-query.php +++ /dev/null @@ -1,61 +0,0 @@ - vendor_id (BIGINT) - * hp_listing => listing_id (BIGINT) - * hp_budget => budget (VARCHAR) - */ -class TMDO_Requests_Query extends TMDO_Query_Interceptor_Base { - - /** - * Returns the module identifier. - * - * @return string Module name. - */ - protected function get_module(): string { - return 'requests'; - } - - /** - * Returns the post types this interceptor handles. - * - * @return string[] Post type slugs. - */ - protected function get_post_types(): array { - return array( 'hp_request' ); - } - - /** - * Returns the custom table name. - * - * @return string Table name without prefix. - */ - protected function get_table(): string { - return 'hpct_requests'; - } - - /** - * Returns the meta_key to column mapping for hpct_requests. - * - * @return array Meta key to column name map. - */ - protected function get_meta_key_map(): array { - return array( - 'hp_vendor' => 'vendor_id', - 'hp_listing' => 'listing_id', - 'hp_budget' => 'budget', - ); - } -} diff --git a/includes/query/class-tmdo-reviews-query.php b/includes/query/class-tmdo-reviews-query.php deleted file mode 100644 index 55804c6..0000000 --- a/includes/query/class-tmdo-reviews-query.php +++ /dev/null @@ -1,64 +0,0 @@ - listing_id (BIGINT) - * _hp_vendor => vendor_id (BIGINT) - * hp_rating => rating (TINYINT) - */ -class TMDO_Reviews_Query extends TMDO_Query_Interceptor_Base { - - /** - * Returns the module identifier. - * - * @return string Module name. - */ - protected function get_module(): string { - return 'reviews'; - } - - /** - * Returns the post types this interceptor handles. - * - * @return string[] Post type slugs. - */ - protected function get_post_types(): array { - return array( 'hp_review' ); - } - - /** - * Returns the custom table name. - * - * @return string Table name without prefix. - */ - protected function get_table(): string { - return 'hpct_reviews'; - } - - /** - * Returns the meta_key to column mapping for hpct_reviews. - * - * @return array Meta key to column name map. - */ - protected function get_meta_key_map(): array { - return array( - 'hp_listing' => 'listing_id', - '_hp_vendor' => 'vendor_id', - 'hp_rating' => 'rating', - ); - } -} diff --git a/tests/integration/ListingMetaInterceptorTest.php b/tests/integration/ListingMetaInterceptorTest.php deleted file mode 100644 index 6f0a04a..0000000 --- a/tests/integration/ListingMetaInterceptorTest.php +++ /dev/null @@ -1,160 +0,0 @@ -query( "DROP TABLE IF EXISTS `{$wpdb->prefix}hpct_listing_meta`" ); - - // Simulate HPCT_Core being present — register_hooks() only logs the skip - // when HPCT is loaded but its table is missing (genuine inconsistency). - if ( ! class_exists( 'HPCT_Core' ) ) { - // phpcs:ignore Generic.Commenting.InlineComment.InvalidEndChar -- runtime stub - eval( 'class HPCT_Core {}' ); // @phpcs:ignore - } - - // Ensure wpdo_errors exists for the skip log; harmless if pre-created. - $wpdb->query( - "CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}wpdo_errors` ( - id bigint(20) unsigned NOT NULL AUTO_INCREMENT, - module varchar(50) NOT NULL DEFAULT '', - zone varchar(10) NOT NULL DEFAULT '', - hook varchar(255) NOT NULL DEFAULT '', - message longtext NOT NULL, - context longtext, - created_at datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (id), - KEY idx_module (module) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" - ); - - // Snapshot pre-call count of skip messages. - $pre = (int) $wpdb->get_var( - $wpdb->prepare( - "SELECT COUNT(*) FROM `{$wpdb->prefix}wpdo_errors` WHERE module = %s AND hook = %s", - 'listing_meta', - 'register_hooks' - ) - ); - - $interceptor = new WPDO_Listing_Meta_Interceptor(); - $interceptor->register_hooks(); - - $post = (int) $wpdb->get_var( - $wpdb->prepare( - "SELECT COUNT(*) FROM `{$wpdb->prefix}wpdo_errors` WHERE module = %s AND hook = %s", - 'listing_meta', - 'register_hooks' - ) - ); - $this->assertSame( $pre + 1, $post, 'Expected exactly one new skip message after register_hooks()' ); - } - - /** - * P-C1: WPDO_DB::upsert() must produce an INSERT ... ON DUPLICATE KEY UPDATE - * statement (single round-trip), and writes must succeed via the composite - * unique key (listing_id, meta_key). - * - * @runInSeparateProcess - */ - public function test_upsert_uses_composite_unique_key(): void { - global $wpdb; - - $table = $wpdb->prefix . 'hpct_listing_meta'; - - // Clean slate. - $wpdb->query( "DROP TABLE IF EXISTS `{$table}`" ); - $wpdb->query( - "CREATE TABLE `{$table}` ( - id bigint(20) unsigned NOT NULL AUTO_INCREMENT, - listing_id bigint(20) unsigned NOT NULL DEFAULT 0, - meta_key varchar(255) NOT NULL DEFAULT '', - meta_value longtext, - PRIMARY KEY (id), - UNIQUE KEY ui_listing_meta (listing_id, meta_key(191)) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" - ); - - // First upsert: INSERT. - $result1 = WPDO_DB::upsert( - $table, - array( - 'listing_id' => 4242, - 'meta_key' => 'hp_price', - 'meta_value' => '100.00', - ), - array( 'meta_value' ), - array( 'listing_id', 'meta_key' ), - array( '%d', '%s', '%s' ) - ); - $this->assertNotFalse( $result1 ); - - // Second upsert: UPDATE via the composite key. - $result2 = WPDO_DB::upsert( - $table, - array( - 'listing_id' => 4242, - 'meta_key' => 'hp_price', - 'meta_value' => '250.50', - ), - array( 'meta_value' ), - array( 'listing_id', 'meta_key' ), - array( '%d', '%s', '%s' ) - ); - $this->assertNotFalse( $result2 ); - - // Verify only one row exists for (4242, 'hp_price') with the latest value. - $rows = $wpdb->get_results( - $wpdb->prepare( - "SELECT meta_value FROM `{$table}` WHERE listing_id = %d AND meta_key = %s", - 4242, - 'hp_price' - ), - ARRAY_A - ); - $this->assertCount( 1, $rows, 'Composite UNIQUE must collapse two upserts into a single row' ); - $this->assertSame( '250.50', $rows[0]['meta_value'] ); - - // Cleanup. - $wpdb->query( "DROP TABLE IF EXISTS `{$table}`" ); - } - - /** - * Verify table_exists() returns false when table missing, true after CREATE. - * - * @runInSeparateProcess - */ - public function test_table_exists_detection(): void { - global $wpdb; - - $wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}hpct_listing_meta`" ); - - // Use reflection to invoke private static. - $ref = new ReflectionClass( WPDO_Listing_Meta_Interceptor::class ); - $method = $ref->getMethod( 'table_exists' ); - $method->setAccessible( true ); - - $this->assertFalse( $method->invoke( null ), 'table_exists must return false when table missing' ); - } -} diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index 72f5713..ec73697 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -29,8 +29,6 @@ define( 'TMDO_HIVEPRESS_FILE', TMDO_HIVEPRESS_PATH . '2meet-data-optimizer-hivep define( 'TMDO_HIVEPRESS_VERSION', '0.1.0' ); // ── AddOn classes ────────────────────────────────────────────────────────── -// Superset of the unit bootstrap's list: the integration suite also exercises -// the interceptors, which the unit suite does not load. $tmdo_hp_files = array( 'includes/class-tmdo-hivepress.php', @@ -54,12 +52,6 @@ $tmdo_hp_files = array( foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/hivepress/adapters/*.php' ) ?: array() as $tmdo_hp_glob ) { $tmdo_hp_files[] = 'includes/hivepress/adapters/' . basename( $tmdo_hp_glob ); } -foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/interceptors/*.php' ) ?: array() as $tmdo_hp_glob ) { - $tmdo_hp_files[] = 'includes/interceptors/' . basename( $tmdo_hp_glob ); -} -foreach ( glob( TMDO_HIVEPRESS_PATH . 'includes/query/*.php' ) ?: array() as $tmdo_hp_glob ) { - $tmdo_hp_files[] = 'includes/query/' . basename( $tmdo_hp_glob ); -} foreach ( $tmdo_hp_files as $tmdo_hp_file ) { $tmdo_hp_path = TMDO_HIVEPRESS_PATH . $tmdo_hp_file; From 19add5fc16c2d379bc45bd3866c0a6d3c5160422 Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 25 Sep 2026 23:43:40 +0800 Subject: [PATCH 06/12] refactor(2meet-data-optimizer-hivepress-addon): take ownership of hp_* entity groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add TMDO_HivePress_Entity_Groups registering hp_listing_core / hp_request_core / hp_vendor_core / hp_user, copied verbatim from core (same order; schema hashes match the persisted wpdo_registry_meta rows) - Drop the core adapter's conflicting hp_user definition (hp_verified / hp_avatar_id — keys HivePress never writes) - Verify: HivePressEntityGroupsTest (identical to core, same shared-key resolution, no collisions); HP unit 144 / integration 49 OK; hashes = dev20 registry_meta - PR: PR-019 --- includes/back-compat-aliases.php | 1 + includes/class-tmdo-hivepress-bootstrap.php | 4 + .../class-tmdo-hivepress-entity-groups.php | 251 ++++++++++++++++++ .../class-tmdo-hivepress-core-adapter.php | 54 +--- tests/bootstrap.php | 1 + tests/integration/bootstrap.php | 1 + tests/unit/HivePressEntityGroupsTest.php | 82 ++++++ 7 files changed, 346 insertions(+), 48 deletions(-) create mode 100644 includes/class-tmdo-hivepress-entity-groups.php create mode 100644 tests/unit/HivePressEntityGroupsTest.php diff --git a/includes/back-compat-aliases.php b/includes/back-compat-aliases.php index 27c5adf..1a729c0 100644 --- a/includes/back-compat-aliases.php +++ b/includes/back-compat-aliases.php @@ -30,6 +30,7 @@ add_action( 'TMDO_HivePress_Bootstrap' => 'WPDO_HivePress_Bootstrap', 'TMDO_HivePress_Conflict_Guard' => 'WPDO_HivePress_Conflict_Guard', 'TMDO_HivePress_Core_Adapter' => 'WPDO_HivePress_Core_Adapter', + 'TMDO_HivePress_Entity_Groups' => 'WPDO_HivePress_Entity_Groups', 'TMDO_HivePress_Cron_Optimizer' => 'WPDO_HivePress_Cron_Optimizer', 'TMDO_HivePress_Detector' => 'WPDO_HivePress_Detector', 'TMDO_HivePress_Favorites_Adapter' => 'WPDO_HivePress_Favorites_Adapter', diff --git a/includes/class-tmdo-hivepress-bootstrap.php b/includes/class-tmdo-hivepress-bootstrap.php index 831d33d..c146afe 100644 --- a/includes/class-tmdo-hivepress-bootstrap.php +++ b/includes/class-tmdo-hivepress-bootstrap.php @@ -68,6 +68,7 @@ final class TMDO_HP_Bootstrap { require_once $base . 'includes/class-tmdo-hivepress-transient-filter.php'; require_once $base . 'includes/class-tmdo-hpct-import.php'; require_once $base . 'includes/class-tmdo-hivepress-term-comment-fields.php'; + require_once $base . 'includes/class-tmdo-hivepress-entity-groups.php'; require_once $base . 'includes/class-tmdo-listing-stats.php'; } @@ -97,6 +98,9 @@ final class TMDO_HP_Bootstrap { TMDO_Hivepress_Term_Comment_Fields::register(); } + // Post + user entity groups (hp_listing_core / hp_request_core / hp_vendor_core / hp_user). + TMDO_HivePress_Entity_Groups::register(); + // Admin tab if ( is_admin() ) { $admin_file = TMDO_HIVEPRESS_PATH . 'admin/class-tmdo-admin-hivepress.php'; diff --git a/includes/class-tmdo-hivepress-entity-groups.php b/includes/class-tmdo-hivepress-entity-groups.php new file mode 100644 index 0000000..8e94bf5 --- /dev/null +++ b/includes/class-tmdo-hivepress-entity-groups.php @@ -0,0 +1,251 @@ + 'hp_price', + 'type' => 'decimal', + 'searchable' => true, + 'label' => 'Listing price', + ), + array( + 'key' => 'hp_status', + 'type' => 'enum', + 'searchable' => true, + 'options' => array( 'publish', 'draft', 'pending', 'expired', 'private' ), + 'label' => 'Listing status', + ), + array( + 'key' => 'hp_featured', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Featured flag (0/1)', + ), + array( + 'key' => 'hp_verified', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Verified flag (0/1)', + ), + array( + 'key' => 'hp_vendor', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Vendor user ID', + ), + array( + 'key' => 'hp_expired_time', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Expiry unix ts', + ), + array( + 'key' => 'hp_featured_time', + 'type' => 'integer', + 'label' => 'Featured-until unix ts', + ), + array( + 'key' => 'hp_view_count', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'View counter', + ), + array( + 'key' => 'hp_rating', + 'type' => 'decimal', + 'searchable' => true, + 'label' => 'Average rating', + ), + array( + 'key' => 'hp_rating_count', + 'type' => 'integer', + 'label' => 'Rating count', + ), + array( + 'key' => 'hp_hourly_rate', + 'type' => 'decimal', + 'label' => 'Hourly rate', + ), + ) + ); + } + /** + * HivePress request core meta keys (post_type=hp_request). + * + * @return void + */ + private static function register_hp_request_core_group(): void { + TMDO_Entity_Registry::register_group( + 'post', + 'hp_request_core', + array( + array( + 'key' => 'hp_status', + 'type' => 'enum', + 'searchable' => true, + 'options' => array( 'publish', 'draft', 'pending', 'expired' ), + 'label' => 'Request status', + ), + array( + 'key' => 'hp_user', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Request author user ID', + ), + array( + 'key' => 'hp_expired_time', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Expiry unix ts', + ), + array( + 'key' => 'hp_budget', + 'type' => 'decimal', + 'searchable' => true, + 'label' => 'Budget amount', + ), + array( + 'key' => 'hp_view_count', + 'type' => 'integer', + 'label' => 'View counter', + ), + ) + ); + } + /** + * HivePress vendor core meta keys (post_type=hp_vendor). + * + * @return void + */ + private static function register_hp_vendor_core_group(): void { + TMDO_Entity_Registry::register_group( + 'post', + 'hp_vendor_core', + array( + array( + 'key' => 'hp_user', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Vendor user ID (linked WP user)', + ), + array( + 'key' => 'hp_verified', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Verified flag (0/1)', + ), + array( + 'key' => 'hp_hourly_rate', + 'type' => 'decimal', + 'searchable' => true, + 'label' => 'Hourly rate', + ), + array( + 'key' => 'hp_rating_count', + 'type' => 'integer', + 'label' => 'Rating count', + ), + array( + 'key' => 'hp_rating', + 'type' => 'decimal', + 'searchable' => true, + 'label' => 'Average rating', + ), + ) + ); + } + /** + * HivePress per-user fields (favorites + avatar attachment). + * + * Hp_favorited_listings is a serialized array of post IDs in legacy storage; + * the migration engine safe_unserialize()s it (allowed_classes=false to block + * PHP-object injection) then the json type encodes back to a JSON array column. + * + * @return void + */ + private static function register_hp_user_group(): void { + TMDO_Entity_Registry::register_group( + 'user', + 'hp_user', + array( + array( + 'key' => 'hp_favorited_listings', + 'type' => 'json', + 'label' => 'HivePress favorited listing IDs (array)', + ), + array( + 'key' => 'hp_image', + 'type' => 'text', + 'label' => 'HivePress avatar attachment ID', + ), + ) + ); + } +} diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php index 25e9664..1580f78 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php @@ -2,15 +2,14 @@ /** * Core HivePress adapter — `hivepress` (the parent plugin). * - * Owns three primary HivePress models: + * Covers the primary HivePress models: * * - `Listing` (post_type=hp_listing) — Hot zone columns for search/filter * (drafted, featured, verified, expired_time, featured_time). * - `Vendor` (post_type=hp_vendor) — Hot column `verified`; cold blob * for image / description (display-only). - * - `User` (entity=user) — Entity group `hp_user_core` for - * `hp_verified` and the WP-stock `first_name` / `last_name` / - * `description`. + * - `User` (entity=user) — no zone fields; its `hp_user` + * entity group is registered by TMDO_HivePress_Entity_Groups. * * This adapter SUPERSEDES the legacy `class-tmdo-hivepress.php` (242 lines). * Behavioural parity: @@ -29,9 +28,9 @@ * - `hp_drafted` (Listing) — Hot tinyint(1) for user-dashboard filtering. * - `hp_expired_time` / `hp_featured_time` (Listing) — Hot bigint indexed * for cron expiry scan acceleration (replaces postmeta full scan). - * - `hp_user_core` entity group — first_name / last_name / hp_verified - * promoted from usermeta to wp_wpdo_user_hp_user (already created by - * TMDO_Member_Fields v2.7.0). + * (This adapter used to register a second `hp_user` definition — + * hp_verified / hp_avatar_id, keys HivePress never writes — which the + * registry silently dropped; TMDO_HivePress_Entity_Groups owns it now.) * * @package WP_Data_Optimizer * @since 3.0.0 @@ -166,47 +165,6 @@ if ( ! class_exists( 'TMDO_HivePress_Core_Adapter' ) ) { ); } - /** - * Register entity field group for HP User model. - * - * The `hp_user` group is already created by `TMDO_Member_Fields` - * (v2.7.0). We add adapter ownership here for cross-cutting tooling - * (`wp wpdo hivepress doctor` reports owner; admin UI shows source). - * - * The `register_entity_fields()` helper from `TMDO_Anti_EAV_Aware` - * delegates to `TMDO_Entity_Registry::register_group()` which has - * internal dedup — so re-registering with same group name is a no-op. - */ - public function on_register_entity_fields(): void { - if ( ! class_exists( 'TMDO_Entity_Registry' ) ) { - return; - } - - // Mirror the (canonical) group definition from TMDO_Member_Fields::HP_USER_FIELDS. - // Source of truth lives in TMDO_Member_Fields; we register defensively for the - // case where Member_Fields didn't load (custom builds without that integration). - $this->register_entity_fields( - 'user', - 'hp_user', - array( - array( - 'key' => 'hp_verified', - 'type' => 'boolean', - 'default' => false, - 'searchable' => true, - 'label' => 'HivePress KYC verified', - ), - array( - 'key' => 'hp_avatar_id', - 'type' => 'integer', - 'default' => 0, - 'searchable' => false, - 'label' => 'HivePress avatar attachment id', - ), - ) - ); - } - // ── Query optimization (preserved from legacy class-tmdo-hivepress.php) ── /** diff --git a/tests/bootstrap.php b/tests/bootstrap.php index e358c4e..cbaaa44 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -45,6 +45,7 @@ $tmdo_hp_files = array( 'includes/hivepress/class-tmdo-hivepress-rest.php', 'includes/class-tmdo-hivepress-transient-filter.php', 'includes/class-tmdo-hivepress-term-comment-fields.php', + 'includes/class-tmdo-hivepress-entity-groups.php', 'includes/class-tmdo-hpct-import.php', 'includes/class-tmdo-listing-stats.php', 'admin/class-tmdo-admin-hivepress.php', diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index ec73697..1b21448 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -44,6 +44,7 @@ $tmdo_hp_files = array( 'includes/hivepress/class-tmdo-hivepress-rest.php', 'includes/class-tmdo-hivepress-transient-filter.php', 'includes/class-tmdo-hivepress-term-comment-fields.php', + 'includes/class-tmdo-hivepress-entity-groups.php', 'includes/class-tmdo-hpct-import.php', 'admin/class-tmdo-admin-hivepress.php', 'cli/class-tmdo-cli-hivepress.php', diff --git a/tests/unit/HivePressEntityGroupsTest.php b/tests/unit/HivePressEntityGroupsTest.php new file mode 100644 index 0000000..fa917d2 --- /dev/null +++ b/tests/unit/HivePressEntityGroupsTest.php @@ -0,0 +1,82 @@ +createMock( TMDO_Entity_Adapter_Interface::class ) ); + TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) ); + } + + private function register_core(): void { + TMDO_Post_Fields::register_entity_fields(); + TMDO_Member_Fields::register_entity_fields(); + } + + public function test_definitions_match_core_exactly(): void { + $this->fresh_registry(); + $this->register_core(); + $core = array(); + foreach ( self::GROUPS as $g ) { + $core[ $g[1] ] = TMDO_Entity_Registry::get_group_fields( $g[0], $g[1] ); + } + + $this->fresh_registry(); + TMDO_HivePress_Entity_Groups::register_entity_fields(); + + foreach ( self::GROUPS as $g ) { + $this->assertNotEmpty( $core[ $g[1] ], "core no longer registers {$g[1]}" ); + $this->assertSame( $core[ $g[1] ], TMDO_Entity_Registry::get_group_fields( $g[0], $g[1] ), "{$g[0]}/{$g[1]} drifted from core" ); + } + } + + public function test_shared_keys_resolve_to_the_same_group_as_core(): void { + // hp_status / hp_user / hp_view_count … appear in several post groups and + // field_index is last-registered-wins, so registration order matters. + $this->fresh_registry(); + $this->register_core(); + $core = array(); + foreach ( self::GROUPS as $g ) { + foreach ( TMDO_Entity_Registry::get_group_keys( $g[0], $g[1] ) as $key ) { + $core[ $g[0] . ':' . $key ] = TMDO_Entity_Registry::get_field( $g[0], $key )['group']; + } + } + + $this->fresh_registry(); + TMDO_HivePress_Entity_Groups::register_entity_fields(); + + foreach ( $core as $id => $group ) { + list( $type, $key ) = explode( ':', $id, 2 ); + $this->assertSame( $group, TMDO_Entity_Registry::get_field( $type, $key )['group'], $id ); + } + } + + public function test_registering_alongside_core_raises_no_collision(): void { + $this->fresh_registry(); + $this->register_core(); + + TMDO_HivePress_Entity_Groups::register_entity_fields(); + ( new TMDO_HivePress_Core_Adapter() )->on_register_entity_fields(); + + $this->assertSame( array(), TMDO_Entity_Registry::get_collisions() ); + } +} From 7d0edb6228d97d1e0e6ea1dc5e336455a12cdef9 Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 25 Sep 2026 23:49:00 +0800 Subject: [PATCH 07/12] test(2meet-data-optimizer-hivepress-addon): pin hp entity group definitions to persisted schema hashes - HivePressEntityGroupsTest no longer loads core TMDO_Post_Fields / TMDO_Member_Fields, so it survives core dropping them (PR-020b) - Pin the 4 groups to their wpdo_registry_meta hashes and the current owner of the 8 shared post keys (documented as existing behaviour, not intent) - Verify: HP unit 144 OK; mutating one field type fails the hash test - PR: PR-020a --- tests/unit/HivePressEntityGroupsTest.php | 93 +++++++++++------------- 1 file changed, 41 insertions(+), 52 deletions(-) diff --git a/tests/unit/HivePressEntityGroupsTest.php b/tests/unit/HivePressEntityGroupsTest.php index fa917d2..dc4be8a 100644 --- a/tests/unit/HivePressEntityGroupsTest.php +++ b/tests/unit/HivePressEntityGroupsTest.php @@ -7,74 +7,63 @@ use PHPUnit\Framework\TestCase; * The AddOn owns the HivePress entity groups core used to register. * * Their definitions are persisted (wpdo_registry_meta schema hash) and the - * flat tables on live sites were built from core's version, so the AddOn must - * register byte-identical definitions: any drift changes the schema hash and - * would ALTER tables that hold aeav_only data once core stops registering - * them (PR-020). + * flat tables on live sites were built from them, so they are pinned to the + * persisted hashes: any drift would ALTER flat tables that may hold + * aeav_only data. (Values recorded on dev20, 2026-09-25.) */ class HivePressEntityGroupsTest extends TestCase { - private const GROUPS = array( - array( 'post', 'hp_listing_core' ), - array( 'post', 'hp_request_core' ), - array( 'post', 'hp_vendor_core' ), - array( 'user', 'hp_user' ), + private const PERSISTED_HASHES = array( + 'post/hp_listing_core' => 'fb5ab760', + 'post/hp_request_core' => '27d6f4f5', + 'post/hp_vendor_core' => '5366e5c8', + 'user/hp_user' => '61ac526f', ); - private function fresh_registry(): void { + /** + * Keys shared by several post groups resolve to the LAST group registered + * (the post entity has a single field index, not one per post_type). + * + * ⚠️ This pins current behaviour, not desired behaviour: a listing's + * hp_rating is routed to the hp_vendor_core table, its hp_status to + * hp_request_core, etc. It is pinned so that moving the registration + * between plugins cannot change routing silently; fixing the routing is + * tracked separately in the backlog. + */ + private const SHARED_KEY_OWNERS = array( + 'hp_status' => 'hp_request_core', + 'hp_expired_time' => 'hp_request_core', + 'hp_view_count' => 'hp_request_core', + 'hp_verified' => 'hp_vendor_core', + 'hp_rating' => 'hp_vendor_core', + 'hp_rating_count' => 'hp_vendor_core', + 'hp_hourly_rate' => 'hp_vendor_core', + 'hp_user' => 'hp_vendor_core', + ); + + protected function setUp(): void { TMDO_Entity_Registry::init(); TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) ); TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) ); - } - - private function register_core(): void { - TMDO_Post_Fields::register_entity_fields(); - TMDO_Member_Fields::register_entity_fields(); - } - - public function test_definitions_match_core_exactly(): void { - $this->fresh_registry(); - $this->register_core(); - $core = array(); - foreach ( self::GROUPS as $g ) { - $core[ $g[1] ] = TMDO_Entity_Registry::get_group_fields( $g[0], $g[1] ); - } - - $this->fresh_registry(); TMDO_HivePress_Entity_Groups::register_entity_fields(); + } - foreach ( self::GROUPS as $g ) { - $this->assertNotEmpty( $core[ $g[1] ], "core no longer registers {$g[1]}" ); - $this->assertSame( $core[ $g[1] ], TMDO_Entity_Registry::get_group_fields( $g[0], $g[1] ), "{$g[0]}/{$g[1]} drifted from core" ); + public function test_definitions_match_persisted_schema_hashes(): void { + foreach ( self::PERSISTED_HASHES as $id => $hash ) { + list( $type, $group ) = explode( '/', $id ); + $fields = TMDO_Entity_Registry::get_group_fields( $type, $group ); + $this->assertNotEmpty( $fields, "{$id} not registered" ); + $this->assertSame( $hash, substr( TMDO_Schema_Manager::calculate_schema_hash( $fields ), 0, 8 ), "{$id} drifted from the persisted definition" ); } } - public function test_shared_keys_resolve_to_the_same_group_as_core(): void { - // hp_status / hp_user / hp_view_count … appear in several post groups and - // field_index is last-registered-wins, so registration order matters. - $this->fresh_registry(); - $this->register_core(); - $core = array(); - foreach ( self::GROUPS as $g ) { - foreach ( TMDO_Entity_Registry::get_group_keys( $g[0], $g[1] ) as $key ) { - $core[ $g[0] . ':' . $key ] = TMDO_Entity_Registry::get_field( $g[0], $key )['group']; - } - } - - $this->fresh_registry(); - TMDO_HivePress_Entity_Groups::register_entity_fields(); - - foreach ( $core as $id => $group ) { - list( $type, $key ) = explode( ':', $id, 2 ); - $this->assertSame( $group, TMDO_Entity_Registry::get_field( $type, $key )['group'], $id ); + public function test_shared_keys_keep_their_current_owner(): void { + foreach ( self::SHARED_KEY_OWNERS as $key => $group ) { + $this->assertSame( $group, TMDO_Entity_Registry::get_field( 'post', $key )['group'], $key ); } } - public function test_registering_alongside_core_raises_no_collision(): void { - $this->fresh_registry(); - $this->register_core(); - - TMDO_HivePress_Entity_Groups::register_entity_fields(); + public function test_core_adapter_does_not_register_a_conflicting_group(): void { ( new TMDO_HivePress_Core_Adapter() )->on_register_entity_fields(); $this->assertSame( array(), TMDO_Entity_Registry::get_collisions() ); From 3c57fb0ee6be969e4adde25e9f04eb16bd35616d Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 25 Sep 2026 23:57:52 +0800 Subject: [PATCH 08/12] fix(2meet-data-optimizer-hivepress-addon): declare post_type scopes on HivePress post groups - hp_listing_core / hp_request_core / hp_vendor_core declare hp_listing / hp_request / hp_vendor, matching core (PR-033), so the AddOn keeps scoped routing once core stops registering them (PR-020b) - Test: with tmdo_post_scoped_routing on, every key of each group resolves to its own post type's group; legacy owner and hash pins unchanged - Verify: HP unit 145 / integration 49 OK - PR: PR-033b --- .../class-tmdo-hivepress-entity-groups.php | 14 ++++++---- tests/unit/HivePressEntityGroupsTest.php | 27 +++++++++++++------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/includes/class-tmdo-hivepress-entity-groups.php b/includes/class-tmdo-hivepress-entity-groups.php index 8e94bf5..f0e5e58 100644 --- a/includes/class-tmdo-hivepress-entity-groups.php +++ b/includes/class-tmdo-hivepress-entity-groups.php @@ -15,8 +15,9 @@ * persisted in wpdo_registry_meta (originally registered by core's * TMDO_Post_Fields / TMDO_Member_Fields). Any drift changes the schema hash * and triggers ALTER TABLE on flat tables that may hold aeav_only data. - * The three post groups also share keys (hp_status, hp_user, …) and the - * entity field index is last-registered-wins, so keep their order. + * The three post groups share keys (hp_status, hp_user, …): each declares + * its post type so a post's key resolves to its own type's group, while + * lookups without a post type keep last-registered-wins — keep their order. * * @package TMDO_HIVEPRESS */ @@ -133,7 +134,8 @@ final class TMDO_HivePress_Entity_Groups { 'type' => 'decimal', 'label' => 'Hourly rate', ), - ) + ), + array( 'post_types' => array( 'hp_listing' ) ) ); } /** @@ -176,7 +178,8 @@ final class TMDO_HivePress_Entity_Groups { 'type' => 'integer', 'label' => 'View counter', ), - ) + ), + array( 'post_types' => array( 'hp_request' ) ) ); } /** @@ -218,7 +221,8 @@ final class TMDO_HivePress_Entity_Groups { 'searchable' => true, 'label' => 'Average rating', ), - ) + ), + array( 'post_types' => array( 'hp_vendor' ) ) ); } /** diff --git a/tests/unit/HivePressEntityGroupsTest.php b/tests/unit/HivePressEntityGroupsTest.php index dc4be8a..1273406 100644 --- a/tests/unit/HivePressEntityGroupsTest.php +++ b/tests/unit/HivePressEntityGroupsTest.php @@ -21,14 +21,10 @@ class HivePressEntityGroupsTest extends TestCase { ); /** - * Keys shared by several post groups resolve to the LAST group registered - * (the post entity has a single field index, not one per post_type). - * - * ⚠️ This pins current behaviour, not desired behaviour: a listing's - * hp_rating is routed to the hp_vendor_core table, its hp_status to - * hp_request_core, etc. It is pinned so that moving the registration - * between plugins cannot change routing silently; fixing the routing is - * tracked separately in the backlog. + * Lookups that do not pass a post type (or run with scoped routing off, + * until a site's rows are reconciled) keep the legacy owner of keys shared + * by several post groups: the LAST group registered. Pinned so the + * registration order cannot change silently. */ private const SHARED_KEY_OWNERS = array( 'hp_status' => 'hp_request_core', @@ -48,6 +44,10 @@ class HivePressEntityGroupsTest extends TestCase { TMDO_HivePress_Entity_Groups::register_entity_fields(); } + protected function tearDown(): void { + unset( $GLOBALS['_wp_options'][ TMDO_Entity_Registry::SCOPED_ROUTING_OPTION ] ); + } + public function test_definitions_match_persisted_schema_hashes(): void { foreach ( self::PERSISTED_HASHES as $id => $hash ) { list( $type, $group ) = explode( '/', $id ); @@ -63,6 +63,17 @@ class HivePressEntityGroupsTest extends TestCase { } } + public function test_shared_keys_follow_the_post_type_when_scoped_routing_is_on(): void { + $GLOBALS['_wp_options'][ TMDO_Entity_Registry::SCOPED_ROUTING_OPTION ] = '1'; + + foreach ( array( 'hp_listing' => 'hp_listing_core', 'hp_request' => 'hp_request_core', 'hp_vendor' => 'hp_vendor_core' ) as $post_type => $group ) { + foreach ( TMDO_Entity_Registry::get_group_keys( 'post', $group ) as $key ) { + $this->assertSame( $group, TMDO_Entity_Registry::get_field( 'post', $key, $post_type )['group'], "{$post_type}:{$key}" ); + } + } + $this->assertNull( TMDO_Entity_Registry::get_field( 'post', 'hp_rating', 'post' ), 'HivePress keys stay native on other post types' ); + } + public function test_core_adapter_does_not_register_a_conflicting_group(): void { ( new TMDO_HivePress_Core_Adapter() )->on_register_entity_fields(); From dca134b5b60826edd36a8e0f825be1084511ac79 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sat, 26 Sep 2026 00:16:00 +0800 Subject: [PATCH 09/12] refactor(2meet-data-optimizer-hivepress-addon): remove TMDO_HPCT_Import - Delete the HPCT importer (no caller left after core dropped HPCT in PR-020c) and its bootstrap/alias/test-bootstrap entries - Conflict guard notice, log line and HivePress admin tab no longer point to the never-existing `wp wpdo hivepress migrate-from-hpct`; they say to deactivate the legacy plugins (their own tables are not imported) - Verify: ConflictGuardTest guard; HP unit 146 / integration 49 OK, bootstrap smoke OK - PR: PR-020d --- admin/class-tmdo-admin-hivepress.php | 2 +- includes/back-compat-aliases.php | 1 - includes/class-tmdo-hivepress-bootstrap.php | 1 - includes/class-tmdo-hpct-import.php | 248 ------------------ .../class-tmdo-hivepress-conflict-guard.php | 9 +- tests/bootstrap.php | 1 - tests/integration/bootstrap.php | 1 - tests/unit/ConflictGuardTest.php | 10 + 8 files changed, 15 insertions(+), 258 deletions(-) delete mode 100644 includes/class-tmdo-hpct-import.php diff --git a/admin/class-tmdo-admin-hivepress.php b/admin/class-tmdo-admin-hivepress.php index 462a725..ebe1c68 100644 --- a/admin/class-tmdo-admin-hivepress.php +++ b/admin/class-tmdo-admin-hivepress.php @@ -138,7 +138,7 @@ wp wpdo hivepress benchmark . esc_html__( '偵測到衝突外掛:', 'tmdo-hivepress' ) . ' ' . esc_html( implode( ', ', $conflicts ) ) . '

' - . esc_html__( 'WPDO HivePress integration 已自動退讓。請執行 `wp wpdo hivepress migrate-from-hpct` 後停用舊外掛。', 'tmdo-hivepress' ) + . esc_html__( 'WPDO HivePress integration 已自動退讓。停用舊外掛後即由 WPDO 接手(舊外掛自有資料表的資料不會匯入)。', 'tmdo-hivepress' ) . '

'; } diff --git a/includes/back-compat-aliases.php b/includes/back-compat-aliases.php index 1a729c0..2d30a89 100644 --- a/includes/back-compat-aliases.php +++ b/includes/back-compat-aliases.php @@ -20,7 +20,6 @@ add_action( $aliases = array( 'TMDO_Admin_HivePress' => 'WPDO_Admin_HivePress', 'TMDO_CLI_HivePress' => 'WPDO_CLI_HivePress', - 'TMDO_HPCT_Import' => 'WPDO_HPCT_Import', 'TMDO_HP_Bootstrap' => 'WPDO_HP_Bootstrap', 'TMDO_HivePress' => 'WPDO_HivePress', 'TMDO_HivePress_Attribute_Bridge' => 'WPDO_HivePress_Attribute_Bridge', diff --git a/includes/class-tmdo-hivepress-bootstrap.php b/includes/class-tmdo-hivepress-bootstrap.php index c146afe..354925f 100644 --- a/includes/class-tmdo-hivepress-bootstrap.php +++ b/includes/class-tmdo-hivepress-bootstrap.php @@ -66,7 +66,6 @@ final class TMDO_HP_Bootstrap { // Misc HP integration files require_once $base . 'includes/class-tmdo-hivepress.php'; require_once $base . 'includes/class-tmdo-hivepress-transient-filter.php'; - require_once $base . 'includes/class-tmdo-hpct-import.php'; require_once $base . 'includes/class-tmdo-hivepress-term-comment-fields.php'; require_once $base . 'includes/class-tmdo-hivepress-entity-groups.php'; require_once $base . 'includes/class-tmdo-listing-stats.php'; diff --git a/includes/class-tmdo-hpct-import.php b/includes/class-tmdo-hpct-import.php deleted file mode 100644 index 71f72d9..0000000 --- a/includes/class-tmdo-hpct-import.php +++ /dev/null @@ -1,248 +0,0 @@ - WP Data Optimizer > HPCT Import tab - * - CLI: wp wpdo import-hpct - */ -class TMDO_HPCT_Import { - - /** Option key that marks import as completed. */ - public const IMPORTED_OPTION = 'wpdo_hpct_imported'; - - /** - * Check if HPCT import has already been completed. - */ - public static function is_imported(): bool { - return (bool) get_option( self::IMPORTED_OPTION, false ); - } - - /** - * Check if hp-custom-tables is active and has data to import. - */ - public static function can_import(): bool { - if ( self::is_imported() ) { - return false; - } - - return TMDO_Compatibility::is_hpct_active(); - } - - /** - * Preview: return what will be imported without actually doing it. - * - * @return array{modules: array, migration_records: int, has_data: bool} - */ - public static function preview(): array { - $hpct_features = get_option( 'hpct_features', array() ); - if ( ! is_array( $hpct_features ) ) { - $hpct_features = array(); - } - - $modules = array(); - foreach ( TMDO_Feature_Flags::HPCT_MODULES as $module ) { - $hpct_status = $hpct_features[ $module ] ?? 'disabled'; - $wpdo_state = TMDO_Feature_Flags::map_hpct_status( $hpct_status ); - - $modules[] = array( - 'module' => $module, - 'hpct_status' => $hpct_status, - 'wpdo_state' => $wpdo_state, - ); - } - - // Count HPCT migration records. - global $wpdb; - $hpct_migrations_table = $wpdb->prefix . 'hpct_migrations'; - $migration_count = 0; - - // Check if hpct_migrations table exists. - if ( TMDO_IS_SQLITE ) { - $table_exists = $wpdb->get_var( - $wpdb->prepare( - "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = %s", - $hpct_migrations_table - ) - ); - } else { - $table_exists = $wpdb->get_var( - $wpdb->prepare( - 'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = %s', - $hpct_migrations_table - ) - ); - } - - if ( $table_exists ) { - $migration_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$hpct_migrations_table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix . 'hpct_migrations'. - } - - return array( - 'modules' => $modules, - 'migration_records' => $migration_count, - 'has_data' => ! empty( $hpct_features ), - ); - } - - /** - * Execute the import. - * - * @return array{success: bool, message: string, imported: array} - */ - public static function run(): array { - if ( self::is_imported() ) { - return array( - 'success' => false, - 'message' => 'HPCT import has already been completed.', - 'imported' => array(), - ); - } - - // Step 1: Read HPCT feature flags. - $hpct_features = get_option( 'hpct_features', array() ); - if ( ! is_array( $hpct_features ) ) { - $hpct_features = array(); - } - - $imported = array(); - - // Step 2: Map each HPCT module state to WPDO state. - foreach ( TMDO_Feature_Flags::HPCT_MODULES as $module ) { - $hpct_status = $hpct_features[ $module ] ?? 'disabled'; - $wpdo_state = TMDO_Feature_Flags::map_hpct_status( $hpct_status ); - - TMDO_Feature_Flags::set( $module, $wpdo_state ); - - $imported[] = array( - 'module' => $module, - 'hpct_status' => $hpct_status, - 'wpdo_state' => $wpdo_state, - ); - } - - // Step 3: Copy HPCT migration records to WPDO. - self::copy_migration_records(); - - // Step 4: Mark import as completed. - update_option( self::IMPORTED_OPTION, true ); - - return array( - 'success' => true, - 'message' => sprintf( - 'Successfully imported %d HPCT modules. You can now deactivate hp-custom-tables.', - count( $imported ) - ), - 'imported' => $imported, - ); - } - - /** - * Copy HPCT migration progress records to wpdo_migrations. - */ - private static function copy_migration_records(): void { - global $wpdb; - - $hpct_table = $wpdb->prefix . 'hpct_migrations'; - $wpdo_table = TMDO_DB::table( 'wpdo_migrations' ); - $now = TMDO_DB::now(); - - // Check if hpct_migrations table exists. - $exists = false; - if ( TMDO_IS_SQLITE ) { - $exists = (bool) $wpdb->get_var( - $wpdb->prepare( - "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = %s", - $hpct_table - ) - ); - } else { - $exists = (bool) $wpdb->get_var( - $wpdb->prepare( - 'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = %s', - $hpct_table - ) - ); - } - - if ( ! $exists ) { - return; - } - - $hpct_records = $wpdb->get_results( "SELECT * FROM `{$hpct_table}`", ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix . 'hpct_migrations'. - if ( ! $hpct_records ) { - return; - } - - foreach ( $hpct_records as $record ) { - $module = $record['module'] ?? ''; - if ( ! $module ) { - continue; - } - - // Map HPCT status to WPDO state. - $hpct_status = $record['status'] ?? 'pending'; - $state = self::map_migration_status( $hpct_status ); - - // Check if WPDO already has a record for this module. - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from TMDO_DB::table(). - $existing = $wpdb->get_var( - $wpdb->prepare( - "SELECT id FROM `{$wpdo_table}` WHERE module = %s", - $module - ) - ); - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - - $data = array( - 'module' => $module, - 'zone' => '', - 'state' => $state, - 'total_rows' => (int) ( $record['total_rows'] ?? 0 ), - 'processed_rows' => (int) ( $record['processed_rows'] ?? 0 ), - 'last_offset' => (int) ( $record['last_offset'] ?? 0 ), - 'error_count' => (int) ( $record['error_count'] ?? 0 ), - 'started_at' => $record['started_at'] ?? null, - 'completed_at' => $record['completed_at'] ?? null, - 'updated_at' => $now, - ); - - if ( $existing ) { - $wpdb->update( $wpdo_table, $data, array( 'id' => (int) $existing ) ); - } else { - $data['created_at'] = $now; - $wpdb->insert( $wpdo_table, $data ); - } - } - } - - /** - * Map HPCT migration record status to WPDO migration state. - * - * @param string $hpct_status HPCT migration status string. - * @return string Corresponding WPDO migration state. - */ - private static function map_migration_status( string $hpct_status ): string { - return match ( $hpct_status ) { - 'pending' => 'idle', - 'running' => 'backfill', - 'completed' => 'verify', - 'failed' => 'idle', - default => 'idle', - }; - } -} diff --git a/includes/hivepress/class-tmdo-hivepress-conflict-guard.php b/includes/hivepress/class-tmdo-hivepress-conflict-guard.php index 56523d2..b0e9fe0 100644 --- a/includes/hivepress/class-tmdo-hivepress-conflict-guard.php +++ b/includes/hivepress/class-tmdo-hivepress-conflict-guard.php @@ -10,9 +10,8 @@ * * Strategy: detect, log, render admin notice with guidance, AND short-circuit * the WPDO HivePress bootstrap via `wpdo_hivepress_should_bind` filter so - * adapters are NOT bound. Operator must explicitly run - * `wp wpdo hivepress migrate-from-hpct` to switch to WPDO and deactivate - * the legacy plugins. + * adapters are NOT bound. The operator deactivates the legacy plugins to let + * WPDO take over; data kept in their own tables is not imported. * * @package WP_Data_Optimizer * @since 3.0.0 @@ -163,7 +162,7 @@ if ( ! class_exists( 'TMDO_HivePress_Conflict_Guard' ) ) { $message = sprintf( /* translators: %s: comma-separated list of conflicting plugin names. */ - __( 'wp-data-optimizer detected legacy HivePress data-layer plugins: %s. WPDO HivePress integration is paused to prevent dual-write corruption. Run `wp wpdo hivepress migrate-from-hpct` to migrate, then deactivate the legacy plugins.', 'tmdo-hivepress' ), + __( 'wp-data-optimizer detected legacy HivePress data-layer plugins: %s. WPDO HivePress integration is paused to prevent dual-write corruption. Deactivate the legacy plugins to let WPDO take over (data in their own tables is not imported).', 'tmdo-hivepress' ), implode( ', ', $names ) ); @@ -206,7 +205,7 @@ if ( ! class_exists( 'TMDO_HivePress_Conflict_Guard' ) ) { } $message = sprintf( - 'Conflict detected: %s. wp-data-optimizer fully supersedes these. Run: wp wpdo hivepress migrate-from-hpct', + 'Conflict detected: %s. wp-data-optimizer fully supersedes these. Deactivate them to let WPDO take over.', implode( ', ', $conflicts ) ); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index cbaaa44..c4a0cc2 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -46,7 +46,6 @@ $tmdo_hp_files = array( 'includes/class-tmdo-hivepress-transient-filter.php', 'includes/class-tmdo-hivepress-term-comment-fields.php', 'includes/class-tmdo-hivepress-entity-groups.php', - 'includes/class-tmdo-hpct-import.php', 'includes/class-tmdo-listing-stats.php', 'admin/class-tmdo-admin-hivepress.php', 'cli/class-tmdo-cli-hivepress.php', diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index 1b21448..b34bf9d 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -45,7 +45,6 @@ $tmdo_hp_files = array( 'includes/class-tmdo-hivepress-transient-filter.php', 'includes/class-tmdo-hivepress-term-comment-fields.php', 'includes/class-tmdo-hivepress-entity-groups.php', - 'includes/class-tmdo-hpct-import.php', 'admin/class-tmdo-admin-hivepress.php', 'cli/class-tmdo-cli-hivepress.php', ); diff --git a/tests/unit/ConflictGuardTest.php b/tests/unit/ConflictGuardTest.php index 8243854..9ef19f0 100644 --- a/tests/unit/ConflictGuardTest.php +++ b/tests/unit/ConflictGuardTest.php @@ -47,6 +47,16 @@ class HivePressConflictGuardTest extends TestCase { $this->assertSame( '', $out ); } + public function test_guidance_does_not_point_to_a_missing_command(): void { + // There is no HPCT importer: `wp wpdo hivepress migrate-from-hpct` never + // existed and TMDO_HPCT_Import had no caller once core dropped HPCT. + $root = dirname( __DIR__, 2 ); + foreach ( array( 'includes/hivepress/class-tmdo-hivepress-conflict-guard.php', 'admin/class-tmdo-admin-hivepress.php' ) as $file ) { + $this->assertStringNotContainsString( 'migrate-from-hpct', (string) file_get_contents( $root . '/' . $file ), $file ); + } + $this->assertFileDoesNotExist( $root . '/includes/class-tmdo-hpct-import.php' ); + } + public function test_memo_cache_avoids_repeat_probing(): void { WPDO_HivePress_Conflict_Guard::reset_for_tests(); $first = WPDO_HivePress_Conflict_Guard::detect_conflicts(); From 69c8bf2a88c641dd2b3a97e6a1309ae360994913 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sat, 26 Sep 2026 00:24:43 +0800 Subject: [PATCH 10/12] feat(2meet-data-optimizer-hivepress-addon): provide HivePress defaults to core via filters - TMDO_HivePress_Entity_Groups hooks tmdo_rest_default_post_type so GET /wpdo/v1/listings keeps defaulting to hp_listing now that core defaults to 'post' (PR-020e) - Test: TMDO_Post_Group_Reconciler::plan() over the AddOn's groups yields the 8 misrouted HivePress keys (coverage moved out of core in PR-020b) - Verify: HP unit 148 / integration 49 OK, bootstrap smoke OK - PR: PR-020f --- .../class-tmdo-hivepress-entity-groups.php | 12 +++++++- tests/unit/HivePressEntityGroupsTest.php | 29 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/includes/class-tmdo-hivepress-entity-groups.php b/includes/class-tmdo-hivepress-entity-groups.php index f0e5e58..798fba3 100644 --- a/includes/class-tmdo-hivepress-entity-groups.php +++ b/includes/class-tmdo-hivepress-entity-groups.php @@ -32,12 +32,22 @@ if ( ! defined( 'ABSPATH' ) ) { final class TMDO_HivePress_Entity_Groups { /** - * Hook into WPDO entity field registration. + * Hook into WPDO entity field registration and the REST default post type. * * @return void */ public static function register(): void { add_action( 'wpdo_register_entity_fields', array( __CLASS__, 'register_entity_fields' ) ); + add_filter( 'tmdo_rest_default_post_type', array( __CLASS__, 'rest_default_post_type' ) ); + } + + /** + * GET /wpdo/v1/listings serves HivePress listings when no post type is given. + * + * @return string + */ + public static function rest_default_post_type(): string { + return 'hp_listing'; } /** diff --git a/tests/unit/HivePressEntityGroupsTest.php b/tests/unit/HivePressEntityGroupsTest.php index 1273406..72fface 100644 --- a/tests/unit/HivePressEntityGroupsTest.php +++ b/tests/unit/HivePressEntityGroupsTest.php @@ -46,6 +46,7 @@ class HivePressEntityGroupsTest extends TestCase { protected function tearDown(): void { unset( $GLOBALS['_wp_options'][ TMDO_Entity_Registry::SCOPED_ROUTING_OPTION ] ); + unset( $GLOBALS['_wp_filter_callbacks']['tmdo_rest_default_post_type'] ); } public function test_definitions_match_persisted_schema_hashes(): void { @@ -74,6 +75,34 @@ class HivePressEntityGroupsTest extends TestCase { $this->assertNull( TMDO_Entity_Registry::get_field( 'post', 'hp_rating', 'post' ), 'HivePress keys stay native on other post types' ); } + public function test_reconciler_plans_the_eight_misrouted_hivepress_keys(): void { + $moves = array(); + foreach ( TMDO_Post_Group_Reconciler::plan() as $m ) { + $moves[] = "{$m['source']}>{$m['target']}:{$m['key']}"; + } + sort( $moves ); + + $this->assertSame( + array( + 'hp_request_core>hp_listing_core:hp_expired_time', + 'hp_request_core>hp_listing_core:hp_status', + 'hp_request_core>hp_listing_core:hp_view_count', + 'hp_vendor_core>hp_listing_core:hp_hourly_rate', + 'hp_vendor_core>hp_listing_core:hp_rating', + 'hp_vendor_core>hp_listing_core:hp_rating_count', + 'hp_vendor_core>hp_listing_core:hp_verified', + 'hp_vendor_core>hp_request_core:hp_user', + ), + $moves + ); + } + + public function test_rest_listings_default_to_hivepress_listings(): void { + TMDO_HivePress_Entity_Groups::register(); + + $this->assertSame( 'hp_listing', TMDO_REST_API::default_post_type() ); + } + public function test_core_adapter_does_not_register_a_conflicting_group(): void { ( new TMDO_HivePress_Core_Adapter() )->on_register_entity_fields(); From 0e260ee3a94670a36965437ee11baa49397d210c Mon Sep 17 00:00:00 2001 From: wpdev Date: Sat, 26 Sep 2026 01:09:34 +0800 Subject: [PATCH 11/12] feat(2meet-data-optimizer-hivepress-addon): register Hot-only HivePress fields as entity groups - New scoped post groups for fields only the Hot zone covered (ADR-003): hp_draft_flags (hp_listing, hp_request: hp_drafted), hp_booking_core, hp_membership_plan_core, hp_membership_core. hp_drafted lives in one group spanning both post types, so it adds no shared key. - New groups register before the existing ones: the legacy (unscoped) owner of hp_expired_time stays hp_request_core; existing hashes unchanged. - Reconciler now also plans hp_membership rows of hp_expired_time out of hp_request_core. dev20-22 hold 0 native rows for these keys. - Verify: HP unit 151 / integration 49 green, phpcs clean - PR: PR-036 --- .../class-tmdo-hivepress-entity-groups.php | 112 ++++++++++++++++++ tests/unit/HivePressEntityGroupsTest.php | 37 +++++- 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/includes/class-tmdo-hivepress-entity-groups.php b/includes/class-tmdo-hivepress-entity-groups.php index 798fba3..2894502 100644 --- a/includes/class-tmdo-hivepress-entity-groups.php +++ b/includes/class-tmdo-hivepress-entity-groups.php @@ -8,6 +8,14 @@ * post:hp_vendor_core → wp_wpdo_post_hp_vendor_core (hp_vendor) * user:hp_user → wp_wpdo_user_hp_user * + * Fields only the Hot zone used to cover (ADR-003), as their own groups: + * post:hp_draft_flags (hp_listing, hp_request) + * post:hp_booking_core (hp_booking) + * post:hp_membership_plan_core (hp_membership_plan) + * post:hp_membership_core (hp_membership) + * They register FIRST so the legacy last-registered owner of hp_expired_time + * stays hp_request_core. + * * The term/comment groups (hp_taxonomy, hp_review) live in * TMDO_Hivepress_Term_Comment_Fields. * @@ -60,6 +68,10 @@ final class TMDO_HivePress_Entity_Groups { return; } + self::register_hp_draft_flags_group(); + self::register_hp_booking_core_group(); + self::register_hp_membership_plan_core_group(); + self::register_hp_membership_core_group(); self::register_hp_listing_core_group(); self::register_hp_request_core_group(); self::register_hp_vendor_core_group(); @@ -68,6 +80,106 @@ final class TMDO_HivePress_Entity_Groups { // ── Group definitions ──────────────────────────────────────────────────── + /** + * Draft flag shared by listings and requests (user-dashboard filtering). + * + * @return void + */ + private static function register_hp_draft_flags_group(): void { + TMDO_Entity_Registry::register_group( + 'post', + 'hp_draft_flags', + array( + array( + 'key' => 'hp_drafted', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Drafted flag (0/1)', + ), + ), + array( 'post_types' => array( 'hp_listing', 'hp_request' ) ) + ); + } + + /** + * HivePress Bookings (post_type=hp_booking): slot-availability lookups. + * + * @return void + */ + private static function register_hp_booking_core_group(): void { + TMDO_Entity_Registry::register_group( + 'post', + 'hp_booking_core', + array( + array( + 'key' => 'hp_start_time', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Booking start unix ts', + ), + array( + 'key' => 'hp_end_time', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Booking end unix ts', + ), + array( + 'key' => 'hp_quantity', + 'type' => 'integer', + 'label' => 'Places booked', + ), + ), + array( 'post_types' => array( 'hp_booking' ) ) + ); + } + + /** + * HivePress Memberships plan (post_type=hp_membership_plan). + * + * @return void + */ + private static function register_hp_membership_plan_core_group(): void { + TMDO_Entity_Registry::register_group( + 'post', + 'hp_membership_plan_core', + array( + array( + 'key' => 'hp_expire_period', + 'type' => 'integer', + 'label' => 'Plan expiry period (days)', + ), + array( + 'key' => 'hp_primary', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Primary plan flag (0/1)', + ), + ), + array( 'post_types' => array( 'hp_membership_plan' ) ) + ); + } + + /** + * HivePress Memberships subscription (post_type=hp_membership). + * + * @return void + */ + private static function register_hp_membership_core_group(): void { + TMDO_Entity_Registry::register_group( + 'post', + 'hp_membership_core', + array( + array( + 'key' => 'hp_expired_time', + 'type' => 'integer', + 'searchable' => true, + 'label' => 'Expiry unix ts', + ), + ), + array( 'post_types' => array( 'hp_membership' ) ) + ); + } + /** * HivePress listing core meta keys (post_type=hp_listing). * Aligns with the existing wpdo_hot_hp_listing flat table; v2.9.5 will diff --git a/tests/unit/HivePressEntityGroupsTest.php b/tests/unit/HivePressEntityGroupsTest.php index 72fface..db610ce 100644 --- a/tests/unit/HivePressEntityGroupsTest.php +++ b/tests/unit/HivePressEntityGroupsTest.php @@ -75,7 +75,41 @@ class HivePressEntityGroupsTest extends TestCase { $this->assertNull( TMDO_Entity_Registry::get_field( 'post', 'hp_rating', 'post' ), 'HivePress keys stay native on other post types' ); } - public function test_reconciler_plans_the_eight_misrouted_hivepress_keys(): void { + /** + * Fields only the Hot zone used to cover (ADR-003), as their own groups. + */ + private const HOT_ONLY_GROUPS = array( + 'hp_draft_flags' => array( array( 'hp_listing', 'hp_request' ), array( 'hp_drafted' ) ), + 'hp_booking_core' => array( array( 'hp_booking' ), array( 'hp_start_time', 'hp_end_time', 'hp_quantity' ) ), + 'hp_membership_plan_core' => array( array( 'hp_membership_plan' ), array( 'hp_expire_period', 'hp_primary' ) ), + 'hp_membership_core' => array( array( 'hp_membership' ), array( 'hp_expired_time' ) ), + ); + + public function test_hot_only_fields_have_entity_groups_scoped_to_their_post_types(): void { + foreach ( self::HOT_ONLY_GROUPS as $group => list( $post_types, $keys ) ) { + $this->assertSame( $post_types, TMDO_Entity_Registry::get_group_post_types( 'post', $group ), $group ); + $this->assertSame( $keys, TMDO_Entity_Registry::get_group_keys( 'post', $group ), $group ); + } + } + + public function test_hot_only_fields_route_to_their_group_when_scoped_routing_is_on(): void { + $GLOBALS['_wp_options'][ TMDO_Entity_Registry::SCOPED_ROUTING_OPTION ] = '1'; + + foreach ( self::HOT_ONLY_GROUPS as $group => list( $post_types, $keys ) ) { + foreach ( $post_types as $post_type ) { + foreach ( $keys as $key ) { + $this->assertSame( $group, TMDO_Entity_Registry::get_field( 'post', $key, $post_type )['group'], "{$post_type}:{$key}" ); + } + } + } + $this->assertSame( 'hp_listing_core', TMDO_Entity_Registry::get_field( 'post', 'hp_expired_time', 'hp_listing' )['group'] ); + } + + public function test_hot_only_groups_add_no_collisions(): void { + $this->assertSame( array(), TMDO_Entity_Registry::get_collisions() ); + } + + public function test_reconciler_plans_the_misrouted_hivepress_keys(): void { $moves = array(); foreach ( TMDO_Post_Group_Reconciler::plan() as $m ) { $moves[] = "{$m['source']}>{$m['target']}:{$m['key']}"; @@ -87,6 +121,7 @@ class HivePressEntityGroupsTest extends TestCase { 'hp_request_core>hp_listing_core:hp_expired_time', 'hp_request_core>hp_listing_core:hp_status', 'hp_request_core>hp_listing_core:hp_view_count', + 'hp_request_core>hp_membership_core:hp_expired_time', 'hp_vendor_core>hp_listing_core:hp_hourly_rate', 'hp_vendor_core>hp_listing_core:hp_rating', 'hp_vendor_core>hp_listing_core:hp_rating_count', From 0254e40d8352068610c9737fb0e11b281f523bf7 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sat, 26 Sep 2026 01:13:35 +0800 Subject: [PATCH 12/12] refactor(2meet-data-optimizer-hivepress-addon): stop registering Hot zone fields - ADR-003 step 4 (AddOn side): core, requests, bookings, memberships and marketplace adapters no longer register zone=hot fields or contribute hot_* FSM modules; their keys live in TMDO_HivePress_Entity_Groups. Cold/warm registrations are unchanged. - Hot-table doctor probes go with them (trait defaults apply). The hot-clause query rewrite and cron optimizer are now inert and are left for the ADR step-5 removal together with core's hot path. - New NoHotZoneRegistrationTest pins the invariant across all 13 adapters; per-adapter hot assertions removed. - Verify: HP unit 142 / integration 49 green, phpcs clean (WordPress) - PR: PR-037 --- .../class-tmdo-hivepress-bookings-adapter.php | 91 +-------------- .../class-tmdo-hivepress-core-adapter.php | 106 +----------------- ...ass-tmdo-hivepress-marketplace-adapter.php | 55 +-------- ...ass-tmdo-hivepress-memberships-adapter.php | 100 +---------------- .../class-tmdo-hivepress-requests-adapter.php | 72 +----------- tests/unit/BookingsAdapterTest.php | 29 ----- tests/unit/CoreAdapterTest.php | 29 ----- tests/unit/MarketplaceAdapterTest.php | 15 --- tests/unit/MembershipsAdapterTest.php | 33 ------ tests/unit/NoHotZoneRegistrationTest.php | 51 +++++++++ tests/unit/RequestsAdapterTest.php | 17 --- 11 files changed, 71 insertions(+), 527 deletions(-) create mode 100644 tests/unit/NoHotZoneRegistrationTest.php diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-bookings-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-bookings-adapter.php index 60cfaa1..a30a5e7 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-bookings-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-bookings-adapter.php @@ -8,18 +8,13 @@ * Booking model (post_type=hp_booking) — fields are best-effort from the * addon's documented field aliases: * - * start_time / end_time → unix timestamp range queries (hot) - * status → enum filter (hot) - * quantity → places booked, hp_quantity (hot for slot-availability; - * only written when "Allow multiple places" is on) + * start_time / end_time → unix timestamp range queries + * quantity → places booked, hp_quantity (only written when + * "Allow multiple places" is on) * listing → post_parent (already a posts column) * - * Hot-zone target: `wp_wpdo_hot_hp_booking` with covering indexes on - * `(listing_id, start_time)` for slot-availability queries and - * `(status, start_time)` for vendor dashboard listings. - * - * The schema MAY drift from real Booking model when the addon is finally - * installed; reconcile with `wp wpdo doctor` then. + * These keys are flattened by the `hp_booking_core` entity group + * (TMDO_HivePress_Entity_Groups); the adapter registers no zone fields. * * @package WP_Data_Optimizer * @since 3.0.0 @@ -65,73 +60,6 @@ if ( ! class_exists( 'TMDO_HivePress_Bookings_Adapter' ) ) { return '1.0.0'; } - /** - * Register hot fields for hp_booking post type. - * - * @param TMDO_Schema_Registry $registry Schema registry singleton. - */ - public function on_register_fields( TMDO_Schema_Registry $registry ): void { - $registry->register_many( - $this->plugin_slug(), - array( - array( - 'post_type' => 'hp_booking', - 'meta_key' => 'hp_start_time', - 'zone' => 'hot', - 'data_type' => 'bigint(20) UNSIGNED NOT NULL DEFAULT 0', - 'column' => 'hp_start_time', - 'indexed' => true, - ), - array( - 'post_type' => 'hp_booking', - 'meta_key' => 'hp_end_time', - 'zone' => 'hot', - 'data_type' => 'bigint(20) UNSIGNED NOT NULL DEFAULT 0', - 'column' => 'hp_end_time', - 'indexed' => true, - ), - array( - 'post_type' => 'hp_booking', - 'meta_key' => 'hp_quantity', - 'zone' => 'hot', - 'data_type' => 'int(11) NOT NULL DEFAULT 0', - 'column' => 'hp_quantity', - 'indexed' => false, - ), - ) - ); - } - - /** - * Doctor probe — verify hot table when addon is active. - * - * @return array{ok:bool, message:string} - */ - public function doctor_check(): array { - global $wpdb; - if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) { - return array( - 'ok' => false, - 'message' => 'wpdb global not available', - ); - } - $table = $wpdb->prefix . 'wpdo_hot_hp_booking'; - try { - $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - return array( - 'ok' => $exists, - 'message' => $exists - ? sprintf( 'hivepress-bookings: %s present', $table ) - : sprintf( 'hivepress-bookings: %s missing (addon installed but not migrated)', $table ), - ); - } catch ( \Throwable $e ) { - return array( - 'ok' => false, - 'message' => sprintf( 'hivepress-bookings doctor failed: %s', $e->getMessage() ), - ); - } - } - /** * 8-D self-score. * @@ -146,15 +74,6 @@ if ( ! class_exists( 'TMDO_HivePress_Bookings_Adapter' ) ) { ) ); } - - /** - * FSM modules this adapter contributes. - * - * @return array - */ - public function migrations(): array { - return array( 'hot_hp_booking' ); - } } } // end if ( ! class_exists ) diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php index 1580f78..9326ee2 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-core-adapter.php @@ -4,13 +4,14 @@ * * Covers the primary HivePress models: * - * - `Listing` (post_type=hp_listing) — Hot zone columns for search/filter - * (drafted, featured, verified, expired_time, featured_time). - * - `Vendor` (post_type=hp_vendor) — Hot column `verified`; cold blob - * for image / description (display-only). + * - `Listing` (post_type=hp_listing) — cold blob for the gallery. + * - `Vendor` (post_type=hp_vendor) — cold blob for the image. * - `User` (entity=user) — no zone fields; its `hp_user` * entity group is registered by TMDO_HivePress_Entity_Groups. * + * Search/filter keys are flattened by TMDO_HivePress_Entity_Groups + * (ADR-003: no Hot zone fields). + * * This adapter SUPERSEDES the legacy `class-tmdo-hivepress.php` (242 lines). * Behavioural parity: * @@ -23,11 +24,6 @@ * adapter only registers `hivepress` core fields. * - Vendor cold fields (`hp_image`, `hp_images`) — preserved. * - * NEW additions vs legacy: - * - * - `hp_drafted` (Listing) — Hot tinyint(1) for user-dashboard filtering. - * - `hp_expired_time` / `hp_featured_time` (Listing) — Hot bigint indexed - * for cron expiry scan acceleration (replaces postmeta full scan). * (This adapter used to register a second `hp_user` definition — * hp_verified / hp_avatar_id, keys HivePress never writes — which the * registry silently dropped; TMDO_HivePress_Entity_Groups owns it now.) @@ -114,37 +110,10 @@ if ( ! class_exists( 'TMDO_HivePress_Core_Adapter' ) ) { * @param TMDO_Schema_Registry $registry Schema registry singleton. */ public function on_register_fields( TMDO_Schema_Registry $registry ): void { - // ── Listing hot fields (search/filter accelerators) ── + // ── Cold blobs (display-only) ── $registry->register_many( $this->plugin_slug(), array( - // Drafted — used in user dashboard "my drafts" filter. - array( - 'post_type' => 'hp_listing', - 'meta_key' => 'hp_drafted', - 'zone' => 'hot', - 'data_type' => 'tinyint(1) NOT NULL DEFAULT 0', - 'column' => 'hp_drafted', - 'indexed' => true, - ), - // Expired time — replaces `meta_query` clause in hourly cron. - array( - 'post_type' => 'hp_listing', - 'meta_key' => 'hp_expired_time', - 'zone' => 'hot', - 'data_type' => 'bigint(20) UNSIGNED NOT NULL DEFAULT 0', - 'column' => 'hp_expired_time', - 'indexed' => true, - ), - // Featured time — same expiry pattern as above. - array( - 'post_type' => 'hp_listing', - 'meta_key' => 'hp_featured_time', - 'zone' => 'hot', - 'data_type' => 'bigint(20) UNSIGNED NOT NULL DEFAULT 0', - 'column' => 'hp_featured_time', - 'indexed' => true, - ), // Vendor cold blob — image attachment id (display-only). array( 'post_type' => 'hp_vendor', @@ -276,57 +245,6 @@ if ( ! class_exists( 'TMDO_HivePress_Core_Adapter' ) ) { // ── Doctor / score / migrations ──────────────────────────────────── - /** - * Health probe: verify hot tables exist + sample row count. - * - * @return array{ok:bool, message:string, details?:array} - */ - public function doctor_check(): array { - global $wpdb; - if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) { - return array( - 'ok' => false, - 'message' => 'wpdb global not available', - ); - } - - $details = array(); - $ok = true; - foreach ( self::HOT_MODELS as $post_type ) { - $table = $wpdb->prefix . 'wpdo_hot_' . sanitize_key( $post_type ); - try { - $exists = (bool) $wpdb->get_var( - $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) - ); - $details[ $post_type ] = array( - 'table' => $table, - 'exists' => $exists, - ); - if ( $exists ) { - $details[ $post_type ]['rows'] = (int) $wpdb->get_var( - "SELECT COUNT(*) FROM `{$table}`" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Sanitized via sanitize_key + wpdb prefix. - ); - } else { - $ok = false; - } - } catch ( \Throwable $e ) { - $details[ $post_type ] = array( - 'table' => $table, - 'error' => $e->getMessage(), - ); - $ok = false; - } - } - - return array( - 'ok' => $ok, - 'message' => $ok - ? 'hivepress core: hot tables OK' - : 'hivepress core: one or more hot tables missing or unreadable', - 'details' => $details, - ); - } - /** * 8-dimension self-score for the core adapter. * @@ -348,18 +266,6 @@ if ( ! class_exists( 'TMDO_HivePress_Core_Adapter' ) ) { ) ); } - - /** - * FSM modules this adapter contributes. - * - * @return array - */ - public function migrations(): array { - return array( - 'hot_hp_listing', - 'hot_hp_vendor', - ); - } } } // end if ( ! class_exists ) diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-marketplace-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-marketplace-adapter.php index f5042b2..316f5d7 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-marketplace-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-marketplace-adapter.php @@ -4,7 +4,9 @@ * * The marketplace addon turns listings into purchasable items, integrating * with WooCommerce. Its filterable, sortable listing attribute `price` is - * stored as `hp_price` → decimal(10,2), used in price-range filters (HOT). + * stored as `hp_price` → decimal(10,2), used in price-range filters. It is + * flattened by the `hp_listing_core` entity group + * (TMDO_HivePress_Entity_Groups); the adapter registers no zone fields. * * Its other listing attributes (price tiers / extras / discounts, quantity, * subscription period, …) are not search filters, and it keeps no per-listing @@ -57,57 +59,6 @@ if ( ! class_exists( 'TMDO_HivePress_Marketplace_Adapter' ) ) { return '1.0.0'; } - /** - * Register marketplace hot fields on the listing post type. - * - * @param TMDO_Schema_Registry $registry Schema registry singleton. - */ - public function on_register_fields( TMDO_Schema_Registry $registry ): void { - $registry->register_many( - $this->plugin_slug(), - array( - array( - 'post_type' => 'hp_listing', - 'meta_key' => 'hp_price', - 'zone' => 'hot', - 'data_type' => 'decimal(10,2) DEFAULT NULL', - 'column' => 'hp_price', - 'indexed' => true, - ), - ) - ); - } - - /** - * Doctor probe — verify hot column exists on listing hot table. - * - * @return array{ok:bool, message:string} - */ - public function doctor_check(): array { - global $wpdb; - if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) { - return array( - 'ok' => false, - 'message' => 'wpdb global not available', - ); - } - $table = $wpdb->prefix . 'wpdo_hot_hp_listing'; - try { - $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - return array( - 'ok' => $exists, - 'message' => $exists - ? 'hivepress-marketplace: shares wp_wpdo_hot_hp_listing' - : 'hivepress-marketplace: hot listing table missing', - ); - } catch ( \Throwable $e ) { - return array( - 'ok' => false, - 'message' => sprintf( 'hivepress-marketplace doctor failed: %s', $e->getMessage() ), - ); - } - } - /** * 8-D self-score. * diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-memberships-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-memberships-adapter.php index e6a2b2e..14a50dc 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-memberships-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-memberships-adapter.php @@ -5,18 +5,13 @@ * Two post-type models: * * - Membership_Plan (post_type=hp_membership_plan) - * hot fields: hp_expire_period (int), hp_primary (bool) + * hp_expire_period (int), hp_primary (bool) * * - Membership (post_type=hp_membership) - * hot fields: hp_expired_time (unix ts) — checked on every page load + * hp_expired_time (unix ts) — checked on every page load * - * Both fields registered to Hot zone so: - * - `hp_expired_time < NOW()` lookups become indexed range scans - * (replaces O(N) postmeta scan on each request). - * - `hp_primary = 1` lookups for "default plan" become O(1). - * - * Tables `wp_wpdo_hot_hp_membership_plan` and `wp_wpdo_hot_hp_membership` - * are created by `TMDO_Schema_Manager` from the registered hot columns. + * Flattened by the `hp_membership_plan_core` / `hp_membership_core` entity + * groups (TMDO_HivePress_Entity_Groups); the adapter registers no zone fields. * * @package WP_Data_Optimizer * @since 3.0.0 @@ -62,81 +57,6 @@ if ( ! class_exists( 'TMDO_HivePress_Memberships_Adapter' ) ) { return '2.0.0'; } - /** - * Register hot fields for both post types. - * - * @param TMDO_Schema_Registry $registry Schema registry singleton. - */ - public function on_register_fields( TMDO_Schema_Registry $registry ): void { - $registry->register_many( - $this->plugin_slug(), - array( - // Plan fields. - array( - 'post_type' => 'hp_membership_plan', - 'meta_key' => 'hp_expire_period', - 'zone' => 'hot', - 'data_type' => 'int(11) NOT NULL DEFAULT 0', - 'column' => 'hp_expire_period', - 'indexed' => false, - ), - array( - 'post_type' => 'hp_membership_plan', - 'meta_key' => 'hp_primary', - 'zone' => 'hot', - 'data_type' => 'tinyint(1) NOT NULL DEFAULT 0', - 'column' => 'hp_primary', - 'indexed' => true, - ), - // Membership (subscription) field — hottest path: every page load. - array( - 'post_type' => 'hp_membership', - 'meta_key' => 'hp_expired_time', - 'zone' => 'hot', - 'data_type' => 'bigint(20) UNSIGNED NOT NULL DEFAULT 0', - 'column' => 'hp_expired_time', - 'indexed' => true, - ), - ) - ); - } - - /** - * Doctor probe — verify both hot tables. - * - * @return array{ok:bool, message:string, details?:array} - */ - public function doctor_check(): array { - global $wpdb; - if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) { - return array( - 'ok' => false, - 'message' => 'wpdb global not available', - ); - } - $details = array(); - $ok = true; - foreach ( array( 'hp_membership_plan', 'hp_membership' ) as $pt ) { - $table = $wpdb->prefix . 'wpdo_hot_' . sanitize_key( $pt ); - try { - $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - $details[ $pt ]['table'] = $table; - $details[ $pt ]['exists'] = $exists; - if ( ! $exists ) { - $ok = false; - } - } catch ( \Throwable $e ) { - $details[ $pt ]['error'] = $e->getMessage(); - $ok = false; - } - } - return array( - 'ok' => $ok, - 'message' => $ok ? 'hivepress-memberships: hot tables OK' : 'hivepress-memberships: hot tables missing', - 'details' => $details, - ); - } - /** * 8-D self-score. * @@ -152,18 +72,6 @@ if ( ! class_exists( 'TMDO_HivePress_Memberships_Adapter' ) ) { public function suitability_score(): array { return $this->compose_score( array() ); } - - /** - * FSM modules this adapter contributes. - * - * @return array - */ - public function migrations(): array { - return array( - 'hot_hp_membership_plan', - 'hot_hp_membership', - ); - } } } // end if ( ! class_exists ) diff --git a/includes/hivepress/adapters/class-tmdo-hivepress-requests-adapter.php b/includes/hivepress/adapters/class-tmdo-hivepress-requests-adapter.php index 4ef0b7f..903ac8e 100644 --- a/includes/hivepress/adapters/class-tmdo-hivepress-requests-adapter.php +++ b/includes/hivepress/adapters/class-tmdo-hivepress-requests-adapter.php @@ -5,8 +5,8 @@ * Two related models: * * - Request (post_type=hp_request) - * hot fields: hp_drafted (bool), hp_expired_time (unix ts) - * — same access pattern as Listing (user dashboard + expiry cron) + * hp_drafted / hp_expired_time are flattened by the `hp_draft_flags` + * and `hp_request_core` entity groups (TMDO_HivePress_Entity_Groups). * * - Offer (comment_type=hp_offer, parent post_type=hp_request) * stored in comment columns only — no comment meta to flatten. @@ -59,74 +59,6 @@ if ( ! class_exists( 'TMDO_HivePress_Requests_Adapter' ) ) { public function minimum_addon_version(): string { return '1.2.0'; } - - /** - * Register hot fields for hp_request post type. - * - * @param TMDO_Schema_Registry $registry Schema registry singleton. - */ - public function on_register_fields( TMDO_Schema_Registry $registry ): void { - $registry->register_many( - $this->plugin_slug(), - array( - array( - 'post_type' => 'hp_request', - 'meta_key' => 'hp_drafted', - 'zone' => 'hot', - 'data_type' => 'tinyint(1) NOT NULL DEFAULT 0', - 'column' => 'hp_drafted', - 'indexed' => true, - ), - array( - 'post_type' => 'hp_request', - 'meta_key' => 'hp_expired_time', - 'zone' => 'hot', - 'data_type' => 'bigint(20) UNSIGNED NOT NULL DEFAULT 0', - 'column' => 'hp_expired_time', - 'indexed' => true, - ), - ) - ); - } - - /** - * Doctor probe — request hot table. - * - * @return array{ok:bool, message:string, details?:array} - */ - public function doctor_check(): array { - global $wpdb; - if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) { - return array( - 'ok' => false, - 'message' => 'wpdb global not available', - ); - } - $table = $wpdb->prefix . 'wpdo_hot_hp_request'; - try { - $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); - return array( - 'ok' => $exists, - 'message' => $exists - ? sprintf( 'hivepress-requests: %s present', $table ) - : sprintf( 'hivepress-requests: %s missing', $table ), - ); - } catch ( \Throwable $e ) { - return array( - 'ok' => false, - 'message' => sprintf( 'hivepress-requests doctor failed: %s', $e->getMessage() ), - ); - } - } - - /** - * FSM modules this adapter contributes. - * - * @return array - */ - public function migrations(): array { - return array( 'hot_hp_request' ); - } } } // end if ( ! class_exists ) diff --git a/tests/unit/BookingsAdapterTest.php b/tests/unit/BookingsAdapterTest.php index b0bd684..20a62ca 100644 --- a/tests/unit/BookingsAdapterTest.php +++ b/tests/unit/BookingsAdapterTest.php @@ -33,31 +33,6 @@ class HivePressBookingsAdapterTest extends TestCase { $this->assertSame( 'HivePress\\Bookings\\Plugin', $this->adapter->detection_class() ); } - public function test_on_register_fields_reserves_booking_hot_columns(): void { - $registry = WPDO_Schema_Registry::instance(); - $this->adapter->on_register_fields( $registry ); - - $start = $registry->get_field( 'hp_booking', 'hp_start_time' ); - $this->assertSame( 'hot', $start['zone'] ); - $this->assertTrue( (bool) $start['indexed'] ); - - $end = $registry->get_field( 'hp_booking', 'hp_end_time' ); - $this->assertSame( 'hot', $end['zone'] ); - $this->assertTrue( (bool) $end['indexed'] ); - } - - public function test_on_register_fields_uses_real_booking_meta_keys(): void { - $registry = WPDO_Schema_Registry::instance(); - $this->adapter->on_register_fields( $registry ); - - // hivepress-bookings has no guests field; places are stored as hp_quantity. - $this->assertNull( $registry->get_field( 'hp_booking', 'hp_guests' ) ); - - $quantity = $registry->get_field( 'hp_booking', 'hp_quantity' ); - $this->assertNotNull( $quantity ); - $this->assertSame( 'hot', $quantity['zone'] ); - } - public function test_score_reflects_unverified_perf_dimension(): void { $score = $this->adapter->suitability_score(); // D8 reduced to 0.9 because schema is best-effort until addon installs. @@ -65,8 +40,4 @@ class HivePressBookingsAdapterTest extends TestCase { // Aggregate = (7 × 1.0 + 0.9) / 8 × 10 = 9.875 → 9.88. $this->assertSame( 9.88, $score['aggregate'] ); } - - public function test_migrations_lists_booking_module(): void { - $this->assertSame( array( 'hot_hp_booking' ), $this->adapter->migrations() ); - } } diff --git a/tests/unit/CoreAdapterTest.php b/tests/unit/CoreAdapterTest.php index 6af0e24..5e1dc03 100644 --- a/tests/unit/CoreAdapterTest.php +++ b/tests/unit/CoreAdapterTest.php @@ -46,29 +46,10 @@ class HivePressCoreAdapterTest extends TestCase { $this->assertSame( '1.7.0', $this->adapter->minimum_addon_version() ); } - public function test_migrations_returns_listing_and_vendor_modules(): void { - $mods = $this->adapter->migrations(); - $this->assertContains( 'hot_hp_listing', $mods ); - $this->assertContains( 'hot_hp_vendor', $mods ); - } - public function test_on_register_fields_populates_schema_registry(): void { $registry = WPDO_Schema_Registry::instance(); $this->adapter->on_register_fields( $registry ); - // Listing hot fields. - $drafted = $registry->get_field( 'hp_listing', 'hp_drafted' ); - $this->assertIsArray( $drafted ); - $this->assertSame( 'hot', $drafted['zone'] ); - $this->assertTrue( (bool) $drafted['indexed'] ); - - $expired = $registry->get_field( 'hp_listing', 'hp_expired_time' ); - $this->assertSame( 'hot', $expired['zone'] ); - $this->assertStringContainsString( 'bigint', $expired['data_type'] ); - - $featured_time = $registry->get_field( 'hp_listing', 'hp_featured_time' ); - $this->assertSame( 'hot', $featured_time['zone'] ); - // Vendor cold blob. $vendor_image = $registry->get_field( 'hp_vendor', 'hp_image' ); $this->assertIsArray( $vendor_image ); @@ -87,16 +68,6 @@ class HivePressCoreAdapterTest extends TestCase { } } - public function test_doctor_check_reports_table_status(): void { - $result = $this->adapter->doctor_check(); - $this->assertIsArray( $result ); - $this->assertArrayHasKey( 'ok', $result ); - $this->assertArrayHasKey( 'message', $result ); - $this->assertArrayHasKey( 'details', $result ); - // The stub $wpdb returns null for SHOW TABLES, so tables appear missing → ok=false. - $this->assertFalse( $result['ok'] ); - } - public function test_optimize_query_extracts_hot_clauses(): void { $registry = WPDO_Schema_Registry::instance(); $this->adapter->on_register_fields( $registry ); diff --git a/tests/unit/MarketplaceAdapterTest.php b/tests/unit/MarketplaceAdapterTest.php index 55e93de..bd7248d 100644 --- a/tests/unit/MarketplaceAdapterTest.php +++ b/tests/unit/MarketplaceAdapterTest.php @@ -33,21 +33,6 @@ class HivePressMarketplaceAdapterTest extends TestCase { $this->assertSame( 'HivePress\\Marketplace\\Plugin', $this->adapter->detection_class() ); } - public function test_on_register_fields_registers_hot_hp_price(): void { - $registry = WPDO_Schema_Registry::instance(); - $this->adapter->on_register_fields( $registry ); - - // Marketplace's filterable, sortable listing "price" attribute is stored as hp_price. - $price = $registry->get_field( 'hp_listing', 'hp_price' ); - $this->assertSame( 'hot', $price['zone'] ); - $this->assertTrue( (bool) $price['indexed'] ); - $this->assertStringContainsString( 'decimal', $price['data_type'] ); - - // Neither key exists in hivepress-marketplace. - $this->assertNull( $registry->get_field( 'hp_listing', 'hp_purchase_price' ) ); - $this->assertNull( $registry->get_field( 'hp_listing', 'hp_purchase_count' ) ); - } - public function test_score_aggregates_to_nine_eight_eight(): void { // D8 = 0.9 → aggregate = (7×1.0 + 0.9)/8 × 10 = 9.875 → 9.88. $this->assertSame( 9.88, $this->adapter->suitability_score()['aggregate'] ); diff --git a/tests/unit/MembershipsAdapterTest.php b/tests/unit/MembershipsAdapterTest.php index fe039a1..8a6e038 100644 --- a/tests/unit/MembershipsAdapterTest.php +++ b/tests/unit/MembershipsAdapterTest.php @@ -33,41 +33,8 @@ class HivePressMembershipsAdapterTest extends TestCase { $this->assertSame( '2.0.0', $this->adapter->minimum_addon_version() ); } - public function test_on_register_fields_registers_three_hot_fields(): void { - $registry = WPDO_Schema_Registry::instance(); - $this->adapter->on_register_fields( $registry ); - - // Plan fields. - $expire_period = $registry->get_field( 'hp_membership_plan', 'hp_expire_period' ); - $this->assertSame( 'hot', $expire_period['zone'] ); - $this->assertStringContainsString( 'int', $expire_period['data_type'] ); - - $primary = $registry->get_field( 'hp_membership_plan', 'hp_primary' ); - $this->assertSame( 'hot', $primary['zone'] ); - $this->assertTrue( (bool) $primary['indexed'] ); - - // Membership field — the hottest path. - $expired = $registry->get_field( 'hp_membership', 'hp_expired_time' ); - $this->assertSame( 'hot', $expired['zone'] ); - $this->assertTrue( (bool) $expired['indexed'] ); - $this->assertStringContainsString( 'bigint', $expired['data_type'] ); - } - - public function test_doctor_check_reports_missing_tables_in_stub_env(): void { - $result = $this->adapter->doctor_check(); - $this->assertFalse( $result['ok'] ); - $this->assertArrayHasKey( 'hp_membership_plan', $result['details'] ); - $this->assertArrayHasKey( 'hp_membership', $result['details'] ); - } - public function test_score_aggregates_to_perfect_ten(): void { $score = $this->adapter->suitability_score(); $this->assertSame( 10.0, $score['aggregate'] ); } - - public function test_migrations_lists_both_hot_modules(): void { - $mods = $this->adapter->migrations(); - $this->assertContains( 'hot_hp_membership_plan', $mods ); - $this->assertContains( 'hot_hp_membership', $mods ); - } } diff --git a/tests/unit/NoHotZoneRegistrationTest.php b/tests/unit/NoHotZoneRegistrationTest.php new file mode 100644 index 0000000..3f6d2d1 --- /dev/null +++ b/tests/unit/NoHotZoneRegistrationTest.php @@ -0,0 +1,51 @@ +getProperty( 'instance' ); + $prop->setAccessible( true ); + $prop->setValue( null, null ); + } + + public function test_no_adapter_registers_hot_fields(): void { + $registry = TMDO_Schema_Registry::instance(); + foreach ( self::ADAPTERS as $class ) { + ( new $class() )->on_register_fields( $registry ); + } + + $this->assertSame( array(), $registry->get_zone_fields( 'hot' ) ); + $this->assertSame( 'cold', $registry->get_field_zone( 'hp_listing', 'hp_images' ), 'cold fields stay' ); + } + + public function test_no_adapter_contributes_hot_modules(): void { + foreach ( self::ADAPTERS as $class ) { + $this->assertSame( array(), ( new $class() )->migrations(), $class ); + } + } +} diff --git a/tests/unit/RequestsAdapterTest.php b/tests/unit/RequestsAdapterTest.php index 0c850b1..d7cf176 100644 --- a/tests/unit/RequestsAdapterTest.php +++ b/tests/unit/RequestsAdapterTest.php @@ -35,19 +35,6 @@ class HivePressRequestsAdapterTest extends TestCase { $this->assertSame( 'hp_offer', WPDO_HivePress_Requests_Adapter::OFFER_COMMENT_TYPE ); } - public function test_on_register_fields_registers_request_hot_columns(): void { - $registry = WPDO_Schema_Registry::instance(); - $this->adapter->on_register_fields( $registry ); - - $drafted = $registry->get_field( 'hp_request', 'hp_drafted' ); - $this->assertSame( 'hot', $drafted['zone'] ); - $this->assertTrue( (bool) $drafted['indexed'] ); - - $expired = $registry->get_field( 'hp_request', 'hp_expired_time' ); - $this->assertSame( 'hot', $expired['zone'] ); - $this->assertTrue( (bool) $expired['indexed'] ); - } - public function test_registers_no_offer_shadow_table(): void { // The offer shadow table was never created, so every mirror write failed. $registry = WPDO_Custom_Table_Registry::instance(); @@ -69,8 +56,4 @@ class HivePressRequestsAdapterTest extends TestCase { public function test_score_aggregates_to_perfect_ten(): void { $this->assertSame( 10.0, $this->adapter->suitability_score()['aggregate'] ); } - - public function test_migrations_lists_only_request_hot_module(): void { - $this->assertSame( array( 'hot_hp_request' ), $this->adapter->migrations() ); - } }