Skip to content

Commit 13b351e

Browse files
committed
Fix DurationField, IntegerField unique constraints for values < or > 32 bits
Whether values less than or greater than 32 bits were problematic depends on the field.
1 parent 7abb80f commit 13b351e

File tree

9 files changed

+131
-5
lines changed

9 files changed

+131
-5
lines changed

.github/workflows/test-python-atlas.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
uses: actions/checkout@v4
3434
with:
3535
repository: 'mongodb-forks/django'
36-
ref: 'mongodb-5.2.x'
36+
ref: 'ints-as-long'
3737
path: 'django_repo'
3838
persist-credentials: false
3939
- name: Install system packages for Django's Python test dependencies

.github/workflows/test-python.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
uses: actions/checkout@v4
3434
with:
3535
repository: 'mongodb-forks/django'
36-
ref: 'mongodb-5.2.x'
36+
ref: 'ints-as-long'
3737
path: 'django_repo'
3838
persist-credentials: false
3939
- name: Install system packages for Django's Python test dependencies

django_mongodb_backend/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ class DatabaseWrapper(BaseDatabaseWrapper):
4646
"FileField": "string",
4747
"FilePathField": "string",
4848
"FloatField": "double",
49-
"IntegerField": "int",
49+
"IntegerField": "long",
5050
"BigIntegerField": "long",
5151
"GenericIPAddressField": "string",
5252
"JSONField": "object",
53-
"PositiveBigIntegerField": "int",
53+
"PositiveBigIntegerField": "long",
5454
"PositiveIntegerField": "long",
5555
"PositiveSmallIntegerField": "int",
5656
"SlugField": "string",

django_mongodb_backend/fields/duration.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from bson import Int64
12
from django.db.models.fields import DurationField
23

34
_get_db_prep_value = DurationField.get_db_prep_value
@@ -8,6 +9,8 @@ def get_db_prep_value(self, value, connection, prepared=False):
89
value = _get_db_prep_value(self, value, connection, prepared)
910
if connection.vendor == "mongodb" and value is not None:
1011
value //= 1000
12+
# Store value as Int64 (long).
13+
value = Int64(value)
1114
return value
1215

1316

django_mongodb_backend/operations.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import uuid
55
from decimal import Decimal
66

7-
from bson.decimal128 import Decimal128
7+
from bson import Decimal128, Int64
88
from django.conf import settings
99
from django.db import DataError
1010
from django.db.backends.base.operations import BaseDatabaseOperations
@@ -58,6 +58,14 @@ def adapt_decimalfield_value(self, value, max_digits=None, decimal_places=None):
5858
return None
5959
return Decimal128(value)
6060

61+
def adapt_integerfield_value(self, value, internal_type):
62+
"""Store all IntegerField variants as Int64 (long)."""
63+
if value is None:
64+
return None
65+
if "Small" not in internal_type:
66+
return Int64(value)
67+
return value
68+
6169
def adapt_json_value(self, value, encoder):
6270
if encoder is None:
6371
return value
@@ -123,8 +131,16 @@ def get_db_converters(self, expression):
123131
converters.append(self.convert_timefield_value)
124132
elif internal_type == "UUIDField":
125133
converters.append(self.convert_uuidfield_value)
134+
elif "IntegerField" in internal_type and "Small" not in internal_type:
135+
converters.append(self.convert_integerfield_value)
126136
return converters
127137

138+
def convert_integerfield_value(self, value, expression, connection):
139+
if value is not None:
140+
# from Int64 to int
141+
value = int(value)
142+
return value
143+
128144
def convert_datefield_value(self, value, expression, connection):
129145
if value is not None:
130146
value = value.date()

docs/source/releases/5.2.x.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ Bug fixes
3232
databases.
3333
- :meth:`QuerySet.explain() <django.db.models.query.QuerySet.explain>` now
3434
:ref:`returns a string that can be parsed as JSON <queryset-explain>`.
35+
- Fixed unique constraint generation for :class:`~django.db.models.IntegerField`
36+
and :class:`~django.db.models.PositiveBigIntegerField`, which incorrectly
37+
allowed duplicate values larger than 32 bits. Existing unique constraints
38+
must be recreated to use ``$type: long`` instead of ``int``.
39+
- :class:`~django.db.models.IntegerField`,
40+
:class:`~django.db.models.BigIntegerField` (as well as the
41+
``Positive`` versions of these fields), and
42+
:class:`~django.db.models.DurationField` values are now sent to MongoDB as
43+
:class:`bson.int64.Int64`, which fixes unique constraints on values less than
44+
32 bits for ``BigIntegerField``, ``PositiveIntegerField``, and
45+
``DurationField``. Existing data must be converted to ``Int64``.
3546

3647
Performance improvements
3748
------------------------

tests/model_fields_/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@
1313
from django_mongodb_backend.models import EmbeddedModel
1414

1515

16+
class Integers(models.Model):
17+
small = models.SmallIntegerField(unique=True, null=True)
18+
normal = models.IntegerField(unique=True, null=True)
19+
big = models.BigIntegerField(unique=True, null=True)
20+
positive_small = models.PositiveSmallIntegerField(unique=True, null=True)
21+
positive = models.PositiveIntegerField(unique=True, null=True)
22+
positive_big = models.PositiveBigIntegerField(unique=True, null=True)
23+
duration = models.DurationField(unique=True, null=True)
24+
25+
1626
# ObjectIdField
1727
class ObjectIdModel(models.Model):
1828
field = ObjectIdField()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from datetime import timedelta
2+
3+
from django.db import IntegrityError
4+
from django.test import TestCase
5+
6+
from .models import Integers
7+
8+
9+
class UniqueTests(TestCase):
10+
def test_small_value(self):
11+
"""
12+
Duplicate values < 32 bits are prohibited. This confirms DurationField values
13+
are cast to Int64 so MongoDB stores them as long. Otherwise, the
14+
partialFilterExpression: {$type: long} unique constraint doesn't work.
15+
"""
16+
Integers.objects.create(duration=timedelta(1))
17+
with self.assertRaises(IntegrityError):
18+
Integers.objects.create(duration=timedelta(1))
19+
20+
def test_large_value(self):
21+
"""
22+
Duplicate values > 32 bits are prohibited. This confirms DurationField uses the
23+
long db_type() rather than the 32 bit int type.
24+
"""
25+
Integers.objects.create(duration=timedelta(1000000))
26+
with self.assertRaises(IntegrityError):
27+
Integers.objects.create(duration=timedelta(1000000))
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from django.db import IntegrityError
2+
from django.test import TestCase
3+
4+
from .models import Integers
5+
6+
7+
class SmallUniqueTests(TestCase):
8+
"""
9+
Duplicate values < 32 bits are prohibited. This confirms integer field
10+
values are cast to Int64 so MongoDB stores it as long. Otherwise, the
11+
partialFilterExpression: {$type: long} unique constraint doesn't work.
12+
"""
13+
14+
def test_integerfield(self):
15+
Integers.objects.create(normal=123)
16+
with self.assertRaises(IntegrityError):
17+
Integers.objects.create(normal=123)
18+
19+
def test_bigintegerfield(self):
20+
Integers.objects.create(big=123)
21+
with self.assertRaises(IntegrityError):
22+
Integers.objects.create(big=123)
23+
24+
def test_positiveintegerfield(self):
25+
Integers.objects.create(positive=123)
26+
with self.assertRaises(IntegrityError):
27+
Integers.objects.create(positive=123)
28+
29+
def test_positivebigintegerfield(self):
30+
Integers.objects.create(positive_big=123)
31+
with self.assertRaises(IntegrityError):
32+
Integers.objects.create(positive_big=123)
33+
34+
35+
class LargeUniqueTests(TestCase):
36+
"""
37+
Duplicate values > 32 bits are prohibited. This confirms each field uses
38+
the long db_type() rather than the 32 bit int type.
39+
"""
40+
41+
def test_integerfield(self):
42+
Integers.objects.create(normal=2**31)
43+
with self.assertRaises(IntegrityError):
44+
Integers.objects.create(normal=2**31)
45+
46+
def test_bigintegerfield(self):
47+
Integers.objects.create(big=2**31)
48+
with self.assertRaises(IntegrityError):
49+
Integers.objects.create(big=2**31)
50+
51+
def test_positiveintegerfield(self):
52+
Integers.objects.create(positive=2**31)
53+
with self.assertRaises(IntegrityError):
54+
Integers.objects.create(positive=2**31)
55+
56+
def test_positivebigintegerfield(self):
57+
Integers.objects.create(positive_big=2**31)
58+
with self.assertRaises(IntegrityError):
59+
Integers.objects.create(positive_big=2**31)

0 commit comments

Comments
 (0)