Fixed #27236 -- Deprecated Meta.index_together in favor of Meta.indexes.

This also deprecates AlterIndexTogether migration operation.
This commit is contained in:
David Wobrock 2022-05-17 16:13:35 +02:00
parent 4f284115a9
commit a6385b382e
No known key found for this signature in database
GPG Key ID: 4885899CFD92B563
22 changed files with 178 additions and 14 deletions

View File

@ -615,6 +615,14 @@ class AlterIndexTogether(AlterTogetherOptionOperation):
option_name = "index_together" option_name = "index_together"
system_check_deprecated_details = {
"msg": (
"AlterIndexTogether is deprecated. Support for it (except in historical "
"migrations) will be removed in Django 5.1."
),
"id": "migrations.W001",
}
def __init__(self, name, index_together): def __init__(self, name, index_together):
super().__init__(name, index_together) super().__init__(name, index_together)

View File

@ -1,6 +1,7 @@
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
@ -10,6 +11,7 @@ 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
@ -200,6 +202,12 @@ class Options:
self.unique_together = normalize_together(self.unique_together) self.unique_together = normalize_together(self.unique_together)
self.index_together = normalize_together(self.index_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

@ -17,6 +17,11 @@ details on these changes.
* The ``BaseUserManager.make_random_password()`` method will be removed. * The ``BaseUserManager.make_random_password()`` method will be removed.
* The model's ``Meta.index_together`` option will be removed.
* The ``AlterIndexTogether`` migration operation will be removed. A stub
operation will remain for compatibility with historical migrations.
.. _deprecation-removed-in-5.0: .. _deprecation-removed-in-5.0:
5.0 5.0

View File

@ -397,6 +397,12 @@ Models
* **models.W045**: Check constraint ``<constraint>`` contains ``RawSQL()`` * **models.W045**: Check constraint ``<constraint>`` contains ``RawSQL()``
expression and won't be validated during the model ``full_clean()``. expression and won't be validated during the model ``full_clean()``.
Migrations
----------
* **migrations.W001**: ``AlterIndexTogether`` is deprecated. Support for it
(except in historical migrations) will be removed in Django 5.1.
Security Security
-------- --------

View File

@ -106,6 +106,13 @@ Changes the model's set of custom indexes (the
:attr:`~django.db.models.Options.index_together` option on the ``Meta`` :attr:`~django.db.models.Options.index_together` option on the ``Meta``
subclass). subclass).
.. deprecated:: 4.2
``AlterIndexTogether`` is deprecated in favor of
:class:`~django.db.migrations.operations.AddIndex`,
:class:`~django.db.migrations.operations.RemoveIndex`, and
:class:`~django.db.migrations.operations.RenameIndex` operations.
``AlterOrderWithRespectTo`` ``AlterOrderWithRespectTo``
--------------------------- ---------------------------

View File

@ -431,12 +431,6 @@ Django quotes column and table names behind the scenes.
.. attribute:: Options.index_together .. attribute:: Options.index_together
.. admonition:: Use the :attr:`~Options.indexes` option instead.
The newer :attr:`~Options.indexes` option provides more functionality
than ``index_together``. ``index_together`` may be deprecated in the
future.
Sets of field names that, taken together, are indexed:: Sets of field names that, taken together, are indexed::
index_together = [ index_together = [
@ -451,6 +445,10 @@ Django quotes column and table names behind the scenes.
index_together = ["pub_date", "deadline"] index_together = ["pub_date", "deadline"]
.. deprecated:: 4.2
Use the :attr:`~Options.indexes` option instead.
``constraints`` ``constraints``
--------------- ---------------

View File

@ -275,6 +275,36 @@ Miscellaneous
Features deprecated in 4.2 Features deprecated in 4.2
========================== ==========================
``index_together`` option is deprecated in favor of ``indexes``
---------------------------------------------------------------
The :attr:`Meta.index_together <django.db.models.Options.index_together>`
option is deprecated in favor of the :attr:`~django.db.models.Options.indexes`
option.
Migrating existing ``index_together`` should be handled as a migration. For
example::
class Author(models.Model):
rank = models.IntegerField()
name = models.CharField(max_length=30)
class Meta:
index_together = [["rank", "name"]]
Should become::
class Author(models.Model):
ranl = models.IntegerField()
name = models.CharField(max_length=30)
class Meta:
indexes = [models.Index(fields=["rank", "name"])]
Running the :djadmin:`makemigrations` command will generate a migration
containing a :class:`~django.db.migrations.operations.RenameIndex` operation
which will rename the existing index.
Miscellaneous Miscellaneous
------------- -------------
@ -282,3 +312,5 @@ Miscellaneous
`recipes and best practices `recipes and best practices
<https://docs.python.org/3/library/secrets.html#recipes-and-best-practices>`_ <https://docs.python.org/3/library/secrets.html#recipes-and-best-practices>`_
for using Python's :py:mod:`secrets` module to generate passwords. for using Python's :py:mod:`secrets` module to generate passwords.
* The ``AlterIndexTogether`` migration operation is deprecated.

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class IndexTogetherAppConfig(AppConfig):
name = "check_framework.migrations_test_apps.index_together_app"

View File

@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
operations = [
migrations.CreateModel(
"SimpleModel",
[
("field", models.IntegerField()),
],
),
migrations.AlterIndexTogether("SimpleModel", index_together=(("id", "field"),)),
]

View File

@ -1,7 +1,11 @@
from unittest.mock import ANY
from django.core import checks from django.core import checks
from django.core.checks.migrations import check_migration_operations
from django.db import migrations from django.db import migrations
from django.db.migrations.operations.base import Operation from django.db.migrations.operations.base import Operation
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings
class DeprecatedMigrationOperationTests(TestCase): class DeprecatedMigrationOperationTests(TestCase):
@ -50,6 +54,23 @@ class DeprecatedMigrationOperationTests(TestCase):
], ],
) )
@override_settings(
INSTALLED_APPS=["check_framework.migrations_test_apps.index_together_app"]
)
def tests_check_alter_index_together(self):
errors = check_migration_operations()
self.assertEqual(
errors,
[
checks.Warning(
"AlterIndexTogether is deprecated. Support for it (except in "
"historical migrations) will be removed in Django 5.1.",
obj=ANY,
id="migrations.W001",
)
],
)
class RemovedMigrationOperationTests(TestCase): class RemovedMigrationOperationTests(TestCase):
def test_default_operation(self): def test_default_operation(self):

View File

@ -16,11 +16,13 @@ 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 isolate_apps, 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
@ -78,6 +80,7 @@ class SchemaIndexesTests(TestCase):
index_sql[0], index_sql[0],
) )
@ignore_warnings(category=RemovedInDjango51Warning)
@isolate_apps("indexes") @isolate_apps("indexes")
def test_index_together_single_list(self): def test_index_together_single_list(self):
class IndexTogetherSingleList(Model): class IndexTogetherSingleList(Model):

View File

@ -5,8 +5,9 @@ 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, skipUnlessDBFeature from django.test import SimpleTestCase, TestCase, ignore_warnings, 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,6 +30,7 @@ def get_max_column_name_length():
@isolate_apps("invalid_models_tests") @isolate_apps("invalid_models_tests")
@ignore_warnings(category=RemovedInDjango51Warning)
class IndexTogetherTests(SimpleTestCase): class IndexTogetherTests(SimpleTestCase):
def test_non_iterable(self): def test_non_iterable(self):
class Model(models.Model): class Model(models.Model):

View File

@ -12,8 +12,9 @@ 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, override_settings from django.test import SimpleTestCase, TestCase, ignore_warnings, 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
@ -4588,6 +4589,7 @@ 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): class AutodetectorIndexTogetherTests(BaseAutodetectorTests):
book_index_together = ModelState( book_index_together = ModelState(
"otherapp", "otherapp",

View File

@ -255,7 +255,7 @@ class OperationTestBase(MigrationTestBase):
unique_together=False, unique_together=False,
options=False, options=False,
db_table=None, db_table=None,
index_together=False, index_together=False, # RemovedInDjango51Warning.
constraints=None, constraints=None,
indexes=None, indexes=None,
): ):
@ -263,6 +263,7 @@ 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 [], "index_together": [["weight", "pink"]] if index_together else [],
"unique_together": [["pink", "weight"]] if unique_together else [], "unique_together": [["pink", "weight"]] if unique_together else [],
} }

View File

@ -5,8 +5,14 @@ from django.db.migrations.operations.fields import FieldOperation
from django.db.migrations.state import ModelState, ProjectState from django.db.migrations.state import ModelState, ProjectState
from django.db.models.functions import Abs from django.db.models.functions import Abs
from django.db.transaction import atomic from django.db.transaction import atomic
from django.test import SimpleTestCase, override_settings, skipUnlessDBFeature from django.test import (
SimpleTestCase,
ignore_warnings,
override_settings,
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
@ -2485,6 +2491,7 @@ class OperationTests(OperationTestBase):
atomic=connection.features.supports_atomic_references_rename, atomic=connection.features.supports_atomic_references_rename,
) )
@ignore_warnings(category=RemovedInDjango51Warning)
def test_rename_field(self): def test_rename_field(self):
""" """
Tests the RenameField operation. Tests the RenameField operation.
@ -2556,6 +2563,7 @@ 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): def test_rename_field_index_together(self):
project_state = self.set_up_test_model("test_rnflit", index_together=True) project_state = self.set_up_test_model("test_rnflit", index_together=True)
operation = migrations.RenameField("Pony", "pink", "blue") operation = migrations.RenameField("Pony", "pink", "blue")
@ -3062,6 +3070,7 @@ 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): def test_rename_index_unnamed_index(self):
app_label = "test_rninui" app_label = "test_rninui"
project_state = self.set_up_test_model(app_label, index_together=True) project_state = self.set_up_test_model(app_label, index_together=True)
@ -3157,6 +3166,7 @@ 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): def test_rename_index_state_forwards_unnamed_index(self):
app_label = "test_rnidsfui" app_label = "test_rnidsfui"
project_state = self.set_up_test_model(app_label, index_together=True) project_state = self.set_up_test_model(app_label, index_together=True)
@ -3291,6 +3301,7 @@ 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): def test_alter_index_together(self):
""" """
Tests the AlterIndexTogether operation. Tests the AlterIndexTogether operation.
@ -3343,6 +3354,7 @@ class OperationTests(OperationTestBase):
definition[2], {"name": "Pony", "index_together": {("pink", "weight")}} definition[2], {"name": "Pony", "index_together": {("pink", "weight")}}
) )
# RemovedInDjango51Warning.
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(
@ -3350,6 +3362,7 @@ class OperationTests(OperationTestBase):
) )
@skipUnlessDBFeature("allows_multiple_constraints_on_same_fields") @skipUnlessDBFeature("allows_multiple_constraints_on_same_fields")
@ignore_warnings(category=RemovedInDjango51Warning)
def test_alter_index_together_remove_with_unique_together(self): def test_alter_index_together_remove_with_unique_together(self):
app_label = "test_alintoremove_wunto" app_label = "test_alintoremove_wunto"
table_name = "%s_pony" % app_label table_name = "%s_pony" % app_label

View File

@ -211,6 +211,7 @@ class OptimizerTests(SimpleTestCase):
migrations.AlterUniqueTogether("Foo", [["a", "b"]]) migrations.AlterUniqueTogether("Foo", [["a", "b"]])
) )
# RemovedInDjango51Warning.
def test_create_alter_index_delete_model(self): def test_create_alter_index_delete_model(self):
self._test_create_alter_foo_delete_model( self._test_create_alter_foo_delete_model(
migrations.AlterIndexTogether("Foo", [["a", "b"]]) migrations.AlterIndexTogether("Foo", [["a", "b"]])
@ -248,6 +249,7 @@ class OptimizerTests(SimpleTestCase):
migrations.AlterUniqueTogether("Foo", [["a", "c"]]), migrations.AlterUniqueTogether("Foo", [["a", "c"]]),
) )
# RemovedInDjango51Warning.
def test_alter_alter_index_model(self): def test_alter_alter_index_model(self):
self._test_alter_alter_model( self._test_alter_alter_model(
migrations.AlterIndexTogether("Foo", [["a", "b"]]), migrations.AlterIndexTogether("Foo", [["a", "b"]]),
@ -1055,6 +1057,7 @@ class OptimizerTests(SimpleTestCase):
migrations.AlterUniqueTogether("Foo", [["a", "b"]]) migrations.AlterUniqueTogether("Foo", [["a", "b"]])
) )
# RemovedInDjango51Warning.
def test_create_alter_index_field(self): def test_create_alter_index_field(self):
self._test_create_alter_foo_field( self._test_create_alter_foo_field(
migrations.AlterIndexTogether("Foo", [["a", "b"]]) migrations.AlterIndexTogether("Foo", [["a", "b"]])

View File

@ -13,8 +13,9 @@ from django.db.migrations.state import (
ProjectState, ProjectState,
get_related_models_recursive, get_related_models_recursive,
) )
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, ignore_warnings, 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,
@ -30,6 +31,9 @@ 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
@ -46,7 +50,7 @@ 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"] index_together = ["bio", "age"] # RemovedInDjango51Warning.
class AuthorProxy(Author): class AuthorProxy(Author):
class Meta: class Meta:
@ -140,7 +144,7 @@ class StateTests(SimpleTestCase):
author_state.options, author_state.options,
{ {
"unique_together": {("name", "bio")}, "unique_together": {("name", "bio")},
"index_together": {("bio", "age")}, "index_together": {("bio", "age")}, # RemovedInDjango51Warning.
"indexes": [], "indexes": [],
"constraints": [], "constraints": [],
}, },

View File

@ -0,0 +1,20 @@
from django.db import models
from django.test import TestCase
from django.utils.deprecation import RemovedInDjango51Warning
class IndexTogetherDeprecationTests(TestCase):
def test_warning(self):
msg = (
"'index_together' is deprecated. Use 'Meta.indexes' in "
"'model_options.MyModel' instead."
)
with self.assertRaisesMessage(RemovedInDjango51Warning, msg):
class MyModel(models.Model):
field_1 = models.IntegerField()
field_2 = models.IntegerField()
class Meta:
app_label = "model_options"
index_together = ["field_1", "field_2"]

View File

@ -53,8 +53,14 @@ 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 TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature from django.test import (
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 (
@ -2888,6 +2894,7 @@ class SchemaTests(TransactionTestCase):
with self.assertRaises(DatabaseError): with self.assertRaises(DatabaseError):
editor.add_constraint(Author, constraint) editor.add_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.
@ -2931,6 +2938,7 @@ class SchemaTests(TransactionTestCase):
False, False,
) )
@ignore_warnings(category=RemovedInDjango51Warning)
def test_index_together_with_fk(self): def test_index_together_with_fk(self):
""" """
Tests removing and adding index_together constraints that include Tests removing and adding index_together constraints that include
@ -2949,6 +2957,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_index_together(Book, [["author", "title"]], []) editor.alter_index_together(Book, [["author", "title"]], [])
@ignore_warnings(category=RemovedInDjango51Warning)
@isolate_apps("schema") @isolate_apps("schema")
def test_create_index_together(self): def test_create_index_together(self):
""" """
@ -2978,6 +2987,7 @@ class SchemaTests(TransactionTestCase):
) )
@skipUnlessDBFeature("allows_multiple_constraints_on_same_fields") @skipUnlessDBFeature("allows_multiple_constraints_on_same_fields")
@ignore_warnings(category=RemovedInDjango51Warning)
@isolate_apps("schema") @isolate_apps("schema")
def test_remove_index_together_does_not_remove_meta_indexes(self): def test_remove_index_together_does_not_remove_meta_indexes(self):
class AuthorWithIndexedNameAndBirthday(Model): class AuthorWithIndexedNameAndBirthday(Model):