Skip to content

Commit c063460

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. Previously opened at ESSolutions/django-mssql-backend#97
1 parent 762c464 commit c063460

File tree

4 files changed

+171
-4
lines changed

4 files changed

+171
-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', '0009_test_drop_table_with_foreign_key_reference_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: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT license.
33

4+
from django.db import connections, migrations, models
5+
from django.db.migrations.state import ProjectState
46
from django.db.utils import IntegrityError
5-
from django.test import TestCase, skipUnlessDBFeature
7+
from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
68

9+
from mssql.base import DatabaseWrapper
710
from ..models import (
8-
Author, Editor, Post,
9-
TestUniqueNullableModel, TestNullableUniqueTogetherModel,
11+
Author,
12+
Editor,
13+
Post,
14+
TestUniqueNullableModel,
15+
TestNullableUniqueTogetherModel,
1016
)
1117

1218

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

0 commit comments

Comments
 (0)