diff --git a/README.md b/README.md index 2d6be07b..5a89d288 100644 --- a/README.md +++ b/README.md @@ -135,10 +135,12 @@ Dictionary. Current available keys are: definition present in the ``freetds.conf`` FreeTDS configuration file instead of a hostname or an IP address. - But if this option is present and it's value is ``True``, this - special behavior is turned off. + But if this option is present and its value is ``True``, this + special behavior is turned off. Instead, connections to the database + server will be established using ``HOST`` and ``PORT`` options, without + requiring ``freetds.conf`` to be configured. - See http://www.freetds.org/userguide/dsnless.htm for more information. + See https://www.freetds.org/userguide/dsnless.html for more information. - unicode_results diff --git a/manage.py b/manage.py index a57ed35e..e88b9965 100755 --- a/manage.py +++ b/manage.py @@ -1,7 +1,8 @@ +#!/usr/bin/env python + # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -#!/usr/bin/env python import os import sys diff --git a/mssql/schema.py b/mssql/schema.py index f85046fb..b7b27c4b 100644 --- a/mssql/schema.py +++ b/mssql/schema.py @@ -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 @@ -689,7 +690,7 @@ def add_field(self, model, field): self.connection.close() def _create_unique_sql(self, model, columns, name=None, condition=None, deferrable=None): - if (deferrable and not self.connection.features.supports_deferrable_unique_constraints): + if (deferrable and not getattr(self.connection.features, 'supports_deferrable_unique_constraints', False)): return None def create_unique_name(*args, **kwargs): @@ -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) diff --git a/testapp/migrations/0010_test_unique_constraints.py b/testapp/migrations/0010_test_unique_constraints.py new file mode 100644 index 00000000..6c84c05e --- /dev/null +++ b/testapp/migrations/0010_test_unique_constraints.py @@ -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', + ), + ), + ] diff --git a/testapp/models.py b/testapp/models.py index 85a571d3..3ef2be3a 100644 --- a/testapp/models.py +++ b/testapp/models.py @@ -4,6 +4,7 @@ import uuid from django.db import models +from django.db.models import Q from django.utils import timezone @@ -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) diff --git a/testapp/tests/test_constraints.py b/testapp/tests/test_constraints.py index 635fa1b1..2c743f11 100644 --- a/testapp/tests/test_constraints.py +++ b/testapp/tests/test_constraints.py @@ -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, ) @@ -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)