diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 89131a4fc4..c3468f0c5f 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -677,6 +677,9 @@ class BaseDatabaseFeatures(object): # What kind of error does the backend throw when accessing closed cursor? closed_cursor_error_class = ProgrammingError + # Does 'a' LIKE 'A' match? + has_case_insensitive_like = True + def __init__(self, connection): self.connection = connection diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index 91e16a1b76..5b5ca4ae4c 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -62,6 +62,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_combined_alters = True nulls_order_largest = True closed_cursor_error_class = InterfaceError + has_case_insensitive_like = False class DatabaseWrapper(BaseDatabaseWrapper): @@ -83,6 +84,11 @@ class DatabaseWrapper(BaseDatabaseWrapper): 'iendswith': 'LIKE UPPER(%s)', } + pattern_ops = { + 'startswith': "LIKE %s || '%%%%'", + 'istartswith': "LIKE UPPER(%s) || '%%%%'", + } + Database = Database def __init__(self, *args, **kwargs): diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 6c9728889f..e55973ea39 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -334,6 +334,11 @@ class DatabaseWrapper(BaseDatabaseWrapper): 'iendswith': "LIKE %s ESCAPE '\\'", } + pattern_ops = { + 'startswith': "LIKE %s || '%%%%'", + 'istartswith': "LIKE UPPER(%s) || '%%%%'", + } + Database = Database def __init__(self, *args, **kwargs): diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py index e736c0332e..00cbcd5173 100644 --- a/django/db/models/lookups.py +++ b/django/db/models/lookups.py @@ -158,12 +158,30 @@ class In(BuiltinLookup): default_lookups['in'] = In -class StartsWith(BuiltinLookup): +class PatternLookup(BuiltinLookup): + def get_rhs_op(self, connection, rhs): + # Assume we are in startswith. We need to produce SQL like: + # col LIKE %s, ['thevalue%'] + # For python values we can (and should) do that directly in Python, + # but if the value is for example reference to other column, then + # we need to add the % pattern match to the lookup by something like + # col LIKE othercol || '%%' + # So, for Python values we don't need any special pattern, but for + # SQL reference values we need the correct pattern added. + value = self.rhs + if (hasattr(value, 'get_compiler') or hasattr(value, 'as_sql') + or hasattr(value, '_as_sql')): + return connection.pattern_ops[self.lookup_name] % rhs + else: + return super(PatternLookup, self).get_rhs_op(connection, rhs) + + +class StartsWith(PatternLookup): lookup_name = 'startswith' default_lookups['startswith'] = StartsWith -class IStartsWith(BuiltinLookup): +class IStartsWith(PatternLookup): lookup_name = 'istartswith' default_lookups['istartswith'] = IStartsWith diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index 0ea4df818c..320271b1dc 100644 --- a/tests/expressions/tests.py +++ b/tests/expressions/tests.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.core.exceptions import FieldError from django.db.models import F from django.db import transaction -from django.test import TestCase +from django.test import TestCase, skipIfDBFeature from django.utils import six from .models import Company, Employee @@ -231,6 +231,18 @@ class ExpressionsTests(TestCase): queryset = Employee.objects.filter(firstname__iexact=F('lastname')) self.assertQuerysetEqual(queryset, [""]) + @skipIfDBFeature('has_case_insensitive_like') + def test_ticket_16731_startswith_lookup(self): + Employee.objects.create(firstname="John", lastname="Doe") + e2 = Employee.objects.create(firstname="Jack", lastname="Jackson") + e3 = Employee.objects.create(firstname="Jack", lastname="jackson") + self.assertQuerysetEqual( + Employee.objects.filter(lastname__startswith=F('firstname')), + [e2], lambda x: x) + self.assertQuerysetEqual( + Employee.objects.filter(lastname__istartswith=F('firstname')).order_by('pk'), + [e2, e3], lambda x: x) + def test_ticket_18375_join_reuse(self): # Test that reverse multijoin F() references and the lookup target # the same join. Pre #18375 the F() join was generated first, and the