Skip to content

Use whereLike for search so MySQL works alongside PostgreSQL - #302

Merged
paulocastellano merged 2 commits into
trypostit:mainfrom
jonto:fix/portable-case-insensitive-search
Aug 26, 2026
Merged

Use whereLike for search so MySQL works alongside PostgreSQL#302
paulocastellano merged 2 commits into
trypostit:mainfrom
jonto:fix/portable-case-insensitive-search

Conversation

@jonto

@jonto jonto commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

The problem

Seven search call sites use the ilike operator, which only PostgreSQL
understands. On MySQL they raise a syntax error, so post search, asset
search, label and signature search, workspace-member search, and the MCP
list-posts tool are all unusable — on an engine config/database.php
has always carried a complete connection block for, and that the
self-hosting docs advertise as supported.

The change

Replace them with whereLike($column, $value). The query grammars
translate it per driver:

  • PostgresGrammar::whereLikeilike
  • MySqlGrammar::whereLikelike

The SQL generated on PostgreSQL is therefore unchanged — this is a
no-op for existing installs.

Verification

Full suite on both engines with the patch applied:

Engine Passed Failed
PostgreSQL 16 3888 0
MySQL 8.0.46 one pre-existing failure fixed, none introduced

The remaining MySQL failures are unrelated to search and pre-date this
change (test-only: an FK-backed index drop, JSON key ordering,
tinyint(1) vs boolean, a 2099 date sentinel, and a DB::listen
predicate that hardcodes Postgres identifier quoting). Happy to open a
follow-up if a MySQL CI leg would be welcome.

Tests

Adds case-insensitivity assertions to the five affected suites that
lacked them, and search coverage for ListPostsTool, which had none.

Note for MySQL installs

like is case-insensitive by virtue of the column collation, not the
operator. Under the default utf8mb4_unicode_ci it is also
accent-insensitive, so searching cafe matches a stored café
PostgreSQL's ilike does not. That comes from the collation rather than
this change. A _bin or _cs collation would make search
case-sensitive on both engines.

jonto and others added 2 commits August 25, 2026 23:38
Seven search call sites used the `ilike` operator, which only PostgreSQL
understands. On MySQL they raise a syntax error, so post, asset, label,
signature and workspace-member search — plus the MCP list-posts tool —
were unusable on an engine `config/database.php` has always supported and
the docs advertise.

Replace them with `whereLike($column, $value)`, which the query grammars
translate per driver: PostgresGrammar emits `ilike` and MySqlGrammar emits
`like`. The generated SQL on PostgreSQL is therefore unchanged.

Verified by running the full suite on both engines:

  PostgreSQL 16    3888 passed, 0 failed
  MySQL 8.0.46     one pre-existing failure fixed, none introduced

Also adds case-insensitivity assertions to the five affected suites that
lacked them, and search coverage for ListPostsTool, which had none.

Note for MySQL installs: `like` is case-insensitive by virtue of the
column collation, not the operator. Under the default `utf8mb4_unicode_ci`
it is also accent-insensitive, so a search for "cafe" matches a stored
"café" — PostgreSQL's `ilike` does not. That difference comes from the
collation rather than this change. A `_bin` or `_cs` collation would make
search case-sensitive on both.
@paulocastellano

Copy link
Copy Markdown
Contributor

Thank you @jonto , reviewing it!

@paulocastellano

paulocastellano commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Thanks for this, Jamie — really solid contribution. I'm approving it.

I went through it carefully on my side. All seven ilike call sites are covered, and there are no others left in the app: the only other search path, GetAutomationInvocations, was already on whereLike. I also confirmed the generated SQL on PostgreSQL is byte-identical before and after, so this is a genuine no-op for existing installs, and the full suite is green on PG.

On the MySQL side not being fully there yet — you listed the remaining failures (the FK-backed index drop, JSON key ordering, tinyint(1) vs boolean, the 2099 date sentinel, and the DB::listen predicate that hardcodes Postgres identifier quoting) and offered to open a follow-up with a MySQL CI leg. I'd genuinely welcome that. Full MySQL compatibility is something I want for the project, and the CI leg is the piece that matters most: without it, the tests you added here pass identically with or without the fix, so nothing stops ilike from creeping back in later.

If it's easier to split, a first PR with just the MySQL job in the workflow would already be valuable on its own, and the individual test fixes could follow after.

Thanks again for taking the time to dig into this.

@paulocastellano
paulocastellano merged commit 6e10e39 into trypostit:main Aug 26, 2026
3 checks passed
@jonto

jonto commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks Paulo — and thanks for reviewing it that carefully.

Your point about the tests is fair and I should have caught it: the assertions I added run only on PostgreSQL, where ilike satisfies them exactly as well as whereLike does. Revert the seven lines and they still pass. They document the
intent without defending it. The CI leg is what makes them load-bearing, so I'm glad to do it.

One wrinkle worth deciding together before I open anything. The MySQL job on its own would land red, because the 32 remaining failures are already there and none of them are about search:

Count Cause
18 SQLSTATE[HY000] 1553 — MySQL won't drop an index backing an FK. Both DuplicateIdentity*Test files drop the unique index in beforeEach to rehearse the pre-migration state. The migration's up() only adds it, so the forward path is fine; it's the test setup that needs restructuring.
5 MySQL normalises JSON object key order (length, then lexicographic), so toBe([...]) on a meta payload fails. Nothing reads that meta positionally.
4 VerifyUpcomingPostConnectionsTest matches select * from "post_platforms" in a DB::listen predicate. MySQL emits backticks, so the simulated mid-run pause never fires and the job takes the other branch.
3 revoked is tinyint(1) on MySQL and boolean on PostgreSQL, so toBeTrue() sees 1.
2 A 2099-12-31 sentinel against a TIMESTAMP column, which tops out at 2038.

So "workflow job first, fixes after" would put a failing job on main for as long as the fixes take. Three ways I can see:

  1. Add the leg with continue-on-error: true, fix the five classes, then flip it to required in a one-line PR. The protection lands immediately, the job reports honestly, and nothing blocks your merges in the meantime.
  2. Fix the 32 first in one or more PRs, then add the leg already green.
  3. Add the leg scoped to a passing subset and widen it — I'd avoid this, it hides the real state.

I'd suggest (1), but it's your repo and your CI, so tell me which you'd prefer and I'll follow it. Either way the fixes are all confined to tests/ — no application changes — and I'd send them as separate reviewable PRs by class rather than one large one. Happy to start with the 18, since that's the only group needing real thought.

For context on how the number was established: I ran the full suite on both engines, PostgreSQL 16 green at 3888, MySQL 8.0.46 at 3856/32, and used the green PostgreSQL run to confirm every one of the 32 is a genuine engine
difference rather than a flaky test or something local to my machine.

Separately, and at your discretion — while testing the queue on MySQL I hit something that affects any self-hoster who sets QUEUE_CONNECTION=database. PublishToSocialPlatform declares $timeout = 900 with a comment to keep queue timeouts above it, and REDIS_QUEUE_RETRY_AFTER is 960 accordingly, but the database connection's retry_after default is 90. I reproduced a publish running past 90s being handed to a second worker while the first was still going. Happy to open a separate issue with the reproduction if it's useful, or drop it if you'd rather keep the focus here.

@paulocastellano

Copy link
Copy Markdown
Contributor

Thanks for laying it out that clearly, Jamie. The breakdown of the 32 made this an easy call.

Please go ahead and open a separate PR that fixes all of it, so MySQL ends up fully supported. Whether that lands as one PR or split by class is your call — you have a better sense of the shape of the work than I do, and you already offered to start with the 18, which sounds right.

If you want to add a MySQL leg to CI as part of that, very welcome too. I'll leave the sequencing to you. Your continue-on-error first and flip-it-later idea is fine by me if you'd rather have the protection in place while the fixes land.

And yes, please open the issue for the retry_after finding. I checked on my side: the database connection sits at 90 while PublishToSocialPlatform declares a 900s timeout and the Redis connection is set to 960. .env.example ships redis, so the hosted side isn't affected, but any self-hoster on QUEUE_CONNECTION=database hits exactly what you reproduced. That deserves its own thread with the reproduction.

Thanks again for sticking with this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants