Commit 91f1b6dc authored by Anubhav Joshi's avatar Anubhav Joshi Committed by Tim Graham
Browse files

Fixed #13711 -- Model check added to ensure that auto-generated column name is...

Fixed #13711 -- Model check added to ensure that auto-generated column name is within limits of the database.

Thanks russellm for report and Tim Graham for review.
parent e4708385
Loading
Loading
Loading
Loading
+0 −3
Original line number Diff line number Diff line
@@ -520,9 +520,6 @@ class BaseDatabaseFeatures(object):
    # at the end of each save operation?
    supports_forward_references = True

    # Does the backend allow very long model names without error?
    supports_long_model_names = True

    # Is there a REAL datatype in addition to floats/doubles?
    has_real_datatype = False
    supports_subqueries_in_group_by = True
+0 −1
Original line number Diff line number Diff line
@@ -171,7 +171,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
    has_select_for_update = True
    has_select_for_update_nowait = False
    supports_forward_references = False
    supports_long_model_names = False
    # XXX MySQL DB-API drivers currently fail on binary data on Python 3.
    supports_binary_field = six.PY2
    supports_microsecond_precision = False
+72 −1
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@ from django.conf import settings
from django.core import checks
from django.core.exceptions import (ObjectDoesNotExist,
    MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS)
from django.db import (router, transaction, DatabaseError,
from django.db import (router, connections, transaction, DatabaseError,
    DEFAULT_DB_ALIAS, DJANGO_VERSION_PICKLE_KEY)
from django.db.models.deletion import Collector
from django.db.models.fields import AutoField, FieldDoesNotExist
@@ -1068,6 +1068,7 @@ class Model(six.with_metaclass(ModelBase)):
        if not cls._meta.swapped:
            errors.extend(cls._check_fields(**kwargs))
            errors.extend(cls._check_m2m_through_same_relationship())
            errors.extend(cls._check_long_column_names())
            clash_errors = cls._check_id_field() + cls._check_field_name_clashes()
            errors.extend(clash_errors)
            # If there are field name clashes, hide consequent column name
@@ -1451,6 +1452,76 @@ class Model(six.with_metaclass(ModelBase)):
                )
        return errors

    @classmethod
    def _check_long_column_names(cls):
        """
        Check that any auto-generated column names are shorter than the limits
        for each database in which the model will be created.
        """
        errors = []
        allowed_len = None
        db_alias = None

        # Find the minimum max allowed length among all specified db_aliases.
        for db in settings.DATABASES.keys():
            # skip databases where the model won't be created
            if not router.allow_migrate(db, cls):
                continue
            connection = connections[db]
            max_name_length = connection.ops.max_name_length()
            if max_name_length is None:
                continue
            else:
                if allowed_len is None:
                    allowed_len = max_name_length
                    db_alias = db
                elif max_name_length < allowed_len:
                    allowed_len = max_name_length
                    db_alias = db

        if allowed_len is None:
            return errors

        for f in cls._meta.local_fields:
            _, column_name = f.get_attname_column()

            # Check if auto-generated name for the field is too long
            # for the database.
            if (f.db_column is None and column_name is not None
                    and len(column_name) > allowed_len):
                errors.append(
                    checks.Error(
                        'Autogenerated column name too long for field "%s". '
                        'Maximum length is "%s" for database "%s".'
                        % (column_name, allowed_len, db_alias),
                        hint="Set the column name manually using 'db_column'.",
                        obj=cls,
                        id='models.E018',
                    )
                )

        for f in cls._meta.local_many_to_many:
            # Check if auto-generated name for the M2M field is too long
            # for the database.
            for m2m in f.rel.through._meta.local_fields:
                _, rel_name = m2m.get_attname_column()
                if (m2m.db_column is None and rel_name is not None
                        and len(rel_name) > allowed_len):
                    errors.append(
                        checks.Error(
                            'Autogenerated column name too long for M2M field '
                            '"%s". Maximum length is "%s" for database "%s".'
                            % (rel_name, allowed_len, db_alias),
                            hint=("Use 'through' to create a separate model "
                                "for M2M and then set column_name using "
                                "'db_column'."),
                            obj=cls,
                            id='models.E019',
                        )
                    )

        return errors


############################################
# HELPER FUNCTIONS (CURRIED MODEL METHODS) #
+5 −0
Original line number Diff line number Diff line
@@ -58,6 +58,11 @@ Models
* **models.E016**: ``index_together/unique_together`` refers to field
  ``<field_name>`` which is not local to model ``<model>``.
* **models.E017**: Proxy model ``<model>`` contains model fields.
* **models.E018**: Autogenerated column name too long for field ``<field>``.
  Maximum length is ``<maximum length>`` for database ``<alias>``.
* **models.E019**: Autogenerated column name too long for M2M field
  ``<M2M field>``. Maximum length is ``<maximum length>`` for database
  ``<alias>``.

Fields
~~~~~~
+22 −0
Original line number Diff line number Diff line
@@ -305,6 +305,28 @@ Now to implement the same behavior, you have to create an
``parser.add_argument`` to add any custom arguments, as parser is now an
:py:class:`argparse.ArgumentParser` instance.

Model check ensures auto-generated column names are within limits specified by database
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A field name that's longer than the column name length supported by a database
can create problems. For example, with MySQL you'll get an exception trying to
create the column, and with PostgreSQL the column name is truncated by the
database (you may see a warning in the PostgreSQL logs).

A model check has been introduced to better alert users to this scenario before
the actual creation of database tables.

If you have an existing model where this check seems to be a false positive,
for example on PostgreSQL where the name was already being truncated, simply
use :attr:`~django.db.models.Field.db_column` to specify the name that's being
used.

The check also applies to the columns generated in an implicit
``ManyToManyField.through`` model. If you run into an issue there, use
:attr:`~django.db.models.ManyToManyField.through` to create an explicit model
and then specify :attr:`~django.db.models.Field.db_column` on its column(s)
as needed.

Miscellaneous
~~~~~~~~~~~~~

Loading