Skip to content

Commit 5a44cf9

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 79e421a commit 5a44cf9

File tree

4 files changed

+171
-5
lines changed

4 files changed

+171
-5
lines changed

sql_server/pyodbc/schema.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
Statement as DjStatement,
1515
Table,
1616
)
17-
from django.db.models import Index
17+
from django.db.models import Index, UniqueConstraint
1818
from django.db.models.fields import AutoField, BigAutoField
19+
from django.db.models.sql.where import AND
1920
from django.db.transaction import TransactionManagementError
2021
from django.utils.encoding import force_str
2122

@@ -970,3 +971,9 @@ def remove_field(self, model, field):
970971
for sql in list(self.deferred_sql):
971972
if isinstance(sql, Statement) and sql.references_column(model._meta.db_table, field.column):
972973
self.deferred_sql.remove(sql)
974+
975+
def add_constraint(self, model, constraint):
976+
if isinstance(constraint, UniqueConstraint) and constraint.condition and constraint.condition.connector != AND:
977+
raise NotImplementedError("The backend does not support %s conditions on unique constraint %s." %
978+
(constraint.condition.connector, constraint.name))
979+
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
@@ -1,6 +1,7 @@
11
import uuid
22

33
from django.db import models
4+
from django.db.models import Q
45
from django.utils import timezone
56

67

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

testapp/tests/test_constraints.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
from django.db.utils import IntegrityError
2-
from django.test import TestCase, skipUnlessDBFeature
1+
from django.db import IntegrityError, connections, migrations, models
2+
from django.db.migrations.state import ProjectState
3+
from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
34

5+
from sql_server.pyodbc.base import DatabaseWrapper
46
from ..models import (
5-
Author, Editor, Post,
6-
TestUniqueNullableModel, TestNullableUniqueTogetherModel,
7+
Author,
8+
Editor,
9+
Post,
10+
TestUniqueNullableModel,
11+
TestNullableUniqueTogetherModel,
712
)
813

914

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

0 commit comments

Comments
 (0)