Refs #27236 -- Removed Meta.index_together per deprecation timeline.

This commit is contained in:
Mariusz Felisiak 2023-09-12 05:56:16 +02:00
parent 00e1879610
commit 2abf417c81
25 changed files with 41 additions and 1308 deletions

View File

@ -501,8 +501,7 @@ class BaseDatabaseSchemaEditor:
model, field, field_type, field.db_comment model, field, field_type, field.db_comment
) )
) )
# Add any field index and index_together's (deferred as SQLite # Add any field index (deferred as SQLite _remake_table needs it).
# _remake_table needs it).
self.deferred_sql.extend(self._model_indexes_sql(model)) self.deferred_sql.extend(self._model_indexes_sql(model))
# Make M2M tables # Make M2M tables
@ -1585,8 +1584,8 @@ class BaseDatabaseSchemaEditor:
def _model_indexes_sql(self, model): def _model_indexes_sql(self, model):
""" """
Return a list of all index SQL statements (field indexes, Return a list of all index SQL statements (field indexes, Meta.indexes)
index_together, Meta.indexes) for the specified model. for the specified model.
""" """
if not model._meta.managed or model._meta.proxy or model._meta.swapped: if not model._meta.managed or model._meta.proxy or model._meta.swapped:
return [] return []
@ -1594,11 +1593,6 @@ class BaseDatabaseSchemaEditor:
for field in model._meta.local_fields: for field in model._meta.local_fields:
output.extend(self._field_indexes_sql(model, field)) output.extend(self._field_indexes_sql(model, field))
# RemovedInDjango51Warning.
for field_names in model._meta.index_together:
fields = [model._meta.get_field(field) for field in field_names]
output.append(self._create_index_sql(model, fields=fields, suffix="_idx"))
for index in model._meta.indexes: for index in model._meta.indexes:
if ( if (
not index.contains_expressions not index.contains_expressions

View File

@ -180,14 +180,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
for unique in model._meta.unique_together for unique in model._meta.unique_together
] ]
# RemovedInDjango51Warning.
# Work out the new value for index_together, taking renames into
# account
index_together = [
[rename_mapping.get(n, n) for n in index]
for index in model._meta.index_together
]
indexes = model._meta.indexes indexes = model._meta.indexes
if delete_field: if delete_field:
indexes = [ indexes = [
@ -210,7 +202,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
"app_label": model._meta.app_label, "app_label": model._meta.app_label,
"db_table": model._meta.db_table, "db_table": model._meta.db_table,
"unique_together": unique_together, "unique_together": unique_together,
"index_together": index_together, # RemovedInDjango51Warning.
"indexes": indexes, "indexes": indexes,
"constraints": constraints, "constraints": constraints,
"apps": apps, "apps": apps,
@ -226,7 +217,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
"app_label": model._meta.app_label, "app_label": model._meta.app_label,
"db_table": "new__%s" % strip_quotes(model._meta.db_table), "db_table": "new__%s" % strip_quotes(model._meta.db_table),
"unique_together": unique_together, "unique_together": unique_together,
"index_together": index_together, # RemovedInDjango51Warning.
"indexes": indexes, "indexes": indexes,
"constraints": constraints, "constraints": constraints,
"apps": apps, "apps": apps,

View File

@ -190,14 +190,12 @@ class MigrationAutodetector:
self.generate_renamed_indexes() self.generate_renamed_indexes()
# Generate removal of foo together. # Generate removal of foo together.
self.generate_removed_altered_unique_together() self.generate_removed_altered_unique_together()
self.generate_removed_altered_index_together() # RemovedInDjango51Warning.
# Generate field operations. # Generate field operations.
self.generate_removed_fields() self.generate_removed_fields()
self.generate_added_fields() self.generate_added_fields()
self.generate_altered_fields() self.generate_altered_fields()
self.generate_altered_order_with_respect_to() self.generate_altered_order_with_respect_to()
self.generate_altered_unique_together() self.generate_altered_unique_together()
self.generate_altered_index_together() # RemovedInDjango51Warning.
self.generate_added_indexes() self.generate_added_indexes()
self.generate_added_constraints() self.generate_added_constraints()
self.generate_altered_db_table() self.generate_altered_db_table()
@ -584,7 +582,7 @@ class MigrationAutodetector:
possible). possible).
Defer any model options that refer to collections of fields that might Defer any model options that refer to collections of fields that might
be deferred (e.g. unique_together, index_together). be deferred (e.g. unique_together).
""" """
old_keys = self.old_model_keys | self.old_unmanaged_keys old_keys = self.old_model_keys | self.old_unmanaged_keys
added_models = self.new_model_keys - old_keys added_models = self.new_model_keys - old_keys
@ -608,12 +606,10 @@ class MigrationAutodetector:
if getattr(field.remote_field, "through", None): if getattr(field.remote_field, "through", None):
related_fields[field_name] = field related_fields[field_name] = field
# Are there indexes/unique|index_together to defer? # Are there indexes/unique_together to defer?
indexes = model_state.options.pop("indexes") indexes = model_state.options.pop("indexes")
constraints = model_state.options.pop("constraints") constraints = model_state.options.pop("constraints")
unique_together = model_state.options.pop("unique_together", None) unique_together = model_state.options.pop("unique_together", None)
# RemovedInDjango51Warning.
index_together = model_state.options.pop("index_together", None)
order_with_respect_to = model_state.options.pop( order_with_respect_to = model_state.options.pop(
"order_with_respect_to", None "order_with_respect_to", None
) )
@ -742,16 +738,6 @@ class MigrationAutodetector:
), ),
dependencies=related_dependencies, dependencies=related_dependencies,
) )
# RemovedInDjango51Warning.
if index_together:
self.add_operation(
app_label,
operations.AlterIndexTogether(
name=model_name,
index_together=index_together,
),
dependencies=related_dependencies,
)
# Fix relationships if the model changed from a proxy model to a # Fix relationships if the model changed from a proxy model to a
# concrete model. # concrete model.
relations = self.to_state.relations relations = self.to_state.relations
@ -832,8 +818,6 @@ class MigrationAutodetector:
related_fields[field_name] = field related_fields[field_name] = field
# Generate option removal first # Generate option removal first
unique_together = model_state.options.pop("unique_together", None) unique_together = model_state.options.pop("unique_together", None)
# RemovedInDjango51Warning.
index_together = model_state.options.pop("index_together", None)
if unique_together: if unique_together:
self.add_operation( self.add_operation(
app_label, app_label,
@ -842,15 +826,6 @@ class MigrationAutodetector:
unique_together=None, unique_together=None,
), ),
) )
# RemovedInDjango51Warning.
if index_together:
self.add_operation(
app_label,
operations.AlterIndexTogether(
name=model_name,
index_together=None,
),
)
# Then remove each related field # Then remove each related field
for name in sorted(related_fields): for name in sorted(related_fields):
self.add_operation( self.add_operation(
@ -1525,10 +1500,6 @@ class MigrationAutodetector:
def generate_removed_altered_unique_together(self): def generate_removed_altered_unique_together(self):
self._generate_removed_altered_foo_together(operations.AlterUniqueTogether) self._generate_removed_altered_foo_together(operations.AlterUniqueTogether)
# RemovedInDjango51Warning.
def generate_removed_altered_index_together(self):
self._generate_removed_altered_foo_together(operations.AlterIndexTogether)
def _generate_altered_foo_together(self, operation): def _generate_altered_foo_together(self, operation):
for ( for (
old_value, old_value,
@ -1548,10 +1519,6 @@ class MigrationAutodetector:
def generate_altered_unique_together(self): def generate_altered_unique_together(self):
self._generate_altered_foo_together(operations.AlterUniqueTogether) self._generate_altered_foo_together(operations.AlterUniqueTogether)
# RemovedInDjango51Warning.
def generate_altered_index_together(self):
self._generate_altered_foo_together(operations.AlterIndexTogether)
def generate_altered_db_table(self): def generate_altered_db_table(self):
models_to_check = self.kept_model_keys.union( models_to_check = self.kept_model_keys.union(
self.kept_proxy_keys, self.kept_unmanaged_keys self.kept_proxy_keys, self.kept_unmanaged_keys

View File

@ -341,33 +341,6 @@ class CreateModel(ModelOperation):
managers=self.managers, managers=self.managers,
), ),
] ]
elif isinstance(operation, RenameIndex) and operation.old_fields:
options_index_together = {
fields
for fields in self.options.get("index_together", [])
if fields != operation.old_fields
}
if options_index_together:
self.options["index_together"] = options_index_together
else:
self.options.pop("index_together", None)
return [
CreateModel(
self.name,
fields=self.fields,
options={
**self.options,
"indexes": [
*self.options.get("indexes", []),
models.Index(
fields=operation.old_fields, name=operation.new_name
),
],
},
bases=self.bases,
managers=self.managers,
),
]
return super().reduce(operation, app_label) return super().reduce(operation, app_label)

View File

@ -310,14 +310,13 @@ class ProjectState:
for from_field_name in from_fields for from_field_name in from_fields
] ]
) )
# Fix index/unique_together to refer to the new field. # Fix unique_together to refer to the new field.
options = model_state.options options = model_state.options
for option in ("index_together", "unique_together"): if "unique_together" in options:
if option in options: options["unique_together"] = [
options[option] = [ [new_name if n == old_name else n for n in together]
[new_name if n == old_name else n for n in together] for together in options["unique_together"]
for together in options[option] ]
]
# Fix to_fields to refer to the new field. # Fix to_fields to refer to the new field.
delay = True delay = True
references = get_references(self, model_key, (old_name, found)) references = get_references(self, model_key, (old_name, found))

View File

@ -1595,7 +1595,6 @@ class Model(AltersData, metaclass=ModelBase):
if not clash_errors: if not clash_errors:
errors.extend(cls._check_column_name_clashes()) errors.extend(cls._check_column_name_clashes())
errors += [ errors += [
*cls._check_index_together(),
*cls._check_unique_together(), *cls._check_unique_together(),
*cls._check_indexes(databases), *cls._check_indexes(databases),
*cls._check_ordering(), *cls._check_ordering(),
@ -1928,36 +1927,6 @@ class Model(AltersData, metaclass=ModelBase):
) )
return errors return errors
# RemovedInDjango51Warning.
@classmethod
def _check_index_together(cls):
"""Check the value of "index_together" option."""
if not isinstance(cls._meta.index_together, (tuple, list)):
return [
checks.Error(
"'index_together' must be a list or tuple.",
obj=cls,
id="models.E008",
)
]
elif any(
not isinstance(fields, (tuple, list)) for fields in cls._meta.index_together
):
return [
checks.Error(
"All 'index_together' elements must be lists or tuples.",
obj=cls,
id="models.E009",
)
]
else:
errors = []
for fields in cls._meta.index_together:
errors.extend(cls._check_local_fields(fields, "index_together"))
return errors
@classmethod @classmethod
def _check_unique_together(cls): def _check_unique_together(cls):
"""Check the value of "unique_together" option.""" """Check the value of "unique_together" option."""

View File

@ -1,7 +1,6 @@
import bisect import bisect
import copy import copy
import inspect import inspect
import warnings
from collections import defaultdict from collections import defaultdict
from django.apps import apps from django.apps import apps
@ -11,7 +10,6 @@ from django.db import connections
from django.db.models import AutoField, Manager, OrderWrt, UniqueConstraint from django.db.models import AutoField, Manager, OrderWrt, UniqueConstraint
from django.db.models.query_utils import PathInfo from django.db.models.query_utils import PathInfo
from django.utils.datastructures import ImmutableList, OrderedSet from django.utils.datastructures import ImmutableList, OrderedSet
from django.utils.deprecation import RemovedInDjango51Warning
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.utils.text import camel_case_to_spaces, format_lazy from django.utils.text import camel_case_to_spaces, format_lazy
@ -43,7 +41,6 @@ DEFAULT_NAMES = (
"proxy", "proxy",
"swappable", "swappable",
"auto_created", "auto_created",
"index_together", # RemovedInDjango51Warning.
"apps", "apps",
"default_permissions", "default_permissions",
"select_on_save", "select_on_save",
@ -119,7 +116,6 @@ class Options:
self.indexes = [] self.indexes = []
self.constraints = [] self.constraints = []
self.unique_together = [] self.unique_together = []
self.index_together = [] # RemovedInDjango51Warning.
self.select_on_save = False self.select_on_save = False
self.default_permissions = ("add", "change", "delete", "view") self.default_permissions = ("add", "change", "delete", "view")
self.permissions = [] self.permissions = []
@ -205,13 +201,6 @@ class Options:
self.original_attrs[attr_name] = getattr(self, attr_name) self.original_attrs[attr_name] = getattr(self, attr_name)
self.unique_together = normalize_together(self.unique_together) self.unique_together = normalize_together(self.unique_together)
self.index_together = normalize_together(self.index_together)
if self.index_together:
warnings.warn(
f"'index_together' is deprecated. Use 'Meta.indexes' in "
f"{self.label!r} instead.",
RemovedInDjango51Warning,
)
# App label/class name interpolation for names of constraints and # App label/class name interpolation for names of constraints and
# indexes. # indexes.
if not getattr(cls._meta, "abstract", False): if not getattr(cls._meta, "abstract", False):

View File

@ -345,21 +345,23 @@ Models
``<field name>`` from model ``<model>``. ``<field name>`` from model ``<model>``.
* **models.E007**: Field ``<field name>`` has column name ``<column name>`` * **models.E007**: Field ``<field name>`` has column name ``<column name>``
that is used by another field. that is used by another field.
* **models.E008**: ``index_together`` must be a list or tuple. * **models.E008**: ``index_together`` must be a list or tuple. *This check
appeared before Django 5.1.*
* **models.E009**: All ``index_together`` elements must be lists or tuples. * **models.E009**: All ``index_together`` elements must be lists or tuples.
*This check appeared before Django 5.1.*
* **models.E010**: ``unique_together`` must be a list or tuple. * **models.E010**: ``unique_together`` must be a list or tuple.
* **models.E011**: All ``unique_together`` elements must be lists or tuples. * **models.E011**: All ``unique_together`` elements must be lists or tuples.
* **models.E012**: ``constraints/indexes/index_together/unique_together`` * **models.E012**: ``constraints/indexes/unique_together`` refers to the
refers to the nonexistent field ``<field name>``. nonexistent field ``<field name>``.
* **models.E013**: ``constraints/indexes/index_together/unique_together`` * **models.E013**: ``constraints/indexes/unique_together`` refers to a
refers to a ``ManyToManyField`` ``<field name>``, but ``ManyToManyField``\s ``ManyToManyField`` ``<field name>``, but ``ManyToManyField``\s are not
are not supported for that option. supported for that option.
* **models.E014**: ``ordering`` must be a tuple or list (even if you want to * **models.E014**: ``ordering`` must be a tuple or list (even if you want to
order by only one field). order by only one field).
* **models.E015**: ``ordering`` refers to the nonexistent field, related field, * **models.E015**: ``ordering`` refers to the nonexistent field, related field,
or lookup ``<field name>``. or lookup ``<field name>``.
* **models.E016**: ``constraints/indexes/index_together/unique_together`` * **models.E016**: ``constraints/indexes/unique_together`` refers to field
refers to field ``<field_name>`` which is not local to model ``<model>``. ``<field_name>`` which is not local to model ``<model>``.
* **models.E017**: Proxy model ``<model>`` contains model fields. * **models.E017**: Proxy model ``<model>`` contains model fields.
* **models.E018**: Autogenerated column name too long for field ``<field>``. * **models.E018**: Autogenerated column name too long for field ``<field>``.
Maximum length is ``<maximum length>`` for database ``<alias>``. Maximum length is ``<maximum length>`` for database ``<alias>``.

View File

@ -247,7 +247,7 @@ Removes the index named ``name`` from the model with ``model_name``.
Renames an index in the database table for the model with ``model_name``. Renames an index in the database table for the model with ``model_name``.
Exactly one of ``old_name`` and ``old_fields`` can be provided. ``old_fields`` Exactly one of ``old_name`` and ``old_fields`` can be provided. ``old_fields``
is an iterable of the strings, often corresponding to fields of is an iterable of the strings, often corresponding to fields of
:attr:`~django.db.models.Options.index_together`. ``index_together`` (pre-Django 5.1 option).
On databases that don't support an index renaming statement (SQLite and MariaDB On databases that don't support an index renaming statement (SQLite and MariaDB
< 10.5.2), the operation will drop and recreate the index, which can be < 10.5.2), the operation will drop and recreate the index, which can be

View File

@ -451,29 +451,6 @@ not be looking at your Django code. For example::
The ``ValidationError`` raised during model validation when the constraint The ``ValidationError`` raised during model validation when the constraint
is violated has the ``unique_together`` error code. is violated has the ``unique_together`` error code.
``index_together``
------------------
.. attribute:: Options.index_together
Sets of field names that, taken together, are indexed::
index_together = [
["pub_date", "deadline"],
]
This list of fields will be indexed together (i.e. the appropriate
``CREATE INDEX`` statement will be issued.)
For convenience, ``index_together`` can be a single list when dealing with a single
set of fields::
index_together = ["pub_date", "deadline"]
.. deprecated:: 4.2
Use the :attr:`~Options.indexes` option instead.
``constraints`` ``constraints``
--------------- ---------------

View File

@ -114,9 +114,8 @@ the new value.
.. method:: BaseDatabaseSchemaEditor.alter_index_together(model, old_index_together, new_index_together) .. method:: BaseDatabaseSchemaEditor.alter_index_together(model, old_index_together, new_index_together)
Changes a model's :attr:`~django.db.models.Options.index_together` value; this Changes a model's ``index_together`` value; this will add or remove indexes
will add or remove indexes from the model's table until they match the new from the model's table until they match the new value.
value.
``alter_db_table()`` ``alter_db_table()``
-------------------- --------------------

View File

@ -59,8 +59,8 @@ creating database indexes. Indexes are added to models using the
The :class:`~django.db.models.Index` class creates a b-tree index, as if you The :class:`~django.db.models.Index` class creates a b-tree index, as if you
used :attr:`~django.db.models.Field.db_index` on the model field or used :attr:`~django.db.models.Field.db_index` on the model field or
:attr:`~django.db.models.Options.index_together` on the model ``Meta`` class. ``index_together`` on the model ``Meta`` class. It can be subclassed to support
It can be subclassed to support different index types, such as different index types, such as
:class:`~django.contrib.postgres.indexes.GinIndex`. It also allows defining the :class:`~django.contrib.postgres.indexes.GinIndex`. It also allows defining the
order (ASC/DESC) for the columns of the index. order (ASC/DESC) for the columns of the index.

View File

@ -329,8 +329,7 @@ Django 1.5 also includes several smaller improvements worth noting:
session data in a non-default cache. session data in a non-default cache.
* Multi-column indexes can now be created on models. Read the * Multi-column indexes can now be created on models. Read the
:attr:`~django.db.models.Options.index_together` documentation for more ``index_together`` documentation for more information.
information.
* During Django's logging configuration verbose Deprecation warnings are * During Django's logging configuration verbose Deprecation warnings are
enabled and warnings are captured into the logging system. Logged warnings enabled and warnings are captured into the logging system. Logged warnings

View File

@ -778,8 +778,8 @@ Models
an error (before that, it would either result in a database error or an error (before that, it would either result in a database error or
incorrect data). incorrect data).
* You can use a single list for :attr:`~django.db.models.Options.index_together` * You can use a single list for ``index_together`` (rather than a list of
(rather than a list of lists) when specifying a single set of fields. lists) when specifying a single set of fields.
* Custom intermediate models having more than one foreign key to any of the * Custom intermediate models having more than one foreign key to any of the
models participating in a many-to-many relationship are now permitted, models participating in a many-to-many relationship are now permitted,

View File

@ -317,7 +317,7 @@ Migrations
* The new :class:`~django.db.migrations.operations.RenameIndex` operation * The new :class:`~django.db.migrations.operations.RenameIndex` operation
allows renaming indexes defined in the allows renaming indexes defined in the
:attr:`Meta.indexes <django.db.models.Options.indexes>` or :attr:`Meta.indexes <django.db.models.Options.indexes>` or
:attr:`~django.db.models.Options.index_together` options. ``index_together`` options.
* The migrations autodetector now generates * The migrations autodetector now generates
:class:`~django.db.migrations.operations.RenameIndex` operations instead of :class:`~django.db.migrations.operations.RenameIndex` operations instead of
@ -327,7 +327,7 @@ Migrations
* The migrations autodetector now generates * The migrations autodetector now generates
:class:`~django.db.migrations.operations.RenameIndex` operations instead of :class:`~django.db.migrations.operations.RenameIndex` operations instead of
``AlterIndexTogether`` and ``AddIndex``, when moving indexes defined in the ``AlterIndexTogether`` and ``AddIndex``, when moving indexes defined in the
:attr:`Meta.index_together <django.db.models.Options.index_together>` to the ``Meta.index_together`` to the
:attr:`Meta.indexes <django.db.models.Options.indexes>`. :attr:`Meta.indexes <django.db.models.Options.indexes>`.
Models Models

View File

@ -477,9 +477,8 @@ Features deprecated in 4.2
``index_together`` option is deprecated in favor of ``indexes`` ``index_together`` option is deprecated in favor of ``indexes``
--------------------------------------------------------------- ---------------------------------------------------------------
The :attr:`Meta.index_together <django.db.models.Options.index_together>` The ``Meta.index_together`` option is deprecated in favor of the
option is deprecated in favor of the :attr:`~django.db.models.Options.indexes` :attr:`~django.db.models.Options.indexes` option.
option.
Migrating existing ``index_together`` should be handled as a migration. For Migrating existing ``index_together`` should be handled as a migration. For
example:: example::

View File

@ -252,3 +252,5 @@ See :ref:`deprecated-features-4.2` for details on these changes, including how
to remove usage of these features. to remove usage of these features.
* The ``BaseUserManager.make_random_password()`` method is removed. * The ``BaseUserManager.make_random_password()`` method is removed.
* The model's ``Meta.index_together`` option is removed.

View File

@ -3,26 +3,16 @@ from unittest import skipUnless
from django.conf import settings from django.conf import settings
from django.db import connection from django.db import connection
from django.db.models import ( from django.db.models import CASCADE, ForeignKey, Index, Q
CASCADE,
CharField,
DateTimeField,
ForeignKey,
Index,
Model,
Q,
)
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.test import ( from django.test import (
TestCase, TestCase,
TransactionTestCase, TransactionTestCase,
ignore_warnings,
skipIfDBFeature, skipIfDBFeature,
skipUnlessDBFeature, skipUnlessDBFeature,
) )
from django.test.utils import isolate_apps, override_settings from django.test.utils import override_settings
from django.utils import timezone from django.utils import timezone
from django.utils.deprecation import RemovedInDjango51Warning
from .models import Article, ArticleTranslation, IndexedArticle2 from .models import Article, ArticleTranslation, IndexedArticle2
@ -80,21 +70,6 @@ class SchemaIndexesTests(TestCase):
index_sql[0], index_sql[0],
) )
@ignore_warnings(category=RemovedInDjango51Warning)
@isolate_apps("indexes")
def test_index_together_single_list(self):
class IndexTogetherSingleList(Model):
headline = CharField(max_length=100)
pub_date = DateTimeField()
class Meta:
index_together = ["headline", "pub_date"]
index_sql = connection.schema_editor()._model_indexes_sql(
IndexTogetherSingleList
)
self.assertEqual(len(index_sql), 1)
def test_columns_list_sql(self): def test_columns_list_sql(self):
index = Index(fields=["headline"], name="whitespace_idx") index = Index(fields=["headline"], name="whitespace_idx")
editor = connection.schema_editor() editor = connection.schema_editor()

View File

@ -5,9 +5,8 @@ from django.core.checks.model_checks import _check_lazy_references
from django.db import connection, connections, models from django.db import connection, connections, models
from django.db.models.functions import Abs, Lower, Round from django.db.models.functions import Abs, Lower, Round
from django.db.models.signals import post_init from django.db.models.signals import post_init
from django.test import SimpleTestCase, TestCase, ignore_warnings, skipUnlessDBFeature from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
from django.test.utils import isolate_apps, override_settings, register_lookup from django.test.utils import isolate_apps, override_settings, register_lookup
from django.utils.deprecation import RemovedInDjango51Warning
class EmptyRouter: class EmptyRouter:
@ -29,134 +28,6 @@ def get_max_column_name_length():
return (allowed_len, db_alias) return (allowed_len, db_alias)
@isolate_apps("invalid_models_tests")
@ignore_warnings(category=RemovedInDjango51Warning)
class IndexTogetherTests(SimpleTestCase):
def test_non_iterable(self):
class Model(models.Model):
class Meta:
index_together = 42
self.assertEqual(
Model.check(),
[
Error(
"'index_together' must be a list or tuple.",
obj=Model,
id="models.E008",
),
],
)
def test_non_list(self):
class Model(models.Model):
class Meta:
index_together = "not-a-list"
self.assertEqual(
Model.check(),
[
Error(
"'index_together' must be a list or tuple.",
obj=Model,
id="models.E008",
),
],
)
def test_list_containing_non_iterable(self):
class Model(models.Model):
class Meta:
index_together = [("a", "b"), 42]
self.assertEqual(
Model.check(),
[
Error(
"All 'index_together' elements must be lists or tuples.",
obj=Model,
id="models.E009",
),
],
)
def test_pointing_to_missing_field(self):
class Model(models.Model):
class Meta:
index_together = [["missing_field"]]
self.assertEqual(
Model.check(),
[
Error(
"'index_together' refers to the nonexistent field 'missing_field'.",
obj=Model,
id="models.E012",
),
],
)
def test_pointing_to_non_local_field(self):
class Foo(models.Model):
field1 = models.IntegerField()
class Bar(Foo):
field2 = models.IntegerField()
class Meta:
index_together = [["field2", "field1"]]
self.assertEqual(
Bar.check(),
[
Error(
"'index_together' refers to field 'field1' which is not "
"local to model 'Bar'.",
hint="This issue may be caused by multi-table inheritance.",
obj=Bar,
id="models.E016",
),
],
)
def test_pointing_to_m2m_field(self):
class Model(models.Model):
m2m = models.ManyToManyField("self")
class Meta:
index_together = [["m2m"]]
self.assertEqual(
Model.check(),
[
Error(
"'index_together' refers to a ManyToManyField 'm2m', but "
"ManyToManyFields are not permitted in 'index_together'.",
obj=Model,
id="models.E013",
),
],
)
def test_pointing_to_fk(self):
class Foo(models.Model):
pass
class Bar(models.Model):
foo_1 = models.ForeignKey(
Foo, on_delete=models.CASCADE, related_name="bar_1"
)
foo_2 = models.ForeignKey(
Foo, on_delete=models.CASCADE, related_name="bar_2"
)
class Meta:
index_together = [["foo_1_id", "foo_2"]]
self.assertEqual(Bar.check(), [])
# unique_together tests are very similar to index_together tests.
@isolate_apps("invalid_models_tests") @isolate_apps("invalid_models_tests")
class UniqueTogetherTests(SimpleTestCase): class UniqueTogetherTests(SimpleTestCase):
def test_non_iterable(self): def test_non_iterable(self):

View File

@ -13,9 +13,8 @@ from django.db.migrations.graph import MigrationGraph
from django.db.migrations.loader import MigrationLoader from django.db.migrations.loader import MigrationLoader
from django.db.migrations.questioner import MigrationQuestioner from django.db.migrations.questioner import MigrationQuestioner
from django.db.migrations.state import ModelState, ProjectState from django.db.migrations.state import ModelState, ProjectState
from django.test import SimpleTestCase, TestCase, ignore_warnings, override_settings from django.test import SimpleTestCase, TestCase, override_settings
from django.test.utils import isolate_lru_cache from django.test.utils import isolate_lru_cache
from django.utils.deprecation import RemovedInDjango51Warning
from .models import FoodManager, FoodQuerySet from .models import FoodManager, FoodQuerySet
@ -4872,592 +4871,6 @@ class AutodetectorTests(BaseAutodetectorTests):
self.assertOperationAttributes(changes, "testapp", 0, 0, name="Book") self.assertOperationAttributes(changes, "testapp", 0, 0, name="Book")
@ignore_warnings(category=RemovedInDjango51Warning)
class AutodetectorIndexTogetherTests(BaseAutodetectorTests):
book_index_together = ModelState(
"otherapp",
"Book",
[
("id", models.AutoField(primary_key=True)),
("author", models.ForeignKey("testapp.Author", models.CASCADE)),
("title", models.CharField(max_length=200)),
],
{
"index_together": {("author", "title")},
},
)
book_index_together_2 = ModelState(
"otherapp",
"Book",
[
("id", models.AutoField(primary_key=True)),
("author", models.ForeignKey("testapp.Author", models.CASCADE)),
("title", models.CharField(max_length=200)),
],
{
"index_together": {("title", "author")},
},
)
book_index_together_3 = ModelState(
"otherapp",
"Book",
[
("id", models.AutoField(primary_key=True)),
("newfield", models.IntegerField()),
("author", models.ForeignKey("testapp.Author", models.CASCADE)),
("title", models.CharField(max_length=200)),
],
{
"index_together": {("title", "newfield")},
},
)
book_index_together_4 = ModelState(
"otherapp",
"Book",
[
("id", models.AutoField(primary_key=True)),
("newfield2", models.IntegerField()),
("author", models.ForeignKey("testapp.Author", models.CASCADE)),
("title", models.CharField(max_length=200)),
],
{
"index_together": {("title", "newfield2")},
},
)
def test_empty_index_together(self):
"""Empty index_together shouldn't generate a migration."""
# Explicitly testing for not specified, since this is the case after
# a CreateModel operation w/o any definition on the original model
model_state_not_specified = ModelState(
"a", "model", [("id", models.AutoField(primary_key=True))]
)
# Explicitly testing for None, since this was the issue in #23452 after
# an AlterIndexTogether operation with e.g. () as value
model_state_none = ModelState(
"a",
"model",
[("id", models.AutoField(primary_key=True))],
{
"index_together": None,
},
)
# Explicitly testing for the empty set, since we now always have sets.
# During removal (('col1', 'col2'),) --> () this becomes set([])
model_state_empty = ModelState(
"a",
"model",
[("id", models.AutoField(primary_key=True))],
{
"index_together": set(),
},
)
def test(from_state, to_state, msg):
changes = self.get_changes([from_state], [to_state])
if changes:
ops = ", ".join(
o.__class__.__name__ for o in changes["a"][0].operations
)
self.fail("Created operation(s) %s from %s" % (ops, msg))
tests = (
(
model_state_not_specified,
model_state_not_specified,
'"not specified" to "not specified"',
),
(model_state_not_specified, model_state_none, '"not specified" to "None"'),
(
model_state_not_specified,
model_state_empty,
'"not specified" to "empty"',
),
(model_state_none, model_state_not_specified, '"None" to "not specified"'),
(model_state_none, model_state_none, '"None" to "None"'),
(model_state_none, model_state_empty, '"None" to "empty"'),
(
model_state_empty,
model_state_not_specified,
'"empty" to "not specified"',
),
(model_state_empty, model_state_none, '"empty" to "None"'),
(model_state_empty, model_state_empty, '"empty" to "empty"'),
)
for t in tests:
test(*t)
def test_rename_index_together_to_index(self):
changes = self.get_changes(
[AutodetectorTests.author_empty, self.book_index_together],
[AutodetectorTests.author_empty, AutodetectorTests.book_indexes],
)
self.assertNumberMigrations(changes, "otherapp", 1)
self.assertOperationTypes(changes, "otherapp", 0, ["RenameIndex"])
self.assertOperationAttributes(
changes,
"otherapp",
0,
0,
model_name="book",
new_name="book_title_author_idx",
old_fields=("author", "title"),
)
def test_rename_index_together_to_index_extra_options(self):
# Indexes with extra options don't match indexes in index_together.
book_partial_index = ModelState(
"otherapp",
"Book",
[
("id", models.AutoField(primary_key=True)),
("author", models.ForeignKey("testapp.Author", models.CASCADE)),
("title", models.CharField(max_length=200)),
],
{
"indexes": [
models.Index(
fields=["author", "title"],
condition=models.Q(title__startswith="The"),
name="book_title_author_idx",
)
],
},
)
changes = self.get_changes(
[AutodetectorTests.author_empty, self.book_index_together],
[AutodetectorTests.author_empty, book_partial_index],
)
self.assertNumberMigrations(changes, "otherapp", 1)
self.assertOperationTypes(
changes,
"otherapp",
0,
["AlterIndexTogether", "AddIndex"],
)
def test_rename_index_together_to_index_order_fields(self):
# Indexes with reordered fields don't match indexes in index_together.
changes = self.get_changes(
[AutodetectorTests.author_empty, self.book_index_together],
[AutodetectorTests.author_empty, AutodetectorTests.book_unordered_indexes],
)
self.assertNumberMigrations(changes, "otherapp", 1)
self.assertOperationTypes(
changes,
"otherapp",
0,
["AlterIndexTogether", "AddIndex"],
)
def test_add_index_together(self):
changes = self.get_changes(
[AutodetectorTests.author_empty, AutodetectorTests.book],
[AutodetectorTests.author_empty, self.book_index_together],
)
self.assertNumberMigrations(changes, "otherapp", 1)
self.assertOperationTypes(changes, "otherapp", 0, ["AlterIndexTogether"])
self.assertOperationAttributes(
changes, "otherapp", 0, 0, name="book", index_together={("author", "title")}
)
def test_remove_index_together(self):
changes = self.get_changes(
[AutodetectorTests.author_empty, self.book_index_together],
[AutodetectorTests.author_empty, AutodetectorTests.book],
)
self.assertNumberMigrations(changes, "otherapp", 1)
self.assertOperationTypes(changes, "otherapp", 0, ["AlterIndexTogether"])
self.assertOperationAttributes(
changes, "otherapp", 0, 0, name="book", index_together=set()
)
def test_index_together_remove_fk(self):
changes = self.get_changes(
[AutodetectorTests.author_empty, self.book_index_together],
[AutodetectorTests.author_empty, AutodetectorTests.book_with_no_author],
)
self.assertNumberMigrations(changes, "otherapp", 1)
self.assertOperationTypes(
changes,
"otherapp",
0,
["AlterIndexTogether", "RemoveField"],
)
self.assertOperationAttributes(
changes, "otherapp", 0, 0, name="book", index_together=set()
)
self.assertOperationAttributes(
changes, "otherapp", 0, 1, model_name="book", name="author"
)
def test_index_together_no_changes(self):
"""
index_together doesn't generate a migration if no changes have been
made.
"""
changes = self.get_changes(
[AutodetectorTests.author_empty, self.book_index_together],
[AutodetectorTests.author_empty, self.book_index_together],
)
self.assertEqual(len(changes), 0)
def test_index_together_ordering(self):
"""index_together triggers on ordering changes."""
changes = self.get_changes(
[AutodetectorTests.author_empty, self.book_index_together],
[AutodetectorTests.author_empty, self.book_index_together_2],
)
self.assertNumberMigrations(changes, "otherapp", 1)
self.assertOperationTypes(
changes,
"otherapp",
0,
["AlterIndexTogether"],
)
self.assertOperationAttributes(
changes,
"otherapp",
0,
0,
name="book",
index_together={("title", "author")},
)
def test_add_field_and_index_together(self):
"""
Added fields will be created before using them in index_together.
"""
changes = self.get_changes(
[AutodetectorTests.author_empty, AutodetectorTests.book],
[AutodetectorTests.author_empty, self.book_index_together_3],
)
self.assertNumberMigrations(changes, "otherapp", 1)
self.assertOperationTypes(
changes,
"otherapp",
0,
["AddField", "AlterIndexTogether"],
)
self.assertOperationAttributes(
changes,
"otherapp",
0,
1,
name="book",
index_together={("title", "newfield")},
)
def test_create_model_and_index_together(self):
author = ModelState(
"otherapp",
"Author",
[
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=200)),
],
)
book_with_author = ModelState(
"otherapp",
"Book",
[
("id", models.AutoField(primary_key=True)),
("author", models.ForeignKey("otherapp.Author", models.CASCADE)),
("title", models.CharField(max_length=200)),
],
{
"index_together": {("title", "author")},
},
)
changes = self.get_changes(
[AutodetectorTests.book_with_no_author], [author, book_with_author]
)
self.assertEqual(len(changes["otherapp"]), 1)
migration = changes["otherapp"][0]
self.assertEqual(len(migration.operations), 3)
self.assertOperationTypes(
changes,
"otherapp",
0,
["CreateModel", "AddField", "AlterIndexTogether"],
)
def test_remove_field_and_index_together(self):
"""
Removed fields will be removed after updating index_together.
"""
changes = self.get_changes(
[AutodetectorTests.author_empty, self.book_index_together_3],
[AutodetectorTests.author_empty, self.book_index_together],
)
self.assertNumberMigrations(changes, "otherapp", 1)
self.assertOperationTypes(
changes,
"otherapp",
0,
["AlterIndexTogether", "RemoveField"],
)
self.assertOperationAttributes(
changes,
"otherapp",
0,
0,
name="book",
index_together={("author", "title")},
)
self.assertOperationAttributes(
changes,
"otherapp",
0,
1,
model_name="book",
name="newfield",
)
def test_alter_field_and_index_together(self):
"""Fields are altered after deleting some index_together."""
initial_author = ModelState(
"testapp",
"Author",
[
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=200)),
("age", models.IntegerField(db_index=True)),
],
{
"index_together": {("name",)},
},
)
author_reversed_constraints = ModelState(
"testapp",
"Author",
[
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=200, unique=True)),
("age", models.IntegerField()),
],
{
"index_together": {("age",)},
},
)
changes = self.get_changes([initial_author], [author_reversed_constraints])
self.assertNumberMigrations(changes, "testapp", 1)
self.assertOperationTypes(
changes,
"testapp",
0,
[
"AlterIndexTogether",
"AlterField",
"AlterField",
"AlterIndexTogether",
],
)
self.assertOperationAttributes(
changes,
"testapp",
0,
0,
name="author",
index_together=set(),
)
self.assertOperationAttributes(
changes,
"testapp",
0,
1,
model_name="author",
name="age",
)
self.assertOperationAttributes(
changes,
"testapp",
0,
2,
model_name="author",
name="name",
)
self.assertOperationAttributes(
changes,
"testapp",
0,
3,
name="author",
index_together={("age",)},
)
def test_partly_alter_index_together_increase(self):
initial_author = ModelState(
"testapp",
"Author",
[
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=200)),
("age", models.IntegerField()),
],
{
"index_together": {("name",)},
},
)
author_new_constraints = ModelState(
"testapp",
"Author",
[
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=200)),
("age", models.IntegerField()),
],
{
"index_together": {("name",), ("age",)},
},
)
changes = self.get_changes([initial_author], [author_new_constraints])
self.assertNumberMigrations(changes, "testapp", 1)
self.assertOperationTypes(
changes,
"testapp",
0,
["AlterIndexTogether"],
)
self.assertOperationAttributes(
changes,
"testapp",
0,
0,
name="author",
index_together={("name",), ("age",)},
)
def test_partly_alter_index_together_decrease(self):
initial_author = ModelState(
"testapp",
"Author",
[
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=200)),
("age", models.IntegerField()),
],
{
"index_together": {("name",), ("age",)},
},
)
author_new_constraints = ModelState(
"testapp",
"Author",
[
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=200)),
("age", models.IntegerField()),
],
{
"index_together": {("age",)},
},
)
changes = self.get_changes([initial_author], [author_new_constraints])
self.assertNumberMigrations(changes, "testapp", 1)
self.assertOperationTypes(
changes,
"testapp",
0,
["AlterIndexTogether"],
)
self.assertOperationAttributes(
changes,
"testapp",
0,
0,
name="author",
index_together={("age",)},
)
def test_rename_field_and_index_together(self):
"""Fields are renamed before updating index_together."""
changes = self.get_changes(
[AutodetectorTests.author_empty, self.book_index_together_3],
[AutodetectorTests.author_empty, self.book_index_together_4],
MigrationQuestioner({"ask_rename": True}),
)
self.assertNumberMigrations(changes, "otherapp", 1)
self.assertOperationTypes(
changes,
"otherapp",
0,
["RenameField", "AlterIndexTogether"],
)
self.assertOperationAttributes(
changes,
"otherapp",
0,
1,
name="book",
index_together={("title", "newfield2")},
)
def test_add_model_order_with_respect_to_index_together(self):
changes = self.get_changes(
[],
[
AutodetectorTests.book,
ModelState(
"testapp",
"Author",
[
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=200)),
("book", models.ForeignKey("otherapp.Book", models.CASCADE)),
],
options={
"order_with_respect_to": "book",
"index_together": {("name", "_order")},
},
),
],
)
self.assertNumberMigrations(changes, "testapp", 1)
self.assertOperationTypes(changes, "testapp", 0, ["CreateModel"])
self.assertOperationAttributes(
changes,
"testapp",
0,
0,
name="Author",
options={
"order_with_respect_to": "book",
"index_together": {("name", "_order")},
},
)
def test_set_alter_order_with_respect_to_index_together(self):
after = ModelState(
"testapp",
"Author",
[
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=200)),
("book", models.ForeignKey("otherapp.Book", models.CASCADE)),
],
options={
"order_with_respect_to": "book",
"index_together": {("name", "_order")},
},
)
changes = self.get_changes(
[AutodetectorTests.book, AutodetectorTests.author_with_book],
[AutodetectorTests.book, after],
)
self.assertNumberMigrations(changes, "testapp", 1)
self.assertOperationTypes(
changes,
"testapp",
0,
["AlterOrderWithRespectTo", "AlterIndexTogether"],
)
class MigrationSuggestNameTests(SimpleTestCase): class MigrationSuggestNameTests(SimpleTestCase):
def test_no_operations(self): def test_no_operations(self):
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -269,7 +269,6 @@ class OperationTestBase(MigrationTestBase):
unique_together=False, unique_together=False,
options=False, options=False,
db_table=None, db_table=None,
index_together=False, # RemovedInDjango51Warning.
constraints=None, constraints=None,
indexes=None, indexes=None,
): ):
@ -277,8 +276,6 @@ class OperationTestBase(MigrationTestBase):
# Make the "current" state. # Make the "current" state.
model_options = { model_options = {
"swappable": "TEST_SWAP_MODEL", "swappable": "TEST_SWAP_MODEL",
# RemovedInDjango51Warning.
"index_together": [["weight", "pink"]] if index_together else [],
"unique_together": [["pink", "weight"]] if unique_together else [], "unique_together": [["pink", "weight"]] if unique_together else [],
} }
if options: if options:

View File

@ -11,13 +11,11 @@ from django.db.models.functions import Abs, Pi
from django.db.transaction import atomic from django.db.transaction import atomic
from django.test import ( from django.test import (
SimpleTestCase, SimpleTestCase,
ignore_warnings,
override_settings, override_settings,
skipIfDBFeature, skipIfDBFeature,
skipUnlessDBFeature, skipUnlessDBFeature,
) )
from django.test.utils import CaptureQueriesContext from django.test.utils import CaptureQueriesContext
from django.utils.deprecation import RemovedInDjango51Warning
from .models import FoodManager, FoodQuerySet, UnicodeModel from .models import FoodManager, FoodQuerySet, UnicodeModel
from .test_base import OperationTestBase from .test_base import OperationTestBase
@ -3094,37 +3092,6 @@ class OperationTests(OperationTestBase):
self.assertColumnExists("test_rnflut_pony", "pink") self.assertColumnExists("test_rnflut_pony", "pink")
self.assertColumnNotExists("test_rnflut_pony", "blue") self.assertColumnNotExists("test_rnflut_pony", "blue")
@ignore_warnings(category=RemovedInDjango51Warning)
def test_rename_field_index_together(self):
project_state = self.set_up_test_model("test_rnflit", index_together=True)
operation = migrations.RenameField("Pony", "pink", "blue")
new_state = project_state.clone()
operation.state_forwards("test_rnflit", new_state)
self.assertIn("blue", new_state.models["test_rnflit", "pony"].fields)
self.assertNotIn("pink", new_state.models["test_rnflit", "pony"].fields)
# index_together has the renamed column.
self.assertIn(
"blue", new_state.models["test_rnflit", "pony"].options["index_together"][0]
)
self.assertNotIn(
"pink", new_state.models["test_rnflit", "pony"].options["index_together"][0]
)
# Rename field.
self.assertColumnExists("test_rnflit_pony", "pink")
self.assertColumnNotExists("test_rnflit_pony", "blue")
with connection.schema_editor() as editor:
operation.database_forwards("test_rnflit", editor, project_state, new_state)
self.assertColumnExists("test_rnflit_pony", "blue")
self.assertColumnNotExists("test_rnflit_pony", "pink")
# The index constraint has been ported over.
self.assertIndexExists("test_rnflit_pony", ["weight", "blue"])
# Reversal.
with connection.schema_editor() as editor:
operation.database_backwards(
"test_rnflit", editor, new_state, project_state
)
self.assertIndexExists("test_rnflit_pony", ["weight", "pink"])
def test_rename_field_with_db_column(self): def test_rename_field_with_db_column(self):
project_state = self.apply_operations( project_state = self.apply_operations(
"test_rfwdbc", "test_rfwdbc",
@ -3601,52 +3568,6 @@ class OperationTests(OperationTestBase):
with self.assertRaisesMessage(ValueError, msg): with self.assertRaisesMessage(ValueError, msg):
migrations.RenameIndex("Pony", new_name="new_idx_name") migrations.RenameIndex("Pony", new_name="new_idx_name")
@ignore_warnings(category=RemovedInDjango51Warning)
def test_rename_index_unnamed_index(self):
app_label = "test_rninui"
project_state = self.set_up_test_model(app_label, index_together=True)
table_name = app_label + "_pony"
self.assertIndexNameNotExists(table_name, "new_pony_test_idx")
operation = migrations.RenameIndex(
"Pony", new_name="new_pony_test_idx", old_fields=("weight", "pink")
)
self.assertEqual(
operation.describe(),
"Rename unnamed index for ('weight', 'pink') on Pony to new_pony_test_idx",
)
self.assertEqual(
operation.migration_name_fragment,
"rename_pony_weight_pink_new_pony_test_idx",
)
new_state = project_state.clone()
operation.state_forwards(app_label, new_state)
# Rename index.
with connection.schema_editor() as editor:
operation.database_forwards(app_label, editor, project_state, new_state)
self.assertIndexNameExists(table_name, "new_pony_test_idx")
# Reverse is a no-op.
with connection.schema_editor() as editor, self.assertNumQueries(0):
operation.database_backwards(app_label, editor, new_state, project_state)
self.assertIndexNameExists(table_name, "new_pony_test_idx")
# Reapply, RenameIndex operation is a noop when the old and new name
# match.
with connection.schema_editor() as editor:
operation.database_forwards(app_label, editor, new_state, project_state)
self.assertIndexNameExists(table_name, "new_pony_test_idx")
# Deconstruction.
definition = operation.deconstruct()
self.assertEqual(definition[0], "RenameIndex")
self.assertEqual(definition[1], [])
self.assertEqual(
definition[2],
{
"model_name": "Pony",
"new_name": "new_pony_test_idx",
"old_fields": ("weight", "pink"),
},
)
def test_rename_index_unknown_unnamed_index(self): def test_rename_index_unknown_unnamed_index(self):
app_label = "test_rninuui" app_label = "test_rninuui"
project_state = self.set_up_test_model(app_label) project_state = self.set_up_test_model(app_label)
@ -3697,22 +3618,6 @@ class OperationTests(OperationTestBase):
self.assertIsNot(old_model, new_model) self.assertIsNot(old_model, new_model)
self.assertEqual(new_model._meta.indexes[0].name, "new_pony_pink_idx") self.assertEqual(new_model._meta.indexes[0].name, "new_pony_pink_idx")
@ignore_warnings(category=RemovedInDjango51Warning)
def test_rename_index_state_forwards_unnamed_index(self):
app_label = "test_rnidsfui"
project_state = self.set_up_test_model(app_label, index_together=True)
old_model = project_state.apps.get_model(app_label, "Pony")
new_state = project_state.clone()
operation = migrations.RenameIndex(
"Pony", new_name="new_pony_pink_idx", old_fields=("weight", "pink")
)
operation.state_forwards(app_label, new_state)
new_model = new_state.apps.get_model(app_label, "Pony")
self.assertIsNot(old_model, new_model)
self.assertEqual(new_model._meta.index_together, tuple())
self.assertEqual(new_model._meta.indexes[0].name, "new_pony_pink_idx")
@skipUnlessDBFeature("supports_expression_indexes") @skipUnlessDBFeature("supports_expression_indexes")
def test_add_func_index(self): def test_add_func_index(self):
app_label = "test_addfuncin" app_label = "test_addfuncin"
@ -3832,89 +3737,12 @@ class OperationTests(OperationTestBase):
# Ensure the index is still there # Ensure the index is still there
self.assertIndexExists("test_alflin_pony", ["pink"]) self.assertIndexExists("test_alflin_pony", ["pink"])
@ignore_warnings(category=RemovedInDjango51Warning)
def test_alter_index_together(self):
"""
Tests the AlterIndexTogether operation.
"""
project_state = self.set_up_test_model("test_alinto")
# Test the state alteration
operation = migrations.AlterIndexTogether("Pony", [("pink", "weight")])
self.assertEqual(
operation.describe(), "Alter index_together for Pony (1 constraint(s))"
)
self.assertEqual(
operation.migration_name_fragment,
"alter_pony_index_together",
)
new_state = project_state.clone()
operation.state_forwards("test_alinto", new_state)
self.assertEqual(
len(
project_state.models["test_alinto", "pony"].options.get(
"index_together", set()
)
),
0,
)
self.assertEqual(
len(
new_state.models["test_alinto", "pony"].options.get(
"index_together", set()
)
),
1,
)
# Make sure there's no matching index
self.assertIndexNotExists("test_alinto_pony", ["pink", "weight"])
# Test the database alteration
with connection.schema_editor() as editor:
operation.database_forwards("test_alinto", editor, project_state, new_state)
self.assertIndexExists("test_alinto_pony", ["pink", "weight"])
# And test reversal
with connection.schema_editor() as editor:
operation.database_backwards(
"test_alinto", editor, new_state, project_state
)
self.assertIndexNotExists("test_alinto_pony", ["pink", "weight"])
# And deconstruction
definition = operation.deconstruct()
self.assertEqual(definition[0], "AlterIndexTogether")
self.assertEqual(definition[1], [])
self.assertEqual(
definition[2], {"name": "Pony", "index_together": {("pink", "weight")}}
)
def test_alter_index_together_remove(self): def test_alter_index_together_remove(self):
operation = migrations.AlterIndexTogether("Pony", None) operation = migrations.AlterIndexTogether("Pony", None)
self.assertEqual( self.assertEqual(
operation.describe(), "Alter index_together for Pony (0 constraint(s))" operation.describe(), "Alter index_together for Pony (0 constraint(s))"
) )
@skipUnlessDBFeature("allows_multiple_constraints_on_same_fields")
@ignore_warnings(category=RemovedInDjango51Warning)
def test_alter_index_together_remove_with_unique_together(self):
app_label = "test_alintoremove_wunto"
table_name = "%s_pony" % app_label
project_state = self.set_up_test_model(app_label, unique_together=True)
self.assertUniqueConstraintExists(table_name, ["pink", "weight"])
# Add index together.
new_state = project_state.clone()
operation = migrations.AlterIndexTogether("Pony", [("pink", "weight")])
operation.state_forwards(app_label, new_state)
with connection.schema_editor() as editor:
operation.database_forwards(app_label, editor, project_state, new_state)
self.assertIndexExists(table_name, ["pink", "weight"])
# Remove index together.
project_state = new_state
new_state = project_state.clone()
operation = migrations.AlterIndexTogether("Pony", set())
operation.state_forwards(app_label, new_state)
with connection.schema_editor() as editor:
operation.database_forwards(app_label, editor, project_state, new_state)
self.assertIndexNotExists(table_name, ["pink", "weight"])
self.assertUniqueConstraintExists(table_name, ["pink", "weight"])
def test_add_constraint(self): def test_add_constraint(self):
project_state = self.set_up_test_model("test_addconstraint") project_state = self.set_up_test_model("test_addconstraint")
gt_check = models.Q(pink__gt=2) gt_check = models.Q(pink__gt=2)

View File

@ -1293,81 +1293,6 @@ class OptimizerTests(SimpleTestCase):
], ],
) )
def test_create_model_remove_index_together_rename_index(self):
self.assertOptimizesTo(
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
],
options={
"index_together": [("age", "weight")],
},
),
migrations.RenameIndex(
"Pony", new_name="idx_pony_age_weight", old_fields=("age", "weight")
),
],
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
],
options={
"indexes": [
models.Index(
fields=["age", "weight"], name="idx_pony_age_weight"
),
],
},
),
],
)
def test_create_model_index_together_rename_index(self):
self.assertOptimizesTo(
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
("height", models.IntegerField()),
("rank", models.IntegerField()),
],
options={
"index_together": [("age", "weight"), ("height", "rank")],
},
),
migrations.RenameIndex(
"Pony", new_name="idx_pony_age_weight", old_fields=("age", "weight")
),
],
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
("height", models.IntegerField()),
("rank", models.IntegerField()),
],
options={
"index_together": {("height", "rank")},
"indexes": [
models.Index(
fields=["age", "weight"], name="idx_pony_age_weight"
),
],
},
),
],
)
def test_create_model_rename_index_no_old_fields(self): def test_create_model_rename_index_no_old_fields(self):
self.assertOptimizesTo( self.assertOptimizesTo(
[ [

View File

@ -13,9 +13,8 @@ from django.db.migrations.state import (
ProjectState, ProjectState,
get_related_models_recursive, get_related_models_recursive,
) )
from django.test import SimpleTestCase, ignore_warnings, override_settings from django.test import SimpleTestCase, override_settings
from django.test.utils import isolate_apps from django.test.utils import isolate_apps
from django.utils.deprecation import RemovedInDjango51Warning
from .models import ( from .models import (
FoodManager, FoodManager,
@ -31,9 +30,6 @@ class StateTests(SimpleTestCase):
Tests state construction, rendering and modification by operations. Tests state construction, rendering and modification by operations.
""" """
# RemovedInDjango51Warning, when deprecation ends, only remove
# Meta.index_together from inline models.
@ignore_warnings(category=RemovedInDjango51Warning)
def test_create(self): def test_create(self):
""" """
Tests making a ProjectState from an Apps Tests making a ProjectState from an Apps
@ -50,7 +46,6 @@ class StateTests(SimpleTestCase):
app_label = "migrations" app_label = "migrations"
apps = new_apps apps = new_apps
unique_together = ["name", "bio"] unique_together = ["name", "bio"]
index_together = ["bio", "age"] # RemovedInDjango51Warning.
class AuthorProxy(Author): class AuthorProxy(Author):
class Meta: class Meta:
@ -142,7 +137,6 @@ class StateTests(SimpleTestCase):
author_state.options, author_state.options,
{ {
"unique_together": {("name", "bio")}, "unique_together": {("name", "bio")},
"index_together": {("bio", "age")}, # RemovedInDjango51Warning.
"indexes": [], "indexes": [],
"constraints": [], "constraints": [],
}, },

View File

@ -54,14 +54,8 @@ from django.db.models.fields.json import KeyTextTransform
from django.db.models.functions import Abs, Cast, Collate, Lower, Random, Upper from django.db.models.functions import Abs, Cast, Collate, Lower, Random, Upper
from django.db.models.indexes import IndexExpression from django.db.models.indexes import IndexExpression
from django.db.transaction import TransactionManagementError, atomic from django.db.transaction import TransactionManagementError, atomic
from django.test import ( from django.test import TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature
TransactionTestCase,
ignore_warnings,
skipIfDBFeature,
skipUnlessDBFeature,
)
from django.test.utils import CaptureQueriesContext, isolate_apps, register_lookup from django.test.utils import CaptureQueriesContext, isolate_apps, register_lookup
from django.utils.deprecation import RemovedInDjango51Warning
from .fields import CustomManyToManyField, InheritedManyToManyField, MediumBlobField from .fields import CustomManyToManyField, InheritedManyToManyField, MediumBlobField
from .models import ( from .models import (
@ -3351,7 +3345,6 @@ class SchemaTests(TransactionTestCase):
self.assertIsNone(editor.add_constraint(Author, constraint)) self.assertIsNone(editor.add_constraint(Author, constraint))
self.assertIsNone(editor.remove_constraint(Author, constraint)) self.assertIsNone(editor.remove_constraint(Author, constraint))
@ignore_warnings(category=RemovedInDjango51Warning)
def test_index_together(self): def test_index_together(self):
""" """
Tests removing and adding index_together constraints on a model. Tests removing and adding index_together constraints on a model.
@ -3395,128 +3388,6 @@ class SchemaTests(TransactionTestCase):
False, False,
) )
@ignore_warnings(category=RemovedInDjango51Warning)
def test_index_together_with_fk(self):
"""
Tests removing and adding index_together constraints that include
a foreign key.
"""
# Create the table
with connection.schema_editor() as editor:
editor.create_model(Author)
editor.create_model(Book)
# Ensure the fields are unique to begin with
self.assertEqual(Book._meta.index_together, ())
# Add the unique_together constraint
with connection.schema_editor() as editor:
editor.alter_index_together(Book, [], [["author", "title"]])
# Alter it back
with connection.schema_editor() as editor:
editor.alter_index_together(Book, [["author", "title"]], [])
@ignore_warnings(category=RemovedInDjango51Warning)
@isolate_apps("schema")
def test_create_index_together(self):
"""
Tests creating models with index_together already defined
"""
class TagIndexed(Model):
title = CharField(max_length=255)
slug = SlugField(unique=True)
class Meta:
app_label = "schema"
index_together = [["slug", "title"]]
# Create the table
with connection.schema_editor() as editor:
editor.create_model(TagIndexed)
self.isolated_local_models = [TagIndexed]
# Ensure there is an index
self.assertIs(
any(
c["index"]
for c in self.get_constraints("schema_tagindexed").values()
if c["columns"] == ["slug", "title"]
),
True,
)
@skipUnlessDBFeature("allows_multiple_constraints_on_same_fields")
@ignore_warnings(category=RemovedInDjango51Warning)
@isolate_apps("schema")
def test_remove_index_together_does_not_remove_meta_indexes(self):
class AuthorWithIndexedNameAndBirthday(Model):
name = CharField(max_length=255)
birthday = DateField()
class Meta:
app_label = "schema"
index_together = [["name", "birthday"]]
with connection.schema_editor() as editor:
editor.create_model(AuthorWithIndexedNameAndBirthday)
self.isolated_local_models = [AuthorWithIndexedNameAndBirthday]
# Add the custom index
index = Index(fields=["name", "birthday"], name="author_name_birthday_idx")
custom_index_name = index.name
AuthorWithIndexedNameAndBirthday._meta.indexes = [index]
with connection.schema_editor() as editor:
editor.add_index(AuthorWithIndexedNameAndBirthday, index)
# Ensure the indexes exist
constraints = self.get_constraints(
AuthorWithIndexedNameAndBirthday._meta.db_table
)
self.assertIn(custom_index_name, constraints)
other_constraints = [
name
for name, details in constraints.items()
if details["columns"] == ["name", "birthday"]
and details["index"]
and name != custom_index_name
]
self.assertEqual(len(other_constraints), 1)
# Remove index together
index_together = AuthorWithIndexedNameAndBirthday._meta.index_together
with connection.schema_editor() as editor:
editor.alter_index_together(
AuthorWithIndexedNameAndBirthday, index_together, []
)
constraints = self.get_constraints(
AuthorWithIndexedNameAndBirthday._meta.db_table
)
self.assertIn(custom_index_name, constraints)
other_constraints = [
name
for name, details in constraints.items()
if details["columns"] == ["name", "birthday"]
and details["index"]
and name != custom_index_name
]
self.assertEqual(len(other_constraints), 0)
# Re-add index together
with connection.schema_editor() as editor:
editor.alter_index_together(
AuthorWithIndexedNameAndBirthday, [], index_together
)
constraints = self.get_constraints(
AuthorWithIndexedNameAndBirthday._meta.db_table
)
self.assertIn(custom_index_name, constraints)
other_constraints = [
name
for name, details in constraints.items()
if details["columns"] == ["name", "birthday"]
and details["index"]
and name != custom_index_name
]
self.assertEqual(len(other_constraints), 1)
# Drop the index
with connection.schema_editor() as editor:
AuthorWithIndexedNameAndBirthday._meta.indexes = []
editor.remove_index(AuthorWithIndexedNameAndBirthday, index)
@isolate_apps("schema") @isolate_apps("schema")
def test_db_table(self): def test_db_table(self):
""" """