Skip to content

wp db query misidentifies MySQL REPLACE() string function as REPLACE INTO write statement #313

@chubes4

Description

@chubes4

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:

  1. Appends ; SELECT ROW_COUNT(); to the query
  2. Captures stdout internally (the false third arg to self::run)
  3. Extracts only the last line as the affected row count (-1 because the prior statement was a SELECT)
  4. 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)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions