diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index 7018123cfa..02c7cdcc63 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -569,7 +569,7 @@ END; if internal_type == 'DateField': lhs_sql, lhs_params = lhs rhs_sql, rhs_params = rhs - return "NUMTODSINTERVAL(%s - %s, 'DAY')" % (lhs_sql, rhs_sql), lhs_params + rhs_params + return "NUMTODSINTERVAL(TO_NUMBER(%s - %s), 'DAY')" % (lhs_sql, rhs_sql), lhs_params + rhs_params return super().subtract_temporals(internal_type, lhs, rhs) def bulk_batch_size(self, fields, objs): diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index a6542db1eb..c1667b5c9b 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -3,6 +3,7 @@ SQLite3 backend for the sqlite3 module in the standard library. """ import datetime import decimal +import functools import math import operator import re @@ -34,6 +35,19 @@ def decoder(conv_func): return lambda s: conv_func(s.decode()) +def none_guard(func): + """ + Decorator that returns None if any of the arguments to the decorated + function are None. Many SQL functions return NULL if any of their arguments + are NULL. This decorator simplifies the implementation of this for the + custom functions registered below. + """ + @functools.wraps(func) + def wrapper(*args, **kwargs): + return None if None in args else func(*args, **kwargs) + return wrapper + + Database.register_converter("bool", b'1'.__eq__) Database.register_converter("time", decoder(parse_time)) Database.register_converter("datetime", decoder(parse_datetime)) @@ -171,30 +185,30 @@ class DatabaseWrapper(BaseDatabaseWrapper): conn.create_function("django_time_trunc", 2, _sqlite_time_trunc) conn.create_function("django_time_diff", 2, _sqlite_time_diff) conn.create_function("django_timestamp_diff", 2, _sqlite_timestamp_diff) - conn.create_function("regexp", 2, _sqlite_regexp) conn.create_function("django_format_dtdelta", 3, _sqlite_format_dtdelta) + conn.create_function('regexp', 2, _sqlite_regexp) + conn.create_function('ACOS', 1, none_guard(math.acos)) + conn.create_function('ASIN', 1, none_guard(math.asin)) + conn.create_function('ATAN', 1, none_guard(math.atan)) + conn.create_function('ATAN2', 2, none_guard(math.atan2)) + conn.create_function('CEILING', 1, none_guard(math.ceil)) + conn.create_function('COS', 1, none_guard(math.cos)) + conn.create_function('COT', 1, none_guard(lambda x: 1 / math.tan(x))) + conn.create_function('DEGREES', 1, none_guard(math.degrees)) + conn.create_function('EXP', 1, none_guard(math.exp)) + conn.create_function('FLOOR', 1, none_guard(math.floor)) + conn.create_function('LN', 1, none_guard(math.log)) + conn.create_function('LOG', 2, none_guard(lambda x, y: math.log(y, x))) conn.create_function('LPAD', 3, _sqlite_lpad) - conn.create_function('REPEAT', 2, operator.mul) - conn.create_function('RPAD', 3, _sqlite_rpad) - conn.create_function('ACOS', 1, math.acos) - conn.create_function('ASIN', 1, math.asin) - conn.create_function('ATAN', 1, math.atan) - conn.create_function('ATAN2', 2, math.atan2) - conn.create_function('CEILING', 1, math.ceil) - conn.create_function('COS', 1, math.cos) - conn.create_function('COT', 1, lambda x: 1 / math.tan(x)) - conn.create_function('DEGREES', 1, math.degrees) - conn.create_function('EXP', 1, math.exp) - conn.create_function('FLOOR', 1, math.floor) - conn.create_function('LN', 1, math.log) - conn.create_function('LOG', 2, lambda x, y: math.log(y, x)) - conn.create_function('MOD', 2, math.fmod) + conn.create_function('MOD', 2, none_guard(math.fmod)) conn.create_function('PI', 0, lambda: math.pi) - conn.create_function('POWER', 2, operator.pow) - conn.create_function('RADIANS', 1, math.radians) - conn.create_function('SIN', 1, math.sin) - conn.create_function('SQRT', 1, math.sqrt) - conn.create_function('TAN', 1, math.tan) + conn.create_function('POWER', 2, none_guard(operator.pow)) + conn.create_function('RADIANS', 1, none_guard(math.radians)) + conn.create_function('REPEAT', 2, none_guard(operator.mul)) + conn.create_function('RPAD', 3, _sqlite_rpad) + conn.create_function('SIN', 1, none_guard(math.sin)) + conn.create_function('SQRT', 1, none_guard(math.sqrt)) + conn.create_function('TAN', 1, none_guard(math.tan)) conn.execute('PRAGMA foreign_keys = ON') return conn @@ -356,6 +370,8 @@ def _sqlite_date_trunc(lookup_type, dt): def _sqlite_time_trunc(lookup_type, dt): + if dt is None: + return None try: dt = backend_utils.typecast_time(dt) except (ValueError, TypeError): @@ -432,6 +448,7 @@ def _sqlite_time_extract(lookup_type, dt): return getattr(dt, lookup_type) +@none_guard def _sqlite_format_dtdelta(conn, lhs, rhs): """ LHS and RHS can be either: @@ -452,6 +469,7 @@ def _sqlite_format_dtdelta(conn, lhs, rhs): return str(out) +@none_guard def _sqlite_time_diff(lhs, rhs): left = backend_utils.typecast_time(lhs) right = backend_utils.typecast_time(rhs) @@ -467,21 +485,25 @@ def _sqlite_time_diff(lhs, rhs): ) +@none_guard def _sqlite_timestamp_diff(lhs, rhs): left = backend_utils.typecast_timestamp(lhs) right = backend_utils.typecast_timestamp(rhs) return duration_microseconds(left - right) +@none_guard def _sqlite_regexp(re_pattern, re_string): - return bool(re.search(re_pattern, str(re_string))) if re_string is not None else False + return bool(re.search(re_pattern, str(re_string))) +@none_guard def _sqlite_lpad(text, length, fill_text): if len(text) >= length: return text[:length] return (fill_text * length)[:length - len(text)] + text +@none_guard def _sqlite_rpad(text, length, fill_text): return (text + fill_text * length)[:length] diff --git a/django/db/models/functions/datetime.py b/django/db/models/functions/datetime.py index 0a68f075aa..51be8408e4 100644 --- a/django/db/models/functions/datetime.py +++ b/django/db/models/functions/datetime.py @@ -218,16 +218,20 @@ class TruncBase(TimezoneMixin, Transform): def convert_value(self, value, expression, connection): if isinstance(self.output_field, DateTimeField): - if settings.USE_TZ: - if value is None: - raise ValueError( - "Database returned an invalid datetime value. " - "Are time zone definitions for your database installed?" - ) + if not settings.USE_TZ: + pass + elif value is not None: value = value.replace(tzinfo=None) value = timezone.make_aware(value, self.tzinfo) + elif not connection.features.has_zoneinfo_database: + raise ValueError( + 'Database returned an invalid datetime value. Are time ' + 'zone definitions for your database installed?' + ) elif isinstance(value, datetime): - if isinstance(self.output_field, DateField): + if value is None: + pass + elif isinstance(self.output_field, DateField): value = value.date() elif isinstance(self.output_field, TimeField): value = value.time() diff --git a/django/db/models/functions/text.py b/django/db/models/functions/text.py index 8cf10bb76d..3c3eade646 100644 --- a/django/db/models/functions/text.py +++ b/django/db/models/functions/text.py @@ -139,7 +139,7 @@ class LPad(BytesToCharFieldConversionMixin, Func): function = 'LPAD' def __init__(self, expression, length, fill_text=Value(' '), **extra): - if not hasattr(length, 'resolve_expression') and length < 0: + if not hasattr(length, 'resolve_expression') and length is not None and length < 0: raise ValueError("'length' must be greater or equal to 0.") super().__init__(expression, length, fill_text, **extra) @@ -165,13 +165,14 @@ class Repeat(BytesToCharFieldConversionMixin, Func): function = 'REPEAT' def __init__(self, expression, number, **extra): - if not hasattr(number, 'resolve_expression') and number < 0: + if not hasattr(number, 'resolve_expression') and number is not None and number < 0: raise ValueError("'number' must be greater or equal to 0.") super().__init__(expression, number, **extra) def as_oracle(self, compiler, connection, **extra_context): expression, number = self.source_expressions - rpad = RPad(expression, Length(expression) * number, expression) + length = None if number is None else Length(expression) * number + rpad = RPad(expression, length, expression) return rpad.as_sql(compiler, connection, **extra_context) diff --git a/tests/backends/sqlite/tests.py b/tests/backends/sqlite/tests.py index c82ed1667d..86723a72d2 100644 --- a/tests/backends/sqlite/tests.py +++ b/tests/backends/sqlite/tests.py @@ -59,6 +59,22 @@ class Tests(TestCase): creation = DatabaseWrapper(settings_dict).creation self.assertEqual(creation._get_test_db_name(), creation.connection.settings_dict['TEST']['NAME']) + def test_regexp_function(self): + tests = ( + ('test', r'[0-9]+', False), + ('test', r'[a-z]+', True), + ('test', None, None), + (None, r'[a-z]+', None), + (None, None, None), + ) + for string, pattern, expected in tests: + with self.subTest((string, pattern)): + with connection.cursor() as cursor: + cursor.execute('SELECT %s REGEXP %s', [string, pattern]) + value = cursor.fetchone()[0] + value = bool(value) if value in {0, 1} else value + self.assertIs(value, expected) + @unittest.skipUnless(connection.vendor == 'sqlite', 'SQLite tests') @isolate_apps('backends') diff --git a/tests/db_functions/datetime/test_extract_trunc.py b/tests/db_functions/datetime/test_extract_trunc.py index 99d33b252c..065a06f4be 100644 --- a/tests/db_functions/datetime/test_extract_trunc.py +++ b/tests/db_functions/datetime/test_extract_trunc.py @@ -66,11 +66,14 @@ class DateFunctionTests(TestCase): def create_model(self, start_datetime, end_datetime): return DTModel.objects.create( - name=start_datetime.isoformat(), - start_datetime=start_datetime, end_datetime=end_datetime, - start_date=start_datetime.date(), end_date=end_datetime.date(), - start_time=start_datetime.time(), end_time=end_datetime.time(), - duration=(end_datetime - start_datetime), + name=start_datetime.isoformat() if start_datetime else 'None', + start_datetime=start_datetime, + end_datetime=end_datetime, + start_date=start_datetime.date() if start_datetime else None, + end_date=end_datetime.date() if end_datetime else None, + start_time=start_datetime.time() if start_datetime else None, + end_time=end_datetime.time() if end_datetime else None, + duration=(end_datetime - start_datetime) if start_datetime and end_datetime else None, ) def test_extract_year_exact_lookup(self): @@ -215,6 +218,12 @@ class DateFunctionTests(TestCase): self.assertEqual(DTModel.objects.filter(start_date__month=Extract('start_date', 'month')).count(), 2) self.assertEqual(DTModel.objects.filter(start_time__hour=Extract('start_time', 'hour')).count(), 2) + def test_extract_none(self): + self.create_model(None, None) + for t in (Extract('start_datetime', 'year'), Extract('start_date', 'year'), Extract('start_time', 'hour')): + with self.subTest(t): + self.assertIsNone(DTModel.objects.annotate(extracted=t).first().extracted) + @skipUnlessDBFeature('has_native_duration_field') def test_extract_duration(self): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) @@ -608,6 +617,12 @@ class DateFunctionTests(TestCase): qs = DTModel.objects.filter(start_datetime__date=Trunc('start_datetime', 'day', output_field=DateField())) self.assertEqual(qs.count(), 2) + def test_trunc_none(self): + self.create_model(None, None) + for t in (Trunc('start_datetime', 'year'), Trunc('start_date', 'year'), Trunc('start_time', 'hour')): + with self.subTest(t): + self.assertIsNone(DTModel.objects.annotate(truncated=t).first().truncated) + def test_trunc_year_func(self): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'year') @@ -761,6 +776,10 @@ class DateFunctionTests(TestCase): with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateField"): list(DTModel.objects.annotate(truncated=TruncDate('start_time', output_field=TimeField()))) + def test_trunc_date_none(self): + self.create_model(None, None) + self.assertIsNone(DTModel.objects.annotate(truncated=TruncDate('start_datetime')).first().truncated) + def test_trunc_time_func(self): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) @@ -785,6 +804,10 @@ class DateFunctionTests(TestCase): with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to TimeField"): list(DTModel.objects.annotate(truncated=TruncTime('start_date', output_field=DateField()))) + def test_trunc_time_none(self): + self.create_model(None, None) + self.assertIsNone(DTModel.objects.annotate(truncated=TruncTime('start_datetime')).first().truncated) + def test_trunc_day_func(self): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'day') diff --git a/tests/db_functions/math/test_abs.py b/tests/db_functions/math/test_abs.py index 484cd2e306..b87f6844bc 100644 --- a/tests/db_functions/math/test_abs.py +++ b/tests/db_functions/math/test_abs.py @@ -10,6 +10,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class AbsTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_abs=Abs('normal')).first() + self.assertIsNone(obj.null_abs) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-0.8'), n2=Decimal('1.2')) obj = DecimalModel.objects.annotate(n1_abs=Abs('n1'), n2_abs=Abs('n2')).first() diff --git a/tests/db_functions/math/test_acos.py b/tests/db_functions/math/test_acos.py index a9ba079e4f..04fdf2cf40 100644 --- a/tests/db_functions/math/test_acos.py +++ b/tests/db_functions/math/test_acos.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class ACosTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_acos=ACos('normal')).first() + self.assertIsNone(obj.null_acos) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-0.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_acos=ACos('n1'), n2_acos=ACos('n2')).first() diff --git a/tests/db_functions/math/test_asin.py b/tests/db_functions/math/test_asin.py index dc135a6786..a9074e4305 100644 --- a/tests/db_functions/math/test_asin.py +++ b/tests/db_functions/math/test_asin.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class ASinTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_asin=ASin('normal')).first() + self.assertIsNone(obj.null_asin) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('0.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_asin=ASin('n1'), n2_asin=ASin('n2')).first() diff --git a/tests/db_functions/math/test_atan.py b/tests/db_functions/math/test_atan.py index 36c07ae306..fbeeded48c 100644 --- a/tests/db_functions/math/test_atan.py +++ b/tests/db_functions/math/test_atan.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class ATanTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_atan=ATan('normal')).first() + self.assertIsNone(obj.null_atan) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_atan=ATan('n1'), n2_atan=ATan('n2')).first() diff --git a/tests/db_functions/math/test_atan2.py b/tests/db_functions/math/test_atan2.py index 195892dfdd..ca12e64479 100644 --- a/tests/db_functions/math/test_atan2.py +++ b/tests/db_functions/math/test_atan2.py @@ -9,6 +9,15 @@ from ..models import DecimalModel, FloatModel, IntegerModel class ATan2Tests(TestCase): + def test_null(self): + IntegerModel.objects.create(big=100) + obj = IntegerModel.objects.annotate( + null_atan2_sn=ATan2('small', 'normal'), + null_atan2_nb=ATan2('normal', 'big'), + ).first() + self.assertIsNone(obj.null_atan2_sn) + self.assertIsNone(obj.null_atan2_nb) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-9.9'), n2=Decimal('4.6')) obj = DecimalModel.objects.annotate(n_atan2=ATan2('n1', 'n2')).first() diff --git a/tests/db_functions/math/test_ceil.py b/tests/db_functions/math/test_ceil.py index a62c33a19f..af4ee44e31 100644 --- a/tests/db_functions/math/test_ceil.py +++ b/tests/db_functions/math/test_ceil.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class CeilTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_ceil=Ceil('normal')).first() + self.assertIsNone(obj.null_ceil) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_ceil=Ceil('n1'), n2_ceil=Ceil('n2')).first() diff --git a/tests/db_functions/math/test_cos.py b/tests/db_functions/math/test_cos.py index 15975e247d..99cf96620e 100644 --- a/tests/db_functions/math/test_cos.py +++ b/tests/db_functions/math/test_cos.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class CosTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_cos=Cos('normal')).first() + self.assertIsNone(obj.null_cos) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_cos=Cos('n1'), n2_cos=Cos('n2')).first() diff --git a/tests/db_functions/math/test_cot.py b/tests/db_functions/math/test_cot.py index 0407f3b45d..5af0403221 100644 --- a/tests/db_functions/math/test_cot.py +++ b/tests/db_functions/math/test_cot.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class CotTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_cot=Cot('normal')).first() + self.assertIsNone(obj.null_cot) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_cot=Cot('n1'), n2_cot=Cot('n2')).first() diff --git a/tests/db_functions/math/test_degrees.py b/tests/db_functions/math/test_degrees.py index e5a551992f..a474d276a5 100644 --- a/tests/db_functions/math/test_degrees.py +++ b/tests/db_functions/math/test_degrees.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class DegreesTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_degrees=Degrees('normal')).first() + self.assertIsNone(obj.null_degrees) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_degrees=Degrees('n1'), n2_degrees=Degrees('n2')).first() diff --git a/tests/db_functions/math/test_exp.py b/tests/db_functions/math/test_exp.py index 0981d4fce3..fac2f6c08d 100644 --- a/tests/db_functions/math/test_exp.py +++ b/tests/db_functions/math/test_exp.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class ExpTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_exp=Exp('normal')).first() + self.assertIsNone(obj.null_exp) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_exp=Exp('n1'), n2_exp=Exp('n2')).first() diff --git a/tests/db_functions/math/test_floor.py b/tests/db_functions/math/test_floor.py index ee567cfea6..0c193ef1af 100644 --- a/tests/db_functions/math/test_floor.py +++ b/tests/db_functions/math/test_floor.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class FloorTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_floor=Floor('normal')).first() + self.assertIsNone(obj.null_floor) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_floor=Floor('n1'), n2_floor=Floor('n2')).first() diff --git a/tests/db_functions/math/test_ln.py b/tests/db_functions/math/test_ln.py index 96d4599bb3..3c690d56cc 100644 --- a/tests/db_functions/math/test_ln.py +++ b/tests/db_functions/math/test_ln.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class LnTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_ln=Ln('normal')).first() + self.assertIsNone(obj.null_ln) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_ln=Ln('n1'), n2_ln=Ln('n2')).first() diff --git a/tests/db_functions/math/test_log.py b/tests/db_functions/math/test_log.py index 02cbe084d3..469bb7cd3a 100644 --- a/tests/db_functions/math/test_log.py +++ b/tests/db_functions/math/test_log.py @@ -9,6 +9,15 @@ from ..models import DecimalModel, FloatModel, IntegerModel class LogTests(TestCase): + def test_null(self): + IntegerModel.objects.create(big=100) + obj = IntegerModel.objects.annotate( + null_log_small=Log('small', 'normal'), + null_log_normal=Log('normal', 'big'), + ).first() + self.assertIsNone(obj.null_log_small) + self.assertIsNone(obj.null_log_normal) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('12.9'), n2=Decimal('3.6')) obj = DecimalModel.objects.annotate(n_log=Log('n1', 'n2')).first() diff --git a/tests/db_functions/math/test_mod.py b/tests/db_functions/math/test_mod.py index 0e90175ddc..dc363432b7 100644 --- a/tests/db_functions/math/test_mod.py +++ b/tests/db_functions/math/test_mod.py @@ -9,6 +9,15 @@ from ..models import DecimalModel, FloatModel, IntegerModel class ModTests(TestCase): + def test_null(self): + IntegerModel.objects.create(big=100) + obj = IntegerModel.objects.annotate( + null_mod_small=Mod('small', 'normal'), + null_mod_normal=Mod('normal', 'big'), + ).first() + self.assertIsNone(obj.null_mod_small) + self.assertIsNone(obj.null_mod_normal) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-9.9'), n2=Decimal('4.6')) obj = DecimalModel.objects.annotate(n_mod=Mod('n1', 'n2')).first() diff --git a/tests/db_functions/math/test_power.py b/tests/db_functions/math/test_power.py index 01ca2b34d9..a2d6156e3d 100644 --- a/tests/db_functions/math/test_power.py +++ b/tests/db_functions/math/test_power.py @@ -8,6 +8,15 @@ from ..models import DecimalModel, FloatModel, IntegerModel class PowerTests(TestCase): + def test_null(self): + IntegerModel.objects.create(big=100) + obj = IntegerModel.objects.annotate( + null_power_small=Power('small', 'normal'), + null_power_normal=Power('normal', 'big'), + ).first() + self.assertIsNone(obj.null_power_small) + self.assertIsNone(obj.null_power_normal) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('1.0'), n2=Decimal('-0.6')) obj = DecimalModel.objects.annotate(n_power=Power('n1', 'n2')).first() diff --git a/tests/db_functions/math/test_radians.py b/tests/db_functions/math/test_radians.py index 873659e7ad..3c257bb278 100644 --- a/tests/db_functions/math/test_radians.py +++ b/tests/db_functions/math/test_radians.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class RadiansTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_radians=Radians('normal')).first() + self.assertIsNone(obj.null_radians) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_radians=Radians('n1'), n2_radians=Radians('n2')).first() diff --git a/tests/db_functions/math/test_round.py b/tests/db_functions/math/test_round.py index d242f2de0f..4c2634c3c2 100644 --- a/tests/db_functions/math/test_round.py +++ b/tests/db_functions/math/test_round.py @@ -10,6 +10,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class RoundTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_round=Round('normal')).first() + self.assertIsNone(obj.null_round) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_round=Round('n1'), n2_round=Round('n2')).first() diff --git a/tests/db_functions/math/test_sin.py b/tests/db_functions/math/test_sin.py index 0f7e0c7c0b..f2e2edd4da 100644 --- a/tests/db_functions/math/test_sin.py +++ b/tests/db_functions/math/test_sin.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class SinTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_sin=Sin('normal')).first() + self.assertIsNone(obj.null_sin) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_sin=Sin('n1'), n2_sin=Sin('n2')).first() diff --git a/tests/db_functions/math/test_sqrt.py b/tests/db_functions/math/test_sqrt.py index 81f13361e1..0e6238a141 100644 --- a/tests/db_functions/math/test_sqrt.py +++ b/tests/db_functions/math/test_sqrt.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class SqrtTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_sqrt=Sqrt('normal')).first() + self.assertIsNone(obj.null_sqrt) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_sqrt=Sqrt('n1'), n2_sqrt=Sqrt('n2')).first() diff --git a/tests/db_functions/math/test_tan.py b/tests/db_functions/math/test_tan.py index 82dcec94fc..6db760725b 100644 --- a/tests/db_functions/math/test_tan.py +++ b/tests/db_functions/math/test_tan.py @@ -11,6 +11,11 @@ from ..models import DecimalModel, FloatModel, IntegerModel class TanTests(TestCase): + def test_null(self): + IntegerModel.objects.create() + obj = IntegerModel.objects.annotate(null_tan=Tan('normal')).first() + self.assertIsNone(obj.null_tan) + def test_decimal(self): DecimalModel.objects.create(n1=Decimal('-12.9'), n2=Decimal('0.6')) obj = DecimalModel.objects.annotate(n1_tan=Tan('n1'), n2_tan=Tan('n2')).first() diff --git a/tests/db_functions/text/test_pad.py b/tests/db_functions/text/test_pad.py index 2cec280b4d..88309e5641 100644 --- a/tests/db_functions/text/test_pad.py +++ b/tests/db_functions/text/test_pad.py @@ -1,3 +1,4 @@ +from django.db import connection from django.db.models import CharField, Value from django.db.models.functions import Length, LPad, RPad from django.test import TestCase @@ -8,6 +9,7 @@ from ..models import Author class PadTests(TestCase): def test_pad(self): Author.objects.create(name='John', alias='j') + none_value = '' if connection.features.interprets_empty_strings_as_nulls else None tests = ( (LPad('name', 7, Value('xy')), 'xyxJohn'), (RPad('name', 7, Value('xy')), 'Johnxyx'), @@ -21,6 +23,10 @@ class PadTests(TestCase): (RPad('name', 2), 'Jo'), (LPad('name', 0), ''), (RPad('name', 0), ''), + (LPad('name', None), none_value), + (RPad('name', None), none_value), + (LPad('goes_by', 1), none_value), + (RPad('goes_by', 1), none_value), ) for function, padded_name in tests: with self.subTest(function=function): diff --git a/tests/db_functions/text/test_repeat.py b/tests/db_functions/text/test_repeat.py index f45544d97e..d302e6da28 100644 --- a/tests/db_functions/text/test_repeat.py +++ b/tests/db_functions/text/test_repeat.py @@ -1,3 +1,4 @@ +from django.db import connection from django.db.models import CharField, Value from django.db.models.functions import Length, Repeat from django.test import TestCase @@ -8,11 +9,14 @@ from ..models import Author class RepeatTests(TestCase): def test_basic(self): Author.objects.create(name='John', alias='xyz') + none_value = '' if connection.features.interprets_empty_strings_as_nulls else None tests = ( (Repeat('name', 0), ''), (Repeat('name', 2), 'JohnJohn'), (Repeat('name', Length('alias'), output_field=CharField()), 'JohnJohnJohn'), (Repeat(Value('x'), 3, output_field=CharField()), 'xxx'), + (Repeat('name', None), none_value), + (Repeat('goes_by', 1), none_value), ) for function, repeated_text in tests: with self.subTest(function=function): diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index 9a646088b1..f12a9388da 100644 --- a/tests/expressions/tests.py +++ b/tests/expressions/tests.py @@ -1249,6 +1249,12 @@ class FTimeDeltaTests(TestCase): ] self.assertEqual(delta_math, ['e4']) + queryset = Experiment.objects.annotate(shifted=ExpressionWrapper( + F('start') + Value(None, output_field=models.DurationField()), + output_field=models.DateTimeField(), + )) + self.assertIsNone(queryset.first().shifted) + @skipUnlessDBFeature('supports_temporal_subtraction') def test_date_subtraction(self): queryset = Experiment.objects.annotate( @@ -1266,6 +1272,18 @@ class FTimeDeltaTests(TestCase): less_than_5_days = {e.name for e in queryset.filter(completion_duration__lt=datetime.timedelta(days=5))} self.assertEqual(less_than_5_days, {'e0', 'e1', 'e2'}) + queryset = Experiment.objects.annotate(difference=ExpressionWrapper( + F('completed') - Value(None, output_field=models.DateField()), + output_field=models.DurationField(), + )) + self.assertIsNone(queryset.first().difference) + + queryset = Experiment.objects.annotate(shifted=ExpressionWrapper( + F('completed') - Value(None, output_field=models.DurationField()), + output_field=models.DateField(), + )) + self.assertIsNone(queryset.first().shifted) + @skipUnlessDBFeature('supports_temporal_subtraction') def test_time_subtraction(self): Time.objects.create(time=datetime.time(12, 30, 15, 2345)) @@ -1280,6 +1298,18 @@ class FTimeDeltaTests(TestCase): datetime.timedelta(hours=1, minutes=15, seconds=15, microseconds=2345) ) + queryset = Time.objects.annotate(difference=ExpressionWrapper( + F('time') - Value(None, output_field=models.TimeField()), + output_field=models.DurationField(), + )) + self.assertIsNone(queryset.first().difference) + + queryset = Time.objects.annotate(shifted=ExpressionWrapper( + F('time') - Value(None, output_field=models.DurationField()), + output_field=models.TimeField(), + )) + self.assertIsNone(queryset.first().shifted) + @skipUnlessDBFeature('supports_temporal_subtraction') def test_datetime_subtraction(self): under_estimate = [ @@ -1292,6 +1322,18 @@ class FTimeDeltaTests(TestCase): ] self.assertEqual(over_estimate, ['e4']) + queryset = Experiment.objects.annotate(difference=ExpressionWrapper( + F('start') - Value(None, output_field=models.DateTimeField()), + output_field=models.DurationField(), + )) + self.assertIsNone(queryset.first().difference) + + queryset = Experiment.objects.annotate(shifted=ExpressionWrapper( + F('start') - Value(None, output_field=models.DurationField()), + output_field=models.DateTimeField(), + )) + self.assertIsNone(queryset.first().shifted) + @skipUnlessDBFeature('supports_temporal_subtraction') def test_datetime_subtraction_microseconds(self): delta = datetime.timedelta(microseconds=8999999999999999)