Fixed #16731 -- Made pattern lookups work properly with F() expressions
This commit is contained in:
parent
f39b0421b4
commit
6b5d82749c
|
@ -425,6 +425,24 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
'iendswith': 'LIKE %s',
|
'iendswith': 'LIKE %s',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The patterns below are used to generate SQL pattern lookup clauses when
|
||||||
|
# the right-hand side of the lookup isn't a raw string (it might be an expression
|
||||||
|
# or the result of a bilateral transformation).
|
||||||
|
# In those cases, special characters for LIKE operators (e.g. \, *, _) should be
|
||||||
|
# escaped on database side.
|
||||||
|
#
|
||||||
|
# Note: we use str.format() here for readability as '%' is used as a wildcard for
|
||||||
|
# the LIKE operator.
|
||||||
|
pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\\', '\\\\'), '%%', '\%%'), '_', '\_')"
|
||||||
|
pattern_ops = {
|
||||||
|
'contains': "LIKE BINARY CONCAT('%%', {}, '%%')",
|
||||||
|
'icontains': "LIKE CONCAT('%%', {}, '%%')",
|
||||||
|
'startswith': "LIKE BINARY CONCAT({}, '%%')",
|
||||||
|
'istartswith': "LIKE CONCAT({}, '%%')",
|
||||||
|
'endswith': "LIKE BINARY CONCAT('%%', {})",
|
||||||
|
'iendswith': "LIKE CONCAT('%%', {})",
|
||||||
|
}
|
||||||
|
|
||||||
Database = Database
|
Database = Database
|
||||||
SchemaEditorClass = DatabaseSchemaEditor
|
SchemaEditorClass = DatabaseSchemaEditor
|
||||||
|
|
||||||
|
|
|
@ -607,6 +607,30 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
'iendswith': "LIKEC UPPER(%s) ESCAPE '\\'",
|
'iendswith': "LIKEC UPPER(%s) ESCAPE '\\'",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# The patterns below are used to generate SQL pattern lookup clauses when
|
||||||
|
# the right-hand side of the lookup isn't a raw string (it might be an expression
|
||||||
|
# or the result of a bilateral transformation).
|
||||||
|
# In those cases, special characters for LIKE operators (e.g. \, *, _) should be
|
||||||
|
# escaped on database side.
|
||||||
|
#
|
||||||
|
# Note: we use str.format() here for readability as '%' is used as a wildcard for
|
||||||
|
# the LIKE operator.
|
||||||
|
pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\', '\\'), '%%', '\%%'), '_', '\_')"
|
||||||
|
_pattern_ops = {
|
||||||
|
'contains': "'%%' || {} || '%%'",
|
||||||
|
'icontains': "'%%' || UPPER({}) || '%%'",
|
||||||
|
'startswith': "{} || '%%'",
|
||||||
|
'istartswith': "UPPER({}) || '%%'",
|
||||||
|
'endswith': "'%%' || {}",
|
||||||
|
'iendswith': "'%%' || UPPER({})",
|
||||||
|
}
|
||||||
|
|
||||||
|
_standard_pattern_ops = {k: "LIKE TRANSLATE( " + v + " USING NCHAR_CS)"
|
||||||
|
" ESCAPE TRANSLATE('\\' USING NCHAR_CS)"
|
||||||
|
for k, v in _pattern_ops.items()}
|
||||||
|
_likec_pattern_ops = {k: "LIKEC " + v + " ESCAPE '\\'"
|
||||||
|
for k, v in _pattern_ops.items()}
|
||||||
|
|
||||||
Database = Database
|
Database = Database
|
||||||
SchemaEditorClass = DatabaseSchemaEditor
|
SchemaEditorClass = DatabaseSchemaEditor
|
||||||
|
|
||||||
|
@ -674,8 +698,10 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
['X'])
|
['X'])
|
||||||
except DatabaseError:
|
except DatabaseError:
|
||||||
self.operators = self._likec_operators
|
self.operators = self._likec_operators
|
||||||
|
self.pattern_ops = self._likec_pattern_ops
|
||||||
else:
|
else:
|
||||||
self.operators = self._standard_operators
|
self.operators = self._standard_operators
|
||||||
|
self.pattern_ops = self._standard_pattern_ops
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -86,9 +86,22 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
'iendswith': 'LIKE UPPER(%s)',
|
'iendswith': 'LIKE UPPER(%s)',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The patterns below are used to generate SQL pattern lookup clauses when
|
||||||
|
# the right-hand side of the lookup isn't a raw string (it might be an expression
|
||||||
|
# or the result of a bilateral transformation).
|
||||||
|
# In those cases, special characters for LIKE operators (e.g. \, *, _) should be
|
||||||
|
# escaped on database side.
|
||||||
|
#
|
||||||
|
# Note: we use str.format() here for readability as '%' is used as a wildcard for
|
||||||
|
# the LIKE operator.
|
||||||
|
pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\', '\\'), '%%', '\%%'), '_', '\_')"
|
||||||
pattern_ops = {
|
pattern_ops = {
|
||||||
'startswith': "LIKE %s || '%%%%'",
|
'contains': "LIKE '%%' || {} || '%%'",
|
||||||
'istartswith': "LIKE UPPER(%s) || '%%%%'",
|
'icontains': "LIKE '%%' || UPPER({}) || '%%'",
|
||||||
|
'startswith': "LIKE {} || '%%'",
|
||||||
|
'istartswith': "LIKE UPPER({}) || '%%'",
|
||||||
|
'endswith': "LIKE '%%' || {}",
|
||||||
|
'iendswith': "LIKE '%%' || UPPER({})",
|
||||||
}
|
}
|
||||||
|
|
||||||
Database = Database
|
Database = Database
|
||||||
|
|
|
@ -343,9 +343,22 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
'iendswith': "LIKE %s ESCAPE '\\'",
|
'iendswith': "LIKE %s ESCAPE '\\'",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The patterns below are used to generate SQL pattern lookup clauses when
|
||||||
|
# the right-hand side of the lookup isn't a raw string (it might be an expression
|
||||||
|
# or the result of a bilateral transformation).
|
||||||
|
# In those cases, special characters for LIKE operators (e.g. \, *, _) should be
|
||||||
|
# escaped on database side.
|
||||||
|
#
|
||||||
|
# Note: we use str.format() here for readability as '%' is used as a wildcard for
|
||||||
|
# the LIKE operator.
|
||||||
|
pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\', '\\'), '%%', '\%%'), '_', '\_')"
|
||||||
pattern_ops = {
|
pattern_ops = {
|
||||||
'startswith': "LIKE %s || '%%%%'",
|
'contains': r"LIKE '%%' || {} || '%%' ESCAPE '\'",
|
||||||
'istartswith': "LIKE UPPER(%s) || '%%%%'",
|
'icontains': r"LIKE '%%' || UPPER({}) || '%%' ESCAPE '\'",
|
||||||
|
'startswith': r"LIKE {} || '%%' ESCAPE '\'",
|
||||||
|
'istartswith': r"LIKE UPPER({}) || '%%' ESCAPE '\'",
|
||||||
|
'endswith': r"LIKE '%%' || {} ESCAPE '\'",
|
||||||
|
'iendswith': r"LIKE '%%' || UPPER({}) ESCAPE '\'",
|
||||||
}
|
}
|
||||||
|
|
||||||
Database = Database
|
Database = Database
|
||||||
|
|
|
@ -225,16 +225,6 @@ class IExact(BuiltinLookup):
|
||||||
default_lookups['iexact'] = IExact
|
default_lookups['iexact'] = IExact
|
||||||
|
|
||||||
|
|
||||||
class Contains(BuiltinLookup):
|
|
||||||
lookup_name = 'contains'
|
|
||||||
default_lookups['contains'] = Contains
|
|
||||||
|
|
||||||
|
|
||||||
class IContains(BuiltinLookup):
|
|
||||||
lookup_name = 'icontains'
|
|
||||||
default_lookups['icontains'] = IContains
|
|
||||||
|
|
||||||
|
|
||||||
class GreaterThan(BuiltinLookup):
|
class GreaterThan(BuiltinLookup):
|
||||||
lookup_name = 'gt'
|
lookup_name = 'gt'
|
||||||
default_lookups['gt'] = GreaterThan
|
default_lookups['gt'] = GreaterThan
|
||||||
|
@ -306,6 +296,7 @@ default_lookups['in'] = In
|
||||||
|
|
||||||
|
|
||||||
class PatternLookup(BuiltinLookup):
|
class PatternLookup(BuiltinLookup):
|
||||||
|
|
||||||
def get_rhs_op(self, connection, rhs):
|
def get_rhs_op(self, connection, rhs):
|
||||||
# Assume we are in startswith. We need to produce SQL like:
|
# Assume we are in startswith. We need to produce SQL like:
|
||||||
# col LIKE %s, ['thevalue%']
|
# col LIKE %s, ['thevalue%']
|
||||||
|
@ -318,11 +309,22 @@ class PatternLookup(BuiltinLookup):
|
||||||
# pattern added.
|
# pattern added.
|
||||||
if (hasattr(self.rhs, 'get_compiler') or hasattr(self.rhs, 'as_sql')
|
if (hasattr(self.rhs, 'get_compiler') or hasattr(self.rhs, 'as_sql')
|
||||||
or hasattr(self.rhs, '_as_sql') or self.bilateral_transforms):
|
or hasattr(self.rhs, '_as_sql') or self.bilateral_transforms):
|
||||||
return connection.pattern_ops[self.lookup_name] % rhs
|
pattern = connection.pattern_ops[self.lookup_name].format(connection.pattern_esc)
|
||||||
|
return pattern.format(rhs)
|
||||||
else:
|
else:
|
||||||
return super(PatternLookup, self).get_rhs_op(connection, rhs)
|
return super(PatternLookup, self).get_rhs_op(connection, rhs)
|
||||||
|
|
||||||
|
|
||||||
|
class Contains(PatternLookup):
|
||||||
|
lookup_name = 'contains'
|
||||||
|
default_lookups['contains'] = Contains
|
||||||
|
|
||||||
|
|
||||||
|
class IContains(PatternLookup):
|
||||||
|
lookup_name = 'icontains'
|
||||||
|
default_lookups['icontains'] = IContains
|
||||||
|
|
||||||
|
|
||||||
class StartsWith(PatternLookup):
|
class StartsWith(PatternLookup):
|
||||||
lookup_name = 'startswith'
|
lookup_name = 'startswith'
|
||||||
default_lookups['startswith'] = StartsWith
|
default_lookups['startswith'] = StartsWith
|
||||||
|
@ -333,12 +335,12 @@ class IStartsWith(PatternLookup):
|
||||||
default_lookups['istartswith'] = IStartsWith
|
default_lookups['istartswith'] = IStartsWith
|
||||||
|
|
||||||
|
|
||||||
class EndsWith(BuiltinLookup):
|
class EndsWith(PatternLookup):
|
||||||
lookup_name = 'endswith'
|
lookup_name = 'endswith'
|
||||||
default_lookups['endswith'] = EndsWith
|
default_lookups['endswith'] = EndsWith
|
||||||
|
|
||||||
|
|
||||||
class IEndsWith(BuiltinLookup):
|
class IEndsWith(PatternLookup):
|
||||||
lookup_name = 'iendswith'
|
lookup_name = 'iendswith'
|
||||||
default_lookups['iendswith'] = IEndsWith
|
default_lookups['iendswith'] = IEndsWith
|
||||||
|
|
||||||
|
|
|
@ -382,6 +382,12 @@ Models
|
||||||
are applied to both ``lhs`` and ``rhs`` when used in a lookup expression,
|
are applied to both ``lhs`` and ``rhs`` when used in a lookup expression,
|
||||||
providing opportunities for more sophisticated lookups.
|
providing opportunities for more sophisticated lookups.
|
||||||
|
|
||||||
|
* SQL special characters (\, %, _) are now escaped properly when a pattern
|
||||||
|
lookup (e.g. ``contains``, ``startswith``, etc.) is used with an ``F()``
|
||||||
|
expression as the right-hand side. In those cases, the escaping is performed
|
||||||
|
by the database, which can lead to somewhat complex queries involving nested
|
||||||
|
``REPLACE`` function calls.
|
||||||
|
|
||||||
Signals
|
Signals
|
||||||
^^^^^^^
|
^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -286,6 +286,9 @@ class BilateralTransformTests(TestCase):
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
Author.objects.filter(name__upper='doe'),
|
Author.objects.filter(name__upper='doe'),
|
||||||
["<Author: Doe>", "<Author: doe>"], ordered=False)
|
["<Author: Doe>", "<Author: doe>"], ordered=False)
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
Author.objects.filter(name__upper__contains='f'),
|
||||||
|
["<Author: Foo>"], ordered=False)
|
||||||
finally:
|
finally:
|
||||||
models.CharField._unregister_lookup(UpperBilateralTransform)
|
models.CharField._unregister_lookup(UpperBilateralTransform)
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,8 @@ from copy import deepcopy
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
from django.db import connection
|
from django.db import connection, transaction
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.db import transaction
|
|
||||||
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
|
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
|
||||||
from django.test.utils import Approximate
|
from django.test.utils import Approximate
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
@ -311,6 +310,70 @@ class ExpressionsTests(TestCase):
|
||||||
# The original query still works correctly
|
# The original query still works correctly
|
||||||
self.assertEqual(c_qs.get(), c)
|
self.assertEqual(c_qs.get(), c)
|
||||||
|
|
||||||
|
def test_patterns_escape(self):
|
||||||
|
"""
|
||||||
|
Test that special characters (e.g. %, _ and \) stored in database are
|
||||||
|
properly escaped when using a pattern lookup with an expression
|
||||||
|
refs #16731
|
||||||
|
"""
|
||||||
|
Employee.objects.bulk_create([
|
||||||
|
Employee(firstname="%Joh\\nny", lastname="%Joh\\n"),
|
||||||
|
Employee(firstname="Johnny", lastname="%John"),
|
||||||
|
Employee(firstname="Jean-Claude", lastname="Claud_"),
|
||||||
|
Employee(firstname="Jean-Claude", lastname="Claude"),
|
||||||
|
Employee(firstname="Jean-Claude", lastname="Claude%"),
|
||||||
|
Employee(firstname="Johnny", lastname="Joh\\n"),
|
||||||
|
Employee(firstname="Johnny", lastname="John"),
|
||||||
|
Employee(firstname="Johnny", lastname="_ohn"),
|
||||||
|
])
|
||||||
|
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
Employee.objects.filter(firstname__contains=F('lastname')),
|
||||||
|
["<Employee: %Joh\\nny %Joh\\n>", "<Employee: Jean-Claude Claude>", "<Employee: Johnny John>"],
|
||||||
|
ordered=False)
|
||||||
|
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
Employee.objects.filter(firstname__startswith=F('lastname')),
|
||||||
|
["<Employee: %Joh\\nny %Joh\\n>", "<Employee: Johnny John>"],
|
||||||
|
ordered=False)
|
||||||
|
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
Employee.objects.filter(firstname__endswith=F('lastname')),
|
||||||
|
["<Employee: Jean-Claude Claude>"],
|
||||||
|
ordered=False)
|
||||||
|
|
||||||
|
def test_insensitive_patterns_escape(self):
|
||||||
|
"""
|
||||||
|
Test that special characters (e.g. %, _ and \) stored in database are
|
||||||
|
properly escaped when using a case insensitive pattern lookup with an
|
||||||
|
expression -- refs #16731
|
||||||
|
"""
|
||||||
|
Employee.objects.bulk_create([
|
||||||
|
Employee(firstname="%Joh\\nny", lastname="%joh\\n"),
|
||||||
|
Employee(firstname="Johnny", lastname="%john"),
|
||||||
|
Employee(firstname="Jean-Claude", lastname="claud_"),
|
||||||
|
Employee(firstname="Jean-Claude", lastname="claude"),
|
||||||
|
Employee(firstname="Jean-Claude", lastname="claude%"),
|
||||||
|
Employee(firstname="Johnny", lastname="joh\\n"),
|
||||||
|
Employee(firstname="Johnny", lastname="john"),
|
||||||
|
Employee(firstname="Johnny", lastname="_ohn"),
|
||||||
|
])
|
||||||
|
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
Employee.objects.filter(firstname__icontains=F('lastname')),
|
||||||
|
["<Employee: %Joh\\nny %joh\\n>", "<Employee: Jean-Claude claude>", "<Employee: Johnny john>"],
|
||||||
|
ordered=False)
|
||||||
|
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
Employee.objects.filter(firstname__istartswith=F('lastname')),
|
||||||
|
["<Employee: %Joh\\nny %joh\\n>", "<Employee: Johnny john>"],
|
||||||
|
ordered=False)
|
||||||
|
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
Employee.objects.filter(firstname__iendswith=F('lastname')),
|
||||||
|
["<Employee: Jean-Claude claude>"],
|
||||||
|
ordered=False)
|
||||||
|
|
||||||
|
|
||||||
class ExpressionsNumericTests(TestCase):
|
class ExpressionsNumericTests(TestCase):
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue