Bug Report
wp db query silently swallows SELECT results when the query contains the MySQL REPLACE() string function. The command outputs Success: Query succeeded. Rows affected: -1 instead of the query results.
This is because the write-query detection regex on line 541 of DB_Command.php matches the word REPLACE without distinguishing between:
REPLACE() — a MySQL string function used in SELECT statements
REPLACE INTO — a DML write statement
Reproduction
# This works (no REPLACE keyword):
$ wp db query "SELECT CONCAT('a', 'b') as result"
result
ab
# This silently fails (REPLACE string function triggers write path):
$ wp db query "SELECT REPLACE('hello world', 'world', 'there') as result"
Success: Query succeeded. Rows affected: -1
# Piping through db cli works fine (bypasses the regex):
$ echo "SELECT REPLACE('hello world', 'world', 'there') as result;" | wp db cli
result
hello there
Root Cause
The regex at line 541:
$is_row_modifying_query = isset( $assoc_args['execute'] )
&& preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE|LOAD DATA)\b/i', $assoc_args['execute'] );
\bREPLACE\b matches the REPLACE in REPLACE('hello world', ...) because \b treats the transition from E to ( as a word boundary.
When the write path is taken, the code:
- Appends
; SELECT ROW_COUNT(); to the query
- Captures stdout internally (the
false third arg to self::run)
- Extracts only the last line as the affected row count (
-1 because the prior statement was a SELECT)
- Prints the success message and discards the actual query results
Suggested Fix
The MySQL REPLACE write statement syntax is always REPLACE INTO. Changing the regex to match REPLACE\s+INTO instead of bare REPLACE would correctly distinguish the two:
preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE\s+INTO|LOAD DATA)\b/i', $assoc_args['execute'] );
Note: MySQL does technically allow REPLACE without INTO (the INTO keyword is optional), but this mirrors how the regex already handles INSERT — bare INSERT matches, which works because INSERT has no equivalent string function collision. For REPLACE, the collision with the string function makes the stricter match necessary.
Introduced in PR #277 (merged March 10, 2025).
Environment
WP-CLI version: 2.12.0
PHP version: 8.4
MySQL: MariaDB
OS: Linux (Debian)
Bug Report
wp db querysilently swallows SELECT results when the query contains the MySQLREPLACE()string function. The command outputsSuccess: Query succeeded. Rows affected: -1instead of the query results.This is because the write-query detection regex on line 541 of DB_Command.php matches the word
REPLACEwithout distinguishing between:REPLACE()— a MySQL string function used in SELECT statementsREPLACE INTO— a DML write statementReproduction
Root Cause
The regex at line 541:
\bREPLACE\bmatches theREPLACEinREPLACE('hello world', ...)because\btreats the transition fromEto(as a word boundary.When the write path is taken, the code:
; SELECT ROW_COUNT();to the queryfalsethird arg toself::run)-1because the prior statement was a SELECT)Suggested Fix
The MySQL
REPLACEwrite statement syntax is alwaysREPLACE INTO. Changing the regex to matchREPLACE\s+INTOinstead of bareREPLACEwould correctly distinguish the two:Note: MySQL does technically allow
REPLACEwithoutINTO(theINTOkeyword is optional), but this mirrors how the regex already handlesINSERT— bareINSERTmatches, which works becauseINSERThas no equivalent string function collision. ForREPLACE, the collision with the string function makes the stricter match necessary.Introduced in PR #277 (merged March 10, 2025).
Environment