mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #29500 -- Fixed SQLite function crashes on null values.
Co-authored-by: Srinivas Reddy Thatiparthy <thatiparthysreenivas@gmail.com> Co-authored-by: Nick Pope <nick.pope@flightdataservices.com>
This commit is contained in:
		
				
					committed by
					
						 Tim Graham
						Tim Graham
					
				
			
			
				
	
			
			
			
						parent
						
							76dfa834e7
						
					
				
				
					commit
					34d6bceec4
				
			| @@ -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): | ||||
|   | ||||
| @@ -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] | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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') | ||||
|   | ||||
| @@ -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') | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user