Skip to content

Commit 59904f4

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 fieldd.
1 parent 2066af9 commit 59904f4

File tree

9 files changed

+146
-7
lines changed

9 files changed

+146
-7
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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,16 @@ 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",
55-
"PositiveSmallIntegerField": "int",
55+
"PositiveSmallIntegerField": "long",
5656
"SlugField": "string",
5757
"SmallAutoField": "", # Not supported
58-
"SmallIntegerField": "int",
58+
"SmallIntegerField": "long",
5959
"TextField": "string",
6060
"TimeField": "date",
6161
"UUIDField": "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: 15 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,12 @@ 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+
return Int64(value)
66+
6167
def adapt_json_value(self, value, encoder):
6268
if encoder is None:
6369
return value
@@ -123,8 +129,16 @@ def get_db_converters(self, expression):
123129
converters.append(self.convert_timefield_value)
124130
elif internal_type == "UUIDField":
125131
converters.append(self.convert_uuidfield_value)
132+
elif "IntegerField" in internal_type:
133+
converters.append(self.convert_integerfield_value)
126134
return converters
127135

136+
def convert_integerfield_value(self, value, expression, connection):
137+
if value is not None:
138+
# from Int64 to int
139+
value = int(value)
140+
return value
141+
128142
def convert_datefield_value(self, value, expression, connection):
129143
if value is not None:
130144
value = value.date()

docs/source/releases/5.2.x.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ Bug fixes
3333
- :meth:`QuerySet.explain() <django.db.models.query.QuerySet.explain>` now
3434
:ref:`returns a string that can be parsed as JSON <queryset-explain>`.
3535
- Improved ``QuerySet`` performance by removing low limit on server-side chunking.
36+
- Fixed unique constraint generation for ``SmallIntegerField``, ``IntegerField``,
37+
``PositiveSmallIntegerField``, and ``PositiveBigIntegerField``, which incorrectly
38+
allowed duplicate values larger than 32 bits. All integer field and ``DurationField``
39+
values are now sent to MongoDB as ``bson.Int64``, which fixes unique constraints on
40+
values less than 32 bits for ``BigIntegerField``, ``PositiveIntegerField``, and
41+
``DurationField``. Existing unique constraints and data must be corrected manually.
3642

3743
5.2.0 beta 1
3844
============

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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 values are
10+
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_smallintegerfield(self):
15+
Integers.objects.create(small=123)
16+
with self.assertRaises(IntegrityError):
17+
Integers.objects.create(small=123)
18+
19+
def test_integerfield(self):
20+
Integers.objects.create(normal=123)
21+
with self.assertRaises(IntegrityError):
22+
Integers.objects.create(normal=123)
23+
24+
def test_bigintegerfield(self):
25+
Integers.objects.create(big=123)
26+
with self.assertRaises(IntegrityError):
27+
Integers.objects.create(big=123)
28+
29+
def test_positivesmallintegerfield(self):
30+
Integers.objects.create(positive_small=123)
31+
with self.assertRaises(IntegrityError):
32+
Integers.objects.create(positive_small=123)
33+
34+
def test_positiveintegerfield(self):
35+
Integers.objects.create(positive=123)
36+
with self.assertRaises(IntegrityError):
37+
Integers.objects.create(positive=123)
38+
39+
def test_positivebigintegerfield(self):
40+
Integers.objects.create(positive_big=123)
41+
with self.assertRaises(IntegrityError):
42+
Integers.objects.create(positive_big=123)
43+
44+
45+
class LargeUniqueTests(TestCase):
46+
"""
47+
Duplicate values > 32 bits are prohibited. This confirms each field uses the long
48+
db_type() rather than the 32 bit int type.
49+
"""
50+
51+
def test_smallintegerfield(self):
52+
Integers.objects.create(small=2**31)
53+
with self.assertRaises(IntegrityError):
54+
Integers.objects.create(small=2**31)
55+
56+
def test_integerfield(self):
57+
Integers.objects.create(normal=2**31)
58+
with self.assertRaises(IntegrityError):
59+
Integers.objects.create(normal=2**31)
60+
61+
def test_bigintegerfield(self):
62+
Integers.objects.create(big=2**31)
63+
with self.assertRaises(IntegrityError):
64+
Integers.objects.create(big=2**31)
65+
66+
def test_positivesmallintegerfield(self):
67+
Integers.objects.create(positive_small=2**31)
68+
with self.assertRaises(IntegrityError):
69+
Integers.objects.create(positive_small=2**31)
70+
71+
def test_positiveintegerfield(self):
72+
Integers.objects.create(positive=2**31)
73+
with self.assertRaises(IntegrityError):
74+
Integers.objects.create(positive=2**31)
75+
76+
def test_positivebigintegerfield(self):
77+
Integers.objects.create(positive_big=2**31)
78+
with self.assertRaises(IntegrityError):
79+
Integers.objects.create(positive_big=2**31)

0 commit comments

Comments
 (0)