diff --git a/2meet-data-optimizer.php b/2meet-data-optimizer.php index f3de8ff..1d11b45 100644 --- a/2meet-data-optimizer.php +++ b/2meet-data-optimizer.php @@ -3,7 +3,7 @@ * Plugin Name: 2meet Data Optimizer * Plugin URI: https://2meet.io/2meet-data-optimizer * Description: 通用 WordPress 反 EAV 引擎:將 postmeta / usermeta / termmeta / commentmeta 自動分流至四象限扁平表(Hot / Warm / Cold / Archive),大幅提升搜尋與篩選效能。從 wp-data-optimizer v2.16.0 提煉的純核心,整合層交給 11 個 AddOn。 - * Version: 1.1.1 + * Version: 1.2.0 * Requires at least: 6.0 * Tested up to: 6.9.4 * Requires PHP: 8.1 @@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) { } // ── Constants ────────────────────────────────────────────────────────────── -define( 'TMDO_VERSION', '1.1.1' ); +define( 'TMDO_VERSION', '1.2.0' ); define( 'TMDO_DB_VERSION', '2.1.0' ); define( 'TMDO_PATH', plugin_dir_path( __FILE__ ) ); define( 'TMDO_URL', plugin_dir_url( __FILE__ ) ); diff --git a/CHANGELOG.md b/CHANGELOG.md index 472e8d2..dc94987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,80 @@ Versioning follows [Semantic Versioning](https://semver.org/). --- +## [1.2.0] — 2026-09-27 — 漂移偵測、範圍收斂、CLI 與診斷(hardening 2.2) + +**性質**:功能 + 資料正確性修正。無 schema 變更。延續 ADR-004 的搜尋索引模式;搭配 +HivePress AddOn 0.3.0、WooCommerce AddOn 0.2.0。 + +### Added + +- **`wp tmdo drift-check [--group] [--sample=] [--format]`**(`TMDO_Drift_Check`): + 逐 key 比對扁平表與原生 meta 的值(第一筆原生 row → 安全 unserialize → + `Type_Caster::to_db`),回報 miss / diff / extra。序列化 vs JSON、`50` vs `50.000000` + 不算漂移;超過 255 字元存 NULL 的 text 不算 miss。結果寫入 `wpdo_drift_status`。(PR-070) +- **`drift-check --fix`**:刪除已刪實體與非本 group post type 的扁平列,其餘漂移列經 + `TMDO_Entity_Migration_Engine::migrate_entity()` 從原生重建,清除列快取後再檢查一次。 + 不寫原生;aeav_only(扁平才是真實資料)一律拒絕。(PR-071、PR-073b) +- **每日抽樣**:`wpdo_daily_health_check` 對 dual_write 以上、非 aeav_only 的 entity 每個 + group 抽 500 筆,結果取代既有紀錄。(PR-073) +- **Hook Bus 關閉期間的寫入補回**:entity 從 disabled 進入 dual_write / shadow_read,或 + Hook Bus flag 由關轉開時,60 秒後以單次 WP-Cron `tmdo_drift_resync` 執行全量 + `drift-check --fix`(不做 `post_modified_gmt` 增量:WooCommerce 直接 SQL 改庫存不會更新 + post_modified)。停用 / 解除安裝時清除排程。(PR-074) +- **`wp tmdo bridge-mode `**:經 `TMDO_Mode_Manager::set()`,受 + Promotion Guard 約束;被拒時 exit 1、模式不變。(PR-069) +- **`wp tmdo member-backfill --all`**:回填所有已註冊的 user group。(PR-068) +- **`wp tmdo cleanup-hp-transients --options [--dry-run|--confirm] [--batch=]`**: + 刪除 HivePress AddOn 0.3.0 之前寫進 wp_options 的 `_transient_wpdo_hp_pm_*`(含 timeout), + 分批刪除(預設 5000),AddOn 未啟用也可執行。dev36 有 2,314,750 列。(PR-080) + +### Changed + +- **Post query router 避開漂移中的 group**:上次 drift-check 有 miss / diff / extra 的 + group 不路由,整個查詢維持原生(全有或全無);從未檢查的 group 照常路由;aeav_only + 不受影響。(PR-072、PR-073b) +- **純排序搜尋 flat-first**:沒有路由篩選的 HivePress `_sort` 搜尋,在該 post type 每篇都有 + 扁平列時改由扁平表驅動(`STRAIGHT_JOIN` + INNER JOIN);Router 在 `wp_insert_post` + 替新文章建空列,覆蓋率以 anti-join 檢查並快取 15 分鐘(隨 post 模式變更失效)。 + flat-first 且扁平表是唯一 JOIN 時拿掉 WordPress 為 tax query 加的 `GROUP BY posts.ID`。 + dev36 `rating__desc`:原生約 3.3 s、1.1.0 約 1.5–1.9 s、現在約 0.51–0.85 s。(PR-079) +- **Auto-promoter** 的第三個條件改為 `TMDO_Drift_Check::clean_since()`:2 天內每個 group 都 + 檢查過(抽樣或全量)且沒有漂移;沒有紀錄就不升級。(PR-075) +- **shadow_read diff 節流**:每個 entity type + key 每小時最多記 20 列(疊加既有的 + 每物件 5 分鐘視窗)。(PR-075) +- **回填 group 改讀 registry**:admin「全部回填」、`post-migrate-group`、`member-backfill`、 + `member-audit` 取所有已註冊 group(排除已還原的孤兒 group),不再是寫死清單;含 json + 欄位的 group(attachment、nav_menu_item)逐列經 migration engine。(PR-067、PR-068) +- **不再註冊 `post/wc_product`**:WooCommerce 直接讀 product meta、以 SQL 寫庫存,扁平副本 + 必然漂移(ADR-004 決策 2)。既有的 `wp_wpdo_post_wc_product` 表保留,以 + `wp tmdo bridge-restore-native post --group=wc_product --retire` 封存;解除安裝仍會刪除。(PR-077) +- `member-sso-sync`、`member-force-logout` 標示為 deprecated(`sso` group 已不註冊)。(PR-069) + +### Fixed + +- Entity 回填替沒有原生 row 的 key 寫入 DDL 預設值(`session_count` / `account_flags` = 0), + 每個 user 都被捏造出 0;現在寫 NULL,required NOT NULL 欄位保留預設值。(PR-066) +- `member-cutover` 在轉換被拒時 fatal(把 `WP_Error` 當陣列)。(PR-068) +- `doctor` 依 registry 檢查 entity 表(不再因從未註冊的 `sso` 表而每站失敗),並顯示 + Promotion Guard 違規與 drift 狀態;`term-comment-diagnose` 依各 filter 的 `is_enabled()` + 顯示狀態;`bridge-status` 只計有值的扁平列、eav_ids 限定 group 的 post type + (dev36 wp_core 15306% → 100%)。(PR-076) +- 2.1 稽核:公開 REST route 註明公開理由;admin 23 處三元 echo 依情境 `esc_attr` / + `esc_html`;migration engine 與 health cron 的例外改記 `TMDO_Logger`。(PR-078) + +### Docs + +- ADR-004「設計(Release 2.2)」、`CONTEXT.md` Entity Bridge 的漂移段落。(PR-086) + +### 升級後 + +- HivePress 站:部署 AddOn 0.3.0 後執行 `wp tmdo cleanup-hp-transients --options --confirm`。 +- 曾註冊 wc_product 的站:`wp tmdo bridge-restore-native post --group=wc_product --retire`。 +- 已進入 dual_write 以上的站:`wp tmdo drift-check ` 建立第一筆紀錄(未檢查的 + group 照常路由)。 + +--- + ## [1.1.1] — 2026-09-26 — Security:公開 REST 不再回傳私有欄位 **性質**:安全修正(Critical)。1.0.x 起即存在;1.1.0 zip 已撤回(`dist/withdrawn/`)。 diff --git a/CONTEXT.md b/CONTEXT.md index 567d15a..9deac33 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -158,6 +158,13 @@ semantics, LIKE as `%esc_like%`. Meta ordering (the HivePress `_sort` NOT EXISTS shape, a named clause, `meta_key` + `meta_value_num`) is routed only as the sole `orderby` on a column that orders like native. +**Drift** (v1.2.0). `TMDO_Drift_Check` compares flat values with native meta per key +(miss / diff / extra) and records the outcome per group in `wpdo_drift_status`; +`--fix` rebuilds flat rows from native. The router routes no clause of a group recorded +as drifting (never checked → routes). A daily sample, and a full fix 60 s after an +entity leaves `disabled` or the Hook Bus is switched back on (`tmdo_drift_resync`), keep +the record current. None of this applies in `aeav_only`, where flat is the source of truth. + --- ## Migration Phase (Strategy pattern) diff --git a/docs/adr-004-search-index-mode-for-hp-wc.md b/docs/adr-004-search-index-mode-for-hp-wc.md index 3aaad77..0885658 100644 --- a/docs/adr-004-search-index-mode-for-hp-wc.md +++ b/docs/adr-004-search-index-mode-for-hp-wc.md @@ -77,6 +77,26 @@ HivePress AddOn 0.2.0 在 post router 啟用時不再綁定舊的 hot-zone `optimize_query` / `optimize_search`。 +## 設計(Release 2.2) + +### 範圍收斂 + +- core 不再註冊 `post/wc_product`;WooCommerce AddOn 0.2.0 不再註冊 product / order / subscription 的 zone 欄位與 user `hot` 群組。WooCommerce 的 meta 只存在原生。既有表以 `bridge-restore-native post --group=wc_product --retire` 封存。 + +### 漂移偵測與修復(`TMDO_Drift_Check`) + +- `wp tmdo drift-check ` 以值比對扁平與原生(第一筆原生 row → 安全 unserialize → `Type_Caster::to_db`),逐 key 回報 miss / diff / extra,結果寫入 `wpdo_drift_status`。 +- `--fix` 從原生重建漂移列,刪除已刪實體與非本 group post type 的列,不寫原生。 +- **aeav_only 不適用**:那時扁平才是真實資料,原生本來就過期。`--fix` 拒絕、每日抽樣略過、Router 不因漂移退回原生。 +- **Router**:上次檢查有漂移的 group 不路由,整個查詢維持原生;從未檢查的 group 照常路由(決策 2026-09-27:升級後不變慢)。 +- **每日抽樣**:`wpdo_daily_health_check`,每 group 500 筆。 +- **Hook Bus 關閉期間的寫入**:entity 從 disabled 進入 dual_write / shadow_read、或 Hook Bus flag 由關轉開時,60 秒後 `tmdo_drift_resync` 執行全量 `--fix`。不用 `post_modified_gmt` 增量,因為 WooCommerce 直接 SQL 寫 meta 不會更新它。 +- **Auto-promoter** 以 `clean_since()`(2 天內乾淨的檢查)取代 shadow diff 數量;Promotion Guard 仍擋 HP / WC 站。 + +### 查詢 + +- 純排序搜尋(HivePress `_sort`、沒有路由篩選)在該 post type 每篇都有扁平列時改由扁平表驅動;Router 在 `wp_insert_post` 替新文章建空列,覆蓋率快取 15 分鐘。 + ## 後果 **好處** @@ -89,7 +109,7 @@ HivePress AddOn 0.2.0 在 post router 啟用時不再綁定舊的 hot-zone `opti - **寫入開銷**:dual_write / shadow_read 每次 meta 寫入多一次原生讀取與一次扁平寫入。dev36 實測 HP listing 整條寫入鏈 +29%、WC product +127%(後者在 2.2 移除 WC 扁平化後消失)。 - **沒有讀取加速**:帶 key 的單筆讀取仍走原生(WordPress meta cache)。 -- **漂移**:繞過 meta API 的寫入(直接 SQL、Hook Bus 關閉期間的匯入)會讓扁平表過期,Router 會回傳過期的篩選結果。2.2 的 `drift-check` 與重新啟用時的增量 backfill(E01 / E02)處理;在那之前,大量匯入後要重新 backfill。 +- **漂移**:繞過 meta API 的寫入(直接 SQL、Hook Bus 關閉期間的匯入)會讓扁平表過期,Router 會回傳過期的篩選結果。2.2 起由 `drift-check`(每日抽樣、Router 避開漂移中的 group)與重新啟用時的全量 `--fix` 處理;兩次抽樣之間的漂移仍可能短暫回傳過期結果,大量匯入後可手動執行 `drift-check --fix`。 - **decimal 欄位搭配 NUMERIC / DECIMAL**:原生先 CAST 成整數(`DECIMAL` 預設 (10,0)),扁平表精確比較,因此帶小數的值在區間邊界上可能不同。dev36 資料沒有這種值。 - **text 欄位超過 255 字元**存 NULL,這類值的篩選結果會與原生不同(有 `text_overflow` 記錄)。 - HivePress 搜尋 p50 為 321–866 ms(原生 600–1,713 ms),尚未達到 R-2.1 的 300 ms 目標(驗收:`notes/tmdo-acceptance-2.1-2026-09-26.md`)。價格篩選頁的 SQL 只剩約 75 ms、其餘為頁面渲染;純排序(如 `rating__desc`)必須 LEFT JOIN 保留沒有值的文章,無法 flat-first。 diff --git a/readme.txt b/readme.txt index 4abaa85..7cb6d7b 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: performance, database, postmeta, optimization, hivepress Requires at least: 6.0 Tested up to: 6.9 Requires PHP: 8.1 -Stable tag: 1.1.1 +Stable tag: 1.2.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -107,6 +107,18 @@ The Classifier tab analyzes your existing `wp_postmeta` data (access frequency, == Changelog == += 1.2.0 = +* `wp tmdo drift-check [--group] [--sample] [--fix]`: reports flat-vs-native drift per key (miss / diff / extra) and rebuilds drifted flat rows from native; native meta is never written, and aeav_only sites are refused +* The post query router keeps a query native while one of its groups is recorded as drifting; a daily sample (500 entities per group) on the existing health-check event keeps the record current +* Switching an entity from disabled to dual_write / shadow_read, or turning the Hook Bus back on, schedules a full drift fix 60 s later (`tmdo_drift_resync`) +* Auto-promotion requires a clean drift check within 2 days; shadow_read diffs are capped at 20 per key per hour +* `wp tmdo bridge-mode ` (under the Promotion Guard); `member-backfill --all`; post / user backfill groups come from the registry, json groups included +* WooCommerce: the `post/wc_product` group is no longer registered — product meta stays native (retire existing tables with `bridge-restore-native post --group=wc_product --retire`) +* Sort-only searches drive from the flat table when every post of the type has a flat row (HivePress rating sort ~3.3 s → ~0.5–0.85 s on 100k listings) +* `wp tmdo cleanup-hp-transients --options` removes HivePress AddOn transients left in wp_options +* Fixed: entity backfill invented default values (0) for keys with no native row; doctor, term-comment-diagnose and bridge-status false alarms; member-cutover fatal on a refused transition +* Admin output escaping and logging clean-ups from the 2.1 audit + = 1.1.1 = * Security: the public GET /wpdo/v1/listings endpoints returned every registered field (show_in_rest defaulted to true), including private values such as integration tokens and WooCommerce purchase notes. Fields are now private unless registered with show_in_rest => true, and protected (underscore) or credential-like keys are never exposed * Security: the listings endpoint no longer returns whole rows when no field is public, and only filters / sorts on public columns