3 Commits

Author SHA1 Message Date
wpdev 655ab88e86 docs(2meet-data-optimizer-woocommerce-addon): 0.2.0 release notes
Tests / PHP Lint (pull_request) Successful in 11s
Tests / Unit Tests (pull_request) Successful in 1m30s
Tests / Integration Tests (pull_request) Successful in 1m27s
- Bump to 0.2.0; CHANGELOG entry for PR-085 with the upgrade path for the user hot table
- Verify: composer test, integration
- PR: PR-088
2026-09-27 09:55:38 +08:00
wpdev c3c4e1de44 refactor(2meet-data-optimizer-woocommerce-addon): drop the product/order hot fields and the user hot group
- Stop registering WC product/order/subscription zone fields and the user hot group; WC reads its meta directly and writes stock by SQL, so a flat copy drifts
- Keep the custom tables registration, the HPOS notice and the commissions
- Verify: composer test / test:integration; dev36 customer save, order stock reduce/restore with no DB errors
- PR: PR-085
2026-09-27 09:48:33 +08:00
wpdev c76e474cbc Merge pull request 'release 0.1.1: 2meet-data-optimizer-woocommerce-addon' (#2) from release/0.1.1 into main
Tests / PHP Lint (push) Successful in 13s
Tests / Integration Tests (push) Failing after 1m29s
Tests / Unit Tests (push) Successful in 1m30s
2026-09-26 21:48:32 +08:00
4 changed files with 56 additions and 333 deletions
+2 -2
View File
@@ -3,7 +3,7 @@
* Plugin Name: 2meet Data Optimizer — Woocommerce AddOn
* Plugin URI: https://2meet.io/2meet-data-optimizer/woocommerce
* Description: WooCommerce 整合:20 表 schema 註冊 + commission tracking (wp_wpdo_wc_commissions) + product/order zone + HPOS notice
* Version: 0.1.1
* Version: 0.2.0
* Requires at least: 6.0
* Tested up to: 6.9.4
* Requires PHP: 8.1
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'TMDO_WOOCOMMERCE_VERSION', '0.1.1' );
define( 'TMDO_WOOCOMMERCE_VERSION', '0.2.0' );
define( 'TMDO_WOOCOMMERCE_PATH', plugin_dir_path( __FILE__ ) );
define( 'TMDO_WOOCOMMERCE_URL', plugin_dir_url( __FILE__ ) );
define( 'TMDO_WOOCOMMERCE_FILE', __FILE__ );
+30
View File
@@ -11,6 +11,36 @@ Versioning follows [Semantic Versioning](https://semver.org/).
---
## [0.2.0] — 2026-09-27
Released with 2meet-data-optimizer 1.2.0 (hardening 2.2, search-index mode
decision 2: WooCommerce meta stays native).
### Removed
- WooCommerce meta is no longer copied to flat tables. WooCommerce reads its
meta directly and writes stock / sales by SQL, so a flat copy drifted
(R-2.1: stock and `total_sales` out of step after an order). No longer
registered:
- product and order zone fields (`wpdo_register_fields`), including the
subscription fields;
- the `user` / `hot` entity group (`wpdo_register_entity_fields`),
including the three fields added in 0.1.1.
- Kept: the `wp_wc_*` custom table registry, the HPOS notice, the
Marketplace commission tracking and the term count filter.
### Upgrading
- Nothing is dropped. With the user entity below `aeav_only`, WooCommerce keeps
reading native user meta and the `wp_wpdo_user_hot` table is simply no
longer written. In `aeav_only`, core restores the group as an orphan so its
values stay readable; `wp tmdo bridge-restore-native user --group=hot --retire`
writes them back to native and archives the table.
- Product flat data (core's former `post/wc_product` group): see
2meet-data-optimizer 1.2.0.
---
## [0.1.1] — 2026-09-26
Hardening 2.0 changes, released alongside 2meet-data-optimizer 1.1.0.
+6 -331
View File
@@ -6,10 +6,9 @@
* `wp_wc_product_meta_lookup` for product search). WPDO's job is to:
* 1. Register the 20 `wp_wc_*` tables to Custom_Table_Registry
* (so doctor / conflict-scan know about them)
* 2. Add hot-zone fields to Schema_Registry for the legacy postmeta keys
* WC still uses on products / orders (when HPOS off)
* 3. Detect HPOS state and surface it to admin
* 4. NOT duplicate what WC already does (no shadow lookup tables)
* 2. Detect HPOS state and surface it to admin
* 3. NOT duplicate what WC already does (no shadow lookup tables, and since
* 0.2.0 no flat copy of product / order / customer meta)
*
* This is "Tier 5" in ENTITY_ADAPTER_COOKBOOK.md: integrating with a plugin
* that has its own anti-EAV solution.
@@ -171,11 +170,9 @@ final class TMDO_WooCommerce {
}
add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ) );
add_action( 'wpdo_register_fields', array( __CLASS__, 'register_schema_fields' ) );
// v2.1.6: customer hot fields (_money_spent / _order_count / _last_order) move
// to Entity_Registry so Hook Bus can route them to wp_wpdo_user_hot once
// `entity_user` mode advances past `disabled`. See PLAN-entity-user-cutover.md.
add_action( 'wpdo_register_entity_fields', array( __CLASS__, 'register_user_entity_fields' ) );
// 0.2.0: no product / order / subscription zone fields and no user hot group.
// WooCommerce reads its meta directly and writes stock by SQL, so a flat
// copy drifts; its meta stays native only (core 1.2.0 dropped wc_product too).
// HPOS recommendation notice — only on WC admin pages, only when:
// (a) HPOS is OFF, (b) the threshold of legacy orders has been crossed.
@@ -339,328 +336,6 @@ final class TMDO_WooCommerce {
);
}
/**
* Register hot-zone meta fields for products + orders + customers.
*
* Delegates to dedicated registrars to keep this method readable.
*
* @param mixed $schema TMDO_Schema_Registry (passed by action).
* @return void
*/
public static function register_schema_fields( $schema = null ): void {
if ( ! class_exists( 'TMDO_Schema_Registry' ) ) {
return;
}
$schema = $schema ?: TMDO_Schema_Registry::instance();
self::register_product_fields( $schema );
// Order fields only matter when HPOS is OFF (otherwise WC stores them in wc_orders_meta).
if ( ! self::is_hpos_enabled() ) {
self::register_order_fields( $schema );
}
// v2.1.6: Customer usermeta hot fields moved out of Schema_Registry — they
// are now registered via `register_user_entity_fields()` on the
// `wpdo_register_entity_fields` action so Hook Bus can intercept them.
// Schema_Registry never had a user-meta interceptor; the previous
// registration was inert. See PLAN-entity-user-cutover.md.
// Subscription fields if WC Subscriptions OR wc-linepay-subscription is active.
if ( self::has_subscriptions() ) {
self::register_subscription_fields( $schema );
}
}
/**
* Product hot fields (search/filter shop pages).
*
* @param TMDO_Schema_Registry $schema Registry.
* @return void
*/
private static function register_product_fields( $schema ): void {
$schema->register_many(
self::PROVIDER,
array(
array(
'post_type' => 'product',
'meta_key' => '_price',
'zone' => 'hot',
'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0',
'column' => '_price',
'indexed' => true,
),
array(
'post_type' => 'product',
'meta_key' => '_regular_price',
'zone' => 'hot',
'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0',
'column' => '_regular_price',
),
array(
'post_type' => 'product',
'meta_key' => '_sale_price',
'zone' => 'hot',
'data_type' => 'decimal(15,4) NULL',
'column' => '_sale_price',
),
array(
'post_type' => 'product',
'meta_key' => '_sku',
'zone' => 'hot',
'data_type' => "varchar(100) NOT NULL DEFAULT ''",
'column' => '_sku',
'indexed' => true,
),
array(
'post_type' => 'product',
'meta_key' => '_stock',
'zone' => 'hot',
'data_type' => 'decimal(15,4) NULL',
'column' => '_stock',
),
array(
'post_type' => 'product',
'meta_key' => '_stock_status',
'zone' => 'hot',
'data_type' => "varchar(20) NOT NULL DEFAULT 'instock'",
'column' => '_stock_status',
'indexed' => true,
),
array(
'post_type' => 'product',
'meta_key' => '_visibility',
'zone' => 'hot',
'data_type' => "varchar(20) NOT NULL DEFAULT 'visible'",
'column' => '_visibility',
),
array(
'post_type' => 'product',
'meta_key' => '_featured',
'zone' => 'hot',
'data_type' => "varchar(3) NOT NULL DEFAULT 'no'",
'column' => '_featured',
'indexed' => true,
),
array(
'post_type' => 'product',
'meta_key' => '_tax_status',
'zone' => 'hot',
'data_type' => "varchar(20) NOT NULL DEFAULT 'taxable'",
'column' => '_tax_status',
),
array(
'post_type' => 'product',
'meta_key' => '_virtual',
'zone' => 'hot',
'data_type' => "varchar(3) NOT NULL DEFAULT 'no'",
'column' => '_virtual',
),
array(
'post_type' => 'product',
'meta_key' => '_downloadable',
'zone' => 'hot',
'data_type' => "varchar(3) NOT NULL DEFAULT 'no'",
'column' => '_downloadable',
),
)
);
// Cold zone — large display fields rarely searched but always shown.
$schema->register_many(
self::PROVIDER,
array(
array(
'post_type' => 'product',
'meta_key' => '_product_attributes',
'zone' => 'cold',
'cache_group' => 'wpdo_cold_product',
'cache_ttl' => HOUR_IN_SECONDS,
),
array(
'post_type' => 'product',
'meta_key' => '_purchase_note',
'zone' => 'cold',
'cache_group' => 'wpdo_cold_product',
'cache_ttl' => HOUR_IN_SECONDS,
),
)
);
}
/**
* Order hot fields (legacy postmeta path; only registered when HPOS off).
*
* @param TMDO_Schema_Registry $schema Registry.
* @return void
*/
private static function register_order_fields( $schema ): void {
$schema->register_many(
self::PROVIDER,
array(
array(
'post_type' => 'shop_order',
'meta_key' => '_order_total',
'zone' => 'hot',
'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0',
'column' => '_order_total',
'indexed' => true,
),
array(
'post_type' => 'shop_order',
'meta_key' => '_order_currency',
'zone' => 'hot',
'data_type' => "varchar(8) NOT NULL DEFAULT ''",
'column' => '_order_currency',
),
array(
'post_type' => 'shop_order',
'meta_key' => '_billing_email',
'zone' => 'hot',
'data_type' => "varchar(254) NOT NULL DEFAULT ''",
'column' => '_billing_email',
'indexed' => true,
),
array(
'post_type' => 'shop_order',
'meta_key' => '_payment_method',
'zone' => 'hot',
'data_type' => "varchar(60) NOT NULL DEFAULT ''",
'column' => '_payment_method',
'indexed' => true,
),
array(
'post_type' => 'shop_order',
'meta_key' => '_customer_user',
'zone' => 'hot',
'data_type' => 'bigint(20) NOT NULL DEFAULT 0',
'column' => '_customer_user',
'indexed' => true,
),
)
);
}
/**
* Customer usermeta hot fields — registered to Entity_Registry so Hook Bus can
* route them to `wp_wpdo_user_hot` once `entity_user` mode advances past `disabled`.
*
* Replaces the v2.1.5 `register_customer_fields()` which targeted Schema_Registry
* (zone-based, no user interceptor) — that path was inert. See
* PLAN-entity-user-cutover.md for the migration playbook.
*
* Type mapping (Entity_Registry uses fixed-precision DECIMAL(18,6) / BIGINT(20),
* which is a superset of the prior `decimal(15,4)` / `bigint(20)` declarations —
* no precision loss):
* - _money_spent → decimal (DECIMAL(18,6))
* - _order_count → integer (BIGINT(20))
* - _last_order → integer (BIGINT(20))
*
* @return void
*/
public static function register_user_entity_fields(): void {
if ( ! class_exists( 'TMDO_Entity_Registry' ) ) {
return;
}
TMDO_Entity_Registry::register_group(
'user',
'hot',
array(
array(
'key' => '_money_spent',
'type' => 'decimal',
'searchable' => true,
'label' => 'WC customer cumulative spend',
),
array(
'key' => '_order_count',
'type' => 'integer',
'searchable' => true,
'label' => 'WC customer order count',
),
array(
'key' => '_last_order',
'type' => 'integer',
'label' => 'WC customer last order ID',
),
// WC runtime stats — written on order/session events, high-frequency
// on busy stores. Unregistered they land back in wp_usermeta.
array(
'key' => 'wc_last_active',
'type' => 'integer',
'label' => 'WC last customer activity timestamp (Unix)',
),
array(
// WooCommerce stores this per-site via Users::update_site_user_meta(),
// which appends the de-suffixed blog prefix ('wc_order_count' + '_wp'
// on the main site, '_wp_2' on a subsite). Compute it the same way
// rather than hardcoding the main-site form.
'key' => 'wc_order_count_' . rtrim( $GLOBALS['wpdb']->get_blog_prefix(), '_' ),
'type' => 'integer',
'label' => 'WC order count for the current site (blog-aware key)',
),
array(
'key' => 'last_update',
'type' => 'integer',
'label' => 'WC customer last profile update timestamp (Unix)',
),
)
);
}
/**
* Subscription product fields (wc-subscriptions or wc-linepay-subscription).
*
* @param TMDO_Schema_Registry $schema Registry.
* @return void
*/
private static function register_subscription_fields( $schema ): void {
$schema->register_many(
self::PROVIDER,
array(
array(
'post_type' => 'product',
'meta_key' => '_subscription_price',
'zone' => 'hot',
'data_type' => 'decimal(15,4) NULL',
'column' => '_subscription_price',
),
array(
'post_type' => 'product',
'meta_key' => '_subscription_period',
'zone' => 'hot',
'data_type' => 'varchar(20) NULL',
'column' => '_subscription_period',
),
array(
'post_type' => 'product',
'meta_key' => '_subscription_period_interval',
'zone' => 'hot',
'data_type' => 'int(11) NULL',
'column' => '_subscription_period_interval',
),
array(
'post_type' => 'product',
'meta_key' => '_subscription_length',
'zone' => 'hot',
'data_type' => 'int(11) NULL',
'column' => '_subscription_length',
),
)
);
}
/**
* Subscription extension detection — WC native or wc-linepay-subscription.
*
* @return bool
*/
private static function has_subscriptions(): bool {
return class_exists( 'WC_Subscriptions' )
|| defined( 'WCS_INIT_TIMESTAMP' )
|| file_exists( WP_PLUGIN_DIR . '/wc-linepay-subscription/wc-linepay-subscription.php' );
}
/**
* WPDO doctor probe for any wc_* table — verifies existence + reports row count.
*
+18
View File
@@ -230,4 +230,22 @@ class WooCommerceIntegrationTest extends TestCase {
$pp->setValue( $registry, $original );
}
}
// ── 2.2 PR-085: WooCommerce data stays in native meta ───────────────────
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function test_register_keeps_woocommerce_meta_out_of_the_flat_tables(): void {
eval( 'class WooCommerce {}' ); // phpcs:ignore Squiz.PHP.Eval -- WooCommerce stand-in.
$GLOBALS['_wp_filter_callbacks'] = array();
TMDO_WooCommerce::register();
$hooks = $GLOBALS['_wp_filter_callbacks'];
$this->assertArrayNotHasKey( 'wpdo_register_fields', $hooks, 'no product / order / subscription zone fields' );
$this->assertArrayNotHasKey( 'wpdo_register_entity_fields', $hooks, 'no user hot group' );
$this->assertArrayHasKey( 'wpdo_register_custom_tables', $hooks, 'WooCommerce custom tables stay registered' );
}
}