diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index d8c711d18f..b1fb28a302 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1775,7 +1775,8 @@ class Query(object): entry_params = [] pos = entry.find("%s") while pos != -1: - entry_params.append(next(param_iter)) + if pos == 0 or entry[pos - 1] != '%': + entry_params.append(next(param_iter)) pos = entry.find("%s", pos + 2) select_pairs[name] = (entry, entry_params) # This is order preserving, since self.extra_select is an OrderedDict. diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 294eeb6ef5..c7d3bf6b90 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1144,11 +1144,12 @@ of the arguments is required, but you should use at least one of them. select=OrderedDict([('a', '%s'), ('b', '%s')]), select_params=('one', 'two')) - The only thing to be careful about when using select parameters in - ``extra()`` is to avoid using the substring ``"%%s"`` (that's *two* - percent characters before the ``s``) in the select strings. Django's - tracking of parameters looks for ``%s`` and an escaped ``%`` character - like this isn't detected. That will lead to incorrect results. + If you need to use a literal ``%s`` inside your select string, use + the sequence ``%%s``. + + .. versionchanged:: 1.8 + + Prior to 1.8, you were unable to escape a literal ``%s``. * ``where`` / ``tables`` diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index c77b40a6a3..15fe657d4b 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -281,6 +281,9 @@ Models Django uses whenever objects are loaded using the ORM. The method allows customizing model loading behavior. +* ``extra(select={...})`` now allows you to escape a literal ``%s`` sequence + using ``%%s``. + Signals ^^^^^^^ diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 20c5f5d8d3..406710f8fd 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -1655,6 +1655,21 @@ class Queries5Tests(TestCase): ['', ''] ) + def test_extra_select_literal_percent_s(self): + # Allow %%s to escape select clauses + self.assertEqual( + Note.objects.extra(select={'foo': "'%%s'"})[0].foo, + '%s' + ) + self.assertEqual( + Note.objects.extra(select={'foo': "'%%s bar %%s'"})[0].foo, + '%s bar %s' + ) + self.assertEqual( + Note.objects.extra(select={'foo': "'bar %%s'"})[0].foo, + 'bar %s' + ) + class SelectRelatedTests(TestCase): def test_tickets_3045_3288(self):