Releases / Sep 15, 2026 / 5 min read / By Marcin Dudek

WP Multitool 1.9.7: the underscore in wp_ is a SQL wildcard

An underscore in a SQL LIKE pattern is a wildcard. It matches exactly one character, any character. Every WordPress table prefix ends in one. So does every transient option name.…

An underscore in a SQL LIKE pattern is a wildcard. It matches exactly one character, any character. Every WordPress table prefix ends in one. So does every transient option name.

I shipped two bugs on the back of that, and they went out in WP Multitool 1.9.7 as fixes. Here's what happened.

The query that looked fine for years

Finding your own tables looks obvious:

SELECT TABLE_NAME FROM information_schema.TABLES
WHERE TABLE_NAME LIKE 'wp_%'

I ran that against a table holding four names to see what it really matches:

naive   LIKE 'wp_%'  =>  wp_posts, wpXanything, wp2_options
esc_like            =>  wp_posts

The underscore matched the X. It matched the 2. On a shared database with more than one WordPress install, or next to any table that happens to start with "wp", that pattern reaches tables I have no business touching.

In my Database Optimizer that mattered twice. Table stats listed foreign tables as if they were yours. Worse, the optimize loop ran OPTIMIZE TABLE on them.

The same bug, one letter wide, in transients

Transient cleanup has the same shape:

SELECT option_name FROM wp_options
WHERE option_name LIKE '_transient_timeout_%'

Three underscores in that prefix, three single-character wildcards. It matches _transient_timeout_something, which is what you want. It also matches xtransient_timeout_y, which is not a transient at all.

That inflated the transient counts I was showing people. And in the cleanup path it meant backing up and deleting options that were never transients. A plugin storing a setting under an unlucky name would have lost it.

The fix is one function

$wpdb->esc_like() escapes the LIKE metacharacters and leaves everything else alone. It does not escape quotes, so it goes inside prepare(), not instead of it:

$pattern = $wpdb->esc_like( $wpdb->prefix ) . '%';
$rows = $wpdb->get_col( $wpdb->prepare(
    "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_NAME LIKE %s",
    $pattern
) );

The trailing % is appended after the escaping, because that one is meant to be a wildcard. esc_like( 'wp_' ) returns wp\_. That is the whole fix.

If you write WordPress code, go grep your own plugins for LIKE ' right now. I found mine by reading a query I'd read a dozen times before.

Then I went looking at the admin, and that went the same way

Someone messaged me that the buttons in the plugin admin looked off. Icons sitting a few pixels below their labels. On the Dashboard Widgets screen you had to hover a row before you could see which widget it referred to.

I went to fix the icons and ended up measuring all 17 admin screens instead - computed font size, contrast ratio, click target size, horizontal overflow, at five widths from 1440 down to 390.

409 elements had text under 14px. The muted grey I use for secondary text everywhere came in at 3.49:1, under the 4.5:1 AA floor. One entire screen - Updates - was rendering in the default light WordPress theme because its wrapper was missing a class, so the heading sat at 1.25:1 against the background. I had been looking at that page for months.

The hover bug had a cause I didn't expect. WordPress core parks row action links off-screen at left:-119988px and only brings them back on hover. That is core behaviour for list tables, and the widget hook IDs were riding along with it.

The audit that gave itself a pass

This is the part I keep thinking about.

After the first round of fixes, my measuring script reported nothing left to fix. That was wrong. The check was "flag anything with a font size below 14px", which silently skips anything at exactly 0.

An element at font-size: 0 renders invisible. Mine was scoring it as a pass. And it was the original reported bug - the widget hook IDs, still at 0px at phone width, certified clean by the thing I built to catch it.

WordPress core sets .row-actions span { font-size: 0 } at mobile widths. My rule set the colour and the font family on those IDs but never declared a font size, so core's zero won uncontested. Two separate misses stacked on the same element.

A test that cannot fail the way the bug actually happens will pass forever. I'd rather learn that on a font size than on a payment.

What else is in 1.9.7

Site Doctor stopped reporting Performance Lab's Server-Timing drop-in as a dead object cache. Every site running Performance Lab got a critical finding telling the owner to delete wp-content/object-cache.php. That file is not an object cache, its own header says so, and deleting it only breaks Server-Timing measurement. It also gets written straight back by the plugin, so the advice now names the constant that actually settles it - PERFLAB_DISABLE_OBJECT_CACHE_DROPIN.

Site Doctor also learned four LiteSpeed misconfigurations, read through LiteSpeed Cache's own filter rather than raw options: the WooCommerce product update interval, Guest Mode blocked by a .htaccess PHP deny-all, Serve Stale turned off, and WebP Express endpoints missing from the allow-list. Nothing is written back, the remediation is copy-paste.

Frontend Optimizer can now stop the empty-cart WooCommerce session cookie for logged-out visitors. WooCommerce sets a session cookie speculatively, and a Set-Cookie on an anonymous request forces a cache bypass on every hit behind a CDN or page cache. It is skipped on cart, checkout, my-account and REST, and only fires when the cart is genuinely empty and the visitor is logged out.

The Action Scheduler screen updates its numbers in place after Unstick, Run Queue and Clean. It used to run the action, tell you it worked, and leave the old counts on screen with a note asking you to refresh.

And the admin sweep shipped: text raised above 14px, contrast fixed, focus rings you can see when you tab, click targets at 24px or more, no sideways scrolling on a phone, and the native blue-and-white checkboxes replaced with the plugin's own toggle. All 1505 strings are translated across six languages.

Updating

If you use Database Optimizer or clean transients, take this one. The two LIKE fixes are the reason it leads the changelog.

Auto-updates handle it if you have them on. Otherwise it's in your Plugins screen, or on the downloads page with your licence key.

Find what is slowing your WordPress

WP Multitool - 19 modules, from $79/year, zero bloat.

Get WP Multitool

Built by Marcin Dudek.