diff --git a/django_mongodb_backend/operations.py b/django_mongodb_backend/operations.py index 6df845e7..435c5bb8 100644 --- a/django_mongodb_backend/operations.py +++ b/django_mongodb_backend/operations.py @@ -256,13 +256,15 @@ def explain_query_prefix(self, format=None, **options): return validated_options def integer_field_range(self, internal_type): - # MongODB doesn't enforce any integer constraints, but it supports - # integers up to 64 bits. - if internal_type in { - "PositiveBigIntegerField", - "PositiveIntegerField", - "PositiveSmallIntegerField", - }: + # MongoDB doesn't enforce any integer constraints, but the + # SmallIntegerFields use "int" for unique constraints which is limited + # to 32 bits. + if internal_type == "PositiveSmallIntegerField": + return (0, 2147483647) + if internal_type == "SmallIntegerField": + return (-2147483648, 2147483647) + # Other fields use "long" which supports up to 64 bits. + if internal_type in {"PositiveBigIntegerField", "PositiveIntegerField"}: return (0, 9223372036854775807) return (-9223372036854775808, 9223372036854775807) diff --git a/docs/source/ref/models/fields.rst b/docs/source/ref/models/fields.rst index 870d9706..eca30425 100644 --- a/docs/source/ref/models/fields.rst +++ b/docs/source/ref/models/fields.rst @@ -1,6 +1,36 @@ +===================== Model field reference ===================== +Supported model fields +====================== + +All of Django's :doc:`model fields ` are +supported, except: + +- :class:`~django.db.models.AutoField` (including + :class:`~django.db.models.BigAutoField` and + :class:`~django.db.models.SmallAutoField`) +- :class:`~django.db.models.CompositePrimaryKey` +- :class:`~django.db.models.GeneratedField` + +A few notes about some of the other fields: + +- :class:`~django.db.models.DateTimeField` is limited to millisecond precision + (rather than microsecond like most other databases), and correspondingly, + :class:`~django.db.models.DurationField` stores milliseconds rather than + microseconds. +- :class:`~django.db.models.SmallIntegerField` and + :class:`~django.db.models.PositiveSmallIntegerField` support 32 bit values + (ranges ``(-2147483648, 2147483647)`` and ``(0, 2147483647)``, respectively), + validated by forms and model validation. Be careful because MongoDB doesn't + prohibit inserting values outside of the supported range and unique + constraints don't work for values outside of the 32-bit range of the BSON + ``int`` type. + +MongoDB-specific model fields +============================= + .. module:: django_mongodb_backend.fields Some MongoDB-specific fields are available in ``django_mongodb_backend.fields``. diff --git a/docs/source/releases/5.2.x.rst b/docs/source/releases/5.2.x.rst index dd213a63..b672aed2 100644 --- a/docs/source/releases/5.2.x.rst +++ b/docs/source/releases/5.2.x.rst @@ -21,6 +21,9 @@ New features Backwards incompatible changes ------------------------------ +- :class:`django.db.models.SmallIntegerField` and + :class:`django.db.models.PositiveSmallIntegerField` are now limited to 32 bit + values in forms and model validation. - Removed support for database caching as the MongoDB security team considers the cache backend's ``pickle`` encoding of cached values a vulnerability. If an attacker compromises the database, they could run arbitrary commands on the application diff --git a/tests/model_fields_/models.py b/tests/model_fields_/models.py index c05b0556..0a001067 100644 --- a/tests/model_fields_/models.py +++ b/tests/model_fields_/models.py @@ -13,6 +13,11 @@ from django_mongodb_backend.models import EmbeddedModel +class UniqueIntegers(models.Model): + small = models.SmallIntegerField(unique=True, blank=True, null=True) + positive_small = models.PositiveSmallIntegerField(unique=True, blank=True, null=True) + + # ObjectIdField class ObjectIdModel(models.Model): field = ObjectIdField() diff --git a/tests/model_fields_/test_integerfield.py b/tests/model_fields_/test_integerfield.py new file mode 100644 index 00000000..bd3ec491 --- /dev/null +++ b/tests/model_fields_/test_integerfield.py @@ -0,0 +1,75 @@ +from django.core.exceptions import ValidationError +from django.db import IntegrityError +from django.test import TestCase + +from .models import UniqueIntegers + + +class SmallIntegerFieldTests(TestCase): + max_value = 2**31 - 1 + min_value = -(2**31) + + def test_unique_max_value(self): + """ + SmallIntegerField.db_type() is "int" which means unique constraints + are only enforced up to 32-bit values. + """ + UniqueIntegers.objects.create(small=self.max_value + 1) + UniqueIntegers.objects.create(small=self.max_value + 1) # no IntegrityError + UniqueIntegers.objects.create(small=self.max_value) + with self.assertRaises(IntegrityError): + UniqueIntegers.objects.create(small=self.max_value) + + def test_unique_min_value(self): + """ + SmallIntegerField.db_type() is "int" which means unique constraints + are only enforced down to negative 32-bit values. + """ + UniqueIntegers.objects.create(small=self.min_value - 1) + UniqueIntegers.objects.create(small=self.min_value - 1) # no IntegrityError + UniqueIntegers.objects.create(small=self.min_value) + with self.assertRaises(IntegrityError): + UniqueIntegers.objects.create(small=self.min_value) + + def test_validate_max_value(self): + UniqueIntegers(small=self.max_value).full_clean() # no error + msg = "{'small': ['Ensure this value is less than or equal to 2147483647.']" + with self.assertRaisesMessage(ValidationError, msg): + UniqueIntegers(small=self.max_value + 1).full_clean() + + def test_validate_min_value(self): + UniqueIntegers(small=self.min_value).full_clean() # no error + msg = "{'small': ['Ensure this value is greater than or equal to -2147483648.']" + with self.assertRaisesMessage(ValidationError, msg): + UniqueIntegers(small=self.min_value - 1).full_clean() + + +class PositiveSmallIntegerFieldTests(TestCase): + max_value = 2**31 - 1 + min_value = 0 + + def test_unique_max_value(self): + """ + SmallIntegerField.db_type() is "int" which means unique constraints + are only enforced up to 32-bit values. + """ + UniqueIntegers.objects.create(positive_small=self.max_value + 1) + UniqueIntegers.objects.create(positive_small=self.max_value + 1) # no IntegrityError + UniqueIntegers.objects.create(positive_small=self.max_value) + with self.assertRaises(IntegrityError): + UniqueIntegers.objects.create(positive_small=self.max_value) + + # test_unique_min_value isn't needed since PositiveSmallIntegerField has a + # limit of zero (enforced only in forms and model validation). + + def test_validate_max_value(self): + UniqueIntegers(positive_small=self.max_value).full_clean() # no error + msg = "{'positive_small': ['Ensure this value is less than or equal to 2147483647.']" + with self.assertRaisesMessage(ValidationError, msg): + UniqueIntegers(positive_small=self.max_value + 1).full_clean() + + def test_validate_min_value(self): + UniqueIntegers(positive_small=self.min_value).full_clean() # no error + msg = "{'positive_small': ['Ensure this value is greater than or equal to 0.']" + with self.assertRaisesMessage(ValidationError, msg): + UniqueIntegers(positive_small=self.min_value - 1).full_clean()