import time import unittest from datetime import date, datetime from django.core.exceptions import FieldError from django.db import connection, models from django.test import SimpleTestCase, TestCase, override_settings from django.test.utils import register_lookup from django.utils import timezone from .models import Article, Author, MySQLUnixTimestamp class Div3Lookup(models.Lookup): lookup_name = "div3" def as_sql(self, compiler, connection): lhs, params = self.process_lhs(compiler, connection) rhs, rhs_params = self.process_rhs(compiler, connection) params.extend(rhs_params) return "(%s) %%%% 3 = %s" % (lhs, rhs), params def as_oracle(self, compiler, connection): lhs, params = self.process_lhs(compiler, connection) rhs, rhs_params = self.process_rhs(compiler, connection) params.extend(rhs_params) return "mod(%s, 3) = %s" % (lhs, rhs), params class Div3Transform(models.Transform): lookup_name = "div3" def as_sql(self, compiler, connection): lhs, lhs_params = compiler.compile(self.lhs) return "(%s) %%%% 3" % lhs, lhs_params def as_oracle(self, compiler, connection, **extra_context): lhs, lhs_params = compiler.compile(self.lhs) return "mod(%s, 3)" % lhs, lhs_params class Div3BilateralTransform(Div3Transform): bilateral = True class Mult3BilateralTransform(models.Transform): bilateral = True lookup_name = "mult3" def as_sql(self, compiler, connection): lhs, lhs_params = compiler.compile(self.lhs) return "3 * (%s)" % lhs, lhs_params class LastDigitTransform(models.Transform): lookup_name = "lastdigit" def as_sql(self, compiler, connection): lhs, lhs_params = compiler.compile(self.lhs) return "SUBSTR(CAST(%s AS CHAR(2)), 2, 1)" % lhs, lhs_params class UpperBilateralTransform(models.Transform): bilateral = True lookup_name = "upper" def as_sql(self, compiler, connection): lhs, lhs_params = compiler.compile(self.lhs) return "UPPER(%s)" % lhs, lhs_params class YearTransform(models.Transform): # Use a name that avoids collision with the built-in year lookup. lookup_name = "testyear" def as_sql(self, compiler, connection): lhs_sql, params = compiler.compile(self.lhs) return connection.ops.date_extract_sql("year", lhs_sql, params) @property def output_field(self): return models.IntegerField() @YearTransform.register_lookup class YearExact(models.lookups.Lookup): lookup_name = "exact" def as_sql(self, compiler, 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(compiler, connection, self.lhs.lhs) rhs_sql, rhs_params = self.process_rhs(compiler, 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, ) @YearTransform.register_lookup class YearLte(models.lookups.LessThanOrEqual): """ The purpose of this lookup is to efficiently compare the year of the field. """ def as_sql(self, compiler, connection): # Skip the YearTransform above us (no possibility for efficient # lookup otherwise). real_lhs = self.lhs.lhs lhs_sql, params = self.process_lhs(compiler, connection, real_lhs) rhs_sql, rhs_params = self.process_rhs(compiler, connection) params.extend(rhs_params) # Build SQL where the integer year is concatenated with last month # and day, then convert that to date. (We try to have SQL like: # 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 class Exactly(models.lookups.Exact): """ This lookup is used to test lookup registration. """ lookup_name = "exactly" def get_rhs_op(self, connection, rhs): return connection.operators["exact"] % rhs class SQLFuncMixin: def as_sql(self, compiler, connection): return "%s()" % self.name, [] @property def output_field(self): return CustomField() class SQLFuncLookup(SQLFuncMixin, models.Lookup): def __init__(self, name, *args, **kwargs): super().__init__(*args, **kwargs) self.name = name class SQLFuncTransform(SQLFuncMixin, models.Transform): def __init__(self, name, *args, **kwargs): super().__init__(*args, **kwargs) self.name = name class SQLFuncFactory: def __init__(self, key, name): self.key = key self.name = name def __call__(self, *args, **kwargs): if self.key == "lookupfunc": return SQLFuncLookup(self.name, *args, **kwargs) return SQLFuncTransform(self.name, *args, **kwargs) class CustomField(models.TextField): def get_lookup(self, lookup_name): if lookup_name.startswith("lookupfunc_"): key, name = lookup_name.split("_", 1) return SQLFuncFactory(key, name) return super().get_lookup(lookup_name) def get_transform(self, lookup_name): if lookup_name.startswith("transformfunc_"): key, name = lookup_name.split("_", 1) return SQLFuncFactory(key, name) return super().get_transform(lookup_name) class CustomModel(models.Model): field = CustomField() # We will register this class temporarily in the test method. class InMonth(models.lookups.Lookup): """ InMonth matches if the column's month is the same as value's month. """ lookup_name = "inmonth" def as_sql(self, compiler, connection): lhs, lhs_params = self.process_lhs(compiler, connection) rhs, rhs_params = self.process_rhs(compiler, connection) # We need to be careful so that we get the params in right # places. 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), params, ) class DateTimeTransform(models.Transform): lookup_name = "as_datetime" @property def output_field(self): return models.DateTimeField() def as_sql(self, compiler, connection): lhs, params = compiler.compile(self.lhs) return "from_unixtime({})".format(lhs), params class LookupTests(TestCase): def test_custom_name_lookup(self): a1 = Author.objects.create(name="a1", birthdate=date(1981, 2, 16)) Author.objects.create(name="a2", birthdate=date(2012, 2, 29)) with register_lookup(models.DateField, YearTransform), register_lookup( models.DateField, YearTransform, lookup_name="justtheyear" ), register_lookup(YearTransform, Exactly), register_lookup( YearTransform, Exactly, lookup_name="isactually" ): qs1 = Author.objects.filter(birthdate__testyear__exactly=1981) qs2 = Author.objects.filter(birthdate__justtheyear__isactually=1981) self.assertSequenceEqual(qs1, [a1]) self.assertSequenceEqual(qs2, [a1]) def test_custom_exact_lookup_none_rhs(self): """ __exact=None is transformed to __isnull=True if a custom lookup class with lookup_name != 'exact' is registered as the `exact` lookup. """ field = Author._meta.get_field("birthdate") OldExactLookup = field.get_lookup("exact") author = Author.objects.create(name="author", birthdate=None) try: field.register_lookup(Exactly, "exact") self.assertEqual(Author.objects.get(birthdate__exact=None), author) finally: field.register_lookup(OldExactLookup, "exact") def test_basic_lookup(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) with register_lookup(models.IntegerField, Div3Lookup): self.assertSequenceEqual(Author.objects.filter(age__div3=0), [a3]) self.assertSequenceEqual( Author.objects.filter(age__div3=1).order_by("age"), [a1, a4] ) self.assertSequenceEqual(Author.objects.filter(age__div3=2), [a2]) self.assertSequenceEqual(Author.objects.filter(age__div3=3), []) @unittest.skipUnless( connection.vendor == "postgresql", "PostgreSQL specific SQL used" ) def test_birthdate_month(self): a1 = Author.objects.create(name="a1", birthdate=date(1981, 2, 16)) a2 = Author.objects.create(name="a2", birthdate=date(2012, 2, 29)) a3 = Author.objects.create(name="a3", birthdate=date(2012, 1, 31)) a4 = Author.objects.create(name="a4", birthdate=date(2012, 3, 1)) with register_lookup(models.DateField, InMonth): self.assertSequenceEqual( Author.objects.filter(birthdate__inmonth=date(2012, 1, 15)), [a3] ) self.assertSequenceEqual( Author.objects.filter(birthdate__inmonth=date(2012, 2, 1)), [a2] ) self.assertSequenceEqual( Author.objects.filter(birthdate__inmonth=date(1981, 2, 28)), [a1] ) self.assertSequenceEqual( Author.objects.filter(birthdate__inmonth=date(2012, 3, 12)), [a4] ) self.assertSequenceEqual( Author.objects.filter(birthdate__inmonth=date(2012, 4, 1)), [] ) def test_div3_extract(self): with register_lookup(models.IntegerField, Div3Transform): 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) baseqs = Author.objects.order_by("name") self.assertSequenceEqual(baseqs.filter(age__div3=2), [a2]) self.assertSequenceEqual(baseqs.filter(age__div3__lte=3), [a1, a2, a3, a4]) self.assertSequenceEqual(baseqs.filter(age__div3__in=[0, 2]), [a2, a3]) self.assertSequenceEqual(baseqs.filter(age__div3__in=[2, 4]), [a2]) self.assertSequenceEqual(baseqs.filter(age__div3__gte=3), []) self.assertSequenceEqual( baseqs.filter(age__div3__range=(1, 2)), [a1, a2, a4] ) def test_foreignobject_lookup_registration(self): field = Article._meta.get_field("author") with register_lookup(models.ForeignObject, Exactly): self.assertIs(field.get_lookup("exactly"), Exactly) # ForeignObject should ignore regular Field lookups with register_lookup(models.Field, Exactly): self.assertIsNone(field.get_lookup("exactly")) def test_lookups_caching(self): field = Article._meta.get_field("author") # clear and re-cache field.get_lookups.cache_clear() self.assertNotIn("exactly", field.get_lookups()) # registration should bust the cache with register_lookup(models.ForeignObject, Exactly): # getting the lookups again should re-cache self.assertIn("exactly", field.get_lookups()) # Unregistration should bust the cache. self.assertNotIn("exactly", field.get_lookups()) class BilateralTransformTests(TestCase): def test_bilateral_upper(self): with register_lookup(models.CharField, UpperBilateralTransform): author1 = Author.objects.create(name="Doe") author2 = Author.objects.create(name="doe") author3 = Author.objects.create(name="Foo") self.assertCountEqual( Author.objects.filter(name__upper="doe"), [author1, author2], ) self.assertSequenceEqual( Author.objects.filter(name__upper__contains="f"), [author3], ) def test_bilateral_inner_qs(self): with register_lookup(models.CharField, UpperBilateralTransform): msg = "Bilateral transformations on nested querysets are not implemented." with self.assertRaisesMessage(NotImplementedError, msg): Author.objects.filter( name__upper__in=Author.objects.values_list("name") ) def test_bilateral_multi_value(self): with register_lookup(models.CharField, UpperBilateralTransform): Author.objects.bulk_create( [ Author(name="Foo"), Author(name="Bar"), Author(name="Ray"), ] ) self.assertQuerysetEqual( Author.objects.filter(name__upper__in=["foo", "bar", "doe"]).order_by( "name" ), ["Bar", "Foo"], lambda a: a.name, ) def test_div3_bilateral_extract(self): with register_lookup(models.IntegerField, Div3BilateralTransform): 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) baseqs = Author.objects.order_by("name") self.assertSequenceEqual(baseqs.filter(age__div3=2), [a2]) self.assertSequenceEqual(baseqs.filter(age__div3__lte=3), [a3]) self.assertSequenceEqual(baseqs.filter(age__div3__in=[0, 2]), [a2, a3]) self.assertSequenceEqual(baseqs.filter(age__div3__in=[2, 4]), [a1, a2, a4]) self.assertSequenceEqual(baseqs.filter(age__div3__gte=3), [a1, a2, a3, a4]) self.assertSequenceEqual( baseqs.filter(age__div3__range=(1, 2)), [a1, a2, a4] ) def test_bilateral_order(self): with register_lookup( models.IntegerField, Mult3BilateralTransform, Div3BilateralTransform ): 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) baseqs = Author.objects.order_by("name") # mult3__div3 always leads to 0 self.assertSequenceEqual( baseqs.filter(age__mult3__div3=42), [a1, a2, a3, a4] ) self.assertSequenceEqual(baseqs.filter(age__div3__mult3=42), [a3]) def test_transform_order_by(self): with register_lookup(models.IntegerField, LastDigitTransform): a1 = Author.objects.create(name="a1", age=11) a2 = Author.objects.create(name="a2", age=23) a3 = Author.objects.create(name="a3", age=32) a4 = Author.objects.create(name="a4", age=40) qs = Author.objects.order_by("age__lastdigit") self.assertSequenceEqual(qs, [a4, a1, a3, a2]) def test_bilateral_fexpr(self): with register_lookup(models.IntegerField, Mult3BilateralTransform): a1 = Author.objects.create(name="a1", age=1, average_rating=3.2) a2 = Author.objects.create(name="a2", age=2, average_rating=0.5) a3 = Author.objects.create(name="a3", age=3, average_rating=1.5) a4 = Author.objects.create(name="a4", age=4) baseqs = Author.objects.order_by("name") self.assertSequenceEqual( baseqs.filter(age__mult3=models.F("age")), [a1, a2, a3, a4] ) # Same as age >= average_rating self.assertSequenceEqual( baseqs.filter(age__mult3__gte=models.F("average_rating")), [a2, a3] ) @override_settings(USE_TZ=True) class DateTimeLookupTests(TestCase): @unittest.skipUnless(connection.vendor == "mysql", "MySQL specific SQL used") def test_datetime_output_field(self): with register_lookup(models.PositiveIntegerField, DateTimeTransform): ut = MySQLUnixTimestamp.objects.create(timestamp=time.time()) y2k = timezone.make_aware(datetime(2000, 1, 1)) self.assertSequenceEqual( MySQLUnixTimestamp.objects.filter(timestamp__as_datetime__gt=y2k), [ut] ) class YearLteTests(TestCase): @classmethod def setUpTestData(cls): cls.a1 = Author.objects.create(name="a1", birthdate=date(1981, 2, 16)) cls.a2 = Author.objects.create(name="a2", birthdate=date(2012, 2, 29)) cls.a3 = Author.objects.create(name="a3", birthdate=date(2012, 1, 31)) cls.a4 = Author.objects.create(name="a4", birthdate=date(2012, 3, 1)) def setUp(self): models.DateField.register_lookup(YearTransform) def tearDown(self): models.DateField._unregister_lookup(YearTransform) @unittest.skipUnless( connection.vendor == "postgresql", "PostgreSQL specific SQL used" ) def test_year_lte(self): baseqs = Author.objects.order_by("name") self.assertSequenceEqual( baseqs.filter(birthdate__testyear__lte=2012), [self.a1, self.a2, self.a3, self.a4], ) self.assertSequenceEqual( baseqs.filter(birthdate__testyear=2012), [self.a2, self.a3, self.a4] ) self.assertNotIn("BETWEEN", str(baseqs.filter(birthdate__testyear=2012).query)) self.assertSequenceEqual( baseqs.filter(birthdate__testyear__lte=2011), [self.a1] ) # The non-optimized version works, too. self.assertSequenceEqual(baseqs.filter(birthdate__testyear__lt=2012), [self.a1]) @unittest.skipUnless( connection.vendor == "postgresql", "PostgreSQL specific SQL used" ) def test_year_lte_fexpr(self): self.a2.age = 2011 self.a2.save() self.a3.age = 2012 self.a3.save() self.a4.age = 2013 self.a4.save() baseqs = Author.objects.order_by("name") self.assertSequenceEqual( baseqs.filter(birthdate__testyear__lte=models.F("age")), [self.a3, self.a4] ) self.assertSequenceEqual( baseqs.filter(birthdate__testyear__lt=models.F("age")), [self.a4] ) def test_year_lte_sql(self): # This test will just check the generated SQL for __lte. This # doesn't require running on PostgreSQL and spots the most likely # error - not running YearLte SQL at all. baseqs = Author.objects.order_by("name") self.assertIn( "<= (2011 || ", str(baseqs.filter(birthdate__testyear__lte=2011).query) ) self.assertIn("-12-31", str(baseqs.filter(birthdate__testyear__lte=2011).query)) def test_postgres_year_exact(self): baseqs = Author.objects.order_by("name") self.assertIn("= (2011 || ", str(baseqs.filter(birthdate__testyear=2011).query)) self.assertIn("-12-31", str(baseqs.filter(birthdate__testyear=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, compiler, connection): lhs_sql, lhs_params = self.process_lhs( compiler, connection, self.lhs.lhs ) rhs_sql, rhs_params = self.process_rhs(compiler, 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__testyear=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, compiler, connection): lhs_sql, lhs_params = self.process_lhs( compiler, connection, self.lhs.lhs ) rhs_sql, rhs_params = self.process_rhs(compiler, 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, ) YearTransform.register_lookup(CustomYearExact) self.assertIn( "CONCAT(", str(Author.objects.filter(birthdate__testyear=2012).query) ) finally: YearTransform._unregister_lookup(CustomYearExact) YearTransform.register_lookup(YearExact) class TrackCallsYearTransform(YearTransform): # Use a name that avoids collision with the built-in year lookup. lookup_name = "testyear" call_order = [] def as_sql(self, compiler, connection): lhs_sql, params = compiler.compile(self.lhs) return connection.ops.date_extract_sql("year", lhs_sql), params @property def output_field(self): return models.IntegerField() def get_lookup(self, lookup_name): self.call_order.append("lookup") return super().get_lookup(lookup_name) def get_transform(self, lookup_name): self.call_order.append("transform") return super().get_transform(lookup_name) class LookupTransformCallOrderTests(SimpleTestCase): def test_call_order(self): with register_lookup(models.DateField, TrackCallsYearTransform): # junk lookup - tries lookup, then transform, then fails msg = ( "Unsupported lookup 'junk' for IntegerField or join on the field not " "permitted." ) with self.assertRaisesMessage(FieldError, msg): Author.objects.filter(birthdate__testyear__junk=2012) self.assertEqual( TrackCallsYearTransform.call_order, ["lookup", "transform"] ) TrackCallsYearTransform.call_order = [] # junk transform - tries transform only, then fails with self.assertRaisesMessage(FieldError, msg): Author.objects.filter(birthdate__testyear__junk__more_junk=2012) self.assertEqual(TrackCallsYearTransform.call_order, ["transform"]) TrackCallsYearTransform.call_order = [] # Just getting the year (implied __exact) - lookup only Author.objects.filter(birthdate__testyear=2012) self.assertEqual(TrackCallsYearTransform.call_order, ["lookup"]) TrackCallsYearTransform.call_order = [] # Just getting the year (explicit __exact) - lookup only Author.objects.filter(birthdate__testyear__exact=2012) self.assertEqual(TrackCallsYearTransform.call_order, ["lookup"]) class CustomisedMethodsTests(SimpleTestCase): def test_overridden_get_lookup(self): q = CustomModel.objects.filter(field__lookupfunc_monkeys=3) self.assertIn("monkeys()", str(q.query)) def test_overridden_get_transform(self): q = CustomModel.objects.filter(field__transformfunc_banana=3) self.assertIn("banana()", str(q.query)) def test_overridden_get_lookup_chain(self): q = CustomModel.objects.filter( field__transformfunc_banana__lookupfunc_elephants=3 ) self.assertIn("elephants()", str(q.query)) def test_overridden_get_transform_chain(self): q = CustomModel.objects.filter( field__transformfunc_banana__transformfunc_pear=3 ) self.assertIn("pear()", str(q.query)) class SubqueryTransformTests(TestCase): def test_subquery_usage(self): with register_lookup(models.IntegerField, Div3Transform): Author.objects.create(name="a1", age=1) a2 = Author.objects.create(name="a2", age=2) Author.objects.create(name="a3", age=3) Author.objects.create(name="a4", age=4) qs = Author.objects.order_by("name").filter( id__in=Author.objects.filter(age__div3=2) ) self.assertSequenceEqual(qs, [a2])