mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #28926 -- Fixed loss of precision of big DurationField values on SQLite and MySQL.
This commit is contained in:
		
				
					committed by
					
						 Tim Graham
						Tim Graham
					
				
			
			
				
	
			
			
			
						parent
						
							46d1af2e82
						
					
				
				
					commit
					ae6fa914aa
				
			| @@ -3,6 +3,7 @@ import uuid | ||||
| from django.conf import settings | ||||
| from django.db.backends.base.operations import BaseDatabaseOperations | ||||
| from django.utils import timezone | ||||
| from django.utils.duration import duration_microseconds | ||||
| from django.utils.encoding import force_text | ||||
|  | ||||
|  | ||||
| @@ -105,7 +106,7 @@ class DatabaseOperations(BaseDatabaseOperations): | ||||
|             return "TIME(%s)" % (field_name) | ||||
|  | ||||
|     def date_interval_sql(self, timedelta): | ||||
|         return "INTERVAL '%06f' SECOND_MICROSECOND" % timedelta.total_seconds() | ||||
|         return 'INTERVAL %s MICROSECOND' % duration_microseconds(timedelta) | ||||
|  | ||||
|     def format_for_duration_arithmetic(self, sql): | ||||
|         return 'INTERVAL %s MICROSECOND' % sql | ||||
|   | ||||
| @@ -17,6 +17,7 @@ from django.utils import timezone | ||||
| from django.utils.dateparse import ( | ||||
|     parse_date, parse_datetime, parse_duration, parse_time, | ||||
| ) | ||||
| from django.utils.duration import duration_microseconds | ||||
|  | ||||
| from .client import DatabaseClient                          # isort:skip | ||||
| from .creation import DatabaseCreation                      # isort:skip | ||||
| @@ -471,7 +472,7 @@ def _sqlite_time_diff(lhs, rhs): | ||||
| def _sqlite_timestamp_diff(lhs, rhs): | ||||
|     left = backend_utils.typecast_timestamp(lhs) | ||||
|     right = backend_utils.typecast_timestamp(rhs) | ||||
|     return (left - right).total_seconds() * 1000000 | ||||
|     return duration_microseconds(left - right) | ||||
|  | ||||
|  | ||||
| def _sqlite_regexp(re_pattern, re_string): | ||||
|   | ||||
| @@ -24,7 +24,7 @@ from django.utils.datastructures import DictWrapper | ||||
| from django.utils.dateparse import ( | ||||
|     parse_date, parse_datetime, parse_duration, parse_time, | ||||
| ) | ||||
| from django.utils.duration import duration_string | ||||
| from django.utils.duration import duration_microseconds, duration_string | ||||
| from django.utils.encoding import force_bytes, smart_text | ||||
| from django.utils.functional import Promise, cached_property | ||||
| from django.utils.ipv6 import clean_ipv6_address | ||||
| @@ -1617,8 +1617,7 @@ class DurationField(Field): | ||||
|             return value | ||||
|         if value is None: | ||||
|             return None | ||||
|         # Discard any fractional microseconds due to floating point arithmetic. | ||||
|         return round(value.total_seconds() * 1000000) | ||||
|         return duration_microseconds(value) | ||||
|  | ||||
|     def get_db_converters(self, connection): | ||||
|         converters = [] | ||||
|   | ||||
| @@ -38,3 +38,7 @@ def duration_iso_string(duration): | ||||
|     days, hours, minutes, seconds, microseconds = _get_duration_components(duration) | ||||
|     ms = '.{:06d}'.format(microseconds) if microseconds else "" | ||||
|     return '{}P{}DT{:02d}H{:02d}M{:02d}{}S'.format(sign, days, hours, minutes, seconds, ms) | ||||
|  | ||||
|  | ||||
| def duration_microseconds(delta): | ||||
|     return (24 * 60 * 60 * delta.days + delta.seconds) * 1000000 + delta.microseconds | ||||
|   | ||||
| @@ -1250,6 +1250,16 @@ class FTimeDeltaTests(TestCase): | ||||
|         ] | ||||
|         self.assertEqual(over_estimate, ['e4']) | ||||
|  | ||||
|     @skipUnlessDBFeature('supports_temporal_subtraction') | ||||
|     def test_datetime_subtraction_microseconds(self): | ||||
|         delta = datetime.timedelta(microseconds=8999999999999999) | ||||
|         Experiment.objects.update(end=F('start') + delta) | ||||
|         qs = Experiment.objects.annotate( | ||||
|             delta=ExpressionWrapper(F('end') - F('start'), output_field=models.DurationField()) | ||||
|         ) | ||||
|         for e in qs: | ||||
|             self.assertEqual(e.delta, delta) | ||||
|  | ||||
|     def test_duration_with_datetime(self): | ||||
|         # Exclude e1 which has very high precision so we can test this on all | ||||
|         # backends regardless of whether or not it supports | ||||
| @@ -1259,6 +1269,15 @@ class FTimeDeltaTests(TestCase): | ||||
|         ).order_by('name') | ||||
|         self.assertQuerysetEqual(over_estimate, ['e3', 'e4', 'e5'], lambda e: e.name) | ||||
|  | ||||
|     def test_duration_with_datetime_microseconds(self): | ||||
|         delta = datetime.timedelta(microseconds=8999999999999999) | ||||
|         qs = Experiment.objects.annotate(dt=ExpressionWrapper( | ||||
|             F('start') + delta, | ||||
|             output_field=models.DateTimeField(), | ||||
|         )) | ||||
|         for e in qs: | ||||
|             self.assertEqual(e.dt, e.start + delta) | ||||
|  | ||||
|     def test_date_minus_duration(self): | ||||
|         more_than_4_days = Experiment.objects.filter( | ||||
|             assigned__lt=F('completed') - Value(datetime.timedelta(days=4), output_field=models.DurationField()) | ||||
|   | ||||
| @@ -12,7 +12,7 @@ from .models import DurationModel, NullDurationModel | ||||
| class TestSaveLoad(TestCase): | ||||
|  | ||||
|     def test_simple_roundtrip(self): | ||||
|         duration = datetime.timedelta(days=123, seconds=123, microseconds=123) | ||||
|         duration = datetime.timedelta(microseconds=8999999999999999) | ||||
|         DurationModel.objects.create(field=duration) | ||||
|         loaded = DurationModel.objects.get() | ||||
|         self.assertEqual(loaded.field, duration) | ||||
|   | ||||
| @@ -2,7 +2,9 @@ import datetime | ||||
| import unittest | ||||
|  | ||||
| from django.utils.dateparse import parse_duration | ||||
| from django.utils.duration import duration_iso_string, duration_string | ||||
| from django.utils.duration import ( | ||||
|     duration_iso_string, duration_microseconds, duration_string, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class TestDurationString(unittest.TestCase): | ||||
| @@ -79,3 +81,17 @@ class TestParseISODurationRoundtrip(unittest.TestCase): | ||||
|     def test_negative(self): | ||||
|         duration = datetime.timedelta(days=-1, hours=1, minutes=3, seconds=5) | ||||
|         self.assertEqual(parse_duration(duration_iso_string(duration)).total_seconds(), duration.total_seconds()) | ||||
|  | ||||
|  | ||||
| class TestDurationMicroseconds(unittest.TestCase): | ||||
|     def test(self): | ||||
|         deltas = [ | ||||
|             datetime.timedelta.max, | ||||
|             datetime.timedelta.min, | ||||
|             datetime.timedelta.resolution, | ||||
|             -datetime.timedelta.resolution, | ||||
|             datetime.timedelta(microseconds=8999999999999999), | ||||
|         ] | ||||
|         for delta in deltas: | ||||
|             with self.subTest(delta=delta): | ||||
|                 self.assertEqual(datetime.timedelta(microseconds=duration_microseconds(delta)), delta) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user