Opened 10 years ago

Closed 10 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 10 years ago.

Download all attachments as: .zip

Change History (12)

comment:1 by Sayid Munawar, 10 years ago

Type: UncategorizedBug

comment:2 by Tim Graham, 10 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, 10 years ago

Owner: changed from nobody to Tim Graham
Status: newassigned

comment:4 by Tim Graham, 10 years ago

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

Now it returns this for historical models:
[('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']

... or this when processing the actual models:
('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'), <django.utils.functional.lazy.<locals>.__proxy__ object at 0x7f3a97c44b00>, 'invalid']

The lazy error message is not resolved, so it's treated as unequal to the string in the historical model.

Version 0, edited 10 years ago by Tim Graham (next)

by Tim Graham, 10 years ago

Attachment: 25280-test.diff added

comment:5 by Tim Graham, 10 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, 10 years ago

Owner: changed from Tim Graham to Markus Holtermann

comment:7 by Markus Holtermann, 10 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, 10 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, 10 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, 10 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@…>, 10 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