From e8c53c89b81d5e1e3e778c4a29d8c2a528f37fc4 Mon Sep 17 00:00:00 2001 From: David Beitey Date: Wed, 3 Mar 2021 12:26:02 +1000 Subject: [PATCH 1/4] Put hashbang on first line of manage.py --- manage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 23524b5a3b8b6221ee393242f1ea6dbe91c428c5 Mon Sep 17 00:00:00 2001 From: David Beitey Date: Sun, 6 Dec 2020 23:39:13 +0000 Subject: [PATCH 2/4] Fix host_is_server URL and expand docs in README --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 From 762c4643853632fbc19530fad07f6fc16231e4aa Mon Sep 17 00:00:00 2001 From: David Beitey Date: Mon, 11 Jan 2021 19:24:39 +0000 Subject: [PATCH 3/4] Backwards compatibility for deferrable kwarg This was previously accepted to the original repository in https://github.com/ESSolutions/django-mssql-backend/pull/86. Backwards compatibility for Django < 3.1 is maintained by not directly trying to load the supports_deferrable_unique_constraints via dot notation, but rather by getattr with a default. --- mssql/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mssql/schema.py b/mssql/schema.py index f85046fb..e617cb5f 100644 --- a/mssql/schema.py +++ b/mssql/schema.py @@ -689,7 +689,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): From c063460d4627b8b2054f0e97cb3f15a48d3cc565 Mon Sep 17 00:00:00 2001 From: David Beitey Date: Fri, 15 Jan 2021 19:03:26 +1000 Subject: [PATCH 4/4] 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 https://github.com/ESSolutions/django-mssql-backend/pull/97 --- mssql/schema.py | 9 ++- .../0010_test_unique_constraints.py | 68 +++++++++++++++++++ testapp/models.py | 37 ++++++++++ testapp/tests/test_constraints.py | 61 ++++++++++++++++- 4 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 testapp/migrations/0010_test_unique_constraints.py diff --git a/mssql/schema.py b/mssql/schema.py index e617cb5f..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 @@ -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)