Repository: 3YOURMIND/django-migration-linter

Issue:
Adding an index with a NOT NULL condition incorrectly triggers NOT_NULL rule
Adding an index with a `WHERE` clause including `NOT NULL` gets flagged as a `NOT NULL constraint on columns` error.

## Steps to reproduce

The follow migration operation:

```python
AddIndexConcurrently(
    model_name="prediction",
    index=models.Index(
        condition=models.Q(
            ("data_deleted_at__isnull", True),
            ("delete_data_after__isnull", False),
        ),
        fields=["delete_data_after"],
        name="delete_data_after_idx",
    ),
),
```

Generates the following SQL:

```sql
CREATE INDEX CONCURRENTLY "delete_data_after_idx" ON "models_prediction" ("delete_data_after") WHERE ("data_deleted_at" IS NULL AND "delete_data_after" IS NOT NULL);
```

When linted this is flagged as an error because of the `NOT NULL`, when it ought to be a safe operation.

## Investigation

Looking at the condition used for this rule, I think it might just need to permit `CREATE INDEX` requests:

```python
re.search("(?<!DROP )NOT NULL", sql) and not sql.startswith("CREATE TABLE") and not sql.startswith("CREATE INDEX") 
```

https://github.com/3YOURMIND/django-migration-linter/blob/202a6d9d5dea83528cb52fd7481a5a0565cc6f83/django_migration_linter/sql_analyser/base.py#L43

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3069d91..beafd65 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,21 @@
 Instead, the linter crashes and lets the `sqlmigrate` error raise, in order to avoid letting a problematic migration pass.
 One common reason for such an error is the SQL generation which requires the database to be actually migrated in order to fetch actual constraint names from it.
 The crash is a sign to double-check the migration. But if you are certain the migration is safe, you can ignore it (issue #209)
+
+Features:
+
 - Fixed `RunPython` model import check when using a `through` object like `MyModel.many_to_many.through.objects.filter(...)` (issue #218)
 - Mark the `IgnoreMigration` operation as `elidable=True`
+
+Bug:
+
+- Don't detect not nullable field on partial index creation (issue #250)
+
+Miscellaneous:
+
 - Add support for Python 3.11
 - Add support for Django 4.1
+- Add support for Django 4.2
 - Drop support for Django 2.2
 - Internally rename "migration tests" to "migration checks"
 - Add dataclasses internally instead of custom dicts
diff --git a/django_migration_linter/sql_analyser/base.py b/django_migration_linter/sql_analyser/base.py
index 2fa0646..131652e 100644
--- a/django_migration_linter/sql_analyser/base.py
+++ b/django_migration_linter/sql_analyser/base.py
@@ -40,7 +40,8 @@ def has_not_null_column(sql_statements: list[str], **kwargs) -> bool:
             ends_with_default = False
     return (
         any(
-            re.search("(?<!DROP )NOT NULL", sql) and not sql.startswith("CREATE TABLE")
+            re.search("(?<!DROP )NOT NULL", sql)
+            and not (sql.startswith("CREATE TABLE") or sql.startswith("CREATE INDEX"))
             for sql in sql_statements
         )
         and ends_with_default is False


Test patch:
diff --git a/tests/unit/test_sql_analyser.py b/tests/unit/test_sql_analyser.py
index d7349fc..012d53c 100644
--- a/tests/unit/test_sql_analyser.py
+++ b/tests/unit/test_sql_analyser.py
@@ -297,6 +297,10 @@ class PostgresqlAnalyserTestCase(SqlAnalyserTestCase):
         sql = "CREATE UNIQUE INDEX CONCURRENTLY title_idx ON films (title);"
         self.assertValidSql(sql)
 
+    def test_create_index_concurrently_where(self):
+        sql = 'CREATE INDEX CONCURRENTLY "index_name" ON "table_name" ("a_column") WHERE ("some_column" IS NOT NULL);'
+        self.assertValidSql(sql)
+
     def test_drop_index_non_concurrently(self):
         sql = "DROP INDEX ON films"
         self.assertWarningSql(sql)