Repository: 3YOURMIND/django-migration-linter (python)

Issue:
Linter fails on CREATE INDEX when creating a new table
Here is an example `CreateModel` from Django:

```python
migrations.CreateModel(
    name='ShipmentMetadataAlert',
    fields=[
        ('deleted_at', models.DateTimeField(blank=True, db_index=True, null=True)),
        ('created_at', common.fields.CreatedField(default=django.utils.timezone.now, editable=False)),
        ('updated_at', common.fields.LastModifiedField(default=django.utils.timezone.now, editable=False)),
        ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')),
        ('message', models.TextField(blank=True, null=True)),
        ('level', models.CharField(blank=True, choices=[('HIGH', 'high'), ('MEDIUM', 'medium'), ('LOW', 'low')], max_length=16, null=True)),
        ('type', models.CharField(blank=True, choices=[('MOBILE_DEVICE_ALERT', 'MOBILE_DEVICE_ALERT'), ('NON_ACTIVE_CARRIER', 'NON_ACTIVE_CARRIER'), ('OTHER', 'OTHER')], max_length=32, null=True)),
        ('subtype', models.CharField(blank=True, choices=[('DRIVER_PERMISSIONS', 'DRIVER_PERMISSIONS'), ('DRIVER_LOCATION', 'DRIVER_LOCATION'), ('OTHER', 'OTHER')], max_length=32, null=True)),
        ('occurred_at', models.DateTimeField(null=True)),
        ('clear_alert_job_id', models.UUIDField(default=None, null=True)),
        ('metadata', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='alerts', to='shipments.ShipmentMetadata')),
    ],
    options={
        'abstract': False,
    }
)
```

Here are the SQL statements that this spits out in `sqlmigrate`:

```sql
BEGIN;
--
-- Create model ShipmentMetadataAlert
--
CREATE TABLE "shipments_shipmentmetadataalert" ("deleted_at" timestamp with time zone NULL, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "id" uuid NOT NULL PRIMARY KEY, "message" text NULL, "level" varchar(16) NULL, "type" varchar(32) NULL, "subtype" varchar(32) NULL, "occurred_at" timestamp with time zone NULL, "clear_alert_job_id" uuid NULL, "metadata_id" uuid NOT NULL);
ALTER TABLE "shipments_shipmentmetadataalert" ADD CONSTRAINT "shipments_shipmentme_metadata_id_f20850e8_fk_shipments" FOREIGN KEY ("metadata_id") REFERENCES "shipments_shipmentmetadata" ("id") DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "shipments_shipmentmetadataalert_deleted_at_c9a93342" ON "shipments_shipmentmetadataalert" ("deleted_at");
CREATE INDEX "shipments_shipmentmetadataalert_metadata_id_f20850e8" ON "shipments_shipmentmetadataalert" ("metadata_id");
COMMIT;
```
 
This is an error from the linter as it outputs the error `CREATE INDEX locks table`. But the table is being created within the migration, it just needs to recognize that.

It seems like the `CREATE INDEX` detection should work the same way that the `ADD_UNIQUE` detection works where it detects that the create table is happening in the same migration:

https://github.com/3YOURMIND/django-migration-linter/blob/db71a9db23746f64d41d681f3fecb9b066c87338/django_migration_linter/sql_analyser/base.py#L26-L40

Pull request:
Fix #178 -- Don't detect an index creation when the table is being created
Fixes #178 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1ec8e5..15fefc0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,8 @@
-## 4.0.0
+## 4.0.0 (unreleased)
 
 - Drop support for Python 2.7 and 3.5
 - Drop support for Django 1.11, 2.0, 2.1, 3.0
+- Fix index creation detection when table is being created in the transaction (issue #178)
 
 ## 3.0.1
 
diff --git a/django_migration_linter/sql_analyser/postgresql.py b/django_migration_linter/sql_analyser/postgresql.py
index 140aba3..3eb18a5 100644
--- a/django_migration_linter/sql_analyser/postgresql.py
+++ b/django_migration_linter/sql_analyser/postgresql.py
@@ -3,14 +3,32 @@ import re
 from .base import BaseAnalyser
 
 
+def has_create_index(sql_statements, **kwargs):
+    regex_result = None
+    for sql in sql_statements:
+        regex_result = re.search(r"CREATE (UNIQUE )?INDEX.*ON (.*) \(", sql)
+        if re.search("INDEX CONCURRENTLY", sql):
+            regex_result = None
+        elif regex_result:
+            break
+    if not regex_result:
+        return False
+
+    concerned_table = regex_result.group(2)
+    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 PostgresqlAnalyser(BaseAnalyser):
     migration_tests = [
         {
             "code": "CREATE_INDEX",
-            "fn": lambda sql, **kw: re.search("CREATE (UNIQUE )?INDEX", sql)
-            and not re.search("INDEX CONCURRENTLY", sql),
+            "fn": has_create_index,
             "msg": "CREATE INDEX locks table",
-            "mode": "one_liner",
+            "mode": "transaction",
             "type": "warning",
         },
         {


Test patch:
diff --git a/tests/unit/test_sql_analyser.py b/tests/unit/test_sql_analyser.py
index 00dd50e..65ab7f0 100644
--- a/tests/unit/test_sql_analyser.py
+++ b/tests/unit/test_sql_analyser.py
@@ -233,6 +233,23 @@ class PostgresqlAnalyserTestCase(SqlAnalyserTestCase):
         sql = "CREATE UNIQUE INDEX title_idx ON films (title);"
         self.assertWarningSql(sql)
 
+    def test_create_index_non_concurrently_with_table_creation(self):
+        sql = [
+            'CREATE TABLE "films" ("title" text);',
+            'CREATE INDEX ON "films" ((lower("title")));',
+        ]
+        self.assertValidSql(sql)
+        sql = [
+            'CREATE TABLE "some_table" ("title" text);',
+            'CREATE INDEX ON "films" ((lower("title")));',
+        ]
+        self.assertWarningSql(sql)
+        sql = [
+            'CREATE TABLE "films" ("title" text);',
+            'CREATE INDEX ON "some_table" ((lower("title")));',
+        ]
+        self.assertWarningSql(sql)
+
     def test_create_index_concurrently(self):
         sql = "CREATE INDEX CONCURRENTLY ON films (lower(title));"
         self.assertValidSql(sql)