Duplicate plugin-owned database tables into the runtime environment - #1453
Duplicate plugin-owned database tables into the runtime environment#1453guzel wants to merge 3 commits into
Conversation
The runtime environment installs a fresh WordPress under an amended table prefix, which creates only the WordPress core tables. Plugins register their own tables in an activation hook, which never runs there, so those tables are missing while the very same plugins are activated in the runtime environment and query them. Every such plugin produces database errors, Yoast SEO and WooCommerce being the most commonly reported. Duplicate the schema of the actual site's plugin-owned tables into the runtime environment, and remove them again during cleanup. Only the structure is duplicated, never any data, so runtime checks still cannot read or modify the actual site's content. Three details are worth calling out: The duplication happens before `wp_install()` rather than after, because the install itself fires hooks such as `update_option` and `user_register` that active plugins already respond to by querying their own tables. Core tables are identified through `wp_get_db_schema()` rather than the `$wpdb->tables` property. That property is public and plugins append to it, WooCommerce among them, so using it would classify precisely those tables as core and skip them. Going through `wp_get_db_schema()` also resolves `CUSTOM_USER_TABLE` and `CUSTOM_USER_META_TABLE`. Cleanup drops only the tables recorded as created during setup, never names derived from the database. A table that already exists is skipped at creation and left unrecorded, so a table of the actual site whose name happens to match the runtime environment's prefix can never be dropped.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @ctschach. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Thanks for this thorough implementation. With Codex’s help reviewing it, I found this to be a relevant and necessary improvement for reliable runtime checks. The runtime environment loads active plugins but previously created only WordPress core tables, so plugins such as WooCommerce or Yoast could query missing custom tables during bootstrap and generate database errors. Duplicating the schema only, without copying data, addresses the root cause while preserving isolation. I also appreciate the safeguards around identifying core tables, skipping views and pre-existing runtime-prefixed tables, and recording only the tables created by the runtime environment for safe cleanup. Before merging, I would mainly like to see integration coverage with real table-owning plugins and consideration of setup time on sites with a large number of custom tables. |
It is defined there rather than in upgrade.php, which merely pulls it in along with the rest of the admin API.
What?
Closes #234
Closes #1127
Duplicates the schema of plugin-owned database tables into the runtime environment, so that plugins active during a runtime check can query their own tables instead of erroring out.
Why?
Runtime_Environment_Setup::set_up()amends the database base prefix (e.g.wp_→wp_pc_) and installs a fresh WordPress under it. That install only creates the WordPress core tables.Plugins almost always register their own tables in an activation hook, which never runs in the runtime environment. But
install_wordpress()deliberately carries the actual site'sactive_pluginsover, so those same plugins are loaded and do query their tables. The result is a stream of database errors for any plugin with custom tables:Reported for Yoast SEO (#1127, #350), WooCommerce (#234) and GatherPress (comment on #234).
The earlier investigation in #234 tried calling
activate_plugin()to trigger table creation and found it silently did nothing —activate_plugin()returns early when the plugin is already active. Duplicating the schema sidesteps that entirely and works no matter how a plugin creates its tables.How?
get_core_table_names()derives the core table list fromwp_get_db_schema(). The$wpdb->tablesproperty is deliberately not used: it is public, and plugins append to it — WooCommerce registers its lookup, meta and Action Scheduler tables there. Using it would classify precisely those tables as core and skip them, leaving the most commonly reported case of this bug unfixed. As a side effectwp_get_db_schema()also resolvesCUSTOM_USER_TABLE/CUSTOM_USER_META_TABLE, so a prefixed custom user table is correctly left alone.get_custom_table_names()lists tables matching the actual site's base prefix and subtracts that core list. On Multisite a leading site-ID segment is stripped before comparison, sowp_3_postsis still recognized as core. Tables already carrying the runtime prefix are skipped, andSHOW FULL TABLESis used so views are excluded (CREATE TABLE ... LIKEdoes not accept them).create_custom_tables()runsCREATE TABLE IF NOT EXISTS <runtime> LIKE <source>for each. Structure only, never data — runtime checks still cannot read or modify the actual site's content.IF NOT EXISTSkeeps it idempotent across repeated runs.create_custom_tables()skips a target that already exists rather than replacing it, and reports back only the tables it genuinely created.record_custom_tables()stores that list in an option under the site's own prefix, merging rather than replacing so a second setup without an intermediate cleanup cannot orphan the first one's tables.drop_custom_tables()inclean_up()drops only the recorded names, never names read back from the database, and the option is deleted afterwards. A table the runtime environment did not create therefore cannot be dropped — including the case of a site that owns bothwp_fooand a realwp_pc_foo(cf. Erroneously drops tables #907).One detail worth flagging for review: the duplication happens before
wp_install(), not after. The install itself firesupdate_option,user_register,set_user_roleetc., and active plugins already respond to those by querying their own tables — so creating the tables afterwards is too late to prevent the errors.Testing Instructions
Table 'db.wp_pc_yoast_indexable' doesn't existerrors. After it, there are none.SHOW TABLES LIKE 'wp_pc_%'now also lists the plugin tables during the run, and that nothing is left behind afterwards.Verified against a local site with Yoast SEO, WooCommerce and a plugin owning two custom tables:
wp_pc_tables createddoesn't existerrorswp_pc_tables after cleanupwp_postsbefore / afterwp_pc_options(confirms the install still runs)wp_pc_woocommerce_tax_ratesVerified with Yoast SEO and WooCommerce active, since the two exercise different paths: Yoast's tables are unknown to
$wpdb, while WooCommerce registers its own on$wpdb->tables.Adversarial check: with a real
wp_pc_zztest_thingpresent alongside a realwp_zztest_thing, a full setup/cleanup cycle leaves the former untouched with its row intact, and a repeated setup without cleanup still leaves no tables behind.New unit tests cover duplication, data isolation, cleanup, the
$wpdb->tablesregistration case, and that cleanup never drops a table it did not create.Full suite passes on both configurations:
composer lintandcomposer phpstanare clean.AI Usage Disclosure
If AI tools were used, please describe how they were used:
Claude Code was used throughout: investigating the root cause, drafting the implementation and tests, and verifying behaviour against a local reproduction with Yoast SEO and WooCommerce active. All changes have been manually reviewed and tested by a human before submission.