Skip to content

fix: #5348 escape literal % in search SQL before Article.objects.raw()#5377

Open
thisismyurl wants to merge 1 commit into
openlibhums:masterfrom
thisismyurl:b-5348-search-term-percent-escaping
Open

fix: #5348 escape literal % in search SQL before Article.objects.raw()#5377
thisismyurl wants to merge 1 commit into
openlibhums:masterfrom
thisismyurl:b-5348-search-term-percent-escaping

Conversation

@thisismyurl

Copy link
Copy Markdown

Hey folks,

Thanks to whoever triaged #5348 — the diagnosis in the issue is spot on and saved me the hunt.

postgres_search() builds its final ordered query by wrapping stringify_queryset() in Article.objects.raw(). The catch is that stringify_queryset() runs the params through cursor.mogrify(), so what comes back is fully-interpolated SQL — every value already baked in as a string literal. When a search term contains a % (someone searching for "50%"), that % is now sitting in the SQL as a literal. Then Article.objects.raw() hands the string to the cursor with params=() — Django's default is an empty tuple, not None — so psycopg runs %-formatting over it again, hits the stray %, and raises TypeError: not enough arguments for format string. Any search for a term with a % in it 500s.

The fix doubles the literal % to %% in the mogrified SQL before it goes into raw(), so they survive that second formatting pass unchanged. Because stringify_queryset() has already interpolated everything, there are no real placeholders left to protect — every % in that string is literal, so doubling them all is safe.

I added a regression test that searches for a %-bearing term and asserts the query executes rather than raising (it guards the Postgres path, since the raw() query only runs there). I confirmed the mechanism directly too — a mogrified string with a literal % raises under %-formatting with empty params, and doubling it clears the error.

One thing I wasn't sure about: I branched this off master, but I noticed recent bug fixes going to r-v1.9.x — happy to retarget or add a backport if that's the flow you'd prefer.

closes #5348

(full disclosure: AI helped me identify the issue and verify my work)

…bjects.raw()

postgres_search() builds its final query by wrapping stringify_queryset()
in Article.objects.raw(). stringify_queryset() returns SQL with all params
already interpolated via cursor.mogrify(), so any '%' left in the string is
literal (e.g. a search term like "50%"). raw() defaults params to () rather
than None, so it re-runs %-formatting on that SQL and raised TypeError on
terms containing '%'. Double the literal '%' so they survive that pass.

Adds a regression test covering a '%'-bearing search term.
Copilot AI review requested due to automatic review settings July 1, 2026 18:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes a Postgres-specific search 500 when the search term contains a literal % by ensuring mogrified SQL passed into Article.objects.raw() survives psycopg’s subsequent %-formatting pass.

Changes:

  • Escape literal percent signs in the fully-interpolated SQL generated by stringify_queryset() before passing it to Article.objects.raw().
  • Add a regression test that executes a search for "50%" to ensure the query evaluates without raising.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/submission/models.py Doubles % characters in mogrified SQL prior to raw() execution to prevent psycopg formatting errors.
src/submission/tests/test_article_search.py Adds a regression test for a %-containing search term to ensure the queryset executes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +198 to +201
if connection.vendor == "sqlite":
# The bug is in postgres_search()'s raw() query; sqlite falls back to
# mysql_search() and never hits the affected code path.
return
Comment thread src/submission/models.py
Comment on lines +894 to +899
# stringify_queryset() returns SQL with every parameter already
# interpolated via cursor.mogrify(), so any '%' left in the string is a
# literal (e.g. from a search term like "50%" or a LIKE pattern). Article
# .objects.raw() defaults params to () rather than None, so it re-runs
# %-formatting on this SQL and chokes on those literal '%'. Double them so
# they survive that pass unchanged. (#5348)
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.

Search with pesky terms result in server error

2 participants