Fixed #30621 -- Fixed crash of __contains lookup for Date/DateTimeRangeField when the right hand side is the same type.
Thanks Tilman Koschnick for the report and initial patch.
Thanks Carlton Gibson the review.
Regression in 6b048b364c
.
This commit is contained in:
parent
ee6e93ec87
commit
7991111af1
|
@ -170,7 +170,12 @@ class DateTimeRangeContains(models.Lookup):
|
||||||
params = lhs_params + rhs_params
|
params = lhs_params + rhs_params
|
||||||
# Cast the rhs if needed.
|
# Cast the rhs if needed.
|
||||||
cast_sql = ''
|
cast_sql = ''
|
||||||
if isinstance(self.rhs, models.Expression) and self.rhs._output_field_or_none:
|
if (
|
||||||
|
isinstance(self.rhs, models.Expression) and
|
||||||
|
self.rhs._output_field_or_none and
|
||||||
|
# Skip cast if rhs has a matching range type.
|
||||||
|
not isinstance(self.rhs._output_field_or_none, self.lhs.output_field.__class__)
|
||||||
|
):
|
||||||
cast_internal_type = self.lhs.output_field.base_field.get_internal_type()
|
cast_internal_type = self.lhs.output_field.base_field.get_internal_type()
|
||||||
cast_sql = '::{}'.format(connection.data_types.get(cast_internal_type))
|
cast_sql = '::{}'.format(connection.data_types.get(cast_internal_type))
|
||||||
return '%s @> %s%s' % (lhs, rhs, cast_sql), params
|
return '%s @> %s%s' % (lhs, rhs, cast_sql), params
|
||||||
|
|
|
@ -12,3 +12,9 @@ Bugfixes
|
||||||
* Fixed a regression in Django 2.2 when ordering a ``QuerySet.union()``,
|
* Fixed a regression in Django 2.2 when ordering a ``QuerySet.union()``,
|
||||||
``intersection()``, or ``difference()`` by a field type present more than
|
``intersection()``, or ``difference()`` by a field type present more than
|
||||||
once results in the wrong ordering being used (:ticket:`30628`).
|
once results in the wrong ordering being used (:ticket:`30628`).
|
||||||
|
|
||||||
|
* Fixed a migration crash on PostgreSQL when adding a check constraint
|
||||||
|
with a ``contains`` lookup on
|
||||||
|
:class:`~django.contrib.postgres.fields.DateRangeField` or
|
||||||
|
:class:`~django.contrib.postgres.fields.DateTimeRangeField`, if the right
|
||||||
|
hand side of an expression is the same type (:ticket:`30621`).
|
||||||
|
|
|
@ -211,7 +211,9 @@ class Migration(migrations.Migration):
|
||||||
('bigints', BigIntegerRangeField(null=True, blank=True)),
|
('bigints', BigIntegerRangeField(null=True, blank=True)),
|
||||||
('decimals', DecimalRangeField(null=True, blank=True)),
|
('decimals', DecimalRangeField(null=True, blank=True)),
|
||||||
('timestamps', DateTimeRangeField(null=True, blank=True)),
|
('timestamps', DateTimeRangeField(null=True, blank=True)),
|
||||||
|
('timestamps_inner', DateTimeRangeField(null=True, blank=True)),
|
||||||
('dates', DateRangeField(null=True, blank=True)),
|
('dates', DateRangeField(null=True, blank=True)),
|
||||||
|
('dates_inner', DateRangeField(null=True, blank=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'required_db_vendor': 'postgresql'
|
'required_db_vendor': 'postgresql'
|
||||||
|
|
|
@ -135,7 +135,9 @@ class RangesModel(PostgreSQLModel):
|
||||||
bigints = BigIntegerRangeField(blank=True, null=True)
|
bigints = BigIntegerRangeField(blank=True, null=True)
|
||||||
decimals = DecimalRangeField(blank=True, null=True)
|
decimals = DecimalRangeField(blank=True, null=True)
|
||||||
timestamps = DateTimeRangeField(blank=True, null=True)
|
timestamps = DateTimeRangeField(blank=True, null=True)
|
||||||
|
timestamps_inner = DateTimeRangeField(blank=True, null=True)
|
||||||
dates = DateRangeField(blank=True, null=True)
|
dates = DateRangeField(blank=True, null=True)
|
||||||
|
dates_inner = DateRangeField(blank=True, null=True)
|
||||||
|
|
||||||
|
|
||||||
class RangeLookupsModel(PostgreSQLModel):
|
class RangeLookupsModel(PostgreSQLModel):
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
from django.db import connection, transaction
|
from django.db import connection, transaction
|
||||||
from django.db.models import Q
|
from django.db.models import F, Q
|
||||||
from django.db.models.constraints import CheckConstraint
|
from django.db.models.constraints import CheckConstraint
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
|
@ -33,3 +35,51 @@ class SchemaTests(PostgreSQLTestCase):
|
||||||
with self.assertRaises(IntegrityError), transaction.atomic():
|
with self.assertRaises(IntegrityError), transaction.atomic():
|
||||||
RangesModel.objects.create(ints=(20, 50))
|
RangesModel.objects.create(ints=(20, 50))
|
||||||
RangesModel.objects.create(ints=(10, 30))
|
RangesModel.objects.create(ints=(10, 30))
|
||||||
|
|
||||||
|
def test_check_constraint_daterange_contains(self):
|
||||||
|
constraint_name = 'dates_contains'
|
||||||
|
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
||||||
|
constraint = CheckConstraint(
|
||||||
|
check=Q(dates__contains=F('dates_inner')),
|
||||||
|
name=constraint_name,
|
||||||
|
)
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
editor.add_constraint(RangesModel, constraint)
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
constraints = connection.introspection.get_constraints(cursor, RangesModel._meta.db_table)
|
||||||
|
self.assertIn(constraint_name, constraints)
|
||||||
|
date_1 = datetime.date(2016, 1, 1)
|
||||||
|
date_2 = datetime.date(2016, 1, 4)
|
||||||
|
with self.assertRaises(IntegrityError), transaction.atomic():
|
||||||
|
RangesModel.objects.create(
|
||||||
|
dates=(date_1, date_2),
|
||||||
|
dates_inner=(date_1, date_2.replace(day=5)),
|
||||||
|
)
|
||||||
|
RangesModel.objects.create(
|
||||||
|
dates=(date_1, date_2),
|
||||||
|
dates_inner=(date_1, date_2),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_check_constraint_datetimerange_contains(self):
|
||||||
|
constraint_name = 'timestamps_contains'
|
||||||
|
self.assertNotIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
|
||||||
|
constraint = CheckConstraint(
|
||||||
|
check=Q(timestamps__contains=F('timestamps_inner')),
|
||||||
|
name=constraint_name,
|
||||||
|
)
|
||||||
|
with connection.schema_editor() as editor:
|
||||||
|
editor.add_constraint(RangesModel, constraint)
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
constraints = connection.introspection.get_constraints(cursor, RangesModel._meta.db_table)
|
||||||
|
self.assertIn(constraint_name, constraints)
|
||||||
|
datetime_1 = datetime.datetime(2016, 1, 1)
|
||||||
|
datetime_2 = datetime.datetime(2016, 1, 2, 12)
|
||||||
|
with self.assertRaises(IntegrityError), transaction.atomic():
|
||||||
|
RangesModel.objects.create(
|
||||||
|
timestamps=(datetime_1, datetime_2),
|
||||||
|
timestamps_inner=(datetime_1, datetime_2.replace(hour=13)),
|
||||||
|
)
|
||||||
|
RangesModel.objects.create(
|
||||||
|
timestamps=(datetime_1, datetime_2),
|
||||||
|
timestamps_inner=(datetime_1, datetime_2),
|
||||||
|
)
|
||||||
|
|
|
@ -115,11 +115,15 @@ class TestRangeContainsLookup(PostgreSQLTestCase):
|
||||||
]
|
]
|
||||||
cls.obj = RangesModel.objects.create(
|
cls.obj = RangesModel.objects.create(
|
||||||
dates=(cls.dates[0], cls.dates[3]),
|
dates=(cls.dates[0], cls.dates[3]),
|
||||||
|
dates_inner=(cls.dates[1], cls.dates[2]),
|
||||||
timestamps=(cls.timestamps[0], cls.timestamps[3]),
|
timestamps=(cls.timestamps[0], cls.timestamps[3]),
|
||||||
|
timestamps_inner=(cls.timestamps[1], cls.timestamps[2]),
|
||||||
)
|
)
|
||||||
cls.aware_obj = RangesModel.objects.create(
|
cls.aware_obj = RangesModel.objects.create(
|
||||||
dates=(cls.dates[0], cls.dates[3]),
|
dates=(cls.dates[0], cls.dates[3]),
|
||||||
|
dates_inner=(cls.dates[1], cls.dates[2]),
|
||||||
timestamps=(cls.aware_timestamps[0], cls.aware_timestamps[3]),
|
timestamps=(cls.aware_timestamps[0], cls.aware_timestamps[3]),
|
||||||
|
timestamps_inner=(cls.timestamps[1], cls.timestamps[2]),
|
||||||
)
|
)
|
||||||
# Objects that don't match any queries.
|
# Objects that don't match any queries.
|
||||||
for i in range(3, 4):
|
for i in range(3, 4):
|
||||||
|
@ -140,6 +144,7 @@ class TestRangeContainsLookup(PostgreSQLTestCase):
|
||||||
(self.aware_timestamps[1], self.aware_timestamps[2]),
|
(self.aware_timestamps[1], self.aware_timestamps[2]),
|
||||||
Value(self.dates[0], output_field=DateTimeField()),
|
Value(self.dates[0], output_field=DateTimeField()),
|
||||||
Func(F('dates'), function='lower', output_field=DateTimeField()),
|
Func(F('dates'), function='lower', output_field=DateTimeField()),
|
||||||
|
F('timestamps_inner'),
|
||||||
)
|
)
|
||||||
for filter_arg in filter_args:
|
for filter_arg in filter_args:
|
||||||
with self.subTest(filter_arg=filter_arg):
|
with self.subTest(filter_arg=filter_arg):
|
||||||
|
@ -154,6 +159,7 @@ class TestRangeContainsLookup(PostgreSQLTestCase):
|
||||||
(self.dates[1], self.dates[2]),
|
(self.dates[1], self.dates[2]),
|
||||||
Value(self.dates[0], output_field=DateField()),
|
Value(self.dates[0], output_field=DateField()),
|
||||||
Func(F('timestamps'), function='lower', output_field=DateField()),
|
Func(F('timestamps'), function='lower', output_field=DateField()),
|
||||||
|
F('dates_inner'),
|
||||||
)
|
)
|
||||||
for filter_arg in filter_args:
|
for filter_arg in filter_args:
|
||||||
with self.subTest(filter_arg=filter_arg):
|
with self.subTest(filter_arg=filter_arg):
|
||||||
|
@ -361,7 +367,9 @@ class TestSerialization(PostgreSQLSimpleTestCase):
|
||||||
'\\"bounds\\": \\"[)\\"}", "decimals": "{\\"empty\\": true}", '
|
'\\"bounds\\": \\"[)\\"}", "decimals": "{\\"empty\\": true}", '
|
||||||
'"bigints": null, "timestamps": "{\\"upper\\": \\"2014-02-02T12:12:12+00:00\\", '
|
'"bigints": null, "timestamps": "{\\"upper\\": \\"2014-02-02T12:12:12+00:00\\", '
|
||||||
'\\"lower\\": \\"2014-01-01T00:00:00+00:00\\", \\"bounds\\": \\"[)\\"}", '
|
'\\"lower\\": \\"2014-01-01T00:00:00+00:00\\", \\"bounds\\": \\"[)\\"}", '
|
||||||
'"dates": "{\\"upper\\": \\"2014-02-02\\", \\"lower\\": \\"2014-01-01\\", \\"bounds\\": \\"[)\\"}" }, '
|
'"timestamps_inner": null, '
|
||||||
|
'"dates": "{\\"upper\\": \\"2014-02-02\\", \\"lower\\": \\"2014-01-01\\", \\"bounds\\": \\"[)\\"}", '
|
||||||
|
'"dates_inner": null }, '
|
||||||
'"model": "postgres_tests.rangesmodel", "pk": null}]'
|
'"model": "postgres_tests.rangesmodel", "pk": null}]'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue