Repository navigation
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
Note
Copilot was unable to run its full agentic suite in this review.
Copilot review overview
3 open findings
What changed in this PR
Updates WordPress table discovery to handle environments where $wpdb->dbname is unavailable (e.g., DB drop-ins), improving compatibility across alternate database layers.
Changes:
- Derive database name at runtime via
SELECT DATABASE()when$wpdb->dbnameis empty. - Add fallbacks to filter table lists in PHP when DB name can’t be determined.
- Add a safer fallback path for fetching specific WP tables when DB name is unknown.
| File | Description |
|---|---|
| php/utils-wp.php | Adds $dbname fallbacks and PHP-side filtering when table metadata queries can’t reference Tables_in_<dbname>. |
🧠 Review effort: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if ( empty( $tables_sql ) ) { | ||
| $tables_sql = $wpdb->prepare( 'SHOW TABLES LIKE %s', esc_like( $wpdb->get_blog_prefix() ) . '%' ); | ||
| } else { | ||
| $tables_sql .= sprintf( " AND %s LIKE '%s'", esc_sql_ident( 'Tables_in_' . $wpdb->dbname ), esc_like( $wpdb->get_blog_prefix() ) . '%' ); | ||
| $dbname = ! empty( $wpdb->dbname ) ? $wpdb->dbname : (string) $wpdb->get_var( 'SELECT DATABASE()' ); | ||
| if ( ! empty( $dbname ) ) { | ||
| $tables_sql .= sprintf( " AND %s LIKE '%s'", esc_sql_ident( 'Tables_in_' . $dbname ), esc_like( $wpdb->get_blog_prefix() ) . '%' ); | ||
| } | ||
| } | ||
|
|
||
| // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared, see above. | ||
| $tables = $wpdb->get_col( $tables_sql, 0 ); | ||
|
|
||
| if ( ! empty( $tables_sql ) && empty( $dbname ) ) { | ||
| $blog_prefix = $wpdb->get_blog_prefix(); |
| // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is safe. | ||
| $all_tables = (array) $wpdb->get_col( 'SHOW TABLES', 0 ); | ||
| $tables = array_values( array_intersect( $wp_tables, $all_tables ) ); |
| array_filter( | ||
| $tables, | ||
| function ( $table ) use ( $blog_prefix ) { | ||
| return 0 === strpos( $table, $blog_prefix ); |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
softglazee
left a comment
There was a problem hiding this comment.
Patch Testing Report
Patch tested: #6432 at 5cfd391, compared with main at a1735b0
Environment
- WordPress: 7.1.3, single site, then the same site converted to a subdirectory multisite with a second site (
/two/) - PHP: 8.5.9 CLI
- Server: n/a (WP-CLI only)
- Database: MariaDB 11.8.9
- Browser: n/a (command line test)
- OS: Alpine Linux 3.24.1 (Docker)
- Theme: Twenty Twenty-Five 1.5 (default)
- MU Plugins: None
- Plugins (drop-ins, one at a time; both sample configs point to the same database, one writer and one reader):
- HyperDB 1.9 from wordpress.org: unmodified
db.phpand the sampledb-config.php - LudicrousDB 5.3.1 from GitHub: unmodified
drop-ins/db.phpanddrop-ins/db-config.php, plugin folder inwp-content/plugins/ludicrousdb
- HyperDB 1.9 from wordpress.org: unmodified
- WP-CLI: search-replace-command
mainat 5a62210, db-command v3.0.1. To switch the framework I rancomposer installin a search-replace-command clone, mounted a wp-cli clone overvendor/wp-cli/wp-cli, and checked outmainor this PR in it. WP_DEBUG: false for HyperDB, true for LudicrousDB. HyperDB 1.9 still loadswp-includes/wp-db.php. WithWP_DEBUGtrue on WordPress 7.1.3 that is a fatal error before any command runs (Call to undefined function wp_kses()from_deprecated_file()).
Steps taken
- On
main, both drop-ins leave$wpdb->dbnameempty, andSELECT DATABASE()returns the right name:
$ wp eval 'global $wpdb; echo "class=" . get_class($wpdb) . " dbname=" . var_export($wpdb->dbname, true) . " DATABASE()=" . var_export($wpdb->get_var("SELECT DATABASE()"), true) . "\n";'
class=hyperdb dbname=NULL DATABASE()='wp_cli_test_pr6432'
class=LudicrousDB dbname=NULL DATABASE()='wp_cli_test_pr6432'
- On
main,wp search-replacefinds no tables and still exits 0. The error is printed twice (table list, then the views lookup):
$ wp search-replace foozle barzle --dry-run
WordPress database error Unknown column 'Tables_in_' in 'WHERE' for query SHOW TABLES WHERE `Tables_in_` IN ('wp_users', 'wp_usermeta', 'wp_posts', 'wp_comments', 'wp_links', 'wp_options', 'wp_postmeta', 'wp_terms', 'wp_term_taxonomy', 'wp_term_relationships', 'wp_termmeta', 'wp_commentmeta') made by WP_CLI\Utils\wp_get_table_names
WordPress database error Unknown column 'Tables_in_' in 'WHERE' for query SHOW TABLES WHERE `Tables_in_` IN ('wp_users', 'wp_usermeta', 'wp_posts', 'wp_comments', 'wp_links', 'wp_options', 'wp_postmeta', 'wp_terms', 'wp_term_taxonomy', 'wp_term_relationships', 'wp_termmeta', 'wp_commentmeta') made by WP_CLI\Utils\wp_get_table_names
Success: 0 replacements to be made.
- Ran the commands below on
mainwithout a drop-in, onmainwith each drop-in, and on this PR with each drop-in.foozleis in one post (and its revision) and inblogdescription. The database also has a custom tablewp_custom, a table without the prefixother_table, and a viewwp_view_posts. "DB error" means theUnknown column 'Tables_in_'error above.
Single site:
| Command | main, no drop-in | main + HyperDB or LudicrousDB | this PR + HyperDB or LudicrousDB |
|---|---|---|---|
wp db tables |
12 tables | DB error, no tables | 12 tables |
wp db tables --scope=global |
2 tables | DB error, no tables | 2 tables |
wp db tables 'wp_p*' |
2 tables | DB error, Error: Couldn't find any tables matching: wp_p* |
2 tables |
wp db size --tables |
12 rows | DB error, header only | 12 rows |
wp db search foozle |
5 matches | DB error, 0 matches | 5 matches |
wp search-replace foozle barzle --dry-run |
5 | DB error twice, 0 | 5 |
wp search-replace foozle barzle wp_posts --dry-run |
4 | DB error, Error: Couldn't find any tables matching: wp_posts |
4 |
wp search-replace foozle barzle --all-tables-with-prefix --dry-run |
6, view skipped | 6, plus DB error from the views query | 6, view skipped |
wp search-replace foozle barzle --all-tables --dry-run |
7 | 7 | 7 |
wp search-replace foozle barzle (real run, then reverted) |
5, rows changed | 0, nothing changed | 5, rows changed |
wp_get_table_names() with all-tables-with-prefix + views-only |
wp_view_posts |
DB error, empty | wp_view_posts |
wp_get_table_names() with all-tables-with-prefix + base-tables-only |
13 tables | DB error, empty | 13 tables |
Multisite:
| Command | main, no drop-in | main + HyperDB or LudicrousDB | this PR + HyperDB or LudicrousDB |
|---|---|---|---|
wp db tables --network |
28 tables | DB error, no tables | 28 tables |
wp db tables --url=<two> --scope=blog |
10 tables | DB error, no tables | 10 tables |
wp search-replace foozle barzle --network --dry-run |
10 | DB error twice, 0 | 10 |
wp search-replace foozle barzle --url=<two> --dry-run |
5 | DB error twice, 0 | 5 |
wp search-replace foozle barzle --url=<two> --all-tables-with-prefix --dry-run |
6 | 6, plus DB error from the views query | 6 |
wp search-replace foozle barzle --network (real run, then reverted) |
10 | 0 | 10 |
HyperDB and LudicrousDB gave the same results in every row. With this PR, the full output of every command above, with either drop-in, is identical to main without a drop-in. I compared the saved outputs with diff, and only the class name in the first wp eval line differs.
- On a normal site without a drop-in (
WP_DEBUGtrue), the output of this PR is identical tomainfor all the single site commands, with no PHP warnings. - ✅ Patch is solving the problem: it fixes wp-cli/search-replace-command#207 with HyperDB and LudicrousDB, for every caller of
wp_get_table_names()I tried, on single site and multisite. - ❌ Patch is failing: on a normal site with
lower_case_table_names=1and an uppercase prefix,--all-tables-with-prefixreturns no tables (see Additional Notes).
Expected result
- With a drop-in that leaves
$wpdb->dbnameempty, the commands find the same tables as without a drop-in, and nothing changes for normal sites.
Additional Notes
-
Regression:
--all-tables-with-prefixreturns nothing withlower_case_table_names=1and an uppercase prefix. This is on a normal site, no drop-in.In the
--all-tables-with-prefixbranch,$dbnameis only set in theelsepath. On the plainSHOW TABLES LIKEpath it is never set, and$tables_sqlis never empty at that point. So! empty( $tables_sql ) && empty( $dbname )is always true there, and the new PHP filter runs on every--all-tables-with-prefixcall, not only when the database name is missing.Usually that only repeats what
LIKEalready did. ButSHOW TABLES LIKEis case-insensitive whenlower_case_table_namesis 1 (the default on Windows), andstrpos()is not. With$table_prefix = 'WP_'the server stores the tables aswp_postsand so on, and the filter removes all of them.Steps: start MariaDB with
--lower-case-table-names=1, runwp config create ... --dbprefix=WP_, thenwp core install ..., then:$ wp db tables --all-tables-with-prefix --format=csvmain:
wp_commentmeta,wp_comments,wp_links,wp_options,wp_postmeta,wp_posts,wp_term_relationships,wp_term_taxonomy,wp_termmeta,wp_terms,wp_usermeta,wp_usersThis PR: an empty line.
$ wp search-replace http://lctn.test http://lctn.example --all-tables-with-prefix --dry-run --format=countmain:
8. This PR:0.wp db cleanusesall-tables-with-prefixtoo. On this PR,wp db clean --yesprintsSuccess: Tables dropped.and all 12 tables are still there.The default scope is not affected:
wp db tableslists 12 tables andwp search-replacewithout flags finds 8 replacements on both.About the first Copilot comment: no notice is raised, because
empty()does not warn on an undefined variable (I ran withWP_DEBUGtrue, and PHPStan passes). But that missing$dbnameis the cause of this problem. -
Minor: order of the last fallback. The
SHOW TABLESplusarray_intersect()fallback only runs when$wpdb->dbnameis empty andSELECT DATABASE()returns nothing. With both real drop-insSELECT DATABASE()returned the name, so they never reach it. On a connection with no database selected,SHOW TABLESfails as well:$ mariadb -h wpcli-db -u wp_cli_test -ppassword1 --skip-ssl -e "SELECT DATABASE(); SHOW TABLES;" DATABASE() NULL -------------- SHOW TABLES -------------- ERROR 1046 (3D000) at line 1: No database selectedTo reach it anyway, I used a test-only
db.phpthat extendswpdb, setsdbnameto''and returnsnullforSELECT DATABASE(). The tables were right, but in$wp_tablesorder instead of sorted:$ wp db tables --format=csv wp_users,wp_usermeta,wp_posts,wp_comments,wp_links,wp_options,wp_postmeta,wp_terms,wp_term_taxonomy,wp_term_relationships,wp_termmeta,wp_commentmeta,wp_blogs,wp_blogmeta,wp_signups,wp_site,wp_sitemeta,wp_registration_logThe comment in the other branch says the sorted order is a BC change since 1.5.0, and the legacy SQLite filter below uses
array_intersect( $tables, $wp_tables ). Swapping the arguments keeps the sorted order. Since this fallback can only help a drop-in that answers those two queries from different connections, removing it would also be fine. -
Other bot comments:
- Copilot, narrowing the fallback to
SHOW TABLES LIKE '<prefix>%': I would not do that.$wpdb->tables()returnsCUSTOM_USER_TABLEandCUSTOM_USER_META_TABLEwhen they are defined, and those do not have to use the prefix. - Copilot,
str_starts_with(): WP-CLI supports PHP 7.2.24+, and CI tests WordPress 4.9, which has no polyfill for it.strpos()is right here. - CodeRabbit: no actionable comments. It notes the drop-in behaviour was not executed, which is what the runs above cover.
- Copilot, narrowing the fallback to
Support Content
- Suggested change for both points above:
Suggested change to php/utils-wp.php
@@ -504,19 +504,23 @@ function wp_get_table_names( $args, $assoc_args = [] ) {
$tables = $wpdb->get_col( $tables_sql, 0 );
} elseif ( get_flag_value( $assoc_args, 'all-tables-with-prefix' ) ) {
+ $filter_by_prefix = false;
if ( empty( $tables_sql ) ) {
$tables_sql = $wpdb->prepare( 'SHOW TABLES LIKE %s', esc_like( $wpdb->get_blog_prefix() ) . '%' );
} else {
$dbname = ! empty( $wpdb->dbname ) ? $wpdb->dbname : (string) $wpdb->get_var( 'SELECT DATABASE()' );
if ( ! empty( $dbname ) ) {
$tables_sql .= sprintf( " AND %s LIKE '%s'", esc_sql_ident( 'Tables_in_' . $dbname ), esc_like( $wpdb->get_blog_prefix() ) . '%' );
+ } else {
+ $filter_by_prefix = true;
}
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared, see above.
$tables = $wpdb->get_col( $tables_sql, 0 );
- if ( ! empty( $tables_sql ) && empty( $dbname ) ) {
+ // Only filter in PHP when the prefix condition could not be added to the query.
+ if ( $filter_by_prefix ) {
$blog_prefix = $wpdb->get_blog_prefix();
$tables = array_values(
array_filter(
@@ -558,7 +562,7 @@ function wp_get_table_names( $args, $assoc_args = [] ) {
// Fallback when database name cannot be determined (e.g. drop-ins like HyperDB or LudicrousDB).
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is safe.
$all_tables = (array) $wpdb->get_col( 'SHOW TABLES', 0 );
- $tables = array_values( array_intersect( $wp_tables, $all_tables ) );
+ $tables = array_values( array_intersect( $all_tables, $wp_tables ) );
} else {
// Note: BC change 1.5.0, tables are sorted (via TABLES view).
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- uses esc_sql_ident() and $wpdb->_escape().- Suggested test. The PR has no test, and Codecov reports 2 lines not covered. This scenario for
features/utils-wp.featureadds adb.phpdrop-in that clears$wpdb->dbname, like HyperDB and LudicrousDB do. It needs@require-mysqlbecause SQLite already useswp-content/db.php.
Behat scenario
# `wp db query` does not yet work on SQLite, and SQLite already uses wp-content/db.php.
@require-mysql
Scenario: Get WP table names when a db.php drop-in leaves $wpdb->dbname empty
Given a WP installation
And I run `wp db query "CREATE TABLE wp_xx_posts ( id int );"`
And I run `wp db query "CREATE VIEW wp_posts_view AS ( SELECT ID from wp_posts );"`
And a wp-content/db.php file:
"""
<?php
// Like HyperDB and LudicrousDB, this drop-in does not set $wpdb->dbname.
class Empty_Dbname_DB extends wpdb {
public function __construct( $dbuser, $dbpassword, $dbname, $dbhost ) {
parent::__construct( $dbuser, $dbpassword, $dbname, $dbhost );
$this->dbname = '';
}
}
$wpdb = new Empty_Dbname_DB( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST );
"""
And a table_names.php file:
"""
<?php
WP_CLI::add_command( 'get_table_names', function ( $args, $assoc_args ) {
echo implode( PHP_EOL, WP_CLI\Utils\wp_get_table_names( $args, $assoc_args ) ) . PHP_EOL;
} );
"""
When I run `wp --require=table_names.php get_table_names`
Then STDOUT should be:
"""
wp_commentmeta
wp_comments
wp_links
wp_options
wp_postmeta
wp_posts
wp_term_relationships
wp_term_taxonomy
wp_termmeta
wp_terms
wp_usermeta
wp_users
"""
When I run `wp --require=table_names.php get_table_names 'wp_post*'`
Then STDOUT should be:
"""
wp_postmeta
wp_posts
"""
When I run `wp --require=table_names.php get_table_names --all-tables-with-prefix --base-tables-only 'wp_*posts'`
Then STDOUT should be:
"""
wp_posts
wp_xx_posts
"""
When I run `wp --require=table_names.php get_table_names --all-tables-with-prefix --views-only`
Then STDOUT should be:
"""
wp_posts_view
"""- Test results on MariaDB 11.8.9. I ran these scenarios with
vendor/bin/behatdirectly, because the tags skip@require-mysqlon MariaDB:
| Test | main | this PR | this PR + suggested change |
|---|---|---|---|
| New scenario above | fails: The command unexpectedly produced STDERR output (the Tables_in_ error) |
passes, 13 steps | passes, 13 steps |
| "Get WP table names for single site install" | passes, 72 steps | passes, 72 steps | passes, 72 steps |
| "Get WP table names for multisite install" | passes, 102 steps | passes, 102 steps | passes, 102 steps |
composer phpunit |
634 tests | 634 tests | 634 tests |
PHPUnit gives the same result on all three: OK, but there were issues! with 1 warning and 5 skipped tests, so nothing changes there.
vendor/bin/run-behat-tests features/utils-wp.feature (9 scenarios after the tag filter) passes on main and this PR on MariaDB, and with the suggested change on SQLite. vendor/bin/phpcs php/utils-wp.php and composer phpstan pass with the suggested change.
With the suggested change, the lower_case_table_names=1 site lists the 12 tables and finds 8 replacements again, wp db clean --yes drops the tables again, the fallback returns sorted tables, and the HyperDB and LudicrousDB multisite runs are still identical to main without a drop-in.
| if ( empty( $tables_sql ) ) { | ||
| $tables_sql = $wpdb->prepare( 'SHOW TABLES LIKE %s', esc_like( $wpdb->get_blog_prefix() ) . '%' ); | ||
| } else { | ||
| $tables_sql .= sprintf( " AND %s LIKE '%s'", esc_sql_ident( 'Tables_in_' . $wpdb->dbname ), esc_like( $wpdb->get_blog_prefix() ) . '%' ); | ||
| $dbname = ! empty( $wpdb->dbname ) ? $wpdb->dbname : (string) $wpdb->get_var( 'SELECT DATABASE()' ); | ||
| if ( ! empty( $dbname ) ) { | ||
| $tables_sql .= sprintf( " AND %s LIKE '%s'", esc_sql_ident( 'Tables_in_' . $dbname ), esc_like( $wpdb->get_blog_prefix() ) . '%' ); | ||
| } | ||
| } | ||
|
|
||
| // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared, see above. | ||
| $tables = $wpdb->get_col( $tables_sql, 0 ); | ||
|
|
||
| if ( ! empty( $tables_sql ) && empty( $dbname ) ) { |
There was a problem hiding this comment.
$dbname is only set in the else path, so on the plain SHOW TABLES LIKE path this PHP filter runs on every --all-tables-with-prefix call. With lower_case_table_names=1 and an uppercase prefix it removes all tables, because SHOW TABLES LIKE is case-insensitive there and strpos() is not. This filters only when the prefix condition could not be added to the query:
| if ( empty( $tables_sql ) ) { | |
| $tables_sql = $wpdb->prepare( 'SHOW TABLES LIKE %s', esc_like( $wpdb->get_blog_prefix() ) . '%' ); | |
| } else { | |
| $tables_sql .= sprintf( " AND %s LIKE '%s'", esc_sql_ident( 'Tables_in_' . $wpdb->dbname ), esc_like( $wpdb->get_blog_prefix() ) . '%' ); | |
| $dbname = ! empty( $wpdb->dbname ) ? $wpdb->dbname : (string) $wpdb->get_var( 'SELECT DATABASE()' ); | |
| if ( ! empty( $dbname ) ) { | |
| $tables_sql .= sprintf( " AND %s LIKE '%s'", esc_sql_ident( 'Tables_in_' . $dbname ), esc_like( $wpdb->get_blog_prefix() ) . '%' ); | |
| } | |
| } | |
| // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared, see above. | |
| $tables = $wpdb->get_col( $tables_sql, 0 ); | |
| if ( ! empty( $tables_sql ) && empty( $dbname ) ) { | |
| $filter_by_prefix = false; | |
| if ( empty( $tables_sql ) ) { | |
| $tables_sql = $wpdb->prepare( 'SHOW TABLES LIKE %s', esc_like( $wpdb->get_blog_prefix() ) . '%' ); | |
| } else { | |
| $dbname = ! empty( $wpdb->dbname ) ? $wpdb->dbname : (string) $wpdb->get_var( 'SELECT DATABASE()' ); | |
| if ( ! empty( $dbname ) ) { | |
| $tables_sql .= sprintf( " AND %s LIKE '%s'", esc_sql_ident( 'Tables_in_' . $dbname ), esc_like( $wpdb->get_blog_prefix() ) . '%' ); | |
| } else { | |
| $filter_by_prefix = true; | |
| } | |
| } | |
| // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared, see above. | |
| $tables = $wpdb->get_col( $tables_sql, 0 ); | |
| // Only filter in PHP when the prefix condition could not be added to the query. | |
| if ( $filter_by_prefix ) { |
| // Fallback when database name cannot be determined (e.g. drop-ins like HyperDB or LudicrousDB). | ||
| // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is safe. | ||
| $all_tables = (array) $wpdb->get_col( 'SHOW TABLES', 0 ); | ||
| $tables = array_values( array_intersect( $wp_tables, $all_tables ) ); |
There was a problem hiding this comment.
With the arguments this way round, the fallback returns the tables in $wp_tables order instead of sorted. Swapping them keeps the sorted order, like the legacy SQLite filter below:
| $tables = array_values( array_intersect( $wp_tables, $all_tables ) ); | |
| $tables = array_values( array_intersect( $all_tables, $wp_tables ) ); |



Resolves wp-cli/search-replace-command#207
Summary
When database drop-ins like HyperDB or LudicrousDB are in use,
$wpdb->dbnameis often empty (""ornull) because the drop-in manages sharded/replica connections dynamically and does not initialize the global property.In
WP_CLI\Utils\wp_get_table_names(), queries like:were constructed because
esc_sql_ident( 'Tables_in_' . $wpdb->dbname )evaluated to`Tables_in_`when$wpdb->dbnamewas empty. This caused MySQL to fail with:This affected commands like
wp search-replace,wp db tables,wp db size, andwp db searchacross multisite, HyperDB, LudicrousDB, and WordPress VIP installations.Changes
$wpdb->dbnameis empty, querySELECT DATABASE()to detect the active database.$dbnameis still empty or cannot be resolved, fallback toSHOW TABLESand intersect the known$wp_tableslist in PHP instead of failing with a malformed SQLWHEREclause.--all-tables-with-prefixsafely when$dbnameis empty.Summary by CodeRabbit