Skip to content

Fix: REPLACE() string function misidentified as write query#314

Merged
swissspidy merged 1 commit intowp-cli:mainfrom
chubes4:fix/replace-function-false-positive
Mar 8, 2026
Merged

Fix: REPLACE() string function misidentified as write query#314
swissspidy merged 1 commit intowp-cli:mainfrom
chubes4:fix/replace-function-false-positive

Conversation

@chubes4
Copy link
Contributor

@chubes4 chubes4 commented Mar 7, 2026

Fixes #313

Description

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 actual query results.

The write-query detection regex added in #277 matches bare REPLACE without distinguishing between:

  • REPLACE() — a MySQL string function used in SELECT statements
  • REPLACE INTO / REPLACE — a DML write statement

Change

One-line regex fix — adds a negative lookahead (?!\s*\() so that REPLACE only matches when it's not followed by an opening parenthesis:

// Before
'/\b(UPDATE|DELETE|INSERT|REPLACE|LOAD DATA)\b/i'

// After
'/\b(UPDATE|DELETE|INSERT|REPLACE(?!\s*\()|LOAD DATA)\b/i'

Testing

Tested against a live WordPress multisite (WP 6.9.1, MariaDB, PHP 8.4) by extracting the 2.12.0 phar, patching DB_Command.php, and running from source. Full results:

SELECT queries with REPLACE() — all correctly return results:

Test Result
SELECT REPLACE('hello world', 'world', 'there') hello there
REPLACE in WHERE clause
REPLACE in JOIN condition
Triple-nested REPLACE(REPLACE(REPLACE(...)))
REPLACE mixed with UPPER() + TRIM()
REPLACE inside CASE expression
REPLACE in subquery
REPLACE in GROUP BY + ORDER BY
REPLACE on real WordPress data

Write queries — all correctly show rows affected:

Test Result
INSERT INTO ✅ Rows affected: 1
UPDATE containing REPLACE() in SET ✅ Rows affected: 1
DELETE ✅ Rows affected: 1
REPLACE INTO (DML write) ✅ Rows affected: 1
REPLACE INTO upsert ✅ Rows affected: 2
Bare REPLACE without INTO (valid MySQL) ✅ Rows affected: 1
UPDATE with nested REPLACE(REPLACE()) ✅ Rows affected: 1
LOAD DATA (regex only) ✅ Match

The negative lookahead handles every combination — including the rare bare REPLACE table (...) syntax (without INTO) which MySQL/MariaDB accept as valid DML.

Discovery Context

Found this bug while running automated audit queries against a WordPress multisite events calendar — using REPLACE() to strip suffixes from taxonomy term names for comparison. The query returned nothing, forcing a workaround through wp db cli piping.


AI disclosure: This PR was prepared with AI assistance (Claude Code). The diagnosis, fix, and tests were reviewed and validated by the submitter against a live WordPress multisite (WP 6.9.1, MariaDB 10.11, PHP 8.4).

Use negative lookahead REPLACE(?!\s*\() instead of bare REPLACE to
distinguish the MySQL REPLACE() string function from the REPLACE INTO
(or bare REPLACE) DML write statement.

This avoids a false positive where SELECT queries containing REPLACE()
are routed through the write-query code path, causing their results
to be silently swallowed.

Fixes wp-cli#313
@chubes4 chubes4 requested a review from a team as a code owner March 7, 2026 16:09
@github-actions
Copy link
Contributor

github-actions bot commented Mar 7, 2026

Hello! 👋

Thanks for opening this pull request! Please check out our contributing guidelines. We appreciate you taking the initiative to contribute to this project.

Contributing isn't limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation.

Here are some useful Composer commands to get you started:

  • composer install: Install dependencies.
  • composer test: Run the full test suite.
  • composer phpcs: Check for code style violations.
  • composer phpcbf: Automatically fix code style violations.
  • composer phpunit: Run unit tests.
  • composer behat: Run behavior-driven tests.

To run a single Behat test, you can use the following command:

# Run all tests in a single file
composer behat features/some-feature.feature

# Run only a specific scenario (where 123 is the line number of the "Scenario:" title)
composer behat features/some-feature.feature:123

You can find a list of all available Behat steps in our handbook.

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a bug where wp db query misclassified SELECT statements containing the MySQL REPLACE() string function as write queries. This misclassification led to incorrect output, suppressing actual query results and reporting an erroneous "Rows affected: -1". The fix involves a precise adjustment to the regular expression used for query type detection, ensuring correct differentiation between the REPLACE() function and REPLACE INTO DML statements, thereby restoring accurate behavior for both read and write operations.

Highlights

  • Corrected REPLACE() function detection: The MySQL REPLACE() string function was incorrectly identified as a DML write query (REPLACE INTO), causing SELECT statements to silently swallow results and report "Rows affected: -1".
  • Regex modification for query type detection: A negative lookahead (?!\s*\() was added to the regex that identifies write queries. This ensures REPLACE is only matched as a write operation when it's not followed by an opening parenthesis, accurately distinguishing it from the REPLACE() string function.
Changelog
  • src/DB_Command.php
    • Modified the regular expression used to detect row-modifying queries to correctly distinguish between the REPLACE() string function and REPLACE DML statements by adding a negative lookahead.
Activity
  • No human activity has occurred on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request provides a good fix for an issue where REPLACE() string functions in SELECT queries were misidentified as write operations. The change to the regex is logical and the pull request description is very clear and includes thorough testing details. I've found one edge case related to SQL comments that the new regex does not handle, which I've detailed in a specific comment. Overall, this is a solid improvement.

}

$is_row_modifying_query = isset( $assoc_args['execute'] ) && preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE|LOAD DATA)\b/i', $assoc_args['execute'] );
$is_row_modifying_query = isset( $assoc_args['execute'] ) && preg_match( '/\b(UPDATE|DELETE|INSERT|REPLACE(?!\s*\()|LOAD DATA)\b/i', $assoc_args['execute'] );

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This is a good fix for the common case. However, this regex can still misidentify a query if there is a SQL comment between REPLACE and the opening parenthesis.

For example, a query like SELECT * FROM my_table WHERE col = REPLACE/* a comment */('a', 'b'); would be incorrectly flagged as a write query.

The negative lookahead (?!\s*\() doesn't account for comments. As a result, REPLACE is matched, and the bug's symptom (swallowing SELECT output) would persist for this edge case.

Handling all SQL comment styles (--, #, /* ... */) could make the regex very complex. It's a trade-off between correctness for all edge cases and regex simplicity. Given the context, you might decide the current level of fix is sufficient, but I wanted to point out this remaining scenario.

Copy link
Member

@swissspidy swissspidy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@swissspidy swissspidy added this to the 2.1.5 milestone Mar 8, 2026
@swissspidy swissspidy merged commit 768a128 into wp-cli:main Mar 8, 2026
43 of 63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants