Detect NULL default value as no default value.

This can happen when using `db_default=None` with `null=False` on a Django model field.
This commit is contained in:
Mikuláš Poul authored and David Wobrock committed 2025-11-30 12:45:24 +01:00
1 parent 8c8a0c65ce
commit 28a0686d3b
9 files changed
+90 -2

No files matched your search

+3
View File
@@ -4,6 +4,9 @@ Feature:
- **Breaking change**: Handle custom Django app label when gathering migrations from git reference. (#262) - **Breaking change**: Handle custom Django app label when gathering migrations from git reference. (#262)
This means that, an app that was previously referenced by its folder name, will now be referenced by its Django app label. This means that, an app that was previously referenced by its folder name, will now be referenced by its Django app label.
Bug:
- Consider `DEFAULT NULL` as no default value being defined (#302)
Miscellaneous: Miscellaneous:
- Add py.typed file (#303) - Add py.typed file (#303)
- Add support for Django 5.2 (#304) - Add support for Django 5.2 (#304)
@@ -42,9 +42,9 @@ def has_not_null_column(sql_statements: list[str], **kwargs) -> bool:
or sql.startswith("CREATE UNIQUE INDEX") or sql.startswith("CREATE UNIQUE INDEX")
): ):
not_null_column = True not_null_column = True
if re.search("DEFAULT.*NOT NULL", sql): if re.search("DEFAULT (?!NULL).*NOT NULL", sql):
has_default_value = True has_default_value = True
if "SET DEFAULT" in sql: if "SET DEFAULT" in sql and "SET DEFAULT NULL" not in sql:
has_default_value = True has_default_value = True
if "DROP DEFAULT" in sql: if "DROP DEFAULT" in sql:
has_default_value = False has_default_value = False
+3
View File
@@ -11,6 +11,9 @@ ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DEFAULT = "app_add_not_null_column_followed_by_d
ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DB_DEFAULT = ( ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DB_DEFAULT = (
"app_add_not_null_column_followed_by_db_default" "app_add_not_null_column_followed_by_db_default"
) )
ADD_NOT_NULL_COLUMN_WITH_NULL_DB_DEFAULT = (
"app_add_not_null_column_with_null_db_default"
)
ALTER_COLUMN = "app_alter_column" ALTER_COLUMN = "app_alter_column"
ALTER_COLUMN_DROP_NOT_NULL = "app_alter_column_drop_not_null" ALTER_COLUMN_DROP_NOT_NULL = "app_alter_column_drop_not_null"
DROP_UNIQUE_TOGETHER = "app_unique_together" DROP_UNIQUE_TOGETHER = "app_unique_together"
+22
View File
@@ -7,6 +7,7 @@ from unittest import skipIf
import django import django
from django.conf import settings from django.conf import settings
from django.core.management import call_command from django.core.management import call_command
from django.test import override_settings
from django_migration_linter import MigrationLinter from django_migration_linter import MigrationLinter
from tests import fixtures from tests import fixtures
@@ -166,6 +167,15 @@ class SqliteBackwardCompatibilityDetectionTestCase(
app = fixtures.ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DB_DEFAULT app = fixtures.ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DB_DEFAULT
self._test_linter_finds_errors(app) self._test_linter_finds_errors(app)
@skipIf(django.VERSION[0] < 5, "db_default was implemented in Django 5.0")
@override_settings(
INSTALLED_APPS=settings.INSTALLED_APPS
+ ["tests.test_project.app_add_not_null_column_with_null_db_default"]
)
def test_detected_not_null_column_with_null_db_default(self):
app = fixtures.ADD_NOT_NULL_COLUMN_WITH_NULL_DB_DEFAULT
self._test_linter_finds_errors(app)
def test_detect_make_column_not_null_with_lib_default(self): def test_detect_make_column_not_null_with_lib_default(self):
# The 'django-add-default-value' doesn't handle sqlite correctly # The 'django-add-default-value' doesn't handle sqlite correctly
app = fixtures.MAKE_NOT_NULL_WITH_LIB_DEFAULT app = fixtures.MAKE_NOT_NULL_WITH_LIB_DEFAULT
@@ -177,6 +187,9 @@ class MySqlBackwardCompatibilityDetectionTestCase(
): ):
databases = ["mysql"] databases = ["mysql"]
# test_detected_not_null_column_with_null_db_default is not included for mysql, because the setup raises
# django.db.utils.OperationalError: (1067, "Invalid default value for 'not_null_field_db_default_null'")
class PostgresqlBackwardCompatibilityDetectionTestCase( class PostgresqlBackwardCompatibilityDetectionTestCase(
BaseBackwardCompatibilityDetection, unittest.TestCase BaseBackwardCompatibilityDetection, unittest.TestCase
@@ -191,3 +204,12 @@ class PostgresqlBackwardCompatibilityDetectionTestCase(
linter = self._launch_linter(fixtures.CREATE_INDEX_EXCLUSIVE) linter = self._launch_linter(fixtures.CREATE_INDEX_EXCLUSIVE)
self.assertFalse(linter.has_errors) self.assertFalse(linter.has_errors)
self.assertTrue(linter.nb_warnings) self.assertTrue(linter.nb_warnings)
@skipIf(django.VERSION[0] < 5, "db_default was implemented in Django 5.0")
@override_settings(
INSTALLED_APPS=settings.INSTALLED_APPS
+ ["tests.test_project.app_add_not_null_column_with_null_db_default"]
)
def test_detected_not_null_column_with_null_db_default(self):
app = fixtures.ADD_NOT_NULL_COLUMN_WITH_NULL_DB_DEFAULT
self._test_linter_finds_errors(app)
@@ -0,0 +1,30 @@
# Generated by Django 5.0.1 on 2024-02-04 20:07
from __future__ import annotations
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="A",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("field", models.IntegerField()),
],
),
]
@@ -0,0 +1,21 @@
# Generated by Django 5.0.1 on 2024-02-04 20:07
from __future__ import annotations
import django.db.models.functions.math
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("app_add_not_null_column_with_null_db_default", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="a",
name="not_null_field_db_default_null",
field=models.IntegerField(db_default=None, default=None, null=False),
),
]
@@ -0,0 +1,9 @@
from __future__ import annotations
from django.db import models
from django.db.models.functions import Pi
class A(models.Model):
field = models.IntegerField()
not_null_field_db_default_null = models.IntegerField(null=False, db_default=None)