diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py index 2683d8971a..9344979c56 100644 --- a/django/db/models/lookups.py +++ b/django/db/models/lookups.py @@ -5,7 +5,7 @@ from copy import copy from django.core.exceptions import EmptyResultSet from django.db.models.expressions import Case, Exists, Func, Value, When from django.db.models.fields import ( - BooleanField, DateTimeField, Field, IntegerField, + BooleanField, CharField, DateTimeField, Field, IntegerField, UUIDField, ) from django.db.models.query_utils import RegisterLookupMixin from django.utils.datastructures import OrderedSet @@ -548,3 +548,53 @@ class YearLt(YearLookup, LessThan): class YearLte(YearLookup, LessThanOrEqual): def get_bound_params(self, start, finish): return (finish,) + + +class UUIDTextMixin: + """ + Strip hyphens from a value when filtering a UUIDField on backends without + a native datatype for UUID. + """ + def process_rhs(self, qn, connection): + if not connection.features.has_native_uuid_field: + from django.db.models.functions import Replace + if self.rhs_is_direct_value(): + self.rhs = Value(self.rhs) + self.rhs = Replace(self.rhs, Value('-'), Value(''), output_field=CharField()) + rhs, params = super().process_rhs(qn, connection) + return rhs, params + + +@UUIDField.register_lookup +class UUIDIExact(UUIDTextMixin, IExact): + pass + + +@UUIDField.register_lookup +class UUIDContains(UUIDTextMixin, Contains): + pass + + +@UUIDField.register_lookup +class UUIDIContains(UUIDTextMixin, IContains): + pass + + +@UUIDField.register_lookup +class UUIDStartsWith(UUIDTextMixin, StartsWith): + pass + + +@UUIDField.register_lookup +class UUIDIStartsWith(UUIDTextMixin, IStartsWith): + pass + + +@UUIDField.register_lookup +class UUIDEndsWith(UUIDTextMixin, EndsWith): + pass + + +@UUIDField.register_lookup +class UUIDIEndsWith(UUIDTextMixin, IEndsWith): + pass diff --git a/tests/model_fields/test_uuid.py b/tests/model_fields/test_uuid.py index 875dac5c84..116e40065c 100644 --- a/tests/model_fields/test_uuid.py +++ b/tests/model_fields/test_uuid.py @@ -4,7 +4,7 @@ import uuid from django.core import exceptions, serializers from django.db import IntegrityError, connection, models from django.db.models import CharField, F, Value -from django.db.models.functions import Concat +from django.db.models.functions import Concat, Repeat from django.test import ( SimpleTestCase, TestCase, TransactionTestCase, skipUnlessDBFeature, ) @@ -121,6 +121,12 @@ class TestQuerying(TestCase): ), [self.objs[1]], ) + self.assertSequenceEqual( + NullableUUIDModel.objects.filter( + field__iexact='550E8400-E29B-41D4-A716-446655440000' + ), + [self.objs[1]], + ) def test_isnull(self): self.assertSequenceEqual( @@ -133,36 +139,60 @@ class TestQuerying(TestCase): NullableUUIDModel.objects.filter(field__contains='8400e29b'), [self.objs[1]], ) + self.assertSequenceEqual( + NullableUUIDModel.objects.filter(field__contains='8400-e29b'), + [self.objs[1]], + ) def test_icontains(self): self.assertSequenceEqualWithoutHyphens( NullableUUIDModel.objects.filter(field__icontains='8400E29B'), [self.objs[1]], ) + self.assertSequenceEqual( + NullableUUIDModel.objects.filter(field__icontains='8400-E29B'), + [self.objs[1]], + ) def test_startswith(self): self.assertSequenceEqualWithoutHyphens( NullableUUIDModel.objects.filter(field__startswith='550e8400e29b4'), [self.objs[1]], ) + self.assertSequenceEqual( + NullableUUIDModel.objects.filter(field__startswith='550e8400-e29b-4'), + [self.objs[1]], + ) def test_istartswith(self): self.assertSequenceEqualWithoutHyphens( NullableUUIDModel.objects.filter(field__istartswith='550E8400E29B4'), [self.objs[1]], ) + self.assertSequenceEqual( + NullableUUIDModel.objects.filter(field__istartswith='550E8400-E29B-4'), + [self.objs[1]], + ) def test_endswith(self): self.assertSequenceEqualWithoutHyphens( NullableUUIDModel.objects.filter(field__endswith='a716446655440000'), [self.objs[1]], ) + self.assertSequenceEqual( + NullableUUIDModel.objects.filter(field__endswith='a716-446655440000'), + [self.objs[1]], + ) def test_iendswith(self): self.assertSequenceEqualWithoutHyphens( NullableUUIDModel.objects.filter(field__iendswith='A716446655440000'), [self.objs[1]], ) + self.assertSequenceEqual( + NullableUUIDModel.objects.filter(field__iendswith='A716-446655440000'), + [self.objs[1]], + ) def test_filter_with_expr(self): self.assertSequenceEqualWithoutHyphens( @@ -171,6 +201,18 @@ class TestQuerying(TestCase): ).filter(field__contains=F('value')), [self.objs[1]], ) + self.assertSequenceEqual( + NullableUUIDModel.objects.annotate( + value=Concat(Value('8400'), Value('-'), Value('e29b'), output_field=CharField()), + ).filter(field__contains=F('value')), + [self.objs[1]], + ) + self.assertSequenceEqual( + NullableUUIDModel.objects.annotate( + value=Repeat(Value('0'), 4, output_field=CharField()), + ).filter(field__contains=F('value')), + [self.objs[1]], + ) class TestSerialization(SimpleTestCase):