Skip to content

Commit

Permalink
Error on unsupported unique constraint conditions
Browse files Browse the repository at this point in the history
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
  • Loading branch information
davidjb committed Mar 3, 2021
1 parent 9ee0830 commit 0d86196
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 4 deletions.
9 changes: 8 additions & 1 deletion mssql/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
Table,
)
from django import VERSION as django_version
from django.db.models import Index
from django.db.models import Index, UniqueConstraint
from django.db.models.fields import AutoField, BigAutoField
from django.db.models.sql.where import AND
from django.db.transaction import TransactionManagementError
from django.utils.encoding import force_str

Expand Down Expand Up @@ -955,3 +956,9 @@ def remove_field(self, model, field):
for sql in list(self.deferred_sql):
if isinstance(sql, Statement) and sql.references_column(model._meta.db_table, field.column):
self.deferred_sql.remove(sql)

def add_constraint(self, model, constraint):
if isinstance(constraint, UniqueConstraint) and constraint.condition and constraint.condition.connector != AND:
raise NotImplementedError("The backend does not support %s conditions on unique constraint %s." %
(constraint.condition.connector, constraint.name))
super().add_constraint(model, constraint)
68 changes: 68 additions & 0 deletions testapp/migrations/0010_test_unique_constraints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Generated by Django 3.1.5 on 2021-01-18 00:05

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('testapp', '0009_test_drop_table_with_foreign_key_reference_part2'),
]

operations = [
migrations.CreateModel(
name='TestUnsupportableUniqueConstraint',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('_type', models.CharField(max_length=50)),
('status', models.CharField(max_length=50)),
],
options={
'managed': False,
},
),
migrations.CreateModel(
name='TestSupportableUniqueConstraint',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('_type', models.CharField(max_length=50)),
('status', models.CharField(max_length=50)),
],
),
migrations.AddConstraint(
model_name='testsupportableuniqueconstraint',
constraint=models.UniqueConstraint(
condition=models.Q(
('status', 'in_progress'),
('status', 'needs_changes'),
('status', 'published'),
),
fields=('_type',),
name='and_constraint',
),
),
migrations.AddConstraint(
model_name='testsupportableuniqueconstraint',
constraint=models.UniqueConstraint(
condition=models.Q(status__in=['in_progress', 'needs_changes']),
fields=('_type',),
name='in_constraint',
),
),
]
37 changes: 37 additions & 0 deletions testapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import uuid

from django.db import models
from django.db.models import Q
from django.utils import timezone


Expand Down Expand Up @@ -74,3 +75,39 @@ class TestRemoveOneToOneFieldModel(models.Model):
# thats already is removed.
# b = models.OneToOneField('self', on_delete=models.SET_NULL, null=True)
a = models.CharField(max_length=50)


class TestUnsupportableUniqueConstraint(models.Model):
class Meta:
managed = False
constraints = [
models.UniqueConstraint(
name='or_constraint',
fields=['_type'],
condition=(Q(status='in_progress') | Q(status='needs_changes')),
),
]

_type = models.CharField(max_length=50)
status = models.CharField(max_length=50)


class TestSupportableUniqueConstraint(models.Model):
class Meta:
constraints = [
models.UniqueConstraint(
name='and_constraint',
fields=['_type'],
condition=(
Q(status='in_progress') & Q(status='needs_changes') & Q(status='published')
),
),
models.UniqueConstraint(
name='in_constraint',
fields=['_type'],
condition=(Q(status__in=['in_progress', 'needs_changes'])),
),
]

_type = models.CharField(max_length=50)
status = models.CharField(max_length=50)
61 changes: 58 additions & 3 deletions testapp/tests/test_constraints.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from django.db import connections, migrations, models
from django.db.migrations.state import ProjectState
from django.db.utils import IntegrityError
from django.test import TestCase, skipUnlessDBFeature
from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature

from mssql.base import DatabaseWrapper
from ..models import (
Author, Editor, Post,
TestUniqueNullableModel, TestNullableUniqueTogetherModel,
Author,
Editor,
Post,
TestUniqueNullableModel,
TestNullableUniqueTogetherModel,
)


Expand Down Expand Up @@ -55,3 +61,52 @@ def test_after_type_change(self):
TestNullableUniqueTogetherModel.objects.create(a='aaa', b='bbb', c='ccc')
with self.assertRaises(IntegrityError):
TestNullableUniqueTogetherModel.objects.create(a='aaa', b='bbb', c='ccc')


class TestUniqueConstraints(TransactionTestCase):
def test_unsupportable_unique_constraint(self):
# Only execute tests when running against SQL Server
connection = connections['default']
if isinstance(connection, DatabaseWrapper):

class TestMigration(migrations.Migration):
initial = True

operations = [
migrations.CreateModel(
name='TestUnsupportableUniqueConstraint',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('_type', models.CharField(max_length=50)),
('status', models.CharField(max_length=50)),
],
),
migrations.AddConstraint(
model_name='testunsupportableuniqueconstraint',
constraint=models.UniqueConstraint(
condition=models.Q(
('status', 'in_progress'),
('status', 'needs_changes'),
_connector='OR',
),
fields=('_type',),
name='or_constraint',
),
),
]

migration = TestMigration('testapp', 'test_unsupportable_unique_constraint')

with connection.schema_editor(atomic=True) as editor:
with self.assertRaisesRegex(
NotImplementedError, "does not support OR conditions"
):
return migration.apply(ProjectState(), editor)

0 comments on commit 0d86196

Please sign in to comment.