- cached counts followed posts' last_changed, which WordPress moves on any
post meta write; themes write meta on page views (on dev36 Blocksy saved a
dynamic-CSS descriptor for each listing a crawler visited), so the cache
was invalidated every few seconds and keyword/filter searches recounted
- counts now follow their own last_changed group (tmdo_posts), bumped on
clean_post_cache (post saved or deleted; hooked in every mode) and on flat
writes through the Hook Bus, plus terms' last_changed; a routed search reads
no unmanaged meta
- Verify: PostQueryRouterFoundRowsTest (unrelated meta write keeps the count,
save or flat write recounts), integration 588, unit 703, phpcs, stan;
dev36 front-end
- PR: PR-117 (2.3 acceptance)
- a deferred group whose stored definition matches again (reverted, or
upgraded by another process) goes through end_deferral() instead of staying
native for good (review F2)
- the tmdo_ensure_group_indexes handler upgrades deferred tables itself: with
ALTERNATE_WP_CRON the event runs after init in a visitor's request (F3)
- a new group table created while the entity writes to flat stays native
until the drift fix has filled it from native meta; an opted-in HivePress
attribute no longer routes searches to an empty table (F6)
- direct_read() returns native values for a deferred group (F10); the Hook
Bus ignores deferral in aeav_only, as the router does
- dbDelta failures: the definition is recorded only when every column exists
(schema.upgrade_failed otherwise), so a failed ALTER is retried and the
group is not released onto missing columns
- test bootstrap: get_metadata() reads the real meta table
- Verify: SchemaUpgradeDeferralTest (6), Hook Bus direct read, integration
587, unit 703, phpcs, stan
- PR: PR-114 (2.3 acceptance correctness review)
- ensure_group_indexes() runs the ALTER only under WP-CLI or WP-Cron (filter
tmdo_schema_ddl_allowed); web requests, including /wp-admin/ before
auth_redirect() and admin-post.php, schedule tmdo_ensure_group_indexes instead
- MySQL GET_LOCK keeps it to one process (add_option() is INSERT ... ON
DUPLICATE KEY UPDATE and cannot serve as a lock); released by MySQL if the
process dies
- the applied signature is recorded only when every ALTER succeeds; failures
log schema.index_failed and stay in pending_indexes(), which doctor reports
- new tables get their declared cidx_ indexes in CREATE TABLE
- test stub wpdb::query()/insert() return false on SQL errors like wpdb
- Verify: GroupCompositeIndexTest 7/7, unit 698, integration 576, phpcs, stan
- PR: PR-090a
- WooCommerce reads product meta directly and writes stock by SQL, so a flat copy drifted (R-2.1 V1 T4); product meta now stays in native only (plan decision 2). TMDO_Post_Fields registers wp_core, attachment and nav_menu_item. Existing wp_wpdo_post_wc_product tables are left in place and still dropped on uninstall; the orphan restore registers nothing for them while native meta holds the keys; `wp tmdo bridge-restore-native post --group=wc_product --retire` archives them.
- Tests that route products register the former group themselves (tmdo_test_register_wc_product_group(), same name and table); live router Q06 runs on wp_core's _wp_page_template instead of _sku. The post stress tester already skips flat tables that do not exist.
- Verify: PostFieldsRegistrationTest (RED before); dev36: wc_product absent from the registry and not restored as an orphan, a CLI order took stock 452 → 450 in native with no DB error (reverted), drift post 10 groups 0.
- PR: PR-077
- doctor: entity flat tables come from the registry (TMDO_CLI::check_entity_tables()) instead of a hard-coded user list naming the never-registered sso group, which failed doctor on every site; it also reports Promotion Guard violations and the recorded drift status per entity (report_guard_and_drift()).
- term-comment-diagnose: filter status comes from each filter's is_enabled() (the garbage filter and misc bucket default to off but were shown enabled); a filter whose add-on is not active shows "not installed".
- bridge-status: flat_rows counts rows holding a value (wp_core keeps an all-NULL row per post: 15306% on dev36) and eav_ids of a post-type group counts only its post types.
- Tests: the WP_CLI stubs record their output in $GLOBALS['_wp_cli_output'].
- Verify: DoctorTest, CliFilterStatusTest, CliBridgeStatusTest (RED before); dev36 doctor exit 1 → 0, wp_core 15306% → 100%.
- PR: PR-076
- The post query router routes no clause of a group whose last drift-check recorded miss / diff / extra; all or nothing, so such a query stays fully native. A group never checked keeps routing (decision 2026-09-27: no slowdown after upgrading; PR-073 samples daily).
- The check sits in route_clause(), which every routed clause, HivePress sort shape and meta_key sort goes through.
- Verify: PostQueryRouterDriftTest (drifting group and drifting sort key RED before); dev36 hp_listing price search in shadow_read: clean → flat, drift → native, unchecked → flat, found 90347 each.
- PR: PR-072
- posts_request only drops SQL_CALC_FOUND_ROWS, but WordPress then treated the request as filtered and selected full post rows through a temporary table; split_the_query now keeps the ID-first split for routed searches when nothing else changed the request, and posts_request_ids drops SQL_CALC_FOUND_ROWS from the split ID query
- Verify: tests/live/run.php router (12/12, IDs-first request, found_posts == native); dev36 fe.sh price 589→340 ms, price__asc 961→427 ms, rating__desc 1724→872 ms, hourly_rate__desc 855→884 ms (was 1206)
- PR: PR-062b
- TMDO_Type_Caster: text / textarea values that are arrays or objects are
stored with maybe_serialize() (was (string) → the literal "Array", which
bridge-restore-native then wrote back over native meta); from_db()
unserializes them with TMDO_Safe_Unserialize. text columns are
VARCHAR(255): longer values are stored NULL (native keeps them) and
logged as text_overflow.
- Add `wp tmdo repair-array-literals [--entity] [--confirm]`
(TMDO_Array_Literal_Repair): rewrites flat "Array" values from native
meta and lists native rows that are "Array" themselves (backup needed).
- Verify: TypeCasterSerializeTest 13 OK, ArrayLiteralRepairTest 3 OK;
unit 685 OK, integration 483 OK, phpcs (phpcs.xml) 0, stan OK;
live `serialized` 3 PASS; dev36 repair dry-run lists the value corrupted
by the 2026-09-26 round trip (user #1 community-events-location)
- PR: PR-055
- Add TMDO_Promotion_Guard: HivePress models read every meta key with an
empty-key get_*_meta() and WooCommerce does the same for products (and
writes stock/sales with direct SQL), so on those sites native meta must
stay authoritative. Promotion to aeav_only is refused in
TMDO_Mode_Manager::set() (covers REST, CLI, phases, auto-promoter) for
post/user/term/comment with HivePress and post/user with WooCommerce;
TMDO_Post_Migration::cleanup() and the cleanup phase refuse too.
Demotion stays open; filter tmdo_promotion_guard_allow overrides.
- Admin notice lists guarded entities already in aeav_only with the
restore-native command (no forced demotion on read).
- Verify: PromotionGuardTest 8 OK; unit 668 OK, integration 480 OK,
phpcs (phpcs.xml) 0, stan OK; dev36 live: 4 promotions + post-cleanup
refused, wpdo_bridge_modes unchanged
- PR: PR-053
- retire() read native meta with wpdb::get_col(), which (via get_var())
returns null for an empty string, so a flat '' written back as '' was
counted as a mismatch and blocked the retirement. It now reads raw rows
with get_results().
- The integration test wpdb stub's get_var() / get_col() now read '' as
null like the real wpdb, so tests see WordPress behaviour.
- Found writing back the sso group on dev21 / dev22 (picture_url = '').
- Verify: core unit 658 / integration 480 green, phpcs clean
- PR: PR-042 follow-up
- TMDO_Post_Query_Router drops SQL_CALC_FOUND_ROWS from routed searches
(posts_request) and answers found_posts_query from a COUNT(*) of the
same query, cached in the object cache under posts' last_changed.
- Hook Bus flat post writes and deletes move posts' last_changed: in
aeav_only they skip update_metadata()'s own hooks, so query caches
would otherwise outlive the write.
- Bench (1M): cached page 8 ms (was 694 / 363 ms for scenarios 1 / 2);
after invalidation 8 ms + COUNT 608 / 344 ms, on par with before.
- REST X-WP-Total COUNT is on the frozen Hot zone path (ADR-003): not
changed.
- Verify: core unit 659 / integration 475, HP 142/49, WC 10/22 green,
phpcs clean
- PR: PR-030
- TMDO_Post_Status_Sync: post group tables get post_status / post_date
columns (posts' collation) and an (post_status, post_date) index.
Brand-new empty tables get them on creation; existing tables only via
`wp tmdo post-status-sync [--dry-run] [--batch]`, never as DDL on a
populated table in a web request. Per-group state in
tmdo_post_status_columns ('columns' | 'ready').
- transition_post_status refreshes the copies; Hook Bus upserts that
create a post row fill them in (existing rows keep theirs).
- Bench (1M rows): add columns 0.05 s, index 5.3 s, batched backfill 32 s,
0 mismatches afterwards.
- Verify: core unit 659 / integration 466 green, phpcs clean
- PR: PR-028
- TMDO_Native_Restorer copies an entity's flat-table values into its native meta table so the value each key reads today is what native returns afterwards: the owning group (by post type for posts) wins, values are stored as WP stores them, NULL removes the native row
- Resumable per-group id cursor, catch-up pass for rows updated during the run, optional demote to dual_write; CLI wp tmdo bridge-restore-native <entity> [--dry-run] [--batch=<n>] [--demote]
- Verify: NativeRestorerTest (5, user) + NativeRestorerPostTest (2, scoped vs legacy routing); core unit 652 / integration 456, HP 148, WC 10 OK; scale estimate dev21 user ~5.1M / post ~4.0M flat rows
- PR: PR-031
- TMDO_Post_Group_Reconciler copies non-NULL values that legacy routing stored in another group's table into the post type's own group (NULL/missing legacy values keep the scoped value), resumable via a cursor, with a catch-up pass for rows updated during the run, then switches on tmdo_post_scoped_routing
- CLI: wp tmdo post-reconcile-groups [--dry-run] [--batch=<n>]; registry exposes get_group_post_types()
- Verify: PostGroupReconcilerPlanTest (4), PostGroupReconcilerTest (6); core unit 635 / integration 449, HP 145/49, WC 10/22 OK; read-only dry-run: dev20 320 values / 0 overwrites, dev21/22 none
- PR: PR-034
- GET /wpdo/v1/listings rejects unregistered or non-viewable post types (400) and joins posts so Zone A only lists published posts of that type
- WP_Query fallback now returns only show_in_rest hot columns, matching the Zone A path
- Verify: core unit 605 / integration 432 OK (new RestApi tests); 1M bench: page 7ms, total COUNT 0.12s → 1.7s (tracked in PR-030)
- PR: PR-015
- install() no longer switches the Hook Bus on or puts user/term/comment
into dual_write; the now-unused maybe_autoactivate_entity_bridge() is gone
- Hook Bus and Mode_Manager defaults are off/disabled; sites with stored
modes (incl. aeav_only) keep them since only unset keys use defaults
- Guard run_dbdelta()'s upgrade.php require like install_v2_tables() so
install() can run under the integration test stub
Verify: InstallerV2Test::test_install_does_not_enable_entity_bridge,
ModeManagerDefaultsTest; unit 602 OK, integration 421 OK
PR: PR-007
- Seed a HivePress-shaped dataset three ways (native meta, entity tables,
hot table) with flat tables built by TMDO's own schema code
- Refuse any database not named *_bench before the bootstrap connects;
bootstrap now honours a pre-defined ABSPATH for the wp-admin stub
- Full scale seeds 9.5M postmeta rows in 7m25s; 0 value mismatches
Verify: TMDO_TEST_DB_NAME=wp_wpdo_bench php tests/bench/seed.php
--vendors=100000 --listings=1000000; unit 600 OK, integration 416 OK
PR: PR-004