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
)
)
# Add any field index and index_together's (deferred as SQLite
# _remake_table needs it).
# Add any field index (deferred as SQLite _remake_table needs it).
self.deferred_sql.extend(self._model_indexes_sql(model))
# Make M2M tables
@ -1585,8 +1584,8 @@ class BaseDatabaseSchemaEditor:
def _model_indexes_sql(self, model):
"""
Return a list of all index SQL statements (field indexes,
index_together, Meta.indexes) for the specified model.
Return a list of all index SQL statements (field indexes, Meta.indexes)
for the specified model.
"""
if not model._meta.managed or model._meta.proxy or model._meta.swapped:
return []
@ -1594,11 +1593,6 @@ class BaseDatabaseSchemaEditor:
for field in model._meta.local_fields:
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:
if (
not index.contains_expressions

View File

@ -180,14 +180,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
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
if delete_field:
indexes = [
@ -210,7 +202,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
"app_label": model._meta.app_label,
"db_table": model._meta.db_table,
"unique_together": unique_together,
"index_together": index_together, # RemovedInDjango51Warning.
"indexes": indexes,
"constraints": constraints,
"apps": apps,
@ -226,7 +217,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
"app_label": model._meta.app_label,
"db_table": "new__%s" % strip_quotes(model._meta.db_table),
"unique_together": unique_together,
"index_together": index_together, # RemovedInDjango51Warning.
"indexes": indexes,
"constraints": constraints,
"apps": apps,

View File

@ -190,14 +190,12 @@ class MigrationAutodetector:
self.generate_renamed_indexes()
# Generate removal of foo together.
self.generate_removed_altered_unique_together()
self.generate_removed_altered_index_together() # RemovedInDjango51Warning.
# Generate field operations.
self.generate_removed_fields()
self.generate_added_fields()
self.generate_altered_fields()
self.generate_altered_order_with_respect_to()
self.generate_altered_unique_together()
self.generate_altered_index_together() # RemovedInDjango51Warning.
self.generate_added_indexes()
self.generate_added_constraints()
self.generate_altered_db_table()
@ -584,7 +582,7 @@ class MigrationAutodetector:
possible).
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
added_models = self.new_model_keys - old_keys
@ -608,12 +606,10 @@ class MigrationAutodetector:
if getattr(field.remote_field, "through", None):
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")
constraints = model_state.options.pop("constraints")
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", None
)
@ -742,16 +738,6 @@ class MigrationAutodetector:
),
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
# concrete model.
relations = self.to_state.relations
@ -832,8 +818,6 @@ class MigrationAutodetector:
related_fields[field_name] = field
# Generate option removal first
unique_together = model_state.options.pop("unique_together", None)
# RemovedInDjango51Warning.
index_together = model_state.options.pop("index_together", None)
if unique_together:
self.add_operation(
app_label,
@ -842,15 +826,6 @@ class MigrationAutodetector:
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
for name in sorted(related_fields):
self.add_operation(
@ -1525,10 +1500,6 @@ class MigrationAutodetector:
def generate_removed_altered_unique_together(self):
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):
for (
old_value,
@ -1548,10 +1519,6 @@ class MigrationAutodetector:
def generate_altered_unique_together(self):
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):
models_to_check = self.kept_model_keys.union(
self.kept_proxy_keys, self.kept_unmanaged_keys

View File

@ -341,33 +341,6 @@ class CreateModel(ModelOperation):
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)

View File

@ -310,14 +310,13 @@ class ProjectState:
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
for option in ("index_together", "unique_together"):
if option in options:
options[option] = [
[new_name if n == old_name else n for n in together]
for together in options[option]
]
if "unique_together" in options:
options["unique_together"] = [
[new_name if n == old_name else n for n in together]
for together in options["unique_together"]
]
# Fix to_fields to refer to the new field.
delay = True
references = get_references(self, model_key, (old_name, found))

View File

@ -1595,7 +1595,6 @@ class Model(AltersData, metaclass=ModelBase):
if not clash_errors:
errors.extend(cls._check_column_name_clashes())
errors += [
*cls._check_index_together(),
*cls._check_unique_together(),
*cls._check_indexes(databases),
*cls._check_ordering(),
@ -1928,36 +1927,6 @@ class Model(AltersData, metaclass=ModelBase):
)
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
def _check_unique_together(cls):
"""Check the value of "unique_together" option."""

View File

@ -1,7 +1,6 @@
import bisect
import copy
import inspect
import warnings
from collections import defaultdict
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.query_utils import PathInfo
from django.utils.datastructures import ImmutableList, OrderedSet
from django.utils.deprecation import RemovedInDjango51Warning
from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from django.utils.text import camel_case_to_spaces, format_lazy
@ -43,7 +41,6 @@ DEFAULT_NAMES = (
"proxy",
"swappable",
"auto_created",
"index_together", # RemovedInDjango51Warning.
"apps",
"default_permissions",
"select_on_save",
@ -119,7 +116,6 @@ class Options:
self.indexes = []
self.constraints = []
self.unique_together = []
self.index_together = [] # RemovedInDjango51Warning.
self.select_on_save = False
self.default_permissions = ("add", "change", "delete", "view")
self.permissions = []
@ -205,13 +201,6 @@ class Options:
self.original_attrs[attr_name] = getattr(self, attr_name)
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
# indexes.
if not getattr(cls._meta, "abstract", False):

View File

@ -345,21 +345,23 @@ Models
``<field name>`` from model ``<model>``.
* **models.E007**: Field ``<field name>`` has column name ``<column name>``
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.
*This check appeared before Django 5.1.*
* **models.E010**: ``unique_together`` must be a list or tuple.
* **models.E011**: All ``unique_together`` elements must be lists or tuples.
* **models.E012**: ``constraints/indexes/index_together/unique_together``
refers to the nonexistent field ``<field name>``.
* **models.E013**: ``constraints/indexes/index_together/unique_together``
refers to a ``ManyToManyField`` ``<field name>``, but ``ManyToManyField``\s
are not supported for that option.
* **models.E012**: ``constraints/indexes/unique_together`` refers to the
nonexistent field ``<field name>``.
* **models.E013**: ``constraints/indexes/unique_together`` refers to a
``ManyToManyField`` ``<field name>``, but ``ManyToManyField``\s are not
supported for that option.
* **models.E014**: ``ordering`` must be a tuple or list (even if you want to
order by only one field).
* **models.E015**: ``ordering`` refers to the nonexistent field, related field,
or lookup ``<field name>``.
* **models.E016**: ``constraints/indexes/index_together/unique_together``
refers to field ``<field_name>`` which is not local to model ``<model>``.
* **models.E016**: ``constraints/indexes/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>``.

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``.
Exactly one of ``old_name`` and ``old_fields`` can be provided. ``old_fields``
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
< 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
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``
---------------

View File

@ -114,9 +114,8 @@ the new value.
.. 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
will add or remove indexes from the model's table until they match the new
value.
Changes a model's ``index_together`` value; this will add or remove indexes
from the model's table until they match the new value.
``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
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.
It can be subclassed to support different index types, such as
``index_together`` on the model ``Meta`` class. It can be subclassed to support
different index types, such as
:class:`~django.contrib.postgres.indexes.GinIndex`. It also allows defining the
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.
* Multi-column indexes can now be created on models. Read the
:attr:`~django.db.models.Options.index_together` documentation for more
information.
``index_together`` documentation for more information.
* During Django's logging configuration verbose Deprecation warnings are
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
incorrect data).
* You can use a single list for :attr:`~django.db.models.Options.index_together`
(rather than a list of lists) when specifying a single set of fields.
* You can use a single list for ``index_together`` (rather than a list of
lists) when specifying a single set of fields.
* Custom intermediate models having more than one foreign key to any of the
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
allows renaming indexes defined in the
: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
:class:`~django.db.migrations.operations.RenameIndex` operations instead of
@ -327,7 +327,7 @@ Migrations
* The migrations autodetector now generates
:class:`~django.db.migrations.operations.RenameIndex` operations instead of
``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>`.
Models

View File

@ -477,9 +477,8 @@ 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.
The ``Meta.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::

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.
* 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.db import connection
from django.db.models import (
CASCADE,
CharField,
DateTimeField,
ForeignKey,
Index,
Model,
Q,
)
from django.db.models import CASCADE, ForeignKey, Index, Q
from django.db.models.functions import Lower
from django.test import (
TestCase,
TransactionTestCase,
ignore_warnings,
skipIfDBFeature,
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.deprecation import RemovedInDjango51Warning
from .models import Article, ArticleTranslation, IndexedArticle2
@ -80,21 +70,6 @@ class SchemaIndexesTests(TestCase):
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):
index = Index(fields=["headline"], name="whitespace_idx")
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.models.functions import Abs, Lower, Round
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.utils.deprecation import RemovedInDjango51Warning
class EmptyRouter:
@ -29,134 +28,6 @@ def get_max_column_name_length():
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")
class UniqueTogetherTests(SimpleTestCase):
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.questioner import MigrationQuestioner
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.utils.deprecation import RemovedInDjango51Warning
from .models import FoodManager, FoodQuerySet
@ -4872,592 +4871,6 @@ class AutodetectorTests(BaseAutodetectorTests):
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):
def test_no_operations(self):
class Migration(migrations.Migration):

View File

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

View File

@ -11,13 +11,11 @@ from django.db.models.functions import Abs, Pi
from django.db.transaction import atomic
from django.test import (
SimpleTestCase,
ignore_warnings,
override_settings,
skipIfDBFeature,
skipUnlessDBFeature,
)
from django.test.utils import CaptureQueriesContext
from django.utils.deprecation import RemovedInDjango51Warning
from .models import FoodManager, FoodQuerySet, UnicodeModel
from .test_base import OperationTestBase
@ -3094,37 +3092,6 @@ class OperationTests(OperationTestBase):
self.assertColumnExists("test_rnflut_pony", "pink")
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):
project_state = self.apply_operations(
"test_rfwdbc",
@ -3601,52 +3568,6 @@ class OperationTests(OperationTestBase):
with self.assertRaisesMessage(ValueError, msg):
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):
app_label = "test_rninuui"
project_state = self.set_up_test_model(app_label)
@ -3697,22 +3618,6 @@ class OperationTests(OperationTestBase):
self.assertIsNot(old_model, new_model)
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")
def test_add_func_index(self):
app_label = "test_addfuncin"
@ -3832,89 +3737,12 @@ class OperationTests(OperationTestBase):
# Ensure the index is still there
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):
operation = migrations.AlterIndexTogether("Pony", None)
self.assertEqual(
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):
project_state = self.set_up_test_model("test_addconstraint")
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):
self.assertOptimizesTo(
[

View File

@ -13,9 +13,8 @@ from django.db.migrations.state import (
ProjectState,
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.utils.deprecation import RemovedInDjango51Warning
from .models import (
FoodManager,
@ -31,9 +30,6 @@ class StateTests(SimpleTestCase):
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):
"""
Tests making a ProjectState from an Apps
@ -50,7 +46,6 @@ class StateTests(SimpleTestCase):
app_label = "migrations"
apps = new_apps
unique_together = ["name", "bio"]
index_together = ["bio", "age"] # RemovedInDjango51Warning.
class AuthorProxy(Author):
class Meta:
@ -142,7 +137,6 @@ class StateTests(SimpleTestCase):
author_state.options,
{
"unique_together": {("name", "bio")},
"index_together": {("bio", "age")}, # RemovedInDjango51Warning.
"indexes": [],
"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.indexes import IndexExpression
from django.db.transaction import TransactionManagementError, atomic
from django.test import (
TransactionTestCase,
ignore_warnings,
skipIfDBFeature,
skipUnlessDBFeature,
)
from django.test import TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature
from django.test.utils import CaptureQueriesContext, isolate_apps, register_lookup
from django.utils.deprecation import RemovedInDjango51Warning
from .fields import CustomManyToManyField, InheritedManyToManyField, MediumBlobField
from .models import (
@ -3351,7 +3345,6 @@ class SchemaTests(TransactionTestCase):
self.assertIsNone(editor.add_constraint(Author, constraint))
self.assertIsNone(editor.remove_constraint(Author, constraint))
@ignore_warnings(category=RemovedInDjango51Warning)
def test_index_together(self):
"""
Tests removing and adding index_together constraints on a model.
@ -3395,128 +3388,6 @@ class SchemaTests(TransactionTestCase):
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")
def test_db_table(self):
"""