Skip to content

Commit 2e289e7

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 d0867c9 commit 2e289e7

File tree

9 files changed

+122
-5
lines changed

9 files changed

+122
-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
@@ -35,6 +35,17 @@ Bug fixes
3535
databases.
3636
- :meth:`QuerySet.explain() <django.db.models.query.QuerySet.explain>` now
3737
:ref:`returns a string that can be parsed as JSON <queryset-explain>`.
38+
- Fixed unique constraint generation for :class:`~django.db.models.IntegerField`
39+
and :class:`~django.db.models.PositiveBigIntegerField`, which incorrectly
40+
allowed duplicate values larger than 32 bits. Existing unique constraints
41+
must be recreated to use ``$type: long`` instead of ``int``.
42+
- :class:`~django.db.models.IntegerField`,
43+
:class:`~django.db.models.BigIntegerField` (as well as the
44+
``Positive`` versions of these fields), and
45+
:class:`~django.db.models.DurationField` values are now sent to MongoDB as
46+
:class:`bson.int64.Int64`, which fixes unique constraints on values less than
47+
32 bits for ``BigIntegerField``, ``PositiveIntegerField``, and
48+
``DurationField``. Existing data must be converted to ``Int64``.
3849

3950
Performance improvements
4051
------------------------

tests/model_fields_/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515

1616
class Integers(models.Model):
1717
small = models.SmallIntegerField(unique=True, blank=True, null=True)
18+
normal = models.IntegerField(unique=True, blank=True, null=True)
19+
big = models.BigIntegerField(unique=True, blank=True, null=True)
1820
positive_small = models.PositiveSmallIntegerField(unique=True, blank=True, null=True)
21+
positive = models.PositiveIntegerField(unique=True, blank=True, null=True)
22+
positive_big = models.PositiveBigIntegerField(unique=True, blank=True, null=True)
23+
duration = models.DurationField(unique=True, blank=True, null=True)
1924

2025

2126
# 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))

tests/model_fields_/test_integerfield.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,58 @@ def test_validate_min_value(self):
7373
msg = "{'positive_small': ['Ensure this value is greater than or equal to 0.']"
7474
with self.assertRaisesMessage(ValidationError, msg):
7575
Integers(positive_small=self.min_value - 1).full_clean()
76+
77+
78+
class SmallUniqueTests(TestCase):
79+
"""
80+
Duplicate values < 32 bits are prohibited. This confirms integer field
81+
values are cast to Int64 so MongoDB stores it as long. Otherwise, the
82+
partialFilterExpression: {$type: long} unique constraint doesn't work.
83+
"""
84+
85+
def test_integerfield(self):
86+
Integers.objects.create(normal=123)
87+
with self.assertRaises(IntegrityError):
88+
Integers.objects.create(normal=123)
89+
90+
def test_bigintegerfield(self):
91+
Integers.objects.create(big=123)
92+
with self.assertRaises(IntegrityError):
93+
Integers.objects.create(big=123)
94+
95+
def test_positiveintegerfield(self):
96+
Integers.objects.create(positive=123)
97+
with self.assertRaises(IntegrityError):
98+
Integers.objects.create(positive=123)
99+
100+
def test_positivebigintegerfield(self):
101+
Integers.objects.create(positive_big=123)
102+
with self.assertRaises(IntegrityError):
103+
Integers.objects.create(positive_big=123)
104+
105+
106+
class LargeUniqueTests(TestCase):
107+
"""
108+
Duplicate values > 32 bits are prohibited. This confirms each field uses
109+
the long db_type() rather than the 32 bit int type.
110+
"""
111+
112+
def test_integerfield(self):
113+
Integers.objects.create(normal=2**31)
114+
with self.assertRaises(IntegrityError):
115+
Integers.objects.create(normal=2**31)
116+
117+
def test_bigintegerfield(self):
118+
Integers.objects.create(big=2**31)
119+
with self.assertRaises(IntegrityError):
120+
Integers.objects.create(big=2**31)
121+
122+
def test_positiveintegerfield(self):
123+
Integers.objects.create(positive=2**31)
124+
with self.assertRaises(IntegrityError):
125+
Integers.objects.create(positive=2**31)
126+
127+
def test_positivebigintegerfield(self):
128+
Integers.objects.create(positive_big=2**31)
129+
with self.assertRaises(IntegrityError):
130+
Integers.objects.create(positive_big=2**31)

0 commit comments

Comments
 (0)