mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
Refs #27236 -- Removed Meta.index_together per deprecation timeline.
This commit is contained in:
parent
00e1879610
commit
2abf417c81
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -310,13 +310,12 @@ 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] = [
|
||||
if "unique_together" in options:
|
||||
options["unique_together"] = [
|
||||
[new_name if n == old_name else n for n in together]
|
||||
for together in options[option]
|
||||
for together in options["unique_together"]
|
||||
]
|
||||
# Fix to_fields to refer to the new field.
|
||||
delay = True
|
||||
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -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>``.
|
||||
|
@ -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
|
||||
|
@ -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``
|
||||
---------------
|
||||
|
||||
|
@ -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()``
|
||||
--------------------
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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::
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
[
|
||||
|
@ -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": [],
|
||||
},
|
||||
|
@ -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):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user