Repository: 3YOURMIND/django-migration-linter
Pull request #194 (2022-01-28 20:18:28)

Handle creating a unique index as adding a unique constraint
Fixes https://github.com/3YOURMIND/django-migration-linter/issues/183

codecov-commenter: # [Codecov](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/194?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#194](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/194?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (f828c55) into [main](https://codecov.io/gh/3YOURMIND/django-migration-linter/commit/3fbc7206508c34ea7076fc1876975d958a48617a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (3fbc720) will **increase** coverage by `0.02%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head f828c55 differs from pull request most recent head 5219acd. Consider uploading reports for the commit 5219acd to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/194/graphs/tree.svg?width=650&height=150&src=pr&token=pgNNXGbCf7&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/194?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@            Coverage Diff             @@
##             main     #194      +/-   ##
==========================================
+ Coverage   93.40%   93.42%   +0.02%     
==========================================
  Files          77       77              
  Lines        1653     1659       +6     
==========================================
+ Hits         1544     1550       +6     
  Misses        109      109              
```


| [Impacted Files](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/194?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [django\_migration\_linter/sql\_analyser/sqlite.py](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/194/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZGphbmdvX21pZ3JhdGlvbl9saW50ZXIvc3FsX2FuYWx5c2VyL3NxbGl0ZS5weQ==) | `91.66% <ø> (-3.99%)` | :arrow_down: |
| [django\_migration\_linter/sql\_analyser/base.py](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/194/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZGphbmdvX21pZ3JhdGlvbl9saW50ZXIvc3FsX2FuYWx5c2VyL2Jhc2UucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/unit/test\_sql\_analyser.py](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/194/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-dGVzdHMvdW5pdC90ZXN0X3NxbF9hbmFseXNlci5weQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/194?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/194?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [3fbc720...5219acd](https://codecov.io/gh/3YOURMIND/django-migration-linter/pull/194?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f306ed3..6fa867c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 - Drop support for Django 1.11, 2.0, 2.1, 3.0 and 3.1
 - Add support for Django 4.0
 - Fix index creation detection when table is being created in the transaction (issue #178)
+- Handle unique index creation as adding a unique constraint (issue #183)
 
 ## 3.0.1
 
diff --git a/django_migration_linter/sql_analyser/base.py b/django_migration_linter/sql_analyser/base.py
index a74b40f..d399ab7 100644
--- a/django_migration_linter/sql_analyser/base.py
+++ b/django_migration_linter/sql_analyser/base.py
@@ -26,7 +26,9 @@ def has_not_null_column(sql_statements, **kwargs):
 def has_add_unique(sql_statements, **kwargs):
     regex_result = None
     for sql in sql_statements:
-        regex_result = re.search("ALTER TABLE (.*) ADD CONSTRAINT .* UNIQUE", sql)
+        regex_result = re.search(
+            "ALTER TABLE (.*) ADD CONSTRAINT .* UNIQUE", sql
+        ) or re.search('CREATE UNIQUE INDEX .* ON (".*?")', sql)
         if regex_result:
             break
     if not regex_result:
diff --git a/django_migration_linter/sql_analyser/sqlite.py b/django_migration_linter/sql_analyser/sqlite.py
index 9a947d1..530788a 100644
--- a/django_migration_linter/sql_analyser/sqlite.py
+++ b/django_migration_linter/sql_analyser/sqlite.py
@@ -3,23 +3,6 @@ import re
 from .base import BaseAnalyser
 
 
-def has_add_unique(sql_statements, **kwargs):
-    regex_result = None
-    for sql in sql_statements:
-        regex_result = re.search('CREATE UNIQUE INDEX .* ON (".*?")', sql)
-        if regex_result:
-            break
-    if not regex_result:
-        return False
-
-    concerned_table = regex_result.group(1)
-    table_is_added_in_transaction = any(
-        sql.startswith("CREATE TABLE {}".format(concerned_table))
-        for sql in sql_statements
-    )
-    return not table_is_added_in_transaction
-
-
 class SqliteAnalyser(BaseAnalyser):
     migration_tests = [
         {
@@ -54,12 +37,6 @@ class SqliteAnalyser(BaseAnalyser):
             "mode": "transaction",
             "type": "error",
         },
-        {
-            "code": "ADD_UNIQUE",
-            "fn": has_add_unique,
-            "mode": "transaction",
-            "type": "error",
-        },
     ]
 
     @staticmethod


Test patch:
diff --git a/tests/unit/test_sql_analyser.py b/tests/unit/test_sql_analyser.py
index 65ab7f0..00929b2 100644
--- a/tests/unit/test_sql_analyser.py
+++ b/tests/unit/test_sql_analyser.py
@@ -21,9 +21,16 @@ class SqlAnalyserTestCase(unittest.TestCase):
                 0, len(warnings), "Found warnings in sql: {}".format(errors)
             )
 
-    def assertBackwardIncompatibleSql(self, sql):
+    def assertBackwardIncompatibleSql(self, sql, code=None):
         errors, _, _ = self.analyse_sql(sql)
         self.assertNotEqual(0, len(errors), "Found no errors in sql")
+        if code:
+            self.assertTrue(
+                any(err["code"] == code for err in errors),
+                "Didn't find error code {} in returned errors ({})".format(
+                    code, [err["code"] for err in errors]
+                ),
+            )
 
     def assertWarningSql(self, sql):
         _, _, warnings = self.analyse_sql(sql)
@@ -63,6 +70,16 @@ class MySqlAnalyserTestCase(SqlAnalyserTestCase):
         sql = "ALTER TABLE `app_unique_together_a` DROP INDEX `app_unique_together_a_int_field_char_field_979ac7d8_uniq`;"
         self.assertValidSql(sql)
 
+    def test_unique_index(self):
+        sql = 'CREATE UNIQUE INDEX "index_name" ON "table" ("col1", "col2");'
+        self.assertBackwardIncompatibleSql(sql, "ADD_UNIQUE")
+
+        sql = [
+            'CREATE TABLE "table" ("col1" integer, "col2" integer);',
+            'CREATE UNIQUE INDEX "index_name" ON "table" ("col1", "col2");',
+        ]
+        self.assertValidSql(sql)
+
     def test_add_many_to_many_field(self):
         sql = [
             "CREATE TABLE `app_add_manytomany_field_b_many_to_many`(`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `b_id` integer NOT NULL, `a_id` integer NOT NULL);",
@@ -146,6 +163,16 @@ class SqliteAnalyserTestCase(SqlAnalyserTestCase):
         sql = 'DROP INDEX "app_unique_together_a_int_field_char_field_979ac7d8_uniq";'
         self.assertValidSql(sql)
 
+    def test_unique_index(self):
+        sql = 'CREATE UNIQUE INDEX "index_name" ON "table" ("col1", "col2");'
+        self.assertBackwardIncompatibleSql(sql, "ADD_UNIQUE")
+
+        sql = [
+            'CREATE TABLE "table" ("col1" integer, "col2" integer);',
+            'CREATE UNIQUE INDEX "index_name" ON "table" ("col1", "col2");',
+        ]
+        self.assertValidSql(sql)
+
     def test_add_many_to_many_field(self):
         sql = [
             'CREATE TABLE "app_add_manytomany_field_b_many_to_many"("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "b_id" integer NOT NULL REFERENCES "app_add_manytomany_field_b"("id") DEFERRABLE INITIALLY DEFERRED, "a_id" integer NOT NULL REFERENCES "app_add_manytomany_field_a"("id") DEFERRABLE INITIALLY DEFERRED);',
@@ -197,6 +224,16 @@ class PostgresqlAnalyserTestCase(SqlAnalyserTestCase):
         sql = 'ALTER TABLE "app_unique_together_a" DROP CONSTRAINT "app_unique_together_a_int_field_char_field_979ac7d8_uniq";'
         self.assertValidSql(sql)
 
+    def test_unique_index(self):
+        sql = 'CREATE UNIQUE INDEX "index_name" ON "table" ("col1", "col2");'
+        self.assertBackwardIncompatibleSql(sql, "ADD_UNIQUE")
+
+        sql = [
+            'CREATE TABLE "table" ("col1" integer, "col2" integer);',
+            'CREATE UNIQUE INDEX "index_name" ON "table" ("col1", "col2");',
+        ]
+        self.assertValidSql(sql)
+
     def test_add_many_to_many_field(self):
         sql = [
             'CREATE TABLE "app_add_manytomany_field_b_many_to_many"("id" serial NOT NULL PRIMARY KEY, "b_id" integer NOT NULL, "a_id" integer NOT NULL);',