Opened 9 years ago

Closed 9 years ago

#25280 closed Bug (fixed)

Infinite migrations created when using regex based validators

Reported by: Sayid Munawar Owned by: Markus Holtermann
Component: Migrations Version: dev
Severity: Release blocker Keywords: migrations, validators, slug
Cc: Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Using django.core.validators.validate_slug in a model will make the migration files on that model increment infinitely, allthough no changes has made in the model.

from django.core.validators import validate_slug

class Organization(models.Model):
    slug = models.SlugField(unique=True, validators=[validate_slug])

manage.py makemigrations

Migrations for 'organizations':
  0009_auto_20150815_2151.py:
    - Alter field slug on organization
Migrations for 'organizations':
  0010_auto_20150815_2151.py:
    - Alter field slug on organization
Migrations for 'organizations':
  0011_auto_20150815_2158.py:
    - Alter field slug on organization
Migrations for 'organizations':
  0012_auto_20150815_2158.py:
    - Alter field slug on organization

A quick fix is to workaround this issue is just copy paste the code in django.core.validators:

from django.core.validators import RegexValidator

orgslug_validator = [
    RegexValidator(regex=r'^[-a-zA-Z0-9_]+\Z',
                   message=_("Enter a valid 'slug' consisting of letters, "
                             "numbers, underscores or hyphens."),
                   code='invalid_slug')
]

class Organization(models.Model):
    slug = models.SlugField(unique=True, validators=orgslug_validator)

Attachments (1)

25280-test.diff (1.7 KB ) - added by Tim Graham 9 years ago.

Download all attachments as: .zip

Change History (12)

comment:1 by Sayid Munawar, 9 years ago

Type: UncategorizedBug

comment:2 by Tim Graham, 9 years ago

Severity: NormalRelease blocker
Summary: django 1.9 using validators.validate_slug will recreate migrations over and overInfinite migrations created when using function based validators
Triage Stage: UnreviewedAccepted

comment:3 by Tim Graham, 9 years ago

Owner: changed from nobody to Tim Graham
Status: newassigned

comment:4 by Tim Graham, 9 years ago

Before the commit that introduced the regression. deep_deconstruct() returns:
[<django.core.validators.RegexValidator object at 0x7fb6ca4cd780>] for validators.

Now it returns something like:
[('django.core.validators.RegexValidator', [re.compile('^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\Z'), 'Enter a valid IPv4 address.', 'invalid'].

It looks like the re.compile objects on the old and new fields are not treated as equal. I believe that's why we use some different comparisons in RegexValidator.__eq__().

Last edited 9 years ago by Tim Graham (previous) (diff)

by Tim Graham, 9 years ago

Attachment: 25280-test.diff added

comment:5 by Tim Graham, 9 years ago

I attached my attempt at a regression test, unfortunately it passes so it doesn't seem to capture the issue.

comment:6 by Markus Holtermann, 9 years ago

Owner: changed from Tim Graham to Markus Holtermann

comment:7 by Markus Holtermann, 9 years ago

Has patch: set

OK, I this is just an idea, and the current tests on master work, but I don't have a test that doesn't work on master yet ;)

  • django/db/migrations/autodetector.py

    diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py
    index 41830f0..1509fa7 100644
    a b from django.db.migrations.migration import Migration  
    1111from django.db.migrations.operations.models import AlterModelOptions
    1212from django.db.migrations.optimizer import MigrationOptimizer
    1313from django.db.migrations.questioner import MigrationQuestioner
     14from django.db.migrations.utils import COMPILED_REGEX_TYPE, RegexObject
    1415from django.utils import six
    1516
    1617from .topological_sort import stable_topological_sort
    class MigrationAutodetector(object):  
    6263                key: self.deep_deconstruct(value)
    6364                for key, value in obj.items()
    6465            }
     66        elif isinstance(obj, COMPILED_REGEX_TYPE):
     67            return RegexObject(obj.pattern, obj.flags)
    6568        elif isinstance(obj, type):
    6669            # If this is a type that implements 'deconstruct' as an instance method,
    6770            # avoid treating this as being deconstructible itself - see #22951
  • new file django/db/migrations/utils.py

    diff --git a/django/db/migrations/utils.py b/django/db/migrations/utils.py
    new file mode 100644
    index 0000000..412db15
    - +  
     1import re
     2
     3COMPILED_REGEX_TYPE = type(re.compile(''))
     4
     5
     6class RegexObject(object):
     7    def __init__(self, pattern, flags):
     8        self.pattern = pattern
     9        self.flags = flags
     10
     11    def __eq__(self, other):
     12        return self.pattern == other.pattern and self.flags == other.flags
  • django/db/migrations/writer.py

    diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py
    index 44a4017..fb6a162 100644
    a b from django.apps import apps  
    1414from django.db import migrations, models
    1515from django.db.migrations.loader import MigrationLoader
    1616from django.db.migrations.operations.base import Operation
     17from django.db.migrations.utils import COMPILED_REGEX_TYPE, RegexObject
    1718from django.utils import datetime_safe, six
    1819from django.utils._os import upath
    1920from django.utils.encoding import force_text
    from django.utils.module_loading import module_dir  
    2324from django.utils.timezone import utc
    2425from django.utils.version import get_docs_version
    2526
    26 COMPILED_REGEX_TYPE = type(re.compile(''))
    27 
    2827
    2928class SettingsReference(str):
    3029    """
    class MigrationWriter(object):  
    506505            format = "(%s)" if len(strings) != 1 else "(%s,)"
    507506            return format % (", ".join(strings)), imports
    508507        # Compiled regex
    509         elif isinstance(value, COMPILED_REGEX_TYPE):
     508        elif isinstance(value, (COMPILED_REGEX_TYPE, RegexObject)):
    510509            imports = {"import re"}
    511510            regex_pattern, pattern_imports = cls.serialize(value.pattern)
    512511            regex_flags, flag_imports = cls.serialize(value.flags)

comment:8 by Sayid Munawar, 9 years ago

using MarkusH patch, i can confirm that my case is resolved

$  ./manage.py makemigrations
No changes detected
$  ./manage.py makemigrations
No changes detected
$  ./manage.py makemigrations
No changes detected

comment:9 by Markus Holtermann, 9 years ago

Needs documentation: set

Here's a PR including tests: https://github.com/django/django/pull/5173

Before backporting it to 1.8 I'd like to get it into 1.9 alpha and see if it actually works in the field as I don't really like the approach.

comment:10 by Tim Graham, 9 years ago

Needs documentation: unset
Summary: Infinite migrations created when using function based validatorsInfinite migrations created when using regex based validators
Triage Stage: AcceptedReady for checkin

The issue doesn't affect 1.8.

comment:11 by Markus Holtermann <info@…>, 9 years ago

Resolution: fixed
Status: assignedclosed

In 91f701f:

Fixed #25280 -- Properly checked regex objects for equality to prevent infinite migrations

Thanks Sayid Munawar and Tim Graham for the report, investigation and
review.

Note: See TracTickets for help on using tickets.
Back to Top