fix: #5348 escape literal % in search SQL before Article.objects.raw()#5377
Open
thisismyurl wants to merge 1 commit into
Open
fix: #5348 escape literal % in search SQL before Article.objects.raw()#5377thisismyurl wants to merge 1 commit into
thisismyurl wants to merge 1 commit into
Conversation
…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.
There was a problem hiding this comment.
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 toArticle.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 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 wrappingstringify_queryset()inArticle.objects.raw(). The catch is thatstringify_queryset()runs the params throughcursor.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. ThenArticle.objects.raw()hands the string to the cursor withparams=()— Django's default is an empty tuple, notNone— so psycopg runs%-formatting over it again, hits the stray%, and raisesTypeError: 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 intoraw(), so they survive that second formatting pass unchanged. Becausestringify_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 theraw()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 tor-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)