Proof-of-concept fix for #16731

Implemented only for SQLite and PostgreSQL, and only for startswith
and istartswith lookups.
This commit is contained in:
Anssi Kääriäinen 2013-12-28 18:37:04 +02:00
parent 193cd097ca
commit 1016159f34
5 changed files with 47 additions and 3 deletions

View File

@ -677,6 +677,9 @@ class BaseDatabaseFeatures(object):
# What kind of error does the backend throw when accessing closed cursor? # What kind of error does the backend throw when accessing closed cursor?
closed_cursor_error_class = ProgrammingError closed_cursor_error_class = ProgrammingError
# Does 'a' LIKE 'A' match?
has_case_insensitive_like = True
def __init__(self, connection): def __init__(self, connection):
self.connection = connection self.connection = connection

View File

@ -62,6 +62,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_combined_alters = True supports_combined_alters = True
nulls_order_largest = True nulls_order_largest = True
closed_cursor_error_class = InterfaceError closed_cursor_error_class = InterfaceError
has_case_insensitive_like = False
class DatabaseWrapper(BaseDatabaseWrapper): class DatabaseWrapper(BaseDatabaseWrapper):
@ -83,6 +84,11 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': 'LIKE UPPER(%s)', 'iendswith': 'LIKE UPPER(%s)',
} }
pattern_ops = {
'startswith': "LIKE %s || '%%%%'",
'istartswith': "LIKE UPPER(%s) || '%%%%'",
}
Database = Database Database = Database
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -334,6 +334,11 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': "LIKE %s ESCAPE '\\'", 'iendswith': "LIKE %s ESCAPE '\\'",
} }
pattern_ops = {
'startswith': "LIKE %s || '%%%%'",
'istartswith': "LIKE UPPER(%s) || '%%%%'",
}
Database = Database Database = Database
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -158,12 +158,30 @@ class In(BuiltinLookup):
default_lookups['in'] = In 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' lookup_name = 'startswith'
default_lookups['startswith'] = StartsWith default_lookups['startswith'] = StartsWith
class IStartsWith(BuiltinLookup): class IStartsWith(PatternLookup):
lookup_name = 'istartswith' lookup_name = 'istartswith'
default_lookups['istartswith'] = IStartsWith default_lookups['istartswith'] = IStartsWith

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.db.models import F from django.db.models import F
from django.db import transaction from django.db import transaction
from django.test import TestCase from django.test import TestCase, skipIfDBFeature
from django.utils import six from django.utils import six
from .models import Company, Employee from .models import Company, Employee
@ -231,6 +231,18 @@ class ExpressionsTests(TestCase):
queryset = Employee.objects.filter(firstname__iexact=F('lastname')) queryset = Employee.objects.filter(firstname__iexact=F('lastname'))
self.assertQuerysetEqual(queryset, ["<Employee: Test test>"]) self.assertQuerysetEqual(queryset, ["<Employee: Test test>"])
@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): def test_ticket_18375_join_reuse(self):
# Test that reverse multijoin F() references and the lookup target # Test that reverse multijoin F() references and the lookup target
# the same join. Pre #18375 the F() join was generated first, and the # the same join. Pre #18375 the F() join was generated first, and the