Skip to content

Commit b611a24

Browse files
committed
Error on unsupported unique constraint conditions
CREATE INDEX in SQL Server only supports AND conditions (not OR) as part of its WHERE syntax. This change handles that situation by raising an error from the schema editor class. This change adds unit tests to confirm this happens against a SQL Server database.
1 parent 115e642 commit b611a24

File tree

4 files changed

+170
-4
lines changed

4 files changed

+170
-4
lines changed

mssql/schema.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
Table,
1818
)
1919
from django import VERSION as django_version
20-
from django.db.models import Index
20+
from django.db.models import Index, UniqueConstraint
2121
from django.db.models.fields import AutoField, BigAutoField
22+
from django.db.models.sql.where import AND
2223
from django.db.transaction import TransactionManagementError
2324
from django.utils.encoding import force_str
2425

@@ -955,3 +956,9 @@ def remove_field(self, model, field):
955956
for sql in list(self.deferred_sql):
956957
if isinstance(sql, Statement) and sql.references_column(model._meta.db_table, field.column):
957958
self.deferred_sql.remove(sql)
959+
960+
def add_constraint(self, model, constraint):
961+
if isinstance(constraint, UniqueConstraint) and constraint.condition and constraint.condition.connector != AND:
962+
raise NotImplementedError("The backend does not support %s conditions on unique constraint %s." %
963+
(constraint.condition.connector, constraint.name))
964+
super().add_constraint(model, constraint)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Generated by Django 3.1.5 on 2021-01-18 00:05
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('testapp', '0007_test_remove_onetoone_field_part2'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='TestUnsupportableUniqueConstraint',
15+
fields=[
16+
(
17+
'id',
18+
models.AutoField(
19+
auto_created=True,
20+
primary_key=True,
21+
serialize=False,
22+
verbose_name='ID',
23+
),
24+
),
25+
('_type', models.CharField(max_length=50)),
26+
('status', models.CharField(max_length=50)),
27+
],
28+
options={
29+
'managed': False,
30+
},
31+
),
32+
migrations.CreateModel(
33+
name='TestSupportableUniqueConstraint',
34+
fields=[
35+
(
36+
'id',
37+
models.AutoField(
38+
auto_created=True,
39+
primary_key=True,
40+
serialize=False,
41+
verbose_name='ID',
42+
),
43+
),
44+
('_type', models.CharField(max_length=50)),
45+
('status', models.CharField(max_length=50)),
46+
],
47+
),
48+
migrations.AddConstraint(
49+
model_name='testsupportableuniqueconstraint',
50+
constraint=models.UniqueConstraint(
51+
condition=models.Q(
52+
('status', 'in_progress'),
53+
('status', 'needs_changes'),
54+
('status', 'published'),
55+
),
56+
fields=('_type',),
57+
name='and_constraint',
58+
),
59+
),
60+
migrations.AddConstraint(
61+
model_name='testsupportableuniqueconstraint',
62+
constraint=models.UniqueConstraint(
63+
condition=models.Q(status__in=['in_progress', 'needs_changes']),
64+
fields=('_type',),
65+
name='in_constraint',
66+
),
67+
),
68+
]

testapp/models.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import uuid
55

66
from django.db import models
7+
from django.db.models import Q
78
from django.utils import timezone
89

910

@@ -74,3 +75,39 @@ class TestRemoveOneToOneFieldModel(models.Model):
7475
# thats already is removed.
7576
# b = models.OneToOneField('self', on_delete=models.SET_NULL, null=True)
7677
a = models.CharField(max_length=50)
78+
79+
80+
class TestUnsupportableUniqueConstraint(models.Model):
81+
class Meta:
82+
managed = False
83+
constraints = [
84+
models.UniqueConstraint(
85+
name='or_constraint',
86+
fields=['_type'],
87+
condition=(Q(status='in_progress') | Q(status='needs_changes')),
88+
),
89+
]
90+
91+
_type = models.CharField(max_length=50)
92+
status = models.CharField(max_length=50)
93+
94+
95+
class TestSupportableUniqueConstraint(models.Model):
96+
class Meta:
97+
constraints = [
98+
models.UniqueConstraint(
99+
name='and_constraint',
100+
fields=['_type'],
101+
condition=(
102+
Q(status='in_progress') & Q(status='needs_changes') & Q(status='published')
103+
),
104+
),
105+
models.UniqueConstraint(
106+
name='in_constraint',
107+
fields=['_type'],
108+
condition=(Q(status__in=['in_progress', 'needs_changes'])),
109+
),
110+
]
111+
112+
_type = models.CharField(max_length=50)
113+
status = models.CharField(max_length=50)

testapp/tests/test_constraints.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT license.
33

4+
from django.db import connections, migrations, models
45
from django.db.utils import IntegrityError
5-
from django.test import TestCase, skipUnlessDBFeature
6+
from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
67

8+
from sql_server.pyodbc.base import DatabaseWrapper
79
from ..models import (
8-
Author, Editor, Post,
9-
TestUniqueNullableModel, TestNullableUniqueTogetherModel,
10+
Author,
11+
Editor,
12+
Post,
13+
TestUniqueNullableModel,
14+
TestNullableUniqueTogetherModel,
1015
)
1116

1217

@@ -55,3 +60,52 @@ def test_after_type_change(self):
5560
TestNullableUniqueTogetherModel.objects.create(a='aaa', b='bbb', c='ccc')
5661
with self.assertRaises(IntegrityError):
5762
TestNullableUniqueTogetherModel.objects.create(a='aaa', b='bbb', c='ccc')
63+
64+
65+
class TestUniqueConstraints(TransactionTestCase):
66+
def test_unsupportable_unique_constraint(self):
67+
# Only execute tests when running against SQL Server
68+
connection = connections['default']
69+
if isinstance(connection, DatabaseWrapper):
70+
71+
class TestMigration(migrations.Migration):
72+
initial = True
73+
74+
operations = [
75+
migrations.CreateModel(
76+
name='TestUnsupportableUniqueConstraint',
77+
fields=[
78+
(
79+
'id',
80+
models.AutoField(
81+
auto_created=True,
82+
primary_key=True,
83+
serialize=False,
84+
verbose_name='ID',
85+
),
86+
),
87+
('_type', models.CharField(max_length=50)),
88+
('status', models.CharField(max_length=50)),
89+
],
90+
),
91+
migrations.AddConstraint(
92+
model_name='testunsupportableuniqueconstraint',
93+
constraint=models.UniqueConstraint(
94+
condition=models.Q(
95+
('status', 'in_progress'),
96+
('status', 'needs_changes'),
97+
_connector='OR',
98+
),
99+
fields=('_type',),
100+
name='or_constraint',
101+
),
102+
),
103+
]
104+
105+
migration = TestMigration('testapp', 'test_unsupportable_unique_constraint')
106+
107+
with connection.schema_editor(atomic=True) as editor:
108+
with self.assertRaisesRegex(
109+
NotImplementedError, "does not support OR conditions"
110+
):
111+
return migration.apply(ProjectState(), editor)

0 commit comments

Comments
 (0)