mirror of
https://github.com/django/django.git
synced 2025-10-26 07:06:08 +00:00
Altered query string customization for backends vendors
The new way is trying to call first method 'as_' + connection.vendor. If that doesn't exist, then call as_sql(). Also altered how lookup registration is done. There is now RegisterLookupMixin class that is used by Field, Extract and sql.Aggregate. This allows one to register lookups for extracts and aggregates in the same way lookup registration is done for fields.
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
from copy import copy
|
||||
from datetime import date
|
||||
import unittest
|
||||
|
||||
@@ -6,7 +5,6 @@ from django.test import TestCase
|
||||
from .models import Author
|
||||
from django.db import models
|
||||
from django.db import connection
|
||||
from django.db.backends.utils import add_implementation
|
||||
|
||||
|
||||
class Div3Lookup(models.lookups.Lookup):
|
||||
@@ -27,6 +25,37 @@ class Div3Extract(models.lookups.Extract):
|
||||
return '%s %%%% 3' % (lhs,), lhs_params
|
||||
|
||||
|
||||
class YearExtract(models.lookups.Extract):
|
||||
lookup_name = 'year'
|
||||
|
||||
def as_sql(self, qn, connection):
|
||||
lhs_sql, params = qn.compile(self.lhs)
|
||||
return connection.ops.date_extract_sql('year', lhs_sql), params
|
||||
|
||||
@property
|
||||
def output_type(self):
|
||||
return models.IntegerField()
|
||||
|
||||
|
||||
class YearExact(models.lookups.Lookup):
|
||||
lookup_name = 'exact'
|
||||
|
||||
def as_sql(self, qn, connection):
|
||||
# We will need to skip the extract part, and instead go
|
||||
# directly with the originating field, that is self.lhs.lhs
|
||||
lhs_sql, lhs_params = self.process_lhs(qn, connection, self.lhs.lhs)
|
||||
rhs_sql, rhs_params = self.process_rhs(qn, connection)
|
||||
# Note that we must be careful so that we have params in the
|
||||
# same order as we have the parts in the SQL.
|
||||
params = lhs_params + rhs_params + lhs_params + rhs_params
|
||||
# We use PostgreSQL specific SQL here. Note that we must do the
|
||||
# conversions in SQL instead of in Python to support F() references.
|
||||
return ("%(lhs)s >= (%(rhs)s || '-01-01')::date "
|
||||
"AND %(lhs)s <= (%(rhs)s || '-12-31')::date" %
|
||||
{'lhs': lhs_sql, 'rhs': rhs_sql}, params)
|
||||
YearExtract.register_lookup(YearExact)
|
||||
|
||||
|
||||
class YearLte(models.lookups.LessThanOrEqual):
|
||||
"""
|
||||
The purpose of this lookup is to efficiently compare the year of the field.
|
||||
@@ -44,80 +73,27 @@ class YearLte(models.lookups.LessThanOrEqual):
|
||||
# WHERE somecol <= '2013-12-31')
|
||||
# but also make it work if the rhs_sql is field reference.
|
||||
return "%s <= (%s || '-12-31')::date" % (lhs_sql, rhs_sql), params
|
||||
YearExtract.register_lookup(YearLte)
|
||||
|
||||
|
||||
class YearExtract(models.lookups.Extract):
|
||||
lookup_name = 'year'
|
||||
|
||||
def as_sql(self, qn, connection):
|
||||
lhs_sql, params = qn.compile(self.lhs)
|
||||
return connection.ops.date_extract_sql('year', lhs_sql), params
|
||||
|
||||
@property
|
||||
def output_type(self):
|
||||
return models.IntegerField()
|
||||
|
||||
def get_lookup(self, lookup):
|
||||
if lookup == 'lte':
|
||||
return YearLte
|
||||
elif lookup == 'exact':
|
||||
return YearExact
|
||||
else:
|
||||
return super(YearExtract, self).get_lookup(lookup)
|
||||
|
||||
|
||||
class YearExact(models.lookups.Lookup):
|
||||
def as_sql(self, qn, connection):
|
||||
# We will need to skip the extract part, and instead go
|
||||
# directly with the originating field, that is self.lhs.lhs
|
||||
lhs_sql, lhs_params = self.process_lhs(qn, connection, self.lhs.lhs)
|
||||
rhs_sql, rhs_params = self.process_rhs(qn, connection)
|
||||
# Note that we must be careful so that we have params in the
|
||||
# same order as we have the parts in the SQL.
|
||||
params = []
|
||||
params.extend(lhs_params)
|
||||
params.extend(rhs_params)
|
||||
params.extend(lhs_params)
|
||||
params.extend(rhs_params)
|
||||
# We use PostgreSQL specific SQL here. Note that we must do the
|
||||
# conversions in SQL instead of in Python to support F() references.
|
||||
return ("%(lhs)s >= (%(rhs)s || '-01-01')::date "
|
||||
"AND %(lhs)s <= (%(rhs)s || '-12-31')::date" %
|
||||
{'lhs': lhs_sql, 'rhs': rhs_sql}, params)
|
||||
|
||||
|
||||
@add_implementation(YearExact, 'mysql')
|
||||
def mysql_year_exact(node, qn, connection):
|
||||
lhs_sql, lhs_params = node.process_lhs(qn, connection, node.lhs.lhs)
|
||||
rhs_sql, rhs_params = node.process_rhs(qn, connection)
|
||||
params = []
|
||||
params.extend(lhs_params)
|
||||
params.extend(rhs_params)
|
||||
params.extend(lhs_params)
|
||||
params.extend(rhs_params)
|
||||
return ("%(lhs)s >= str_to_date(concat(%(rhs)s, '-01-01'), '%%%%Y-%%%%m-%%%%d') "
|
||||
"AND %(lhs)s <= str_to_date(concat(%(rhs)s, '-12-31'), '%%%%Y-%%%%m-%%%%d')" %
|
||||
{'lhs': lhs_sql, 'rhs': rhs_sql}, params)
|
||||
# We will register this class temporarily in the test method.
|
||||
|
||||
|
||||
class InMonth(models.lookups.Lookup):
|
||||
"""
|
||||
InMonth matches if the column's month is contained in the value's month.
|
||||
InMonth matches if the column's month is the same as value's month.
|
||||
"""
|
||||
lookup_name = 'inmonth'
|
||||
|
||||
def as_sql(self, qn, connection):
|
||||
lhs, params = self.process_lhs(qn, connection)
|
||||
lhs, lhs_params = self.process_lhs(qn, connection)
|
||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
||||
# We need to be careful so that we get the params in right
|
||||
# places.
|
||||
full_params = params[:]
|
||||
full_params.extend(rhs_params)
|
||||
full_params.extend(params)
|
||||
full_params.extend(rhs_params)
|
||||
params = lhs_params + rhs_params + lhs_params + rhs_params
|
||||
return ("%s >= date_trunc('month', %s) and "
|
||||
"%s < date_trunc('month', %s) + interval '1 months'" %
|
||||
(lhs, rhs, lhs, rhs), full_params)
|
||||
(lhs, rhs, lhs, rhs), params)
|
||||
|
||||
|
||||
class LookupTests(TestCase):
|
||||
@@ -178,44 +154,6 @@ class LookupTests(TestCase):
|
||||
finally:
|
||||
models.DateField._unregister_lookup(InMonth)
|
||||
|
||||
def test_custom_compiles(self):
|
||||
a1 = Author.objects.create(name='a1', age=1)
|
||||
a2 = Author.objects.create(name='a2', age=2)
|
||||
a3 = Author.objects.create(name='a3', age=3)
|
||||
a4 = Author.objects.create(name='a4', age=4)
|
||||
|
||||
class AnotherEqual(models.lookups.Exact):
|
||||
lookup_name = 'anotherequal'
|
||||
models.Field.register_lookup(AnotherEqual)
|
||||
try:
|
||||
@add_implementation(AnotherEqual, connection.vendor)
|
||||
def custom_eq_sql(node, qn, connection):
|
||||
return '1 = 1', []
|
||||
|
||||
self.assertIn('1 = 1', str(Author.objects.filter(name__anotherequal='asdf').query))
|
||||
self.assertQuerysetEqual(
|
||||
Author.objects.filter(name__anotherequal='asdf').order_by('name'),
|
||||
[a1, a2, a3, a4], lambda x: x)
|
||||
|
||||
@add_implementation(AnotherEqual, connection.vendor)
|
||||
def another_custom_eq_sql(node, qn, connection):
|
||||
# If you need to override one method, it seems this is the best
|
||||
# option.
|
||||
node = copy(node)
|
||||
|
||||
class OverriddenAnotherEqual(AnotherEqual):
|
||||
def get_rhs_op(self, connection, rhs):
|
||||
return ' <> %s'
|
||||
node.__class__ = OverriddenAnotherEqual
|
||||
return node.as_sql(qn, connection)
|
||||
self.assertIn(' <> ', str(Author.objects.filter(name__anotherequal='a1').query))
|
||||
self.assertQuerysetEqual(
|
||||
Author.objects.filter(name__anotherequal='a1').order_by('name'),
|
||||
[a2, a3, a4], lambda x: x
|
||||
)
|
||||
finally:
|
||||
models.Field._unregister_lookup(AnotherEqual)
|
||||
|
||||
def test_div3_extract(self):
|
||||
models.IntegerField.register_lookup(Div3Extract)
|
||||
try:
|
||||
@@ -293,11 +231,49 @@ class YearLteTests(TestCase):
|
||||
self.assertIn(
|
||||
'-12-31', str(baseqs.filter(birthdate__year__lte=2011).query))
|
||||
|
||||
@unittest.skipUnless(connection.vendor == 'mysql', 'MySQL specific SQL used')
|
||||
def test_mysql_year_exact(self):
|
||||
self.assertQuerysetEqual(
|
||||
Author.objects.filter(birthdate__year=2012).order_by('name'),
|
||||
[self.a2, self.a3, self.a4], lambda x: x)
|
||||
def test_postgres_year_exact(self):
|
||||
baseqs = Author.objects.order_by('name')
|
||||
self.assertIn(
|
||||
'concat(',
|
||||
str(Author.objects.filter(birthdate__year=2012).query))
|
||||
'= (2011 || ', str(baseqs.filter(birthdate__year=2011).query))
|
||||
self.assertIn(
|
||||
'-12-31', str(baseqs.filter(birthdate__year=2011).query))
|
||||
|
||||
def test_custom_implementation_year_exact(self):
|
||||
try:
|
||||
# Two ways to add a customized implementation for different backends:
|
||||
# First is MonkeyPatch of the class.
|
||||
def as_custom_sql(self, qn, connection):
|
||||
lhs_sql, lhs_params = self.process_lhs(qn, connection, self.lhs.lhs)
|
||||
rhs_sql, rhs_params = self.process_rhs(qn, connection)
|
||||
params = lhs_params + rhs_params + lhs_params + rhs_params
|
||||
return ("%(lhs)s >= str_to_date(concat(%(rhs)s, '-01-01'), '%%%%Y-%%%%m-%%%%d') "
|
||||
"AND %(lhs)s <= str_to_date(concat(%(rhs)s, '-12-31'), '%%%%Y-%%%%m-%%%%d')" %
|
||||
{'lhs': lhs_sql, 'rhs': rhs_sql}, params)
|
||||
setattr(YearExact, 'as_' + connection.vendor, as_custom_sql)
|
||||
self.assertIn(
|
||||
'concat(',
|
||||
str(Author.objects.filter(birthdate__year=2012).query))
|
||||
finally:
|
||||
delattr(YearExact, 'as_' + connection.vendor)
|
||||
try:
|
||||
# The other way is to subclass the original lookup and register the subclassed
|
||||
# lookup instead of the original.
|
||||
class CustomYearExact(YearExact):
|
||||
# This method should be named "as_mysql" for MySQL, "as_postgresql" for postgres
|
||||
# and so on, but as we don't know which DB we are running on, we need to use
|
||||
# setattr.
|
||||
def as_custom_sql(self, qn, connection):
|
||||
lhs_sql, lhs_params = self.process_lhs(qn, connection, self.lhs.lhs)
|
||||
rhs_sql, rhs_params = self.process_rhs(qn, connection)
|
||||
params = lhs_params + rhs_params + lhs_params + rhs_params
|
||||
return ("%(lhs)s >= str_to_date(CONCAT(%(rhs)s, '-01-01'), '%%%%Y-%%%%m-%%%%d') "
|
||||
"AND %(lhs)s <= str_to_date(CONCAT(%(rhs)s, '-12-31'), '%%%%Y-%%%%m-%%%%d')" %
|
||||
{'lhs': lhs_sql, 'rhs': rhs_sql}, params)
|
||||
setattr(CustomYearExact, 'as_' + connection.vendor, CustomYearExact.as_custom_sql)
|
||||
YearExtract.register_lookup(CustomYearExact)
|
||||
self.assertIn(
|
||||
'CONCAT(',
|
||||
str(Author.objects.filter(birthdate__year=2012).query))
|
||||
finally:
|
||||
YearExtract._unregister_lookup(CustomYearExact)
|
||||
YearExtract.register_lookup(YearExact)
|
||||
|
||||
Reference in New Issue
Block a user