Proof-of-concept fix for #16731
Implemented only for SQLite and PostgreSQL, and only for startswith and istartswith lookups.
This commit is contained in:
parent
193cd097ca
commit
1016159f34
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue